Skip to content

[Fix] Task history disappears when user reopens a task - #1319

Open
zoomote[bot] wants to merge 4 commits into
mainfrom
fix/preserve-task-history-25qz019og2l3b
Open

[Fix] Task history disappears when user reopens a task#1319
zoomote[bot] wants to merge 4 commits into
mainfrom
fix/preserve-task-history-25qz019og2l3b

Conversation

@zoomote

@zoomote zoomote Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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

What changed

  • Preserve finalized reasoning and other saved messages while a task is rehydrated for resume.
  • Distinguish missing, invalid, and unreadable task history from an explicitly empty history instead of converting failures to an empty conversation.
  • Retry a transient missing-file read once with jitter to bridge the safe-write rename window.
  • Merge ordinary UI and API history snapshots under the existing advisory file lock so stale writers preserve newer disk messages.
  • Keep explicit rewind, truncation, and overwrite operations authoritative rather than merging removed history back in.
  • Stop abandoned history-task hydration before it can persist after eviction.
  • Abort delegated-parent reopening when its saved UI history cannot be loaded safely.
  • Add deterministic regression coverage for hydration failures, finalized reasoning, resume/eviction races, retry behavior, merge ordering, legacy histories, and tool call/result ordering.

Why this change was made

Reopening or quickly leaving a saved task could replace valid history with an empty or shortened message list. Concurrent extension instances could also overwrite a newer history suffix with a stale snapshot. This resolves #1279 and extends the lifecycle and persistence safety work tracked by #355, #208, and #1231.

Impact

Users keep their last saved task content when reopening or resuming tasks. Temporary storage gaps are retried, ordinary concurrent writes preserve newer messages, and stale task instances cannot write after being evicted. Explicit user-driven history rewinds continue to replace history as intended.

Related PRs

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.58065% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/task-persistence/apiMessages.ts 96.66% 1 Missing ⚠️
.../core/task-persistence/readFileWithMissingRetry.ts 90.90% 0 Missing and 1 partial ⚠️
src/core/webview/ClineProvider.ts 80.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@martin-rueegg

Copy link
Copy Markdown

Looks like this might also contribute to fixing some of the things I've experienced in #1231, too

@martin-rueegg martin-rueegg left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think the fix is the right approach but can be strengthened by using the new merge functionality of the safeWriteJson() method and updating the local copy with the merged result.

@roomote

Comment thread src/core/task-persistence/taskMessages.ts
Comment thread src/core/task-persistence/taskMessages.ts Outdated
Comment thread src/core/task/Task.ts
@zoomote

zoomote Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the retry and merge feedback in 3f5b2d04:

  • retries the safe-write ENOENT window once with 10–300 ms jitter;
  • merges ordinary UI/API snapshots under the advisory lock while preserving authoritative rewinds;
  • preserves equal timestamps, legacy ordering, completed/answered UI state, and API tool call/result order;
  • adds focused coverage for retry, merge routing, second-stage abort, and delegated-parent read failure.

The full Zoo Code test suite passes (7,560 tests), and the previously uncovered Task/ClineProvider branches now have direct regression coverage. All three review threads were replied to and resolved.

@edelauna
edelauna force-pushed the fix/preserve-task-history-25qz019og2l3b branch from c3492b0 to 8754395 Compare August 26, 2026 02:53
@edelauna
edelauna marked this pull request as ready for review August 26, 2026 03:16
@github-actions github-actions Bot added the awaiting-author PR is waiting for the author to address requested changes label Aug 26, 2026
)
return []
try {
await fs.unlink(oldPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we should write a wrdning. Although the data is migrated, a history file gets deleted. worth at least a warning in the log:

			console.warn(
				`[readApiMessages] Found OLD API conversation history file (claude_messages.json): contained data was migrated to the new history and the file removed.`,
			)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

actually, the migration does NOT happen here. Only the file deleted after successful read. So where do they get merged or saved into the new message? - I only ever see saveApiMessages() called from tests, but not from live code. It's probably me, not seeing it ... ;-)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed locally: readApiMessages() now persists parsed legacy claude_messages.json history through the locked safeWriteJson(..., { merge: mergeApiMessageSnapshots }) path before deleting the legacy file. Regression coverage in apiMessages.spec.ts verifies the migrated history is persisted and that cleanup only happens after persistence succeeds.

function mergeTimestampedSnapshots(
existing: unknown,
incoming: unknown,
mergeMatch: (disk: MessageRecord, next: MessageRecord) => MessageRecord,

@martin-rueegg martin-rueegg Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should this maybe default to undefined?

see comment line 40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed locally: mergeTimestampedSnapshots() now declares mergeMatch as optional, so API snapshot merging can use the helper without supplying an identity callback.

const consumed = consumedByTimestamp.get(message.ts) ?? 0
consumedByTimestamp.set(message.ts, consumed + 1)
const diskMessage = existingGroups.get(message.ts)?.[consumed]
return diskMessage ? mergeMatch(diskMessage, message) : message

@martin-rueegg martin-rueegg Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

see line 10

I think this line would be easier to understand if it would read:

if (mergeMatch !== undefined && diskMessage !== undefined) {
    return mergeMatch(diskMessage, message)
}
return message

rather then the same functionality being burried in mergeApiMessageSnapshots's (_disk, next) => next on line 84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed locally: mergeTimestampedSnapshots() now explicitly returns the incoming message when there is no callback or matching disk message. mergeApiMessageSnapshots() therefore calls the helper directly and no longer passes (_disk, next) => next.

@github-actions github-actions Bot removed the awaiting-author PR is waiting for the author to address requested changes label Aug 26, 2026
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.

[BUG] Many times, it lost my latest session content

3 participants