@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 +25 -1
- package/dist/index.js +197 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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(
|
|
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") {
|