fix(orchestration): submit mail pointers to Cursor CLI instead of leaving them in the composer (#33347)

Orchestration mailbox pointers now get their delayed Enter for Cursor too (the Cursor no-Enter skip from #3156 left mail delivered but never acted on). Verified with real cursor-agent. Fixes #19760. Supersedes #19761.

Co-authored-by: Maple <54388301+zleo-ai@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-25 00:07:42 -07:00
committed by GitHub
co-authored by Maple
parent f27d8717dd
commit e13e5e8b7d
7 changed files with 36 additions and 64 deletions
@@ -246,7 +246,6 @@ export abstract class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWith
isAgentSettledForDelivery: (leaf) => this.checkDeliverySettledAndArmRecheck(leaf),
isPtyAwaitingUserInput: (ptyId) => this.isPtyAwaitingUserInput(ptyId),
getMessageWaiters: (mailboxHandle) => this.messageWaiters.get(mailboxHandle),
getTabTitle: (tabId) => this.tabs.get(tabId)?.title,
getCliCommand: (terminalHandle) => this.getTerminalOrchestrationCliCommand(terminalHandle),
getTerminalHandleForLeafKey: (leafKey) => this.handleByLeafKey.get(leafKey),
resolveSubmitTarget: (leaf, ptyId) => this.resolveOrchestrationPointerSubmitTarget(leaf, ptyId),
@@ -579,7 +579,7 @@ describe('OrcaRuntimeService', () => {
}
})
it('injects pending orchestration messages into Cursor Agent without auto-submitting', async () => {
it('submits the mail pointer to Cursor Agent only after the paste-burst delay', async () => {
vi.useFakeTimers()
try {
const runtime = new OrcaRuntimeService(store)
@@ -606,11 +606,12 @@ describe('OrcaRuntimeService', () => {
'pty-1',
expect.stringContaining('You have 1 orchestration message')
)
await vi.advanceTimersByTimeAsync(500)
const submitWrites = write.mock.calls.filter(
([ptyId, text]) => ptyId === 'pty-1' && text === '\r'
)
expect(submitWrites).toHaveLength(0)
const isSubmit = ([ptyId, text]: unknown[]): boolean => ptyId === 'pty-1' && text === '\r'
// Why: Cursor reads Enter within a 35ms keystroke burst as a newline, so it must trail the pointer.
await vi.advanceTimersByTimeAsync(100)
expect(write.mock.calls.filter(isSubmit)).toHaveLength(0)
await vi.advanceTimersByTimeAsync(400)
expect(write.mock.calls.filter(isSubmit)).toHaveLength(1)
const unread = db.getUnreadMessages(mailbox)
expect(unread).toHaveLength(1)
@@ -622,41 +623,6 @@ describe('OrcaRuntimeService', () => {
}
})
it('still auto-submits to a non-Cursor agent when its idle title mentions Cursor Agent', async () => {
vi.useFakeTimers()
try {
const runtime = new OrcaRuntimeService(store)
const db = new InMemoryOrchestrationMessages()
const write = vi.fn().mockReturnValue(true)
setInMemoryOrchestrationMessages(runtime, db)
runtime.setPtyController({
write,
writeWithSettlement: settledWriteStub(write),
kill: vi.fn(),
getForegroundProcess: async () => null
})
syncSinglePty(runtime, 'pty-1', { tabTitle: 'cursor-repro-branch' })
const [terminal] = (await runtime.listTerminals()).terminals
bindSinglePtyRun(db, terminal.handle)
runtime.onPtyData('pty-1', '\x1b]0;. Investigate Cursor Agent\x07', 100)
runtime.onPtyData('pty-1', '\x1b]0;* Investigate Cursor Agent\x07', 101)
db.insertMessage({ from: 'term_sender', to: terminal.handle, subject: 'hello claude' })
runtime.deliverPendingMessagesForHandle(terminal.handle)
await vi.advanceTimersByTimeAsync(500)
expect(write).toHaveBeenCalledWith(
'pty-1',
expect.stringContaining('You have 1 orchestration message')
)
expect(write).toHaveBeenCalledWith('pty-1', '\r')
db.close()
} finally {
vi.useRealTimers()
}
})
it('does not replay an already-delivered message on a later idle transition', async () => {
vi.useFakeTimers()
try {
@@ -26,7 +26,6 @@ export type PointerDeliveryDependencies<TWaiter extends OrchestrationMessageWait
isAgentSettledForDelivery: (leaf: OrchestrationMailboxLeaf) => boolean
isPtyAwaitingUserInput: (ptyId: string) => boolean
getMessageWaiters: (mailboxHandle: string) => ReadonlySet<TWaiter> | undefined
getTabTitle: (tabId: string) => string | null | undefined
getCliCommand: (terminalHandle: string) => OrchestrationCliCommand
getTerminalHandleForLeafKey: (leafKey: string) => string | undefined
resolveSubmitTarget: (
@@ -19,7 +19,10 @@ const LEAF = {
lastOscTitle: null
}
function pointerDeps(db: OrchestrationDb, writePty: () => WriteSettlement) {
function pointerDeps(
db: OrchestrationDb,
writePty: (ptyId: string, data: string) => WriteSettlement
) {
return {
mailboxOwner: { resolve: () => 'run:run-1' },
deliveryTarget: { resolveTerminalHandle: () => 'term-1', deferForAbsenceProbe: () => false },
@@ -30,7 +33,6 @@ function pointerDeps(db: OrchestrationDb, writePty: () => WriteSettlement) {
// These cases exercise staging and Enter phases, not the idle gate; the pane is settled.
isAgentSettledForDelivery: () => true,
getMessageWaiters: () => undefined,
getTabTitle: () => null,
getCliCommand: () => 'orca' as const,
getTerminalHandleForLeafKey: () => 'term-1',
resolveSubmitTarget: () => ({
@@ -204,6 +206,26 @@ describe('mailbox pointer staging watermark', () => {
expect(writePty.mock.calls.length).toBeGreaterThan(0)
db.close()
})
// Why: a pointer left unsubmitted in Cursor's composer stacks up and rides along with the user's next prompt.
it('submits the pointer with Enter for a Cursor Agent pane', async () => {
const db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'mail' })
const writePty = vi.fn((_ptyId: string, _data: string) => WRITE_ACCEPTED)
const state = new OrchestrationMailboxPointerState()
const cursorLeaf = { ...LEAF, lastOscTitle: 'Cursor Agent' }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture supplies only the deps the staging path reads.
stageOrchestrationMailboxPointer({
...stageArgs(db, state),
deps: pointerDeps(db, writePty),
leaf: cursorLeaf,
messages: [{ id: message.id, type: 'status', sequence: 1 }]
} as never)
await vi.waitFor(() => expect(writePty).toHaveBeenLastCalledWith('pty-1', '\r'))
expect(writePty.mock.calls[0]?.[1]).toContain('You have 1 orchestration message')
db.close()
})
})
describe('retiring a pty mid-delivery', () => {
@@ -1,4 +1,3 @@
import { isCursorAgentTitle } from '../../../shared/agent-detection'
import { formatMessagePointer } from './formatter'
import type {
OrchestrationMailboxPointerMessage,
@@ -147,16 +146,6 @@ function finishPointerWriteAndStageEnter<TWaiter extends OrchestrationMessageWai
}
return
}
if (
[args.leaf.lastOscTitle, args.leaf.paneTitle, args.deps.getTabTitle(args.leaf.tabId)].some(
isCursorAgentTitle
)
) {
db.markAsDelivered(flight.stagedMessageIds, args.mailboxHandle)
args.state.clearWatermark(args.mailboxHandle, args.newestSequence, ptyId)
args.redrive(args.mailboxHandle)
return
}
const submitEnter = (): void =>
submitOrchestrationMailboxPointer(
{
@@ -28,7 +28,7 @@ import path from 'node:path'
/** `detectAgentStatusFromTitle` reads these as agent-name + strong keyword. */
export const CODEX_IDLE_TITLE = 'Codex done'
export const CODEX_WORKING_TITLE = 'Codex working'
/** Also satisfies `isCursorAgentTitle`, which suppresses the synthesized Enter. */
/** Cursor Agent idle title (`isCursorAgentTitle`). */
export const CURSOR_IDLE_TITLE = 'Cursor Ready'
export type AgentLedgerEntry = {
@@ -627,25 +627,22 @@ test.describe('orchestration push-on-idle mail delivery', () => {
await expectSubmitted(pane)
})
test('writes the pointer but never Enter for a Cursor agent pane', async ({
test('writes and submits the pointer for a Cursor agent pane', async ({
orcaPage,
electronApp
}) => {
test.setTimeout(180_000)
const { client, openAgentPane } = await setUpMailFixture(orcaPage, electronApp)
const pane = await openAgentPane()
// Cursor treats injected PTY text as editable prompt content, so submitting
// has to stay under user control there too.
pane.agent.setTitle(CURSOR_IDLE_TITLE)
await waitForObservedTitle(client, pane.handle, CURSOR_IDLE_TITLE)
const mailbox = await createRunMailbox(client, pane, 'Cursor no-submit')
const mailbox = await createRunMailbox(client, pane, 'Cursor pointer submit')
const subject = 'Cursor no-submit'
const subject = 'Cursor pointer submit'
await sendMail(client, mailbox, { subject })
await expectPointed(pane)
await orcaPage.waitForTimeout(2_000)
expectNotSubmitted(pane)
await expectSubmitted(pane)
})
})