mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
fix(runtime): keep listed handles when graph sync learns a PTY incarnation
reconcilePtyIncarnationHandles compared a null retained incarnation against the learned one and staled the handle. Daemon-hosted PTYs are recorded from first output before the spawn commit reports an incarnation, so on Windows `orca terminal create` returned a handle that was stale by the next graph publish. Treat null-to-known as un-fenced like every other site; keep the known-to-different and preallocated-handle invalidations.
This commit is contained in:
@@ -54,16 +54,17 @@ export class OrcaRuntimeWithBindPtyIncarnationHandle extends OrcaRuntimeWithBuil
|
||||
for (const [ptyId, retained] of this.handleByPtyIncarnation) {
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
const leaves = this.getLeavesForPty(ptyId)
|
||||
if (!pty || leaves.length !== 1) {
|
||||
// Why: a handle issued before the host reported the incarnation is un-fenced, so
|
||||
// learning it is not a replacement; only a known-to-different incarnation is.
|
||||
const incarnationReplaced =
|
||||
retained.incarnationId !== null &&
|
||||
pty !== undefined &&
|
||||
pty.incarnationId !== retained.incarnationId
|
||||
if (!pty || incarnationReplaced || leaves.length !== 1 || this.handleByPtyId.has(ptyId)) {
|
||||
this.invalidatePtyIncarnationHandle(ptyId)
|
||||
continue
|
||||
}
|
||||
// PTY startup can replace the shell incarnation (for example Git Bash
|
||||
// entering cmd.exe on Windows). Keep the caller's preallocated handle
|
||||
// bound to the live PTY instead of making the create receipt stale.
|
||||
if (pty.incarnationId !== retained.incarnationId) {
|
||||
retained.incarnationId = pty.incarnationId
|
||||
}
|
||||
retained.incarnationId = pty.incarnationId
|
||||
this.bindPtyIncarnationHandle(retained, leaves[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,40 @@ describe('runtime terminal handle incarnation fencing', () => {
|
||||
await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ handle, status: 'running' })
|
||||
})
|
||||
|
||||
it('keeps a listed handle when graph sync learns the incarnation after issue', async () => {
|
||||
// Daemon-hosted PTYs are recorded from first output before the spawn commit reports an
|
||||
// incarnation, so the handle is issued un-fenced and must survive learning it.
|
||||
const { runtime } = makeRuntime()
|
||||
runtime.registerPty(PTY_ID, WORKTREE_ID, 'target', { tabId: TAB_ID, leafId: LEAF_ID })
|
||||
syncGraph(runtime)
|
||||
const [listed] = (await runtime.listTerminals()).terminals
|
||||
|
||||
register(runtime, 'incarnation-learned')
|
||||
syncGraph(runtime)
|
||||
|
||||
await expect(runtime.readTerminal(listed.handle)).resolves.toMatchObject({
|
||||
handle: listed.handle,
|
||||
status: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
it('stales a listed handle when graph sync sees a replaced incarnation', async () => {
|
||||
const { runtime } = makeRuntime()
|
||||
register(runtime, 'incarnation-old')
|
||||
syncGraph(runtime)
|
||||
const [listed] = (await runtime.listTerminals()).terminals
|
||||
|
||||
// Rotate the record directly so reconcile is the only fence exercised.
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test reaches the runtime's protected pty record map to bypass the registerPty fence.
|
||||
const internals = runtime as unknown as {
|
||||
ptysById: Map<string, { incarnationId: string | null }>
|
||||
}
|
||||
internals.ptysById.get(PTY_ID)!.incarnationId = 'incarnation-new'
|
||||
syncGraph(runtime)
|
||||
|
||||
await expect(runtime.readTerminal(listed.handle)).rejects.toThrow('terminal_handle_stale')
|
||||
})
|
||||
|
||||
it('invalidates a direct handle when a reused PTY id gets a new incarnation', async () => {
|
||||
const { runtime, writes } = makeRuntime()
|
||||
const staleHandle = runtime.preAllocateHandleForPty(PTY_ID)
|
||||
|
||||
Reference in New Issue
Block a user