mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
@@ -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" &&
|
||||
|
||||
Reference in New Issue
Block a user