fix(omp): publish owning session transcript paths

Adopt stablyai/orca#19529 on the child-session ownership fence. Preserve id-based resume and execution-host transcript boundaries.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
Neil
2026-09-19 00:42:03 -07:00
co-authored by Brennan Benson
parent 57fdf68ab3
commit 7d8fb599f9
8 changed files with 292 additions and 46 deletions
@@ -87,32 +87,40 @@ describe('resolveSessionFilePath on a Windows host with WSL', () => {
expect(resolved).toBe(ROLLOUT_UNC)
})
it('keeps an attested distro when another guest has the same transcript path', async () => {
READABLE_WSL_UNC_PATHS.add(DEBIAN_ROLLOUT_UNC)
it.each(['codex', 'omp'] as const)(
'keeps an attested distro when another guest has the same transcript path (%s)',
async (agent) => {
READABLE_WSL_UNC_PATHS.add(DEBIAN_ROLLOUT_UNC)
const resolved = await resolveSessionFilePath('codex', 'wsl-sess', {
transcriptPath: ROLLOUT_LINUX,
wslDistro: 'Ubuntu',
codexSessionsDirs: []
})
expect(resolved).toBe(ROLLOUT_UNC)
expect(vi.mocked(listWslDistrosAsync)).not.toHaveBeenCalled()
expect(vi.mocked(getWslHomeAsync)).not.toHaveBeenCalled()
})
it('does not fall through to another guest when the attested path is missing', async () => {
READABLE_WSL_UNC_PATHS.delete(ROLLOUT_UNC)
READABLE_WSL_UNC_PATHS.add(DEBIAN_ROLLOUT_UNC)
await expect(
resolveSessionFilePath('codex', 'wsl-sess', {
const resolved = await resolveSessionFilePath(agent, 'wsl-sess', {
transcriptPath: ROLLOUT_LINUX,
wslDistro: 'Ubuntu',
codexSessionsDirs: []
})
).resolves.toBeNull()
})
expect(resolved).toBe(ROLLOUT_UNC)
expect(vi.mocked(listWslDistrosAsync)).not.toHaveBeenCalled()
expect(vi.mocked(getWslHomeAsync)).not.toHaveBeenCalled()
}
)
it.each(['codex', 'omp'] as const)(
'does not fall through to another guest when the attested path is missing (%s)',
async (agent) => {
READABLE_WSL_UNC_PATHS.delete(ROLLOUT_UNC)
READABLE_WSL_UNC_PATHS.add(DEBIAN_ROLLOUT_UNC)
scanned.hostRootHasRollout = true
await expect(
resolveSessionFilePath(agent, 'wsl-sess', {
transcriptPath: ROLLOUT_LINUX,
wslDistro: 'Ubuntu',
codexSessionsDirs: []
})
).resolves.toBeNull()
expect(scanned.dirs).toEqual([])
}
)
it('does not return a UNC twin that no distro actually has', async () => {
const resolved = await resolveSessionFilePath('codex', 'wsl-sess', {
@@ -122,18 +130,21 @@ describe('resolveSessionFilePath on a Windows host with WSL', () => {
expect(resolved).toBeNull()
})
it('does not fall back by id from an unattested guest hook path', async () => {
READABLE_WSL_UNC_PATHS.delete(ROLLOUT_UNC)
scanned.hostRootHasRollout = true
it.each(['codex', 'omp'] as const)(
'does not fall back by id from an unattested guest hook path (%s)',
async (agent) => {
READABLE_WSL_UNC_PATHS.delete(ROLLOUT_UNC)
scanned.hostRootHasRollout = true
await expect(
resolveSessionFilePath('codex', 'wsl-sess', {
transcriptPath: ROLLOUT_LINUX,
codexSessionsDirs: ['C:\\host\\sessions']
})
).resolves.toBeNull()
expect(scanned.dirs).toEqual([])
})
await expect(
resolveSessionFilePath(agent, 'wsl-sess', {
transcriptPath: ROLLOUT_LINUX,
codexSessionsDirs: ['C:\\host\\sessions']
})
).resolves.toBeNull()
expect(scanned.dirs).toEqual([])
}
)
it('does not fall back to a host id match for an unattested guest hook path', async () => {
scanned.hostRootHasRollout = true
@@ -210,11 +210,12 @@ describe('getPiAgentStatusExtensionSource', () => {
expect(
harness.fetchMock.mock.calls.map(([_event, init]) => JSON.parse(String(init?.body)).payload)
).toEqual([
{ hook_event_name: 'agent_start', session_id: 'omp-session-8' },
{ hook_event_name: 'agent_start', session_id: 'omp-session-8', session_file: '/tmp/s' },
{
hook_event_name: 'before_agent_start',
prompt: 'hi',
session_id: 'omp-session-9'
session_id: 'omp-session-9',
session_file: '/tmp/s'
},
{ hook_event_name: 'agent_end' }
])
@@ -240,7 +241,7 @@ describe('getPiAgentStatusExtensionSource', () => {
let sessionId = 'omp-session-8'
const sessionManager = {
getSessionId: () => sessionId,
getSessionFile: () => '/tmp/session.jsonl'
getSessionFile: () => `/tmp/${sessionId}.jsonl`
}
await harness.callHook('agent_start', undefined, { sessionManager })
sessionId = 'omp-session-9'
@@ -258,9 +259,9 @@ describe('getPiAgentStatusExtensionSource', () => {
hook_event_name: 'message_end',
role: 'assistant',
text: 'done',
session_id: 'omp-session-9'
session_id: 'omp-session-9',
session_file: '/tmp/omp-session-9.jsonl'
})
expect(body.payload).not.toHaveProperty('session_file')
expect(harness.fetchMock.mock.calls[1]?.[0]).toBe('http://127.0.0.1:4321/hook/omp')
expect(harness.spawnMock).not.toHaveBeenCalled()
finishDeliveries[1]?.()
+3 -3
View File
@@ -19,7 +19,7 @@ import { getPiAgentStatusWslCurlSourceLines } from './agent-status-wsl-curl-sour
export const ORCA_PI_AGENT_STATUS_EXTENSION_FILE = 'orca-agent-status.ts'
export function getPiAgentStatusExtensionSource(kind: PiAgentKind = 'pi'): string {
// Why: OMP needs the file only to reject ephemeral sessions; disclose just its resume id.
// OMP resumes by id; its persistent file path lets native chat skip discovery.
const sessionMetadataSourceLines =
kind !== 'omp'
? [
@@ -41,7 +41,7 @@ export function getPiAgentStatusExtensionSource(kind: PiAgentKind = 'pi'): strin
' const sessionManager = (ctx as { sessionManager?: { getSessionId?: () => unknown; getSessionFile?: () => unknown } } | null)?.sessionManager',
' const sessionId = sessionManager?.getSessionId?.()',
' const sessionFile = sessionManager?.getSessionFile?.()',
" runtimeOmpSessionMetadata = typeof sessionId === 'string' && sessionId && typeof sessionFile === 'string' && sessionFile ? { session_id: sessionId } : {}",
" runtimeOmpSessionMetadata = typeof sessionId === 'string' && sessionId && typeof sessionFile === 'string' && sessionFile ? { session_id: sessionId, session_file: sessionFile } : {}",
'}',
'',
'function getPostSessionMetadata(ompRuntime: boolean): Record<string, unknown> {',
@@ -69,7 +69,7 @@ export function getPiAgentStatusExtensionSource(kind: PiAgentKind = 'pi'): strin
' const sessionManager = (ctx as { sessionManager?: { getSessionId?: () => unknown; getSessionFile?: () => unknown } } | null)?.sessionManager',
' const sessionId = sessionManager?.getSessionId?.()',
' const sessionFile = sessionManager?.getSessionFile?.()',
" sessionMetadata = typeof sessionId === 'string' && sessionId && typeof sessionFile === 'string' && sessionFile ? { session_id: sessionId } : {}",
" sessionMetadata = typeof sessionId === 'string' && sessionId && typeof sessionFile === 'string' && sessionFile ? { session_id: sessionId, session_file: sessionFile } : {}",
'}',
'',
'function updateRuntimeOmpSessionMetadata(ctx: unknown): void {',
@@ -50,6 +50,10 @@ describe('OMP session status ownership', () => {
await settle()
const bodies = harness.fetchMock.mock.calls.map((call) => JSON.parse(call[1].body))
expect(bodies.map((body) => body.payload.session_id)).toEqual(['root', 'root'])
expect(bodies.map((body) => body.payload.session_file)).toEqual([
'/root.jsonl',
'/root.jsonl'
])
expect(bodies.at(-1).payload.hook_event_name).toBe('agent_end')
}
)
+116
View File
@@ -0,0 +1,116 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { extractAgentProviderSession, getAgentResumeArgv } from '../../shared/agent-session-resume'
import type * as SessionScannerDiscovery from '../ai-vault/session-scanner-discovery'
import { walkSessionFiles } from '../ai-vault/session-scanner-discovery'
import { resolveSessionFilePath } from '../native-chat/session-file-resolver'
import { createAgentStatusExtensionHarness } from './agent-status-extension-test-harness'
vi.mock('../ai-vault/session-scanner-discovery', async (importOriginal) => {
const actual = await importOriginal<typeof SessionScannerDiscovery>()
return { ...actual, walkSessionFiles: vi.fn(actual.walkSessionFiles) }
})
let root: string
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-omp-metadata-'))
vi.mocked(walkSessionFiles).mockClear()
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
describe.each([
['dedicated OMP', { kind: 'omp' as const }],
['Pi-routed OMP', { kind: 'pi' as const, title: 'omp' }]
])('%s transcript metadata', (_name, args) => {
it('resolves custom files without scanning and retains id-based resume across switches', async () => {
const harness = createAgentStatusExtensionHarness(args)
let id = ''
let file = ''
const sessionManager = { getSessionId: () => id, getSessionFile: () => file }
for (const next of ['first', 'second']) {
id = next
file = join(root, `${id}.jsonl`)
await writeFile(file, `${JSON.stringify({ type: 'session', id })}\n`)
await harness.callHook('agent_start', undefined, {
sessionManager
})
await vi.waitFor(() =>
expect(harness.fetchMock).toHaveBeenCalledTimes(id === 'first' ? 1 : 2)
)
const payload = JSON.parse(String(harness.fetchMock.mock.lastCall?.[1]?.body)).payload
const session = extractAgentProviderSession('omp', payload)
expect(session).toEqual({ key: 'session_id', id, transcriptPath: file })
expect(getAgentResumeArgv('omp', session!)).toEqual(['omp', '--resume', id])
expect(getAgentResumeArgv('omp', session!, 'explicit.jsonl')).toEqual([
'omp',
'--resume',
'explicit.jsonl'
])
await expect(
resolveSessionFilePath('omp', id, {
transcriptPath: session!.transcriptPath,
ompSessionsDir: join(root, 'unused-default')
})
).resolves.toBe(file)
}
expect(walkSessionFiles).not.toHaveBeenCalled()
expect(harness.fsMock.existsSync).not.toHaveBeenCalled()
})
it('publishes planned persistent paths, then resolves after delayed creation', async () => {
const harness = createAgentStatusExtensionHarness(args)
const file = join(root, 'delayed.jsonl')
await harness.callHook('agent_start', undefined, {
sessionManager: { getSessionId: () => 'delayed', getSessionFile: () => file }
})
const payload = JSON.parse(String(harness.fetchMock.mock.lastCall?.[1]?.body)).payload
const session = extractAgentProviderSession('omp', payload)!
expect(session.transcriptPath).toBe(file)
const options = { transcriptPath: session.transcriptPath, ompSessionsDir: join(root, 'empty') }
await expect(resolveSessionFilePath('omp', session.id, options)).resolves.toBeNull()
expect(walkSessionFiles).toHaveBeenCalledTimes(1)
await writeFile(file, '{}\n')
vi.mocked(walkSessionFiles).mockClear()
await expect(resolveSessionFilePath('omp', session.id, options)).resolves.toBe(file)
expect(walkSessionFiles).not.toHaveBeenCalled()
const controller = new AbortController()
controller.abort()
await expect(
resolveSessionFilePath('omp', session.id, options, controller.signal)
).rejects.toThrow()
expect(walkSessionFiles).not.toHaveBeenCalled()
})
it.each([undefined, '', 123])(
'clears persistent metadata for ephemeral path %s',
async (file) => {
const harness = createAgentStatusExtensionHarness(args)
let sessionFile: unknown = join(root, 'p.jsonl')
const sessionManager = { getSessionId: () => 'same-owner', getSessionFile: () => sessionFile }
await harness.callHook('agent_start', undefined, { sessionManager })
sessionFile = file
await harness.callHook('agent_end', undefined, { sessionManager })
await vi.waitFor(() => expect(harness.fetchMock).toHaveBeenCalledTimes(2))
const payload = JSON.parse(String(harness.fetchMock.mock.lastCall?.[1]?.body)).payload
expect(payload).toEqual({ hook_event_name: 'agent_end' })
expect(extractAgentProviderSession('omp', payload)).toBeNull()
}
)
})
it('retains id-based discovery with an old extension and rejects malformed optional paths', async () => {
const id = 'legacy-session'
const file = join(root, `${id}.jsonl`)
await writeFile(file, '{}\n')
for (const session_file of [undefined, '', 123, '/bad\npath.jsonl']) {
const session = extractAgentProviderSession('omp', { session_id: id, session_file })!
expect(session).toEqual({ key: 'session_id', id })
expect(getAgentResumeArgv('omp', session)).toEqual(['omp', '--resume', id])
await expect(resolveSessionFilePath('omp', id, { ompSessionsDir: root })).resolves.toBe(file)
}
expect(walkSessionFiles).toHaveBeenCalledTimes(4)
})
+2 -2
View File
@@ -224,10 +224,10 @@ export function extractAgentProviderSession(
const id = readSessionId(payload, ['session_id', 'sessionId'])
return id ? { key: 'session_id', id } : null
}
// Why: OMP's managed extension reports the authoritative CLI resume id.
// OMP keeps id-based resume while optionally locating its native-chat transcript.
case 'omp': {
const id = readSessionId(payload, ['session_id'])
return id ? { key: 'session_id', id } : null
return id ? withTranscriptPath({ key: 'session_id', id }, payload, ['session_file']) : null
}
// Why: Copilot's hook `session_id` is also its `~/.copilot/session-state/<id>/`
// directory name, so the same id is the CLI's resume locator.
+2 -4
View File
@@ -20,10 +20,8 @@ export function isNativeChatSupportedAgent(agent: string | null | undefined): bo
return agent != null && NATIVE_CHAT_SUPPORTED_AGENTS.has(agent)
}
/** Agents whose hook discloses no transcript path (`extractAgentProviderSession`),
* so native chat can only reach the session file by scanning a sessions root on
* a disk THIS process can read. Under Model-A SSH that disk is the wrong host,
* so the chat view must stay closed instead of loading forever. */
/** Agents whose Model-A SSH transcript reader is not supported. A hook path alone
* does not establish owning-host reads, so OMP remains gated even with metadata. */
export function nativeChatRequiresLocalTranscript(agent: string | null | undefined): boolean {
const transcriptAgent = resolveNativeChatTranscriptAgent(agent)
return transcriptAgent === 'grok' || transcriptAgent === 'omp'
@@ -0,0 +1,116 @@
// Run with Bun and a read-only OMP checkout path as the first argument.
import assert from 'node:assert/strict'
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { getPiAgentStatusExtensionSource } from '../../src/main/pi/agent-status-extension-source.ts'
import {
extractAgentProviderSession,
getAgentResumeArgv
} from '../../src/shared/agent-session-resume.ts'
import { readNativeChatTranscript } from '../../src/main/native-chat/transcript-reader.ts'
const reference = process.argv[2]
assert.ok(reference, 'Pass the read-only oh-my-pi source checkout path')
const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-transcript-'))
process.env.HOME = join(scratch, 'home')
process.env.USERPROFILE = process.env.HOME
process.env.OMP_CODING_AGENT_DIR = join(scratch, 'agent')
await mkdir(process.env.HOME, { recursive: true })
const source = (path) =>
pathToFileURL(join(resolve(reference), 'packages/coding-agent/src', path)).href
const { loadExtensions } = await import(source('extensibility/extensions/loader.ts'))
const { EventBus } = await import(source('utils/event-bus.ts'))
const { SessionManager } = await import(source('session/session-manager.ts'))
const posts = []
const server = createServer(async (request, response) => {
let body = ''
for await (const chunk of request) {
body += chunk
}
posts.push(JSON.parse(body).payload)
response.writeHead(200).end()
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const managers = []
try {
process.env.ORCA_AGENT_HOOK_PORT = String(server.address().port)
process.env.ORCA_AGENT_HOOK_TOKEN = 'test-token'
delete process.env.ORCA_AGENT_HOOK_ENDPOINT
delete process.env.ORCA_PI_STATUS_OWNED
process.title = 'omp'
for (const kind of ['omp', 'pi']) {
process.env.ORCA_PANE_KEY = `transcript-${kind}`
process.env.ORCA_AGENT_LAUNCH_TOKEN = `transcript-${kind}`
const extensionPath = join(scratch, `${kind}-agent-status.ts`)
await writeFile(extensionPath, getPiAgentStatusExtensionSource(kind))
const load = async () => {
const result = await loadExtensions([extensionPath], scratch, new EventBus())
assert.deepEqual(result.errors, [])
return result.extensions[0]
}
const root = SessionManager.create(scratch, join(scratch, `${kind}-custom-sessions`))
const child = SessionManager.create(scratch, join(scratch, `${kind}-child-sessions`))
managers.push(root, child)
const emit = async (extension, type, manager) => {
for (const handler of extension.handlers.get(type) ?? []) {
await handler({ type }, { sessionManager: manager, hasUI: false })
}
await new Promise((resolve) => setTimeout(resolve, 60))
}
const extension = await load()
await emit(extension, 'session_start', root)
for (const phase of ['initial', 'new']) {
if (phase === 'new') {
await root.newSession()
}
assert.equal(root.isSessionOnDisk(), false)
await emit(extension, 'agent_start', root)
const session = extractAgentProviderSession('omp', posts.at(-1))
assert.equal(session.transcriptPath, root.getSessionFile())
assert.deepEqual(getAgentResumeArgv('omp', session), ['omp', '--resume', root.getSessionId()])
const options = {
transcriptPath: session.transcriptPath,
ompSessionsDir: join(scratch, 'unused')
}
assert.equal((await readNativeChatTranscript('omp', session.id, options)).notFound, true)
root.appendMessage({
role: 'user',
content: `Transcript proof ${kind} ${phase}`,
timestamp: Date.now()
})
await root.ensureOnDisk()
await root.flush()
const transcript = await readNativeChatTranscript('omp', session.id, options)
assert.ok('messages' in transcript, JSON.stringify(transcript))
assert.equal(transcript.messages.length, 1)
assert.ok(JSON.stringify(transcript.messages).includes(`Transcript proof ${kind} ${phase}`))
const beforeChild = posts.length
const childExtension = await load()
await emit(childExtension, 'session_start', child)
await emit(childExtension, 'agent_start', child)
await emit(childExtension, 'agent_end', child)
assert.equal(posts.length, beforeChild)
}
}
console.log(
JSON.stringify({
platform: process.platform,
producers: ['omp', 'pi'],
reports: posts.length,
proof:
'Actual OMP loader and persistent SessionManager, custom directory, lazy creation, new-session switch, HTTP metadata, Orca transcript reader, child suppression',
modelCalls: 0,
rendered: false
})
)
} finally {
for (const manager of managers) {
await manager.close()
}
server.closeAllConnections()
await new Promise((resolve) => server.close(resolve))
await rm(scratch, { recursive: true, force: true })
}