Repository navigation
fix(session): keep a parked approval in the turn log beside its call - #1495
Conversation
A parked call's approval is moving into the turn log as a role-less line next to the call. Teach every reader of the log to pass over that line first, so history, readAt, the recent window, and the subagent trace never see it as a turn or count it as malformed. readAt stops going through the base store's strict parser, which knows turns only.
An approved call's pending operation was removed only in memory: the tool cycle writes turns, not metadata, so an agent rebuilt from the store before the next commit re-armed the gate and later answered the call a second time with "approval timed out". Store the approval as a record line right after the turn holding the call, written in the same commit, so the call's tool_result closes it with no second write. load() returns only approvals whose call is in history and unanswered, from the log and from a legacy metadata.json, and metadata.json now holds only what the log does not, removed from the commit once empty.
|
Review points:
|
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review · Request changes
A parked approval is written as a role-less line in the turn log beside its call, and load() re-arms only calls that are still in history and unanswered.
Findings
-
src/session/optimized-context-store.ts:1313—writeTurnLogadds every included correlation id tologgedApprovalIdsafter a segmented write, including the versioned basedivertToVersionedBasesswitches to (:1375).load()only readsturns.jsonl. The nextwriteMetadataLivethen drops that op frommetadata.json(:1344) because the id looks logged. A second owner that diverts, writes the approval, commits, then writes metadata again leaves the unanswered call in canonical history withpendingOperations: [], so the rebuild this change is for times the gate out again. Same-owner appends are fine. Record the id only when the line landed on the logload()reads, or keep the metadata copy until it does. -
src/session/optimized-context-store.ts:1150—openPendingOperationsinsertscorrelationIdintoseenbefore it decides the record is open. A log line that does not re-arm (call missing or already answered) still tombstones that id, andload()lists log records beforemetadata.pendingOperations(:1713). A legacy metadata approval for the same id is then dropped even though the log copy never became the live op. Skipseen.adduntil the op is actually pushed, so a closed record does not erase a later open one. -
src/session/optimized-context-store.ts:1152— openness is call-id membership only. A schema-valid record whosesuspendedCall.idmatches an unansweredtool_callis returned even when name or arguments differ, and resume compares that record to its own snapshot rather than to the transcript call. Reject a record whosesuspendedCallis not the unansweredtool_callwith that id, or the parked body can replace the call that was actually approved.
Commit subjects and bodies match CONTRIBUTING.md (git log origin/main..HEAD). No Bin entries in git diff origin/main...HEAD --stat. The divert case was reproduced with a temporary test that failed and was removed; the other two are from reading the new load path. I did not re-run the suite.
A diverted turn write marked the approval logged even though load() only reads turns.jsonl, so the next metadata write deleted it. A closed log record also tombstoned a later open op with the same correlation id, and a record was accepted when only the call id matched. Record an id as logged only on the canonical turn log. Accept a correlation id only once the op is open, and only when the suspended call matches the unanswered tool call's name and arguments.
|
Pushed
|
writeTurns stages an append so writeMetadata can attach the approval and flush the segment a single time. commit only git-adds that flush. A parked approval is never written to metadata.json. A versioned divert is not the log load() reads, and is not copied into metadata to compensate. Legacy open approvals move into the log on the next commit and are not written back. Async markers and connectorState stay on metadata.json.
writeTurns of an append hits turns.jsonl before it returns, so a new store can load those turns with no metadata write and no commit. writeMetadata then appends only the parked approval's record line. That append also lands on the published log while a rewrite is still staged, and a failed commit restores it with the published generation.
Summary
turns.jsonlas a record line ({"pendingApproval": ...}) right after the turn that holds the call, in the same commit. The call'stool_resultline closes it. "Waiting" and "answered" live in one file, so an agent rebuilt from the store can no longer re-arm an approval whose call already has a result.load()keeps record lines out of the turns. They never reach the provider, prompt-cache hashing, compaction, history or transcripts. It returns as pending only approvals whose call is present with no result.readAt,loadRecentTurnsand the subagent trace reader skip record lines.pendingOperationsinmetadata.jsonstill load through the same open/closed filter. An open one moves into the log on the next commit, andmetadata.jsonis removed once nothing is left in it.vendor/.Verification
bun run checkandbun run testpass (9152 tests)Malformed tool sequence: duplicate tool_resultand passes heremetadata.json, the snapshot scrub, compaction while parked, a failed staged commit, and a torn tailFixes CL-10781
Fixes CL-10767
Part of CL-10763