fix(agents): Confirm initial input after transcript flush

This commit is contained in:
yiliang114 2026-09-08 16:44:39 +08:00
parent f20fc3346b
commit 954c1ffa29
6 changed files with 73 additions and 8 deletions

View file

@ -414,9 +414,10 @@ Message sequence is monotonic per thread. `triggerMessageIds` means durably
booked; `acceptedMessageIds` means the runtime queue accepted those inputs;
`consumedMessageIds` is recorded from the correlated `EXTERNAL_MESSAGE` event
when the old background runtime drains them. In the ACP session path, daemon
queue-only input carries the run metadata and delivery id; the child checks the
ambient binding, flushes its mid-turn transcript record, then records the consumed
window under the workspace lock. Queue acceptance is not that receipt.
input carries the run metadata and delivery watermark; the session checks the
ambient binding, flushes the initial or mid-turn transcript record, then records
the consumed window under the workspace lock. Queue acceptance is not that
receipt.
`committedThroughSequence` advances only across a
contiguous consumed context window. On a failed enqueue, execution failure, or
daemon restart, reconciliation rebooks everything not consumed and committed.
@ -435,10 +436,11 @@ different processes and their wall clocks can disagree.
At run start and direct delivery, the dispatcher sends one contiguous context
window through `contextThroughSequence`, not just the triggering ids. The
initial prompt is consumed when the turn starts; direct input becomes consumed
only on its correlated runtime event. This makes the scalar watermark honest
even when intervening posts targeted another agent. A clean but `unclosed`
return blocks the workflow yet commits only demonstrably consumed input. A
initial prompt becomes consumed only after its transcript record is durable;
direct input follows the same transcript-before-receipt order. This makes the
scalar watermark honest even when intervening posts targeted another agent. A
clean but `unclosed` return blocks the workflow yet commits only demonstrably
consumed input. A
durable `blocked`/`review` close marker likewise lets restart reconciliation
finish the workflow close without pretending an accepted-but-undrained message
was read.

View file

@ -27,6 +27,7 @@ Current run ids, timestamps and limitations are recorded in
| Concurrent same-thread handoff and human acceptance | Clean three-run peer handoff; 8,999 ms overlapping run lifetimes; both results submitted and Chrome Mark done succeeded. |
| Child delegation and parent report | Earlier ACP demo reached done on child and parent, with manual startup retries; not a clean first-attempt run. |
| Live human input | Same-run mid-turn transcript and consumed window verified; final review contains the correction. Late-drain/crash cases remain open. |
| Initial input receipt | A real assigned run persisted its initial prompt, consumed the matching trigger and reached review. The transcript-before-receipt crash window remains to be force-killed. |
| Cancellation | Working → stopping → cancelled observed; usage retained; unresponsive-child case remains open. |
| Running task after daemon restart | SIGKILL after a durable checkpoint exposed a startup-discovery defect; after its fix, the same run/session recovered as attempt 2 and reached human acceptance. Other crash windows remain open. |
| Human resolves a blocker | Real thread_block question, human selection, same-session continuation, JSON review, and Chrome Mark done verified. Other blocker-acknowledgement scopes remain an owner decision. |
@ -239,6 +240,18 @@ the watchdog. The long replay run also exposed the conservative accounting
policy visibly: it closed at `666,749 / 200,000` tokens because the limit is
checked at admission, not between tool rounds.
**Initial-receipt observation (2026-09-08).** Real ACP run
`rn_bcd7f190-5958-4dcd-9c92-ea207d4aad16` posted
`INITIAL-RECEIPT-8391` and closed with `thread_review`. Its durable trigger
`ms_c7dcb7be-dda0-496f-90f7-e3663dec5130` is the sole consumed id and the
committed watermark is sequence 1. Reloading session
`b48bad11-6e7c-5110-92a1-e560bf56eec6` from disk found the assigned prompt and
marker in its transcript. The session path now flushes that initial user record
before writing the receipt; dispatch no longer marks it consumed before
activation. This proves the normal path and source order, not the named
process-exit injection between those two writes. No build, lint, test suite, or
CI ran.
### Step 9 — REST and Web Shell
Design direction is settled ahead of the build in [`2026-09-07-workspace-agents-web-shell-design.md`](./2026-09-07-workspace-agents-web-shell-design.md): the thread view is a ledger of outstanding obligations with the conversation as evidence, not a chat log with a status badge. It inherits Web Shell's existing tokens and adds no new colour or typeface. Step 9 renders `resolveThreadStatus`'s `status` and `reason` rather than inventing a second status vocabulary, shows one lane per agent that has worked the thread, and bounds every transcript view to the run's own slice.

View file

@ -140,6 +140,23 @@ recorded a valid repeated receipt once. No build, lint, typecheck or test suite
ran. Crash/late-drain/close races on the new ACP path still require live checks;
this supersedes earlier statements below that live input is unconnected.
#### ACP initial input — transcript before receipt
The initial launch path no longer reports its input consumed while the session
is merely prepared. `Session` records and flushes the initial user message
before advancing the durable consumed watermark; a failed receipt leaves the
input eligible for replay.
Real task `th_c7646c7b-7f4c-4996-9560-42721f125d0d` ran on the existing
demo-worker session. Run `rn_bcd7f190-5958-4dcd-9c92-ea207d4aad16` posted
`INITIAL-RECEIPT-8391`, called `thread_review`, and finished with trigger
`ms_c7dcb7be-dda0-496f-90f7-e3663dec5130` consumed at watermark 1. Reloading
the session from disk found both the assigned prompt and marker in its
transcript. This observes the normal model path and checks the persisted result;
the exact process-exit window between transcript flush and receipt remains an
unrun failure injection. No build, lint, typecheck, test suite, CI, or new PR
was used.
#### Unread input at close
Source inspection found that explicit close used to promote every accepted id

View file

@ -232,6 +232,7 @@ import {
runWithAgentRunContext,
requireAgentRunContext,
consumeAgentInput,
readThread,
type AgentRunContext,
} from '@qwen-code/qwen-code-core';
import { NOT_CURRENTLY_GENERATING_CANCEL_MESSAGE } from '@qwen-code/acp-bridge/bridgeErrors';
@ -5456,6 +5457,37 @@ export class Session implements SessionContext {
} else {
recorder?.recordUserMessage(promptText);
}
const agentRun = parsePromptAgentRun(params);
if (agentRun) {
try {
const thread = await readThread(
this.config.getWorkingDir(),
agentRun.threadId,
);
const delivered = thread?.messages.find(
(message) =>
message.sequence === agentRun.contextThroughSequence,
);
if (!recorder || !delivered) {
throw new Error(
'Agent input requires a transcript and delivery watermark',
);
}
await recorder.flush();
await consumeAgentInput(
this.config.getWorkingDir(),
delivered.id,
delivered.sequence,
);
} catch (error) {
// The model may run twice after a receipt failure; losing the
// task would be worse than replaying its durable input.
debugLogger.warn(
'Agent input receipt failed; replay remains pending',
error,
);
}
}
}
if (

View file

@ -68,6 +68,7 @@ describe('session dispatch port', () => {
expect(result.status).toBe('started');
expect(sendPrompt).not.toHaveBeenCalled();
if (result.status !== 'started') throw new Error('Expected prepared session');
expect(result.consumedOnStart).toBe(false);
result.activate?.();
expect(contextOf(sendPrompt)?.agentRun).toEqual({
workspaceId: 'ws_1',

View file

@ -176,7 +176,7 @@ export function createSessionDispatchPort(
return {
status: 'started',
sessionId,
consumedOnStart: true,
consumedOnStart: false,
activate() {
const execution: AgentBodyState = {
kind: 'running',