diff --git a/CHANGELOG.md b/CHANGELOG.md index 573a2fb5e90..3ac343e712b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,9 @@ Docs: https://docs.openclaw.ai and honor configured `params.chat_template_kwargs` for OpenAI-compatible completions, so vLLM/Nemotron replies stay visible instead of becoming thinking-only. Fixes #71891. Thanks @jmystaki-create and @dennis-lynch. +- Subagents/memory: keep inter-session completion wakes out of memory and + dreaming session exports, and strip internal runtime-context blocks from + realtime Control UI chat events. - Agents/Claude: treat zero-token empty `stop` turns as failed provider output, retry once, repair replay, and allow configured model fallback instead of preserving them as successful silent replies. Fixes #71880. Thanks @MagnaAI. diff --git a/packages/memory-host-sdk/src/host/session-files.test.ts b/packages/memory-host-sdk/src/host/session-files.test.ts index b846bc7e6a2..956e5bbb065 100644 --- a/packages/memory-host-sdk/src/host/session-files.test.ts +++ b/packages/memory-host-sdk/src/host/session-files.test.ts @@ -185,4 +185,32 @@ describe("buildSessionEntry", () => { expect(entry).not.toBeNull(); expect(entry!.content).toBe("User: Actual user text"); }); + + it("skips inter-session user messages", async () => { + const jsonlLines = [ + JSON.stringify({ + type: "message", + message: { + role: "user", + content: "A background task completed. Internal relay text.", + provenance: { kind: "inter_session", sourceTool: "subagent_announce" }, + }, + }), + JSON.stringify({ + type: "message", + message: { role: "assistant", content: "User-facing summary." }, + }), + JSON.stringify({ + type: "message", + message: { role: "user", content: "Actual user follow-up." }, + }), + ]; + const filePath = path.join(tmpDir, "inter-session-session.jsonl"); + fsSync.writeFileSync(filePath, jsonlLines.join("\n")); + + const entry = await buildSessionEntry(filePath); + expect(entry).not.toBeNull(); + expect(entry!.content).toBe("Assistant: User-facing summary.\nUser: Actual user follow-up."); + expect(entry!.lineMap).toEqual([2, 3]); + }); }); diff --git a/packages/memory-host-sdk/src/host/session-files.ts b/packages/memory-host-sdk/src/host/session-files.ts index 7e1b516bec2..34e6c6c88de 100644 --- a/packages/memory-host-sdk/src/host/session-files.ts +++ b/packages/memory-host-sdk/src/host/session-files.ts @@ -8,6 +8,7 @@ import { } from "../../../../src/config/sessions/artifacts.js"; import { resolveSessionTranscriptsDirForAgent } from "../../../../src/config/sessions/paths.js"; import { redactSensitiveText } from "../../../../src/logging/redact.js"; +import { hasInterSessionUserProvenance } from "../../../../src/sessions/input-provenance.js"; import { hashText } from "./hash.js"; export type SessionFileEntry = { @@ -170,7 +171,7 @@ export async function buildSessionEntry(absPath: string): Promise { nowSpy?.mockRestore(); }); + it("strips internal runtime context from assistant chat events", () => { + const { broadcast, nodeSendToSession, nowSpy } = emitRun1AssistantText( + createHarness({ now: 1_000 }), + [ + "Visible before.", + "", + "<<>>", + "OpenClaw runtime context (internal):", + "[Internal task completion event]", + "secret child result", + "<<>>", + "", + "Visible after.", + ].join("\n"), + ); + + const chatCalls = chatBroadcastCalls(broadcast); + expect(chatCalls).toHaveLength(1); + const payload = chatCalls[0]?.[1] as { + message?: { content?: Array<{ text?: string }> }; + }; + expect(payload.message?.content?.[0]?.text).toBe("Visible before.\n\nVisible after."); + expect(payload.message?.content?.[0]?.text).not.toContain("BEGIN_OPENCLAW_INTERNAL_CONTEXT"); + expect(payload.message?.content?.[0]?.text).not.toContain("secret child result"); + expect(sessionChatCalls(nodeSendToSession)).toHaveLength(1); + nowSpy?.mockRestore(); + }); + it.each([" NO_REPLY ", " ANNOUNCE_SKIP ", " REPLY_SKIP "])( "does not emit chat delta for suppressed control text %s", (replyText) => { diff --git a/src/gateway/server-chat.ts b/src/gateway/server-chat.ts index 10ba7dbbcd0..2c0131cee29 100644 --- a/src/gateway/server-chat.ts +++ b/src/gateway/server-chat.ts @@ -1,3 +1,4 @@ +import { stripInternalRuntimeContext } from "../agents/internal-runtime-context.js"; import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "../auto-reply/heartbeat.js"; import { normalizeVerboseLevel } from "../auto-reply/thinking.js"; import { @@ -688,9 +689,11 @@ export function createAgentEventHandler({ text: string, delta?: unknown, ) => { - const cleanedText = stripInlineDirectiveTagsForDisplay(text).text; + const cleanedText = stripInternalRuntimeContext(stripInlineDirectiveTagsForDisplay(text).text); const cleanedDelta = - typeof delta === "string" ? stripInlineDirectiveTagsForDisplay(delta).text : ""; + typeof delta === "string" + ? stripInternalRuntimeContext(stripInlineDirectiveTagsForDisplay(delta).text) + : ""; const previousRawText = chatRunState.rawBuffers.get(clientRunId) ?? ""; const mergedRawText = resolveMergedAssistantText({ previousText: previousRawText, diff --git a/src/memory-host-sdk/host/session-files.test.ts b/src/memory-host-sdk/host/session-files.test.ts index 9b077fac336..6a369b376fa 100644 --- a/src/memory-host-sdk/host/session-files.test.ts +++ b/src/memory-host-sdk/host/session-files.test.ts @@ -598,6 +598,24 @@ describe("buildSessionEntry", () => { content: "User: Actual user text", lineMap: [3], }, + { + name: "inter-session user provenance", + fileName: "inter-session-session.jsonl", + records: [ + { + type: "message", + message: { + role: "user", + content: "A background task completed. Internal relay text.", + provenance: { kind: "inter_session", sourceTool: "subagent_announce" }, + }, + }, + { type: "message", message: { role: "assistant", content: "User-facing summary." } }, + { type: "message", message: { role: "user", content: "Actual user follow-up." } }, + ], + content: "Assistant: User-facing summary.\nUser: Actual user follow-up.", + lineMap: [2, 3], + }, ] as const; for (const testCase of cases) { diff --git a/src/memory-host-sdk/host/session-files.ts b/src/memory-host-sdk/host/session-files.ts index e86e3b171e0..aa53eb8e89f 100644 --- a/src/memory-host-sdk/host/session-files.ts +++ b/src/memory-host-sdk/host/session-files.ts @@ -14,6 +14,7 @@ import { import { resolveSessionTranscriptsDirForAgent } from "../../config/sessions/paths.js"; import { isExecCompletionEvent } from "../../infra/heartbeat-events-filter.js"; import { redactSensitiveText } from "../../logging/redact.js"; +import { hasInterSessionUserProvenance } from "../../sessions/input-provenance.js"; import { isCronRunSessionKey } from "../../sessions/session-key-utils.js"; import { hashText } from "./hash.js"; @@ -504,7 +505,7 @@ export async function buildSessionEntry( continue; } const message = (record as { message?: unknown }).message as - | { role?: unknown; content?: unknown } + | { role?: unknown; content?: unknown; provenance?: unknown } | undefined; if (!message || typeof message.role !== "string") { continue; @@ -512,6 +513,9 @@ export async function buildSessionEntry( if (message.role !== "user" && message.role !== "assistant") { continue; } + if (message.role === "user" && hasInterSessionUserProvenance(message)) { + continue; + } const rawText = collectRawSessionText(message.content); if (rawText === null) { continue;