Repository navigation
fix(task): merge stale partial reasoning snapshot instead of duplicating #1708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
51e8261
38d0edc
7f4b23d
bcd3879
31b031c
5815295
ecce021
a372707
ff56613
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2750,6 +2750,40 @@ | |||||||||||||||||||||||||
| this.processQueuedMessages() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Returns the index of a stranded in-flight partial "say" snapshot of the | ||||||||||||||||||||||||||
| * same type whose text is a prefix (or extension) of `text`, or -1 if none | ||||||||||||||||||||||||||
| * exists. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * A partial stream can be interrupted by other messages (tool use, api_req, | ||||||||||||||||||||||||||
| * ...) being appended while it is in flight, which strands its partial | ||||||||||||||||||||||||||
| * snapshot before the stream resumes or completes. Appending a fresh message | ||||||||||||||||||||||||||
| * in that case would persist — and the webview would render — the same | ||||||||||||||||||||||||||
| * content twice, so callers merge into the stranded snapshot instead. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * Empty text never matches, on either side. `m.text.startsWith("")` is true | ||||||||||||||||||||||||||
| * for every stored text, so an empty write (`AttemptCompletionTool` passes | ||||||||||||||||||||||||||
| * `result ?? ""`) would otherwise adopt an abandoned snapshot from an | ||||||||||||||||||||||||||
| * earlier turn: the streaming write would overwrite its text and the | ||||||||||||||||||||||||||
| * completing write would finalize it as this turn's message, re-stamping | ||||||||||||||||||||||||||
| * `lastMessageTs` with the dead row's ts. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| private findStalePartialSaySnapshotIndex(type: ClineSay, text: string | undefined): number { | ||||||||||||||||||||||||||
| if (typeof text !== "string" || text === "") { | ||||||||||||||||||||||||||
| return -1 | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return findLastIndex( | ||||||||||||||||||||||||||
| this.clineMessages, | ||||||||||||||||||||||||||
| (m) => | ||||||||||||||||||||||||||
| m.partial === true && | ||||||||||||||||||||||||||
| m.type === "say" && | ||||||||||||||||||||||||||
| m.say === type && | ||||||||||||||||||||||||||
| typeof m.text === "string" && | ||||||||||||||||||||||||||
| m.text !== "" && | ||||||||||||||||||||||||||
|
Check warning on line 2782 in src/core/task/Task.ts
|
||||||||||||||||||||||||||
| (text.startsWith(m.text) || m.text.startsWith(text)), | ||||||||||||||||||||||||||
|
Check warning on line 2783 in src/core/task/Task.ts
|
||||||||||||||||||||||||||
|
myk1yt marked this conversation as resolved.
Resolved
|
||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async say( | ||||||||||||||||||||||||||
| type: ClineSay, | ||||||||||||||||||||||||||
| text?: string, | ||||||||||||||||||||||||||
|
|
@@ -2789,22 +2823,57 @@ | |||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| // This is a new partial message, so add it with partial state. | ||||||||||||||||||||||||||
| const sayTs = Date.now() | ||||||||||||||||||||||||||
| const staleSnapshotIndex = this.findStalePartialSaySnapshotIndex(type, text) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (staleSnapshotIndex !== -1) { | ||||||||||||||||||||||||||
| // The in-flight partial snapshot of this stream was stranded | ||||||||||||||||||||||||||
| // by an intervening message; keep streaming into it instead of | ||||||||||||||||||||||||||
| // starting a duplicate entry that would render twice. | ||||||||||||||||||||||||||
| const staleSnapshot = this.clineMessages[staleSnapshotIndex] | ||||||||||||||||||||||||||
| staleSnapshot.text = text | ||||||||||||||||||||||||||
| staleSnapshot.images = images | ||||||||||||||||||||||||||
| staleSnapshot.partial = partial | ||||||||||||||||||||||||||
| staleSnapshot.progressStatus = progressStatus | ||||||||||||||||||||||||||
| // Move the merged row to the tail so the next delta takes the | ||||||||||||||||||||||||||
| // in-place fast path above instead of re-entering this branch | ||||||||||||||||||||||||||
| // and awaiting another full save. `ts` is untouched: it is the | ||||||||||||||||||||||||||
| // webview's virtuoso key, so re-stamping it would remount the row. | ||||||||||||||||||||||||||
| this.clineMessages.splice(staleSnapshotIndex, 1) | ||||||||||||||||||||||||||
| this.clineMessages.push(staleSnapshot) | ||||||||||||||||||||||||||
| // A stranded snapshot merge has no guaranteed later save, so | ||||||||||||||||||||||||||
| // persist the merged delta before the webview update — the | ||||||||||||||||||||||||||
| // same save-then-post pattern as the complete-merge branch. | ||||||||||||||||||||||||||
| const saved = await this.saveClineMessages() | ||||||||||||||||||||||||||
|
myk1yt marked this conversation as resolved.
Resolved
|
||||||||||||||||||||||||||
| if (!saved) { | ||||||||||||||||||||||||||
| // The persistence write failed: the on-disk record still carries | ||||||||||||||||||||||||||
| // the pre-merge snapshot while the in-memory message is updated. | ||||||||||||||||||||||||||
| // Skip the webview-only post so the two views do not diverge (a | ||||||||||||||||||||||||||
| // reload would restore the stale text). The next saveClineMessages() | ||||||||||||||||||||||||||
| // call re-persists the full message array and repairs the disk record. | ||||||||||||||||||||||||||
| console.error("[Task#say] saveClineMessages failed; skipping webview update") | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| this.updateClineMessage(staleSnapshot).catch((error) => { | ||||||||||||||||||||||||||
| console.error("[Task#say] updateClineMessage failed:", error) | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| const sayTs = Date.now() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!options.isNonInteractive) { | ||||||||||||||||||||||||||
| this.lastMessageTs = sayTs | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if (!options.isNonInteractive) { | ||||||||||||||||||||||||||
|
Check warning on line 2862 in src/core/task/Task.ts
|
||||||||||||||||||||||||||
| this.lastMessageTs = sayTs | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| await this.addToClineMessages({ | ||||||||||||||||||||||||||
| ts: sayTs, | ||||||||||||||||||||||||||
| type: "say", | ||||||||||||||||||||||||||
| say: type, | ||||||||||||||||||||||||||
| text, | ||||||||||||||||||||||||||
| images, | ||||||||||||||||||||||||||
| partial, | ||||||||||||||||||||||||||
| contextCondense, | ||||||||||||||||||||||||||
| contextTruncation, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| await this.addToClineMessages({ | ||||||||||||||||||||||||||
| ts: sayTs, | ||||||||||||||||||||||||||
| type: "say", | ||||||||||||||||||||||||||
| say: type, | ||||||||||||||||||||||||||
| text, | ||||||||||||||||||||||||||
| images, | ||||||||||||||||||||||||||
| partial, | ||||||||||||||||||||||||||
| contextCondense, | ||||||||||||||||||||||||||
| contextTruncation, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| // New now have a complete version of a previously partial message. | ||||||||||||||||||||||||||
|
|
@@ -2832,21 +2901,56 @@ | |||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| // This is a new and complete message, so add it like normal. | ||||||||||||||||||||||||||
| const sayTs = Date.now() | ||||||||||||||||||||||||||
| const staleSnapshotIndex = this.findStalePartialSaySnapshotIndex(type, text) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!options.isNonInteractive) { | ||||||||||||||||||||||||||
| this.lastMessageTs = sayTs | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if (staleSnapshotIndex !== -1) { | ||||||||||||||||||||||||||
| // The complete version of a stranded partial snapshot: replace | ||||||||||||||||||||||||||
| // it in place so the frozen snapshot is not persisted (and | ||||||||||||||||||||||||||
| // rendered) as a duplicate. | ||||||||||||||||||||||||||
| const staleSnapshot = this.clineMessages[staleSnapshotIndex] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| await this.addToClineMessages({ | ||||||||||||||||||||||||||
| ts: sayTs, | ||||||||||||||||||||||||||
| type: "say", | ||||||||||||||||||||||||||
| say: type, | ||||||||||||||||||||||||||
| text, | ||||||||||||||||||||||||||
| images, | ||||||||||||||||||||||||||
| contextCondense, | ||||||||||||||||||||||||||
| contextTruncation, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| if (!options.isNonInteractive) { | ||||||||||||||||||||||||||
| this.lastMessageTs = staleSnapshot.ts | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| staleSnapshot.text = text | ||||||||||||||||||||||||||
| staleSnapshot.images = images | ||||||||||||||||||||||||||
| staleSnapshot.partial = false | ||||||||||||||||||||||||||
| staleSnapshot.progressStatus = progressStatus | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Instead of streaming partialMessage events, we do a save | ||||||||||||||||||||||||||
| // and post like normal to persist to disk. | ||||||||||||||||||||||||||
| const saved = await this.saveClineMessages() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!saved) { | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This early return skips all of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right, and I am not going to change it in this round: this is a policy question and it should be your call, not mine. The asymmetry is real and it is wider than the two branches you pointed at. Three sibling branches in
So the gate you are asking about is also present in the in-repo precedent I copied it from. Fixing it properly means deciding the policy once, not patching one branch. The design I would propose: split the gate.
That is a net deletion: both branches lose the The counter-argument, which is why I am not pre-empting you: it inverts two existing specs that currently assert Say the word and I will implement the split gate in a follow-up commit on this branch, with the two specs inverted to assert "emit fired, post did not" and a red-on-head proof that the emit is currently suppressed when |
||||||||||||||||||||||||||
| // Same durable-state gate as the partial-merge branch above: the | ||||||||||||||||||||||||||
| // on-disk record still carries the partial snapshot, so posting the | ||||||||||||||||||||||||||
| // completed row would let a reload restore a spinner over a | ||||||||||||||||||||||||||
| // finished message. The next saveClineMessages() repairs the disk. | ||||||||||||||||||||||||||
| console.error("[Task#say] saveClineMessages failed; skipping webview update") | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| this.updateClineMessage(staleSnapshot).catch((error) => { | ||||||||||||||||||||||||||
| console.error("[Task#say] updateClineMessage failed:", error) | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| const sayTs = Date.now() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!options.isNonInteractive) { | ||||||||||||||||||||||||||
| this.lastMessageTs = sayTs | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| await this.addToClineMessages({ | ||||||||||||||||||||||||||
| ts: sayTs, | ||||||||||||||||||||||||||
| type: "say", | ||||||||||||||||||||||||||
| say: type, | ||||||||||||||||||||||||||
| text, | ||||||||||||||||||||||||||
| images, | ||||||||||||||||||||||||||
| contextCondense, | ||||||||||||||||||||||||||
| contextTruncation, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.