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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user