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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/adapters/opencode/auto-capture-summary.js +50 -8
  2. package/dist/adapters/opencode/import-command.d.ts +3 -7
  3. package/dist/adapters/opencode/import-command.js +3 -54
  4. package/dist/adapters/pi/extension.js +3 -0
  5. package/dist/adapters/pi/profile.js +2 -1
  6. package/dist/adapters/pi/provider.js +41 -14
  7. package/dist/config.d.ts +18 -1
  8. package/dist/config.js +103 -9
  9. package/dist/core/capture-context.js +1 -1
  10. package/dist/core/capture.d.ts +4 -0
  11. package/dist/core/capture.js +30 -0
  12. package/dist/core/extraction.d.ts +23 -1
  13. package/dist/core/extraction.js +41 -0
  14. package/dist/core/host.d.ts +23 -0
  15. package/dist/core/host.js +9 -1
  16. package/dist/core/profile-analysis.js +9 -1
  17. package/dist/importer/discovery.d.ts +32 -5
  18. package/dist/importer/discovery.js +88 -36
  19. package/dist/importer/import-args.d.ts +23 -1
  20. package/dist/importer/import-args.js +29 -1
  21. package/dist/importer/import-project.d.ts +16 -0
  22. package/dist/importer/import-project.js +35 -0
  23. package/dist/importer/import-readiness.d.ts +47 -0
  24. package/dist/importer/import-readiness.js +54 -0
  25. package/dist/importer/import-sessions.d.ts +98 -0
  26. package/dist/importer/import-sessions.js +222 -0
  27. package/dist/importer/import-sources.d.ts +47 -0
  28. package/dist/importer/import-sources.js +147 -0
  29. package/dist/importer/importer.d.ts +31 -1
  30. package/dist/importer/importer.js +82 -24
  31. package/dist/importer/opencode-import.d.ts +11 -1
  32. package/dist/importer/opencode-import.js +133 -69
  33. package/dist/importer/opencode-project.d.ts +1 -4
  34. package/dist/importer/opencode-project.js +3 -36
  35. package/dist/importer/opencode-reader.d.ts +25 -5
  36. package/dist/importer/opencode-reader.js +92 -106
  37. package/dist/importer/opencode-snapshot.d.ts +82 -0
  38. package/dist/importer/opencode-snapshot.js +276 -0
  39. package/dist/importer/profile-import.d.ts +1 -0
  40. package/dist/importer/profile-import.js +7 -1
  41. package/dist/importer/run-import.d.ts +10 -0
  42. package/dist/importer/run-import.js +21 -0
  43. package/dist/importer/settings-health.d.ts +25 -0
  44. package/dist/importer/settings-health.js +136 -0
  45. package/dist/importer/web-import-api.d.ts +8 -0
  46. package/dist/importer/web-import-api.js +8 -0
  47. package/dist/importer/web-import-jobs.d.ts +53 -0
  48. package/dist/importer/web-import-jobs.js +227 -0
  49. package/dist/index.js +4 -0
  50. package/dist/services/ai/live-model-choice.js +6 -3
  51. package/dist/services/ai/opencode-import-models.d.ts +11 -0
  52. package/dist/services/ai/opencode-import-models.js +55 -0
  53. package/dist/services/ai/opencode-provider.d.ts +9 -0
  54. package/dist/services/ai/opencode-provider.js +31 -4
  55. package/dist/services/ai/providers/anthropic-messages.js +14 -6
  56. package/dist/services/ai/providers/base-provider.d.ts +8 -0
  57. package/dist/services/ai/providers/base-provider.js +12 -0
  58. package/dist/services/ai/providers/google-gemini.js +21 -7
  59. package/dist/services/ai/providers/openai-chat-completion.js +20 -7
  60. package/dist/services/ai/providers/openai-responses.js +11 -0
  61. package/dist/services/capture-attempt-store.d.ts +21 -0
  62. package/dist/services/capture-attempt-store.js +108 -0
  63. package/dist/services/capture-diagnostics.d.ts +66 -0
  64. package/dist/services/capture-diagnostics.js +175 -0
  65. package/dist/services/cleanup-service.js +9 -0
  66. package/dist/services/global-config-writer.d.ts +10 -0
  67. package/dist/services/global-config-writer.js +106 -0
  68. package/dist/services/log-path.d.ts +2 -0
  69. package/dist/services/log-path.js +15 -0
  70. package/dist/services/logger.js +1 -12
  71. package/dist/services/settings-log.d.ts +5 -0
  72. package/dist/services/settings-log.js +36 -0
  73. package/dist/services/settings-models.d.ts +35 -0
  74. package/dist/services/settings-models.js +39 -0
  75. package/dist/services/settings-snapshot.d.ts +24 -0
  76. package/dist/services/settings-snapshot.js +71 -0
  77. package/dist/services/settings-traces.d.ts +7 -0
  78. package/dist/services/settings-traces.js +39 -0
  79. package/dist/services/user-memory-learning.js +2 -1
  80. package/dist/services/web-server.d.ts +5 -0
  81. package/dist/services/web-server.js +176 -3
  82. package/dist/v2/legacy-client.js +19 -2
  83. package/dist/web/assets/index-BMkPht7g.js +124 -0
  84. package/dist/web/assets/index-D5ak5KsI.css +2 -0
  85. package/dist/web/index.html +2 -2
  86. package/package.json +2 -1
  87. package/dist/web/assets/index-DPjEpD1h.css +0 -2
  88. package/dist/web/assets/index-do26c4JJ.js +0 -122
@@ -18,6 +18,15 @@ export async function runHistoryImport(host, args, run) {
18
18
  ...(args.since !== undefined ? { since: args.since } : {}),
19
19
  ...(args.until !== undefined ? { until: args.until } : {}),
20
20
  ...(args.session ? { session: args.session } : {}),
21
+ ...(run.selection
22
+ ? {
23
+ selectionKeys: run.selection.keys,
24
+ cutoff: run.selection.cutoff,
25
+ ...(run.selection.snapshotKey
26
+ ? { snapshot: { key: run.selection.snapshotKey, mode: "reuse" } }
27
+ : {}),
28
+ }
29
+ : {}),
21
30
  ...(project ? { project } : {}),
22
31
  ...(args.maxSessions ? { maxSessions: args.maxSessions } : {}),
23
32
  ...(args.profileBatch ? { profileBatch: args.profileBatch } : {}),
@@ -25,6 +34,8 @@ export async function runHistoryImport(host, args, run) {
25
34
  force: args.force,
26
35
  skipMemories: args.skipMemories,
27
36
  skipProfile: args.skipProfile,
37
+ ...(run.signal ? { signal: run.signal } : {}),
38
+ ...(run.onProgress ? { onProgress: run.onProgress } : {}),
28
39
  ...(run.models.capture ? { provider: capture } : {}),
29
40
  ...(run.models.profile ? { profileModel: run.models.profile } : {}),
30
41
  });
@@ -35,6 +46,7 @@ export async function runHistoryImport(host, args, run) {
35
46
  loadSession: loadPiSessionForImport,
36
47
  provider: capture,
37
48
  ...(run.onProgress ? { onProgress: run.onProgress } : {}),
49
+ ...(run.signal ? { signal: run.signal } : {}),
38
50
  ...(!args.skipProfile
39
51
  ? {
40
52
  profile: {
@@ -47,6 +59,7 @@ export async function runHistoryImport(host, args, run) {
47
59
  scope: args.scope,
48
60
  currentDirectory: project ?? run.cwd,
49
61
  ...(args.session ? { session: args.session } : {}),
62
+ ...(run.selection ? { selectionKeys: run.selection.keys, cutoff: run.selection.cutoff } : {}),
50
63
  ...(args.since !== undefined ? { since: args.since } : {}),
51
64
  ...(args.until !== undefined ? { until: args.until } : {}),
52
65
  ...(args.maxSessions ? { maxSessions: args.maxSessions } : {}),
@@ -79,6 +92,12 @@ export function formatHistoryImportReport(host, report, model) {
79
92
  for (const unresolvable of report.unresolvableSessions) {
80
93
  lines.push(` unresolved ${unresolvable.cwd ?? "(no cwd)"}: ${unresolvable.file} (use --map)`);
81
94
  }
95
+ if (report.unitsUntimed) {
96
+ lines.push(` turns without a timestamp: ${report.unitsUntimed} (date limits cannot exclude them)`);
97
+ }
98
+ if (report.unitsHeldBack) {
99
+ lines.push(` newer turns held back: ${report.unitsHeldBack}; list the sessions again and import to include them`);
100
+ }
82
101
  if (report.loadErrors.length > 0)
83
102
  lines.push(` load errors: ${report.loadErrors.length}`);
84
103
  if (report.profile) {
@@ -103,6 +122,8 @@ export function summarizeHistoryImportReport(report) {
103
122
  unitsSkipped: report.unitsSkipped,
104
123
  unitsFailed: report.unitsFailed,
105
124
  unitsAlreadyHandled: report.unitsAlreadyHandled,
125
+ unitsHeldBack: report.unitsHeldBack ?? 0,
126
+ unitsUntimed: report.unitsUntimed ?? 0,
106
127
  projects: report.projects.length,
107
128
  unresolved: (report.unresolvedProjects?.length ?? 0) + report.unresolvableSessions.length,
108
129
  loadErrors: report.loadErrors.length,
@@ -0,0 +1,25 @@
1
+ export type HealthRow = {
2
+ check: string;
3
+ status: "pass" | "warn" | "fail";
4
+ reason: string;
5
+ };
6
+ /** Remove configured credentials before returning a model error to the browser. */
7
+ export declare function safeHealthError(error: unknown, secrets: Array<string | undefined>): string;
8
+ export declare function captureFailureHealth(byModel: Array<{
9
+ total: number;
10
+ failed: number;
11
+ }>, byReason: Array<{
12
+ reason: string;
13
+ count: number;
14
+ }>): HealthRow;
15
+ export interface HealthInput {
16
+ directory: string;
17
+ host: string;
18
+ authEnabled: boolean;
19
+ apiTokenSet: boolean;
20
+ testModels: boolean;
21
+ }
22
+ /** Run checks independently so a failed dependency cannot hide the remaining checks. */
23
+ export declare function runSettingsHealth(input: HealthInput): Promise<{
24
+ checks: HealthRow[];
25
+ }>;
@@ -0,0 +1,136 @@
1
+ import { CONFIG } from "../config.js";
2
+ import { getAutoCaptureProviderStatus, resolvePiLiveModel, } from "../services/ai/live-model-choice.js";
3
+ import { isLoopbackHost } from "../services/web-api-auth.js";
4
+ const PROBE = "Reply with a short acknowledgement.";
5
+ /** Remove configured credentials before returning a model error to the browser. */
6
+ export function safeHealthError(error, secrets) {
7
+ let message = error instanceof Error ? error.message : String(error);
8
+ for (const secret of secrets) {
9
+ if (secret && secret.length > 2)
10
+ message = message.replaceAll(secret, "[redacted]");
11
+ }
12
+ return message
13
+ .replace(/(Bearer\s+)[^\s]+/gi, "$1[redacted]")
14
+ .replace(/(api[_-]?key[=:]\s*)[^\s&]+/gi, "$1[redacted]");
15
+ }
16
+ export function captureFailureHealth(byModel, byReason) {
17
+ const total = byModel.reduce((count, row) => count + row.total, 0);
18
+ const failed = byModel.reduce((count, row) => count + row.failed, 0);
19
+ const reason = [...byReason].sort((a, b) => b.count - a.count)[0]?.reason;
20
+ return {
21
+ check: "Capture failures (24 hours)",
22
+ status: total > 0 && failed / total > 0.2 ? "warn" : "pass",
23
+ reason: total
24
+ ? `${failed}/${total} failed${reason ? `; most common reason: ${reason}` : ""}`
25
+ : "No recent attempts",
26
+ };
27
+ }
28
+ /** Run checks independently so a failed dependency cannot hide the remaining checks. */
29
+ export async function runSettingsHealth(input) {
30
+ const checks = [];
31
+ async function check(name, action) {
32
+ try {
33
+ checks.push({ check: name, status: "pass", reason: await action() });
34
+ }
35
+ catch (error) {
36
+ checks.push({
37
+ check: name,
38
+ status: "fail",
39
+ reason: safeHealthError(error, [
40
+ CONFIG.memoryApiKey,
41
+ CONFIG.embeddingApiKey,
42
+ CONFIG.webServerApiToken,
43
+ ]),
44
+ });
45
+ }
46
+ }
47
+ await check("Config files", async () => {
48
+ const { getSettingsSnapshot } = await import("../services/settings-snapshot.js");
49
+ getSettingsSnapshot(input.directory);
50
+ return "Global and project config parsed";
51
+ });
52
+ await check("Memory store", async () => {
53
+ const { tursoConnectionManager } = await import("../services/turso/connection-manager.js");
54
+ const { join } = await import("node:path");
55
+ const db = await tursoConnectionManager.getConnection(join(CONFIG.storagePath, "user-prompts.db"));
56
+ await db.all("SELECT 1");
57
+ return "Store opened and queried";
58
+ });
59
+ await check("Embedding", async () => {
60
+ const { embeddingService } = await import("../services/embedding.js");
61
+ const result = await embeddingService.embedWithTimeout("OMMS health check", { task: "query" });
62
+ if (!result.length)
63
+ throw new Error("Embedding returned no values");
64
+ return "Test text embedded";
65
+ });
66
+ const secure = isLoopbackHost(input.host) || input.authEnabled || input.apiTokenSet;
67
+ checks.push({
68
+ check: "Web binding",
69
+ status: secure ? "pass" : "fail",
70
+ reason: secure
71
+ ? "Binding and access settings are safe"
72
+ : "Network binding requires an API token or Basic Auth",
73
+ });
74
+ const opencode = getAutoCaptureProviderStatus(CONFIG);
75
+ checks.push({
76
+ check: "OpenCode model",
77
+ status: opencode.ready ? "pass" : "fail",
78
+ reason: opencode.ready ? `Resolved: ${opencode.mode}` : opencode.issues.join("; "),
79
+ });
80
+ const pi = resolvePiLiveModel(CONFIG);
81
+ checks.push({
82
+ check: "Pi model",
83
+ status: pi.kind === "unready" ? "fail" : "pass",
84
+ reason: pi.kind === "unready" ? pi.issues.join("; ") : `Resolved: ${pi.kind}`,
85
+ });
86
+ try {
87
+ const { queryCaptureAttempts } = await import("../services/capture-attempt-store.js");
88
+ const { byModel, byReason } = await queryCaptureAttempts(Date.now() - 86400000, Date.now());
89
+ checks.push(captureFailureHealth(byModel, byReason.map((row) => ({ count: row.count, reason: String(row.reason) }))));
90
+ }
91
+ catch (error) {
92
+ checks.push({
93
+ check: "Capture failures (24 hours)",
94
+ status: "fail",
95
+ reason: safeHealthError(error, [CONFIG.memoryApiKey]),
96
+ });
97
+ }
98
+ if (input.testModels) {
99
+ await check("OpenCode model test", async () => {
100
+ const { resolveOpencodeHostModel } = await import("../services/ai/live-model-choice.js");
101
+ const ref = resolveOpencodeHostModel(CONFIG);
102
+ if (ref?.modelID === "inherit")
103
+ throw new Error("A session model needs an active session to test");
104
+ const capture = ref
105
+ ? (await (await import("../services/ai/opencode-import-models.js")).createOpencodeImportModels(ref, input.directory)).capture
106
+ : (await import("./model-selection.js")).selectImportModel({}).capture;
107
+ await capture.summarize({
108
+ userPrompt: PROBE,
109
+ context: PROBE,
110
+ sessionId: "health",
111
+ projectDirectory: input.directory,
112
+ });
113
+ return "Fixed prompt completed";
114
+ });
115
+ if (pi.kind === "manual") {
116
+ await check("Pi model test", async () => {
117
+ const { selectImportModel } = await import("./model-selection.js");
118
+ await selectImportModel({}).capture.summarize({
119
+ userPrompt: PROBE,
120
+ context: PROBE,
121
+ sessionId: "health",
122
+ projectDirectory: input.directory,
123
+ });
124
+ return "Fixed prompt completed";
125
+ });
126
+ }
127
+ else {
128
+ checks.push({
129
+ check: "Pi model test",
130
+ status: "warn",
131
+ reason: "Pi models need an active Pi session to test",
132
+ });
133
+ }
134
+ }
135
+ return { checks };
136
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The import pieces the web server loads for the Settings page, behind one
3
+ * dynamic import so the server's boundary with the importer stays small.
4
+ */
5
+ export { importReadiness } from "./import-readiness.js";
6
+ export { browseImportSources, validateImportSource } from "./import-sources.js";
7
+ export { listImportSessions, validateSessionListRequest } from "./import-sessions.js";
8
+ export { opencodeSnapshots, sweepOrphanSnapshots } from "./opencode-snapshot.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The import pieces the web server loads for the Settings page, behind one
3
+ * dynamic import so the server's boundary with the importer stays small.
4
+ */
5
+ export { importReadiness } from "./import-readiness.js";
6
+ export { browseImportSources, validateImportSource } from "./import-sources.js";
7
+ export { listImportSessions, validateSessionListRequest } from "./import-sessions.js";
8
+ export { opencodeSnapshots, sweepOrphanSnapshots } from "./opencode-snapshot.js";
@@ -0,0 +1,53 @@
1
+ import { type ImportHost, type WebImportOptions } from "./import-args.js";
2
+ import { type ImportReadiness } from "./import-readiness.js";
3
+ import { resolveImportSelection, type ImportSelection } from "./import-sessions.js";
4
+ import { runHistoryImport, summarizeHistoryImportReport } from "./run-import.js";
5
+ type JobState = "running" | "cancelling" | "cancelled" | "done" | "failed";
6
+ type Job = {
7
+ id: string;
8
+ host: ImportHost;
9
+ dryRun: boolean;
10
+ state: JobState;
11
+ /** Sessions in the resolved selection. */
12
+ sessions: number;
13
+ processed: number;
14
+ total: number;
15
+ report?: string;
16
+ summary?: ReturnType<typeof summarizeHistoryImportReport>;
17
+ error?: string;
18
+ };
19
+ type Request = {
20
+ host: ImportHost;
21
+ source: unknown;
22
+ selection: ImportSelection;
23
+ options: WebImportOptions;
24
+ modelChoice?: string;
25
+ };
26
+ type Runner = typeof runHistoryImport;
27
+ /** A refused job; `status` is the HTTP status the page receives. */
28
+ export declare class ImportJobError extends Error {
29
+ readonly status: number;
30
+ constructor(message: string, status?: number);
31
+ }
32
+ export declare function validateWebImportRequest(value: unknown): Request;
33
+ export interface ImportJobDeps {
34
+ runner?: Runner;
35
+ resolveSelection?: typeof resolveImportSelection;
36
+ readiness?: () => Promise<ImportReadiness>;
37
+ }
38
+ /** One in-memory slot per web server, shared by previews and imports. */
39
+ export declare class SettingsImportJobs {
40
+ private job?;
41
+ private controller?;
42
+ private starting;
43
+ private readonly runner;
44
+ private readonly resolveSelection;
45
+ private readonly readiness;
46
+ constructor(deps?: ImportJobDeps);
47
+ current(): Job | null;
48
+ private busy;
49
+ start(request: unknown, directory: string): Promise<Job>;
50
+ private begin;
51
+ cancel(): Job;
52
+ }
53
+ export {};
@@ -0,0 +1,227 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isAbsolute } from "node:path";
3
+ import { CONFIG } from "../config.js";
4
+ import { importNeedsModel, parseHistoryImportArgs, webImportTokens, } from "./import-args.js";
5
+ import { importBlockedReason, importReadiness } from "./import-readiness.js";
6
+ import { resolveImportSelection, validateImportSelection, } from "./import-sessions.js";
7
+ import { importSourceKey } from "./import-sources.js";
8
+ import { formatHistoryImportReport, runHistoryImport, summarizeHistoryImportReport, } from "./run-import.js";
9
+ import { safeHealthError } from "./settings-health.js";
10
+ /** A refused job; `status` is the HTTP status the page receives. */
11
+ export class ImportJobError extends Error {
12
+ status;
13
+ constructor(message, status = 400) {
14
+ super(message);
15
+ this.status = status;
16
+ this.name = "ImportJobError";
17
+ }
18
+ }
19
+ // Selection replaces --session and --max-sessions; the source comes as a token.
20
+ const fields = new Set([
21
+ "dryRun",
22
+ "force",
23
+ "skipMemories",
24
+ "skipProfile",
25
+ "scope",
26
+ "project",
27
+ "since",
28
+ "until",
29
+ "profileBatch",
30
+ "pathMaps",
31
+ ]);
32
+ export function validateWebImportRequest(value) {
33
+ if (!value || typeof value !== "object" || Array.isArray(value))
34
+ throw new Error("Invalid import request");
35
+ const input = value;
36
+ if (input.host !== "pi" && input.host !== "opencode")
37
+ throw new Error("Choose Pi or OpenCode");
38
+ if (!input.options || typeof input.options !== "object" || Array.isArray(input.options))
39
+ throw new Error("Invalid import options");
40
+ const options = input.options;
41
+ for (const [key, option] of Object.entries(options)) {
42
+ if (!fields.has(key))
43
+ throw new Error(`Unknown import option: ${key}`);
44
+ if (["dryRun", "force", "skipMemories", "skipProfile"].includes(key)) {
45
+ if (typeof option !== "boolean")
46
+ throw new Error(`Invalid ${key}`);
47
+ }
48
+ else if (key === "pathMaps") {
49
+ if (!Array.isArray(option) ||
50
+ !option.every((map) => map &&
51
+ typeof map === "object" &&
52
+ typeof map.from === "string" &&
53
+ typeof map.to === "string"))
54
+ throw new Error("Invalid directory maps");
55
+ }
56
+ else if (key === "profileBatch") {
57
+ if (!Number.isSafeInteger(option) || option < 1)
58
+ throw new Error(`Invalid ${key}`);
59
+ }
60
+ else if (key === "since" || key === "until") {
61
+ if (typeof option !== "string" && typeof option !== "number")
62
+ throw new Error(`Invalid ${key}`);
63
+ }
64
+ else if (typeof option !== "string") {
65
+ throw new Error(`Invalid ${key}`);
66
+ }
67
+ else if (key === "project" && !isAbsolute(option)) {
68
+ throw new Error("The project must be an absolute path");
69
+ }
70
+ }
71
+ if (input.modelChoice !== undefined &&
72
+ (typeof input.modelChoice !== "string" ||
73
+ (input.modelChoice !== "external" && !/^[^/]+\/.+$/.test(input.modelChoice))))
74
+ throw new Error("Choose a signed-in provider/model or the external API");
75
+ return {
76
+ host: input.host,
77
+ source: input.source,
78
+ selection: validateImportSelection(input.selection),
79
+ options: options,
80
+ modelChoice: input.modelChoice,
81
+ };
82
+ }
83
+ /** One in-memory slot per web server, shared by previews and imports. */
84
+ export class SettingsImportJobs {
85
+ job;
86
+ controller;
87
+ starting = false;
88
+ runner;
89
+ resolveSelection;
90
+ readiness;
91
+ constructor(deps = {}) {
92
+ this.runner = deps.runner ?? runHistoryImport;
93
+ this.resolveSelection = deps.resolveSelection ?? resolveImportSelection;
94
+ this.readiness = deps.readiness ?? (() => importReadiness());
95
+ }
96
+ current() {
97
+ return this.job ? { ...this.job } : null;
98
+ }
99
+ busy() {
100
+ return this.starting || this.job?.state === "running" || this.job?.state === "cancelling";
101
+ }
102
+ async start(request, directory) {
103
+ if (this.busy())
104
+ throw new ImportJobError("An import is already running", 409);
105
+ this.starting = true;
106
+ try {
107
+ return await this.begin(request, directory);
108
+ }
109
+ finally {
110
+ this.starting = false;
111
+ }
112
+ }
113
+ async begin(request, directory) {
114
+ let input;
115
+ try {
116
+ input = validateWebImportRequest(request);
117
+ }
118
+ catch (error) {
119
+ throw new ImportJobError(error.message);
120
+ }
121
+ const options = { ...input.options };
122
+ if (input.modelChoice && input.modelChoice !== "external")
123
+ options.model = input.modelChoice;
124
+ const args = parseHistoryImportArgs(webImportTokens(options, input.host), {
125
+ host: input.host,
126
+ surface: "web",
127
+ });
128
+ if (args.errors.length)
129
+ throw new ImportJobError(args.errors.join("; "));
130
+ const needsModel = importNeedsModel(args);
131
+ const blocked = importBlockedReason(await this.readiness(), {
132
+ host: input.host,
133
+ needsModel,
134
+ ...(input.modelChoice ? { modelChoice: input.modelChoice } : {}),
135
+ });
136
+ if (blocked)
137
+ throw new ImportJobError(blocked);
138
+ let selection;
139
+ try {
140
+ selection = await this.resolveSelection(input.source, input.selection, {
141
+ host: input.host,
142
+ scope: args.scope,
143
+ ...(args.project ? { project: args.project } : {}),
144
+ pathMaps: args.pathMaps,
145
+ cwd: directory,
146
+ });
147
+ }
148
+ catch (error) {
149
+ const status = error.status ?? 400;
150
+ throw new ImportJobError(error.message, status);
151
+ }
152
+ args.source = selection.identity.realPath;
153
+ const job = {
154
+ id: randomUUID(),
155
+ host: input.host,
156
+ dryRun: args.dryRun,
157
+ state: "running",
158
+ sessions: selection.keys.length,
159
+ processed: 0,
160
+ total: 0,
161
+ };
162
+ const controller = new AbortController();
163
+ this.job = job;
164
+ this.controller = controller;
165
+ void Promise.resolve().then(async () => {
166
+ try {
167
+ let models = {};
168
+ if (needsModel) {
169
+ if (input.modelChoice === "external") {
170
+ const { selectImportModel } = await import("./model-selection.js");
171
+ const selected = selectImportModel({});
172
+ models = { capture: selected.capture, profile: selected.profile };
173
+ }
174
+ else {
175
+ const [providerID = "", ...id] = input.modelChoice.split("/");
176
+ const { loadOpencodeProvider } = await import("../services/ai/opencode-provider-loader.js");
177
+ const { isProviderConnected } = await loadOpencodeProvider();
178
+ if (!isProviderConnected(providerID))
179
+ throw new Error("OpenCode provider is not connected");
180
+ const { createOpencodeImportModels } = await import("../services/ai/opencode-import-models.js");
181
+ models = await createOpencodeImportModels({ providerID, modelID: id.join("/") }, directory);
182
+ }
183
+ const { memoryClient } = await import("../services/client.js");
184
+ await memoryClient.warmup();
185
+ }
186
+ const report = await this.runner(input.host, args, {
187
+ cwd: directory,
188
+ models,
189
+ signal: controller.signal,
190
+ selection: {
191
+ keys: selection.keys,
192
+ cutoff: selection.cutoff,
193
+ ...(selection.identity.kind === "opencode-db"
194
+ ? { snapshotKey: importSourceKey(selection.identity) }
195
+ : {}),
196
+ },
197
+ onProgress: (processed, total) => {
198
+ job.processed = processed;
199
+ job.total = total;
200
+ },
201
+ });
202
+ job.summary = summarizeHistoryImportReport(report);
203
+ job.report = safeHealthError(formatHistoryImportReport(input.host, report, input.modelChoice), [CONFIG.memoryApiKey]);
204
+ job.state = controller.signal.aborted ? "cancelled" : "done";
205
+ }
206
+ catch (error) {
207
+ job.error = safeHealthError(error, [CONFIG.memoryApiKey, CONFIG.embeddingApiKey]);
208
+ job.state = controller.signal.aborted ? "cancelled" : "failed";
209
+ }
210
+ finally {
211
+ // A preview keeps the OpenCode copy for the import that follows; an import is the end.
212
+ if (!args.dryRun && selection.identity.kind === "opencode-db") {
213
+ const { opencodeSnapshots } = await import("./opencode-snapshot.js");
214
+ opencodeSnapshots.discard(importSourceKey(selection.identity));
215
+ }
216
+ }
217
+ });
218
+ return { ...job };
219
+ }
220
+ cancel() {
221
+ if (!this.job || !this.controller || this.job.state !== "running")
222
+ throw new Error("No running import");
223
+ this.controller.abort();
224
+ this.job.state = "cancelling";
225
+ return { ...this.job };
226
+ }
227
+ }
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { formatMemoriesForCompaction } from "./core/retrieval.js";
11
11
  import { performUserProfileLearning } from "./services/user-memory-learning.js";
12
12
  import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
13
13
  import { startWebServer, WebServer } from "./services/web-server.js";
14
+ import { pruneTraces } from "./services/capture-diagnostics.js";
14
15
  import { ensureTursoReady } from "./services/turso/ready.js";
15
16
  import { tursoConnectionManager } from "./services/turso/connection-manager.js";
16
17
  import { WebAuth } from "./services/web-auth.js";
@@ -180,6 +181,8 @@ function logAutoCaptureProviderStatus() {
180
181
  export const OmmsPlugin = async (ctx) => {
181
182
  const { directory } = ctx;
182
183
  initConfigWithLegacyMigration(directory);
184
+ // Runs even with tracing off, so turning it off does not leave old traces behind.
185
+ pruneTraces(CONFIG);
183
186
  logAutoCaptureProviderStatus();
184
187
  const tags = getTags(directory);
185
188
  const autoCaptureHost = createOpenCodeAutoCaptureHost(ctx);
@@ -248,6 +251,7 @@ export const OmmsPlugin = async (ctx) => {
248
251
  username: CONFIG.webServerAuthUsername,
249
252
  });
250
253
  startWebServer({
254
+ directory,
251
255
  port: CONFIG.webServerPort,
252
256
  host: CONFIG.webServerHost,
253
257
  enabled: CONFIG.webServerEnabled,
@@ -36,6 +36,9 @@ function getManualProviderStatus(config) {
36
36
  export function getAutoCaptureProviderStatus(config) {
37
37
  const hasOpencodeProvider = hasValue(config.opencodeProvider);
38
38
  const hasOpencodeModel = hasValue(config.opencodeModel);
39
+ if (config.opencodeModel?.trim() === "inherit") {
40
+ return { ready: true, mode: "session", issues: [] };
41
+ }
39
42
  if (hasOpencodeProvider && hasOpencodeModel) {
40
43
  return { ready: true, mode: "opencode", issues: [] };
41
44
  }
@@ -65,10 +68,10 @@ export function resolveOpencodeHostModel(config) {
65
68
  }
66
69
  /** Pi's live model, by the same rule as OpenCode's (`piProvider`/`piModel` in place of `opencode*`). */
67
70
  export function resolvePiLiveModel(config) {
71
+ if (config.piModel?.trim() === "inherit")
72
+ return { kind: "session" };
68
73
  if (hasValue(config.piProvider) && hasValue(config.piModel)) {
69
- return config.piModel.trim() === "inherit"
70
- ? { kind: "session" }
71
- : { kind: "pi", provider: config.piProvider.trim(), model: config.piModel.trim() };
74
+ return { kind: "pi", provider: config.piProvider.trim(), model: config.piModel.trim() };
72
75
  }
73
76
  const manual = getManualProviderStatus(config);
74
77
  if (manual.ready)
@@ -0,0 +1,11 @@
1
+ import type { CaptureSummaryProvider } from "../../core/host.js";
2
+ import type { ModelPort } from "../../core/profile-analysis.js";
3
+ export interface OpencodeModelRef {
4
+ providerID: string;
5
+ modelID: string;
6
+ }
7
+ /** Capture and profile calls routed through OpenCode's structured-output sessions. */
8
+ export declare function createOpencodeImportModels(ref: OpencodeModelRef, directory: string): Promise<{
9
+ capture: CaptureSummaryProvider;
10
+ profile: ModelPort;
11
+ }>;
@@ -0,0 +1,55 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
3
+ import { loadOpencodeProvider } from "./opencode-provider-loader.js";
4
+ /** Capture and profile calls routed through OpenCode's structured-output sessions. */
5
+ export async function createOpencodeImportModels(ref, directory) {
6
+ const { getV2Client, generateStructuredOutput } = await loadOpencodeProvider();
7
+ const client = getV2Client();
8
+ if (!client)
9
+ throw new Error("the OpenCode client is not ready; retry in a moment");
10
+ const { z } = await import("zod");
11
+ const { buildCaptureSystemPrompt, createUserProfileAnalysisSchema, parseCaptureSummary } = await import("../../core/extraction.js");
12
+ const { detectLanguage, getLanguageName } = await import("../language-detector.js");
13
+ const captureSchema = z.object({
14
+ summary: z.string().optional(),
15
+ type: z.string(),
16
+ tags: z.array(z.string()).optional(),
17
+ });
18
+ const capture = {
19
+ async summarize(request) {
20
+ const target = CONFIG.autoCaptureLanguage && CONFIG.autoCaptureLanguage !== "auto"
21
+ ? CONFIG.autoCaptureLanguage
22
+ : detectLanguage(request.userPrompt);
23
+ const systemPrompt = buildCaptureSystemPrompt(getLanguageName(target));
24
+ const result = await generateStructuredOutput({
25
+ client,
26
+ ...ref,
27
+ systemPrompt,
28
+ userPrompt: buildBoundedSummaryPrompt(request.context, systemPrompt, z.toJSONSchema(captureSchema)),
29
+ schema: captureSchema,
30
+ directory,
31
+ });
32
+ const parsed = parseCaptureSummary(JSON.stringify(result));
33
+ if (!parsed)
34
+ throw new Error("History capture returned an invalid summary");
35
+ return parsed;
36
+ },
37
+ };
38
+ const profileSchema = createUserProfileAnalysisSchema(z);
39
+ const profile = {
40
+ provider: ref.providerID,
41
+ modelId: ref.modelID,
42
+ async complete(systemPrompt, userPrompt) {
43
+ const result = await generateStructuredOutput({
44
+ client,
45
+ ...ref,
46
+ systemPrompt,
47
+ userPrompt,
48
+ schema: profileSchema,
49
+ directory,
50
+ });
51
+ return JSON.stringify(result);
52
+ },
53
+ };
54
+ return { capture, profile };
55
+ }
@@ -46,6 +46,8 @@ export declare const STRUCTURED_OUTPUT_METADATA: {
46
46
  };
47
47
  export declare function setHostFetch(customFetch: typeof fetch): void;
48
48
  export declare function resetHostFetch(): void;
49
+ /** Test helper: forget the server URL, as in the native V2 plugin. */
50
+ export declare function resetV2BaseUrlForTests(): void;
49
51
  export declare function setConnectedProviders(providers: string[]): void;
50
52
  export declare function isProviderConnected(providerName: string): boolean;
51
53
  export declare function setV2Client(client: OpencodeClient): void;
@@ -64,6 +66,13 @@ export interface StructuredOutputOptions<T> {
64
66
  schema: z.ZodType<T>;
65
67
  directory?: string;
66
68
  retryCount?: number;
69
+ /** Called with the raw reply before it is parsed, for capture diagnostics. */
70
+ onReply?: (reply: StructuredOutputReply) => void;
71
+ }
72
+ export interface StructuredOutputReply {
73
+ finish?: string;
74
+ partTypes: string[];
75
+ structuredOutput: unknown;
67
76
  }
68
77
  /**
69
78
  * Resolve `opencodeModel: "inherit"` to a concrete provider/model.