fix: keep internal completion wakes out of chat memory · openclaw/openclaw@6df120f
steipete
·
2026-04-26
·
via Recent Commits to openclaw:main
File tree
packages/memory-host-sdk/src/host
| Original file line number | Diff line number | Diff line change |
|---|
@@ -73,6 +73,9 @@ Docs: https://docs.openclaw.ai
|
73 | 73 | and honor configured `params.chat_template_kwargs` for OpenAI-compatible |
74 | 74 | completions, so vLLM/Nemotron replies stay visible instead of becoming |
75 | 75 | thinking-only. Fixes #71891. Thanks @jmystaki-create and @dennis-lynch. |
| 76 | +- Subagents/memory: keep inter-session completion wakes out of memory and |
| 77 | + dreaming session exports, and strip internal runtime-context blocks from |
| 78 | + realtime Control UI chat events. |
76 | 79 | - Agents/Claude: treat zero-token empty `stop` turns as failed provider output, |
77 | 80 | retry once, repair replay, and allow configured model fallback instead of |
78 | 81 | preserving them as successful silent replies. Fixes #71880. Thanks @MagnaAI. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -185,4 +185,32 @@ describe("buildSessionEntry", () => {
|
185 | 185 | expect(entry).not.toBeNull(); |
186 | 186 | expect(entry!.content).toBe("User: Actual user text"); |
187 | 187 | }); |
| 188 | + |
| 189 | +it("skips inter-session user messages", async () => { |
| 190 | +const jsonlLines = [ |
| 191 | +JSON.stringify({ |
| 192 | +type: "message", |
| 193 | +message: { |
| 194 | +role: "user", |
| 195 | +content: "A background task completed. Internal relay text.", |
| 196 | +provenance: { kind: "inter_session", sourceTool: "subagent_announce" }, |
| 197 | +}, |
| 198 | +}), |
| 199 | +JSON.stringify({ |
| 200 | +type: "message", |
| 201 | +message: { role: "assistant", content: "User-facing summary." }, |
| 202 | +}), |
| 203 | +JSON.stringify({ |
| 204 | +type: "message", |
| 205 | +message: { role: "user", content: "Actual user follow-up." }, |
| 206 | +}), |
| 207 | +]; |
| 208 | +const filePath = path.join(tmpDir, "inter-session-session.jsonl"); |
| 209 | +fsSync.writeFileSync(filePath, jsonlLines.join("\n")); |
| 210 | + |
| 211 | +const entry = await buildSessionEntry(filePath); |
| 212 | +expect(entry).not.toBeNull(); |
| 213 | +expect(entry!.content).toBe("Assistant: User-facing summary.\nUser: Actual user follow-up."); |
| 214 | +expect(entry!.lineMap).toEqual([2, 3]); |
| 215 | +}); |
188 | 216 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -8,6 +8,7 @@ import {
|
8 | 8 | } from "../../../../src/config/sessions/artifacts.js"; |
9 | 9 | import { resolveSessionTranscriptsDirForAgent } from "../../../../src/config/sessions/paths.js"; |
10 | 10 | import { redactSensitiveText } from "../../../../src/logging/redact.js"; |
| 11 | +import { hasInterSessionUserProvenance } from "../../../../src/sessions/input-provenance.js"; |
11 | 12 | import { hashText } from "./hash.js"; |
12 | 13 | |
13 | 14 | export type SessionFileEntry = { |
@@ -170,14 +171,17 @@ export async function buildSessionEntry(absPath: string): Promise<SessionFileEnt
|
170 | 171 | continue; |
171 | 172 | } |
172 | 173 | const message = (record as { message?: unknown }).message as |
173 | | -| { role?: unknown; content?: unknown } |
| 174 | +| { role?: unknown; content?: unknown; provenance?: unknown } |
174 | 175 | | undefined; |
175 | 176 | if (!message || typeof message.role !== "string") { |
176 | 177 | continue; |
177 | 178 | } |
178 | 179 | if (message.role !== "user" && message.role !== "assistant") { |
179 | 180 | continue; |
180 | 181 | } |
| 182 | +if (message.role === "user" && hasInterSessionUserProvenance(message)) { |
| 183 | +continue; |
| 184 | +} |
181 | 185 | const text = extractSessionText(message.content, message.role); |
182 | 186 | if (!text) { |
183 | 187 | continue; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -215,6 +215,34 @@ describe("agent event handler", () => {
|
215 | 215 | nowSpy?.mockRestore(); |
216 | 216 | }); |
217 | 217 | |
| 218 | +it("strips internal runtime context from assistant chat events", () => { |
| 219 | +const { broadcast, nodeSendToSession, nowSpy } = emitRun1AssistantText( |
| 220 | +createHarness({ now: 1_000 }), |
| 221 | +[ |
| 222 | +"Visible before.", |
| 223 | +"", |
| 224 | +"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>", |
| 225 | +"OpenClaw runtime context (internal):", |
| 226 | +"[Internal task completion event]", |
| 227 | +"secret child result", |
| 228 | +"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>", |
| 229 | +"", |
| 230 | +"Visible after.", |
| 231 | +].join("\n"), |
| 232 | +); |
| 233 | + |
| 234 | +const chatCalls = chatBroadcastCalls(broadcast); |
| 235 | +expect(chatCalls).toHaveLength(1); |
| 236 | +const payload = chatCalls[0]?.[1] as { |
| 237 | +message?: { content?: Array<{ text?: string }> }; |
| 238 | +}; |
| 239 | +expect(payload.message?.content?.[0]?.text).toBe("Visible before.\n\nVisible after."); |
| 240 | +expect(payload.message?.content?.[0]?.text).not.toContain("BEGIN_OPENCLAW_INTERNAL_CONTEXT"); |
| 241 | +expect(payload.message?.content?.[0]?.text).not.toContain("secret child result"); |
| 242 | +expect(sessionChatCalls(nodeSendToSession)).toHaveLength(1); |
| 243 | +nowSpy?.mockRestore(); |
| 244 | +}); |
| 245 | + |
218 | 246 | it.each([" NO_REPLY ", " ANNOUNCE_SKIP ", " REPLY_SKIP "])( |
219 | 247 | "does not emit chat delta for suppressed control text %s", |
220 | 248 | (replyText) => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
| 1 | +import { stripInternalRuntimeContext } from "../agents/internal-runtime-context.js"; |
1 | 2 | import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "../auto-reply/heartbeat.js"; |
2 | 3 | import { normalizeVerboseLevel } from "../auto-reply/thinking.js"; |
3 | 4 | import { |
@@ -688,9 +689,11 @@ export function createAgentEventHandler({
|
688 | 689 | text: string, |
689 | 690 | delta?: unknown, |
690 | 691 | ) => { |
691 | | -const cleanedText = stripInlineDirectiveTagsForDisplay(text).text; |
| 692 | +const cleanedText = stripInternalRuntimeContext(stripInlineDirectiveTagsForDisplay(text).text); |
692 | 693 | const cleanedDelta = |
693 | | -typeof delta === "string" ? stripInlineDirectiveTagsForDisplay(delta).text : ""; |
| 694 | +typeof delta === "string" |
| 695 | + ? stripInternalRuntimeContext(stripInlineDirectiveTagsForDisplay(delta).text) |
| 696 | + : ""; |
694 | 697 | const previousRawText = chatRunState.rawBuffers.get(clientRunId) ?? ""; |
695 | 698 | const mergedRawText = resolveMergedAssistantText({ |
696 | 699 | previousText: previousRawText, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -598,6 +598,24 @@ describe("buildSessionEntry", () => {
|
598 | 598 | content: "User: Actual user text", |
599 | 599 | lineMap: [3], |
600 | 600 | }, |
| 601 | +{ |
| 602 | +name: "inter-session user provenance", |
| 603 | +fileName: "inter-session-session.jsonl", |
| 604 | +records: [ |
| 605 | +{ |
| 606 | +type: "message", |
| 607 | +message: { |
| 608 | +role: "user", |
| 609 | +content: "A background task completed. Internal relay text.", |
| 610 | +provenance: { kind: "inter_session", sourceTool: "subagent_announce" }, |
| 611 | +}, |
| 612 | +}, |
| 613 | +{ type: "message", message: { role: "assistant", content: "User-facing summary." } }, |
| 614 | +{ type: "message", message: { role: "user", content: "Actual user follow-up." } }, |
| 615 | +], |
| 616 | +content: "Assistant: User-facing summary.\nUser: Actual user follow-up.", |
| 617 | +lineMap: [2, 3], |
| 618 | +}, |
601 | 619 | ] as const; |
602 | 620 | |
603 | 621 | for (const testCase of cases) { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -14,6 +14,7 @@ import {
|
14 | 14 | import { resolveSessionTranscriptsDirForAgent } from "../../config/sessions/paths.js"; |
15 | 15 | import { isExecCompletionEvent } from "../../infra/heartbeat-events-filter.js"; |
16 | 16 | import { redactSensitiveText } from "../../logging/redact.js"; |
| 17 | +import { hasInterSessionUserProvenance } from "../../sessions/input-provenance.js"; |
17 | 18 | import { isCronRunSessionKey } from "../../sessions/session-key-utils.js"; |
18 | 19 | import { hashText } from "./hash.js"; |
19 | 20 | |
@@ -504,14 +505,17 @@ export async function buildSessionEntry(
|
504 | 505 | continue; |
505 | 506 | } |
506 | 507 | const message = (record as { message?: unknown }).message as |
507 | | -| { role?: unknown; content?: unknown } |
| 508 | +| { role?: unknown; content?: unknown; provenance?: unknown } |
508 | 509 | | undefined; |
509 | 510 | if (!message || typeof message.role !== "string") { |
510 | 511 | continue; |
511 | 512 | } |
512 | 513 | if (message.role !== "user" && message.role !== "assistant") { |
513 | 514 | continue; |
514 | 515 | } |
| 516 | +if (message.role === "user" && hasInterSessionUserProvenance(message)) { |
| 517 | +continue; |
| 518 | +} |
515 | 519 | const rawText = collectRawSessionText(message.content); |
516 | 520 | if (rawText === null) { |
517 | 521 | continue; |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。