Files
ycode/scripts/omp-protocol-probe.ts
T

711 lines
32 KiB
TypeScript

import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { createServer, type IncomingMessage } from 'node:http'
import { createInterface } from 'node:readline'
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { homedir, tmpdir } from 'node:os'
import { join, parse, relative, resolve, sep } from 'node:path'
import { randomUUID } from 'node:crypto'
interface Frame {
type?: string
id?: string
command?: string
success?: boolean
data?: unknown
error?: { message?: string }
[key: string]: unknown
}
interface ProbeRun {
child: ChildProcessWithoutNullStreams
frames: Frame[]
rawLines: string[]
stderrLines: string[]
}
interface MockRequest {
model?: string
stream: boolean
roles: string[]
toolNames: string[]
toolProperties: Record<string, string[]>
containsImage: boolean
messageShape: Array<Record<string, unknown>>
}
const STARTUP_WAIT_MS = 12_000
const COMMAND_WAIT_MS = 20_000
const SHUTDOWN_WAIT_MS = 1_500
const PROVIDER_ID = 'loopback-probe'
const MODEL_ID = 'probe-model'
const MODEL = `${PROVIDER_ID}/${MODEL_ID}`
function delay(ms: number): Promise<void> {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms))
}
function makeIsolatedEnvironment(home: string, tempRoot: string, profile: string): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {}
for (const name of ['PATH', 'SystemRoot', 'SystemDrive', 'ComSpec', 'WINDIR']) {
const value = process.env[name]
if (value) env[name] = value
}
const root = parse(home).root
env.USERPROFILE = home
env.HOME = home
env.HOMEDRIVE = root.replace(/[\\/]$/, '')
env.HOMEPATH = relative(root, home).replaceAll('/', '\\')
env.APPDATA = join(home, 'AppData', 'Roaming')
env.LOCALAPPDATA = join(home, 'AppData', 'Local')
env.TEMP = tempRoot
env.TMP = tempRoot
env.OMP_PROFILE = profile
return env
}
function summarizeFrame(frame: Frame): Record<string, unknown> {
const data = frame.data
const dataKeys = data && typeof data === 'object' ? Object.keys(data as object) : []
const event = frame.assistantMessageEvent
const eventKeys = event && typeof event === 'object' ? Object.keys(event as object) : []
return {
type: frame.type,
frameKeys: Object.keys(frame),
id: frame.id,
command: frame.command,
success: frame.success,
status: frame.status,
assistantEventType: event && typeof event === 'object' ? (event as Record<string, unknown>).type : undefined,
toolCallId: frame.toolCallId,
toolName: frame.toolName,
argumentKeys: frame.arguments && typeof frame.arguments === 'object' ? Object.keys(frame.arguments as object) : [],
eventKeys,
dataKeys,
error: typeof frame.error === 'string' ? frame.error : frame.error?.message
}
}
async function waitForFrame(
run: ProbeRun,
predicate: (frame: Frame) => boolean,
timeoutMs = COMMAND_WAIT_MS,
startIndex = 0
): Promise<Frame | null> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const frame = run.frames.slice(startIndex).find(predicate)
if (frame) return frame
if (run.child.exitCode !== null || run.child.signalCode !== null) break
await delay(20)
}
return run.frames.slice(startIndex).find(predicate) ?? null
}
async function sendCommand(run: ProbeRun, id: string, command: Record<string, unknown>): Promise<Frame | null> {
run.child.stdin.write(`${JSON.stringify({ id, ...command })}\n`)
return waitForFrame(run, (frame) => frame.id === id)
}
async function sendLooseCommand(run: ProbeRun, id: string, command: Record<string, unknown>): Promise<Frame | null> {
const startIndex = run.frames.length
run.child.stdin.write(`${JSON.stringify({ id, ...command })}\n`)
return waitForFrame(run, (frame) => frame.id === id || frame.type === 'response', 3_000, startIndex)
}
async function installProbeHostTool(run: ProbeRun, suffix: string): Promise<Frame | null> {
return sendCommand(run, `probe-host-tools-${suffix}`, {
type: 'set_host_tools',
tools: [{
name: 'read',
label: 'Read local probe file',
description: 'Read a file from the isolated protocol probe directory.',
parameters: {
type: 'object',
properties: { path: { type: 'string' } },
required: ['path'],
additionalProperties: false
}
}]
})
}
async function stopRun(run: ProbeRun): Promise<void> {
try {
run.child.stdin.end()
} catch {
// The sidecar may already have exited.
}
await new Promise<void>((resolveExit) => {
if (run.child.exitCode !== null || run.child.signalCode !== null) {
resolveExit()
return
}
let settled = false
const finish = (): void => {
if (settled) return
settled = true
clearTimeout(timer)
resolveExit()
}
const timer = setTimeout(finish, SHUTDOWN_WAIT_MS)
run.child.once('exit', finish)
})
if (run.child.exitCode === null && run.child.signalCode === null) {
run.child.kill()
await new Promise<void>((resolveExit) => {
if (run.child.exitCode !== null || run.child.signalCode !== null) resolveExit()
else run.child.once('exit', () => resolveExit())
})
}
}
async function startRun(
exePath: string,
tempRoot: string,
profile: string,
home: string,
sessionDir: string,
sessionPath: string
): Promise<ProbeRun> {
const args = [
'--mode', 'rpc',
'--profile', profile,
'--cwd', tempRoot,
`--model=${MODEL}`,
'--no-tools',
`--session-dir=${sessionDir}`,
`--resume=${sessionPath}`,
'--no-ui',
'--no-title',
'--no-extensions',
'--no-skills',
'--no-rules',
'--no-lsp'
]
const child = spawn(exePath, args, {
cwd: tempRoot,
env: makeIsolatedEnvironment(home, tempRoot, profile),
windowsHide: true,
stdio: ['pipe', 'pipe', 'pipe']
})
const run: ProbeRun = { child, frames: [], rawLines: [], stderrLines: [] }
const stdout = createInterface({ input: child.stdout })
const stderr = createInterface({ input: child.stderr })
stdout.on('line', (line) => {
run.rawLines.push(line)
try {
run.frames.push(JSON.parse(line) as Frame)
} catch {
// Keep non-JSON stdout separate from RPC frames.
}
})
stderr.on('line', (line) => run.stderrLines.push(line))
const ready = await waitForFrame(run, (frame) => frame.type === 'ready', STARTUP_WAIT_MS)
if (!ready) {
const stderrText = run.stderrLines.join('\n').slice(0, 1000)
await stopRun(run)
throw new Error(`OMP did not send ready; exit=${child.exitCode}; stderr=${stderrText}`)
}
return run
}
async function readRequest(request: IncomingMessage): Promise<Record<string, unknown>> {
const chunks: Buffer[] = []
for await (const chunk of request) chunks.push(Buffer.from(chunk))
try {
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>
} catch {
return {}
}
}
function toolNamesFrom(body: Record<string, unknown>): string[] {
if (!Array.isArray(body.tools)) return []
return body.tools.flatMap((tool) => {
if (!tool || typeof tool !== 'object') return []
const item = tool as Record<string, unknown>
const fn = item.function && typeof item.function === 'object' ? item.function as Record<string, unknown> : null
const name = fn?.name ?? item.name
return typeof name === 'string' ? [name] : []
})
}
function requestHasImage(body: Record<string, unknown>): boolean {
if (!Array.isArray(body.messages)) return false
return body.messages.some((message) => {
if (!message || typeof message !== 'object') return false
const content = (message as Record<string, unknown>).content
return Array.isArray(content) && content.some((part) => {
if (!part || typeof part !== 'object') return false
const item = part as Record<string, unknown>
return item.type === 'image_url' || item.type === 'image'
})
})
}
async function startMockProvider(probeFile: string): Promise<{
server: ReturnType<typeof createServer>
url: string
requests: MockRequest[]
toolCallSent: boolean
}> {
const requests: MockRequest[] = []
let toolCallSent = false
const server = createServer(async (request, response) => {
const body = await readRequest(request)
const names = toolNamesFrom(body)
const messages = Array.isArray(body.messages) ? body.messages as Array<Record<string, unknown>> : []
const latestUser = [...messages].reverse().find((message) => message.role === 'user')
const latestUserContent = Array.isArray(latestUser?.content)
? (latestUser.content as Array<Record<string, unknown>>).map((block) => block.text).filter((value) => typeof value === 'string').join('\n')
: typeof latestUser?.content === 'string' ? latestUser.content : ''
const toolProperties: Record<string, string[]> = {}
for (const tool of Array.isArray(body.tools) ? body.tools : []) {
if (!tool || typeof tool !== 'object') continue
const item = tool as Record<string, unknown>
const fn = item.function && typeof item.function === 'object' ? item.function as Record<string, unknown> : null
const name = fn?.name ?? item.name
const parameters = fn?.parameters && typeof fn.parameters === 'object' ? fn.parameters as Record<string, unknown> : null
const properties = parameters?.properties && typeof parameters.properties === 'object'
? parameters.properties as Record<string, unknown> : null
if (typeof name === 'string') toolProperties[name] = Object.keys(properties ?? {})
}
requests.push({
model: typeof body.model === 'string' ? body.model : undefined,
stream: body.stream === true,
roles: messages.map((message) => typeof message.role === 'string' ? message.role : '?'),
toolNames: names,
toolProperties,
containsImage: requestHasImage(body),
messageShape: messages.map((message) => {
const content = message.content
const blocks = Array.isArray(content) ? content as Array<Record<string, unknown>> : []
const calls = Array.isArray(message.tool_calls) ? message.tool_calls as Array<Record<string, unknown>> : []
return {
role: message.role,
contentType: typeof content === 'string' ? 'string' : Array.isArray(content) ? 'array' : typeof content,
contentBlockTypes: blocks.map((block) => block.type),
toolCallCount: calls.length,
hasToolCallId: typeof message.tool_call_id === 'string'
}
})
})
const readToolName = names.find((name) => name === 'read' || name.endsWith('__read'))
const sendReadTool = latestUserContent.includes('TOOL_PROBE') && readToolName && !toolCallSent
const completion = sendReadTool
? {
message: {
role: 'assistant',
content: '',
tool_calls: [{
id: 'call_local_read_1',
type: 'function',
function: { name: readToolName, arguments: JSON.stringify({ path: probeFile }) }
}]
},
finish_reason: 'tool_calls'
}
: {
message: { role: 'assistant', content: 'PROBE_OK' },
finish_reason: 'stop'
}
if (sendReadTool) toolCallSent = true
if (body.stream === true) {
response.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive'
})
const completionId = `chatcmpl-local-${requests.length}`
const writeChunk = (delta: Record<string, unknown>, finishReason: string | null = null): void => {
response.write(`data: ${JSON.stringify({
id: completionId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: MODEL_ID,
choices: [{ index: 0, delta, finish_reason: finishReason }]
})}\n\n`)
}
writeChunk({ role: 'assistant' })
if (sendReadTool) {
writeChunk({ tool_calls: [{
index: 0,
id: 'call_local_read_1',
type: 'function',
function: { name: readToolName, arguments: JSON.stringify({ path: probeFile }) }
}] })
writeChunk({}, 'tool_calls')
} else {
writeChunk({ reasoning_content: 'LOCAL_THINKING_PROBE' })
writeChunk({ content: 'PROBE_OK' })
writeChunk({}, 'stop')
}
response.write(`data: ${JSON.stringify({
id: completionId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: MODEL_ID,
choices: [],
usage: { prompt_tokens: 17, completion_tokens: 4, total_tokens: 21 }
})}\n\n`)
response.end('data: [DONE]\n\n')
return
}
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({
id: `chatcmpl-local-${requests.length}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: MODEL_ID,
choices: [{ index: 0, ...completion }],
usage: { prompt_tokens: 17, completion_tokens: 4, total_tokens: 21 }
}))
})
await new Promise<void>((resolveListen, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
server.off('error', reject)
resolveListen()
})
})
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Loopback mock did not bind to a TCP port')
return { server, url: `http://127.0.0.1:${address.port}/v1`, requests, get toolCallSent() { return toolCallSent } }
}
function summarizeMessages(data: unknown): Record<string, unknown> {
const root = data && typeof data === 'object' ? data as Record<string, unknown> : {}
const possibleArrays = [root.messages, root.entries, root.branchMessages]
const records = possibleArrays.find(Array.isArray) as unknown[] | undefined
const items = (records ?? (Array.isArray(data) ? data : [])).map((value) => {
if (!value || typeof value !== 'object') return { valueType: typeof value }
const item = value as Record<string, unknown>
const message = item.message && typeof item.message === 'object' ? item.message as Record<string, unknown> : item
const content = Array.isArray(message.content) ? message.content as Array<Record<string, unknown>> : []
const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls as Array<Record<string, unknown>>
: Array.isArray(message.tool_calls) ? message.tool_calls as Array<Record<string, unknown>> : []
return {
itemKeys: Object.keys(item),
messageKeys: Object.keys(message),
type: item.type,
role: message.role,
entryId: item.entryId ?? item.id,
parentId: item.parentId,
textBlocks: content.filter((block) => block.type === 'text').length,
toolCalls: toolCalls.map((call) => call.name ?? (call.function as Record<string, unknown> | undefined)?.name)
}
})
return { keys: Object.keys(root), count: items.length, messages: items }
}
async function inspectTranscript(path: string): Promise<Record<string, unknown>> {
try {
const text = await readFile(path, 'utf8')
const records = text.split(/\r?\n/).filter(Boolean).map((record) => JSON.parse(record) as Record<string, unknown>)
return {
exists: true,
records: records.length,
entries: records.map((record) => ({
type: record.type,
id: record.id,
parentId: record.parentId,
messageRole: (record.message as Record<string, unknown> | undefined)?.role,
toolName: (record.toolName ?? (record.message as Record<string, unknown> | undefined)?.toolName) ?? undefined
}))
}
} catch {
return { exists: false }
}
}
function summarizeTree(value: unknown): unknown {
if (Array.isArray(value)) return value.map(summarizeTree)
if (!value || typeof value !== 'object') return value
const node = value as Record<string, unknown>
const entry = node.entry && typeof node.entry === 'object' ? node.entry as Record<string, unknown> : node
const message = entry.message && typeof entry.message === 'object' ? entry.message as Record<string, unknown> : null
return {
nodeKeys: Object.keys(node),
entryKeys: Object.keys(entry),
id: entry.id,
parentId: entry.parentId,
type: entry.type,
role: message?.role,
label: node.label,
children: Array.isArray(node.children) ? summarizeTree(node.children) : []
}
}
function findUserNode(value: unknown): { id: string; parentId: string | null } | null {
if (Array.isArray(value)) {
let latest: { id: string; parentId: string | null } | null = null
for (const child of value) {
const found = findUserNode(child)
if (found) latest = found
}
return latest
}
if (!value || typeof value !== 'object') return null
const node = value as Record<string, unknown>
const entry = node.entry && typeof node.entry === 'object' ? node.entry as Record<string, unknown> : node
const message = entry.message && typeof entry.message === 'object' ? entry.message as Record<string, unknown> : null
let latest: { id: string; parentId: string | null } | null = null
if (entry.type === 'message' && message?.role === 'user' && typeof entry.id === 'string') {
latest = { id: entry.id, parentId: typeof entry.parentId === 'string' ? entry.parentId : null }
}
const child = Array.isArray(node.children) ? findUserNode(node.children) : null
return child ?? latest
}
async function main(): Promise<void> {
const exePath = process.env.YCODE_OMP_PATH ?? 'C:\\Users\\Administrator\\AppData\\Local\\Ygent\\omp.exe'
if (!existsSync(exePath)) throw new Error(`OMP executable not found: ${exePath}`)
const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-omp-protocol-'))
const home = join(tempRoot, 'isolated-home')
const sessionDir = join(tempRoot, 'sessions')
const profile = `protocol-probe-${randomUUID().replaceAll('-', '')}`
const sessionPath = join(sessionDir, 'probe.jsonl')
const probeFile = join(tempRoot, 'probe.txt')
const realProfilePath = resolve(homedir(), '.omp', 'profiles', profile)
const realProfileRoot = resolve(homedir(), '.omp', 'profiles')
const realProfileRelative = relative(realProfileRoot, realProfilePath)
if (!realProfileRelative || realProfileRelative.startsWith(`..${sep}`) || resolve(realProfileRoot, realProfileRelative) !== realProfilePath) {
throw new Error('Refusing an invalid isolated profile path')
}
if (existsSync(realProfilePath)) throw new Error('The unique probe profile unexpectedly exists')
await mkdir(home, { recursive: true })
await mkdir(sessionDir, { recursive: true })
await writeFile(probeFile, 'LOCAL_OMP_TOOL_SENTINEL\n', 'utf8')
const profileAgentDir = join(home, '.omp', 'profiles', profile, 'agent')
await mkdir(profileAgentDir, { recursive: true })
const mock = await startMockProvider(probeFile)
await writeFile(join(profileAgentDir, 'models.yml'), [
'providers:',
` ${PROVIDER_ID}:`,
` baseUrl: ${mock.url}`,
' api: openai-completions',
' auth: none',
' models:',
` - id: ${MODEL_ID}`,
' name: Local protocol probe',
' reasoning: true',
' input: [text, image]',
' contextWindow: 64000',
' maxTokens: 4096',
''
].join('\n'), 'utf8')
const runs: ProbeRun[] = []
try {
const version = spawnSync(exePath, ['--version'], {
encoding: 'utf8',
windowsHide: true,
env: makeIsolatedEnvironment(home, tempRoot, profile)
})
if (version.error) throw version.error
if (version.status !== 0) throw new Error(`OMP --version failed with exit ${version.status}`)
console.log(`OMP_VERSION: ${version.stdout.trim()}`)
console.log(`ISOLATED_PROFILE: ${profile}`)
console.log('PROVIDER_NETWORK: loopback only')
const first = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
runs.push(first)
const hostToolsFirst = await installProbeHostTool(first, '1')
const stateResponse = await sendCommand(first, 'probe-state-1', { type: 'get_state' })
console.log(`FIRST_READY: true`)
console.log(`SET_HOST_TOOLS: ${JSON.stringify(hostToolsFirst ? summarizeFrame(hostToolsFirst) : null)}`)
console.log(`GET_STATE: ${JSON.stringify(stateResponse ? summarizeFrame(stateResponse) : null)}`)
const image = {
type: 'image' as const,
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/dXEAAAAASUVORK5CYII=',
mimeType: 'image/png'
}
first.child.stdin.write(`${JSON.stringify({
id: 'probe-prompt-1',
type: 'prompt',
message: 'Reply exactly PROBE_OK.',
images: [image]
})}\n`)
const promptAck = await waitForFrame(first, (frame) => frame.id === 'probe-prompt-1')
const turnDone = await waitForFrame(first, (frame) =>
frame.type === 'agent_end' || (frame.type === 'prompt_result' && frame.agentInvoked === false), 60_000)
const toolTurnStart = first.frames.length
first.child.stdin.write(`${JSON.stringify({
id: 'probe-prompt-tool',
type: 'prompt',
message: 'TOOL_PROBE: call the read tool for probe.txt, then reply exactly PROBE_OK.'
})}\n`)
const toolPromptAck = await waitForFrame(first, (frame) => frame.id === 'probe-prompt-tool')
const hostToolCall = await waitForFrame(first, (frame) => frame.type === 'host_tool_call', 20_000, toolTurnStart)
let hostToolResponseSent = false
if (hostToolCall && typeof hostToolCall.id === 'string') {
const args = hostToolCall.arguments && typeof hostToolCall.arguments === 'object'
? hostToolCall.arguments as Record<string, unknown> : {}
const targetPath = typeof args.path === 'string' ? args.path : probeFile
const content = await readFile(targetPath, 'utf8')
first.child.stdin.write(`${JSON.stringify({
type: 'host_tool_result',
id: hostToolCall.id,
result: { content: [{ type: 'text', text: content }] }
})}\n`)
hostToolResponseSent = true
}
const toolTurnDone = await waitForFrame(first, (frame) =>
frame.type === 'agent_end' || (frame.type === 'prompt_result' && frame.agentInvoked === false), 60_000, toolTurnStart)
const getMessages = await sendCommand(first, 'probe-messages-1', { type: 'get_messages' })
const tree = await sendCommand(first, 'probe-tree-1', { type: 'get_tree' })
const branchMessages = await sendCommand(first, 'probe-branch-messages-1', { type: 'get_branch_messages' })
const stateAfterTurn = await sendCommand(first, 'probe-state-after-turn', { type: 'get_state' })
const entries = getMessages ? summarizeMessages(getMessages.data) : null
const treeData = tree?.data && typeof tree.data === 'object' ? tree.data as Record<string, unknown> : {}
const treeShape = summarizeTree(treeData.tree)
const firstUserNode = findUserNode(treeData.tree)
const fork = firstUserNode
? await sendCommand(first, 'probe-fork-before-branch', { type: 'fork', entryId: firstUserNode.id })
: null
const stateAfterFork = await sendCommand(first, 'probe-state-after-fork', { type: 'get_state' })
const forkStateData = stateAfterFork?.data && typeof stateAfterFork.data === 'object'
? stateAfterFork.data as Record<string, unknown> : {}
const forkSessionPath = typeof forkStateData.sessionFile === 'string' ? forkStateData.sessionFile : ''
const relativeForkSession = forkSessionPath ? relative(tempRoot, resolve(forkSessionPath)) : ''
const forkSessionInProbe = !!relativeForkSession && relativeForkSession !== '..' &&
!relativeForkSession.startsWith(`..${sep}`) && !relativeForkSession.startsWith(sep)
const messagesAfterFork = await sendCommand(first, 'probe-messages-after-fork', { type: 'get_messages' })
const treeAfterFork = await sendCommand(first, 'probe-tree-after-fork', { type: 'get_tree' })
const treeAfterForkData = treeAfterFork?.data && typeof treeAfterFork.data === 'object'
? treeAfterFork.data as Record<string, unknown> : {}
const forkData = fork?.data && typeof fork.data === 'object' ? fork.data as Record<string, unknown> : {}
const stateBeforeForkData = stateAfterTurn?.data && typeof stateAfterTurn.data === 'object'
? stateAfterTurn.data as Record<string, unknown> : {}
console.log(`PROMPT_ACK: ${JSON.stringify(promptAck ? summarizeFrame(promptAck) : null)}`)
console.log(`FIRST_TURN_END_FRAME: ${JSON.stringify(turnDone ? summarizeFrame(turnDone) : null)}`)
console.log(`TOOL_PROMPT_ACK: ${JSON.stringify(toolPromptAck ? summarizeFrame(toolPromptAck) : null)}`)
console.log(`HOST_TOOL_CALL: ${JSON.stringify(hostToolCall ? summarizeFrame(hostToolCall) : null)}`)
console.log(`HOST_TOOL_RESPONSE_SENT: ${hostToolResponseSent}`)
console.log(`TOOL_TURN_END_FRAME: ${JSON.stringify(toolTurnDone ? summarizeFrame(toolTurnDone) : null)}`)
console.log(`TURN_EVENTS: ${JSON.stringify(first.frames.filter((frame) => [
'agent_start', 'message_start', 'message_update', 'message_end', 'tool_execution_start',
'tool_execution_update', 'tool_execution_end', 'host_tool_call', 'turn_start', 'turn_end', 'agent_end', 'prompt_result'
].includes(frame.type ?? '')).map(summarizeFrame))}`)
console.log(`GET_MESSAGES_RESPONSE: ${JSON.stringify(getMessages ? {
...summarizeFrame(getMessages),
dataSummary: entries
} : null)}`)
console.log(`GET_TREE_RESPONSE: ${JSON.stringify(tree ? {
...summarizeFrame(tree), leafId: treeData.leafId, tree: treeShape
} : null)}`)
console.log(`GET_BRANCH_MESSAGES_RESPONSE: ${JSON.stringify(branchMessages ? {
...summarizeFrame(branchMessages), dataSummary: summarizeMessages(branchMessages.data)
} : null)}`)
console.log(`FORK_ENTRY_ID: ${JSON.stringify(firstUserNode?.id ?? null)}`)
console.log(`FORK_RESPONSE: ${JSON.stringify(fork ? summarizeFrame(fork) : null)}`)
console.log(`FORK_CANCELLED: ${forkData.cancelled}`)
console.log(`FORK_STATE: ${JSON.stringify(stateAfterFork ? summarizeFrame(stateAfterFork) : null)}`)
console.log(`FORK_SESSION_ID_CHANGED: ${stateBeforeForkData.sessionId !== forkStateData.sessionId}`)
console.log(`FORK_SESSION_FILE_IS_PROBE_PATH: ${forkSessionInProbe}`)
console.log(`FORK_SESSION_FILE_EXISTS: ${forkSessionInProbe ? existsSync(forkSessionPath) : false}`)
console.log(`FORK_MESSAGES: ${JSON.stringify(messagesAfterFork ? summarizeMessages(messagesAfterFork.data) : null)}`)
console.log(`FORK_TREE_LEAF_ID: ${treeAfterForkData.leafId ?? null}`)
console.log(`TRANSCRIPT_AFTER_FORK: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
console.log(`STATE_AFTER_TURN: ${JSON.stringify(stateAfterTurn ? summarizeFrame(stateAfterTurn) : null)}`)
console.log(`LOOPBACK_REQUESTS: ${JSON.stringify(mock.requests)}`)
console.log(`TOOL_CALL_SENT: ${mock.toolCallSent}`)
console.log(`TRANSCRIPT_AFTER_TURN: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
await stopRun(first)
const second = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
runs.push(second)
const hostToolsSecond = await installProbeHostTool(second, '2')
const resumedState = await sendCommand(second, 'probe-state-2', { type: 'get_state' })
const resumedMessages = await sendCommand(second, 'probe-messages-2', { type: 'get_messages' })
console.log(`RESUME_READY: true`)
console.log(`RESUME_SET_HOST_TOOLS: ${JSON.stringify(hostToolsSecond ? summarizeFrame(hostToolsSecond) : null)}`)
console.log(`RESUME_GET_STATE: ${JSON.stringify(resumedState ? summarizeFrame(resumedState) : null)}`)
console.log(`RESUME_GET_MESSAGES: ${JSON.stringify(resumedMessages ? {
...summarizeFrame(resumedMessages),
dataSummary: summarizeMessages(resumedMessages.data)
} : null)}`)
console.log(`RESUMED_TRANSCRIPT: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
let currentRun: ProbeRun = second
const resumeTree = await sendCommand(second, 'probe-tree-2', { type: 'get_tree' })
const resumeTreeData = resumeTree?.data && typeof resumeTree.data === 'object' ? resumeTree.data as Record<string, unknown> : {}
const resumeUserNode = findUserNode(resumeTreeData.tree)
console.log(`RESUME_TREE: ${JSON.stringify(resumeTree ? {
...summarizeFrame(resumeTree), leafId: resumeTreeData.leafId, tree: summarizeTree(resumeTreeData.tree)
} : null)}`)
if (resumeUserNode) {
const branch = await sendCommand(second, 'probe-branch-1', { type: 'branch', entryId: resumeUserNode.id })
const branchState = await sendCommand(second, 'probe-state-after-branch', { type: 'get_state' })
const branchHistory = await sendCommand(second, 'probe-messages-after-branch', { type: 'get_messages' })
const branchTree = await sendCommand(second, 'probe-tree-after-branch', { type: 'get_tree' })
const branchTreeData = branchTree?.data && typeof branchTree.data === 'object' ? branchTree.data as Record<string, unknown> : {}
console.log(`BRANCH_ENTRY_ID: ${JSON.stringify(resumeUserNode.id)}`)
console.log(`BRANCH_RESPONSE: ${JSON.stringify(branch ? summarizeFrame(branch) : null)}`)
console.log(`BRANCH_STATE: ${JSON.stringify(branchState ? summarizeFrame(branchState) : null)}`)
console.log(`BRANCH_MESSAGES: ${JSON.stringify(branchHistory ? {
...summarizeFrame(branchHistory),
dataSummary: summarizeMessages(branchHistory.data)
} : null)}`)
console.log(`BRANCH_TREE: ${JSON.stringify(branchTree ? {
...summarizeFrame(branchTree), leafId: branchTreeData.leafId, tree: summarizeTree(branchTreeData.tree)
} : null)}`)
if (branch?.success === true) {
await stopRun(second)
const branchResume = await startRun(exePath, tempRoot, profile, home, sessionDir, sessionPath)
runs.push(branchResume)
currentRun = branchResume
const resumeHostTools = await installProbeHostTool(branchResume, 'after-branch')
const messagesAfterBranchResume = await sendCommand(branchResume, 'probe-messages-after-branch-resume', { type: 'get_messages' })
const treeAfterBranchResume = await sendCommand(branchResume, 'probe-tree-after-branch-resume', { type: 'get_tree' })
const treeAfterBranchResumeData = treeAfterBranchResume?.data && typeof treeAfterBranchResume.data === 'object'
? treeAfterBranchResume.data as Record<string, unknown> : {}
console.log(`BRANCH_RESUME_READY: true`)
console.log(`BRANCH_RESUME_SET_HOST_TOOLS: ${JSON.stringify(resumeHostTools ? summarizeFrame(resumeHostTools) : null)}`)
console.log(`BRANCH_RESUME_MESSAGES: ${JSON.stringify(messagesAfterBranchResume ? summarizeMessages(messagesAfterBranchResume.data) : null)}`)
console.log(`BRANCH_RESUME_TREE_LEAF_ID: ${treeAfterBranchResumeData.leafId ?? null}`)
console.log(`TRANSCRIPT_AFTER_BRANCH_WITHOUT_PROMPT: ${JSON.stringify(await inspectTranscript(sessionPath))}`)
}
const navigateTree = await sendLooseCommand(currentRun, 'probe-navigate-tree', {
type: 'navigate_tree', entryId: resumeUserNode.id
})
const navigateToEntry = await sendLooseCommand(currentRun, 'probe-navigate-to-entry', {
type: 'navigate_to_entry', entryId: resumeUserNode.id
})
const switchSession = await sendCommand(currentRun, 'probe-switch-session', {
type: 'switch_session', sessionPath
})
const clear = await sendLooseCommand(currentRun, 'probe-clear', { type: 'clear' })
console.log(`NAVIGATE_TREE_RESPONSE: ${JSON.stringify(navigateTree ? summarizeFrame(navigateTree) : null)}`)
console.log(`NAVIGATE_TO_ENTRY_RESPONSE: ${JSON.stringify(navigateToEntry ? summarizeFrame(navigateToEntry) : null)}`)
console.log(`SWITCH_SESSION_SAME_PATH: ${JSON.stringify(switchSession ? summarizeFrame(switchSession) : null)}`)
console.log(`CLEAR_RESPONSE: ${JSON.stringify(clear ? summarizeFrame(clear) : null)}`)
} else {
console.log('BRANCH_ENTRY_ID: unavailable from get_tree parent relation')
}
await stopRun(currentRun)
} finally {
for (const run of runs) await stopRun(run)
await new Promise<void>((resolveClose) => mock.server.close(() => resolveClose()))
await rm(tempRoot, { recursive: true, force: true })
if (existsSync(realProfilePath)) await rm(realProfilePath, { recursive: true, force: true })
}
}
main().catch((error: unknown) => {
console.error('OMP protocol probe failed:', error)
process.exitCode = 1
})