@bd7pil/opencode-deep-memory 0.8.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -172
- package/dist/ccr-REOCHH53.js +12 -0
- package/dist/ccr-REOCHH53.js.map +1 -0
- package/dist/chunk-FUQATBWM.js +43 -0
- package/dist/chunk-FUQATBWM.js.map +1 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +472 -1872
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
/* opencode-deep-memory — zero runtime dependencies */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
});
|
|
9
|
-
var __export = (target, all) => {
|
|
10
|
-
for (var name in all)
|
|
11
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
-
};
|
|
2
|
+
import {
|
|
3
|
+
__export,
|
|
4
|
+
ccrInjectMarker,
|
|
5
|
+
ccrRetrieve,
|
|
6
|
+
ccrStore
|
|
7
|
+
} from "./chunk-FUQATBWM.js";
|
|
13
8
|
|
|
14
9
|
// src/index.ts
|
|
15
|
-
import { appendFile, mkdir as
|
|
16
|
-
import
|
|
10
|
+
import { appendFile, mkdir as mkdir3 } from "fs/promises";
|
|
11
|
+
import path8 from "path";
|
|
17
12
|
|
|
18
13
|
// src/shared/log.ts
|
|
19
14
|
import fs from "fs";
|
|
@@ -122,20 +117,12 @@ function memoryFilePath(scope, type, projectPath, sessionID, _legacyDataRoot) {
|
|
|
122
117
|
const file2 = type === "memory" ? "MEMORY.md" : type === "notes" ? "notes.md" : "checkpoint.md";
|
|
123
118
|
return path2.join(dir, file2);
|
|
124
119
|
}
|
|
125
|
-
function scheduleFilePath(projectPath, _legacyDataRoot) {
|
|
126
|
-
return path2.join(projectMemoryDir(projectPath), ".schedule.json");
|
|
127
|
-
}
|
|
128
120
|
function indexStateFilePath(projectPath, _legacyDataRoot) {
|
|
129
121
|
return path2.join(projectMemoryDir(projectPath), ".index-state.json");
|
|
130
122
|
}
|
|
131
123
|
function checkpointRawPath(projectPath, _sessionID, _legacyDataRoot) {
|
|
132
124
|
return path2.join(projectMemoryDir(projectPath), "checkpoint.raw.json");
|
|
133
125
|
}
|
|
134
|
-
function hashProject(absProjectPath) {
|
|
135
|
-
const { createHash: createHash4 } = __require("crypto");
|
|
136
|
-
const normalized = path2.resolve(absProjectPath);
|
|
137
|
-
return createHash4("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
138
|
-
}
|
|
139
126
|
|
|
140
127
|
// src/shared/tokens.ts
|
|
141
128
|
var CHARS_PER_TOKEN = 4;
|
|
@@ -247,6 +234,61 @@ function sleep(ms) {
|
|
|
247
234
|
return new Promise((r) => setTimeout(r, ms));
|
|
248
235
|
}
|
|
249
236
|
|
|
237
|
+
// src/shared/migrate.ts
|
|
238
|
+
import fs3 from "fs";
|
|
239
|
+
import nodePath from "path";
|
|
240
|
+
var MIGRATED_MARKER = ".migrated-v4";
|
|
241
|
+
async function migrateV3toV4(projectPath, logger) {
|
|
242
|
+
const dir = scopeDir("project", projectPath);
|
|
243
|
+
const marker = nodePath.join(dir, MIGRATED_MARKER);
|
|
244
|
+
if (fs3.existsSync(marker)) return;
|
|
245
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
246
|
+
const deleted = [];
|
|
247
|
+
const archived = [];
|
|
248
|
+
const filesToDelete = [
|
|
249
|
+
"checkpoint.raw.json",
|
|
250
|
+
"notes.md",
|
|
251
|
+
".schedule.json"
|
|
252
|
+
];
|
|
253
|
+
for (const fname of filesToDelete) {
|
|
254
|
+
const fpath = nodePath.join(dir, fname);
|
|
255
|
+
try {
|
|
256
|
+
fs3.unlinkSync(fpath);
|
|
257
|
+
deleted.push(fname);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const archiveDir = nodePath.join(dir, "archive");
|
|
262
|
+
const distillFiles = fs3.existsSync(dir) ? fs3.readdirSync(dir).filter((f) => f.startsWith("distill-") && f.endsWith(".md")) : [];
|
|
263
|
+
if (distillFiles.length > 0) {
|
|
264
|
+
fs3.mkdirSync(archiveDir, { recursive: true });
|
|
265
|
+
for (const fname of distillFiles) {
|
|
266
|
+
const src = nodePath.join(dir, fname);
|
|
267
|
+
const dst = nodePath.join(archiveDir, fname);
|
|
268
|
+
fs3.renameSync(src, dst);
|
|
269
|
+
archived.push(fname);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const memoryPath = nodePath.join(dir, "MEMORY.md");
|
|
273
|
+
if (fs3.existsSync(memoryPath)) {
|
|
274
|
+
const content = fs3.readFileSync(memoryPath, "utf8");
|
|
275
|
+
const lines = content.split("\n");
|
|
276
|
+
if (lines.length > 200) {
|
|
277
|
+
const archivePath = nodePath.join(dir, "MEMORY-archive.md");
|
|
278
|
+
const overflow = lines.slice(200).join("\n");
|
|
279
|
+
fs3.writeFileSync(memoryPath, lines.slice(0, 200).join("\n"), "utf8");
|
|
280
|
+
fs3.appendFileSync(archivePath, `
|
|
281
|
+
${overflow}
|
|
282
|
+
`, "utf8");
|
|
283
|
+
archived.push("MEMORY.md (trimmed to 200 lines, overflow to MEMORY-archive.md)");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
fs3.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString(), "utf8");
|
|
287
|
+
if (deleted.length > 0 || archived.length > 0) {
|
|
288
|
+
logger?.info("V3\u2192V4 migration complete", { deleted, archived });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
250
292
|
// src/hooks/shared-state.ts
|
|
251
293
|
var PluginState = class {
|
|
252
294
|
_agents = /* @__PURE__ */ new Map();
|
|
@@ -254,7 +296,6 @@ var PluginState = class {
|
|
|
254
296
|
_projectModel;
|
|
255
297
|
_fallbackModel;
|
|
256
298
|
_pendingResumes = /* @__PURE__ */ new Map();
|
|
257
|
-
_pendingEnrichments = /* @__PURE__ */ new Set();
|
|
258
299
|
_lastUserText = /* @__PURE__ */ new Map();
|
|
259
300
|
_pendingNotify = null;
|
|
260
301
|
_toolSignatures = /* @__PURE__ */ new Map();
|
|
@@ -265,6 +306,9 @@ var PluginState = class {
|
|
|
265
306
|
_lastCCRCleanup = 0;
|
|
266
307
|
_modelContextWindow = 0;
|
|
267
308
|
_recentEdits = /* @__PURE__ */ new Set();
|
|
309
|
+
_memoryCache;
|
|
310
|
+
_pendingCompression;
|
|
311
|
+
_greetedSessions = /* @__PURE__ */ new Set();
|
|
268
312
|
agentOf(sessionID) {
|
|
269
313
|
return this._agents.get(sessionID);
|
|
270
314
|
}
|
|
@@ -323,27 +367,6 @@ var PluginState = class {
|
|
|
323
367
|
hasPendingResume(sessionID) {
|
|
324
368
|
return this._pendingResumes.has(sessionID);
|
|
325
369
|
}
|
|
326
|
-
/**
|
|
327
|
-
* Mark a sessionID as having a pending enrichment.
|
|
328
|
-
* Called by the compacting hook after writing checkpoint.md.
|
|
329
|
-
*/
|
|
330
|
-
setPendingEnrichment(sessionID) {
|
|
331
|
-
this._pendingEnrichments.add(sessionID);
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Consume (read + delete) the pending enrichment flag.
|
|
335
|
-
* Returns true if the flag was set, false if not.
|
|
336
|
-
* Idempotent: second call returns false.
|
|
337
|
-
*/
|
|
338
|
-
consumePendingEnrichment(sessionID) {
|
|
339
|
-
const had = this._pendingEnrichments.has(sessionID);
|
|
340
|
-
this._pendingEnrichments.delete(sessionID);
|
|
341
|
-
return had;
|
|
342
|
-
}
|
|
343
|
-
/** Check whether a pending enrichment flag exists for a sessionID. */
|
|
344
|
-
hasPendingEnrichment(sessionID) {
|
|
345
|
-
return this._pendingEnrichments.has(sessionID);
|
|
346
|
-
}
|
|
347
370
|
recordLastUserText(sessionID, text) {
|
|
348
371
|
this._lastUserText.set(sessionID, text.slice(0, 500));
|
|
349
372
|
}
|
|
@@ -442,6 +465,35 @@ var PluginState = class {
|
|
|
442
465
|
getRecentEdits() {
|
|
443
466
|
return Array.from(this._recentEdits);
|
|
444
467
|
}
|
|
468
|
+
/** D5: mtime-based MEMORY.md cache for byte-stable system prompts. */
|
|
469
|
+
setMemoryCache(content, mtime) {
|
|
470
|
+
this._memoryCache = { content, mtime };
|
|
471
|
+
}
|
|
472
|
+
getMemoryCache() {
|
|
473
|
+
return this._memoryCache;
|
|
474
|
+
}
|
|
475
|
+
isMemoryCacheFresh(currentMtime) {
|
|
476
|
+
return this._memoryCache?.mtime === currentMtime;
|
|
477
|
+
}
|
|
478
|
+
clearMemoryCache() {
|
|
479
|
+
this._memoryCache = void 0;
|
|
480
|
+
}
|
|
481
|
+
requestCompression(keepRecent) {
|
|
482
|
+
this._pendingCompression = { keepRecent, requestedAt: Date.now() };
|
|
483
|
+
}
|
|
484
|
+
consumeCompressionRequest() {
|
|
485
|
+
if (!this._pendingCompression) return void 0;
|
|
486
|
+
const req = this._pendingCompression;
|
|
487
|
+
this._pendingCompression = void 0;
|
|
488
|
+
return { keepRecent: req.keepRecent };
|
|
489
|
+
}
|
|
490
|
+
/** A: Session-start greeting — only inject memory whisper once per session. */
|
|
491
|
+
hasGreetedSession(sessionID) {
|
|
492
|
+
return this._greetedSessions.has(sessionID);
|
|
493
|
+
}
|
|
494
|
+
markGreetedSession(sessionID) {
|
|
495
|
+
this._greetedSessions.add(sessionID);
|
|
496
|
+
}
|
|
445
497
|
};
|
|
446
498
|
function createPluginState() {
|
|
447
499
|
return new PluginState();
|
|
@@ -463,1118 +515,116 @@ function createChatParamsHandler(state, logger) {
|
|
|
463
515
|
};
|
|
464
516
|
}
|
|
465
517
|
|
|
466
|
-
// src/
|
|
467
|
-
import
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
"
|
|
473
|
-
|
|
474
|
-
"note:",
|
|
475
|
-
"important:",
|
|
476
|
-
"constraint:",
|
|
477
|
-
"must not",
|
|
478
|
-
"never do",
|
|
479
|
-
"\u8BB0\u4F4F",
|
|
480
|
-
"\u522B\u5FD8",
|
|
481
|
-
"\u6CE8\u610F\uFF1A",
|
|
482
|
-
"\u91CD\u8981\uFF1A",
|
|
483
|
-
"\u7EA6\u675F\uFF1A",
|
|
484
|
-
"\u7EDD\u4E0D\u80FD",
|
|
485
|
-
"\u5FC5\u987B"
|
|
486
|
-
];
|
|
487
|
-
function matchesKeyword(text) {
|
|
488
|
-
const lower = text.toLowerCase();
|
|
489
|
-
return KEYWORDS.some((kw) => lower.includes(kw));
|
|
490
|
-
}
|
|
491
|
-
function truncate(text, max) {
|
|
492
|
-
if (text.length <= max) return text;
|
|
493
|
-
return text.slice(0, max) + " [truncated]";
|
|
494
|
-
}
|
|
495
|
-
function deduplicateEntries(content) {
|
|
496
|
-
const seenHashes = /* @__PURE__ */ new Set();
|
|
497
|
-
const blocks = content.split(/\n(?=## )/);
|
|
498
|
-
const kept = [];
|
|
499
|
-
for (const block of blocks) {
|
|
500
|
-
const match = block.match(/\[([a-f0-9]{8})\]/);
|
|
501
|
-
if (match) {
|
|
502
|
-
const hash2 = match[1];
|
|
503
|
-
if (seenHashes.has(hash2)) continue;
|
|
504
|
-
seenHashes.add(hash2);
|
|
505
|
-
}
|
|
506
|
-
kept.push(block);
|
|
507
|
-
}
|
|
508
|
-
return kept.join("\n");
|
|
509
|
-
}
|
|
510
|
-
function createChatMessageHandler(config2) {
|
|
511
|
-
const { projectPath, state, logger } = config2;
|
|
512
|
-
return async (input, output) => {
|
|
513
|
-
if (output.message.role !== "user") return;
|
|
514
|
-
if (input.agent) return;
|
|
515
|
-
const textParts = output.parts.filter(
|
|
516
|
-
(p) => p.type === "text"
|
|
517
|
-
);
|
|
518
|
-
if (textParts.length === 0) return;
|
|
519
|
-
const fullText = textParts.map((p) => p.text).join("");
|
|
520
|
-
state.recordLastUserText(input.sessionID, fullText);
|
|
521
|
-
if (!matchesKeyword(fullText)) return;
|
|
522
|
-
const truncated = truncate(fullText, MAX_NOTE_LENGTH);
|
|
523
|
-
const contentHash = createHash("md5").update(truncated).digest("hex").slice(0, 8);
|
|
524
|
-
const notesFile = memoryFilePath("project", "notes", projectPath);
|
|
525
|
-
const sid8 = input.sessionID.slice(0, 8);
|
|
526
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
527
|
-
try {
|
|
528
|
-
await mkdir(path4.dirname(notesFile), { recursive: true });
|
|
529
|
-
const release = await acquireLock(notesFile);
|
|
530
|
-
try {
|
|
531
|
-
let content = await readFile(notesFile, "utf8").catch(() => "");
|
|
532
|
-
if (content.includes(`[${contentHash}]`)) {
|
|
533
|
-
logger?.debug("notes: skipped duplicate", { hash: contentHash });
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const entry = `
|
|
537
|
-
## ${timestamp} (session ${sid8}) [${contentHash}]
|
|
538
|
-
${truncated}
|
|
539
|
-
`;
|
|
540
|
-
content = deduplicateEntries(content + entry);
|
|
541
|
-
await writeFile(notesFile, content, "utf8");
|
|
542
|
-
} finally {
|
|
543
|
-
release();
|
|
544
|
-
}
|
|
545
|
-
logger?.debug("notes: captured keyword match", {
|
|
546
|
-
sessionID: input.sessionID,
|
|
547
|
-
hash: contentHash
|
|
548
|
-
});
|
|
549
|
-
} catch (err) {
|
|
550
|
-
logger?.warn("notes: failed to write notes.md", {
|
|
551
|
-
error: err instanceof Error ? err.message : String(err)
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// src/inject/agent-budget.ts
|
|
558
|
-
function classifyAgent(agent) {
|
|
559
|
-
if (agent === void 0) return "main";
|
|
560
|
-
const lower = agent.toLowerCase();
|
|
561
|
-
const mainAgents = ["build", "sisyphus", "open-craft", "opencode"];
|
|
562
|
-
if (mainAgents.includes(lower)) return "main";
|
|
563
|
-
const deepAgents = ["oracle", "metis", "momus"];
|
|
564
|
-
if (deepAgents.includes(lower)) return "deep-reasoning";
|
|
565
|
-
const toolAgents = [
|
|
566
|
-
"explore",
|
|
567
|
-
"librarian",
|
|
568
|
-
"quick",
|
|
569
|
-
"task",
|
|
570
|
-
"sisyphus-junior",
|
|
571
|
-
"general"
|
|
572
|
-
];
|
|
573
|
-
if (toolAgents.includes(lower)) return "tool-subagent";
|
|
574
|
-
return "main";
|
|
575
|
-
}
|
|
576
|
-
var BUDGET_TABLE = {
|
|
577
|
-
main: {
|
|
578
|
-
normal: { total: 800, toolPrompt: 80, memorySummary: 400, checkpointSummary: 220, repomap: 100 },
|
|
579
|
-
"post-compaction": { total: 3e3, toolPrompt: 80, memorySummary: 1200, checkpointSummary: 1420, repomap: 300 },
|
|
580
|
-
"post-resume": { total: 3e3, toolPrompt: 80, memorySummary: 1200, checkpointSummary: 1420, repomap: 300 }
|
|
581
|
-
},
|
|
582
|
-
"deep-reasoning": {
|
|
583
|
-
normal: { total: 400, toolPrompt: 80, memorySummary: 240, checkpointSummary: 80, repomap: 0 },
|
|
584
|
-
"post-compaction": { total: 800, toolPrompt: 80, memorySummary: 500, checkpointSummary: 220, repomap: 0 },
|
|
585
|
-
"post-resume": { total: 400, toolPrompt: 80, memorySummary: 240, checkpointSummary: 80, repomap: 0 }
|
|
586
|
-
},
|
|
587
|
-
"tool-subagent": {
|
|
588
|
-
normal: { total: 80, toolPrompt: 80, memorySummary: 0, checkpointSummary: 0, repomap: 0 },
|
|
589
|
-
"post-compaction": { total: 80, toolPrompt: 80, memorySummary: 0, checkpointSummary: 0, repomap: 0 },
|
|
590
|
-
"post-resume": { total: 80, toolPrompt: 80, memorySummary: 0, checkpointSummary: 0, repomap: 0 }
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
function budgetFor(tier, mode) {
|
|
594
|
-
return BUDGET_TABLE[tier][mode];
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// src/inject/budgeted-read.ts
|
|
598
|
-
import fs3 from "fs";
|
|
599
|
-
function parseMarkdownSections(content) {
|
|
600
|
-
if (!content) return [];
|
|
601
|
-
const sections = [];
|
|
602
|
-
const lines = content.split("\n");
|
|
603
|
-
let currentHeading = "";
|
|
604
|
-
let currentBody = [];
|
|
605
|
-
for (const line of lines) {
|
|
606
|
-
if (line.startsWith("## ")) {
|
|
607
|
-
if (currentHeading || currentBody.length > 0) {
|
|
608
|
-
sections.push({
|
|
609
|
-
heading: currentHeading,
|
|
610
|
-
body: currentBody.join("\n")
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
currentHeading = line;
|
|
614
|
-
currentBody = [];
|
|
615
|
-
} else {
|
|
616
|
-
currentBody.push(line);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
if (currentHeading || currentBody.length > 0) {
|
|
620
|
-
sections.push({
|
|
621
|
-
heading: currentHeading,
|
|
622
|
-
body: currentBody.join("\n")
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
return sections;
|
|
626
|
-
}
|
|
627
|
-
function sortByPriority(sections, priority) {
|
|
628
|
-
const priorityLower = priority.map((p) => p.toLowerCase());
|
|
629
|
-
function priorityIndex(section) {
|
|
630
|
-
const headingLower = section.heading.toLowerCase();
|
|
631
|
-
for (let i = 0; i < priorityLower.length; i++) {
|
|
632
|
-
if (headingLower.includes(priorityLower[i])) {
|
|
633
|
-
return i;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return priorityLower.length;
|
|
637
|
-
}
|
|
638
|
-
return [...sections].sort((a, b) => {
|
|
639
|
-
const ai = priorityIndex(a);
|
|
640
|
-
const bi = priorityIndex(b);
|
|
641
|
-
if (ai !== bi) return ai - bi;
|
|
642
|
-
return 0;
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
function budgetedRead(filePath, budgetTokens, sectionPriority) {
|
|
646
|
-
if (budgetTokens <= 0) return "";
|
|
647
|
-
let content;
|
|
518
|
+
// src/inject/system-payload.ts
|
|
519
|
+
import fs4 from "fs";
|
|
520
|
+
var TOOL_HINT = 'Memory tools available: memory_search, memory_store, memory_forget.\nGuidelines:\n (1) BEFORE making ANY technical decision, search: memory_search(query="decision OR decided OR chose", scope="project")\n (2) BEFORE fixing an error, search for known pitfalls: memory_search(query="gotcha OR error OR bug", scope="project")\n (3) AFTER fixing an error, store it: memory_store(type="gotcha", content="[error]: ... \u2192 [fix]: ...", scope="project")\n (4) WHEN user states a constraint/rule, store it: memory_store(type="constraint", content="...", scope="project")\n (5) WHEN a technical decision is made, store it: memory_store(type="decision", content="[decision]: ... \u2192 [reason]: ...", scope="project")';
|
|
521
|
+
async function composeSystemPayload(opts) {
|
|
522
|
+
const { state, projectPath, logger } = opts;
|
|
523
|
+
const memoryPath = memoryFilePath("project", "memory", projectPath);
|
|
524
|
+
let memoryContent = "";
|
|
525
|
+
let cacheHit = true;
|
|
648
526
|
try {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
if (!content.trim()) return "";
|
|
656
|
-
const sections = parseMarkdownSections(content);
|
|
657
|
-
if (sections.length === 0) return "";
|
|
658
|
-
const sorted = sortByPriority(sections, sectionPriority);
|
|
659
|
-
let output = "";
|
|
660
|
-
let remaining = budgetTokens;
|
|
661
|
-
for (const section of sorted) {
|
|
662
|
-
const sectionText = (section.heading ? section.heading + "\n" : "") + section.body + "\n";
|
|
663
|
-
const sectionTokens = estimateTokens(sectionText);
|
|
664
|
-
if (sectionTokens <= remaining) {
|
|
665
|
-
output += sectionText + "\n";
|
|
666
|
-
remaining -= sectionTokens;
|
|
527
|
+
const stat = fs4.statSync(memoryPath);
|
|
528
|
+
const mtime = stat.mtimeMs;
|
|
529
|
+
if (state.isMemoryCacheFresh(mtime)) {
|
|
530
|
+
memoryContent = state.getMemoryCache()?.content ?? "";
|
|
531
|
+
cacheHit = true;
|
|
667
532
|
} else {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const truncatedBody = truncateToTokenBudget(section.body, availableForBody);
|
|
672
|
-
output += headingPart + truncatedBody + "\n\n";
|
|
673
|
-
}
|
|
674
|
-
break;
|
|
533
|
+
memoryContent = fs4.readFileSync(memoryPath, "utf8");
|
|
534
|
+
state.setMemoryCache(memoryContent, mtime);
|
|
535
|
+
cacheHit = false;
|
|
675
536
|
}
|
|
537
|
+
} catch {
|
|
538
|
+
state.clearMemoryCache();
|
|
539
|
+
cacheHit = false;
|
|
540
|
+
}
|
|
541
|
+
const memorySize = memoryContent.length;
|
|
542
|
+
let payload = `<deep-memory-stable>
|
|
543
|
+
<tool-hint>
|
|
544
|
+
${TOOL_HINT}
|
|
545
|
+
</tool-hint>`;
|
|
546
|
+
if (memoryContent.trim().length > 0) {
|
|
547
|
+
payload += `
|
|
548
|
+
<constraints>
|
|
549
|
+
${memoryContent}
|
|
550
|
+
</constraints>`;
|
|
676
551
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
var BASE_IMPORTANCE = {
|
|
682
|
-
constraint: 80,
|
|
683
|
-
decision: 70,
|
|
684
|
-
gotcha: 60,
|
|
685
|
-
fact: 50,
|
|
686
|
-
note: 30
|
|
687
|
-
};
|
|
688
|
-
var COMMON_WORDS = [
|
|
689
|
-
"test",
|
|
690
|
-
"util",
|
|
691
|
-
"helper",
|
|
692
|
-
"config",
|
|
693
|
-
"index",
|
|
694
|
-
"main",
|
|
695
|
-
"init",
|
|
696
|
-
"setup"
|
|
697
|
-
];
|
|
698
|
-
function computeImportance(factors) {
|
|
699
|
-
let score = BASE_IMPORTANCE[factors.type] ?? 40;
|
|
700
|
-
score += Math.min(20, factors.notesOccurrences * 5);
|
|
701
|
-
score += Math.min(15, factors.searchHits * 5);
|
|
702
|
-
if (factors.ageDays < 7) score += 10;
|
|
703
|
-
else if (factors.ageDays < 30) score += 5;
|
|
704
|
-
const content = factors.content ?? "";
|
|
705
|
-
const heading = factors.heading ?? "";
|
|
706
|
-
if (content.startsWith("_") || heading.startsWith("_")) {
|
|
707
|
-
score *= 0.3;
|
|
708
|
-
}
|
|
709
|
-
if (content.length >= 50) {
|
|
710
|
-
score *= 1.3;
|
|
711
|
-
}
|
|
712
|
-
const headingLower = heading.toLowerCase();
|
|
713
|
-
if (COMMON_WORDS.some((w) => headingLower.includes(w))) {
|
|
714
|
-
score *= 0.5;
|
|
715
|
-
}
|
|
716
|
-
return Math.max(1, Math.min(100, Math.round(score)));
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// src/inject/tier-renderer.ts
|
|
720
|
-
var TIER_MAX_TOKENS = {
|
|
721
|
-
1: 200,
|
|
722
|
-
2: 60,
|
|
723
|
-
3: 25,
|
|
724
|
-
4: 15,
|
|
725
|
-
5: 0
|
|
726
|
-
};
|
|
727
|
-
function renderTier(content, type, heading, tier) {
|
|
728
|
-
if (tier === 5) return "";
|
|
729
|
-
const maxTokens = TIER_MAX_TOKENS[tier];
|
|
730
|
-
const contentTokens = Math.ceil(content.length / 4);
|
|
731
|
-
if (tier === 1) {
|
|
732
|
-
return contentTokens <= maxTokens ? `- [${heading}] ${content}` : `- [${heading}] ${content.slice(0, maxTokens * 4)}... [truncated]`;
|
|
733
|
-
}
|
|
734
|
-
if (tier === 2) {
|
|
735
|
-
const firstSentence = content.split(/[.。!!??\n]/)[0] ?? content.slice(0, 200);
|
|
736
|
-
return `- [${type}] ${firstSentence.slice(0, 200)}`;
|
|
737
|
-
}
|
|
738
|
-
if (tier === 3) {
|
|
739
|
-
const words = content.split(/\s+/).slice(0, 10).join(" ");
|
|
740
|
-
return `- [${type}] ${words.slice(0, 80)}`;
|
|
741
|
-
}
|
|
742
|
-
return `- [${type}]`;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// src/inject/budget-allocator.ts
|
|
746
|
-
var TIER_COST = { 1: 200, 2: 60, 3: 25, 4: 15 };
|
|
747
|
-
function allocateAndRender(results, opts) {
|
|
748
|
-
if (results.length === 0) return [];
|
|
749
|
-
const sortedScores = results.map((r) => r.score).sort((a, b) => b - a);
|
|
750
|
-
const top20 = sortedScores[Math.floor(sortedScores.length * 0.2)] ?? 0;
|
|
751
|
-
const top50 = sortedScores[Math.floor(sortedScores.length * 0.5)] ?? 0;
|
|
752
|
-
const withImportance = results.map((r) => {
|
|
753
|
-
const type = opts.typeOf?.(r) ?? inferType(r.heading);
|
|
754
|
-
const ageDays = opts.ageDays?.(r) ?? 0;
|
|
755
|
-
const base = computeImportance({
|
|
756
|
-
type,
|
|
757
|
-
ageDays,
|
|
758
|
-
notesOccurrences: 0,
|
|
759
|
-
searchHits: 0
|
|
760
|
-
});
|
|
761
|
-
let boost = 0;
|
|
762
|
-
if (r.score >= top20) boost = 30;
|
|
763
|
-
else if (r.score >= top50) boost = 15;
|
|
764
|
-
return { result: r, type, fusedImportance: base + boost };
|
|
765
|
-
});
|
|
766
|
-
withImportance.sort((a, b) => b.fusedImportance - a.fusedImportance);
|
|
767
|
-
const maxAtP4 = Math.floor(opts.budget / TIER_COST[4]);
|
|
768
|
-
const selected = withImportance.slice(0, maxAtP4);
|
|
769
|
-
let remaining = opts.budget - selected.length * TIER_COST[4];
|
|
770
|
-
const allocations = selected.map((item) => {
|
|
771
|
-
const rendered = renderTier(item.result.snippet, item.type, item.result.heading, 4);
|
|
772
|
-
return {
|
|
773
|
-
content: item.result.snippet,
|
|
774
|
-
type: item.type,
|
|
775
|
-
heading: item.result.heading,
|
|
776
|
-
tier: 4,
|
|
777
|
-
rendered,
|
|
778
|
-
tokens: TIER_COST[4]
|
|
779
|
-
};
|
|
780
|
-
});
|
|
781
|
-
for (let i = 0; i < allocations.length; i++) {
|
|
782
|
-
const alloc = allocations[i];
|
|
783
|
-
if (remaining >= TIER_COST[3] - TIER_COST[4] && alloc.tier === 4) {
|
|
784
|
-
alloc.tier = 3;
|
|
785
|
-
alloc.rendered = renderTier(alloc.content, alloc.type, alloc.heading, 3);
|
|
786
|
-
alloc.tokens = TIER_COST[3];
|
|
787
|
-
remaining -= TIER_COST[3] - TIER_COST[4];
|
|
788
|
-
}
|
|
789
|
-
if (remaining >= TIER_COST[2] - TIER_COST[3] && alloc.tier === 3) {
|
|
790
|
-
alloc.tier = 2;
|
|
791
|
-
alloc.rendered = renderTier(alloc.content, alloc.type, alloc.heading, 2);
|
|
792
|
-
alloc.tokens = TIER_COST[2];
|
|
793
|
-
remaining -= TIER_COST[2] - TIER_COST[3];
|
|
794
|
-
}
|
|
795
|
-
if (remaining >= TIER_COST[1] - TIER_COST[2] && alloc.tier === 2) {
|
|
796
|
-
alloc.tier = 1;
|
|
797
|
-
alloc.rendered = renderTier(alloc.content, alloc.type, alloc.heading, 1);
|
|
798
|
-
alloc.tokens = TIER_COST[1];
|
|
799
|
-
remaining -= TIER_COST[1] - TIER_COST[2];
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
return allocations;
|
|
803
|
-
}
|
|
804
|
-
function inferType(heading) {
|
|
805
|
-
const h = heading.toLowerCase();
|
|
806
|
-
if (h.includes("constraint") || h.includes("rule")) return "constraint";
|
|
807
|
-
if (h.includes("decision")) return "decision";
|
|
808
|
-
if (h.includes("gotcha") || h.includes("error")) return "gotcha";
|
|
809
|
-
if (h.includes("fact")) return "fact";
|
|
810
|
-
return "note";
|
|
552
|
+
payload += `
|
|
553
|
+
</deep-memory-stable>`;
|
|
554
|
+
logger?.debug("composeSystemPayload V4", { cacheHit, memorySize });
|
|
555
|
+
return { payload, cacheHit, memorySize };
|
|
811
556
|
}
|
|
812
557
|
|
|
813
|
-
// src/inject/
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
let list = inverted.get(token);
|
|
821
|
-
if (!list) {
|
|
822
|
-
list = [];
|
|
823
|
-
inverted.set(token, list);
|
|
824
|
-
}
|
|
825
|
-
list.push(i);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
const isDuplicate = /* @__PURE__ */ new Set();
|
|
829
|
-
const compared = /* @__PURE__ */ new Set();
|
|
830
|
-
for (const indices of inverted.values()) {
|
|
831
|
-
for (let a = 0; a < indices.length; a++) {
|
|
832
|
-
const i = indices[a];
|
|
833
|
-
if (isDuplicate.has(i)) continue;
|
|
834
|
-
for (let b = a + 1; b < indices.length; b++) {
|
|
835
|
-
const j = indices[b];
|
|
836
|
-
if (isDuplicate.has(j)) continue;
|
|
837
|
-
const key = i < j ? `${i}-${j}` : `${j}-${i}`;
|
|
838
|
-
if (compared.has(key)) continue;
|
|
839
|
-
compared.add(key);
|
|
840
|
-
if (jaccardSimilarity(tokenSets[i], tokenSets[j]) > threshold) {
|
|
841
|
-
isDuplicate.add(j);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
return items.filter((_, i) => !isDuplicate.has(i));
|
|
847
|
-
}
|
|
848
|
-
function tokenize(text) {
|
|
849
|
-
return text.toLowerCase().split(/[\s\p{P}]+/u).filter((t) => t.length > 0);
|
|
850
|
-
}
|
|
851
|
-
function jaccardSimilarity(a, b) {
|
|
852
|
-
let intersection2 = 0;
|
|
853
|
-
for (const t of a) if (b.has(t)) intersection2++;
|
|
854
|
-
const union2 = a.size + b.size - intersection2;
|
|
855
|
-
return union2 === 0 ? 0 : intersection2 / union2;
|
|
558
|
+
// src/inject/auto-search.ts
|
|
559
|
+
var WHISPER_MIN_SCORE = 2;
|
|
560
|
+
function shouldWhisper(results) {
|
|
561
|
+
if (results.length === 0) return false;
|
|
562
|
+
const top1 = results[0];
|
|
563
|
+
if (!top1 || top1.score < WHISPER_MIN_SCORE) return false;
|
|
564
|
+
return true;
|
|
856
565
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
(e) => `${e.file}: ${e.symbols.join(", ")}`
|
|
863
|
-
);
|
|
864
|
-
return `<deep-memory-repomap>
|
|
865
|
-
${lines.join("\n")}
|
|
866
|
-
</deep-memory-repomap>`;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// src/inject/system-payload.ts
|
|
870
|
-
var TOOL_HINT = 'Memory tools available: memory_search, memory_store, memory_forget, memory_expand, deep_expand. Guidelines:\n (1) BEFORE making ANY technical decision, search: memory_search(query="decision OR decided OR chose OR \u9009\u62E9 OR \u51B3\u5B9A", scope="project")\n (2) BEFORE fixing an error, search for known pitfalls: memory_search(query="gotcha OR error OR bug OR \u5751 OR \u9519\u8BEF", scope="project")\n (3) AFTER fixing an error, store it: memory_store(type="gotcha", content="[error]: ... \u2192 [fix]: ...", scope="project")\n (4) WHEN user states a constraint/rule, store it: memory_store(type="constraint", content="...", scope="project")\n (5) WHEN a technical decision is made, store it: memory_store(type="decision", content="[decision]: ... \u2192 [reason]: ...", scope="project")';
|
|
871
|
-
async function composeSystemPayload(opts) {
|
|
872
|
-
const { state, sessionID, projectPath, mode, searchService, userQuery, logger, tracker } = opts;
|
|
873
|
-
const agent = sessionID ? state.agentOf(sessionID) : void 0;
|
|
874
|
-
const tier = classifyAgent(agent);
|
|
875
|
-
const budget = budgetFor(tier, mode);
|
|
876
|
-
if (budget.total <= 80) {
|
|
877
|
-
return {
|
|
878
|
-
stable: `<deep-memory-stable>
|
|
879
|
-
<tool-hint>${TOOL_HINT}</tool-hint>
|
|
880
|
-
</deep-memory-stable>`,
|
|
881
|
-
volatile: "",
|
|
882
|
-
stats: { searchEntries: 0, repoMapEntries: 0, hasCheckpoint: false }
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
const staticBudget = Math.floor(budget.memorySummary * 0.4);
|
|
886
|
-
const searchBudget = budget.memorySummary - staticBudget;
|
|
887
|
-
let staticMemory = "";
|
|
888
|
-
if (staticBudget > 0) {
|
|
889
|
-
const memoryPath = memoryFilePath("project", "memory", projectPath);
|
|
890
|
-
staticMemory = budgetedRead(memoryPath, staticBudget, ["Constraints", "Rules", "Decisions"]);
|
|
891
|
-
}
|
|
892
|
-
const stable = `<deep-memory-stable>
|
|
893
|
-
<tool-hint>${TOOL_HINT}</tool-hint>
|
|
894
|
-
<constraints>
|
|
895
|
-
${staticMemory || "(empty)"}
|
|
896
|
-
</constraints>
|
|
897
|
-
</deep-memory-stable>`;
|
|
898
|
-
let volatileContent = "";
|
|
899
|
-
let searchEntries = 0;
|
|
900
|
-
if (userQuery && searchService && searchBudget > 0) {
|
|
901
|
-
try {
|
|
902
|
-
const results = await searchService.search(userQuery, { scope: "all", limit: 20, applyDecay: true });
|
|
903
|
-
if (results.length > 0) {
|
|
904
|
-
const deduped = dedupByJaccard(results, (r) => r.snippet);
|
|
905
|
-
const allocated = allocateAndRender(
|
|
906
|
-
deduped.map((r) => ({
|
|
907
|
-
score: r.score,
|
|
908
|
-
heading: r.heading,
|
|
909
|
-
snippet: r.snippet,
|
|
910
|
-
scope: r.scope
|
|
911
|
-
})),
|
|
912
|
-
{ budget: searchBudget }
|
|
913
|
-
);
|
|
914
|
-
searchEntries = allocated.length;
|
|
915
|
-
volatileContent = allocated.map((a) => a.rendered).join("\n");
|
|
916
|
-
}
|
|
917
|
-
} catch {
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
let checkpointContent = "";
|
|
921
|
-
let hasCheckpoint = false;
|
|
922
|
-
if (budget.checkpointSummary > 0) {
|
|
923
|
-
const checkpointPath = memoryFilePath("project", "checkpoint", projectPath);
|
|
924
|
-
checkpointContent = budgetedRead(checkpointPath, budget.checkpointSummary, [
|
|
925
|
-
"User Intent",
|
|
926
|
-
"Decisions",
|
|
927
|
-
"Constraints",
|
|
928
|
-
"Gotchas",
|
|
929
|
-
"File Changes"
|
|
930
|
-
]);
|
|
931
|
-
hasCheckpoint = !!checkpointContent;
|
|
932
|
-
}
|
|
933
|
-
let volatile = `<deep-memory-volatile>
|
|
934
|
-
<relevant>
|
|
935
|
-
${volatileContent || "(none)"}
|
|
936
|
-
</relevant>`;
|
|
937
|
-
if (checkpointContent) {
|
|
938
|
-
volatile += `
|
|
939
|
-
<last-checkpoint>
|
|
940
|
-
${checkpointContent}
|
|
941
|
-
</last-checkpoint>`;
|
|
942
|
-
}
|
|
943
|
-
let repoMapSymbols = 0;
|
|
944
|
-
if (tracker && budget.repomap > 0) {
|
|
945
|
-
const repomapEntries = tracker.getTopSymbols(budget.repomap);
|
|
946
|
-
if (repomapEntries.length > 0) {
|
|
947
|
-
repoMapSymbols = repomapEntries.length;
|
|
948
|
-
volatile += "\n" + formatRepoMap(repomapEntries);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
volatile += `
|
|
952
|
-
</deep-memory-volatile>`;
|
|
953
|
-
logger?.debug("composeSystemPayload", {
|
|
954
|
-
agent: agent ?? "(undefined)",
|
|
955
|
-
tier,
|
|
956
|
-
mode,
|
|
957
|
-
stableSize: stable.length,
|
|
958
|
-
volatileSize: volatile.length
|
|
959
|
-
});
|
|
960
|
-
return { stable, volatile, stats: { searchEntries, repoMapEntries: repoMapSymbols, hasCheckpoint } };
|
|
566
|
+
function formatWhisper(results, query) {
|
|
567
|
+
if (results.length === 0) return "";
|
|
568
|
+
const n = Math.min(results.length, 3);
|
|
569
|
+
const headings = results.slice(0, n).map((r) => r.heading).join(", ");
|
|
570
|
+
return `[memory hint: ${n} relevant entries (${headings}) \u2014 call memory_search("${query.slice(0, 40)}") for details]`;
|
|
961
571
|
}
|
|
962
572
|
|
|
963
573
|
// src/hooks/system-transform.ts
|
|
964
|
-
function createSystemTransformHandler(state, projectPath, searchService, logger
|
|
574
|
+
function createSystemTransformHandler(state, projectPath, searchService, logger) {
|
|
965
575
|
return async (input, output) => {
|
|
966
576
|
if (!input.sessionID) {
|
|
967
577
|
logger?.debug("system.transform: no sessionID, skipping");
|
|
968
578
|
return;
|
|
969
579
|
}
|
|
970
580
|
const sessionID = input.sessionID;
|
|
971
|
-
let mode = "normal";
|
|
972
|
-
if (state.hasPendingResume(sessionID)) {
|
|
973
|
-
const agent2 = state.agentOf(sessionID);
|
|
974
|
-
const tier2 = classifyAgent(agent2);
|
|
975
|
-
if (tier2 === "main") {
|
|
976
|
-
mode = "post-resume";
|
|
977
|
-
state.consumePendingResume(sessionID);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
581
|
const userQuery = state.consumeLastUserText(sessionID);
|
|
981
|
-
const {
|
|
582
|
+
const { payload, cacheHit, memorySize } = await composeSystemPayload({
|
|
982
583
|
state,
|
|
983
584
|
sessionID,
|
|
984
585
|
projectPath,
|
|
985
|
-
|
|
986
|
-
searchService,
|
|
987
|
-
userQuery,
|
|
988
|
-
logger,
|
|
989
|
-
tracker
|
|
586
|
+
logger
|
|
990
587
|
});
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
588
|
+
let finalPayload = payload;
|
|
589
|
+
if (!state.hasGreetedSession(sessionID)) {
|
|
590
|
+
state.markGreetedSession(sessionID);
|
|
591
|
+
if (searchService && userQuery) {
|
|
592
|
+
try {
|
|
593
|
+
await searchService.ensureIndex();
|
|
594
|
+
const results = await searchService.search(userQuery, { scope: "all", limit: 10 });
|
|
595
|
+
if (shouldWhisper(results)) {
|
|
596
|
+
finalPayload += `
|
|
597
|
+
${formatWhisper(results, userQuery)}`;
|
|
598
|
+
}
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
996
602
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
logger?.debug("system.transform: injected", {
|
|
603
|
+
output.system.push(finalPayload);
|
|
604
|
+
logger?.debug("system.transform V4: injected", {
|
|
1000
605
|
sessionID,
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
stableSize: stable.length,
|
|
1005
|
-
volatileSize: volatile.length
|
|
606
|
+
cacheHit,
|
|
607
|
+
memorySize,
|
|
608
|
+
payloadSize: finalPayload.length
|
|
1006
609
|
});
|
|
1007
610
|
state.mergeNotify({
|
|
1008
611
|
injection: {
|
|
1009
|
-
stableSize:
|
|
1010
|
-
volatileSize:
|
|
1011
|
-
tier,
|
|
1012
|
-
mode,
|
|
1013
|
-
searchEntries:
|
|
1014
|
-
repoMapEntries:
|
|
1015
|
-
hasCheckpoint:
|
|
612
|
+
stableSize: finalPayload.length,
|
|
613
|
+
volatileSize: 0,
|
|
614
|
+
tier: "v4",
|
|
615
|
+
mode: "normal",
|
|
616
|
+
searchEntries: 0,
|
|
617
|
+
repoMapEntries: 0,
|
|
618
|
+
hasCheckpoint: false
|
|
1016
619
|
}
|
|
1017
620
|
});
|
|
1018
621
|
};
|
|
1019
622
|
}
|
|
1020
623
|
|
|
1021
|
-
// src/schedule/resume.ts
|
|
1022
|
-
import fs4 from "fs";
|
|
1023
|
-
async function handleSessionCreated(args) {
|
|
1024
|
-
const { state, event, projectPath, logger } = args;
|
|
1025
|
-
const info = event.properties.info;
|
|
1026
|
-
const sessionID = info.id;
|
|
1027
|
-
if (info.parentID) {
|
|
1028
|
-
logger?.debug("resume: skipping sub-session", {
|
|
1029
|
-
sessionID,
|
|
1030
|
-
parentID: info.parentID
|
|
1031
|
-
});
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
if (info.title.startsWith("Memory ")) {
|
|
1035
|
-
logger?.debug("resume: skipping Memory-* session", {
|
|
1036
|
-
sessionID,
|
|
1037
|
-
title: info.title
|
|
1038
|
-
});
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
const projectHash = hashProject(projectPath);
|
|
1042
|
-
const memoryPath = memoryFilePath("project", "memory", projectPath);
|
|
1043
|
-
let memoryExists = false;
|
|
1044
|
-
let memorySize = 0;
|
|
1045
|
-
try {
|
|
1046
|
-
const stat2 = fs4.statSync(memoryPath);
|
|
1047
|
-
memoryExists = stat2.isFile();
|
|
1048
|
-
memorySize = stat2.size;
|
|
1049
|
-
} catch {
|
|
1050
|
-
memoryExists = false;
|
|
1051
|
-
}
|
|
1052
|
-
if (!memoryExists) {
|
|
1053
|
-
logger?.debug("resume: no MEMORY.md found, skipping", {
|
|
1054
|
-
sessionID,
|
|
1055
|
-
projectHash
|
|
1056
|
-
});
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
const wasSet = state.setPendingResume(sessionID, {
|
|
1060
|
-
budgetTokens: 3e3,
|
|
1061
|
-
projectHash
|
|
1062
|
-
});
|
|
1063
|
-
if (wasSet) {
|
|
1064
|
-
logger?.info(
|
|
1065
|
-
`Resume detected for session ${sessionID}, project ${projectHash}, MEMORY.md size ${memorySize}`
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// src/schedule/auto-dream.ts
|
|
1071
|
-
import fs5 from "fs";
|
|
1072
|
-
import path5 from "path";
|
|
1073
|
-
|
|
1074
|
-
// src/schedule/dream-executor.ts
|
|
1075
|
-
var DREAM_PROMPT_TEMPLATE = `You are a memory consolidation agent. Your task is to refine the project's persistent memory by reviewing raw notes and checkpoints, then storing durable findings.
|
|
1076
|
-
|
|
1077
|
-
Project context:
|
|
1078
|
-
- Project path: {{projectPath}}
|
|
1079
|
-
- Notes file: {{notesFilePath}}
|
|
1080
|
-
- Sessions dir: {{sessionsDir}}
|
|
1081
|
-
|
|
1082
|
-
Steps:
|
|
1083
|
-
1. Read the notes file at {{notesFilePath}}. These are raw captures from recent sessions (user messages with trigger keywords like "\u8BB0\u4F4F", "remember", "decided").
|
|
1084
|
-
2. Use the \`list\` tool to find checkpoint.md files under {{sessionsDir}}. Read the 5 most recent ones.
|
|
1085
|
-
3. Identify recurring themes across notes + checkpoints:
|
|
1086
|
-
- Decisions that have been confirmed or acted upon (call memory_store with type="decision")
|
|
1087
|
-
- Hard constraints or rules the user explicitly stated (call memory_store with type="constraint")
|
|
1088
|
-
- Gotchas, errors, and their fixes (call memory_store with type="gotcha")
|
|
1089
|
-
- Important facts about the codebase or domain (call memory_store with type="fact")
|
|
1090
|
-
4. Before storing each finding, call memory_search with a relevant phrase to avoid duplicating existing entries. Skip if a near-identical entry already exists.
|
|
1091
|
-
5. After storing all findings, append a section to {{notesFilePath}}:
|
|
1092
|
-
## Consolidated {{ISO timestamp}}
|
|
1093
|
-
(Move processed entries under this header \u2014 do NOT delete them, preserve audit trail.)
|
|
1094
|
-
|
|
1095
|
-
VERIFICATION STEP (before storing each finding):
|
|
1096
|
-
For each memory that references a specific source file:
|
|
1097
|
-
1. Use the read tool to check the file still exists and contains the referenced symbol
|
|
1098
|
-
2. If the file no longer exists or the referenced function/class/variable was removed/renamed:
|
|
1099
|
-
- Call memory_forget to remove the stale entry
|
|
1100
|
-
- Do NOT store the new finding
|
|
1101
|
-
3. Only store memories that reference files and symbols that STILL EXIST in the codebase
|
|
1102
|
-
4. Limit verification to 5 files maximum (do not read more than 5 files during this dream cycle)
|
|
1103
|
-
|
|
1104
|
-
Be selective: only store findings that will matter in future sessions. Skip transient details, tool output noise, and one-off questions. Aim for 5-15 high-quality entries per dream cycle.
|
|
1105
|
-
|
|
1106
|
-
IMPORTANT: Only consolidate findings about the PROJECT DOMAIN. Do NOT store meta-patterns about the memory plugin itself (e.g., "user says \u8BB0\u4F4F \u2192 call memory_store"). Those are plugin internals, not project knowledge.
|
|
1107
|
-
|
|
1108
|
-
When done, output a brief summary: "Consolidated N findings (D decisions, C constraints, G gotchas, F facts)."`;
|
|
1109
|
-
function buildPrompt(projectPath) {
|
|
1110
|
-
const notesFilePath = memoryFilePath("project", "notes", projectPath);
|
|
1111
|
-
const sessionsDir = scopeDir("project", projectPath) + "/sessions";
|
|
1112
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1113
|
-
return DREAM_PROMPT_TEMPLATE.replaceAll("{{projectPath}}", projectPath).replaceAll("{{notesFilePath}}", notesFilePath).replaceAll("{{sessionsDir}}", sessionsDir).replaceAll("{{ISO timestamp}}", timestamp);
|
|
1114
|
-
}
|
|
1115
|
-
async function runDream(opts) {
|
|
1116
|
-
const { client, parentSessionID, projectPath, directory, logger } = opts;
|
|
1117
|
-
let dreamSessionID = "";
|
|
1118
|
-
try {
|
|
1119
|
-
const result = await client.session.create({
|
|
1120
|
-
body: {
|
|
1121
|
-
parentID: parentSessionID,
|
|
1122
|
-
title: `Memory Dream Consolidation ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
|
|
1123
|
-
},
|
|
1124
|
-
query: { directory }
|
|
1125
|
-
});
|
|
1126
|
-
dreamSessionID = result.data?.id ?? "";
|
|
1127
|
-
if (!dreamSessionID) {
|
|
1128
|
-
logger?.error("dream-executor: session.create returned no ID", {
|
|
1129
|
-
parentSessionID
|
|
1130
|
-
});
|
|
1131
|
-
return { sessionID: "", status: "failed" };
|
|
1132
|
-
}
|
|
1133
|
-
const prompt = buildPrompt(projectPath);
|
|
1134
|
-
await client.session.promptAsync({
|
|
1135
|
-
path: { id: dreamSessionID },
|
|
1136
|
-
body: {
|
|
1137
|
-
parts: [{ type: "text", text: prompt }],
|
|
1138
|
-
agent: "general",
|
|
1139
|
-
...opts.model ? { model: opts.model } : {},
|
|
1140
|
-
tools: {
|
|
1141
|
-
memory_search: true,
|
|
1142
|
-
memory_store: true,
|
|
1143
|
-
memory_forget: true,
|
|
1144
|
-
read: true,
|
|
1145
|
-
list: true
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
|
-
logger?.info("dream-executor: dream session spawned", {
|
|
1150
|
-
dreamSessionID,
|
|
1151
|
-
parentSessionID
|
|
1152
|
-
});
|
|
1153
|
-
return { sessionID: dreamSessionID, status: "spawned" };
|
|
1154
|
-
} catch (err) {
|
|
1155
|
-
logger?.error("dream-executor: failed to run dream", {
|
|
1156
|
-
dreamSessionID,
|
|
1157
|
-
parentSessionID,
|
|
1158
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1159
|
-
});
|
|
1160
|
-
return { sessionID: dreamSessionID || "", status: "failed" };
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// src/schedule/auto-dream.ts
|
|
1165
|
-
var DREAM_INTERVAL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1166
|
-
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1167
|
-
var NOTES_ACCUMULATION_THRESHOLD = 20;
|
|
1168
|
-
var DEFAULT_SCHEDULE = {
|
|
1169
|
-
lastDream: null,
|
|
1170
|
-
lastDistill: null
|
|
1171
|
-
};
|
|
1172
|
-
function readScheduleFile(projectPath, dataRoot) {
|
|
1173
|
-
const filePath = scheduleFilePath(projectPath, dataRoot);
|
|
1174
|
-
try {
|
|
1175
|
-
const raw = fs5.readFileSync(filePath, "utf8");
|
|
1176
|
-
const parsed = JSON.parse(raw);
|
|
1177
|
-
return {
|
|
1178
|
-
lastDream: parsed.lastDream ?? null,
|
|
1179
|
-
lastDistill: parsed.lastDistill ?? null,
|
|
1180
|
-
queuedDream: parsed.queuedDream,
|
|
1181
|
-
queuedDreamReason: parsed.queuedDreamReason
|
|
1182
|
-
};
|
|
1183
|
-
} catch {
|
|
1184
|
-
return { ...DEFAULT_SCHEDULE };
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
function writeScheduleFile(projectPath, data, dataRoot) {
|
|
1188
|
-
const filePath = scheduleFilePath(projectPath, dataRoot);
|
|
1189
|
-
fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
|
|
1190
|
-
fs5.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1191
|
-
}
|
|
1192
|
-
async function handleSessionCreatedForDream(args) {
|
|
1193
|
-
const { event, config: config2 } = args;
|
|
1194
|
-
const { client, projectPath, logger } = config2;
|
|
1195
|
-
const info = event.properties.info;
|
|
1196
|
-
if (info.parentID) {
|
|
1197
|
-
logger?.debug("auto-dream: skipping sub-session", {
|
|
1198
|
-
sessionID: info.id,
|
|
1199
|
-
parentID: info.parentID
|
|
1200
|
-
});
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
if (info.title.startsWith("Memory ")) {
|
|
1204
|
-
logger?.debug("auto-dream: skipping Memory session", {
|
|
1205
|
-
sessionID: info.id,
|
|
1206
|
-
title: info.title
|
|
1207
|
-
});
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
const schedule = readScheduleFile(projectPath);
|
|
1211
|
-
logger?.debug("auto-dream: schedule state", {
|
|
1212
|
-
lastDream: schedule.lastDream,
|
|
1213
|
-
queuedDream: schedule.queuedDream
|
|
1214
|
-
});
|
|
1215
|
-
if (schedule.queuedDream) {
|
|
1216
|
-
logger?.info("auto-dream: attempting queued dream", {
|
|
1217
|
-
reason: schedule.queuedDreamReason
|
|
1218
|
-
});
|
|
1219
|
-
try {
|
|
1220
|
-
const result = await runDream({
|
|
1221
|
-
client,
|
|
1222
|
-
parentSessionID: info.id,
|
|
1223
|
-
projectPath,
|
|
1224
|
-
directory: info.directory,
|
|
1225
|
-
logger
|
|
1226
|
-
});
|
|
1227
|
-
if (result.status === "spawned") {
|
|
1228
|
-
schedule.queuedDream = void 0;
|
|
1229
|
-
schedule.queuedDreamReason = void 0;
|
|
1230
|
-
writeScheduleFile(projectPath, schedule);
|
|
1231
|
-
logger?.info("auto-dream: queued dream succeeded, flag cleared");
|
|
1232
|
-
} else {
|
|
1233
|
-
logger?.warn("auto-dream: queued dream still failing, leaving flag");
|
|
1234
|
-
}
|
|
1235
|
-
} catch (err) {
|
|
1236
|
-
logger?.warn("auto-dream: queued dream attempt threw", {
|
|
1237
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
const notesPath = memoryFilePath("project", "notes", projectPath);
|
|
1243
|
-
let notesLines = 0;
|
|
1244
|
-
let notesContent = "";
|
|
1245
|
-
try {
|
|
1246
|
-
notesContent = fs5.readFileSync(notesPath, "utf8");
|
|
1247
|
-
if (notesContent.trim().length === 0) {
|
|
1248
|
-
logger?.debug("auto-dream: notes.md is empty, skipping spawn");
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
notesLines = notesContent.split("\n").filter((l) => l.trim()).length;
|
|
1252
|
-
} catch {
|
|
1253
|
-
logger?.debug("auto-dream: notes.md not found, skipping spawn");
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
const memoryPath = memoryFilePath("project", "memory", projectPath);
|
|
1257
|
-
if (!fs5.existsSync(memoryPath) || fs5.statSync(memoryPath).size < 50) {
|
|
1258
|
-
if (notesLines >= 5) {
|
|
1259
|
-
try {
|
|
1260
|
-
fs5.writeFileSync(memoryPath, notesContent, "utf8");
|
|
1261
|
-
logger?.info("auto-dream: bootstrapped MEMORY.md from notes.md", {
|
|
1262
|
-
notesLines
|
|
1263
|
-
});
|
|
1264
|
-
} catch (err) {
|
|
1265
|
-
logger?.warn("auto-dream: failed to bootstrap MEMORY.md", {
|
|
1266
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1267
|
-
});
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
} else {
|
|
1271
|
-
logger?.debug("auto-dream: MEMORY.md missing and notes too small, skipping", {
|
|
1272
|
-
sessionID: info.id
|
|
1273
|
-
});
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
const isSevenDayDue = schedule.lastDream === null || Date.now() - Date.parse(schedule.lastDream) > DREAM_INTERVAL_MS;
|
|
1278
|
-
let isAccumulationDue = false;
|
|
1279
|
-
if (!isSevenDayDue && schedule.lastDream !== null) {
|
|
1280
|
-
const hoursSinceLastDream = (Date.now() - Date.parse(schedule.lastDream)) / ONE_DAY_MS;
|
|
1281
|
-
if (hoursSinceLastDream >= 1 && notesLines > NOTES_ACCUMULATION_THRESHOLD) {
|
|
1282
|
-
isAccumulationDue = true;
|
|
1283
|
-
logger?.info("auto-dream: accumulation trigger", {
|
|
1284
|
-
notesLines,
|
|
1285
|
-
hoursSinceLastDream: hoursSinceLastDream.toFixed(1)
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
if (!isSevenDayDue && !isAccumulationDue) {
|
|
1290
|
-
logger?.debug("auto-dream: not due, skipping");
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
schedule.lastDream = (/* @__PURE__ */ new Date()).toISOString();
|
|
1294
|
-
try {
|
|
1295
|
-
writeScheduleFile(projectPath, schedule);
|
|
1296
|
-
} catch (err) {
|
|
1297
|
-
logger?.error("auto-dream: failed to write schedule file", {
|
|
1298
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1299
|
-
});
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
logger?.info("auto-dream: spawning dream session", {
|
|
1303
|
-
sessionID: info.id,
|
|
1304
|
-
directory: info.directory
|
|
1305
|
-
});
|
|
1306
|
-
try {
|
|
1307
|
-
runDream({
|
|
1308
|
-
client,
|
|
1309
|
-
parentSessionID: info.id,
|
|
1310
|
-
projectPath,
|
|
1311
|
-
directory: info.directory,
|
|
1312
|
-
logger
|
|
1313
|
-
}).catch((err) => {
|
|
1314
|
-
logger?.error("auto-dream: dream spawn failed unexpectedly", {
|
|
1315
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1316
|
-
});
|
|
1317
|
-
try {
|
|
1318
|
-
const fallback = readScheduleFile(projectPath);
|
|
1319
|
-
fallback.queuedDream = true;
|
|
1320
|
-
fallback.queuedDreamReason = `Unexpected failure: ${err instanceof Error ? err.message : String(err)}`;
|
|
1321
|
-
writeScheduleFile(projectPath, fallback);
|
|
1322
|
-
} catch {
|
|
1323
|
-
logger?.error("auto-dream: failed to set queuedDream flag after error");
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
} catch (err) {
|
|
1327
|
-
logger?.error("auto-dream: failed to kick off dream", {
|
|
1328
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1329
|
-
});
|
|
1330
|
-
try {
|
|
1331
|
-
schedule.queuedDream = true;
|
|
1332
|
-
schedule.queuedDreamReason = `Kickoff failure: ${err instanceof Error ? err.message : String(err)}`;
|
|
1333
|
-
writeScheduleFile(projectPath, schedule);
|
|
1334
|
-
} catch {
|
|
1335
|
-
logger?.error("auto-dream: failed to set queuedDream flag");
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
// src/schedule/auto-distill.ts
|
|
1341
|
-
import fs6 from "fs";
|
|
1342
|
-
import path6 from "path";
|
|
1343
|
-
|
|
1344
|
-
// src/schedule/distill-executor.ts
|
|
1345
|
-
var DISTILL_INTERVAL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1346
|
-
var DISTILL_PROMPT_TEMPLATE = `You are a workflow distillation agent. Your task is to identify recurring patterns in the project's memory and package them as reusable skill candidates.
|
|
1347
|
-
|
|
1348
|
-
Project context:
|
|
1349
|
-
- Project path: {{projectPath}}
|
|
1350
|
-
- Memory file: {{memoryFilePath}}
|
|
1351
|
-
- Notes file: {{notesFilePath}}
|
|
1352
|
-
- Sessions dir: {{sessionsDir}}
|
|
1353
|
-
- Output file: {{outputFilePath}}
|
|
1354
|
-
|
|
1355
|
-
Steps:
|
|
1356
|
-
1. Read the memory file at {{memoryFilePath}} and the notes file at {{notesFilePath}}. Identify recurring workflows \u2014 patterns of tool calls or multi-step procedures that appear 3+ times across sessions.
|
|
1357
|
-
2. Use the \`list\` tool to find checkpoint.md files under {{sessionsDir}}. Read the 10 most recent ones to find additional recurring patterns.
|
|
1358
|
-
3. For each recurring workflow you identify, draft a skill candidate as Markdown with these sections:
|
|
1359
|
-
- **Trigger**: when to use this workflow (natural language description)
|
|
1360
|
-
- **Steps**: ordered list of concrete actions
|
|
1361
|
-
- **Tools**: which tools are involved
|
|
1362
|
-
- **Example**: one concrete instance from session history
|
|
1363
|
-
4. Before storing each finding, call memory_search with a relevant phrase to avoid duplicating existing entries. Skip if a near-identical entry already exists.
|
|
1364
|
-
5. Use memory_store with type="fact" and scope="project" to record each distilled workflow. Each entry must be at most 300 characters \u2014 be concise, no code blocks.
|
|
1365
|
-
6. Write all skill candidates to {{outputFilePath}} for human review.
|
|
1366
|
-
|
|
1367
|
-
IMPORTANT: Only distill workflows related to the PROJECT DOMAIN (e.g., code patterns, testing procedures, deployment steps). Do NOT distill meta-patterns about the memory plugin itself (e.g., "user says \u8BB0\u4F4F \u2192 call memory_store"). Those are plugin internals, not reusable project knowledge.
|
|
1368
|
-
|
|
1369
|
-
VERIFICATION STEP (before storing each finding):
|
|
1370
|
-
For each memory that references a specific source file:
|
|
1371
|
-
1. Use the read tool to check the file still exists and contains the referenced symbol
|
|
1372
|
-
2. If the file no longer exists or the referenced function/class/variable was removed/renamed:
|
|
1373
|
-
- Call memory_forget to remove the stale entry
|
|
1374
|
-
- Do NOT store the new finding
|
|
1375
|
-
3. Only store memories that reference files and symbols that STILL EXIST in the codebase
|
|
1376
|
-
4. Limit verification to 5 files maximum (do not read more than 5 files during this distill cycle)
|
|
1377
|
-
|
|
1378
|
-
Distillation is about reusable patterns, not one-off actions. Skip anything that happened only once.
|
|
1379
|
-
|
|
1380
|
-
When done, output a brief summary: "Distilled N workflow candidates."`;
|
|
1381
|
-
function buildPrompt2(projectPath) {
|
|
1382
|
-
const memoryFilePathStr = memoryFilePath("project", "memory", projectPath);
|
|
1383
|
-
const notesFilePath = memoryFilePath("project", "notes", projectPath);
|
|
1384
|
-
const sessionsDir = scopeDir("project", projectPath) + "/sessions";
|
|
1385
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1386
|
-
const outputFilePath = scopeDir("project", projectPath) + `/distill-${timestamp.slice(0, 10)}.md`;
|
|
1387
|
-
return DISTILL_PROMPT_TEMPLATE.replaceAll("{{projectPath}}", projectPath).replaceAll("{{memoryFilePath}}", memoryFilePathStr).replaceAll("{{notesFilePath}}", notesFilePath).replaceAll("{{sessionsDir}}", sessionsDir).replaceAll("{{outputFilePath}}", outputFilePath).replaceAll("{{ISO timestamp}}", timestamp);
|
|
1388
|
-
}
|
|
1389
|
-
async function runDistill(opts) {
|
|
1390
|
-
const { client, parentSessionID, projectPath, directory, logger } = opts;
|
|
1391
|
-
let distillSessionID = "";
|
|
1392
|
-
try {
|
|
1393
|
-
const result = await client.session.create({
|
|
1394
|
-
body: {
|
|
1395
|
-
parentID: parentSessionID,
|
|
1396
|
-
title: `Memory Distill Workflow Packaging ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
|
|
1397
|
-
},
|
|
1398
|
-
query: { directory }
|
|
1399
|
-
});
|
|
1400
|
-
distillSessionID = result.data?.id ?? "";
|
|
1401
|
-
if (!distillSessionID) {
|
|
1402
|
-
logger?.error("distill-executor: session.create returned no ID", {
|
|
1403
|
-
parentSessionID
|
|
1404
|
-
});
|
|
1405
|
-
return { sessionID: "", status: "failed" };
|
|
1406
|
-
}
|
|
1407
|
-
const prompt = buildPrompt2(projectPath);
|
|
1408
|
-
await client.session.promptAsync({
|
|
1409
|
-
path: { id: distillSessionID },
|
|
1410
|
-
body: {
|
|
1411
|
-
parts: [{ type: "text", text: prompt }],
|
|
1412
|
-
agent: "general",
|
|
1413
|
-
...opts.model ? { model: opts.model } : {},
|
|
1414
|
-
tools: {
|
|
1415
|
-
memory_search: true,
|
|
1416
|
-
memory_store: true,
|
|
1417
|
-
memory_forget: true,
|
|
1418
|
-
read: true,
|
|
1419
|
-
list: true
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
});
|
|
1423
|
-
logger?.info("distill-executor: distill session spawned", {
|
|
1424
|
-
distillSessionID,
|
|
1425
|
-
parentSessionID
|
|
1426
|
-
});
|
|
1427
|
-
return { sessionID: distillSessionID, status: "spawned" };
|
|
1428
|
-
} catch (err) {
|
|
1429
|
-
logger?.error("distill-executor: failed to run distill", {
|
|
1430
|
-
distillSessionID,
|
|
1431
|
-
parentSessionID,
|
|
1432
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1433
|
-
});
|
|
1434
|
-
return { sessionID: distillSessionID || "", status: "failed" };
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// src/schedule/auto-distill.ts
|
|
1439
|
-
var DEFAULT_SCHEDULE2 = {
|
|
1440
|
-
lastDream: null,
|
|
1441
|
-
lastDistill: null
|
|
1442
|
-
};
|
|
1443
|
-
function readScheduleFile2(projectPath) {
|
|
1444
|
-
const filePath = scheduleFilePath(projectPath);
|
|
1445
|
-
try {
|
|
1446
|
-
const raw = fs6.readFileSync(filePath, "utf8");
|
|
1447
|
-
const parsed = JSON.parse(raw);
|
|
1448
|
-
return {
|
|
1449
|
-
lastDream: parsed["lastDream"] ?? null,
|
|
1450
|
-
lastDistill: parsed["lastDistill"] ?? null,
|
|
1451
|
-
queuedDream: parsed["queuedDream"],
|
|
1452
|
-
queuedDreamReason: parsed["queuedDreamReason"],
|
|
1453
|
-
queuedDistill: parsed["queuedDistill"],
|
|
1454
|
-
queuedDistillReason: parsed["queuedDistillReason"]
|
|
1455
|
-
};
|
|
1456
|
-
} catch {
|
|
1457
|
-
return { ...DEFAULT_SCHEDULE2 };
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
function writeScheduleFile2(projectPath, data) {
|
|
1461
|
-
const filePath = scheduleFilePath(projectPath);
|
|
1462
|
-
fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
1463
|
-
fs6.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1464
|
-
}
|
|
1465
|
-
async function handleSessionCreatedForDistill(args) {
|
|
1466
|
-
const { event, config: config2 } = args;
|
|
1467
|
-
const { client, projectPath, logger } = config2;
|
|
1468
|
-
const info = event.properties.info;
|
|
1469
|
-
if (info.parentID) {
|
|
1470
|
-
logger?.debug("auto-distill: skipping sub-session", {
|
|
1471
|
-
sessionID: info.id,
|
|
1472
|
-
parentID: info.parentID
|
|
1473
|
-
});
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
if (info.title.startsWith("Memory ")) {
|
|
1477
|
-
logger?.debug("auto-distill: skipping Memory session", {
|
|
1478
|
-
sessionID: info.id,
|
|
1479
|
-
title: info.title
|
|
1480
|
-
});
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
const schedule = readScheduleFile2(projectPath);
|
|
1484
|
-
logger?.debug("auto-distill: schedule state", {
|
|
1485
|
-
lastDistill: schedule.lastDistill,
|
|
1486
|
-
queuedDistill: schedule.queuedDistill
|
|
1487
|
-
});
|
|
1488
|
-
if (schedule.queuedDistill) {
|
|
1489
|
-
logger?.info("auto-distill: attempting queued distill", {
|
|
1490
|
-
reason: schedule.queuedDistillReason
|
|
1491
|
-
});
|
|
1492
|
-
try {
|
|
1493
|
-
const result = await runDistill({
|
|
1494
|
-
client,
|
|
1495
|
-
parentSessionID: info.id,
|
|
1496
|
-
projectPath,
|
|
1497
|
-
directory: info.directory,
|
|
1498
|
-
logger
|
|
1499
|
-
});
|
|
1500
|
-
if (result.status === "spawned") {
|
|
1501
|
-
schedule.queuedDistill = void 0;
|
|
1502
|
-
schedule.queuedDistillReason = void 0;
|
|
1503
|
-
writeScheduleFile2(projectPath, schedule);
|
|
1504
|
-
logger?.info("auto-distill: queued distill succeeded, flag cleared");
|
|
1505
|
-
} else {
|
|
1506
|
-
logger?.warn("auto-distill: queued distill still failing, leaving flag");
|
|
1507
|
-
}
|
|
1508
|
-
} catch (err) {
|
|
1509
|
-
logger?.warn("auto-distill: queued distill attempt threw", {
|
|
1510
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
const memoryPath = memoryFilePath("project", "memory", projectPath);
|
|
1516
|
-
if (!fs6.existsSync(memoryPath) || fs6.statSync(memoryPath).size < 50) {
|
|
1517
|
-
logger?.debug("auto-distill: MEMORY.md missing or too small, skipping", {
|
|
1518
|
-
sessionID: info.id
|
|
1519
|
-
});
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
const isDue = schedule.lastDistill == null || Date.now() - Date.parse(schedule.lastDistill) > DISTILL_INTERVAL_MS;
|
|
1523
|
-
if (!isDue) {
|
|
1524
|
-
logger?.debug("auto-distill: not due, skipping");
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
schedule.lastDistill = (/* @__PURE__ */ new Date()).toISOString();
|
|
1528
|
-
try {
|
|
1529
|
-
writeScheduleFile2(projectPath, schedule);
|
|
1530
|
-
} catch (err) {
|
|
1531
|
-
logger?.error("auto-distill: failed to write schedule file", {
|
|
1532
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1533
|
-
});
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
logger?.info("auto-distill: spawning distill session", {
|
|
1537
|
-
sessionID: info.id,
|
|
1538
|
-
directory: info.directory
|
|
1539
|
-
});
|
|
1540
|
-
try {
|
|
1541
|
-
runDistill({
|
|
1542
|
-
client,
|
|
1543
|
-
parentSessionID: info.id,
|
|
1544
|
-
projectPath,
|
|
1545
|
-
directory: info.directory,
|
|
1546
|
-
logger
|
|
1547
|
-
}).catch((err) => {
|
|
1548
|
-
logger?.error("auto-distill: distill spawn failed unexpectedly", {
|
|
1549
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1550
|
-
});
|
|
1551
|
-
try {
|
|
1552
|
-
const fallback = readScheduleFile2(projectPath);
|
|
1553
|
-
fallback.queuedDistill = true;
|
|
1554
|
-
fallback.queuedDistillReason = `Unexpected failure: ${err instanceof Error ? err.message : String(err)}`;
|
|
1555
|
-
writeScheduleFile2(projectPath, fallback);
|
|
1556
|
-
} catch {
|
|
1557
|
-
logger?.error("auto-distill: failed to set queuedDistill flag after error");
|
|
1558
|
-
}
|
|
1559
|
-
});
|
|
1560
|
-
} catch (err) {
|
|
1561
|
-
logger?.error("auto-distill: failed to kick off distill", {
|
|
1562
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1563
|
-
});
|
|
1564
|
-
try {
|
|
1565
|
-
schedule.queuedDistill = true;
|
|
1566
|
-
schedule.queuedDistillReason = `Kickoff failure: ${err instanceof Error ? err.message : String(err)}`;
|
|
1567
|
-
writeScheduleFile2(projectPath, schedule);
|
|
1568
|
-
} catch {
|
|
1569
|
-
logger?.error("auto-distill: failed to set queuedDistill flag");
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
624
|
// src/search/service.ts
|
|
1575
|
-
import
|
|
625
|
+
import fs6 from "fs/promises";
|
|
1576
626
|
import { existsSync as existsSync2 } from "fs";
|
|
1577
|
-
import
|
|
627
|
+
import path5 from "path";
|
|
1578
628
|
|
|
1579
629
|
// src/search/bm25.ts
|
|
1580
630
|
var BM25Index = class _BM25Index {
|
|
@@ -1764,14 +814,14 @@ var BM25Index = class _BM25Index {
|
|
|
1764
814
|
};
|
|
1765
815
|
|
|
1766
816
|
// src/search/reconcile.ts
|
|
1767
|
-
import
|
|
1768
|
-
import
|
|
817
|
+
import fs5 from "fs/promises";
|
|
818
|
+
import path4 from "path";
|
|
1769
819
|
import { existsSync, readFileSync } from "fs";
|
|
1770
820
|
|
|
1771
821
|
// src/search/tokenizer.ts
|
|
1772
822
|
var CJK_RE = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3040-\u309F\u30A0-\u30FF]/;
|
|
1773
823
|
var TOKEN_SPLIT_RE = /[\s\p{P}]+/u;
|
|
1774
|
-
function
|
|
824
|
+
function tokenize(text) {
|
|
1775
825
|
if (!text) return [];
|
|
1776
826
|
const tokens = [];
|
|
1777
827
|
let cjkRun = "";
|
|
@@ -1812,7 +862,7 @@ function tokenizeQuery(text) {
|
|
|
1812
862
|
const phrases = text.split("|");
|
|
1813
863
|
const result = [];
|
|
1814
864
|
for (const phrase of phrases) {
|
|
1815
|
-
const tokens =
|
|
865
|
+
const tokens = tokenize(phrase.trim());
|
|
1816
866
|
if (tokens.length > 0) result.push(tokens);
|
|
1817
867
|
}
|
|
1818
868
|
return result;
|
|
@@ -1921,13 +971,13 @@ var Reconciler = class {
|
|
|
1921
971
|
dir = projectMemoryDir(this.projectPath);
|
|
1922
972
|
break;
|
|
1923
973
|
case "session": {
|
|
1924
|
-
const sessionsDir =
|
|
974
|
+
const sessionsDir = path4.join(projectMemoryDir(this.projectPath), "sessions");
|
|
1925
975
|
if (!existsSync(sessionsDir)) return [];
|
|
1926
|
-
const sessionDirs = await
|
|
976
|
+
const sessionDirs = await fs5.readdir(sessionsDir);
|
|
1927
977
|
for (const sid of sessionDirs) {
|
|
1928
|
-
const sessionDir =
|
|
1929
|
-
const
|
|
1930
|
-
if (!
|
|
978
|
+
const sessionDir = path4.join(sessionsDir, sid);
|
|
979
|
+
const stat = await fs5.stat(sessionDir);
|
|
980
|
+
if (!stat.isDirectory()) continue;
|
|
1931
981
|
const files = await this.walkMarkdown(sessionDir, scope);
|
|
1932
982
|
results.push(...files);
|
|
1933
983
|
}
|
|
@@ -1944,16 +994,16 @@ var Reconciler = class {
|
|
|
1944
994
|
const results = [];
|
|
1945
995
|
let entries;
|
|
1946
996
|
try {
|
|
1947
|
-
entries = await
|
|
997
|
+
entries = await fs5.readdir(dir);
|
|
1948
998
|
} catch {
|
|
1949
999
|
return [];
|
|
1950
1000
|
}
|
|
1951
1001
|
for (const entry of entries) {
|
|
1952
|
-
const fullPath =
|
|
1002
|
+
const fullPath = path4.join(dir, entry);
|
|
1953
1003
|
try {
|
|
1954
|
-
const
|
|
1955
|
-
if (
|
|
1956
|
-
results.push({ path: fullPath, scope, mtime:
|
|
1004
|
+
const stat = await fs5.stat(fullPath);
|
|
1005
|
+
if (stat.isFile() && entry.endsWith(".md")) {
|
|
1006
|
+
results.push({ path: fullPath, scope, mtime: stat.mtimeMs });
|
|
1957
1007
|
}
|
|
1958
1008
|
} catch {
|
|
1959
1009
|
}
|
|
@@ -1966,7 +1016,7 @@ var Reconciler = class {
|
|
|
1966
1016
|
async indexFile(file2) {
|
|
1967
1017
|
let content;
|
|
1968
1018
|
try {
|
|
1969
|
-
content = await
|
|
1019
|
+
content = await fs5.readFile(file2.path, "utf8");
|
|
1970
1020
|
} catch {
|
|
1971
1021
|
return;
|
|
1972
1022
|
}
|
|
@@ -1975,7 +1025,7 @@ var Reconciler = class {
|
|
|
1975
1025
|
for (const section of sections) {
|
|
1976
1026
|
const docId = section.heading ? `${file2.path}#${section.heading}` : file2.path;
|
|
1977
1027
|
const textToTokenize = section.heading ? `${section.heading} ${section.body}` : section.body;
|
|
1978
|
-
const tokens =
|
|
1028
|
+
const tokens = tokenize(textToTokenize);
|
|
1979
1029
|
if (tokens.length > 0) {
|
|
1980
1030
|
let timestamp;
|
|
1981
1031
|
const tsMatch = section.body.match(/\[(\d{4}-\d{2}-\d{2})\]/);
|
|
@@ -2022,11 +1072,11 @@ var Reconciler = class {
|
|
|
2022
1072
|
*/
|
|
2023
1073
|
async saveIndexState() {
|
|
2024
1074
|
const statePath = this.getStatePath();
|
|
2025
|
-
const dir =
|
|
2026
|
-
await
|
|
1075
|
+
const dir = path4.dirname(statePath);
|
|
1076
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
2027
1077
|
const release = await acquireLock(statePath);
|
|
2028
1078
|
try {
|
|
2029
|
-
await
|
|
1079
|
+
await fs5.writeFile(
|
|
2030
1080
|
statePath,
|
|
2031
1081
|
JSON.stringify(this.indexState, null, 2),
|
|
2032
1082
|
"utf8"
|
|
@@ -2062,6 +1112,9 @@ var SearchService = class {
|
|
|
2062
1112
|
index: this.index
|
|
2063
1113
|
});
|
|
2064
1114
|
}
|
|
1115
|
+
get project() {
|
|
1116
|
+
return this.projectPath;
|
|
1117
|
+
}
|
|
2065
1118
|
/**
|
|
2066
1119
|
* Ensure the index is initialized. Lazy — calls Reconciler.sync() on first call.
|
|
2067
1120
|
*/
|
|
@@ -2137,14 +1190,14 @@ var SearchService = class {
|
|
|
2137
1190
|
void 0,
|
|
2138
1191
|
this.dataRoot
|
|
2139
1192
|
);
|
|
2140
|
-
await
|
|
1193
|
+
await fs6.mkdir(path5.dirname(filePath), { recursive: true });
|
|
2141
1194
|
const heading = `## ${section}`;
|
|
2142
1195
|
const entry = `- ${content.trim()}`;
|
|
2143
1196
|
const release = await acquireLock(filePath);
|
|
2144
1197
|
try {
|
|
2145
1198
|
let existing = "";
|
|
2146
1199
|
if (existsSync2(filePath)) {
|
|
2147
|
-
existing = await
|
|
1200
|
+
existing = await fs6.readFile(filePath, "utf8");
|
|
2148
1201
|
}
|
|
2149
1202
|
const headingIdx = existing.indexOf(heading);
|
|
2150
1203
|
if (headingIdx !== -1) {
|
|
@@ -2153,14 +1206,14 @@ var SearchService = class {
|
|
|
2153
1206
|
const before = existing.slice(0, afterHeading);
|
|
2154
1207
|
const after = existing.slice(afterHeading);
|
|
2155
1208
|
const newContent = before + "\n" + entry + "\n" + after;
|
|
2156
|
-
await
|
|
1209
|
+
await fs6.writeFile(filePath, newContent, "utf8");
|
|
2157
1210
|
} else {
|
|
2158
1211
|
const newContent = existing.trimEnd() + "\n" + entry + "\n";
|
|
2159
|
-
await
|
|
1212
|
+
await fs6.writeFile(filePath, newContent, "utf8");
|
|
2160
1213
|
}
|
|
2161
1214
|
} else {
|
|
2162
1215
|
const newContent = existing.trimEnd() + "\n\n" + heading + "\n" + entry + "\n";
|
|
2163
|
-
await
|
|
1216
|
+
await fs6.writeFile(filePath, newContent, "utf8");
|
|
2164
1217
|
}
|
|
2165
1218
|
} finally {
|
|
2166
1219
|
release();
|
|
@@ -2192,7 +1245,7 @@ var SearchService = class {
|
|
|
2192
1245
|
const release = await acquireLock(filePath);
|
|
2193
1246
|
let removed = 0;
|
|
2194
1247
|
try {
|
|
2195
|
-
const content = await
|
|
1248
|
+
const content = await fs6.readFile(filePath, "utf8");
|
|
2196
1249
|
const lines = content.split("\n");
|
|
2197
1250
|
const kept = [];
|
|
2198
1251
|
for (const line of lines) {
|
|
@@ -2206,7 +1259,7 @@ var SearchService = class {
|
|
|
2206
1259
|
kept.push(line);
|
|
2207
1260
|
}
|
|
2208
1261
|
if (removed > 0) {
|
|
2209
|
-
await
|
|
1262
|
+
await fs6.writeFile(filePath, kept.join("\n"), "utf8");
|
|
2210
1263
|
}
|
|
2211
1264
|
} finally {
|
|
2212
1265
|
release();
|
|
@@ -2253,7 +1306,7 @@ var SearchService = class {
|
|
|
2253
1306
|
async extractSnippet(filePath, heading, matchedTerms) {
|
|
2254
1307
|
let content;
|
|
2255
1308
|
try {
|
|
2256
|
-
content = await
|
|
1309
|
+
content = await fs6.readFile(filePath, "utf8");
|
|
2257
1310
|
} catch {
|
|
2258
1311
|
return "";
|
|
2259
1312
|
}
|
|
@@ -2288,7 +1341,7 @@ var SearchService = class {
|
|
|
2288
1341
|
};
|
|
2289
1342
|
|
|
2290
1343
|
// src/tools/index.ts
|
|
2291
|
-
import { tool as
|
|
1344
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
2292
1345
|
|
|
2293
1346
|
// src/tools/memory-search.ts
|
|
2294
1347
|
import { tool } from "@opencode-ai/plugin";
|
|
@@ -2325,6 +1378,25 @@ function createMemorySearchTool(service) {
|
|
|
2325
1378
|
|
|
2326
1379
|
// src/tools/memory-store.ts
|
|
2327
1380
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
1381
|
+
import fs7 from "fs";
|
|
1382
|
+
import nodePath2 from "path";
|
|
1383
|
+
var MEMORY_MAX_LINES = 200;
|
|
1384
|
+
var MEMORY_MAX_BYTES = 25e3;
|
|
1385
|
+
async function checkOverflow(filePath) {
|
|
1386
|
+
try {
|
|
1387
|
+
const content = await fs7.promises.readFile(filePath, "utf8");
|
|
1388
|
+
return { lines: content.split("\n").length, bytes: content.length };
|
|
1389
|
+
} catch {
|
|
1390
|
+
return { lines: 0, bytes: 0 };
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
async function archiveEntry(filePath, entry) {
|
|
1394
|
+
const archivePath = filePath.replace("MEMORY.md", "MEMORY-archive.md");
|
|
1395
|
+
await fs7.promises.mkdir(nodePath2.dirname(archivePath), { recursive: true });
|
|
1396
|
+
await fs7.promises.appendFile(archivePath, `
|
|
1397
|
+
${entry}
|
|
1398
|
+
`, "utf8");
|
|
1399
|
+
}
|
|
2328
1400
|
function createMemoryStoreTool(service) {
|
|
2329
1401
|
return tool2({
|
|
2330
1402
|
description: "Store a memory entry (decision, constraint, gotcha, fact, note) to persistent memory.",
|
|
@@ -2344,6 +1416,12 @@ function createMemoryStoreTool(service) {
|
|
|
2344
1416
|
const section = sectionMap[args.type] ?? "Notes";
|
|
2345
1417
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2346
1418
|
const contentWithDate = `${args.content} [${today}]`;
|
|
1419
|
+
const memoryPath = memoryFilePath(args.scope, "memory", service.project);
|
|
1420
|
+
const { lines, bytes } = await checkOverflow(memoryPath);
|
|
1421
|
+
if (lines >= MEMORY_MAX_LINES || bytes >= MEMORY_MAX_BYTES) {
|
|
1422
|
+
await archiveEntry(memoryPath, `- ${contentWithDate}`);
|
|
1423
|
+
return `MEMORY.md at cap (${lines} lines/${bytes} bytes). Entry archived to MEMORY-archive.md. Use memory_search on MEMORY.md content; archived entries are available for manual review.`;
|
|
1424
|
+
}
|
|
2347
1425
|
await service.addEntry(args.scope, "memory", section, contentWithDate);
|
|
2348
1426
|
return `Stored ${args.type} in ${args.scope} memory under ## ${section}`;
|
|
2349
1427
|
}
|
|
@@ -3125,10 +2203,10 @@ function mergeDefs(...defs) {
|
|
|
3125
2203
|
function cloneDef(schema) {
|
|
3126
2204
|
return mergeDefs(schema._zod.def);
|
|
3127
2205
|
}
|
|
3128
|
-
function getElementAtPath(obj,
|
|
3129
|
-
if (!
|
|
2206
|
+
function getElementAtPath(obj, path9) {
|
|
2207
|
+
if (!path9)
|
|
3130
2208
|
return obj;
|
|
3131
|
-
return
|
|
2209
|
+
return path9.reduce((acc, key) => acc?.[key], obj);
|
|
3132
2210
|
}
|
|
3133
2211
|
function promiseAllObject(promisesObj) {
|
|
3134
2212
|
const keys = Object.keys(promisesObj);
|
|
@@ -3489,11 +2567,11 @@ function aborted(x, startIndex = 0) {
|
|
|
3489
2567
|
}
|
|
3490
2568
|
return false;
|
|
3491
2569
|
}
|
|
3492
|
-
function prefixIssues(
|
|
2570
|
+
function prefixIssues(path9, issues) {
|
|
3493
2571
|
return issues.map((iss) => {
|
|
3494
2572
|
var _a;
|
|
3495
2573
|
(_a = iss).path ?? (_a.path = []);
|
|
3496
|
-
iss.path.unshift(
|
|
2574
|
+
iss.path.unshift(path9);
|
|
3497
2575
|
return iss;
|
|
3498
2576
|
});
|
|
3499
2577
|
}
|
|
@@ -3661,7 +2739,7 @@ function treeifyError(error45, _mapper) {
|
|
|
3661
2739
|
return issue2.message;
|
|
3662
2740
|
};
|
|
3663
2741
|
const result = { errors: [] };
|
|
3664
|
-
const processError = (error46,
|
|
2742
|
+
const processError = (error46, path9 = []) => {
|
|
3665
2743
|
var _a, _b;
|
|
3666
2744
|
for (const issue2 of error46.issues) {
|
|
3667
2745
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -3671,7 +2749,7 @@ function treeifyError(error45, _mapper) {
|
|
|
3671
2749
|
} else if (issue2.code === "invalid_element") {
|
|
3672
2750
|
processError({ issues: issue2.issues }, issue2.path);
|
|
3673
2751
|
} else {
|
|
3674
|
-
const fullpath = [...
|
|
2752
|
+
const fullpath = [...path9, ...issue2.path];
|
|
3675
2753
|
if (fullpath.length === 0) {
|
|
3676
2754
|
result.errors.push(mapper(issue2));
|
|
3677
2755
|
continue;
|
|
@@ -3703,8 +2781,8 @@ function treeifyError(error45, _mapper) {
|
|
|
3703
2781
|
}
|
|
3704
2782
|
function toDotPath(_path) {
|
|
3705
2783
|
const segs = [];
|
|
3706
|
-
const
|
|
3707
|
-
for (const seg of
|
|
2784
|
+
const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
2785
|
+
for (const seg of path9) {
|
|
3708
2786
|
if (typeof seg === "number")
|
|
3709
2787
|
segs.push(`[${seg}]`);
|
|
3710
2788
|
else if (typeof seg === "symbol")
|
|
@@ -14811,7 +13889,7 @@ function date4(params) {
|
|
|
14811
13889
|
config(en_default());
|
|
14812
13890
|
|
|
14813
13891
|
// src/tools/memory-expand.ts
|
|
14814
|
-
import
|
|
13892
|
+
import fs8 from "fs";
|
|
14815
13893
|
function createMemoryExpandTool(opts) {
|
|
14816
13894
|
return {
|
|
14817
13895
|
description: "Expand compressed context \u2014 retrieve original content of a message that was stripped by the memory plugin's context compression. Use when you need to see the full reasoning, tool output, or text of an old message that was compressed.",
|
|
@@ -14820,11 +13898,11 @@ function createMemoryExpandTool(opts) {
|
|
|
14820
13898
|
}).shape,
|
|
14821
13899
|
execute: async (args) => {
|
|
14822
13900
|
const rawPath = checkpointRawPath(opts.projectPath, "");
|
|
14823
|
-
if (!
|
|
13901
|
+
if (!fs8.existsSync(rawPath)) {
|
|
14824
13902
|
return "No checkpoint.raw.json found. No compressed messages to expand.";
|
|
14825
13903
|
}
|
|
14826
13904
|
try {
|
|
14827
|
-
const raw = JSON.parse(
|
|
13905
|
+
const raw = JSON.parse(fs8.readFileSync(rawPath, "utf8"));
|
|
14828
13906
|
const messages = raw.messages || [];
|
|
14829
13907
|
const msg = messages.find(
|
|
14830
13908
|
(m) => m.info?.id === args.messageID
|
|
@@ -14876,53 +13954,45 @@ ${part.thinking || part.text || "[empty]"}
|
|
|
14876
13954
|
return output;
|
|
14877
13955
|
}
|
|
14878
13956
|
|
|
14879
|
-
// src/compress
|
|
14880
|
-
import {
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
13957
|
+
// src/tools/context-compress.ts
|
|
13958
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
13959
|
+
function createContextCompressTool(state) {
|
|
13960
|
+
return tool4({
|
|
13961
|
+
description: "Compress older conversation context to reclaim token budget. Triggers compression of old tool outputs on the next turn \u2014 originals recoverable via deep_expand. Use when the conversation feels long or you're losing track of early context.",
|
|
13962
|
+
args: {
|
|
13963
|
+
keep_recent: tool4.schema.number().default(8).describe("Number of recent messages to protect from compression (default 8)")
|
|
13964
|
+
},
|
|
13965
|
+
async execute(args) {
|
|
13966
|
+
const keep = Math.max(2, Math.floor(args.keep_recent));
|
|
13967
|
+
state.requestCompression(keep);
|
|
13968
|
+
return {
|
|
13969
|
+
title: "Compression requested",
|
|
13970
|
+
output: `Will compress tool outputs older than the last ${keep} messages on the next turn. Protected: memory_*, edit, write, todowrite, skill. Originals stored in CCR \u2014 call deep_expand("<hash>") to restore any compressed content.`
|
|
13971
|
+
};
|
|
13972
|
+
}
|
|
14891
13973
|
});
|
|
14892
|
-
return hash2;
|
|
14893
|
-
}
|
|
14894
|
-
function ccrRetrieve(state, hash2) {
|
|
14895
|
-
const entry = state.ccrGet(hash2);
|
|
14896
|
-
if (!entry) return void 0;
|
|
14897
|
-
if (Date.now() - entry.createdAt > CCR_TTL_MS) return void 0;
|
|
14898
|
-
return entry.original;
|
|
14899
|
-
}
|
|
14900
|
-
function ccrInjectMarker(compressed, hash2) {
|
|
14901
|
-
return `${compressed}
|
|
14902
|
-
[ccr:${hash2}]`;
|
|
14903
|
-
}
|
|
14904
|
-
function sha256(data) {
|
|
14905
|
-
return createHash2("sha256").update(data).digest("hex");
|
|
14906
13974
|
}
|
|
14907
13975
|
|
|
14908
13976
|
// src/tools/index.ts
|
|
14909
|
-
function createMemoryTools(service, opts) {
|
|
13977
|
+
function createMemoryTools(service, state, opts) {
|
|
14910
13978
|
const search = createMemorySearchTool(service);
|
|
14911
13979
|
const store = createMemoryStoreTool(service);
|
|
14912
13980
|
const forget = createMemoryForgetTool(service);
|
|
14913
13981
|
const expand = opts?.projectPath ? createMemoryExpandTool({ projectPath: opts.projectPath }) : createMemoryExpandTool({ projectPath: "" });
|
|
13982
|
+
const compress = createContextCompressTool(state);
|
|
14914
13983
|
return {
|
|
14915
13984
|
memory_search: search,
|
|
14916
13985
|
memory_store: store,
|
|
14917
13986
|
memory_forget: forget,
|
|
14918
|
-
memory_expand: expand
|
|
13987
|
+
memory_expand: expand,
|
|
13988
|
+
context_compress: compress
|
|
14919
13989
|
};
|
|
14920
13990
|
}
|
|
14921
13991
|
function createDeepExpandTool(state) {
|
|
14922
|
-
return
|
|
13992
|
+
return tool5({
|
|
14923
13993
|
description: "Retrieve original content that was previously compressed. Use hash from [ccr:...] markers.",
|
|
14924
13994
|
args: {
|
|
14925
|
-
hash:
|
|
13995
|
+
hash: tool5.schema.string().describe("The hash from the [ccr:HASH] marker")
|
|
14926
13996
|
},
|
|
14927
13997
|
execute: async (args) => {
|
|
14928
13998
|
const original = ccrRetrieve(state, args.hash);
|
|
@@ -14933,8 +14003,8 @@ function createDeepExpandTool(state) {
|
|
|
14933
14003
|
}
|
|
14934
14004
|
|
|
14935
14005
|
// src/extract/capture.ts
|
|
14936
|
-
import { writeFile
|
|
14937
|
-
import
|
|
14006
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
14007
|
+
import path6 from "path";
|
|
14938
14008
|
async function captureMessages(args) {
|
|
14939
14009
|
const { client, sessionID, projectPath, logger } = args;
|
|
14940
14010
|
let result;
|
|
@@ -14962,10 +14032,10 @@ async function captureMessages(args) {
|
|
|
14962
14032
|
null,
|
|
14963
14033
|
2
|
|
14964
14034
|
);
|
|
14965
|
-
await
|
|
14035
|
+
await mkdir(path6.dirname(rawFilePath), { recursive: true });
|
|
14966
14036
|
const release = await acquireLock(rawFilePath);
|
|
14967
14037
|
try {
|
|
14968
|
-
await
|
|
14038
|
+
await writeFile(rawFilePath, payload, "utf-8");
|
|
14969
14039
|
} finally {
|
|
14970
14040
|
release();
|
|
14971
14041
|
}
|
|
@@ -15089,17 +14159,17 @@ function extractFileChanges(messages) {
|
|
|
15089
14159
|
for (const msg of messages) {
|
|
15090
14160
|
for (const part of msg.parts) {
|
|
15091
14161
|
if (part.type !== "tool" || !part.tool) continue;
|
|
15092
|
-
const
|
|
15093
|
-
if (
|
|
14162
|
+
const tool6 = part.tool.toLowerCase();
|
|
14163
|
+
if (tool6 === "write" || tool6 === "edit") {
|
|
15094
14164
|
const filePath = part.args?.filePath || part.args?.path || "";
|
|
15095
14165
|
if (filePath) {
|
|
15096
|
-
const key = `${filePath}:${
|
|
14166
|
+
const key = `${filePath}:${tool6}`;
|
|
15097
14167
|
if (!seen.has(key)) {
|
|
15098
14168
|
seen.add(key);
|
|
15099
|
-
changes.push({ path: filePath, operation:
|
|
14169
|
+
changes.push({ path: filePath, operation: tool6 });
|
|
15100
14170
|
}
|
|
15101
14171
|
}
|
|
15102
|
-
} else if (
|
|
14172
|
+
} else if (tool6 === "bash" || tool6 === "execute") {
|
|
15103
14173
|
const cmd = part.args?.command || "";
|
|
15104
14174
|
const match = BASH_FILE_OP_RE.exec(cmd);
|
|
15105
14175
|
if (match?.[1]) {
|
|
@@ -15125,8 +14195,8 @@ function extractHeuristics(messages) {
|
|
|
15125
14195
|
}
|
|
15126
14196
|
|
|
15127
14197
|
// src/extract/checkpoint-writer.ts
|
|
15128
|
-
import { writeFile as
|
|
15129
|
-
import
|
|
14198
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
14199
|
+
import path7 from "path";
|
|
15130
14200
|
function renderCheckpoint(args) {
|
|
15131
14201
|
const { sessionID, tokenEstimate, result } = args;
|
|
15132
14202
|
const lines = [];
|
|
@@ -15181,10 +14251,10 @@ function renderCheckpoint(args) {
|
|
|
15181
14251
|
async function writeCheckpoint(args) {
|
|
15182
14252
|
const { projectPath, content, logger } = args;
|
|
15183
14253
|
const filePath = memoryFilePath("project", "checkpoint", projectPath);
|
|
15184
|
-
await
|
|
14254
|
+
await mkdir2(path7.dirname(filePath), { recursive: true });
|
|
15185
14255
|
const release = await acquireLock(filePath);
|
|
15186
14256
|
try {
|
|
15187
|
-
await
|
|
14257
|
+
await writeFile2(filePath, content, "utf-8");
|
|
15188
14258
|
} finally {
|
|
15189
14259
|
release();
|
|
15190
14260
|
}
|
|
@@ -15192,8 +14262,71 @@ async function writeCheckpoint(args) {
|
|
|
15192
14262
|
return filePath;
|
|
15193
14263
|
}
|
|
15194
14264
|
|
|
14265
|
+
// src/extract/consolidate.ts
|
|
14266
|
+
function tokenize2(s) {
|
|
14267
|
+
return s.toLowerCase().split(/[\s\-,.\[\](){}:]+/).filter((w) => w.length > 2);
|
|
14268
|
+
}
|
|
14269
|
+
function simHash(s, bits = 64) {
|
|
14270
|
+
const tokens = tokenize2(s);
|
|
14271
|
+
if (tokens.length === 0) return 0;
|
|
14272
|
+
const v = new Int8Array(bits);
|
|
14273
|
+
for (const token of tokens) {
|
|
14274
|
+
let h = 0;
|
|
14275
|
+
for (let i = 0; i < token.length; i++) {
|
|
14276
|
+
h = (h << 5) - h + token.charCodeAt(i) | 0;
|
|
14277
|
+
}
|
|
14278
|
+
for (let i = 0; i < bits; i++) {
|
|
14279
|
+
if (h >> i & 1) v[i]++;
|
|
14280
|
+
else v[i]--;
|
|
14281
|
+
}
|
|
14282
|
+
}
|
|
14283
|
+
let hash2 = 0;
|
|
14284
|
+
for (let i = 0; i < bits; i++) {
|
|
14285
|
+
if (v[i] > 0) hash2 |= 1 << i;
|
|
14286
|
+
}
|
|
14287
|
+
return hash2;
|
|
14288
|
+
}
|
|
14289
|
+
function hammingDistance(a, b) {
|
|
14290
|
+
let xor = a ^ b;
|
|
14291
|
+
let dist = 0;
|
|
14292
|
+
while (xor) {
|
|
14293
|
+
dist += xor & 1;
|
|
14294
|
+
xor >>>= 1;
|
|
14295
|
+
}
|
|
14296
|
+
return dist;
|
|
14297
|
+
}
|
|
14298
|
+
function similarity(a, b, bits = 64) {
|
|
14299
|
+
return 1 - hammingDistance(a, b) / bits;
|
|
14300
|
+
}
|
|
14301
|
+
var SIMILARITY_THRESHOLD = 0.92;
|
|
14302
|
+
var STALE_BINDING_RE = /^(- \[[^\]]+\] )(src\/[^\s:]+:[^\s:]+)(?::[a-f0-9]+)?\s/;
|
|
14303
|
+
function consolidateMemory(content, opts = {}) {
|
|
14304
|
+
if (!content.trim()) return content;
|
|
14305
|
+
const lines = content.split("\n");
|
|
14306
|
+
const staleSet = new Set(opts.staleFilePaths ?? []);
|
|
14307
|
+
const seen = [];
|
|
14308
|
+
const result = [];
|
|
14309
|
+
for (const line of lines) {
|
|
14310
|
+
if (!line.startsWith("- [")) {
|
|
14311
|
+
result.push(line);
|
|
14312
|
+
continue;
|
|
14313
|
+
}
|
|
14314
|
+
if (staleSet.size > 0) {
|
|
14315
|
+
const m = line.match(STALE_BINDING_RE);
|
|
14316
|
+
if (m && staleSet.has(m[2])) continue;
|
|
14317
|
+
}
|
|
14318
|
+
const hash2 = simHash(line);
|
|
14319
|
+
const isDup = seen.some((s) => similarity(hash2, s.hash) >= SIMILARITY_THRESHOLD);
|
|
14320
|
+
if (isDup) continue;
|
|
14321
|
+
seen.push({ hash: hash2, line });
|
|
14322
|
+
result.push(line);
|
|
14323
|
+
}
|
|
14324
|
+
return result.join("\n");
|
|
14325
|
+
}
|
|
14326
|
+
|
|
15195
14327
|
// src/hooks/compacting.ts
|
|
15196
|
-
import { readFile as
|
|
14328
|
+
import { readFile, writeFile as writeFile3 } from "fs/promises";
|
|
14329
|
+
import { existsSync as existsSync3 } from "fs";
|
|
15197
14330
|
|
|
15198
14331
|
// src/extract/summarize.ts
|
|
15199
14332
|
var HANDOFF_PREFIX = `Another OpenCode session started by the same user was working on this task. It was compacted mid-conversation to save context space. Review the summary below to understand what happened and continue from where it left off.`;
|
|
@@ -15242,7 +14375,7 @@ Be concise. Prefer structured lists over prose. Focus on what the next LLM NEEDS
|
|
|
15242
14375
|
|
|
15243
14376
|
// src/hooks/compacting.ts
|
|
15244
14377
|
function createCompactingHandler(args) {
|
|
15245
|
-
const { client,
|
|
14378
|
+
const { client, projectPath, logger, tracker } = args;
|
|
15246
14379
|
return async (input, output) => {
|
|
15247
14380
|
const { sessionID } = input;
|
|
15248
14381
|
try {
|
|
@@ -15256,7 +14389,7 @@ function createCompactingHandler(args) {
|
|
|
15256
14389
|
}
|
|
15257
14390
|
let rawMessages;
|
|
15258
14391
|
try {
|
|
15259
|
-
const raw = await
|
|
14392
|
+
const raw = await readFile(capture.rawFilePath, "utf-8");
|
|
15260
14393
|
const parsed = JSON.parse(raw);
|
|
15261
14394
|
rawMessages = parsed.messages;
|
|
15262
14395
|
} catch (readErr) {
|
|
@@ -15285,7 +14418,30 @@ function createCompactingHandler(args) {
|
|
|
15285
14418
|
content: markdown,
|
|
15286
14419
|
logger
|
|
15287
14420
|
});
|
|
15288
|
-
|
|
14421
|
+
try {
|
|
14422
|
+
const memPath = memoryFilePath("project", "memory", projectPath);
|
|
14423
|
+
if (existsSync3(memPath)) {
|
|
14424
|
+
const release = await acquireLock(memPath);
|
|
14425
|
+
try {
|
|
14426
|
+
const content = await readFile(memPath, "utf8");
|
|
14427
|
+
const consolidated = consolidateMemory(content);
|
|
14428
|
+
if (consolidated !== content) {
|
|
14429
|
+
await writeFile3(memPath, consolidated, "utf8");
|
|
14430
|
+
logger?.info("compacting: consolidated MEMORY.md", {
|
|
14431
|
+
beforeBytes: content.length,
|
|
14432
|
+
afterBytes: consolidated.length,
|
|
14433
|
+
diff: content.length - consolidated.length
|
|
14434
|
+
});
|
|
14435
|
+
}
|
|
14436
|
+
} finally {
|
|
14437
|
+
release();
|
|
14438
|
+
}
|
|
14439
|
+
}
|
|
14440
|
+
} catch (err) {
|
|
14441
|
+
logger?.warn("compacting: consolidate failed (non-fatal)", {
|
|
14442
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14443
|
+
});
|
|
14444
|
+
}
|
|
15289
14445
|
if (capture.messageCount >= 20) {
|
|
15290
14446
|
output.prompt = STRUCTURED_COMPACTION_PROMPT;
|
|
15291
14447
|
}
|
|
@@ -15396,390 +14552,50 @@ function detectPressure(messages, modelContextWindow) {
|
|
|
15396
14552
|
return { level, ratio, estimatedTokens: estimated, maxContext: ctx };
|
|
15397
14553
|
}
|
|
15398
14554
|
|
|
15399
|
-
// src/compress/
|
|
15400
|
-
var
|
|
15401
|
-
|
|
15402
|
-
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
}
|
|
15410
|
-
return "";
|
|
15411
|
-
}
|
|
15412
|
-
|
|
15413
|
-
// src/compress/memory-nudge.ts
|
|
15414
|
-
var MEMORY_NUDGE_COOLDOWN = 3;
|
|
15415
|
-
var DECISION_PATTERNS = [
|
|
15416
|
-
/\b(?:decided|decision|chose|chosen|picked|selected)\b/i,
|
|
15417
|
-
/(?:采用|选择|决定|确定|选用)/,
|
|
15418
|
-
/\b(?:use|using|go with|went with)\b.*\b(?:because|since|due to)\b/i
|
|
15419
|
-
];
|
|
15420
|
-
var CONSTRAINT_PATTERNS = [
|
|
15421
|
-
/\b(?:must not|cannot|should not|do not|never|always)\b/i,
|
|
15422
|
-
/\b(?:constraint|restriction|limitation|requirement)\b/i,
|
|
15423
|
-
/(?:不能|必须|禁止|约束|限制|要求|务必)/
|
|
15424
|
-
];
|
|
15425
|
-
var ERROR_FIX_PATTERNS = [
|
|
15426
|
-
/\b(?:fix|fixed|resolve|resolved|patch|corrected)\b/i,
|
|
15427
|
-
/(?:修复|修复了|解决|解决了)/,
|
|
15428
|
-
/\b(?:the (?:bug|error|issue) (?:was|is)|root cause)\b/i
|
|
15429
|
-
];
|
|
15430
|
-
function detectMemoryNudge(messages, messagesSinceLastNudge) {
|
|
15431
|
-
if (messagesSinceLastNudge < MEMORY_NUDGE_COOLDOWN) {
|
|
15432
|
-
return { injected: false, type: null };
|
|
15433
|
-
}
|
|
15434
|
-
const protectedTail = Math.max(0, messages.length - 3);
|
|
15435
|
-
const recentMessages = messages.slice(protectedTail);
|
|
15436
|
-
const recentAssistantText = recentMessages.filter((m) => m.info.role === "assistant").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
|
|
15437
|
-
const recentUserText = recentMessages.filter((m) => m.info.role === "user").flatMap((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text || "")).join("\n");
|
|
15438
|
-
const hasRecentToolError = recentMessages.some(
|
|
15439
|
-
(m) => m.parts.some((p) => p.type === "tool" && p.state?.status === "error")
|
|
15440
|
-
);
|
|
15441
|
-
const recentAll = recentUserText + "\n" + recentAssistantText;
|
|
15442
|
-
if (hasRecentToolError && ERROR_FIX_PATTERNS.some((p) => p.test(recentAssistantText))) {
|
|
15443
|
-
return { injected: true, type: "gotcha" };
|
|
15444
|
-
}
|
|
15445
|
-
if (CONSTRAINT_PATTERNS.some((p) => p.test(recentAll))) {
|
|
15446
|
-
return { injected: true, type: "constraint" };
|
|
15447
|
-
}
|
|
15448
|
-
if (DECISION_PATTERNS.some((p) => p.test(recentAll))) {
|
|
15449
|
-
return { injected: true, type: "decision" };
|
|
15450
|
-
}
|
|
15451
|
-
return { injected: false, type: null };
|
|
15452
|
-
}
|
|
15453
|
-
function buildMemoryNudge(type) {
|
|
15454
|
-
switch (type) {
|
|
15455
|
-
case "gotcha":
|
|
15456
|
-
return `
|
|
15457
|
-
<memory-nudge type="gotcha">You just fixed an error. Use memory_store(type="gotcha") to save what went wrong and how you fixed it, so future sessions don't repeat this mistake.</memory-nudge>`;
|
|
15458
|
-
case "constraint":
|
|
15459
|
-
return '\n<memory-nudge type="constraint">The user expressed a constraint or rule. Use memory_store(type="constraint") to persist it across sessions.</memory-nudge>';
|
|
15460
|
-
case "decision":
|
|
15461
|
-
return `
|
|
15462
|
-
<memory-nudge type="decision">A technical decision was made. Use memory_store(type="decision") to record what was decided and why, so future sessions don't re-decide.</memory-nudge>`;
|
|
15463
|
-
default:
|
|
15464
|
-
return "";
|
|
15465
|
-
}
|
|
15466
|
-
}
|
|
15467
|
-
|
|
15468
|
-
// src/compress/dedup.ts
|
|
15469
|
-
function createToolSignature(tool5, args) {
|
|
15470
|
-
if (!args) return tool5;
|
|
15471
|
-
const sorted = Object.keys(args).sort().map((k) => `${k}:${JSON.stringify(args[k])}`).join(",");
|
|
15472
|
-
return `${tool5}::${sorted}`;
|
|
15473
|
-
}
|
|
15474
|
-
|
|
15475
|
-
// src/compress/detector.ts
|
|
15476
|
-
function detectContentType(content) {
|
|
15477
|
-
const trimmed = content.trimStart();
|
|
15478
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
15479
|
-
try {
|
|
15480
|
-
JSON.parse(content);
|
|
15481
|
-
return "json";
|
|
15482
|
-
} catch {
|
|
15483
|
-
}
|
|
15484
|
-
}
|
|
15485
|
-
if (/^diff --git |^@@ -\d+,\d+ \+\d+,\d+ @@|^[+-]{3} \//m.test(content)) return "diff";
|
|
15486
|
-
if (/Traceback \(most recent call last\)|at \S+\.\S+\(|Error: |Exception: |TypeError: |ReferenceError: /m.test(content)) return "error-trace";
|
|
15487
|
-
if (/<[a-z][\s\S]*>/i.test(content) && /<(html|div|span|body|head|script|style)[\s>]/i.test(content)) return "html";
|
|
15488
|
-
const lines = content.split("\n");
|
|
15489
|
-
const logLineCount = lines.filter((l) => /^\s*(\d{4}-\d{2}-\d{2}|\[\d{4}|ERROR\b|WARN\b|INFO\b|DEBUG\b|FATAL\b|TRACE\b)/.test(l)).length;
|
|
15490
|
-
if (lines.length > 5 && logLineCount / lines.length > 0.3) return "log";
|
|
15491
|
-
const codePatterns = /\b(function |class |def |import |from .+ import|const |let |var |export |interface |type |struct |fn |func |pub |private |protected )\b/;
|
|
15492
|
-
const codeLines = lines.filter((l) => codePatterns.test(l)).length;
|
|
15493
|
-
if (lines.length > 10 && codeLines / lines.length > 0.15) return "code";
|
|
15494
|
-
return "text";
|
|
15495
|
-
}
|
|
15496
|
-
|
|
15497
|
-
// src/compress/tool-compress.ts
|
|
15498
|
-
var TOOL_COMPRESS_STRATEGIES = {
|
|
15499
|
-
read: compressFileRead,
|
|
15500
|
-
bash: compressBash,
|
|
15501
|
-
grep: compressSearchResults,
|
|
15502
|
-
glob: compressGlob,
|
|
15503
|
-
ripgrep: compressSearchResults,
|
|
15504
|
-
rg: compressSearchResults,
|
|
15505
|
-
find: compressGlob,
|
|
15506
|
-
search: compressSearchResults,
|
|
15507
|
-
grep_app_searchGitHub: compressSearchResults,
|
|
15508
|
-
searxng_searxng_web_search: compressSearchResults,
|
|
15509
|
-
websearch_web_search_exa: compressSearchResults,
|
|
15510
|
-
tavily_tavily_search: compressSearchResults,
|
|
15511
|
-
background_output: compressAgentOutput,
|
|
15512
|
-
task: compressAgentOutput,
|
|
15513
|
-
skill: compressSkillOutput,
|
|
15514
|
-
session_read: compressAgentOutput,
|
|
15515
|
-
webfetch: compressAgentOutput
|
|
14555
|
+
// src/compress/capture-cap.ts
|
|
14556
|
+
var DEFAULT_CAPS = {
|
|
14557
|
+
bash: 48e3,
|
|
14558
|
+
read: 5e4,
|
|
14559
|
+
grep: 1e4,
|
|
14560
|
+
glob: 1e4,
|
|
14561
|
+
task: 3e4,
|
|
14562
|
+
background_output: 3e4,
|
|
14563
|
+
webfetch: 2e4,
|
|
14564
|
+
generic: 4e4
|
|
15516
14565
|
};
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
|
|
15522
|
-
const
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
}
|
|
15526
|
-
|
|
15527
|
-
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
return lines.map((l) => l.length > MAX_LINE_LENGTH ? l.slice(0, MAX_LINE_LENGTH) + "..." : l).join("\n");
|
|
15543
|
-
}
|
|
15544
|
-
const errorLines = lines.filter((l) => /error|fail|exception|fatal|panic/i.test(l)).slice(0, 5);
|
|
15545
|
-
const tail = lines.slice(-30);
|
|
15546
|
-
return [...errorLines, ...tail].join("\n");
|
|
15547
|
-
}
|
|
15548
|
-
function compressSearchResults(output) {
|
|
15549
|
-
const lines = output.split("\n");
|
|
15550
|
-
if (lines.length <= 30) return output;
|
|
15551
|
-
const grouped = groupByFile(lines);
|
|
15552
|
-
const result = [];
|
|
15553
|
-
let count = 0;
|
|
15554
|
-
for (const [file2, matches] of grouped) {
|
|
15555
|
-
if (count >= 20) break;
|
|
15556
|
-
result.push(`--- ${file2} ---`);
|
|
15557
|
-
const kept = matches.slice(0, 5);
|
|
15558
|
-
for (const m of kept) {
|
|
15559
|
-
result.push(truncateLine(m, MAX_LINE_LENGTH));
|
|
15560
|
-
count++;
|
|
15561
|
-
}
|
|
15562
|
-
if (matches.length > 5) result.push(` ...[${matches.length - 5} more matches]`);
|
|
15563
|
-
}
|
|
15564
|
-
if (count >= 20 && lines.length > 30) {
|
|
15565
|
-
result.push(`
|
|
15566
|
-
...[${lines.length - count} more lines truncated]`);
|
|
15567
|
-
}
|
|
15568
|
-
return result.join("\n");
|
|
15569
|
-
}
|
|
15570
|
-
function compressGlob(output) {
|
|
15571
|
-
const lines = output.split("\n").filter((l) => l.trim());
|
|
15572
|
-
if (lines.length <= 30) return output;
|
|
15573
|
-
const head = lines.slice(0, 30);
|
|
15574
|
-
return [...head, `
|
|
15575
|
-
...[${lines.length - 30} more files]`].join("\n");
|
|
15576
|
-
}
|
|
15577
|
-
function compressGeneric(output) {
|
|
15578
|
-
const lines = output.split("\n");
|
|
15579
|
-
if (lines.length <= 50) {
|
|
15580
|
-
if (output.length <= 2e3) return output;
|
|
15581
|
-
return output.slice(0, 1500) + "\n...[truncated]" + output.slice(-500);
|
|
15582
|
-
}
|
|
15583
|
-
const head = lines.slice(0, 30);
|
|
15584
|
-
const tail = lines.slice(-15);
|
|
15585
|
-
const errorLines = lines.filter((l) => /error|fail|exception|fatal/i.test(l)).slice(0, 5);
|
|
15586
|
-
return [...head, "...[truncated]", ...errorLines, "...[truncated]", ...tail].join("\n");
|
|
15587
|
-
}
|
|
15588
|
-
function extractKeyLines(lines) {
|
|
15589
|
-
return lines.filter(
|
|
15590
|
-
(l) => /\b(function |class |def |import |export |interface |type |const |let |var |return |throw |Error|Exception)\b/.test(l) || /error|warn|fail|exception/i.test(l)
|
|
15591
|
-
);
|
|
15592
|
-
}
|
|
15593
|
-
function groupByFile(lines) {
|
|
15594
|
-
const groups = /* @__PURE__ */ new Map();
|
|
15595
|
-
let currentFile = "unknown";
|
|
15596
|
-
for (const line of lines) {
|
|
15597
|
-
const fileMatch = line.match(/^(\/[^\s:]+):/);
|
|
15598
|
-
if (fileMatch) {
|
|
15599
|
-
currentFile = fileMatch[1];
|
|
15600
|
-
}
|
|
15601
|
-
if (!groups.has(currentFile)) groups.set(currentFile, []);
|
|
15602
|
-
groups.get(currentFile).push(line);
|
|
15603
|
-
}
|
|
15604
|
-
return groups;
|
|
15605
|
-
}
|
|
15606
|
-
function truncateLine(line, maxLen) {
|
|
15607
|
-
if (line.length <= maxLen) return line;
|
|
15608
|
-
return line.slice(0, maxLen - 15) + "...[truncated]";
|
|
15609
|
-
}
|
|
15610
|
-
function compressJsonOutput(output) {
|
|
15611
|
-
try {
|
|
15612
|
-
const parsed = JSON.parse(output);
|
|
15613
|
-
if (Array.isArray(parsed)) {
|
|
15614
|
-
return compressJsonArray(parsed);
|
|
15615
|
-
}
|
|
15616
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
15617
|
-
return compressJsonObject(parsed);
|
|
15618
|
-
}
|
|
15619
|
-
return output;
|
|
15620
|
-
} catch {
|
|
15621
|
-
return output;
|
|
15622
|
-
}
|
|
15623
|
-
}
|
|
15624
|
-
function compressJsonArray(arr) {
|
|
15625
|
-
const head = 30;
|
|
15626
|
-
const tail = 15;
|
|
15627
|
-
const maxItems = 50;
|
|
15628
|
-
if (arr.length <= maxItems) return JSON.stringify(arr, null, 2);
|
|
15629
|
-
const kept = [...arr.slice(0, head), { _truncated: true, total: arr.length }, ...arr.slice(-tail)];
|
|
15630
|
-
return JSON.stringify(kept, null, 2);
|
|
15631
|
-
}
|
|
15632
|
-
function compressJsonObject(obj) {
|
|
15633
|
-
const MAX_CHILD_ITEMS = 30;
|
|
15634
|
-
let modified = false;
|
|
15635
|
-
const result = {};
|
|
15636
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
15637
|
-
if (Array.isArray(value) && value.length > MAX_CHILD_ITEMS) {
|
|
15638
|
-
result[key] = {
|
|
15639
|
-
_truncated: true,
|
|
15640
|
-
total: value.length,
|
|
15641
|
-
items: [...value.slice(0, 10), toStringPlaceholder(value.slice(10, 20)), ...value.slice(-10)]
|
|
15642
|
-
};
|
|
15643
|
-
modified = true;
|
|
15644
|
-
} else {
|
|
15645
|
-
result[key] = value;
|
|
15646
|
-
}
|
|
15647
|
-
}
|
|
15648
|
-
if (modified) {
|
|
15649
|
-
return JSON.stringify(result, null, 2);
|
|
15650
|
-
}
|
|
15651
|
-
return JSON.stringify(obj, null, 2);
|
|
15652
|
-
}
|
|
15653
|
-
function toStringPlaceholder(items) {
|
|
15654
|
-
return { _skipped: items.length };
|
|
15655
|
-
}
|
|
15656
|
-
function compressAgentOutput(output) {
|
|
15657
|
-
if (detectContentType(output) === "json") {
|
|
15658
|
-
return compressJsonOutput(output);
|
|
15659
|
-
}
|
|
15660
|
-
const lines = output.split("\n");
|
|
15661
|
-
if (lines.length <= 40 && output.length <= 3e3) return output;
|
|
15662
|
-
const MAX_SECTION_LINES = 5;
|
|
15663
|
-
const result = [];
|
|
15664
|
-
for (let i = 0; i < lines.length; i++) {
|
|
15665
|
-
const line = lines[i];
|
|
15666
|
-
if (line.includes("[ccr:") || line.includes("[superseded")) {
|
|
15667
|
-
result.push(line);
|
|
15668
|
-
continue;
|
|
15669
|
-
}
|
|
15670
|
-
const isHeader = /^#{1,4}\s/.test(line) || /^---/.test(line) || /^\*\*$/.test(line);
|
|
15671
|
-
const hasCode = line.includes("```");
|
|
15672
|
-
const hasKey = /\b(error|fail|success|completed|result|summary|warning)\b/i.test(line);
|
|
15673
|
-
if (isHeader || hasCode || hasKey) {
|
|
15674
|
-
result.push(truncateLine(line, 300));
|
|
15675
|
-
continue;
|
|
15676
|
-
}
|
|
15677
|
-
if (i < 5 || i >= lines.length - 10) {
|
|
15678
|
-
result.push(truncateLine(line, 300));
|
|
15679
|
-
continue;
|
|
15680
|
-
}
|
|
15681
|
-
const inSection = result.length > 0 && result[result.length - 1] !== "";
|
|
15682
|
-
if (!inSection) {
|
|
15683
|
-
if (line.trim()) {
|
|
15684
|
-
result.push(line);
|
|
15685
|
-
}
|
|
15686
|
-
} else {
|
|
15687
|
-
const recentLines = result.slice(-MAX_SECTION_LINES).filter((l) => l.trim() && l !== "...");
|
|
15688
|
-
if (recentLines.length >= MAX_SECTION_LINES) {
|
|
15689
|
-
result.push("...[truncated]");
|
|
15690
|
-
while (i < lines.length && !/^#{1,4}\s/.test(lines[i]) && !lines[i].includes("```") && !/\b(error|fail|summary)\b/i.test(lines[i])) {
|
|
15691
|
-
i++;
|
|
15692
|
-
}
|
|
15693
|
-
i--;
|
|
15694
|
-
} else {
|
|
15695
|
-
result.push(truncateLine(line, 300));
|
|
15696
|
-
}
|
|
15697
|
-
}
|
|
15698
|
-
}
|
|
15699
|
-
return result.join("\n");
|
|
15700
|
-
}
|
|
15701
|
-
function compressSkillOutput(output) {
|
|
15702
|
-
const lines = output.split("\n");
|
|
15703
|
-
if (lines.length <= 60 && output.length <= 4e3) return output;
|
|
15704
|
-
const result = [];
|
|
15705
|
-
const FRONTMATTER_END = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
15706
|
-
for (let i = 0; i < lines.length; i++) {
|
|
15707
|
-
if (i <= FRONTMATTER_END || i < 10) {
|
|
15708
|
-
result.push(lines[i]);
|
|
15709
|
-
continue;
|
|
15710
|
-
}
|
|
15711
|
-
if (i >= lines.length - 10) {
|
|
15712
|
-
result.push(lines[i]);
|
|
15713
|
-
continue;
|
|
15714
|
-
}
|
|
15715
|
-
const line = lines[i];
|
|
15716
|
-
if (/^#{1,4}\s/.test(line) || /^```/.test(line) || /^---/.test(line)) {
|
|
15717
|
-
result.push(line);
|
|
15718
|
-
continue;
|
|
15719
|
-
}
|
|
15720
|
-
if (/\b(must|must not|required|forbidden|never|always)\b/i.test(line)) {
|
|
15721
|
-
result.push(line);
|
|
15722
|
-
continue;
|
|
15723
|
-
}
|
|
15724
|
-
const recentNonEmpty = result.slice(-8).filter((l) => l.trim());
|
|
15725
|
-
if (recentNonEmpty.length >= 8 && !result[result.length - 1].startsWith("...")) {
|
|
15726
|
-
result.push("...[truncated]");
|
|
15727
|
-
while (i < lines.length && !/^#{1,4}\s/.test(lines[i]) && !/^```/.test(lines[i])) {
|
|
15728
|
-
i++;
|
|
15729
|
-
}
|
|
15730
|
-
i--;
|
|
15731
|
-
} else {
|
|
15732
|
-
result.push(line);
|
|
15733
|
-
}
|
|
15734
|
-
}
|
|
15735
|
-
if (result.length < lines.length * 0.7) {
|
|
15736
|
-
return result.join("\n");
|
|
15737
|
-
}
|
|
15738
|
-
return output;
|
|
15739
|
-
}
|
|
15740
|
-
|
|
15741
|
-
// src/compress/json-crush.ts
|
|
15742
|
-
import { createHash as createHash3 } from "crypto";
|
|
15743
|
-
function crushJsonArray(content, maxItems = 15) {
|
|
15744
|
-
try {
|
|
15745
|
-
const parsed = JSON.parse(content);
|
|
15746
|
-
if (!Array.isArray(parsed)) return content;
|
|
15747
|
-
if (parsed.length <= maxItems) return content;
|
|
15748
|
-
const firstFraction = 0.3;
|
|
15749
|
-
const lastFraction = 0.15;
|
|
15750
|
-
const firstCount = Math.max(1, Math.floor(maxItems * firstFraction));
|
|
15751
|
-
const lastCount = Math.max(1, Math.floor(maxItems * lastFraction));
|
|
15752
|
-
const midCount = maxItems - firstCount - lastCount;
|
|
15753
|
-
const first = parsed.slice(0, firstCount);
|
|
15754
|
-
const last = parsed.slice(-lastCount);
|
|
15755
|
-
const mid = deduplicateMiddle(parsed.slice(firstCount, -lastCount), midCount);
|
|
15756
|
-
const result = [...first, ...mid, ...last];
|
|
15757
|
-
const dropped = parsed.length - result.length;
|
|
15758
|
-
if (dropped > 0) {
|
|
15759
|
-
const hash2 = sha2562(content).slice(0, 12);
|
|
15760
|
-
result.push({ _ccr_dropped: `[${dropped} items offloaded, hash=${hash2}]` });
|
|
15761
|
-
}
|
|
15762
|
-
return JSON.stringify(result, null, 2);
|
|
15763
|
-
} catch {
|
|
15764
|
-
return content;
|
|
14566
|
+
function truncateMiddle(content, maxChars, hint) {
|
|
14567
|
+
if (content.length <= maxChars) return content;
|
|
14568
|
+
const headLen = Math.floor(maxChars * 0.45);
|
|
14569
|
+
const tailLen = Math.floor(maxChars * 0.4);
|
|
14570
|
+
const head = content.slice(0, headLen);
|
|
14571
|
+
const tail = content.slice(content.length - tailLen);
|
|
14572
|
+
return `${head}
|
|
14573
|
+
|
|
14574
|
+
[... truncated \u2014 ${hint} ...]
|
|
14575
|
+
|
|
14576
|
+
${tail}`;
|
|
14577
|
+
}
|
|
14578
|
+
function hintFor(tool6) {
|
|
14579
|
+
switch (tool6) {
|
|
14580
|
+
case "bash":
|
|
14581
|
+
return "use grep or read with offset for specifics";
|
|
14582
|
+
case "read":
|
|
14583
|
+
return "re-read with offset parameter for the omitted section";
|
|
14584
|
+
case "grep":
|
|
14585
|
+
case "search":
|
|
14586
|
+
return "narrow your search pattern for fewer results";
|
|
14587
|
+
case "webfetch":
|
|
14588
|
+
return "the full page was larger than the cap";
|
|
14589
|
+
default:
|
|
14590
|
+
return "output was capped at capture time";
|
|
15765
14591
|
}
|
|
15766
14592
|
}
|
|
15767
|
-
function
|
|
15768
|
-
|
|
15769
|
-
|
|
15770
|
-
|
|
15771
|
-
for (const item of items) {
|
|
15772
|
-
const key = typeof item === "object" ? JSON.stringify(item) : String(item);
|
|
15773
|
-
if (!seen.has(key)) {
|
|
15774
|
-
seen.add(key);
|
|
15775
|
-
unique.push(item);
|
|
15776
|
-
if (unique.length >= maxCount) break;
|
|
15777
|
-
}
|
|
14593
|
+
function capToolOutput(content, tool6, opts) {
|
|
14594
|
+
const limit = opts?.cap ?? DEFAULT_CAPS[tool6] ?? DEFAULT_CAPS.generic;
|
|
14595
|
+
if (content.length <= limit) {
|
|
14596
|
+
return { output: content, capped: false };
|
|
15778
14597
|
}
|
|
15779
|
-
return
|
|
15780
|
-
}
|
|
15781
|
-
function sha2562(data) {
|
|
15782
|
-
return createHash3("sha256").update(data).digest("hex");
|
|
14598
|
+
return { output: truncateMiddle(content, limit, hintFor(tool6)), capped: true };
|
|
15783
14599
|
}
|
|
15784
14600
|
|
|
15785
14601
|
// src/compress/single-pass.ts
|
|
@@ -15875,47 +14691,33 @@ function singlePassCompress(messages, state, protectedTail) {
|
|
|
15875
14691
|
if (toolState?.["status"] !== "completed") continue;
|
|
15876
14692
|
const output = typeof toolState?.["output"] === "string" ? toolState["output"] : void 0;
|
|
15877
14693
|
if (!output) continue;
|
|
15878
|
-
if (output === "[superseded by duplicate call]") continue;
|
|
15879
|
-
if (output.includes("
|
|
14694
|
+
if (output === "[OUTDATED \u2014 superseded by duplicate call]") continue;
|
|
14695
|
+
if (output.includes("deep_expand(")) continue;
|
|
15880
14696
|
if (!PROTECTED_TOOLS.has(toolName) && !NEVER_DEDUP.has(toolName)) {
|
|
15881
14697
|
const input = toolState["input"];
|
|
15882
|
-
const signature =
|
|
14698
|
+
const signature = `${toolName}:${JSON.stringify(input ?? {})}`;
|
|
15883
14699
|
const outputHash = simpleHash(output);
|
|
15884
14700
|
const existing = seen.get(signature);
|
|
15885
|
-
if (existing) {
|
|
15886
|
-
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
ppState["output"] = "[superseded by duplicate call]";
|
|
15895
|
-
stats.toolDedup++;
|
|
15896
|
-
}
|
|
14701
|
+
if (existing && existing.outputHash === outputHash) {
|
|
14702
|
+
const prevMsg = messages[existing.msgIdx];
|
|
14703
|
+
for (const prevPart of prevMsg.parts) {
|
|
14704
|
+
if (typeof prevPart !== "object" || prevPart === null) continue;
|
|
14705
|
+
const pp = prevPart;
|
|
14706
|
+
const ppState = pp["state"];
|
|
14707
|
+
if (typeof ppState?.["output"] === "string" && !ppState["output"].includes("[OUTDATED") && simpleHash(ppState["output"]) === outputHash) {
|
|
14708
|
+
ppState["output"] = "[OUTDATED \u2014 superseded by newer identical call]";
|
|
14709
|
+
stats.toolDedup++;
|
|
15897
14710
|
}
|
|
15898
14711
|
}
|
|
15899
|
-
seen.set(signature, { msgIdx: i, outputHash });
|
|
15900
|
-
} else {
|
|
15901
|
-
seen.set(signature, { msgIdx: i, outputHash });
|
|
15902
14712
|
}
|
|
14713
|
+
seen.set(signature, { msgIdx: i, outputHash });
|
|
15903
14714
|
}
|
|
15904
14715
|
if (output.length >= 200 && !PROTECTED_TOOLS.has(toolName)) {
|
|
15905
|
-
const
|
|
15906
|
-
if (
|
|
15907
|
-
const hash2 = ccrStore(state, output,
|
|
15908
|
-
toolState["output"] = ccrInjectMarker(
|
|
14716
|
+
const capResult = capToolOutput(output, toolName);
|
|
14717
|
+
if (capResult.capped) {
|
|
14718
|
+
const hash2 = ccrStore(state, output, capResult.output, toolName, callID);
|
|
14719
|
+
toolState["output"] = ccrInjectMarker(capResult.output, hash2);
|
|
15909
14720
|
stats.toolOutputCompressed++;
|
|
15910
|
-
continue;
|
|
15911
|
-
}
|
|
15912
|
-
}
|
|
15913
|
-
if (output.length >= 200 && detectContentType(output) === "json" && !PROTECTED_TOOLS.has(toolName)) {
|
|
15914
|
-
const crushed = crushJsonArray(output);
|
|
15915
|
-
if (crushed.length < output.length * 0.85) {
|
|
15916
|
-
const hash2 = ccrStore(state, output, crushed, toolName, callID);
|
|
15917
|
-
toolState["output"] = ccrInjectMarker(crushed, hash2);
|
|
15918
|
-
stats.jsonCrushed++;
|
|
15919
14721
|
}
|
|
15920
14722
|
}
|
|
15921
14723
|
}
|
|
@@ -15965,7 +14767,7 @@ function computeProtectedTail(messages) {
|
|
|
15965
14767
|
return 0;
|
|
15966
14768
|
}
|
|
15967
14769
|
function runCompressionPipeline(ctx) {
|
|
15968
|
-
const { messages, state,
|
|
14770
|
+
const { messages, state, logger } = ctx;
|
|
15969
14771
|
const pressure = detectPressure(messages, state.getModelContextWindow());
|
|
15970
14772
|
state.recordInputTokens(pressure.estimatedTokens);
|
|
15971
14773
|
const protectedTail = computeProtectedTail(messages);
|
|
@@ -15974,31 +14776,14 @@ function runCompressionPipeline(ctx) {
|
|
|
15974
14776
|
toolDedup: spStats.toolDedup,
|
|
15975
14777
|
errorPurge: spStats.errorPurge,
|
|
15976
14778
|
toolOutputCompressed: spStats.toolOutputCompressed,
|
|
15977
|
-
jsonCrushed:
|
|
14779
|
+
jsonCrushed: 0,
|
|
15978
14780
|
assistantCompressed: spStats.assistantCompressed,
|
|
15979
14781
|
ccrStored: spStats.ccrStored,
|
|
15980
14782
|
nudgeInjected: false,
|
|
15981
14783
|
pressureLevel: pressure.level,
|
|
15982
14784
|
estimatedTokens: pressure.estimatedTokens
|
|
15983
14785
|
};
|
|
15984
|
-
const
|
|
15985
|
-
const currentMsgCount = messages.length;
|
|
15986
|
-
const pressureSince = state.messagesSinceLastNudge(sid, currentMsgCount);
|
|
15987
|
-
if (shouldInjectNudge(pressure.level, pressureSince)) {
|
|
15988
|
-
if (injectIntoLastAssistant(messages, buildNudgeText(pressure.level))) {
|
|
15989
|
-
stats.nudgeInjected = true;
|
|
15990
|
-
state.recordNudge(sid, currentMsgCount);
|
|
15991
|
-
}
|
|
15992
|
-
}
|
|
15993
|
-
const memorySince = state.messagesSinceLastMemoryNudge(sid, currentMsgCount);
|
|
15994
|
-
const memoryNudge = detectMemoryNudge(messages, memorySince);
|
|
15995
|
-
if (memoryNudge.injected) {
|
|
15996
|
-
if (injectIntoLastAssistant(messages, buildMemoryNudge(memoryNudge.type))) {
|
|
15997
|
-
state.recordMemoryNudge(sid, currentMsgCount);
|
|
15998
|
-
logger?.debug("compress: memory nudge", { type: memoryNudge.type });
|
|
15999
|
-
}
|
|
16000
|
-
}
|
|
16001
|
-
const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.jsonCrushed > 0 || stats.assistantCompressed > 0 || stats.nudgeInjected;
|
|
14786
|
+
const active = stats.toolDedup > 0 || stats.errorPurge > 0 || stats.toolOutputCompressed > 0 || stats.assistantCompressed > 0;
|
|
16002
14787
|
if (active) {
|
|
16003
14788
|
logger?.debug("compress: pipeline result", { ...stats });
|
|
16004
14789
|
} else {
|
|
@@ -16006,20 +14791,6 @@ function runCompressionPipeline(ctx) {
|
|
|
16006
14791
|
}
|
|
16007
14792
|
return { stats };
|
|
16008
14793
|
}
|
|
16009
|
-
function injectIntoLastAssistant(messages, text) {
|
|
16010
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
16011
|
-
const msg = messages[i];
|
|
16012
|
-
if (msg.info.role !== "assistant") continue;
|
|
16013
|
-
for (let j = msg.parts.length - 1; j >= 0; j--) {
|
|
16014
|
-
const p = msg.parts[j];
|
|
16015
|
-
if (p["type"] === "text" && typeof p["text"] === "string") {
|
|
16016
|
-
p["text"] += text;
|
|
16017
|
-
return true;
|
|
16018
|
-
}
|
|
16019
|
-
}
|
|
16020
|
-
}
|
|
16021
|
-
return false;
|
|
16022
|
-
}
|
|
16023
14794
|
|
|
16024
14795
|
// src/hooks/messages-transform.ts
|
|
16025
14796
|
var KEEP_RECENT = 8;
|
|
@@ -16068,44 +14839,6 @@ function isSystemInjected(msg) {
|
|
|
16068
14839
|
}
|
|
16069
14840
|
return hasText && allInjected;
|
|
16070
14841
|
}
|
|
16071
|
-
function repairOrphanedToolCalls(messages) {
|
|
16072
|
-
const toolUseIds = /* @__PURE__ */ new Set();
|
|
16073
|
-
for (const msg of messages) {
|
|
16074
|
-
if (msg.info.role !== "assistant") continue;
|
|
16075
|
-
for (const part of msg.parts) {
|
|
16076
|
-
if (typeof part !== "object" || part === null) continue;
|
|
16077
|
-
const p = part;
|
|
16078
|
-
if (p["type"] === "tool_use" && typeof p["id"] === "string") {
|
|
16079
|
-
toolUseIds.add(p["id"]);
|
|
16080
|
-
}
|
|
16081
|
-
}
|
|
16082
|
-
}
|
|
16083
|
-
if (toolUseIds.size === 0) return;
|
|
16084
|
-
const toolResultIds = /* @__PURE__ */ new Set();
|
|
16085
|
-
for (const msg of messages) {
|
|
16086
|
-
for (const part of msg.parts) {
|
|
16087
|
-
if (typeof part !== "object" || part === null) continue;
|
|
16088
|
-
const p = part;
|
|
16089
|
-
if (p["type"] === "tool_result" && typeof p["tool_use_id"] === "string") {
|
|
16090
|
-
toolResultIds.add(p["tool_use_id"]);
|
|
16091
|
-
}
|
|
16092
|
-
}
|
|
16093
|
-
}
|
|
16094
|
-
for (const msg of messages) {
|
|
16095
|
-
if (msg.info.role !== "assistant") continue;
|
|
16096
|
-
for (const part of msg.parts) {
|
|
16097
|
-
if (typeof part !== "object" || part === null) continue;
|
|
16098
|
-
const p = part;
|
|
16099
|
-
if (p["type"] === "tool_use" && typeof p["id"] === "string") {
|
|
16100
|
-
if (!toolResultIds.has(p["id"])) {
|
|
16101
|
-
p["type"] = "tool";
|
|
16102
|
-
p["state"] = { status: "ok" };
|
|
16103
|
-
p["text"] = "[context-stripped]";
|
|
16104
|
-
}
|
|
16105
|
-
}
|
|
16106
|
-
}
|
|
16107
|
-
}
|
|
16108
|
-
}
|
|
16109
14842
|
function createMessagesTransformHandler(state, logger) {
|
|
16110
14843
|
return async (input, output) => {
|
|
16111
14844
|
const messages = output.messages;
|
|
@@ -16177,10 +14910,38 @@ function createMessagesTransformHandler(state, logger) {
|
|
|
16177
14910
|
for (let r = toRemove.length - 1; r >= 0; r--) {
|
|
16178
14911
|
messages.splice(toRemove[r], 1);
|
|
16179
14912
|
}
|
|
16180
|
-
repairOrphanedToolCalls(messages);
|
|
16181
14913
|
if (Object.values(stats).some((v) => v > 0)) {
|
|
16182
14914
|
logger?.debug("messages.transform: stripped", stats);
|
|
16183
14915
|
}
|
|
14916
|
+
const compressReq = state.consumeCompressionRequest();
|
|
14917
|
+
if (compressReq) {
|
|
14918
|
+
const cutoff = messages.length - compressReq.keepRecent;
|
|
14919
|
+
let agentCompressed = 0;
|
|
14920
|
+
for (let i = 2; i < cutoff; i++) {
|
|
14921
|
+
const msg = messages[i];
|
|
14922
|
+
if (!msg?.parts?.length) continue;
|
|
14923
|
+
for (const part of msg.parts) {
|
|
14924
|
+
if (typeof part !== "object" || part === null) continue;
|
|
14925
|
+
const p = part;
|
|
14926
|
+
if (p["type"] !== "tool") continue;
|
|
14927
|
+
const toolState = p["state"];
|
|
14928
|
+
const output2 = typeof toolState?.["output"] === "string" ? toolState["output"] : "";
|
|
14929
|
+
if (output2.length < 500 || output2.includes("deep_expand(")) continue;
|
|
14930
|
+
const lines = output2.split("\n");
|
|
14931
|
+
if (lines.length < 20) continue;
|
|
14932
|
+
const summary = lines.slice(0, 3).join("\n") + `
|
|
14933
|
+
[... ${lines.length - 6} lines compressed \u2014 call deep_expand to restore ...]
|
|
14934
|
+
` + lines.slice(-3).join("\n");
|
|
14935
|
+
const { ccrStore: ccrStore2, ccrInjectMarker: ccrInjectMarker2 } = await import("./ccr-REOCHH53.js");
|
|
14936
|
+
const hash2 = ccrStore2(state, output2, summary, "context_compress");
|
|
14937
|
+
toolState["output"] = ccrInjectMarker2(summary, hash2);
|
|
14938
|
+
agentCompressed++;
|
|
14939
|
+
}
|
|
14940
|
+
}
|
|
14941
|
+
if (agentCompressed > 0) {
|
|
14942
|
+
logger?.debug("messages.transform: agent-initiated compression", { agentCompressed });
|
|
14943
|
+
}
|
|
14944
|
+
}
|
|
16184
14945
|
const pipelineResult = runCompressionPipeline({
|
|
16185
14946
|
messages: output.messages,
|
|
16186
14947
|
state,
|
|
@@ -16188,7 +14949,7 @@ function createMessagesTransformHandler(state, logger) {
|
|
|
16188
14949
|
logger
|
|
16189
14950
|
});
|
|
16190
14951
|
const ds = pipelineResult.stats;
|
|
16191
|
-
if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.
|
|
14952
|
+
if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.assistantCompressed > 0) {
|
|
16192
14953
|
logger?.debug("messages.transform: deep compression", { ...ds });
|
|
16193
14954
|
state.mergeNotify({
|
|
16194
14955
|
compression: stats,
|
|
@@ -16197,23 +14958,6 @@ function createMessagesTransformHandler(state, logger) {
|
|
|
16197
14958
|
protectedHead: PROTECTED_HEAD,
|
|
16198
14959
|
protectedTail: KEEP_RECENT
|
|
16199
14960
|
});
|
|
16200
|
-
const recentEdits = state.getRecentEdits();
|
|
16201
|
-
if (recentEdits.length > 0) {
|
|
16202
|
-
const fileList = recentEdits.slice(0, 5).join(", ");
|
|
16203
|
-
const nudge = '\n\n<dm-nudge level="medium">Context was compressed. Recent files may have shifted: ' + fileList + ". Use `read` to re-verify if needed.</dm-nudge>";
|
|
16204
|
-
for (let k = output.messages.length - 1; k >= 0; k--) {
|
|
16205
|
-
const msg = output.messages[k];
|
|
16206
|
-
if (msg.info.role !== "assistant") continue;
|
|
16207
|
-
for (const part of msg.parts) {
|
|
16208
|
-
const p = part;
|
|
16209
|
-
if (p["type"] === "text" && typeof p["text"] === "string") {
|
|
16210
|
-
p.text += nudge;
|
|
16211
|
-
break;
|
|
16212
|
-
}
|
|
16213
|
-
}
|
|
16214
|
-
break;
|
|
16215
|
-
}
|
|
16216
|
-
}
|
|
16217
14961
|
} else if (Object.values(stats).some((v) => v > 0)) {
|
|
16218
14962
|
state.mergeNotify({
|
|
16219
14963
|
compression: stats,
|
|
@@ -16345,101 +15089,6 @@ function createNotifyHandler(client, logger) {
|
|
|
16345
15089
|
};
|
|
16346
15090
|
}
|
|
16347
15091
|
|
|
16348
|
-
// src/extract/enrich.ts
|
|
16349
|
-
import { stat } from "fs/promises";
|
|
16350
|
-
|
|
16351
|
-
// src/extract/enrich-prompt.ts
|
|
16352
|
-
var ENRICH_PROMPT_TEMPLATE = `You are a checkpoint enrichment agent. The compacting hook already wrote a checkpoint.md with instant heuristic extraction. Your job is to cross-reference it with the raw message dump to produce a richer checkpoint.
|
|
16353
|
-
|
|
16354
|
-
Files to read:
|
|
16355
|
-
- Checkpoint: {{checkpointPath}}
|
|
16356
|
-
- Raw messages: {{rawPath}}
|
|
16357
|
-
- Project path: {{projectPath}}
|
|
16358
|
-
|
|
16359
|
-
Steps:
|
|
16360
|
-
1. Read the current checkpoint.md \u2014 note its structure and contents.
|
|
16361
|
-
2. Read the raw messages JSON \u2014 these are the original conversation messages before compaction.
|
|
16362
|
-
3. Cross-reference:
|
|
16363
|
-
a. Find related decisions that were made across multiple messages (consolidate fragments)
|
|
16364
|
-
b. Identify constraints that were implicitly assumed but never explicitly stated
|
|
16365
|
-
c. Link error messages to their fixes more precisely (the heuristic might have missed some)
|
|
16366
|
-
d. Identify file changes that are part of larger refactoring patterns
|
|
16367
|
-
4. Update the checkpoint.md using memory_store or write tool \u2014 keep the same section structure but:
|
|
16368
|
-
- Add cross-reference notes where relevant (e.g., "See also: [related decision]")
|
|
16369
|
-
- Refine gotchas into more actionable descriptions
|
|
16370
|
-
- Add a "## Synthesis" section summarizing the conversation's main themes (2-5 bullets)
|
|
16371
|
-
|
|
16372
|
-
Quality target: the checkpoint should contain everything a restart session needs to continue the work without re-reading the original conversation. Be selective \u2014 only add value, don't pad.
|
|
16373
|
-
`;
|
|
16374
|
-
|
|
16375
|
-
// src/extract/enrich.ts
|
|
16376
|
-
var MAX_RAW_AGE_MS = 10 * 60 * 1e3;
|
|
16377
|
-
async function runEnrichment(opts) {
|
|
16378
|
-
const { client, projectPath, sessionID, logger } = opts;
|
|
16379
|
-
const emptyResult = { sessionID: "", status: "skipped" };
|
|
16380
|
-
const rawPath = checkpointRawPath(projectPath, sessionID);
|
|
16381
|
-
let rawStat;
|
|
16382
|
-
try {
|
|
16383
|
-
rawStat = await stat(rawPath);
|
|
16384
|
-
} catch {
|
|
16385
|
-
logger?.debug("enrichment: checkpoint.raw.json missing, skipping", { sessionID, rawPath });
|
|
16386
|
-
return emptyResult;
|
|
16387
|
-
}
|
|
16388
|
-
const rawAge = Date.now() - rawStat.mtimeMs;
|
|
16389
|
-
if (rawAge > MAX_RAW_AGE_MS) {
|
|
16390
|
-
logger?.debug("enrichment: checkpoint.raw.json is stale, skipping", {
|
|
16391
|
-
sessionID,
|
|
16392
|
-
rawPath,
|
|
16393
|
-
ageMinutes: Math.round(rawAge / 6e4)
|
|
16394
|
-
});
|
|
16395
|
-
return emptyResult;
|
|
16396
|
-
}
|
|
16397
|
-
const checkpointPath = memoryFilePath("project", "checkpoint", projectPath);
|
|
16398
|
-
try {
|
|
16399
|
-
await stat(checkpointPath);
|
|
16400
|
-
} catch {
|
|
16401
|
-
logger?.debug("enrichment: checkpoint.md missing, skipping", { sessionID, checkpointPath });
|
|
16402
|
-
return emptyResult;
|
|
16403
|
-
}
|
|
16404
|
-
try {
|
|
16405
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16406
|
-
const title = `Memory Checkpoint Enrichment ${now}`;
|
|
16407
|
-
const resp = await client.session.create({
|
|
16408
|
-
body: { title }
|
|
16409
|
-
});
|
|
16410
|
-
const enrichSessionID = resp.data?.id;
|
|
16411
|
-
if (!enrichSessionID) {
|
|
16412
|
-
logger?.warn("enrichment: session.create returned no ID", { sessionID });
|
|
16413
|
-
return { sessionID: "", status: "failed" };
|
|
16414
|
-
}
|
|
16415
|
-
const prompt = ENRICH_PROMPT_TEMPLATE.replaceAll("{{checkpointPath}}", checkpointPath).replaceAll("{{rawPath}}", rawPath).replaceAll("{{projectPath}}", projectPath).replaceAll("{{ISO timestamp}}", now);
|
|
16416
|
-
await client.session.promptAsync({
|
|
16417
|
-
path: { id: enrichSessionID },
|
|
16418
|
-
body: {
|
|
16419
|
-
parts: [{ type: "text", text: prompt }],
|
|
16420
|
-
agent: "sisyphus",
|
|
16421
|
-
tools: {
|
|
16422
|
-
memory_search: true,
|
|
16423
|
-
memory_store: true,
|
|
16424
|
-
read: true,
|
|
16425
|
-
list: true
|
|
16426
|
-
}
|
|
16427
|
-
}
|
|
16428
|
-
});
|
|
16429
|
-
logger?.info("enrichment: spawned background session", {
|
|
16430
|
-
sessionID,
|
|
16431
|
-
enrichSessionID
|
|
16432
|
-
});
|
|
16433
|
-
return { sessionID: enrichSessionID, status: "spawned" };
|
|
16434
|
-
} catch (err) {
|
|
16435
|
-
logger?.warn("enrichment: failed to spawn background session", {
|
|
16436
|
-
sessionID,
|
|
16437
|
-
error: err instanceof Error ? err.message : String(err)
|
|
16438
|
-
});
|
|
16439
|
-
return { sessionID: "", status: "failed" };
|
|
16440
|
-
}
|
|
16441
|
-
}
|
|
16442
|
-
|
|
16443
15092
|
// src/repomap/extractor.ts
|
|
16444
15093
|
var EXT_TO_LANG = {
|
|
16445
15094
|
".ts": "typescript",
|
|
@@ -16589,18 +15238,18 @@ function recencyDecay(lastRead) {
|
|
|
16589
15238
|
}
|
|
16590
15239
|
var RepoMapTracker = class {
|
|
16591
15240
|
files = /* @__PURE__ */ new Map();
|
|
16592
|
-
recordRead(
|
|
16593
|
-
const lang = getLanguage(
|
|
15241
|
+
recordRead(path9, content) {
|
|
15242
|
+
const lang = getLanguage(path9);
|
|
16594
15243
|
if (!lang) return;
|
|
16595
|
-
const symbols = extractSymbols(
|
|
16596
|
-
const existing = this.files.get(
|
|
15244
|
+
const symbols = extractSymbols(path9, content);
|
|
15245
|
+
const existing = this.files.get(path9);
|
|
16597
15246
|
if (existing) {
|
|
16598
15247
|
existing.symbols = symbols;
|
|
16599
15248
|
existing.readCount += 1;
|
|
16600
15249
|
existing.lastRead = Date.now();
|
|
16601
15250
|
} else {
|
|
16602
|
-
this.files.set(
|
|
16603
|
-
path:
|
|
15251
|
+
this.files.set(path9, {
|
|
15252
|
+
path: path9,
|
|
16604
15253
|
symbols,
|
|
16605
15254
|
readCount: 1,
|
|
16606
15255
|
lastRead: Date.now(),
|
|
@@ -16665,6 +15314,13 @@ var deepMemoryPlugin = async (input) => {
|
|
|
16665
15314
|
dataRoot,
|
|
16666
15315
|
serverUrl: input.serverUrl.toString()
|
|
16667
15316
|
});
|
|
15317
|
+
try {
|
|
15318
|
+
await migrateV3toV4(projectPath, logger.for("migrate"));
|
|
15319
|
+
} catch (err) {
|
|
15320
|
+
logger.warn("V3\u2192V4 migration failed (non-blocking)", {
|
|
15321
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15322
|
+
});
|
|
15323
|
+
}
|
|
16668
15324
|
const searchService = new SearchService({
|
|
16669
15325
|
dataRoot,
|
|
16670
15326
|
projectPath,
|
|
@@ -16696,82 +15352,26 @@ var deepMemoryPlugin = async (input) => {
|
|
|
16696
15352
|
error: err instanceof Error ? err.message : String(err)
|
|
16697
15353
|
});
|
|
16698
15354
|
});
|
|
16699
|
-
const memoryTools = createMemoryTools(searchService, { projectPath });
|
|
15355
|
+
const memoryTools = createMemoryTools(searchService, state, { projectPath });
|
|
16700
15356
|
const notify = createNotifyHandler(input.client, logger.for("notify"));
|
|
16701
15357
|
const hooks = {
|
|
16702
15358
|
"chat.params": createChatParamsHandler(
|
|
16703
15359
|
state,
|
|
16704
15360
|
logger.for("chat-params")
|
|
16705
15361
|
),
|
|
16706
|
-
"chat.message": createChatMessageHandler({
|
|
16707
|
-
projectPath,
|
|
16708
|
-
state,
|
|
16709
|
-
logger: logger.for("chat-message")
|
|
16710
|
-
}),
|
|
16711
15362
|
"experimental.chat.system.transform": createSystemTransformHandler(
|
|
16712
15363
|
state,
|
|
16713
15364
|
projectPath,
|
|
16714
15365
|
searchService,
|
|
16715
|
-
logger.for("system-transform")
|
|
16716
|
-
tracker
|
|
15366
|
+
logger.for("system-transform")
|
|
16717
15367
|
),
|
|
16718
15368
|
event: async ({ event }) => {
|
|
16719
15369
|
try {
|
|
16720
15370
|
if (event.type === "session.created") {
|
|
16721
|
-
const info = event.properties.info;
|
|
16722
|
-
if (!info || typeof info !== "object") {
|
|
16723
|
-
logger.debug("event session.created: missing info, skipping");
|
|
16724
|
-
return;
|
|
16725
|
-
}
|
|
16726
|
-
const i = info;
|
|
16727
|
-
if (typeof i.id !== "string") {
|
|
16728
|
-
logger.debug("event session.created: info.id not string, skipping");
|
|
16729
|
-
return;
|
|
16730
|
-
}
|
|
16731
|
-
const narrowed = {
|
|
16732
|
-
type: "session.created",
|
|
16733
|
-
properties: {
|
|
16734
|
-
info: {
|
|
16735
|
-
id: i.id,
|
|
16736
|
-
parentID: typeof i.parentID === "string" ? i.parentID : void 0,
|
|
16737
|
-
title: typeof i.title === "string" ? i.title : "",
|
|
16738
|
-
directory: typeof i.directory === "string" ? i.directory : projectPath
|
|
16739
|
-
}
|
|
16740
|
-
}
|
|
16741
|
-
};
|
|
16742
|
-
await Promise.allSettled([
|
|
16743
|
-
handleSessionCreated({ state, event: narrowed, projectPath, logger: logger.for("resume") }),
|
|
16744
|
-
handleSessionCreatedForDream({
|
|
16745
|
-
event: narrowed,
|
|
16746
|
-
config: { client: input.client, projectPath, model: state.bestModel(), logger: logger.for("auto-dream") }
|
|
16747
|
-
}),
|
|
16748
|
-
handleSessionCreatedForDistill({
|
|
16749
|
-
event: narrowed,
|
|
16750
|
-
config: { client: input.client, projectPath, model: state.bestModel(), logger: logger.for("auto-distill") }
|
|
16751
|
-
})
|
|
16752
|
-
]);
|
|
16753
15371
|
return;
|
|
16754
15372
|
}
|
|
16755
15373
|
if (event.type === "session.idle") {
|
|
16756
15374
|
const idleSessionID = event.properties.sessionID;
|
|
16757
|
-
if (idleSessionID && state.hasPendingEnrichment(idleSessionID)) {
|
|
16758
|
-
state.consumePendingEnrichment(idleSessionID);
|
|
16759
|
-
try {
|
|
16760
|
-
const result = await runEnrichment({
|
|
16761
|
-
client: input.client,
|
|
16762
|
-
projectPath,
|
|
16763
|
-
sessionID: idleSessionID,
|
|
16764
|
-
logger: logger.for("enrichment")
|
|
16765
|
-
});
|
|
16766
|
-
logger.info("idle enrichment result", { ...result });
|
|
16767
|
-
} catch (err) {
|
|
16768
|
-
logger.warn("idle enrichment failed", {
|
|
16769
|
-
error: err instanceof Error ? err.message : String(err)
|
|
16770
|
-
});
|
|
16771
|
-
}
|
|
16772
|
-
} else {
|
|
16773
|
-
logger.debug("event session.idle (no pending enrichment)");
|
|
16774
|
-
}
|
|
16775
15375
|
if (idleSessionID) {
|
|
16776
15376
|
const pending = state.consumePendingNotify();
|
|
16777
15377
|
if (pending) {
|
|
@@ -16800,8 +15400,8 @@ var deepMemoryPlugin = async (input) => {
|
|
|
16800
15400
|
}
|
|
16801
15401
|
try {
|
|
16802
15402
|
const auditLogDir = projectMemoryDir(projectPath);
|
|
16803
|
-
await
|
|
16804
|
-
const auditLogPath =
|
|
15403
|
+
await mkdir3(auditLogDir, { recursive: true });
|
|
15404
|
+
const auditLogPath = path8.join(auditLogDir, ".compaction-log.jsonl");
|
|
16805
15405
|
const line = JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionID: compactedSessionID }) + "\n";
|
|
16806
15406
|
const releaseLock = await acquireLock(auditLogPath);
|
|
16807
15407
|
try {
|