pi-orchestrator-config 4.2.0 → 4.3.2

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 (100) hide show
  1. package/README.md +76 -67
  2. package/agents/code-reviewer-docs.md +11 -1
  3. package/agents/code-reviewer-guidelines.md +11 -1
  4. package/agents/code-reviewer-quality.md +11 -1
  5. package/agents/code-reviewer-security.md +11 -1
  6. package/agents/code-reviewer-spec.md +11 -1
  7. package/agents/git-expert.md +42 -3
  8. package/agents/github-expert.md +11 -0
  9. package/extensions/acpx-provider/runtime-models.ts +18 -6
  10. package/extensions/cli-provider/agents/claude.ts +3 -2
  11. package/extensions/cli-provider/agents/cursor.ts +3 -5
  12. package/extensions/cli-provider/agents/gemini.ts +3 -2
  13. package/extensions/cli-provider/discover.ts +7 -7
  14. package/extensions/cli-provider/index.ts +5 -0
  15. package/extensions/cli-provider/runtime-models.ts +18 -6
  16. package/extensions/cli-provider/session-reaper.ts +5 -7
  17. package/extensions/cli-provider/sessions.ts +4 -0
  18. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  19. package/extensions/coms/coms-p2p.ts +1719 -423
  20. package/extensions/coms/coms-shared.ts +261 -39
  21. package/extensions/coms/coms-wrapper.ts +100 -62
  22. package/extensions/coms/index.ts +8 -4
  23. package/extensions/coms/mixed-turn.ts +26 -0
  24. package/extensions/coms/probe-socket.ts +42 -0
  25. package/extensions/coms/themeMap.ts +0 -1
  26. package/extensions/image-gen/image-gen.ts +19 -1
  27. package/extensions/orchestrator/async-agents.ts +563 -102
  28. package/extensions/orchestrator/async-capability.ts +8 -8
  29. package/extensions/orchestrator/cron.ts +15 -9
  30. package/extensions/orchestrator/dreaming.ts +32 -25
  31. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  32. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  33. package/extensions/orchestrator/enforcement.ts +80 -27
  34. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  35. package/extensions/orchestrator/git-helpers.ts +2 -2
  36. package/extensions/orchestrator/index.ts +78 -10
  37. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  38. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  39. package/extensions/orchestrator/memory-provenance.ts +29 -25
  40. package/extensions/orchestrator/memory-scoring.ts +23 -18
  41. package/extensions/orchestrator/memory-tools.ts +10 -3
  42. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  43. package/extensions/orchestrator/parse-model-override.ts +29 -0
  44. package/extensions/orchestrator/pi-config-review-state.ts +57 -33
  45. package/extensions/orchestrator/project-settings.ts +333 -127
  46. package/extensions/orchestrator/resolve-agent-model.ts +24 -13
  47. package/extensions/orchestrator/review-porcelain.ts +4 -0
  48. package/extensions/orchestrator/review-ui.ts +63 -15
  49. package/extensions/orchestrator/rule-placeholders.ts +411 -2
  50. package/extensions/orchestrator/rules.ts +80 -132
  51. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  52. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  53. package/extensions/orchestrator/settings-tui.ts +636 -0
  54. package/extensions/orchestrator/state-jsonl.ts +431 -0
  55. package/extensions/orchestrator/subagent-tool.ts +157 -59
  56. package/extensions/orchestrator/sync-limit.ts +23 -0
  57. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  58. package/extensions/orchestrator/utils.ts +8 -5
  59. package/extensions/pidash/pidash.ts +193 -156
  60. package/extensions/pidiff/pidiff.ts +96 -38
  61. package/extensions/pidiff/spawn-lock.ts +62 -0
  62. package/extensions/pitasks/index.ts +430 -0
  63. package/extensions/pitasks/reminders.ts +37 -0
  64. package/extensions/pitasks/task-store.ts +467 -0
  65. package/extensions/pitasks/task-tools.ts +375 -0
  66. package/extensions/pitasks/task-widget.ts +181 -0
  67. package/extensions/pitasks/types.ts +38 -0
  68. package/extensions/providers/acpx-driver.ts +12 -2
  69. package/extensions/providers/built-in-drivers.ts +6 -6
  70. package/extensions/providers/claude-driver.ts +1 -1
  71. package/extensions/providers/cursor-cli-driver.ts +1 -1
  72. package/extensions/providers/gemini-driver.ts +1 -1
  73. package/extensions/providers/index.ts +144 -26
  74. package/extensions/providers/initialized-guard.ts +27 -0
  75. package/extensions/providers/restore-default-model.ts +596 -0
  76. package/extensions/providers/session-shutdown.ts +39 -0
  77. package/extensions/shared/coms-active.ts +23 -0
  78. package/extensions/shared/create-runtime-provider.ts +2 -0
  79. package/extensions/shared/daemon-manager.ts +0 -8
  80. package/extensions/shared/file-logger.ts +132 -25
  81. package/extensions/shared/logger.ts +50 -0
  82. package/extensions/shared/models-dev.ts +421 -0
  83. package/extensions/shared/oneshot.ts +116 -0
  84. package/extensions/shared/resolve-binary.ts +191 -0
  85. package/extensions/shared/types.ts +3 -0
  86. package/package.json +9 -3
  87. package/prompts/coderabbit-rate-limit.md +2 -0
  88. package/rules/00-orchestrator-core.md +3 -0
  89. package/rules/10-agent-routing.md +12 -1
  90. package/rules/20-code-review-loop.md +52 -25
  91. package/rules/30-prompt-templates.md +4 -0
  92. package/rules/35-memory.md +3 -3
  93. package/rules/40-critical-rules.md +3 -0
  94. package/rules/55-coms-protocol.md +84 -18
  95. package/rules/60-task-tracking.md +2 -0
  96. package/settings-keys.json +563 -0
  97. package/templates/coms-feature-manager-prompt.md +4 -6
  98. package/extensions/coms/coms-net-server.ts +0 -1642
  99. package/extensions/coms/coms-net-wrapper.ts +0 -521
  100. package/extensions/coms/coms-net.ts +0 -1589
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Markdown transformer for rich TUI display formatting.
3
+ *
4
+ * Transforms assistant message markdown for display only —
5
+ * doesn't affect stored messages or model context.
6
+ *
7
+ * Uses pi 0.84.0's registerMarkdownTransformer() API.
8
+ */
9
+
10
+ import type {
11
+ ExtensionAPI,
12
+ MarkdownTransformContext,
13
+ } from "@earendil-works/pi-coding-agent";
14
+
15
+ // ── Badge definitions ──
16
+
17
+ /** Memory category → styled badge (inline code for visual distinction in TUI) */
18
+ export const MEMORY_BADGES: Record<string, string> = {
19
+ preference: "`🎯 pref`",
20
+ lesson: "`📝 lesson`",
21
+ mistake: "`⚠️ mistake`",
22
+ pattern: "`🔄 pattern`",
23
+ decision: "`📌 decision`",
24
+ done: "`✅ done`",
25
+ };
26
+
27
+ /** Memory section header → emoji prefix */
28
+ const SECTION_ICONS: Record<string, string> = {
29
+ "Active Preferences": "🎯",
30
+ "Active Lessons": "📝",
31
+ "Vetoes & Mistakes": "⚠️",
32
+ Patterns: "🔄",
33
+ "Recent Decisions": "📌",
34
+ "Recent Completions": "✅",
35
+ Pinned: "📌",
36
+ "Project Memory": "🧠",
37
+ };
38
+
39
+ /** Severity → styled badge for review findings */
40
+ export const SEVERITY_BADGES: Record<string, string> = {
41
+ CRITICAL: "🔴 **CRITICAL**",
42
+ WARNING: "🟡 **WARNING**",
43
+ SUGGESTION: "🟢 SUGGESTION",
44
+ INFO: "🔵 INFO",
45
+ };
46
+
47
+ /** Task status → styled badge */
48
+ export const TASK_STATUS_BADGES: Record<string, string> = {
49
+ pending: "⏳",
50
+ in_progress: "🔄",
51
+ completed: "✅",
52
+ deleted: "🗑️",
53
+ };
54
+
55
+ /** Async agent status → styled badge */
56
+ export const ASYNC_STATUS_BADGES: Record<string, string> = {
57
+ running: "🟡 **running**",
58
+ queued: "🟡 queued",
59
+ complete: "🟢 **done**",
60
+ failed: "🔴 **failed**",
61
+ killed: "⚪ killed",
62
+ };
63
+
64
+ // ── Code block protection ──
65
+
66
+ /**
67
+ * Apply a transform function only to text outside fenced code blocks.
68
+ * Code blocks (``` or ~~~) are preserved as-is.
69
+ */
70
+ export function transformOutsideCodeBlocks(
71
+ markdown: string,
72
+ transform: (text: string) => string,
73
+ ): string {
74
+ const parts = markdown.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~)/g);
75
+ return parts
76
+ .map((part, i) => (i % 2 === 1 ? part : transform(part)))
77
+ .join("");
78
+ }
79
+
80
+ /** Escape markdown metacharacters in a string to prevent formatting corruption. */
81
+ function escapeMarkdown(text: string): string {
82
+ return text.replace(/([*_~`\\[\]()#>+\-!|{}])/g, "\\$1");
83
+ }
84
+
85
+ /**
86
+ * Wrap text in a CommonMark inline code span, safely handling backticks.
87
+ * Uses a delimiter longer than the longest backtick run in the content.
88
+ */
89
+ function inlineCode(text: string): string {
90
+ const maxRun = (text.match(/`+/g) || []).reduce(
91
+ (max, run) => Math.max(max, run.length),
92
+ 0,
93
+ );
94
+ const delim = "`".repeat(maxRun + 1);
95
+ // CommonMark requires a space after/before delimiter when content starts/ends with backtick
96
+ const needsSpace = text.startsWith("`") || text.endsWith("`");
97
+ return needsSpace ? `${delim} ${text} ${delim}` : `${delim}${text}${delim}`;
98
+ }
99
+
100
+ // ── Transform functions (exported for testing) ──
101
+
102
+ /**
103
+ * Transform memory category tags into styled badges.
104
+ * Matches `- [category] text` in memory list items.
105
+ */
106
+ export function transformMemoryBadges(markdown: string): string {
107
+ return transformOutsideCodeBlocks(markdown, (text) =>
108
+ text.replace(
109
+ /^(- )\[(preference|lesson|mistake|pattern|decision|done)\] /gm,
110
+ (_match, prefix, category) => {
111
+ const badge = MEMORY_BADGES[category];
112
+ return badge ? `${prefix}${badge} ` : _match;
113
+ },
114
+ ),
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Transform memory section headers with emoji icons.
120
+ * `## Active Preferences` → `## 🎯 Active Preferences`
121
+ *
122
+ * Only matches exact section header lines (end-of-line or followed by ` [`
123
+ * for the Project Memory capacity header).
124
+ */
125
+ export function transformMemorySectionHeaders(markdown: string): string {
126
+ const sectionPattern = Object.keys(SECTION_ICONS)
127
+ .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
128
+ .join("|");
129
+ const re = new RegExp(
130
+ `^(#{1,3}) (${sectionPattern})(?= \\[|$)`,
131
+ "gm",
132
+ );
133
+ return transformOutsideCodeBlocks(markdown, (text) =>
134
+ text.replace(re, (_match, hashes, section) => {
135
+ const icon = SECTION_ICONS[section];
136
+ return icon ? `${hashes} ${icon} ${section}` : _match;
137
+ }),
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Transform coms message headers with styled formatting.
143
+ * `[from peer @ /path/to/cwd]` → `📨 **peer** \`@ /path/to/cwd\``
144
+ * `[from peer → self @ /path/to/cwd]` → `📨 **peer** → **self** \`@ /path/to/cwd\``
145
+ */
146
+ export function transformComsHeaders(markdown: string): string {
147
+ return transformOutsideCodeBlocks(markdown, (text) =>
148
+ text.replace(
149
+ /\[from (\S+)(?: → (\S+))? @ ([^\]]+)\]/g,
150
+ (_match, peer, self, cwd) => {
151
+ const arrow = self ? ` → **${escapeMarkdown(self)}**` : "";
152
+ return `📨 **${escapeMarkdown(peer)}**${arrow} ${inlineCode(`@ ${cwd.trim()}`)}`;
153
+ },
154
+ ),
155
+ );
156
+ }
157
+
158
+ // ── Review findings (Deliverable 3) ──
159
+
160
+ /** Shape of a single review finding from code reviewers */
161
+ interface ReviewFinding {
162
+ severity?: string;
163
+ file?: string;
164
+ line?: number;
165
+ description?: string;
166
+ suggestion?: string;
167
+ impact?: string;
168
+ rule?: string;
169
+ }
170
+
171
+ /**
172
+ * Transform review findings JSON blocks into readable tables.
173
+ * Matches ```json blocks containing {"findings": [...]} and renders
174
+ * each finding with severity badges, file:line refs, and descriptions.
175
+ */
176
+ export function transformReviewFindings(markdown: string): string {
177
+ return markdown.replace(
178
+ /```(?:json)?\s*\n(\{[\s\S]*?"findings"\s*:\s*\[[\s\S]*?\]\s*\})\s*\n```/g,
179
+ (_match, jsonStr) => {
180
+ try {
181
+ const parsed = JSON.parse(jsonStr);
182
+ if (!parsed || !Array.isArray(parsed.findings)) return _match;
183
+ const findings: ReviewFinding[] = parsed.findings;
184
+ if (findings.length === 0) return "✅ **No findings** — review passed";
185
+
186
+ const lines: string[] = [`**${findings.length} finding${findings.length !== 1 ? "s" : ""}:**`, ""];
187
+ for (const f of findings) {
188
+ const sev = SEVERITY_BADGES[(f.severity || "INFO").toUpperCase()] || f.severity || "INFO";
189
+ const loc = f.file ? `\`${f.file}${f.line ? `:${f.line}` : ""}\`` : "";
190
+ lines.push(`${sev}${loc ? ` — ${loc}` : ""}`);
191
+ if (f.description) lines.push(` ${f.description}`);
192
+ if (f.suggestion) lines.push(` 💡 ${f.suggestion}`);
193
+ if (f.impact) lines.push(` ⚡ ${f.impact}`);
194
+ lines.push("");
195
+ }
196
+ return lines.join("\n");
197
+ } catch {
198
+ return _match;
199
+ }
200
+ },
201
+ );
202
+ }
203
+
204
+ // ── Settings display (Deliverable 4) ──
205
+
206
+ /**
207
+ * Transform settings display output into a styled table.
208
+ * Matches lines like `key = value (source: project)` or `key: value`
209
+ * from settings get output.
210
+ */
211
+ export function transformSettingsDisplay(markdown: string): string {
212
+ return transformOutsideCodeBlocks(markdown, (text) => {
213
+ // Match blocks of settings lines: `key = value (source: ...)` pattern
214
+ return text.replace(
215
+ /^([ \t]*)([\w.]+)\s*=\s*(.+?)\s*\(source:\s*(\w+)\)\s*$/gm,
216
+ (_match, indent, key, value, source) => {
217
+ const sourceIcon = source === "project" ? "📁" : source === "global" ? "🌐" : source === "env" ? "🔧" : "⚙️";
218
+ return `${indent}\`${key}\` = **${value.trim()}** ${sourceIcon} _${source}_`;
219
+ },
220
+ );
221
+ });
222
+ }
223
+
224
+ // ── Task list formatting (Deliverable 5) ──
225
+
226
+ /**
227
+ * Transform task list status markers into styled badges.
228
+ * Matches patterns like `status: pending`, `status: in_progress`, `status: completed`
229
+ * in task list output lines, and also `[pending]`, `[in_progress]`, `[completed]` markers.
230
+ */
231
+ export function transformTaskStatus(markdown: string): string {
232
+ return transformOutsideCodeBlocks(markdown, (text) => {
233
+ // Match `status: <value>` patterns
234
+ let result = text.replace(
235
+ /\bstatus:\s*(pending|in_progress|completed|deleted)\b/g,
236
+ (_match, status) => {
237
+ const badge = TASK_STATUS_BADGES[status];
238
+ return badge ? `status: ${badge} ${status}` : _match;
239
+ },
240
+ );
241
+ // Match `[pending]`, `[in_progress]`, `[completed]` standalone markers
242
+ result = result.replace(
243
+ /\[(pending|in_progress|completed|deleted)\](?!\()/g,
244
+ (_match, status) => {
245
+ const badge = TASK_STATUS_BADGES[status];
246
+ return badge ? `${badge} ${status}` : _match;
247
+ },
248
+ );
249
+ return result;
250
+ });
251
+ }
252
+
253
+ // ── Async agent status formatting (Deliverable 6) ──
254
+
255
+ /**
256
+ * Transform async agent status indicators into styled badges.
257
+ * Matches patterns like `status: running`, `[running]`, `[complete]`, etc.
258
+ * Also formats duration strings like `duration: 45s` or `(12.3s)`.
259
+ */
260
+ export function transformAsyncStatus(markdown: string): string {
261
+ return transformOutsideCodeBlocks(markdown, (text) => {
262
+ // Match `status: <value>` patterns for async agents
263
+ let result = text.replace(
264
+ /\bstatus:\s*(running|queued|complete|failed|killed)\b/g,
265
+ (_match, status) => {
266
+ const badge = ASYNC_STATUS_BADGES[status];
267
+ return badge ? `status: ${badge}` : _match;
268
+ },
269
+ );
270
+ // Match `[running]`, `[complete]`, `[failed]` standalone markers
271
+ result = result.replace(
272
+ /\[(running|queued|complete|failed|killed)\](?!\()/g,
273
+ (_match, status) => {
274
+ const badge = ASYNC_STATUS_BADGES[status];
275
+ return badge ? badge : _match;
276
+ },
277
+ );
278
+ return result;
279
+ });
280
+ }
281
+
282
+ // ── Main dispatcher ──
283
+
284
+ /**
285
+ * Main markdown transformer — dispatches to individual transformers.
286
+ * Only transforms completed assistant messages; skips streaming to avoid flicker.
287
+ */
288
+ export function transformMarkdown(
289
+ markdown: string,
290
+ context: MarkdownTransformContext,
291
+ ): string {
292
+ if (context.messageType !== "assistant" || context.isStreaming) {
293
+ return markdown;
294
+ }
295
+
296
+ let result = markdown;
297
+ result = transformMemoryBadges(result);
298
+ result = transformMemorySectionHeaders(result);
299
+ result = transformComsHeaders(result);
300
+ result = transformSettingsDisplay(result);
301
+ result = transformTaskStatus(result);
302
+ result = transformAsyncStatus(result);
303
+ // Review findings last — its output should not be mutated by other transforms
304
+ result = transformReviewFindings(result);
305
+ return result;
306
+ }
307
+
308
+ // ── Registration ──
309
+
310
+ export function registerMarkdownTransformer(pi: ExtensionAPI): void {
311
+ if (typeof (pi as any).registerMarkdownTransformer !== "function") return;
312
+ pi.registerMarkdownTransformer(transformMarkdown);
313
+ }
@@ -3,15 +3,17 @@
3
3
  *
4
4
  * Embeds memory entries and search queries using Xenova/bge-small-en-v1.5 via
5
5
  * @huggingface/transformers (in-process ONNX, no Python, no subprocess).
6
- * Stores embeddings in .pi/memory/embeddings.json.
6
+ * Stores embeddings in .pi/memory/embeddings.jsonl (JSONL persistence, issue #724).
7
+ * Legacy embeddings.json auto-migrated on first access.
7
8
  *
8
9
  * Falls back gracefully when the model is unavailable — callers always get a
9
10
  * result (possibly empty) and never throw.
10
11
  */
11
12
 
12
- import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { createHash } from "node:crypto";
15
+ import { createCachedStore } from "./state-jsonl.js";
16
+ import type { JsonlStateStore } from "./state-jsonl.js";
15
17
 
16
18
  const EMBEDDING_DIM = 384;
17
19
  export const EMBEDDING_POOLING = "mean";
@@ -31,41 +33,41 @@ interface EmbeddingStore {
31
33
  entries: Record<string, number[]>; // text hash → vector
32
34
  }
33
35
 
36
+ const STORE_FILENAME_JSONL = "embeddings.jsonl";
37
+ const LEGACY_STORE_FILENAME = "embeddings.json";
38
+
39
+ function getEmbeddingStore(cwd: string): JsonlStateStore<EmbeddingStore> {
40
+ return createCachedStore<EmbeddingStore>(
41
+ join(cwd, ".pi", "memory"), STORE_FILENAME_JSONL, LEGACY_STORE_FILENAME, { compactThreshold: 30 },
42
+ );
43
+ }
44
+
34
45
  function getStorePath(cwd: string): string {
35
- return join(cwd, ".pi", "memory", "embeddings.json");
46
+ return join(cwd, ".pi", "memory", STORE_FILENAME_JSONL);
36
47
  }
37
48
 
38
49
  function loadStore(cwd: string): EmbeddingStore {
39
- const storePath = getStorePath(cwd);
40
- if (existsSync(storePath)) {
41
- try {
42
- const store = JSON.parse(readFileSync(storePath, "utf-8")) as EmbeddingStore;
43
- // Invalidate cache when pooling strategy changes — CLS and mean vectors are incompatible
44
- if (store.pooling !== EMBEDDING_POOLING) {
45
- console.debug(`[memory-embeddings] pooling changed (${store.pooling ?? "cls"} → ${EMBEDDING_POOLING}), clearing ${Object.keys(store.entries).length} cached embeddings`);
46
- store.entries = {};
47
- store.pooling = EMBEDDING_POOLING;
48
- queryCache.clear();
49
- saveStore(cwd, store);
50
- }
51
- return store;
52
- } catch {
53
- // Corrupted file — start fresh
50
+ const store = getEmbeddingStore(cwd);
51
+ const data = store.read();
52
+ if (data !== null) {
53
+ // Invalidate cache when pooling strategy changes — CLS and mean vectors are incompatible
54
+ if (data.pooling !== EMBEDDING_POOLING) {
55
+ // pooling strategy changed — clear incompatible cached embeddings
56
+ data.entries = {};
57
+ data.pooling = EMBEDDING_POOLING;
58
+ queryCache.clear();
59
+ saveStore(cwd, data);
54
60
  }
61
+ return data;
55
62
  }
56
63
  return { model: "Xenova/bge-small-en-v1.5", dim: EMBEDDING_DIM, pooling: EMBEDDING_POOLING, entries: {} };
57
64
  }
58
65
 
59
66
  function saveStore(cwd: string, store: EmbeddingStore): void {
60
67
  try {
61
- const dir = join(cwd, ".pi", "memory");
62
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
63
- const target = getStorePath(cwd);
64
- const tmp = target + ".tmp";
65
- writeFileSync(tmp, JSON.stringify(store), "utf-8");
66
- renameSync(tmp, target);
67
- } catch (err: any) {
68
- console.debug("[memory-embeddings] save failed:", err?.message?.slice(0, 100));
68
+ getEmbeddingStore(cwd).write(store);
69
+ } catch {
70
+ // save failed — non-fatal, embeddings will be recomputed on next access
69
71
  }
70
72
  }
71
73
 
@@ -92,8 +94,8 @@ async function getPipeline(): Promise<any> {
92
94
  });
93
95
  pipelineInstance = extractor;
94
96
  return extractor;
95
- } catch (err: any) {
96
- console.debug("[memory-embeddings] model init failed:", err?.message?.slice(0, 100));
97
+ } catch {
98
+ // model init failed — vector search will be unavailable
97
99
  // Don't permanently disable — allow retry on next call
98
100
  return null;
99
101
  } finally {
@@ -123,8 +125,8 @@ async function embed(texts: string[]): Promise<number[][] | null> {
123
125
  vectors.push(Array.from(result[i].data as Float32Array));
124
126
  }
125
127
  return vectors;
126
- } catch (err: any) {
127
- console.debug("[memory-embeddings] embed failed:", err?.message?.slice(0, 100));
128
+ } catch {
129
+ // embed failed — return null, caller handles gracefully
128
130
  return null;
129
131
  }
130
132
  }
@@ -189,8 +191,8 @@ export async function embedEntry(cwd: string, text: string, category?: string):
189
191
 
190
192
  store.entries[hash] = vectors[0];
191
193
  saveStore(cwd, store);
192
- } catch (err: any) {
193
- console.debug("[memory-embeddings] embedEntry failed:", err?.message?.slice(0, 100));
194
+ } catch {
195
+ // embedEntry failed — non-fatal
194
196
  }
195
197
  }
196
198
 
@@ -205,8 +207,8 @@ export function removeEmbedding(cwd: string, text: string, category?: string): v
205
207
  delete store.entries[hash];
206
208
  saveStore(cwd, store);
207
209
  }
208
- } catch (err: any) {
209
- console.debug("[memory-embeddings] removeEmbedding failed:", err?.message?.slice(0, 100));
210
+ } catch {
211
+ // removeEmbedding failed — non-fatal, stale embedding is harmless
210
212
  }
211
213
  }
212
214
 
@@ -1,14 +1,18 @@
1
1
  /**
2
- * Provenance pending merge — dream writes a sidecar JSON; onComplete merges
3
- * into memory-scores.json without asking the LLM to edit scores JSON.
2
+ * Provenance pending merge — dream writes a sidecar JSONL; onComplete merges
3
+ * into memory-scores.jsonl without asking the LLM to edit scores JSONL.
4
4
  *
5
- * Sidecar: `.pi/memory/provenance-pending.json`
6
- * Shape: { entries: [{ category, text, sourceSession?, derivedFrom?, informs? }] }
5
+ * Sidecar: `.pi/memory/provenance-pending.jsonl`
6
+ * Shape per line: { category, text, sourceSession?, derivedFrom?, informs? }
7
+ *
8
+ * Persistence: JsonlStateStore (issue #724). Legacy JSON auto-migrated.
7
9
  */
8
10
 
9
- import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
10
- import { dirname, join } from "node:path";
11
+ import { unlinkSync } from "node:fs";
12
+ import { join } from "node:path";
11
13
  import { entryHash, loadScores, saveScores, type MemoryCategory } from "./memory-scoring.js";
14
+ import { createCachedStore } from "./state-jsonl.js";
15
+ import type { JsonlStateStore } from "./state-jsonl.js";
12
16
 
13
17
  export interface ProvenancePendingItem {
14
18
  category: MemoryCategory | string;
@@ -22,39 +26,38 @@ export interface ProvenancePendingFile {
22
26
  entries: ProvenancePendingItem[];
23
27
  }
24
28
 
29
+ const PROVENANCE_FILENAME_JSONL = "provenance-pending.jsonl";
30
+ const LEGACY_PROVENANCE_FILENAME = "provenance-pending.json";
31
+
32
+ function getProvenanceStore(cwd: string): JsonlStateStore<ProvenancePendingFile> {
33
+ return createCachedStore<ProvenancePendingFile>(
34
+ join(cwd, ".pi", "memory"), PROVENANCE_FILENAME_JSONL, LEGACY_PROVENANCE_FILENAME,
35
+ );
36
+ }
37
+
25
38
  export function getProvenancePendingPath(cwd: string): string {
26
- return join(cwd, ".pi", "memory", "provenance-pending.json");
39
+ return join(cwd, ".pi", "memory", PROVENANCE_FILENAME_JSONL);
27
40
  }
28
41
 
29
42
  export function writeProvenancePending(
30
43
  cwd: string,
31
44
  entries: ProvenancePendingItem[],
32
45
  ): void {
33
- const path = getProvenancePendingPath(cwd);
34
- const dir = dirname(path);
35
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
46
  const payload: ProvenancePendingFile = { entries };
37
- writeFileSync(path, JSON.stringify(payload, null, 2) + "\n", "utf-8");
47
+ getProvenanceStore(cwd).write(payload);
38
48
  }
39
49
 
40
50
  /**
41
- * Merge provenance-pending.json into scores, then delete the sidecar.
51
+ * Merge provenance-pending.jsonl into scores, then delete the sidecar.
42
52
  * Returns number of entries updated.
43
53
  */
44
54
  export function mergeProvenancePending(cwd: string): number {
45
- const path = getProvenancePendingPath(cwd);
46
- if (!existsSync(path)) return 0;
47
-
48
- let pending: ProvenancePendingFile;
49
- try {
50
- pending = JSON.parse(readFileSync(path, "utf-8")) as ProvenancePendingFile;
51
- } catch (e: any) {
52
- console.debug("[memory-provenance] parse failed:", e?.message || e);
53
- return 0;
54
- }
55
-
55
+ const store = getProvenanceStore(cwd);
56
+ const pending = store.read();
56
57
  if (!pending?.entries?.length) {
57
- try { unlinkSync(path); } catch { /* ignore */ }
58
+ // Clean up empty/absent sidecar
59
+ const p = getProvenancePendingPath(cwd);
60
+ try { unlinkSync(p); } catch { /* ignore */ }
58
61
  return 0;
59
62
  }
60
63
 
@@ -75,6 +78,7 @@ export function mergeProvenancePending(cwd: string): number {
75
78
 
76
79
  if (updated > 0) saveScores(cwd, scores);
77
80
 
78
- try { unlinkSync(path); } catch { /* ignore */ }
81
+ // Delete the consumed sidecar
82
+ try { unlinkSync(getProvenancePendingPath(cwd)); } catch { /* ignore */ }
79
83
  return updated;
80
84
  }
@@ -9,11 +9,16 @@
9
9
  * Clean-room TypeScript implementation under MIT — not a code translation.
10
10
  */
11
11
 
12
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
13
- import { join, dirname } from "node:path";
12
+ import { createLogger } from "../shared/logger.js";
13
+ import { join } from "node:path";
14
+ import { createCachedStore } from "./state-jsonl.js";
15
+ import type { JsonlStateStore } from "./state-jsonl.js";
14
16
 
15
17
  // ── Types ──────────────────────────────────────────────────────────────────
16
18
 
19
+ const memLog = createLogger("memory");
20
+ memLog.debug("memory-scoring module loaded");
21
+
17
22
  export type MemoryCategory = "preference" | "lesson" | "pattern" | "decision" | "done" | "mistake";
18
23
  export type CueType = "explicit" | "structural" | "behavioral" | "recurrence";
19
24
  export type UserState = "auto" | "pinned" | "forgotten";
@@ -175,33 +180,33 @@ export function entryHash(text: string): string {
175
180
  }
176
181
 
177
182
  // ── Scores File I/O ────────────────────────────────────────────────────────
183
+ // Persistence: append-only JSONL via JsonlStateStore (issue #724).
184
+ // Legacy memory-scores.json is auto-migrated on first access.
185
+
186
+ const SCORES_FILENAME_JSONL = "memory-scores.jsonl";
187
+ const LEGACY_SCORES_FILENAME = "memory-scores.json";
178
188
 
179
- const SCORES_FILENAME = "memory-scores.json";
189
+ function getScoresStore(cwd: string): JsonlStateStore<ScoresFile> {
190
+ return createCachedStore<ScoresFile>(
191
+ join(cwd, ".pi", "memory"), SCORES_FILENAME_JSONL, LEGACY_SCORES_FILENAME, { compactThreshold: 50 },
192
+ );
193
+ }
180
194
 
181
195
  export function getScoresPath(cwd: string): string {
182
- return join(cwd, ".pi", "memory", SCORES_FILENAME);
196
+ return join(cwd, ".pi", "memory", SCORES_FILENAME_JSONL);
183
197
  }
184
198
 
185
199
  export function loadScores(cwd: string): ScoresFile {
186
- const path = getScoresPath(cwd);
187
- if (!existsSync(path)) {
188
- return { entries: {}, lastRebuild: new Date().toISOString() };
189
- }
190
- try {
191
- const raw = readFileSync(path, "utf-8");
192
- return JSON.parse(raw) as ScoresFile;
193
- } catch {
200
+ const store = getScoresStore(cwd);
201
+ const data = store.read();
202
+ if (data === null) {
194
203
  return { entries: {}, lastRebuild: new Date().toISOString() };
195
204
  }
205
+ return data;
196
206
  }
197
207
 
198
208
  export function saveScores(cwd: string, scores: ScoresFile): void {
199
- const path = getScoresPath(cwd);
200
- const dir = dirname(path);
201
- if (!existsSync(dir)) {
202
- mkdirSync(dir, { recursive: true });
203
- }
204
- writeFileSync(path, JSON.stringify(scores, null, 2) + "\n", "utf-8");
209
+ getScoresStore(cwd).write(scores);
205
210
  }
206
211
 
207
212
  // ── Internal Types ─────────────────────────────────────────────────────────
@@ -12,6 +12,7 @@
12
12
  * Clean-room TypeScript implementation under MIT — not a code translation.
13
13
  */
14
14
 
15
+ import { createLogger } from "../shared/logger.js";
15
16
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
17
  import { Type } from "typebox";
17
18
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
@@ -61,7 +62,7 @@ function registerMemorySearch(pi: ExtensionAPI, state: { embeddingsMigrated: boo
61
62
  await embedMissing(cwd, topicEntries);
62
63
  state.embeddingsMigrated = true; // Only mark done on success
63
64
  } catch {
64
- console.debug("[memory] embedding migration failed, will retry next search");
65
+ log.debug("memory: embedding migration failed, will retry next search");
65
66
  }
66
67
  }
67
68
  const scores = loadScores(cwd);
@@ -159,6 +160,7 @@ function registerMemorySearch(pi: ExtensionAPI, state: { embeddingsMigrated: boo
159
160
  return `- [${r.category}] ${r.text}${pin} — score: ${typeof r.score === "number" ? r.score.toFixed(2) : r.score}, evidence: ${r.evidenceCount}${sim}${prov}`;
160
161
  });
161
162
 
163
+ log.info("memory_search", params.query, results.length);
162
164
  const text = `Found ${results.length} memories matching "${params.query}":\n\n${lines.join("\n")}`;
163
165
  return { content: [{ type: "text", text }] };
164
166
  },
@@ -188,6 +190,7 @@ function registerMemoryReinforce(pi: ExtensionAPI): void {
188
190
  const reinforced = reinforce(ctx.cwd, entryLine);
189
191
 
190
192
  if (reinforced) {
193
+ log.info("memory_reinforce", params.entryText.slice(0, 60));
191
194
  const scores = loadScores(ctx.cwd);
192
195
  const hash = entryHash(entryLine);
193
196
  const entry = scores.entries[hash];
@@ -199,7 +202,7 @@ function registerMemoryReinforce(pi: ExtensionAPI): void {
199
202
  (promo.details.length ? ` (${promo.details.join("; ")})` : "");
200
203
  }
201
204
  } catch (e: any) {
202
- console.debug("[memory] promote-after-reinforce failed:", e?.message || e);
205
+ log.debug("memory: promote-after-reinforce failed:", e?.message || e);
203
206
  }
204
207
  return {
205
208
  content: [{
@@ -410,7 +413,7 @@ function registerMemoryAdd(pi: ExtensionAPI): void {
410
413
  }
411
414
  }
412
415
  } catch (err) {
413
- console.debug(`[memory] memory_add: vector dedup skipped: ${err}`);
416
+ log.debug(`memory: memory_add: vector dedup skipped: ${err}`);
414
417
  }
415
418
 
416
419
  // Exact match check (fast O(n) string comparison after expensive vector check)
@@ -522,6 +525,7 @@ function registerMemoryAdd(pi: ExtensionAPI): void {
522
525
  // Embed the new entry for future similarity searches
523
526
  await embedEntry(cwd, text, category);
524
527
 
528
+ log.info("memory_add", category, text.slice(0, 60));
525
529
  const pin = isPinned ? " (pinned)" : "";
526
530
  return {
527
531
  content: [{ type: "text", text: `Added: [${category}] ${text}${pin}` }],
@@ -602,6 +606,7 @@ function registerMemoryRemove(pi: ExtensionAPI): void {
602
606
  saveScores(cwd, scores);
603
607
  }
604
608
 
609
+ log.info("memory_remove", category, text.slice(0, 60));
605
610
  return {
606
611
  content: [{ type: "text", text: `Removed: [${category}] ${text}` }],
607
612
  };
@@ -866,6 +871,8 @@ function registerMemoryConsolidate(pi: ExtensionAPI): void {
866
871
 
867
872
  // ── registerMemoryTools (entry point) ───────────────────────────────
868
873
 
874
+ const log = createLogger("memory");
875
+
869
876
  export function registerMemoryTools(pi: ExtensionAPI): void {
870
877
  // Only register in the orchestrator, not subagents
871
878
  if (process.env.PI_SUBAGENT_CHILD === "1") return;