fix(codex): classify accepted/created/updated dynamic-tool statuses as success (#96856)

isCodexToolResultError fail-closes every tool-result status not in its
non-error allowlist, but the allowlist omitted several success statuses
emitted by OpenClaw tools that are exposed to codex agents:

- sessions_spawn accepted launches  -> details.status "accepted"
- create_goal / update_goal results -> details.status "created" / "updated"

So a successful accepted spawn (#96833), and successful goal create/update,
were classified as errors: reported to codex as success: false (mapped to a
Failed item status) and persisted on the transcript as isError: true. This
adds those statuses to the allowlist alongside their sibling success statuses
(completed/recorded/started/running). Genuinely failed or forbidden results
(status "error"/"forbidden") stay fail-closed.

Adds regression tests: accepted spawn and created/updated goal results are
reported as successful dynamic tool calls; a forbidden spawn still fails.
This commit is contained in:
David
2026-06-28 18:37:00 -07:00
committed by GitHub
parent 31b531e791
commit b3ff64145e
2 changed files with 97 additions and 0 deletions
@@ -308,6 +308,100 @@ describe("createCodexDynamicToolBridge", () => {
});
});
it("treats an accepted child session spawn result as a successful dynamic tool call", async () => {
// An accepted sessions_spawn launch carries details.status "accepted" with a
// runId + childSessionKey. The launch succeeded (the child session was
// accepted), so Codex must see a successful tool call, not an error.
// Regression for #96833: "accepted" was missing from the non-error status
// allowlist, so the launch was classified as an error and persisted with
// isError: true (and reported to Codex as success: false).
const onAgentToolResult = vi.fn();
const bridge = createBridgeWithToolResult(
"sessions_spawn",
textToolResult("Accepted: launching child session to scan logs.", {
status: "accepted",
runId: "run_5f3a9c",
childSessionKey: "child-7b21",
mode: "run",
}),
);
const result = await bridge.handleToolCall(
{
threadId: "thread-1",
turnId: "turn-1",
callId: "call-accepted",
namespace: null,
tool: "sessions_spawn",
arguments: { task: "scan logs" },
},
{ onAgentToolResult },
);
// success: true proves the accepted launch is not classified as an error;
// the content assertion proves the tool actually executed (not a denial path).
expect(result.success).toBe(true);
expect(result.contentItems).toEqual([
{ type: "inputText", text: "Accepted: launching child session to scan logs." },
]);
expect(onAgentToolResult).toHaveBeenCalledWith(
expect.objectContaining({ toolName: "sessions_spawn", isError: false }),
);
});
it("still reports a forbidden sessions_spawn result as a failed dynamic tool call", async () => {
// Deny symmetry: a genuinely rejected spawn (status "forbidden") must stay an
// error so the accepted-status allowlist entry does not over-correct.
const bridge = createBridgeWithToolResult(
"sessions_spawn",
textToolResult("Forbidden: spawn limit reached.", { status: "forbidden" }),
);
const result = await bridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-forbidden",
namespace: null,
tool: "sessions_spawn",
arguments: { task: "deploy" },
});
expect(result.success).toBe(false);
});
it("treats accepted goal tool statuses (created / updated) as successful dynamic tool calls", async () => {
// Same classifier-completeness class as the accepted spawn fix: create_goal /
// update_goal return details.status "created" / "updated", reach codex agents
// through the dynamic-tool bridge, and must not be classified as errors (#96833).
const createdBridge = createBridgeWithToolResult(
"create_goal",
textToolResult("Goal created.", { status: "created" }),
);
const createdResult = await createdBridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-created",
namespace: null,
tool: "create_goal",
arguments: { text: "ship the fix" },
});
expect(createdResult.success).toBe(true);
const updatedBridge = createBridgeWithToolResult(
"update_goal",
textToolResult("Goal updated.", { status: "updated" }),
);
const updatedResult = await updatedBridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-updated",
namespace: null,
tool: "update_goal",
arguments: { status: "completed" },
});
expect(updatedResult.success).toBe(true);
});
it("keeps available and registered schemas paired with their tools", () => {
const bridge = createCodexDynamicToolBridge({
tools: [
@@ -1184,6 +1184,9 @@ function isCodexToolResultError(result: AgentToolResult<unknown>): boolean {
status !== "success" &&
status !== "completed" &&
status !== "recorded" &&
status !== "created" &&
status !== "updated" &&
status !== "accepted" &&
status !== "pending" &&
status !== "started" &&
status !== "running" &&