





















@@ -5,13 +5,19 @@ import {
55registerExecApprovalFollowupRuntimeHandoff,
66resetExecApprovalFollowupRuntimeHandoffsForTests,
77} from "../../agents/bash-tools.exec-approval-followup-state.js";
8+import {
9+getSubagentRunByChildSessionKey,
10+resetSubagentRegistryForTests,
11+testing as subagentRegistryTesting,
12+} from "../../agents/subagent-registry.js";
813import {
914getDetachedTaskLifecycleRuntime,
1015resetDetachedTaskLifecycleRuntimeForTests,
1116setDetachedTaskLifecycleRuntime,
1217} from "../../tasks/detached-task-runtime.js";
1318import {
1419findTaskByRunId,
20+listTaskRecords,
1521markTaskTerminalById,
1622resetTaskRegistryForTests,
1723} from "../../tasks/task-registry.js";
@@ -508,6 +514,8 @@ describe("gateway agent handler", () => {
508514}
509515resetDetachedTaskLifecycleRuntimeForTests();
510516resetTaskRegistryForTests();
517+resetSubagentRegistryForTests({ persist: false });
518+subagentRegistryTesting.setDepsForTest();
511519mocks.loadConfigReturn = {};
512520mocks.emitGatewaySessionEndPluginHook.mockReset();
513521mocks.emitGatewaySessionStartPluginHook.mockReset();
@@ -2801,6 +2809,200 @@ describe("gateway agent handler", () => {
28012809});
28022810});
280328112812+it("tracks plugin SDK subagent agent runs through the subagent registry only", async () => {
2813+await withTempDir({ prefix: "openclaw-gateway-plugin-subagent-task-" }, async (root) => {
2814+process.env.OPENCLAW_STATE_DIR = root;
2815+resetTaskRegistryForTests();
2816+resetSubagentRegistryForTests({ persist: false });
2817+const runId = "plugin-subagent-task-run";
2818+const childSessionKey = "agent:work:subagent:plugin-helper";
2819+const cfg = {
2820+session: { mainKey: "main", scope: "per-sender" },
2821+agents: { list: [{ id: "main", default: true }, { id: "work" }] },
2822+};
2823+mocks.listAgentIds.mockReturnValue(["main", "work"]);
2824+mocks.loadConfigReturn = cfg;
2825+mocks.loadSessionEntry.mockReturnValue({
2826+ cfg,
2827+storePath: "/tmp/sessions.json",
2828+entry: {
2829+sessionId: "plugin-subagent-session",
2830+updatedAt: Date.now(),
2831+},
2832+canonicalKey: childSessionKey,
2833+});
2834+mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
2835+const store: Record<string, unknown> = {
2836+[childSessionKey]: {
2837+sessionId: "plugin-subagent-session",
2838+updatedAt: Date.now(),
2839+},
2840+};
2841+return await updater(store);
2842+});
2843+mocks.agentCommand.mockResolvedValue({
2844+payloads: [{ text: "ok" }],
2845+meta: { durationMs: 100 },
2846+});
2847+const context = makeContext();
2848+const baseClient = requireValue(backendGatewayClient(), "expected backend client");
2849+const pluginClient: AgentHandlerArgs["client"] = {
2850+connect: baseClient.connect,
2851+internal: {
2852+ ...baseClient.internal,
2853+agentRunTracking: "plugin_subagent",
2854+pluginRuntimeOwnerId: "memory-core",
2855+},
2856+};
2857+2858+await invokeAgent(
2859+{
2860+message: "background plugin subagent task",
2861+sessionKey: childSessionKey,
2862+idempotencyKey: runId,
2863+},
2864+{
2865+ context,
2866+reqId: runId,
2867+client: pluginClient,
2868+},
2869+);
2870+2871+await waitForAssertion(() => {
2872+const tasks = listTaskRecords().filter((task) => task.runId === runId);
2873+expect(tasks).toHaveLength(1);
2874+const task = requireValue(tasks[0], "expected one plugin subagent task");
2875+expectRecordFields(task, {
2876+runtime: "subagent",
2877+ childSessionKey,
2878+ownerKey: "agent:work:main",
2879+label: "plugin:memory-core",
2880+task: "background plugin subagent task",
2881+deliveryStatus: "not_applicable",
2882+});
2883+expect(task.runtime).not.toBe("cli");
2884+});
2885+2886+const run = requireValue(
2887+getSubagentRunByChildSessionKey(childSessionKey),
2888+"expected subagent registry run",
2889+);
2890+expectRecordFields(run, {
2891+ runId,
2892+ childSessionKey,
2893+controllerSessionKey: "agent:work:main",
2894+requesterSessionKey: "agent:work:main",
2895+requesterDisplayKey: "main",
2896+cleanup: "keep",
2897+spawnMode: "run",
2898+label: "plugin:memory-core",
2899+});
2900+expectRecordFields(run.completion, { required: false });
2901+expectRecordFields(run.delivery, { status: "not_required" });
2902+2903+const commandCallCount = mocks.agentCommand.mock.calls.length;
2904+const createdAt = run.createdAt;
2905+await invokeAgent(
2906+{
2907+message: "background plugin subagent task",
2908+sessionKey: childSessionKey,
2909+idempotencyKey: runId,
2910+},
2911+{
2912+ context,
2913+reqId: `${runId}-retry`,
2914+client: pluginClient,
2915+},
2916+);
2917+2918+expect(mocks.agentCommand).toHaveBeenCalledTimes(commandCallCount);
2919+const retryTasks = listTaskRecords().filter((task) => task.runId === runId);
2920+expect(retryTasks).toHaveLength(1);
2921+expect(getSubagentRunByChildSessionKey(childSessionKey)?.createdAt).toBe(createdAt);
2922+});
2923+});
2924+2925+it("keeps plugin SDK subagent runs best-effort when registry persistence fails", async () => {
2926+await withTempDir(
2927+{ prefix: "openclaw-gateway-plugin-subagent-registry-fail-" },
2928+async (root) => {
2929+process.env.OPENCLAW_STATE_DIR = root;
2930+resetTaskRegistryForTests();
2931+resetSubagentRegistryForTests({ persist: false });
2932+subagentRegistryTesting.setDepsForTest({
2933+persistSubagentRunsToDiskOrThrow: () => {
2934+throw new Error("disk full");
2935+},
2936+});
2937+const runId = "plugin-subagent-registry-fail";
2938+const childSessionKey = "agent:main:subagent:registry-fail";
2939+const cfg = {
2940+session: { mainKey: "main", scope: "per-sender" },
2941+};
2942+mocks.loadConfigReturn = cfg;
2943+mocks.loadSessionEntry.mockReturnValue({
2944+ cfg,
2945+storePath: "/tmp/sessions.json",
2946+entry: {
2947+sessionId: "plugin-subagent-registry-fail-session",
2948+updatedAt: Date.now(),
2949+},
2950+canonicalKey: childSessionKey,
2951+});
2952+mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
2953+const store: Record<string, unknown> = {
2954+[childSessionKey]: {
2955+sessionId: "plugin-subagent-registry-fail-session",
2956+updatedAt: Date.now(),
2957+},
2958+};
2959+return await updater(store);
2960+});
2961+mocks.agentCommand.mockResolvedValue({
2962+payloads: [{ text: "ok" }],
2963+meta: { durationMs: 100 },
2964+});
2965+const context = makeContext();
2966+const baseClient = requireValue(backendGatewayClient(), "expected backend client");
2967+const commandCallCount = mocks.agentCommand.mock.calls.length;
2968+2969+await invokeAgent(
2970+{
2971+message: "background plugin subagent task",
2972+sessionKey: childSessionKey,
2973+idempotencyKey: runId,
2974+},
2975+{
2976+ context,
2977+reqId: runId,
2978+client: {
2979+connect: baseClient.connect,
2980+internal: {
2981+ ...baseClient.internal,
2982+agentRunTracking: "plugin_subagent",
2983+pluginRuntimeOwnerId: "memory-core",
2984+},
2985+},
2986+},
2987+);
2988+2989+expect(mocks.agentCommand).toHaveBeenCalledTimes(commandCallCount + 1);
2990+await waitForAssertion(() => {
2991+const task = requireValue(findTaskByRunId(runId), "expected fallback cli task");
2992+expectRecordFields(task, {
2993+runtime: "cli",
2994+ childSessionKey,
2995+status: "succeeded",
2996+terminalSummary: "completed",
2997+});
2998+});
2999+expect(context.logGateway.warn).toHaveBeenCalledWith(
3000+expect.stringContaining("falling back to cli task tracking"),
3001+);
3002+},
3003+);
3004+});
3005+28043006it("terminalizes failed async gateway agent runs in the shared task registry", async () => {
28053007await withTempDir({ prefix: "openclaw-gateway-agent-task-error-" }, async (root) => {
28063008process.env.OPENCLAW_STATE_DIR = root;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。