Merge branch 'codex/simp-agent-entry-state' into codex/simp-unify-agent-session-id

# Conflicts:
#	docs/cordis-catalog/services.md
This commit is contained in:
Tianyi Cui
2026-07-14 12:41:47 +08:00
14 changed files with 125 additions and 8 deletions

View File

@@ -178,13 +178,14 @@ export function launchAcpTestAgent(options: AcpTestLaunchOptions): LaunchedAcpTe
// `closed` follows parser exhaustion. Capture both eagerly so a caller that
// invokes close after process exit still joins the complete drain boundary.
const stdioClosed = new Promise<void>(resolve => child.once('close', () => { resolve() }))
const drained = Promise.all([stdioClosed, client.closed]).then(async () => {
const drained = Promise.allSettled([stdioClosed, client.closed]).then(async ([, clientResult]) => {
// The ACP SDK's readable loop dispatches client callbacks without awaiting
// them. Once `closed` settles no new callbacks can start, but callbacks
// already in flight still belong to this launch's teardown boundary.
while (inFlightClientCallbacks.size > 0) {
await Promise.allSettled([...inFlightClientCallbacks])
}
if (clientResult.status === 'rejected') throw clientResult.reason
})
// A caller may await a pending update without calling close(). Make natural
// stream exhaustion terminal for those waiters too, but only after the
@@ -206,6 +207,7 @@ export function launchAcpTestAgent(options: AcpTestLaunchOptions): LaunchedAcpTe
try {
await spawned
} catch (error: unknown) {
await drained.catch(() => undefined)
closeUpdateStream()
throw error
}

View File

@@ -486,8 +486,10 @@ export function defineAcpSnapshotSuite(options: SnapshotSuiteOptions): void {
const entries = await readdir(dir, { withFileTypes: true })
await Promise.all(entries
.filter(entry => entry.isFile()
&& entry.name.startsWith('session.')
&& entry.name.endsWith('.jsonl')
// Only valid numbered children are record-owned stale output.
// Malformed session-like names stay for the inventory guard to
// reject instead of being silently deleted during mutation.
&& /^session\.[1-9]\d*\.jsonl$/.test(entry.name)
&& !outputNames.has(entry.name))
.map(entry => rm(join(dir, entry.name))))
fixtureFiles = outputFixtureFiles

View File

@@ -62,8 +62,17 @@ describe('runScenario', () => {
it('surfaces an asynchronous child spawn failure through startup and close', async () => {
const { dir } = await scenario({})
const launched = launchAcpTestAgent({ agent: AGENT, cwd: join(dir, 'missing') })
let stdioClosed = false
let clientClosed = false
launched.child.once('close', () => { stdioClosed = true })
void launched.client.closed.then(
() => { clientClosed = true },
() => { clientClosed = true },
)
await expect(launched.spawned).rejects.toMatchObject({ code: 'ENOENT' })
await expect(launched.close()).rejects.toMatchObject({ code: 'ENOENT' })
expect(stdioClosed).toBe(true)
expect(clientClosed).toBe(true)
})
it('centralizes ACP boot, captures, updates, fail-closed permissions, and shutdown', { timeout: 20_000 }, async () => {