dist / memory / sessionCompact.js
dist / memory / sessionCompact.js
"use strict";
/**
* @file sessionCompact.ts
* F2 (direction F): session compaction — the chat history cannot be REWRITTEN
* by a plugin (platform limit, §5.3/T13), but it CAN be read (pullHistory) and
* DIGESTED: the conversation is compressed into dense memory notes tagged
* "session-compact", so a fresh chat inherits the state via memory injection.
*
* Flow (deferred by design):
* 1. agent (or gauge at ≥90%) calls requestCompact() → JSON request file;
* 2. the NEXT incoming message's preprocessor hook runs runCompactHook():
* pullHistory → digest prompt → chat's own model (tokenSource — idle
* during preprocessing, no on-demand load) → parse → store to memory.
* Deferral avoids deadlock: mid-turn the model is busy generating, so the
* tool itself never summarizes — it only schedules.
* Never throws in the hook path; bounded by a timeout; poison-capped.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDigestPrompt = buildDigestPrompt;
exports.sanitizeHistoryText = sanitizeHistoryText;
exports.parseCompactNotes = parseCompactNotes;
exports.persistDigest = persistDigest;
exports.requestCompact = requestCompact;
exports.peekCompactRequest = peekCompactRequest;
exports.clearCompactRequest = clearCompactRequest;
exports.bumpCompactAttempt = bumpCompactAttempt;
exports.runCompactHook = runCompactHook;
exports.compactTool = void 0;
exports.setRequestFileForTests = setRequestFileForTests;
const fs_1 = require("fs/promises");
const fsSync = require("fs");
const path_1 = require("path");
const os_1 = require("os");
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const constants_1 = require("./constants");
const projectMemory_1 = require("./projectMemory");
const DEFAULT_REQUEST_FILE = (0, path_1.join)((0, os_1.homedir)(), ".maestro-toolbox", "compact-request.json");
let REQUEST_FILE = DEFAULT_REQUEST_FILE;
function setRequestFileForTests(p) { REQUEST_FILE = p ?? DEFAULT_REQUEST_FILE; }
const MAX_INPUT_CHARS = 24_000;
const MAX_PER_MSG_CHARS = 700;
const MAX_NOTES = 15;
const NOTE_MAX_CHARS = 400;
const MAX_ATTEMPTS = 3;
const DIGEST_TIMEOUT_MS = 90_000; // generous: local 27B ~12-15 t/s, digest up to 700 tok
// ── request lifecycle ────────────────────────────────────────────────────
async function requestCompact(meta = {}) {
const entry = { at: Date.now(), source: meta.source ?? "tool", focus: String(meta.focus ?? "").slice(0, 500), note: String(meta.note ?? "").slice(0, 500), attempts: 0 };
await (0, fs_1.mkdir)((0, path_1.dirname)(REQUEST_FILE), { recursive: true });
await (0, fs_1.writeFile)(REQUEST_FILE, JSON.stringify(entry, null, 2), "utf8");
return entry;
}
async function peekCompactRequest() {
try {
const e = JSON.parse(await (0, fs_1.readFile)(REQUEST_FILE, "utf8"));
if (typeof e.at !== "number" || typeof e.attempts !== "number")
return null;
return e;
}
catch {
return null;
}
}
async function clearCompactRequest() {
await (0, fs_1.rm)(REQUEST_FILE, { force: true }).catch(() => { });
}
async function bumpCompactAttempt() {
const e = await peekCompactRequest();
if (!e)
return null;
e.attempts++;
try {
await (0, fs_1.writeFile)(REQUEST_FILE, JSON.stringify(e, null, 2), "utf8");
}
catch { }
return e;
}
// ── digest prompt + parsing ──────────────────────────────────────────────
/** Text parts of one ChatMessage/raw message (defensive, mirrors contextGauge). */
function textOfMessage(m) {
try {
if (typeof m?.getText === "function")
return m.getText() ?? ""; // real ChatMessage API (pullHistory)
const data = typeof m?.get === "function" ? m.get() : m;
const content = data?.content;
if (typeof content === "string")
return content;
if (Array.isArray(content)) {
let s = "";
for (const p of content) {
if (p && p.type === "text" && typeof p.content === "string")
s += p.content;
}
return s;
}
}
catch { }
return "";
}
/** F3: strip prompt-injection artifacts — digest must see only real conversation text. */
function sanitizeHistoryText(t) {
let s = String(t ?? "");
const cut = s.indexOf("[Persistent Memory");
if (cut > 0)
s = s.slice(0, cut);
return s.split("\n").filter((l) => !/\[Persistent Memory|\[🔍 Phase:|⚠️ \[System:|⚠️ \[CONTEXT GAUGE|📦 \[Session compaction|💡 \*\*Delegation|⛔ ACTIVE RESTRICTIONS|\[System: /.test(l)
&& !/^Use this knowledge naturally/.test(l.trim())).join("\n").replace(/\n+---\s*$/, "").trim();
}
/** Budgeted conversation block: newest messages win, per-msg clamp. */
function buildDigestPrompt(messages, opts = {}) {
const kept = [];
let budget = MAX_INPUT_CHARS;
for (let i = messages.length - 1; i >= 0 && budget > 0; i--) {
const raw = (messages[i].text ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_PER_MSG_CHARS);
if (!raw)
continue;
const line = `${messages[i].role}: ${raw}`;
if (line.length > budget)
break;
budget -= line.length;
kept.unshift(line);
}
return `You are a session-compaction system. Compress the conversation into DENSE notes so a fresh assistant WITHOUT any other history can continue seamlessly.
${opts.focus ? `FOCUS: ${opts.focus}\n` : ""}${opts.note ? `TRIGGER NOTE: ${opts.note}\n` : ""}
Output ONLY lines of this exact format (no preamble, no numbering):
FACT: <one self-sufficient sentence, <=400 chars> | CATEGORY: <${constants_1.VALID_CATEGORIES.join("/")}> | TAGS: <comma-separated>
Cover, most important first: current task state (done / in progress / next step), decisions + reasons, exact file paths and identifiers, user constraints/preferences, numbers and facts about the environment, open questions. Max ${MAX_NOTES} lines.` +
`\n\nCONVERSATION (oldest→newest, possibly truncated):\n${kept.join("\n")}`;
}
function parseCompactNotes(raw) {
const notes = [];
const push = (content, category, tags) => {
content = (content ?? "").trim();
if (content.length < 8)
return;
const cat = constants_1.VALID_CATEGORIES.includes((category ?? "").toLowerCase()) ? category.toLowerCase() : "context";
const tagList = (tags ?? "").split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length >= 2 && t.length <= 50).slice(0, 5);
notes.push({ content: content.slice(0, NOTE_MAX_CHARS), category: cat, tags: tagList });
};
for (const line of String(raw ?? "").split("\n")) {
const t = line.trim();
if (!t.toUpperCase().startsWith("FACT:"))
continue;
const fact = /FACT:\s*(.+?)(?=\s*\|\s*CATEGORY:|\s*$)/i.exec(t);
const cat = /CATEGORY:\s*([\w-]+)/i.exec(t);
const tags = /TAGS:\s*([^|]+)/i.exec(t);
push(fact?.[1], cat?.[1], tags?.[1]);
if (notes.length >= MAX_NOTES)
return notes;
}
if (notes.length === 0) { // fallback: bullet lines
for (const line of String(raw ?? "").split("\n")) {
const t = line.trim();
const m2 = /^[-•*]\s+(.{8,})$/.exec(t);
if (m2)
push(m2[1], "context", "");
if (notes.length >= MAX_NOTES)
break;
}
}
return notes;
}
// ── persistence ──────────────────────────────────────────────────────────
function persistDigest(notes, db, engine, cfg) {
let stored = 0;
for (const n of notes) {
try {
const target = (0, projectMemory_1.resolveProjectMemoryTarget)(cfg, { scope: cfg?.activeProject ? "project" : "global" });
const tags = [...new Set([...n.tags, "session-compact"])];
const finalTags = target.scope === "project" && target.project
? (0, projectMemory_1.buildProjectMemoryTags)(tags, target.project)
: tags;
const id = db.store(n.content, n.category, finalTags, 0.9, "session-compact", null, target.scope, target.project);
engine.indexMemory(id, n.content, finalTags, n.category);
stored++;
}
catch { }
}
return { stored };
}
// ── orchestration (preprocessor hook) ────────────────────────────────────
async function runCompactHook(ctl, engine, db, cfg, opts = {}) {
const TRACE = (0, path_1.join)((0, path_1.dirname)(REQUEST_FILE), "compact-trace.log");
try { fsSync.appendFileSync(TRACE, new Date().toISOString() + " hook-enter\n"); } catch { }
const req = await peekCompactRequest();
try { fsSync.appendFileSync(TRACE, "peek=" + (req ? "attempts:" + req.attempts : "null") + "\n"); } catch { }
if (!req)
return null;
if (req.attempts >= MAX_ATTEMPTS) {
await clearCompactRequest();
return "⚠️ [Session compaction dropped after 3 failed attempts — save anything critical via Remember manually.]";
}
try {
const chat = await ctl.pullHistory();
const msgs = (chat?.getMessagesArray?.() ?? [])
.map((m) => {
let role = "msg";
try {
role = (typeof m?.getRole === "function" ? m.getRole() : (m.get?.() ?? m)?.role) ?? role;
}
catch { }
return { role, text: sanitizeHistoryText(textOfMessage(m)) };
})
.filter((x) => x.text);
if (msgs.length === 0) {
await clearCompactRequest();
return "[Session compaction: history had no text — nothing to digest.]";
}
// chat model via tokenSource; respond -> stream-promise: iterate WITHOUT await
const prompt = buildDigestPrompt(msgs, { focus: req.focus, note: req.note });
let stream = null;
try {
const ts = typeof ctl.tokenSource === "function" ? await ctl.tokenSource() : null;
if (ts && typeof ts.respond === "function")
stream = ts.respond([{ role: "user", content: prompt }], { maxTokens: 700, temperature: 0.2 });
}
catch { /* fall through to attached client */ }
if (!stream) {
const client = (opts && opts.client) || ctl.client;
if (!client || !client.llm) throw new Error("no model path available");
const loaded = await client.llm.listLoaded();
if (!Array.isArray(loaded) || loaded.length === 0)
throw new Error("no loaded model for digest");
const model = await client.llm.model(loaded[0].identifier);
stream = model.respond([{ role: "user", content: prompt }], { maxTokens: 700, temperature: 0.2 });
}
let raw = "";
const deadline = Date.now() + DIGEST_TIMEOUT_MS;
for await (const chunk of stream) {
raw += typeof chunk === "string" ? chunk : (chunk?.content ?? chunk?.text ?? "");
if (Date.now() > deadline)
throw new Error("digest timeout");
if (raw.length > 20_000)
break;
}
const notes = parseCompactNotes(raw);
if (notes.length === 0) {
await clearCompactRequest();
return "[Session compaction: model produced no notes — skipped.]";
}
const r = persistDigest(notes, db, engine, cfg ?? {});
await clearCompactRequest();
const scope = cfg?.activeProject ? `project:${cfg.activeProject}` : "global";
return `📦 [Session compaction done: ${r.stored}/${notes.length} digest notes stored to memory (scope ${scope}, tags: session-compact). A NEW chat will inherit them via memory injection — safe to continue there.]`;
}
catch (e) {
try { fsSync.appendFileSync(TRACE, "ERR " + (e && e.stack ? String(e.stack).split("\n").slice(0,3).join(" | ") : String(e)) + "\n"); } catch { }
const bumped = await bumpCompactAttempt();
const n = bumped?.attempts ?? "?";
return `[Session compaction failed (attempt ${n}/3): ${e instanceof Error ? e.message.slice(0, 120) : String(e).slice(0, 120)}] — will retry on next message.`;
}
}
// ── the tool (schedules only — see header comment for why) ───────────────
const compactTool = (0, sdk_1.tool)({
name: "compact_session",
description: "Schedule a session digest: this chat's history will be compressed into memory notes (tags: session-compact) so a new chat inherits the state. The digest actually runs at the NEXT incoming user message (history + chat model are only available to the preprocessor). Use when the CONTEXT GAUGE warning fires or before proposing a new chat. Also save critical facts via Remember yourself as belt-and-braces insurance.",
parameters: {
focus: zod_1.z.string().max(500).optional().describe("What the digest must prioritize (task state, decisions, files, open questions)"),
},
implementation: async ({ focus }) => {
const existing = await peekCompactRequest();
if (existing && existing.attempts < MAX_ATTEMPTS) {
return `📦 Already scheduled (${existing.source}, ${new Date(existing.at).toISOString()}). The digest runs on the next message in this chat. Nothing to do now.`;
}
const e = await requestCompact({ source: "tool", focus: focus ?? "" });
return "📦 Session compaction scheduled. At the next incoming message the preprocessor will: read this chat's history → digest it into memory notes (tags: session-compact) via the chat model → confirm. Now: (1) store anything CRITICAL via Remember (insurance — in case this chat dies before the next message), (2) wrap up your current step, (3) recommend the user to start a new chat when convenient.";
},
});
exports.compactTool = compactTool;
//# sourceMappingURL=sessionCompact.js.map
"use strict";
/**
* @file sessionCompact.ts
* F2 (direction F): session compaction — the chat history cannot be REWRITTEN
* by a plugin (platform limit, §5.3/T13), but it CAN be read (pullHistory) and
* DIGESTED: the conversation is compressed into dense memory notes tagged
* "session-compact", so a fresh chat inherits the state via memory injection.
*
* Flow (deferred by design):
* 1. agent (or gauge at ≥90%) calls requestCompact() → JSON request file;
* 2. the NEXT incoming message's preprocessor hook runs runCompactHook():
* pullHistory → digest prompt → chat's own model (tokenSource — idle
* during preprocessing, no on-demand load) → parse → store to memory.
* Deferral avoids deadlock: mid-turn the model is busy generating, so the
* tool itself never summarizes — it only schedules.
* Never throws in the hook path; bounded by a timeout; poison-capped.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDigestPrompt = buildDigestPrompt;
exports.sanitizeHistoryText = sanitizeHistoryText;
exports.parseCompactNotes = parseCompactNotes;
exports.persistDigest = persistDigest;
exports.requestCompact = requestCompact;
exports.peekCompactRequest = peekCompactRequest;
exports.clearCompactRequest = clearCompactRequest;
exports.bumpCompactAttempt = bumpCompactAttempt;
exports.runCompactHook = runCompactHook;
exports.compactTool = void 0;
exports.setRequestFileForTests = setRequestFileForTests;
const fs_1 = require("fs/promises");
const fsSync = require("fs");
const path_1 = require("path");
const os_1 = require("os");
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const constants_1 = require("./constants");
const projectMemory_1 = require("./projectMemory");
const DEFAULT_REQUEST_FILE = (0, path_1.join)((0, os_1.homedir)(), ".maestro-toolbox", "compact-request.json");
let REQUEST_FILE = DEFAULT_REQUEST_FILE;
function setRequestFileForTests(p) { REQUEST_FILE = p ?? DEFAULT_REQUEST_FILE; }
const MAX_INPUT_CHARS = 24_000;
const MAX_PER_MSG_CHARS = 700;
const MAX_NOTES = 15;
const NOTE_MAX_CHARS = 400;
const MAX_ATTEMPTS = 3;
const DIGEST_TIMEOUT_MS = 90_000; // generous: local 27B ~12-15 t/s, digest up to 700 tok
// ── request lifecycle ────────────────────────────────────────────────────
async function requestCompact(meta = {}) {
const entry = { at: Date.now(), source: meta.source ?? "tool", focus: String(meta.focus ?? "").slice(0, 500), note: String(meta.note ?? "").slice(0, 500), attempts: 0 };
await (0, fs_1.mkdir)((0, path_1.dirname)(REQUEST_FILE), { recursive: true });
await (0, fs_1.writeFile)(REQUEST_FILE, JSON.stringify(entry, null, 2), "utf8");
return entry;
}
async function peekCompactRequest() {
try {
const e = JSON.parse(await (0, fs_1.readFile)(REQUEST_FILE, "utf8"));
if (typeof e.at !== "number" || typeof e.attempts !== "number")
return null;
return e;
}
catch {
return null;
}
}
async function clearCompactRequest() {
await (0, fs_1.rm)(REQUEST_FILE, { force: true }).catch(() => { });
}
async function bumpCompactAttempt() {
const e = await peekCompactRequest();
if (!e)
return null;
e.attempts++;
try {
await (0, fs_1.writeFile)(REQUEST_FILE, JSON.stringify(e, null, 2), "utf8");
}
catch { }
return e;
}
// ── digest prompt + parsing ──────────────────────────────────────────────
/** Text parts of one ChatMessage/raw message (defensive, mirrors contextGauge). */
function textOfMessage(m) {
try {
if (typeof m?.getText === "function")
return m.getText() ?? ""; // real ChatMessage API (pullHistory)
const data = typeof m?.get === "function" ? m.get() : m;
const content = data?.content;
if (typeof content === "string")
return content;
if (Array.isArray(content)) {
let s = "";
for (const p of content) {
if (p && p.type === "text" && typeof p.content === "string")
s += p.content;
}
return s;
}
}
catch { }
return "";
}
/** F3: strip prompt-injection artifacts — digest must see only real conversation text. */
function sanitizeHistoryText(t) {
let s = String(t ?? "");
const cut = s.indexOf("[Persistent Memory");
if (cut > 0)
s = s.slice(0, cut);
return s.split("\n").filter((l) => !/\[Persistent Memory|\[🔍 Phase:|⚠️ \[System:|⚠️ \[CONTEXT GAUGE|📦 \[Session compaction|💡 \*\*Delegation|⛔ ACTIVE RESTRICTIONS|\[System: /.test(l)
&& !/^Use this knowledge naturally/.test(l.trim())).join("\n").replace(/\n+---\s*$/, "").trim();
}
/** Budgeted conversation block: newest messages win, per-msg clamp. */
function buildDigestPrompt(messages, opts = {}) {
const kept = [];
let budget = MAX_INPUT_CHARS;
for (let i = messages.length - 1; i >= 0 && budget > 0; i--) {
const raw = (messages[i].text ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_PER_MSG_CHARS);
if (!raw)
continue;
const line = `${messages[i].role}: ${raw}`;
if (line.length > budget)
break;
budget -= line.length;
kept.unshift(line);
}
return `You are a session-compaction system. Compress the conversation into DENSE notes so a fresh assistant WITHOUT any other history can continue seamlessly.
${opts.focus ? `FOCUS: ${opts.focus}\n` : ""}${opts.note ? `TRIGGER NOTE: ${opts.note}\n` : ""}
Output ONLY lines of this exact format (no preamble, no numbering):
FACT: <one self-sufficient sentence, <=400 chars> | CATEGORY: <${constants_1.VALID_CATEGORIES.join("/")}> | TAGS: <comma-separated>
Cover, most important first: current task state (done / in progress / next step), decisions + reasons, exact file paths and identifiers, user constraints/preferences, numbers and facts about the environment, open questions. Max ${MAX_NOTES} lines.` +
`\n\nCONVERSATION (oldest→newest, possibly truncated):\n${kept.join("\n")}`;
}
function parseCompactNotes(raw) {
const notes = [];
const push = (content, category, tags) => {
content = (content ?? "").trim();
if (content.length < 8)
return;
const cat = constants_1.VALID_CATEGORIES.includes((category ?? "").toLowerCase()) ? category.toLowerCase() : "context";
const tagList = (tags ?? "").split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length >= 2 && t.length <= 50).slice(0, 5);
notes.push({ content: content.slice(0, NOTE_MAX_CHARS), category: cat, tags: tagList });
};
for (const line of String(raw ?? "").split("\n")) {
const t = line.trim();
if (!t.toUpperCase().startsWith("FACT:"))
continue;
const fact = /FACT:\s*(.+?)(?=\s*\|\s*CATEGORY:|\s*$)/i.exec(t);
const cat = /CATEGORY:\s*([\w-]+)/i.exec(t);
const tags = /TAGS:\s*([^|]+)/i.exec(t);
push(fact?.[1], cat?.[1], tags?.[1]);
if (notes.length >= MAX_NOTES)
return notes;
}
if (notes.length === 0) { // fallback: bullet lines
for (const line of String(raw ?? "").split("\n")) {
const t = line.trim();
const m2 = /^[-•*]\s+(.{8,})$/.exec(t);
if (m2)
push(m2[1], "context", "");
if (notes.length >= MAX_NOTES)
break;
}
}
return notes;
}
// ── persistence ──────────────────────────────────────────────────────────
function persistDigest(notes, db, engine, cfg) {
let stored = 0;
for (const n of notes) {
try {
const target = (0, projectMemory_1.resolveProjectMemoryTarget)(cfg, { scope: cfg?.activeProject ? "project" : "global" });
const tags = [...new Set([...n.tags, "session-compact"])];
const finalTags = target.scope === "project" && target.project
? (0, projectMemory_1.buildProjectMemoryTags)(tags, target.project)
: tags;
const id = db.store(n.content, n.category, finalTags, 0.9, "session-compact", null, target.scope, target.project);
engine.indexMemory(id, n.content, finalTags, n.category);
stored++;
}
catch { }
}
return { stored };
}
// ── orchestration (preprocessor hook) ────────────────────────────────────
async function runCompactHook(ctl, engine, db, cfg, opts = {}) {
const TRACE = (0, path_1.join)((0, path_1.dirname)(REQUEST_FILE), "compact-trace.log");
try { fsSync.appendFileSync(TRACE, new Date().toISOString() + " hook-enter\n"); } catch { }
const req = await peekCompactRequest();
try { fsSync.appendFileSync(TRACE, "peek=" + (req ? "attempts:" + req.attempts : "null") + "\n"); } catch { }
if (!req)
return null;
if (req.attempts >= MAX_ATTEMPTS) {
await clearCompactRequest();
return "⚠️ [Session compaction dropped after 3 failed attempts — save anything critical via Remember manually.]";
}
try {
const chat = await ctl.pullHistory();
const msgs = (chat?.getMessagesArray?.() ?? [])
.map((m) => {
let role = "msg";
try {
role = (typeof m?.getRole === "function" ? m.getRole() : (m.get?.() ?? m)?.role) ?? role;
}
catch { }
return { role, text: sanitizeHistoryText(textOfMessage(m)) };
})
.filter((x) => x.text);
if (msgs.length === 0) {
await clearCompactRequest();
return "[Session compaction: history had no text — nothing to digest.]";
}
// chat model via tokenSource; respond -> stream-promise: iterate WITHOUT await
const prompt = buildDigestPrompt(msgs, { focus: req.focus, note: req.note });
let stream = null;
try {
const ts = typeof ctl.tokenSource === "function" ? await ctl.tokenSource() : null;
if (ts && typeof ts.respond === "function")
stream = ts.respond([{ role: "user", content: prompt }], { maxTokens: 700, temperature: 0.2 });
}
catch { /* fall through to attached client */ }
if (!stream) {
const client = (opts && opts.client) || ctl.client;
if (!client || !client.llm) throw new Error("no model path available");
const loaded = await client.llm.listLoaded();
if (!Array.isArray(loaded) || loaded.length === 0)
throw new Error("no loaded model for digest");
const model = await client.llm.model(loaded[0].identifier);
stream = model.respond([{ role: "user", content: prompt }], { maxTokens: 700, temperature: 0.2 });
}
let raw = "";
const deadline = Date.now() + DIGEST_TIMEOUT_MS;
for await (const chunk of stream) {
raw += typeof chunk === "string" ? chunk : (chunk?.content ?? chunk?.text ?? "");
if (Date.now() > deadline)
throw new Error("digest timeout");
if (raw.length > 20_000)
break;
}
const notes = parseCompactNotes(raw);
if (notes.length === 0) {
await clearCompactRequest();
return "[Session compaction: model produced no notes — skipped.]";
}
const r = persistDigest(notes, db, engine, cfg ?? {});
await clearCompactRequest();
const scope = cfg?.activeProject ? `project:${cfg.activeProject}` : "global";
return `📦 [Session compaction done: ${r.stored}/${notes.length} digest notes stored to memory (scope ${scope}, tags: session-compact). A NEW chat will inherit them via memory injection — safe to continue there.]`;
}
catch (e) {
try { fsSync.appendFileSync(TRACE, "ERR " + (e && e.stack ? String(e.stack).split("\n").slice(0,3).join(" | ") : String(e)) + "\n"); } catch { }
const bumped = await bumpCompactAttempt();
const n = bumped?.attempts ?? "?";
return `[Session compaction failed (attempt ${n}/3): ${e instanceof Error ? e.message.slice(0, 120) : String(e).slice(0, 120)}] — will retry on next message.`;
}
}
// ── the tool (schedules only — see header comment for why) ───────────────
const compactTool = (0, sdk_1.tool)({
name: "compact_session",
description: "Schedule a session digest: this chat's history will be compressed into memory notes (tags: session-compact) so a new chat inherits the state. The digest actually runs at the NEXT incoming user message (history + chat model are only available to the preprocessor). Use when the CONTEXT GAUGE warning fires or before proposing a new chat. Also save critical facts via Remember yourself as belt-and-braces insurance.",
parameters: {
focus: zod_1.z.string().max(500).optional().describe("What the digest must prioritize (task state, decisions, files, open questions)"),
},
implementation: async ({ focus }) => {
const existing = await peekCompactRequest();
if (existing && existing.attempts < MAX_ATTEMPTS) {
return `📦 Already scheduled (${existing.source}, ${new Date(existing.at).toISOString()}). The digest runs on the next message in this chat. Nothing to do now.`;
}
const e = await requestCompact({ source: "tool", focus: focus ?? "" });
return "📦 Session compaction scheduled. At the next incoming message the preprocessor will: read this chat's history → digest it into memory notes (tags: session-compact) via the chat model → confirm. Now: (1) store anything CRITICAL via Remember (insurance — in case this chat dies before the next message), (2) wrap up your current step, (3) recommend the user to start a new chat when convenient.";
},
});
exports.compactTool = compactTool;
//# sourceMappingURL=sessionCompact.js.map