@lebronj/pi-suite 0.1.16 → 0.1.18

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 (29) hide show
  1. package/README.md +13 -4
  2. package/extensions/goal-mode.ts +261 -33
  3. package/package.json +1 -1
  4. package/skills/pi-skill/SKILL.md +32 -7
  5. package/vendor/pi-memory/README.md +87 -56
  6. package/vendor/pi-memory/index.ts +522 -310
  7. package/vendor/pi-memory/package.json +1 -1
  8. package/vendor/pi-memory/src/cli.ts +56 -32
  9. package/vendor/pi-memory/src/evolution/config.ts +8 -2
  10. package/vendor/pi-memory/src/governance/share-candidates.ts +72 -0
  11. package/vendor/pi-memory/src/index.ts +68 -25
  12. package/vendor/pi-memory/src/learning/review-compact.ts +36 -0
  13. package/vendor/pi-memory/src/learning/review-summary.ts +81 -0
  14. package/vendor/pi-memory/src/manager/local-curator-manager.ts +146 -0
  15. package/vendor/pi-memory/src/paths/resolve-roots.ts +155 -0
  16. package/vendor/pi-memory/src/profile/generator.ts +45 -0
  17. package/vendor/pi-memory/src/service-controller.ts +156 -84
  18. package/vendor/pi-memory/src/skills/lifecycle.ts +205 -0
  19. package/vendor/pi-memory/src/sync/connector.ts +146 -0
  20. package/vendor/pi-memory/src/sync/downflow.ts +54 -0
  21. package/vendor/pi-memory/src/sync/feedback.ts +30 -0
  22. package/vendor/pi-memory/src/sync/queue.ts +40 -0
  23. package/vendor/pi-memory/src/sync/schemas.ts +44 -0
  24. package/vendor/pi-memory/src/sync/sensitivity.ts +18 -0
  25. package/vendor/pi-memory/test/manager-service.test.ts +17 -0
  26. package/vendor/pi-memory/test/resolve-roots.test.ts +63 -0
  27. package/vendor/pi-memory/test/review-summary.test.ts +36 -0
  28. package/vendor/pi-memory/test/skill-lifecycle.test.ts +75 -0
  29. package/vendor/pi-memory/test/sync-local-loop.test.ts +101 -0
@@ -49,71 +49,102 @@ import {
49
49
  type ReviewCandidateInput,
50
50
  } from "./src/learning/candidates.ts";
51
51
  import { applyReviewLifecycle, approveMemoryPromotion, proposeMemoryPromotions, rejectReviewItem } from "./src/learning/memory.ts";
52
+ import { generateShareCandidatesFromReview } from "./src/governance/share-candidates.ts";
53
+ import { compactProcessedReviewEntries } from "./src/learning/review-compact.ts";
54
+ import { countPendingReviewItems, formatPendingReviewList, formatPendingReviewSummary, listPendingReviewItems } from "./src/learning/review-summary.ts";
55
+ import { defaultRegistryPath, markCurrentRootDirty, scanDirtyRoots } from "./src/manager/local-curator-manager.ts";
52
56
  import { approveSkillDraft, listSkillDraftProposals, proposeSkillDrafts } from "./src/learning/skills.ts";
57
+ import { disableMemorySkill, enableMemorySkill, formatEnabledSkillsForPrompt, formatSkillList, listMemorySkills } from "./src/skills/lifecycle.ts";
58
+ import { generateProfiles } from "./src/profile/generator.ts";
59
+ import { syncPull, syncUpload } from "./src/sync/connector.ts";
60
+ import { appendFeedbackEvent, buildFeedbackEvent } from "./src/sync/feedback.ts";
61
+ import { detectSensitivity } from "./src/sync/sensitivity.ts";
53
62
  import { FileMemoryStore } from "./src/curator-store/file-store.ts";
54
63
  import {
55
- createEvolutionSnapshot,
56
- getEvolutionGitStatus,
57
- listManifests,
58
- pushEvolution,
59
- resolveEvolutionConfig,
60
- restoreEvolutionSnapshot,
61
- syncEvolutionAfterChange,
62
- type RestoreTarget,
63
- } from "./src/evolution/index.ts";
64
- import { disableCuratorService, enableCuratorService, getCuratorServiceStatus } from "./src/service-controller.ts";
64
+ ensureAgentRoot,
65
+ resolveAgentRoot,
66
+ resolveAgentRoots,
67
+ resolveFeedbackDir,
68
+ resolveInboxDir,
69
+ resolveMemoryRoot,
70
+ resolveProfileDir,
71
+ resolveSharedCacheDir,
72
+ resolveSkillDraftRoot,
73
+ resolveSyncQueueDir,
74
+ type PiAgentEnv,
75
+ } from "./src/paths/resolve-roots.ts";
76
+ import {
77
+ disableCuratorManagerService,
78
+ disableCuratorService,
79
+ enableCuratorManagerService,
80
+ enableCuratorService,
81
+ getCuratorManagerServiceStatus,
82
+ getCuratorServiceStatus,
83
+ } from "./src/service-controller.ts";
65
84
 
66
85
 
67
86
  // ---------------------------------------------------------------------------
68
87
  // Paths (mutable for testing via _setBaseDir / _resetBaseDir)
69
88
  // ---------------------------------------------------------------------------
70
89
 
71
- type MemoryEnv = Partial<
72
- Record<"PI_MEMORY_DIR" | "HOME" | "USERPROFILE" | "HOMEDRIVE" | "HOMEPATH", string | undefined>
73
- > & {
74
- [key: string]: string | undefined;
75
- };
90
+ type MemoryEnv = PiAgentEnv & { [key: string]: string | undefined };
76
91
 
77
92
  export function resolveMemoryDir(env: MemoryEnv = process.env): string {
78
- if (env.PI_MEMORY_DIR) return env.PI_MEMORY_DIR;
79
- const home =
80
- env.HOME ??
81
- env.USERPROFILE ??
82
- (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined) ??
83
- "~";
84
- return path.join(home, ".pi", "agent", "memory");
93
+ return resolveMemoryRoot(env);
94
+ }
95
+
96
+ export function resolveSkillDraftDir(env: MemoryEnv = process.env): string {
97
+ return resolveSkillDraftRoot(env);
85
98
  }
86
99
 
87
- let MEMORY_DIR = resolveMemoryDir();
100
+ export {
101
+ ensureAgentRoot,
102
+ resolveAgentRoot,
103
+ resolveFeedbackDir,
104
+ resolveInboxDir,
105
+ resolveMemoryRoot,
106
+ resolveProfileDir,
107
+ resolveSharedCacheDir,
108
+ resolveSkillDraftRoot,
109
+ resolveSyncQueueDir,
110
+ };
111
+
112
+ const INITIAL_ROOTS = resolveAgentRoots();
113
+ let AGENT_ROOT = INITIAL_ROOTS.agentRoot;
114
+ let MEMORY_DIR = INITIAL_ROOTS.memoryDir;
88
115
  let MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
89
116
  let USER_FILE = path.join(MEMORY_DIR, "USER.md");
90
117
  let STATE_FILE = path.join(MEMORY_DIR, "STATE.md");
91
118
  let REVIEW_FILE = path.join(MEMORY_DIR, "REVIEW.md");
92
119
  let SCRATCHPAD_FILE = path.join(MEMORY_DIR, "SCRATCHPAD.md");
93
- let LEARNING_STATE_FILE = path.join(MEMORY_DIR, ".learning-state.json");
94
120
  let DAILY_DIR = path.join(MEMORY_DIR, "daily");
95
- let SKILL_DRAFTS_DIR = path.join(path.dirname(MEMORY_DIR), "skill-drafts");
121
+ let SKILL_DRAFTS_DIR = INITIAL_ROOTS.skillDraftsDir;
96
122
 
97
- function currentEvolutionConfig() {
98
- return resolveEvolutionConfig(MEMORY_DIR);
123
+ function setResolvedDirs(memoryDir: string, skillDraftsDir: string, agentRoot?: string) {
124
+ AGENT_ROOT = agentRoot;
125
+ MEMORY_DIR = memoryDir;
126
+ MEMORY_FILE = path.join(memoryDir, "MEMORY.md");
127
+ USER_FILE = path.join(memoryDir, "USER.md");
128
+ STATE_FILE = path.join(memoryDir, "STATE.md");
129
+ REVIEW_FILE = path.join(memoryDir, "REVIEW.md");
130
+ SCRATCHPAD_FILE = path.join(memoryDir, "SCRATCHPAD.md");
131
+ DAILY_DIR = path.join(memoryDir, "daily");
132
+ SKILL_DRAFTS_DIR = skillDraftsDir;
99
133
  }
100
134
 
101
135
  /** Override base directory (for testing). */
102
- export function _setBaseDir(baseDir: string) {
103
- MEMORY_DIR = baseDir;
104
- MEMORY_FILE = path.join(baseDir, "MEMORY.md");
105
- USER_FILE = path.join(baseDir, "USER.md");
106
- STATE_FILE = path.join(baseDir, "STATE.md");
107
- REVIEW_FILE = path.join(baseDir, "REVIEW.md");
108
- SCRATCHPAD_FILE = path.join(baseDir, "SCRATCHPAD.md");
109
- LEARNING_STATE_FILE = path.join(baseDir, ".learning-state.json");
110
- DAILY_DIR = path.join(baseDir, "daily");
111
- SKILL_DRAFTS_DIR = path.join(path.dirname(baseDir), "skill-drafts");
136
+ export function _setBaseDir(baseDir: string, skillDraftsDir = path.join(path.dirname(baseDir), "skill-drafts")) {
137
+ setResolvedDirs(baseDir, skillDraftsDir);
138
+ }
139
+
140
+ function refreshResolvedDirsFromEnv() {
141
+ const roots = resolveAgentRoots();
142
+ setResolvedDirs(roots.memoryDir, roots.skillDraftsDir, roots.agentRoot);
112
143
  }
113
144
 
114
145
  /** Reset to default paths (for testing). */
115
146
  export function _resetBaseDir() {
116
- _setBaseDir(resolveMemoryDir());
147
+ refreshResolvedDirsFromEnv();
117
148
  }
118
149
 
119
150
  // ---------------------------------------------------------------------------
@@ -123,6 +154,22 @@ export function _resetBaseDir() {
123
154
  export function ensureDirs() {
124
155
  fs.mkdirSync(MEMORY_DIR, { recursive: true });
125
156
  fs.mkdirSync(DAILY_DIR, { recursive: true });
157
+ fs.mkdirSync(path.join(MEMORY_DIR, "audit"), { recursive: true });
158
+ fs.mkdirSync(SKILL_DRAFTS_DIR, { recursive: true });
159
+ for (const filePath of [MEMORY_FILE, USER_FILE, STATE_FILE, REVIEW_FILE]) {
160
+ if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
161
+ }
162
+ if (!fs.existsSync(SCRATCHPAD_FILE)) fs.writeFileSync(SCRATCHPAD_FILE, "# Scratchpad\n", "utf-8");
163
+ if (AGENT_ROOT) ensureAgentRoot(process.env);
164
+ }
165
+
166
+ function markDirtyBestEffort(): void {
167
+ if (!AGENT_ROOT) return;
168
+ try {
169
+ markCurrentRootDirty(process.env);
170
+ } catch {
171
+ // Dirty marking must not break memory writes or session shutdown.
172
+ }
126
173
  }
127
174
 
128
175
  export function todayStr(): string {
@@ -330,14 +377,12 @@ const STRUCTURED_MEMORY_TARGETS = ["memory", "user", "state", "review"] as const
330
377
  const MEMORY_WRITE_TARGETS = ["long_term", "daily", "state", "user", "review"] as const;
331
378
  const MEMORY_READ_TARGETS = ["long_term", "scratchpad", "daily", "list", "user", "state", "review", "all"] as const;
332
379
  const MEMORY_EDIT_ACTIONS = ["read", "add", "replace", "remove", "replace_all", "compact"] as const;
333
- const MEMORY_VERSION_RESTORE_TARGETS = ["memory", "skill-drafts", "all"] as const;
334
380
  const STRUCTURED_MEMORY_TYPES = ["fact", "preference", "event", "temporary", "quota", "review"] as const;
335
381
 
336
382
  type StructuredMemoryTarget = (typeof STRUCTURED_MEMORY_TARGETS)[number];
337
383
  type MemoryWriteTarget = (typeof MEMORY_WRITE_TARGETS)[number];
338
384
  type MemoryReadTarget = (typeof MEMORY_READ_TARGETS)[number];
339
385
  type MemoryEditAction = (typeof MEMORY_EDIT_ACTIONS)[number];
340
- type MemoryVersionRestoreTarget = (typeof MEMORY_VERSION_RESTORE_TARGETS)[number];
341
386
 
342
387
  type StructuredWriteOptions = {
343
388
  target: StructuredMemoryTarget;
@@ -565,9 +610,9 @@ function shouldKeepLearningCandidate(confidence: "low" | "medium" | "high", env:
565
610
  return confidenceRank(confidence) >= confidenceRank(getMemoryLearningMinConfidence(env));
566
611
  }
567
612
 
568
- function buildLearningExtractorPrompt(conversationText: string, truncated: boolean, totalChars: number, sourceLabel = "session transcript"): string {
613
+ function buildLearningExtractorPrompt(conversationText: string, truncated: boolean, totalChars: number): string {
569
614
  const lines = [
570
- `Extract zero or more review candidates from this ${sourceLabel}.`,
615
+ "Extract zero or more review candidates from this session transcript.",
571
616
  "Return JSON exactly shaped as: {\"candidates\":[{\"kind\":\"bug_fix|skill_candidate|preference|project_fact\",\"confidence\":\"low|medium|high\",\"signature\":\"short stable signature\",\"summary\":\"optional concise summary\",\"targetHints\":[\"memory\",\"skill\"],\"evidence\":\"optional compact evidence\"}]}",
572
617
  "Only include verified bug fixes when a failure was followed by an edit/action and successful validation.",
573
618
  "Drop one-off trivia, transient status, workflow artifacts, and loop artifacts.",
@@ -659,7 +704,7 @@ export function parseLearningExtractorResponse(raw: string): ReviewCandidateInpu
659
704
  const signature = typeof record.signature === "string" ? record.signature.trim() : "";
660
705
  if (!kind || !confidence || !signature || !shouldKeepLearningCandidate(confidence)) continue;
661
706
  const targetHints = Array.isArray(record.targetHints)
662
- ? record.targetHints.filter((hint): hint is ReviewCandidateInput["targetHints"][number] => typeof hint === "string" && REVIEW_TARGET_HINTS.includes(hint as ReviewCandidateInput["targetHints"][number]))
707
+ ? record.targetHints.filter((hint): hint is NonNullable<ReviewCandidateInput["targetHints"]>[number] => typeof hint === "string" && REVIEW_TARGET_HINTS.includes(hint as NonNullable<ReviewCandidateInput["targetHints"]>[number]))
663
708
  : undefined;
664
709
  candidates.push({
665
710
  kind,
@@ -674,90 +719,31 @@ export function parseLearningExtractorResponse(raw: string): ReviewCandidateInpu
674
719
  return candidates;
675
720
  }
676
721
 
677
- type DailyLearningState = {
678
- daily?: Record<string, { hash: string; scannedAt: string; candidates: number }>;
679
- };
680
-
681
- function readLearningState(): DailyLearningState {
682
- try {
683
- const parsed = JSON.parse(fs.readFileSync(LEARNING_STATE_FILE, "utf-8"));
684
- return parsed && typeof parsed === "object" ? parsed as DailyLearningState : {};
685
- } catch {
686
- return {};
687
- }
688
- }
689
-
690
- function writeLearningState(state: DailyLearningState): void {
691
- fs.writeFileSync(LEARNING_STATE_FILE, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
692
- }
693
-
694
- function contentHash(content: string): string {
695
- return createHash("sha256").update(content).digest("hex");
696
- }
697
-
698
- async function extractLearningCandidates(
699
- ctx: ExtensionContext,
700
- text: string,
701
- sourceLabel: string,
702
- source: string,
703
- date?: string,
704
- ): Promise<ReviewCandidateInput[]> {
705
- if (getMemoryLearningMode() === "off" || !ctx.model) return [];
722
+ async function runSessionLearningExtractor(ctx: ExtensionContext): Promise<number> {
723
+ if (getMemoryLearningMode() === "off") return 0;
724
+ const branch = getSessionBranch(ctx);
725
+ if (!branch || !ctx.model) return 0;
706
726
  const apiKey = await resolveExitSummaryApiKey(ctx);
707
- if (!apiKey) return [];
708
- const trimmed = text.trim();
709
- if (!trimmed) return [];
710
- const truncated = truncateText(trimmed, LEARNING_EXTRACTOR_MAX_CHARS, "end");
727
+ if (!apiKey) return 0;
728
+ const conversation = serializeSessionConversation(branch);
729
+ if (!conversation.hasMessages || !conversation.text.trim()) return 0;
730
+ const truncated = truncateText(conversation.text.trim(), LEARNING_EXTRACTOR_MAX_CHARS, "end");
711
731
  const messages: Message[] = [{
712
732
  role: "user",
713
- content: [{ type: "text", text: buildLearningExtractorPrompt(truncated.text, truncated.truncated, trimmed.length, sourceLabel) }],
733
+ content: [{ type: "text", text: buildLearningExtractorPrompt(truncated.text, truncated.truncated, conversation.text.trim().length) }],
714
734
  timestamp: Date.now(),
715
735
  }];
716
- const response = await complete(ctx.model, { systemPrompt: LEARNING_EXTRACTOR_SYSTEM_PROMPT, messages }, { apiKey, reasoningEffort: "low" });
717
- const raw = response.content.filter((part): part is { type: "text"; text: string } => part.type === "text").map((part) => part.text).join("\n");
718
- return parseLearningExtractorResponse(raw).map((candidate) => ({ ...candidate, source, date }));
719
- }
720
-
721
- async function writeLearningCandidates(candidates: ReviewCandidateInput[]): Promise<number> {
722
- let written = 0;
723
- const store = new FileMemoryStore(MEMORY_DIR);
724
- for (const candidate of candidates) {
725
- const result = await upsertReviewCandidate(store, candidate);
726
- if (result.changed) written += 1;
727
- }
728
- return written;
729
- }
730
-
731
- type DailyLearningScanResult = { scanned: boolean; changed: number; skipped?: string };
732
-
733
- async function runYesterdayDailyLearningScan(ctx: ExtensionContext): Promise<DailyLearningScanResult> {
734
- if (getMemoryLearningMode() === "off") return { scanned: false, changed: 0, skipped: "learning off" };
735
- const date = yesterdayStr();
736
- const dailyContent = readFileSafe(dailyPath(date));
737
- if (!dailyContent?.trim()) return { scanned: false, changed: 0, skipped: `daily/${date}.md empty or missing` };
738
- const hash = contentHash(dailyContent);
739
- const state = readLearningState();
740
- const previous = state.daily?.[date];
741
- if (previous?.hash === hash) return { scanned: false, changed: 0, skipped: `daily/${date}.md already scanned` };
742
736
  try {
743
- const candidates = await extractLearningCandidates(ctx, dailyContent, `daily log for ${date}`, `daily/${date}`, date);
744
- const changed = await writeLearningCandidates(candidates);
745
- state.daily = { ...(state.daily || {}), [date]: { hash, scannedAt: new Date().toISOString(), candidates: candidates.length } };
746
- writeLearningState(state);
747
- return { scanned: true, changed };
748
- } catch {
749
- return { scanned: false, changed: 0, skipped: `daily/${date}.md scan failed` };
750
- }
751
- }
752
-
753
- async function runSessionLearningExtractor(ctx: ExtensionContext): Promise<number> {
754
- const branch = getSessionBranch(ctx);
755
- if (!branch) return 0;
756
- const conversation = serializeSessionConversation(branch);
757
- if (!conversation.hasMessages || !conversation.text.trim()) return 0;
758
- try {
759
- const candidates = await extractLearningCandidates(ctx, conversation.text, "session transcript", "session_shutdown");
760
- return writeLearningCandidates(candidates);
737
+ const response = await complete(ctx.model, { systemPrompt: LEARNING_EXTRACTOR_SYSTEM_PROMPT, messages }, { apiKey, reasoningEffort: "low" });
738
+ const raw = response.content.filter((part): part is { type: "text"; text: string } => part.type === "text").map((part) => part.text).join("\n");
739
+ const candidates = parseLearningExtractorResponse(raw);
740
+ let written = 0;
741
+ const store = new FileMemoryStore(MEMORY_DIR);
742
+ for (const candidate of candidates) {
743
+ const result = await upsertReviewCandidate(store, candidate);
744
+ if (result.changed) written += 1;
745
+ }
746
+ return written;
761
747
  } catch {
762
748
  return 0;
763
749
  }
@@ -856,7 +842,9 @@ function formatTransitionHandoffReason(reason: TransitionHandoffReason): string
856
842
  }
857
843
 
858
844
  function getMessageText(message: Message): string {
859
- return message.content
845
+ const content = message.content;
846
+ if (typeof content === "string") return content.trim();
847
+ return content
860
848
  .map((part) => (part.type === "text" ? part.text : ""))
861
849
  .join("\n")
862
850
  .trim();
@@ -871,7 +859,7 @@ export function buildTransitionHandoff(ctx: ExtensionContext, reason: Transition
871
859
  const branch = getSessionBranch(ctx);
872
860
  const recentMessages = (branch ?? [])
873
861
  .filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
874
- .map((entry) => entry.message)
862
+ .map((entry) => entry.message as Message)
875
863
  .map((message) => ({ role: message.role, text: previewMessageText(getMessageText(message)) }))
876
864
  .filter((message) => message.text)
877
865
  .slice(-6);
@@ -908,7 +896,6 @@ export function buildTransitionHandoff(ctx: ExtensionContext, reason: Transition
908
896
 
909
897
  async function writeTransitionHandoff(ctx: ExtensionContext, reason: TransitionHandoffReason): Promise<boolean> {
910
898
  ensureDirs();
911
- await evolutionBeforeChange(`session transition handoff ${formatTransitionHandoffReason(reason)}`, "memory: snapshot before handoff", "session", shortSessionId(ctx.sessionManager.getSessionId()));
912
899
  const sid = shortSessionId(ctx.sessionManager.getSessionId());
913
900
  const ts = nowTimestamp();
914
901
  const handoff = buildTransitionHandoff(ctx, reason, sid, ts);
@@ -919,7 +906,6 @@ async function writeTransitionHandoff(ctx: ExtensionContext, reason: TransitionH
919
906
  fs.writeFileSync(filePath, existing + separator + handoff, "utf-8");
920
907
  await ensureQmdAvailableForUpdate();
921
908
  await runQmdUpdateNow();
922
- await evolutionAfterChange("memory: sync after handoff");
923
909
  return true;
924
910
  }
925
911
 
@@ -1092,6 +1078,61 @@ export function buildMemoryContext(searchResults?: string): string {
1092
1078
  return context;
1093
1079
  }
1094
1080
 
1081
+
1082
+ function buildSharedCacheContext(prompt: string): string {
1083
+ const roots = resolveAgentRoots(process.env);
1084
+ if (!roots.sharedCacheDir && !roots.agentRoot) return "";
1085
+ const promptText = prompt.toLowerCase();
1086
+ const units: Array<{ id: string; unitType: "memory" | "skill"; title: string; text: string; score: number }> = [];
1087
+ const memoryDir = roots.sharedCacheDir ? path.join(roots.sharedCacheDir, "memory") : undefined;
1088
+ if (memoryDir && fs.existsSync(memoryDir)) {
1089
+ for (const name of fs.readdirSync(memoryDir).filter((file) => file.endsWith(".json"))) {
1090
+ try {
1091
+ const delivery = JSON.parse(fs.readFileSync(path.join(memoryDir, name), "utf-8")) as { shared_unit_id?: string; id?: string; content?: string; tags?: string[]; score?: number };
1092
+ const content = String(delivery.content || "").trim();
1093
+ if (!content || detectSensitivity(content) === "secret") continue;
1094
+ const id = delivery.shared_unit_id || delivery.id || name.replace(/\.json$/, "");
1095
+ units.push({ id, unitType: "memory", title: `Shared memory ${id}`, text: content, score: sharedUnitScore(promptText, content, delivery.tags, delivery.score) });
1096
+ } catch {
1097
+ // Ignore malformed shared-cache entries.
1098
+ }
1099
+ }
1100
+ }
1101
+ const generatedDir = roots.agentRoot ? path.join(roots.agentRoot, "skills", "generated") : undefined;
1102
+ if (generatedDir && fs.existsSync(generatedDir)) {
1103
+ for (const name of fs.readdirSync(generatedDir)) {
1104
+ const skillPath = path.join(generatedDir, name, "SKILL.md");
1105
+ if (!fs.existsSync(skillPath)) continue;
1106
+ const content = fs.readFileSync(skillPath, "utf-8").trim();
1107
+ if (!content || detectSensitivity(content) === "secret") continue;
1108
+ const score = sharedUnitScore(promptText, content, undefined, undefined);
1109
+ if (score > 0) units.push({ id: name, unitType: "skill", title: `Generated shared skill ${name}`, text: content.split("\n").slice(0, 20).join("\n"), score });
1110
+ }
1111
+ }
1112
+ const selected = units.filter((unit) => unit.score > 0).sort((a, b) => b.score - a.score).slice(0, 4);
1113
+ if (selected.length === 0) return "";
1114
+ for (const unit of selected) {
1115
+ try {
1116
+ appendFeedbackEvent(buildFeedbackEvent({ shared_unit_id: unit.id, unit_type: unit.unitType, event: "injected", outcome: "neutral" }), process.env);
1117
+ } catch {
1118
+ // Feedback must not block context injection.
1119
+ }
1120
+ }
1121
+ return selected.map((unit) => `### ${unit.title}\n${unit.text}`).join("\n\n---\n\n");
1122
+ }
1123
+
1124
+ function sharedUnitScore(promptText: string, content: string, tags: string[] | undefined, remoteScore: number | undefined): number {
1125
+ let score = typeof remoteScore === "number" ? remoteScore : 0;
1126
+ for (const tag of tags || []) {
1127
+ if (promptText.includes(tag.toLowerCase())) score += 2;
1128
+ }
1129
+ const words = new Set(promptText.split(/[^a-zA-Z0-9_\u4e00-\u9fff]+/).filter((word) => word.length >= 3));
1130
+ for (const word of words) {
1131
+ if (content.toLowerCase().includes(word)) score += 1;
1132
+ }
1133
+ return score;
1134
+ }
1135
+
1095
1136
  // ---------------------------------------------------------------------------
1096
1137
  // QMD integration
1097
1138
  // ---------------------------------------------------------------------------
@@ -1222,6 +1263,12 @@ export function _clearQmdStatusCaches() {
1222
1263
 
1223
1264
  const QMD_REPO_URL = "https://github.com/tobi/qmd";
1224
1265
 
1266
+ function qmdCollectionName(): string {
1267
+ const scoped = process.env.PI_MEMORY_DIR || process.env.PI_AGENT_ROOT || (process.env.MULTICA_WORKSPACE_ID && process.env.MULTICA_AGENT_ID);
1268
+ if (!scoped) return "pi-memory";
1269
+ return `pi-memory-${createHash("sha1").update(MEMORY_DIR).digest("hex").slice(0, 12)}`;
1270
+ }
1271
+
1225
1272
  export function qmdInstallInstructions(): string {
1226
1273
  return [
1227
1274
  "memory_search requires qmd.",
@@ -1231,17 +1278,17 @@ export function qmdInstallInstructions(): string {
1231
1278
  " # ensure ~/.bun/bin is in your PATH",
1232
1279
  "",
1233
1280
  "Then set up the collection (one-time):",
1234
- ` qmd collection add ${MEMORY_DIR} --name pi-memory`,
1281
+ ` qmd collection add ${MEMORY_DIR} --name ${qmdCollectionName()}`,
1235
1282
  " qmd embed",
1236
1283
  ].join("\n");
1237
1284
  }
1238
1285
 
1239
1286
  export function qmdCollectionInstructions(): string {
1240
1287
  return [
1241
- "qmd collection pi-memory is not configured.",
1288
+ "qmd collection for the current memory root is not configured.",
1242
1289
  "",
1243
1290
  "Set up the collection (one-time):",
1244
- ` qmd collection add ${MEMORY_DIR} --name pi-memory`,
1291
+ ` qmd collection add ${MEMORY_DIR} --name ${qmdCollectionName()}`,
1245
1292
  " qmd embed",
1246
1293
  ].join("\n");
1247
1294
  }
@@ -1250,7 +1297,7 @@ export function qmdCollectionInstructions(): string {
1250
1297
  export async function setupQmdCollection(): Promise<boolean> {
1251
1298
  try {
1252
1299
  await new Promise<void>((resolve, reject) => {
1253
- execFileFn("qmd", ["collection", "add", MEMORY_DIR, "--name", "pi-memory"], { timeout: 10_000 }, (err) =>
1300
+ execFileFn("qmd", ["collection", "add", MEMORY_DIR, "--name", qmdCollectionName()], { timeout: 10_000 }, (err) =>
1254
1301
  err ? reject(err) : resolve(),
1255
1302
  );
1256
1303
  });
@@ -1270,7 +1317,7 @@ export async function setupQmdCollection(): Promise<boolean> {
1270
1317
  for (const [ctxPath, desc] of contexts) {
1271
1318
  try {
1272
1319
  await new Promise<void>((resolve, reject) => {
1273
- execFileFn("qmd", ["context", "add", ctxPath, desc, "-c", "pi-memory"], { timeout: 10_000 }, (err) =>
1320
+ execFileFn("qmd", ["context", "add", ctxPath, desc, "-c", qmdCollectionName()], { timeout: 10_000 }, (err) =>
1274
1321
  err ? reject(err) : resolve(),
1275
1322
  );
1276
1323
  });
@@ -1278,9 +1325,9 @@ export async function setupQmdCollection(): Promise<boolean> {
1278
1325
  // Ignore — context may already exist
1279
1326
  }
1280
1327
  }
1281
- // Seed the cache so checkCollection("pi-memory") doesn't redundantly re-run
1328
+ // Seed the cache so checkCollection(qmdCollectionName()) doesn't redundantly re-run
1282
1329
  // setupQmdCollection during the short negative-cache window.
1283
- qmdCollectionStatusCache.set("pi-memory", { checkedAt: Date.now(), exists: true });
1330
+ qmdCollectionStatusCache.set(qmdCollectionName(), { checkedAt: Date.now(), exists: true });
1284
1331
  return true;
1285
1332
  }
1286
1333
 
@@ -1339,6 +1386,7 @@ export function checkCollection(name: string): Promise<boolean> {
1339
1386
  }
1340
1387
 
1341
1388
  export function scheduleQmdUpdate() {
1389
+ markDirtyBestEffort();
1342
1390
  if (getQmdUpdateMode() !== "background") return;
1343
1391
  if (!qmdAvailable) return;
1344
1392
  if (updateTimer) clearTimeout(updateTimer);
@@ -1349,6 +1397,7 @@ export function scheduleQmdUpdate() {
1349
1397
  }
1350
1398
 
1351
1399
  async function runQmdUpdateNow() {
1400
+ markDirtyBestEffort();
1352
1401
  if (getQmdUpdateMode() !== "background") return;
1353
1402
  if (!qmdAvailable) return;
1354
1403
  await new Promise<void>((resolve) => {
@@ -1369,7 +1418,7 @@ export async function searchRelevantMemories(prompt: string): Promise<string> {
1369
1418
  if (!sanitized) return "";
1370
1419
 
1371
1420
  try {
1372
- const hasCollection = await checkCollection("pi-memory");
1421
+ const hasCollection = await checkCollection(qmdCollectionName());
1373
1422
  if (!hasCollection) return "";
1374
1423
 
1375
1424
  const results = await Promise.race([
@@ -1448,7 +1497,7 @@ export function runQmdSearch(
1448
1497
  limit: number,
1449
1498
  ): Promise<{ results: QmdSearchResult[]; stderr: string }> {
1450
1499
  const subcommand = mode === "keyword" ? "search" : mode === "semantic" ? "vsearch" : "query";
1451
- const args = [subcommand, "--json", "-c", "pi-memory", "-n", String(limit), query];
1500
+ const args = [subcommand, "--json", "-c", qmdCollectionName(), "-n", String(limit), query];
1452
1501
 
1453
1502
  return new Promise((resolve, reject) => {
1454
1503
  execFileFn("qmd", args, { timeout: 60_000 }, (err, stdout, stderr) => {
@@ -1501,16 +1550,14 @@ function getSnapshotMode(): "stable" | "per-turn" {
1501
1550
  }
1502
1551
 
1503
1552
 
1504
- async function runCurator(reason: string, ctx?: ExtensionContext): Promise<string> {
1553
+ async function runCurator(reason: string): Promise<string> {
1505
1554
  ensureDirs();
1506
- await evolutionBeforeChange(`curator before ${reason}`, "memory: snapshot before curate", "tool");
1507
1555
  const store = new FileMemoryStore(MEMORY_DIR);
1508
1556
  const result = await runMemoryCuratorOnce({
1509
1557
  memoryStore: store,
1510
1558
  auditLog: new JsonlAuditLog(MEMORY_DIR),
1511
1559
  reason,
1512
1560
  });
1513
- const dailyLearningResult = ctx ? await runYesterdayDailyLearningScan(ctx) : { scanned: false, changed: 0 };
1514
1561
  const lifecycleResult = await applyReviewLifecycle(store);
1515
1562
  const memoryResult = await proposeMemoryPromotions(store);
1516
1563
  const skillResult = getMemorySkillDraftsMode() === "off" ? { created: 0, proposals: [] } : await proposeSkillDrafts(store, { draftsDir: SKILL_DRAFTS_DIR });
@@ -1528,80 +1575,33 @@ async function runCurator(reason: string, ctx?: ExtensionContext): Promise<strin
1528
1575
  autoApprovedSkills += 1;
1529
1576
  }
1530
1577
  }
1531
- const learningChanges = dailyLearningResult.changed + lifecycleResult.changed + memoryResult.created + skillResult.created + autoApprovedMemory + autoApprovedSkills;
1578
+ const learningChanges = lifecycleResult.changed + memoryResult.created + skillResult.created + autoApprovedMemory + autoApprovedSkills;
1532
1579
  if (result.patches.length > 0 || learningChanges > 0) {
1533
1580
  snapshotDirty = true;
1534
1581
  await ensureQmdAvailableForUpdate();
1535
1582
  scheduleQmdUpdate();
1536
1583
  }
1537
- await evolutionAfterChange("memory: sync after curate");
1538
1584
  const notes = [
1539
- dailyLearningResult.scanned ? `scanned yesterday daily, wrote ${dailyLearningResult.changed} review candidate change(s)` : dailyLearningResult.skipped ? `daily learning skipped: ${dailyLearningResult.skipped}` : "",
1540
1585
  memoryResult.created > 0 ? `proposed ${memoryResult.created} memory promotion(s)` : "",
1541
1586
  skillResult.created > 0 ? `proposed ${skillResult.created} skill draft(s)` : "",
1542
1587
  autoApprovedMemory > 0 ? `auto-approved ${autoApprovedMemory} memory promotion(s)` : "",
1543
1588
  autoApprovedSkills > 0 ? `auto-approved ${autoApprovedSkills} skill draft(s)` : "",
1544
1589
  ].filter(Boolean);
1545
- return notes.length > 0 ? `${result.summary}; ${notes.join("; ")}` : result.summary;
1546
- }
1547
-
1548
- async function evolutionBeforeChange(reason: string, commitMessage: string, trigger = "tool", sessionId?: string): Promise<void> {
1549
- try {
1550
- createEvolutionSnapshot(currentEvolutionConfig(), { reason, trigger, sessionId, commitMessage });
1551
- } catch {
1552
- // Memory operations should not fail just because versioning is unavailable.
1553
- }
1554
- }
1555
-
1556
- async function evolutionAfterChange(commitMessage: string): Promise<void> {
1557
- try {
1558
- const config = currentEvolutionConfig();
1559
- syncEvolutionAfterChange(config, commitMessage);
1560
- if (config.autoPush) pushEvolution(config);
1561
- } catch {
1562
- // Best-effort only; the memory write itself remains authoritative.
1563
- }
1564
- }
1565
-
1566
- function formatEvolutionStatusText(status: ReturnType<typeof getEvolutionGitStatus>): string {
1567
- return [
1568
- `enabled: ${status.enabled}`,
1569
- `repo: ${status.repoDir}`,
1570
- `initialized: ${status.initialized}`,
1571
- `branch: ${status.branch || "n/a"}`,
1572
- `remote: ${status.remote || "n/a"}`,
1573
- `dirty: ${status.dirty}`,
1574
- `autoCommit: ${status.autoCommit}`,
1575
- `autoPush: ${status.autoPush}`,
1576
- `snapshotLimit: ${status.maxSnapshots}`,
1577
- `lastCommit: ${status.lastCommit || "n/a"}`,
1578
- status.status ? `status:\n${status.status}` : "status: clean",
1579
- ].join("\n");
1580
- }
1581
-
1582
- function startupCuratorHintEnabled(): boolean {
1583
- const value = process.env.PI_MEMORY_CURATOR_STARTUP_HINT?.trim().toLowerCase();
1584
- return !value || !["0", "false", "off", "no"].includes(value);
1585
- }
1586
-
1587
- function notifyDisabledCuratorService(ctx: ExtensionContext): void {
1588
- if (!ctx.hasUI || !startupCuratorHintEnabled()) return;
1589
- try {
1590
- const result = getCuratorServiceStatus({ memoryDir: MEMORY_DIR, cliPath: new URL("./src/cli.ts", import.meta.url).pathname });
1591
- if (result.state.enabled) return;
1592
- ctx.ui.notify(
1593
- [
1594
- "Memory self-evolution curator is off.",
1595
- "It can run daily outside pi to maintain memory lifecycle, review repeated learnings, and propose disabled skill drafts.",
1596
- "Enable: /memory-curator-enable 03:00",
1597
- "Status: /memory-curator-status",
1598
- "Disable: /memory-curator-disable",
1599
- ].join("\n"),
1600
- "info",
1601
- );
1602
- } catch {
1603
- // Startup hints should never block memory initialization.
1590
+ const baseSummary = notes.length > 0 ? `${result.summary}; ${notes.join("; ")}` : result.summary;
1591
+ let shareCandidateNote = "";
1592
+ if (AGENT_ROOT) {
1593
+ try {
1594
+ const shareResult = await generateShareCandidatesFromReview(store, process.env);
1595
+ generateProfiles(process.env);
1596
+ if (shareResult.created > 0 || shareResult.errors.length > 0) {
1597
+ shareCandidateNote = `\nGenerated ${shareResult.created} share candidate(s)${shareResult.errors.length ? `; ${shareResult.errors.length} error(s)` : ""}.`;
1598
+ }
1599
+ } catch {
1600
+ // Share candidate/profile generation is best-effort and must not block local curation.
1601
+ }
1604
1602
  }
1603
+ const pending = countPendingReviewItems(readFileSafe(REVIEW_FILE) ?? "");
1604
+ return `${baseSummary}${shareCandidateNote}\n${formatPendingReviewSummary(pending)}`;
1605
1605
  }
1606
1606
 
1607
1607
  /** Reset snapshot state (for testing). */
@@ -1618,33 +1618,9 @@ export function _resetMemorySnapshot() {
1618
1618
  // ---------------------------------------------------------------------------
1619
1619
 
1620
1620
  export default function (pi: ExtensionAPI) {
1621
- const versionedToolCommitMessages: Record<string, string> = {
1622
- memory_write: "memory: sync after memory_write",
1623
- memory_edit: "memory: sync after memory_edit",
1624
- scratchpad: "memory: sync after scratchpad",
1625
- memory_learning_approve: "memory: sync after learning approve",
1626
- memory_learning_reject: "memory: sync after learning reject",
1627
- };
1628
-
1629
- function shouldVersionToolCall(toolName: string, input: unknown): boolean {
1630
- if (!(toolName in versionedToolCommitMessages)) return false;
1631
- if (toolName === "scratchpad" && (input as { action?: string })?.action === "list") return false;
1632
- if (toolName === "memory_edit" && (input as { action?: string })?.action === "read") return false;
1633
- return true;
1634
- }
1635
-
1636
- pi.on("tool_call", async (event, ctx) => {
1637
- if (!shouldVersionToolCall(event.toolName, event.input)) return;
1638
- await evolutionBeforeChange(`${event.toolName} tool`, `memory: snapshot before ${event.toolName}`, "tool", shortSessionId(ctx.sessionManager.getSessionId()));
1639
- });
1640
-
1641
- pi.on("tool_result", async (event) => {
1642
- if (event.isError || !shouldVersionToolCall(event.toolName, event.input)) return;
1643
- await evolutionAfterChange(versionedToolCommitMessages[event.toolName]);
1644
- });
1645
-
1646
1621
  // --- session_start: detect qmd, auto-setup collection ---
1647
1622
  pi.on("session_start", async (_event, ctx) => {
1623
+ refreshResolvedDirsFromEnv();
1648
1624
  ensureDirs();
1649
1625
  exitSummaryReason = null;
1650
1626
  if (terminalInputUnsubscribe) {
@@ -1659,8 +1635,13 @@ export default function (pi: ExtensionAPI) {
1659
1635
  exitSummaryReason = "ctrl+d";
1660
1636
  return undefined;
1661
1637
  });
1638
+ if (process.env.PI_MEMORY_REVIEW_STARTUP_HINT !== "0") {
1639
+ const pending = countPendingReviewItems(readFileSafe(REVIEW_FILE) ?? "");
1640
+ if (pending.total > 0) {
1641
+ ctx.ui.notify(`Memory review: ${pending.memory} memory / ${pending.skill} skill proposals pending. Run /memory-review.`, "info");
1642
+ }
1643
+ }
1662
1644
  }
1663
- notifyDisabledCuratorService(ctx);
1664
1645
 
1665
1646
  qmdAvailable = await detectQmd();
1666
1647
  if (!qmdAvailable) {
@@ -1671,7 +1652,7 @@ export default function (pi: ExtensionAPI) {
1671
1652
  return;
1672
1653
  }
1673
1654
 
1674
- const hasCollection = await checkCollection("pi-memory");
1655
+ const hasCollection = await checkCollection(qmdCollectionName());
1675
1656
  if (!hasCollection) {
1676
1657
  await setupQmdCollection();
1677
1658
  }
@@ -1717,7 +1698,6 @@ export default function (pi: ExtensionAPI) {
1717
1698
  const result = await generateExitSummary(ctx);
1718
1699
  if (result.hasMessages) {
1719
1700
  const summary = result.summary ?? buildExitSummaryFallback(result.error);
1720
- await evolutionBeforeChange("session shutdown summary", "memory: snapshot before session summary", "session", shortSessionId(ctx.sessionManager.getSessionId()));
1721
1701
  const sid = shortSessionId(ctx.sessionManager.getSessionId());
1722
1702
  const ts = nowTimestamp();
1723
1703
  const entry = formatExitSummaryEntry(summary, reason, sid, ts);
@@ -1725,10 +1705,13 @@ export default function (pi: ExtensionAPI) {
1725
1705
  const existing = readFileSafe(filePath) ?? "";
1726
1706
  const separator = existing.trim() ? "\n\n" : "";
1727
1707
  fs.writeFileSync(filePath, existing + separator + entry, "utf-8");
1728
- await runSessionLearningExtractor(ctx);
1708
+ const newCandidates = await runSessionLearningExtractor(ctx);
1709
+ if (ctx.hasUI && process.env.PI_MEMORY_REVIEW_SESSION_SUMMARY !== "0") {
1710
+ const pending = countPendingReviewItems(readFileSafe(REVIEW_FILE) ?? "");
1711
+ ctx.ui.notify(`Memory learning today: ${newCandidates} new candidate(s), ${pending.memory} memory proposal(s) pending, ${pending.skill} skill proposal(s) pending.`, "info");
1712
+ }
1729
1713
  await ensureQmdAvailableForUpdate();
1730
1714
  await runQmdUpdateNow();
1731
- await evolutionAfterChange("memory: sync after session summary");
1732
1715
  }
1733
1716
  }
1734
1717
  } finally {
@@ -1773,6 +1756,20 @@ export default function (pi: ExtensionAPI) {
1773
1756
  "recent writes may also be visible in tool-call history.";
1774
1757
  }
1775
1758
 
1759
+ const sharedContext = buildSharedCacheContext(event.prompt ?? "");
1760
+ if (sharedContext) {
1761
+ memoryContext = memoryContext
1762
+ ? `${memoryContext}\n\n---\n\n## Matched Shared Cache\n\n${sharedContext}`
1763
+ : `# Memory\n\n## Matched Shared Cache\n\n${sharedContext}`;
1764
+ }
1765
+
1766
+ const enabledSkillsContext = formatEnabledSkillsForPrompt(process.env);
1767
+ if (enabledSkillsContext) {
1768
+ memoryContext = memoryContext
1769
+ ? `${memoryContext}\n\n---\n\n## Enabled Agent Skills\n\n${enabledSkillsContext}`
1770
+ : `# Memory\n\n## Enabled Agent Skills\n\n${enabledSkillsContext}`;
1771
+ }
1772
+
1776
1773
  if (!memoryContext) return;
1777
1774
 
1778
1775
  const headerLines = ["\n\n## Memory"];
@@ -1831,7 +1828,6 @@ export default function (pi: ExtensionAPI) {
1831
1828
  // source files) would keep being injected.
1832
1829
  try {
1833
1830
  if (parts.length === 0) return;
1834
- await evolutionBeforeChange("session before compact handoff", "memory: snapshot before compact handoff", "session", sid);
1835
1831
 
1836
1832
  const handoff = [`<!-- HANDOFF ${ts} [${sid}] -->`, "## Session Handoff", ...parts].join("\n");
1837
1833
 
@@ -1841,7 +1837,6 @@ export default function (pi: ExtensionAPI) {
1841
1837
  fs.writeFileSync(filePath, existing + separator + handoff, "utf-8");
1842
1838
  await ensureQmdAvailableForUpdate();
1843
1839
  scheduleQmdUpdate();
1844
- await evolutionAfterChange("memory: sync after compact handoff");
1845
1840
  } finally {
1846
1841
  refreshMemorySnapshot("session_before_compact");
1847
1842
  }
@@ -2331,15 +2326,68 @@ export default function (pi: ExtensionAPI) {
2331
2326
  },
2332
2327
  });
2333
2328
 
2329
+ pi.registerTool({
2330
+ name: "memory_skill_list",
2331
+ label: "Memory Skill List",
2332
+ description: "List current-agent draft, generated, and enabled memory-managed skills.",
2333
+ parameters: Type.Object({}),
2334
+ async execute(): Promise<any> {
2335
+ try {
2336
+ const skills = listMemorySkills(process.env);
2337
+ return { content: [{ type: "text", text: formatSkillList(skills) }], details: skills };
2338
+ } catch (error) {
2339
+ const message = error instanceof Error ? error.message : String(error);
2340
+ return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
2341
+ }
2342
+ },
2343
+ });
2344
+
2345
+ pi.registerTool({
2346
+ name: "memory_skill_enable",
2347
+ label: "Memory Skill Enable",
2348
+ description: "Enable a current-agent skill from drafts or generated deliveries. Use source like draft:<slug> or generated:<id>.",
2349
+ parameters: Type.Object({
2350
+ source: Type.String({ description: "Skill source, e.g. draft:my-skill or generated:unit_123" }),
2351
+ force: Type.Optional(Type.Boolean({ description: "Replace an existing enabled skill with the same name" })),
2352
+ }),
2353
+ async execute(_toolCallId, params): Promise<any> {
2354
+ try {
2355
+ const result = enableMemorySkill(params.source, { force: params.force, env: process.env });
2356
+ return { content: [{ type: "text", text: `Enabled skill ${result.enabled.name}: ${result.path}` }], details: result };
2357
+ } catch (error) {
2358
+ const message = error instanceof Error ? error.message : String(error);
2359
+ return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
2360
+ }
2361
+ },
2362
+ });
2363
+
2364
+ pi.registerTool({
2365
+ name: "memory_skill_disable",
2366
+ label: "Memory Skill Disable",
2367
+ description: "Disable an enabled current-agent memory-managed skill by id or skill name. Draft/generated sources are kept.",
2368
+ parameters: Type.Object({
2369
+ id: Type.String({ description: "Enabled skill id or frontmatter name" }),
2370
+ }),
2371
+ async execute(_toolCallId, params): Promise<any> {
2372
+ try {
2373
+ const result = disableMemorySkill(params.id, process.env);
2374
+ return { content: [{ type: "text", text: `Disabled skill ${result.id}: ${result.path}` }], details: result };
2375
+ } catch (error) {
2376
+ const message = error instanceof Error ? error.message : String(error);
2377
+ return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
2378
+ }
2379
+ },
2380
+ });
2381
+
2334
2382
  // --- memory_curate tool ---
2335
2383
  pi.registerTool({
2336
2384
  name: "memory_curate",
2337
2385
  label: "Memory Curate",
2338
- description: "Run the time-aware memory curator now. It scans yesterday's daily log into REVIEW.md, deduplicates exact entries, updates event/quota lifecycle metadata, and appends stale temporary memories to REVIEW.md.",
2386
+ description: "Run the time-aware memory curator now. It deduplicates exact entries, updates event/quota lifecycle metadata, and appends stale temporary memories to REVIEW.md.",
2339
2387
  parameters: Type.Object({}),
2340
2388
  async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
2341
2389
  try {
2342
- const summary = await runCurator("memory_curate tool", _ctx);
2390
+ const summary = await runCurator("memory_curate tool");
2343
2391
  return { content: [{ type: "text", text: summary }], details: { summary } };
2344
2392
  } catch (error) {
2345
2393
  const message = error instanceof Error ? error.message : String(error);
@@ -2410,16 +2458,29 @@ export default function (pi: ExtensionAPI) {
2410
2458
  },
2411
2459
  });
2412
2460
 
2413
- // --- memory versioning tools ---
2414
2461
  pi.registerTool({
2415
- name: "memory_version_status",
2416
- label: "Memory Version Status",
2417
- description: "View evolution repo status, remote, branch, dirty state, last commit, and auto-push setting.",
2418
- parameters: Type.Object({}),
2419
- async execute() {
2462
+ name: "memory_feedback",
2463
+ label: "Memory Feedback",
2464
+ description: "Record injected/used/ignored/success/failure/conflict feedback for a downflowed shared memory or skill.",
2465
+ parameters: Type.Object({
2466
+ shared_unit_id: Type.String({ description: "Shared unit id" }),
2467
+ unit_type: StringEnum(["memory", "skill"] as const, { description: "Shared unit type" }),
2468
+ event: StringEnum(["injected", "used", "ignored", "success", "failure", "conflict"] as const, { description: "Feedback event" }),
2469
+ outcome: Type.Optional(StringEnum(["success", "failure", "neutral"] as const, { description: "Optional outcome" })),
2470
+ task_type: Type.Optional(Type.String({ description: "Optional task type" })),
2471
+ }),
2472
+ async execute(_toolCallId, params): Promise<any> {
2420
2473
  try {
2421
- const status = getEvolutionGitStatus(currentEvolutionConfig());
2422
- return { content: [{ type: "text", text: formatEvolutionStatusText(status) }], details: status };
2474
+ const event = buildFeedbackEvent({
2475
+ shared_unit_id: params.shared_unit_id,
2476
+ unit_type: params.unit_type as "memory" | "skill",
2477
+ event: params.event as "injected" | "used" | "ignored" | "success" | "failure" | "conflict",
2478
+ outcome: params.outcome as "success" | "failure" | "neutral" | undefined,
2479
+ task_type: params.task_type,
2480
+ });
2481
+ const filePath = appendFeedbackEvent(event);
2482
+ markDirtyBestEffort();
2483
+ return { content: [{ type: "text", text: `Recorded feedback: ${filePath}` }], details: { path: filePath, event } };
2423
2484
  } catch (error) {
2424
2485
  const message = error instanceof Error ? error.message : String(error);
2425
2486
  return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
@@ -2428,22 +2489,14 @@ export default function (pi: ExtensionAPI) {
2428
2489
  });
2429
2490
 
2430
2491
  pi.registerTool({
2431
- name: "memory_version_snapshot",
2432
- label: "Memory Version Snapshot",
2433
- description: "Manually create a memory + skill-drafts snapshot, sync current mirrors, and commit if changed.",
2434
- parameters: Type.Object({
2435
- reason: Type.Optional(Type.String({ description: "Snapshot reason" })),
2436
- }),
2437
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2492
+ name: "memory_curator_manager_mark_dirty",
2493
+ label: "Memory Curator Manager Mark Dirty",
2494
+ description: "Register and mark the current Multica agent root dirty for the singleton Local Curator Manager.",
2495
+ parameters: Type.Object({}),
2496
+ async execute(): Promise<any> {
2438
2497
  try {
2439
- const result = createEvolutionSnapshot(currentEvolutionConfig(), {
2440
- reason: params.reason || "manual snapshot",
2441
- trigger: "tool",
2442
- sessionId: shortSessionId(ctx.sessionManager.getSessionId()),
2443
- commitMessage: "memory: manual snapshot",
2444
- });
2445
- if (currentEvolutionConfig().autoPush) pushEvolution(currentEvolutionConfig());
2446
- return { content: [{ type: "text", text: result.manifest ? `Snapshot ${result.manifest.id}` : `Snapshot skipped: ${result.skipped}` }], details: result };
2498
+ const record = markCurrentRootDirty(process.env);
2499
+ return { content: [{ type: "text", text: `Marked dirty: ${record.agent_root}` }], details: record };
2447
2500
  } catch (error) {
2448
2501
  const message = error instanceof Error ? error.message : String(error);
2449
2502
  return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
@@ -2452,19 +2505,17 @@ export default function (pi: ExtensionAPI) {
2452
2505
  });
2453
2506
 
2454
2507
  pi.registerTool({
2455
- name: "memory_version_list",
2456
- label: "Memory Version List",
2457
- description: "List recent memory evolution snapshots.",
2508
+ name: "memory_curator_manager_scan",
2509
+ label: "Memory Curator Manager Scan",
2510
+ description: "Process dirty roots from the singleton Local Curator Manager registry with per-root locks.",
2458
2511
  parameters: Type.Object({
2459
- limit: Type.Optional(Type.Number({ description: "Max snapshots to list. Default: 20" })),
2512
+ registry: Type.Optional(Type.String({ description: "Optional registry path" })),
2460
2513
  }),
2461
- async execute(_toolCallId, params) {
2514
+ async execute(_toolCallId, params): Promise<any> {
2462
2515
  try {
2463
- const manifests = listManifests(currentEvolutionConfig(), params.limit || 20);
2464
- const text = manifests.length === 0
2465
- ? "No snapshots found."
2466
- : manifests.map((manifest) => `- ${manifest.id} ${manifest.createdAt} ${manifest.reason} (${manifest.files.memory} memory files, ${manifest.files.skillDrafts} skill files)`).join("\n");
2467
- return { content: [{ type: "text", text }], details: { manifests } };
2516
+ const registryPath = params.registry || defaultRegistryPath();
2517
+ const result = await scanDirtyRoots(registryPath);
2518
+ return { content: [{ type: "text", text: `Local curator manager processed ${result.processed} root(s), ${result.failures} failure(s).` }], details: { registryPath, ...result } };
2468
2519
  } catch (error) {
2469
2520
  const message = error instanceof Error ? error.message : String(error);
2470
2521
  return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
@@ -2473,81 +2524,242 @@ export default function (pi: ExtensionAPI) {
2473
2524
  });
2474
2525
 
2475
2526
  pi.registerTool({
2476
- name: "memory_version_restore",
2477
- label: "Memory Version Restore",
2478
- description: "Restore memory and/or skill drafts from a snapshot id. Creates a pre-restore snapshot first.",
2527
+ name: "memory_curator_manager_enable",
2528
+ label: "Memory Curator Manager Enable",
2529
+ description: "Enable the singleton Local Curator Manager service. Default schedule checks dirty roots every 6 hours.",
2479
2530
  parameters: Type.Object({
2480
- id: Type.String({ description: "Snapshot id" }),
2481
- target: Type.Optional(StringEnum(MEMORY_VERSION_RESTORE_TARGETS, { description: "Restore target. Default: all" })),
2531
+ registry: Type.Optional(Type.String({ description: "Optional registry path" })),
2532
+ schedule: Type.Optional(Type.String({ description: "Cron or systemd calendar schedule. Default: 0 */6 * * *." })),
2482
2533
  }),
2483
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2534
+ execute(_toolCallId, params): any {
2535
+ const registryPath = params.registry || defaultRegistryPath();
2536
+ const result = enableCuratorManagerService({ registryPath, cliPath: new URL("./src/cli.ts", import.meta.url).pathname, schedule: params.schedule });
2537
+ return { content: [{ type: "text", text: result.message }], details: result, isError: !result.ok };
2538
+ },
2539
+ });
2540
+
2541
+ pi.registerTool({
2542
+ name: "memory_curator_manager_disable",
2543
+ label: "Memory Curator Manager Disable",
2544
+ description: "Disable the singleton Local Curator Manager service.",
2545
+ parameters: Type.Object({
2546
+ registry: Type.Optional(Type.String({ description: "Optional registry path" })),
2547
+ }),
2548
+ execute(_toolCallId, params): any {
2549
+ const registryPath = params.registry || defaultRegistryPath();
2550
+ const result = disableCuratorManagerService({ registryPath, cliPath: new URL("./src/cli.ts", import.meta.url).pathname });
2551
+ return { content: [{ type: "text", text: result.message }], details: result, isError: !result.ok };
2552
+ },
2553
+ });
2554
+
2555
+ pi.registerTool({
2556
+ name: "memory_curator_manager_status",
2557
+ label: "Memory Curator Manager Status",
2558
+ description: "Show the singleton Local Curator Manager service status.",
2559
+ parameters: Type.Object({
2560
+ registry: Type.Optional(Type.String({ description: "Optional registry path" })),
2561
+ }),
2562
+ execute(_toolCallId, params): any {
2563
+ const registryPath = params.registry || defaultRegistryPath();
2564
+ const result = getCuratorManagerServiceStatus({ registryPath, cliPath: new URL("./src/cli.ts", import.meta.url).pathname });
2565
+ return { content: [{ type: "text", text: result.message }], details: result, isError: !result.ok };
2566
+ },
2567
+ });
2568
+
2569
+ pi.registerTool({
2570
+ name: "memory_sync_upload",
2571
+ label: "Memory Sync Upload",
2572
+ description: "Upload governed local memory/skill candidates, profiles, and feedback to Multica when PI_MEMORY_REMOTE_URL/TOKEN are configured.",
2573
+ parameters: Type.Object({}),
2574
+ async execute() {
2484
2575
  try {
2485
- const target = (params.target || "all") as MemoryVersionRestoreTarget;
2486
- const result = restoreEvolutionSnapshot(currentEvolutionConfig(), params.id, target as RestoreTarget, shortSessionId(ctx.sessionManager.getSessionId()));
2487
- snapshotDirty = true;
2488
- return { content: [{ type: "text", text: `Restored ${target} from snapshot ${result.restored.id}.` }], details: result };
2576
+ const result = await syncUpload();
2577
+ return { content: [{ type: "text", text: result.skipped || `Uploaded ${result.candidates} candidate(s), ${result.profiles} profile file(s), ${result.feedback} feedback event(s).` }], details: result };
2489
2578
  } catch (error) {
2490
2579
  const message = error instanceof Error ? error.message : String(error);
2491
- return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
2580
+ return { content: [{ type: "text", text: message }], details: { ok: false, skipped: message, candidates: 0, feedback: 0, profiles: 0 }, isError: true };
2492
2581
  }
2493
2582
  },
2494
2583
  });
2495
2584
 
2496
2585
  pi.registerTool({
2497
- name: "memory_version_push",
2498
- label: "Memory Version Push",
2499
- description: "Manually push the local evolution repo to GitHub.",
2500
- parameters: Type.Object({}),
2501
- async execute() {
2586
+ name: "memory_sync_pull",
2587
+ label: "Memory Sync Pull",
2588
+ description: "Pull Multica evolution deliveries for the current agent and write only inbox/shared-cache/generated-skill files.",
2589
+ parameters: Type.Object({
2590
+ limit: Type.Optional(Type.Number({ description: "Maximum deliveries to pull. Default: 20." })),
2591
+ }),
2592
+ async execute(_toolCallId, params) {
2502
2593
  try {
2503
- const output = pushEvolution(currentEvolutionConfig());
2504
- return { content: [{ type: "text", text: output || "Pushed evolution repo." }], details: {} };
2594
+ const result = await syncPull(process.env, params.limit ?? 20);
2595
+ return { content: [{ type: "text", text: result.skipped || `Pulled ${result.received} delivery(s), rejected ${result.rejected}.` }], details: result };
2505
2596
  } catch (error) {
2506
2597
  const message = error instanceof Error ? error.message : String(error);
2507
- return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
2598
+ return { content: [{ type: "text", text: message }], details: { ok: false, skipped: message, received: 0, rejected: 0, written: [] }, isError: true };
2508
2599
  }
2509
2600
  },
2510
2601
  });
2511
2602
 
2512
- pi.registerCommand("memory-version-status", {
2513
- description: "Show memory evolution repo status",
2514
- handler: async (_args, ctx) => ctx.ui.notify(formatEvolutionStatusText(getEvolutionGitStatus(currentEvolutionConfig())), "info"),
2603
+ pi.registerCommand("memory-curator-manager-scan", {
2604
+ description: "Run the singleton Local Curator Manager over dirty roots",
2605
+ handler: async (args, ctx) => {
2606
+ try {
2607
+ const registryPath = args.trim() || defaultRegistryPath();
2608
+ const result = await scanDirtyRoots(registryPath);
2609
+ ctx.ui.notify(`Local curator manager processed ${result.processed} root(s), ${result.failures} failure(s).`, "info");
2610
+ } catch (error) {
2611
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2612
+ }
2613
+ },
2515
2614
  });
2516
2615
 
2517
- pi.registerCommand("memory-version-snapshot", {
2518
- description: "Create a memory evolution snapshot",
2616
+ pi.registerCommand("memory-curator-manager-enable", {
2617
+ description: "Enable the singleton Local Curator Manager service",
2519
2618
  handler: async (args, ctx) => {
2520
- const result = createEvolutionSnapshot(currentEvolutionConfig(), { reason: args.trim() || "manual snapshot", trigger: "slash_command", sessionId: shortSessionId(ctx.sessionManager.getSessionId()), commitMessage: "memory: manual snapshot" });
2521
- ctx.ui.notify(result.manifest ? `Snapshot ${result.manifest.id}` : `Snapshot skipped: ${result.skipped}`, "info");
2619
+ const result = enableCuratorManagerService({ registryPath: defaultRegistryPath(), cliPath: new URL("./src/cli.ts", import.meta.url).pathname, schedule: args.trim() || undefined });
2620
+ ctx.ui.notify(result.message, result.ok ? "info" : "error");
2522
2621
  },
2523
2622
  });
2524
2623
 
2525
- pi.registerCommand("memory-version-list", {
2526
- description: "List memory evolution snapshots",
2624
+ pi.registerCommand("memory-curator-manager-disable", {
2625
+ description: "Disable the singleton Local Curator Manager service",
2527
2626
  handler: async (_args, ctx) => {
2528
- const manifests = listManifests(currentEvolutionConfig(), 20);
2529
- ctx.ui.notify(manifests.length === 0 ? "No snapshots found." : manifests.map((manifest) => `${manifest.id} ${manifest.reason}`).join("\n"), "info");
2627
+ const result = disableCuratorManagerService({ registryPath: defaultRegistryPath(), cliPath: new URL("./src/cli.ts", import.meta.url).pathname });
2628
+ ctx.ui.notify(result.message, result.ok ? "info" : "error");
2629
+ },
2630
+ });
2631
+
2632
+ pi.registerCommand("memory-curator-manager-status", {
2633
+ description: "Show the singleton Local Curator Manager service status",
2634
+ handler: async (_args, ctx) => {
2635
+ const result = getCuratorManagerServiceStatus({ registryPath: defaultRegistryPath(), cliPath: new URL("./src/cli.ts", import.meta.url).pathname });
2636
+ ctx.ui.notify(result.message, result.ok ? "info" : "error");
2530
2637
  },
2531
2638
  });
2532
2639
 
2533
- pi.registerCommand("memory-version-restore", {
2534
- description: "Restore memory evolution snapshot: /memory-version-restore <snapshot-id> [memory|skill-drafts|all]",
2640
+ pi.registerCommand("memory-skill", {
2641
+ description: "List, enable, or disable current-agent memory-managed skills",
2535
2642
  handler: async (args, ctx) => {
2536
- const [id, rawTarget] = args.trim().split(/\s+/);
2537
- if (!id) {
2538
- ctx.ui.notify("Usage: /memory-version-restore <snapshot-id> [memory|skill-drafts|all]", "error");
2539
- return;
2643
+ try {
2644
+ const [command, value, ...rest] = args.trim().split(/\s+/).filter(Boolean);
2645
+ if (!command || command === "list") {
2646
+ ctx.ui.notify(formatSkillList(listMemorySkills(process.env)), "info");
2647
+ return;
2648
+ }
2649
+ if (command === "enable") {
2650
+ if (!value) throw new Error("Usage: /memory-skill enable <draft:slug|generated:id> [--force]");
2651
+ const result = enableMemorySkill(value, { force: rest.includes("--force"), env: process.env });
2652
+ ctx.ui.notify(`Enabled skill ${result.enabled.name}: ${result.path}`, "info");
2653
+ return;
2654
+ }
2655
+ if (command === "disable") {
2656
+ if (!value) throw new Error("Usage: /memory-skill disable <id-or-name>");
2657
+ const result = disableMemorySkill(value, process.env);
2658
+ ctx.ui.notify(`Disabled skill ${result.id}: ${result.path}`, "info");
2659
+ return;
2660
+ }
2661
+ throw new Error("Usage: /memory-skill [list|enable|disable]");
2662
+ } catch (error) {
2663
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2664
+ }
2665
+ },
2666
+ });
2667
+
2668
+ pi.registerCommand("memory-sync-upload", {
2669
+ description: "Upload governed memory candidates, profiles, and feedback to Multica",
2670
+ handler: async (_args, ctx) => {
2671
+ try {
2672
+ const result = await syncUpload();
2673
+ ctx.ui.notify(result.skipped || `Uploaded ${result.candidates} candidate(s), ${result.profiles} profile file(s), ${result.feedback} feedback event(s).`, "info");
2674
+ } catch (error) {
2675
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2540
2676
  }
2541
- const target = MEMORY_VERSION_RESTORE_TARGETS.includes(rawTarget as MemoryVersionRestoreTarget) ? rawTarget as MemoryVersionRestoreTarget : "all";
2542
- const result = restoreEvolutionSnapshot(currentEvolutionConfig(), id, target as RestoreTarget, shortSessionId(ctx.sessionManager.getSessionId()));
2543
- snapshotDirty = true;
2544
- ctx.ui.notify(`Restored ${target} from snapshot ${result.restored.id}.`, "info");
2545
2677
  },
2546
2678
  });
2547
2679
 
2548
- pi.registerCommand("memory-version-push", {
2549
- description: "Push memory evolution repo",
2550
- handler: async (_args, ctx) => ctx.ui.notify(pushEvolution(currentEvolutionConfig()) || "Pushed evolution repo.", "info"),
2680
+ pi.registerCommand("memory-sync-pull", {
2681
+ description: "Pull current-agent Multica memory/skill deliveries into local cache",
2682
+ handler: async (args, ctx) => {
2683
+ try {
2684
+ const limit = Number.parseInt(args.trim() || "20", 10);
2685
+ const result = await syncPull(process.env, Number.isFinite(limit) ? limit : 20);
2686
+ ctx.ui.notify(result.skipped || `Pulled ${result.received} delivery(s), rejected ${result.rejected}.`, "info");
2687
+ } catch (error) {
2688
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2689
+ }
2690
+ },
2691
+ });
2692
+
2693
+ pi.registerCommand("memory-review", {
2694
+ description: "List, show, approve, reject, or archive pending memory/skill review proposals",
2695
+ handler: async (args, ctx) => {
2696
+ try {
2697
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
2698
+ const command = tokens[0];
2699
+ const store = new FileMemoryStore(MEMORY_DIR);
2700
+ if (command === "approve") {
2701
+ const id = tokens[1];
2702
+ if (!id) throw new Error("Usage: /memory-review approve <id>");
2703
+ try {
2704
+ const memoryResult = await approveMemoryPromotion(store, id);
2705
+ snapshotDirty = true;
2706
+ await ensureQmdAvailableForUpdate();
2707
+ scheduleQmdUpdate();
2708
+ ctx.ui.notify(`Approved ${memoryResult.proposalId}. Wrote ${memoryResult.target}.`, "info");
2709
+ return;
2710
+ } catch {
2711
+ const skillResult = await approveSkillDraft(store, id);
2712
+ snapshotDirty = true;
2713
+ await ensureQmdAvailableForUpdate();
2714
+ scheduleQmdUpdate();
2715
+ ctx.ui.notify(`Approved ${skillResult.proposalId}. Created disabled skill draft: ${skillResult.path}`, "info");
2716
+ return;
2717
+ }
2718
+ }
2719
+ if (command === "reject" || command === "archive") {
2720
+ const id = tokens[1];
2721
+ if (!id) throw new Error(`Usage: /memory-review ${command} <id>`);
2722
+ await rejectReviewItem(store, id, command === "archive" ? "archived" : "rejected");
2723
+ snapshotDirty = true;
2724
+ await ensureQmdAvailableForUpdate();
2725
+ scheduleQmdUpdate();
2726
+ ctx.ui.notify(`Marked ${id} as ${command === "archive" ? "archived" : "rejected"}.`, "info");
2727
+ return;
2728
+ }
2729
+ if (command === "compact") {
2730
+ const reviewText = readFileSafe(REVIEW_FILE) ?? "";
2731
+ const compacted = compactProcessedReviewEntries(reviewText, { compactDays: Number.parseInt(process.env.PI_MEMORY_REVIEW_COMPACT_DAYS || "30", 10) });
2732
+ if (compacted.removed > 0) {
2733
+ fs.writeFileSync(REVIEW_FILE, compacted.activeEntries.join(ENTRY_DELIMITER), "utf-8");
2734
+ markDirtyBestEffort();
2735
+ fs.mkdirSync(path.join(MEMORY_DIR, "audit"), { recursive: true });
2736
+ fs.appendFileSync(path.join(MEMORY_DIR, "audit", "curator.jsonl"), `${JSON.stringify({ timestamp: new Date().toISOString(), action: "review_compact", removed: compacted.removed })}\n`, "utf-8");
2737
+ }
2738
+ ctx.ui.notify(`Compacted REVIEW.md: removed ${compacted.removed} processed item(s).`, "info");
2739
+ return;
2740
+ }
2741
+ if (command === "show") {
2742
+ const id = tokens[1];
2743
+ if (!id) throw new Error("Usage: /memory-review show <id>");
2744
+ const entry = (await store.readEntries("review")).find((candidate) => parseStructuredEntry(candidate).metadata.id === id);
2745
+ ctx.ui.notify(entry || `No review entry found for id '${id}'.`, entry ? "info" : "error");
2746
+ return;
2747
+ }
2748
+ const typeFlagIndex = tokens.indexOf("--type");
2749
+ const type = typeFlagIndex >= 0 ? tokens[typeFlagIndex + 1] : undefined;
2750
+ const limitFlagIndex = tokens.indexOf("--limit");
2751
+ const limit = limitFlagIndex >= 0 ? Number.parseInt(tokens[limitFlagIndex + 1] || "20", 10) : 20;
2752
+ const reviewText = readFileSafe(REVIEW_FILE) ?? "";
2753
+ const counts = countPendingReviewItems(reviewText);
2754
+ const items = listPendingReviewItems(reviewText, {
2755
+ type: type === "memory" || type === "skill" ? type : undefined,
2756
+ limit: Number.isFinite(limit) ? limit : 20,
2757
+ });
2758
+ ctx.ui.notify(formatPendingReviewList(items, counts), "info");
2759
+ } catch (error) {
2760
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2761
+ }
2762
+ },
2551
2763
  });
2552
2764
 
2553
2765
  // --- memory_search tool ---
@@ -2591,7 +2803,7 @@ export default function (pi: ExtensionAPI) {
2591
2803
  };
2592
2804
  }
2593
2805
 
2594
- let hasCollection = await checkCollection("pi-memory");
2806
+ let hasCollection = await checkCollection(qmdCollectionName());
2595
2807
  if (!hasCollection) {
2596
2808
  const created = await setupQmdCollection();
2597
2809
  if (created) {