Files
Arvin Xu 230b8037f1 🐛 fix(hetero): stop settling a run whose output ingest discarded as success (#19658)
* 🐛 fix(hetero): stop settling a run whose output ingest discarded as success

A device/sandbox hetero run can finish with every one of its events thrown
away and still be recorded as a clean turn: the user is left on the `...`
assistant placeholder forever, with no error, no retry affordance, and the
topic/task retired as answered.

`heteroIngest` has two paths that discard a whole batch and answer 200 — the
operation row is no longer `running`, and the topic's `runningOperation`
marker no longer names this operation. The producer reads 200 as delivery, so
`BatchIngester.drain()` succeeds, `exitedClean` is true, and `heteroFinish`
receives `result: 'success'` for a run that persisted nothing.

Observed on a real run: the CLI did all the work (16 steps, opened the PR it
was asked for, $5.31 of tokens) while the server stored one `stream_start` and
then silently dropped the rest.

Three changes, so the loss is reported wherever it can be:

- `heteroIngest` returns `{ accepted, reason }` and stamps the first refusal
  on the operation row. A refusal is permanent, so it is an ack, not a
  transport error the producer should retry.
- `BatchIngester` fails the stream on a refusal instead of retrying, which
  surfaces at `drain()` and makes the run finish as an error carrying why.
- `heteroFinish` reads the row marker back and downgrades a `success` receipt
  to a failure, covering producers that predate the ack contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(hetero): let terminal-operation callbacks reach the refusal path

`authorizeOperationCallback` rejects an operation-token callback with CONFLICT
as soon as its row is terminal, so `heteroIngest` never reached the refusal it
was just taught to report: the ingester spent its whole retry budget on a
permanent failure, no rejection marker was recorded, and `heteroFinish` was
turned away by the same guard — leaving the turn on its unresolved placeholder.

`hetero:ingest` and `hetero:finish` now opt into `allowTerminalOperation`. The
allowance is per call site, never implied by the capability, so token renewal
(which also asks for `hetero:ingest`), goal mutations and `model:invoke` stay
strict. Every other check still runs, and neither callback gains any write
power: the service refuses them on the same terminal status, which is precisely
the answer they exist to deliver. It also aligns the narrow token with the
user-session producers, which skip this guard entirely today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 23:30:05 +08:00
..