mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix: keep internal completion wakes out of chat memory
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<SessionFileEnt
|
||||
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;
|
||||
@@ -178,6 +179,9 @@ export async function buildSessionEntry(absPath: string): Promise<SessionFileEnt
|
||||
if (message.role !== "user" && message.role !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
if (message.role === "user" && hasInterSessionUserProvenance(message)) {
|
||||
continue;
|
||||
}
|
||||
const text = extractSessionText(message.content, message.role);
|
||||
if (!text) {
|
||||
continue;
|
||||
|
||||
@@ -215,6 +215,34 @@ describe("agent event handler", () => {
|
||||
nowSpy?.mockRestore();
|
||||
});
|
||||
|
||||
it("strips internal runtime context from assistant chat events", () => {
|
||||
const { broadcast, nodeSendToSession, nowSpy } = emitRun1AssistantText(
|
||||
createHarness({ now: 1_000 }),
|
||||
[
|
||||
"Visible before.",
|
||||
"",
|
||||
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
"OpenClaw runtime context (internal):",
|
||||
"[Internal task completion event]",
|
||||
"secret child result",
|
||||
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
"",
|
||||
"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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user