Skip to content

fix(server): transport result.answer carries the last segment, reset set aligned with the stream layer - #34

Merged
fengzhi09 merged 2 commits into
mainfrom
fix/transport-answer-accumulator
Sep 26, 2026
Merged

fengzhi09 merged 2 commits into
mainfrom
fix/transport-answer-accumulator

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

What

Closes ticket 07 — the transport layer's turn-long accumulator made [send] result.answer (and the log line, ● finalize rewrite, token estimate, empty-turn note derived from it) a concatenation of every segment, inconsistent with the per-segment semantics established in #30/#32.

  • acp.mjs mirrors the stream layer's lastChunkKind discriminator: same-kind chunks append; chat-line-breaking events (tool_call, tool_call_update) and the thought↔message kind change reset. usage_update/plan_update/session_info_update/mode events keep accumulating (they can interleave mid-segment; an early reset would truncate the answer before the settle merge).
  • Reset sets on both sides carry cross-referencing comments naming the canonical rule (a shared helper was rejected: two event vocabularies across the transport boundary, with the kind-change reset duplicated regardless).
  • Semantics: result.answer/result.thinking carry the LAST segment — streamAcpPrompt is the transport's only consumer; a segments array would be unused surface.

Acceptance (independent agent, 2 rounds)

Round 1 PASS-WITH-CONCERNS — flagged the reset-set drift (broad transport resets vs chat-line-breaking stream resets). Round 2 PASS after alignment: the bidirectional pin drives usage/session_info/plan events mid-segment and asserts accumulation holds (fails on both the old concatenating code and the briefly-broad reset). Gates: test file 7/7 ×3 · relevant suite 91 pass · typecheck 0 · test:webapp 213 · build ✓. Live: fake two-segment engine logs answer:"part two".

Full pnpm verify deferred to CI.

… turn

Close the transport-level accumulator left behind by the per-segment
rendering work (session-isolation/06) and the run-mirror routing
(session-isolation/02): acp.mjs#prompt accumulated result.answer /
result.thinking across the whole turn, and streamAcpPrompt copied that
concatenation over its own per-segment value when the prompt settled.
Every consumer of the settled value then saw turn-long text — the
[send] result log, the ● finalize rewrite, the no-usage token estimate
and the empty-turn note.

Semantics chosen: result.answer / result.thinking carry the LAST
segment. acp.mjs now mirrors streamAcpPrompt's lastChunkKind
discriminator — same-kind chunks append (streaming growth), any kind
change or intervening event (thought ↔ message, tool_call,
tool_update, plan_update, ...) resets. A segments array was considered
and rejected: streamAcpPrompt is the transport's only consumer and it
wants exactly the last segment, so the array would be unused surface
in a reviewed public-projection repo. The `|| r.answer` fallback in
the settle path is kept and now agrees with the callback-derived value
(both sides use the same segment rule); it only matters when the
transport saw no chunks of that kind.

Tests: new lib/acp-transport-answer.test.js — transport-level pins
(multi-segment turn resolves to the final segment; same-kind growth
stays one segment; thought/message resets mirror the live ▲ / ●
discriminator; a tool_update between message segments starts a new
segment) and integration pins through the real runMcodeAcp (no-usage
token estimate computed from segment text; a thought-only turn still
produces the empty-turn note). Regenerated release/public-source.json
for the new test file.

Live check: isolated instance on port 18096 with a fake two-segment
engine — [send] result logs "answer":"part two" (previously
"part one part two").

Gates: pnpm typecheck 0 errors; relevant suites 156 tests × 3 runs
green; pnpm test:webapp 213 pass; pnpm build OK.
Acceptance follow-up to the transport per-segment accumulator: the
first cut reset result.lastChunkKind on ANY non-chunk session event,
while the server-side lastChunkKind only breaks the chain on
chat-line-breaking tool events. A usage_update or session_info_update
interleaving MID-segment would reset the transport accumulator early,
and the `|| r.answer` settle fallback in streamAcpPrompt would then
overwrite r.answer with a post-event fragment — truncating the ●
finalize rewrite, the token estimate and the empty-turn note input.

Chose aligning the reset set (option a) over a shared helper: the two
sides speak different event vocabularies (u.sessionUpdate vs c.kind)
across the transport boundary and the thought ↔ message kind-change
reset would stay duplicated anyway, so a helper would only partially
dedup while coupling the zero-dep transport to more server surface.
The reset set is now pinned by tests in both directions and both sites
carry cross-referencing comments.

Reset set after this change: tool_call and tool_call_update (chat-line
breaking) plus the thought ↔ message kind change. usage_update,
plan_update, session_info_update, mode/goal/config updates and other
non-rendering events keep the segment accumulating.

Tests: new transport pin — mid-segment usage_update /
session_info_update / plan_update events do NOT reset (answer keeps
accumulating across them) while tool_call / tool_call_update DO
(existing pin). File now 7 tests.

Gates: test file 3× green (7 pass per run); relevant suite 91 pass;
pnpm typecheck 0 errors.
@fengzhi09
fengzhi09 merged commit 7661314 into main Sep 26, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the fix/transport-answer-accumulator branch September 26, 2026 02:54
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