Skip to content

Commit 26d1293

Browse files
committed
fix(compaction): restrict post-usage marker to structured records
ClawSweeper re-review flagged a P1: transcriptLineHasPostUsageCompactionMarker also matched the post-compaction refresh phrases ("[Post-compaction context refresh]", "Session was just compacted.") in free message text. Those phrases are prompt-injected context, not persisted markers, so ordinary user/tool content echoing them could masquerade as a compaction marker and wrongly drop stale-usage pressure. Detect only structured compaction records (type "compaction"/"session.compacted", the records the runtime actually writes via transcript-file-state / session-manager) and drop the free-text fallback plus the now-unused collectTranscriptText helper. Add a regression test proving ordinary transcript text echoing the refresh phrase (with no structured marker) keeps preflight compaction conservative.
1 parent 74ba2e3 commit 26d1293

2 files changed

Lines changed: 83 additions & 36 deletions

File tree

src/auto-reply/reply/agent-runner-memory.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,6 +1662,78 @@ describe("runMemoryFlushIfNeeded", () => {
16621662
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
16631663
});
16641664

1665+
it("does not treat ordinary transcript text echoing the refresh phrase as a compaction marker", async () => {
1666+
const sessionFile = path.join(rootDir, "phrase-echo-without-marker.jsonl");
1667+
// Same stale-usage shape as the marker tests, but the post-compaction phrases
1668+
// appear only as ordinary user/assistant message text — there is NO structured
1669+
// {type:"compaction"} record. The estimator must keep the stale usage active and
1670+
// still compact, instead of being fooled into dropping pressure.
1671+
await fs.writeFile(
1672+
sessionFile,
1673+
[
1674+
JSON.stringify({
1675+
message: {
1676+
role: "assistant",
1677+
content: [{ type: "text", text: "pre-compaction giant turn" }],
1678+
usage: { input: 240_000, output: 120_000 },
1679+
},
1680+
}),
1681+
JSON.stringify({
1682+
message: {
1683+
role: "tool",
1684+
content: `pre-compaction stale tool result ${"x".repeat(450_000)}`,
1685+
},
1686+
}),
1687+
JSON.stringify({
1688+
message: {
1689+
role: "user",
1690+
content: [
1691+
{
1692+
type: "text",
1693+
text: "[Post-compaction context refresh]\n\nSession was just compacted.",
1694+
},
1695+
],
1696+
},
1697+
}),
1698+
].join("\n"),
1699+
"utf8",
1700+
);
1701+
1702+
registerMemoryFlushPlanResolverForTest(() => ({
1703+
softThresholdTokens: 4_000,
1704+
forceFlushTranscriptBytes: 1_000_000_000,
1705+
reserveTokensFloor: 0,
1706+
prompt: "Pre-compaction memory flush.\nNO_REPLY",
1707+
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
1708+
relativePath: "memory/2023-11-14.md",
1709+
}));
1710+
const sessionEntry: SessionEntry = {
1711+
sessionId: "session",
1712+
sessionFile,
1713+
updatedAt: Date.now(),
1714+
totalTokensFresh: false,
1715+
};
1716+
1717+
await runPreflightCompactionIfNeeded({
1718+
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
1719+
followupRun: createTestFollowupRun({
1720+
sessionId: "session",
1721+
sessionFile,
1722+
sessionKey: "main",
1723+
}),
1724+
defaultModel: "anthropic/claude-opus-4-6",
1725+
agentCfgContextTokens: 100_000,
1726+
sessionEntry,
1727+
sessionStore: { main: sessionEntry },
1728+
sessionKey: "main",
1729+
storePath: path.join(rootDir, "sessions.json"),
1730+
isHeartbeat: false,
1731+
replyOperation: createReplyOperation(),
1732+
});
1733+
1734+
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalled();
1735+
});
1736+
16651737
it("skips OpenClaw preflight compaction for persisted Codex runtime sessions", async () => {
16661738
registerMemoryFlushPlanResolverForTest(() => ({
16671739
softThresholdTokens: 4_000,

src/auto-reply/reply/agent-runner-memory.ts

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -360,52 +360,27 @@ function parseUsageFromTranscriptLine(line: string): ReturnType<typeof normalize
360360
return undefined;
361361
}
362362

363-
function collectTranscriptText(value: unknown): string {
364-
if (typeof value === "string") {
365-
return value;
366-
}
367-
if (!Array.isArray(value)) {
368-
return "";
369-
}
370-
return value
371-
.map((item) => {
372-
if (!item || typeof item !== "object" || Array.isArray(item)) {
373-
return "";
374-
}
375-
const text = (item as { text?: unknown }).text;
376-
return typeof text === "string" ? text : "";
377-
})
378-
.filter(Boolean)
379-
.join("\n");
380-
}
381-
382363
function transcriptLineHasPostUsageCompactionMarker(line: string): boolean {
383364
const trimmed = line.trim();
384365
if (!trimmed) {
385366
return false;
386367
}
387368
try {
369+
// Only trust structured compaction records (written by the runtime at
370+
// compaction time, e.g. transcript-file-state / session-manager). The
371+
// post-compaction refresh phrases are prompt-injected context, so matching
372+
// them in free message text would let ordinary user/tool content that
373+
// echoes the phrase masquerade as a marker and wrongly drop stale-usage
374+
// pressure.
388375
const parsed = JSON.parse(trimmed) as {
389376
type?: unknown;
390-
message?: { content?: unknown };
391-
payload?: { type?: unknown; text?: unknown };
377+
payload?: { type?: unknown };
392378
};
393-
if (parsed.type === "compaction" || parsed.type === "session.compacted") {
394-
return true;
395-
}
396-
const payloadType = parsed.payload?.type;
397-
if (payloadType === "compaction" || payloadType === "session.compacted") {
398-
return true;
399-
}
400-
const text = [
401-
collectTranscriptText(parsed.message?.content),
402-
collectTranscriptText(parsed.payload?.text),
403-
]
404-
.filter(Boolean)
405-
.join("\n");
406379
return (
407-
text.includes("[Post-compaction context refresh]") ||
408-
text.includes("Session was just compacted.")
380+
parsed.type === "compaction" ||
381+
parsed.type === "session.compacted" ||
382+
parsed.payload?.type === "compaction" ||
383+
parsed.payload?.type === "session.compacted"
409384
);
410385
} catch {
411386
return false;

0 commit comments

Comments
 (0)