feat(rpc): abort awaits quiesce, clears the queue, and names who aborted

Merged
CO
8d ago
+430-33

Fixes #3156. RPC seam plan todo 17 (F10b, D-ABORT).

What changed

  • abort gains three optional fields; plain abort is unchanged (still acknowledged on dispatch).
    • awaitIdle: true answers once the session is idle or graceMs passed (integer 0..60000, default 10000), with stopped / idle. On expiry both are false, the abort continues, and an effectId receipt stays applied until agent_idle moves it to done.
    • clearQueue: true gives every queued admission a failed receipt with outcome cleared_by_abort (reason, detail, action), then drains both queues atomically (queue_update is emitted) before the abort; cleared counts them. Works on an idle session too.
    • An out-of-range graceMs is refused: errorCode: "invalid_argument", errorData { reason: "invalid_grace_ms", action }.
  • Aborted agent_end and a client-caused session_abort carry abortedBy: { 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 the abortSource values (user, provider, system).
  • interrupt is 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):

  1. abort { effectId, awaitIdle } from a second client while the first streams -> { stopped: true, idle: true } after agent_idle; both clients' agent_end.abortedBy = { connectionId, effectId: "a1" }; receipt done.
  2. A turn held open past the abort by an extension agent_end handler (released by a file + fs.watch, no sleeps): graceMs: 300 -> { stopped: false, idle: false }, receipt applied; after release, agent_idle -> done.
  3. Queued steer q1 + abort { clearQueue, awaitIdle } -> cleared: 1, empty queue_update, receipt failed / cleared_by_abort.
  4. graceMs: 70000 -> invalid_argument with reason + action; plain abort -> { success: true } with no data.
  • 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 --noEmit has no errors outside the two packages/ai catalog files main currently breaks (fixed by open #3342), so the commit used --no-verify. Locally the host runs from source (SENPI_RPC_TEST_BUN) because packages/ai cannot 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: true answers once the session is idle, or after graceMs (integer 0–60000, default 10000); on expiry stopped and idle are both false, the abort continues, and an effectId receipt stays applied until agent_idle moves it to done.
  • clearQueue: true drains the steering and follow-up queues atomically before the abort (an idle session's too), emits queue_update, gives each drained input with a clientMessageId a failed receipt with outcome cleared_by_abort, and the answer reports how many were cleared and whether the turn stopped.
  • An out-of-range graceMs is refused with invalid_argument, reason: "invalid_grace_ms", and an action.
  • A client-caused abort of a turn carries abortedBy: { connectionId, effectId? } on every attached connection's agent_end and session_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. interrupt records its caller the same way, and docs/rpc.md documents the new fields and the abortSource values (user, provider, system).