@goondocks/myco 0.4.3 → 0.5.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.
Files changed (96) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +5 -1
  4. package/dist/chunk-2AMAOSRF.js +105 -0
  5. package/dist/chunk-2AMAOSRF.js.map +1 -0
  6. package/dist/{chunk-I7PNZEBO.js → chunk-6LTNFMXO.js} +12 -1
  7. package/dist/{chunk-I7PNZEBO.js.map → chunk-6LTNFMXO.js.map} +1 -1
  8. package/dist/{chunk-2GJFTIWX.js → chunk-7KQB22DP.js} +2 -2
  9. package/dist/{chunk-JBD5KP5G.js → chunk-B6WVNDA5.js} +14 -2
  10. package/dist/chunk-B6WVNDA5.js.map +1 -0
  11. package/dist/chunk-FIA5NTRH.js +159 -0
  12. package/dist/chunk-FIA5NTRH.js.map +1 -0
  13. package/dist/{chunk-GFBG73P4.js → chunk-FIRMTYFH.js} +3 -3
  14. package/dist/{chunk-XCPQHC4X.js → chunk-HJG7Z6SJ.js} +2 -2
  15. package/dist/chunk-HL2S5QZG.js +385 -0
  16. package/dist/chunk-HL2S5QZG.js.map +1 -0
  17. package/dist/{chunk-WBT5DWGC.js → chunk-IURC35BF.js} +2 -2
  18. package/dist/{chunk-67R6EMYD.js → chunk-JI6M2L2W.js} +31 -52
  19. package/dist/chunk-JI6M2L2W.js.map +1 -0
  20. package/dist/{chunk-FPEDTLQ6.js → chunk-JJL6AMDA.js} +3 -101
  21. package/dist/chunk-JJL6AMDA.js.map +1 -0
  22. package/dist/chunk-KYL67SKZ.js +150 -0
  23. package/dist/chunk-KYL67SKZ.js.map +1 -0
  24. package/dist/{chunk-ZCBL5HER.js → chunk-ND4VK6C7.js} +2 -2
  25. package/dist/{chunk-V2OWD2VV.js → chunk-R6LQT3U7.js} +24 -146
  26. package/dist/chunk-R6LQT3U7.js.map +1 -0
  27. package/dist/{chunk-IYFKPSRP.js → chunk-RCV2I4AI.js} +3 -3
  28. package/dist/{chunk-BNIYWCST.js → chunk-X6TKHO22.js} +2 -2
  29. package/dist/{chunk-OUFSLZTX.js → chunk-ZWUFTOG3.js} +21 -9
  30. package/dist/chunk-ZWUFTOG3.js.map +1 -0
  31. package/dist/{cli-PMOFCZQL.js → cli-BLYNNKGJ.js} +24 -18
  32. package/dist/cli-BLYNNKGJ.js.map +1 -0
  33. package/dist/{client-5SUO2UYH.js → client-5GB4WVXE.js} +5 -5
  34. package/dist/curate-S4HOYWXA.js +231 -0
  35. package/dist/curate-S4HOYWXA.js.map +1 -0
  36. package/dist/{detect-providers-IRL2TTLK.js → detect-providers-BIHYFK5M.js} +3 -3
  37. package/dist/digest-7NKYXM6G.js +96 -0
  38. package/dist/digest-7NKYXM6G.js.map +1 -0
  39. package/dist/{init-NUF5UBUJ.js → init-HPQ77WWF.js} +5 -5
  40. package/dist/{main-2XEBVUR6.js → main-NFQ4II75.js} +253 -576
  41. package/dist/main-NFQ4II75.js.map +1 -0
  42. package/dist/{rebuild-E6YFIRYZ.js → rebuild-KQ6G2GZM.js} +8 -7
  43. package/dist/{rebuild-E6YFIRYZ.js.map → rebuild-KQ6G2GZM.js.map} +1 -1
  44. package/dist/{reprocess-7G7KQWCN.js → reprocess-ZL4HKTSC.js} +95 -24
  45. package/dist/reprocess-ZL4HKTSC.js.map +1 -0
  46. package/dist/{restart-ABW4ZK3P.js → restart-FYW662DR.js} +6 -6
  47. package/dist/{search-MPD7SFK6.js → search-E5JQMTXV.js} +6 -6
  48. package/dist/{server-NZLZRITH.js → server-TV3D35HZ.js} +38 -15
  49. package/dist/{server-NZLZRITH.js.map → server-TV3D35HZ.js.map} +1 -1
  50. package/dist/{session-start-YB4A4PZB.js → session-start-5MFEOVQ5.js} +6 -6
  51. package/dist/{setup-digest-K732MGOJ.js → setup-digest-DZAFIBEF.js} +5 -5
  52. package/dist/{setup-llm-XCCH5LYD.js → setup-llm-4BZM33YT.js} +5 -5
  53. package/dist/src/cli.js +4 -4
  54. package/dist/src/daemon/main.js +4 -4
  55. package/dist/src/hooks/post-tool-use.js +5 -5
  56. package/dist/src/hooks/session-end.js +5 -5
  57. package/dist/src/hooks/session-start.js +4 -4
  58. package/dist/src/hooks/stop.js +6 -6
  59. package/dist/src/hooks/stop.js.map +1 -1
  60. package/dist/src/hooks/user-prompt-submit.js +5 -5
  61. package/dist/src/mcp/server.js +4 -4
  62. package/dist/src/prompts/extraction.md +1 -1
  63. package/dist/src/prompts/summary.md +1 -11
  64. package/dist/src/prompts/supersession.md +32 -0
  65. package/dist/{stats-6G7SN5YZ.js → stats-ZIIJ2GB3.js} +5 -5
  66. package/dist/{verify-JFHQH55Z.js → verify-RACBFT2P.js} +4 -4
  67. package/dist/{version-5B2TWXQJ.js → version-HJTVNPOO.js} +4 -4
  68. package/package.json +1 -1
  69. package/skills/setup/SKILL.md +56 -28
  70. package/skills/setup/references/model-recommendations.md +49 -43
  71. package/dist/chunk-67R6EMYD.js.map +0 -1
  72. package/dist/chunk-FPEDTLQ6.js.map +0 -1
  73. package/dist/chunk-JBD5KP5G.js.map +0 -1
  74. package/dist/chunk-OUFSLZTX.js.map +0 -1
  75. package/dist/chunk-V2OWD2VV.js.map +0 -1
  76. package/dist/cli-PMOFCZQL.js.map +0 -1
  77. package/dist/main-2XEBVUR6.js.map +0 -1
  78. package/dist/reprocess-7G7KQWCN.js.map +0 -1
  79. /package/dist/{chunk-2GJFTIWX.js.map → chunk-7KQB22DP.js.map} +0 -0
  80. /package/dist/{chunk-GFBG73P4.js.map → chunk-FIRMTYFH.js.map} +0 -0
  81. /package/dist/{chunk-XCPQHC4X.js.map → chunk-HJG7Z6SJ.js.map} +0 -0
  82. /package/dist/{chunk-WBT5DWGC.js.map → chunk-IURC35BF.js.map} +0 -0
  83. /package/dist/{chunk-ZCBL5HER.js.map → chunk-ND4VK6C7.js.map} +0 -0
  84. /package/dist/{chunk-IYFKPSRP.js.map → chunk-RCV2I4AI.js.map} +0 -0
  85. /package/dist/{chunk-BNIYWCST.js.map → chunk-X6TKHO22.js.map} +0 -0
  86. /package/dist/{client-5SUO2UYH.js.map → client-5GB4WVXE.js.map} +0 -0
  87. /package/dist/{detect-providers-IRL2TTLK.js.map → detect-providers-BIHYFK5M.js.map} +0 -0
  88. /package/dist/{init-NUF5UBUJ.js.map → init-HPQ77WWF.js.map} +0 -0
  89. /package/dist/{restart-ABW4ZK3P.js.map → restart-FYW662DR.js.map} +0 -0
  90. /package/dist/{search-MPD7SFK6.js.map → search-E5JQMTXV.js.map} +0 -0
  91. /package/dist/{session-start-YB4A4PZB.js.map → session-start-5MFEOVQ5.js.map} +0 -0
  92. /package/dist/{setup-digest-K732MGOJ.js.map → setup-digest-DZAFIBEF.js.map} +0 -0
  93. /package/dist/{setup-llm-XCCH5LYD.js.map → setup-llm-4BZM33YT.js.map} +0 -0
  94. /package/dist/{stats-6G7SN5YZ.js.map → stats-ZIIJ2GB3.js.map} +0 -0
  95. /package/dist/{verify-JFHQH55Z.js.map → verify-RACBFT2P.js.map} +0 -0
  96. /package/dist/{version-5B2TWXQJ.js.map → version-HJTVNPOO.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
2
  import {
3
3
  AgentRegistry
4
- } from "./chunk-BNIYWCST.js";
4
+ } from "./chunk-X6TKHO22.js";
5
5
 
6
6
  // src/native-deps.ts
7
7
  import { execFileSync } from "child_process";
@@ -53,4 +53,4 @@ function ensureNativeDeps() {
53
53
  export {
54
54
  ensureNativeDeps
55
55
  };
56
- //# sourceMappingURL=chunk-XCPQHC4X.js.map
56
+ //# sourceMappingURL=chunk-HJG7Z6SJ.js.map
@@ -0,0 +1,385 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ loadPrompt,
4
+ stripReasoningTokens
5
+ } from "./chunk-KYL67SKZ.js";
6
+ import {
7
+ stripFrontmatter
8
+ } from "./chunk-MIU3DKLN.js";
9
+ import {
10
+ require_dist
11
+ } from "./chunk-6UJWI4IW.js";
12
+ import {
13
+ CHARS_PER_TOKEN,
14
+ DIGEST_LLM_REQUEST_TIMEOUT_MS,
15
+ DIGEST_SUBSTRATE_TYPE_WEIGHTS,
16
+ DIGEST_TIER_MIN_CONTEXT,
17
+ LLM_REASONING_MODE,
18
+ estimateTokens
19
+ } from "./chunk-B6WVNDA5.js";
20
+ import {
21
+ __toESM
22
+ } from "./chunk-PZUWP5VK.js";
23
+
24
+ // src/daemon/digest.ts
25
+ var import_yaml = __toESM(require_dist(), 1);
26
+ import fs from "fs";
27
+ import path from "path";
28
+ import crypto from "crypto";
29
+ var PREVIOUS_EXTRACT_OVERHEAD_TOKENS = 50;
30
+ var CONTEXT_SAFETY_MARGIN = 0.7;
31
+ var EXTRACT_TYPE = "extract";
32
+ var DigestEngine = class {
33
+ vaultDir;
34
+ index;
35
+ llm;
36
+ config;
37
+ log;
38
+ lastCycleTimestampCache = void 0;
39
+ cycleInProgress = false;
40
+ constructor(engineConfig) {
41
+ this.vaultDir = engineConfig.vaultDir;
42
+ this.index = engineConfig.index;
43
+ this.llm = engineConfig.llmProvider;
44
+ this.config = engineConfig.config;
45
+ this.log = engineConfig.log ?? (() => {
46
+ });
47
+ }
48
+ /**
49
+ * Query index for recent vault notes to feed into the digest.
50
+ * Filters out extract notes (our own output) and caps at max_notes_per_cycle.
51
+ */
52
+ discoverSubstrate(lastCycleTimestamp) {
53
+ const maxNotes = this.config.digest.substrate.max_notes_per_cycle;
54
+ const notes = lastCycleTimestamp ? this.index.query({ updatedSince: lastCycleTimestamp, limit: maxNotes }) : this.index.query({ limit: maxNotes });
55
+ const filtered = notes.filter((n) => n.type !== EXTRACT_TYPE).filter((n) => {
56
+ if (n.type !== "spore") return true;
57
+ const status = n.frontmatter.status;
58
+ return !status || status === "active";
59
+ });
60
+ filtered.sort((a, b) => {
61
+ const weightA = DIGEST_SUBSTRATE_TYPE_WEIGHTS[a.type] ?? 0;
62
+ const weightB = DIGEST_SUBSTRATE_TYPE_WEIGHTS[b.type] ?? 0;
63
+ if (weightB !== weightA) return weightB - weightA;
64
+ return b.created.localeCompare(a.created);
65
+ });
66
+ return filtered.slice(0, maxNotes);
67
+ }
68
+ /**
69
+ * Filter configured tiers by the context window available.
70
+ * Only tiers whose minimum context requirement is met are eligible.
71
+ */
72
+ getEligibleTiers() {
73
+ const contextWindow = this.config.digest.intelligence.context_window;
74
+ return this.config.digest.tiers.filter((tier) => {
75
+ const minContext = DIGEST_TIER_MIN_CONTEXT[tier];
76
+ return minContext !== void 0 && minContext <= contextWindow;
77
+ });
78
+ }
79
+ /**
80
+ * Format notes compactly for inclusion in the digest prompt.
81
+ * Stops adding notes once the token budget is exceeded.
82
+ */
83
+ formatSubstrate(notes, tokenBudget) {
84
+ const charBudget = tokenBudget * CHARS_PER_TOKEN;
85
+ const parts = [];
86
+ let usedChars = 0;
87
+ for (const note of notes) {
88
+ const entry = `### [${note.type}] ${note.id} \u2014 "${note.title}"
89
+ ${note.content}`;
90
+ if (usedChars + entry.length > charBudget && parts.length > 0) break;
91
+ parts.push(entry);
92
+ usedChars += entry.length;
93
+ }
94
+ return parts.join("\n\n");
95
+ }
96
+ /**
97
+ * Read a previously generated extract for a given tier.
98
+ * Returns the body (stripped of YAML frontmatter), or null if not found.
99
+ */
100
+ readPreviousExtract(tier) {
101
+ const extractPath = path.join(this.vaultDir, "digest", `extract-${tier}.md`);
102
+ let content;
103
+ try {
104
+ content = fs.readFileSync(extractPath, "utf-8");
105
+ } catch {
106
+ return null;
107
+ }
108
+ return stripFrontmatter(content).body;
109
+ }
110
+ /**
111
+ * Write a digest extract to the vault with YAML frontmatter.
112
+ * Uses atomic write pattern (temp file + rename).
113
+ */
114
+ writeExtract(tier, body, cycleId, model, substrateCount) {
115
+ const digestDir = path.join(this.vaultDir, "digest");
116
+ fs.mkdirSync(digestDir, { recursive: true });
117
+ const frontmatter = {
118
+ type: EXTRACT_TYPE,
119
+ tier,
120
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
121
+ cycle_id: cycleId,
122
+ substrate_count: substrateCount,
123
+ model
124
+ };
125
+ const fmYaml = import_yaml.default.stringify(frontmatter, {
126
+ defaultStringType: "QUOTE_DOUBLE",
127
+ defaultKeyType: "PLAIN"
128
+ }).trim();
129
+ const file = `---
130
+ ${fmYaml}
131
+ ---
132
+
133
+ ${body}
134
+ `;
135
+ const fullPath = path.join(digestDir, `extract-${tier}.md`);
136
+ const tmpPath = `${fullPath}.tmp`;
137
+ fs.writeFileSync(tmpPath, file, "utf-8");
138
+ fs.renameSync(tmpPath, fullPath);
139
+ }
140
+ /**
141
+ * Append a digest cycle result as a JSON line to trace.jsonl.
142
+ */
143
+ appendTrace(record) {
144
+ const digestDir = path.join(this.vaultDir, "digest");
145
+ fs.mkdirSync(digestDir, { recursive: true });
146
+ const tracePath = path.join(digestDir, "trace.jsonl");
147
+ fs.appendFileSync(tracePath, JSON.stringify(record) + "\n", "utf-8");
148
+ this.lastCycleTimestampCache = record.timestamp;
149
+ }
150
+ /**
151
+ * Read the last cycle timestamp from trace.jsonl.
152
+ * Cached in memory after first read — subsequent calls are O(1).
153
+ */
154
+ getLastCycleTimestamp() {
155
+ if (this.lastCycleTimestampCache !== void 0) return this.lastCycleTimestampCache;
156
+ const tracePath = path.join(this.vaultDir, "digest", "trace.jsonl");
157
+ let content;
158
+ try {
159
+ content = fs.readFileSync(tracePath, "utf-8").trim();
160
+ } catch {
161
+ this.lastCycleTimestampCache = null;
162
+ return null;
163
+ }
164
+ if (!content) {
165
+ this.lastCycleTimestampCache = null;
166
+ return null;
167
+ }
168
+ const lines = content.split("\n");
169
+ const lastLine = lines[lines.length - 1];
170
+ try {
171
+ const record = JSON.parse(lastLine);
172
+ this.lastCycleTimestampCache = record.timestamp;
173
+ return record.timestamp;
174
+ } catch {
175
+ this.lastCycleTimestampCache = null;
176
+ return null;
177
+ }
178
+ }
179
+ /**
180
+ * Run a full digest cycle: discover substrate, generate extracts for each tier.
181
+ * Returns the cycle result, or null if no substrate was found.
182
+ */
183
+ async runCycle(opts) {
184
+ if (this.cycleInProgress) {
185
+ this.log("debug", "Cycle already in progress \u2014 skipping");
186
+ return null;
187
+ }
188
+ this.cycleInProgress = true;
189
+ try {
190
+ return await this.runCycleInternal(opts);
191
+ } finally {
192
+ this.cycleInProgress = false;
193
+ }
194
+ }
195
+ async runCycleInternal(opts) {
196
+ if (this.llm.ensureLoaded) {
197
+ const { context_window: contextWindow, gpu_kv_cache: gpuKvCache } = this.config.digest.intelligence;
198
+ this.log("debug", "Verifying digest model", { contextWindow, gpuKvCache });
199
+ await this.llm.ensureLoaded(contextWindow, gpuKvCache);
200
+ }
201
+ const startTime = Date.now();
202
+ const fullReprocess = opts?.fullReprocess ?? false;
203
+ const lastTimestamp = fullReprocess ? null : this.getLastCycleTimestamp();
204
+ const substrate = this.discoverSubstrate(lastTimestamp);
205
+ this.log("debug", "Discovering substrate", { lastTimestamp: lastTimestamp ?? "full reprocess", substrateCount: substrate.length });
206
+ if (substrate.length === 0) {
207
+ this.log("debug", "No substrate found \u2014 skipping cycle");
208
+ return null;
209
+ }
210
+ this.log("info", `Starting digest cycle`, { substrateCount: substrate.length, fullReprocess });
211
+ const cycleId = crypto.randomUUID();
212
+ const allEligible = this.getEligibleTiers();
213
+ const eligibleTiers = opts?.tiers ? allEligible.filter((t) => opts.tiers.includes(t)) : allEligible;
214
+ this.log("debug", `Eligible tiers: [${eligibleTiers.join(", ")}]`);
215
+ const tiersGenerated = [];
216
+ let totalTokensUsed = 0;
217
+ let model = "";
218
+ const typeToKey = {
219
+ session: "sessions",
220
+ spore: "spores",
221
+ plan: "plans",
222
+ artifact: "artifacts",
223
+ "team-member": "team"
224
+ };
225
+ const substrateIndex = {
226
+ sessions: [],
227
+ spores: [],
228
+ plans: [],
229
+ artifacts: [],
230
+ team: []
231
+ };
232
+ for (const note of substrate) {
233
+ const key = typeToKey[note.type];
234
+ if (key) {
235
+ substrateIndex[key].push(note.id);
236
+ }
237
+ }
238
+ const cycleTimestamp = (/* @__PURE__ */ new Date()).toISOString();
239
+ const systemPrompt = loadPrompt("digest-system");
240
+ for (const tier of eligibleTiers) {
241
+ const tierPrompt = loadPrompt(`digest-${tier}`);
242
+ const previousExtract = opts?.cleanSlate ? null : this.readPreviousExtract(tier);
243
+ const contextWindow = this.config.digest.intelligence.context_window;
244
+ const systemPromptTokens = estimateTokens(systemPrompt);
245
+ const tierPromptTokens = estimateTokens(tierPrompt);
246
+ const previousExtractTokens = previousExtract ? estimateTokens(previousExtract) + PREVIOUS_EXTRACT_OVERHEAD_TOKENS : 0;
247
+ const availableTokens = Math.floor(contextWindow * CONTEXT_SAFETY_MARGIN);
248
+ const substrateBudget = availableTokens - tier - systemPromptTokens - tierPromptTokens - previousExtractTokens;
249
+ if (substrateBudget <= 0) continue;
250
+ const formattedSubstrate = this.formatSubstrate(substrate, substrateBudget);
251
+ const promptParts = [tierPrompt];
252
+ if (previousExtract) {
253
+ promptParts.push("", "## Previous Synthesis", "", previousExtract);
254
+ }
255
+ promptParts.push("", "## New Substrate", "", formattedSubstrate);
256
+ promptParts.push(
257
+ "",
258
+ "---",
259
+ "Produce your updated synthesis now. Stay within the token budget specified above."
260
+ );
261
+ const userPrompt = promptParts.join("\n");
262
+ const promptTokens = estimateTokens(systemPrompt + userPrompt);
263
+ this.log("debug", `Tier ${tier}: sending LLM request`, { promptTokens, maxTokens: tier, substrateBudget });
264
+ try {
265
+ const tierStart = Date.now();
266
+ const digestConfig = this.config.digest.intelligence;
267
+ const opts2 = {
268
+ maxTokens: tier,
269
+ timeoutMs: DIGEST_LLM_REQUEST_TIMEOUT_MS,
270
+ contextLength: contextWindow,
271
+ reasoning: LLM_REASONING_MODE,
272
+ systemPrompt,
273
+ keepAlive: digestConfig.keep_alive ?? void 0
274
+ };
275
+ const response = await this.llm.summarize(userPrompt, opts2);
276
+ const tierDuration = Date.now() - tierStart;
277
+ const extractText = stripReasoningTokens(response.text);
278
+ model = response.model;
279
+ const responseTokens = estimateTokens(extractText);
280
+ totalTokensUsed += promptTokens + responseTokens;
281
+ this.log("info", `Tier ${tier}: completed`, { durationMs: tierDuration, responseTokens, model: response.model });
282
+ this.writeExtract(tier, extractText, cycleId, response.model, substrate.length);
283
+ tiersGenerated.push(tier);
284
+ } catch (err) {
285
+ this.log("warn", `Tier ${tier}: failed`, { error: err.message });
286
+ }
287
+ }
288
+ const result = {
289
+ cycleId,
290
+ timestamp: cycleTimestamp,
291
+ substrate: substrateIndex,
292
+ tiersGenerated,
293
+ model,
294
+ durationMs: Date.now() - startTime,
295
+ tokensUsed: totalTokensUsed
296
+ };
297
+ this.appendTrace(result);
298
+ return result;
299
+ }
300
+ };
301
+ var MS_PER_SECOND = 1e3;
302
+ var Metabolism = class {
303
+ state = "active";
304
+ currentIntervalMs;
305
+ cooldownStep = 0;
306
+ lastSubstrateTime;
307
+ timer = null;
308
+ activeIntervalMs;
309
+ cooldownIntervalsMs;
310
+ dormancyThresholdMs;
311
+ constructor(config) {
312
+ this.activeIntervalMs = config.active_interval * MS_PER_SECOND;
313
+ this.cooldownIntervalsMs = config.cooldown_intervals.map((s) => s * MS_PER_SECOND);
314
+ this.dormancyThresholdMs = config.dormancy_threshold * MS_PER_SECOND;
315
+ this.currentIntervalMs = this.activeIntervalMs;
316
+ this.lastSubstrateTime = Date.now();
317
+ }
318
+ /** Reset to active state when new substrate is found. */
319
+ onSubstrateFound() {
320
+ this.state = "active";
321
+ this.cooldownStep = 0;
322
+ this.currentIntervalMs = this.activeIntervalMs;
323
+ this.lastSubstrateTime = Date.now();
324
+ }
325
+ /** Advance cooldown when a cycle finds no new substrate. */
326
+ onEmptyCycle() {
327
+ if (this.state === "dormant") return;
328
+ this.state = "cooling";
329
+ if (this.cooldownStep < this.cooldownIntervalsMs.length) {
330
+ this.currentIntervalMs = this.cooldownIntervalsMs[this.cooldownStep];
331
+ this.cooldownStep++;
332
+ }
333
+ this.checkDormancy();
334
+ }
335
+ /** Enter dormant state if enough time has elapsed since last substrate. */
336
+ checkDormancy() {
337
+ const elapsed = Date.now() - this.lastSubstrateTime;
338
+ if (elapsed >= this.dormancyThresholdMs) {
339
+ this.state = "dormant";
340
+ }
341
+ }
342
+ /** Return to active from any state, resetting timers and rescheduling immediately. */
343
+ activate() {
344
+ this.onSubstrateFound();
345
+ if (this.callback) {
346
+ this.reschedule();
347
+ }
348
+ }
349
+ /** Set lastSubstrateTime explicitly (for testing). */
350
+ markLastSubstrate(time) {
351
+ this.lastSubstrateTime = time;
352
+ }
353
+ /** Begin scheduling digest cycles with adaptive intervals. */
354
+ start(callback) {
355
+ this.callback = callback;
356
+ this.reschedule();
357
+ }
358
+ /** Stop the timer. */
359
+ stop() {
360
+ if (this.timer) {
361
+ clearTimeout(this.timer);
362
+ this.timer = null;
363
+ }
364
+ }
365
+ callback = null;
366
+ reschedule() {
367
+ this.stop();
368
+ if (!this.callback) return;
369
+ const cb = this.callback;
370
+ const schedule = () => {
371
+ this.timer = setTimeout(async () => {
372
+ await cb();
373
+ schedule();
374
+ }, this.currentIntervalMs);
375
+ this.timer.unref();
376
+ };
377
+ schedule();
378
+ }
379
+ };
380
+
381
+ export {
382
+ DigestEngine,
383
+ Metabolism
384
+ };
385
+ //# sourceMappingURL=chunk-HL2S5QZG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/daemon/digest.ts"],"sourcesContent":["/**\n * DigestEngine — synthesizes vault knowledge into tiered context extracts.\n * Metabolism — adaptive timer that throttles digest cycles based on activity.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport crypto from 'node:crypto';\nimport YAML from 'yaml';\n\nimport type { MycoIndex, IndexedNote } from '@myco/index/sqlite.js';\nimport type { LlmProvider, LlmRequestOptions } from '@myco/intelligence/llm.js';\nimport type { MycoConfig } from '@myco/config/schema.js';\nimport { loadPrompt } from '@myco/prompts/index.js';\nimport { stripReasoningTokens } from '@myco/intelligence/response.js';\nimport { stripFrontmatter } from '@myco/vault/frontmatter.js';\nimport {\n estimateTokens,\n CHARS_PER_TOKEN,\n DIGEST_TIER_MIN_CONTEXT,\n DIGEST_SUBSTRATE_TYPE_WEIGHTS,\n DIGEST_LLM_REQUEST_TIMEOUT_MS,\n LLM_REASONING_MODE,\n} from '@myco/constants.js';\n\n// --- Interfaces ---\n\nexport interface DigestCycleResult {\n cycleId: string;\n timestamp: string;\n substrate: {\n sessions: string[];\n spores: string[];\n plans: string[];\n artifacts: string[];\n team: string[];\n };\n tiersGenerated: number[];\n model: string;\n durationMs: number;\n tokensUsed: number;\n}\n\n/** Simple log function signature for digest progress reporting. */\nexport type DigestLogFn = (level: 'debug' | 'info' | 'warn', message: string, data?: Record<string, unknown>) => void;\n\nexport interface DigestCycleOptions {\n /** Process all substrate regardless of last cycle timestamp. */\n fullReprocess?: boolean;\n /** Only generate these tiers (default: all eligible). */\n tiers?: number[];\n /** Skip previous extract — start from clean slate. */\n cleanSlate?: boolean;\n}\n\nexport interface DigestEngineConfig {\n vaultDir: string;\n index: MycoIndex;\n llmProvider: LlmProvider;\n config: MycoConfig;\n log?: DigestLogFn;\n}\n\n// --- Constants ---\n\n/** Token overhead estimate for previous extract section wrapper. */\nconst PREVIOUS_EXTRACT_OVERHEAD_TOKENS = 50;\n\n/** Safety margin for context window — our CHARS_PER_TOKEN=4 heuristic significantly\n * underestimates real token counts (observed ~3.2 chars/token for mixed content).\n * 0.70 provides a safe buffer: 32K * 0.70 = 22.4K usable tokens. */\nconst CONTEXT_SAFETY_MARGIN = 0.70;\n\n/** Types that are digest output — excluded from substrate to avoid self-digestion. */\nconst EXTRACT_TYPE = 'extract';\n\n// --- DigestEngine ---\n\nexport class DigestEngine {\n private vaultDir: string;\n private index: MycoIndex;\n private llm: LlmProvider;\n private config: MycoConfig;\n private log: DigestLogFn;\n private lastCycleTimestampCache: string | null | undefined = undefined;\n private cycleInProgress = false;\n\n constructor(engineConfig: DigestEngineConfig) {\n this.vaultDir = engineConfig.vaultDir;\n this.index = engineConfig.index;\n this.llm = engineConfig.llmProvider;\n this.config = engineConfig.config;\n this.log = engineConfig.log ?? (() => {});\n }\n\n /**\n * Query index for recent vault notes to feed into the digest.\n * Filters out extract notes (our own output) and caps at max_notes_per_cycle.\n */\n discoverSubstrate(lastCycleTimestamp: string | null): IndexedNote[] {\n const maxNotes = this.config.digest.substrate.max_notes_per_cycle;\n\n const notes = lastCycleTimestamp\n ? this.index.query({ updatedSince: lastCycleTimestamp, limit: maxNotes })\n : this.index.query({ limit: maxNotes });\n\n // Guard against self-digestion: extract files are not currently indexed,\n // but this filter prevents feedback loops if they ever are (e.g., via rebuild)\n const filtered = notes\n .filter((n) => n.type !== EXTRACT_TYPE)\n .filter((n) => {\n if (n.type !== 'spore') return true;\n const status = n.frontmatter.status as string | undefined;\n return !status || status === 'active';\n });\n\n // Sort by type weight (descending) then by recency (descending)\n filtered.sort((a, b) => {\n const weightA = DIGEST_SUBSTRATE_TYPE_WEIGHTS[a.type] ?? 0;\n const weightB = DIGEST_SUBSTRATE_TYPE_WEIGHTS[b.type] ?? 0;\n if (weightB !== weightA) return weightB - weightA;\n // More recent first — created is ISO string, lexicographic sort works\n return b.created.localeCompare(a.created);\n });\n\n return filtered.slice(0, maxNotes);\n }\n\n /**\n * Filter configured tiers by the context window available.\n * Only tiers whose minimum context requirement is met are eligible.\n */\n getEligibleTiers(): number[] {\n const contextWindow = this.config.digest.intelligence.context_window;\n return this.config.digest.tiers.filter((tier) => {\n const minContext = DIGEST_TIER_MIN_CONTEXT[tier];\n return minContext !== undefined && minContext <= contextWindow;\n });\n }\n\n /**\n * Format notes compactly for inclusion in the digest prompt.\n * Stops adding notes once the token budget is exceeded.\n */\n formatSubstrate(notes: IndexedNote[], tokenBudget: number): string {\n const charBudget = tokenBudget * CHARS_PER_TOKEN;\n const parts: string[] = [];\n let usedChars = 0;\n\n for (const note of notes) {\n const entry = `### [${note.type}] ${note.id} — \"${note.title}\"\\n${note.content}`;\n if (usedChars + entry.length > charBudget && parts.length > 0) break;\n parts.push(entry);\n usedChars += entry.length;\n }\n\n return parts.join('\\n\\n');\n }\n\n /**\n * Read a previously generated extract for a given tier.\n * Returns the body (stripped of YAML frontmatter), or null if not found.\n */\n readPreviousExtract(tier: number): string | null {\n const extractPath = path.join(this.vaultDir, 'digest', `extract-${tier}.md`);\n let content: string;\n try {\n content = fs.readFileSync(extractPath, 'utf-8');\n } catch {\n return null;\n }\n\n return stripFrontmatter(content).body;\n }\n\n /**\n * Write a digest extract to the vault with YAML frontmatter.\n * Uses atomic write pattern (temp file + rename).\n */\n writeExtract(\n tier: number,\n body: string,\n cycleId: string,\n model: string,\n substrateCount: number,\n ): void {\n const digestDir = path.join(this.vaultDir, 'digest');\n fs.mkdirSync(digestDir, { recursive: true });\n\n const frontmatter: Record<string, unknown> = {\n type: EXTRACT_TYPE,\n tier,\n generated: new Date().toISOString(),\n cycle_id: cycleId,\n substrate_count: substrateCount,\n model,\n };\n\n const fmYaml = YAML.stringify(frontmatter, {\n defaultStringType: 'QUOTE_DOUBLE',\n defaultKeyType: 'PLAIN',\n }).trim();\n const file = `---\\n${fmYaml}\\n---\\n\\n${body}\\n`;\n\n const fullPath = path.join(digestDir, `extract-${tier}.md`);\n const tmpPath = `${fullPath}.tmp`;\n fs.writeFileSync(tmpPath, file, 'utf-8');\n fs.renameSync(tmpPath, fullPath);\n }\n\n /**\n * Append a digest cycle result as a JSON line to trace.jsonl.\n */\n appendTrace(record: DigestCycleResult): void {\n const digestDir = path.join(this.vaultDir, 'digest');\n fs.mkdirSync(digestDir, { recursive: true });\n const tracePath = path.join(digestDir, 'trace.jsonl');\n fs.appendFileSync(tracePath, JSON.stringify(record) + '\\n', 'utf-8');\n this.lastCycleTimestampCache = record.timestamp;\n }\n\n /**\n * Read the last cycle timestamp from trace.jsonl.\n * Cached in memory after first read — subsequent calls are O(1).\n */\n getLastCycleTimestamp(): string | null {\n if (this.lastCycleTimestampCache !== undefined) return this.lastCycleTimestampCache;\n\n const tracePath = path.join(this.vaultDir, 'digest', 'trace.jsonl');\n let content: string;\n try {\n content = fs.readFileSync(tracePath, 'utf-8').trim();\n } catch {\n this.lastCycleTimestampCache = null;\n return null;\n }\n\n if (!content) {\n this.lastCycleTimestampCache = null;\n return null;\n }\n\n const lines = content.split('\\n');\n const lastLine = lines[lines.length - 1];\n try {\n const record = JSON.parse(lastLine) as DigestCycleResult;\n this.lastCycleTimestampCache = record.timestamp;\n return record.timestamp;\n } catch {\n this.lastCycleTimestampCache = null;\n return null;\n }\n }\n\n /**\n * Run a full digest cycle: discover substrate, generate extracts for each tier.\n * Returns the cycle result, or null if no substrate was found.\n */\n async runCycle(opts?: DigestCycleOptions): Promise<DigestCycleResult | null> {\n if (this.cycleInProgress) {\n this.log('debug', 'Cycle already in progress — skipping');\n return null;\n }\n this.cycleInProgress = true;\n\n try {\n return await this.runCycleInternal(opts);\n } finally {\n this.cycleInProgress = false;\n }\n }\n\n private async runCycleInternal(opts?: DigestCycleOptions): Promise<DigestCycleResult | null> {\n // Ensure model is loaded with correct settings every cycle.\n // LM Studio's idle TTL can evict our instance between cycles — without\n // re-running ensureLoaded, the auto-reloaded instance would use LM Studio's\n // UI defaults (wrong KV cache setting). This is fast (~26ms) when the\n // instance is still alive (just a getLoadedInstances check).\n if (this.llm.ensureLoaded) {\n const { context_window: contextWindow, gpu_kv_cache: gpuKvCache } = this.config.digest.intelligence;\n this.log('debug', 'Verifying digest model', { contextWindow, gpuKvCache });\n await this.llm.ensureLoaded(contextWindow, gpuKvCache);\n }\n\n const startTime = Date.now();\n const fullReprocess = opts?.fullReprocess ?? false;\n const lastTimestamp = fullReprocess ? null : this.getLastCycleTimestamp();\n const substrate = this.discoverSubstrate(lastTimestamp);\n\n this.log('debug', 'Discovering substrate', { lastTimestamp: lastTimestamp ?? 'full reprocess', substrateCount: substrate.length });\n if (substrate.length === 0) {\n this.log('debug', 'No substrate found — skipping cycle');\n return null;\n }\n\n this.log('info', `Starting digest cycle`, { substrateCount: substrate.length, fullReprocess });\n const cycleId = crypto.randomUUID();\n const allEligible = this.getEligibleTiers();\n const eligibleTiers = opts?.tiers\n ? allEligible.filter((t) => opts.tiers!.includes(t))\n : allEligible;\n this.log('debug', `Eligible tiers: [${eligibleTiers.join(', ')}]`);\n const tiersGenerated: number[] = [];\n let totalTokensUsed = 0;\n let model = '';\n\n // Categorize substrate by type for the result\n const typeToKey: Record<string, keyof DigestCycleResult['substrate']> = {\n session: 'sessions',\n spore: 'spores',\n plan: 'plans',\n artifact: 'artifacts',\n 'team-member': 'team',\n };\n const substrateIndex: DigestCycleResult['substrate'] = {\n sessions: [],\n spores: [],\n plans: [],\n artifacts: [],\n team: [],\n };\n for (const note of substrate) {\n const key = typeToKey[note.type];\n if (key) {\n substrateIndex[key].push(note.id);\n }\n }\n\n // Record the cycle timestamp NOW, before tier processing. This ensures the\n // timestamp advances even if LLM calls fail, preventing the same substrate\n // from being rediscovered on every subsequent timer fire.\n const cycleTimestamp = new Date().toISOString();\n\n const systemPrompt = loadPrompt('digest-system');\n\n for (const tier of eligibleTiers) {\n const tierPrompt = loadPrompt(`digest-${tier}`);\n const previousExtract = opts?.cleanSlate ? null : this.readPreviousExtract(tier);\n\n // Calculate token budget for substrate:\n // (context_window * safety_margin) - output - system_prompt - tier_prompt - previous_extract\n const contextWindow = this.config.digest.intelligence.context_window;\n const systemPromptTokens = estimateTokens(systemPrompt);\n const tierPromptTokens = estimateTokens(tierPrompt);\n const previousExtractTokens = previousExtract\n ? estimateTokens(previousExtract) + PREVIOUS_EXTRACT_OVERHEAD_TOKENS\n : 0;\n const availableTokens = Math.floor(contextWindow * CONTEXT_SAFETY_MARGIN);\n const substrateBudget = availableTokens - tier - systemPromptTokens - tierPromptTokens - previousExtractTokens;\n\n if (substrateBudget <= 0) continue;\n\n const formattedSubstrate = this.formatSubstrate(substrate, substrateBudget);\n\n // Build user prompt (system prompt sent separately via LlmRequestOptions)\n const promptParts = [tierPrompt];\n\n if (previousExtract) {\n promptParts.push('', '## Previous Synthesis', '', previousExtract);\n }\n\n promptParts.push('', '## New Substrate', '', formattedSubstrate);\n promptParts.push(\n '',\n '---',\n 'Produce your updated synthesis now. Stay within the token budget specified above.',\n );\n\n const userPrompt = promptParts.join('\\n');\n const promptTokens = estimateTokens(systemPrompt + userPrompt);\n this.log('debug', `Tier ${tier}: sending LLM request`, { promptTokens, maxTokens: tier, substrateBudget });\n\n try {\n const tierStart = Date.now();\n const digestConfig = this.config.digest.intelligence;\n const opts: LlmRequestOptions = {\n maxTokens: tier,\n timeoutMs: DIGEST_LLM_REQUEST_TIMEOUT_MS,\n contextLength: contextWindow,\n reasoning: LLM_REASONING_MODE,\n systemPrompt,\n keepAlive: digestConfig.keep_alive ?? undefined,\n };\n const response = await this.llm.summarize(userPrompt, opts);\n const tierDuration = Date.now() - tierStart;\n\n // Strip reasoning tokens if present (some models output chain-of-thought)\n const extractText = stripReasoningTokens(response.text);\n model = response.model;\n const responseTokens = estimateTokens(extractText);\n totalTokensUsed += promptTokens + responseTokens;\n\n this.log('info', `Tier ${tier}: completed`, { durationMs: tierDuration, responseTokens, model: response.model });\n this.writeExtract(tier, extractText, cycleId, response.model, substrate.length);\n tiersGenerated.push(tier);\n } catch (err) {\n this.log('warn', `Tier ${tier}: failed`, { error: (err as Error).message });\n }\n }\n\n const result: DigestCycleResult = {\n cycleId,\n timestamp: cycleTimestamp,\n substrate: substrateIndex,\n tiersGenerated,\n model,\n durationMs: Date.now() - startTime,\n tokensUsed: totalTokensUsed,\n };\n\n this.appendTrace(result);\n return result;\n }\n}\n\n// --- Metabolism (Adaptive Timer) ---\n\nexport type MetabolismState = 'active' | 'cooling' | 'dormant';\n\n/** Milliseconds per second for config conversion. */\nconst MS_PER_SECOND = 1000;\n\nexport class Metabolism {\n state: MetabolismState = 'active';\n currentIntervalMs: number;\n\n private cooldownStep = 0;\n private lastSubstrateTime: number;\n private timer: ReturnType<typeof setTimeout> | null = null;\n private activeIntervalMs: number;\n private cooldownIntervalsMs: number[];\n private dormancyThresholdMs: number;\n\n constructor(config: MycoConfig['digest']['metabolism']) {\n this.activeIntervalMs = config.active_interval * MS_PER_SECOND;\n this.cooldownIntervalsMs = config.cooldown_intervals.map((s) => s * MS_PER_SECOND);\n this.dormancyThresholdMs = config.dormancy_threshold * MS_PER_SECOND;\n this.currentIntervalMs = this.activeIntervalMs;\n this.lastSubstrateTime = Date.now();\n }\n\n /** Reset to active state when new substrate is found. */\n onSubstrateFound(): void {\n this.state = 'active';\n this.cooldownStep = 0;\n this.currentIntervalMs = this.activeIntervalMs;\n this.lastSubstrateTime = Date.now();\n }\n\n /** Advance cooldown when a cycle finds no new substrate. */\n onEmptyCycle(): void {\n if (this.state === 'dormant') return;\n\n this.state = 'cooling';\n if (this.cooldownStep < this.cooldownIntervalsMs.length) {\n this.currentIntervalMs = this.cooldownIntervalsMs[this.cooldownStep];\n this.cooldownStep++;\n }\n\n this.checkDormancy();\n }\n\n /** Enter dormant state if enough time has elapsed since last substrate. */\n checkDormancy(): void {\n const elapsed = Date.now() - this.lastSubstrateTime;\n if (elapsed >= this.dormancyThresholdMs) {\n this.state = 'dormant';\n // Keep the last cooldown interval as the dormant polling rate\n }\n }\n\n /** Return to active from any state, resetting timers and rescheduling immediately. */\n activate(): void {\n this.onSubstrateFound();\n // Reschedule with the new active interval — without this, the old\n // (possibly dormant) timer continues ticking at the wrong rate\n if (this.callback) {\n this.reschedule();\n }\n }\n\n /** Set lastSubstrateTime explicitly (for testing). */\n markLastSubstrate(time: number): void {\n this.lastSubstrateTime = time;\n }\n\n /** Begin scheduling digest cycles with adaptive intervals. */\n start(callback: () => Promise<void>): void {\n this.callback = callback;\n this.reschedule();\n }\n\n /** Stop the timer. */\n stop(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n private callback: (() => Promise<void>) | null = null;\n\n private reschedule(): void {\n this.stop();\n if (!this.callback) return;\n const cb = this.callback;\n const schedule = (): void => {\n this.timer = setTimeout(async () => {\n await cb();\n schedule();\n }, this.currentIntervalMs);\n this.timer.unref();\n };\n schedule();\n }\n}\n"],"mappings":"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","names":["YAML","opts"]}
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-MIU3DKLN.js";
5
5
  import {
6
6
  DIGEST_TIERS
7
- } from "./chunk-JBD5KP5G.js";
7
+ } from "./chunk-B6WVNDA5.js";
8
8
 
9
9
  // src/mcp/tools/context.ts
10
10
  import fs from "fs";
@@ -46,4 +46,4 @@ function handleMycoContext(vaultDir, input) {
46
46
  export {
47
47
  handleMycoContext
48
48
  };
49
- //# sourceMappingURL=chunk-WBT5DWGC.js.map
49
+ //# sourceMappingURL=chunk-IURC35BF.js.map
@@ -2,9 +2,8 @@ import { createRequire as __cr } from 'node:module'; const require = __cr(import
2
2
  import {
3
3
  DAEMON_CLIENT_TIMEOUT_MS,
4
4
  EMBEDDING_REQUEST_TIMEOUT_MS,
5
- LLM_REQUEST_TIMEOUT_MS,
6
- estimateTokens
7
- } from "./chunk-JBD5KP5G.js";
5
+ LLM_REQUEST_TIMEOUT_MS
6
+ } from "./chunk-B6WVNDA5.js";
8
7
 
9
8
  // src/intelligence/ollama.ts
10
9
  var ENDPOINT_GENERATE = "/api/generate";
@@ -15,27 +14,23 @@ var OllamaBackend = class _OllamaBackend {
15
14
  name = "ollama";
16
15
  baseUrl;
17
16
  model;
18
- contextWindow;
19
17
  defaultMaxTokens;
20
18
  constructor(config) {
21
19
  this.baseUrl = config?.base_url ?? _OllamaBackend.DEFAULT_BASE_URL;
22
20
  this.model = config?.model ?? config?.summary_model ?? "llama3.2";
23
- this.contextWindow = config?.context_window ?? 8192;
24
21
  this.defaultMaxTokens = config?.max_tokens ?? 1024;
25
22
  }
26
23
  async summarize(prompt, opts) {
27
24
  const maxTokens = opts?.maxTokens ?? this.defaultMaxTokens;
28
- const contextLength = opts?.contextLength ?? this.contextWindow;
29
- const promptTokens = estimateTokens(prompt);
30
- const numCtx = Math.max(promptTokens + maxTokens, contextLength);
25
+ const options = { num_predict: maxTokens };
26
+ if (opts?.contextLength) {
27
+ options.num_ctx = opts.contextLength;
28
+ }
31
29
  const body = {
32
30
  model: this.model,
33
31
  prompt,
34
32
  stream: false,
35
- options: {
36
- num_ctx: numCtx,
37
- num_predict: maxTokens
38
- }
33
+ options
39
34
  };
40
35
  if (opts?.systemPrompt) {
41
36
  body.system = opts.systemPrompt;
@@ -103,7 +98,6 @@ var OllamaBackend = class _OllamaBackend {
103
98
  // src/intelligence/lm-studio.ts
104
99
  var ENDPOINT_CHAT = "/api/v1/chat";
105
100
  var ENDPOINT_MODELS_LOAD = "/api/v1/models/load";
106
- var ENDPOINT_MODELS_UNLOAD = "/api/v1/models/unload";
107
101
  var ENDPOINT_MODELS_LIST = "/v1/models";
108
102
  var ENDPOINT_MODELS_NATIVE = "/api/v1/models";
109
103
  var ENDPOINT_EMBEDDINGS = "/v1/embeddings";
@@ -112,7 +106,7 @@ var LmStudioBackend = class _LmStudioBackend {
112
106
  name = "lm-studio";
113
107
  baseUrl;
114
108
  model;
115
- loadedInstanceId = null;
109
+ instanceId = null;
116
110
  contextWindow;
117
111
  defaultMaxTokens;
118
112
  constructor(config) {
@@ -123,21 +117,22 @@ var LmStudioBackend = class _LmStudioBackend {
123
117
  }
124
118
  /**
125
119
  * Generate text using LM Studio's native REST API (/api/v1/chat).
126
- * Supports per-request context_length, reasoning control, and system_prompt.
120
+ * Routes to our specific instance by ID when available, with model name +
121
+ * context_length as fallback. This ensures correct routing when multiple
122
+ * daemons share the same LM Studio, and graceful degradation when our
123
+ * instance is evicted by idle TTL.
127
124
  */
128
125
  async summarize(prompt, opts) {
129
126
  const maxTokens = opts?.maxTokens ?? this.defaultMaxTokens;
127
+ const contextLength = opts?.contextLength ?? this.contextWindow;
130
128
  const body = {
131
- model: this.loadedInstanceId ?? this.model,
129
+ model: this.instanceId ?? this.model,
132
130
  input: prompt,
133
131
  max_output_tokens: maxTokens,
134
132
  store: false
135
133
  };
136
- if (!this.loadedInstanceId) {
137
- const contextLength = opts?.contextLength ?? this.contextWindow;
138
- if (contextLength) {
139
- body.context_length = contextLength;
140
- }
134
+ if (contextLength) {
135
+ body.context_length = contextLength;
141
136
  }
142
137
  if (opts?.systemPrompt) {
143
138
  body.system_prompt = opts.systemPrompt;
@@ -153,6 +148,9 @@ var LmStudioBackend = class _LmStudioBackend {
153
148
  });
154
149
  if (!response.ok) {
155
150
  const errorBody = await response.text().catch(() => "");
151
+ if (response.status === 404 && this.instanceId) {
152
+ this.instanceId = null;
153
+ }
156
154
  throw new Error(`LM Studio summarize failed: ${response.status} ${errorBody.slice(0, 500)}`);
157
155
  }
158
156
  const data = await response.json();
@@ -183,9 +181,13 @@ var LmStudioBackend = class _LmStudioBackend {
183
181
  }
184
182
  /**
185
183
  * Ensure a model instance is loaded with the desired settings.
186
- * First checks for an existing compatible instance to reuse (prevents
187
- * accumulation across daemon restarts), then loads a new one only if needed.
188
- * Unloads incompatible instances of the same model to prevent resource exhaustion.
184
+ * Called every digest cycle (not cached) so it recovers from idle TTL eviction.
185
+ *
186
+ * The load API is necessary to control offload_kv_cache_to_gpu a load-time
187
+ * setting that cannot be set per-request via the chat API.
188
+ *
189
+ * Multi-daemon safe: finds or loads our own compatible instance without
190
+ * touching instances from other daemons/projects. Routes by instance ID.
189
191
  */
190
192
  async ensureLoaded(contextLength, gpuKvCache) {
191
193
  const ctx = contextLength ?? this.contextWindow;
@@ -195,12 +197,10 @@ var LmStudioBackend = class _LmStudioBackend {
195
197
  const matchesContext = !ctx || instance.config.context_length === ctx;
196
198
  const matchesKvCache = instance.config.offload_kv_cache_to_gpu === kvCache;
197
199
  if (matchesContext && matchesKvCache) {
198
- this.loadedInstanceId = instance.id;
199
- await this.unloadIncompatibleInstances(instances, ctx, kvCache);
200
+ this.instanceId = instance.id;
200
201
  return;
201
202
  }
202
203
  }
203
- await this.unloadIncompatibleInstances(instances, ctx, kvCache);
204
204
  const body = {
205
205
  model: this.model,
206
206
  flash_attention: true,
@@ -220,9 +220,9 @@ var LmStudioBackend = class _LmStudioBackend {
220
220
  throw new Error(`LM Studio model load failed: ${response.status} ${errorBody.slice(0, 200)}`);
221
221
  }
222
222
  const loadResult = await response.json();
223
- const instanceId = loadResult.id ?? loadResult.instance_id ?? loadResult.model_instance_id;
224
- if (instanceId) {
225
- this.loadedInstanceId = instanceId;
223
+ const id = loadResult.instance_id ?? loadResult.id ?? loadResult.model_instance_id;
224
+ if (id) {
225
+ this.instanceId = id;
226
226
  }
227
227
  }
228
228
  /**
@@ -242,27 +242,6 @@ var LmStudioBackend = class _LmStudioBackend {
242
242
  return [];
243
243
  }
244
244
  }
245
- /**
246
- * Unload instances of this model that don't match the desired settings.
247
- * Best-effort — failures are silently ignored to avoid blocking the load path.
248
- */
249
- async unloadIncompatibleInstances(instances, contextLength, gpuKvCache) {
250
- for (const instance of instances) {
251
- const matchesContext = !contextLength || instance.config.context_length === contextLength;
252
- const matchesKvCache = instance.config.offload_kv_cache_to_gpu === gpuKvCache;
253
- if (!matchesContext || !matchesKvCache) {
254
- try {
255
- await fetch(`${this.baseUrl}${ENDPOINT_MODELS_UNLOAD}`, {
256
- method: "POST",
257
- headers: { "Content-Type": "application/json" },
258
- body: JSON.stringify({ model: instance.id }),
259
- signal: AbortSignal.timeout(DAEMON_CLIENT_TIMEOUT_MS)
260
- });
261
- } catch {
262
- }
263
- }
264
- }
265
- }
266
245
  async isAvailable() {
267
246
  try {
268
247
  const response = await fetch(`${this.baseUrl}${ENDPOINT_MODELS_LIST}`, {
@@ -291,4 +270,4 @@ export {
291
270
  OllamaBackend,
292
271
  LmStudioBackend
293
272
  };
294
- //# sourceMappingURL=chunk-67R6EMYD.js.map
273
+ //# sourceMappingURL=chunk-JI6M2L2W.js.map