Skip to content

Commit 82dfd89

Browse files
fix(agents): stop recording accepted sessions_spawn launches as tool failures (#96851)
* fix(agents): stop recording accepted sessions_spawn launches as tool failures * fix(agents): stop recording accepted sessions_spawn launches as tool failures * fix(clownfish): address review for live-pr-inventory-20260629T175323-001 (1) --------- Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
1 parent 38ab207 commit 82dfd89

2 files changed

Lines changed: 182 additions & 2 deletions

File tree

src/agents/embedded-agent-runner.extensions.test.ts

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import {
44
textToolResult,
55
} from "openclaw/plugin-sdk/agent-runtime-test-contracts";
66
// Covers embedded runner extension factories and tool-result middleware bridge.
7-
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
7+
import {
8+
AuthStorage,
9+
createEventBus,
10+
createExtensionRuntime,
11+
ExtensionRunner,
12+
loadExtensionFromFactory,
13+
ModelRegistry,
14+
SessionManager,
15+
} from "openclaw/plugin-sdk/agent-sessions";
816
import { afterEach, describe, expect, it, vi } from "vitest";
917
import { createEmptyPluginRegistry } from "../plugins/registry.js";
1018
import { setActivePluginRegistry } from "../plugins/runtime.js";
@@ -15,6 +23,7 @@ import {
1523
import { buildEmbeddedExtensionFactories } from "./embedded-agent-runner/extensions.js";
1624
import { consumeEmbeddedToolSendReceipt } from "./embedded-agent-runner/tool-send-receipts.js";
1725
import { cleanupTempPluginTestEnvironment } from "./test-helpers/temp-plugin-extension-fixtures.js";
26+
import { jsonResult } from "./tools/common.js";
1827

1928
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
2029
const tempDirs: string[] = [];
@@ -497,6 +506,168 @@ describe("buildEmbeddedExtensionFactories", () => {
497506
});
498507
});
499508

509+
it("keeps an accepted sessions_spawn launch successful even when the event is flagged as an error", async () => {
510+
setActivePluginRegistry(createEmptyPluginRegistry());
511+
512+
const factories = buildEmbeddedExtensionFactories({
513+
cfg: undefined,
514+
sessionManager: SessionManager.inMemory(),
515+
provider: "openai",
516+
modelId: "gpt-5.4",
517+
model: undefined,
518+
});
519+
520+
const factory = factories[0];
521+
expect(factory).toBeDefined();
522+
if (!factory) {
523+
throw new Error("Expected embedded tool-result extension factory");
524+
}
525+
const runtime = createExtensionRuntime();
526+
const extension = await loadExtensionFromFactory(
527+
factory,
528+
"/tmp",
529+
createEventBus(),
530+
runtime,
531+
"<embedded-test>",
532+
);
533+
const runner = new ExtensionRunner(
534+
[extension],
535+
runtime,
536+
"/tmp",
537+
SessionManager.inMemory(),
538+
ModelRegistry.inMemory(AuthStorage.inMemory()),
539+
);
540+
const acceptedResult = jsonResult({
541+
status: "accepted",
542+
childSessionKey: "agent:watcher:subagent:abc",
543+
runId: "run-123",
544+
mode: "run",
545+
});
546+
547+
const result = await runner.emitToolResult({
548+
type: "tool_result",
549+
toolName: "sessions_spawn",
550+
toolCallId: "call-spawn",
551+
input: {},
552+
content: acceptedResult.content,
553+
details: acceptedResult.details,
554+
isError: true,
555+
});
556+
557+
expect(result).toEqual({ ...acceptedResult, isError: false });
558+
});
559+
560+
it("still marks a forbidden sessions_spawn as a model-visible failure", async () => {
561+
setActivePluginRegistry(createEmptyPluginRegistry());
562+
563+
const factories = buildEmbeddedExtensionFactories({
564+
cfg: undefined,
565+
sessionManager: SessionManager.inMemory(),
566+
provider: "openai",
567+
modelId: "gpt-5.4",
568+
model: undefined,
569+
});
570+
571+
const handlers = new Map<string, Function>();
572+
await factories[0]?.({
573+
on(event: string, handler: Function) {
574+
handlers.set(event, handler);
575+
},
576+
} as never);
577+
const handler = handlers.get("tool_result");
578+
const content = [{ type: "text", text: "spawn denied" }];
579+
const details = { status: "forbidden", reason: "subagents disabled" };
580+
581+
const result = await handler?.(
582+
{
583+
toolName: "sessions_spawn",
584+
toolCallId: "call-spawn-forbidden",
585+
content,
586+
details,
587+
isError: true,
588+
},
589+
{ cwd: "/tmp" },
590+
);
591+
592+
expect(result).toEqual({ content, details, isError: true });
593+
});
594+
595+
it("still flags an accepted-status sessions_spawn that is missing spawn identity", async () => {
596+
// Only the full accepted contract (runId + childSessionKey) is a launch; an
597+
// accepted status alone must not clear an error flag.
598+
setActivePluginRegistry(createEmptyPluginRegistry());
599+
600+
const factories = buildEmbeddedExtensionFactories({
601+
cfg: undefined,
602+
sessionManager: SessionManager.inMemory(),
603+
provider: "openai",
604+
modelId: "gpt-5.4",
605+
model: undefined,
606+
});
607+
608+
const handlers = new Map<string, Function>();
609+
await factories[0]?.({
610+
on(event: string, handler: Function) {
611+
handlers.set(event, handler);
612+
},
613+
} as never);
614+
const handler = handlers.get("tool_result");
615+
const content = [{ type: "text", text: "partial" }];
616+
const details = { status: "accepted" };
617+
618+
const result = await handler?.(
619+
{
620+
toolName: "sessions_spawn",
621+
toolCallId: "call-spawn-partial",
622+
content,
623+
details,
624+
isError: true,
625+
},
626+
{ cwd: "/tmp" },
627+
);
628+
629+
expect(result).toEqual({ content, details, isError: true });
630+
});
631+
632+
it("does not clear the error flag for a non-spawn tool with accepted-shaped details", async () => {
633+
setActivePluginRegistry(createEmptyPluginRegistry());
634+
635+
const factories = buildEmbeddedExtensionFactories({
636+
cfg: undefined,
637+
sessionManager: SessionManager.inMemory(),
638+
provider: "openai",
639+
modelId: "gpt-5.4",
640+
model: undefined,
641+
});
642+
643+
const handlers = new Map<string, Function>();
644+
await factories[0]?.({
645+
on(event: string, handler: Function) {
646+
handlers.set(event, handler);
647+
},
648+
} as never);
649+
const handler = handlers.get("tool_result");
650+
const content = [{ type: "text", text: "boom" }];
651+
const details = jsonResult({
652+
status: "accepted",
653+
childSessionKey: "agent:watcher:subagent:abc",
654+
runId: "run-123",
655+
}).details;
656+
657+
const result = await handler?.(
658+
{
659+
toolName: "exec",
660+
toolCallId: "call-exec-accepted-shape",
661+
content,
662+
details,
663+
isError: true,
664+
},
665+
{ cwd: "/tmp" },
666+
);
667+
668+
expect(result).toEqual({ content, details, isError: true });
669+
});
670+
500671
it("does not mark results as errors when status is absent or non-error", async () => {
501672
setActivePluginRegistry(createEmptyPluginRegistry());
502673

src/agents/embedded-agent-runner/extensions.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { randomUUID } from "node:crypto";
55
import type { OpenClawConfig } from "../../config/types.openclaw.js";
66
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
7+
import { normalizeAcceptedSessionSpawnResult } from "../accepted-session-spawn.js";
78
import { setCompactionSafeguardRuntime } from "../agent-hooks/compaction-safeguard-runtime.js";
89
import compactionSafeguardExtension from "../agent-hooks/compaction-safeguard.js";
910
import contextPruningExtension from "../agent-hooks/context-pruning.js";
@@ -89,7 +90,14 @@ function buildAgentToolResultMiddlewareFactory(
8990
isError: event.isError,
9091
result: current,
9192
});
92-
const isError = event.isError === true || inputHadErrorStatus || isToolResultError(result);
93+
const isAcceptedSessionSpawn =
94+
event.toolName === "sessions_spawn" && normalizeAcceptedSessionSpawnResult(result) !== null;
95+
const isError =
96+
!isAcceptedSessionSpawn &&
97+
(event.isError === true || inputHadErrorStatus || isToolResultError(result));
98+
const clearsAcceptedSessionSpawnError =
99+
isAcceptedSessionSpawn &&
100+
(event.isError === true || inputHadErrorStatus || isToolResultError(result));
93101
if (eventToolCallId) {
94102
finalizeToolTerminalPresentation({
95103
toolCallId: eventToolCallId,
@@ -102,6 +110,7 @@ function buildAgentToolResultMiddlewareFactory(
102110
content: result.content,
103111
details: result.details,
104112
...(isError ? { isError: true } : {}),
113+
...(clearsAcceptedSessionSpawnError ? { isError: false } : {}),
105114
};
106115
});
107116
};

0 commit comments

Comments
 (0)