mlx-bun 0.0.4 → 0.0.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.
Files changed (90) hide show
  1. package/README.md +175 -93
  2. package/docs/dag/training-inference-map.html +575 -0
  3. package/docs/design/adapters-end-to-end.md +50 -46
  4. package/docs/design/compat-cli-surface-design.md +10 -8
  5. package/docs/design/docs-reorg-plan.md +14 -1
  6. package/docs/design/hlg-sampling.md +12 -2
  7. package/docs/design/memory-system.md +193 -295
  8. package/docs/design/optimization_plan.md +4 -2
  9. package/docs/design/orpo-dynamic-lambda.md +179 -0
  10. package/docs/design/orpo-future-enhancements.md +340 -0
  11. package/docs/design/orpo-training.md +1212 -0
  12. package/docs/design/parallel-slots.md +34 -26
  13. package/docs/design/parity-tier-dag.md +143 -0
  14. package/docs/design/runtime-isolation.md +1 -1
  15. package/docs/design/segmented-backward-training.md +58 -39
  16. package/docs/design/tauri-desktop-app.md +127 -0
  17. package/docs/design/w4a16-compute-precision-spike.md +232 -0
  18. package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
  19. package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
  20. package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
  21. package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
  22. package/docs/investigations/starter-model-port-handoff.md +32 -13
  23. package/docs/investigations/steel-flash-cce-handoff.md +489 -0
  24. package/docs/investigations/trainer-validation-experiment.md +173 -0
  25. package/docs/planning/IDEAS.md +29 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +147 -27
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/journal.md +161 -0
  29. package/docs/reference/distribution.md +61 -5
  30. package/docs/reference/library-api.md +29 -14
  31. package/docs/reference/memory.md +77 -0
  32. package/docs/reference/orpo-quickstart.md +130 -0
  33. package/docs/reference/server-api.md +105 -7
  34. package/docs/reference/server-config.md +12 -5
  35. package/docs/reference/training.md +296 -0
  36. package/package.json +1 -1
  37. package/src/cli.ts +651 -18
  38. package/src/eval/ifeval.ts +138 -0
  39. package/src/eval/runner.ts +76 -6
  40. package/src/eval/tasks/bfcl.ts +49 -15
  41. package/src/eval/tasks/gsm8k.ts +21 -9
  42. package/src/eval/tasks/hashhop.ts +33 -4
  43. package/src/eval/tasks/humaneval.ts +30 -8
  44. package/src/eval/tasks/ifeval.ts +52 -18
  45. package/src/eval/tasks/mmlu.ts +85 -74
  46. package/src/eval/ultrafeedback.ts +60 -0
  47. package/src/jobs/runner.ts +6 -0
  48. package/src/lora.ts +12 -7
  49. package/src/memory/chunk.ts +25 -0
  50. package/src/memory/cluster.ts +24 -0
  51. package/src/memory/db.ts +114 -0
  52. package/src/memory/ingest.ts +22 -0
  53. package/src/memory/pipeline.ts +63 -0
  54. package/src/memory/prompts.ts +23 -0
  55. package/src/memory/schedule.ts +163 -0
  56. package/src/memory/synthesize.ts +29 -0
  57. package/src/memory/tools.ts +399 -0
  58. package/src/memory/vault.ts +694 -0
  59. package/src/memory/wikify.ts +23 -0
  60. package/src/mlx/metal-kernel.ts +31 -4
  61. package/src/model/flash-attention.ts +37 -19
  62. package/src/model/fused-geglu-kernel.ts +111 -0
  63. package/src/model/gemma4-base.ts +39 -1
  64. package/src/model/gemma4.ts +196 -21
  65. package/src/model/minicpm5.ts +44 -3
  66. package/src/pi-provider.ts +29 -13
  67. package/src/pi-session.ts +81 -0
  68. package/src/pi-terminal.ts +13 -24
  69. package/src/pi-web.ts +246 -166
  70. package/src/quantize/job.ts +17 -2
  71. package/src/quantize/quantizer.ts +26 -2
  72. package/src/server.ts +216 -47
  73. package/src/tokenizer.ts +6 -2
  74. package/src/train/dataset.ts +29 -6
  75. package/src/train/flash-cce.ts +1175 -0
  76. package/src/train/job.ts +64 -3
  77. package/src/train/lora-params.ts +53 -4
  78. package/src/train/loss.ts +947 -0
  79. package/src/train/optimizer.ts +10 -3
  80. package/src/train/prefix-shared.ts +435 -0
  81. package/src/train/rank.ts +36 -0
  82. package/src/train/segmented.ts +1148 -3
  83. package/src/train/steel-qmm-header.ts +1461 -0
  84. package/src/train/trainer.ts +570 -54
  85. package/src/train/watch.ts +542 -0
  86. package/src/web/app.html +285 -19
  87. package/src/web/skills/memory/SKILL.md +97 -0
  88. package/src/web/skills/web-research/SKILL.md +3 -1
  89. package/src/web/skills.ts +46 -8
  90. /package/{docs → src/assets}/curve-designer.html +0 -0
@@ -0,0 +1,399 @@
1
+ // mlx-bun memory — the read tools the local assistant uses to consult your wiki.
2
+ //
3
+ // These are pi custom tools (defineTool), registered alongside the coding +
4
+ // web tools in both front doors (pi-terminal.ts, pi-web.ts). They're read-only,
5
+ // so they auto-allow (not in GATED_TOOLS) — consulting your own memory never
6
+ // needs an approval gate. They read ~/.mlx-bun/wiki via src/memory/vault.ts.
7
+ //
8
+ // Mirrors lucien's read surface: search (discovery) → focused read (toc /
9
+ // section / links) → full read / list / status. Knowing *when* to consult is
10
+ // driven by these tool descriptions and the bundled memory skill (pi surfaces
11
+ // its description and loads the body on demand); the system prompt carries only
12
+ // a one-line presence hint (memoryIndexHint), not a title dump.
13
+
14
+ import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
15
+ import { Type } from "typebox";
16
+ import { scheduleStatus } from "./schedule";
17
+ import {
18
+ extractSection,
19
+ getArticleLinks,
20
+ listArticles,
21
+ listReferenceDocs,
22
+ parseToc,
23
+ readArticle,
24
+ searchArticles,
25
+ vaultRoot,
26
+ vaultStatus,
27
+ type SearchHit,
28
+ } from "./vault";
29
+
30
+ /** Tool names, exported so the front doors can add them to the allowlist. */
31
+ export const MEMORY_TOOL_NAMES = [
32
+ "memory_search",
33
+ "memory_read",
34
+ "memory_toc",
35
+ "memory_section",
36
+ "memory_links",
37
+ "memory_list",
38
+ "memory_status",
39
+ ] as const;
40
+
41
+ export const REFERENCE_TOOL_NAMES = [
42
+ "reference_search",
43
+ "reference_read",
44
+ "reference_list",
45
+ ] as const;
46
+
47
+ const MAX_SEARCH_HITS = 30;
48
+
49
+ function textResult(text: string): { content: [{ type: "text"; text: string }]; details: Record<string, never> } {
50
+ return { content: [{ type: "text", text }], details: {} };
51
+ }
52
+
53
+ export async function isMemoryEnabled(): Promise<boolean> {
54
+ try {
55
+ return (await vaultStatus()).exists;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ function isReferenceId(article: string): boolean {
62
+ return article.trim().startsWith("Reference/");
63
+ }
64
+
65
+ async function memoryUnavailableMessage(): Promise<string | null> {
66
+ const st = await vaultStatus();
67
+ if (st.exists) return null;
68
+ return `Memory is not set up yet at ${st.root}. Run \`mlx-bun memory init\` to create the local Markdown wiki, or ask the user before setting it up.`;
69
+ }
70
+
71
+ /** Compact, model-friendly rendering of search hits grouped by relevance. */
72
+ function formatSearch(
73
+ query: string,
74
+ summaries: { article: string; occurrences: number; matched_terms?: string[] }[],
75
+ hits: SearchHit[],
76
+ opts: { corpusLabel?: string; readTool?: string } = {},
77
+ ): string {
78
+ const corpus = opts.corpusLabel ?? "personal-memory articles";
79
+ const readTool = opts.readTool ?? "memory_read";
80
+ if (summaries.length === 0) return `No ${corpus} match "${query}".`;
81
+ const top = summaries.slice(0, 12);
82
+ const lines = [`${corpus[0]!.toUpperCase()}${corpus.slice(1)} matching "${query}" (most relevant first):`, ""];
83
+ for (const s of top) {
84
+ const terms = s.matched_terms && s.matched_terms.length ? ` [${s.matched_terms.join(", ")}]` : "";
85
+ lines.push(`- ${s.article} (${s.occurrences} hit${s.occurrences === 1 ? "" : "s"})${terms}`);
86
+ }
87
+ if (summaries.length > top.length) lines.push(` …and ${summaries.length - top.length} more.`);
88
+ if (hits.length) {
89
+ lines.push("", "Sample lines:");
90
+ for (const h of hits.slice(0, 8)) {
91
+ const where = h.anchor ? `${h.article}#${h.anchor}` : h.article;
92
+ lines.push(`- ${where}:${h.line} ${h.excerpt}`);
93
+ }
94
+ }
95
+ lines.push("", `Read a full result with ${readTool} (e.g. ${readTool} article="${top[0]!.article}").`);
96
+ return lines.join("\n");
97
+ }
98
+
99
+ const memorySearchTool = defineTool({
100
+ name: "memory_search",
101
+ label: "Search Memory",
102
+ description:
103
+ "Search the user's local personal-memory articles for user-specific continuity: prior conversations, named personal projects, people, preferences, decisions, or history. " +
104
+ "Use when the answer depends on missing personal context; do not use for weather, current public facts, generic web research, or ordinary coding/file tasks when current files are sufficient. " +
105
+ "Returns ranked article matches and sample lines; follow up with memory_read to read a relevant article in full.",
106
+ parameters: Type.Object({
107
+ query: Type.String({ description: "Word or phrase to search for across the user's memory articles." }),
108
+ limit: Type.Optional(Type.Number({ description: `Max sample line-hits to return (default ${MAX_SEARCH_HITS}).` })),
109
+ }),
110
+ execute: async (_id, params) => {
111
+ const query = params.query.trim();
112
+ if (!query) return textResult("memory_search needs a non-empty query.");
113
+ const limit = Math.max(1, Math.min(MAX_SEARCH_HITS, Math.round(params.limit ?? MAX_SEARCH_HITS)));
114
+ try {
115
+ const unavailable = await memoryUnavailableMessage();
116
+ if (unavailable) return textResult(unavailable);
117
+ const { summaries, hits } = await searchArticles(vaultRoot(), query, { limit, scope: "articles" });
118
+ return textResult(formatSearch(query, summaries, hits));
119
+ } catch (err) {
120
+ return textResult(`memory_search failed: ${err instanceof Error ? err.message : String(err)}`);
121
+ }
122
+ },
123
+ });
124
+
125
+ const memoryReadTool = defineTool({
126
+ name: "memory_read",
127
+ label: "Read Memory",
128
+ description:
129
+ "Read one full Markdown article from the user's personal memory. " +
130
+ "Use after memory_search when user-specific prior context is needed. " +
131
+ "Pass the article stem exactly as returned by memory_search or memory_list (underscored, e.g. \"Archie_Project\").",
132
+ parameters: Type.Object({
133
+ article: Type.String({ description: "Article stem to read (underscored filename, no .md, e.g. \"Archie_Project\")." }),
134
+ }),
135
+ execute: async (_id, params) => {
136
+ const article = params.article.trim();
137
+ if (!article) return textResult("memory_read needs an article name.");
138
+ try {
139
+ const unavailable = await memoryUnavailableMessage();
140
+ if (unavailable) return textResult(unavailable);
141
+ if (isReferenceId(article)) return textResult("memory_read is for personal-memory articles. Use reference_read for Reference/ documents.");
142
+ const { content } = await readArticle(vaultRoot(), article);
143
+ return textResult(content);
144
+ } catch (err) {
145
+ return textResult(`${err instanceof Error ? err.message : String(err)} — try memory_search to find the right article name.`);
146
+ }
147
+ },
148
+ });
149
+
150
+ const memoryTocTool = defineTool({
151
+ name: "memory_toc",
152
+ label: "Memory TOC",
153
+ description:
154
+ "List the Markdown headings in one memory article, with anchors. Use before memory_section when the article is long or you need a specific part.",
155
+ parameters: Type.Object({
156
+ article: Type.String({ description: "Article stem (underscored filename, no .md, e.g. \"Archie_Project\")." }),
157
+ }),
158
+ execute: async (_id, params) => {
159
+ const article = params.article.trim();
160
+ if (!article) return textResult("memory_toc needs an article name.");
161
+ try {
162
+ const unavailable = await memoryUnavailableMessage();
163
+ if (unavailable) return textResult(unavailable);
164
+ if (isReferenceId(article)) return textResult("memory_toc is for personal-memory articles. Use reference_read for Reference/ documents.");
165
+ const { content } = await readArticle(vaultRoot(), article);
166
+ const toc = parseToc(content);
167
+ if (toc.length === 0) return textResult(`${article} has no Markdown headings.`);
168
+ return textResult(
169
+ `Headings in ${article}:\n\n` +
170
+ toc.map((h) => `${" ".repeat(Math.max(0, h.depth - 1))}- ${h.title} (#${h.anchor})`).join("\n"),
171
+ );
172
+ } catch (err) {
173
+ return textResult(`${err instanceof Error ? err.message : String(err)} — try memory_search to find the right article name.`);
174
+ }
175
+ },
176
+ });
177
+
178
+ const memorySectionTool = defineTool({
179
+ name: "memory_section",
180
+ label: "Read Memory Section",
181
+ description:
182
+ "Read one section from a memory article by heading anchor. Use memory_toc or memory_search first to find anchors; this is cheaper than reading a long article in full.",
183
+ parameters: Type.Object({
184
+ article: Type.String({ description: "Article stem (underscored filename, no .md, e.g. \"Archie_Project\")." }),
185
+ anchor: Type.String({ description: "Heading anchor, e.g. \"origin\" or \"current-design\"." }),
186
+ }),
187
+ execute: async (_id, params) => {
188
+ const article = params.article.trim();
189
+ const anchor = params.anchor.trim().replace(/^#/, "");
190
+ if (!article) return textResult("memory_section needs an article name.");
191
+ if (!anchor) return textResult("memory_section needs a heading anchor.");
192
+ try {
193
+ const unavailable = await memoryUnavailableMessage();
194
+ if (unavailable) return textResult(unavailable);
195
+ if (isReferenceId(article)) return textResult("memory_section is for personal-memory articles. Use reference_read for Reference/ documents.");
196
+ const { content } = await readArticle(vaultRoot(), article);
197
+ const section = extractSection(content, anchor);
198
+ if (!section) return textResult(`No section #${anchor} found in ${article}. Try memory_toc article=\"${article}\".`);
199
+ return textResult(section);
200
+ } catch (err) {
201
+ return textResult(`${err instanceof Error ? err.message : String(err)} — try memory_search to find the right article name.`);
202
+ }
203
+ },
204
+ });
205
+
206
+ const memoryLinksTool = defineTool({
207
+ name: "memory_links",
208
+ label: "Memory Links",
209
+ description:
210
+ "Show resolved outgoing and incoming wikilinks for one memory article. Use when nearby concepts/backlinks might matter to the answer.",
211
+ parameters: Type.Object({
212
+ article: Type.String({ description: "Article stem (underscored filename, no .md, e.g. \"Archie_Project\")." }),
213
+ }),
214
+ execute: async (_id, params) => {
215
+ const article = params.article.trim();
216
+ if (!article) return textResult("memory_links needs an article name.");
217
+ try {
218
+ const unavailable = await memoryUnavailableMessage();
219
+ if (unavailable) return textResult(unavailable);
220
+ if (isReferenceId(article)) return textResult("memory_links is for personal-memory articles; Reference/ documents do not participate in the personal-memory link graph.");
221
+ const { outbound, inbound } = await getArticleLinks(vaultRoot(), article);
222
+ const lines = [`Links for ${article}:`, "", "Outbound:"];
223
+ lines.push(outbound.length ? outbound.map((s) => `- ${s}`).join("\n") : "- (none)");
224
+ lines.push("", "Inbound:");
225
+ lines.push(inbound.length ? inbound.map((s) => `- ${s}`).join("\n") : "- (none)");
226
+ return textResult(lines.join("\n"));
227
+ } catch (err) {
228
+ return textResult(`${err instanceof Error ? err.message : String(err)} — try memory_search to find the right article name.`);
229
+ }
230
+ },
231
+ });
232
+
233
+ const memoryListTool = defineTool({
234
+ name: "memory_list",
235
+ label: "List Memory",
236
+ description:
237
+ "List the titles of the user's personal-memory articles. Use as an overview, or as a fallback when memory_search doesn't surface what you expected.",
238
+ parameters: Type.Object({}),
239
+ execute: async () => {
240
+ try {
241
+ const unavailable = await memoryUnavailableMessage();
242
+ if (unavailable) return textResult(unavailable);
243
+ const articles = await listArticles(vaultRoot());
244
+ if (articles.length === 0) return textResult("The user's memory has no personal articles yet.");
245
+ return textResult(
246
+ `${articles.length} personal-memory article(s):\n\n` +
247
+ articles.map((s) => `- ${s}`).join("\n"),
248
+ );
249
+ } catch (err) {
250
+ return textResult(`memory_list failed: ${err instanceof Error ? err.message : String(err)}`);
251
+ }
252
+ },
253
+ });
254
+
255
+ const memoryStatusTool = defineTool({
256
+ name: "memory_status",
257
+ label: "Memory Status",
258
+ description:
259
+ "Show read-only status for the user's memory: vault path, setup state, article count, git state, and nightly schedule state. Use when the user asks whether memory is on or where it lives.",
260
+ parameters: Type.Object({}),
261
+ execute: async () => {
262
+ try {
263
+ const st = await vaultStatus();
264
+ const sched = await scheduleStatus();
265
+ const recent = st.recentArticles.length
266
+ ? st.recentArticles.slice(0, 5).map((r) => ` - ${r.article} (${new Date(r.mtimeMs).toISOString()})`).join("\n")
267
+ : " - (none)";
268
+ return textResult([
269
+ "Memory status:",
270
+ `- vault: ${st.root}`,
271
+ `- setup: ${st.exists ? "yes" : "no"}`,
272
+ `- articles: ${st.articleCount}`,
273
+ `- read-only references: ${st.referenceCount}`,
274
+ `- git: ${st.isGitRepo ? "tracked" : "not tracked"}`,
275
+ `- last synthesis: not available yet (M1 synthesis is stubbed)`,
276
+ `- synthesis: M1 stub (manual/nightly writes are not implemented yet)`,
277
+ `- nightly: ${sched.installed ? (sched.loaded ? "scheduled" : "installed but not loaded") : "not scheduled"}`,
278
+ sched.installed ? `- launchd plist: ${sched.plistPath}` : "- setup command: mlx-bun memory init",
279
+ "- recent changed articles:",
280
+ recent,
281
+ ].join("\n"));
282
+ } catch (err) {
283
+ return textResult(`memory_status failed: ${err instanceof Error ? err.message : String(err)}`);
284
+ }
285
+ },
286
+ });
287
+
288
+ // ---- reference docs (separate from personal memory) -------------------
289
+
290
+ const referenceSearchTool = defineTool({
291
+ name: "reference_search",
292
+ label: "Search mlx-bun Reference",
293
+ description:
294
+ "Search read-only mlx-bun reference documents that are symlinked into the memory vault. " +
295
+ "Use for mlx-bun documentation questions when repository files are not enough. This is project reference, not personal memory.",
296
+ parameters: Type.Object({
297
+ query: Type.String({ description: "Word or phrase to search for across mlx-bun reference documents." }),
298
+ limit: Type.Optional(Type.Number({ description: `Max sample line-hits to return (default ${MAX_SEARCH_HITS}).` })),
299
+ }),
300
+ execute: async (_id, params) => {
301
+ const query = params.query.trim();
302
+ if (!query) return textResult("reference_search needs a non-empty query.");
303
+ const limit = Math.max(1, Math.min(MAX_SEARCH_HITS, Math.round(params.limit ?? MAX_SEARCH_HITS)));
304
+ try {
305
+ const unavailable = await memoryUnavailableMessage();
306
+ if (unavailable) return textResult(unavailable);
307
+ const { summaries, hits } = await searchArticles(vaultRoot(), query, { limit, scope: "reference" });
308
+ return textResult(formatSearch(query, summaries, hits, { corpusLabel: "read-only reference documents", readTool: "reference_read" }));
309
+ } catch (err) {
310
+ return textResult(`reference_search failed: ${err instanceof Error ? err.message : String(err)}`);
311
+ }
312
+ },
313
+ });
314
+
315
+ const referenceReadTool = defineTool({
316
+ name: "reference_read",
317
+ label: "Read mlx-bun Reference",
318
+ description:
319
+ "Read one read-only mlx-bun reference document from the vault's Reference/ namespace. " +
320
+ "Pass the id exactly as returned by reference_search or reference_list, e.g. \"Reference/mlx-bun_Server_API\".",
321
+ parameters: Type.Object({
322
+ article: Type.String({ description: "Reference doc id, e.g. \"Reference/mlx-bun_Server_API\"." }),
323
+ }),
324
+ execute: async (_id, params) => {
325
+ const article = params.article.trim();
326
+ if (!article) return textResult("reference_read needs a Reference/... document id.");
327
+ try {
328
+ const unavailable = await memoryUnavailableMessage();
329
+ if (unavailable) return textResult(unavailable);
330
+ const id = article.startsWith("Reference/") ? article : `Reference/${article}`;
331
+ const { content } = await readArticle(vaultRoot(), id);
332
+ return textResult(content);
333
+ } catch (err) {
334
+ return textResult(`${err instanceof Error ? err.message : String(err)} — try reference_search to find the right reference document.`);
335
+ }
336
+ },
337
+ });
338
+
339
+ const referenceListTool = defineTool({
340
+ name: "reference_list",
341
+ label: "List mlx-bun References",
342
+ description: "List read-only mlx-bun reference documents available under the vault's Reference/ namespace.",
343
+ parameters: Type.Object({}),
344
+ execute: async () => {
345
+ try {
346
+ const unavailable = await memoryUnavailableMessage();
347
+ if (unavailable) return textResult(unavailable);
348
+ const refs = await listReferenceDocs(vaultRoot());
349
+ if (refs.length === 0) return textResult("No read-only reference documents are linked in the memory vault.");
350
+ return textResult(`${refs.length} read-only reference document(s):\n\n` + refs.map((s) => `- ${s}`).join("\n"));
351
+ } catch (err) {
352
+ return textResult(`reference_list failed: ${err instanceof Error ? err.message : String(err)}`);
353
+ }
354
+ },
355
+ });
356
+
357
+ export function createMemoryTools(): ToolDefinition[] {
358
+ return [
359
+ memorySearchTool,
360
+ memoryReadTool,
361
+ memoryTocTool,
362
+ memorySectionTool,
363
+ memoryLinksTool,
364
+ memoryListTool,
365
+ memoryStatusTool,
366
+ ];
367
+ }
368
+
369
+ export function createReferenceTools(): ToolDefinition[] {
370
+ return [referenceSearchTool, referenceReadTool, referenceListTool];
371
+ }
372
+
373
+ /**
374
+ * One gated line for the system prompt: states that memory is ON for this user
375
+ * and points at the `memory` skill. NOT a title dump and NOT a how-to. pi
376
+ * already auto-injects every skill's name+description+location
377
+ * (formatSkillsForPrompt) and loads the SKILL.md body on demand, so the skill
378
+ * owns the detailed workflow. This hint is intentionally soft: memory is a
379
+ * scoped retrieval capability for user-specific continuity, not a default first
380
+ * step for every prompt. Returns "" when memory is not turned on (vault absent)
381
+ * so we never advertise memory the user hasn't enabled. Runs once at session
382
+ * construction.
383
+ */
384
+ export async function memoryIndexHint(): Promise<string> {
385
+ let st: Awaited<ReturnType<typeof vaultStatus>>;
386
+ try {
387
+ st = await vaultStatus();
388
+ } catch {
389
+ return "";
390
+ }
391
+ if (!st.exists) return ""; // memory not turned on → don't advertise it
392
+ const articleCount = st.articleCount > 0 ? `${st.articleCount} article${st.articleCount === 1 ? "" : "s"}` : "no user articles yet";
393
+ const refCount = st.referenceCount > 0 ? ` plus ${st.referenceCount} read-only mlx-bun reference doc${st.referenceCount === 1 ? "" : "s"}` : "";
394
+ return (
395
+ `\n\nThe user's personal memory is on (${articleCount}${refCount}). Use it only when the answer depends on ` +
396
+ `user-specific continuity or missing personal context; do not use it for weather, current public facts, ` +
397
+ `generic web research, or ordinary coding/file tasks when the current files are enough. See the \`memory\` skill for the memory_* workflow.`
398
+ );
399
+ }