项目文件夹

文件
2026-07-13 13:05:33 +08:00

505 行
14 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from 'node:child_process'
import { createHash, randomUUID } from 'node:crypto'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
const SCRIPT_PATH = import.meta.filename
const BEGIN = '\x1b[200~'
const END = '\x1b[201~'
const DEFAULT_SIZE_KB = 80
const DEFAULT_TIMEOUT_MS = 20_000
const DEFAULT_MODE = 'codex-like'
function argValue(name, fallback = undefined) {
const prefix = `--${name}=`
const inline = process.argv.find((arg) => arg.startsWith(prefix))
if (inline) {
return inline.slice(prefix.length)
}
const index = process.argv.indexOf(`--${name}`)
if (index !== -1) {
return process.argv[index + 1] ?? fallback
}
return fallback
}
function hasFlag(name) {
return process.argv.includes(`--${name}`)
}
function parsePositiveInteger(name, fallback) {
const raw = argValue(name)
if (raw === undefined) {
return fallback
}
const value = Number(raw)
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
throw new Error(`--${name} must be a positive integer`)
}
return value
}
function shellQuote(value) {
if (process.platform === 'win32') {
return `"${String(value).replace(/"/g, '\\"')}"`
}
return `'${String(value).replace(/'/g, "'\\''")}'`
}
function runCommand(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd,
env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
})
let stdout = ''
let stderr = ''
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
child.stdout.on('data', (chunk) => {
stdout += chunk
})
child.stderr.on('data', (chunk) => {
stderr += chunk
})
child.on('error', reject)
child.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr })
return
}
const error = new Error(`${command} ${args.join(' ')} exited ${code}`)
error.stdout = stdout
error.stderr = stderr
error.code = code
reject(error)
})
})
}
function parseCliJson(output) {
const text = output.trim()
if (!text) {
throw new Error('CLI returned empty JSON output')
}
return JSON.parse(text)
}
async function callOrca(cli, args, options = {}) {
const result = await runCommand(cli, [...args, '--json'], options)
const parsed = parseCliJson(result.stdout)
if (parsed.ok === false) {
throw new Error(parsed.error?.message ?? JSON.stringify(parsed.error))
}
return parsed.result ?? parsed
}
function buildLongSpec(sizeKb, marker) {
const targetBytes = sizeKb * 1024
const header = [
`ORCA_LONG_PROMPT_REPRO_START ${marker}`,
'This task is intentionally long so orchestration dispatch crosses terminal input chunks.',
'The receiver expects the end marker to arrive before the submit byte.'
].join('\n')
const lines = [header]
let index = 0
while (
Buffer.byteLength(`${lines.join('\n')}\nORCA_LONG_PROMPT_REPRO_END ${marker}`, 'utf8') <
targetBytes
) {
lines.push(
`line ${String(index).padStart(5, '0')} ${marker} ${'abcdefghijklmnopqrstuvwxyz 0123456789 '.repeat(3)}`
)
index += 1
}
lines.push(`ORCA_LONG_PROMPT_REPRO_END ${marker}`)
return lines.join('\n')
}
function readNested(result, pathParts) {
let current = result
for (const part of pathParts) {
current = current?.[part]
}
return current
}
async function waitForReport(reportPath, timeoutMs) {
const startedAt = Date.now()
let lastError = null
while (Date.now() - startedAt < timeoutMs) {
try {
return JSON.parse(await readFile(reportPath, 'utf8'))
} catch (error) {
lastError = error
await new Promise((resolve) => setTimeout(resolve, 200))
}
}
const error = new Error(`Timed out waiting for report: ${reportPath}`)
error.cause = lastError
throw error
}
async function tryCloseTerminal(cli, handle, cwd) {
if (!handle) {
return
}
try {
await callOrca(cli, ['terminal', 'close', '--terminal', handle], { cwd })
} catch {
// Best-effort cleanup; the report is more useful than a close failure.
}
}
async function parentMain() {
const cli = argValue('cli', process.env.ORCA_REPRO_CLI ?? 'orca')
const mode = argValue('mode', DEFAULT_MODE)
if (!new Set(['wire', 'codex-like']).has(mode)) {
throw new Error('--mode must be wire or codex-like')
}
const sizeKb = parsePositiveInteger('size-kb', DEFAULT_SIZE_KB)
const timeoutMs = parsePositiveInteger('timeout-ms', DEFAULT_TIMEOUT_MS)
const cwd = path.resolve(argValue('worktree', process.cwd()))
const keepTerminals = hasFlag('keep-terminals')
const discardReport = hasFlag('discard-report')
const marker = `marker_${randomUUID().replace(/-/g, '')}`
const spec = buildLongSpec(sizeKb, marker)
const tempDir = path.join(tmpdir(), `orca-orchestration-long-prompt-${process.pid}-${Date.now()}`)
await mkdir(tempDir, { recursive: true })
const workerReportPath = path.join(tempDir, 'worker-report.json')
console.log(`runtime: ${cli} status`)
await callOrca(cli, ['status'], { cwd })
const coordinator = await callOrca(
cli,
[
'terminal',
'create',
'--worktree',
`path:${cwd}`,
'--title',
'orchestration repro coordinator',
'--command',
`${shellQuote(process.execPath)} ${shellQuote(SCRIPT_PATH)} --fake-coordinator`
],
{ cwd }
)
const coordinatorHandle = readNested(coordinator, ['terminal', 'handle'])
if (!coordinatorHandle) {
throw new Error('Could not create coordinator terminal')
}
const workerCommand = [
shellQuote(process.execPath),
shellQuote(SCRIPT_PATH),
'--fake-worker',
'--cli',
shellQuote(cli),
'--mode',
shellQuote(mode),
'--report',
shellQuote(workerReportPath),
'--marker',
shellQuote(marker),
'--timeout-ms',
String(timeoutMs)
].join(' ')
const worker = await callOrca(
cli,
[
'terminal',
'create',
'--worktree',
`path:${cwd}`,
'--title',
'orchestration repro fake codex',
'--command',
workerCommand
],
{ cwd }
)
const workerHandle = readNested(worker, ['terminal', 'handle'])
if (!workerHandle) {
throw new Error('Could not create worker terminal')
}
let taskId = null
try {
await callOrca(
cli,
[
'terminal',
'wait',
'--terminal',
workerHandle,
'--for',
'tui-idle',
'--timeout-ms',
'10000'
],
{ cwd }
)
const task = await callOrca(
cli,
[
'orchestration',
'task-create',
'--spec',
spec,
'--task-title',
'Long prompt repro',
'--display-name',
'Long prompt repro'
],
{ cwd }
)
taskId = readNested(task, ['task', 'id'])
if (!taskId) {
throw new Error('Could not create orchestration task')
}
const dispatch = await callOrca(
cli,
[
'orchestration',
'dispatch',
'--task',
taskId,
'--to',
workerHandle,
'--from',
coordinatorHandle,
'--inject'
],
{ cwd }
)
const dispatchId = readNested(dispatch, ['dispatch', 'id'])
const report = await waitForReport(workerReportPath, timeoutMs)
const summary = {
mode,
sizeKb,
taskId,
dispatchId,
coordinatorHandle,
workerHandle,
reportPath: workerReportPath,
expectedMarker: marker,
expectedSpecBytes: Buffer.byteLength(spec, 'utf8'),
...report
}
console.log(JSON.stringify(summary, null, 2))
if (report.contractOk !== true) {
process.exitCode = 1
}
} finally {
if (!keepTerminals) {
await tryCloseTerminal(cli, workerHandle, cwd)
await tryCloseTerminal(cli, coordinatorHandle, cwd)
}
if (discardReport) {
await rm(tempDir, { recursive: true, force: true })
}
}
}
function countUnframedLineBreaks(text) {
let inPaste = false
let count = 0
for (let index = 0; index < text.length; index += 1) {
if (text.startsWith(BEGIN, index)) {
inPaste = true
index += BEGIN.length - 1
continue
}
if (text.startsWith(END, index)) {
inPaste = false
index += END.length - 1
continue
}
const char = text[index]
if (!inPaste && (char === '\n' || char === '\r')) {
count += 1
}
}
return count
}
function buildReport(payload, mode, marker) {
const firstSubmit = payload.indexOf(0x0d)
const body = firstSubmit === -1 ? payload : payload.subarray(0, firstSubmit)
const text = body.toString('utf8')
const hasBracketedPasteFrame = text.includes(BEGIN) && text.includes(END)
const hasSubmit = firstSubmit !== -1
const rawContainsMarker = text.includes(marker)
const unframedLineBreaks = countUnframedLineBreaks(text)
const base = {
mode,
receivedBytesBeforeSubmit: body.length,
receivedSha256BeforeSubmit: createHash('sha256').update(body).digest('hex'),
hasSubmit,
rawContainsMarker,
hasBracketedPasteFrame,
unframedLineBreaks,
previewStart: text.slice(0, 120),
previewEnd: text.slice(-120)
}
if (mode === 'wire') {
return {
...base,
contractOk: hasSubmit && rawContainsMarker
}
}
return {
...base,
// Codex/Claude-like TUIs need generated multi-line prompts to arrive as
// one paste frame; unframed newlines are treated as live editor keys.
contractOk: hasSubmit && rawContainsMarker && hasBracketedPasteFrame && unframedLineBreaks === 0
}
}
function parseInjectedIds(text) {
return {
taskId: /Your task ID is:\s*(task_[A-Za-z0-9_]+)/.exec(text)?.[1] ?? null,
dispatchId: /--dispatch-id\s+(ctx_[A-Za-z0-9_]+)/.exec(text)?.[1] ?? null,
coordinatorHandle: /Your coordinator's terminal handle is:\s*([^\s]+)/.exec(text)?.[1] ?? null
}
}
async function fakeWorkerMain() {
const cli = argValue('cli', process.env.ORCA_REPRO_CLI ?? 'orca')
const mode = argValue('mode', DEFAULT_MODE)
const reportPath = argValue('report')
const marker = argValue('marker')
const timeoutMs = parsePositiveInteger('timeout-ms', DEFAULT_TIMEOUT_MS)
if (!reportPath || !marker) {
throw new Error('--fake-worker requires --report and --marker')
}
process.stdout.write('\x1b]0;codex ready\x07')
process.stdout.write('fake codex ready\n> ')
if (process.stdin.isTTY) {
process.stdin.setRawMode(true)
}
process.stdin.resume()
const chunks = []
let finished = false
async function sendWorkerDone(ids, report) {
if (!ids.taskId || !ids.dispatchId || !ids.coordinatorHandle) {
return { attempted: false, reason: 'missing-dispatch-identifiers' }
}
try {
await callOrca(cli, [
'orchestration',
'send',
'--to',
ids.coordinatorHandle,
'--type',
'worker_done',
'--subject',
report.contractOk
? 'Harness observed safe prompt delivery'
: 'Harness reproduced unsafe prompt delivery',
'--body',
[
`The fake worker received ${report.receivedBytesBeforeSubmit} bytes before submit.`,
`Bracketed paste frame present: ${String(report.hasBracketedPasteFrame)}.`,
`Unframed line breaks: ${String(report.unframedLineBreaks)}.`
].join(' '),
'--task-id',
ids.taskId,
'--dispatch-id',
ids.dispatchId,
'--report-path',
reportPath
])
return { attempted: true, ok: true }
} catch (error) {
return {
attempted: true,
ok: false,
error: error instanceof Error ? error.message : String(error)
}
}
}
const finish = async (reason) => {
if (finished) {
return
}
finished = true
const payload = Buffer.concat(chunks)
const report = {
reason,
...buildReport(payload, mode, marker)
}
const ids = parseInjectedIds(payload.toString('utf8'))
report.parsedTaskId = ids.taskId
report.parsedDispatchId = ids.dispatchId
report.parsedCoordinatorHandle = ids.coordinatorHandle
report.workerDone = await sendWorkerDone(ids, report)
await writeFile(reportPath, JSON.stringify(report, null, 2))
process.stdout.write(
`\nORCA_REPRO_REPORT ${report.contractOk ? 'ok' : 'failed'} ${reportPath}\n`
)
process.exit(report.contractOk ? 0 : 7)
}
const timer = setTimeout(() => {
finish('timeout').catch(() => process.exit(8))
}, timeoutMs)
process.stdin.on('data', (chunk) => {
chunks.push(Buffer.from(chunk))
if (Buffer.concat(chunks).includes(0x0d)) {
clearTimeout(timer)
finish('submit').catch(() => process.exit(8))
}
})
}
function fakeCoordinatorMain() {
process.stdout.write('\x1b]0;orchestration repro coordinator\x07')
process.stdout.write('orchestration repro coordinator ready\n')
setInterval(() => {}, 60_000)
}
async function main() {
if (hasFlag('help')) {
console.log(`Usage:
node tools/repro-orchestration-long-prompt.mjs [--mode codex-like|wire] [--size-kb 80]
The parent mode requires a running Orca runtime and creates temporary Orca
terminals. The fake worker records whether orchestration dispatch delivered a
long prompt in a safe agent-input contract.
Options:
--cli <path> Orca CLI command (default: ORCA_REPRO_CLI or orca)
--worktree <path> Worktree path for temporary terminals (default: cwd)
--timeout-ms <n> Wait budget for terminal/report operations
--keep-terminals Leave temporary terminals open for inspection
--discard-report Delete the temporary JSON report after printing summary
`)
return
}
if (hasFlag('fake-worker')) {
await fakeWorkerMain()
return
}
if (hasFlag('fake-coordinator')) {
fakeCoordinatorMain()
return
}
await parentMain()
}
main().catch((error) => {
console.error(error instanceof Error ? (error.stack ?? error.message) : error)
process.exit(1)
})