om-memory-system 4.25.0 → 4.26.1

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 (84) hide show
  1. package/README.md +17 -15
  2. package/dist/adapters/opencode/auto-capture-summary.js +1 -1
  3. package/dist/adapters/opencode/import-command.js +2 -2
  4. package/dist/adapters/opencode/profile-learning.js +9 -9
  5. package/dist/adapters/opencode/profile-model.js +1 -1
  6. package/dist/adapters/pi/capture.js +2 -2
  7. package/dist/adapters/pi/extension.js +8 -8
  8. package/dist/adapters/pi/import-command.js +2 -2
  9. package/dist/adapters/pi/live-model.js +1 -1
  10. package/dist/adapters/pi/profile.js +2 -2
  11. package/dist/cli/web-power.d.ts +2 -1
  12. package/dist/cli/web-power.js +2 -2
  13. package/dist/config.js +1 -1
  14. package/dist/core/profile-analysis.js +1 -1
  15. package/dist/core/profile-backoff.js +1 -1
  16. package/dist/importer/auto-backfill.js +14 -4
  17. package/dist/importer/backfill-controls.js +1 -1
  18. package/dist/importer/claude-hook-api.js +5 -5
  19. package/dist/importer/claude-profile-learning.js +1 -1
  20. package/dist/importer/import-readiness.d.ts +3 -0
  21. package/dist/importer/import-readiness.js +11 -4
  22. package/dist/importer/run-import.js +4 -3
  23. package/dist/index.js +19 -19
  24. package/dist/services/ai/providers/anthropic-messages.js +2 -2
  25. package/dist/services/ai/providers/google-gemini.js +1 -1
  26. package/dist/services/ai/providers/openai-chat-completion.js +3 -3
  27. package/dist/services/ai/providers/openai-responses.js +2 -2
  28. package/dist/services/ai-cleanup-handlers.js +1 -1
  29. package/dist/services/api-handlers.js +27 -27
  30. package/dist/services/auto-capture.js +3 -3
  31. package/dist/services/auto-cleanup-handlers.js +1 -1
  32. package/dist/services/backfill-state.d.ts +8 -2
  33. package/dist/services/backfill-state.js +14 -4
  34. package/dist/services/capture-diagnostics.js +3 -3
  35. package/dist/services/capture-retry-drain.js +5 -5
  36. package/dist/services/capture-retry-queue.js +1 -1
  37. package/dist/services/chat/chat-routes.js +1 -1
  38. package/dist/services/cleanup-service.js +5 -5
  39. package/dist/services/client.js +10 -10
  40. package/dist/services/deduplication-service.js +1 -1
  41. package/dist/services/embedding.js +2 -2
  42. package/dist/services/host-appearance.d.ts +28 -0
  43. package/dist/services/host-appearance.js +117 -0
  44. package/dist/services/logger.d.ts +7 -1
  45. package/dist/services/logger.js +9 -3
  46. package/dist/services/memory-host-routes.d.ts +11 -0
  47. package/dist/services/memory-host-routes.js +56 -0
  48. package/dist/services/memory-portability-service.js +1 -1
  49. package/dist/services/migration-service.js +2 -2
  50. package/dist/services/settings-log.d.ts +23 -2
  51. package/dist/services/settings-log.js +65 -7
  52. package/dist/services/shard-path-migration-service.js +4 -4
  53. package/dist/services/tag-prefix-migration.js +1 -1
  54. package/dist/services/turso/connection-manager.js +3 -3
  55. package/dist/services/turso/keyword-search.js +1 -1
  56. package/dist/services/turso/legacy-migrator.js +1 -1
  57. package/dist/services/turso/ready.js +1 -1
  58. package/dist/services/turso/shard-manager.js +2 -2
  59. package/dist/services/turso/vector-search.js +3 -3
  60. package/dist/services/user-profile/ai-cleanup-batches.js +1 -1
  61. package/dist/services/user-profile/ai-cleanup-job.js +1 -1
  62. package/dist/services/user-profile/auto-cleanup.js +2 -2
  63. package/dist/services/user-profile/category-cleanup-job.js +1 -1
  64. package/dist/services/user-profile/category-cleanup.js +3 -3
  65. package/dist/services/user-profile/merge-and-save.js +1 -1
  66. package/dist/services/user-profile/profile-context.js +1 -1
  67. package/dist/services/user-profile/profile-model.js +1 -1
  68. package/dist/services/user-profile/user-profile-manager.js +12 -12
  69. package/dist/services/web-autostart.js +2 -2
  70. package/dist/services/web-route-modules.js +4 -0
  71. package/dist/services/web-server.js +13 -13
  72. package/dist/services/web-update.d.ts +2 -1
  73. package/dist/services/web-update.js +1 -1
  74. package/dist/services/write-health-checks.d.ts +3 -0
  75. package/dist/services/write-health-checks.js +20 -0
  76. package/dist/services/write-health.d.ts +26 -0
  77. package/dist/services/write-health.js +58 -0
  78. package/dist/v2/adapter.js +4 -4
  79. package/dist/web/assets/index-CIlyLof-.js +126 -0
  80. package/dist/web/assets/index-DH6qnFfk.css +2 -0
  81. package/dist/web/index.html +2 -2
  82. package/package.json +1 -1
  83. package/dist/web/assets/index-DWk9C9Sm.css +0 -2
  84. package/dist/web/assets/index-rHcTKCkC.js +0 -126
package/README.md CHANGED
@@ -106,10 +106,12 @@ Browse captured notes alongside example user input and technical outcomes.
106
106
 
107
107
  ### Memory
108
108
 
109
- Review history imports, manage profile learning and set memory limits. This page
110
- also helps resolve missing project folders.
109
+ Pick a host card (Pi, OpenCode or Claude Code) to see its import status, sessions
110
+ and folders that need a map. The Shared card holds profile learning, memory limits
111
+ and other settings that apply to every host. The cards also warn you when memories
112
+ cannot be saved.
111
113
 
112
- ![Memory page with synthetic import and profile examples](.github/screenshots/memory.png)
114
+ ![Memory page showing the Pi host view with synthetic import status](.github/screenshots/memory.png)
113
115
 
114
116
  ### Settings
115
117
 
@@ -119,18 +121,18 @@ Choose models and manage access. Diagnostics show capture outcomes and component
119
121
 
120
122
  ## Documentation
121
123
 
122
- | Guide | Covers |
123
- | ------------------------------------------- | ------------------------------------------------------------ |
124
- | [Using memory](docs/using-memory.md) | Daily use, capture, recall and the user profile |
125
- | [Web UI](docs/web-ui.md) | Starting the app, browsing memories and safe network access |
126
- | [Memory page](docs/web-ui-memory.md) | History imports, profile actions, limits and missing folders |
127
- | [Settings page](docs/web-ui-settings.md) | Models, credentials, diagnostics and profile identities |
128
- | [Configuration](docs/configuration.md) | Config files, model selection, privacy and embeddings |
129
- | [Moving projects](docs/moving-projects.md) | Folder moves, backup and restore |
130
- | [Updating and upgrading](docs/upgrading.md) | Updates, version pinning and older stores |
131
- | [CLI reference](docs/cli.md) | Terminal commands |
132
- | [For developers](docs/developers.md) | Building, testing and architecture |
133
- | [Contributing](CONTRIBUTING.md) | Development workflow and pull requests |
124
+ | Guide | Covers |
125
+ | ------------------------------------------- | ----------------------------------------------------------- |
126
+ | [Using memory](docs/using-memory.md) | Daily use, capture, recall and the user profile |
127
+ | [Web UI](docs/web-ui.md) | Starting the app, browsing memories and safe network access |
128
+ | [Memory page](docs/web-ui-memory.md) | One card per host, imports, shared settings and avatars |
129
+ | [Settings page](docs/web-ui-settings.md) | Models, credentials, diagnostics and profile identities |
130
+ | [Configuration](docs/configuration.md) | Config files, model selection, privacy and embeddings |
131
+ | [Moving projects](docs/moving-projects.md) | Folder moves, backup and restore |
132
+ | [Updating and upgrading](docs/upgrading.md) | Updates, version pinning and older stores |
133
+ | [CLI reference](docs/cli.md) | Terminal commands |
134
+ | [For developers](docs/developers.md) | Building, testing and architecture |
135
+ | [Contributing](CONTRIBUTING.md) | Development workflow and pull requests |
134
136
 
135
137
  History import guides: [OpenCode](docs/opencode-history-import.md),
136
138
  [Pi](docs/pi-history-import.md) and [Claude Code](docs/claude-code-history-import.md).
@@ -103,7 +103,7 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
103
103
  diagnostics.retryAfterMs = errorRetryAfterMs(error);
104
104
  log("auto-capture: opencode provider failed, falling back to external API", {
105
105
  error: String(error),
106
- });
106
+ }, "warn");
107
107
  }
108
108
  }
109
109
  if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl) {
@@ -61,7 +61,7 @@ export async function runOpencodeImportCommand(input) {
61
61
  await memoryClient.warmup();
62
62
  }
63
63
  catch (error) {
64
- log("OpenCode import: memory warmup failed", { error: String(error) });
64
+ log("OpenCode import: memory warmup failed", { error: String(error) }, "error");
65
65
  }
66
66
  }
67
67
  let lastProgress = 0;
@@ -81,7 +81,7 @@ export async function runOpencodeImportCommand(input) {
81
81
  }
82
82
  catch (error) {
83
83
  const message = error instanceof Error ? error.message : String(error);
84
- log("OpenCode history import failed", { error: message });
84
+ log("OpenCode history import failed", { error: message }, "error");
85
85
  return `${name} failed: ${message}`;
86
86
  }
87
87
  finally {
@@ -24,7 +24,7 @@ export async function performUserProfileLearning(ctx, directory) {
24
24
  if (!CONFIG.autoCaptureProviderStatus || !CONFIG.autoCaptureProviderStatus.ready) {
25
25
  log("user-profile-learning: skipped (provider not ready)", {
26
26
  issues: CONFIG.autoCaptureProviderStatus?.issues ?? ["status undefined"],
27
- });
27
+ }, "warn");
28
28
  return;
29
29
  }
30
30
  isLearningRunning = true;
@@ -92,7 +92,7 @@ Rules:
92
92
  log("user-profile-learning: validation skipped", {
93
93
  prefCount: topPrefs.length,
94
94
  reason: "needs ≥ 5 preferences",
95
- });
95
+ }, "warn");
96
96
  }
97
97
  }
98
98
  else if (existingProfile) {
@@ -139,7 +139,7 @@ Rules:
139
139
  log("user-profile-learning: retry merge", {
140
140
  retry: retries,
141
141
  profileId: existingProfile.id,
142
- });
142
+ }, "warn");
143
143
  }
144
144
  cleanupPreviousPromptCount = existingProfile.totalPromptsAnalyzed;
145
145
  let changeSummary = generateChangeSummary(JSON.parse(existingProfile.profileData), updatedProfileData);
@@ -153,7 +153,7 @@ Rules:
153
153
  profileId: existingProfile.id,
154
154
  userId,
155
155
  retry: retries,
156
- });
156
+ }, "warn");
157
157
  }
158
158
  retries++;
159
159
  }
@@ -397,7 +397,7 @@ async function applyValidations(profileData, llmResult, profileId, prefKeys) {
397
397
  continue;
398
398
  const item = allItems.find((i) => `${i.category || "_"}||${(i.description || "").substring(0, 30)}` === key);
399
399
  if (!item) {
400
- log("user-profile-learning: validation match failed", { index: v.index, key });
400
+ log("user-profile-learning: validation match failed", { index: v.index, key }, "warn");
401
401
  continue;
402
402
  }
403
403
  if (v.verdict === "confirmed") {
@@ -509,7 +509,7 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne
509
509
  opencodeProviderError = e;
510
510
  log("user-profile-learning: opencode provider failed, falling back to external API", {
511
511
  error: String(e),
512
- });
512
+ }, "warn");
513
513
  }
514
514
  }
515
515
  if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl) {
@@ -519,7 +519,7 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne
519
519
  log("User Profile Config Check Failed:", {
520
520
  memoryModel: CONFIG.memoryModel,
521
521
  memoryApiUrl: CONFIG.memoryApiUrl,
522
- });
522
+ }, "error");
523
523
  throw new Error("External API not configured for user memory learning");
524
524
  }
525
525
  const { AIProviderFactory } = await import("../../services/ai/ai-provider-factory.js");
@@ -594,7 +594,7 @@ If no clear chains, return { "paths": [] }.`;
594
594
  }
595
595
  }
596
596
  catch (e) {
597
- log("learning paths: native provider failed", { error: String(e) });
597
+ log("learning paths: native provider failed", { error: String(e) }, "warn");
598
598
  }
599
599
  }
600
600
  if (!result && CONFIG.memoryModel && CONFIG.memoryApiUrl) {
@@ -624,7 +624,7 @@ If no clear chains, return { "paths": [] }.`;
624
624
  }
625
625
  }
626
626
  catch (e) {
627
- log("learning paths: external API failed", { error: String(e) });
627
+ log("learning paths: external API failed", { error: String(e) }, "error");
628
628
  }
629
629
  }
630
630
  if (!result?.paths?.length)
@@ -109,7 +109,7 @@ export function registerOpencodeProfileModel() {
109
109
  return null;
110
110
  const v2Client = (await loadOpencodeProvider()).getV2Client();
111
111
  if (!v2Client) {
112
- log("profile model: opencode client unavailable, falling back to external API");
112
+ log("profile model: opencode client unavailable, falling back to external API", undefined, "warn");
113
113
  return null;
114
114
  }
115
115
  return adaptOpencodeProfileModel(v2Client, hostModel);
@@ -20,7 +20,7 @@ async function captureWithQuickRetries(unit, provider) {
20
20
  throw error;
21
21
  log(`Pi auto-capture warning (attempt ${attempt}/${maxRetries})`, {
22
22
  sessionID: unit.hostSessionId,
23
- });
23
+ }, "warn");
24
24
  await new Promise((resolve) => setTimeout(resolve, quickRetryDelayMs(attempt)));
25
25
  }
26
26
  }
@@ -95,7 +95,7 @@ export async function capturePiSettledWorkUnit(input) {
95
95
  }
96
96
  catch (error) {
97
97
  const message = error instanceof Error ? error.message : String(error);
98
- log(`Pi auto-capture error: ${message}`, { sessionID: input.sessionId });
98
+ log(`Pi auto-capture error: ${message}`, { sessionID: input.sessionId }, "error");
99
99
  // The next prompt moves capture on to a newer turn, so keep this one for a later retry.
100
100
  if (workUnit)
101
101
  await queueFailedCapture(workUnit, error, CONFIG);
@@ -110,14 +110,14 @@ export default function ommsPiExtension(pi) {
110
110
  // Record this copy first: the login item and the other hosts run the newest recorded copy.
111
111
  void import("../../services/runtime-handoff.js")
112
112
  .then(({ registerOwnCopy }) => registerOwnCopy())
113
- .catch((error) => log("Pi runtime record failed", { code: error instanceof Error ? error.name : "unknown" }));
113
+ .catch((error) => log("Pi runtime record failed", { code: error instanceof Error ? error.name : "unknown" }, "error"));
114
114
  if (CONFIG.webServerAutoStart !== undefined &&
115
115
  process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
116
116
  void import("../../services/web-autostart.js")
117
117
  .then(({ reconcileWebAutostart }) => reconcileWebAutostart(CONFIG))
118
118
  .catch((error) => log("Pi login item reconciliation failed", {
119
119
  code: error instanceof Error ? error.name : "unknown",
120
- }));
120
+ }, "error"));
121
121
  }
122
122
  if (process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
123
123
  // Start the shared web app if none runs. The session does not wait for it.
@@ -141,7 +141,7 @@ export default function ommsPiExtension(pi) {
141
141
  })
142
142
  .catch((error) => log("Pi web app start failed", {
143
143
  code: error instanceof Error ? error.name : "unknown",
144
- }));
144
+ }, "error"));
145
145
  }
146
146
  // Retry turns whose capture failed earlier, with this session's capture model.
147
147
  void import("../../services/capture-retry-drain.js")
@@ -196,7 +196,7 @@ export default function ommsPiExtension(pi) {
196
196
  })()
197
197
  .catch((error) => log("Pi backfill failed", {
198
198
  error: error instanceof Error ? error.message : String(error),
199
- }))
199
+ }, "error"))
200
200
  .finally(() => {
201
201
  if (globalScope[GLOBAL_PI_BACKFILL_KEY] === task) {
202
202
  delete globalScope[GLOBAL_PI_BACKFILL_KEY];
@@ -211,7 +211,7 @@ export default function ommsPiExtension(pi) {
211
211
  finishStatus(ctx, status, "connected");
212
212
  }
213
213
  catch (error) {
214
- log("Pi plugin memory warmup failed", { error: String(error) });
214
+ log("Pi plugin memory warmup failed", { error: String(error) }, "error");
215
215
  finishStatus(ctx, status, "error");
216
216
  }
217
217
  })();
@@ -221,7 +221,7 @@ export default function ommsPiExtension(pi) {
221
221
  }
222
222
  }
223
223
  catch (error) {
224
- log("Pi session_start error", { error: String(error) });
224
+ log("Pi session_start error", { error: String(error) }, "error");
225
225
  finishStatus(ctx, status, "error");
226
226
  }
227
227
  });
@@ -247,7 +247,7 @@ export default function ommsPiExtension(pi) {
247
247
  };
248
248
  }
249
249
  catch (error) {
250
- log("Pi before_agent_start retrieval error", { error: String(error) });
250
+ log("Pi before_agent_start retrieval error", { error: String(error) }, "error");
251
251
  finishStatus(ctx, status, "error");
252
252
  return undefined;
253
253
  }
@@ -293,7 +293,7 @@ export default function ommsPiExtension(pi) {
293
293
  finishStatus(ctx, status, captureResult.status === "failed" ? "error" : "connected");
294
294
  }
295
295
  catch (error) {
296
- log("Pi agent_settled error", { error: String(error) });
296
+ log("Pi agent_settled error", { error: String(error) }, "error");
297
297
  finishStatus(ctx, status, "error");
298
298
  }
299
299
  });
@@ -84,7 +84,7 @@ export function registerPiHistoryImportCommand(pi, getExtensionContext) {
84
84
  await memoryClient.warmup();
85
85
  }
86
86
  catch (error) {
87
- log("Pi import: memory warmup failed", { error: String(error) });
87
+ log("Pi import: memory warmup failed", { error: String(error) }, "error");
88
88
  }
89
89
  }
90
90
  let lastProgressNotify = 0;
@@ -109,7 +109,7 @@ export function registerPiHistoryImportCommand(pi, getExtensionContext) {
109
109
  }
110
110
  catch (error) {
111
111
  const message = error instanceof Error ? error.message : String(error);
112
- log("Pi history import failed", { error: message });
112
+ log("Pi history import failed", { error: message }, "error");
113
113
  notify(`memory-import-pi-history failed: ${message}`);
114
114
  }
115
115
  finally {
@@ -40,7 +40,7 @@ async function withFallback(primary, run, external, notify) {
40
40
  if (!isExternalModelReady(CONFIG))
41
41
  throw failure;
42
42
  const reason = failure instanceof Error ? failure.message : String(failure);
43
- log("Pi live model failed, falling back to external API", { error: reason });
43
+ log("Pi live model failed, falling back to external API", { error: reason }, "warn");
44
44
  await Promise.resolve(notify?.({
45
45
  title: "Using fallback provider",
46
46
  message: `Pi model failed (${reason.length > 100 ? `${reason.slice(0, 100)}...` : reason}); using configured fallback.`,
@@ -65,7 +65,7 @@ export async function performPiProfileLearning(input) {
65
65
  if (!tags.user.userEmail) {
66
66
  log("pi profile learning: skipped (no user email resolved)", {
67
67
  directory: input.directory,
68
- });
68
+ }, "warn");
69
69
  return;
70
70
  }
71
71
  const userId = tags.user.userEmail;
@@ -84,7 +84,7 @@ export async function performPiProfileLearning(input) {
84
84
  const context = input.prompts.map((prompt, i) => `${i + 1}. ${prompt}`).join("\n");
85
85
  const analysis = await analyzer.analyzeProfile(context, existing ? { id: existing.id, profileData: existing.profileData } : null);
86
86
  if (!analysis) {
87
- log("pi profile learning: skipped (no model available)", { userId });
87
+ log("pi profile learning: skipped (no model available)", { userId }, "warn");
88
88
  return;
89
89
  }
90
90
  if (existing && analysis.merged) {
@@ -1,3 +1,4 @@
1
+ import type { LogLevel } from "../services/logger.js";
1
2
  import type { WebAutostartOptions } from "../services/web-autostart.js";
2
3
  /** The two choices the page offers; the web server passes them through unchanged. */
3
4
  export type PowerAction = "stop" | "restart";
@@ -51,7 +52,7 @@ export interface PowerDeps {
51
52
  launcher: string | null;
52
53
  cwd: string;
53
54
  pid: number;
54
- log: (message: string, data: Record<string, unknown>) => void | Promise<void>;
55
+ log: (message: string, data: Record<string, unknown>, level?: LogLevel) => void | Promise<void>;
55
56
  }
56
57
  /**
57
58
  * The callback the web server runs after it replies to Stop or Restart.
@@ -79,7 +79,7 @@ function productionDeps(options) {
79
79
  launcher: existsSync(launcherPath(ommsDir())) ? launcherPath(ommsDir()) : null,
80
80
  cwd: homedir(),
81
81
  pid: process.pid,
82
- log: async (message, data) => (await import("../services/logger.js")).log(message, data),
82
+ log: async (message, data, level) => (await import("../services/logger.js")).log(message, data, level),
83
83
  };
84
84
  }
85
85
  /**
@@ -92,7 +92,7 @@ function productionDeps(options) {
92
92
  */
93
93
  export function createPowerAction(options) {
94
94
  const deps = { ...productionDeps(options.autostart ?? {}), ...options.deps };
95
- const logFailure = (code) => Promise.resolve(deps.log("Web app restart failed", { code })).catch(() => undefined);
95
+ const logFailure = (code) => Promise.resolve(deps.log("Web app restart failed", { code }, "error")).catch(() => undefined);
96
96
  let stopped = false;
97
97
  /** Start the copy, check it, stop serving, and wait until the copy answers. */
98
98
  async function handOff(args) {
package/dist/config.js CHANGED
@@ -498,7 +498,7 @@ export function refreshConfigIfChanged(directory) {
498
498
  // every unit. The last good directory and signature stay as they were.
499
499
  log("Config reload failed; keeping the previous settings", {
500
500
  error: error instanceof Error ? error.message : String(error),
501
- });
501
+ }, "error");
502
502
  lastFailedConfig = attempt;
503
503
  }
504
504
  }
@@ -42,7 +42,7 @@ export async function analyzeProfile(model, context, existingProfile, timeoutMs
42
42
  log("pi profile analysis: model reply failed shared schema validation", {
43
43
  provider: model.provider,
44
44
  modelId: model.modelId,
45
- });
45
+ }, "error");
46
46
  throw new Error("pi profile analysis: invalid profile payload");
47
47
  }
48
48
  const raw = parsed.data;
@@ -22,7 +22,7 @@ export const profileBackoff = {
22
22
  export function recordProfileFailure(host, message, error, now = Date.now()) {
23
23
  const reason = profileFailureCode(error);
24
24
  profileBackoff.recordFailure(now);
25
- log(message, { host, reason });
25
+ log(message, { host, reason }, "error");
26
26
  return reason;
27
27
  }
28
28
  let catchUpActive = false;
@@ -4,7 +4,7 @@ import { tryAcquireBackfillLock } from "./backfill-lock.js";
4
4
  import { getBackfillCutoff, recordUnresolvedDirectories, unresolvedDirectoriesOf, unresolvedSessionCount, updateBackfillStatus, } from "../services/backfill-state.js";
5
5
  import { runHistoryImport, } from "./run-import.js";
6
6
  import { isManualImportRunning } from "./manual-import-guard.js";
7
- import { isBackfillPaused } from "./import-runs.js";
7
+ import { isBackfillPaused, startImportRun } from "./import-runs.js";
8
8
  import { workProgress } from "./import-progress.js";
9
9
  const zero = () => ({ imported: 0, skipped: 0, failed: 0, pending: 0, unresolved: 0 });
10
10
  function counts(report) {
@@ -79,15 +79,25 @@ export async function scheduleAutoBackfill(options) {
79
79
  signal: controller.signal,
80
80
  });
81
81
  current = counts(dry);
82
- await recordUnresolvedDirectories(options.host, unresolvedDirectoriesOf(dry)).catch((error) => log("Backfill unresolved directories write failed", {
82
+ // The dry run stops at the cutoff, so its list covers only history before it.
83
+ await recordUnresolvedDirectories(options.host, unresolvedDirectoriesOf(dry), clock(), "partial").catch((error) => log("Backfill unresolved directories write failed", {
83
84
  host: options.host,
84
85
  error: error instanceof Error ? error.message : String(error),
85
- }));
86
+ }, "error"));
86
87
  if (controller.signal.aborted) {
87
88
  state = "stopped";
88
89
  return;
89
90
  }
90
91
  if (!current.pending) {
92
+ // A pressed Run now needs an answer on the page: record a finished run.
93
+ if (options.userStarted) {
94
+ await startImportRun(options.host, options.surface ?? "web")
95
+ .then((recorder) => recorder.finish("done", {}))
96
+ .catch((error) => log("Backfill run record write failed", {
97
+ host: options.host,
98
+ error: error instanceof Error ? error.message : String(error),
99
+ }, "error"));
100
+ }
91
101
  state = "done";
92
102
  return;
93
103
  }
@@ -155,7 +165,7 @@ export async function scheduleAutoBackfill(options) {
155
165
  log("Backfill status write failed", {
156
166
  host: options.host,
157
167
  error: error instanceof Error ? error.message : String(error),
158
- });
168
+ }, "error");
159
169
  });
160
170
  },
161
171
  });
@@ -126,7 +126,7 @@ export class BackfillControls {
126
126
  .catch((error) => log(`${source} failed`, {
127
127
  host,
128
128
  error: error instanceof Error ? error.name : "unknown",
129
- }))
129
+ }, "error"))
130
130
  .finally(() => {
131
131
  if (this.controllers.get(host) === controller)
132
132
  this.controllers.delete(host);
@@ -136,7 +136,7 @@ function maybeStartBackfill(deps) {
136
136
  backfillRequested = true;
137
137
  void deps
138
138
  .startBackfill()
139
- .catch((error) => log("Claude Code backfill start failed", { code: errorCode(error) }));
139
+ .catch((error) => log("Claude Code backfill start failed", { code: errorCode(error) }, "error"));
140
140
  }
141
141
  async function retrievalSection(request, budgetBytes) {
142
142
  // Every section this api returns is wrapped in the retrieval tag, so each
@@ -195,7 +195,7 @@ export async function handleClaudeRetrieve(body, deps = {}) {
195
195
  event: request.event,
196
196
  sessionID: request.sessionId,
197
197
  code: errorCode(error),
198
- });
198
+ }, "error");
199
199
  return { additionalContext: "" };
200
200
  }
201
201
  }
@@ -218,7 +218,7 @@ export function handleClaudeCapture(body, deps = {}) {
218
218
  const request = parseCaptureRequest(body);
219
219
  captureChain = captureChain
220
220
  .then(() => captureTurn(request, deps))
221
- .catch((error) => log("Claude Code capture failed", { sessionID: request.sessionId, code: errorCode(error) }));
221
+ .catch((error) => log("Claude Code capture failed", { sessionID: request.sessionId, code: errorCode(error) }, "error"));
222
222
  return { queued: true };
223
223
  }
224
224
  /** Resolves when every queued capture has finished. */
@@ -226,7 +226,7 @@ export function whenClaudeCaptureIdle() {
226
226
  return captureChain;
227
227
  }
228
228
  function skipped(request, code, data = {}) {
229
- log("Claude Code capture skipped", { sessionID: request.sessionId, code, ...data });
229
+ log("Claude Code capture skipped", { sessionID: request.sessionId, code, ...data }, "warn");
230
230
  }
231
231
  async function readTranscript(request) {
232
232
  let text;
@@ -406,7 +406,7 @@ async function recordPrompt(request, unit, memoryId) {
406
406
  log("Claude Code prompt record failed", {
407
407
  sessionID: request.sessionId,
408
408
  code: errorCode(error),
409
- });
409
+ }, "error");
410
410
  }
411
411
  }
412
412
  // --- Web app start ---
@@ -31,7 +31,7 @@ export async function learnClaudeProfile(directory, resolveModel) {
31
31
  return;
32
32
  const user = getTags(directory).user;
33
33
  if (!user.userEmail) {
34
- log("Claude Code profile learning skipped", { code: "no-user-email" });
34
+ log("Claude Code profile learning skipped", { code: "no-user-email" }, "warn");
35
35
  return;
36
36
  }
37
37
  const batch = await userPromptManager.getPromptsForUserLearning(interval, {
@@ -34,6 +34,8 @@ export interface ImportReadiness {
34
34
  defaultRootFound: boolean;
35
35
  modelChoices: ["external"];
36
36
  };
37
+ /** Whether each host's default history source exists on this machine. */
38
+ defaultSourceFound: Record<ImportHost, boolean>;
37
39
  }
38
40
  export interface ReadinessDeps {
39
41
  supportedProviders?: () => Promise<string[]>;
@@ -47,6 +49,7 @@ export interface ReadinessDeps {
47
49
  }>;
48
50
  loadPiSdk?: () => Promise<unknown>;
49
51
  claudeRoot?: () => string;
52
+ sourceExists?: (path: string) => boolean;
50
53
  }
51
54
  /** Mirrors the checks in `selectImportModel`, without building a provider. */
52
55
  export declare function externalApiState(supportedProviders?: () => Promise<string[]>): Promise<ExternalApiState>;
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { CONFIG } from "../config.js";
3
- import { defaultClaudeSourcePath } from "./import-sources.js";
3
+ import { defaultClaudeSourcePath, defaultImportSourcePath } from "./import-sources.js";
4
4
  const EXTERNAL_REASONS = {
5
5
  "missing-model": "The external API has no memoryModel in the global config",
6
6
  "missing-url": "The external API has no memoryApiUrl in the global config",
@@ -33,18 +33,25 @@ export async function importReadiness(deps = {}) {
33
33
  reason: "The Pi SDK (@earendil-works/pi-coding-agent) was not found. Install Pi on this computer.",
34
34
  })),
35
35
  ]);
36
+ const exists = deps.sourceExists ?? existsSync;
37
+ const claudeCode = claudeReadiness(deps.claudeRoot?.() ?? defaultClaudeSourcePath(CONFIG.claudeConfigDir), exists);
36
38
  return {
37
39
  external: { state, provider: CONFIG.memoryProvider, model: CONFIG.memoryModel ?? null },
38
40
  opencode: { available: opencode.available, models: opencode.models ?? [] },
39
41
  piReader,
40
- claudeCode: claudeReadiness(deps.claudeRoot?.() ?? defaultClaudeSourcePath(CONFIG.claudeConfigDir)),
42
+ claudeCode,
43
+ defaultSourceFound: {
44
+ pi: exists(defaultImportSourcePath("pi")),
45
+ opencode: exists(defaultImportSourcePath("opencode")),
46
+ "claude-code": claudeCode.defaultRootFound,
47
+ },
41
48
  };
42
49
  }
43
- function claudeReadiness(defaultRoot) {
50
+ function claudeReadiness(defaultRoot, exists) {
44
51
  return {
45
52
  available: true,
46
53
  defaultRoot,
47
- defaultRootFound: existsSync(defaultRoot),
54
+ defaultRootFound: exists(defaultRoot),
48
55
  modelChoices: ["external"],
49
56
  };
50
57
  }
@@ -72,7 +72,7 @@ export async function runHistoryImport(host, args, run) {
72
72
  }, report.profile?.error ? profileFailureCode(report.profile.error) : undefined);
73
73
  // Only a run over every project and every session sees the full list.
74
74
  if (args.scope === "all-projects" && !run.selection)
75
- await recordUnresolved(host, report);
75
+ await recordUnresolved(host, report, args.since !== undefined || args.until !== undefined);
76
76
  return report;
77
77
  }
78
78
  catch (error) {
@@ -89,10 +89,11 @@ export async function runHistoryImport(host, args, run) {
89
89
  await release?.();
90
90
  }
91
91
  }
92
- async function recordUnresolved(host, report) {
92
+ async function recordUnresolved(host, report, dateLimited) {
93
93
  try {
94
94
  const { recordUnresolvedDirectories, unresolvedDirectoriesOf } = await import("../services/backfill-state.js");
95
- await recordUnresolvedDirectories(host, unresolvedDirectoriesOf(report));
95
+ // A run limited by date misses unresolved sessions outside it, so it must not replace a whole-history list.
96
+ await recordUnresolvedDirectories(host, unresolvedDirectoriesOf(report), Date.now(), dateLimited ? "partial" : "full");
96
97
  }
97
98
  catch {
98
99
  // The list is a convenience for the Settings page; the import itself succeeded.