fix(agent): carry cancel(reason) through the marker-only windows (review)
A reviewer found that `cancel(reason)` only preserved the caller's reason when
an active AbortController observed it (the mid-step path, via
`abort.signal.reason`). The marker-only windows (step-start at loop.ts and the
continuation gate) hardcoded `reason: 'cancelled'`, so the logged `turn/end`
reason was race-dependent on WHERE the cancel landed and the public
`cancel(reason?)` parameter was half-effective.
Capture the resolved reason (`reason ?? 'cancelled'`) on the agent when the
marker is armed, expose it on the LoopHandle as `cancelReason()`, and use it in
both marker branches so a turn dropped without a live controller records the
SAME `{kind:'aborted', reason}` the mid-step path produces.
The two existing window tests asserted `reason: 'cancelled'` while passing
`'from turn-start'` / `'from continuation'` — they documented the bug. Updated
both to assert the caller's reason (behavior + test changed together, per
AGENTS.md "tests document behavior, not golden truth").
Also fixes two stale docs the PR's contract change left behind: the
module-level ACP mapping comment and `codec.ts` both still said `session/cancel
-> agent.abort()`.
This commit is contained in:
5 files changed
+37
-8
No files matched your search
@@ -116,6 +116,14 @@ export interface LoopHandle {
|
||||
* marker governs exactly one cancellation and never leaks to a later prompt.
|
||||
*/
|
||||
isCancelled(): boolean
|
||||
/**
|
||||
* The resolved reason for the pending cancel (`reason ?? 'cancelled'`), read
|
||||
* by the marker branches (pre-step / continuation) so a turn dropped where no
|
||||
* `AbortController` carries the reason still records the caller's
|
||||
* `cancel(reason)` value — matching the mid-step abort path. Only meaningful
|
||||
* when {@link isCancelled} is true.
|
||||
*/
|
||||
cancelReason(): string
|
||||
/** Clear the cancel marker (called once per iteration after the turn returns). */
|
||||
clearCancel(): void
|
||||
/**
|
||||
@@ -396,7 +404,7 @@ async function runTurn(ctx: Context, agent: ReactLoopAgent, handle: LoopHandle,
|
||||
// already-appended step/start.
|
||||
if (handle.isCancelled()) {
|
||||
handle.setAbort(undefined)
|
||||
reason = { kind: 'aborted', reason: 'cancelled' }
|
||||
reason = { kind: 'aborted', reason: handle.cancelReason() }
|
||||
closeStep()
|
||||
break
|
||||
}
|
||||
@@ -466,7 +474,7 @@ async function runTurn(ctx: Context, agent: ReactLoopAgent, handle: LoopHandle,
|
||||
// ends the turn here. cancel() also cleared the steering FIFO, so the
|
||||
// override above did not re-arm continuation.
|
||||
if (handle.isCancelled()) {
|
||||
reason = { kind: 'aborted', reason: 'cancelled' }
|
||||
reason = { kind: 'aborted', reason: handle.cancelReason() }
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user