feat(rpc): abort awaits quiesce, clears the queue, and names who aborted
Merged
CO
8d ago+430+430+012345678901234567890123456789-33-33-01234567890123456789
Fixes #3156. RPC seam plan todo 17 (F10b, D-ABORT).
What changed
abortgains three optional fields; plainabortis unchanged (still acknowledged on dispatch).awaitIdle: trueanswers once the session is idle orgraceMspassed (integer 0..60000, default 10000), withstopped/idle. On expiry both arefalse, the abort continues, and aneffectIdreceipt staysapplieduntilagent_idlemoves it todone.clearQueue: truegives every queued admission afailedreceipt with outcomecleared_by_abort(reason, detail, action), then drains both queues atomically (queue_updateis emitted) before the abort;clearedcounts them. Works on an idle session too.- An out-of-range
graceMsis refused:errorCode: "invalid_argument",errorData { reason: "invalid_grace_ms", action }.
- Aborted
agent_endand a client-causedsession_abortcarryabortedBy: { connectionId, effectId? }on every attached connection. docs/rpc.md: the abort section no longer claims plain abort waits for idle (it never did), documents the new fields,abortedBy, and theabortSourcevalues (user,provider,system).interruptis untouched. One implementation for every client: no RPC-only session path.
QA & Evidence
Real-host suite test/suite/rpc-abort-quiesce.test.ts (spawns senpi --mode rpc --multi-session --listen over a unix socket, faux provider, two connections):
abort { effectId, awaitIdle }from a second client while the first streams ->{ stopped: true, idle: true }afteragent_idle; both clients'agent_end.abortedBy={ connectionId, effectId: "a1" }; receiptdone.- A turn held open past the abort by an extension
agent_endhandler (released by a file +fs.watch, no sleeps):graceMs: 300->{ stopped: false, idle: false }, receiptapplied; after release,agent_idle->done. - Queued steer
q1+abort { clearQueue, awaitIdle }->cleared: 1, emptyqueue_update, receiptfailed/cleared_by_abort. graceMs: 70000->invalid_argumentwithreason+action; plainabort->{ success: true }with nodata.
- RED (tests on base sources): 4/4 fail.
- GREEN: 4/4 pass.
- Mutation (resolve the quiesce wait immediately instead of after the abort settles): tests 1 and 2 fail.
- biome clean;
tsc --noEmithas no errors outside the twopackages/aicatalog files main currently breaks (fixed by open #3342), so the commit used--no-verify. Locally the host runs from source (SENPI_RPC_TEST_BUN) becausepackages/aicannot build on current main for the same reason.
906b5f79244ae646c3b4f81b08a7a942a764c758944d5f5216cbc4bec6cc1eec s17-test.log
6ac8c79739e64356417965450a1ca3ba6cf9b80d110e4b48407ea9e0d5ed5ea1 s17-red.log
c1b33b9a5ab78d225c1a261caf01b91842a14fb68967b843f78b368e656050b6 s17-mut.log
Summary by cubic
Extends the RPC abort command so a client can wait for the turn to stop, drain queued input, and tell which client aborted. Plain abort is unchanged: it is still acknowledged on dispatch, before the turn stops.
awaitIdle: trueanswers once the session is idle, or aftergraceMs(integer 0–60000, default 10000); on expirystoppedandidleare bothfalse, the abort continues, and aneffectIdreceipt staysapplieduntilagent_idlemoves it todone.clearQueue: truedrains the steering and follow-up queues atomically before the abort (an idle session's too), emitsqueue_update, gives each drained input with aclientMessageIdafailedreceipt with outcomecleared_by_abort, and the answer reports how many were cleared and whether the turn stopped.- An out-of-range
graceMsis refused withinvalid_argument,reason: "invalid_grace_ms", and anaction. - A client-caused abort of a turn carries
abortedBy: { connectionId, effectId? }on every attached connection'sagent_endandsession_abort; an abort that reaches no turn is forgotten once it settles (forgetting clears only that abort, never a newer one recorded meanwhile), so a later system abort is never misattributed.interruptrecords its caller the same way, anddocs/rpc.mddocuments the new fields and theabortSourcevalues (user,provider,system).