fix(native-chat): a refused Stop is said even when the refusal answers a resend

When the first Stop's answer was lost, the press stayed quiet because Orca would send the Stop
again, but a refusal answering that resend was never said either, so the person was never told
the agent wasn't stopped. A refusal is now said on any attempt; a lost answer stays quiet only
while a resend is still owed.
This commit is contained in:
Brennan Benson
2026-10-04 04:02:52 -07:00
parent 7cfc4b3d08
commit 8fa5974453
2 changed files with 33 additions and 3 deletions
@@ -282,4 +282,29 @@ describe("a conversation Stop's first press", () => {
await pressWithStamp({ kind: 'not-done', notice: 'No.', answered: true }, true)
expect(toast.error).toHaveBeenCalledExactlyOnceWith('No.')
})
it('says a refusal that answers a resend, after a quiet lost answer on the press', async () => {
const outcomes: StructuredAgentSessionWriteOutcome<unknown>[] = [
{ kind: 'not-done', notice: 'Lost.', answered: false },
{ kind: 'not-done', notice: "The agent wasn't stopped.", answered: true }
]
const writeAs = vi.fn<StopWrite>(async () => outcomes.shift() ?? DONE)
const { view, stopOutbox } = harness(writeAs)
let pressed: Promise<void> = Promise.resolve()
act(() => {
pressed = view.result.current()
})
view.rerender({ outbox: [stamped(stopOutbox.mock.calls[0]?.[0])], submissions: [] })
await act(async () => {
await pressed
})
for (let step = 0; step < 5; step += 1) {
await act(async () => {
await vi.advanceTimersByTimeAsync(1_000)
})
}
// The mock outbox keeps the stamp owed, so the Stop may go once more; its answer is said once.
expect(writeAs.mock.calls.length).toBeGreaterThanOrEqual(2)
expect(toast.error).toHaveBeenCalledExactlyOnceWith("The agent wasn't stopped.")
})
})
@@ -102,12 +102,17 @@ export function useStructuredAgentSessionConversationStop(args: {
recordStopAnswer(stopOperationId, { kind: 'answered', cursor: outcome.cursor })
return
}
// A refusal is said; a lost answer only when nothing will send the Stop again.
// A refusal is said on any attempt, a resend's too; a lost answer only on the press, and
// only when nothing will send the Stop again.
if (
outcome.kind === 'not-done' &&
firstPress &&
(outcome.answered ||
!stopWillBeResent(latest.current.outbox, latest.current.submissions, stopOperationId))
(firstPress &&
!stopWillBeResent(
latest.current.outbox,
latest.current.submissions,
stopOperationId
)))
) {
toast.error(outcome.notice)
}