diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index c5b840402..e27d30fa7 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -676,24 +676,39 @@ export async function getReplyFromConfig( } } - // For new main sessions, prepend a provider snapshot. - // Note: We intentionally do NOT prepend queued system events to the user prompt, - // since that bloats session logs (token cost) and clutters chat history. + // Prepend queued system events (transitions only) and (for new main sessions) a provider snapshot. + // Token efficiency: we filter out periodic/heartbeat noise and keep the lines compact. const isGroupSession = typeof ctx.From === "string" && (ctx.From.includes("@g.us") || ctx.From.startsWith("group:")); const isMainSession = !isGroupSession && sessionKey === (sessionCfg?.mainKey ?? "main"); if (isMainSession) { - // Drain (discard) queued system events so they remain ephemeral. - // They are still available via presence/health in the gateway UI. - drainSystemEvents(); + const compactSystemEvent = (line: string): string | null => { + const trimmed = line.trim(); + if (!trimmed) return null; + const lower = trimmed.toLowerCase(); + if (lower.includes("reason periodic")) return null; + if (lower.includes("heartbeat")) return null; + if (trimmed.startsWith("Node:")) { + // Drop the chatty "last input … ago" segment; keep connect/disconnect/launch reasons. + return trimmed.replace(/ · last input [^·]+/i, "").trim(); + } + return trimmed; + }; + + const systemLines: string[] = []; + const queued = drainSystemEvents(); + systemLines.push( + ...queued.map(compactSystemEvent).filter((v): v is string => Boolean(v)), + ); if (isNewSession) { const summary = await buildProviderSummary(cfg); - if (summary.length > 0) { - const block = summary.map((l) => `System: ${l}`).join("\n"); - prefixedBodyBase = `${block}\n\n${prefixedBodyBase}`; - } + if (summary.length > 0) systemLines.unshift(...summary); + } + if (systemLines.length > 0) { + const block = systemLines.map((l) => `System: ${l}`).join("\n"); + prefixedBodyBase = `${block}\n\n${prefixedBodyBase}`; } } if ( diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index 166badfc1..670bb5802 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -1845,17 +1845,7 @@ describe("gateway server", () => { await server.close(); }); - test("chat.history strips injected System blocks and caps payload bytes", async () => { - const firstContentText = (msg: unknown): string | undefined => { - if (!msg || typeof msg !== "object") return undefined; - const content = (msg as { content?: unknown }).content; - if (!Array.isArray(content) || content.length === 0) return undefined; - const first = content[0]; - if (!first || typeof first !== "object") return undefined; - const text = (first as { text?: unknown }).text; - return typeof text === "string" ? text : undefined; - }; - + test("chat.history caps payload bytes", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-gw-")); testSessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( @@ -1873,35 +1863,9 @@ describe("gateway server", () => { "utf-8", ); - const injected = - "System: Node: Peter’s Mac · app 2.0.0 · last input 0s ago · mode local · reason periodic\n" + - "System: WhatsApp gateway connected.\n\n" + - "Hello from user"; - await fs.writeFile( - path.join(dir, "sess-main.jsonl"), - JSON.stringify({ - message: { - role: "user", - content: [{ type: "text", text: injected }], - timestamp: Date.now(), - }, - }), - "utf-8", - ); - const { server, ws } = await startServerWithClient(); await connectOk(ws); - const scrubbedRes = await rpcReq<{ messages?: unknown[] }>( - ws, - "chat.history", - { sessionKey: "main", limit: 5 }, - ); - expect(scrubbedRes.ok).toBe(true); - const scrubbedMsgs = scrubbedRes.payload?.messages ?? []; - expect(scrubbedMsgs.length).toBe(1); - expect(firstContentText(scrubbedMsgs[0])).toBe("Hello from user"); - const bigText = "x".repeat(300_000); const largeLines: string[] = []; for (let i = 0; i < 60; i += 1) { diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 02de061c8..03039c6cf 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -351,42 +351,6 @@ function readSessionMessages( return messages; } -function stripInjectedSystemBlock(text: string): string { - if (!text.startsWith("System: ")) return text; - const sep = text.indexOf("\n\n"); - if (sep <= 0) return text; - const head = text.slice(0, sep); - const lines = head.split("\n"); - if (lines.length === 0) return text; - if (!lines.every((l) => l.startsWith("System: "))) return text; - return text.slice(sep + 2); -} - -function scrubInjectedSystemBlocks(messages: unknown[]): unknown[] { - let changed = false; - const out = messages.map((msg) => { - if (!msg || typeof msg !== "object") return msg; - const obj = msg as Record; - if (obj.role !== "user") return msg; - const content = obj.content; - if (!Array.isArray(content) || content.length === 0) return msg; - const first = content[0]; - if (!first || typeof first !== "object") return msg; - const firstObj = first as Record; - if (firstObj.type !== "text") return msg; - const text = firstObj.text; - if (typeof text !== "string") return msg; - const stripped = stripInjectedSystemBlock(text); - if (stripped === text) return msg; - changed = true; - const nextFirst = { ...firstObj, text: stripped }; - const nextContent = [...content]; - nextContent[0] = nextFirst; - return { ...obj, content: nextContent }; - }); - return changed ? out : messages; -} - function jsonUtf8Bytes(value: unknown): number { try { return Buffer.byteLength(JSON.stringify(value), "utf8"); @@ -916,9 +880,8 @@ export async function startGatewayServer( const max = typeof limit === "number" ? limit : 200; const sliced = rawMessages.length > max ? rawMessages.slice(-max) : rawMessages; - const scrubbed = scrubInjectedSystemBlocks(sliced); const capped = capArrayByJsonBytes( - scrubbed, + sliced, MAX_CHAT_HISTORY_MESSAGES_BYTES, ).items; const thinkingLevel = @@ -1895,9 +1858,8 @@ export async function startGatewayServer( const max = Math.min(hardMax, requested); const sliced = rawMessages.length > max ? rawMessages.slice(-max) : rawMessages; - const scrubbed = scrubInjectedSystemBlocks(sliced); const capped = capArrayByJsonBytes( - scrubbed, + sliced, MAX_CHAT_HISTORY_MESSAGES_BYTES, ).items; const thinkingLevel = @@ -2412,13 +2374,17 @@ export async function startGatewayServer( reason, tags, }); + const isNodePresenceLine = text.startsWith("Node:"); const normalizedReason = (reason ?? "").toLowerCase(); const looksPeriodic = normalizedReason.startsWith("periodic") || normalizedReason === "heartbeat"; - const isNodePresenceLine = text.startsWith("Node:"); if (!(isNodePresenceLine && looksPeriodic)) { - enqueueSystemEvent(text); + const compactNodeText = + isNodePresenceLine && (host || ip || version || mode || reason) + ? `Node: ${host?.trim() || "Unknown"}${ip ? ` (${ip})` : ""} · app ${version?.trim() || "unknown"} · mode ${mode?.trim() || "unknown"} · reason ${reason?.trim() || "event"}` + : text; + enqueueSystemEvent(compactNodeText); } presenceVersion += 1; broadcast(