feat(session): add turn diff route - #47821
Open
nexxeln wants to merge 4 commits into
Open
Conversation
Git.tree.diff ran --name-status, --numstat, and a patch once per changed file, sequentially, so a turn or revert touching N files cost 1 + 3N git processes (~50ms per file). Run the three once over the tree pair, split the patch with VcsPatch.chunksByFile, cap patch output at MAX_TOTAL_PATCH_BYTES like VCS diffs (capped files get an empty patch, stats stay exact), keep core.quotepath=false so non-ASCII paths still match their chunk, and pass --no-ext-diff. Snapshot.diff diffs first and filters ignored paths from the result instead of listing changed files twice and passing every path as a pathspec.
Project an idle message when a busy period ends (execution succeeded, failed, or interrupted for any reason other than shutdown, which resumes the same turn). Every step since the previous marker is one turn, including prompts steered in while the Session was busy, so turns are derivable from session_message alone without persisting events or a separate table. The marker is invisible to the model and to the TUI and web transcripts.
GET /api/session/:sessionID/diff?messageID&to&context returns FileDiff.Info[] for the turn containing a user message (default: the newest one), or the contiguous range through a later user message's turn. A turn runs from the first prompt after the Session was last idle until its idle marker, so steers belong to the turn they interrupted; Sessions without markers fall back to prompt-to-next-prompt. The diff compares the range's first recorded step snapshot with its last recorded one, or with the working copy only while the Session is actively executing, resolves the snapshot repository from the Location in effect at the range (rejecting ranges that span a move), and defaults to full-file patches like vcs.diff. Shared missingMessage and failedSnapshot handler helpers replace the inlined mappings in the session handlers.
The solid data layer mirrors every projected marker message from its event so the in-memory transcript matches the server before the next read; do the same for the idle marker on execution succeeded, failed, and non-shutdown interrupted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #47795 (same feature, clean history).
summary
idlemessage (Session.Message.Idle,outcome: succeeded | failed | interrupted): projected when a busy period ends (session.execution.succeeded/failed/interrupted;shutdownis skipped because the resumed execution continues the same turn). Everything since the previous marker is one turn, steers included, so turns are derivable fromsession_messagealone — no event persistence, no new table, no migration. Invisible to the model (toLLMMessageskips it) and to the TUI/web transcripts (skipped inreduceSessionRowsandisNotice). The shared solid data layer synthesizes it live from the execution events like the other marker messages, so in-memory transcripts match the server before the next read; the UI footer anchoring is deliberately left for a follow-up.GET /api/session/:sessionID/diff?messageID&to&context(session.diff) →{ data: FileDiff.Info[] }: the files changed by the turn containingmessageID(default: the newest user message's turn);toextends the range through a later user message's turn (contiguous, commit-range style). Compares the range's first recorded step snapshot with its last recorded one; only a step still running in the actively executing session compares against the working copy. Omittedcontext→ full-file patches likevcs.diff. Snapshot trees are resolved from the Location in effect at the range (derived fromlocation-switchedmessages viaLocationServiceMap.get), so pre-move turns stay diffable and a range spanning a move is rejected. Errors: 400InvalidRequestError(tobeforemessageID, not a user message, location change), 404MessageNotFoundError/SessionNotFoundError, 500UnknownErrorwith a logged ref on snapshot failure. SharedmissingMessage/failedSnapshothandler helpers replace three inlined copies each.Git.tree.diffruns--name-status,--numstat, and the patch once per tree pair instead of three spawns per changed file (~50 ms/file before), caps patch output atMAX_TOTAL_PATCH_BYTES(capped files get an empty patch, stats stay exact), usescore.quotepath=falseso non-ASCII paths still match their patch chunk, and passes--no-ext-diff.Snapshot.diffdiffs first and filters ignored paths from the result instead of listing changed files twice. Revert benefits too.Backend only; no TUI/app feature work in this PR.
Note for reviewers (not changed here):
packages/core/src/session/stats.ts:296filtersEventTable.typewith the unversionedSessionEvent.UsageRecorded.typewhilebus.tsstores versioned type names (session.usage.recorded.1), so that query cannot match persisted rows.testing
bun typecheckin schema, protocol, core, server, client, plugin, tui, session-ui, app, cli, sdk, www.test/session-diff.test.tsis one scenario with real git snapshots: legacy prompt→prompt fallback, marker-era turn with a steer inside it (default and by the steer's message), pre-marker history merging into the first marked turn, ranges viato, error cases, unrecorded-end fallback, running-step working-copy diff only when active, revert (markers included), fork.test/git.test.tscovers the patch cap and the non-ASCII header match (assertion verified to fail withoutcore.quotepath=false). One existing recovery test now excludes idle markers from its message-list assertion.test/session-diff.test.ts(200/400/404 contract). client 160 pass; tui data + diff-viewer 140 pass.generate-openapi.ts --check, wwwcheck:generated+build, prettier on touched files.