diff --git a/packages/api/src/agents/steering/request.ts b/packages/api/src/agents/steering/request.ts index 929db8010c..fb0bfef524 100644 --- a/packages/api/src/agents/steering/request.ts +++ b/packages/api/src/agents/steering/request.ts @@ -363,19 +363,21 @@ export async function handleSteerCancel( return { status: 200, body: { removed } }; } /** - * Awaited: a dropped disarm leaves the owner armed for a steer that no - * longer exists, costing one sealed-and-empty boundary — a visibly - * truncated answer, not just a stale label. + * Awaited so a failed disarm is retried and logged before the response, + * but its outcome is deliberately NOT reported to the client. * - * `removed` stays true regardless: the steer really did leave the queue, - * and reporting otherwise would make the client re-show a chip for a steer - * that can never arrive. `disarmed: false` surfaces the residual risk - * without lying about the removal. + * A resolved publish is not proof the owner heard it — the delivery count + * includes this replica's own facade subscription — so any `disarmed` flag + * would claim a certainty the transport cannot provide, which is the same + * over-promise the `preempt` flag was corrected for. Disarm is best effort + * with a bounded, self-healing failure: if the clear is lost the owner + * seals once, its empty boundary self-clears, and the turn is persisted + * `unfinished: true` rather than silently truncated. + * + * `removed` stays true because it is true — the steer really did leave the + * queue, and inverting it would make the client re-show a chip for a steer + * that can never arrive. */ - const disarmed = await GenerationJobManager.noteSteersRemoved( - streamId, - [body.steerId], - job.createdAt, - ); - return { status: 200, body: { removed, ...(disarmed === false && { disarmed: false }) } }; + await GenerationJobManager.noteSteersRemoved(streamId, [body.steerId], job.createdAt); + return { status: 200, body: { removed } }; } diff --git a/packages/api/src/stream/GenerationJobManager.ts b/packages/api/src/stream/GenerationJobManager.ts index e38c7668bf..ad29a63162 100644 --- a/packages/api/src/stream/GenerationJobManager.ts +++ b/packages/api/src/stream/GenerationJobManager.ts @@ -3302,15 +3302,21 @@ class GenerationJobManagerClass { return Promise.resolve(true); } /** - * Awaitable so a CANCEL can react to a failed disarm. A dropped clear is - * worse than a dropped arm: the owner keeps a level-triggered request for - * a steer that no longer exists, seals its next chunk, drains nothing, - * and truncates an unrelated answer. Retried once — a transient publish - * error is the common case and the retry is cheap — then reported to the - * caller so it is not silently swallowed. + * Awaitable so a CANCEL retries and logs before responding. A dropped + * clear is worse than a dropped arm: the owner keeps a level-triggered + * request for a steer that no longer exists, seals its next chunk, + * drains nothing, and truncates an unrelated answer. Retried once — a + * transient publish error is the common case and the retry is cheap. * - * Damage is bounded even if both attempts fail: the empty-boundary - * self-clear disarms the generation after that single seal. + * The returned boolean means "published without error", NOT "the owner + * disarmed": the delivery count includes this replica's own facade + * subscription, so publication cannot prove receipt. It is for logging + * only and must not be surfaced as a guarantee. Proving receipt needs a + * correlated request/response over pub-sub. + * + * Damage is bounded regardless: if the clear is lost the owner seals + * once, the empty-boundary self-clear disarms the generation, and the + * turn is persisted `unfinished: true` rather than silently truncated. */ const publish = (): Promise => Promise.resolve(