tau-all-agent 0.1.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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/telegram/README.md +81 -0
  19. package/extensions/telegram/daemon.mjs +1942 -0
  20. package/extensions/telegram/index.ts +1073 -0
  21. package/extensions/telegram/message-text.d.mts +1 -0
  22. package/extensions/telegram/message-text.mjs +16 -0
  23. package/extensions/usage/anthropic.ts +198 -0
  24. package/extensions/usage/github-copilot.ts +204 -0
  25. package/extensions/usage/google-gemini-cli.ts +232 -0
  26. package/extensions/usage/index.ts +2310 -0
  27. package/extensions/usage/minimax.ts +208 -0
  28. package/extensions/usage/openai-codex.ts +180 -0
  29. package/extensions/usage/openrouter.ts +168 -0
  30. package/extensions/usage/providers.ts +26 -0
  31. package/extensions/usage/shared.ts +156 -0
  32. package/extensions/usage/types.ts +53 -0
  33. package/extensions/usage/zai.ts +186 -0
  34. package/extensions/websearch/README.md +68 -0
  35. package/extensions/websearch/browser/chromium.ts +330 -0
  36. package/extensions/websearch/browser/discovery.ts +75 -0
  37. package/extensions/websearch/browser/firefox.ts +150 -0
  38. package/extensions/websearch/browser/sqlite.ts +38 -0
  39. package/extensions/websearch/config.ts +76 -0
  40. package/extensions/websearch/index.ts +312 -0
  41. package/extensions/websearch/normalize.ts +155 -0
  42. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  43. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  44. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  45. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  46. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  47. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  48. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  49. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  50. package/extensions/websearch/providers/shared.ts +76 -0
  51. package/extensions/websearch/types.ts +52 -0
  52. package/extensions/worktree.ts +2223 -0
  53. package/package.json +76 -0
  54. package/skills/browser-tools/SKILL.md +253 -0
  55. package/skills/browser-tools/scripts/browser-content.js +100 -0
  56. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  57. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  58. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  59. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  60. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  61. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  62. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  63. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  64. package/skills/browser-tools/scripts/browser-start.js +281 -0
  65. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  66. package/skills/browser-tools/scripts/utils.js +60 -0
  67. package/skills/git-clean-history/SKILL.md +45 -0
  68. package/skills/git-commit/SKILL.md +55 -0
  69. package/skills/homeassistant-ops/SKILL.md +184 -0
  70. package/skills/homeassistant-ops/references/api.md +64 -0
  71. package/skills/homeassistant-ops/references/id_conventions.md +71 -0
  72. package/skills/homeassistant-ops/references/playbook.md +150 -0
  73. package/skills/homeassistant-ops/scripts/ha_ops.js +2942 -0
  74. package/skills/openscad/SKILL.md +149 -0
  75. package/skills/openscad/examples/parametric_box.scad +92 -0
  76. package/skills/openscad/examples/phone_stand.scad +95 -0
  77. package/skills/openscad/scripts/common.sh +49 -0
  78. package/skills/openscad/scripts/export-stl.sh +56 -0
  79. package/skills/openscad/scripts/extract-params.sh +147 -0
  80. package/skills/openscad/scripts/multi-preview.sh +68 -0
  81. package/skills/openscad/scripts/preview.sh +74 -0
  82. package/skills/openscad/scripts/render-with-params.sh +91 -0
  83. package/skills/openscad/scripts/validate.sh +46 -0
  84. package/skills/oracle/SKILL.md +64 -0
  85. package/skills/oracle/scripts/oracle +249 -0
  86. package/skills/oracle/scripts/oracle-bundle +227 -0
  87. package/skills/sentry/SKILL.md +198 -0
  88. package/skills/sentry/lib/auth.js +99 -0
  89. package/skills/sentry/scripts/fetch-event.js +325 -0
  90. package/skills/sentry/scripts/fetch-issue.js +368 -0
  91. package/skills/sentry/scripts/list-issues.js +245 -0
  92. package/skills/sentry/scripts/search-events.js +291 -0
  93. package/skills/sentry/scripts/search-logs.js +234 -0
  94. package/skills/update-changelog/SKILL.md +135 -0
  95. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,1889 @@
1
+ import { complete, StringEnum, type Api, type Model, type UserMessage } from "@mariozechner/pi-ai";
2
+ import {
3
+ defineTool,
4
+ withFileMutationQueue,
5
+ type ExtensionAPI,
6
+ type ExtensionCommandContext,
7
+ type ExtensionContext,
8
+ } from "@mariozechner/pi-coding-agent";
9
+ import { Type } from "typebox";
10
+ import fs from "node:fs/promises";
11
+ import path from "node:path";
12
+
13
+ // --- Constants and schemas ---
14
+
15
+ const MEMORY_DIR_NAME = ".agents/memory";
16
+ const CORE_BLOCK_NAMES = ["directives", "context", "focus", "pending"] as const;
17
+ const LOG_TYPES = ["decision", "prompt", "plan", "experiment"] as const;
18
+ const IMPORTANCE_LEVELS = ["high", "medium", "low"] as const;
19
+
20
+ // Keep core small and readable. The line cap is the primary structure constraint.
21
+ // The character cap is a secondary backstop against packing too much dense text into too few lines.
22
+ const CORE_LINE_CAP = 300;
23
+ const CORE_CHAR_CAP = 20_000;
24
+ const AUTO_DREAM_MIN_UNDREAMED_LOGS = 8;
25
+ const AUTO_DREAM_MAX_AGE_MS = 12 * 60 * 60 * 1000;
26
+ const AUTO_DREAM_CORE_LINE_THRESHOLD = 240;
27
+ const AUTO_DREAM_CORE_CHAR_THRESHOLD = 16_000;
28
+ const MAX_PENDING_COMPACTIONS = 8;
29
+ const DEFAULT_LOG_IMPORTANCE: ImportanceLevel = "medium";
30
+
31
+ const MEMORY_UPDATE_BLOCK_PARAMS = Type.Object({
32
+ name: StringEnum(CORE_BLOCK_NAMES, { description: "Core block name" }),
33
+ content: Type.String({ description: "Markdown content for the core block" }),
34
+ });
35
+
36
+ const MEMORY_APPEND_LOG_PARAMS = Type.Object({
37
+ type: StringEnum(LOG_TYPES, { description: "Log entry type" }),
38
+ title: Type.String({ minLength: 1, description: "Short log entry title" }),
39
+ body: Type.String({ description: "Markdown log entry body" }),
40
+ importance: Type.Optional(StringEnum(IMPORTANCE_LEVELS, { description: "Importance label" })),
41
+ supersedes: Type.Optional(
42
+ Type.Array(
43
+ Type.String({ description: "Prior memory references superseded by this log entry" }),
44
+ ),
45
+ ),
46
+ invalidates: Type.Optional(
47
+ Type.Array(Type.String({ description: "Prior assumptions invalidated by this log entry" })),
48
+ ),
49
+ });
50
+
51
+ const MEMORY_DREAM_PARAMS = Type.Object({
52
+ reason: Type.Optional(Type.String({ description: "Why consolidation is needed" })),
53
+ });
54
+
55
+ const MEMORY_DREAM_SYSTEM_PROMPT = `You consolidate repo-local memory for a Pi extension.
56
+
57
+ Return valid JSON only. No markdown fences. No prose outside JSON.
58
+
59
+ Schema:
60
+ {
61
+ "blocks": {
62
+ "directives": "...",
63
+ "context": "...",
64
+ "focus": "...",
65
+ "pending": "..."
66
+ },
67
+ "summary": "..."
68
+ }
69
+
70
+ Rules:
71
+ - Update only the four core blocks.
72
+ - Keep the combined total across all returned blocks at or below ${CORE_LINE_CAP} lines and ${CORE_CHAR_CAP} characters.
73
+ - Enforce both caps only in your output, not by dropping input context from the read phase.
74
+ - Consolidate only from the provided core, undreamed logs, and compaction notes. Do not retrieve or invent older log entries.
75
+ - directives: stable rules, preferences, and standing constraints.
76
+ - context: stable architecture, behavior, important decisions, and lessons from failed or rejected attempts.
77
+ - focus: the current objective and immediate next steps.
78
+ - pending: unresolved follow-ups, open questions, blocked work.
79
+ - Use explicit invalidates/supersedes links when resolving contradictions.
80
+ - Preserve unresolved pending items. Do not clear pending unless the summary explicitly says they were resolved or abandoned.
81
+ - Keep every block short, concrete, and high-signal.
82
+ - Self-check that JSON.parse(output) succeeds before responding.`;
83
+
84
+ // --- Types and errors ---
85
+
86
+ type MemoryBlockName = (typeof CORE_BLOCK_NAMES)[number];
87
+ type LogType = (typeof LOG_TYPES)[number];
88
+ type ImportanceLevel = (typeof IMPORTANCE_LEVELS)[number];
89
+
90
+ type CoreBlocks = Record<MemoryBlockName, string>;
91
+
92
+ type LogEntry = {
93
+ timestamp: string;
94
+ type: LogType;
95
+ importance: ImportanceLevel;
96
+ title: string;
97
+ body: string;
98
+ supersedes: string[];
99
+ invalidates: string[];
100
+ };
101
+
102
+ type PendingCompaction = {
103
+ timestamp: string;
104
+ summary: string;
105
+ };
106
+
107
+ type MemoryState = {
108
+ // Newest timestamp appended to log.md.
109
+ lastLogAt: string | null;
110
+ // Timestamp of the most recent successful dream run.
111
+ lastDreamAt: string | null;
112
+ // Next dream replays log.md entries newer than this timestamp.
113
+ lastDreamedLogAt: string | null;
114
+ // session_compact summaries waiting to be folded into core.
115
+ pendingCompactions: PendingCompaction[];
116
+ };
117
+
118
+ type MemoryStateFile = {
119
+ last_log_at: string | null;
120
+ last_dream_at: string | null;
121
+ last_dreamed_log_at: string | null;
122
+ pending_compactions: PendingCompaction[];
123
+ };
124
+
125
+ type CoreReadResult = {
126
+ exists: boolean;
127
+ blocks: CoreBlocks;
128
+ missing: MemoryBlockName[];
129
+ totalLines: number;
130
+ totalChars: number;
131
+ };
132
+
133
+ type MemoryStatus = {
134
+ initialized: boolean;
135
+ coreExists: boolean;
136
+ coreLines: number;
137
+ coreChars: number;
138
+ missingCoreFiles: MemoryBlockName[];
139
+ lastLogAt: string | null;
140
+ lastDreamAt: string | null;
141
+ lastDreamedLogAt: string | null;
142
+ undreamedLogs: number;
143
+ pendingCompactions: number;
144
+ researchFiles: number;
145
+ hasLog: boolean;
146
+ };
147
+
148
+ type DreamReplay = {
149
+ readme: string;
150
+ blocks: CoreBlocks;
151
+ totalLines: number;
152
+ totalChars: number;
153
+ undreamedEntries: LogEntry[];
154
+ pendingCompactions: PendingCompaction[];
155
+ researchFiles: string[];
156
+ };
157
+
158
+ type DreamOutput = {
159
+ blocks: CoreBlocks;
160
+ summary: string;
161
+ };
162
+
163
+ type DreamResult = {
164
+ summary: string;
165
+ updatedBlocks: MemoryBlockName[];
166
+ consumedLogs: number;
167
+ consumedCompactions: number;
168
+ lastDreamAt: string;
169
+ lastDreamedLogAt: string | null;
170
+ summaryPath: string | null;
171
+ };
172
+
173
+ type AutoDreamStatus = {
174
+ coreLines: number;
175
+ coreChars: number;
176
+ undreamedLogs: number;
177
+ oldestUndreamedAt: string | null;
178
+ pendingCompactions: number;
179
+ };
180
+
181
+ type AutoDreamTrigger = "session_start" | "session_compact";
182
+
183
+ type AutoDreamState = {
184
+ promise: Promise<void> | null;
185
+ };
186
+
187
+ type ModelSelection = {
188
+ model: Model<Api>;
189
+ apiKey?: string;
190
+ headers?: Record<string, string>;
191
+ };
192
+
193
+ class MemoryReadmeMissingError extends Error {
194
+ constructor(readmePath: string) {
195
+ super(`${readmePath} is missing. Run /memory init to recreate it or restore it from git.`);
196
+ this.name = "MemoryReadmeMissingError";
197
+ }
198
+ }
199
+
200
+ // --- Core memory operations ---
201
+
202
+ async function toolDream(
203
+ cwd: string,
204
+ ctx: ExtensionContext,
205
+ reason?: string,
206
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: DreamResult }> {
207
+ try {
208
+ const result = await runMemoryDream(cwd, ctx, reason);
209
+ return {
210
+ content: [{ type: "text", text: result.summary }],
211
+ details: result,
212
+ };
213
+ } catch (error) {
214
+ if (error instanceof MemoryReadmeMissingError) {
215
+ notify(ctx, error.message, "warning");
216
+ }
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ async function maybeScheduleAutoDream(
222
+ cwd: string,
223
+ ctx: ExtensionContext,
224
+ trigger: AutoDreamTrigger,
225
+ state: AutoDreamState,
226
+ ): Promise<boolean> {
227
+ if (!ctx.model) {
228
+ return false;
229
+ }
230
+
231
+ if (state.promise) {
232
+ return true;
233
+ }
234
+
235
+ const paths = getMemoryPaths(cwd);
236
+ if (!(await pathExists(paths.memoryRoot))) {
237
+ return true;
238
+ }
239
+
240
+ const startAutoDream = (status: AutoDreamStatus): boolean => {
241
+ const reason = buildAutoDreamReason(status, trigger);
242
+ state.promise = runMemoryDream(cwd, ctx, reason)
243
+ .then(() => undefined)
244
+ .catch((error) => {
245
+ if (error instanceof MemoryReadmeMissingError) {
246
+ return;
247
+ }
248
+ notify(ctx, formatAutoDreamError(error), "warning");
249
+ })
250
+ .finally(() => {
251
+ state.promise = null;
252
+ });
253
+
254
+ return true;
255
+ };
256
+
257
+ const [core, stateFile] = await Promise.all([readCoreBlocksUnsafe(cwd), readStateUnsafe(cwd)]);
258
+ const quickStatus = {
259
+ coreLines: core.totalLines,
260
+ coreChars: core.totalChars,
261
+ undreamedLogs: 0,
262
+ oldestUndreamedAt: null,
263
+ pendingCompactions: stateFile.pendingCompactions.length,
264
+ } satisfies AutoDreamStatus;
265
+
266
+ if (quickStatus.pendingCompactions > 0) {
267
+ return startAutoDream(quickStatus);
268
+ }
269
+
270
+ if (trigger !== "session_start") {
271
+ return true;
272
+ }
273
+
274
+ const noUndreamedLogs =
275
+ stateFile.lastLogAt !== null && stateFile.lastLogAt === stateFile.lastDreamedLogAt;
276
+ if (noUndreamedLogs) {
277
+ return shouldAutoDream(quickStatus, trigger) ? startAutoDream(quickStatus) : true;
278
+ }
279
+
280
+ const status = await loadAutoDreamStatus(cwd, core, stateFile);
281
+ return shouldAutoDream(status, trigger) ? startAutoDream(status) : true;
282
+ }
283
+
284
+ // Mutating entry points acquire the path locks they need before touching memory files.
285
+ async function initMemory(cwd: string): Promise<{ created: string[]; gitignoreUpdated: boolean }> {
286
+ const paths = getMemoryPaths(cwd);
287
+ const lockPaths = [
288
+ paths.gitignoreFile,
289
+ paths.readmeFile,
290
+ paths.logFile,
291
+ paths.stateFile,
292
+ paths.compactionsDir,
293
+ paths.researchDir,
294
+ paths.attachmentsDir,
295
+ ...Object.values(paths.coreFiles),
296
+ ];
297
+
298
+ return withMemoryMutationQueue(lockPaths, async () => initMemoryUnsafe(cwd));
299
+ }
300
+
301
+ async function updateCoreBlock(
302
+ cwd: string,
303
+ name: MemoryBlockName,
304
+ content: string,
305
+ ): Promise<{ totalLines: number; totalChars: number }> {
306
+ const paths = getMemoryPaths(cwd);
307
+ return withMemoryMutationQueue(Object.values(paths.coreFiles), async () =>
308
+ updateCoreBlockUnsafe(cwd, name, content),
309
+ );
310
+ }
311
+
312
+ async function appendMemoryLog(
313
+ cwd: string,
314
+ entry: {
315
+ type: LogType;
316
+ title: string;
317
+ body: string;
318
+ importance?: ImportanceLevel;
319
+ supersedes?: string[];
320
+ invalidates?: string[];
321
+ },
322
+ ): Promise<LogEntry> {
323
+ const paths = getMemoryPaths(cwd);
324
+ return withMemoryMutationQueue([paths.logFile, paths.stateFile], async () =>
325
+ appendMemoryLogUnsafe(cwd, {
326
+ timestamp: nowIso(),
327
+ type: entry.type,
328
+ importance: entry.importance ?? DEFAULT_LOG_IMPORTANCE,
329
+ title: normalizeTitle(entry.title) || "Memory log entry",
330
+ body: normalizeMarkdownBlock(entry.body),
331
+ supersedes: normalizeMemoryReferences(entry.supersedes),
332
+ invalidates: normalizeMemoryReferences(entry.invalidates),
333
+ }),
334
+ );
335
+ }
336
+
337
+ async function enqueuePendingCompaction(cwd: string, compaction: PendingCompaction): Promise<void> {
338
+ const paths = getMemoryPaths(cwd);
339
+ await withFileMutationQueue(paths.stateFile, async () => {
340
+ const state = await readStateUnsafe(cwd);
341
+ const pending = [...state.pendingCompactions, normalizePendingCompaction(compaction)]
342
+ .filter((entry): entry is PendingCompaction => entry !== null)
343
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp))
344
+ .slice(-MAX_PENDING_COMPACTIONS);
345
+
346
+ await writeStateUnsafe(cwd, {
347
+ ...state,
348
+ pendingCompactions: pending,
349
+ });
350
+ });
351
+ }
352
+
353
+ function getDreamReplaySignature(replay: DreamReplay): string {
354
+ return JSON.stringify({
355
+ readme: replay.readme,
356
+ blocks: replay.blocks,
357
+ undreamedEntries: replay.undreamedEntries,
358
+ pendingCompactions: replay.pendingCompactions,
359
+ researchFiles: replay.researchFiles,
360
+ });
361
+ }
362
+
363
+ // Dream reads current core, log.md entries newer than lastDreamedLogAt,
364
+ // pending compactions, and the memory README rules.
365
+ // Dream writes rewritten core blocks, a dream summary in compactions/,
366
+ // and state.json with lastDreamAt, lastDreamedLogAt, and an empty pendingCompactions queue.
367
+ async function runMemoryDream(
368
+ cwd: string,
369
+ ctx: ExtensionContext,
370
+ reason?: string,
371
+ ): Promise<DreamResult> {
372
+ const paths = getMemoryPaths(cwd);
373
+ const lockPaths = [
374
+ paths.readmeFile,
375
+ paths.logFile,
376
+ paths.stateFile,
377
+ paths.compactionsDir,
378
+ ...Object.values(paths.coreFiles),
379
+ ];
380
+
381
+ const snapshot = await withMemoryMutationQueue(lockPaths, async () => {
382
+ ensureMemoryInitialized(await pathExists(paths.memoryRoot));
383
+
384
+ const replay = await collectDreamReplay(cwd);
385
+ return {
386
+ replay,
387
+ replaySignature: getDreamReplaySignature(replay),
388
+ state: shouldRunDream(replay, reason) ? null : await readStateUnsafe(cwd),
389
+ };
390
+ });
391
+
392
+ if (!shouldRunDream(snapshot.replay, reason)) {
393
+ const summary = "Memory dream: nothing to consolidate.";
394
+ notify(ctx, summary, "info");
395
+ return {
396
+ summary,
397
+ updatedBlocks: [],
398
+ consumedLogs: 0,
399
+ consumedCompactions: 0,
400
+ lastDreamAt: snapshot.state?.lastDreamAt ?? nowIso(),
401
+ lastDreamedLogAt: snapshot.state?.lastDreamedLogAt ?? null,
402
+ summaryPath: null,
403
+ };
404
+ }
405
+
406
+ const selection = await selectDreamModel(ctx);
407
+ const response = await complete(
408
+ selection.model,
409
+ {
410
+ systemPrompt: MEMORY_DREAM_SYSTEM_PROMPT,
411
+ messages: [buildDreamUserMessage(snapshot.replay, reason)],
412
+ },
413
+ {
414
+ apiKey: selection.apiKey,
415
+ headers: selection.headers,
416
+ signal: ctx.signal,
417
+ },
418
+ );
419
+
420
+ if (response.stopReason === "aborted") {
421
+ throw new Error("Memory dream aborted.");
422
+ }
423
+
424
+ if (response.stopReason === "error") {
425
+ throw new Error("Memory dream failed.");
426
+ }
427
+
428
+ const rawText = response.content
429
+ .filter((item): item is { type: "text"; text: string } => item.type === "text")
430
+ .map((item) => item.text)
431
+ .join("\n")
432
+ .trim();
433
+
434
+ if (!rawText) {
435
+ throw new Error("Memory dream returned no text.");
436
+ }
437
+
438
+ const abstraction = parseDreamOutput(rawText);
439
+ const finalBlocks = abstraction.blocks;
440
+ if (
441
+ wouldWipePendingWithoutResolution(
442
+ snapshot.replay.blocks.pending,
443
+ finalBlocks.pending,
444
+ abstraction.summary,
445
+ )
446
+ ) {
447
+ throw new Error(
448
+ "Memory dream proposed clearing pending.md without saying it was resolved or abandoned.",
449
+ );
450
+ }
451
+
452
+ const finalCoreLines = getCoreLineCount(finalBlocks);
453
+ if (finalCoreLines > CORE_LINE_CAP) {
454
+ throw new Error(
455
+ `Memory dream proposed ${finalCoreLines} core lines, exceeding the ${CORE_LINE_CAP}-line cap.`,
456
+ );
457
+ }
458
+
459
+ const finalCoreChars = getCoreCharCount(finalBlocks);
460
+ if (finalCoreChars > CORE_CHAR_CAP) {
461
+ throw new Error(
462
+ `Memory dream proposed ${finalCoreChars} core characters, exceeding the ${CORE_CHAR_CAP}-character cap.`,
463
+ );
464
+ }
465
+
466
+ return withMemoryMutationQueue(lockPaths, async () => {
467
+ ensureMemoryInitialized(await pathExists(paths.memoryRoot));
468
+
469
+ const currentReplay = await collectDreamReplay(cwd);
470
+ if (getDreamReplaySignature(currentReplay) !== snapshot.replaySignature) {
471
+ throw new Error("Memory changed while dream was running. Retry /memory dream.");
472
+ }
473
+
474
+ await writeCoreBlocksUnsafe(cwd, finalBlocks);
475
+
476
+ const previousState = await readStateUnsafe(cwd);
477
+ const dreamTimestamp = nowIso();
478
+ const lastDreamedLogAt =
479
+ snapshot.replay.undreamedEntries.at(-1)?.timestamp ?? previousState.lastDreamedLogAt;
480
+ await writeStateUnsafe(cwd, {
481
+ ...previousState,
482
+ lastDreamAt: dreamTimestamp,
483
+ lastDreamedLogAt,
484
+ pendingCompactions: [],
485
+ });
486
+
487
+ const updatedBlocks = CORE_BLOCK_NAMES.filter(
488
+ (name) =>
489
+ normalizeMarkdownBlock(snapshot.replay.blocks[name]) !==
490
+ normalizeMarkdownBlock(finalBlocks[name]),
491
+ );
492
+ const summary = buildDreamSummary(updatedBlocks, snapshot.replay, abstraction.summary);
493
+ const summaryPath = await writeDreamSummary(cwd, {
494
+ timestamp: dreamTimestamp,
495
+ reason,
496
+ summary,
497
+ updatedBlocks,
498
+ undreamedEntries: snapshot.replay.undreamedEntries,
499
+ pendingCompactions: snapshot.replay.pendingCompactions,
500
+ lastDreamedLogAt,
501
+ });
502
+ notify(ctx, summary, "info");
503
+
504
+ return {
505
+ summary,
506
+ updatedBlocks,
507
+ consumedLogs: snapshot.replay.undreamedEntries.length,
508
+ consumedCompactions: snapshot.replay.pendingCompactions.length,
509
+ lastDreamAt: dreamTimestamp,
510
+ lastDreamedLogAt,
511
+ summaryPath,
512
+ };
513
+ });
514
+ }
515
+
516
+ async function loadMemoryPrompt(cwd: string): Promise<string | undefined> {
517
+ const paths = getMemoryPaths(cwd);
518
+ if (!(await pathExists(paths.memoryRoot))) {
519
+ return undefined;
520
+ }
521
+
522
+ const [readme, core, researchFiles] = await Promise.all([
523
+ readMemoryReadme(cwd),
524
+ readCoreBlocksUnsafe(cwd),
525
+ listResearchFiles(cwd),
526
+ ]);
527
+
528
+ return buildMemoryPrompt(readme, core.blocks, researchFiles);
529
+ }
530
+
531
+ async function loadMemoryStatus(cwd: string): Promise<MemoryStatus> {
532
+ const paths = getMemoryPaths(cwd);
533
+ const initialized = await pathExists(paths.memoryRoot);
534
+ if (!initialized) {
535
+ return {
536
+ initialized: false,
537
+ coreExists: false,
538
+ coreLines: 0,
539
+ coreChars: 0,
540
+ missingCoreFiles: [...CORE_BLOCK_NAMES],
541
+ lastLogAt: null,
542
+ lastDreamAt: null,
543
+ lastDreamedLogAt: null,
544
+ undreamedLogs: 0,
545
+ pendingCompactions: 0,
546
+ researchFiles: 0,
547
+ hasLog: false,
548
+ };
549
+ }
550
+
551
+ const [core, state, logText, researchFiles, hasLog] = await Promise.all([
552
+ readCoreBlocksUnsafe(cwd),
553
+ readStateUnsafe(cwd),
554
+ readTextIfExists(paths.logFile),
555
+ listResearchFiles(cwd),
556
+ pathExists(paths.logFile),
557
+ ]);
558
+ const entries = parseMemoryLog(logText ?? "");
559
+
560
+ return {
561
+ initialized: true,
562
+ coreExists: core.exists,
563
+ coreLines: core.totalLines,
564
+ coreChars: core.totalChars,
565
+ missingCoreFiles: core.missing,
566
+ lastLogAt: entries.at(-1)?.timestamp ?? state.lastLogAt,
567
+ lastDreamAt: state.lastDreamAt,
568
+ lastDreamedLogAt: state.lastDreamedLogAt,
569
+ undreamedLogs: countUndreamedLogs(entries, state.lastDreamedLogAt),
570
+ pendingCompactions: state.pendingCompactions.length,
571
+ researchFiles: researchFiles.length,
572
+ hasLog,
573
+ };
574
+ }
575
+
576
+ async function loadAutoDreamStatus(
577
+ cwd: string,
578
+ core: CoreReadResult,
579
+ state: MemoryState,
580
+ ): Promise<AutoDreamStatus> {
581
+ const logText = await readTextIfExists(getMemoryPaths(cwd).logFile);
582
+ const entries = parseMemoryLog(logText ?? "");
583
+ const undreamedEntries = selectUndreamedLogEntries(entries, state.lastDreamedLogAt);
584
+
585
+ return {
586
+ coreLines: core.totalLines,
587
+ coreChars: core.totalChars,
588
+ undreamedLogs: undreamedEntries.length,
589
+ oldestUndreamedAt: undreamedEntries[0]?.timestamp ?? null,
590
+ pendingCompactions: state.pendingCompactions.length,
591
+ };
592
+ }
593
+
594
+ // Mutating *Unsafe helpers assume the corresponding path locks are already held.
595
+ async function initMemoryUnsafe(
596
+ cwd: string,
597
+ ): Promise<{ created: string[]; gitignoreUpdated: boolean }> {
598
+ const paths = getMemoryPaths(cwd);
599
+ const created: string[] = [];
600
+
601
+ await ensureDir(paths.memoryRoot);
602
+ await ensureDir(paths.coreDir);
603
+ await ensureDir(paths.compactionsDir);
604
+ await ensureDir(paths.researchDir);
605
+ await ensureDir(paths.attachmentsDir);
606
+
607
+ for (const name of CORE_BLOCK_NAMES) {
608
+ if (await writeIfMissing(paths.coreFiles[name], "")) {
609
+ created.push(path.relative(cwd, paths.coreFiles[name]));
610
+ }
611
+ }
612
+
613
+ if (await writeIfMissing(paths.logFile, "# Memory log\n")) {
614
+ created.push(path.relative(cwd, paths.logFile));
615
+ }
616
+
617
+ if (!(await pathExists(paths.stateFile))) {
618
+ await writeStateUnsafe(cwd, defaultMemoryState());
619
+ created.push(path.relative(cwd, paths.stateFile));
620
+ }
621
+
622
+ if (await writeIfMissing(paths.readmeFile, buildDefaultMemoryReadme())) {
623
+ created.push(path.relative(cwd, paths.readmeFile));
624
+ }
625
+
626
+ const gitignoreUpdated = await ensureAttachmentsPathIgnored(paths.gitignoreFile);
627
+ return { created, gitignoreUpdated };
628
+ }
629
+
630
+ async function updateCoreBlockUnsafe(
631
+ cwd: string,
632
+ name: MemoryBlockName,
633
+ content: string,
634
+ ): Promise<{ totalLines: number; totalChars: number }> {
635
+ const paths = getMemoryPaths(cwd);
636
+ ensureMemoryInitialized(await pathExists(paths.memoryRoot));
637
+
638
+ const current = await readCoreBlocksUnsafe(cwd);
639
+ const nextBlocks: CoreBlocks = {
640
+ ...current.blocks,
641
+ [name]: normalizeMarkdownBlock(content),
642
+ };
643
+ const totalLines = getCoreLineCount(nextBlocks);
644
+ if (totalLines > CORE_LINE_CAP) {
645
+ throw new Error(
646
+ `Core block update would exceed the ${CORE_LINE_CAP}-line cap (${totalLines}). Run memory_dream or remove content first.`,
647
+ );
648
+ }
649
+
650
+ const totalChars = getCoreCharCount(nextBlocks);
651
+ if (totalChars > CORE_CHAR_CAP) {
652
+ throw new Error(
653
+ `Core block update would exceed the ${CORE_CHAR_CAP}-character cap (${totalChars}). Run memory_dream or remove content first.`,
654
+ );
655
+ }
656
+
657
+ await ensureDir(paths.coreDir);
658
+ await fs.writeFile(paths.coreFiles[name], nextBlocks[name], "utf8");
659
+ return { totalLines, totalChars };
660
+ }
661
+
662
+ async function appendMemoryLogUnsafe(cwd: string, entry: LogEntry): Promise<LogEntry> {
663
+ const paths = getMemoryPaths(cwd);
664
+ ensureMemoryInitialized(await pathExists(paths.memoryRoot));
665
+
666
+ const chunk = [
667
+ `## ${entry.timestamp} | ${entry.type} | ${entry.importance} | ${entry.title}`,
668
+ "",
669
+ renderLogEntryBody(entry),
670
+ ].join("\n");
671
+
672
+ await ensureDir(path.dirname(paths.logFile));
673
+ let writeFreshLog = false;
674
+ try {
675
+ writeFreshLog = (await fs.stat(paths.logFile)).size === 0;
676
+ } catch (error) {
677
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
678
+ writeFreshLog = true;
679
+ } else {
680
+ throw error;
681
+ }
682
+ }
683
+
684
+ if (writeFreshLog) {
685
+ await fs.writeFile(paths.logFile, `# Memory log\n\n${chunk}\n`, "utf8");
686
+ } else {
687
+ await fs.appendFile(paths.logFile, `\n${chunk}\n`, "utf8");
688
+ }
689
+
690
+ const state = await readStateUnsafe(cwd);
691
+ await writeStateUnsafe(cwd, {
692
+ ...state,
693
+ lastLogAt: entry.timestamp,
694
+ });
695
+
696
+ return entry;
697
+ }
698
+
699
+ async function collectDreamReplay(cwd: string): Promise<DreamReplay> {
700
+ const paths = getMemoryPaths(cwd);
701
+ const [readme, core, state, logText, researchFiles] = await Promise.all([
702
+ readMemoryReadme(cwd),
703
+ readCoreBlocksUnsafe(cwd),
704
+ readStateUnsafe(cwd),
705
+ readTextIfExists(paths.logFile),
706
+ listResearchFiles(cwd),
707
+ ]);
708
+ const entries = parseMemoryLog(logText ?? "");
709
+ const undreamedEntries = selectUndreamedLogEntries(entries, state.lastDreamedLogAt);
710
+
711
+ return {
712
+ readme,
713
+ blocks: core.blocks,
714
+ totalLines: core.totalLines,
715
+ totalChars: core.totalChars,
716
+ undreamedEntries,
717
+ pendingCompactions: state.pendingCompactions,
718
+ researchFiles,
719
+ };
720
+ }
721
+
722
+ async function writeCoreBlocksUnsafe(cwd: string, blocks: CoreBlocks): Promise<void> {
723
+ const paths = getMemoryPaths(cwd);
724
+ await ensureDir(paths.coreDir);
725
+
726
+ await Promise.all(
727
+ CORE_BLOCK_NAMES.map((name) =>
728
+ fs.writeFile(paths.coreFiles[name], normalizeMarkdownBlock(blocks[name]), "utf8"),
729
+ ),
730
+ );
731
+ }
732
+
733
+ async function readCoreBlocksUnsafe(cwd: string): Promise<CoreReadResult> {
734
+ const paths = getMemoryPaths(cwd);
735
+ const exists = await pathExists(paths.coreDir);
736
+ const blocks = createEmptyCoreBlocks();
737
+ const missing: MemoryBlockName[] = [];
738
+
739
+ await Promise.all(
740
+ CORE_BLOCK_NAMES.map(async (name) => {
741
+ const text = await readTextIfExists(paths.coreFiles[name]);
742
+ if (text === undefined) {
743
+ if (exists) {
744
+ missing.push(name);
745
+ }
746
+ return;
747
+ }
748
+ blocks[name] = text;
749
+ }),
750
+ );
751
+
752
+ return {
753
+ exists,
754
+ blocks,
755
+ missing: missing.sort((a, b) => CORE_BLOCK_NAMES.indexOf(a) - CORE_BLOCK_NAMES.indexOf(b)),
756
+ totalLines: getCoreLineCount(blocks),
757
+ totalChars: getCoreCharCount(blocks),
758
+ };
759
+ }
760
+
761
+ async function readStateUnsafe(cwd: string): Promise<MemoryState> {
762
+ const statePath = getMemoryPaths(cwd).stateFile;
763
+ const raw = await readTextIfExists(statePath);
764
+ if (!raw?.trim()) {
765
+ return defaultMemoryState();
766
+ }
767
+
768
+ try {
769
+ const parsed = JSON.parse(raw) as Partial<MemoryStateFile>;
770
+ return {
771
+ lastLogAt: typeof parsed.last_log_at === "string" ? parsed.last_log_at : null,
772
+ lastDreamAt: typeof parsed.last_dream_at === "string" ? parsed.last_dream_at : null,
773
+ lastDreamedLogAt:
774
+ typeof parsed.last_dreamed_log_at === "string" ? parsed.last_dreamed_log_at : null,
775
+ pendingCompactions: Array.isArray(parsed.pending_compactions)
776
+ ? parsed.pending_compactions
777
+ .map((entry) => normalizePendingCompaction(entry))
778
+ .filter((entry): entry is PendingCompaction => entry !== null)
779
+ .slice(-MAX_PENDING_COMPACTIONS)
780
+ : [],
781
+ };
782
+ } catch (error) {
783
+ const message = error instanceof Error ? error.message : String(error);
784
+ throw new Error(
785
+ `${path.relative(cwd, statePath)} is invalid JSON. Repair or restore it before using repo memory. ${message}`,
786
+ );
787
+ }
788
+ }
789
+
790
+ async function writeStateUnsafe(cwd: string, state: MemoryState): Promise<void> {
791
+ const paths = getMemoryPaths(cwd);
792
+ const stateFile: MemoryStateFile = {
793
+ last_log_at: state.lastLogAt,
794
+ last_dream_at: state.lastDreamAt,
795
+ last_dreamed_log_at: state.lastDreamedLogAt,
796
+ pending_compactions: state.pendingCompactions,
797
+ };
798
+ const payload = `${JSON.stringify(stateFile, null, 2)}\n`;
799
+ const tempPath = `${paths.stateFile}.${process.pid}.${Date.now()}.tmp`;
800
+
801
+ await ensureDir(path.dirname(paths.stateFile));
802
+ try {
803
+ await fs.writeFile(tempPath, payload, "utf8");
804
+ await fs.rename(tempPath, paths.stateFile);
805
+ } catch (error) {
806
+ void fs.rm(tempPath, { force: true }).catch(() => undefined);
807
+ throw error;
808
+ }
809
+ }
810
+
811
+ async function writeCompactionSummary(cwd: string, compaction: PendingCompaction): Promise<void> {
812
+ const paths = getMemoryPaths(cwd);
813
+ await withFileMutationQueue(paths.compactionsDir, async () => {
814
+ ensureMemoryInitialized(await pathExists(paths.memoryRoot));
815
+
816
+ const filePath = path.join(
817
+ paths.compactionsDir,
818
+ `${formatTimestampForFilename(compaction.timestamp)}-compaction.md`,
819
+ );
820
+ await ensureDir(paths.compactionsDir);
821
+ await fs.writeFile(filePath, buildCompactionSummaryContent(compaction), "utf8");
822
+ });
823
+ }
824
+
825
+ async function writeDreamSummary(
826
+ cwd: string,
827
+ summary: {
828
+ timestamp: string;
829
+ reason?: string;
830
+ summary: string;
831
+ updatedBlocks: MemoryBlockName[];
832
+ undreamedEntries: LogEntry[];
833
+ pendingCompactions: PendingCompaction[];
834
+ lastDreamedLogAt: string | null;
835
+ },
836
+ ): Promise<string> {
837
+ const paths = getMemoryPaths(cwd);
838
+ const filePath = path.join(
839
+ paths.compactionsDir,
840
+ `${formatTimestampForFilename(summary.timestamp)}-dream.md`,
841
+ );
842
+ await ensureDir(paths.compactionsDir);
843
+ await fs.writeFile(filePath, buildDreamSummaryContent(summary), "utf8");
844
+ return path.relative(cwd, filePath);
845
+ }
846
+
847
+ // Runtime memory use reads .agents/memory/README.md from disk.
848
+ // /memory init recreates it when missing.
849
+ async function readMemoryReadme(cwd: string): Promise<string> {
850
+ const readmePath = getMemoryPaths(cwd).readmeFile;
851
+ const readme = await readTextIfExists(readmePath);
852
+ if (readme !== undefined) {
853
+ return readme;
854
+ }
855
+
856
+ throw new MemoryReadmeMissingError(path.relative(cwd, readmePath));
857
+ }
858
+
859
+ async function listResearchFiles(cwd: string): Promise<string[]> {
860
+ const researchDir = getMemoryPaths(cwd).researchDir;
861
+ if (!(await pathExists(researchDir))) {
862
+ return [];
863
+ }
864
+
865
+ const entries = await fs.readdir(researchDir, { withFileTypes: true });
866
+ return entries
867
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
868
+ .map((entry) => path.relative(cwd, path.join(researchDir, entry.name)))
869
+ .sort();
870
+ }
871
+
872
+ async function ensureAttachmentsPathIgnored(gitignoreFile: string): Promise<boolean> {
873
+ const attachmentsLine = "/.agents/memory/attachments/";
874
+ const current = (await readTextIfExists(gitignoreFile)) ?? "";
875
+ const lines = current.replace(/\r/g, "").split("\n");
876
+ const alreadyIgnored = lines.some((line) => {
877
+ const trimmed = line.trim();
878
+ return (
879
+ trimmed === attachmentsLine ||
880
+ trimmed === ".agents/memory/attachments/" ||
881
+ trimmed === "/.agents/memory/attachments"
882
+ );
883
+ });
884
+
885
+ if (alreadyIgnored) {
886
+ return false;
887
+ }
888
+
889
+ const prefix = current.length === 0 ? "" : current.endsWith("\n") ? "" : "\n";
890
+ await fs.writeFile(gitignoreFile, `${current}${prefix}${attachmentsLine}\n`, "utf8");
891
+ return true;
892
+ }
893
+
894
+ async function writeIfMissing(filePath: string, content: string): Promise<boolean> {
895
+ if (await pathExists(filePath)) {
896
+ return false;
897
+ }
898
+
899
+ await ensureDir(path.dirname(filePath));
900
+ await fs.writeFile(filePath, content, "utf8");
901
+ return true;
902
+ }
903
+
904
+ // --- Prompt and command helpers ---
905
+
906
+ function buildDreamUserMessage(replay: DreamReplay, reason?: string): UserMessage {
907
+ const prompt = [
908
+ reason?.trim() ? `Dream reason: ${reason.trim()}` : "Dream reason: consolidate repo memory.",
909
+ `Current core size: ${replay.totalLines}/${CORE_LINE_CAP} lines, ${replay.totalChars}/${CORE_CHAR_CAP} chars`,
910
+ "",
911
+ "Source-of-truth memory rules:",
912
+ "<memory-readme>",
913
+ replay.readme.trimEnd() || "(missing)",
914
+ "</memory-readme>",
915
+ "",
916
+ "<core>",
917
+ "## directives.md",
918
+ replay.blocks.directives.trimEnd() || "(empty)",
919
+ "",
920
+ "## context.md",
921
+ replay.blocks.context.trimEnd() || "(empty)",
922
+ "",
923
+ "## focus.md",
924
+ replay.blocks.focus.trimEnd() || "(empty)",
925
+ "",
926
+ "## pending.md",
927
+ replay.blocks.pending.trimEnd() || "(empty)",
928
+ "</core>",
929
+ "",
930
+ "<undreamed-log>",
931
+ formatLogEntriesForPrompt(replay.undreamedEntries) || "(none)",
932
+ "</undreamed-log>",
933
+ "",
934
+ "<compaction-notes>",
935
+ formatPendingCompactionsForPrompt(replay.pendingCompactions) || "(none)",
936
+ "</compaction-notes>",
937
+ "",
938
+ "<research-files>",
939
+ replay.researchFiles.join("\n") || "(none)",
940
+ "</research-files>",
941
+ ].join("\n");
942
+
943
+ return {
944
+ role: "user",
945
+ content: [{ type: "text", text: prompt }],
946
+ timestamp: Date.now(),
947
+ };
948
+ }
949
+
950
+ async function selectDreamModel(ctx: ExtensionContext): Promise<ModelSelection> {
951
+ if (!ctx.model) {
952
+ throw new Error("No active model selected for memory dream.");
953
+ }
954
+
955
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
956
+ if (!auth.ok) {
957
+ throw new Error("error" in auth ? auth.error : "Memory dream is missing model auth.");
958
+ }
959
+
960
+ return {
961
+ model: ctx.model,
962
+ apiKey: auth.apiKey,
963
+ headers: auth.headers,
964
+ };
965
+ }
966
+
967
+ function parseDreamOutput(rawText: string): DreamOutput {
968
+ const parsed = parsePossiblyWrappedJson(rawText);
969
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
970
+ throw new Error("Memory dream output must be a JSON object.");
971
+ }
972
+
973
+ const record = parsed as Record<string, unknown>;
974
+ const rawBlocks = record.blocks;
975
+ if (!rawBlocks || typeof rawBlocks !== "object" || Array.isArray(rawBlocks)) {
976
+ throw new Error("Memory dream output must include a blocks object.");
977
+ }
978
+
979
+ const blocksRecord = rawBlocks as Record<string, unknown>;
980
+ for (const key of Object.keys(blocksRecord)) {
981
+ if (!CORE_BLOCK_NAMES.includes(key as MemoryBlockName)) {
982
+ throw new Error(`Memory dream output has unexpected block key: ${key}`);
983
+ }
984
+ }
985
+
986
+ const blocks = createEmptyCoreBlocks();
987
+ for (const name of CORE_BLOCK_NAMES) {
988
+ const value = blocksRecord[name];
989
+ if (typeof value !== "string") {
990
+ throw new Error(`Memory dream output is missing a string block for ${name}.`);
991
+ }
992
+ blocks[name] = normalizeMarkdownBlock(value);
993
+ }
994
+
995
+ const summary =
996
+ typeof record.summary === "string" && record.summary.trim()
997
+ ? normalizeInlineText(record.summary)
998
+ : "Consolidated repo memory.";
999
+
1000
+ return { blocks, summary };
1001
+ }
1002
+
1003
+ // Reject dream proposals that clear pending.md unless the summary says the work
1004
+ // was resolved or abandoned.
1005
+ function wouldWipePendingWithoutResolution(
1006
+ previousPending: string,
1007
+ nextPending: string,
1008
+ summary: string,
1009
+ ): boolean {
1010
+ if (!previousPending.trim()) {
1011
+ return false;
1012
+ }
1013
+
1014
+ if (nextPending.trim()) {
1015
+ return false;
1016
+ }
1017
+
1018
+ return !mentionsResolution(summary);
1019
+ }
1020
+
1021
+ function mentionsResolution(text: string): boolean {
1022
+ return /\b(resolve|resolved|abandon|abandoned|drop|dropped|complete|completed|close|closed)\b/i.test(
1023
+ text,
1024
+ );
1025
+ }
1026
+
1027
+ function buildMemoryPrompt(readme: string, blocks: CoreBlocks, researchFiles: string[]): string {
1028
+ const sections = [
1029
+ "<repo_memory>",
1030
+ "Use repo memory for continuity across sessions in this repo.",
1031
+ "Source-of-truth memory rules:",
1032
+ "<memory-readme>",
1033
+ readme.trimEnd() || "(missing)",
1034
+ "</memory-readme>",
1035
+ "",
1036
+ researchFiles.length > 0
1037
+ ? `Research abstracts available: ${researchFiles.join(", ")}`
1038
+ : "Research abstracts available: none.",
1039
+ "",
1040
+ `### directives.md\n${blocks.directives.trimEnd() || "(empty)"}`,
1041
+ "",
1042
+ `### context.md\n${blocks.context.trimEnd() || "(empty)"}`,
1043
+ "",
1044
+ `### focus.md\n${blocks.focus.trimEnd() || "(empty)"}`,
1045
+ "",
1046
+ `### pending.md\n${blocks.pending.trimEnd() || "(empty)"}`,
1047
+ "</repo_memory>",
1048
+ ];
1049
+ return sections.join("\n");
1050
+ }
1051
+
1052
+ function buildDefaultMemoryReadme(): string {
1053
+ return [
1054
+ "# Repo memory",
1055
+ "",
1056
+ "This repository uses Pi-managed project-local memory under `.agents/memory/`.",
1057
+ "",
1058
+ "## Layout",
1059
+ "",
1060
+ `- \`core/\` — four short markdown blocks (\`directives.md\`, \`context.md\`, \`focus.md\`, \`pending.md\`). Their combined total must stay at or below ${CORE_LINE_CAP} lines and ${CORE_CHAR_CAP} characters. Enforce both caps only when writing core or finalizing a dream, not when reading.`,
1061
+ "- `log.md` — append-only markdown log for decisions, prompts, plans, experiments, attachment additions, and lessons from failed or rejected attempts. Use explicit `supersedes` and `invalidates` links when an entry replaces or corrects prior memory.",
1062
+ "- `compactions/` — immutable dream and compaction summaries with provenance.",
1063
+ "- `research/` — use it for short abstracts of actual external SOTA research that materially informs the current work. Read relevant files on demand before relying on them.",
1064
+ "- `attachments/` — only user-requested or user-facing files used to collaborate with the user. This folder is gitignored. Do not use it as scratch space. If you add something here, also append a log entry naming the file and why it exists.",
1065
+ "",
1066
+ "## Write rules",
1067
+ "",
1068
+ "- If work is left unfinished, update `core/pending.md`.",
1069
+ "- If architecture or behavior changes, update `core/context.md`.",
1070
+ "- If user preferences or standing constraints change, update `core/directives.md`.",
1071
+ "- If focus shifts, update `core/focus.md`.",
1072
+ "- If an important decision, prompt ingest, plan, experiment, attachment addition, or lesson from a failed or rejected attempt happens, append to `log.md`.",
1073
+ "- If a new log entry replaces or corrects prior memory, include explicit `supersedes` and/or `invalidates` links.",
1074
+ "- If the user provides a substantial brief, append it as a `prompt | high` log entry.",
1075
+ "",
1076
+ "## Dream",
1077
+ "",
1078
+ "Dream is the only consolidation mechanism for `core/`.",
1079
+ "",
1080
+ "Pi can trigger dreaming automatically on session start when logs are stale and after compaction when new compaction context should be folded into memory. You can also run `/memory dream` manually.",
1081
+ "",
1082
+ "Dream reads all log entries newer than the last dreamed log timestamp plus pending compaction summaries. It does not automatically retrieve older log entries.",
1083
+ "",
1084
+ ].join("\n");
1085
+ }
1086
+
1087
+ function buildDreamSummary(
1088
+ updatedBlocks: MemoryBlockName[],
1089
+ replay: DreamReplay,
1090
+ summary: string,
1091
+ ): string {
1092
+ const parts = [
1093
+ updatedBlocks.length > 0 ? `updated ${updatedBlocks.join(", ")}` : "updated no core blocks",
1094
+ replay.undreamedEntries.length > 0
1095
+ ? `consumed ${replay.undreamedEntries.length} new log entr${replay.undreamedEntries.length === 1 ? "y" : "ies"}`
1096
+ : "consumed no new log entries",
1097
+ replay.pendingCompactions.length > 0
1098
+ ? `folded ${replay.pendingCompactions.length} compaction note${replay.pendingCompactions.length === 1 ? "" : "s"}`
1099
+ : "folded no compaction notes",
1100
+ ];
1101
+
1102
+ return `Memory dream: ${parts.join("; ")}. ${truncateText(summary, 160)}`;
1103
+ }
1104
+
1105
+ function buildAutoDreamReason(status: AutoDreamStatus, trigger: AutoDreamTrigger): string {
1106
+ if (trigger === "session_compact") {
1107
+ return `Automatic dream after compaction with ${status.pendingCompactions} pending compaction note${status.pendingCompactions === 1 ? "" : "s"}.`;
1108
+ }
1109
+
1110
+ if (
1111
+ status.coreLines >= AUTO_DREAM_CORE_LINE_THRESHOLD ||
1112
+ status.coreChars >= AUTO_DREAM_CORE_CHAR_THRESHOLD
1113
+ ) {
1114
+ const coreDetails = [
1115
+ status.coreLines >= AUTO_DREAM_CORE_LINE_THRESHOLD
1116
+ ? `${status.coreLines}/${CORE_LINE_CAP} lines`
1117
+ : null,
1118
+ status.coreChars >= AUTO_DREAM_CORE_CHAR_THRESHOLD
1119
+ ? `${status.coreChars}/${CORE_CHAR_CAP} chars`
1120
+ : null,
1121
+ ].filter((detail): detail is string => detail !== null);
1122
+ return `Automatic dream on session start with core at ${coreDetails.join(" and ")}.`;
1123
+ }
1124
+
1125
+ if (status.undreamedLogs >= AUTO_DREAM_MIN_UNDREAMED_LOGS) {
1126
+ return `Automatic dream on session start with ${status.undreamedLogs} undreamed log entries.`;
1127
+ }
1128
+
1129
+ return "Automatic dream on session start because undreamed memory is stale.";
1130
+ }
1131
+
1132
+ function formatLogEntriesForPrompt(entries: LogEntry[]): string {
1133
+ if (entries.length === 0) {
1134
+ return "";
1135
+ }
1136
+
1137
+ return entries
1138
+ .map((entry) => {
1139
+ return [
1140
+ `## ${entry.timestamp} | ${entry.type} | ${entry.importance} | ${entry.title}`,
1141
+ "",
1142
+ renderLogEntryBody(entry),
1143
+ ].join("\n");
1144
+ })
1145
+ .join("\n\n");
1146
+ }
1147
+
1148
+ function formatPendingCompactionsForPrompt(compactions: PendingCompaction[]): string {
1149
+ if (compactions.length === 0) {
1150
+ return "";
1151
+ }
1152
+
1153
+ return compactions
1154
+ .map((compaction) => {
1155
+ return [`## ${compaction.timestamp}`, "", compaction.summary.trim() || "_No summary._"].join(
1156
+ "\n",
1157
+ );
1158
+ })
1159
+ .join("\n\n");
1160
+ }
1161
+
1162
+ function formatInitResult(result: { created: string[]; gitignoreUpdated: boolean }): string {
1163
+ const parts: string[] = [];
1164
+ if (result.created.length > 0) {
1165
+ parts.push(`created ${result.created.length} path${result.created.length === 1 ? "" : "s"}`);
1166
+ }
1167
+ if (result.gitignoreUpdated) {
1168
+ parts.push("updated .gitignore");
1169
+ }
1170
+
1171
+ return parts.length > 0
1172
+ ? `Initialized .agents/memory/ (${parts.join(", ")}).`
1173
+ : "Memory already initialized.";
1174
+ }
1175
+
1176
+ function formatMemoryStatus(status: MemoryStatus): string {
1177
+ if (!status.initialized) {
1178
+ return "Memory: not initialized. Run /memory init.";
1179
+ }
1180
+
1181
+ const lines = [
1182
+ "Memory: initialized",
1183
+ `Core lines: ${status.coreLines}/${CORE_LINE_CAP}`,
1184
+ `Core chars: ${status.coreChars}/${CORE_CHAR_CAP}`,
1185
+ `Last log: ${status.lastLogAt ?? "never"}`,
1186
+ `Last dream: ${status.lastDreamAt ?? "never"}`,
1187
+ `Last dreamed log timestamp: ${status.lastDreamedLogAt ?? "never"}`,
1188
+ `Undreamed logs: ${status.undreamedLogs}`,
1189
+ `Pending compactions: ${status.pendingCompactions}`,
1190
+ `Research abstracts: ${status.researchFiles}`,
1191
+ `Log: ${status.hasLog ? "present" : "missing"}`,
1192
+ ];
1193
+
1194
+ if (!status.coreExists) {
1195
+ lines.push("Core blocks: missing");
1196
+ } else if (status.missingCoreFiles.length > 0) {
1197
+ lines.push(`Missing core files: ${status.missingCoreFiles.join(", ")}`);
1198
+ }
1199
+
1200
+ return lines.join("\n");
1201
+ }
1202
+
1203
+ function formatAutoDreamError(error: unknown): string {
1204
+ const message = error instanceof Error ? error.message : String(error);
1205
+ return `Memory dream skipped: ${message}`;
1206
+ }
1207
+
1208
+ function parseMemoryCommandArgs(
1209
+ args: string,
1210
+ ): { command: "init" | "status" | "dream"; text: string } | null {
1211
+ const trimmed = args.trim();
1212
+ if (!trimmed) {
1213
+ return null;
1214
+ }
1215
+
1216
+ const firstSpace = trimmed.indexOf(" ");
1217
+ const command = (firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace)).toLowerCase();
1218
+ const text = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1).trim();
1219
+
1220
+ if (command === "init" || command === "status" || command === "dream") {
1221
+ return { command, text };
1222
+ }
1223
+
1224
+ return null;
1225
+ }
1226
+
1227
+ function getMemoryArgumentCompletions(
1228
+ prefix: string,
1229
+ ): Array<{ value: string; label: string }> | null {
1230
+ const normalized = prefix.trimStart().toLowerCase();
1231
+ if (normalized.includes(" ")) {
1232
+ return null;
1233
+ }
1234
+
1235
+ const options = [
1236
+ { value: "init", label: "init" },
1237
+ { value: "status", label: "status" },
1238
+ { value: "dream", label: "dream" },
1239
+ ];
1240
+ const matches = options.filter((option) => option.label.startsWith(normalized));
1241
+ return matches.length > 0 ? matches : null;
1242
+ }
1243
+
1244
+ // --- Parsing and utilities ---
1245
+
1246
+ function createEmptyCoreBlocks(): CoreBlocks {
1247
+ return {
1248
+ directives: "",
1249
+ context: "",
1250
+ focus: "",
1251
+ pending: "",
1252
+ };
1253
+ }
1254
+
1255
+ function defaultMemoryState(): MemoryState {
1256
+ return {
1257
+ lastLogAt: null,
1258
+ lastDreamAt: null,
1259
+ lastDreamedLogAt: null,
1260
+ pendingCompactions: [],
1261
+ };
1262
+ }
1263
+
1264
+ function getMemoryPaths(cwd: string): {
1265
+ memoryRoot: string;
1266
+ coreDir: string;
1267
+ compactionsDir: string;
1268
+ researchDir: string;
1269
+ attachmentsDir: string;
1270
+ logFile: string;
1271
+ stateFile: string;
1272
+ readmeFile: string;
1273
+ gitignoreFile: string;
1274
+ coreFiles: Record<MemoryBlockName, string>;
1275
+ } {
1276
+ const memoryRoot = path.join(cwd, MEMORY_DIR_NAME);
1277
+ const coreDir = path.join(memoryRoot, "core");
1278
+
1279
+ return {
1280
+ memoryRoot,
1281
+ coreDir,
1282
+ compactionsDir: path.join(memoryRoot, "compactions"),
1283
+ researchDir: path.join(memoryRoot, "research"),
1284
+ attachmentsDir: path.join(memoryRoot, "attachments"),
1285
+ logFile: path.join(memoryRoot, "log.md"),
1286
+ stateFile: path.join(memoryRoot, "state.json"),
1287
+ readmeFile: path.join(memoryRoot, "README.md"),
1288
+ gitignoreFile: path.join(cwd, ".gitignore"),
1289
+ coreFiles: {
1290
+ directives: path.join(coreDir, "directives.md"),
1291
+ context: path.join(coreDir, "context.md"),
1292
+ focus: path.join(coreDir, "focus.md"),
1293
+ pending: path.join(coreDir, "pending.md"),
1294
+ },
1295
+ };
1296
+ }
1297
+
1298
+ function shouldRunDream(replay: DreamReplay, reason?: string): boolean {
1299
+ return (
1300
+ replay.undreamedEntries.length > 0 ||
1301
+ replay.pendingCompactions.length > 0 ||
1302
+ replay.totalLines >= AUTO_DREAM_CORE_LINE_THRESHOLD ||
1303
+ replay.totalChars >= AUTO_DREAM_CORE_CHAR_THRESHOLD ||
1304
+ Boolean(reason?.trim())
1305
+ );
1306
+ }
1307
+
1308
+ // Auto-dream runs immediately when pending compactions exist.
1309
+ // Otherwise it only runs on session_start, based on log backlog, core size, or log age.
1310
+ function shouldAutoDream(status: AutoDreamStatus, trigger: AutoDreamTrigger): boolean {
1311
+ if (status.pendingCompactions > 0) {
1312
+ return true;
1313
+ }
1314
+
1315
+ if (trigger !== "session_start") {
1316
+ return false;
1317
+ }
1318
+
1319
+ if (
1320
+ status.coreLines >= AUTO_DREAM_CORE_LINE_THRESHOLD ||
1321
+ status.coreChars >= AUTO_DREAM_CORE_CHAR_THRESHOLD
1322
+ ) {
1323
+ return true;
1324
+ }
1325
+
1326
+ if (status.undreamedLogs === 0) {
1327
+ return false;
1328
+ }
1329
+
1330
+ if (status.undreamedLogs >= AUTO_DREAM_MIN_UNDREAMED_LOGS) {
1331
+ return true;
1332
+ }
1333
+
1334
+ if (!status.oldestUndreamedAt) {
1335
+ return false;
1336
+ }
1337
+
1338
+ return Date.now() - Date.parse(status.oldestUndreamedAt) >= AUTO_DREAM_MAX_AGE_MS;
1339
+ }
1340
+
1341
+ function parseMemoryLog(text: string): LogEntry[] {
1342
+ const lines = text.replace(/\r/g, "").split("\n");
1343
+ const entries: LogEntry[] = [];
1344
+ let current: Omit<LogEntry, "body" | "supersedes" | "invalidates"> | null = null;
1345
+ let bodyLines: string[] = [];
1346
+
1347
+ const flush = () => {
1348
+ if (!current) {
1349
+ return;
1350
+ }
1351
+
1352
+ const parsedBody = parseLogEntryBody(bodyLines.join("\n"));
1353
+ entries.push({
1354
+ ...current,
1355
+ body: parsedBody.body,
1356
+ supersedes: parsedBody.supersedes,
1357
+ invalidates: parsedBody.invalidates,
1358
+ });
1359
+ current = null;
1360
+ bodyLines = [];
1361
+ };
1362
+
1363
+ for (const line of lines) {
1364
+ const match = line.match(
1365
+ /^##\s+([^|]+?)\s+\|\s+(decision|prompt|plan|experiment)\s+\|\s+(high|medium|low)\s+\|\s+(.+?)\s*$/,
1366
+ );
1367
+ if (match) {
1368
+ flush();
1369
+ current = {
1370
+ timestamp: match[1].trim(),
1371
+ type: match[2] as LogType,
1372
+ importance: match[3] as ImportanceLevel,
1373
+ title: normalizeTitle(match[4]) || "Memory log entry",
1374
+ };
1375
+ continue;
1376
+ }
1377
+
1378
+ if (current) {
1379
+ bodyLines.push(line);
1380
+ }
1381
+ }
1382
+
1383
+ flush();
1384
+ return entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1385
+ }
1386
+
1387
+ function selectUndreamedLogEntries(
1388
+ entries: LogEntry[],
1389
+ lastDreamedLogAt: string | null,
1390
+ ): LogEntry[] {
1391
+ if (!lastDreamedLogAt) {
1392
+ return [...entries];
1393
+ }
1394
+ return entries.filter((entry) => entry.timestamp > lastDreamedLogAt);
1395
+ }
1396
+
1397
+ function countUndreamedLogs(entries: LogEntry[], lastDreamedLogAt: string | null): number {
1398
+ return selectUndreamedLogEntries(entries, lastDreamedLogAt).length;
1399
+ }
1400
+
1401
+ function parseLogEntryBody(body: string): {
1402
+ body: string;
1403
+ supersedes: string[];
1404
+ invalidates: string[];
1405
+ } {
1406
+ const normalized = body.replace(/\r/g, "").trimEnd();
1407
+ if (!normalized) {
1408
+ return {
1409
+ body: "",
1410
+ supersedes: [],
1411
+ invalidates: [],
1412
+ };
1413
+ }
1414
+
1415
+ const lines = normalized.split("\n");
1416
+ const headingIndex = lines.lastIndexOf("## Memory links");
1417
+ if (headingIndex === -1) {
1418
+ return {
1419
+ body: normalizeMarkdownBlock(normalized),
1420
+ supersedes: [],
1421
+ invalidates: [],
1422
+ };
1423
+ }
1424
+
1425
+ const linkLines = lines.slice(headingIndex + 1).filter((line) => line.trim().length > 0);
1426
+ const supersedes: string[] = [];
1427
+ const invalidates: string[] = [];
1428
+
1429
+ for (const line of linkLines) {
1430
+ const supersedesMatch = line.match(/^-\s+Supersedes:\s+(.+)$/);
1431
+ if (supersedesMatch) {
1432
+ supersedes.push(normalizeInlineText(supersedesMatch[1]));
1433
+ continue;
1434
+ }
1435
+
1436
+ const invalidatesMatch = line.match(/^-\s+Invalidates:\s+(.+)$/);
1437
+ if (invalidatesMatch) {
1438
+ invalidates.push(normalizeInlineText(invalidatesMatch[1]));
1439
+ continue;
1440
+ }
1441
+
1442
+ return {
1443
+ body: normalizeMarkdownBlock(normalized),
1444
+ supersedes: [],
1445
+ invalidates: [],
1446
+ };
1447
+ }
1448
+
1449
+ return {
1450
+ body: normalizeMarkdownBlock(lines.slice(0, headingIndex).join("\n")),
1451
+ supersedes: normalizeMemoryReferences(supersedes),
1452
+ invalidates: normalizeMemoryReferences(invalidates),
1453
+ };
1454
+ }
1455
+
1456
+ function renderLogEntryBody(entry: LogEntry): string {
1457
+ const body = entry.body.trim() || "_No details._";
1458
+ const linkLines = [
1459
+ ...entry.supersedes.map((reference) => `- Supersedes: ${reference}`),
1460
+ ...entry.invalidates.map((reference) => `- Invalidates: ${reference}`),
1461
+ ];
1462
+
1463
+ if (linkLines.length === 0) {
1464
+ return body;
1465
+ }
1466
+
1467
+ return [body, "", "## Memory links", "", ...linkLines].join("\n");
1468
+ }
1469
+
1470
+ function normalizeMemoryReferences(input: string[] | undefined): string[] {
1471
+ const seen = new Set<string>();
1472
+ const references: string[] = [];
1473
+
1474
+ for (const value of input ?? []) {
1475
+ const normalized = normalizeInlineText(value);
1476
+ if (!normalized || seen.has(normalized)) {
1477
+ continue;
1478
+ }
1479
+ seen.add(normalized);
1480
+ references.push(normalized);
1481
+ }
1482
+
1483
+ return references;
1484
+ }
1485
+
1486
+ function formatTimestampForFilename(timestamp: string): string {
1487
+ return timestamp.replace(/[:.]/g, "-");
1488
+ }
1489
+
1490
+ function buildCompactionSummaryContent(compaction: PendingCompaction): string {
1491
+ return [
1492
+ "# Compaction summary",
1493
+ "",
1494
+ `- Timestamp: ${compaction.timestamp}`,
1495
+ "- Source: session_compact",
1496
+ "",
1497
+ "## Summary",
1498
+ "",
1499
+ compaction.summary.trim() || "_No summary._",
1500
+ "",
1501
+ ].join("\n");
1502
+ }
1503
+
1504
+ function buildDreamSummaryContent(summary: {
1505
+ timestamp: string;
1506
+ reason?: string;
1507
+ summary: string;
1508
+ updatedBlocks: MemoryBlockName[];
1509
+ undreamedEntries: LogEntry[];
1510
+ pendingCompactions: PendingCompaction[];
1511
+ lastDreamedLogAt: string | null;
1512
+ }): string {
1513
+ const logRange =
1514
+ summary.undreamedEntries.length > 0
1515
+ ? `${summary.undreamedEntries[0]?.timestamp} → ${summary.undreamedEntries.at(-1)?.timestamp}`
1516
+ : "none";
1517
+
1518
+ return [
1519
+ "# Dream summary",
1520
+ "",
1521
+ `- Timestamp: ${summary.timestamp}`,
1522
+ `- Reason: ${normalizeInlineText(summary.reason ?? "") || "automatic or manual consolidation"}`,
1523
+ `- Updated blocks: ${summary.updatedBlocks.join(", ") || "none"}`,
1524
+ `- Undreamed log count: ${summary.undreamedEntries.length}`,
1525
+ `- Undreamed log range: ${logRange}`,
1526
+ `- Pending compaction count: ${summary.pendingCompactions.length}`,
1527
+ `- Last dreamed log timestamp: ${summary.lastDreamedLogAt ?? "none"}`,
1528
+ "",
1529
+ "## Pending compaction timestamps",
1530
+ "",
1531
+ summary.pendingCompactions.length > 0
1532
+ ? summary.pendingCompactions.map((entry) => `- ${entry.timestamp}`).join("\n")
1533
+ : "- none",
1534
+ "",
1535
+ "## Summary",
1536
+ "",
1537
+ summary.summary.trim() || "_No summary._",
1538
+ "",
1539
+ ].join("\n");
1540
+ }
1541
+
1542
+ function normalizePendingCompaction(input: unknown): PendingCompaction | null {
1543
+ if (!input || typeof input !== "object") {
1544
+ return null;
1545
+ }
1546
+
1547
+ const record = input as Record<string, unknown>;
1548
+ const timestamp = record.timestamp;
1549
+ const summary = record.summary;
1550
+ if (typeof timestamp !== "string" || !timestamp.trim() || typeof summary !== "string") {
1551
+ return null;
1552
+ }
1553
+
1554
+ return {
1555
+ timestamp: timestamp.trim(),
1556
+ summary: normalizeMarkdownBlock(summary),
1557
+ };
1558
+ }
1559
+
1560
+ function getCoreLineCount(blocks: CoreBlocks): number {
1561
+ return CORE_BLOCK_NAMES.reduce((total, name) => total + countLines(blocks[name]), 0);
1562
+ }
1563
+
1564
+ function getCoreCharCount(blocks: CoreBlocks): number {
1565
+ return CORE_BLOCK_NAMES.reduce((total, name) => total + countChars(blocks[name]), 0);
1566
+ }
1567
+
1568
+ function countLines(text: string): number {
1569
+ const normalized = text.replace(/\r/g, "").replace(/\n+$/g, "");
1570
+ return normalized ? normalized.split("\n").length : 0;
1571
+ }
1572
+
1573
+ function countChars(text: string): number {
1574
+ return text.replace(/\r/g, "").replace(/\n+$/g, "").length;
1575
+ }
1576
+
1577
+ function normalizeMarkdownBlock(text: string): string {
1578
+ const normalized = text.replace(/\r/g, "").trimEnd();
1579
+ return normalized ? `${normalized}\n` : "";
1580
+ }
1581
+
1582
+ function normalizeTitle(text: string): string {
1583
+ return truncateText(normalizeInlineText(text), 100);
1584
+ }
1585
+
1586
+ function normalizeInlineText(text: unknown): string {
1587
+ return typeof text === "string" ? text.replace(/\s+/g, " ").trim() : "";
1588
+ }
1589
+
1590
+ function truncateText(text: string, maxLength: number): string {
1591
+ if (text.length <= maxLength) {
1592
+ return text;
1593
+ }
1594
+ return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
1595
+ }
1596
+
1597
+ function nowIso(): string {
1598
+ return new Date().toISOString();
1599
+ }
1600
+
1601
+ function ensureMemoryInitialized(initialized: boolean): void {
1602
+ if (!initialized) {
1603
+ throw new Error("Memory is not initialized. Run /memory init first.");
1604
+ }
1605
+ }
1606
+
1607
+ async function ensureDir(dirPath: string): Promise<void> {
1608
+ await fs.mkdir(dirPath, { recursive: true });
1609
+ }
1610
+
1611
+ async function pathExists(filePath: string): Promise<boolean> {
1612
+ try {
1613
+ await fs.access(filePath);
1614
+ return true;
1615
+ } catch {
1616
+ return false;
1617
+ }
1618
+ }
1619
+
1620
+ async function readTextIfExists(filePath: string): Promise<string | undefined> {
1621
+ try {
1622
+ return await fs.readFile(filePath, "utf8");
1623
+ } catch (error) {
1624
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
1625
+ return undefined;
1626
+ }
1627
+ throw error;
1628
+ }
1629
+ }
1630
+
1631
+ async function withMemoryMutationQueue<T>(filePaths: string[], fn: () => Promise<T>): Promise<T> {
1632
+ const uniquePaths = Array.from(
1633
+ new Set(filePaths.map((filePath) => path.resolve(filePath))),
1634
+ ).sort();
1635
+ return acquireQueuedPaths(uniquePaths, 0, fn);
1636
+ }
1637
+
1638
+ async function acquireQueuedPaths<T>(
1639
+ filePaths: string[],
1640
+ index: number,
1641
+ fn: () => Promise<T>,
1642
+ ): Promise<T> {
1643
+ if (index >= filePaths.length) {
1644
+ return fn();
1645
+ }
1646
+
1647
+ return withFileMutationQueue(filePaths[index], () =>
1648
+ acquireQueuedPaths(filePaths, index + 1, fn),
1649
+ );
1650
+ }
1651
+
1652
+ function notify(
1653
+ ctx: Pick<ExtensionContext | ExtensionCommandContext, "hasUI" | "ui">,
1654
+ text: string,
1655
+ level: "info" | "warning" | "error",
1656
+ ): void {
1657
+ if (ctx.hasUI) {
1658
+ ctx.ui.notify(text, level);
1659
+ return;
1660
+ }
1661
+
1662
+ const stream = level === "error" ? process.stderr : process.stdout;
1663
+ stream.write(`${text}\n`);
1664
+ }
1665
+
1666
+ function parsePossiblyWrappedJson(raw: string): unknown {
1667
+ const trimmed = raw.trim();
1668
+ if (!trimmed) {
1669
+ throw new Error("Empty output");
1670
+ }
1671
+
1672
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim();
1673
+ const candidate = fenced || trimmed;
1674
+
1675
+ try {
1676
+ return JSON.parse(candidate);
1677
+ } catch {
1678
+ const firstBrace = candidate.indexOf("{");
1679
+ const lastBrace = candidate.lastIndexOf("}");
1680
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
1681
+ return JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
1682
+ }
1683
+ throw new Error("Output is not valid JSON");
1684
+ }
1685
+ }
1686
+
1687
+ // --- Extension registration ---
1688
+
1689
+ export default function memoryExtension(pi: ExtensionAPI): void {
1690
+ const autoDreamState: AutoDreamState = { promise: null };
1691
+ let pendingStartupDreamCheck = false;
1692
+ let memoryToolsRegistered = false;
1693
+
1694
+ const registerMemoryTools = (): void => {
1695
+ if (memoryToolsRegistered) {
1696
+ return;
1697
+ }
1698
+ memoryToolsRegistered = true;
1699
+
1700
+ pi.registerTool(
1701
+ defineTool({
1702
+ name: "memory_update_block",
1703
+ label: "Memory Update Block",
1704
+ description: `Update one .agents/memory/core block while enforcing the shared ${CORE_LINE_CAP}-line and ${CORE_CHAR_CAP}-character caps`,
1705
+ promptSnippet: `Update one core memory block in .agents/memory/core with ${CORE_LINE_CAP}-line and ${CORE_CHAR_CAP}-character cap enforcement`,
1706
+ promptGuidelines: [
1707
+ "Use this tool when updating .agents/memory/core/directives.md, context.md, focus.md, or pending.md.",
1708
+ `If a write would exceed the ${CORE_LINE_CAP}-line or ${CORE_CHAR_CAP}-character core cap, run memory_dream or remove content first.`,
1709
+ ],
1710
+ parameters: MEMORY_UPDATE_BLOCK_PARAMS,
1711
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1712
+ const result = await updateCoreBlock(ctx.cwd, params.name, params.content);
1713
+
1714
+ return {
1715
+ content: [
1716
+ {
1717
+ type: "text",
1718
+ text: `Updated .agents/memory/core/${params.name}.md (${result.totalLines}/${CORE_LINE_CAP} lines, ${result.totalChars}/${CORE_CHAR_CAP} chars).`,
1719
+ },
1720
+ ],
1721
+ details: {
1722
+ block: params.name,
1723
+ total_lines: result.totalLines,
1724
+ total_chars: result.totalChars,
1725
+ },
1726
+ };
1727
+ },
1728
+ }),
1729
+ );
1730
+
1731
+ pi.registerTool(
1732
+ defineTool({
1733
+ name: "memory_append_log",
1734
+ label: "Memory Append Log",
1735
+ description: "Append an entry to .agents/memory/log.md using the repo memory log format",
1736
+ promptSnippet: "Append an entry to the repo memory log at .agents/memory/log.md",
1737
+ promptGuidelines: [
1738
+ "Use this tool for important decisions, discoveries, plans, experiments, prompt ingests, and attachment additions worth remembering.",
1739
+ "Use importance labels high, medium, or low.",
1740
+ "If this entry replaces or corrects prior memory, set supersedes and/or invalidates links explicitly.",
1741
+ "The memory log is append-only. Do not rewrite or truncate older entries.",
1742
+ ],
1743
+ parameters: MEMORY_APPEND_LOG_PARAMS,
1744
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1745
+ const entry = await appendMemoryLog(ctx.cwd, {
1746
+ type: params.type,
1747
+ title: params.title,
1748
+ body: params.body,
1749
+ importance: params.importance,
1750
+ supersedes: params.supersedes,
1751
+ invalidates: params.invalidates,
1752
+ });
1753
+
1754
+ return {
1755
+ content: [
1756
+ {
1757
+ type: "text",
1758
+ text: `Appended .agents/memory/log.md entry: ${entry.title}.`,
1759
+ },
1760
+ ],
1761
+ details: entry,
1762
+ };
1763
+ },
1764
+ }),
1765
+ );
1766
+
1767
+ pi.registerTool(
1768
+ defineTool({
1769
+ name: "memory_dream",
1770
+ label: "Memory Dream",
1771
+ description:
1772
+ "Consolidate repo memory into .agents/memory/core from newer log entries and compaction context",
1773
+ promptSnippet: "Consolidate repo memory with dream-based core compression",
1774
+ promptGuidelines: [
1775
+ "Dream is the only consolidation mechanism for .agents/memory/core.",
1776
+ "Use this tool when logs have accumulated, core needs compression, or recent compaction context should be folded into memory.",
1777
+ ],
1778
+ parameters: MEMORY_DREAM_PARAMS,
1779
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1780
+ return toolDream(ctx.cwd, ctx, params.reason);
1781
+ },
1782
+ }),
1783
+ );
1784
+ };
1785
+
1786
+ const maybeRegisterMemoryTools = async (cwd: string): Promise<void> => {
1787
+ if (memoryToolsRegistered) {
1788
+ return;
1789
+ }
1790
+
1791
+ if (await pathExists(getMemoryPaths(cwd).memoryRoot)) {
1792
+ registerMemoryTools();
1793
+ }
1794
+ };
1795
+
1796
+ pi.registerCommand("memory", {
1797
+ description: "Manage project-local memory in .agents/memory/",
1798
+ getArgumentCompletions: getMemoryArgumentCompletions,
1799
+ handler: async (args, ctx) => {
1800
+ const parsed = parseMemoryCommandArgs(args);
1801
+ if (!parsed) {
1802
+ notify(ctx, "Usage: /memory init | status | dream [reason]", "warning");
1803
+ return;
1804
+ }
1805
+
1806
+ switch (parsed.command) {
1807
+ case "init": {
1808
+ const result = await initMemory(ctx.cwd);
1809
+ registerMemoryTools();
1810
+ notify(ctx, formatInitResult(result), "info");
1811
+ return;
1812
+ }
1813
+ case "status": {
1814
+ const status = await loadMemoryStatus(ctx.cwd);
1815
+ notify(ctx, formatMemoryStatus(status), "info");
1816
+ return;
1817
+ }
1818
+ case "dream": {
1819
+ await toolDream(ctx.cwd, ctx, parsed.text || undefined);
1820
+ return;
1821
+ }
1822
+ }
1823
+ },
1824
+ });
1825
+
1826
+ pi.on("before_agent_start", async (event, ctx) => {
1827
+ try {
1828
+ if (autoDreamState.promise) {
1829
+ await autoDreamState.promise;
1830
+ }
1831
+
1832
+ const prompt = await loadMemoryPrompt(ctx.cwd);
1833
+ if (!prompt) {
1834
+ return;
1835
+ }
1836
+
1837
+ return {
1838
+ systemPrompt: `${event.systemPrompt}\n\n${prompt}`,
1839
+ };
1840
+ } catch (error) {
1841
+ if (error instanceof MemoryReadmeMissingError) {
1842
+ notify(ctx, `Repo memory disabled: ${error.message}`, "warning");
1843
+ return;
1844
+ }
1845
+ throw error;
1846
+ }
1847
+ });
1848
+
1849
+ pi.on("session_start", async (_event, ctx) => {
1850
+ await maybeRegisterMemoryTools(ctx.cwd);
1851
+ pendingStartupDreamCheck = !(await maybeScheduleAutoDream(
1852
+ ctx.cwd,
1853
+ ctx,
1854
+ "session_start",
1855
+ autoDreamState,
1856
+ ));
1857
+ });
1858
+
1859
+ pi.on("model_select", async (event, ctx) => {
1860
+ if (!pendingStartupDreamCheck) {
1861
+ return;
1862
+ }
1863
+
1864
+ if (event.source !== "restore" && event.previousModel) {
1865
+ return;
1866
+ }
1867
+
1868
+ pendingStartupDreamCheck = false;
1869
+ await maybeScheduleAutoDream(ctx.cwd, ctx, "session_start", autoDreamState);
1870
+ });
1871
+
1872
+ pi.on("session_compact", async (event, ctx) => {
1873
+ const paths = getMemoryPaths(ctx.cwd);
1874
+ if (!(await pathExists(paths.memoryRoot))) {
1875
+ return;
1876
+ }
1877
+
1878
+ const pendingCompaction = {
1879
+ timestamp: event.compactionEntry.timestamp,
1880
+ summary: event.compactionEntry.summary,
1881
+ };
1882
+
1883
+ await Promise.all([
1884
+ enqueuePendingCompaction(ctx.cwd, pendingCompaction),
1885
+ writeCompactionSummary(ctx.cwd, pendingCompaction),
1886
+ ]);
1887
+ await maybeScheduleAutoDream(ctx.cwd, ctx, "session_compact", autoDreamState);
1888
+ });
1889
+ }