fix: allow follow-up messages after cancelling remote OpenCode questions

Cancelling a managed SSH prompt while answering a question could leave the conversation bound to its previous operation and reject the next message. Record the Agent cancellation outcome and reconcile and close the desktop binding before the next prompt.

Add two cancellation regressions and document real SSH/Electron question validation across eight theme and viewport combinations. Local queues and remote question routing are already included in 3b560c9.

Validation: prior focused checks passed 275 tests with 5 skipped; typecheck and lint passed. The final full run passed 4087 tests with 66 skipped and one packaging timeout; that packaging file separately passed 22 tests with 1 skipped. Staged diff whitespace check passed.

Release note: After cancelling a pending OpenCode question on a managed SSH task, you can send another message in the same conversation without an operation identity error.
This commit is contained in:
mesalogo
2026-09-13 13:57:57 +08:00
parent e9679fecf1
commit ea45533137
6 changed files with 127 additions and 15 deletions
@@ -1337,6 +1337,25 @@ describe('RuntimeAcpBackend', () => {
).toEqual(['offline'])
})
it('retains the owned cancellation outcome when the semantic terminal already exists', async () => {
const fixture = harness({ agentOwned: true })
let terminalWritten = false
fixture.semanticPrompts!.append.mockImplementation(() => {
if (terminalWritten) throw new Error('Prompt is already terminal')
terminalWritten = true
})
await open(fixture)
await invoke(fixture, 'runtime/preparePrompt', fixture.preparation())
await invoke(fixture, 'runtime/escalateCancellation', {
bindingId: 'binding-1', sessionId: 'session-1',
operationId: 'request-1', requestId: 'request-1', reason: 'requested'
})
await expect(invoke(fixture, 'runtime/reconcilePrompt', {
bindingId: 'binding-1', operationId: 'request-1', requestId: 'request-1'
})).resolves.toEqual({ status: 'terminal', terminalState: 'cancelled', processTree: 'empty' })
await fixture.backend.dispose()
})
it('closes an idle model bridge on detach without poisoning or stopping Runtime work', async () => {
const fixture = harness()
await open(fixture)
+5 -14
View File
@@ -1847,20 +1847,11 @@ export class RuntimeAcpBackend {
'Runtime prompt cancellation timed out'
).catch(() => undefined)
}
try {
this.#options.semanticPrompts.append({
bindingId: binding.request.bindingId,
operationId: binding.activeOperationId,
kind: 'prompt-terminal',
payload: {
status: 'cancelled',
reason: escalation.reason
},
terminalState: 'cancelled'
})
} catch {
// A concurrently completed prompt already committed terminal evidence.
}
this.#appendAbnormalOwnedPromptTerminal(binding, {
status: 'cancelled',
name: 'PromptCancelled',
message: 'Prompt execution was cancelled'
})
}
await this.#stopAndReconcile(binding, 'user-cancelled')
return { bindingId: escalation.bindingId, stopped: true }