From 13bde98bca292932e628bef0d00de5ecc1d36d1e Mon Sep 17 00:00:00 2001 From: Neil Date: Sat, 19 Sep 2026 00:11:24 -0700 Subject: [PATCH] fix(omp): preserve child status ownership provenance --- .../omp-runtime-session-provenance.md | 19 ++++++------ .../pi/omp-session-status-owner-source.ts | 29 ++++++++++++++++--- src/main/pi/omp-session-status-owner.test.ts | 27 ++++++++++++++++- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/docs/reference/omp-runtime-session-provenance.md b/docs/reference/omp-runtime-session-provenance.md index 658a736241f..8d6e33577d9 100644 --- a/docs/reference/omp-runtime-session-provenance.md +++ b/docs/reference/omp-runtime-session-provenance.md @@ -1,15 +1,16 @@ # OMP runtime session provenance -The status extension uses `ctx.agentKind === 'sub'`, when provided by OMP, to -reject child callbacks before they can claim a pane. The value must describe the -current runtime session, not the ancestry of its transcript. A child transcript -resumed directly as the pane's main session must still report status. +OMP computes whether a runtime session is a task child, but the released +`ExtensionContext` does not expose that value. The status extension therefore uses +the session manager's parent header and nested task transcript path only when a +root owner is already known. A nested transcript with no known owner remains +eligible because it may have been resumed directly as the pane's main session. -This optional extension-context field requires an OMP runtime change. The reviewed -OMP source at e7546987ca526eac8f605fac19ef9805b8f01898 already computes `agentKind` -in its SDK, but does not expose it through ExtensionRunner.createContext. A local -runtime proposal forwards that value to extension callbacks. It is not released -or installed by this Orca change. +The remaining child-first case is inherently ambiguous to Orca: task children and +resumed child transcripts have the same public session-manager shape. A complete +child-first fence requires OMP to expose its computed `agentKind` through +`ExtensionRunner.createContext`; until then the conservative fallback avoids +silencing valid resumed sessions. Older runtimes retain the manager-identity guard. That guard assumes the main session reaches Orca's callback before any child. An earlier user extension can diff --git a/src/main/pi/omp-session-status-owner-source.ts b/src/main/pi/omp-session-status-owner-source.ts index 72ac45456ec..8072393e376 100644 --- a/src/main/pi/omp-session-status-owner-source.ts +++ b/src/main/pi/omp-session-status-owner-source.ts @@ -2,11 +2,27 @@ export function getOmpSessionOwnerHandlerSourceLines(): string[] { return [ ' // SessionManager survives reload/new/resume; task children own a different instance.', + ' function sessionProvenance(ctx): { manager: unknown; id?: string; file?: string; parent?: string } | undefined {', + ' const manager = ctx?.sessionManager', + " if (!manager || typeof manager !== 'object') return undefined", + ' const id = typeof manager.getSessionId === "function" ? manager.getSessionId() : undefined', + ' const file = typeof manager.getSessionFile === "function" ? manager.getSessionFile() : undefined', + ' const header = typeof manager.getHeader === "function" ? manager.getHeader() : undefined', + ' return { manager, id, file, parent: typeof header?.parentSession === "string" ? header.parentSession : undefined }', + ' }', + '', + ' function isNestedTaskTranscript(parentFile, candidateFile): boolean {', + ' if (typeof parentFile !== "string" || typeof candidateFile !== "string") return false', + ' const root = parentFile.endsWith(".jsonl") ? parentFile.slice(0, -6) : parentFile', + ' return candidateFile.startsWith(`${root}/`) && candidateFile !== parentFile', + ' }', + '', ' function ownsSessionStatus(ctx): boolean {', ' if (!isOmpRuntime()) return true', + ' // Newer OMP builds may expose this computed runtime provenance directly.', ' if (ctx?.agentKind === "sub") return false', - ' const manager = ctx?.sessionManager', - " if (!manager || typeof manager !== 'object') return true", + ' const current = sessionProvenance(ctx)', + ' if (!current) return true', ' // Keep ownership through module reload and shutdown while child sessions drain.', " const key = Symbol.for('orca.omp.status-session-owners')", ' let owners = Reflect.get(globalThis, key)', @@ -16,8 +32,13 @@ export function getOmpSessionOwnerHandlerSourceLines(): string[] { ' }', ' const pane = JSON.stringify([process.env.ORCA_PANE_KEY, process.env.ORCA_AGENT_LAUNCH_TOKEN])', ' const owner = owners.get(pane)', - ' if (owner) return owner === manager', - ' owners.set(pane, manager)', + ' if (owner) {', + ' if (owner.manager === current.manager) return true', + ' if (current.parent === owner.file || current.parent === owner.id) return false', + ' if (isNestedTaskTranscript(owner.file, current.file)) return false', + ' return false', + ' }', + ' owners.set(pane, current)', ' return true', ' }', '', diff --git a/src/main/pi/omp-session-status-owner.test.ts b/src/main/pi/omp-session-status-owner.test.ts index b8bd0e9142a..2759eed988b 100644 --- a/src/main/pi/omp-session-status-owner.test.ts +++ b/src/main/pi/omp-session-status-owner.test.ts @@ -89,6 +89,32 @@ describe('OMP session status ownership', () => { await settle() expect(JSON.parse(harness.fetchMock.mock.calls[0][1].body).payload.session_id).toBe('separate') }) + + it('uses OMP parent metadata and nested task paths when the root already owns the pane', async () => { + const harness = createAgentStatusExtensionHarness({ kind: 'omp' }) + const rootFile = '/sessions/root.jsonl' + const root = { + sessionManager: { + getSessionId: () => 'root', + getSessionFile: () => rootFile, + getHeader: () => ({ parentSession: undefined }) + } + } + await harness.callHook('session_start', {}, root) + await settle() + harness.fetchMock.mockClear() + harness.reload() + const child = { + sessionManager: { + getSessionId: () => 'child', + getSessionFile: () => '/sessions/root/child.jsonl', + getHeader: () => ({ parentSession: rootFile }) + } + } + await harness.callHook('agent_start', {}, child) + await settle() + expect(harness.fetchMock).not.toHaveBeenCalled() + }) it('keeps reporting for legacy callbacks without a session manager', async () => { const harness = createAgentStatusExtensionHarness({ kind: 'omp' }) await harness.callHook('agent_start') @@ -134,7 +160,6 @@ describe('OMP runtime session provenance', () => { it('allows a former child transcript resumed as the runtime main session', async () => { const harness = createAgentStatusExtensionHarness({ kind: 'omp' }) const root = { - agentKind: 'main', hasUI: false, sessionManager: { getSessionId: () => 'resumed-child',