fix(omp): preserve child status ownership provenance

This commit is contained in:
Neil
2026-09-19 00:21:40 -07:00
parent 81673f5c35
commit 13bde98bca
3 changed files with 61 additions and 14 deletions
@@ -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
+25 -4
View File
@@ -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',
' }',
'',
+26 -1
View File
@@ -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',