mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 00:02:48 +00:00
fix: attach PTY dispatcher before background spawn
This commit is contained in:
@@ -3247,8 +3247,8 @@
|
||||
"motivatingLinks": [
|
||||
"https://github.com/stablyai/orca/pull/7695"
|
||||
],
|
||||
"invariant": "Before a PTY handler mounts, retained data, exit, and warning identities remain capped; data drains before exit; a fresh provider-reused PTY id cannot consume an older lifecycle's events; explicit reattach keeps its current backlog; and failed shutdown restore drains detached data before later live output.",
|
||||
"oracle": "Drive more than 64 exit-before-handler identities, warning-producing data churn, duplicate/reused ids, fresh-spawn and explicit-reattach ordering, handler registration at cap, and failed-shutdown restoration; assert bounded eviction, exact latest-code delivery once, fresh-generation filtering, data-before-exit order, and detached-before-live output.",
|
||||
"invariant": "Before a PTY handler mounts, the singleton dispatcher is already listening and retained data, exit, and warning identities remain capped; data drains before exit; a fresh provider-reused PTY id cannot consume an older lifecycle's events; explicit reattach keeps its current backlog; and failed shutdown restore drains detached data before later live output.",
|
||||
"oracle": "Drive first-use background exit-before-spawn-resolution, more than 64 exit-before-handler identities, warning-producing data churn, duplicate/reused ids, fresh-spawn and explicit-reattach ordering, handler registration at cap, and failed-shutdown restoration; assert listener-first capture, bounded eviction, exact latest-code delivery once, fresh-generation filtering, data-before-exit order, and detached-before-live output.",
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport.test.ts src/renderer/src/components/terminal-pane/pty-dispatcher-pi-routing.test.ts src/renderer/src/lib/launch-agent-background-session.test.ts src/renderer/src/lib/launch-worktree-background-terminals.test.ts"
|
||||
],
|
||||
@@ -3273,6 +3273,7 @@
|
||||
"assertions": [
|
||||
"pre-handler data drains before its exit",
|
||||
"fresh PTY-id reuse and failed-reattach fresh fallback filter old lifecycle events while explicit daemon reattach preserves pre-connect backlog",
|
||||
"first-use eager registration attaches the singleton dispatcher before a fast background PTY can emit data or exit",
|
||||
"a registered exit handler remains authoritative while 65 orphan exits churn through the bounded buffer"
|
||||
]
|
||||
},
|
||||
@@ -3291,7 +3292,7 @@
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport.test.ts src/renderer/src/components/terminal-pane/pty-dispatcher-pi-routing.test.ts src/renderer/src/lib/launch-agent-background-session.test.ts src/renderer/src/lib/launch-worktree-background-terminals.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 1,
|
||||
"summary": "5 test files passed, 114 tests passed on main@20ebe858af plus the #7695 candidate. Main@20ebe858af retained the 65th oldest exit (received code 0 instead of eviction); the bounded candidate evicts it and preserves all ordering/reuse cases."
|
||||
"summary": "5 test files passed, 115 tests passed on main@20ebe858af plus the #7695 candidate. Main@20ebe858af retained the 65th oldest exit (received code 0 instead of eviction); the bounded candidate evicts it and preserves listener-first and ordering/reuse cases."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
|
||||
@@ -441,6 +441,24 @@ describe('dispatcher → transport → onTitleChange for Pi spinner', () => {
|
||||
expect(registeredExit).toHaveBeenCalledWith('pty-registered', 9)
|
||||
})
|
||||
|
||||
it('attaches the dispatcher before first-use eager-buffer spawn can emit an exit', async () => {
|
||||
const { captureEagerPtyBufferRegistration } = await import('./pty-dispatcher')
|
||||
const eagerExit = vi.fn()
|
||||
|
||||
expect(exitDispatcherCallback).toBeNull()
|
||||
const registerEagerPtyBuffer = captureEagerPtyBufferRegistration()
|
||||
expect(exitDispatcherCallback).not.toBeNull()
|
||||
expect(window.api.pty.rendererDispatcherReady).toHaveBeenCalledOnce()
|
||||
|
||||
dispatcherCallback?.({ id: 'pty-first-background', data: 'fast-output' })
|
||||
exitDispatcherCallback?.({ id: 'pty-first-background', code: 7 })
|
||||
const handle = registerEagerPtyBuffer('pty-first-background', eagerExit)
|
||||
|
||||
expect(handle.flush()).toBe('fast-output')
|
||||
await Promise.resolve()
|
||||
expect(eagerExit).toHaveBeenCalledWith('pty-first-background', 7)
|
||||
})
|
||||
|
||||
it('filters old lifecycle events for delayed background eager-buffer registration', async () => {
|
||||
const { captureEagerPtyBufferRegistration, ensurePtyDispatcher } =
|
||||
await import('./pty-dispatcher')
|
||||
|
||||
@@ -311,6 +311,9 @@ export function getEagerPtyBufferHandle(ptyId: string): EagerPtyHandle | undefin
|
||||
const EAGER_BUFFER_MAX_BYTES = TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT
|
||||
|
||||
export function captureEagerPtyBufferRegistration(): typeof registerEagerPtyBuffer {
|
||||
// Why: pty:exit is not boot-gated like pty:data. Attach the singleton before
|
||||
// spawn so a first-use background PTY cannot exit into a listener-less page.
|
||||
ensurePtyDispatcher()
|
||||
const afterCursor = capturePreHandlerPtyEventCursor()
|
||||
return (ptyId, onExit) => registerEagerPtyBuffer(ptyId, onExit, afterCursor)
|
||||
}
|
||||
|
||||
@@ -191,14 +191,18 @@ describe('pre-handler PTY buffer', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const warningChunk = 'x'.repeat(64 * 1_024 + 1)
|
||||
|
||||
for (const ptyId of WARN_PTY_IDS) {
|
||||
bufferPreHandlerPtyData(ptyId, warningChunk)
|
||||
}
|
||||
expect(warn).toHaveBeenCalledTimes(PRE_HANDLER_PTY_MAX_PTYS + 1)
|
||||
try {
|
||||
for (const ptyId of WARN_PTY_IDS) {
|
||||
bufferPreHandlerPtyData(ptyId, warningChunk)
|
||||
}
|
||||
expect(warn).toHaveBeenCalledTimes(PRE_HANDLER_PTY_MAX_PTYS + 1)
|
||||
|
||||
// The first PTY was evicted when the 65th identity arrived. Reusing it
|
||||
// must be warnable again instead of remaining forever in the warning set.
|
||||
bufferPreHandlerPtyData(WARN_PTY_IDS[0], warningChunk)
|
||||
expect(warn).toHaveBeenCalledTimes(PRE_HANDLER_PTY_MAX_PTYS + 2)
|
||||
// The first PTY was evicted when the 65th identity arrived. Reusing it
|
||||
// must be warnable again instead of remaining forever in the warning set.
|
||||
bufferPreHandlerPtyData(WARN_PTY_IDS[0], warningChunk)
|
||||
expect(warn).toHaveBeenCalledTimes(PRE_HANDLER_PTY_MAX_PTYS + 2)
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -62,7 +62,6 @@ export async function launchAgentBackgroundSession(
|
||||
// Best-effort: continue with launch. The user can still accept the trust menu.
|
||||
}
|
||||
}
|
||||
const cmdOverrides = store.settings?.agentCmdOverrides ?? {}
|
||||
const agentArgs = resolveTuiAgentLaunchArgs(agent, store.settings?.agentDefaultArgs)
|
||||
const agentEnv = resolveTuiAgentLaunchEnv(agent, store.settings?.agentDefaultEnv)
|
||||
const launchPlatform = repo
|
||||
@@ -87,7 +86,7 @@ export async function launchAgentBackgroundSession(
|
||||
const startupPlan = buildAgentStartupPlan({
|
||||
agent,
|
||||
prompt: hasPrompt && !isFollowupPath ? trimmedPrompt : '',
|
||||
cmdOverrides,
|
||||
cmdOverrides: store.settings?.agentCmdOverrides ?? {},
|
||||
agentArgs,
|
||||
agentEnv,
|
||||
platform: launchPlatform,
|
||||
|
||||
@@ -8,6 +8,7 @@ const mockSetTabLayout = vi.fn()
|
||||
const mockUpdateTabPtyId = vi.fn()
|
||||
const mockClearTabPtyId = vi.fn()
|
||||
const mockCloseTab = vi.fn()
|
||||
const mockEnsurePtyDispatcher = vi.fn()
|
||||
const mockRegisterEagerPtyBuffer = vi.fn()
|
||||
const mockGetActiveRuntimeTarget = vi.fn()
|
||||
|
||||
@@ -52,6 +53,7 @@ vi.mock('@/lib/browser-uuid', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/components/terminal-pane/pty-dispatcher', () => ({
|
||||
ensurePtyDispatcher: mockEnsurePtyDispatcher,
|
||||
registerEagerPtyBuffer: mockRegisterEagerPtyBuffer
|
||||
}))
|
||||
|
||||
@@ -140,6 +142,9 @@ describe('launchWorktreeBackgroundTerminals', () => {
|
||||
expect(mockUpdateTabPtyId).toHaveBeenCalledWith('tab-1', 'pty-1')
|
||||
expect(mockUpdateTabPtyId).toHaveBeenCalledWith('tab-2', 'pty-2')
|
||||
expect(mockRegisterEagerPtyBuffer).toHaveBeenCalledTimes(2)
|
||||
expect(mockEnsurePtyDispatcher.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockSpawn.mock.invocationCallOrder[0] ?? 0
|
||||
)
|
||||
})
|
||||
|
||||
it('spawns setup in a split when setup launch mode requests a split', async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ensurePtyDispatcher,
|
||||
registerEagerPtyBuffer,
|
||||
type EagerPtyHandle
|
||||
} from '@/components/terminal-pane/pty-dispatcher'
|
||||
@@ -177,6 +178,8 @@ async function createBackgroundTab(args: {
|
||||
const leafId = createBrowserUuid()
|
||||
store.setTabLayout(tab.id, singlePaneLayoutSnapshot(leafId))
|
||||
let ptyId: string
|
||||
// Why: pty:exit is not held by main's renderer-ready data gate.
|
||||
ensurePtyDispatcher()
|
||||
const preHandlerCursor = capturePreHandlerPtyEventCursor()
|
||||
try {
|
||||
ptyId = await spawnPane({
|
||||
@@ -206,6 +209,8 @@ async function addSetupSplit(args: {
|
||||
}): Promise<void> {
|
||||
const store = useAppStore.getState()
|
||||
const setupLeafId = createBrowserUuid()
|
||||
// Why: a setup shell may exit before spawn resolves on a first-use renderer.
|
||||
ensurePtyDispatcher()
|
||||
const preHandlerCursor = capturePreHandlerPtyEventCursor()
|
||||
const setupPtyId = await spawnPane({
|
||||
worktree: args.worktree,
|
||||
|
||||
Reference in New Issue
Block a user