om-memory-system 3.2.0-next.9 → 3.3.0-next.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +3 -5
  2. package/dist/adapters/opencode/auto-capture-summary.js +50 -8
  3. package/dist/adapters/opencode/backfill-models.d.ts +25 -0
  4. package/dist/adapters/opencode/backfill-models.js +39 -0
  5. package/dist/adapters/opencode/backfill-startup.d.ts +7 -0
  6. package/dist/adapters/opencode/backfill-startup.js +12 -0
  7. package/dist/adapters/opencode/import-command.d.ts +3 -7
  8. package/dist/adapters/opencode/import-command.js +6 -54
  9. package/dist/adapters/pi/backfill-models.d.ts +9 -0
  10. package/dist/adapters/pi/backfill-models.js +30 -0
  11. package/dist/adapters/pi/extension.js +53 -0
  12. package/dist/adapters/pi/import-command.js +3 -0
  13. package/dist/adapters/pi/profile.js +2 -1
  14. package/dist/adapters/pi/provider.js +41 -14
  15. package/dist/cli/index.js +13 -1
  16. package/dist/cli/web-command.d.ts +3 -0
  17. package/dist/cli/web-command.js +72 -0
  18. package/dist/config.d.ts +30 -1
  19. package/dist/config.js +131 -9
  20. package/dist/core/capture-context.js +1 -1
  21. package/dist/core/capture.d.ts +4 -0
  22. package/dist/core/capture.js +30 -0
  23. package/dist/core/extraction.d.ts +23 -1
  24. package/dist/core/extraction.js +41 -0
  25. package/dist/core/host.d.ts +23 -0
  26. package/dist/core/host.js +9 -1
  27. package/dist/core/profile-analysis.js +9 -1
  28. package/dist/importer/auto-backfill.d.ts +20 -0
  29. package/dist/importer/auto-backfill.js +165 -0
  30. package/dist/importer/backfill-lock.d.ts +3 -0
  31. package/dist/importer/backfill-lock.js +44 -0
  32. package/dist/importer/backfill-model.d.ts +10 -0
  33. package/dist/importer/backfill-model.js +13 -0
  34. package/dist/importer/discovery.d.ts +32 -5
  35. package/dist/importer/discovery.js +88 -36
  36. package/dist/importer/import-args.d.ts +23 -1
  37. package/dist/importer/import-args.js +29 -1
  38. package/dist/importer/import-project.d.ts +16 -0
  39. package/dist/importer/import-project.js +35 -0
  40. package/dist/importer/import-readiness.d.ts +47 -0
  41. package/dist/importer/import-readiness.js +54 -0
  42. package/dist/importer/import-sessions.d.ts +98 -0
  43. package/dist/importer/import-sessions.js +222 -0
  44. package/dist/importer/import-sources.d.ts +47 -0
  45. package/dist/importer/import-sources.js +147 -0
  46. package/dist/importer/importer.d.ts +31 -1
  47. package/dist/importer/importer.js +96 -24
  48. package/dist/importer/manual-import-guard.d.ts +4 -0
  49. package/dist/importer/manual-import-guard.js +9 -0
  50. package/dist/importer/opencode-import.d.ts +11 -1
  51. package/dist/importer/opencode-import.js +133 -69
  52. package/dist/importer/opencode-project.d.ts +1 -4
  53. package/dist/importer/opencode-project.js +3 -36
  54. package/dist/importer/opencode-reader.d.ts +25 -5
  55. package/dist/importer/opencode-reader.js +92 -106
  56. package/dist/importer/opencode-snapshot.d.ts +82 -0
  57. package/dist/importer/opencode-snapshot.js +276 -0
  58. package/dist/importer/profile-import.d.ts +1 -0
  59. package/dist/importer/profile-import.js +7 -1
  60. package/dist/importer/run-import.d.ts +10 -0
  61. package/dist/importer/run-import.js +21 -0
  62. package/dist/importer/settings-health.d.ts +24 -0
  63. package/dist/importer/settings-health.js +127 -0
  64. package/dist/importer/web-import-api.d.ts +8 -0
  65. package/dist/importer/web-import-api.js +8 -0
  66. package/dist/importer/web-import-jobs.d.ts +53 -0
  67. package/dist/importer/web-import-jobs.js +227 -0
  68. package/dist/index.js +34 -0
  69. package/dist/services/ai/live-model-choice.js +6 -3
  70. package/dist/services/ai/opencode-import-models.d.ts +11 -0
  71. package/dist/services/ai/opencode-import-models.js +55 -0
  72. package/dist/services/ai/opencode-provider.d.ts +9 -0
  73. package/dist/services/ai/opencode-provider.js +31 -4
  74. package/dist/services/ai/providers/anthropic-messages.js +14 -6
  75. package/dist/services/ai/providers/base-provider.d.ts +8 -0
  76. package/dist/services/ai/providers/base-provider.js +12 -0
  77. package/dist/services/ai/providers/google-gemini.js +21 -7
  78. package/dist/services/ai/providers/openai-chat-completion.js +20 -7
  79. package/dist/services/ai/providers/openai-responses.js +11 -0
  80. package/dist/services/backfill-state.d.ts +25 -0
  81. package/dist/services/backfill-state.js +68 -0
  82. package/dist/services/capture-attempt-store.d.ts +21 -0
  83. package/dist/services/capture-attempt-store.js +108 -0
  84. package/dist/services/capture-diagnostics.d.ts +66 -0
  85. package/dist/services/capture-diagnostics.js +175 -0
  86. package/dist/services/cleanup-service.js +9 -0
  87. package/dist/services/global-config-writer.d.ts +10 -0
  88. package/dist/services/global-config-writer.js +110 -0
  89. package/dist/services/live-captured-entries.d.ts +3 -0
  90. package/dist/services/live-captured-entries.js +34 -0
  91. package/dist/services/log-path.d.ts +2 -0
  92. package/dist/services/log-path.js +15 -0
  93. package/dist/services/logger.js +1 -12
  94. package/dist/services/safe-health-error.d.ts +2 -0
  95. package/dist/services/safe-health-error.js +11 -0
  96. package/dist/services/settings-log.d.ts +5 -0
  97. package/dist/services/settings-log.js +36 -0
  98. package/dist/services/settings-models.d.ts +35 -0
  99. package/dist/services/settings-models.js +39 -0
  100. package/dist/services/settings-snapshot.d.ts +24 -0
  101. package/dist/services/settings-snapshot.js +83 -0
  102. package/dist/services/settings-traces.d.ts +7 -0
  103. package/dist/services/settings-traces.js +39 -0
  104. package/dist/services/user-memory-learning.js +2 -1
  105. package/dist/services/web-autostart.d.ts +29 -0
  106. package/dist/services/web-autostart.js +208 -0
  107. package/dist/services/web-server.d.ts +5 -0
  108. package/dist/services/web-server.js +187 -3
  109. package/dist/v2/legacy-client.js +19 -2
  110. package/dist/web/assets/index-B-_gMSn9.js +124 -0
  111. package/dist/web/assets/index-D5ak5KsI.css +2 -0
  112. package/dist/web/index.html +2 -2
  113. package/package.json +2 -1
  114. package/dist/web/assets/index-DPjEpD1h.css +0 -2
  115. package/dist/web/assets/index-do26c4JJ.js +0 -122
package/README.md CHANGED
@@ -26,8 +26,7 @@ the other. Everything is stored locally on your machine.
26
26
  OpenCode v1.
27
27
  - **Learns your preferences.** A user profile of your habits builds up over
28
28
  time and follows you across projects.
29
- - **Imports your past sessions.** One command turns old OpenCode or Pi
30
- history into memories and a profile.
29
+ - **Imports your past sessions.** Pi and OpenCode import older history after startup. A command provides a preview and manual control.
31
30
  - **Lets you look and edit.** A local web page shows every memory and your
32
31
  profile.
33
32
  - **Keeps private things private.** Text inside `<private>` tags is never
@@ -98,13 +97,12 @@ See [Configuration](docs/configuration.md#choosing-the-model).
98
97
  ### 3. Check it works
99
98
 
100
99
  Work normally for a few turns, then open `http://127.0.0.1:4747` in your
101
- browser (OpenCode serves this page). New memories appear on the timeline.
100
+ browser. OpenCode or the login web app serves this page. New memories appear on the timeline.
102
101
  You can also ask the agent: "search memory for what we changed today".
103
102
 
104
103
  ## Import your past history
105
104
 
106
- To turn earlier sessions into memories, preview first and then run it inside
107
- the agent:
105
+ Older sessions import automatically after a host starts. This makes model calls. To opt out, set `"autoBackfill": false` in `~/.config/omms/omms.jsonc` before starting. For a manual import or custom source, preview inside the agent:
108
106
 
109
107
  ```text
110
108
  /memory-import-opencode-history --dry-run
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { CONFIG } from "../../config.js";
3
3
  import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
4
4
  import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
5
- import { parseCaptureSummary } from "../../core/extraction.js";
5
+ import { classifyCaptureReply, normalizeStopReason, parseCaptureSummary, } from "../../core/extraction.js";
6
6
  import { log } from "../../services/logger.js";
7
7
  import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
8
8
  function buildSystemPrompt(languageName) {
@@ -26,8 +26,19 @@ FORMAT:
26
26
  SKIP if: greetings, casual chat, no code/decisions made
27
27
  CAPTURE if: code changed, bug fixed, feature added, decision made`;
28
28
  }
29
+ /** Reason code for an error thrown on the OpenCode host-model path. */
30
+ function hostModelFailureReason(error) {
31
+ if (error instanceof Error && error.name === "ZodError")
32
+ return "schema-mismatch";
33
+ if (String(error).includes("returned no structured output"))
34
+ return "empty-text";
35
+ return "call-error";
36
+ }
29
37
  export async function generateOpenCodeAutoCaptureSummary(request, notify) {
30
38
  let opencodeProviderError;
39
+ // Filled per path; when the host model falls back, the external API attempt
40
+ // overwrites it, so the record describes the call that decided the outcome.
41
+ const diagnostics = request.diagnostics ?? {};
31
42
  const hostModel = resolveOpencodeHostModel(CONFIG);
32
43
  if (hostModel) {
33
44
  try {
@@ -40,6 +51,9 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
40
51
  ...hostModel,
41
52
  prompt: request.prompt,
42
53
  });
54
+ diagnostics.path = "host-model";
55
+ diagnostics.provider = providerID;
56
+ diagnostics.model = modelID;
43
57
  if (!isProviderConnected(providerID)) {
44
58
  throw new Error(`opencode provider '${providerID}' is not connected. Check your opencode provider configuration.`);
45
59
  }
@@ -59,6 +73,8 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
59
73
  tags: z.array(z.string()),
60
74
  });
61
75
  const aiPrompt = buildBoundedSummaryPrompt(request.context, systemPrompt, z.toJSONSchema(schema));
76
+ diagnostics.systemPrompt = systemPrompt;
77
+ diagnostics.userPrompt = aiPrompt;
62
78
  const result = await generateStructuredOutput({
63
79
  client: v2Client,
64
80
  providerID,
@@ -66,6 +82,12 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
66
82
  systemPrompt,
67
83
  userPrompt: aiPrompt,
68
84
  schema,
85
+ onReply: (reply) => {
86
+ diagnostics.stopReason = normalizeStopReason(reply.finish);
87
+ diagnostics.blockTypes = reply.partTypes;
88
+ diagnostics.rawReply =
89
+ reply.structuredOutput === undefined ? "" : JSON.stringify(reply.structuredOutput);
90
+ },
69
91
  });
70
92
  return {
71
93
  summary: result.summary,
@@ -75,6 +97,7 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
75
97
  }
76
98
  catch (error) {
77
99
  opencodeProviderError = error;
100
+ diagnostics.failureReason = hostModelFailureReason(error);
78
101
  log("auto-capture: opencode provider failed, falling back to external API", {
79
102
  error: String(error),
80
103
  });
@@ -134,19 +157,38 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
134
157
  };
135
158
  const aiPrompt = buildBoundedSummaryPrompt(request.context, systemPrompt, toolSchema);
136
159
  const captureSessionID = `auto-capture-${request.prompt?.id ?? request.sessionId}-${randomUUID()}`;
137
- const result = await provider.executeToolCall(systemPrompt, aiPrompt, toolSchema, captureSessionID);
160
+ // Replace whatever the failed host-model attempt recorded.
161
+ Object.assign(diagnostics, {
162
+ path: "external-api",
163
+ provider: CONFIG.memoryProvider,
164
+ model: CONFIG.memoryModel,
165
+ systemPrompt,
166
+ userPrompt: aiPrompt,
167
+ stopReason: undefined,
168
+ blockTypes: undefined,
169
+ rawReply: undefined,
170
+ failureReason: undefined,
171
+ });
172
+ let result;
173
+ try {
174
+ result = await provider.executeToolCall(systemPrompt, aiPrompt, toolSchema, captureSessionID);
175
+ }
176
+ catch (error) {
177
+ diagnostics.failureReason = "call-error";
178
+ throw error;
179
+ }
180
+ diagnostics.stopReason = normalizeStopReason(result.stopReason);
138
181
  if (!result.success || !result.data) {
182
+ diagnostics.failureReason = diagnostics.stopReason === "length" ? "truncated" : "call-error";
139
183
  throw new Error(result.error || "Failed to generate summary");
140
184
  }
141
185
  const rawReply = JSON.stringify(result.data);
186
+ diagnostics.rawReply = rawReply;
142
187
  const summary = parseCaptureSummary(rawReply);
143
188
  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
- });
189
+ // The capture pipeline logs the attempt's metadata; never the reply.
190
+ diagnostics.failureReason =
191
+ classifyCaptureReply({ text: rawReply, stopReason: result.stopReason }) ?? "schema-mismatch";
150
192
  throw new Error("omms: OpenCode extraction returned an invalid summary payload");
151
193
  }
152
194
  return summary;
@@ -0,0 +1,25 @@
1
+ import { selectImportModel } from "../../importer/model-selection.js";
2
+ import type { HistoryImportModels } from "../../importer/run-import.js";
3
+ import { createOpencodeImportModels } from "../../services/ai/opencode-import-models.js";
4
+ interface BackfillConfig {
5
+ opencodeBackfillModel?: string;
6
+ opencodeProvider?: string;
7
+ opencodeModel?: string;
8
+ memoryProvider?: string;
9
+ memoryModel?: string;
10
+ memoryApiUrl?: string;
11
+ memoryApiKey?: string;
12
+ }
13
+ export interface OpenCodeBackfillContext {
14
+ connected: string[];
15
+ directory: string;
16
+ configModel: () => Promise<string | null>;
17
+ makeModels?: typeof createOpencodeImportModels;
18
+ externalModels?: typeof selectImportModel;
19
+ }
20
+ /** Apply the live-model rule when inherit is selected; named models need sign-in. */
21
+ export declare function resolveOpencodeBackfillModels(config: BackfillConfig | undefined, context: OpenCodeBackfillContext): Promise<{
22
+ model: string;
23
+ models: HistoryImportModels;
24
+ }>;
25
+ export {};
@@ -0,0 +1,39 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { parseBackfillModel } from "../../importer/backfill-model.js";
3
+ import { selectImportModel } from "../../importer/model-selection.js";
4
+ import { getAutoCaptureProviderStatus } from "../../services/ai/live-model-choice.js";
5
+ import { createOpencodeImportModels, } from "../../services/ai/opencode-import-models.js";
6
+ /** Apply the live-model rule when inherit is selected; named models need sign-in. */
7
+ export async function resolveOpencodeBackfillModels(config = CONFIG, context) {
8
+ const choice = parseBackfillModel(config, "opencode");
9
+ let ref;
10
+ if (choice === "inherit") {
11
+ const live = getAutoCaptureProviderStatus(config);
12
+ if (!live.ready)
13
+ throw new Error(`OpenCode backfill: ${live.issues.join("; ")}`);
14
+ if (live.mode === "manual") {
15
+ const external = (context.externalModels ?? selectImportModel)({});
16
+ return {
17
+ model: `${external.provider}/${external.modelId}`,
18
+ models: { capture: external.capture, profile: external.profile },
19
+ };
20
+ }
21
+ const selected = live.mode === "opencode"
22
+ ? `${config.opencodeProvider}/${config.opencodeModel}`
23
+ : await context.configModel();
24
+ if (!selected)
25
+ throw new Error("OpenCode backfill: no configured default model");
26
+ const parsed = parseBackfillModel({ opencodeBackfillModel: selected }, "opencode");
27
+ if (parsed === "inherit")
28
+ throw new Error("OpenCode backfill: no configured default model");
29
+ ref = { providerID: parsed.provider, modelID: parsed.model };
30
+ }
31
+ else {
32
+ ref = { providerID: choice.provider, modelID: choice.model };
33
+ }
34
+ if (!context.connected.includes(ref.providerID)) {
35
+ throw new Error(`OpenCode backfill provider ${ref.providerID} is not connected`);
36
+ }
37
+ const models = await (context.makeModels ?? createOpencodeImportModels)(ref, context.directory);
38
+ return { model: `${ref.providerID}/${ref.modelID}`, models };
39
+ }
@@ -0,0 +1,7 @@
1
+ /** Start after connected providers load, without delaying plugin initialisation. */
2
+ export declare function startOpencodeBackfill(input: {
3
+ directory: string;
4
+ connected: string[];
5
+ configModel: () => Promise<string | null>;
6
+ notify: (message: string) => void;
7
+ }): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { scheduleAutoBackfill } from "../../importer/auto-backfill.js";
3
+ import { resolveOpencodeBackfillModels } from "./backfill-models.js";
4
+ /** Start after connected providers load, without delaying plugin initialisation. */
5
+ export function startOpencodeBackfill(input) {
6
+ return scheduleAutoBackfill({
7
+ host: "opencode",
8
+ cwd: input.directory,
9
+ resolveModels: () => resolveOpencodeBackfillModels(CONFIG, input),
10
+ notify: input.notify,
11
+ });
12
+ }
@@ -1,4 +1,6 @@
1
- import { type HistoryImportModels } from "../../importer/run-import.js";
1
+ import { type OpencodeModelRef } from "../../services/ai/opencode-import-models.js";
2
+ export { createOpencodeImportModels } from "../../services/ai/opencode-import-models.js";
3
+ export type { OpencodeModelRef } from "../../services/ai/opencode-import-models.js";
2
4
  /**
3
5
  * OpenCode command surface for the historical-session importer:
4
6
  *
@@ -12,10 +14,6 @@ import { type HistoryImportModels } from "../../importer/run-import.js";
12
14
  export declare const OPENCODE_IMPORT_COMMAND = "memory-import-opencode-history";
13
15
  export declare const OPENCODE_IMPORT_DESCRIPTION = "Import historical OpenCode sessions into the shared memory store (try --dry-run first)";
14
16
  export declare const OPENCODE_IMPORT_USAGE: string;
15
- export interface OpencodeModelRef {
16
- providerID: string;
17
- modelID: string;
18
- }
19
17
  export interface OpencodeImportCommandInput {
20
18
  argsText: string;
21
19
  /** Session directory; relative paths and the default project resolve here. */
@@ -24,8 +22,6 @@ export interface OpencodeImportCommandInput {
24
22
  sessionModel: () => Promise<OpencodeModelRef | null>;
25
23
  notify?: (message: string) => void;
26
24
  }
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
25
  /** Parse, run, and describe one import; always resolves to text for the session. */
30
26
  export declare function runOpencodeImportCommand(input: OpencodeImportCommandInput): Promise<string>;
31
27
  /** The model of the session's latest user message, from the V1 message list. */
@@ -1,8 +1,10 @@
1
- import { CONFIG, isConfigured } from "../../config.js";
2
- import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
1
+ import { isConfigured } from "../../config.js";
3
2
  import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, tokenizeImportArgs, } from "../../importer/import-args.js";
4
3
  import { formatHistoryImportReport, summarizeHistoryImportReport, runHistoryImport, } from "../../importer/run-import.js";
4
+ import { createOpencodeImportModels, } from "../../services/ai/opencode-import-models.js";
5
+ export { createOpencodeImportModels } from "../../services/ai/opencode-import-models.js";
5
6
  import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
7
+ import { beginManualImport } from "../../importer/manual-import-guard.js";
6
8
  import { memoryClient } from "../../services/client.js";
7
9
  import { log } from "../../services/logger.js";
8
10
  /**
@@ -23,58 +25,6 @@ function splitModel(value) {
23
25
  const separator = value.indexOf("/");
24
26
  return { providerID: value.slice(0, separator), modelID: value.slice(separator + 1) };
25
27
  }
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
28
  /** Parse, run, and describe one import; always resolves to text for the session. */
79
29
  export async function runOpencodeImportCommand(input) {
80
30
  const name = OPENCODE_IMPORT_COMMAND;
@@ -93,6 +43,7 @@ export async function runOpencodeImportCommand(input) {
93
43
  if (importCommandRunning)
94
44
  return `${name}: an import is already running`;
95
45
  importCommandRunning = true;
46
+ const endManualImport = beginManualImport("opencode");
96
47
  try {
97
48
  let ref = null;
98
49
  if (parsed.model || importNeedsModel(parsed)) {
@@ -133,6 +84,7 @@ export async function runOpencodeImportCommand(input) {
133
84
  return `${name} failed: ${message}`;
134
85
  }
135
86
  finally {
87
+ endManualImport();
136
88
  importCommandRunning = false;
137
89
  }
138
90
  }
@@ -0,0 +1,9 @@
1
+ import type { HistoryImportModels } from "../../importer/run-import.js";
2
+ import { type PiModelContext } from "./provider.js";
3
+ /** Resolve the Pi backfill independently of the manual import's session-model rule. */
4
+ export declare function resolvePiBackfillModels(ctx: PiModelContext, config?: {
5
+ piBackfillModel?: string;
6
+ }): Promise<{
7
+ model: string;
8
+ models: HistoryImportModels;
9
+ }>;
@@ -0,0 +1,30 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { parseBackfillModel } from "../../importer/backfill-model.js";
3
+ import { createPiLiveModels } from "./live-model.js";
4
+ import { adaptPiProfileModel } from "./profile.js";
5
+ import { createPiCaptureProvider, resolveImportModel } from "./provider.js";
6
+ /** Resolve the Pi backfill independently of the manual import's session-model rule. */
7
+ export async function resolvePiBackfillModels(ctx, config = CONFIG) {
8
+ const choice = parseBackfillModel(config, "pi");
9
+ if (choice === "inherit") {
10
+ const live = createPiLiveModels(ctx);
11
+ const profile = live.profile();
12
+ if (!profile)
13
+ throw new Error("Pi backfill: no live-capture model is available");
14
+ const model = profile.modelId || CONFIG.memoryModel;
15
+ return {
16
+ model: `${profile.provider}/${model ?? "unknown"}`,
17
+ models: { capture: live.capture, profile },
18
+ };
19
+ }
20
+ const selected = resolveImportModel(ctx, `${choice.provider}/${choice.model}`);
21
+ if (!selected)
22
+ throw new Error(`Pi backfill model not found: ${choice.provider}/${choice.model}`);
23
+ return {
24
+ model: `${selected.provider}/${selected.modelId}`,
25
+ models: {
26
+ capture: createPiCaptureProvider(() => selected),
27
+ profile: adaptPiProfileModel(selected),
28
+ },
29
+ };
30
+ }
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { CONFIG, initConfigWithLegacyMigration, isConfigured } from "../../config.js";
3
+ import { pruneTraces } from "../../services/capture-diagnostics.js";
3
4
  import { executeMemoryOperation } from "../../core/memory-operations.js";
4
5
  import { getLanguageName } from "../../services/language-detector.js";
5
6
  import { log } from "../../services/logger.js";
@@ -10,6 +11,7 @@ import { registerPiHistoryImportCommand } from "./import-command.js";
10
11
  import { performPiProfileLearning } from "./profile.js";
11
12
  import { buildRetrievalSection, wrapRetrievalSection } from "../../core/retrieval.js";
12
13
  const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("omms.plugin.warmedup");
14
+ const GLOBAL_PI_BACKFILL_KEY = Symbol.for("omms.pi.backfill.scheduled");
13
15
  const OMMS_STATUS_KEY = "omms";
14
16
  const MEMORY_TOOL_MODES = [
15
17
  "add",
@@ -69,6 +71,7 @@ export default function ommsPiExtension(pi) {
69
71
  // Latest session context, refreshed on session_start and consumed by the
70
72
  // import command (command contexts do not expose the model registry).
71
73
  let latestCtx = null;
74
+ let backfillSessionId = null;
72
75
  let statusVersion = 0;
73
76
  const startStatus = (ctx, status) => {
74
77
  const version = ++statusVersion;
@@ -97,8 +100,53 @@ export default function ommsPiExtension(pi) {
97
100
  try {
98
101
  latestCtx = ctx;
99
102
  initConfigWithLegacyMigration(ctx.cwd);
103
+ // Runs even with tracing off, so turning it off does not leave old traces behind.
104
+ pruneTraces(CONFIG);
100
105
  captureState = createPiCaptureState();
106
+ if (CONFIG.webServerAutoStart !== undefined &&
107
+ process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
108
+ void import("../../services/web-autostart.js")
109
+ .then(({ reconcileWebAutostart }) => reconcileWebAutostart(CONFIG))
110
+ .catch((error) => log("Pi login item reconciliation failed", {
111
+ code: error instanceof Error ? error.name : "unknown",
112
+ }));
113
+ }
101
114
  const globalScope = globalThis;
115
+ const sessionId = ctx.sessionManager.getSessionId();
116
+ if (isConfigured() &&
117
+ CONFIG.autoBackfill &&
118
+ process.env.OMMS_DISABLE_AUTO_BACKFILL !== "1" &&
119
+ !globalScope[GLOBAL_PI_BACKFILL_KEY] &&
120
+ backfillSessionId !== sessionId) {
121
+ backfillSessionId = sessionId;
122
+ const controller = new AbortController();
123
+ const task = { controller, promise: Promise.resolve() };
124
+ globalScope[GLOBAL_PI_BACKFILL_KEY] = task;
125
+ task.promise = (async () => {
126
+ const { scheduleAutoBackfill } = await import("../../importer/auto-backfill.js");
127
+ await scheduleAutoBackfill({
128
+ host: "pi",
129
+ cwd: ctx.cwd,
130
+ signal: controller.signal,
131
+ resolveModels: async () => {
132
+ const { resolvePiBackfillModels } = await import("./backfill-models.js");
133
+ return resolvePiBackfillModels(ctx);
134
+ },
135
+ notify: (message) => {
136
+ if (!controller.signal.aborted && ctx.hasUI)
137
+ ctx.ui.notify(message, "info");
138
+ },
139
+ });
140
+ })()
141
+ .catch((error) => log("Pi backfill failed", {
142
+ error: error instanceof Error ? error.message : String(error),
143
+ }))
144
+ .finally(() => {
145
+ if (globalScope[GLOBAL_PI_BACKFILL_KEY] === task) {
146
+ delete globalScope[GLOBAL_PI_BACKFILL_KEY];
147
+ }
148
+ });
149
+ }
102
150
  if (!globalScope[GLOBAL_PLUGIN_WARMUP_KEY] && isConfigured()) {
103
151
  (async () => {
104
152
  try {
@@ -189,6 +237,11 @@ export default function ommsPiExtension(pi) {
189
237
  captureState = createPiCaptureState();
190
238
  promptsSinceProfileAnalysis = [];
191
239
  globalThis[GLOBAL_PLUGIN_WARMUP_KEY] = false;
240
+ const task = globalThis[GLOBAL_PI_BACKFILL_KEY];
241
+ task?.controller.abort();
242
+ if (task)
243
+ await task.promise;
244
+ backfillSessionId = null;
192
245
  try {
193
246
  await memoryClient.close();
194
247
  }
@@ -5,6 +5,7 @@ import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, tokenizeI
5
5
  import { formatHistoryImportReport, summarizeHistoryImportReport, runHistoryImport, } from "../../importer/run-import.js";
6
6
  import { createPiCaptureProvider, resolveImportModel } from "./provider.js";
7
7
  import { adaptPiProfileModel } from "./profile.js";
8
+ import { beginManualImport } from "../../importer/manual-import-guard.js";
8
9
  /**
9
10
  * Pi command surface for the historical-session importer:
10
11
  *
@@ -74,6 +75,7 @@ export function registerPiHistoryImportCommand(pi, getExtensionContext) {
74
75
  return;
75
76
  }
76
77
  importCommandRunning = true;
78
+ const endManualImport = beginManualImport("pi");
77
79
  try {
78
80
  // Best-effort warmup so failures surface as per-unit errors instead of
79
81
  // every unit failing on embedding initialisation.
@@ -110,6 +112,7 @@ export function registerPiHistoryImportCommand(pi, getExtensionContext) {
110
112
  notify(`memory-import-pi-history failed: ${message}`);
111
113
  }
112
114
  finally {
115
+ endManualImport();
113
116
  importCommandRunning = false;
114
117
  }
115
118
  },
@@ -1,4 +1,4 @@
1
- import { CONFIG } from "../../config.js";
1
+ import { CONFIG, refreshConfigIfChanged } from "../../config.js";
2
2
  import { analyzeProfile, } from "../../core/profile-analysis.js";
3
3
  import { log } from "../../services/logger.js";
4
4
  import { getTags } from "../../services/tags.js";
@@ -47,6 +47,7 @@ export async function performPiProfileLearning(input) {
47
47
  if (input.prompts.length === 0)
48
48
  return;
49
49
  try {
50
+ refreshConfigIfChanged(input.directory);
50
51
  const tags = getTags(input.directory);
51
52
  if (!tags.user.userEmail) {
52
53
  log("pi profile learning: skipped (no user email resolved)", {
@@ -1,8 +1,7 @@
1
1
  import { CONFIG } from "../../config.js";
2
2
  import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
3
- import { buildCaptureSystemPrompt, captureSummaryToolSchema, parseCaptureSummary, } from "../../core/extraction.js";
3
+ import { buildCaptureReplyInstruction, buildCaptureSystemPrompt, captureSummaryToolSchema, classifyCaptureReply, normalizeStopReason, parseCaptureSummary, } from "../../core/extraction.js";
4
4
  import { detectLanguage, getLanguageName } from "../../services/language-detector.js";
5
- import { log } from "../../services/logger.js";
6
5
  /**
7
6
  * Active-model-first resolution: the explicit `piProvider`/`piModel`
8
7
  * configuration when both are set, otherwise the active `ctx.model`.
@@ -80,23 +79,51 @@ export function createPiCaptureProvider(resolveModel) {
80
79
  ? detectLanguage(request.userPrompt)
81
80
  : CONFIG.autoCaptureLanguage;
82
81
  const systemPrompt = buildCaptureSystemPrompt(getLanguageName(targetLang));
83
- const userPrompt = buildBoundedSummaryPrompt(request.context, systemPrompt, captureSummaryToolSchema);
84
- const reply = await model.complete({
85
- systemPrompt,
86
- messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }],
87
- });
82
+ const replyInstruction = buildCaptureReplyInstruction();
83
+ // The instruction is counted as schema bytes so the context budget leaves room for it.
84
+ const userPrompt = `${buildBoundedSummaryPrompt(request.context, systemPrompt, {
85
+ schema: captureSummaryToolSchema,
86
+ replyInstruction,
87
+ })}\n\n${replyInstruction}`;
88
+ const diagnostics = request.diagnostics;
89
+ if (diagnostics) {
90
+ diagnostics.path = "host-model";
91
+ diagnostics.provider = model.provider;
92
+ diagnostics.model = model.modelId;
93
+ diagnostics.systemPrompt = systemPrompt;
94
+ diagnostics.userPrompt = userPrompt;
95
+ }
96
+ let reply;
97
+ try {
98
+ reply = await model.complete({
99
+ systemPrompt,
100
+ messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }],
101
+ });
102
+ }
103
+ catch (error) {
104
+ if (diagnostics)
105
+ diagnostics.failureReason = "call-error";
106
+ throw error;
107
+ }
108
+ const rawReply = replyText(reply);
109
+ if (diagnostics) {
110
+ diagnostics.stopReason = normalizeStopReason(reply.stopReason);
111
+ diagnostics.blockTypes = reply.content.map((block) => block.type);
112
+ diagnostics.rawReply = rawReply;
113
+ }
88
114
  if (reply.stopReason === "error") {
115
+ if (diagnostics)
116
+ diagnostics.failureReason = "call-error";
89
117
  throw new Error(`omms: Pi extraction call failed: ${reply.errorMessage || "unknown error"}`);
90
118
  }
91
- const rawReply = replyText(reply);
92
119
  const summary = parseCaptureSummary(rawReply);
93
120
  if (!summary) {
94
- log("Pi capture: model reply was not a valid capture summary", {
95
- provider: model.provider,
96
- modelId: model.modelId,
97
- // The reply can carry conversation content, so log only its size.
98
- replyLength: rawReply.length,
99
- });
121
+ // The capture pipeline logs the attempt's metadata; never the reply.
122
+ if (diagnostics) {
123
+ diagnostics.failureReason =
124
+ classifyCaptureReply({ text: rawReply, stopReason: reply.stopReason }) ??
125
+ "schema-mismatch";
126
+ }
100
127
  throw new Error("omms: Pi extraction returned an invalid summary payload");
101
128
  }
102
129
  return summary;
package/dist/cli/index.js CHANGED
@@ -11,8 +11,10 @@ const usage = `Usage: om-memory-system <command> [options]
11
11
  Commands:
12
12
  import-opencode-history Import OpenCode history into omms
13
13
  import-pi-history Import Pi history into omms
14
+ web Start the web app in the foreground
15
+ web install|uninstall|status Manage the web app login item
14
16
 
15
- Both commands take the same options; run one with --help to see them.
17
+ Import commands take the same options; run one with --help to see them.
16
18
  Inside a session, /memory-import-opencode-history and /memory-import-pi-history
17
19
  use that session's model instead of an API key.`;
18
20
  export function parseImportArgs(argv) {
@@ -30,6 +32,16 @@ export async function runCli(argv) {
30
32
  console.log(usage);
31
33
  return 0;
32
34
  }
35
+ if (argv[0] === "web") {
36
+ try {
37
+ const { runWebCommand } = await import("./web-command.js");
38
+ return await runWebCommand(argv.slice(1));
39
+ }
40
+ catch (error) {
41
+ console.error(`OMMS web command failed: ${error instanceof Error ? error.message : String(error)}`);
42
+ return 1;
43
+ }
44
+ }
33
45
  const apiKeyEnv = argv.find((_, index) => argv[index - 1] === "--api-key-env") ??
34
46
  argv.find((value) => value.startsWith("--api-key-env="))?.slice("--api-key-env=".length);
35
47
  try {
@@ -0,0 +1,3 @@
1
+ import type { WebAutostartOptions } from "../services/web-autostart.js";
2
+ /** Run the standalone web app or manage its per-user login item. */
3
+ export declare function runWebCommand(args: string[], options?: WebAutostartOptions, online?: () => Promise<boolean>): Promise<number>;