@bd7pil/opencode-deep-memory 0.8.6 → 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/dist/index.js CHANGED
@@ -1,19 +1,14 @@
1
1
  /* opencode-deep-memory — zero runtime dependencies */
2
- var __defProp = Object.defineProperty;
3
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
- }) : x)(function(x) {
6
- if (typeof require !== "undefined") return require.apply(this, arguments);
7
- throw Error('Dynamic require of "' + x + '" is not supported');
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 mkdir4 } from "fs/promises";
16
- import path11 from "path";
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/hooks/chat-message.ts
467
- import path4 from "path";
468
- import { mkdir, readFile, writeFile } from "fs/promises";
469
- import { createHash } from "crypto";
470
- var MAX_NOTE_LENGTH = 500;
471
- var KEYWORDS = [
472
- "remember",
473
- "don't forget",
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
- content = fs3.readFileSync(filePath, "utf8");
650
- } catch {
651
- const logger = createLogger();
652
- logger.debug(`budgetedRead: file not found or unreadable: ${filePath}`);
653
- return "";
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
- const headingPart = section.heading ? section.heading + "\n" : "";
669
- const availableForBody = remaining - estimateTokens(headingPart);
670
- if (availableForBody > 0) {
671
- const truncatedBody = truncateToTokenBudget(section.body, availableForBody);
672
- output += headingPart + truncatedBody + "\n\n";
673
- }
674
- break;
675
- }
676
- }
677
- return output.trimEnd();
678
- }
679
-
680
- // src/inject/importance.ts
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";
811
- }
812
-
813
- // src/inject/dedup.ts
814
- function dedupByJaccard(items, getText, threshold = 0.85) {
815
- if (items.length === 0) return [];
816
- const tokenSets = items.map((item) => new Set(tokenize(getText(item))));
817
- const inverted = /* @__PURE__ */ new Map();
818
- for (let i = 0; i < tokenSets.length; i++) {
819
- for (const token of tokenSets[i]) {
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
- }
533
+ memoryContent = fs4.readFileSync(memoryPath, "utf8");
534
+ state.setMemoryCache(memoryContent, mtime);
535
+ cacheHit = false;
844
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>`;
845
551
  }
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;
552
+ payload += `
553
+ </deep-memory-stable>`;
554
+ logger?.debug("composeSystemPayload V4", { cacheHit, memorySize });
555
+ return { payload, cacheHit, memorySize };
856
556
  }
857
557
 
858
- // src/repomap/injector.ts
859
- function formatRepoMap(entries) {
860
- if (entries.length === 0) return "";
861
- const lines = entries.map(
862
- (e) => `${e.file}: ${e.symbols.join(", ")}`
863
- );
864
- return `<deep-memory-repomap>
865
- ${lines.join("\n")}
866
- </deep-memory-repomap>`;
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;
867
565
  }
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, tracker) {
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 { stable, volatile, stats: payloadStats } = await composeSystemPayload({
582
+ const { payload, cacheHit, memorySize } = await composeSystemPayload({
982
583
  state,
983
584
  sessionID,
984
585
  projectPath,
985
- mode,
986
- searchService,
987
- userQuery,
988
- logger,
989
- tracker
586
+ logger
990
587
  });
991
- if (stable) {
992
- output.system.push(stable);
993
- }
994
- if (volatile) {
995
- output.system.push(volatile);
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
- const agent = state.agentOf(sessionID);
998
- const tier = classifyAgent(agent);
999
- logger?.debug("system.transform: injected", {
603
+ output.system.push(finalPayload);
604
+ logger?.debug("system.transform V4: injected", {
1000
605
  sessionID,
1001
- agent: agent ?? "(undefined)",
1002
- tier,
1003
- mode,
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: stable.length,
1010
- volatileSize: volatile.length,
1011
- tier,
1012
- mode,
1013
- searchEntries: payloadStats.searchEntries,
1014
- repoMapEntries: payloadStats.repoMapEntries,
1015
- hasCheckpoint: payloadStats.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 fs8 from "fs/promises";
625
+ import fs6 from "fs/promises";
1576
626
  import { existsSync as existsSync2 } from "fs";
1577
- import path8 from "path";
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 fs7 from "fs/promises";
1768
- import path7 from "path";
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 tokenize2(text) {
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 = tokenize2(phrase.trim());
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 = path7.join(projectMemoryDir(this.projectPath), "sessions");
974
+ const sessionsDir = path4.join(projectMemoryDir(this.projectPath), "sessions");
1925
975
  if (!existsSync(sessionsDir)) return [];
1926
- const sessionDirs = await fs7.readdir(sessionsDir);
976
+ const sessionDirs = await fs5.readdir(sessionsDir);
1927
977
  for (const sid of sessionDirs) {
1928
- const sessionDir = path7.join(sessionsDir, sid);
1929
- const stat2 = await fs7.stat(sessionDir);
1930
- if (!stat2.isDirectory()) continue;
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 fs7.readdir(dir);
997
+ entries = await fs5.readdir(dir);
1948
998
  } catch {
1949
999
  return [];
1950
1000
  }
1951
1001
  for (const entry of entries) {
1952
- const fullPath = path7.join(dir, entry);
1002
+ const fullPath = path4.join(dir, entry);
1953
1003
  try {
1954
- const stat2 = await fs7.stat(fullPath);
1955
- if (stat2.isFile() && entry.endsWith(".md")) {
1956
- results.push({ path: fullPath, scope, mtime: stat2.mtimeMs });
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 fs7.readFile(file2.path, "utf8");
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 = tokenize2(textToTokenize);
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 = path7.dirname(statePath);
2026
- await fs7.mkdir(dir, { recursive: true });
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 fs7.writeFile(
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 fs8.mkdir(path8.dirname(filePath), { recursive: true });
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 fs8.readFile(filePath, "utf8");
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 fs8.writeFile(filePath, newContent, "utf8");
1209
+ await fs6.writeFile(filePath, newContent, "utf8");
2157
1210
  } else {
2158
1211
  const newContent = existing.trimEnd() + "\n" + entry + "\n";
2159
- await fs8.writeFile(filePath, newContent, "utf8");
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 fs8.writeFile(filePath, newContent, "utf8");
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 fs8.readFile(filePath, "utf8");
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 fs8.writeFile(filePath, kept.join("\n"), "utf8");
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 fs8.readFile(filePath, "utf8");
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 tool4 } from "@opencode-ai/plugin";
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, path12) {
3129
- if (!path12)
2206
+ function getElementAtPath(obj, path9) {
2207
+ if (!path9)
3130
2208
  return obj;
3131
- return path12.reduce((acc, key) => acc?.[key], obj);
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(path12, issues) {
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(path12);
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, path12 = []) => {
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 = [...path12, ...issue2.path];
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 path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3707
- for (const seg of path12) {
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 fs9 from "fs";
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 (!fs9.existsSync(rawPath)) {
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(fs9.readFileSync(rawPath, "utf8"));
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/ccr.ts
14880
- import { createHash as createHash2 } from "crypto";
14881
- var CCR_TTL_MS = 30 * 60 * 1e3;
14882
- function ccrStore(state, original, compressed, toolName, callID) {
14883
- const hash2 = sha256(original).slice(0, 24);
14884
- state.ccStore(hash2, {
14885
- hash: hash2,
14886
- original,
14887
- compressed,
14888
- createdAt: Date.now(),
14889
- toolName,
14890
- callID
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 tool4({
13992
+ return tool5({
14923
13993
  description: "Retrieve original content that was previously compressed. Use hash from [ccr:...] markers.",
14924
13994
  args: {
14925
- hash: tool4.schema.string().describe("The hash from the [ccr:HASH] marker")
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 as writeFile2, mkdir as mkdir2 } from "fs/promises";
14937
- import path9 from "path";
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 mkdir2(path9.dirname(rawFilePath), { recursive: true });
14035
+ await mkdir(path6.dirname(rawFilePath), { recursive: true });
14966
14036
  const release = await acquireLock(rawFilePath);
14967
14037
  try {
14968
- await writeFile2(rawFilePath, payload, "utf-8");
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 tool5 = part.tool.toLowerCase();
15093
- if (tool5 === "write" || tool5 === "edit") {
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}:${tool5}`;
14166
+ const key = `${filePath}:${tool6}`;
15097
14167
  if (!seen.has(key)) {
15098
14168
  seen.add(key);
15099
- changes.push({ path: filePath, operation: tool5 });
14169
+ changes.push({ path: filePath, operation: tool6 });
15100
14170
  }
15101
14171
  }
15102
- } else if (tool5 === "bash" || tool5 === "execute") {
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 writeFile3, mkdir as mkdir3 } from "fs/promises";
15129
- import path10 from "path";
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 mkdir3(path10.dirname(filePath), { recursive: true });
14254
+ await mkdir2(path7.dirname(filePath), { recursive: true });
15185
14255
  const release = await acquireLock(filePath);
15186
14256
  try {
15187
- await writeFile3(filePath, content, "utf-8");
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 readFile2 } from "fs/promises";
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, state, projectPath, logger, tracker } = args;
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 readFile2(capture.rawFilePath, "utf-8");
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
- state.setPendingEnrichment(sessionID);
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/nudge.ts
15400
- var NUDGE_COOLDOWN = 3;
15401
- function shouldInjectNudge(level, messagesSinceLastNudge) {
15402
- if (level !== "high") return false;
15403
- if (messagesSinceLastNudge < NUDGE_COOLDOWN) return false;
15404
- return true;
15405
- }
15406
- function buildNudgeText(level) {
15407
- if (level === "high") {
15408
- return '\n<dm-nudge level="high">Context pressure is high. Consider summarizing old completed tasks and moving on. Use memory_store to persist important findings before they are compressed.</dm-nudge>';
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
- var DEFAULT_HEAD_LINES = 50;
15518
- var DEFAULT_TAIL_LINES = 20;
15519
- var MAX_LINE_LENGTH = 500;
15520
- function compressToolOutput(toolName, output) {
15521
- if (!output || output.length < 200) return output;
15522
- const strategy = TOOL_COMPRESS_STRATEGIES[toolName];
15523
- if (strategy) return strategy(output);
15524
- return compressGeneric(output);
15525
- }
15526
- function compressFileRead(output) {
15527
- const lines = output.split("\n");
15528
- if (lines.length <= 100) return output;
15529
- const head = lines.slice(0, DEFAULT_HEAD_LINES);
15530
- const tail = lines.slice(-DEFAULT_TAIL_LINES);
15531
- const keyLines = extractKeyLines(lines.slice(DEFAULT_HEAD_LINES, -DEFAULT_TAIL_LINES));
15532
- const parts = [...head, "...[truncated]", ...keyLines.slice(0, 10), "...[truncated]", ...tail];
15533
- return parts.join("\n");
15534
- }
15535
- function compressBash(output) {
15536
- const lines = output.split("\n");
15537
- if (lines.length <= 50 && output.length <= 5e3) return output;
15538
- if (lines.length <= 50) {
15539
- if (detectContentType(output) === "json") {
15540
- return compressJsonOutput(output);
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 deduplicateMiddle(items, maxCount) {
15768
- if (items.length <= maxCount) return items;
15769
- const seen = /* @__PURE__ */ new Set();
15770
- const unique = [];
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 unique;
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
@@ -15815,26 +14631,17 @@ function simpleHash(s) {
15815
14631
  }
15816
14632
  function compressAssistantText(text) {
15817
14633
  if (text.length < ASSISTANT_COMPRESS_MIN_LENGTH) return text;
14634
+ if (text.includes("```")) return text;
15818
14635
  const lines = text.split("\n");
15819
14636
  const head = 3;
15820
14637
  const tail = 3;
15821
14638
  const kept = [];
15822
- let inCodeBlock = false;
15823
14639
  for (let i = 0; i < lines.length; i++) {
15824
14640
  const line = lines[i];
15825
14641
  if (i < head || i >= lines.length - tail) {
15826
14642
  kept.push(line);
15827
14643
  continue;
15828
14644
  }
15829
- if (line.trim().startsWith("```")) {
15830
- inCodeBlock = !inCodeBlock;
15831
- kept.push(line);
15832
- continue;
15833
- }
15834
- if (inCodeBlock) {
15835
- kept.push(line);
15836
- continue;
15837
- }
15838
14645
  if (/^#{1,3}\s/.test(line) || /error|fail|warning|critical|important/i.test(line) || /^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line) || /^\/[^\s:]+/.test(line)) {
15839
14646
  kept.push(line);
15840
14647
  }
@@ -15884,47 +14691,33 @@ function singlePassCompress(messages, state, protectedTail) {
15884
14691
  if (toolState?.["status"] !== "completed") continue;
15885
14692
  const output = typeof toolState?.["output"] === "string" ? toolState["output"] : void 0;
15886
14693
  if (!output) continue;
15887
- if (output === "[superseded by duplicate call]") continue;
15888
- if (output.includes("[ccr:")) continue;
14694
+ if (output === "[OUTDATED \u2014 superseded by duplicate call]") continue;
14695
+ if (output.includes("deep_expand(")) continue;
15889
14696
  if (!PROTECTED_TOOLS.has(toolName) && !NEVER_DEDUP.has(toolName)) {
15890
14697
  const input = toolState["input"];
15891
- const signature = createToolSignature(toolName, input);
14698
+ const signature = `${toolName}:${JSON.stringify(input ?? {})}`;
15892
14699
  const outputHash = simpleHash(output);
15893
14700
  const existing = seen.get(signature);
15894
- if (existing) {
15895
- if (existing.outputHash === outputHash) {
15896
- const prevMsg = messages[existing.msgIdx];
15897
- for (const prevPart of prevMsg.parts) {
15898
- if (typeof prevPart !== "object" || prevPart === null) continue;
15899
- const pp = prevPart;
15900
- const ppState = pp["state"];
15901
- if (ppState?.["output"] === "[superseded by duplicate call]") continue;
15902
- if (typeof ppState?.["output"] === "string" && simpleHash(ppState["output"]) === outputHash) {
15903
- ppState["output"] = "[superseded by duplicate call]";
15904
- stats.toolDedup++;
15905
- }
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++;
15906
14710
  }
15907
14711
  }
15908
- seen.set(signature, { msgIdx: i, outputHash });
15909
- } else {
15910
- seen.set(signature, { msgIdx: i, outputHash });
15911
14712
  }
14713
+ seen.set(signature, { msgIdx: i, outputHash });
15912
14714
  }
15913
14715
  if (output.length >= 200 && !PROTECTED_TOOLS.has(toolName)) {
15914
- const result = compressToolOutput(toolName, output);
15915
- if (result.length < output.length * 0.85) {
15916
- const hash2 = ccrStore(state, output, result, toolName, callID);
15917
- toolState["output"] = ccrInjectMarker(result, hash2);
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);
15918
14720
  stats.toolOutputCompressed++;
15919
- continue;
15920
- }
15921
- }
15922
- if (output.length >= 200 && detectContentType(output) === "json" && !PROTECTED_TOOLS.has(toolName)) {
15923
- const crushed = crushJsonArray(output);
15924
- if (crushed.length < output.length * 0.85) {
15925
- const hash2 = ccrStore(state, output, crushed, toolName, callID);
15926
- toolState["output"] = ccrInjectMarker(crushed, hash2);
15927
- stats.jsonCrushed++;
15928
14721
  }
15929
14722
  }
15930
14723
  }
@@ -15974,7 +14767,7 @@ function computeProtectedTail(messages) {
15974
14767
  return 0;
15975
14768
  }
15976
14769
  function runCompressionPipeline(ctx) {
15977
- const { messages, state, sessionID, logger } = ctx;
14770
+ const { messages, state, logger } = ctx;
15978
14771
  const pressure = detectPressure(messages, state.getModelContextWindow());
15979
14772
  state.recordInputTokens(pressure.estimatedTokens);
15980
14773
  const protectedTail = computeProtectedTail(messages);
@@ -15983,31 +14776,14 @@ function runCompressionPipeline(ctx) {
15983
14776
  toolDedup: spStats.toolDedup,
15984
14777
  errorPurge: spStats.errorPurge,
15985
14778
  toolOutputCompressed: spStats.toolOutputCompressed,
15986
- jsonCrushed: spStats.jsonCrushed,
14779
+ jsonCrushed: 0,
15987
14780
  assistantCompressed: spStats.assistantCompressed,
15988
14781
  ccrStored: spStats.ccrStored,
15989
14782
  nudgeInjected: false,
15990
14783
  pressureLevel: pressure.level,
15991
14784
  estimatedTokens: pressure.estimatedTokens
15992
14785
  };
15993
- const sid = sessionID || "default";
15994
- const currentMsgCount = messages.length;
15995
- const pressureSince = state.messagesSinceLastNudge(sid, currentMsgCount);
15996
- if (shouldInjectNudge(pressure.level, pressureSince)) {
15997
- if (injectIntoLastAssistant(messages, buildNudgeText(pressure.level))) {
15998
- stats.nudgeInjected = true;
15999
- state.recordNudge(sid, currentMsgCount);
16000
- }
16001
- }
16002
- const memorySince = state.messagesSinceLastMemoryNudge(sid, currentMsgCount);
16003
- const memoryNudge = detectMemoryNudge(messages, memorySince);
16004
- if (memoryNudge.injected) {
16005
- if (injectIntoLastAssistant(messages, buildMemoryNudge(memoryNudge.type))) {
16006
- state.recordMemoryNudge(sid, currentMsgCount);
16007
- logger?.debug("compress: memory nudge", { type: memoryNudge.type });
16008
- }
16009
- }
16010
- 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;
16011
14787
  if (active) {
16012
14788
  logger?.debug("compress: pipeline result", { ...stats });
16013
14789
  } else {
@@ -16015,20 +14791,6 @@ function runCompressionPipeline(ctx) {
16015
14791
  }
16016
14792
  return { stats };
16017
14793
  }
16018
- function injectIntoLastAssistant(messages, text) {
16019
- for (let i = messages.length - 1; i >= 0; i--) {
16020
- const msg = messages[i];
16021
- if (msg.info.role !== "assistant") continue;
16022
- for (let j = msg.parts.length - 1; j >= 0; j--) {
16023
- const p = msg.parts[j];
16024
- if (p["type"] === "text" && typeof p["text"] === "string") {
16025
- p["text"] += text;
16026
- return true;
16027
- }
16028
- }
16029
- }
16030
- return false;
16031
- }
16032
14794
 
16033
14795
  // src/hooks/messages-transform.ts
16034
14796
  var KEEP_RECENT = 8;
@@ -16077,44 +14839,6 @@ function isSystemInjected(msg) {
16077
14839
  }
16078
14840
  return hasText && allInjected;
16079
14841
  }
16080
- function repairOrphanedToolCalls(messages) {
16081
- const toolUseIds = /* @__PURE__ */ new Set();
16082
- for (const msg of messages) {
16083
- if (msg.info.role !== "assistant") continue;
16084
- for (const part of msg.parts) {
16085
- if (typeof part !== "object" || part === null) continue;
16086
- const p = part;
16087
- if (p["type"] === "tool_use" && typeof p["id"] === "string") {
16088
- toolUseIds.add(p["id"]);
16089
- }
16090
- }
16091
- }
16092
- if (toolUseIds.size === 0) return;
16093
- const toolResultIds = /* @__PURE__ */ new Set();
16094
- for (const msg of messages) {
16095
- for (const part of msg.parts) {
16096
- if (typeof part !== "object" || part === null) continue;
16097
- const p = part;
16098
- if (p["type"] === "tool_result" && typeof p["tool_use_id"] === "string") {
16099
- toolResultIds.add(p["tool_use_id"]);
16100
- }
16101
- }
16102
- }
16103
- for (const msg of messages) {
16104
- if (msg.info.role !== "assistant") continue;
16105
- for (const part of msg.parts) {
16106
- if (typeof part !== "object" || part === null) continue;
16107
- const p = part;
16108
- if (p["type"] === "tool_use" && typeof p["id"] === "string") {
16109
- if (!toolResultIds.has(p["id"])) {
16110
- p["type"] = "tool";
16111
- p["state"] = { status: "ok" };
16112
- p["text"] = "[context-stripped]";
16113
- }
16114
- }
16115
- }
16116
- }
16117
- }
16118
14842
  function createMessagesTransformHandler(state, logger) {
16119
14843
  return async (input, output) => {
16120
14844
  const messages = output.messages;
@@ -16186,10 +14910,38 @@ function createMessagesTransformHandler(state, logger) {
16186
14910
  for (let r = toRemove.length - 1; r >= 0; r--) {
16187
14911
  messages.splice(toRemove[r], 1);
16188
14912
  }
16189
- repairOrphanedToolCalls(messages);
16190
14913
  if (Object.values(stats).some((v) => v > 0)) {
16191
14914
  logger?.debug("messages.transform: stripped", stats);
16192
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
+ }
16193
14945
  const pipelineResult = runCompressionPipeline({
16194
14946
  messages: output.messages,
16195
14947
  state,
@@ -16197,7 +14949,7 @@ function createMessagesTransformHandler(state, logger) {
16197
14949
  logger
16198
14950
  });
16199
14951
  const ds = pipelineResult.stats;
16200
- if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.jsonCrushed > 0 || ds.assistantCompressed > 0 || ds.nudgeInjected) {
14952
+ if (ds.toolDedup > 0 || ds.errorPurge > 0 || ds.toolOutputCompressed > 0 || ds.assistantCompressed > 0) {
16201
14953
  logger?.debug("messages.transform: deep compression", { ...ds });
16202
14954
  state.mergeNotify({
16203
14955
  compression: stats,
@@ -16206,23 +14958,6 @@ function createMessagesTransformHandler(state, logger) {
16206
14958
  protectedHead: PROTECTED_HEAD,
16207
14959
  protectedTail: KEEP_RECENT
16208
14960
  });
16209
- const recentEdits = state.getRecentEdits();
16210
- if (recentEdits.length > 0) {
16211
- const fileList = recentEdits.slice(0, 5).join(", ");
16212
- 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>";
16213
- for (let k = output.messages.length - 1; k >= 0; k--) {
16214
- const msg = output.messages[k];
16215
- if (msg.info.role !== "assistant") continue;
16216
- for (const part of msg.parts) {
16217
- const p = part;
16218
- if (p["type"] === "text" && typeof p["text"] === "string") {
16219
- p.text += nudge;
16220
- break;
16221
- }
16222
- }
16223
- break;
16224
- }
16225
- }
16226
14961
  } else if (Object.values(stats).some((v) => v > 0)) {
16227
14962
  state.mergeNotify({
16228
14963
  compression: stats,
@@ -16354,101 +15089,6 @@ function createNotifyHandler(client, logger) {
16354
15089
  };
16355
15090
  }
16356
15091
 
16357
- // src/extract/enrich.ts
16358
- import { stat } from "fs/promises";
16359
-
16360
- // src/extract/enrich-prompt.ts
16361
- 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.
16362
-
16363
- Files to read:
16364
- - Checkpoint: {{checkpointPath}}
16365
- - Raw messages: {{rawPath}}
16366
- - Project path: {{projectPath}}
16367
-
16368
- Steps:
16369
- 1. Read the current checkpoint.md \u2014 note its structure and contents.
16370
- 2. Read the raw messages JSON \u2014 these are the original conversation messages before compaction.
16371
- 3. Cross-reference:
16372
- a. Find related decisions that were made across multiple messages (consolidate fragments)
16373
- b. Identify constraints that were implicitly assumed but never explicitly stated
16374
- c. Link error messages to their fixes more precisely (the heuristic might have missed some)
16375
- d. Identify file changes that are part of larger refactoring patterns
16376
- 4. Update the checkpoint.md using memory_store or write tool \u2014 keep the same section structure but:
16377
- - Add cross-reference notes where relevant (e.g., "See also: [related decision]")
16378
- - Refine gotchas into more actionable descriptions
16379
- - Add a "## Synthesis" section summarizing the conversation's main themes (2-5 bullets)
16380
-
16381
- 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.
16382
- `;
16383
-
16384
- // src/extract/enrich.ts
16385
- var MAX_RAW_AGE_MS = 10 * 60 * 1e3;
16386
- async function runEnrichment(opts) {
16387
- const { client, projectPath, sessionID, logger } = opts;
16388
- const emptyResult = { sessionID: "", status: "skipped" };
16389
- const rawPath = checkpointRawPath(projectPath, sessionID);
16390
- let rawStat;
16391
- try {
16392
- rawStat = await stat(rawPath);
16393
- } catch {
16394
- logger?.debug("enrichment: checkpoint.raw.json missing, skipping", { sessionID, rawPath });
16395
- return emptyResult;
16396
- }
16397
- const rawAge = Date.now() - rawStat.mtimeMs;
16398
- if (rawAge > MAX_RAW_AGE_MS) {
16399
- logger?.debug("enrichment: checkpoint.raw.json is stale, skipping", {
16400
- sessionID,
16401
- rawPath,
16402
- ageMinutes: Math.round(rawAge / 6e4)
16403
- });
16404
- return emptyResult;
16405
- }
16406
- const checkpointPath = memoryFilePath("project", "checkpoint", projectPath);
16407
- try {
16408
- await stat(checkpointPath);
16409
- } catch {
16410
- logger?.debug("enrichment: checkpoint.md missing, skipping", { sessionID, checkpointPath });
16411
- return emptyResult;
16412
- }
16413
- try {
16414
- const now = (/* @__PURE__ */ new Date()).toISOString();
16415
- const title = `Memory Checkpoint Enrichment ${now}`;
16416
- const resp = await client.session.create({
16417
- body: { title }
16418
- });
16419
- const enrichSessionID = resp.data?.id;
16420
- if (!enrichSessionID) {
16421
- logger?.warn("enrichment: session.create returned no ID", { sessionID });
16422
- return { sessionID: "", status: "failed" };
16423
- }
16424
- const prompt = ENRICH_PROMPT_TEMPLATE.replaceAll("{{checkpointPath}}", checkpointPath).replaceAll("{{rawPath}}", rawPath).replaceAll("{{projectPath}}", projectPath).replaceAll("{{ISO timestamp}}", now);
16425
- await client.session.promptAsync({
16426
- path: { id: enrichSessionID },
16427
- body: {
16428
- parts: [{ type: "text", text: prompt }],
16429
- agent: "sisyphus",
16430
- tools: {
16431
- memory_search: true,
16432
- memory_store: true,
16433
- read: true,
16434
- list: true
16435
- }
16436
- }
16437
- });
16438
- logger?.info("enrichment: spawned background session", {
16439
- sessionID,
16440
- enrichSessionID
16441
- });
16442
- return { sessionID: enrichSessionID, status: "spawned" };
16443
- } catch (err) {
16444
- logger?.warn("enrichment: failed to spawn background session", {
16445
- sessionID,
16446
- error: err instanceof Error ? err.message : String(err)
16447
- });
16448
- return { sessionID: "", status: "failed" };
16449
- }
16450
- }
16451
-
16452
15092
  // src/repomap/extractor.ts
16453
15093
  var EXT_TO_LANG = {
16454
15094
  ".ts": "typescript",
@@ -16598,18 +15238,18 @@ function recencyDecay(lastRead) {
16598
15238
  }
16599
15239
  var RepoMapTracker = class {
16600
15240
  files = /* @__PURE__ */ new Map();
16601
- recordRead(path12, content) {
16602
- const lang = getLanguage(path12);
15241
+ recordRead(path9, content) {
15242
+ const lang = getLanguage(path9);
16603
15243
  if (!lang) return;
16604
- const symbols = extractSymbols(path12, content);
16605
- const existing = this.files.get(path12);
15244
+ const symbols = extractSymbols(path9, content);
15245
+ const existing = this.files.get(path9);
16606
15246
  if (existing) {
16607
15247
  existing.symbols = symbols;
16608
15248
  existing.readCount += 1;
16609
15249
  existing.lastRead = Date.now();
16610
15250
  } else {
16611
- this.files.set(path12, {
16612
- path: path12,
15251
+ this.files.set(path9, {
15252
+ path: path9,
16613
15253
  symbols,
16614
15254
  readCount: 1,
16615
15255
  lastRead: Date.now(),
@@ -16674,6 +15314,13 @@ var deepMemoryPlugin = async (input) => {
16674
15314
  dataRoot,
16675
15315
  serverUrl: input.serverUrl.toString()
16676
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
+ }
16677
15324
  const searchService = new SearchService({
16678
15325
  dataRoot,
16679
15326
  projectPath,
@@ -16705,82 +15352,26 @@ var deepMemoryPlugin = async (input) => {
16705
15352
  error: err instanceof Error ? err.message : String(err)
16706
15353
  });
16707
15354
  });
16708
- const memoryTools = createMemoryTools(searchService, { projectPath });
15355
+ const memoryTools = createMemoryTools(searchService, state, { projectPath });
16709
15356
  const notify = createNotifyHandler(input.client, logger.for("notify"));
16710
15357
  const hooks = {
16711
15358
  "chat.params": createChatParamsHandler(
16712
15359
  state,
16713
15360
  logger.for("chat-params")
16714
15361
  ),
16715
- "chat.message": createChatMessageHandler({
16716
- projectPath,
16717
- state,
16718
- logger: logger.for("chat-message")
16719
- }),
16720
15362
  "experimental.chat.system.transform": createSystemTransformHandler(
16721
15363
  state,
16722
15364
  projectPath,
16723
15365
  searchService,
16724
- logger.for("system-transform"),
16725
- tracker
15366
+ logger.for("system-transform")
16726
15367
  ),
16727
15368
  event: async ({ event }) => {
16728
15369
  try {
16729
15370
  if (event.type === "session.created") {
16730
- const info = event.properties.info;
16731
- if (!info || typeof info !== "object") {
16732
- logger.debug("event session.created: missing info, skipping");
16733
- return;
16734
- }
16735
- const i = info;
16736
- if (typeof i.id !== "string") {
16737
- logger.debug("event session.created: info.id not string, skipping");
16738
- return;
16739
- }
16740
- const narrowed = {
16741
- type: "session.created",
16742
- properties: {
16743
- info: {
16744
- id: i.id,
16745
- parentID: typeof i.parentID === "string" ? i.parentID : void 0,
16746
- title: typeof i.title === "string" ? i.title : "",
16747
- directory: typeof i.directory === "string" ? i.directory : projectPath
16748
- }
16749
- }
16750
- };
16751
- await Promise.allSettled([
16752
- handleSessionCreated({ state, event: narrowed, projectPath, logger: logger.for("resume") }),
16753
- handleSessionCreatedForDream({
16754
- event: narrowed,
16755
- config: { client: input.client, projectPath, model: state.bestModel(), logger: logger.for("auto-dream") }
16756
- }),
16757
- handleSessionCreatedForDistill({
16758
- event: narrowed,
16759
- config: { client: input.client, projectPath, model: state.bestModel(), logger: logger.for("auto-distill") }
16760
- })
16761
- ]);
16762
15371
  return;
16763
15372
  }
16764
15373
  if (event.type === "session.idle") {
16765
15374
  const idleSessionID = event.properties.sessionID;
16766
- if (idleSessionID && state.hasPendingEnrichment(idleSessionID)) {
16767
- state.consumePendingEnrichment(idleSessionID);
16768
- try {
16769
- const result = await runEnrichment({
16770
- client: input.client,
16771
- projectPath,
16772
- sessionID: idleSessionID,
16773
- logger: logger.for("enrichment")
16774
- });
16775
- logger.info("idle enrichment result", { ...result });
16776
- } catch (err) {
16777
- logger.warn("idle enrichment failed", {
16778
- error: err instanceof Error ? err.message : String(err)
16779
- });
16780
- }
16781
- } else {
16782
- logger.debug("event session.idle (no pending enrichment)");
16783
- }
16784
15375
  if (idleSessionID) {
16785
15376
  const pending = state.consumePendingNotify();
16786
15377
  if (pending) {
@@ -16809,8 +15400,8 @@ var deepMemoryPlugin = async (input) => {
16809
15400
  }
16810
15401
  try {
16811
15402
  const auditLogDir = projectMemoryDir(projectPath);
16812
- await mkdir4(auditLogDir, { recursive: true });
16813
- const auditLogPath = path11.join(auditLogDir, ".compaction-log.jsonl");
15403
+ await mkdir3(auditLogDir, { recursive: true });
15404
+ const auditLogPath = path8.join(auditLogDir, ".compaction-log.jsonl");
16814
15405
  const line = JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionID: compactedSessionID }) + "\n";
16815
15406
  const releaseLock = await acquireLock(auditLogPath);
16816
15407
  try {