Skip to content

fix(state): transcript step records duplicate the prior conversation — record per-step deltas (#175)#198

Merged
OGtwelve merged 1 commit into
mainfrom
fix/transcript-per-step-delta-175
Jul 14, 2026
Merged

fix(state): transcript step records duplicate the prior conversation — record per-step deltas (#175)#198
OGtwelve merged 1 commit into
mainfrom
fix/transcript-per-step-delta-175

Conversation

@OGtwelve

@OGtwelve OGtwelve commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Closes #175. Second of the two P0s from the post-#100 parity verification (#196), same root cause as #176.

Root cause (verified)

ai@6.0.182's onStepFinish exposes event.response.messages as the cumulative response-message list for the whole run so far, not a per-step delta. Confirmed against a live run (sum/working-1.jsonl): per-record counts [2, 4, 6], record 0 a strict prefix of record 1.

All four transcript recording sites (plannerRecorder/recorder×2/ciRecorder) wrote it verbatim:

The unit tests passed because their fakes modelled response.messages as per-step deltas — the wrong SDK assumption.

Fix

  • recordStepDeltas(recorder) — an onStepFinish handler that tracks the count already recorded and appends only the new tail (messages.slice(recorded)), so each record is a true delta. Wired at all four sites; the recorder stays a dumb serialized append (SDK-quirk knowledge lives in one adapter helper, not the store).
  • reconstructTranscript de-overlaps a record whose leading run equals the accumulated messages → appends only its suffix. This makes transcripts written by prior releases (cumulative) reconstruct duplicate-free too; new per-step-delta records don't overlap and append whole. One rule handles both.

Tests

  • recordStepDeltas: cumulative [2,4,6] events → recorded deltas [2,2,2]; usage forwarded; an unchanged-cumulative step records nothing.
  • reconstructTranscript: a pre-fix cumulative transcript de-overlaps to 4 messages, not 6.
  • Green: typecheck (both) + test:node (aitm 905 / compat 250) + biome.

Same root cause as #176 — fixed separately since it touches different files.

Part of #196.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented duplicate transcript messages when recording multi-step runs.
    • Improved transcript reconstruction for older records containing cumulative step messages.
    • Preserved accurate usage data while recording only newly added messages.
    • Empty step updates no longer create unnecessary transcript entries.
  • Tests

    • Added coverage for incremental recording, duplicate prevention, and legacy transcript reconstruction.

…essages duplicated resume context (#175)

ai@6's onStepFinish exposes event.response.messages as the CUMULATIVE response
list for the whole run so far, not a per-step delta (live evidence: per-record
counts [2,4,6], record 0 a strict prefix of record 1). All four transcript
recording sites wrote it verbatim, so files grew O(N^2) and reconstructTranscript
concatenated overlapping records — the #108 resume path (findResumable ->
priorHandle) handed the Worker/CI-fix session a duplicated conversation.

- recordStepDeltas(recorder): an onStepFinish handler that tracks the count already
  recorded and slices the new tail, so each record is a true delta. Wired at all
  four sites (planner/worker/reviewer/ci-fix); the recorder stays a dumb append.
- reconstructTranscript de-overlaps records whose leading run equals the accumulated
  messages, so transcripts written by prior releases (cumulative) reconstruct
  duplicate-free too; per-step-delta records don't overlap and append whole.
- Tests: recordStepDeltas emits [2,2,2] deltas from cumulative [2,4,6] events (+ a
  no-new-messages step records nothing); reconstruct de-overlaps a pre-fix
  cumulative transcript to 4 messages, not 6.

Part of #196. Shares its root cause with #176 (compaction sizing).
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Transcript step records now contain only newly observed response messages. Transcript reconstruction also removes duplicated prefixes from transcripts written with the previous cumulative-message format.

Changes

Transcript delta handling

Layer / File(s) Summary
Record per-step message deltas
packages/aitm/src/loop/run-loop-adapter.ts, packages/aitm/src/loop/run-loop-adapter.test.ts
Adds recordStepDeltas and applies it to Planner, Reviewer, Worker, and CI-fix transcript callbacks; tests cover cumulative and empty deltas.
Remove legacy transcript overlap
packages/aitm/src/state/transcript-store.ts, packages/aitm/src/state/transcript-store.test.ts
Detects accumulated prefixes during reconstruction and appends only non-overlapping message suffixes, with a regression test for legacy transcripts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • Issue 176 — Both changes account for cumulative response.messages semantics, but this PR addresses transcript recording and reconstruction while issue 176 addresses compaction sizing.

Suggested reviewers: sebyx07

🚥 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 states the main fix: recording transcript step deltas to avoid duplicated prior conversation.
Linked Issues check ✅ Passed The changes implement per-step delta recording, legacy transcript de-overlap, and regression tests, matching issue #175's goals.
Out of Scope Changes check ✅ Passed The changes stay within the transcript-recording and reconstruction fix, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/transcript-per-step-delta-175

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

@OGtwelve

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 58 minutes.

@OGtwelve

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 50 minutes.

@OGtwelve

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 28 minutes.

@OGtwelve

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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.

🧹 Nitpick comments (1)
packages/aitm/src/state/transcript-store.ts (1)

56-65: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Prefer a real deep-equal over JSON.stringify comparison.

JSON.stringify equality is order-sensitive — two structurally-identical message objects with differently-ordered keys would compare unequal, causing isLeadingRun to (falsely) report no overlap and reintroduce duplication for exactly the legacy transcripts this function targets. Both arrays here come from JSON.parse, so this mostly holds in practice, but it's a fragile invariant for a correctness-critical dedup check.

♻️ Suggested fix using `node:util.isDeepStrictEqual`
+import { isDeepStrictEqual } from 'node:util';
+
 function isLeadingRun(arr: readonly ModelMessage[], prefix: readonly ModelMessage[]): boolean {
   if (prefix.length === 0) return true;
   if (arr.length < prefix.length) return false;
-  for (let i = 0; i < prefix.length; i++) {
-    if (JSON.stringify(arr[i]) !== JSON.stringify(prefix[i])) return false;
-  }
-  return true;
+  return prefix.every((m, i) => isDeepStrictEqual(arr[i], m));
 }

Please confirm node:util's isDeepStrictEqual is available under the project's Bun/Node/Deno portability targets before adopting this.

🤖 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 `@packages/aitm/src/state/transcript-store.ts` around lines 56 - 65, Update
isLeadingRun to compare each arr/prefix message pair with a real deep-equality
helper instead of JSON.stringify, preserving the existing empty-prefix, length,
and early-exit behavior. Verify that node:util isDeepStrictEqual is supported
across the project’s Bun, Node, and Deno targets before using it; otherwise
reuse an existing portable deep-equality utility.
🤖 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.

Nitpick comments:
In `@packages/aitm/src/state/transcript-store.ts`:
- Around line 56-65: Update isLeadingRun to compare each arr/prefix message pair
with a real deep-equality helper instead of JSON.stringify, preserving the
existing empty-prefix, length, and early-exit behavior. Verify that node:util
isDeepStrictEqual is supported across the project’s Bun, Node, and Deno targets
before using it; otherwise reuse an existing portable deep-equality utility.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: af64d34f-9842-4082-9513-126641caa411

📥 Commits

Reviewing files that changed from the base of the PR and between 82921ca and 186215d.

📒 Files selected for processing (4)
  • packages/aitm/src/loop/run-loop-adapter.test.ts
  • packages/aitm/src/loop/run-loop-adapter.ts
  • packages/aitm/src/state/transcript-store.test.ts
  • packages/aitm/src/state/transcript-store.ts

@OGtwelve OGtwelve merged commit da2719f into main Jul 14, 2026
4 checks passed
@OGtwelve OGtwelve deleted the fix/transcript-per-step-delta-175 branch July 14, 2026 04:24
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.

fix(state): transcript step records duplicate the prior conversation — record per-step deltas

1 participant