Skip to content

fix(runner): preserve structured live turn history - #183

Merged
cnjack merged 2 commits into
mainfrom
codex/fix-structured-turn-history
Aug 4, 2026
Merged

fix(runner): preserve structured live turn history#183
cnjack merged 2 commits into
mainfrom
codex/fix-structured-turn-history

Conversation

@cnjack

@cnjack cnjack commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • replace the runner's text-only return contract with RunResult{Response, Messages}
  • preserve the complete assistant/tool transcript in live history across Web, TUI, and ACP turns
  • feed structured output into todo/goal/Stop continuations without mutating caller history
  • keep live tool outputs aligned with the persisted/replayed transcript, including large-output truncation
  • discard incomplete streamed tool calls on cancellation or stream failure while preserving visible partial text

Design

Response remains the flattened assistant text used by trace output and plan completion. Messages contains the persistable assistant/tool delta for the turn. Internal continuation prompts remain transient, matching current session replay semantics.

All transports now append Messages instead of synthesizing one assistant message from a possibly empty string.

Acceptance coverage

  • structured assistant → tool → assistant history survives with and without a recorder
  • live history deep-matches session.ReconstructState
  • parallel tool-call batches preserve call/result pairing
  • large tool outputs use the same truncated form in live and replayed history
  • cancellation backfills announced calls
  • cancellation during an incomplete assistant tool-call stream drops the partial call but preserves visible text

Verification

  • go test ./...
  • golangci-lint run --new-from-rev=origin/main — 0 issues
  • make lint-web
  • git diff --check

Full make lint remains blocked by 44 pre-existing SA5011 findings in tests on the base branch; this PR introduces no new lint findings.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of incomplete streaming tool calls during cancellation.
    • Enhanced message history tracking to preserve all assistant and tool messages correctly.
  • Tests

    • Added comprehensive tests for chat history management and streaming cancellation scenarios.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cnjack, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc99f532-4d39-4e8e-bd1a-587aa819d3f5

📥 Commits

Reviewing files that changed from the base of the PR and between 9d9b558 and b576d41.

📒 Files selected for processing (7)
  • internal/command/interactive.go
  • internal/command/interactive_plan_test.go
  • internal/runner/cancel_backfill_test.go
  • internal/runner/history_result_test.go
  • internal/runner/review_feedback_test.go
  • internal/runner/runner.go
  • internal/session/session.go
📝 Walkthrough

Walkthrough

The runner now returns flattened response text and structured assistant/tool messages. Command and web callers append the full message history. Session recording returns persisted tool output. Tests cover replay, parallel tools, truncation, and cancellation.

Changes

Structured runner history

Layer / File(s) Summary
Runner result and transcript assembly
internal/runner/runner.go
Run and runInner now return RunResult. The runner preserves assistant messages, tool calls, tool types, response text, continuation history, and partial streaming output across completion and error paths.
Tool-result history persistence
internal/session/session.go, internal/runner/runner.go
RecordToolResult returns the stored output. The runner uses that output in structured tool messages.
Command and web history integration
internal/command/acp.go, internal/command/interactive.go, internal/web/chat.go
Callers append all returned messages. Interactive plan completion continues to use the result response text.
Structured history and cancellation validation
internal/runner/history_result_test.go, internal/runner/cancel_backfill_test.go
Tests cover replay equivalence, operation without a recorder, parallel tool calls, output truncation, cancellation backfill, and discarded incomplete streamed calls.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Runner
  participant Model
  participant Session
  Caller->>Runner: Submit run request
  Runner->>Model: Generate response and tool calls
  Model-->>Runner: Assistant text and structured calls
  Runner->>Session: Record tool result
  Session-->>Runner: Return persisted tool output
  Runner-->>Caller: Return RunResult
  Caller->>Caller: Append returned messages to history
Loading

Possibly related PRs

  • cnjack/jcode#114: Both changes modify runner and command integration, but this PR changes structured run results while that PR adds hook dispatching.
  • cnjack/jcode#150: Both changes affect structured tool-result history and interrupted tool handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the runner change that preserves structured live turn history.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-structured-turn-history

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/session/session.go (1)

580-596: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Return recorder write failures to the runner.

RecordToolResult discards the r.writeEntry error and returns output as if it were persisted. If session storage fails, internal/runner/runner.go still appends that output to live history, but session replay lacks the tool result. A later resume can then contain an unmatched assistant tool call.

Return a wrapped persistence error that includes the session path and toolCallID. Propagate it through the runner completion path instead of claiming that the output was stored.

As per coding guidelines, “Wrap non-tool errors with fmt.Errorf("context: %w", err) and return descriptive errors containing relevant paths, line numbers, or command output.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/session.go` around lines 580 - 596, The
Recorder.RecordToolResult method must surface persistence failures instead of
discarding r.writeEntry errors. Update its return contract to return the output
and an error, wrapping write failures with session-path and toolCallID context,
then propagate that error through the runner’s tool-completion path so live
history is not updated as though the result was persisted.

Source: Coding guidelines

🧹 Nitpick comments (2)
internal/runner/cancel_backfill_test.go (1)

223-231: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Compare cancellation results with session replay.

The backfill test validates only the result ID. The incomplete-call test validates only that tool entries are absent. The recorder can store different backfill content or omit "partial", and both tests still pass. Compare reconstructed history with the matching live RunResult.Messages in both tests.

assertMessagesEqual already provides this check in internal/runner/history_result_test.go Lines 261-271.

Proposed test change
 	state := session.ReconstructState(entries)
+	assertMessagesEqual(t, state.History, outcome.result.Messages)
 	for i, m := range state.History {
 		// ...
 	}

 	for _, entry := range entries {
 		// ...
 	}
+	assertMessagesEqual(t, session.ReconstructState(entries).History, result.Messages)

Also applies to: 313-321

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runner/cancel_backfill_test.go` around lines 223 - 231, Update both
cancellation test assertions around the live history checks and the
incomplete-call case to compare reconstructed history against the matching live
RunResult.Messages using the existing assertMessagesEqual helper. Keep the
current structural assertions where useful, but ensure the comparisons validate
complete message content, including backfill fields such as “partial”, rather
than only IDs, roles, or tool-entry absence.
internal/runner/history_result_test.go (1)

190-201: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert tool-result pairing with the emitted tool calls.

The test checks the tool-call count, but it does not prove that each tool result answers a tool call in result.Messages[0]. A transcript with mismatched call IDs can pass this test. Build wantIDs from the assistant tool calls, then consume each tool-result ID from that set.

Proposed test change
-	wantIDs := map[string]bool{"call-a": true, "call-b": true}
+	wantIDs := make(map[string]bool, len(result.Messages[0].ToolCalls))
+	for _, toolCall := range result.Messages[0].ToolCalls {
+		if toolCall.ID == "" || wantIDs[toolCall.ID] {
+			t.Fatalf("assistant tool calls = %#v, want two unique IDs", result.Messages[0].ToolCalls)
+		}
+		wantIDs[toolCall.ID] = true
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runner/history_result_test.go` around lines 190 - 201, Update the
assertions around the first assistant message to build wantIDs from its emitted
ToolCalls rather than hard-coding call-a and call-b. Continue consuming each
ToolCallID from result.Messages[1:3], rejecting IDs not present in the assistant
calls and ensuring no expected call IDs remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/command/interactive.go`:
- Around line 618-623: Update the plan-completion flow around
handlePlanCompletion and both runner.Run result-handling sites to detect
cancellation before submitting or approving a plan. Use the existing session
context cancellation state, or propagate an explicit cancellation indicator
through RunResult, and return without waiting on planRespCh when the run was
canceled; preserve normal plan approval for non-canceled runs.

In `@internal/runner/cancel_backfill_test.go`:
- Around line 294-295: Bound the cancellation test around runInner by executing
it in a goroutine that sends its result through a buffered channel, and add a
short local context deadline or timeout select. Update the test to fail promptly
if the handler is not invoked or runInner does not observe cancellation, while
preserving the existing result and completion assertions.

In `@internal/runner/runner.go`:
- Around line 312-320: Update the tool-stream error path around emitToolResult
and MessageStream.Recv so a receive failure before the first chunk reuses the
announced tool-call ID instead of an empty toolCallID when recording and
appending the result. Add a test covering first-receive failure and verify the
handler/session output remains paired with the announced ID.

In `@internal/web/chat.go`:
- Around line 315-320: Ensure result messages committed around runner.Run are
serialized in submit order, not merely protected by eng.emu; use the engine’s
existing sequencing mechanism or add per-turn ordering so an older run cannot
append after a newer submission or result. Update the append path near
eng.history and ensure ordering remains correct even when runner.Run calls
finish out of order; do not rely on the runGen cleanup check.

---

Outside diff comments:
In `@internal/session/session.go`:
- Around line 580-596: The Recorder.RecordToolResult method must surface
persistence failures instead of discarding r.writeEntry errors. Update its
return contract to return the output and an error, wrapping write failures with
session-path and toolCallID context, then propagate that error through the
runner’s tool-completion path so live history is not updated as though the
result was persisted.

---

Nitpick comments:
In `@internal/runner/cancel_backfill_test.go`:
- Around line 223-231: Update both cancellation test assertions around the live
history checks and the incomplete-call case to compare reconstructed history
against the matching live RunResult.Messages using the existing
assertMessagesEqual helper. Keep the current structural assertions where useful,
but ensure the comparisons validate complete message content, including backfill
fields such as “partial”, rather than only IDs, roles, or tool-entry absence.

In `@internal/runner/history_result_test.go`:
- Around line 190-201: Update the assertions around the first assistant message
to build wantIDs from its emitted ToolCalls rather than hard-coding call-a and
call-b. Continue consuming each ToolCallID from result.Messages[1:3], rejecting
IDs not present in the assistant calls and ensuring no expected call IDs remain.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 926f148e-ec2c-4697-a456-078def3774a3

📥 Commits

Reviewing files that changed from the base of the PR and between dc2464e and 9d9b558.

📒 Files selected for processing (7)
  • internal/command/acp.go
  • internal/command/interactive.go
  • internal/runner/cancel_backfill_test.go
  • internal/runner/history_result_test.go
  • internal/runner/runner.go
  • internal/session/session.go
  • internal/web/chat.go

Comment thread internal/command/interactive.go Outdated
Comment thread internal/runner/cancel_backfill_test.go Outdated
Comment thread internal/runner/runner.go
Comment thread internal/web/chat.go
Comment on lines +315 to 320
result := runner.Run(hookCtx, agent, history, eng.eventHandler, recorder, eng.todoStore, eng.env.GoalStore, s.tracer, eng.tokenUsage)
if len(result.Messages) > 0 {
eng.emu.Lock()
eng.history = append(eng.history, &schema.Message{Role: schema.Assistant, Content: resp})
eng.history = append(eng.history, result.Messages...)
eng.emu.Unlock()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve Web turn order when committing result messages.

When a newer submitMessage call starts before an older runner.Run call returns, both goroutines append to eng.history when they finish. The eng.emu lock protects the slice but does not preserve submission order. An older result can appear after a newer user message or after a newer result. The next turn can then receive an invalid transcript. Serialize runs per engine or commit per-turn results in sequence order. The runGen cleanup check does not protect this append.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/web/chat.go` around lines 315 - 320, Ensure result messages
committed around runner.Run are serialized in submit order, not merely protected
by eng.emu; use the engine’s existing sequencing mechanism or add per-turn
ordering so an older run cannot append after a newer submission or result.
Update the append path near eng.history and ensure ordering remains correct even
when runner.Run calls finish out of order; do not rely on the runGen cleanup
check.

@jcode-cloud-app jcode-cloud-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jcode review

Clean. The PR correctly restructures runner.Run/runInner to return a RunResult carrying the structured assistant+tool transcript, updates all four production call sites (acp.go, interactive.go x3, web/chat.go) consistently, and wires RecordToolResult's returned (truncated) output back into live history so live and replayed sessions match. The incomplete-streaming-tool-call handling on stream error is sound (announced tool calls only happen after a successful stream flush, so a mid-stream error leaves no dangling calls and text-only partial content is preserved), and the cancellation/backfill path preserves the assistant-tool-call -> tool-result invariant required by model APIs. gofmt and go vet are clean; go build ./... and the runner/session/command/web test packages all pass. No high-confidence defects introduced.

0 findings

Checks performed
  • Read full diff at .git/jcode-review.diff
  • Read internal/runner/runner.go (Run, runInner, streaming/non-streaming assistant + tool paths)
  • Read internal/session/history.go ReconstructState/repairDanglingToolCalls
  • Read internal/session/truncate.go TruncateToolOutput and RecordToolResult return wiring
  • Read internal/agent/history.go SyncSummarization/findToolBoundary interaction
  • Grepped all runner.Run/runInner callers (6 production + tests, all updated)
  • Inspected acp.go Prompt, interactive.go handlePrompt/handlePlanCompletion/runEventLoop, web/chat.go submitMessage call sites
  • go generate ./internal/model/... (registry_generated.go)
  • Created internal/web/dist stub; go build ./... OK
  • go test ./internal/runner/... ./internal/session/... ./internal/command/... ./internal/web/... all pass
  • gofmt -l and go vet on changed files clean

jcode posts a non-blocking COMMENT review. Merge decisions remain with your team.

@cnjack
cnjack merged commit 83673db into main Aug 4, 2026
4 checks passed
@cnjack
cnjack deleted the codex/fix-structured-turn-history branch August 4, 2026 18:02

@jcode-cloud-app jcode-cloud-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jcode review

Clean. The PR refactors runner.Run/runInner to return a structured RunResult (Response + Messages + Err) instead of a flattened string, and threads structured assistant/tool messages into live history across the TUI, ACP, and Web callers. The full module builds, go vet is clean, and all tests pass for internal/runner, internal/session, internal/command, internal/agent, internal/handler, and internal/tools. All runInner return paths route through the finish() helper so Response/Err are always populated; the new errors.Join result is consumed via errors.Is in every handler; the caller's history slice is copied before continuation mutation (fixing a prior aliasing bug); incomplete streaming tool calls are correctly dropped; persistence failures append the replay-equivalent InterruptedOutput and surface a non-nil Err so plan submission is skipped on interruption. No correctness, security, reliability, or data-loss defect was verified with >=80% confidence.

0 findings

Checks performed
  • go build ./... (full module, clean)
  • go test ./internal/runner/... -count=1 (pass)
  • go test ./internal/session/... -count=1 (pass)
  • go test ./internal/command/... -run Plan (pass)
  • go test ./internal/agent/... ./internal/handler/... ./internal/tools/... (pass)
  • go vet on runner/session/command/web (clean)
  • inspected runInner return paths all use finish()
  • verified OnAgentDone errors.Is handling in web/acp handlers
  • verified Run copies caller history before continuation append
  • single RecordToolResult caller handles new (string,error) return
  • read full diff incl. history_result_test.go, review_feedback_test.go, cancel_backfill_test.go

jcode posts a non-blocking COMMENT review. Merge decisions remain with your team.

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