Ignore Droid Ctrl-C for interrupt inference

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-05-19 01:01:09 -07:00
co-authored by Orca
parent b9b1f46ad3
commit 31fb65bf0b
4 changed files with 74 additions and 0 deletions
+39
View File
@@ -317,6 +317,45 @@ describe('AgentHookServer listener replay', () => {
}
)
it('rejects Ctrl+C inference for Droid', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: { state: 'working', prompt: 'long task', agentType: 'droid' }
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
const applied = server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'long task',
baselineAgentType: 'droid',
intent: 'ctrl-c'
})
expect(applied).toBe(false)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
state: 'working',
prompt: 'long task',
agentType: 'droid'
})
])
} finally {
vi.useRealTimers()
}
})
it('does not let late same-turn working hooks resurrect an inferred interrupt', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
+5
View File
@@ -311,6 +311,11 @@ export class AgentHookServer {
}
const payload = existing.payload
const agentType: AgentType | undefined = payload.agentType
// Why: Droid's Ctrl+C does not interrupt the current turn; repeated Ctrl+C
// exits the CLI, which is handled by process/PTY lifecycle cleanup.
if (agentType === 'droid' && request.intent === 'ctrl-c') {
return false
}
// Why: these agents use the first Escape as a TUI/editor cancel. A single
// Escape can leave the turn running, so only a deliberate double Escape
// may infer an interrupted turn.
@@ -128,6 +128,25 @@ describe('agent interrupt inference', () => {
entry = undefined
})
it('does not infer Ctrl+C for Droid', () => {
vi.useFakeTimers()
let entry: AgentStatusEntry | undefined = makeEntry({ agentType: 'droid' })
const inferInterrupt = vi.fn()
const tracker = createAgentInterruptInference({
paneKey: PANE_KEY,
getStatusEntry: () => entry,
inferInterrupt,
now: () => 1_100
})
tracker.observeInputIntent('ctrl-c')
vi.advanceTimersByTime(500)
expect(inferInterrupt).not.toHaveBeenCalled()
tracker.dispose()
entry = undefined
})
it.each(['opencode', 'copilot'] as const)(
'infers immediately on double Escape for %s',
(agentType) => {
@@ -49,6 +49,13 @@ function shouldFlushInterruptImmediately(
)
}
function shouldIgnoreInterruptIntent(
agentType: AgentStatusEntry['agentType'],
intent: AgentInterruptInputIntent
): boolean {
return agentType === 'droid' && intent === 'ctrl-c'
}
function isSameTurnBaseline(
left: CapturedInterruptBaseline,
right: CapturedInterruptBaseline
@@ -192,6 +199,10 @@ export function createAgentInterruptInference({
clearPending()
return
}
if (shouldIgnoreInterruptIntent(baseline.agentType, intent)) {
clearPending()
return
}
if (requiresDoubleEscapeForAgent(baseline.agentType, intent)) {
const isSecondEscape =
doubleEscapeBaseline !== null && isSameTurnBaseline(doubleEscapeBaseline, baseline)