om-memory-system 3.0.0 → 3.1.0-next.3
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/{LICENSE → LICENSE.md} +4 -2
- package/README.md +108 -532
- package/dist/adapters/opencode/auto-capture-summary.js +23 -17
- package/dist/adapters/opencode/import-command.d.ts +42 -0
- package/dist/adapters/opencode/import-command.js +154 -0
- package/dist/adapters/pi/extension.js +4 -5
- package/dist/adapters/pi/import-command.d.ts +7 -19
- package/dist/adapters/pi/import-command.js +46 -161
- package/dist/adapters/pi/live-model.d.ts +17 -0
- package/dist/adapters/pi/live-model.js +86 -0
- package/dist/adapters/pi/profile.d.ts +7 -17
- package/dist/adapters/pi/profile.js +28 -66
- package/dist/adapters/pi/provider.d.ts +8 -1
- package/dist/adapters/pi/provider.js +34 -6
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +94 -0
- package/dist/config.d.ts +7 -20
- package/dist/config.js +24 -61
- package/dist/core/extraction.d.ts +2 -2
- package/dist/core/extraction.js +8 -3
- package/dist/core/profile-analysis.d.ts +16 -0
- package/dist/core/profile-analysis.js +47 -0
- package/dist/importer/import-args.d.ts +44 -0
- package/dist/importer/import-args.js +219 -0
- package/dist/importer/importer.d.ts +33 -9
- package/dist/importer/importer.js +153 -89
- package/dist/importer/ledger.d.ts +9 -2
- package/dist/importer/ledger.js +16 -2
- package/dist/importer/model-selection.d.ts +16 -0
- package/dist/importer/model-selection.js +84 -0
- package/dist/importer/opencode-import.d.ts +28 -0
- package/dist/importer/opencode-import.js +87 -0
- package/dist/importer/opencode-project.d.ts +11 -0
- package/dist/importer/opencode-project.js +38 -0
- package/dist/importer/opencode-reader.d.ts +19 -0
- package/dist/importer/opencode-reader.js +214 -0
- package/dist/importer/profile-import.d.ts +29 -0
- package/dist/importer/profile-import.js +111 -0
- package/dist/importer/run-import.d.ts +49 -0
- package/dist/importer/run-import.js +123 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +37 -1
- package/dist/services/ai/internal-capture-sessions.d.ts +2 -0
- package/dist/services/ai/internal-capture-sessions.js +7 -0
- package/dist/services/ai/live-model-choice.d.ts +52 -0
- package/dist/services/ai/live-model-choice.js +83 -0
- package/dist/services/ai/profile-llm-client.js +7 -5
- package/dist/services/user-memory-learning.js +9 -8
- package/dist/services/user-profile/ai-cleanup.js +8 -5
- package/dist/services/user-profile/user-profile-manager.js +11 -10
- package/dist/v2/adapter.js +27 -0
- package/package.json +5 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { CONFIG } from "../../config.js";
|
|
3
|
+
import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
|
|
3
4
|
import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
|
|
5
|
+
import { parseCaptureSummary } from "../../core/extraction.js";
|
|
4
6
|
import { log } from "../../services/logger.js";
|
|
5
7
|
import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
|
|
6
8
|
function buildSystemPrompt(languageName) {
|
|
@@ -26,21 +28,18 @@ CAPTURE if: code changed, bug fixed, feature added, decision made`;
|
|
|
26
28
|
}
|
|
27
29
|
export async function generateOpenCodeAutoCaptureSummary(request, notify) {
|
|
28
30
|
let opencodeProviderError;
|
|
29
|
-
|
|
31
|
+
const hostModel = resolveOpencodeHostModel(CONFIG);
|
|
32
|
+
if (hostModel) {
|
|
30
33
|
try {
|
|
31
|
-
if (CONFIG.memoryModel) {
|
|
34
|
+
if (CONFIG.memoryModel && CONFIG.opencodeProvider) {
|
|
32
35
|
log("opencodeProvider takes precedence over memoryModel for auto-capture");
|
|
33
36
|
}
|
|
34
|
-
const { isProviderConnected, getV2Client, generateStructuredOutput } = await loadOpencodeProvider();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
providerID = request.prompt.providerId;
|
|
42
|
-
modelID = request.prompt.modelId;
|
|
43
|
-
}
|
|
37
|
+
const { isProviderConnected, getV2Client, generateStructuredOutput, resolveOpencodeModelRef, } = await loadOpencodeProvider();
|
|
38
|
+
// "inherit" (explicit, or no model configured at all) follows this prompt's session model.
|
|
39
|
+
const { providerID, modelID } = resolveOpencodeModelRef({
|
|
40
|
+
...hostModel,
|
|
41
|
+
prompt: request.prompt,
|
|
42
|
+
});
|
|
44
43
|
if (!isProviderConnected(providerID)) {
|
|
45
44
|
throw new Error(`opencode provider '${providerID}' is not connected. Check your opencode provider configuration.`);
|
|
46
45
|
}
|
|
@@ -139,9 +138,16 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
|
|
|
139
138
|
if (!result.success || !result.data) {
|
|
140
139
|
throw new Error(result.error || "Failed to generate summary");
|
|
141
140
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
const rawReply = JSON.stringify(result.data);
|
|
142
|
+
const summary = parseCaptureSummary(rawReply);
|
|
143
|
+
if (!summary) {
|
|
144
|
+
log("OpenCode capture: model reply was not a valid capture summary", {
|
|
145
|
+
provider: CONFIG.memoryProvider,
|
|
146
|
+
modelId: CONFIG.memoryModel,
|
|
147
|
+
// The reply can carry conversation content, so log only its size.
|
|
148
|
+
replyLength: rawReply.length,
|
|
149
|
+
});
|
|
150
|
+
throw new Error("omms: OpenCode extraction returned an invalid summary payload");
|
|
151
|
+
}
|
|
152
|
+
return summary;
|
|
147
153
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type HistoryImportModels } from "../../importer/run-import.js";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode command surface for the historical-session importer:
|
|
4
|
+
*
|
|
5
|
+
* /memory-import-opencode-history --dry-run
|
|
6
|
+
* /memory-import-opencode-history
|
|
7
|
+
*
|
|
8
|
+
* The same options and model rules as Pi's command: this session's model by
|
|
9
|
+
* default, or `--model provider/id` from OpenCode's connected providers. Model
|
|
10
|
+
* calls go through OpenCode's own sign-in, so no omms API key is involved.
|
|
11
|
+
*/
|
|
12
|
+
export declare const OPENCODE_IMPORT_COMMAND = "memory-import-opencode-history";
|
|
13
|
+
export declare const OPENCODE_IMPORT_DESCRIPTION = "Import historical OpenCode sessions into the shared memory store (try --dry-run first)";
|
|
14
|
+
export declare const OPENCODE_IMPORT_USAGE: string;
|
|
15
|
+
export interface OpencodeModelRef {
|
|
16
|
+
providerID: string;
|
|
17
|
+
modelID: string;
|
|
18
|
+
}
|
|
19
|
+
export interface OpencodeImportCommandInput {
|
|
20
|
+
argsText: string;
|
|
21
|
+
/** Session directory; relative paths and the default project resolve here. */
|
|
22
|
+
directory: string;
|
|
23
|
+
/** The model the invoking session currently uses, if the host can tell. */
|
|
24
|
+
sessionModel: () => Promise<OpencodeModelRef | null>;
|
|
25
|
+
notify?: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
/** Capture and profile calls routed through OpenCode's structured-output sessions. */
|
|
28
|
+
export declare function createOpencodeImportModels(ref: OpencodeModelRef, directory: string): Promise<Required<HistoryImportModels>>;
|
|
29
|
+
/** Parse, run, and describe one import; always resolves to text for the session. */
|
|
30
|
+
export declare function runOpencodeImportCommand(input: OpencodeImportCommandInput): Promise<string>;
|
|
31
|
+
/** The model of the session's latest user message, from the V1 message list. */
|
|
32
|
+
export declare function latestUserMessageModel(client: {
|
|
33
|
+
session: {
|
|
34
|
+
messages: (input: {
|
|
35
|
+
path: {
|
|
36
|
+
id: string;
|
|
37
|
+
};
|
|
38
|
+
}) => Promise<{
|
|
39
|
+
data?: any;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
}, sessionID: string): Promise<OpencodeModelRef | null>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { CONFIG, isConfigured } from "../../config.js";
|
|
2
|
+
import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
|
|
3
|
+
import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, tokenizeImportArgs, } from "../../importer/import-args.js";
|
|
4
|
+
import { formatHistoryImportReport, summarizeHistoryImportReport, runHistoryImport, } from "../../importer/run-import.js";
|
|
5
|
+
import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
|
|
6
|
+
import { memoryClient } from "../../services/client.js";
|
|
7
|
+
import { log } from "../../services/logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* OpenCode command surface for the historical-session importer:
|
|
10
|
+
*
|
|
11
|
+
* /memory-import-opencode-history --dry-run
|
|
12
|
+
* /memory-import-opencode-history
|
|
13
|
+
*
|
|
14
|
+
* The same options and model rules as Pi's command: this session's model by
|
|
15
|
+
* default, or `--model provider/id` from OpenCode's connected providers. Model
|
|
16
|
+
* calls go through OpenCode's own sign-in, so no omms API key is involved.
|
|
17
|
+
*/
|
|
18
|
+
export const OPENCODE_IMPORT_COMMAND = "memory-import-opencode-history";
|
|
19
|
+
export const OPENCODE_IMPORT_DESCRIPTION = "Import historical OpenCode sessions into the shared memory store (try --dry-run first)";
|
|
20
|
+
export const OPENCODE_IMPORT_USAGE = historyImportUsage("opencode", "session");
|
|
21
|
+
let importCommandRunning = false;
|
|
22
|
+
function splitModel(value) {
|
|
23
|
+
const separator = value.indexOf("/");
|
|
24
|
+
return { providerID: value.slice(0, separator), modelID: value.slice(separator + 1) };
|
|
25
|
+
}
|
|
26
|
+
/** Capture and profile calls routed through OpenCode's structured-output sessions. */
|
|
27
|
+
export async function createOpencodeImportModels(ref, directory) {
|
|
28
|
+
const { getV2Client, generateStructuredOutput } = await loadOpencodeProvider();
|
|
29
|
+
const client = getV2Client();
|
|
30
|
+
if (!client)
|
|
31
|
+
throw new Error("the OpenCode client is not ready; retry in a moment");
|
|
32
|
+
const { z } = await import("zod");
|
|
33
|
+
const { buildCaptureSystemPrompt, createUserProfileAnalysisSchema, parseCaptureSummary } = await import("../../core/extraction.js");
|
|
34
|
+
const { detectLanguage, getLanguageName } = await import("../../services/language-detector.js");
|
|
35
|
+
const captureSchema = z.object({
|
|
36
|
+
summary: z.string().optional(),
|
|
37
|
+
type: z.string(),
|
|
38
|
+
tags: z.array(z.string()).optional(),
|
|
39
|
+
});
|
|
40
|
+
const capture = {
|
|
41
|
+
async summarize(request) {
|
|
42
|
+
const target = CONFIG.autoCaptureLanguage && CONFIG.autoCaptureLanguage !== "auto"
|
|
43
|
+
? CONFIG.autoCaptureLanguage
|
|
44
|
+
: detectLanguage(request.userPrompt);
|
|
45
|
+
const systemPrompt = buildCaptureSystemPrompt(getLanguageName(target));
|
|
46
|
+
const result = await generateStructuredOutput({
|
|
47
|
+
client,
|
|
48
|
+
...ref,
|
|
49
|
+
systemPrompt,
|
|
50
|
+
userPrompt: buildBoundedSummaryPrompt(request.context, systemPrompt, z.toJSONSchema(captureSchema)),
|
|
51
|
+
schema: captureSchema,
|
|
52
|
+
directory,
|
|
53
|
+
});
|
|
54
|
+
const parsed = parseCaptureSummary(JSON.stringify(result));
|
|
55
|
+
if (!parsed)
|
|
56
|
+
throw new Error("History capture returned an invalid summary");
|
|
57
|
+
return parsed;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const profileSchema = createUserProfileAnalysisSchema(z);
|
|
61
|
+
const profile = {
|
|
62
|
+
provider: ref.providerID,
|
|
63
|
+
modelId: ref.modelID,
|
|
64
|
+
async complete(systemPrompt, userPrompt) {
|
|
65
|
+
const result = await generateStructuredOutput({
|
|
66
|
+
client,
|
|
67
|
+
...ref,
|
|
68
|
+
systemPrompt,
|
|
69
|
+
userPrompt,
|
|
70
|
+
schema: profileSchema,
|
|
71
|
+
directory,
|
|
72
|
+
});
|
|
73
|
+
return JSON.stringify(result);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return { capture, profile };
|
|
77
|
+
}
|
|
78
|
+
/** Parse, run, and describe one import; always resolves to text for the session. */
|
|
79
|
+
export async function runOpencodeImportCommand(input) {
|
|
80
|
+
const name = OPENCODE_IMPORT_COMMAND;
|
|
81
|
+
// Some hosts pass the command name through with its arguments.
|
|
82
|
+
const argsText = input.argsText.trim().replace(new RegExp(`^/?${name}(\\s|$)`), "");
|
|
83
|
+
const parsed = parseHistoryImportArgs(tokenizeImportArgs(argsText), {
|
|
84
|
+
host: "opencode",
|
|
85
|
+
surface: "session",
|
|
86
|
+
});
|
|
87
|
+
if (parsed.help)
|
|
88
|
+
return OPENCODE_IMPORT_USAGE;
|
|
89
|
+
if (parsed.errors.length > 0)
|
|
90
|
+
return `${name}: ${parsed.errors.join("; ")}`;
|
|
91
|
+
if (!isConfigured())
|
|
92
|
+
return `${name}: memory system not configured`;
|
|
93
|
+
if (importCommandRunning)
|
|
94
|
+
return `${name}: an import is already running`;
|
|
95
|
+
importCommandRunning = true;
|
|
96
|
+
try {
|
|
97
|
+
let ref = null;
|
|
98
|
+
if (parsed.model || importNeedsModel(parsed)) {
|
|
99
|
+
ref = parsed.model ? splitModel(parsed.model) : await input.sessionModel();
|
|
100
|
+
if (!ref)
|
|
101
|
+
return `${name}: this session has no model yet; pass --model provider/id`;
|
|
102
|
+
const { isProviderConnected } = await loadOpencodeProvider();
|
|
103
|
+
if (!isProviderConnected(ref.providerID)) {
|
|
104
|
+
return `${name}: OpenCode provider "${ref.providerID}" is not connected`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const models = ref && !parsed.dryRun ? await createOpencodeImportModels(ref, input.directory) : {};
|
|
108
|
+
if (!parsed.dryRun) {
|
|
109
|
+
try {
|
|
110
|
+
await memoryClient.warmup();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
log("OpenCode import: memory warmup failed", { error: String(error) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let lastProgress = 0;
|
|
117
|
+
const report = await runHistoryImport("opencode", parsed, {
|
|
118
|
+
cwd: input.directory,
|
|
119
|
+
models,
|
|
120
|
+
onProgress: (processed, total, promptPreview) => {
|
|
121
|
+
if (processed - lastProgress >= 25 || processed === total) {
|
|
122
|
+
lastProgress = processed;
|
|
123
|
+
input.notify?.(`OpenCode import: ${processed}/${total} units (${promptPreview})`);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
log("OpenCode history import report", summarizeHistoryImportReport(report));
|
|
128
|
+
return formatHistoryImportReport("opencode", report, ref ? `${ref.providerID}/${ref.modelID}` : undefined);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
log("OpenCode history import failed", { error: message });
|
|
133
|
+
return `${name} failed: ${message}`;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
importCommandRunning = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** The model of the session's latest user message, from the V1 message list. */
|
|
140
|
+
export async function latestUserMessageModel(client, sessionID) {
|
|
141
|
+
try {
|
|
142
|
+
const messages = (await client.session.messages({ path: { id: sessionID } })).data ?? [];
|
|
143
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
144
|
+
const model = messages[i]?.info?.role === "user" ? messages[i].info.model : undefined;
|
|
145
|
+
if (model?.providerID && model?.modelID) {
|
|
146
|
+
return { providerID: model.providerID, modelID: model.modelID };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
log("OpenCode import: could not read the session model", { error: String(error) });
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
@@ -5,8 +5,7 @@ import { getLanguageName } from "../../services/language-detector.js";
|
|
|
5
5
|
import { log } from "../../services/logger.js";
|
|
6
6
|
import { memoryClient } from "../../services/client.js";
|
|
7
7
|
import { capturePiSettledWorkUnit, createPiCaptureState } from "./capture.js";
|
|
8
|
-
import {
|
|
9
|
-
import { resolveModelFromContext } from "./provider.js";
|
|
8
|
+
import { createPiLiveModels } from "./live-model.js";
|
|
10
9
|
import { registerPiHistoryImportCommand } from "./import-command.js";
|
|
11
10
|
import { performPiProfileLearning } from "./profile.js";
|
|
12
11
|
import { buildRetrievalSection, wrapRetrievalSection } from "../../core/retrieval.js";
|
|
@@ -93,7 +92,6 @@ export default function ommsPiExtension(pi) {
|
|
|
93
92
|
const level = notification.variant === "success" ? "info" : notification.variant;
|
|
94
93
|
ctx.ui.notify(`${notification.title}: ${notification.message}`, level);
|
|
95
94
|
};
|
|
96
|
-
const resolveModel = (ctx) => resolveModelFromContext(ctx);
|
|
97
95
|
pi.on("session_start", async (_event, ctx) => {
|
|
98
96
|
const status = startStatus(ctx, "warming");
|
|
99
97
|
try {
|
|
@@ -148,7 +146,8 @@ export default function ommsPiExtension(pi) {
|
|
|
148
146
|
const status = startStatus(ctx, "capturing");
|
|
149
147
|
try {
|
|
150
148
|
const entries = toSessionEntries(ctx.sessionManager.getBranch());
|
|
151
|
-
const
|
|
149
|
+
const liveModels = createPiLiveModels(ctx, notify(ctx));
|
|
150
|
+
const provider = liveModels.capture;
|
|
152
151
|
const captureResult = await capturePiSettledWorkUnit({
|
|
153
152
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
154
153
|
directory: ctx.cwd,
|
|
@@ -172,7 +171,7 @@ export default function ommsPiExtension(pi) {
|
|
|
172
171
|
await performPiProfileLearning({
|
|
173
172
|
directory: ctx.cwd,
|
|
174
173
|
prompts: batch,
|
|
175
|
-
|
|
174
|
+
resolveProfileModel: liveModels.profile,
|
|
176
175
|
notify: notify(ctx),
|
|
177
176
|
});
|
|
178
177
|
}
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type
|
|
2
|
+
import { type HistoryImportArgs } from "../../importer/import-args.js";
|
|
3
3
|
/**
|
|
4
4
|
* Pi command surface for the historical-session importer:
|
|
5
5
|
*
|
|
6
6
|
* /memory-import-pi-history --dry-run
|
|
7
7
|
* /memory-import-pi-history
|
|
8
8
|
*
|
|
9
|
-
* Runs over the shared importer service
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Runs over the shared importer service with the same options as the OpenCode
|
|
10
|
+
* command and the CLI. Extraction uses this session's current model, or
|
|
11
|
+
* `--model provider/id` from Pi's signed-in models. Source session files are
|
|
12
|
+
* only ever read.
|
|
12
13
|
*/
|
|
13
14
|
export declare const PI_IMPORT_COMMAND = "memory-import-pi-history";
|
|
14
|
-
export declare const PI_IMPORT_USAGE
|
|
15
|
-
export
|
|
16
|
-
dryRun: boolean;
|
|
17
|
-
force: boolean;
|
|
18
|
-
scope: "current-project" | "all-projects";
|
|
19
|
-
session?: string;
|
|
20
|
-
since?: number;
|
|
21
|
-
until?: number;
|
|
22
|
-
maxSessions?: number;
|
|
23
|
-
root?: string;
|
|
24
|
-
pathMaps: ImportPathMap[];
|
|
25
|
-
help: boolean;
|
|
26
|
-
errors: string[];
|
|
27
|
-
}
|
|
28
|
-
export declare function parseImportArgs(raw: string): ParsedImportArgs;
|
|
15
|
+
export declare const PI_IMPORT_USAGE: string;
|
|
16
|
+
export declare function parseImportArgs(raw: string): HistoryImportArgs;
|
|
29
17
|
export declare function registerPiHistoryImportCommand(pi: ExtensionAPI, getExtensionContext: () => ExtensionContext | null): void;
|
|
@@ -1,149 +1,25 @@
|
|
|
1
|
-
import { initConfigWithLegacyMigration, isConfigured } from "../../config.js";
|
|
1
|
+
import { initConfig, initConfigWithLegacyMigration, isConfigured } from "../../config.js";
|
|
2
2
|
import { log } from "../../services/logger.js";
|
|
3
3
|
import { memoryClient } from "../../services/client.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { createPiCaptureProvider,
|
|
4
|
+
import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, tokenizeImportArgs, } from "../../importer/import-args.js";
|
|
5
|
+
import { formatHistoryImportReport, summarizeHistoryImportReport, runHistoryImport, } from "../../importer/run-import.js";
|
|
6
|
+
import { createPiCaptureProvider, resolveImportModel } from "./provider.js";
|
|
7
|
+
import { adaptPiProfileModel } from "./profile.js";
|
|
7
8
|
/**
|
|
8
9
|
* Pi command surface for the historical-session importer:
|
|
9
10
|
*
|
|
10
11
|
* /memory-import-pi-history --dry-run
|
|
11
12
|
* /memory-import-pi-history
|
|
12
13
|
*
|
|
13
|
-
* Runs over the shared importer service
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Runs over the shared importer service with the same options as the OpenCode
|
|
15
|
+
* command and the CLI. Extraction uses this session's current model, or
|
|
16
|
+
* `--model provider/id` from Pi's signed-in models. Source session files are
|
|
17
|
+
* only ever read.
|
|
16
18
|
*/
|
|
17
19
|
export const PI_IMPORT_COMMAND = "memory-import-pi-history";
|
|
18
|
-
export const PI_IMPORT_USAGE =
|
|
19
|
-
|
|
20
|
-
Options:
|
|
21
|
-
--dry-run Discover, map, and report; write nothing
|
|
22
|
-
--force Reprocess units with terminal ledger states
|
|
23
|
-
--scope=current-project Import only sessions from this project (default)
|
|
24
|
-
--scope=all-projects Import every discovered session
|
|
25
|
-
--session=<id-or-file> Import one exact session
|
|
26
|
-
--since=<date> Only work units at/after this time
|
|
27
|
-
--until=<date> Only work units at/before this time
|
|
28
|
-
--max-sessions=<n> Limit discovery to the oldest n sessions
|
|
29
|
-
--map=<oldPath>=<newPath> Remap a recorded cwd that no longer exists
|
|
30
|
-
--root=<dir> Session root (default ~/.pi/agent/sessions)
|
|
31
|
-
|
|
32
|
-
Dates accept ISO 8601 (2026-01-01, 2026-01-01T10:00:00Z) or epoch
|
|
33
|
-
milliseconds. --since/--until filter on each work unit's user-entry
|
|
34
|
-
timestamp, inclusive. Unresolvable recorded cwds are skipped and listed
|
|
35
|
-
in the report; use --map to import them into an existing directory.`;
|
|
36
|
-
function parseTimestamp(raw) {
|
|
37
|
-
if (!raw)
|
|
38
|
-
return undefined;
|
|
39
|
-
if (/^\d+$/.test(raw))
|
|
40
|
-
return Number(raw);
|
|
41
|
-
const parsed = Date.parse(raw);
|
|
42
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
43
|
-
}
|
|
20
|
+
export const PI_IMPORT_USAGE = historyImportUsage("pi", "session");
|
|
44
21
|
export function parseImportArgs(raw) {
|
|
45
|
-
|
|
46
|
-
dryRun: false,
|
|
47
|
-
force: false,
|
|
48
|
-
scope: "current-project",
|
|
49
|
-
pathMaps: [],
|
|
50
|
-
help: false,
|
|
51
|
-
errors: [],
|
|
52
|
-
};
|
|
53
|
-
for (const token of raw.trim().split(/\s+/).filter(Boolean)) {
|
|
54
|
-
if (token === "--help" || token === "-h") {
|
|
55
|
-
result.help = true;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (token === "--dry-run") {
|
|
59
|
-
result.dryRun = true;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (token === "--force") {
|
|
63
|
-
result.force = true;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const equals = token.indexOf("=");
|
|
67
|
-
const flag = equals === -1 ? token : token.slice(0, equals);
|
|
68
|
-
const value = equals === -1 ? "" : token.slice(equals + 1);
|
|
69
|
-
switch (flag) {
|
|
70
|
-
case "--scope":
|
|
71
|
-
if (value === "current-project" || value === "all-projects") {
|
|
72
|
-
result.scope = value;
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
result.errors.push(`--scope must be current-project or all-projects, got "${value}"`);
|
|
76
|
-
}
|
|
77
|
-
break;
|
|
78
|
-
case "--session":
|
|
79
|
-
if (value)
|
|
80
|
-
result.session = value;
|
|
81
|
-
else
|
|
82
|
-
result.errors.push("--session requires a value");
|
|
83
|
-
break;
|
|
84
|
-
case "--since":
|
|
85
|
-
result.since = parseTimestamp(value);
|
|
86
|
-
if (result.since === undefined)
|
|
87
|
-
result.errors.push(`--since is not a valid date: "${value}"`);
|
|
88
|
-
break;
|
|
89
|
-
case "--until":
|
|
90
|
-
result.until = parseTimestamp(value);
|
|
91
|
-
if (result.until === undefined)
|
|
92
|
-
result.errors.push(`--until is not a valid date: "${value}"`);
|
|
93
|
-
break;
|
|
94
|
-
case "--max-sessions": {
|
|
95
|
-
const parsed = Number(value);
|
|
96
|
-
if (Number.isInteger(parsed) && parsed >= 0)
|
|
97
|
-
result.maxSessions = parsed;
|
|
98
|
-
else
|
|
99
|
-
result.errors.push(`--max-sessions must be a non-negative integer, got "${value}"`);
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
case "--root":
|
|
103
|
-
if (value)
|
|
104
|
-
result.root = value;
|
|
105
|
-
else
|
|
106
|
-
result.errors.push("--root requires a value");
|
|
107
|
-
break;
|
|
108
|
-
case "--map": {
|
|
109
|
-
const separator = value.indexOf("=");
|
|
110
|
-
if (separator > 0 && separator < value.length - 1) {
|
|
111
|
-
result.pathMaps.push({
|
|
112
|
-
from: value.slice(0, separator),
|
|
113
|
-
to: value.slice(separator + 1),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
result.errors.push(`--map must be <oldPath>=<newPath>, got "${value}"`);
|
|
118
|
-
}
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
default:
|
|
122
|
-
result.errors.push(`Unknown option: "${token}"`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
function formatReport(report) {
|
|
128
|
-
const lines = [
|
|
129
|
-
`Pi history import ${report.dryRun ? "(dry-run)" : ""}`.trim(),
|
|
130
|
-
` sessions: ${report.sessionsLoaded} loaded, ${report.sessionsFilteredOut} filtered, ` +
|
|
131
|
-
`${report.sessionsUnrecognized} unrecognized, ` +
|
|
132
|
-
`${report.unresolvableSessions.length} unresolvable cwd`,
|
|
133
|
-
` work units: ${report.unitsTotal} total, ` +
|
|
134
|
-
(report.dryRun
|
|
135
|
-
? `${report.unitsWouldImport} would import`
|
|
136
|
-
: `${report.unitsImported} imported, ${report.unitsSkipped} skipped, ` +
|
|
137
|
-
`${report.unitsFailed} failed`) +
|
|
138
|
-
`, ${report.unitsAlreadyHandled} already handled`,
|
|
139
|
-
];
|
|
140
|
-
for (const project of report.projects) {
|
|
141
|
-
lines.push(` project ${project.tag} (${project.sessions} sessions, ${project.units} units) -> ${project.directory}`);
|
|
142
|
-
}
|
|
143
|
-
if (report.loadErrors.length > 0) {
|
|
144
|
-
lines.push(` load errors: ${report.loadErrors.length}`);
|
|
145
|
-
}
|
|
146
|
-
return lines.join("\n");
|
|
22
|
+
return parseHistoryImportArgs(tokenizeImportArgs(raw), { host: "pi", surface: "session" });
|
|
147
23
|
}
|
|
148
24
|
let importCommandRunning = false;
|
|
149
25
|
export function registerPiHistoryImportCommand(pi, getExtensionContext) {
|
|
@@ -177,47 +53,56 @@ export function registerPiHistoryImportCommand(pi, getExtensionContext) {
|
|
|
177
53
|
notify("memory-import-pi-history: an import is already running");
|
|
178
54
|
return;
|
|
179
55
|
}
|
|
180
|
-
//
|
|
181
|
-
|
|
56
|
+
// A dry-run must not trigger the one-time storage migration.
|
|
57
|
+
if (parsed.dryRun)
|
|
58
|
+
initConfig(ctx.cwd);
|
|
59
|
+
else
|
|
60
|
+
initConfigWithLegacyMigration(ctx.cwd);
|
|
182
61
|
if (typeof commandCtx.waitForIdle === "function") {
|
|
183
62
|
await commandCtx.waitForIdle();
|
|
184
63
|
}
|
|
64
|
+
// The command context carries the live session model; fall back to the
|
|
65
|
+
// last session context for hosts whose command context lacks a registry.
|
|
66
|
+
const modelCtx = commandCtx?.modelRegistry ? commandCtx : ctx;
|
|
67
|
+
const selectedModel = parsed.model || importNeedsModel(parsed)
|
|
68
|
+
? resolveImportModel(modelCtx, parsed.model)
|
|
69
|
+
: null;
|
|
70
|
+
if ((parsed.model || importNeedsModel(parsed)) && !selectedModel) {
|
|
71
|
+
notify(parsed.model
|
|
72
|
+
? `memory-import-pi-history: model not found: ${parsed.model}`
|
|
73
|
+
: "memory-import-pi-history: this session has no model; pass --model provider/id");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
185
76
|
importCommandRunning = true;
|
|
186
77
|
try {
|
|
187
78
|
// Best-effort warmup so failures surface as per-unit errors instead of
|
|
188
79
|
// every unit failing on embedding initialisation.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
80
|
+
if (!parsed.dryRun) {
|
|
81
|
+
try {
|
|
82
|
+
await memoryClient.warmup();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
log("Pi import: memory warmup failed", { error: String(error) });
|
|
86
|
+
}
|
|
194
87
|
}
|
|
195
|
-
const filters = {
|
|
196
|
-
scope: parsed.scope,
|
|
197
|
-
currentDirectory: ctx.cwd,
|
|
198
|
-
...(parsed.session !== undefined ? { session: parsed.session } : {}),
|
|
199
|
-
...(parsed.since !== undefined ? { since: parsed.since } : {}),
|
|
200
|
-
...(parsed.until !== undefined ? { until: parsed.until } : {}),
|
|
201
|
-
...(parsed.maxSessions !== undefined ? { maxSessions: parsed.maxSessions } : {}),
|
|
202
|
-
...(parsed.root !== undefined ? { root: parsed.root } : {}),
|
|
203
|
-
...(parsed.force ? { force: true } : {}),
|
|
204
|
-
...(parsed.dryRun ? { dryRun: true } : {}),
|
|
205
|
-
pathMaps: parsed.pathMaps,
|
|
206
|
-
};
|
|
207
|
-
const provider = createPiCaptureProvider(() => resolveModelFromContext(ctx));
|
|
208
88
|
let lastProgressNotify = 0;
|
|
209
|
-
const report = await
|
|
210
|
-
|
|
211
|
-
|
|
89
|
+
const report = await runHistoryImport("pi", parsed, {
|
|
90
|
+
cwd: ctx.cwd,
|
|
91
|
+
models: selectedModel && !parsed.dryRun
|
|
92
|
+
? {
|
|
93
|
+
capture: createPiCaptureProvider(() => selectedModel),
|
|
94
|
+
profile: adaptPiProfileModel(selectedModel),
|
|
95
|
+
}
|
|
96
|
+
: {},
|
|
212
97
|
onProgress: (processed, total, promptPreview) => {
|
|
213
98
|
if (processed - lastProgressNotify >= 25 || processed === total) {
|
|
214
99
|
lastProgressNotify = processed;
|
|
215
100
|
notify(`Pi import: ${processed}/${total} units (${promptPreview})`);
|
|
216
101
|
}
|
|
217
102
|
},
|
|
218
|
-
}
|
|
219
|
-
log("Pi history import report",
|
|
220
|
-
notify(
|
|
103
|
+
});
|
|
104
|
+
log("Pi history import report", summarizeHistoryImportReport(report));
|
|
105
|
+
notify(formatHistoryImportReport("pi", report, selectedModel ? `${selectedModel.provider}/${selectedModel.modelId}` : undefined));
|
|
221
106
|
}
|
|
222
107
|
catch (error) {
|
|
223
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AutoCaptureNotification, CaptureSummaryProvider } from "../../core/host.js";
|
|
2
|
+
import type { ModelPort } from "../../core/profile-analysis.js";
|
|
3
|
+
import { type PiModelContext } from "./provider.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pi live capture and profile learning, by the same rule as OpenCode:
|
|
6
|
+
* `piProvider`/`piModel` ("inherit" = this session's model), else the
|
|
7
|
+
* external API (`memoryModel`/`memoryApiUrl`/`memoryApiKey`), else this
|
|
8
|
+
* session's model. A failing Pi model falls back to the external API when it
|
|
9
|
+
* is configured, as a failing OpenCode model does.
|
|
10
|
+
*/
|
|
11
|
+
export interface PiLiveModels {
|
|
12
|
+
capture: CaptureSummaryProvider;
|
|
13
|
+
profile: () => ModelPort | null;
|
|
14
|
+
}
|
|
15
|
+
type Notify = (notification: AutoCaptureNotification) => Promise<void> | void;
|
|
16
|
+
export declare function createPiLiveModels(ctx: PiModelContext, notify?: Notify): PiLiveModels;
|
|
17
|
+
export {};
|