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/dist/index.js CHANGED
@@ -57,7 +57,7 @@ export async function resolveSessionAgent(client, sessionID) {
57
57
  }
58
58
  }
59
59
  catch (error) {
60
- log("resolveSessionAgent: session.get failed", { sessionID, error: String(error) });
60
+ log("resolveSessionAgent: session.get failed", { sessionID, error: String(error) }, "error");
61
61
  }
62
62
  }
63
63
  if (typeof sessionClient?.messages !== "function") {
@@ -89,7 +89,7 @@ export async function resolveSessionAgent(client, sessionID) {
89
89
  }
90
90
  }
91
91
  catch (error) {
92
- log("resolveSessionAgent: session.messages failed", { sessionID, error: String(error) });
92
+ log("resolveSessionAgent: session.messages failed", { sessionID, error: String(error) }, "error");
93
93
  }
94
94
  return undefined;
95
95
  }
@@ -117,11 +117,11 @@ async function isInternalCaptureSession(client, sessionID) {
117
117
  log("internal capture session check via session.get failed", {
118
118
  sessionID,
119
119
  error: String(error),
120
- });
120
+ }, "error");
121
121
  }
122
122
  }
123
123
  else {
124
- log("internal capture session check: session.get unavailable", { sessionID });
124
+ log("internal capture session check: session.get unavailable", { sessionID }, "warn");
125
125
  }
126
126
  return false;
127
127
  }
@@ -164,7 +164,7 @@ export async function configureOpencodeHostTransport(ctx) {
164
164
  log("OpenCode host fetch unavailable; falling back to global fetch", {
165
165
  clientKeys: hostConfig.clientKeys,
166
166
  sdkConfigCount: hostConfig.sdkConfigCount,
167
- });
167
+ }, "warn");
168
168
  }
169
169
  const serverUrl = hostConfig.baseUrl ?? ctx.serverUrl;
170
170
  if (serverUrl) {
@@ -195,13 +195,13 @@ export const OmmsPlugin = async (ctx) => {
195
195
  .then(({ registerOwnCopy }) => registerOwnCopy())
196
196
  .catch((error) => log("OpenCode runtime record failed", {
197
197
  code: error instanceof Error ? error.name : "unknown",
198
- }));
198
+ }, "error"));
199
199
  if (CONFIG.webServerAutoStart !== undefined && process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
200
200
  void import("./services/web-autostart.js")
201
201
  .then(({ reconcileWebAutostart }) => reconcileWebAutostart(CONFIG))
202
202
  .catch((error) => log("OpenCode login item reconciliation failed", {
203
203
  code: error instanceof Error ? error.name : "unknown",
204
- }));
204
+ }, "error"));
205
205
  }
206
206
  logAutoCaptureProviderStatus();
207
207
  const tags = getTags(directory);
@@ -216,7 +216,7 @@ export const OmmsPlugin = async (ctx) => {
216
216
  globalThis[GLOBAL_PLUGIN_WARMUP_KEY] = true;
217
217
  }
218
218
  catch (error) {
219
- log("Plugin memory warmup failed", { error: String(error) });
219
+ log("Plugin memory warmup failed", { error: String(error) }, "error");
220
220
  }
221
221
  })();
222
222
  }
@@ -247,11 +247,11 @@ export const OmmsPlugin = async (ctx) => {
247
247
  else {
248
248
  log("opencode provider list empty or failed", {
249
249
  data: JSON.stringify(providerResult.data).substring(0, 100),
250
- });
250
+ }, "warn");
251
251
  }
252
252
  }
253
253
  catch (error) {
254
- log("Failed to initialize opencode provider state", { error: String(error) });
254
+ log("Failed to initialize opencode provider state", { error: String(error) }, "error");
255
255
  }
256
256
  const configModel = async () => {
257
257
  const response = await ctx.client.config.get();
@@ -277,7 +277,7 @@ export const OmmsPlugin = async (ctx) => {
277
277
  }))
278
278
  .catch((error) => log("OpenCode backfill failed", {
279
279
  error: error instanceof Error ? error.message : String(error),
280
- }));
280
+ }, "error"));
281
281
  }
282
282
  })();
283
283
  let tursoReadyForWeb = !isConfigured();
@@ -287,7 +287,7 @@ export const OmmsPlugin = async (ctx) => {
287
287
  tursoReadyForWeb = true;
288
288
  }
289
289
  catch (error) {
290
- log("Turso ready gate failed before web server start", { error: String(error) });
290
+ log("Turso ready gate failed before web server start", { error: String(error) }, "error");
291
291
  if (ctx.client?.tui) {
292
292
  ctx.client.tui
293
293
  .showToast({
@@ -338,7 +338,7 @@ export const OmmsPlugin = async (ctx) => {
338
338
  }
339
339
  });
340
340
  })
341
- .catch((error) => log("Web app start failed", { error: String(error) }));
341
+ .catch((error) => log("Web app start failed", { error: String(error) }, "error"));
342
342
  }
343
343
  let cleanedUp = false;
344
344
  const cleanupPlugin = async () => {
@@ -357,7 +357,7 @@ export const OmmsPlugin = async (ctx) => {
357
357
  await cleanupPlugin();
358
358
  }
359
359
  catch (error) {
360
- log("Shutdown error", { error: String(error) });
360
+ log("Shutdown error", { error: String(error) }, "error");
361
361
  process.exitCode = 1;
362
362
  }
363
363
  };
@@ -477,7 +477,7 @@ export const OmmsPlugin = async (ctx) => {
477
477
  }
478
478
  }
479
479
  catch (error) {
480
- log("chat.message: ERROR", { error: String(error) });
480
+ log("chat.message: ERROR", { error: String(error) }, "error");
481
481
  if (ctx.client?.tui && CONFIG.showErrorToasts) {
482
482
  await ctx.client.tui
483
483
  .showToast({
@@ -500,7 +500,7 @@ export const OmmsPlugin = async (ctx) => {
500
500
  await userPromptManager.setPromptModel(input.message.id, input.model.providerID, input.model.id);
501
501
  }
502
502
  catch (error) {
503
- log("chat.params: ERROR", { error: String(error) });
503
+ log("chat.params: ERROR", { error: String(error) }, "error");
504
504
  }
505
505
  },
506
506
  tool: {
@@ -581,7 +581,7 @@ export const OmmsPlugin = async (ctx) => {
581
581
  await cleanupService.runCleanup();
582
582
  }
583
583
  catch (error) {
584
- log("Idle processing error", { error: String(error) });
584
+ log("Idle processing error", { error: String(error) }, "error");
585
585
  }
586
586
  finally {
587
587
  idleTimeout = null;
@@ -610,7 +610,7 @@ export const OmmsPlugin = async (ctx) => {
610
610
  if (!agent) {
611
611
  log("Compaction: skipped memory injection because session agent could not be resolved", {
612
612
  sessionID,
613
- });
613
+ }, "warn");
614
614
  return;
615
615
  }
616
616
  await ctx.client.session.prompt({
@@ -647,7 +647,7 @@ export const OmmsPlugin = async (ctx) => {
647
647
  });
648
648
  }
649
649
  catch (error) {
650
- log("Compaction handler error", { error: String(error) });
650
+ log("Compaction handler error", { error: String(error) }, "error");
651
651
  }
652
652
  }
653
653
  },
@@ -120,7 +120,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
120
120
  status: response.status,
121
121
  error: errorText,
122
122
  iteration: iterations,
123
- });
123
+ }, "error");
124
124
  return {
125
125
  stopReason,
126
126
  success: false,
@@ -168,7 +168,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
168
168
  iteration: iterations,
169
169
  // Log only the size: the tool input is the model's reply.
170
170
  inputChars: JSON.stringify(toolUse)?.length ?? 0,
171
- });
171
+ }, "error");
172
172
  return {
173
173
  stopReason,
174
174
  success: false,
@@ -153,7 +153,7 @@ export class GoogleGeminiProvider extends BaseAIProvider {
153
153
  status: response.status,
154
154
  error: errorText,
155
155
  iteration: iterations,
156
- });
156
+ }, "error");
157
157
  return {
158
158
  stopReason,
159
159
  success: false,
@@ -212,7 +212,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
212
212
  status: response.status,
213
213
  error: errorText,
214
214
  iteration: iterations,
215
- });
215
+ }, "error");
216
216
  let errorMessage = `API error: ${response.status} - ${errorText}`;
217
217
  if (response.status === 400 &&
218
218
  errorText.includes("unsupported_value") &&
@@ -236,7 +236,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
236
236
  model: this.config.model,
237
237
  status: data.status,
238
238
  msg: data.msg,
239
- });
239
+ }, "error");
240
240
  return {
241
241
  stopReason,
242
242
  success: false,
@@ -346,7 +346,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
346
346
  iteration: iterations,
347
347
  // Log only the size: the arguments are the model's reply.
348
348
  argumentsChars: toolCall.function.arguments.length,
349
- });
349
+ }, "error");
350
350
  const errorMessage = `Validation failed: ${describeValidationError(validationError)}`;
351
351
  lastErrorMessage = errorMessage;
352
352
  await this.addToolResponse(session.id, messages, toolCallId, JSON.stringify({ success: false, error: errorMessage }));
@@ -68,7 +68,7 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
68
68
  status: response.status,
69
69
  error: errorText,
70
70
  iteration: iterations,
71
- });
71
+ }, "error");
72
72
  return {
73
73
  stopReason,
74
74
  success: false,
@@ -150,7 +150,7 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
150
150
  error: String(error),
151
151
  toolName: item.name,
152
152
  arguments: item.arguments,
153
- });
153
+ }, "error");
154
154
  return null;
155
155
  }
156
156
  }
@@ -12,7 +12,7 @@ async function managerOf(deps) {
12
12
  }
13
13
  function handlerError(step, error) {
14
14
  // The log never holds profile text, so only the error's kind is kept.
15
- log(`AI cleanup ${step}: error`, { code: error instanceof Error ? error.name : "unknown" });
15
+ log(`AI cleanup ${step}: error`, { code: error instanceof Error ? error.name : "unknown" }, "error");
16
16
  return { success: false, error: String(error) };
17
17
  }
18
18
  /** The profile in the order the dialog lists it, so item ids match its positions. */
@@ -97,7 +97,7 @@ export async function handleListTags() {
97
97
  return { success: true, data: { project: projectTags } };
98
98
  }
99
99
  catch (error) {
100
- log("handleListTags: error", { error: String(error) });
100
+ log("handleListTags: error", { error: String(error) }, "error");
101
101
  return { success: false, error: String(error) };
102
102
  }
103
103
  }
@@ -140,7 +140,7 @@ export async function handleListKeywords(tag) {
140
140
  return { success: true, data: { keywords: countKeywords(rows.map((r) => r.tags)) } };
141
141
  }
142
142
  catch (error) {
143
- log("handleListKeywords: error", { error: String(error) });
143
+ log("handleListKeywords: error", { error: String(error) }, "error");
144
144
  return { success: false, error: String(error) };
145
145
  }
146
146
  }
@@ -279,7 +279,7 @@ export async function handleListMemories(tag, page = 1, pageSize = 20, includePr
279
279
  return { success: true, data: { items, total, page, pageSize, totalPages } };
280
280
  }
281
281
  catch (error) {
282
- log("handleListMemories: error", { error: String(error) });
282
+ log("handleListMemories: error", { error: String(error) }, "error");
283
283
  return { success: false, error: String(error) };
284
284
  }
285
285
  }
@@ -329,7 +329,7 @@ export async function handleAddMemory(data) {
329
329
  });
330
330
  }
331
331
  catch (error) {
332
- log("handleAddMemory: error", { error: String(error) });
332
+ log("handleAddMemory: error", { error: String(error) }, "error");
333
333
  return { success: false, error: String(error) };
334
334
  }
335
335
  }
@@ -359,7 +359,7 @@ export async function handleDeleteMemory(id, cascade = false) {
359
359
  return { success: false, error: "Memory not found" };
360
360
  }
361
361
  catch (error) {
362
- log("handleDeleteMemory: error", { error: String(error) });
362
+ log("handleDeleteMemory: error", { error: String(error) }, "error");
363
363
  return { success: false, error: String(error) };
364
364
  }
365
365
  }
@@ -376,7 +376,7 @@ export async function handleBulkDelete(ids, cascade = false) {
376
376
  return { success: true, data: { deleted } };
377
377
  }
378
378
  catch (error) {
379
- log("handleBulkDelete: error", { error: String(error) });
379
+ log("handleBulkDelete: error", { error: String(error) }, "error");
380
380
  return { success: false, error: String(error) };
381
381
  }
382
382
  }
@@ -443,7 +443,7 @@ export async function handleUpdateMemory(id, data) {
443
443
  return { success: true };
444
444
  }
445
445
  catch (error) {
446
- log("handleUpdateMemory: error", { error: String(error) });
446
+ log("handleUpdateMemory: error", { error: String(error) }, "error");
447
447
  return { success: false, error: String(error) };
448
448
  }
449
449
  }
@@ -465,7 +465,7 @@ export async function handleSearch(query, tag, page = 1, pageSize = 20) {
465
465
  memoryResults.push(...results);
466
466
  }
467
467
  catch (error) {
468
- log("Shard search error", { shardId: shard.id, error: String(error) });
468
+ log("Shard search error", { shardId: shard.id, error: String(error) }, "error");
469
469
  }
470
470
  }
471
471
  const projectPath = await getProjectPathFromTag(tag);
@@ -485,7 +485,7 @@ export async function handleSearch(query, tag, page = 1, pageSize = 20) {
485
485
  for (const containerTag of uniqueTags) {
486
486
  const parsed = tryExtractScopeFromContainerTag(containerTag);
487
487
  if (!parsed) {
488
- log("Skipping invalid container_tag during global search", { containerTag });
488
+ log("Skipping invalid container_tag during global search", { containerTag }, "warn");
489
489
  continue;
490
490
  }
491
491
  const { scope, hash } = parsed;
@@ -496,7 +496,7 @@ export async function handleSearch(query, tag, page = 1, pageSize = 20) {
496
496
  memoryResults.push(...results);
497
497
  }
498
498
  catch (error) {
499
- log("Shard search error", { shardId: shard.id, error: String(error) });
499
+ log("Shard search error", { shardId: shard.id, error: String(error) }, "error");
500
500
  }
501
501
  }
502
502
  }
@@ -602,7 +602,7 @@ export async function handleSearch(query, tag, page = 1, pageSize = 20) {
602
602
  return { success: true, data: { items: paginatedResults, total, page, pageSize, totalPages } };
603
603
  }
604
604
  catch (error) {
605
- log("handleSearch: error", { error: String(error) });
605
+ log("handleSearch: error", { error: String(error) }, "error");
606
606
  return { success: false, error: String(error) };
607
607
  }
608
608
  }
@@ -637,7 +637,7 @@ export async function handleStats() {
637
637
  };
638
638
  }
639
639
  catch (error) {
640
- log("handleStats: error", { error: String(error) });
640
+ log("handleStats: error", { error: String(error) }, "error");
641
641
  return { success: false, error: String(error) };
642
642
  }
643
643
  }
@@ -657,7 +657,7 @@ export async function handlePinMemory(id) {
657
657
  return { success: false, error: "Memory not found" };
658
658
  }
659
659
  catch (error) {
660
- log("handlePinMemory: error", { error: String(error) });
660
+ log("handlePinMemory: error", { error: String(error) }, "error");
661
661
  return { success: false, error: String(error) };
662
662
  }
663
663
  }
@@ -677,7 +677,7 @@ export async function handleUnpinMemory(id) {
677
677
  return { success: false, error: "Memory not found" };
678
678
  }
679
679
  catch (error) {
680
- log("handleUnpinMemory: error", { error: String(error) });
680
+ log("handleUnpinMemory: error", { error: String(error) }, "error");
681
681
  return { success: false, error: String(error) };
682
682
  }
683
683
  }
@@ -688,7 +688,7 @@ export async function handleRunCleanup() {
688
688
  return { success: true, data: result };
689
689
  }
690
690
  catch (error) {
691
- log("handleRunCleanup: error", { error: String(error) });
691
+ log("handleRunCleanup: error", { error: String(error) }, "error");
692
692
  return { success: false, error: String(error) };
693
693
  }
694
694
  }
@@ -699,7 +699,7 @@ export async function handleRunDeduplication() {
699
699
  return { success: true, data: result };
700
700
  }
701
701
  catch (error) {
702
- log("handleRunDeduplication: error", { error: String(error) });
702
+ log("handleRunDeduplication: error", { error: String(error) }, "error");
703
703
  return { success: false, error: String(error) };
704
704
  }
705
705
  }
@@ -710,7 +710,7 @@ export async function handleDetectMigration() {
710
710
  return { success: true, data: result };
711
711
  }
712
712
  catch (error) {
713
- log("handleDetectMigration: error", { error: String(error) });
713
+ log("handleDetectMigration: error", { error: String(error) }, "error");
714
714
  return { success: false, error: String(error) };
715
715
  }
716
716
  }
@@ -721,7 +721,7 @@ export async function handleRunMigration(strategy) {
721
721
  return { success: result.success, data: result };
722
722
  }
723
723
  catch (error) {
724
- log("handleRunMigration: error", { error: String(error) });
724
+ log("handleRunMigration: error", { error: String(error) }, "error");
725
725
  return { success: false, error: String(error) };
726
726
  }
727
727
  }
@@ -742,7 +742,7 @@ export async function handleDeletePrompt(id, cascade = false) {
742
742
  return { success: true, data: { deletedMemory } };
743
743
  }
744
744
  catch (error) {
745
- log("handleDeletePrompt: error", { error: String(error) });
745
+ log("handleDeletePrompt: error", { error: String(error) }, "error");
746
746
  return { success: false, error: String(error) };
747
747
  }
748
748
  }
@@ -759,7 +759,7 @@ export async function handleBulkDeletePrompts(ids, cascade = false) {
759
759
  return { success: true, data: { deleted } };
760
760
  }
761
761
  catch (error) {
762
- log("handleBulkDeletePrompts: error", { error: String(error) });
762
+ log("handleBulkDeletePrompts: error", { error: String(error) }, "error");
763
763
  return { success: false, error: String(error) };
764
764
  }
765
765
  }
@@ -802,7 +802,7 @@ export async function handleGetUserProfile(userId) {
802
802
  };
803
803
  }
804
804
  catch (error) {
805
- log("handleGetUserProfile: error", { error: String(error) });
805
+ log("handleGetUserProfile: error", { error: String(error) }, "error");
806
806
  return { success: false, error: String(error) };
807
807
  }
808
808
  }
@@ -823,7 +823,7 @@ export async function handleGetProfileChangelog(profileId, limit = 5) {
823
823
  return { success: true, data: formattedChangelogs };
824
824
  }
825
825
  catch (error) {
826
- log("handleGetProfileChangelog: error", { error: String(error) });
826
+ log("handleGetProfileChangelog: error", { error: String(error) }, "error");
827
827
  return { success: false, error: String(error) };
828
828
  }
829
829
  }
@@ -846,7 +846,7 @@ export async function handleGetProfileSnapshot(changelogId) {
846
846
  };
847
847
  }
848
848
  catch (error) {
849
- log("handleGetProfileSnapshot: error", { error: String(error) });
849
+ log("handleGetProfileSnapshot: error", { error: String(error) }, "error");
850
850
  return { success: false, error: String(error) };
851
851
  }
852
852
  }
@@ -880,7 +880,7 @@ export async function handleRefreshProfile(userId) {
880
880
  };
881
881
  }
882
882
  catch (error) {
883
- log("handleRefreshProfile: error", { error: String(error) });
883
+ log("handleRefreshProfile: error", { error: String(error) }, "error");
884
884
  return { success: false, error: String(error) };
885
885
  }
886
886
  }
@@ -889,7 +889,7 @@ async function categoryCleanupUser(userId) {
889
889
  }
890
890
  function categoryCleanupError(step, error) {
891
891
  // The log never holds category names or descriptions, so only the error's kind is kept.
892
- log(`category cleanup ${step}: error`, { code: error instanceof Error ? error.name : "unknown" });
892
+ log(`category cleanup ${step}: error`, { code: error instanceof Error ? error.name : "unknown" }, "error");
893
893
  return { success: false, error: String(error) };
894
894
  }
895
895
  /** Starts a background preview job, or returns the one already running. Changes nothing in the profile. */
@@ -1013,7 +1013,7 @@ export async function handleUpdateProfileItem(body) {
1013
1013
  };
1014
1014
  }
1015
1015
  catch (error) {
1016
- log("handleUpdateProfileItem: error", { error: String(error) });
1016
+ log("handleUpdateProfileItem: error", { error: String(error) }, "error");
1017
1017
  return { success: false, error: String(error) };
1018
1018
  }
1019
1019
  }
@@ -1114,7 +1114,7 @@ export async function handleRunTagMigrationBatch(batchSize = 5) {
1114
1114
  catch (e) {
1115
1115
  const errorMsg = String(e);
1116
1116
  migrationProgress.errors.push(errorMsg);
1117
- log("Migration error for memory", { id: item.id, error: errorMsg });
1117
+ log("Migration error for memory", { id: item.id, error: errorMsg }, "error");
1118
1118
  }
1119
1119
  finally {
1120
1120
  migrationProgress.processed++;
@@ -97,7 +97,7 @@ async function capturePrompt(host, sessionID, directory, prompt, maxRetries) {
97
97
  const errMsg = error instanceof Error ? error.message : String(error);
98
98
  await userPromptManager.recordFailedAttempt(prompt.id);
99
99
  if (attempt < maxRetries) {
100
- log(`Auto-capture warning (attempt ${attempt}/${maxRetries})`, { error: errMsg });
100
+ log(`Auto-capture warning (attempt ${attempt}/${maxRetries})`, { error: errMsg }, "warn");
101
101
  await new Promise((resolve) => setTimeout(resolve, quickRetryDelayMs(attempt)));
102
102
  }
103
103
  else {
@@ -108,7 +108,7 @@ async function capturePrompt(host, sessionID, directory, prompt, maxRetries) {
108
108
  }
109
109
  catch (error) {
110
110
  const errMsg = error instanceof Error ? error.message : String(error);
111
- log(`Auto-capture final error after ${attempt} attempts`, { error: errMsg });
111
+ log(`Auto-capture final error after ${attempt} attempts`, { error: errMsg }, "error");
112
112
  // Only after the quick retries fail; the queue decides whether a retry can help.
113
113
  if (workUnit)
114
114
  await queueFailedCapture(workUnit, error, CONFIG);
@@ -128,7 +128,7 @@ async function capturePrompt(host, sessionID, directory, prompt, maxRetries) {
128
128
  await userPromptManager.releaseClaim(claimedPromptId);
129
129
  }
130
130
  catch (releaseErr) {
131
- log(`Failed to release captured=2 claim for prompt ${claimedPromptId}: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
131
+ log(`Failed to release captured=2 claim for prompt ${claimedPromptId}: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`, undefined, "error");
132
132
  }
133
133
  }
134
134
  }
@@ -13,7 +13,7 @@ function handlerError(step, error) {
13
13
  // The log never holds profile text, so only the error's kind is kept.
14
14
  log(`Automatic cleanup ${step}: error`, {
15
15
  code: error instanceof Error ? error.name : "unknown",
16
- });
16
+ }, "error");
17
17
  return { success: false, error: String(error) };
18
18
  }
19
19
  /** The newest automatic cleanup that can still be undone, without its items. */
@@ -62,10 +62,16 @@ export declare function unresolvedSessionCount(report: {
62
62
  */
63
63
  export declare function visibleUnresolvedCount(count: number, directories: readonly UnresolvedDirectory[], ignored: readonly string[]): number;
64
64
  /**
65
- * Save the unresolved directories of a host's latest full run or listing. They
65
+ * How much history a saved unresolved list covers. A `partial` list comes from
66
+ * a run limited to history before a cutoff or a date range; it never replaces
67
+ * a `full` list, which covers the host's whole source.
68
+ */
69
+ export type UnresolvedCoverage = "full" | "partial";
70
+ /**
71
+ * Save the unresolved directories of a host's latest run or listing. They
66
72
  * live in their own table, so recording them never sets a backfill cutoff.
67
73
  */
68
- export declare function recordUnresolvedDirectories(host: BackfillHost, directories: UnresolvedDirectory[], now?: number): Promise<void>;
74
+ export declare function recordUnresolvedDirectories(host: BackfillHost, directories: UnresolvedDirectory[], now?: number, coverage?: UnresolvedCoverage): Promise<void>;
69
75
  /** A host's latest unresolved directories, without creating a store or table. */
70
76
  export declare function readUnresolvedDirectories(host: BackfillHost): Promise<UnresolvedDirectory[]>;
71
77
  /** Save only numeric counts and a redacted error, never report units or prompts. */
@@ -98,16 +98,26 @@ export function visibleUnresolvedCount(count, directories, ignored) {
98
98
  return Math.max(0, count - sum(directories.filter(isIgnored)));
99
99
  }
100
100
  /**
101
- * Save the unresolved directories of a host's latest full run or listing. They
101
+ * Save the unresolved directories of a host's latest run or listing. They
102
102
  * live in their own table, so recording them never sets a backfill cutoff.
103
103
  */
104
- export async function recordUnresolvedDirectories(host, directories, now = Date.now()) {
104
+ export async function recordUnresolvedDirectories(host, directories, now = Date.now(), coverage = "full") {
105
105
  const db = await tursoConnectionManager.getConnection(importLedgerDbPath());
106
106
  await db.run(`CREATE TABLE IF NOT EXISTS unresolved_directories (
107
107
  host TEXT PRIMARY KEY, directories TEXT NOT NULL, updated_at INTEGER NOT NULL
108
108
  )`);
109
- await db.run(`INSERT INTO unresolved_directories (host, directories, updated_at) VALUES (?, ?, ?)
110
- ON CONFLICT(host) DO UPDATE SET directories = excluded.directories, updated_at = excluded.updated_at`, [host, JSON.stringify(directories.slice(0, MAX_UNRESOLVED_DIRECTORIES)), now]);
109
+ try {
110
+ await db.run("ALTER TABLE unresolved_directories ADD COLUMN coverage TEXT NOT NULL DEFAULT 'full'");
111
+ }
112
+ catch (error) {
113
+ if (!String(error?.message ?? error).includes("duplicate column"))
114
+ throw error;
115
+ }
116
+ // One statement keeps two processes writing at once correct.
117
+ await db.run(`INSERT INTO unresolved_directories (host, directories, updated_at, coverage) VALUES (?, ?, ?, ?)
118
+ ON CONFLICT(host) DO UPDATE SET directories = excluded.directories,
119
+ updated_at = excluded.updated_at, coverage = excluded.coverage
120
+ WHERE unresolved_directories.coverage = 'partial' OR excluded.coverage = 'full'`, [host, JSON.stringify(directories.slice(0, MAX_UNRESOLVED_DIRECTORIES)), now, coverage]);
111
121
  }
112
122
  /** A host's latest unresolved directories, without creating a store or table. */
113
123
  export async function readUnresolvedDirectories(host) {
@@ -108,7 +108,7 @@ export function pruneTraces(config, now = new Date()) {
108
108
  }
109
109
  }
110
110
  catch (error) {
111
- log("Capture trace cleanup failed", { code: errorCode(error) });
111
+ log("Capture trace cleanup failed", { code: errorCode(error) }, "error");
112
112
  }
113
113
  return removed;
114
114
  }
@@ -163,7 +163,7 @@ export function writeTraceEntry(record, diagnostics, config, now = new Date()) {
163
163
  protectTracePath(file, 0o600);
164
164
  }
165
165
  catch (error) {
166
- log("Capture trace write failed", { code: errorCode(error) });
166
+ log("Capture trace write failed", { code: errorCode(error) }, "error");
167
167
  }
168
168
  }
169
169
  /**
@@ -179,7 +179,7 @@ export function emitCaptureAttempt(record, diagnostics, config, now = new Date()
179
179
  }
180
180
  void import("./capture-attempt-store.js")
181
181
  .then(({ saveCaptureAttempt }) => saveCaptureAttempt(record, now.getTime()))
182
- .catch((error) => log("Capture attempt store failed", { code: errorCode(error) }));
182
+ .catch((error) => log("Capture attempt store failed", { code: errorCode(error) }, "error"));
183
183
  if (config.captureTrace === true) {
184
184
  writeTraceEntry(record, diagnostics, config, now);
185
185
  }
@@ -34,12 +34,12 @@ export async function queueFailedCapture(unit, error, config) {
34
34
  sessionID: unit.hostSessionId,
35
35
  reason: failure.reason,
36
36
  httpStatus: failure.httpStatus ?? null,
37
- });
37
+ }, "warn");
38
38
  }
39
39
  return result === "queued";
40
40
  }
41
41
  catch (queueError) {
42
- log("Capture retry queue write failed", { code: errorCode(queueError) });
42
+ log("Capture retry queue write failed", { code: errorCode(queueError) }, "error");
43
43
  return false;
44
44
  }
45
45
  }
@@ -96,7 +96,7 @@ export async function drainCaptureRetries(options) {
96
96
  await options.onSettled?.(row.workUnit, outcome);
97
97
  }
98
98
  catch (error) {
99
- log("Capture retry follow-up failed", { host, code: errorCode(error) });
99
+ log("Capture retry follow-up failed", { host, code: errorCode(error) }, "error");
100
100
  }
101
101
  }
102
102
  if (result.captured + result.skipped + result.dropped > 0 || result.stopped) {
@@ -105,7 +105,7 @@ export async function drainCaptureRetries(options) {
105
105
  return result;
106
106
  }
107
107
  catch (error) {
108
- log("Capture retry pass failed", { host, code: errorCode(error) });
108
+ log("Capture retry pass failed", { host, code: errorCode(error) }, "error");
109
109
  return { ...result, status: "error" };
110
110
  }
111
111
  finally {
@@ -125,7 +125,7 @@ export function startCaptureRetryDrain(host) {
125
125
  const drain = registeredDrains.get(host);
126
126
  if (!drain)
127
127
  return;
128
- void drain().catch((error) => log("Capture retry pass failed", { host, code: errorCode(error) }));
128
+ void drain().catch((error) => log("Capture retry pass failed", { host, code: errorCode(error) }, "error"));
129
129
  }
130
130
  /**
131
131
  * Make every queued turn of one host due now. Starts a pass when this process
@@ -93,7 +93,7 @@ export async function enqueueCaptureRetry(unit, failure, config, now = Date.now(
93
93
  const json = JSON.stringify(cleaned);
94
94
  const size = Buffer.byteLength(json, "utf8");
95
95
  if (size > MAX_QUEUED_TURN_BYTES) {
96
- log("Capture retry skipped: turn too large", { sessionID: unit.hostSessionId, size });
96
+ log("Capture retry skipped: turn too large", { sessionID: unit.hostSessionId, size }, "warn");
97
97
  return "too-large";
98
98
  }
99
99
  const db = await database(config);
@@ -38,7 +38,7 @@ export async function routeChat(path, method, readBody, config) {
38
38
  if (error instanceof ChatStoreError) {
39
39
  return fail(STATUS_BY_CODE[error.code], error.code, error.message);
40
40
  }
41
- log("chat route: error", { path: path.replace(/\/[0-9a-f-]{36}/g, "/:id"), method });
41
+ log("chat route: error", { path: path.replace(/\/[0-9a-f-]{36}/g, "/:id"), method }, "error");
42
42
  return fail(500, "internal", "Chat request failed");
43
43
  }
44
44
  }