mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
chore(subagents): add regression coverage and changelog
This commit is contained in:
@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/Configure: make the `/model picker` allowlist prompt searchable with tokenized matching in `openclaw configure` so users can filter huge model lists by typing terms like `gpt-5.2 openai/`. (#19010) Thanks @bjesuiter.
|
||||
- Voice Call: add an optional stale call reaper (`staleCallReaperSeconds`) to end stuck calls when enabled. (#18437)
|
||||
- Auto-reply/Subagents: propagate group context (`groupId`, `groupChannel`, `space`) when spawning via `/subagents spawn`, matching tool-triggered subagent spawn behavior.
|
||||
- Subagents: route nested announce results back to the parent session after the parent run ends, falling back only when the parent session is deleted. (#18043)
|
||||
- Subagents: cap announce retry loops with max attempts and expiry to prevent infinite retry spam after deferred announces. (#18444)
|
||||
- Agents/Tools/exec: add a preflight guard that detects likely shell env var injection (e.g. `$DM_JSON`, `$TMPDIR`) in Python/Node scripts before execution, preventing recurring cron failures and wasted tokens when models emit mixed shell+language source. (#12836)
|
||||
- Agents/Tools/exec: treat normal non-zero exit codes as completed and append the exit code to tool output to avoid false tool-failure warnings. (#18425)
|
||||
|
||||
@@ -62,8 +62,14 @@ function summarizeSeries(values: number[]): {
|
||||
return { count: 0, minMs: 0, maxMs: 0, avgMs: 0, p50Ms: 0, p95Ms: 0 };
|
||||
}
|
||||
|
||||
const minMs = values.reduce((min, value) => (value < min ? value : min), Number.POSITIVE_INFINITY);
|
||||
const maxMs = values.reduce((max, value) => (value > max ? value : max), Number.NEGATIVE_INFINITY);
|
||||
const minMs = values.reduce(
|
||||
(min, value) => (value < min ? value : min),
|
||||
Number.POSITIVE_INFINITY,
|
||||
);
|
||||
const maxMs = values.reduce(
|
||||
(max, value) => (value > max ? value : max),
|
||||
Number.NEGATIVE_INFINITY,
|
||||
);
|
||||
const avgMs = values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||
return {
|
||||
count: values.length,
|
||||
|
||||
@@ -838,4 +838,48 @@ describe("subagent announce formatting", () => {
|
||||
expect(call?.params?.deliver).toBe(true);
|
||||
expect(call?.params?.channel).toBe("discord");
|
||||
});
|
||||
|
||||
it("falls back when parent session is missing a sessionId (#18037)", async () => {
|
||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
|
||||
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
|
||||
|
||||
subagentRegistryMock.isSubagentSessionRunActive.mockReturnValue(false);
|
||||
sessionStore = {
|
||||
"agent:main:subagent:newton": {
|
||||
sessionId: " ",
|
||||
inputTokens: 100,
|
||||
outputTokens: 50,
|
||||
},
|
||||
"agent:main:subagent:newton:subagent:birdie": {
|
||||
sessionId: "birdie-session-id",
|
||||
inputTokens: 20,
|
||||
outputTokens: 10,
|
||||
},
|
||||
};
|
||||
subagentRegistryMock.resolveRequesterForChildSession.mockReturnValue({
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: { channel: "discord" },
|
||||
});
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:newton:subagent:birdie",
|
||||
childRunId: "run-birdie-empty-parent",
|
||||
requesterSessionKey: "agent:main:subagent:newton",
|
||||
requesterDisplayKey: "subagent:newton",
|
||||
task: "QA task",
|
||||
timeoutMs: 1000,
|
||||
cleanup: "keep",
|
||||
waitForCompletion: false,
|
||||
startedAt: 10,
|
||||
endedAt: 20,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(true);
|
||||
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
||||
expect(call?.params?.sessionKey).toBe("agent:main:main");
|
||||
expect(call?.params?.deliver).toBe(true);
|
||||
expect(call?.params?.channel).toBe("discord");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,12 +29,12 @@ vi.mock("./session-utils.js", () => ({
|
||||
}));
|
||||
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import type { HealthSummary } from "../commands/health.js";
|
||||
import type { NodeEventContext } from "./server-node-events-types.js";
|
||||
import { agentCommand } from "../commands/agent.js";
|
||||
import type { HealthSummary } from "../commands/health.js";
|
||||
import { updateSessionStore } from "../config/sessions.js";
|
||||
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
import type { NodeEventContext } from "./server-node-events-types.js";
|
||||
import { handleNodeEvent } from "./server-node-events.js";
|
||||
|
||||
const enqueueSystemEventMock = vi.mocked(enqueueSystemEvent);
|
||||
|
||||
@@ -76,7 +76,10 @@ function shouldDropDuplicateVoiceTranscript(params: {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
recentVoiceTranscripts.set(params.sessionKey, { fingerprint: params.fingerprint, ts: params.now });
|
||||
recentVoiceTranscripts.set(params.sessionKey, {
|
||||
fingerprint: params.fingerprint,
|
||||
ts: params.now,
|
||||
});
|
||||
|
||||
if (recentVoiceTranscripts.size > MAX_RECENT_VOICE_TRANSCRIPTS) {
|
||||
const cutoff = params.now - VOICE_TRANSCRIPT_DEDUPE_WINDOW_MS * 2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Command } from "commander";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let runtimeStub: {
|
||||
|
||||
Reference in New Issue
Block a user