Skip to content

feat(http): take the last plain question and answer back - #1162

Draft
sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1298-rollback-last-turn
Draft

sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1298-rollback-last-turn

Conversation

@sosidudku1

@sosidudku1 sosidudku1 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

What

Adds POST /api/sessions/{id}/rollback-last-turn. It removes the chat's last question and its answer and returns 200 { id, text } with the question's text. The client then sends text again through /v1/chat/completions, so a retry runs the same path as a first try. Before this, "retry" in every client resent the question on top of the old pair: the chat showed it twice and the model read its first answer before answering again.

  • src/session/session-truncate.ts: truncateSessionAt(state, index) cuts a chat back to an earlier row and fixes every field that remembers a transcript position: macroTurnStarts, turnCount, the held conversationPackStart, rollingSummary, the cloud context mirror (trimmed so the next extension does not throw), contextUsage, lastError, status back to pending. A compaction checkpoint that covers the cut is reported, not repaired. No tool policy here, so /rewind and editing a sent message can reuse it.
  • planLastTurnRollback(state, { expectText? }) allows it only for a turn that changed nothing outside the transcript: a question answered by one reply, or by nothing (a provider error). A stopped turn is refused (turn_stopped): a stop can cut a tool batch short, and a local turn stores no trace of that step, so a resend could run the tool twice.
  • Refusals are 409 with a code: turn_in_progress, turn_has_tools, turn_steered, turn_compacted, turn_stopped, nothing_to_undo, turn_unrecognized, turn_mismatch. expectText and expectPriorQuestions name the question the client means, by its text and by how many questions come before it (steers not counted). If the stored last question differs in either (for example a failed "continue" that never reached the store, after an earlier "continue"), the answer is turn_mismatch and nothing is removed. An unknown chat is 404 session_not_found. An agent without the route answers 404 with no code, which is how a client tells the two apart.
  • Before writing, the route stops a compaction running on the chat and clears the turn-progress marker of a stopped turn.

Why

It is the base for a real Regenerate in the desktop and a real [try again] in the terminal. Tool turns are refused on purpose: their effects stay, so a resend would not be a fresh try of the same question. Limits, also written in src/http/README.md: what the turn's reflection already wrote to long-term memory stays, and a chat cut back to nothing keeps its title.

Related PRs: #1164 (terminal retry, stacked on this branch) and #1165 (desktop bridge and buttons, on rel/xp-build).

How it was verified

  • New unit tests: src/session/session-truncate.test.ts, src/http/route-rollback-last-turn.test.ts.
  • An adversarial code review ran on the diff, and its findings are fixed: the status after a cut, the turn count for a turn that never closed, and comments.
  • Lint, tsc and vitest have not been run yet.

Refs ATO-1297, ATO-1298

Not yet checked on the second Mac.

Adds truncateSessionAt, which cuts a chat back to an earlier row and
fixes every field that remembers a position in the transcript (macro-turn
boundaries, the held pack start, the rolling summary, the cloud context,
the context gauge), and planLastTurnRollback, which allows it only for a
turn that changed nothing outside the transcript.

POST /api/sessions/{id}/rollback-last-turn uses them so a client can ask
the same question again without the model reading the old answer.
Retry used to resend the question on top of the old pair, so the chat
showed it twice and the model saw both tries.

Refs ATO-1297, ATO-1298
…tion

A stop that lands during a tool batch leaves a local turn stored as
[question, stop marker]: the cancelled step is not committed, so the
transcript shows no tool even though one ran. Taking such a turn back and
resending it could run the tool a second time and drops the only trace of
the stop. Stopped turns are now refused with turn_stopped.

expectText alone cannot tell two questions with the same text apart: when
a failed "continue" never reached the store, the stored last question is
an earlier "continue" with a real answer. Clients can now also send
expectPriorQuestions, the number of questions before the one they mean
(steers not counted); a different count refuses with turn_mismatch.

Refs ATO-1298

This branch has not been deployed

No deployments
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.

1 participant