fix(runner): restore ACPX backend identity fallback

This commit is contained in:
Dotta 2026-09-02 21:37:34 -05:00
parent 9611da2b38
commit 6cba271e65
2 changed files with 56 additions and 27 deletions

View File

@ -1428,26 +1428,45 @@ describe("Codex ACPX runtime adapter", () => {
},
);
it("fails closed and closes the session when ACPX omits recovery identity", async () => {
it("uses the real ACP session when no second agent identity is advertised", async () => {
const runtime = fakeRuntime({ ...HANDLE, agentSessionId: undefined });
await expect(
openCodexAcpxRuntime(openOptions(fakeCommand()), {
createRegistry: () => registry(),
createStore: () => store(),
createRuntime: () => runtime,
}),
).rejects.toThrow("ACPX runtime omitted agentSessionId");
expect(runtime.close).toHaveBeenCalledWith({
handle: { ...HANDLE, agentSessionId: undefined },
reason: "ACPX runtime identity validation failed",
discardPersistentState: false,
const durableStore: AcpSessionStore = {
load: vi.fn(async () =>
structuredClone({
acpxRecordId: "record-1",
acpSessionId: "backend-1",
acpx: { current_model_id: "gpt-5.6-sol" },
} as never),
),
save: vi.fn(),
};
const port = await openCodexAcpxRuntime(openOptions(fakeCommand()), {
createRegistry: () => registry(),
createStore: () => durableStore,
createRuntime: () => runtime,
});
await expect(port.identity()).resolves.toEqual({
acpxRecordId: "record-1",
backendSessionId: "backend-1",
agentSessionId: "backend-1",
});
await expect(port.getStatus()).resolves.toMatchObject({
backendSessionId: "backend-1",
agentSessionId: "backend-1",
models: { currentModelId: "gpt-5.6-sol" },
});
expect(runtime.close).not.toHaveBeenCalled();
});
it("bounds invalid-identity cleanup before terminating the provider", async () => {
vi.useFakeTimers();
try {
const runtime = fakeRuntime({ ...HANDLE, agentSessionId: undefined });
const runtime = fakeRuntime({
...HANDLE,
backendSessionId: undefined,
agentSessionId: undefined,
});
vi.mocked(runtime.close).mockImplementation(
() => new Promise<void>(() => undefined),
);
@ -1466,7 +1485,11 @@ describe("Codex ACPX runtime adapter", () => {
args: ["--stdio"],
options: {},
});
return { ...HANDLE, agentSessionId: undefined };
return {
...HANDLE,
backendSessionId: undefined,
agentSessionId: undefined,
};
}),
}),
});

View File

@ -1048,10 +1048,12 @@ async function persistedRuntimeStatus(
{ code: "ACPX_PERSISTED_SESSION_MISSING" },
);
}
const persistedAgentSessionId =
nonEmptyRuntimeIdentity(record.agentSessionId) ?? record.acpSessionId;
if (
record.acpxRecordId !== identity.acpxRecordId ||
record.acpSessionId !== identity.backendSessionId ||
record.agentSessionId !== identity.agentSessionId
persistedAgentSessionId !== identity.agentSessionId
) {
throw Object.assign(
new Error("The persisted ACPX session identity changed after admission"),
@ -1064,12 +1066,12 @@ async function persistedRuntimeStatus(
summary: [
`session=${record.acpxRecordId}`,
`backendSessionId=${record.acpSessionId}`,
`agentSessionId=${record.agentSessionId}`,
`agentSessionId=${persistedAgentSessionId}`,
record.closed === true ? "closed" : "open",
].join(" "),
acpxRecordId: record.acpxRecordId,
backendSessionId: record.acpSessionId,
agentSessionId: record.agentSessionId,
agentSessionId: persistedAgentSessionId,
...(currentModelId === undefined && !availableModelIds?.length
? {}
: {
@ -1491,17 +1493,21 @@ function pushUnique(errors: unknown[], error: unknown): void {
}
function requireIdentity(handle: AcpRuntimeHandle): AcpxRuntimePortIdentity {
const identity = {
acpxRecordId: handle.acpxRecordId,
backendSessionId: handle.backendSessionId,
agentSessionId: handle.agentSessionId,
};
for (const [name, value] of Object.entries(identity)) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`ACPX runtime omitted ${name}`);
}
const acpxRecordId = nonEmptyRuntimeIdentity(handle.acpxRecordId);
if (!acpxRecordId) throw new Error("ACPX runtime omitted acpxRecordId");
const backendSessionId = nonEmptyRuntimeIdentity(handle.backendSessionId);
if (!backendSessionId) {
throw new Error("ACPX runtime omitted backendSessionId");
}
return identity as AcpxRuntimePortIdentity;
return {
acpxRecordId,
backendSessionId,
// ACPX agents do not all advertise a distinct native thread identity.
// In that case the backend ID is the real ACP protocol session, so retain
// it explicitly rather than inventing a Paperclip-owned identifier.
agentSessionId:
nonEmptyRuntimeIdentity(handle.agentSessionId) ?? backendSessionId,
};
}
function definedEnvironment(