A rewound turn's id is forgotten, so sending it again runs the turn anew
The turn log answered a repeated id with what that turn had answered, which was right for a prompt sent twice over a dropped link and wrong for a turn the web UI had just rewound to edit: the same id came back with the old reply. rewind now returns the ids it took out, both handlers forget them from the log, and the turn file drops them too, or the next load would have brought them back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
7 files changed
+49
-15
No files matched your search
+2
-1
@@ -1008,7 +1008,8 @@ export class AcpAgent {
|
||||
if (s.prompting || s.turns.queued().length) throw new RpcError(INVALID_PARAMS, "this session is working — rewind it when the reply is done")
|
||||
const reverted = s.app.turns.rewind(turnId)
|
||||
if (reverted === undefined) throw new RpcError(INVALID_PARAMS, `this session has no turn ${turnId}`)
|
||||
return { reverted }
|
||||
s.turns.forget(reverted)
|
||||
return { reverted: reverted.length }
|
||||
}
|
||||
|
||||
/** `_lembas/session/delete`: the chat was deleted in the web UI, so the session is too —
|
||||
|
||||
+3
-2
@@ -511,9 +511,10 @@ export class Share {
|
||||
const turnId = String(p?.turnId ?? "")
|
||||
const reverted = this.app.turns.rewind(turnId)
|
||||
if (reverted === undefined) throw new RpcError(INVALID_PARAMS, `this session has no turn ${turnId}`)
|
||||
this.turns.forget(reverted)
|
||||
this.hooks.rewound?.()
|
||||
this.hooks.notice(`the web UI rewound ${reverted} turn${reverted === 1 ? "" : "s"} — the files they changed stay as they are`)
|
||||
return { reverted }
|
||||
this.hooks.notice(`the web UI rewound ${reverted.length} turn${reverted.length === 1 ? "" : "s"} — the files they changed stay as they are`)
|
||||
return { reverted: reverted.length }
|
||||
})
|
||||
peer.handle("hub/steer", (p) => {
|
||||
this.mine(p)
|
||||
|
||||
@@ -30,6 +30,8 @@ const TURN_FILE_DAYS = 30
|
||||
export interface TurnStore<R> {
|
||||
load(): [string, R | null][]
|
||||
save(done: [string, R | null][]): void
|
||||
/** Turns a rewind took out: gone from the file too, or the next load would bring them back. */
|
||||
forget?(ids: string[]): void
|
||||
}
|
||||
|
||||
/** The file's entries, those that are entries: a damaged file is an empty one, never a crash. */
|
||||
@@ -64,6 +66,16 @@ export function turnFile<R>(file: string): TurnStore<R> & { drop(): void } {
|
||||
renameSync(tmp, file)
|
||||
} catch {}
|
||||
},
|
||||
forget(ids) {
|
||||
try {
|
||||
const gone = new Set(ids)
|
||||
const kept = readEntries<R>(file).filter(([id]) => !gone.has(id))
|
||||
mkdirSync(dirname(file), { recursive: true, mode: 0o700 })
|
||||
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
||||
writeFileSync(tmp, JSON.stringify(kept), { mode: 0o600 })
|
||||
renameSync(tmp, file)
|
||||
} catch {}
|
||||
},
|
||||
drop() {
|
||||
rmSync(file, { force: true })
|
||||
},
|
||||
@@ -178,6 +190,21 @@ export class TurnLog<R = unknown> {
|
||||
this.running = id
|
||||
}
|
||||
|
||||
/** Turns a rewind took out of the conversation: forgotten, so the same id sent again runs
|
||||
* anew instead of answering what the reverted turn answered. */
|
||||
forget(ids: Iterable<string>) {
|
||||
const gone: string[] = []
|
||||
for (const id of ids) {
|
||||
if (this.entries.get(id)?.state === "done") {
|
||||
this.entries.delete(id)
|
||||
this.values.delete(id)
|
||||
if (this.last === id) this.last = undefined
|
||||
gone.push(id)
|
||||
}
|
||||
}
|
||||
if (gone.length) this.store?.forget?.(gone)
|
||||
}
|
||||
|
||||
ended(id: string) {
|
||||
const e = this.entries.get(id)
|
||||
if (e) e.state = "done"
|
||||
|
||||
Reference in new issue
Block a user