om-memory-system 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +587 -0
- package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
- package/dist/adapters/opencode/auto-capture-host.js +29 -0
- package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
- package/dist/adapters/opencode/auto-capture-summary.js +147 -0
- package/dist/adapters/opencode/conversation.d.ts +2 -0
- package/dist/adapters/opencode/conversation.js +50 -0
- package/dist/adapters/opencode/user-prompt.d.ts +9 -0
- package/dist/adapters/opencode/user-prompt.js +26 -0
- package/dist/adapters/pi/capture.d.ts +41 -0
- package/dist/adapters/pi/capture.js +91 -0
- package/dist/adapters/pi/conversation.d.ts +38 -0
- package/dist/adapters/pi/conversation.js +118 -0
- package/dist/adapters/pi/extension.d.ts +18 -0
- package/dist/adapters/pi/extension.js +234 -0
- package/dist/adapters/pi/import-command.d.ts +29 -0
- package/dist/adapters/pi/import-command.js +232 -0
- package/dist/adapters/pi/profile.d.ts +39 -0
- package/dist/adapters/pi/profile.js +148 -0
- package/dist/adapters/pi/provider.d.ts +50 -0
- package/dist/adapters/pi/provider.js +77 -0
- package/dist/config.d.ts +290 -0
- package/dist/config.js +742 -0
- package/dist/core/capture-context.d.ts +4 -0
- package/dist/core/capture-context.js +132 -0
- package/dist/core/capture.d.ts +15 -0
- package/dist/core/capture.js +68 -0
- package/dist/core/extraction.d.ts +80 -0
- package/dist/core/extraction.js +140 -0
- package/dist/core/host.d.ts +52 -0
- package/dist/core/host.js +1 -0
- package/dist/core/memory-operations.d.ts +27 -0
- package/dist/core/memory-operations.js +247 -0
- package/dist/core/retrieval.d.ts +12 -0
- package/dist/core/retrieval.js +72 -0
- package/dist/importer/discovery.d.ts +31 -0
- package/dist/importer/discovery.js +88 -0
- package/dist/importer/importer.d.ts +87 -0
- package/dist/importer/importer.js +260 -0
- package/dist/importer/ledger.d.ts +44 -0
- package/dist/importer/ledger.js +137 -0
- package/dist/importer/session-loader.d.ts +19 -0
- package/dist/importer/session-loader.js +12 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +601 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +11 -0
- package/dist/services/ai/ai-provider-factory.d.ts +7 -0
- package/dist/services/ai/ai-provider-factory.js +41 -0
- package/dist/services/ai/api-key-placeholder.d.ts +1 -0
- package/dist/services/ai/api-key-placeholder.js +14 -0
- package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
- package/dist/services/ai/internal-capture-sessions.js +33 -0
- package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
- package/dist/services/ai/opencode-diagnostics.js +36 -0
- package/dist/services/ai/opencode-host-config.d.ts +10 -0
- package/dist/services/ai/opencode-host-config.js +51 -0
- package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
- package/dist/services/ai/opencode-provider-loader.js +4 -0
- package/dist/services/ai/opencode-provider.d.ts +92 -0
- package/dist/services/ai/opencode-provider.js +419 -0
- package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
- package/dist/services/ai/opencode-sdk-client.js +38 -0
- package/dist/services/ai/profile-llm-client.d.ts +2 -0
- package/dist/services/ai/profile-llm-client.js +27 -0
- package/dist/services/ai/provider-config.d.ts +17 -0
- package/dist/services/ai/provider-config.js +30 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
- package/dist/services/ai/providers/anthropic-messages.js +220 -0
- package/dist/services/ai/providers/base-provider.d.ts +24 -0
- package/dist/services/ai/providers/base-provider.js +24 -0
- package/dist/services/ai/providers/google-gemini.d.ts +15 -0
- package/dist/services/ai/providers/google-gemini.js +228 -0
- package/dist/services/ai/providers/minimax.d.ts +35 -0
- package/dist/services/ai/providers/minimax.js +55 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
- package/dist/services/ai/providers/openai-chat-completion.js +387 -0
- package/dist/services/ai/providers/openai-responses.d.ts +13 -0
- package/dist/services/ai/providers/openai-responses.js +182 -0
- package/dist/services/ai/providers/orcarouter.d.ts +51 -0
- package/dist/services/ai/providers/orcarouter.js +66 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
- package/dist/services/ai/session/ai-session-manager.js +211 -0
- package/dist/services/ai/session/session-types.d.ts +42 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +40 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
- package/dist/services/ai/validators/user-profile-validator.js +114 -0
- package/dist/services/api-handlers.d.ts +186 -0
- package/dist/services/api-handlers.js +1284 -0
- package/dist/services/auth-token.d.ts +14 -0
- package/dist/services/auth-token.js +59 -0
- package/dist/services/auto-capture.d.ts +3 -0
- package/dist/services/auto-capture.js +127 -0
- package/dist/services/cleanup-service.d.ts +22 -0
- package/dist/services/cleanup-service.js +109 -0
- package/dist/services/client.d.ts +134 -0
- package/dist/services/client.js +327 -0
- package/dist/services/context.d.ts +10 -0
- package/dist/services/context.js +27 -0
- package/dist/services/cors.d.ts +12 -0
- package/dist/services/cors.js +52 -0
- package/dist/services/deduplication-service.d.ts +28 -0
- package/dist/services/deduplication-service.js +111 -0
- package/dist/services/embedding.d.ts +36 -0
- package/dist/services/embedding.js +200 -0
- package/dist/services/jsonc.d.ts +6 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/language-detector.d.ts +2 -0
- package/dist/services/language-detector.js +33 -0
- package/dist/services/legacy-migration.d.ts +77 -0
- package/dist/services/legacy-migration.js +343 -0
- package/dist/services/logger.d.ts +1 -0
- package/dist/services/logger.js +119 -0
- package/dist/services/memory-portability-service.d.ts +37 -0
- package/dist/services/memory-portability-service.js +340 -0
- package/dist/services/memory-scope.d.ts +16 -0
- package/dist/services/memory-scope.js +43 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.js +307 -0
- package/dist/services/onnxruntime-resolve.d.ts +24 -0
- package/dist/services/onnxruntime-resolve.js +174 -0
- package/dist/services/privacy.d.ts +26 -0
- package/dist/services/privacy.js +69 -0
- package/dist/services/request-timeouts.d.ts +9 -0
- package/dist/services/request-timeouts.js +11 -0
- package/dist/services/runtime-require.d.ts +17 -0
- package/dist/services/runtime-require.js +75 -0
- package/dist/services/secret-resolver.d.ts +1 -0
- package/dist/services/secret-resolver.js +65 -0
- package/dist/services/shard-inventory-service.d.ts +69 -0
- package/dist/services/shard-inventory-service.js +315 -0
- package/dist/services/shard-path-migration-service.d.ts +31 -0
- package/dist/services/shard-path-migration-service.js +367 -0
- package/dist/services/tag-prefix-migration.d.ts +60 -0
- package/dist/services/tag-prefix-migration.js +439 -0
- package/dist/services/tag-prefix-scan.d.ts +26 -0
- package/dist/services/tag-prefix-scan.js +91 -0
- package/dist/services/tags.d.ts +29 -0
- package/dist/services/tags.js +251 -0
- package/dist/services/turso/connection-manager.d.ts +15 -0
- package/dist/services/turso/connection-manager.js +163 -0
- package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
- package/dist/services/turso/cross-process-write-lock.js +89 -0
- package/dist/services/turso/legacy-migrator.d.ts +1 -0
- package/dist/services/turso/legacy-migrator.js +540 -0
- package/dist/services/turso/lifecycle.d.ts +1 -0
- package/dist/services/turso/lifecycle.js +14 -0
- package/dist/services/turso/operation-lock.d.ts +2 -0
- package/dist/services/turso/operation-lock.js +61 -0
- package/dist/services/turso/ready.d.ts +2 -0
- package/dist/services/turso/ready.js +36 -0
- package/dist/services/turso/shard-manager.d.ts +38 -0
- package/dist/services/turso/shard-manager.js +496 -0
- package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
- package/dist/services/turso/sqlite-handle-release.js +56 -0
- package/dist/services/turso/turso-db.d.ts +18 -0
- package/dist/services/turso/turso-db.js +53 -0
- package/dist/services/turso/types.d.ts +42 -0
- package/dist/services/turso/types.js +1 -0
- package/dist/services/turso/vector-search.d.ts +41 -0
- package/dist/services/turso/vector-search.js +384 -0
- package/dist/services/turso/vector-utils.d.ts +7 -0
- package/dist/services/turso/vector-utils.js +67 -0
- package/dist/services/user-memory-learning.d.ts +97 -0
- package/dist/services/user-memory-learning.js +673 -0
- package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
- package/dist/services/user-profile/ai-cleanup.js +486 -0
- package/dist/services/user-profile/profile-context.d.ts +1 -0
- package/dist/services/user-profile/profile-context.js +105 -0
- package/dist/services/user-profile/profile-utils.d.ts +2 -0
- package/dist/services/user-profile/profile-utils.js +45 -0
- package/dist/services/user-profile/types.d.ts +92 -0
- package/dist/services/user-profile/types.js +1 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
- package/dist/services/user-profile/user-profile-manager.js +1660 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
- package/dist/services/user-prompt/user-prompt-manager.js +303 -0
- package/dist/services/web-api-auth.d.ts +3 -0
- package/dist/services/web-api-auth.js +25 -0
- package/dist/services/web-auth.d.ts +46 -0
- package/dist/services/web-auth.js +105 -0
- package/dist/services/web-server.d.ts +57 -0
- package/dist/services/web-server.js +659 -0
- package/dist/shared/api/index.d.ts +4 -0
- package/dist/shared/api/index.js +2 -0
- package/dist/shared/api/portability-schemas.d.ts +56 -0
- package/dist/shared/api/portability-schemas.js +39 -0
- package/dist/shared/api/schemas.d.ts +295 -0
- package/dist/shared/api/schemas.js +134 -0
- package/dist/shared/api/types.d.ts +28 -0
- package/dist/shared/api/types.js +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/context-limit.d.ts +10 -0
- package/dist/utils/context-limit.js +43 -0
- package/dist/utils/math.d.ts +7 -0
- package/dist/utils/math.js +48 -0
- package/dist/utils/profile.d.ts +2 -0
- package/dist/utils/profile.js +15 -0
- package/dist/v2/adapter.d.ts +5 -0
- package/dist/v2/adapter.js +218 -0
- package/dist/v2/legacy-client.d.ts +15 -0
- package/dist/v2/legacy-client.js +257 -0
- package/dist/v2/memory-bridge.d.ts +18 -0
- package/dist/v2/memory-bridge.js +29 -0
- package/dist/v2/plugin.d.ts +3 -0
- package/dist/v2/plugin.js +24 -0
- package/dist/web/assets/index-DPjEpD1h.css +2 -0
- package/dist/web/assets/index-do26c4JJ.js +122 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/omms-icon.png +0 -0
- package/package.json +138 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { CONFIG, initConfigWithLegacyMigration, isConfigured } from "../../config.js";
|
|
3
|
+
import { executeMemoryOperation } from "../../core/memory-operations.js";
|
|
4
|
+
import { getLanguageName } from "../../services/language-detector.js";
|
|
5
|
+
import { log } from "../../services/logger.js";
|
|
6
|
+
import { memoryClient } from "../../services/client.js";
|
|
7
|
+
import { capturePiSettledWorkUnit, createPiCaptureState } from "./capture.js";
|
|
8
|
+
import { createPiCaptureProvider } from "./provider.js";
|
|
9
|
+
import { resolveModelFromContext } from "./provider.js";
|
|
10
|
+
import { registerPiHistoryImportCommand } from "./import-command.js";
|
|
11
|
+
import { performPiProfileLearning } from "./profile.js";
|
|
12
|
+
import { buildRetrievalSection, wrapRetrievalSection } from "../../core/retrieval.js";
|
|
13
|
+
const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("omms.plugin.warmedup");
|
|
14
|
+
const OMMS_STATUS_KEY = "omms";
|
|
15
|
+
const MEMORY_TOOL_MODES = [
|
|
16
|
+
"add",
|
|
17
|
+
"search",
|
|
18
|
+
"profile",
|
|
19
|
+
"list",
|
|
20
|
+
"forget",
|
|
21
|
+
"help",
|
|
22
|
+
"migrate",
|
|
23
|
+
"list-shards",
|
|
24
|
+
"export",
|
|
25
|
+
"import",
|
|
26
|
+
];
|
|
27
|
+
function toSessionEntries(branch) {
|
|
28
|
+
return Array.isArray(branch) ? branch : [];
|
|
29
|
+
}
|
|
30
|
+
function lastSettledUserPrompt(entries) {
|
|
31
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
32
|
+
const entry = entries[i];
|
|
33
|
+
if (entry.type !== "message" || entry.message?.role !== "user")
|
|
34
|
+
continue;
|
|
35
|
+
const content = entry.message.content;
|
|
36
|
+
if (typeof content === "string" && content.trim())
|
|
37
|
+
return content;
|
|
38
|
+
if (Array.isArray(content)) {
|
|
39
|
+
const text = content
|
|
40
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
41
|
+
.map((block) => block.text)
|
|
42
|
+
.join("\n")
|
|
43
|
+
.trim();
|
|
44
|
+
if (text)
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Pi extension entry point for the shared omms engine.
|
|
53
|
+
*
|
|
54
|
+
* Lifecycle mapping:
|
|
55
|
+
* - `session_start` -> load shared config for ctx.cwd, warm storage
|
|
56
|
+
* - `before_agent_start` -> prompt-aware semantic retrieval, injected as a
|
|
57
|
+
* delimited system-prompt section (never a fake
|
|
58
|
+
* user message)
|
|
59
|
+
* - `agent_settled` -> automatic capture of the settled work unit
|
|
60
|
+
* - `session_shutdown` -> idempotent session-scoped cleanup
|
|
61
|
+
* - `memory` tool -> shared add/search/profile/list/forget/help and
|
|
62
|
+
* portability operations
|
|
63
|
+
*
|
|
64
|
+
* Pi types are imported type-only: the compiled extension carries no Pi
|
|
65
|
+
* runtime dependency and uses the host Pi runtime provided at load time.
|
|
66
|
+
*/
|
|
67
|
+
export default function ommsPiExtension(pi) {
|
|
68
|
+
let captureState = createPiCaptureState();
|
|
69
|
+
let promptsSinceProfileAnalysis = [];
|
|
70
|
+
// Latest session context, refreshed on session_start and consumed by the
|
|
71
|
+
// import command (command contexts do not expose the model registry).
|
|
72
|
+
let latestCtx = null;
|
|
73
|
+
let statusVersion = 0;
|
|
74
|
+
const startStatus = (ctx, status) => {
|
|
75
|
+
const version = ++statusVersion;
|
|
76
|
+
if (ctx.hasUI)
|
|
77
|
+
ctx.ui.setStatus(OMMS_STATUS_KEY, `omms:${status}`);
|
|
78
|
+
return version;
|
|
79
|
+
};
|
|
80
|
+
const finishStatus = (ctx, version, status) => {
|
|
81
|
+
if (version !== statusVersion || !ctx.hasUI)
|
|
82
|
+
return;
|
|
83
|
+
ctx.ui.setStatus(OMMS_STATUS_KEY, `omms:${status}`);
|
|
84
|
+
};
|
|
85
|
+
const clearStatus = (ctx) => {
|
|
86
|
+
statusVersion++;
|
|
87
|
+
if (ctx.hasUI)
|
|
88
|
+
ctx.ui.setStatus(OMMS_STATUS_KEY, undefined);
|
|
89
|
+
};
|
|
90
|
+
const notify = (ctx) => (notification) => {
|
|
91
|
+
if (!ctx.hasUI)
|
|
92
|
+
return;
|
|
93
|
+
const level = notification.variant === "success" ? "info" : notification.variant;
|
|
94
|
+
ctx.ui.notify(`${notification.title}: ${notification.message}`, level);
|
|
95
|
+
};
|
|
96
|
+
const resolveModel = (ctx) => resolveModelFromContext(ctx);
|
|
97
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
98
|
+
const status = startStatus(ctx, "warming");
|
|
99
|
+
try {
|
|
100
|
+
latestCtx = ctx;
|
|
101
|
+
initConfigWithLegacyMigration(ctx.cwd);
|
|
102
|
+
captureState = createPiCaptureState();
|
|
103
|
+
const globalScope = globalThis;
|
|
104
|
+
if (!globalScope[GLOBAL_PLUGIN_WARMUP_KEY] && isConfigured()) {
|
|
105
|
+
(async () => {
|
|
106
|
+
try {
|
|
107
|
+
await memoryClient.warmup();
|
|
108
|
+
globalScope[GLOBAL_PLUGIN_WARMUP_KEY] = true;
|
|
109
|
+
finishStatus(ctx, status, "connected");
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
log("Pi plugin memory warmup failed", { error: String(error) });
|
|
113
|
+
finishStatus(ctx, status, "error");
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
finishStatus(ctx, status, "connected");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
log("Pi session_start error", { error: String(error) });
|
|
123
|
+
finishStatus(ctx, status, "error");
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
127
|
+
if (!isConfigured() || !CONFIG.chatMessage.enabled)
|
|
128
|
+
return;
|
|
129
|
+
const status = startStatus(ctx, "recalling");
|
|
130
|
+
try {
|
|
131
|
+
const section = await buildRetrievalSection(event.prompt, ctx.cwd, ctx.sessionManager.getSessionId());
|
|
132
|
+
finishStatus(ctx, status, "connected");
|
|
133
|
+
if (!section)
|
|
134
|
+
return;
|
|
135
|
+
return {
|
|
136
|
+
systemPrompt: event.systemPrompt + "\n\n" + wrapRetrievalSection(section),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
log("Pi before_agent_start retrieval error", { error: String(error) });
|
|
141
|
+
finishStatus(ctx, status, "error");
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
146
|
+
if (!isConfigured())
|
|
147
|
+
return;
|
|
148
|
+
const status = startStatus(ctx, "capturing");
|
|
149
|
+
try {
|
|
150
|
+
const entries = toSessionEntries(ctx.sessionManager.getBranch());
|
|
151
|
+
const provider = createPiCaptureProvider(() => resolveModel(ctx));
|
|
152
|
+
const captureResult = await capturePiSettledWorkUnit({
|
|
153
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
154
|
+
directory: ctx.cwd,
|
|
155
|
+
entries,
|
|
156
|
+
provider,
|
|
157
|
+
state: captureState,
|
|
158
|
+
prompt: {
|
|
159
|
+
providerId: ctx.model?.provider ?? null,
|
|
160
|
+
modelId: ctx.model?.id ?? null,
|
|
161
|
+
},
|
|
162
|
+
notify: notify(ctx),
|
|
163
|
+
});
|
|
164
|
+
const settledPrompt = lastSettledUserPrompt(entries);
|
|
165
|
+
if (settledPrompt &&
|
|
166
|
+
CONFIG.userProfileAnalysisInterval > 0 &&
|
|
167
|
+
!memoryClient.getEmbeddingInitError?.()) {
|
|
168
|
+
promptsSinceProfileAnalysis.push(settledPrompt);
|
|
169
|
+
if (promptsSinceProfileAnalysis.length >= CONFIG.userProfileAnalysisInterval) {
|
|
170
|
+
const batch = promptsSinceProfileAnalysis;
|
|
171
|
+
promptsSinceProfileAnalysis = [];
|
|
172
|
+
await performPiProfileLearning({
|
|
173
|
+
directory: ctx.cwd,
|
|
174
|
+
prompts: batch,
|
|
175
|
+
resolveModel: () => resolveModel(ctx),
|
|
176
|
+
notify: notify(ctx),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finishStatus(ctx, status, captureResult.status === "failed" ? "error" : "connected");
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
log("Pi agent_settled error", { error: String(error) });
|
|
184
|
+
finishStatus(ctx, status, "error");
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
188
|
+
clearStatus(ctx);
|
|
189
|
+
latestCtx = null;
|
|
190
|
+
captureState = createPiCaptureState();
|
|
191
|
+
promptsSinceProfileAnalysis = [];
|
|
192
|
+
globalThis[GLOBAL_PLUGIN_WARMUP_KEY] = false;
|
|
193
|
+
try {
|
|
194
|
+
await memoryClient.close();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Idempotent cleanup: closing an already-closed client must never fail
|
|
198
|
+
// the shutdown flow across quit, reload, new, resume, and fork.
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
pi.registerTool({
|
|
202
|
+
name: "memory",
|
|
203
|
+
label: "Memory",
|
|
204
|
+
description: `Manage and query project memory (MATCH USER LANGUAGE: ${getLanguageName(CONFIG.autoCaptureLanguage || "en")}). Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Use migrate/list-shards/export/import when a project directory moves. Search/list scope: project or all-projects.`,
|
|
205
|
+
parameters: Type.Object({
|
|
206
|
+
mode: Type.Optional(Type.Union(MEMORY_TOOL_MODES.map((mode) => Type.Literal(mode)))),
|
|
207
|
+
content: Type.Optional(Type.String()),
|
|
208
|
+
query: Type.Optional(Type.String()),
|
|
209
|
+
tags: Type.Optional(Type.String()),
|
|
210
|
+
type: Type.Optional(Type.String()),
|
|
211
|
+
memoryId: Type.Optional(Type.String()),
|
|
212
|
+
limit: Type.Optional(Type.Number()),
|
|
213
|
+
scope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("all-projects")])),
|
|
214
|
+
fromPath: Type.Optional(Type.String()),
|
|
215
|
+
fromHash: Type.Optional(Type.String()),
|
|
216
|
+
outputPath: Type.Optional(Type.String()),
|
|
217
|
+
inputPath: Type.Optional(Type.String()),
|
|
218
|
+
dryRun: Type.Optional(Type.Boolean()),
|
|
219
|
+
allowLinkedSource: Type.Optional(Type.Boolean()),
|
|
220
|
+
}),
|
|
221
|
+
async execute(_toolCallId, args, _signal, _onUpdate, ctx) {
|
|
222
|
+
const result = await executeMemoryOperation(args, {
|
|
223
|
+
directory: ctx.cwd,
|
|
224
|
+
host: "pi",
|
|
225
|
+
hostSessionId: ctx.sessionManager.getSessionId(),
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
229
|
+
details: {},
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
registerPiHistoryImportCommand(pi, () => latestCtx);
|
|
234
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ImportPathMap } from "../../importer/importer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pi command surface for the historical-session importer:
|
|
5
|
+
*
|
|
6
|
+
* /memory-import-pi-history --dry-run
|
|
7
|
+
* /memory-import-pi-history
|
|
8
|
+
*
|
|
9
|
+
* Runs over the shared importer service. Extraction inherits the active Pi
|
|
10
|
+
* model (or the piProvider/piModel override). Source session files are only
|
|
11
|
+
* ever read.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PI_IMPORT_COMMAND = "memory-import-pi-history";
|
|
14
|
+
export declare const PI_IMPORT_USAGE = "Usage: /memory-import-pi-history [options]\n\nOptions:\n --dry-run Discover, map, and report; write nothing\n --force Reprocess units with terminal ledger states\n --scope=current-project Import only sessions from this project (default)\n --scope=all-projects Import every discovered session\n --session=<id-or-file> Import one exact session\n --since=<date> Only work units at/after this time\n --until=<date> Only work units at/before this time\n --max-sessions=<n> Limit discovery to the oldest n sessions\n --map=<oldPath>=<newPath> Remap a recorded cwd that no longer exists\n --root=<dir> Session root (default ~/.pi/agent/sessions)\n\nDates accept ISO 8601 (2026-01-01, 2026-01-01T10:00:00Z) or epoch\nmilliseconds. --since/--until filter on each work unit's user-entry\ntimestamp, inclusive. Unresolvable recorded cwds are skipped and listed\nin the report; use --map to import them into an existing directory.";
|
|
15
|
+
export interface ParsedImportArgs {
|
|
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;
|
|
29
|
+
export declare function registerPiHistoryImportCommand(pi: ExtensionAPI, getExtensionContext: () => ExtensionContext | null): void;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { initConfigWithLegacyMigration, isConfigured } from "../../config.js";
|
|
2
|
+
import { log } from "../../services/logger.js";
|
|
3
|
+
import { memoryClient } from "../../services/client.js";
|
|
4
|
+
import { importPiHistory, } from "../../importer/importer.js";
|
|
5
|
+
import { loadPiSessionForImport } from "../../importer/session-loader.js";
|
|
6
|
+
import { createPiCaptureProvider, resolveModelFromContext } from "./provider.js";
|
|
7
|
+
/**
|
|
8
|
+
* Pi command surface for the historical-session importer:
|
|
9
|
+
*
|
|
10
|
+
* /memory-import-pi-history --dry-run
|
|
11
|
+
* /memory-import-pi-history
|
|
12
|
+
*
|
|
13
|
+
* Runs over the shared importer service. Extraction inherits the active Pi
|
|
14
|
+
* model (or the piProvider/piModel override). Source session files are only
|
|
15
|
+
* ever read.
|
|
16
|
+
*/
|
|
17
|
+
export const PI_IMPORT_COMMAND = "memory-import-pi-history";
|
|
18
|
+
export const PI_IMPORT_USAGE = `Usage: /${PI_IMPORT_COMMAND} [options]
|
|
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
|
+
}
|
|
44
|
+
export function parseImportArgs(raw) {
|
|
45
|
+
const result = {
|
|
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");
|
|
147
|
+
}
|
|
148
|
+
let importCommandRunning = false;
|
|
149
|
+
export function registerPiHistoryImportCommand(pi, getExtensionContext) {
|
|
150
|
+
pi.registerCommand(PI_IMPORT_COMMAND, {
|
|
151
|
+
description: "Import historical Pi sessions into the shared memory store (try --dry-run first)",
|
|
152
|
+
handler: async (args, commandCtx) => {
|
|
153
|
+
const notify = (message) => {
|
|
154
|
+
if (getExtensionContext()?.hasUI) {
|
|
155
|
+
getExtensionContext()?.ui.notify(message, "info");
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const parsed = parseImportArgs(args ?? "");
|
|
159
|
+
if (parsed.help) {
|
|
160
|
+
notify(PI_IMPORT_USAGE);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (parsed.errors.length > 0) {
|
|
164
|
+
notify(`memory-import-pi-history: ${parsed.errors.join("; ")}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const ctx = getExtensionContext();
|
|
168
|
+
if (!ctx) {
|
|
169
|
+
notify("memory-import-pi-history: no active Pi session context; open a session first");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!isConfigured()) {
|
|
173
|
+
notify("memory-import-pi-history: memory system not configured");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (importCommandRunning) {
|
|
177
|
+
notify("memory-import-pi-history: an import is already running");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Import against the current session's project and configuration.
|
|
181
|
+
initConfigWithLegacyMigration(ctx.cwd);
|
|
182
|
+
if (typeof commandCtx.waitForIdle === "function") {
|
|
183
|
+
await commandCtx.waitForIdle();
|
|
184
|
+
}
|
|
185
|
+
importCommandRunning = true;
|
|
186
|
+
try {
|
|
187
|
+
// Best-effort warmup so failures surface as per-unit errors instead of
|
|
188
|
+
// every unit failing on embedding initialisation.
|
|
189
|
+
try {
|
|
190
|
+
await memoryClient.warmup();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
log("Pi import: memory warmup failed", { error: String(error) });
|
|
194
|
+
}
|
|
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
|
+
let lastProgressNotify = 0;
|
|
209
|
+
const report = await importPiHistory({
|
|
210
|
+
loadSession: loadPiSessionForImport,
|
|
211
|
+
provider,
|
|
212
|
+
onProgress: (processed, total, promptPreview) => {
|
|
213
|
+
if (processed - lastProgressNotify >= 25 || processed === total) {
|
|
214
|
+
lastProgressNotify = processed;
|
|
215
|
+
notify(`Pi import: ${processed}/${total} units (${promptPreview})`);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
}, filters);
|
|
219
|
+
log("Pi history import report", { report });
|
|
220
|
+
notify(formatReport(report));
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
224
|
+
log("Pi history import failed", { error: message });
|
|
225
|
+
notify(`memory-import-pi-history failed: ${message}`);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
importCommandRunning = false;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { UserProfileData } from "../../services/user-profile/types.js";
|
|
2
|
+
import type { PiModelHandle } from "./provider.js";
|
|
3
|
+
export interface ExistingProfileInput {
|
|
4
|
+
id: string;
|
|
5
|
+
profileData: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProfileAnalysisResult {
|
|
8
|
+
raw: UserProfileData;
|
|
9
|
+
merged: UserProfileData | null;
|
|
10
|
+
}
|
|
11
|
+
export interface PiProfileAnalyzer {
|
|
12
|
+
analyzeProfile(context: string, existingProfile: ExistingProfileInput | null): Promise<ProfileAnalysisResult | null>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bridge the shared user-profile analysis onto Pi's model runtime. Uses the
|
|
16
|
+
* same schema and system prompt as the OpenCode structured-output path, so
|
|
17
|
+
* analysis results are validated with the shared schema before they reach the
|
|
18
|
+
* profile storage layer. Model failures throw (mirroring provider semantics);
|
|
19
|
+
* a missing model returns null so callers can skip without disabling anything.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createPiProfileAnalyzer(resolveModel: () => PiModelHandle | null, timeoutMs?: number): PiProfileAnalyzer;
|
|
22
|
+
export interface PiProfileLearningInput {
|
|
23
|
+
directory: string;
|
|
24
|
+
prompts: string[];
|
|
25
|
+
resolveModel: () => PiModelHandle | null;
|
|
26
|
+
notify?: (notification: {
|
|
27
|
+
title: string;
|
|
28
|
+
message: string;
|
|
29
|
+
variant: "success" | "warning" | "error" | "info";
|
|
30
|
+
}) => Promise<void> | void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Pi-side profile learning lifecycle: analyse a batch of settled user prompts
|
|
34
|
+
* through the model bridge and persist via the shared profile manager. Failures
|
|
35
|
+
* log (and optionally toast) without ever disabling memory operations. The
|
|
36
|
+
* decay/validation-task machinery of the OpenCode idle path is not ported;
|
|
37
|
+
* this applies the analysed batch directly (documented limitation).
|
|
38
|
+
*/
|
|
39
|
+
export declare function performPiProfileLearning(input: PiProfileLearningInput): Promise<void>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CONFIG } from "../../config.js";
|
|
3
|
+
import { createUserProfileAnalysisSchema, extractJsonObject } from "../../core/extraction.js";
|
|
4
|
+
import { log } from "../../services/logger.js";
|
|
5
|
+
import { getTags } from "../../services/tags.js";
|
|
6
|
+
/**
|
|
7
|
+
* System prompt mirrored from the OpenCode profile-learning path
|
|
8
|
+
* (src/services/user-memory-learning.ts, analyzeUserProfile). Kept local so
|
|
9
|
+
* the Pi adapter does not import an OpenCode-coupled module; update both when
|
|
10
|
+
* the analysis contract changes.
|
|
11
|
+
*/
|
|
12
|
+
function buildProfileAnalysisSystemPrompt(existingProfile) {
|
|
13
|
+
return `You are a user behavior analyst for a coding assistant.
|
|
14
|
+
|
|
15
|
+
Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
|
|
16
|
+
|
|
17
|
+
CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
|
|
18
|
+
|
|
19
|
+
CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
|
|
20
|
+
|
|
21
|
+
Respond with a single JSON object matching the update_user_profile contract.`;
|
|
22
|
+
}
|
|
23
|
+
function replyText(reply) {
|
|
24
|
+
return reply.content
|
|
25
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
26
|
+
.map((block) => block.text)
|
|
27
|
+
.join("\n")
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Bridge the shared user-profile analysis onto Pi's model runtime. Uses the
|
|
32
|
+
* same schema and system prompt as the OpenCode structured-output path, so
|
|
33
|
+
* analysis results are validated with the shared schema before they reach the
|
|
34
|
+
* profile storage layer. Model failures throw (mirroring provider semantics);
|
|
35
|
+
* a missing model returns null so callers can skip without disabling anything.
|
|
36
|
+
*/
|
|
37
|
+
export function createPiProfileAnalyzer(resolveModel, timeoutMs = 120000) {
|
|
38
|
+
return {
|
|
39
|
+
async analyzeProfile(context, existingProfile) {
|
|
40
|
+
const model = resolveModel();
|
|
41
|
+
if (!model)
|
|
42
|
+
return null;
|
|
43
|
+
// Unref'd and cleared so the losing timeout timer never keeps the host
|
|
44
|
+
// process alive after the race settles.
|
|
45
|
+
let timeoutId;
|
|
46
|
+
try {
|
|
47
|
+
const reply = await Promise.race([
|
|
48
|
+
model.complete({
|
|
49
|
+
systemPrompt: buildProfileAnalysisSystemPrompt(Boolean(existingProfile)),
|
|
50
|
+
messages: [{ role: "user", content: context, timestamp: Date.now() }],
|
|
51
|
+
}),
|
|
52
|
+
new Promise((_, reject) => {
|
|
53
|
+
timeoutId = setTimeout(() => reject(new Error("pi profile analysis: timeout")), timeoutMs);
|
|
54
|
+
timeoutId?.unref?.();
|
|
55
|
+
}),
|
|
56
|
+
]);
|
|
57
|
+
if (reply.stopReason === "error") {
|
|
58
|
+
throw new Error(`pi profile analysis: model call failed: ${reply.errorMessage || "unknown error"}`);
|
|
59
|
+
}
|
|
60
|
+
const schema = createUserProfileAnalysisSchema(z);
|
|
61
|
+
const parsed = schema.safeParse(extractJsonObject(replyText(reply)));
|
|
62
|
+
if (!parsed.success) {
|
|
63
|
+
log("pi profile analysis: model reply failed shared schema validation", {
|
|
64
|
+
provider: model.provider,
|
|
65
|
+
modelId: model.modelId,
|
|
66
|
+
});
|
|
67
|
+
throw new Error("pi profile analysis: invalid profile payload");
|
|
68
|
+
}
|
|
69
|
+
const rawData = parsed.data;
|
|
70
|
+
if (existingProfile) {
|
|
71
|
+
const { userProfileManager } = await import("../../services/user-profile/user-profile-manager.js");
|
|
72
|
+
const existingData = JSON.parse(existingProfile.profileData);
|
|
73
|
+
const merged = await userProfileManager.mergeProfileData(existingData, rawData, undefined, existingProfile.id);
|
|
74
|
+
return { raw: rawData, merged };
|
|
75
|
+
}
|
|
76
|
+
return { raw: rawData, merged: null };
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (timeoutId)
|
|
80
|
+
clearTimeout(timeoutId);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Pi-side profile learning lifecycle: analyse a batch of settled user prompts
|
|
87
|
+
* through the model bridge and persist via the shared profile manager. Failures
|
|
88
|
+
* log (and optionally toast) without ever disabling memory operations. The
|
|
89
|
+
* decay/validation-task machinery of the OpenCode idle path is not ported;
|
|
90
|
+
* this applies the analysed batch directly (documented limitation).
|
|
91
|
+
*/
|
|
92
|
+
export async function performPiProfileLearning(input) {
|
|
93
|
+
if (input.prompts.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
try {
|
|
96
|
+
const tags = getTags(input.directory);
|
|
97
|
+
if (!tags.user.userEmail) {
|
|
98
|
+
log("pi profile learning: skipped (no user email resolved)", {
|
|
99
|
+
directory: input.directory,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const userId = tags.user.userEmail;
|
|
104
|
+
const { userProfileManager } = await import("../../services/user-profile/user-profile-manager.js");
|
|
105
|
+
const existing = await userProfileManager.getActiveProfile(userId);
|
|
106
|
+
const analyzer = createPiProfileAnalyzer(input.resolveModel);
|
|
107
|
+
const context = input.prompts.map((prompt, i) => `${i + 1}. ${prompt}`).join("\n");
|
|
108
|
+
const analysis = await analyzer.analyzeProfile(context, existing ? { id: existing.id, profileData: existing.profileData } : null);
|
|
109
|
+
if (!analysis) {
|
|
110
|
+
log("pi profile learning: skipped (no model available)", { userId });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (existing && analysis.merged) {
|
|
114
|
+
const updated = await userProfileManager.updateProfile(existing.id, analysis.merged, input.prompts.length, `Pi analysis of ${input.prompts.length} prompts`);
|
|
115
|
+
if (!updated) {
|
|
116
|
+
log("pi profile learning: update conflict", { profileId: existing.id });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await userProfileManager.createProfile(userId, tags.user.displayName || userId, tags.user.userName || userId, tags.user.userEmail || userId, analysis.raw, input.prompts.length);
|
|
122
|
+
}
|
|
123
|
+
log("pi profile learning: profile updated", {
|
|
124
|
+
userId,
|
|
125
|
+
prompts: input.prompts.length,
|
|
126
|
+
hadExisting: Boolean(existing),
|
|
127
|
+
});
|
|
128
|
+
if (CONFIG.showUserProfileToasts && input.notify) {
|
|
129
|
+
await Promise.resolve(input.notify({
|
|
130
|
+
title: "User Profile Updated",
|
|
131
|
+
message: `Analyzed ${input.prompts.length} prompts and updated your profile`,
|
|
132
|
+
variant: "success",
|
|
133
|
+
})).catch(() => { });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
+
log(`pi profile learning: aborted (${message})`);
|
|
139
|
+
if (CONFIG.showErrorToasts && input.notify) {
|
|
140
|
+
const shortReason = message.length > 100 ? message.substring(0, 100) + "..." : message;
|
|
141
|
+
await Promise.resolve(input.notify({
|
|
142
|
+
title: "Profile Learning Failed",
|
|
143
|
+
message: shortReason,
|
|
144
|
+
variant: "error",
|
|
145
|
+
})).catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|