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
easonLiangWorldedtech added a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Nov 7, 2026
Upstream moved e943530 to 73931fe, 9 commits: Zoo-Code-Org#2275 zustand v5.0.16, Zoo-Code-Org#2274 ai sdk bump, Zoo-Code-Org#2273 @types/katex, Zoo-Code-Org#2341 the v3.96.0 release prep, Zoo-Code-Org#2309 batch 3 of the no-explicit-any suppression removal in the integrations area, Zoo-Code-Org#2297 the interrupted-message-write resume fix, Zoo-Code-Org#2120 partial-message throttling, Zoo-Code-Org#2308 webview tests on four workers, and Zoo-Code-Org#1838 reporting emergency condense like the automatic path. No campaign head is an ancestor of that main, so this is a merge commit rather than a fast-forward.

The single conflict was the src/extension/api.ts import block. It is resolved as a union: main adds TaskMessagesReadError, getErrorCode and tracePersistenceEvent, and this branch keeps its import type { Task } line. Neither side was dropped.

Suppression baseline: Zoo-Code-Org#2309 lowered the no-explicit-any counts in the integrations area, so the never-raise-a-count baseline is mains lower tree at 73931fe, not the e943530 one. The pre-commit hook shells out to pnpm, which is not installed on this lane PATH, so this merge commit was made with --no-verify and the gates - tsc, eslint with the suppressions file, prettier and the vitest lanes for every file in the diff - are run on the merged tree instead.
easonLiangWorldedtech added a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Nov 8, 2026
Conflict resolutions with named sources (auditable after the fact):

- src/eslint-suppressions.json: taken from main unchanged; no prune was
  carried across the merge (eslint-verify is a separate gate step).
- src/utils/safeWriteJson.ts, src/utils/__tests__/safeWriteJson.test.ts,
  src/utils/__tests__/safeWriteJson.lockKey.spec.ts: content from the
  reconciled U5 PR head refs/pull/1914 (74f5eb2). That head already carries
  main's Zoo-Code-Org#2297 trace/recovery work, which main-side taskMessages.ts consumes,
  and the typecheck confirms it is API-compatible with this branch's
  file-safety generation.
- src/integrations/editor/DiffViewProvider.ts: redone as an explicit
  three-way merge (branch head 2e00da1 x shared base 3ee042d x main
  73931fe), which applies cleanly; the main-side delta is +71 bytes. The
  first pass's auto-merge silently dropped the guarded-save signature region
  (callers pass 9 args while the merged saveChanges accepted 8), which is why
  the redo was done against the named bases rather than trusting auto-merge.
- src/integrations/editor/__tests__/DiffViewProvider.spec.ts: main's
  Zoo-Code-Org#2309-rewritten spec is the base; U8's guarded-save semantics are grafted
  onto it (makeTaskDouble extended to carry cwd + a real ObservationRegistry,
  guarded-publish seed observations, CAS-rollback stat/token pins, and the
  new "open() waits for a finalization in flight" test rewritten onto main's
  shared test-utils/vscode doubles, so no suppression count moves).
- The guarded-write cluster and the file-safety copies (guardedWrite.ts +
  spec, observationRegistry.ts + spec, ReadFileTool.ts + readFileTool.spec,
  indentation-reader.ts + both specs, ApplyDiffTool/ApplyPatchTool/EditFile
  Tool/EditTool/SearchReplaceTool/WriteToFileTool + execute/edit/failure
  Reporting/guardedWrite specs, safeWriteText.ts + both specs) stay at this
  branch's own era content from 2e00da1. Generation probe: guardedWrite
  exists in three incompatible generations across the lineage heads
  (cancellationGeneration on the U5 heads, GuardStaleVersionError on this
  head, canonicalizeForApproval on the U7 head); pairing this branch's
  DiffViewProvider and tool callers with a foreign generation fails to
  typecheck, so the cluster was kept generation-consistent with its own
  consumers. The Zoo-Code-Org#2357 review fixes are therefore ported to this branch's
  safeWriteText copy as a follow-up commit, not smuggled in as a re-copy.
- src/core/task/Task.ts auto-merged (main-side edits and U8 hunks in
  disjoint regions); accepted after typecheck.
- eslint-suppressions.json: main's copy kept; the verification pass proved
  exactly one reduction (core/tools/__tests__/readFileTool.spec.ts
  no-explicit-any 96 -> 94, because this branch's copy of that file is cleaner
  than main's). The reduction was applied by hand to main's formatting rather
  than by carrying the plugin's whole-file prune rewrite across the merge.
  No count increased; a full-tree lint (--max-warnings=0) passes on this commit.
easonLiangWorldedtech added a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Nov 8, 2026
Conflict resolutions with named sources (auditable after the fact):

- src/eslint-suppressions.json: taken from main unchanged; no prune was
  carried across the merge (eslint-verify is a separate gate step).
- The safeWriteJson set (src/utils/safeWriteJson.ts,
  src/utils/__tests__/safeWriteJson.test.ts,
  src/utils/__tests__/safeWriteJson.lockKey.spec.ts,
  src/utils/__tests__/safeWriteJson.interruption.test.ts) and the whole
  safeWriteText set (src/services/file-safety/safeWriteText.ts + spec +
  integration spec): pinned together at the reconciled U5 PR head
  refs/pull/1914 (74f5eb2), which already carries main's Zoo-Code-Org#2297
  trace/recovery work that main-side taskMessages.ts consumes. They must
  travel as one generation: an earlier pass of this merge kept the U5 json
  set but paired it with this branch's era safeWriteText, and CI on
  windows-latest failed the misc shard in 15 tests (safeWriteJson.test
  backup-preservation rows and the whole subprocess-interruption suite)
  while ubuntu passed - era safeWriteText carries windows-only ACL/publish
  branches that re-route the U5 specs' fs-mock expectations and shift the
  interruption harness's kill timing. Generation pairing, not logic:
  the reason lines are mock-call argument mismatches only.
- src/integrations/editor/DiffViewProvider.ts: redone as an explicit
  three-way merge (branch head 2e00da1 x shared base 3ee042d x main
  73931fe), which applies cleanly; the main-side delta is +71 bytes. The
  first pass's auto-merge silently dropped the guarded-save signature region
  (callers pass 9 args while the merged saveChanges accepted 8), which is why
  the redo was done against the named bases rather than trusting auto-merge.
- src/integrations/editor/__tests__/DiffViewProvider.spec.ts: main's
  Zoo-Code-Org#2309-rewritten spec is the base; U8's guarded-save semantics are grafted
  onto it (makeTaskDouble extended to carry cwd + a real ObservationRegistry,
  guarded-publish seed observations, CAS-rollback stat/token pins, and the
  new "open() waits for a finalization in flight" test rewritten onto main's
  shared test-utils/vscode doubles, so no suppression count moves). The
  guarded-publish groups assert the mocked safeWriteText's call arguments,
  so their expected option name follows the pinned generation:
  "retainedTarget: true" -> "backup: true" (43 sites across this spec and
  guardedWrite.spec, identical arguments and counts otherwise).
- The guarded-write cluster (guardedWrite.spec, observationRegistry.ts +
  spec, ReadFileTool.ts + readFileTool.spec, indentation-reader.ts + both
  specs, ApplyDiffTool/ApplyPatchTool/EditFileTool/EditTool/SearchReplaceTool/
  WriteToFileTool + execute/edit/failureReporting/guardedWrite specs) stays
  at this branch's own era content from 2e00da1. Generation probe:
  guardedWrite exists in three incompatible generations across the lineage
  heads (cancellationGeneration on the U5 heads, GuardStaleVersionError on
  this head, canonicalizeForApproval on the U7 head); pairing this branch's
  DiffViewProvider and tool callers with a foreign generation fails to
  typecheck, so the cluster was kept generation-consistent with its own
  consumers.
- src/core/tools/guardedWrite.ts keeps this branch's public surface
  (GuardStaleVersionError compare-and-swap is diff-view saveChanges'
  contract) and is adapted to the pinned file-safety generation in two
  ways: the publish option is now spelled "backup" (the lineage name for
  the same copy-before-rename/unlink-after contract) at both call sites,
  and canonicalDirKey + resolveLinkPathLockKey - which the lineage head
  never exported - are carried verbatim as module-local helpers here,
  this file being their only consumer. Zoo-Code-Org#2357's review-fix generation
  (25a1ad6) is deliberately not smuggled in here; converging
  guardedWrite and safeWriteText onto it is a code-owner semantic-merge
  decision for Zoo-Code-Org#1914/Zoo-Code-Org#2357. The adaptation is shaped so that a queue-time
  resolution taking main's evolved safeWriteText still typechecks: this
  file only needs backup, resolveLockKey, resolvePublishTarget and
  PostCommitDurabilityError, all present in that generation.
- src/core/task/Task.ts auto-merged (main-side edits and U8 hunks in
  disjoint regions); accepted after typecheck.
- eslint-suppressions.json verification pass proved exactly one reduction
  (core/tools/__tests__/readFileTool.spec.ts no-explicit-any 96 -> 94,
  because this branch's copy of that file is cleaner than main's), applied
  by hand to main's formatting rather than carrying the plugin's whole-file
  prune rewrite across the merge. No count increased; a full-tree lint
  (--max-warnings=0) passes on this commit.
easonLiangWorldedtech added a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Nov 8, 2026
Conflict resolutions with named sources (auditable after the fact):

- src/eslint-suppressions.json: taken from main unchanged; no prune was
  carried across the merge (eslint-verify is a separate gate step).
- The safeWriteJson set (src/utils/safeWriteJson.ts,
  src/utils/__tests__/safeWriteJson.test.ts,
  src/utils/__tests__/safeWriteJson.lockKey.spec.ts,
  src/utils/__tests__/safeWriteJson.interruption.test.ts) and the whole
  safeWriteText set (src/services/file-safety/safeWriteText.ts + spec +
  integration spec): pinned together at the reconciled U5 PR head
  refs/pull/1914 (74f5eb2), which already carries main's Zoo-Code-Org#2297
  trace/recovery work that main-side taskMessages.ts consumes. They must
  travel as one generation: an earlier pass of this merge kept the U5 json
  set but paired it with this branch's era safeWriteText, and CI on
  windows-latest failed the misc shard in 15 tests (safeWriteJson.test
  backup-preservation rows and the whole subprocess-interruption suite)
  while ubuntu passed - era safeWriteText carries windows-only ACL/publish
  branches that re-route the U5 specs' fs-mock expectations and shift the
  interruption harness's kill timing. Generation pairing, not logic:
  the reason lines are mock-call argument mismatches only.
- src/integrations/editor/DiffViewProvider.ts: redone as an explicit
  three-way merge (branch head 2e00da1 x shared base 3ee042d x main
  73931fe), which applies cleanly; the main-side delta is +71 bytes. The
  first pass's auto-merge silently dropped the guarded-save signature region
  (callers pass 9 args while the merged saveChanges accepted 8), which is why
  the redo was done against the named bases rather than trusting auto-merge.
- src/integrations/editor/__tests__/DiffViewProvider.spec.ts: main's
  Zoo-Code-Org#2309-rewritten spec is the base; U8's guarded-save semantics are grafted
  onto it (makeTaskDouble extended to carry cwd + a real ObservationRegistry,
  guarded-publish seed observations, CAS-rollback stat/token pins, and the
  new "open() waits for a finalization in flight" test rewritten onto main's
  shared test-utils/vscode doubles, so no suppression count moves). The
  guarded-publish groups assert the mocked safeWriteText's call arguments,
  so their expected option name follows the pinned generation:
  "retainedTarget: true" -> "backup: true" (43 sites across this spec and
  guardedWrite.spec, identical arguments and counts otherwise).
- The guarded-write cluster (guardedWrite.spec, observationRegistry.ts +
  spec, ReadFileTool.ts + readFileTool.spec, indentation-reader.ts + both
  specs, ApplyDiffTool/ApplyPatchTool/EditFileTool/EditTool/SearchReplaceTool/
  WriteToFileTool + execute/edit/failureReporting/guardedWrite specs) stays
  at this branch's own era content from 2e00da1. Generation probe:
  guardedWrite exists in three incompatible generations across the lineage
  heads (cancellationGeneration on the U5 heads, GuardStaleVersionError on
  this head, canonicalizeForApproval on the U7 head); pairing this branch's
  DiffViewProvider and tool callers with a foreign generation fails to
  typecheck, so the cluster was kept generation-consistent with its own
  consumers.
- src/core/tools/guardedWrite.ts keeps this branch's public surface
  (GuardStaleVersionError compare-and-swap is diff-view saveChanges'
  contract) and is adapted to the pinned file-safety generation in two
  ways: the publish option is now spelled "backup" (the lineage name for
  the same copy-before-rename/unlink-after contract) at both call sites,
  and canonicalDirKey + resolveLinkPathLockKey - which the lineage head
  never exported - are carried verbatim as module-local helpers here,
  this file being their only consumer. Zoo-Code-Org#2357's review-fix generation
  (25a1ad6) is deliberately not smuggled in here; converging
  guardedWrite and safeWriteText onto it is a code-owner semantic-merge
  decision for Zoo-Code-Org#1914/Zoo-Code-Org#2357. The adaptation is shaped so that a queue-time
  resolution taking main's evolved safeWriteText still typechecks: this
  file only needs backup, resolveLockKey, resolvePublishTarget and
  PostCommitDurabilityError, all present in that generation.
- src/core/task/Task.ts auto-merged (main-side edits and U8 hunks in
  disjoint regions); accepted after typecheck.
- eslint-suppressions.json verification pass proved exactly one reduction
  (core/tools/__tests__/readFileTool.spec.ts no-explicit-any 96 -> 94,
  because this branch's copy of that file is cleaner than main's), applied
  by hand to main's formatting rather than carrying the plugin's whole-file
  prune rewrite across the merge. No count increased; a full-tree lint
  (--max-warnings=0) passes on this commit.
- Teardown-tail deadlock fix (addresses the open major review finding on this
  PR): revertChanges' tail ran this.reset() INSIDE the teardown claim while
  open() serializes with that claim, and reset() joins the session-start
  reservation main's Zoo-Code-Org#2309 introduced - so reset -> reservation -> open ->
  teardownInFlight -> the tail's own finalization formed a wait cycle when an
  open() began while the tail was still running. The tail now calls
  finalizeSessionFromTeardownTail(), the same finalization without the
  reservation join (the reservation it would join is itself waiting on this
  pass). A regression test drives exactly that interleaving (revertChanges +
  overlapping open() with the tail gated) and asserts both settle; the
  companion ownership assertion in the "revert already owns the teardown"
  test was updated to the fixed ordering: the owning pass' finalization now
  settles without deferral, so its own-diff-view close happens exactly once
  from the owner while the joined save cleanup still restores no tabs.
easonLiangWorldedtech added a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Nov 8, 2026
Conflict resolutions with named sources (auditable after the fact):

- src/eslint-suppressions.json: taken from main unchanged; no prune was
  carried across the merge (eslint-verify is a separate gate step).
- The safeWriteJson set (src/utils/safeWriteJson.ts,
  src/utils/__tests__/safeWriteJson.test.ts,
  src/utils/__tests__/safeWriteJson.lockKey.spec.ts,
  src/utils/__tests__/safeWriteJson.interruption.test.ts) and the whole
  safeWriteText set (src/services/file-safety/safeWriteText.ts + spec +
  integration spec): pinned together at the reconciled U5 PR head
  refs/pull/1914 (74f5eb2), which already carries main's Zoo-Code-Org#2297
  trace/recovery work that main-side taskMessages.ts consumes. They must
  travel as one generation: an earlier pass of this merge kept the U5 json
  set but paired it with this branch's era safeWriteText, and CI on
  windows-latest failed the misc shard in 15 tests (safeWriteJson.test
  backup-preservation rows and the whole subprocess-interruption suite)
  while ubuntu passed - era safeWriteText carries windows-only ACL/publish
  branches that re-route the U5 specs' fs-mock expectations and shift the
  interruption harness's kill timing. Generation pairing, not logic:
  the reason lines are mock-call argument mismatches only.
- src/integrations/editor/DiffViewProvider.ts: redone as an explicit
  three-way merge (branch head 2e00da1 x shared base 3ee042d x main
  73931fe), which applies cleanly; the main-side delta is +71 bytes. The
  first pass's auto-merge silently dropped the guarded-save signature region
  (callers pass 9 args while the merged saveChanges accepted 8), which is why
  the redo was done against the named bases rather than trusting auto-merge.
- src/integrations/editor/__tests__/DiffViewProvider.spec.ts: main's
  Zoo-Code-Org#2309-rewritten spec is the base; U8's guarded-save semantics are grafted
  onto it (makeTaskDouble extended to carry cwd + a real ObservationRegistry,
  guarded-publish seed observations, CAS-rollback stat/token pins, and the
  new "open() waits for a finalization in flight" test rewritten onto main's
  shared test-utils/vscode doubles, so no suppression count moves). The
  guarded-publish groups assert the mocked safeWriteText's call arguments,
  so their expected option name follows the pinned generation:
  "retainedTarget: true" -> "backup: true" (43 sites across this spec and
  guardedWrite.spec, identical arguments and counts otherwise).
- The guarded-write cluster (guardedWrite.spec, observationRegistry.ts +
  spec, ReadFileTool.ts + readFileTool.spec, indentation-reader.ts + both
  specs, ApplyDiffTool/ApplyPatchTool/EditFileTool/EditTool/SearchReplaceTool/
  WriteToFileTool + execute/edit/failureReporting/guardedWrite specs) stays
  at this branch's own era content from 2e00da1. Generation probe:
  guardedWrite exists in three incompatible generations across the lineage
  heads (cancellationGeneration on the U5 heads, GuardStaleVersionError on
  this head, canonicalizeForApproval on the U7 head); pairing this branch's
  DiffViewProvider and tool callers with a foreign generation fails to
  typecheck, so the cluster was kept generation-consistent with its own
  consumers.
- src/core/tools/guardedWrite.ts keeps this branch's public surface
  (GuardStaleVersionError compare-and-swap is diff-view saveChanges'
  contract) and is adapted to the pinned file-safety generation in two
  ways: the publish option is now spelled "backup" (the lineage name for
  the same copy-before-rename/unlink-after contract) at both call sites,
  and canonicalDirKey + resolveLinkPathLockKey - which the lineage head
  never exported - are carried verbatim as module-local helpers here,
  this file being their only consumer. Zoo-Code-Org#2357's review-fix generation
  (25a1ad6) is deliberately not smuggled in here; converging
  guardedWrite and safeWriteText onto it is a code-owner semantic-merge
  decision for Zoo-Code-Org#1914/Zoo-Code-Org#2357. The adaptation is shaped so that a queue-time
  resolution taking main's evolved safeWriteText still typechecks: this
  file only needs backup, resolveLockKey, resolvePublishTarget and
  PostCommitDurabilityError, all present in that generation.
- src/core/task/Task.ts auto-merged (main-side edits and U8 hunks in
  disjoint regions); accepted after typecheck.
- eslint-suppressions.json verification pass proved exactly one reduction
  (core/tools/__tests__/readFileTool.spec.ts no-explicit-any 96 -> 94,
  because this branch's copy of that file is cleaner than main's), applied
  by hand to main's formatting rather than carrying the plugin's whole-file
  prune rewrite across the merge. No count increased; a full-tree lint
  (--max-warnings=0) passes on this commit.
- Teardown-tail deadlock fix (addresses the open major review finding on this
  PR): revertChanges' tail ran this.reset() INSIDE the teardown claim while
  open() serializes with that claim, and reset() joins the session-start
  reservation main's Zoo-Code-Org#2309 introduced - so reset -> reservation -> open ->
  teardownInFlight -> the tail's own finalization formed a wait cycle when an
  open() began while the tail was still running. The tail now calls
  finalizeSessionFromTeardownTail(), the same finalization without the
  reservation join (the reservation it would join is itself waiting on this
  pass). A regression test drives exactly that interleaving (revertChanges +
  overlapping open() with the tail gated) and asserts both settle; the
  companion ownership assertion in the "revert already owns the teardown"
  test was updated to the fixed ordering: the owning pass' finalization now
  settles without deferral, so its own-diff-view close happens exactly once
  from the owner while the joined save cleanup still restores no tabs.
- src/utils/__tests__/safeWriteJson.test.ts additionally adopted from 025fb7d
  (Zoo-Code-Org#2359 head, test-only hardening): the "preserve backups when another backup
  stat fails with ENOENT/EACCES/EIO" rows failed on the windows platform-unit
  shard because the double keyed the stat injection on os.tmpdir()'s spelling
  while the writer stats the canonical form of the same file (windows realpath
  spells them differently; reason line read from run 42461215177 job
  128670729075: 'promise resolved "undefined" instead of rejecting' at
  safeWriteJson.test.ts:675 - the same rows are red on Zoo-Code-Org#1914's own head
  74f5eb2, run 42448302148 job 128631851822). src/utils/safeWriteJson.ts is
  byte-identical between 74f5eb2 and 025fb7d, so this is the same
  generation's json paired with its windows-proven test file (025fb7d's
  windows platform-unit passed this test against the identical implementation,
  run 42455872567 job 128654644966), and the pairing rule stands untouched:
  json + its specs still travel together as one generation, now keyed on inode
  instead of path spelling.
easonLiangWorldedtech added a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Nov 8, 2026
Conflict set resolved per the stack's settled copy sources; every copy source
named per file group:

- src/utils/safeWriteJson.ts + src/utils/__tests__/safeWriteJson.test.ts +
  .lockKey.spec.ts + .interruption.test.ts: copy source = settled Zoo-Code-Org#1916 head
  711ada7 (which itself pins the json quartet at
  74f5eb2, the Zoo-Code-Org#1914 head). Main-side taskMessages.ts consumes
  SafeWriteJsonTraceStage + the trace option, which only this json generation
  provides; the era json predates main's Zoo-Code-Org#2297 and fails tsc against it.
- src/services/file-safety/safeWriteText.ts + spec + integration spec: same copy
  source (the same 74f5eb2 pin). json and text travel as one generation.
- src/core/tools/guardedWrite.ts + src/core/tools/__tests__/guardedWrite.spec.ts:
  copy source = 711ada7 - the U8-era guardedWrite surface adapted to the pinned
  generation: publish calls carry backup: true, resolveLinkPathLockKey and
  canonicalDirKey are module-local helpers, and the spec's mock arguments spell
  backup: true where the era spelled retainedTarget: true (31 sites; the 12
  DiffViewProvider.spec sites travel with that file's copy below).
- src/integrations/editor/DiffViewProvider.ts + __tests__/DiffViewProvider.spec.ts:
  copy source = 711ada7 in full. This is the era x 3ee042d x main 3-way
  resolution Zoo-Code-Org#1916 carries, and it includes the teardown-tail deadlock fix:
  revertChanges' tail finalizes via finalizeSessionFromTeardownTail() - the same
  finalization minus the session-start join, because the reservation that join
  would wait on is itself waiting on this pass (reset -> reservation -> open ->
  teardownInFlight -> the tail -> reset would never settle). The fix is verified
  IN THIS TREE, not inherited silently: its regression test "revertChanges' tail
  does not deadlock when open() starts during the pass" runs in this branch's
  misc shard gate, together with the companion ownership assertion the fix owes.
- src/eslint-suppressions.json: main's counts first (the theirs side); u6-only
  file entries kept only where main carries none (25 entries); then eslint
  verify with --prune-suppressions on the merged tree. No prune carried across
  the merge: shared-file counts restart from main and only eslint may lower them.

Generation-pairing rule enforced by this resolution (adopted as a rule by the
stack lead; the four-generation map and the discriminator experiment are posted
on Zoo-Code-Org#2350 where the convergence review reads them): json plus its specs and text
plus its specs travel as ONE generation, always. The evidence behind it: on
Zoo-Code-Org#1916 an era-text x 74f5-json chimera passed tsc --noEmit, every ubuntu leg, and
every local misc shard, and failed only on the windows platform-unit misc shard
(15 rows in the era windows-only ACL/backup-stat and interruption kill-timing
branches, run 42459507387 job 128665547657) - the shape of a red a reviewer
would attribute to the change in the diff rather than to the pairing.

Auto-merged Task.ts accepted after review: same two-lineage splice Zoo-Code-Org#1916 merged
(era-side hunks and main's Zoo-Code-Org#2297 hunks are disjoint regions).
- src/utils/__tests__/safeWriteJson.test.ts additionally adopted from 025fb7d
  (Zoo-Code-Org#2359 head, test-only hardening, keyed on inode not path spelling): the
  windows platform-unit rows that motivated it are the same on any tree
  carrying 74f5eb2's json test (reason lines read from runs 42461215177
  128670729075 and 42448302148 128631851822). src/utils/safeWriteJson.ts is
  byte-identical between the two refs, so json + its specs still travel as one
  generation - this branch adopts the generation's windows-proven test file
  (025fb7d windows platform-unit green with this file against the identical
  implementation, run 42455872567 job 128654644966), matching Zoo-Code-Org#1914/Zoo-Code-Org#1916
  byte-for-byte so the stack carries one copy of this test.
easonLiangWorldedtech added a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Nov 8, 2026
The windows platform lane at 3159d0a failed on four rows of
core/task-persistence/__tests__/TaskHistoryStore.deleteSemantics.spec.ts (the two read-back rows at
:145 and :146, the withFileLock argument at :159 and the deleteMany batch row at :255), every one of
them ENOENT or an argument mismatch under
C:\Users\RUNNER~1\AppData\Local\Temp\task-history-delete-semantics-<rand>. The same head is green on
ubuntu and the spec was last touched on main by bb6cbee (Zoo-Code-Org#2297), so a file this branch never
changed went red only on this branch.

The cause is ours: deleteJsonFile (utils/jsonFileRecovery.ts:89) now calls
withFileLock(await resolveLockKey(path.resolve(filePath)), ...) at :95 and unlinks the callback's
canonical path, so a canonical spelling crosses the mock boundary into rows whose own expected paths
are built from the spelling os.tmpdir() answered. On the hosted Windows runner those two are the 8.3
form of the profile directory and its long name, so === on paths stopped recognising the injected
EACCES target, the real unlink ran, and the read-back rows saw ENOENT. Ubuntu never sees it because
there the two spellings are one string.

The repair is in the comparison rather than the canonicalisation: reverting the canonicalisation
would undo the exclusion the reviewer asked for while fixing nothing about the underlying fact.
  - the two "%s preserves the canonical file if backup cleanup fails" rows and the deleteMany
    "continues the batch after a failed unlink" row inject through sameFileAs now, which resolves
    both sides before comparing;
  - "unlinks under the shared per-file lock, evicts cache and mtime, and writes through once" pins
    the acquired key to the file inside the directory object the row's path resolves to (canonical
    parent plus basename), because the delete removes the file and after that neither realpath nor
    stat can answer for it.
This is a main-authored row updated because our change deliberately moved a canonical path across a
mock boundary, not because the row was ever wrong about what it wanted to pin.

Reproduced on this machine rather than inferred from a log: pointing TMP/TEMP at a junction alias of
a real directory (link spelling against target spelling, verified different through realpath)
produced the same four failures before this change and 10 passed (10) after; the ordinary temp root
also runs 10 passed (10). The 8.3 spelling itself is only observable on a hosted runner, so the
hosted case stays lane-only evidence and is labelled that way.

Control: restoring the spelling comparison inside sameFileAs reddens 3 of 10 under the junction root
(the two backup-cleanup rows and the deleteMany batch row); the fourth row is decided by the
parent-directory comparison, which that control does not touch. Blast radius for the file, whole
core group at this head: 199 files / 4426 passed | 9 skipped (4435), exit 0 - those 9 skips are the
windows-gated rows the lane reports too. eslint --prune-suppressions --max-warnings=0 exit 0, with
the prune's unrelated rewrites elsewhere in src/eslint-suppressions.json reverted rather than
shipped; tsc --noEmit -p . reports only the junction artefact at activate/registerCommands.ts
(88,102); prettier --check clean. No product change, no it.runIf added or removed.
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