fix: block failed codex stdio sessions

Tighten Code Agent long-lived session gate so failed/interrupted/expired Codex stdio sessions remain blocked, and surface missing stdio tools in structured blocker payloads.
This commit is contained in:
Lyon
2026-05-23 23:20:30 +08:00
committed by GitHub
parent 997b96dc65
commit 43a6059b78
3 changed files with 110 additions and 1 deletions
+1
View File
@@ -773,6 +773,7 @@ async function callCodexStdioRunner({ message, conversationId, sessionId, traceI
blockers: error.blockers ?? availability.blockers,
route: null,
toolName: error.missingTools?.length ? "codex-stdio.required-tools" : "codex-stdio.session",
missingTools: error.missingTools,
availability: describeCodeAgentAvailability(env, { codexStdioManager: manager, workspace })
});
}
@@ -787,3 +787,103 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
assert.equal(calls[1].name, "codex-reply");
assert.equal(calls[1].args.threadId, "thread_stdio_ready");
});
test("Codex stdio runner startup failure marks session failed and blocks long-lived gate", async () => {
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_stdio_start_failed",
createRpcClient: async () => ({
async initialize() {
throw new Error("MCP initialize failed with sk-test-secret and TOKEN=abc123");
},
close() {}
})
});
const env = {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
};
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_stdio_start_failed",
traceId: "trc_stdio_start_failed",
message: "启动后回答"
},
{
now: () => "2026-05-23T00:08:00.000Z",
codexStdioManager: manager,
env
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "failed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.error.code, "codex_stdio_failed");
assert.equal(payload.session.sessionId, "ses_stdio_start_failed");
assert.equal(payload.session.status, "failed");
assert.equal(payload.session.statusReason, "codex_stdio_failed");
assert.equal(payload.longLivedSessionGate.status, "blocked");
assert.equal(payload.longLivedSessionGate.pass, false);
assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "session_failed"));
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(payload.runnerTrace.sessionStatus, "failed");
assert.equal(payload.runnerTrace.valuesPrinted, false);
const serialized = JSON.stringify(payload);
assert.equal(serialized.includes("sk-test-secret"), false);
assert.equal(serialized.includes("TOKEN=abc123"), false);
assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
});
test("Codex stdio protocol missing tools is a structured blocker, not a passed session", async () => {
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_stdio_missing_tool",
createRpcClient: async () => ({
async initialize() {
return { tools: ["codex"] };
},
async listTools() {
return ["codex"];
},
close() {}
})
});
const env = {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
};
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_stdio_missing_tool",
traceId: "trc_stdio_missing_tool",
message: "需要长会话"
},
{
now: () => "2026-05-23T00:08:30.000Z",
codexStdioManager: manager,
env
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "failed");
assert.equal(payload.error.code, "codex_stdio_protocol_blocked");
assert.deepEqual(payload.error.missingTools, ["codex-reply"]);
assert.equal(payload.session.status, "failed");
assert.equal(payload.longLivedSessionGate.status, "blocked");
assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "session_failed"));
assert.equal(payload.availability.codexStdio.ready, true);
assert.deepEqual(payload.availability.codexStdio.protocol.toolsObserved, ["codex"]);
assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
});
+9 -1
View File
@@ -286,7 +286,7 @@ export function createCodexStdioSessionManager(options = {}) {
}) ?? session;
if (error.code && error.code.startsWith("codex_stdio")) {
error.session = session;
error.availability = error.availability ?? availability;
error.availability = error.availability ?? describe({ ...params, env, workspace, sandbox });
error.runnerTrace = runnerTrace({
traceId,
workspace,
@@ -691,6 +691,13 @@ export function longLivedSessionGate({
summary: "This response is backed by the controlled read-only session registry, not Codex stdio or an equivalent long-lived protocol adapter."
});
}
if (session && !["idle", "ready", "busy"].includes(session.status)) {
blockers.push({
code: `session_${session.status || "inactive"}`,
sourceIssue: "pikasTech/HWLAB#317",
summary: `Long-lived Codex stdio session is not active: status=${session.status || "unknown"}.`
});
}
for (const blocker of codexStdioFeasibility?.blockers ?? []) {
if (!blocker?.code || blockers.some((item) => item.code === blocker.code)) continue;
blockers.push({
@@ -710,6 +717,7 @@ export function longLivedSessionGate({
session?.codexStdio === true &&
session?.writeCapable === true &&
session?.durable === true &&
["idle", "ready", "busy"].includes(session?.status) &&
feasible &&
blockers.length === 0;
const feasiblePass =