Skip to content

[Fix] Completed task fails to resume after restart when a message write is interrupted - #2297

Merged
edelauna merged 5 commits into
mainfrom
fix/ui-messages-write-interruption-2130-v2
Nov 6, 2026
Merged

edelauna merged 5 commits into
mainfrom
fix/ui-messages-write-interruption-2130-v2

Conversation

@zoomote2

@zoomote2 zoomote2 Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

​Created by Roomote. Follow up by mentioning @roomote, in the web UI, or in Discord.

Related GitHub Issue

Closes #2130. Recreates #2136 after its original author account was deleted.

Description

An interrupted two-rename replacement can leave a committed task file absent at its canonical path. A fresh host then loses access to message history or cannot discover the task's history record.

This change commits each replacement with one atomic rename after flushing its temp file. It retains the EPERM fallback required for Windows open-file replacement. Writers recover interrupted replacements under the shared advisory lock before merging data.

Readers and startup now recover missing canonical files from committed backups without requiring another write. Recovery waits for a crashed writer's lock to become stale rather than stealing a live lock. Startup recovers history_item.json before its stat/read, and UI/API readers use the same recovery path.

Recovery rejects uncertain directory scans, canonical access, backup metadata, and restore operations. These failures preserve committed backups and emit recovery error codes. Single and batch history deletion remove recoverable leftovers before deleting the canonical record under the same lock.

Persistence trace events identify writer stages, recovery failures, marker-check error kinds, and shutdown. They use the existing output-channel logger. Failed restart E2E scenarios retain phase results, logs, and task storage as CI artifacts.

Verification

  • Full workspace tests: pnpm test --concurrency=1, 14/14 tasks pass. Extension: 567 files pass, 4 skipped; 12,611 tests pass, 39 skipped.
  • Full workspace lint: 12/12 tasks pass.
  • Full workspace typecheck: 11/11 tasks pass.
  • pnpm lifecycle:model-check: pass, including the new independent replacement/restart model.
  • Mocked restart-persistence E2E: create and fresh-host verify phases pass.
  • E2E package lint and typecheck: pass.
  • ESLint prune checks on edited extension files: pass; suppression counts unchanged.

The real-writer subprocess suite covers all three task filenames, both replacement paths, and SIGKILL before and after commit. Production readers and fresh-store startup recover before another write. One case waits out an unmodified crash lock. Focused tests cover recovery I/O failure, later retry, flush/close ordering, deletion cleanup, and preventing recovery from resurrecting deleted history.

The bounded model first requires the old protocol's #2130 counterexample, then checks preservation, restart discovery, recovery failure/retry, and explicit deletion for three files, one later replacement, and one process crash. It distinguishes UI/API persistence from history publication. The E2E readiness check explicitly waits for both persisted histories and the store's published history item; neither TaskCompleted nor the UI marker alone promises all three.

Limits and documentation

This is a process-crash guarantee, not a power-loss proof. Parent directories are not fsynced. All participants must honor advisory locks; fresh recovery can wait about 35 seconds for staleness and reports an error if the lock or filesystem remains unavailable. Successful deletion prevents recovery from old leftovers; it does not prohibit deliberate new writes to a deleted path.

See docs/architecture/file-replacement-restart-model.md for the production mapping, bounded invariants, assumptions, and exclusions. The completion-readiness model remains separate.

Checklist

  • Linked issue and focused scope.
  • Regression tests and self-review.
  • No rendered UI changes.
  • No changesets or changelog entries.

@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one): 1

  • coderabbit-review-active

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@zoomote2

zoomote2 Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

1 issue outstanding: startup recovery now defers behind a fresh crash lock, the removed test case and the architecture doc no longer match that behavior. The earlier recovery/flush/Windows findings stay fixed in their refactored locations. platform-unit-test (ubuntu-latest), CodeQL, and the mocked restart-persistence E2E pass on this head; platform-unit-test (windows-latest), extension-host-visual, and mutation-diff were still running at review time. See session

  • src/utils/safeWriteJson.ts:107 — backup restore triggers on any fs.access failure, not just ENOENT; a transient access error can rename a stale .bak_*.tmp over a newer committed file and silently roll committed content back. Fixed: restore now runs only on ENOENT; other access errors abort the write without touching backups.
  • src/utils/safeWriteJson.ts:372 — _flushFileToDisk opens the temp file read-only; on Windows FileHandle.sync() fails on every write (FlushFileBuffers requires write access), so the flush never works there and an error is logged for every write. Fixed: the flush handle now opens with "r+".
  • src/utils/safeWriteJson.ts:111-115 — fs.stat on orphaned backups is unguarded; a rejected stat aborts the whole write with an unrelated fs error instead of skipping the unreadable entry. Fixed: stat failures are now guarded; ENOENT skips the entry and other errors abort the write deliberately, so an unreadable backup is never garbage-collected while it may be the newest committed state.
  • src/utils/__tests__/safeWriteJson.interruption.test.ts:146 — the child is killed with process.kill(pid, "SIGKILL"), and the test asserts exit.code is null; on Windows the emulated SIGKILL exits with code 1, so both interruption scenarios fail platform-unit-test (windows-latest) on this head with AssertionError: expected 1 to be null. Fixed: the assertion now accepts the emulated Windows exit code (process.platform === "win32" ? 1 : null), and the Windows unit-test job passes on 01f7b1c.
  • src/core/task-persistence/TaskHistoryStore.ts:362 — startup recovery now fails fast behind a fresh crash lock (waitForStaleLock: false), so a fresh host restarting within 31s of an EPERM-fallback crash cannot discover a missing history_item.json at startup; the removed ageLock: false test case and docs/architecture/file-replacement-restart-model.md (lines 50–51) still claim the fresh store waits out staleness. Update the doc/PR description (and ideally pin the new deferred-discovery contract with a test), or wait only when the hint scan finds leftovers and the cache lacks the task.

Reviewed b460112

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Review status

This PR was opened by an automated account. A human maintainer must verify the change intent, provenance, and validation before merging.

Current step: The required review sequence passed. Remaining merge requirements apply.

Review-state labels are managed by this workflow; do not edit them manually. community-approved is managed the same way — do not add or remove it manually. It signals a fresh community code approval for the current head as an advisory priority only; maintainer review is still required.

@codecov

codecov Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Comment thread src/utils/safeWriteJson.ts Resolved
Comment thread src/utils/safeWriteJson.ts Resolved
Comment thread src/utils/safeWriteJson.ts Resolved
@edelauna

edelauna commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

The merge-queue failure on #2294 provides another occurrence of #2130's missing-history-record symptom:

Please include this failure class in the lifecycle verification work for this fix. I checked this PR at 25edb7ab9745a25f5bf9c643a787cde235dc283f: it already adds real subprocess interruption tests, which are useful. However, a passing pnpm lifecycle:model-check currently does not cover the replacement/restart protocol: scripts/check-completion-persistence.ts collapses persistence into a single history: "durable" phase. It cannot express individual task files, a later replacement removing a previously readable canonical path, process termination between renames, or startup discovering the task.

Suggested focused scope:

  1. Add a small bounded replacement/restart submodel to the lifecycle umbrella, with explicit mapping to the production writer, readers and startup reconciliation. Keep the existing completion-readiness model; avoid multiplying unrelated lifecycle state spaces.
  2. Model canonical/temp/backup state, successful initial persistence, a subsequent replacement, the EPERM fallback, interruption at each relevant boundary, and a fresh read/startup without requiring another write first. Distinguish UI/API message persistence from history_item.json publication, including shutdown after the UI marker becomes visible while the history record is still pending. Map readiness acknowledgments to actual production signals; do not assume TaskCompleted already promises all three files.
  3. Assert that an acknowledged committed task remains discoverable and its required histories recoverable after interruption of a later write, absent explicit deletion. Do not require an unfinished first write to survive. Include positive successful-write/recovery landmarks, preservation of the last committed content through failed recovery, and no resurrection after explicit deletion. State lock-release/staleness assumptions and process-crash bounds explicitly; this is not a power-loss proof.
  4. Connect the abstract transitions to the existing real-writer subprocess tests. Cover both replacement paths and the history-record case; exercise production read/startup recovery after the crash, not only recovery triggered by the next writer. The current head recovers backups at the start of a write, while readFileWithMissingRetry only retries; that distinction matters for a fresh host trying to discover an absent history_item.json.
  5. Demonstrate that the old protocol produces the expected Restart persistence still fails after #2080: ui_messages.json missing after successful create-phase disk check #2130 counterexample and the fixed protocol satisfies the bounded invariant. If the model lands before the runtime fix, use the suite's explicit issue-keyed known-unsafe-witness convention, then promote it to a universal bounded invariant with the fix. Document what remains outside the model and retain the fresh-extension-host E2E.

This adds deterministic coverage for the suspected mechanism rather than relying on repeated green E2E runs. It should also prevent claiming that the already-green completion model proves this file-level recovery contract.

roomote and others added 4 commits November 6, 2026 00:29
safeWriteJson replaced a file with two renames (target to backup, then
replacement to target). A process exit between the renames left the
canonical path absent, so readers saw a permanent ENOENT that no retry
could repair.

The replacement is now committed with a single atomic rename. Only a
Windows-style EPERM (target held open by a reader) falls back to the
two-step sequence, and a recovery pass under the advisory lock restores
the newest orphaned backup and removes leftover temp files before any
write. Task-message saves emit trace points with the task ID and
process ID, the marker check and shutdown are traced, and a failed
restart-persistence scenario now preserves phase results, VS Code logs,
and task files for CI artifact upload.
@edelauna
edelauna force-pushed the fix/ui-messages-write-interruption-2130-v2 branch from 25edb7a to 01f7b1c Compare November 6, 2026 00:54
@edelauna
edelauna marked this pull request as ready for review November 6, 2026 00:55
@github-actions github-actions Bot added the awaiting-maintainer CodeRabbit approved; waiting for a human maintainer label Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Changed-code mutation testing

Report for PR head b460112c091d.

Surviving / uncovered mutants Advisories
78 0
Details: mutants, guidance, and reproduction

Changed-code mutation testing

PackageChanged executable linesValidKilledTimeoutSurvivedNo coverageResult
extension30030122306414Advisory findings

Focused tests

  • extension (cwd src): core/task/__tests__/ask-auto-deny.spec.ts, core/webview/__tests__/ClineProvider.spec.ts, api/providers/__tests__/kimi-code.spec.ts, core/webview/__tests__/ClineProvider.taskHistory.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.deleteSemantics.spec.ts, __tests__/extension.spec.ts, utils/__tests__/safeWriteJson.interruption.test.ts, core/task/__tests__/ask-queued-message-drain.spec.ts, core/task/__tests__/Task.spec.ts, core/task/__tests__/Task.persistence.spec.ts, core/tools/__tests__/executeCommand.spec.ts, activate/__tests__/registerCommands.spec.ts, core/webview/__tests__/ClineProvider.sticky-mode.spec.ts, core/webview/__tests__/webviewMessageHandler.spec.ts, core/tools/__tests__/executeCommandTool.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts, core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts, api/providers/fetchers/__tests__/modelCache.spec.ts, core/webview/__tests__/ClineProvider.sticky-profile.spec.ts, core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts, api/providers/__tests__/native-ollama.spec.ts, core/config/__tests__/importExport.spec.ts, api/providers/__tests__/lmstudio.spec.ts, __tests__/ClineProvider.history-resume-delegation.spec.ts, core/task/__tests__/Task.dispose.test.ts, core/task/__tests__/ask-clear-approval-buttons.spec.ts, api/providers/__tests__/lmstudio-native-tools.spec.ts, api/providers/__tests__/lm-studio.spec.ts, services/mcp/__tests__/McpHub.spec.ts, api/providers/__tests__/lite-llm.spec.ts, core/task/__tests__/ask-allowlist-cwd.spec.ts, api/providers/__tests__/opencode-go.spec.ts, utils/__tests__/safeWriteJson.test.ts, core/task-persistence/__tests__/TaskHistoryStore.crossInstance.spec.ts, core/task/__tests__/Task.streaming-tool-calls.spec.ts, services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts, core/webview/__tests__/ClineProvider.stale-view.spec.ts, api/providers/__tests__/lm-studio-timeout.spec.ts, utils/__tests__/jsonFileRecovery.test.ts, core/config/__tests__/ProviderSettingsManager.spec.ts, core/task/__tests__/grace-retry-errors.spec.ts, core/task/__tests__/flushPendingToolResultsToHistory.spec.ts, core/task-persistence/__tests__/apiMessages.spec.ts, __tests__/single-open-invariant.spec.ts, core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts, api/providers/__tests__/openrouter.spec.ts, api/providers/fetchers/__tests__/openrouter.spec.ts, core/task/__tests__/Task.throttle.test.ts, api/providers/__tests__/model-catalog-adapters.spec.ts, core/task/__tests__/Task.run-shadow.spec.ts, core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts, core/task-persistence/__tests__/taskMessages.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts, api/providers/__tests__/zoo-gateway.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts, __tests__/ClineProvider.delegation.spec.ts, core/tools/__tests__/CodebaseSearchTool.spec.ts, core/task/__tests__/reasoning-preservation.test.ts, core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts, api/providers/__tests__/nanogpt.spec.ts, core/webview/__tests__/webviewMessageHandler.delete.spec.ts, core/webview/__tests__/checkpointRestoreHandler.spec.ts, services/code-index/__tests__/code-index-status-manager.spec.ts, services/code-index/__tests__/manager.spec.ts, core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts, activate/__tests__/registerCodeActions.spec.ts, api/providers/__tests__/vercel-ai-gateway.spec.ts, core/webview/__tests__/webviewMessageHandler.edit.spec.ts, core/prompts/__tests__/add-custom-instructions.spec.ts, core/task/__tests__/grounding-sources.test.ts, utils/__tests__/autoImportSettings.spec.ts, api/providers/__tests__/requesty.spec.ts, api/providers/__tests__/friendli.spec.ts, api/providers/__tests__/io-intelligence.spec.ts, __tests__/ClineProvider.subtask-links.spec.ts, core/webview/__tests__/webviewMessageHandler.openFile.spec.ts, core/webview/__tests__/generateSystemPrompt.spec.ts, api/providers/fetchers/__tests__/lmstudio.test.ts, core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts, core/prompts/__tests__/system-prompt.spec.ts, core/task/__tests__/Task.ignored-disabled-tools-notice.spec.ts, core/task-persistence/__tests__/readFileWithMissingRetry.spec.ts, services/code-index/processors/__tests__/processor-factories.spec.ts, services/code-index/__tests__/code-index-manager-registry.spec.ts, api/providers/__tests__/poe.spec.ts, extension/__tests__/api-task-conversation-history-length.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-auto-deny.spec.ts, core/task/__tests__/build-tools-readiness.integration.spec.ts, core/task/__tests__/Task.abort-reason-race.spec.ts, core/task/__tests__/Task.resume-eviction-race.spec.ts, api/providers/__tests__/kenari.spec.ts, __tests__/removeClineFromStack-delegation.spec.ts, extension/__tests__/api-start-task-logging.spec.ts, services/code-index/__tests__/cache-manager.spec.ts, activate/__tests__/handleUri.spec.ts, core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts, core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts, core/webview/__tests__/messageEnhancer.test.ts, core/task/__tests__/build-tools.spec.ts, core/tools/__tests__/generateImageTool.test.ts, core/assistant-message/__tests__/presentAssistantMessage-tool-repetition.spec.ts, api/__tests__/index.spec.ts, extension/__tests__/api-configuration.spec.ts, api/providers/fetchers/__tests__/modelEndpointCache.spec.ts, core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts, core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts, services/code-index/__tests__/code-index-workspace-scope.spec.ts, core/webview/__tests__/webviewMessageHandler.readFileContent.spec.ts, extension/__tests__/api-has-task-ui-message-text.spec.ts, extension/__tests__/api-start-new-task.spec.ts, core/webview/__tests__/webviewMessageHandler.readOriginalContent.spec.ts, core/task/__tests__/Task.pending-action.spec.ts, extension/__tests__/api-delegation-events.spec.ts, core/webview/__tests__/webviewMessageHandler.cloudAuth.spec.ts, core/webview/__tests__/webviewMessageHandler.abandonSubtask.spec.ts, __tests__/abandonSubtask.spec.ts, utils/__tests__/enhance-prompt.spec.ts, activate/__tests__/handleTask.spec.ts, core/task/__tests__/ask-abort-race.spec.ts, api/providers/__tests__/unbound.spec.ts, services/marketplace/__tests__/marketplace-setting-check.spec.ts, core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts, core/assistant-message/__tests__/presentAssistantMessage.spec.ts, extension/__tests__/api-send-message.spec.ts, __tests__/nested-delegation-resume.spec.ts, core/webview/__tests__/ClineProvider.pending-action.spec.ts, core/task/__tests__/ToolExecutionContext.spec.ts, utils/__tests__/fileLock.test.ts, __tests__/delegation-concurrent.spec.ts, __tests__/task-run-dispatch.spec.ts, extension/__tests__/api-delete-queued-message.spec.ts, core/webview/__tests__/ClineProviderFactory.spec.ts, services/code-index/__tests__/code-index-scope.spec.ts, core/task/__tests__/Task.sticky-profile-race.spec.ts, __tests__/api-subtask.spec.ts, core/task-persistence/__tests__/persistenceTrace.spec.ts, extension/__tests__/api-theme-fixture.spec.ts, core/prompts/__tests__/get-prompt-component.spec.ts, __tests__/new-task-delegation.spec.ts, core/task/__tests__/Task.terminal-operation.spec.ts, test-utils/__tests__/provider.spec.ts, core/assistant-message/__tests__/toTelemetryToolName.spec.ts, extension/__tests__/api-terminal-profile.spec.ts, core/context-tracking/__tests__/FileContextTracker.spec.ts

All surviving and uncovered mutants

Warning annotations highlight up to 20 unique locations (maximum 7 per file). This summary lists every advisory mutant.

extension

src/core/task-persistence/readFileWithMissingRetry.ts

LineStatusMutatorReplacement
46SurvivedStringLiteral""
48SurvivedConditionalExpressionfalse
50SurvivedStringLiteral""

src/core/task-persistence/TaskHistoryStore.ts

LineStatusMutatorReplacement
287SurvivedConditionalExpressionfalse
287SurvivedConditionalExpressiontrue
287SurvivedEqualityOperatorgetErrorCode(error) === "ENOENT"
287SurvivedStringLiteral""
288SurvivedStringLiteral""
317SurvivedConditionalExpressiontrue
317SurvivedConditionalExpressionfalse
317SurvivedEqualityOperatorgetErrorCode(error) === "ENOENT"
317SurvivedStringLiteral""
318SurvivedStringLiteral""
361SurvivedConditionalExpressionfalse
385SurvivedStringLiteral""
847SurvivedConditionalExpressionfalse
852SurvivedConditionalExpressiontrue
854NoCoverageStringLiteral""
854SurvivedConditionalExpressionfalse

src/core/webview/ClineProvider.ts

LineStatusMutatorReplacement
2728SurvivedStringLiteral``
2746NoCoverageStringLiteral""
2746SurvivedConditionalExpressionfalse
2750NoCoverageStringLiteral""
2750SurvivedConditionalExpressionfalse

src/extension.ts

LineStatusMutatorReplacement
132SurvivedCallExpression;
410SurvivedStringLiteral""

src/extension/api.ts

LineStatusMutatorReplacement
367SurvivedStringLiteral""
369SurvivedStringLiteral""
372NoCoverageStringLiteral""
373SurvivedLogicalOperatorgetErrorCode(error) && "unknown"
373SurvivedStringLiteral""
378SurvivedStringLiteral""
378SurvivedObjectLiteral{}
378SurvivedStringLiteral""
378SurvivedStringLiteral""

src/utils/errorCode.ts

LineStatusMutatorReplacement
3SurvivedConditionalExpressiontrue
3SurvivedLogicalOperatortypeof error === "object" && error !== null && "code" in error || typeof error.code === "string"
3SurvivedConditionalExpressiontrue
3SurvivedConditionalExpressiontrue
3SurvivedConditionalExpressiontrue

src/utils/fileLock.ts

LineStatusMutatorReplacement
31SurvivedConditionalExpressiontrue
31SurvivedObjectLiteral{}

src/utils/jsonFileRecovery.ts

LineStatusMutatorReplacement
36SurvivedStringLiteral""
43SurvivedConditionalExpressionfalse
50SurvivedStringLiteral""
56SurvivedBooleanLiteralfalse
58SurvivedConditionalExpressiontrue
62SurvivedConditionalExpressiontrue
62SurvivedConditionalExpressionfalse
62SurvivedEqualityOperatoroptions.waitForStaleLock !== false
62SurvivedBooleanLiteraltrue
65SurvivedStringLiteral""
84NoCoverageBlockStatement{}
85NoCoverageConditionalExpressiontrue
85NoCoverageConditionalExpressionfalse
85NoCoverageEqualityOperatorgetErrorCode(error) !== "ENOENT"
85NoCoverageStringLiteral""
132SurvivedObjectLiteral{}
132SurvivedBooleanLiteraltrue
143SurvivedObjectLiteral{}
183SurvivedMethodExpressionbackupStats
183SurvivedArithmeticOperatorb.mtimeMs + a.mtimeMs
183SurvivedArrowFunction() => undefined
190SurvivedStringLiteral``
199SurvivedConditionalExpressionfalse
205NoCoverageStringLiteral``
209SurvivedBooleanLiteralfalse

src/utils/safeWriteJson.ts

LineStatusMutatorReplacement
54SurvivedMethodExpressionMath.random().toString(36)
112SurvivedOptionalChainingoptions.trace
115SurvivedConditionalExpressionfalse
116NoCoverageObjectLiteral{}
116NoCoverageStringLiteral""
116NoCoverageLogicalOperatorrecovery.restoredFrom && "cleaned-leftovers"
116NoCoverageStringLiteral""
161SurvivedOptionalChainingoptions?.trace({ stage: "backup-rename", detail: "eperm-fallback" })
164SurvivedOptionalChainingoptions?.trace({ stage: "replacement-rename", detail: "eperm-fallback" })
164SurvivedStringLiteral""
281SurvivedStringLiteral``

Resolve a mutation gap

Add or strengthen a focused test that fails under the mutation. If the mutant is equivalent, request maintainer approval for the narrowest mutator-specific exclusion and explain why it cannot change behavior:

// Stryker disable next-line ConditionalExpression: normalized input cannot reach the alternate branch
const result = condition ? value : fallback

Broad all exclusions and exclusions without a concrete reason are reported as advisories.

Reproduce locally

From a full checkout containing both commits:

pnpm install --frozen-lockfile
node scripts/stryker-diff.mjs ci --base 7ec58d4d4ccc39d16cae2a8c2ce3c072bd7d9915 --head c75610beb81e0a9845d6c8b4b8d611cbd9eb2838

Mutation reports

  • extension: reports/mutation/extension/mutation.html

The workflow uploads generated reports in the changed-code-mutation-report artifact. A direct artifact link appears below after upload.

@github-actions github-actions Bot added awaiting-maintainer CodeRabbit approved; waiting for a human maintainer and removed awaiting-maintainer CodeRabbit approved; waiting for a human maintainer labels Nov 6, 2026
const { mtimeMs } = await fs.stat(taskFilePath)
const { mtimeMs } = await fs.stat(taskFilePath).catch(async (error: unknown) => {
if (getErrorCode(error) !== "ENOENT") throw error
await recoverMissingJsonFile(taskFilePath, { waitForStaleLock: false })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch to { waitForStaleLock: false } makes startup recovery fail fast while a crashed writer's lock is still fresh. For an EPERM-fallback crash that leaves history_item.json absent with a committed backup, a fresh host restarting within 31 seconds now cannot discover the record at startup: reconcile sees ELOCKED, keeps the task live, and the record only appears on a later reconcile tick (watcher activity or the 5-minute periodic pass). The immediate-mode default is clearly deliberate (avoid blocking the store mutex for ~31–70s), but this commit also removes the only test that proved the old guarantee — the ageLock: false case in safeWriteJson.interruption.test.ts — while docs/architecture/file-replacement-restart-model.md (lines 50–51) still claims "One case leaves the crash lock's timestamp unchanged, proving the fresh store can wait for staleness", and the PR description repeats that claim.

If the deferral is intended, the doc and PR body should describe the new startup behavior and its deferred-discovery window, and ideally a focused test should pin the new contract (ELOCKED at startup → record recovered by a reconcile after the lock ages). Otherwise, consider waiting (waitForStaleLock: true) only when the hint scan finds leftovers and the cache lacks the task, which bounds the mutex block to genuinely-missing records.

@github-actions github-actions Bot removed the awaiting-maintainer CodeRabbit approved; waiting for a human maintainer label Nov 6, 2026
@edelauna
edelauna added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit bb6cbee Nov 6, 2026
26 checks passed
@edelauna
edelauna deleted the fix/ui-messages-write-interruption-2130-v2 branch November 6, 2026 04:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restart persistence still fails after #2080: ui_messages.json missing after successful create-phase disk check

2 participants