@bd7pil/opencode-deep-memory 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,7 @@ OpenCode auto-installs on startup. Memory appears at `.deep-memory/` in your pro
49
49
  ┌─────────────────────┴───────────────────────┐
50
50
  │ event │
51
51
  │ session.created → resume + dream schedule │
52
- │ session.idle → enrichment
52
+ │ session.idle → enrichment + notify
53
53
  │ session.compacted → checkpoint │
54
54
  └─────────────────────────────────────────────┘
55
55
  ```
@@ -74,6 +74,30 @@ with sentinels so message structure stays intact and prompt caching is preserved
74
74
  **Never touched**: user messages (anchor turn boundaries), recent 8 messages (working context),
75
75
  tool calls and their results (API pairing integrity).
76
76
 
77
+ ## Toast notifications
78
+
79
+ After each LLM turn, deep-memory shows a toast notification (bottom-right corner) summarizing
80
+ what was compressed and injected. The notification level is chosen automatically:
81
+
82
+ | Scenario | Level | Content |
83
+ |----------|-------|---------|
84
+ | Injection only (no compression) | minimal | One-line summary: `-8.5K stripped` |
85
+ | Compression (short session) | detailed | Progress bar + per-category breakdown |
86
+ | Compression + rich context (repo-map, memory, checkpoint) | extended | Full panel with budget usage |
87
+
88
+ Example toast (detailed level):
89
+
90
+ ```
91
+ deep-memory | compressed
92
+ ─ Compression ─────────────────────────────
93
+ │████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
94
+ reasoning -6.2K | metadata -2.1K | tool_err -0.8K
95
+ ─ Injection ───────────────────────────────
96
+ m[0] stable 1055B ✓ m[1] volatile 574B
97
+ tier=main | mode=normal
98
+ repo-map: 12 symbols | memory: 8 entries
99
+ ```
100
+
77
101
  ## Cache-stable injection
78
102
 
79
103
  Each turn pushes two system prompt fragments:
package/dist/index.js CHANGED
@@ -256,6 +256,7 @@ var PluginState = class {
256
256
  _pendingResumes = /* @__PURE__ */ new Map();
257
257
  _pendingEnrichments = /* @__PURE__ */ new Set();
258
258
  _lastUserText = /* @__PURE__ */ new Map();
259
+ _pendingNotify = null;
259
260
  agentOf(sessionID) {
260
261
  return this._agents.get(sessionID);
261
262
  }
@@ -341,6 +342,37 @@ var PluginState = class {
341
342
  this._lastUserText.delete(sessionID);
342
343
  return text;
343
344
  }
345
+ /**
346
+ * Merge stats into the pending notification.
347
+ * Called from messages.transform (compression) and system.transform (injection).
348
+ * Creates a new PendingNotify on first call per turn.
349
+ */
350
+ mergeNotify(patch) {
351
+ if (!this._pendingNotify) {
352
+ this._pendingNotify = { ...patch, setAt: Date.now() };
353
+ return;
354
+ }
355
+ if (patch.compression) {
356
+ this._pendingNotify.compression = patch.compression;
357
+ }
358
+ if (patch.injection) {
359
+ this._pendingNotify.injection = patch.injection;
360
+ }
361
+ if (patch.messageCount !== void 0) {
362
+ this._pendingNotify.messageCount = patch.messageCount;
363
+ }
364
+ if (patch.protectedHead !== void 0) {
365
+ this._pendingNotify.protectedHead = patch.protectedHead;
366
+ }
367
+ if (patch.protectedTail !== void 0) {
368
+ this._pendingNotify.protectedTail = patch.protectedTail;
369
+ }
370
+ }
371
+ consumePendingNotify() {
372
+ const n = this._pendingNotify;
373
+ this._pendingNotify = null;
374
+ return n;
375
+ }
344
376
  };
345
377
  function createPluginState() {
346
378
  return new PluginState();
@@ -777,7 +809,8 @@ async function composeSystemPayload(opts) {
777
809
  stable: `<deep-memory-stable>
778
810
  <tool-hint>${TOOL_HINT}</tool-hint>
779
811
  </deep-memory-stable>`,
780
- volatile: ""
812
+ volatile: "",
813
+ stats: { searchEntries: 0, repoMapEntries: 0, hasCheckpoint: false }
781
814
  };
782
815
  }
783
816
  const staticBudget = Math.floor(budget.memorySummary * 0.4);
@@ -794,6 +827,7 @@ ${staticMemory || "(empty)"}
794
827
  </constraints>
795
828
  </deep-memory-stable>`;
796
829
  let volatileContent = "";
830
+ let searchEntries = 0;
797
831
  if (userQuery && searchService && searchBudget > 0) {
798
832
  try {
799
833
  const results = await searchService.search(userQuery, { scope: "all", limit: 20, applyDecay: true });
@@ -808,12 +842,14 @@ ${staticMemory || "(empty)"}
808
842
  })),
809
843
  { budget: searchBudget }
810
844
  );
845
+ searchEntries = allocated.length;
811
846
  volatileContent = allocated.map((a) => a.rendered).join("\n");
812
847
  }
813
848
  } catch {
814
849
  }
815
850
  }
816
851
  let checkpointContent = "";
852
+ let hasCheckpoint = false;
817
853
  if (budget.checkpointSummary > 0) {
818
854
  const checkpointPath = memoryFilePath("project", "checkpoint", projectPath);
819
855
  checkpointContent = budgetedRead(checkpointPath, budget.checkpointSummary, [
@@ -823,6 +859,7 @@ ${staticMemory || "(empty)"}
823
859
  "Gotchas",
824
860
  "File Changes"
825
861
  ]);
862
+ hasCheckpoint = !!checkpointContent;
826
863
  }
827
864
  let volatile = `<deep-memory-volatile>
828
865
  <relevant>
@@ -834,9 +871,11 @@ ${volatileContent || "(none)"}
834
871
  ${checkpointContent}
835
872
  </last-checkpoint>`;
836
873
  }
874
+ let repoMapSymbols = 0;
837
875
  if (tracker && budget.repomap > 0) {
838
876
  const repomapEntries = tracker.getTopSymbols(budget.repomap);
839
877
  if (repomapEntries.length > 0) {
878
+ repoMapSymbols = repomapEntries.length;
840
879
  volatile += "\n" + formatRepoMap(repomapEntries);
841
880
  }
842
881
  }
@@ -849,7 +888,7 @@ ${checkpointContent}
849
888
  stableSize: stable.length,
850
889
  volatileSize: volatile.length
851
890
  });
852
- return { stable, volatile };
891
+ return { stable, volatile, stats: { searchEntries, repoMapEntries: repoMapSymbols, hasCheckpoint } };
853
892
  }
854
893
 
855
894
  // src/hooks/system-transform.ts
@@ -870,7 +909,7 @@ function createSystemTransformHandler(state, projectPath, searchService, logger,
870
909
  }
871
910
  }
872
911
  const userQuery = state.consumeLastUserText(sessionID);
873
- const { stable, volatile } = await composeSystemPayload({
912
+ const { stable, volatile, stats: payloadStats } = await composeSystemPayload({
874
913
  state,
875
914
  sessionID,
876
915
  projectPath,
@@ -896,6 +935,17 @@ function createSystemTransformHandler(state, projectPath, searchService, logger,
896
935
  stableSize: stable.length,
897
936
  volatileSize: volatile.length
898
937
  });
938
+ state.mergeNotify({
939
+ injection: {
940
+ stableSize: stable.length,
941
+ volatileSize: volatile.length,
942
+ tier,
943
+ mode,
944
+ searchEntries: payloadStats.searchEntries,
945
+ repoMapEntries: payloadStats.repoMapEntries,
946
+ hasCheckpoint: payloadStats.hasCheckpoint
947
+ }
948
+ });
899
949
  };
900
950
  }
901
951
 
@@ -15166,7 +15216,7 @@ function repairOrphanedToolCalls(messages) {
15166
15216
  }
15167
15217
  }
15168
15218
  }
15169
- function createMessagesTransformHandler(_state, logger) {
15219
+ function createMessagesTransformHandler(state, logger) {
15170
15220
  return async (_input, output) => {
15171
15221
  const messages = output.messages;
15172
15222
  if (messages.length <= KEEP_RECENT) return;
@@ -15238,6 +15288,132 @@ function createMessagesTransformHandler(_state, logger) {
15238
15288
  repairOrphanedToolCalls(messages);
15239
15289
  if (Object.values(stats).some((v) => v > 0)) {
15240
15290
  logger?.debug("messages.transform: stripped", stats);
15291
+ state.mergeNotify({
15292
+ compression: stats,
15293
+ messageCount: messages.length,
15294
+ protectedHead: PROTECTED_HEAD,
15295
+ protectedTail: KEEP_RECENT
15296
+ });
15297
+ }
15298
+ };
15299
+ }
15300
+
15301
+ // src/hooks/notify.ts
15302
+ var COOLDOWN_MS = 5e3;
15303
+ function formatK(n) {
15304
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
15305
+ return String(n);
15306
+ }
15307
+ function totalStripped(c) {
15308
+ return c.reasoning_cleared + c.metadata_stripped + c.system_neutralized + c.tool_errors_truncated + c.thinking_stripped;
15309
+ }
15310
+ function renderProgressBar(total, processed, width = 40) {
15311
+ if (total === 0) return `\u2502${"\u2591".repeat(width)}\u2502`;
15312
+ const filled = Math.round(processed / total * width);
15313
+ const bar = "\u2588".repeat(Math.min(filled, width)) + "\u2591".repeat(Math.max(0, width - filled));
15314
+ return `\u2502${bar}\u2502`;
15315
+ }
15316
+ function formatCompressionBlock(c, msgCount, head, tail) {
15317
+ const lines = [];
15318
+ if (msgCount && msgCount > 0) {
15319
+ const protectedZones = (head ?? 0) + (tail ?? 0);
15320
+ const scannable = Math.max(0, msgCount - protectedZones);
15321
+ const affected = Math.min(scannable, totalStripped(c));
15322
+ lines.push(renderProgressBar(scannable, affected));
15323
+ }
15324
+ const parts = [];
15325
+ if (c.reasoning_cleared > 0) parts.push(`reasoning -${formatK(c.reasoning_cleared)}`);
15326
+ if (c.metadata_stripped > 0) parts.push(`metadata -${formatK(c.metadata_stripped)}`);
15327
+ if (c.tool_errors_truncated > 0) parts.push(`tool_err -${formatK(c.tool_errors_truncated)}`);
15328
+ if (c.thinking_stripped > 0) parts.push(`thinking -${formatK(c.thinking_stripped)}`);
15329
+ if (c.system_neutralized > 0) parts.push(`sys_inject -${formatK(c.system_neutralized)}`);
15330
+ if (parts.length > 0) lines.push(` ${parts.join(" | ")}`);
15331
+ return lines.join("\n");
15332
+ }
15333
+ function formatInjectionBlock(i) {
15334
+ const cacheStatus = i.stableSize > 0 ? "\u2713" : "\u2014";
15335
+ const lines = [
15336
+ ` m[0] stable ${formatK(i.stableSize)}B ${cacheStatus} m[1] volatile ${formatK(i.volatileSize)}B`,
15337
+ ` tier=${i.tier} | mode=${i.mode}`
15338
+ ];
15339
+ const details = [];
15340
+ if (i.repoMapEntries > 0) details.push(`repo-map: ${i.repoMapEntries} symbols`);
15341
+ if (i.searchEntries > 0) details.push(`memory: ${i.searchEntries} entries`);
15342
+ if (i.hasCheckpoint) details.push(`checkpoint \u2713`);
15343
+ if (details.length > 0) lines.push(` ${details.join(" | ")}`);
15344
+ return lines.join("\n");
15345
+ }
15346
+ function chooseLevel(n) {
15347
+ if (!n.compression && n.injection) return "minimal";
15348
+ if (!n.compression) return "minimal";
15349
+ const hasRichContext = n.injection && (n.injection.repoMapEntries > 0 || n.injection.searchEntries > 0 || n.injection.hasCheckpoint);
15350
+ if (hasRichContext && n.messageCount && n.messageCount > 20) return "extended";
15351
+ return "detailed";
15352
+ }
15353
+ function formatNotify(n) {
15354
+ const level = chooseLevel(n);
15355
+ if (level === "minimal") {
15356
+ const parts = ["\u25A3 deep-memory"];
15357
+ if (n.compression) parts.push(`-${formatK(totalStripped(n.compression))} stripped`);
15358
+ if (n.injection) parts.push(`+${formatK(n.injection.stableSize + n.injection.volatileSize)}B injected`);
15359
+ return parts.join(" | ");
15360
+ }
15361
+ if (level === "extended") {
15362
+ const sections2 = [];
15363
+ if (n.compression) {
15364
+ sections2.push("\u2500 Compression \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15365
+ if (n.messageCount) {
15366
+ const head = n.protectedHead ?? 0;
15367
+ const tail = n.protectedTail ?? 0;
15368
+ sections2.push(` messages: ${n.messageCount} (protected: head=${head} tail=${tail})`);
15369
+ }
15370
+ sections2.push(formatCompressionBlock(n.compression, n.messageCount, n.protectedHead, n.protectedTail));
15371
+ }
15372
+ if (n.injection) {
15373
+ sections2.push("\u2500 Injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15374
+ sections2.push(formatInjectionBlock(n.injection));
15375
+ const budgetUsed = n.injection.stableSize + n.injection.volatileSize;
15376
+ const maxBudget = 4e3;
15377
+ const pct = Math.round(budgetUsed / maxBudget * 100);
15378
+ sections2.push(` budget: ${formatK(budgetUsed)}B / ${formatK(maxBudget)}B (${pct}%)`);
15379
+ }
15380
+ return ["\u25A3 deep-memory", ...sections2].join("\n");
15381
+ }
15382
+ const sections = [];
15383
+ if (n.compression) {
15384
+ sections.push("\u2500 Compression \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15385
+ sections.push(formatCompressionBlock(n.compression, n.messageCount, n.protectedHead, n.protectedTail));
15386
+ }
15387
+ if (n.injection) {
15388
+ sections.push("\u2500 Injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
15389
+ sections.push(formatInjectionBlock(n.injection));
15390
+ }
15391
+ return ["\u25A3 deep-memory", ...sections].join("\n");
15392
+ }
15393
+ function createNotifyHandler(client, logger) {
15394
+ let lastNotifyAt = 0;
15395
+ return async (sessionID, notify) => {
15396
+ const hasCompression = notify.compression && totalStripped(notify.compression) > 0;
15397
+ const hasInjection = !!notify.injection;
15398
+ if (!hasCompression && !hasInjection) return;
15399
+ if (notify.setAt - lastNotifyAt < COOLDOWN_MS) return;
15400
+ lastNotifyAt = Date.now();
15401
+ const message = formatNotify(notify);
15402
+ const title = hasCompression ? "deep-memory | compressed" : "deep-memory | injected";
15403
+ try {
15404
+ await client.tui.showToast({
15405
+ body: {
15406
+ title,
15407
+ message,
15408
+ variant: "info",
15409
+ duration: 5e3
15410
+ }
15411
+ });
15412
+ logger?.debug("notify: sent", { level: chooseLevel(notify) });
15413
+ } catch (err) {
15414
+ logger?.debug("notify: failed (non-fatal)", {
15415
+ error: err instanceof Error ? err.message : String(err)
15416
+ });
15241
15417
  }
15242
15418
  };
15243
15419
  }
@@ -15595,8 +15771,12 @@ var deepMemoryPlugin = async (input) => {
15595
15771
  });
15596
15772
  });
15597
15773
  const memoryTools = createMemoryTools(searchService, { projectPath });
15774
+ const notify = createNotifyHandler(input.client, logger.for("notify"));
15598
15775
  const hooks = {
15599
- "chat.params": createChatParamsHandler(state, logger.for("chat-params")),
15776
+ "chat.params": createChatParamsHandler(
15777
+ state,
15778
+ logger.for("chat-params")
15779
+ ),
15600
15780
  "chat.message": createChatMessageHandler({
15601
15781
  projectPath,
15602
15782
  state,
@@ -15666,6 +15846,18 @@ var deepMemoryPlugin = async (input) => {
15666
15846
  } else {
15667
15847
  logger.debug("event session.idle (no pending enrichment)");
15668
15848
  }
15849
+ if (idleSessionID) {
15850
+ const pending = state.consumePendingNotify();
15851
+ if (pending) {
15852
+ try {
15853
+ await notify(idleSessionID, pending);
15854
+ } catch (err) {
15855
+ logger.debug("idle notify failed (non-fatal)", {
15856
+ error: err instanceof Error ? err.message : String(err)
15857
+ });
15858
+ }
15859
+ }
15860
+ }
15669
15861
  return;
15670
15862
  }
15671
15863
  if (event.type === "session.compacted") {