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
@@ -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,24 @@
1
+ export { safeHealthError } from "../services/safe-health-error.js";
2
+ export type HealthRow = {
3
+ check: string;
4
+ status: "pass" | "warn" | "fail";
5
+ reason: string;
6
+ };
7
+ export declare function captureFailureHealth(byModel: Array<{
8
+ total: number;
9
+ failed: number;
10
+ }>, byReason: Array<{
11
+ reason: string;
12
+ count: number;
13
+ }>): HealthRow;
14
+ export interface HealthInput {
15
+ directory: string;
16
+ host: string;
17
+ authEnabled: boolean;
18
+ apiTokenSet: boolean;
19
+ testModels: boolean;
20
+ }
21
+ /** Run checks independently so a failed dependency cannot hide the remaining checks. */
22
+ export declare function runSettingsHealth(input: HealthInput): Promise<{
23
+ checks: HealthRow[];
24
+ }>;
@@ -0,0 +1,127 @@
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
+ import { safeHealthError } from "../services/safe-health-error.js";
5
+ export { safeHealthError } from "../services/safe-health-error.js";
6
+ const PROBE = "Reply with a short acknowledgement.";
7
+ export function captureFailureHealth(byModel, byReason) {
8
+ const total = byModel.reduce((count, row) => count + row.total, 0);
9
+ const failed = byModel.reduce((count, row) => count + row.failed, 0);
10
+ const reason = [...byReason].sort((a, b) => b.count - a.count)[0]?.reason;
11
+ return {
12
+ check: "Capture failures (24 hours)",
13
+ status: total > 0 && failed / total > 0.2 ? "warn" : "pass",
14
+ reason: total
15
+ ? `${failed}/${total} failed${reason ? `; most common reason: ${reason}` : ""}`
16
+ : "No recent attempts",
17
+ };
18
+ }
19
+ /** Run checks independently so a failed dependency cannot hide the remaining checks. */
20
+ export async function runSettingsHealth(input) {
21
+ const checks = [];
22
+ async function check(name, action) {
23
+ try {
24
+ checks.push({ check: name, status: "pass", reason: await action() });
25
+ }
26
+ catch (error) {
27
+ checks.push({
28
+ check: name,
29
+ status: "fail",
30
+ reason: safeHealthError(error, [
31
+ CONFIG.memoryApiKey,
32
+ CONFIG.embeddingApiKey,
33
+ CONFIG.webServerApiToken,
34
+ ]),
35
+ });
36
+ }
37
+ }
38
+ await check("Config files", async () => {
39
+ const { getSettingsSnapshot } = await import("../services/settings-snapshot.js");
40
+ getSettingsSnapshot(input.directory);
41
+ return "Global and project config parsed";
42
+ });
43
+ await check("Memory store", async () => {
44
+ const { tursoConnectionManager } = await import("../services/turso/connection-manager.js");
45
+ const { join } = await import("node:path");
46
+ const db = await tursoConnectionManager.getConnection(join(CONFIG.storagePath, "user-prompts.db"));
47
+ await db.all("SELECT 1");
48
+ return "Store opened and queried";
49
+ });
50
+ await check("Embedding", async () => {
51
+ const { embeddingService } = await import("../services/embedding.js");
52
+ const result = await embeddingService.embedWithTimeout("OMMS health check", { task: "query" });
53
+ if (!result.length)
54
+ throw new Error("Embedding returned no values");
55
+ return "Test text embedded";
56
+ });
57
+ const secure = isLoopbackHost(input.host) || input.authEnabled || input.apiTokenSet;
58
+ checks.push({
59
+ check: "Web binding",
60
+ status: secure ? "pass" : "fail",
61
+ reason: secure
62
+ ? "Binding and access settings are safe"
63
+ : "Network binding requires an API token or Basic Auth",
64
+ });
65
+ const opencode = getAutoCaptureProviderStatus(CONFIG);
66
+ checks.push({
67
+ check: "OpenCode model",
68
+ status: opencode.ready ? "pass" : "fail",
69
+ reason: opencode.ready ? `Resolved: ${opencode.mode}` : opencode.issues.join("; "),
70
+ });
71
+ const pi = resolvePiLiveModel(CONFIG);
72
+ checks.push({
73
+ check: "Pi model",
74
+ status: pi.kind === "unready" ? "fail" : "pass",
75
+ reason: pi.kind === "unready" ? pi.issues.join("; ") : `Resolved: ${pi.kind}`,
76
+ });
77
+ try {
78
+ const { queryCaptureAttempts } = await import("../services/capture-attempt-store.js");
79
+ const { byModel, byReason } = await queryCaptureAttempts(Date.now() - 86400000, Date.now());
80
+ checks.push(captureFailureHealth(byModel, byReason.map((row) => ({ count: row.count, reason: String(row.reason) }))));
81
+ }
82
+ catch (error) {
83
+ checks.push({
84
+ check: "Capture failures (24 hours)",
85
+ status: "fail",
86
+ reason: safeHealthError(error, [CONFIG.memoryApiKey]),
87
+ });
88
+ }
89
+ if (input.testModels) {
90
+ await check("OpenCode model test", async () => {
91
+ const { resolveOpencodeHostModel } = await import("../services/ai/live-model-choice.js");
92
+ const ref = resolveOpencodeHostModel(CONFIG);
93
+ if (ref?.modelID === "inherit")
94
+ throw new Error("A session model needs an active session to test");
95
+ const capture = ref
96
+ ? (await (await import("../services/ai/opencode-import-models.js")).createOpencodeImportModels(ref, input.directory)).capture
97
+ : (await import("./model-selection.js")).selectImportModel({}).capture;
98
+ await capture.summarize({
99
+ userPrompt: PROBE,
100
+ context: PROBE,
101
+ sessionId: "health",
102
+ projectDirectory: input.directory,
103
+ });
104
+ return "Fixed prompt completed";
105
+ });
106
+ if (pi.kind === "manual") {
107
+ await check("Pi model test", async () => {
108
+ const { selectImportModel } = await import("./model-selection.js");
109
+ await selectImportModel({}).capture.summarize({
110
+ userPrompt: PROBE,
111
+ context: PROBE,
112
+ sessionId: "health",
113
+ projectDirectory: input.directory,
114
+ });
115
+ return "Fixed prompt completed";
116
+ });
117
+ }
118
+ else {
119
+ checks.push({
120
+ check: "Pi model test",
121
+ status: "warn",
122
+ reason: "Pi models need an active Pi session to test",
123
+ });
124
+ }
125
+ }
126
+ return { checks };
127
+ }
@@ -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,15 @@ 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);
186
+ if (CONFIG.webServerAutoStart !== undefined && process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
187
+ void import("./services/web-autostart.js")
188
+ .then(({ reconcileWebAutostart }) => reconcileWebAutostart(CONFIG))
189
+ .catch((error) => log("OpenCode login item reconciliation failed", {
190
+ code: error instanceof Error ? error.name : "unknown",
191
+ }));
192
+ }
183
193
  logAutoCaptureProviderStatus();
184
194
  const tags = getTags(directory);
185
195
  const autoCaptureHost = createOpenCodeAutoCaptureHost(ctx);
@@ -200,9 +210,11 @@ export const OmmsPlugin = async (ctx) => {
200
210
  }
201
211
  await configureOpencodeHostTransport(ctx);
202
212
  (async () => {
213
+ let connected = [];
203
214
  try {
204
215
  const providerResult = await ctx.client.provider.list();
205
216
  if (providerResult.data?.connected) {
217
+ connected = providerResult.data.connected;
206
218
  const { setConnectedProviders } = await loadOpencodeProvider();
207
219
  setConnectedProviders(providerResult.data.connected);
208
220
  log("opencode providers connected", {
@@ -219,6 +231,27 @@ export const OmmsPlugin = async (ctx) => {
219
231
  catch (error) {
220
232
  log("Failed to initialize opencode provider state", { error: String(error) });
221
233
  }
234
+ if (CONFIG.autoBackfill && process.env.OMMS_DISABLE_AUTO_BACKFILL !== "1" && isConfigured()) {
235
+ void import("./adapters/opencode/backfill-startup.js")
236
+ .then(({ startOpencodeBackfill }) => startOpencodeBackfill({
237
+ connected,
238
+ directory,
239
+ configModel: async () => {
240
+ const response = await ctx.client.config.get();
241
+ return unwrapSdkData(response)?.model ?? null;
242
+ },
243
+ notify: (message) => {
244
+ void ctx.client.tui
245
+ ?.showToast({
246
+ body: { title: "Automatic import", message, variant: "info", duration: 5000 },
247
+ })
248
+ .catch(() => { });
249
+ },
250
+ }))
251
+ .catch((error) => log("OpenCode backfill failed", {
252
+ error: error instanceof Error ? error.message : String(error),
253
+ }));
254
+ }
222
255
  })();
223
256
  let tursoReadyForWeb = !isConfigured();
224
257
  if (CONFIG.webServerEnabled && isConfigured()) {
@@ -248,6 +281,7 @@ export const OmmsPlugin = async (ctx) => {
248
281
  username: CONFIG.webServerAuthUsername,
249
282
  });
250
283
  startWebServer({
284
+ directory,
251
285
  port: CONFIG.webServerPort,
252
286
  host: CONFIG.webServerHost,
253
287
  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
+ }>;