diff --git a/CHANGELOG.md b/CHANGELOG.md index 067d04f2b2..97cea44031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/extensions/voice-call/src/cli.ts b/extensions/voice-call/src/cli.ts index 963fa6f517..eaf4e3fc0a 100644 --- a/extensions/voice-call/src/cli.ts +++ b/extensions/voice-call/src/cli.ts @@ -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, diff --git a/src/agents/subagent-announce.format.e2e.test.ts b/src/agents/subagent-announce.format.e2e.test.ts index e77c7c81de..44c7c2cc27 100644 --- a/src/agents/subagent-announce.format.e2e.test.ts +++ b/src/agents/subagent-announce.format.e2e.test.ts @@ -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 }; + expect(call?.params?.sessionKey).toBe("agent:main:main"); + expect(call?.params?.deliver).toBe(true); + expect(call?.params?.channel).toBe("discord"); + }); }); diff --git a/src/gateway/server-node-events.test.ts b/src/gateway/server-node-events.test.ts index 77530882e3..07525311a0 100644 --- a/src/gateway/server-node-events.test.ts +++ b/src/gateway/server-node-events.test.ts @@ -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); diff --git a/src/gateway/server-node-events.ts b/src/gateway/server-node-events.ts index bd662a126a..267d53f7ec 100644 --- a/src/gateway/server-node-events.ts +++ b/src/gateway/server-node-events.ts @@ -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; diff --git a/src/plugins/voice-call.plugin.test.ts b/src/plugins/voice-call.plugin.test.ts index a9c4c49b74..f978defe19 100644 --- a/src/plugins/voice-call.plugin.test.ts +++ b/src/plugins/voice-call.plugin.test.ts @@ -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: {