mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:40:43 +00:00
fix: enrich direct subagent terminal outcomes
This commit is contained in:
54
src/agents/subagent-registry-helpers.test.ts
Normal file
54
src/agents/subagent-registry-helpers.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { reconcileOrphanedRun } from "./subagent-registry-helpers.js";
|
||||
import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
||||
|
||||
function createRunEntry(overrides: Partial<SubagentRunRecord> = {}): SubagentRunRecord {
|
||||
return {
|
||||
runId: "run-1",
|
||||
childSessionKey: "agent:main:subagent:child",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "finish the task",
|
||||
cleanup: "keep",
|
||||
retainAttachmentsOnKeep: true,
|
||||
createdAt: 500,
|
||||
startedAt: 1_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("reconcileOrphanedRun", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("preserves timing on orphaned error outcomes", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(4_000);
|
||||
const entry = createRunEntry();
|
||||
const runs = new Map([[entry.runId, entry]]);
|
||||
const resumedRuns = new Set([entry.runId]);
|
||||
|
||||
expect(
|
||||
reconcileOrphanedRun({
|
||||
runId: entry.runId,
|
||||
entry,
|
||||
reason: "missing-session-id",
|
||||
source: "resume",
|
||||
runs,
|
||||
resumedRuns,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(entry.endedAt).toBe(4_000);
|
||||
expect(entry.outcome).toEqual({
|
||||
status: "error",
|
||||
error: "orphaned subagent run (missing-session-id)",
|
||||
startedAt: 1_000,
|
||||
endedAt: 4_000,
|
||||
elapsedMs: 3_000,
|
||||
});
|
||||
expect(runs.has(entry.runId)).toBe(false);
|
||||
expect(resumedRuns.has(entry.runId)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { type SubagentRunOutcome } from "./subagent-announce-output.js";
|
||||
import { withSubagentOutcomeTiming } from "./subagent-announce-output.js";
|
||||
import { SUBAGENT_ENDED_REASON_ERROR } from "./subagent-lifecycle-events.js";
|
||||
import { runOutcomesEqual } from "./subagent-registry-completion.js";
|
||||
import { shouldUpdateRunOutcome } from "./subagent-registry-completion.js";
|
||||
import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
||||
import {
|
||||
getSubagentSessionRuntimeMs,
|
||||
@@ -219,11 +219,17 @@ export function reconcileOrphanedRun(params: {
|
||||
params.entry.endedAt = now;
|
||||
changed = true;
|
||||
}
|
||||
const orphanOutcome: SubagentRunOutcome = {
|
||||
status: "error",
|
||||
error: `orphaned subagent run (${params.reason})`,
|
||||
};
|
||||
if (!runOutcomesEqual(params.entry.outcome, orphanOutcome)) {
|
||||
const orphanOutcome = withSubagentOutcomeTiming(
|
||||
{
|
||||
status: "error",
|
||||
error: `orphaned subagent run (${params.reason})`,
|
||||
},
|
||||
{
|
||||
startedAt: params.entry.startedAt,
|
||||
endedAt: params.entry.endedAt,
|
||||
},
|
||||
);
|
||||
if (shouldUpdateRunOutcome(params.entry.outcome, orphanOutcome)) {
|
||||
params.entry.outcome = orphanOutcome;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createRunningTaskRun } from "../tasks/detached-task-runtime.js";
|
||||
import { type DeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
|
||||
import { waitForAgentRun } from "./run-wait.js";
|
||||
import type { ensureRuntimePluginsLoaded as ensureRuntimePluginsLoadedFn } from "./runtime-plugins.js";
|
||||
import type { SubagentRunOutcome } from "./subagent-announce-output.js";
|
||||
import { type SubagentRunOutcome, withSubagentOutcomeTiming } from "./subagent-announce-output.js";
|
||||
import {
|
||||
SUBAGENT_ENDED_OUTCOME_KILLED,
|
||||
SUBAGENT_ENDED_REASON_COMPLETE,
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
SUBAGENT_ENDED_REASON_KILLED,
|
||||
type SubagentLifecycleEndedReason,
|
||||
} from "./subagent-lifecycle-events.js";
|
||||
import { emitSubagentEndedHookOnce, runOutcomesEqual } from "./subagent-registry-completion.js";
|
||||
import {
|
||||
emitSubagentEndedHookOnce,
|
||||
shouldUpdateRunOutcome,
|
||||
} from "./subagent-registry-completion.js";
|
||||
import {
|
||||
getSubagentSessionRuntimeMs,
|
||||
getSubagentSessionStartedAt,
|
||||
@@ -127,13 +130,17 @@ export function createSubagentRunManager(params: {
|
||||
mutated = true;
|
||||
}
|
||||
const waitError = typeof wait.error === "string" ? wait.error : undefined;
|
||||
const outcome: SubagentRunOutcome =
|
||||
const baseOutcome: SubagentRunOutcome =
|
||||
wait.status === "error"
|
||||
? { status: "error", error: waitError }
|
||||
: wait.status === "timeout"
|
||||
? { status: "timeout" }
|
||||
: { status: "ok" };
|
||||
if (!runOutcomesEqual(entry.outcome, outcome)) {
|
||||
const outcome = withSubagentOutcomeTiming(baseOutcome, {
|
||||
startedAt: entry.startedAt,
|
||||
endedAt: entry.endedAt,
|
||||
});
|
||||
if (shouldUpdateRunOutcome(entry.outcome, outcome)) {
|
||||
entry.outcome = outcome;
|
||||
mutated = true;
|
||||
}
|
||||
@@ -417,7 +424,13 @@ export function createSubagentRunManager(params: {
|
||||
continue;
|
||||
}
|
||||
entry.endedAt = now;
|
||||
entry.outcome = { status: "error", error: reason };
|
||||
entry.outcome = withSubagentOutcomeTiming(
|
||||
{ status: "error", error: reason },
|
||||
{
|
||||
startedAt: entry.startedAt,
|
||||
endedAt: now,
|
||||
},
|
||||
);
|
||||
entry.endedReason = SUBAGENT_ENDED_REASON_KILLED;
|
||||
entry.cleanupHandled = true;
|
||||
entry.cleanupCompletedAt = now;
|
||||
|
||||
@@ -402,6 +402,17 @@ describe("subagent registry seam flow", () => {
|
||||
});
|
||||
|
||||
expect(updated).toBe(1);
|
||||
const killedRun = mod
|
||||
.listSubagentRunsForRequester("agent:main:main")
|
||||
.find((entry) => entry.runId === "run-killed-init");
|
||||
const killedAt = Date.parse("2026-03-24T12:00:00Z");
|
||||
expect(killedRun?.outcome).toEqual({
|
||||
status: "error",
|
||||
error: "manual kill",
|
||||
startedAt: killedAt,
|
||||
endedAt: killedAt,
|
||||
elapsedMs: 0,
|
||||
});
|
||||
await waitForFast(() => {
|
||||
expect(mocks.ensureRuntimePluginsLoaded).toHaveBeenCalledWith({
|
||||
config: {
|
||||
|
||||
Reference in New Issue
Block a user