@pi-unipi/unipi 2.4.2 → 2.6.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.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -0
- package/docs/prefix-cache-architecture.md +89 -0
- package/package.json +24 -22
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +2 -0
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/README.md +2 -1
- package/packages/cocoindex/index.ts +6 -13
- package/packages/cocoindex/package.json +4 -3
- package/packages/cocoindex/tools.ts +45 -11
- package/packages/compactor/README.md +4 -2
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/session/snapshot.ts +3 -2
- package/packages/compactor/src/tools/register.ts +6 -2
- package/packages/compactor/src/tools/vcc-recall.ts +18 -3
- package/packages/core/bounded-output.ts +106 -0
- package/packages/core/constants.ts +2 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/core/sandbox.ts +7 -6
- package/packages/footer/package.json +2 -2
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/README.md +13 -0
- package/packages/mcp/package.json +5 -2
- package/packages/mcp/src/bridge/registry.ts +244 -137
- package/packages/mcp/src/bridge/translator.ts +96 -21
- package/packages/mcp/src/index.ts +43 -67
- package/packages/mcp/src/tui/settings-overlay.ts +3 -9
- package/packages/memory/README.md +15 -11
- package/packages/memory/bridge/mempalace_bridge.py +636 -0
- package/packages/memory/index.ts +56 -34
- package/packages/memory/mempalace.ts +169 -18
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +27 -10
- package/packages/milestone/README.md +11 -3
- package/packages/milestone/hooks.ts +103 -29
- package/packages/milestone/index.ts +1 -1
- package/packages/milestone/package.json +5 -2
- package/packages/notify/package.json +2 -2
- package/packages/ralph/index.ts +12 -18
- package/packages/ralph/package.json +6 -3
- package/packages/ralph/reminder.ts +40 -0
- package/packages/ralph/tools.ts +5 -1
- package/packages/subagents/README.md +2 -0
- package/packages/subagents/package.json +1 -1
- package/packages/subagents/src/agent-manager.ts +5 -1
- package/packages/subagents/src/agent-runner.ts +2 -2
- package/packages/subagents/src/core-compat.ts +73 -0
- package/packages/subagents/src/custom-agents.ts +10 -2
- package/packages/subagents/src/index.ts +14 -3
- package/packages/subagents/src/types.ts +2 -0
- package/packages/unipi/bundled.js +1445 -686
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +9 -0
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/index.ts +48 -0
- package/packages/utility/src/lifecycle/cleanup.ts +29 -0
- package/packages/utility/src/prefix-cache.ts +263 -0
- package/packages/utility/src/types.ts +1 -1
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +8 -0
- package/packages/workflow/commands.ts +16 -26
- package/packages/workflow/index.ts +165 -85
- package/packages/workflow/package.json +5 -2
package/packages/memory/index.ts
CHANGED
|
@@ -35,6 +35,54 @@ import { isEmbeddingReady, hasModelChanged } from "./settings.js";
|
|
|
35
35
|
/** Package version */
|
|
36
36
|
const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
|
|
37
37
|
|
|
38
|
+
interface MemoryReminderInput {
|
|
39
|
+
projectName: string;
|
|
40
|
+
memories: Array<{ title: string }>;
|
|
41
|
+
canSearch: boolean;
|
|
42
|
+
canStore: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the deterministic, model-visible first-turn memory reminder.
|
|
47
|
+
* Exported so provider-payload regressions can exercise the exact production
|
|
48
|
+
* content without initializing a storage backend.
|
|
49
|
+
*/
|
|
50
|
+
export function buildMemoryRecallReminder(input: MemoryReminderInput): string {
|
|
51
|
+
const lines = [
|
|
52
|
+
"## 🧠 Memory System Active",
|
|
53
|
+
"",
|
|
54
|
+
`You have ${input.memories.length} memories stored for project "${input.projectName}".`,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (input.canSearch && input.memories.length > 0) {
|
|
58
|
+
const titleList = input.memories.slice(0, 20).map((memory) => `- ${memory.title}`).join("\n");
|
|
59
|
+
const extra = input.memories.length > 20
|
|
60
|
+
? `\n... and ${input.memories.length - 20} more`
|
|
61
|
+
: "";
|
|
62
|
+
lines.push(
|
|
63
|
+
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
64
|
+
"",
|
|
65
|
+
"Available memories:",
|
|
66
|
+
titleList + extra,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (input.canStore) {
|
|
71
|
+
lines.push(
|
|
72
|
+
"",
|
|
73
|
+
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
74
|
+
"call `memory_store` to save it for future sessions.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push(
|
|
79
|
+
"",
|
|
80
|
+
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
/** Storage instance for current project */
|
|
39
87
|
let projectStorage: MemoryStorage | null = null;
|
|
40
88
|
|
|
@@ -251,44 +299,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
299
|
return;
|
|
252
300
|
}
|
|
253
301
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"",
|
|
257
|
-
`You have ${projectMemories.length} memories stored for project "${projectName}".`,
|
|
258
|
-
];
|
|
259
|
-
|
|
260
|
-
if (canSearch && projectMemories.length > 0) {
|
|
261
|
-
const titleList = projectMemories.slice(0, 20).map(m => `- ${m.title}`).join("\n");
|
|
262
|
-
const extra = projectMemories.length > 20 ? `\n... and ${projectMemories.length - 20} more` : "";
|
|
263
|
-
lines.push(
|
|
264
|
-
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
265
|
-
"",
|
|
266
|
-
"Available memories:",
|
|
267
|
-
titleList + extra,
|
|
268
|
-
);
|
|
269
|
-
} else {
|
|
270
|
-
recallDone = true;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (canStore) {
|
|
274
|
-
lines.push(
|
|
275
|
-
"",
|
|
276
|
-
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
277
|
-
"call `memory_store` to save it for future sessions.",
|
|
278
|
-
);
|
|
279
|
-
} else {
|
|
280
|
-
storeDone = true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
lines.push(
|
|
284
|
-
"",
|
|
285
|
-
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
286
|
-
);
|
|
302
|
+
if (!canSearch || projectMemories.length === 0) recallDone = true;
|
|
303
|
+
if (!canStore) storeDone = true;
|
|
287
304
|
|
|
288
305
|
return {
|
|
289
306
|
message: {
|
|
290
307
|
customType: "unipi-memory-recall-reminder",
|
|
291
|
-
content:
|
|
308
|
+
content: buildMemoryRecallReminder({
|
|
309
|
+
projectName,
|
|
310
|
+
memories: projectMemories,
|
|
311
|
+
canSearch,
|
|
312
|
+
canStore,
|
|
313
|
+
}),
|
|
292
314
|
display: false,
|
|
293
315
|
},
|
|
294
316
|
};
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { spawnSync, spawn } from "node:child_process";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
15
17
|
import * as fs from "node:fs";
|
|
16
18
|
import * as path from "node:path";
|
|
17
19
|
import * as os from "node:os";
|
|
@@ -27,11 +29,74 @@ const MIGRATED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-mi
|
|
|
27
29
|
const PING_VERIFIED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-ping-verified");
|
|
28
30
|
const PING_VERIFIED_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
29
31
|
|
|
30
|
-
/**
|
|
31
|
-
const
|
|
32
|
+
/** Migration marker schema. Increment when migration semantics change. */
|
|
33
|
+
export const MIGRATION_STATE_VERSION = 2;
|
|
34
|
+
|
|
35
|
+
export interface MigrationResult {
|
|
36
|
+
discovered: number;
|
|
37
|
+
imported: number;
|
|
38
|
+
updated: number;
|
|
39
|
+
skipped: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
verified: number;
|
|
42
|
+
errors?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MigrationState {
|
|
46
|
+
version: number;
|
|
47
|
+
completedAt: string;
|
|
48
|
+
sourceFingerprint: string;
|
|
49
|
+
result: MigrationResult;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let cachedBridgePath: string | null | undefined;
|
|
53
|
+
|
|
54
|
+
function isReadableFile(candidate: string): boolean {
|
|
55
|
+
try {
|
|
56
|
+
return fs.statSync(candidate).isFile() && fs.accessSync(candidate, fs.constants.R_OK) === undefined;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the Python bridge in both supported layouts:
|
|
64
|
+
*
|
|
65
|
+
* - standalone @pi-unipi/memory: <package>/bridge/mempalace_bridge.py
|
|
66
|
+
* - bundled @pi-unipi/unipi: <umbrella>/packages/memory/bridge/...
|
|
67
|
+
*
|
|
68
|
+
* The explicit environment override is useful for custom packagers. The
|
|
69
|
+
* package-resolution fallback handles npm layouts where dependencies are not
|
|
70
|
+
* hoisted beside the umbrella package.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveMempalaceBridgePath(moduleUrl = import.meta.url): string | null {
|
|
73
|
+
const moduleDir = path.dirname(fileURLToPath(moduleUrl));
|
|
74
|
+
const candidates: string[] = [];
|
|
75
|
+
if (process.env.UNIPI_MEMPALACE_BRIDGE) {
|
|
76
|
+
candidates.push(path.resolve(process.env.UNIPI_MEMPALACE_BRIDGE));
|
|
77
|
+
}
|
|
78
|
+
candidates.push(
|
|
79
|
+
path.join(moduleDir, "bridge", "mempalace_bridge.py"),
|
|
80
|
+
path.join(moduleDir, "..", "memory", "bridge", "mempalace_bridge.py"),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const require = createRequire(moduleUrl);
|
|
85
|
+
const memoryPackage = require.resolve("@pi-unipi/memory/package.json");
|
|
86
|
+
candidates.push(path.join(path.dirname(memoryPackage), "bridge", "mempalace_bridge.py"));
|
|
87
|
+
} catch { /* standalone/source layout may not expose package resolution */ }
|
|
88
|
+
|
|
89
|
+
return candidates.find(isReadableFile) ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getBridgePath(): string | null {
|
|
93
|
+
if (cachedBridgePath === undefined) cachedBridgePath = resolveMempalaceBridgePath();
|
|
94
|
+
return cachedBridgePath;
|
|
95
|
+
}
|
|
32
96
|
|
|
33
|
-
|
|
34
|
-
|
|
97
|
+
/** Clear bridge discovery cache (primarily for recovery/tests). */
|
|
98
|
+
export function invalidateBridgePathCache(): void {
|
|
99
|
+
cachedBridgePath = undefined;
|
|
35
100
|
}
|
|
36
101
|
|
|
37
102
|
export interface BridgeResponse<T> {
|
|
@@ -196,22 +261,99 @@ export function invalidatePingVerified(): void {
|
|
|
196
261
|
try { if (fs.existsSync(PING_VERIFIED_FLAG)) fs.unlinkSync(PING_VERIFIED_FLAG); } catch { /* ignore */ }
|
|
197
262
|
}
|
|
198
263
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Fingerprint all durable legacy sources. This makes migration catch-up
|
|
266
|
+
* automatic for existing installations instead of treating a years-old
|
|
267
|
+
* timestamp flag as permanently complete.
|
|
268
|
+
*/
|
|
269
|
+
export function getMemorySourceFingerprint(
|
|
270
|
+
sourceDir = path.join(os.homedir(), ".unipi", "memory"),
|
|
271
|
+
): string {
|
|
272
|
+
const hash = createHash("sha256");
|
|
273
|
+
if (!fs.existsSync(sourceDir)) return hash.update("missing").digest("hex");
|
|
274
|
+
|
|
275
|
+
const visit = (dir: string): void => {
|
|
276
|
+
let entries: fs.Dirent[];
|
|
277
|
+
try {
|
|
278
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
279
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
280
|
+
} catch {
|
|
281
|
+
hash.update(`unreadable:${dir}`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
if (entry.name.startsWith(".")) continue;
|
|
286
|
+
const full = path.join(dir, entry.name);
|
|
287
|
+
if (entry.isDirectory()) {
|
|
288
|
+
visit(full);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!entry.isFile() || (entry.name !== "memory.db" && !entry.name.endsWith(".md"))) continue;
|
|
292
|
+
try {
|
|
293
|
+
const stat = fs.statSync(full);
|
|
294
|
+
hash.update(`${path.relative(sourceDir, full)}\0${stat.size}\0${stat.mtimeMs}\n`);
|
|
295
|
+
} catch {
|
|
296
|
+
hash.update(`unreadable:${path.relative(sourceDir, full)}\n`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
visit(sourceDir);
|
|
301
|
+
return hash.digest("hex");
|
|
202
302
|
}
|
|
203
303
|
|
|
204
|
-
/**
|
|
205
|
-
export function
|
|
304
|
+
/** Read a verified v2 migration state. Legacy timestamp markers return null. */
|
|
305
|
+
export function readMigrationState(flagPath = MIGRATED_FLAG): MigrationState | null {
|
|
206
306
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
307
|
+
const parsed = JSON.parse(fs.readFileSync(flagPath, "utf-8")) as MigrationState;
|
|
308
|
+
if (
|
|
309
|
+
parsed?.version !== MIGRATION_STATE_VERSION ||
|
|
310
|
+
typeof parsed.completedAt !== "string" ||
|
|
311
|
+
typeof parsed.sourceFingerprint !== "string" ||
|
|
312
|
+
!parsed.result ||
|
|
313
|
+
parsed.result.failed !== 0 ||
|
|
314
|
+
parsed.result.verified !== parsed.result.discovered
|
|
315
|
+
) return null;
|
|
316
|
+
return parsed;
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Is the palace verified against the current durable source set? */
|
|
323
|
+
export function isMigrated(
|
|
324
|
+
sourceFingerprint = getMemorySourceFingerprint(),
|
|
325
|
+
flagPath = MIGRATED_FLAG,
|
|
326
|
+
): boolean {
|
|
327
|
+
return readMigrationState(flagPath)?.sourceFingerprint === sourceFingerprint;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Mark migration complete only after the caller has verified every record. */
|
|
331
|
+
export function markMigrated(
|
|
332
|
+
sourceFingerprint: string,
|
|
333
|
+
result: MigrationResult,
|
|
334
|
+
flagPath = MIGRATED_FLAG,
|
|
335
|
+
): boolean {
|
|
336
|
+
if (result.failed !== 0 || result.verified !== result.discovered) return false;
|
|
337
|
+
try {
|
|
338
|
+
fs.mkdirSync(path.dirname(flagPath), { recursive: true });
|
|
339
|
+
const state: MigrationState = {
|
|
340
|
+
version: MIGRATION_STATE_VERSION,
|
|
341
|
+
completedAt: new Date().toISOString(),
|
|
342
|
+
sourceFingerprint,
|
|
343
|
+
result,
|
|
344
|
+
};
|
|
345
|
+
const temp = `${flagPath}.${process.pid}.tmp`;
|
|
346
|
+
fs.writeFileSync(temp, JSON.stringify(state, null, 2), "utf-8");
|
|
347
|
+
fs.renameSync(temp, flagPath);
|
|
348
|
+
return true;
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
210
352
|
}
|
|
211
353
|
|
|
212
354
|
/** Force re-migration by clearing the flag. */
|
|
213
|
-
export function clearMigratedFlag(): void {
|
|
214
|
-
try { if (fs.existsSync(
|
|
355
|
+
export function clearMigratedFlag(flagPath = MIGRATED_FLAG): void {
|
|
356
|
+
try { if (fs.existsSync(flagPath)) fs.unlinkSync(flagPath); } catch { /* ignore */ }
|
|
215
357
|
}
|
|
216
358
|
|
|
217
359
|
/**
|
|
@@ -223,7 +365,10 @@ export function runBridge<T = unknown>(
|
|
|
223
365
|
palace: string,
|
|
224
366
|
cmd: string,
|
|
225
367
|
args: Record<string, unknown> = {},
|
|
368
|
+
timeoutMs = 60_000,
|
|
226
369
|
): T | null {
|
|
370
|
+
const bridgePath = getBridgePath();
|
|
371
|
+
if (!bridgePath) return null;
|
|
227
372
|
let argsJson: string;
|
|
228
373
|
try {
|
|
229
374
|
argsJson = JSON.stringify(args);
|
|
@@ -232,9 +377,9 @@ export function runBridge<T = unknown>(
|
|
|
232
377
|
}
|
|
233
378
|
let res;
|
|
234
379
|
try {
|
|
235
|
-
res = spawnSync(install.python, [
|
|
380
|
+
res = spawnSync(install.python, [bridgePath, palace, cmd, argsJson], {
|
|
236
381
|
encoding: "utf-8",
|
|
237
|
-
timeout:
|
|
382
|
+
timeout: timeoutMs,
|
|
238
383
|
maxBuffer: 64 * 1024 * 1024,
|
|
239
384
|
});
|
|
240
385
|
} catch {
|
|
@@ -266,8 +411,14 @@ export function runBridgeAsync<T = unknown>(
|
|
|
266
411
|
palace: string,
|
|
267
412
|
cmd: string,
|
|
268
413
|
args: Record<string, unknown> = {},
|
|
414
|
+
timeoutMs = 60_000,
|
|
269
415
|
): Promise<T | null> {
|
|
270
416
|
return new Promise((resolve) => {
|
|
417
|
+
const bridgePath = getBridgePath();
|
|
418
|
+
if (!bridgePath) {
|
|
419
|
+
resolve(null);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
271
422
|
let argsJson: string;
|
|
272
423
|
try {
|
|
273
424
|
argsJson = JSON.stringify(args);
|
|
@@ -278,7 +429,7 @@ export function runBridgeAsync<T = unknown>(
|
|
|
278
429
|
|
|
279
430
|
let child;
|
|
280
431
|
try {
|
|
281
|
-
child = spawn(install.python, [
|
|
432
|
+
child = spawn(install.python, [bridgePath, palace, cmd, argsJson], {
|
|
282
433
|
stdio: ["ignore", "pipe", "ignore"],
|
|
283
434
|
});
|
|
284
435
|
} catch {
|
|
@@ -298,7 +449,7 @@ export function runBridgeAsync<T = unknown>(
|
|
|
298
449
|
const timer = setTimeout(() => {
|
|
299
450
|
try { child.kill(); } catch { /* already gone */ }
|
|
300
451
|
finish(null);
|
|
301
|
-
},
|
|
452
|
+
}, timeoutMs);
|
|
302
453
|
// Do not hold the process open purely for a background bridge call.
|
|
303
454
|
timer.unref?.();
|
|
304
455
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/memory",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"better-sqlite3": "^12.9.0",
|
|
44
44
|
"sqlite-vec": "^0.1.9",
|
|
45
45
|
"js-yaml": "^4.1.0",
|
|
46
|
-
"@pi-unipi/core": "2.
|
|
47
|
-
"@pi-unipi/info-screen": "2.
|
|
46
|
+
"@pi-unipi/core": "2.6.0",
|
|
47
|
+
"@pi-unipi/info-screen": "2.6.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
runBridgeAsync,
|
|
21
21
|
isMigrated,
|
|
22
22
|
markMigrated,
|
|
23
|
+
getMemorySourceFingerprint,
|
|
23
24
|
isPingVerified,
|
|
24
25
|
markPingVerified,
|
|
25
26
|
invalidatePingVerified,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
type MempalaceSearchResult,
|
|
30
31
|
type MempalaceListItem,
|
|
31
32
|
type MempalaceListItemAll,
|
|
33
|
+
type MigrationResult,
|
|
32
34
|
} from "./mempalace.js";
|
|
33
35
|
|
|
34
36
|
export type MemoryBackend = "mempalace" | "sqlite";
|
|
@@ -102,6 +104,7 @@ export interface SearchResult {
|
|
|
102
104
|
|
|
103
105
|
/** Memory file frontmatter */
|
|
104
106
|
interface MemoryFrontmatter {
|
|
107
|
+
id?: string;
|
|
105
108
|
title: string;
|
|
106
109
|
tags: string[];
|
|
107
110
|
project: string;
|
|
@@ -201,7 +204,7 @@ export function parseMemoryContent(content: string): MemoryRecord | null {
|
|
|
201
204
|
const frontmatter = yaml.load(frontmatterStr) as MemoryFrontmatter;
|
|
202
205
|
|
|
203
206
|
return {
|
|
204
|
-
id: "",
|
|
207
|
+
id: frontmatter.id || "",
|
|
205
208
|
title: frontmatter.title,
|
|
206
209
|
content: body.trim(),
|
|
207
210
|
tags: frontmatter.tags || [],
|
|
@@ -217,6 +220,7 @@ export function parseMemoryContent(content: string): MemoryRecord | null {
|
|
|
217
220
|
*/
|
|
218
221
|
export function writeMemoryFile(filePath: string, record: MemoryRecord): void {
|
|
219
222
|
const frontmatter: MemoryFrontmatter = {
|
|
223
|
+
id: record.id,
|
|
220
224
|
title: record.title,
|
|
221
225
|
tags: record.tags,
|
|
222
226
|
project: record.project,
|
|
@@ -383,16 +387,28 @@ export class MemoryStorage {
|
|
|
383
387
|
this.mempalaceInstall = install;
|
|
384
388
|
this.backend = "mempalace";
|
|
385
389
|
|
|
386
|
-
//
|
|
387
|
-
|
|
390
|
+
// Idempotent migration + automatic catch-up. The source fingerprint turns
|
|
391
|
+
// the old one-shot timestamp into a resumable state: new/changed markdown
|
|
392
|
+
// or SQLite sources trigger another verified upsert pass. Never mark a
|
|
393
|
+
// failed/partial run complete; it will retry on a later session.
|
|
394
|
+
const sourceFingerprint = getMemorySourceFingerprint(getMemoryBaseDir());
|
|
395
|
+
if (!isMigrated(sourceFingerprint)) {
|
|
388
396
|
try {
|
|
389
|
-
|
|
397
|
+
// First migrations can embed thousands of records. Give the bridge a
|
|
398
|
+
// practical bounded window rather than the normal per-operation 60s.
|
|
399
|
+
const result = runBridge<MigrationResult>(install, this.palacePath, "migrate", {
|
|
390
400
|
source_dir: getMemoryBaseDir(),
|
|
391
|
-
});
|
|
392
|
-
|
|
401
|
+
}, 15 * 60_000);
|
|
402
|
+
if (
|
|
403
|
+
result &&
|
|
404
|
+
result.failed === 0 &&
|
|
405
|
+
result.verified === result.discovered
|
|
406
|
+
) {
|
|
407
|
+
markMigrated(sourceFingerprint, result);
|
|
408
|
+
}
|
|
393
409
|
} catch {
|
|
394
|
-
//
|
|
395
|
-
//
|
|
410
|
+
// Palace remains available for current writes; durable markdown/SQLite
|
|
411
|
+
// sources are untouched and migration retries because no state is set.
|
|
396
412
|
}
|
|
397
413
|
}
|
|
398
414
|
|
|
@@ -629,8 +645,9 @@ export class MemoryStorage {
|
|
|
629
645
|
const record = parseMemoryFile(filePath);
|
|
630
646
|
if (!record) continue;
|
|
631
647
|
|
|
632
|
-
//
|
|
633
|
-
|
|
648
|
+
// New files preserve the authoritative store ID. Legacy files have no
|
|
649
|
+
// `id` frontmatter, so retain the historical title-derived fallback.
|
|
650
|
+
const id = record.id || record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
634
651
|
|
|
635
652
|
if (existingIds.has(id)) continue; // Already in DB
|
|
636
653
|
|
|
@@ -15,9 +15,17 @@ Workflow operates at the task level — brainstorm, plan, work, review. Project
|
|
|
15
15
|
|
|
16
16
|
### Session Start
|
|
17
17
|
|
|
18
|
-
On `before_agent_start`, milestone reads `.unipi/docs/MILESTONES.md` and appends a
|
|
18
|
+
On `before_agent_start`, milestone reads `.unipi/docs/MILESTONES.md` from `ctx.cwd` and appends a hidden `unipi-milestone-snapshot` custom message to the session:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
+
# UniPi Milestone Snapshot
|
|
22
|
+
|
|
23
|
+
This snapshot supersedes all prior UniPi milestone snapshots; use only this snapshot for milestone status.
|
|
24
|
+
|
|
25
|
+
Workspace: /path/to/project
|
|
26
|
+
|
|
27
|
+
Status: active
|
|
28
|
+
|
|
21
29
|
## Project Milestones
|
|
22
30
|
Overall progress: 5/10 items (50%)
|
|
23
31
|
Phase 1: Foundation: 3/5 done
|
|
@@ -25,11 +33,11 @@ Overall progress: 5/10 items (50%)
|
|
|
25
33
|
Current focus: Phase 1: Foundation
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
Snapshots are append-only and hidden from the transcript. They keep the system-prompt prefix stable, persist milestone context in session history, and are deduplicated against the latest milestone custom message in the effective (compaction-aware) LLM context. If milestones disappear while an older active snapshot remains effective, an inactive snapshot is appended to supersede it. A clean workspace with no milestones and no effective snapshot receives no injected message.
|
|
29
37
|
|
|
30
38
|
### Session End
|
|
31
39
|
|
|
32
|
-
On `session_shutdown`, milestone scans workflow docs modified during the session.
|
|
40
|
+
On `session_shutdown`, milestone scans workflow docs modified during the session. It uses the workspace captured from `session_start`'s `ctx.cwd`, detects items that changed from `- [ ]` to `- [x]`, and auto-updates MILESTONES.md using exact text matching.
|
|
33
41
|
|
|
34
42
|
### Coexist Triggers
|
|
35
43
|
|