@lebronj/pi-suite 0.1.16 → 0.1.17
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 +11 -3
- package/package.json +1 -1
- package/skills/pi-skill/SKILL.md +29 -5
- package/vendor/pi-memory/README.md +87 -56
- package/vendor/pi-memory/index.ts +522 -310
- package/vendor/pi-memory/package.json +1 -1
- package/vendor/pi-memory/src/cli.ts +56 -32
- package/vendor/pi-memory/src/evolution/config.ts +8 -2
- package/vendor/pi-memory/src/governance/share-candidates.ts +72 -0
- package/vendor/pi-memory/src/index.ts +68 -25
- package/vendor/pi-memory/src/learning/review-compact.ts +36 -0
- package/vendor/pi-memory/src/learning/review-summary.ts +81 -0
- package/vendor/pi-memory/src/manager/local-curator-manager.ts +146 -0
- package/vendor/pi-memory/src/paths/resolve-roots.ts +155 -0
- package/vendor/pi-memory/src/profile/generator.ts +45 -0
- package/vendor/pi-memory/src/service-controller.ts +156 -84
- package/vendor/pi-memory/src/skills/lifecycle.ts +205 -0
- package/vendor/pi-memory/src/sync/connector.ts +146 -0
- package/vendor/pi-memory/src/sync/downflow.ts +54 -0
- package/vendor/pi-memory/src/sync/feedback.ts +30 -0
- package/vendor/pi-memory/src/sync/queue.ts +40 -0
- package/vendor/pi-memory/src/sync/schemas.ts +44 -0
- package/vendor/pi-memory/src/sync/sensitivity.ts +18 -0
- package/vendor/pi-memory/test/manager-service.test.ts +17 -0
- package/vendor/pi-memory/test/resolve-roots.test.ts +63 -0
- package/vendor/pi-memory/test/review-summary.test.ts +36 -0
- package/vendor/pi-memory/test/skill-lifecycle.test.ts +75 -0
- 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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 =
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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 =
|
|
121
|
+
let SKILL_DRAFTS_DIR = INITIAL_ROOTS.skillDraftsDir;
|
|
96
122
|
|
|
97
|
-
function
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
613
|
+
function buildLearningExtractorPrompt(conversationText: string, truncated: boolean, totalChars: number): string {
|
|
569
614
|
const lines = [
|
|
570
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
|
709
|
-
if (!
|
|
710
|
-
const truncated = truncateText(
|
|
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,
|
|
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
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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",
|
|
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(
|
|
1328
|
+
// Seed the cache so checkCollection(qmdCollectionName()) doesn't redundantly re-run
|
|
1282
1329
|
// setupQmdCollection during the short negative-cache window.
|
|
1283
|
-
qmdCollectionStatusCache.set(
|
|
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(
|
|
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",
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
|
|
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(
|
|
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
|
|
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"
|
|
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: "
|
|
2416
|
-
label: "Memory
|
|
2417
|
-
description: "
|
|
2418
|
-
parameters: Type.Object({
|
|
2419
|
-
|
|
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
|
|
2422
|
-
|
|
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: "
|
|
2432
|
-
label: "Memory
|
|
2433
|
-
description: "
|
|
2434
|
-
parameters: Type.Object({
|
|
2435
|
-
|
|
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
|
|
2440
|
-
|
|
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: "
|
|
2456
|
-
label: "Memory
|
|
2457
|
-
description: "
|
|
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
|
-
|
|
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
|
|
2464
|
-
const
|
|
2465
|
-
|
|
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: "
|
|
2477
|
-
label: "Memory
|
|
2478
|
-
description: "
|
|
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
|
-
|
|
2481
|
-
|
|
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
|
-
|
|
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
|
|
2486
|
-
|
|
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: {
|
|
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: "
|
|
2498
|
-
label: "Memory
|
|
2499
|
-
description: "
|
|
2500
|
-
parameters: Type.Object({
|
|
2501
|
-
|
|
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
|
|
2504
|
-
return { content: [{ type: "text", text:
|
|
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: {
|
|
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-
|
|
2513
|
-
description: "
|
|
2514
|
-
handler: async (
|
|
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-
|
|
2518
|
-
description: "
|
|
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 =
|
|
2521
|
-
ctx.ui.notify(result.
|
|
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-
|
|
2526
|
-
description: "
|
|
2624
|
+
pi.registerCommand("memory-curator-manager-disable", {
|
|
2625
|
+
description: "Disable the singleton Local Curator Manager service",
|
|
2527
2626
|
handler: async (_args, ctx) => {
|
|
2528
|
-
const
|
|
2529
|
-
ctx.ui.notify(
|
|
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-
|
|
2534
|
-
description: "
|
|
2640
|
+
pi.registerCommand("memory-skill", {
|
|
2641
|
+
description: "List, enable, or disable current-agent memory-managed skills",
|
|
2535
2642
|
handler: async (args, ctx) => {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
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-
|
|
2549
|
-
description: "
|
|
2550
|
-
handler: async (
|
|
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(
|
|
2806
|
+
let hasCollection = await checkCollection(qmdCollectionName());
|
|
2595
2807
|
if (!hasCollection) {
|
|
2596
2808
|
const created = await setupQmdCollection();
|
|
2597
2809
|
if (created) {
|