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.
- package/README.md +76 -67
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +42 -3
- package/agents/github-expert.md +11 -0
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +563 -102
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +15 -9
- package/extensions/orchestrator/dreaming.ts +32 -25
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +80 -27
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +78 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +57 -33
- package/extensions/orchestrator/project-settings.ts +333 -127
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +63 -15
- package/extensions/orchestrator/rule-placeholders.ts +411 -2
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +157 -59
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +96 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +9 -3
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +12 -1
- package/rules/20-code-review-loop.md +52 -25
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +563 -0
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- 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.
|
|
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",
|
|
46
|
+
return join(cwd, ".pi", "memory", STORE_FILENAME_JSONL);
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
function loadStore(cwd: string): EmbeddingStore {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
96
|
-
|
|
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
|
|
127
|
-
|
|
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
|
|
193
|
-
|
|
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
|
|
209
|
-
|
|
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
|
|
3
|
-
* into memory-scores.
|
|
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.
|
|
6
|
-
* Shape
|
|
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 {
|
|
10
|
-
import {
|
|
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",
|
|
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
|
-
|
|
47
|
+
getProvenanceStore(cwd).write(payload);
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
|
-
* Merge provenance-pending.
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
13
|
-
import { join
|
|
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
|
-
|
|
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",
|
|
196
|
+
return join(cwd, ".pi", "memory", SCORES_FILENAME_JSONL);
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
export function loadScores(cwd: string): ScoresFile {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|