Skip to content

[fix] Transcript replay respects a cancelled interaction's terminal status - #5912

Merged
mmabrouk merged 3 commits into
release/v0.112.0from
fix/replay-respects-interaction-status
Aug 10, 2026
Merged

[fix] Transcript replay respects a cancelled interaction's terminal status#5912
mmabrouk merged 3 commits into
release/v0.112.0from
fix/replay-respects-interaction-status

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Context

Live evidence, session 3975e362-f64c-4e2d-8f4f-4f36c584bd91 on the 8180 dev stack: after a hard reload, a client-tool interaction whose session_interactions row was already terminally cancelled (e.g. the stale-interaction sweep) re-rendered as fully PENDING. The elicitation form came back at question 1, blank, with live Accept/Decline/Dismiss buttons, stacked above the real, current pending interaction. This is very plausibly what was actually seen as "no UI showed up" in the connect-flow investigation (#5909): the dead earlier form rendered prominently and hid the real one below it.

Live-verified the extent of the bug before this fix: clicking "Decline" on the resurrected form is a pure client-side cosmetic no-op. The chip flips to "Declined the request.", but the network log shows zero mutating requests and the session_interactions row stays byte-identical (status=cancelled, original updated_at). The real pending interaction beside it is untouched. So the UI lies twice (a dead form renders live, then a click on it pretends to work), but there is no backend gap to close for this symptom. The fix is entirely in transcript replay.

Changes

replayClientTool (in transcriptToMessages.ts) rebuilds a parked client tool's part straight from its own interaction_request record. That record has no way to know the interaction's later lifecycle — that lives in session_interactions, a separate table, not the record log. A normal live settle (a successful connect, an answered elicitation) does leave a trace: the browser's addToolOutput resubmits the result, and the runner re-emits it as a tool_result record that settles the part on replay too, so no join is needed for that path. But a server-side cancellation leaves the transcript with nothing but the original request, forever.

transcriptToMessages now takes an optional cancelledClientToolTokens: ReadonlySet<string> and, after the full record sweep completes (so a real, later tool_result always wins over this fallback), settles any part still stuck at input-available whose token is in that set. The synthesized output mirrors each widget's OWN cancelled-terminal shape, so a resurrected part renders exactly like a live cancel:

request_connection -> {connected: false, reason: "cancelled"}   (ConnectToolWidget's generic
                                                                   "Connection not completed" chip)
request_input       -> {action: "cancel"}                        (ElicitationWidget's "Dismissed
                                                                   the request." chip)

The join key is session_interactions.token, which equals the record's toolCallId — confirmed live against an 8180 row (token call_n7Gec... == the interaction_request record's toolCallId). New fetchCancelledClientToolTokensAtom in @agenta/entities/session (mirrors fetchSessionRecordsAtom's "imperative fetch through the shared query cache" pattern) queries the session's client_tool interactions and reduces them to the cancelled tokens. It's best-effort: any failure resolves to an empty set rather than throwing, so a resurrected-form miss just degrades to today's behavior instead of blocking the whole transcript from loading. loadSessionMessages fetches it alongside records and threads it through.

Same treatment for the agenta-chat package's byte-parity copy of both files (per that pair's own "keep byte-parity if either side changes" convention).

Tests / notes

  • 7 new unit tests per copy (14 total) in the transcriptToMessages test family: cancelled -> inert for both known client-tool kinds, pending -> unaffected (both "token not in the set" and "option omitted entirely" — the existing behavior, unchanged), a real tool_result still overriding a stale cancelled-token entry, and an unregistered client-tool kind still settling (empty output) instead of crashing.
  • Updated loadSession.test.ts's @agenta/entities/session mock to stub the new atom.
  • tsc --noEmit clean across oss, @agenta/entities, @agenta/chat. pnpm turbo run build --filter=@agenta/entities --filter=@agenta/chat clean. pnpm turbo run lint --filter=@agenta/oss --filter=@agenta/chat --filter=@agenta/entities clean (no new warnings).
  • Full suites green: @agenta/oss AgentChatSlice (167 tests), @agenta/chat (252 tests), @agenta/entities (954 tests).
  • Live-verified the before-state (the resurrected-form bug, and that "Decline" on it is a cosmetic no-op) on the 8180 dev stack per the evidence above. This worktree's actual fix could not be re-verified live the same way (the 8180 stack runs whatever checkout is deployed to it, not this branch directly) — verified by unit test + code trace instead.

What to QA

  • Open a session with a client-tool interaction (connect or elicitation) that got cancelled server-side (the stale-interaction sweep, or any other path that never round-trips a settle) while this browser wasn't watching, then hard-reload. The interaction should render as an inert, already-resolved chip ("Connection not completed" / "Dismissed the request."), not as a live form.
  • Regression: a session with a genuinely still-pending client-tool interaction reloads exactly as before — the interactive form/dock still appears and still works.
  • Regression: a session with a SUCCESSFULLY completed client-tool interaction (one that has a tool_result in its transcript) still replays as connected/submitted, not as cancelled.

…t replay

New `fetchCancelledClientToolTokensAtom` (mirrors `fetchSessionRecordsAtom`'s
imperative-fetch-through-the-shared-cache pattern): a best-effort, never-throwing
query for a session's `client_tool` interactions, reduced to the set of tokens
whose `session_interactions.status` is `cancelled`.

This is the join key transcript replay needs but the durable record log alone
doesn't carry: `session_interactions.token` equals the record's `toolCallId`
(confirmed against a live 8180 row: token `call_n7Gec...` == the
`interaction_request` record's `toolCallId`). Consumed by the next commit.
…atus

After a hard reload, a client-tool interaction whose session_interactions row
was already terminally cancelled (e.g. the stale-interaction sweep) replayed
as fully PENDING: the elicitation form came back at question 1, blank, with
live Accept/Decline/Dismiss buttons, stacked above the real current pending
interaction. Reload-proof — live evidence on the 8180 dev stack, session
3975e362-f64c-4e2d-8f4f-4f36c584bd91. Clicking "Decline" on the resurrected
form was a pure client-side cosmetic no-op: the chip flipped to "Declined the
request." but the network log showed zero mutating requests and the
session_interactions row stayed byte-identical (status=cancelled, original
updated_at) — confirmed live, so this is entirely a replay bug, not a missing
backend endpoint.

Root cause: `replayClientTool` rebuilds a parked client tool's part straight
from its own `interaction_request` record, with no way to know the
interaction's later lifecycle — that lives in `session_interactions`, not the
record log. A normal live settle (connect, an elicitation answer) DOES leave a
trace: the browser's `addToolOutput` resubmits the result and the runner
re-emits it as a `tool_result` record that settles the part on replay too, no
join needed. But a server-side cancellation leaves the transcript with nothing
but the original request.

`transcriptToMessages` now takes an optional `cancelledClientToolTokens` set
and, after the full record sweep (so a real, later `tool_result` always wins),
settles any still-`input-available` client-tool part whose token is in that
set — mirroring each widget's OWN cancelled-terminal shape so the resurrected
part renders exactly like a live cancel (ConnectOutput `{connected:false,
reason:"cancelled"}`, ElicitationResult `{action:"cancel"}`), never as an
interactive form. `loadSessionMessages` fetches the join via the new
`fetchCancelledClientToolTokensAtom` alongside records, best-effort (a failure
there degrades to today's behavior, never blocks the transcript from loading).

7 new tests in the transcriptToMessages family: cancelled -> inert (both known
client-tool kinds), pending -> unaffected (existing behavior, both "not in the
set" and "option omitted entirely"), a real tool_result still overriding a
stale cancelled-token entry, and an unregistered kind still settling instead
of crashing.
Byte-parity re-sync of packages/agenta-chat/src/assets/{transcriptToMessages,
loadSession}.ts with the OSS originals (per this file pair's own maintenance
convention — see their header comments) for the previous commit's fix: a
terminally-cancelled client-tool interaction now replays inert instead of as
a live, answerable form.

Also updates loadSession.test.ts's `@agenta/entities/session` mock to stub
the new `fetchCancelledClientToolTokensAtom` (defaults to an empty set — that
file's own assertions only cover the records half; the join itself is covered
by the 7 new tests mirrored into transcriptToMessages.test.ts here).
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 10, 2026
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Blocked Blocked Aug 10, 2026 3:21pm

Request Review

@dosubot dosubot Bot added the bug report Something isn't working label Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Cancelled client-tool interactions now replay as safely settled, non-interactive outputs when reopening or refreshing a session.
    • Real tool results continue to take precedence over cancellation status.
    • Unknown client-tool interactions are handled safely without blocking session hydration.
  • Tests

    • Added coverage for cancelled, uncancelled, completed, and unknown client-tool interactions across session loading and message replay.

Walkthrough

Session hydration now loads cancelled client-tool tokens with session records. Transcript replay settles matching elicitation and connection tools as inert outputs. Real tool results remain authoritative, and failures return an empty token set.

Changes

Cancelled client-tool replay

Layer / File(s) Summary
Fetch and expose cancelled tool tokens
web/packages/agenta-entities/src/session/state/interactionStatus.ts, web/packages/agenta-entities/src/session/index.ts
The session state layer queries and caches cancelled client-tool tokens, handles missing scope and failures, and exports the atom and query key.
Reconcile cancelled transcript tools
web/oss/src/components/AgentChatSlice/assets/transcriptToMessages.ts, web/packages/agenta-chat/src/assets/transcriptToMessages.ts, web/oss/src/components/AgentChatSlice/assets/transcriptToMessages.test.ts, web/packages/agenta-chat/tests/unit/assets/transcriptToMessages.test.ts
Transcript conversion accepts cancellation tokens and settles matching client-tool parts. Tests cover supported tools, uncancelled tools, unknown kinds, and real-result precedence.
Hydrate messages with cancellation state
web/oss/src/components/AgentChatSlice/assets/loadSession.ts, web/packages/agenta-chat/src/assets/loadSession.ts, web/packages/agenta-chat/tests/unit/assets/loadSession.test.ts
Both session loaders fetch records and cancellation tokens concurrently and pass them to initial and refreshed transcript conversion. Loader mocks provide the token set.

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

Sequence Diagram(s)

sequenceDiagram
  participant SessionLoader
  participant SessionState
  participant TranscriptConverter
  participant MessageState

  SessionLoader->>SessionState: Fetch records and cancelled client-tool tokens
  SessionState-->>SessionLoader: Return records and token Set
  SessionLoader->>TranscriptConverter: Convert records with token Set
  TranscriptConverter->>TranscriptConverter: Settle matching client-tool parts
  TranscriptConverter-->>MessageState: Return hydrated messages
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: transcript replay now respects cancelled interaction terminal status.
Description check ✅ Passed The description directly explains the cancelled interaction replay bug, implementation, tests, and validation results.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/replay-respects-interaction-status

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: 1

🧹 Nitpick comments (1)
web/packages/agenta-entities/src/session/state/interactionStatus.ts (1)

33-37: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Filter cancelled interactions in the API request.

Line 33 fetches every client_tool interaction and discards non-cancelled entries locally. Pass status: "cancelled" to queryInteractions so hydration requests only the tokens that this flow needs.

Proposed fix
-const interactions = await queryInteractions({sessionId, projectId, kind: "client_tool"})
+const interactions = await queryInteractions({
+    sessionId,
+    projectId,
+    kind: "client_tool",
+    status: "cancelled",
+})

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 91a5ad60-64b7-4218-af9b-b2fb2b8406b3

📥 Commits

Reviewing files that changed from the base of the PR and between 965851e and efb5a0b.

📒 Files selected for processing (9)
  • web/oss/src/components/AgentChatSlice/assets/loadSession.ts
  • web/oss/src/components/AgentChatSlice/assets/transcriptToMessages.test.ts
  • web/oss/src/components/AgentChatSlice/assets/transcriptToMessages.ts
  • web/packages/agenta-chat/src/assets/loadSession.ts
  • web/packages/agenta-chat/src/assets/transcriptToMessages.ts
  • web/packages/agenta-chat/tests/unit/assets/loadSession.test.ts
  • web/packages/agenta-chat/tests/unit/assets/transcriptToMessages.test.ts
  • web/packages/agenta-entities/src/session/index.ts
  • web/packages/agenta-entities/src/session/state/interactionStatus.ts

Comment thread web/packages/agenta-chat/tests/unit/assets/loadSession.test.ts
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-08-10T16:51:26.006Z

@mmabrouk
mmabrouk merged commit 71cfce7 into release/v0.112.0 Aug 10, 2026
82 of 85 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug report Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant