Skip to content

Commit e442b57

Browse files
authored
refactor: add compact trim lifecycle seam (#93695)
* clawdbot-d02.1.9.1.32: add compact trim lifecycle seam * fix: repair session compact type narrowing
1 parent 846591b commit e442b57

6 files changed

Lines changed: 510 additions & 61 deletions

File tree

scripts/check-session-accessor-boundary.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ const sessionCreateLifecycleWriterNames = new Set([
4141
"updateSessionStoreEntry",
4242
"ensureSessionTranscriptFile",
4343
]);
44+
const legacyManualCompactTrimNames = new Set([
45+
"archiveFileOnDisk",
46+
"readRecentSessionTranscriptLines",
47+
]);
4448

4549
export const migratedSessionAccessorFiles = new Set([
4650
"src/agents/embedded-agent-runner/compaction-successor-transcript.ts",
@@ -111,6 +115,10 @@ export const migratedTranscriptWriterFiles = new Set([
111115
"src/sessions/user-turn-transcript.ts",
112116
]);
113117

118+
export const migratedSessionCompactManualTrimFiles = new Set([
119+
"src/gateway/server-methods/sessions.ts",
120+
]);
121+
114122
function normalizeRelativePath(filePath) {
115123
return filePath.replaceAll(path.sep, "/");
116124
}
@@ -280,6 +288,15 @@ export function findGatewaySessionCreateLifecycleViolations(content, fileName =
280288
return violations;
281289
}
282290

291+
export function findSessionCompactManualTrimBoundaryViolations(content, fileName = "source.ts") {
292+
return findNamedSessionStoreViolations(
293+
content,
294+
fileName,
295+
legacyManualCompactTrimNames,
296+
"manual compact trim",
297+
);
298+
}
299+
283300
export async function main() {
284301
const repoRoot = resolveRepoRoot(import.meta.url);
285302
const readSourceRoots = resolveSourceRoots(repoRoot, [
@@ -342,11 +359,21 @@ export async function main() {
342359
"src/gateway/server-methods/sessions.ts",
343360
findViolations: findGatewaySessionCreateLifecycleViolations,
344361
});
362+
const manualCompactTrimViolations = await collectFileViolations({
363+
repoRoot,
364+
sourceRoots: resolveSourceRoots(repoRoot, ["src/gateway/server-methods"]),
365+
skipFile: (filePath) =>
366+
!migratedSessionCompactManualTrimFiles.has(
367+
normalizeRelativePath(path.relative(repoRoot, filePath)),
368+
),
369+
findViolations: findSessionCompactManualTrimBoundaryViolations,
370+
});
345371
const violations = [
346372
...readViolations,
347373
...writeViolations,
348374
...transcriptWriterViolations,
349375
...sessionCreateLifecycleViolations,
376+
...manualCompactTrimViolations,
350377
];
351378

352379
if (violations.length === 0) {

src/config/sessions/session-accessor.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
replaceSessionEntry,
2121
resolveSessionTranscriptRuntimeReadTarget,
2222
resolveSessionTranscriptRuntimeTarget,
23+
trimSessionTranscriptForManualCompact,
2324
updateSessionEntry,
2425
upsertSessionEntry,
2526
} from "./session-accessor.js";
@@ -543,6 +544,102 @@ describe("session accessor file-backed seam", () => {
543544
expect(listSessionEntries({ storePath })).toEqual([]);
544545
});
545546

547+
it("trims a manual compact transcript and clears stale token metadata", async () => {
548+
const sessionId = "11111111-1111-4111-8111-111111111111";
549+
const manualTranscriptPath = path.join(tempDir, `${sessionId}.jsonl`);
550+
const scope = {
551+
agentId: "main",
552+
sessionId,
553+
sessionKey: "agent:main:main",
554+
storePath,
555+
};
556+
const contextBudgetStatus: NonNullable<SessionEntry["contextBudgetStatus"]> = {
557+
schemaVersion: 1,
558+
source: "pre-prompt-estimate",
559+
updatedAt: 90,
560+
provider: "openai",
561+
model: "gpt-5.5",
562+
route: "fits",
563+
shouldCompact: false,
564+
estimatedPromptTokens: 10,
565+
contextTokenBudget: 100,
566+
promptBudgetBeforeReserve: 80,
567+
reserveTokens: 20,
568+
effectiveReserveTokens: 20,
569+
remainingPromptBudgetTokens: 70,
570+
overflowTokens: 0,
571+
toolResultReducibleChars: 0,
572+
messageCount: 1,
573+
unwindowedMessageCount: 1,
574+
};
575+
await upsertSessionEntry(scope, {
576+
contextBudgetStatus,
577+
inputTokens: 10,
578+
outputTokens: 20,
579+
sessionFile: manualTranscriptPath,
580+
sessionId,
581+
totalTokens: 30,
582+
totalTokensFresh: true,
583+
updatedAt: 100,
584+
});
585+
fs.writeFileSync(manualTranscriptPath, "line 1\n\nline 2\nline 3\nline 4\n", "utf-8");
586+
const updates: unknown[] = [];
587+
const unsubscribe = onSessionTranscriptUpdate((update) => updates.push(update));
588+
589+
const result = await trimSessionTranscriptForManualCompact(scope, {
590+
maxLines: 2,
591+
nowMs: 500,
592+
});
593+
594+
unsubscribe();
595+
expect(result).toMatchObject({ compacted: true, kept: 2 });
596+
const archived = result.compacted ? result.archived : "";
597+
expect(path.basename(archived)).toMatch(new RegExp(`^${sessionId}\\.jsonl\\.bak\\.`));
598+
expect(fs.readFileSync(archived, "utf-8")).toBe("line 1\n\nline 2\nline 3\nline 4\n");
599+
expect(fs.readFileSync(manualTranscriptPath, "utf-8")).toBe("line 3\nline 4\n");
600+
const updatedEntry = loadSessionEntry(scope);
601+
expect(updatedEntry).toMatchObject({
602+
sessionFile: manualTranscriptPath,
603+
sessionId,
604+
updatedAt: 500,
605+
});
606+
expect(updatedEntry?.contextBudgetStatus).toBeUndefined();
607+
expect(updatedEntry?.inputTokens).toBeUndefined();
608+
expect(updatedEntry?.outputTokens).toBeUndefined();
609+
expect(updatedEntry?.totalTokens).toBeUndefined();
610+
expect(updatedEntry?.totalTokensFresh).toBeUndefined();
611+
expect(updates).toEqual([{ sessionFile: archived }]);
612+
});
613+
614+
it("prefers the current generated transcript over a stale generated sessionFile", async () => {
615+
const currentSessionId = "11111111-1111-4111-8111-111111111111";
616+
const staleSessionId = "22222222-2222-4222-8222-222222222222";
617+
const currentTranscriptPath = path.join(tempDir, `${currentSessionId}.jsonl`);
618+
const staleTranscriptPath = path.join(tempDir, `${staleSessionId}.jsonl`);
619+
const scope = {
620+
agentId: "main",
621+
sessionId: currentSessionId,
622+
sessionKey: "agent:main:main",
623+
storePath,
624+
};
625+
await upsertSessionEntry(scope, {
626+
sessionFile: staleTranscriptPath,
627+
sessionId: currentSessionId,
628+
updatedAt: 100,
629+
});
630+
fs.writeFileSync(currentTranscriptPath, "current one\ncurrent two\n", "utf-8");
631+
fs.writeFileSync(staleTranscriptPath, "stale one\nstale two\n", "utf-8");
632+
633+
const result = await trimSessionTranscriptForManualCompact(scope, {
634+
maxLines: 1,
635+
sessionFile: staleTranscriptPath,
636+
});
637+
638+
expect(result).toMatchObject({ compacted: true, kept: 1 });
639+
expect(fs.readFileSync(currentTranscriptPath, "utf-8")).toBe("current two\n");
640+
expect(fs.readFileSync(staleTranscriptPath, "utf-8")).toBe("stale one\nstale two\n");
641+
});
642+
546643
it("rejects transcript writes without a session key or explicit file", async () => {
547644
await expect(
548645
appendTranscriptEvent({ sessionId: "session-1", storePath }, { type: "metadata" }),

0 commit comments

Comments
 (0)