Skip to content

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

Open

Description

@edelauna

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

Observed failure

Timeline (UTC, 2026-10-21):

  1. 02:15:08.127: create phase passes. It has awaited TaskCompleted and successfully read RESTART_PERSISTENCE_MARKER from ui_messages.json.
  2. 02:15:08.147: create extension host exits.
  3. 02:15:08.233: verify VS Code starts, sharing the same user-data directory.
  4. 02:15:12.410: resume scheduling fails with TaskMessagesReadError / ENOENT reading ui_messages.json.
  5. 02:15:42.388: verify times out after 30 seconds waiting for the resume_completed_task message.

The timeout is downstream of the failed message read. PR #2092 changes development tooling, with no task-persistence runtime changes.

Confirmed implementation weakness

safeWriteJson writes a replacement temp file, renames the existing target to a backup, then renames the replacement to the target:

await fs.rename(absoluteFilePath, actualTempBackupFilePath)
} catch (accessError: any) {
// Explicitly type accessError
if (accessError.code !== "ENOENT") {
// An error other than "file not found" occurred during access check.
throw accessError
}
// Target file does not exist, so no backup is made. actualTempBackupFilePath remains null.
}
// Step 3: Rename the new temporary file to the target file path.
// This is the main "commit" step.
await fs.rename(actualTempNewFilePath, absoluteFilePath)
// If we reach here, the new file is successfully in place.
// The original actualTempNewFilePath is now the main file, so we shouldn't try to clean it up as "temp".
// Mark as "used" or "committed"
actualTempNewFilePath = null
// Step 4: If a backup was created, attempt to delete it.
if (actualTempBackupFilePath) {
try {

A process exit between the two renames leaves the canonical path absent, even when a previous completed write contained the marker. Exception rollback cannot run after process termination. Reading the marker once proves visibility at that instant; it does not prevent a subsequent write from removing that path.

readFileWithMissingRetry only retries ENOENT once after a random 1–10 ms delay. It does not recover backup files. Longer retries cannot repair an interrupted replacement when the writer has exited.

There are independent UI-message writers, including background usage collection and checkpoint messages. The logs do not identify which writer, if any, ran after the marker check.

Deterministic fault-injection reproduction

Using the repository's bundled safeWriteJson and readFileWithMissingRetry implementations:

  1. Write ui_messages.json containing RESTART_PERSISTENCE_MARKER.
  2. Read the marker back successfully (the condition introduced by [Fix] Restart-persistence E2E flakes when the final ui_messages.json write lands after VS Code quits #2080).
  3. Start a child process that calls safeWriteJson again for the same path.
  4. Wrap fs/promises.rename to perform the real rename and exit the child with code 0 immediately after target-to-backup succeeds, before replacement-to-target.
  5. Read the canonical path with readFileWithMissingRetry.

Result: 5/5 injected runs return ENOENT. Both .ui_messages.json.bak_.tmp and .ui_messages.json.new_.tmp remain, and the backup still contains the marker. This reproduces the storage failure mechanism, not the full VS Code shutdown interleaving.

Confidence and missing evidence

The crash window is reproduced. It is a strong candidate for this CI failure, but the CI interleaving is not proven. In particular, a live write/read gap during verify is another possible explanation for ENOENT. Logs do not record rename operations or directory contents.

The run has no uploaded artifacts. runTest.ts removes the scenario workspace in finally, including on failure, so the task directory, backup files, replacement files, and VS Code profile cannot be inspected afterward.

Proposed follow-up

  • Preserve the scenario directory on failure and upload task files, phase results, and VS Code logs as CI artifacts.
  • Add trace points for UI-message write start, backup rename, replacement rename, completion, marker check, and shutdown, including task ID and process ID.
  • Add a subprocess regression test that interrupts the real writer between renames and checks the restart recovery contract.
  • Evaluate a replacement protocol that keeps the committed target available through interruption, or explicit backup recovery under the shared lock. Respect the Windows open-file EPERM behavior documented in [Fix] Restart-persistence E2E flakes when the final ui_messages.json write lands after VS Code quits #2080.
  • If shutdown coordination is used, it must stop new writes and drain existing writes; another marker poll or arbitrary sleep does not establish that condition.
  • Test transient reader gaps separately from permanent absence after writer termination. Avoid treating a larger read retry as a complete fix.

Activity

  1. edelauna commented on Oct 21, 2026

    @edelauna
    ContributorAuthor

    Second symptom: history_item.json missing after restart

    What the log shows

    1. 03:13:48.03: create phase passes. It waited for the marker in ui_messages.json.
    2. 03:13:48.05: create extension host exits.
    3. 03:13:51.41: verify host finishes initializeTaskHistoryStore. There is no initialization error.
    4. 03:13:52.63: verify fails at isTaskInHistory(taskId) with "Task should be present after restart".

    There is no TaskMessagesReadError and no ENOENT on ui_messages.json. Verify fails 4 seconds earlier than in the first report.

    Code trace

    • isTaskInHistory reads the TaskHistoryStore cache.
    • At startup, reconcile fills the cache from tasks/*/history_item.json.
    • If history_item.json is missing or unreadable, reconcile does not add the task to the cache.
    • writeTaskFile writes history_item.json with safeWriteJson. It has the same rename window as ui_messages.json.
    • In Task.saveClineMessages, the history item write comes after the ui_messages.json write. The [Fix] Restart-persistence E2E flakes when the final ui_messages.json write lands after VS Code quits #2080 wait covers only the first write.

    Status

    This is a suspected cause. The log has no rename trace and no directory listing. The run uploaded no artifacts, so nobody can check whether history_item.json was absent.

    Proposed scope change

    • Treat the safeWriteJson rename window as affecting every file it writes, not only ui_messages.json.
    • On read, recover from a leftover .bak_*.tmp file when the target is absent.
    • In the create phase, wait for the persisted history item before quit.
    • Keep the artifact upload and rename trace points already proposed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions