@equationalapplications/core-llm-wiki 6.4.0 → 6.5.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.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-DWpAgNLt.mjs';
2
- export { C as ChunkFailure, D as DegradedRecord, E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, I as IngestDocumentResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, J as WikiCheckpoint, K as WikiConfig, N as WikiDuplicateHashError, Q as WikiEdge, T as WikiEvent, U as WikiFact, X as WikiIngestEmptyError, Y as WikiInvalidReadOptions, Z as WikiMemoryTestAccess, _ as WikiOutboxEvent, $ as WikiParseError, a0 as WikiSourceRefHashCollision, a1 as WikiStrictOntologyViolation, a2 as WikiTask, a3 as WikiTransactionError } from './testing-DWpAgNLt.mjs';
1
+ import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-BtsN1pTK.mjs';
2
+ export { C as ChunkFailure, D as DegradedRecord, E as EmbedFactResult, d as EmbedFailureKind, e as EmbeddingMarkerKind, f as EntityStatus, g as ExtractedFact, h as ExtractedFactEdge, i as ExtractedFactWithOntology, j as ExtractedTask, k as GraphTraversalOptions, H as HEAL_BATCH_SIZE, l as HEAL_RECHECK_MS, m as HOOK_TIMEOUT_MARKER, n as HealResult, I as IngestDocumentResult, L as LLMProvider, o as ONTOLOGY_BACKFILL_BATCH_SIZE, p as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, q as ONTOLOGY_BACKFILL_RECHECK_MS, r as OntologyBackfillResult, s as OntologyConfig, t as OntologyEdgeType, u as OntologyMode, v as OntologyNodeType, w as OntologyPromptContext, x as OntologyUpdates, P as PromptOverrides, y as PromptService, z as PrunePartialFailureError, A as ReembedResult, V as VectorRanker, B as VectorRankerFallback, J as VectorRankerRankArgs, K as VectorRankerSemanticResult, N as WikiBusyError, Q as WikiBusyOperation, T as WikiCheckpoint, U as WikiConfig, X as WikiDuplicateHashError, Y as WikiEdge, Z as WikiEvent, _ as WikiFact, $ as WikiIngestEmptyError, a0 as WikiInvalidReadOptions, a1 as WikiMemoryTestAccess, a2 as WikiOutboxEvent, a3 as WikiParseError, a4 as WikiSourceRefHashCollision, a5 as WikiStrictOntologyViolation, a6 as WikiTask, a7 as WikiTransactionError } from './testing-BtsN1pTK.mjs';
3
3
  import { OkfFile } from '@equationalapplications/core-okf';
4
4
  import 'minisearch';
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-DWpAgNLt.js';
2
- export { C as ChunkFailure, D as DegradedRecord, E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, I as IngestDocumentResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, J as WikiCheckpoint, K as WikiConfig, N as WikiDuplicateHashError, Q as WikiEdge, T as WikiEvent, U as WikiFact, X as WikiIngestEmptyError, Y as WikiInvalidReadOptions, Z as WikiMemoryTestAccess, _ as WikiOutboxEvent, $ as WikiParseError, a0 as WikiSourceRefHashCollision, a1 as WikiStrictOntologyViolation, a2 as WikiTask, a3 as WikiTransactionError } from './testing-DWpAgNLt.js';
1
+ import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-BtsN1pTK.js';
2
+ export { C as ChunkFailure, D as DegradedRecord, E as EmbedFactResult, d as EmbedFailureKind, e as EmbeddingMarkerKind, f as EntityStatus, g as ExtractedFact, h as ExtractedFactEdge, i as ExtractedFactWithOntology, j as ExtractedTask, k as GraphTraversalOptions, H as HEAL_BATCH_SIZE, l as HEAL_RECHECK_MS, m as HOOK_TIMEOUT_MARKER, n as HealResult, I as IngestDocumentResult, L as LLMProvider, o as ONTOLOGY_BACKFILL_BATCH_SIZE, p as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, q as ONTOLOGY_BACKFILL_RECHECK_MS, r as OntologyBackfillResult, s as OntologyConfig, t as OntologyEdgeType, u as OntologyMode, v as OntologyNodeType, w as OntologyPromptContext, x as OntologyUpdates, P as PromptOverrides, y as PromptService, z as PrunePartialFailureError, A as ReembedResult, V as VectorRanker, B as VectorRankerFallback, J as VectorRankerRankArgs, K as VectorRankerSemanticResult, N as WikiBusyError, Q as WikiBusyOperation, T as WikiCheckpoint, U as WikiConfig, X as WikiDuplicateHashError, Y as WikiEdge, Z as WikiEvent, _ as WikiFact, $ as WikiIngestEmptyError, a0 as WikiInvalidReadOptions, a1 as WikiMemoryTestAccess, a2 as WikiOutboxEvent, a3 as WikiParseError, a4 as WikiSourceRefHashCollision, a5 as WikiStrictOntologyViolation, a6 as WikiTask, a7 as WikiTransactionError } from './testing-BtsN1pTK.js';
3
3
  import { OkfFile } from '@equationalapplications/core-okf';
4
4
  import 'minisearch';
5
5
 
package/dist/index.js CHANGED
@@ -45,7 +45,10 @@ async function setupDatabase(db, prefix) {
45
45
  last_verified_by TEXT,
46
46
  okf_sources TEXT,
47
47
  okf_verified TEXT,
48
- okf_usage_window TEXT
48
+ okf_usage_window TEXT,
49
+ embedding_failed_at INTEGER,
50
+ embedding_failure_kind TEXT,
51
+ embedding_attempts INTEGER NOT NULL DEFAULT 0
49
52
  );
50
53
 
51
54
  CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
@@ -576,6 +579,24 @@ var MIGRATIONS = [
576
579
  CREATE INDEX IF NOT EXISTS ${prefix}tasks_last_verified_at_idx ON ${prefix}tasks(last_verified_at);
577
580
  `);
578
581
  }
582
+ },
583
+ {
584
+ version: 11,
585
+ description: "Add embedding_failed_at, embedding_failure_kind, embedding_attempts to entries for embed retry state",
586
+ run: async (db, prefix) => {
587
+ const cols = await db.getAllAsync(
588
+ `PRAGMA table_info(${prefix}entries)`
589
+ );
590
+ const existing = new Set(cols.map((c) => c.name));
591
+ const adds = [
592
+ ["embedding_failed_at", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_failed_at INTEGER`],
593
+ ["embedding_failure_kind", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_failure_kind TEXT`],
594
+ ["embedding_attempts", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_attempts INTEGER NOT NULL DEFAULT 0`]
595
+ ];
596
+ for (const [name, sql] of adds) {
597
+ if (!existing.has(name)) await db.execAsync(sql);
598
+ }
599
+ }
579
600
  }
580
601
  ];
581
602
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -736,6 +757,13 @@ var EntryRepository = class extends BaseRepository {
736
757
  * New-row INSERTs still bind a value so the column is populated; if the
737
758
  * caller supplied nothing, the schema DEFAULT 'stable' applies when the
738
759
  * bind is bound as 'stable' explicitly here.
760
+ *
761
+ * Marker discipline (spec §4.1): a conflict carrying a real
762
+ * `embedding_blob` clears the embedding failure markers, because the row now
763
+ * HAS a valid embedding and a surviving marker would be a self-contradictory
764
+ * diagnostic that inflates runReembed's permanentlyFailed counter. A conflict
765
+ * with no blob leaves marker state alone, matching the "absent means don't
766
+ * touch" semantics used for embedding_blob itself.
739
767
  */
740
768
  async upsert(fact, tx) {
741
769
  const executor = this.getExecutor(tx);
@@ -773,6 +801,9 @@ var EntryRepository = class extends BaseRepository {
773
801
  deleted_at = excluded.deleted_at,
774
802
  embedding_blob = CASE WHEN excluded.embedding_blob IS NULL THEN embedding_blob ELSE excluded.embedding_blob END,
775
803
  embedding = NULL,
804
+ embedding_failed_at = CASE WHEN excluded.embedding_blob IS NOT NULL THEN NULL ELSE embedding_failed_at END,
805
+ embedding_failure_kind = CASE WHEN excluded.embedding_blob IS NOT NULL THEN NULL ELSE embedding_failure_kind END,
806
+ embedding_attempts = CASE WHEN excluded.embedding_blob IS NOT NULL THEN 0 ELSE embedding_attempts END,
776
807
  okf_type = excluded.okf_type`,
777
808
  [
778
809
  fact.id,
@@ -901,6 +932,9 @@ var EntryRepository = class extends BaseRepository {
901
932
  deleted_at = excluded.deleted_at,
902
933
  embedding_blob = excluded.embedding_blob,
903
934
  embedding = NULL,
935
+ embedding_failed_at = CASE WHEN excluded.embedding_blob IS NOT NULL THEN NULL ELSE embedding_failed_at END,
936
+ embedding_failure_kind = CASE WHEN excluded.embedding_blob IS NOT NULL THEN NULL ELSE embedding_failure_kind END,
937
+ embedding_attempts = CASE WHEN excluded.embedding_blob IS NOT NULL THEN 0 ELSE embedding_attempts END,
904
938
  okf_type = excluded.okf_type,
905
939
  lifecycle_status = excluded.lifecycle_status,
906
940
  stale_after = excluded.stale_after,
@@ -1337,10 +1371,58 @@ var EntryRepository = class extends BaseRepository {
1337
1371
  async updateEmbeddingBlob(id, blob, tx) {
1338
1372
  const executor = this.getExecutor(tx);
1339
1373
  await executor.runAsync(
1340
- `UPDATE ${this.prefix}entries SET embedding_blob = ?, embedding = NULL WHERE id = ?`,
1374
+ `UPDATE ${this.prefix}entries
1375
+ SET embedding_blob = ?,
1376
+ embedding = NULL,
1377
+ embedding_failed_at = NULL,
1378
+ embedding_failure_kind = NULL,
1379
+ embedding_attempts = 0
1380
+ WHERE id = ?`,
1341
1381
  [blob, id]
1342
1382
  );
1343
1383
  }
1384
+ /**
1385
+ * Record a failed embedding attempt. Deliberately does NOT touch updated_at
1386
+ * (import merge is last-write-wins on it) and pushes no outbox event —
1387
+ * embedding lifecycle is local state, not replicated. Same discipline as
1388
+ * updateEmbeddingBlob. See spec §3.5.
1389
+ *
1390
+ * Only marker-eligible kinds (spec §3.3) are accepted: `no_provider` never
1391
+ * marks and `storage_error` never marks, so retry/permanent-failure policy
1392
+ * built on these rows only ever sees real embed failures.
1393
+ */
1394
+ async markEmbeddingFailure(id, kind, now, tx) {
1395
+ const executor = this.getExecutor(tx);
1396
+ await executor.runAsync(
1397
+ `UPDATE ${this.prefix}entries
1398
+ SET embedding_failed_at = ?,
1399
+ embedding_failure_kind = ?,
1400
+ embedding_attempts = embedding_attempts + 1
1401
+ WHERE id = ?`,
1402
+ [now, kind, id]
1403
+ );
1404
+ }
1405
+ /**
1406
+ * Clear marker state for every marked row. Called when the embedding
1407
+ * dimension is promoted: a new model produces different vectors, so past
1408
+ * failures — including `float32_overflow`, which is otherwise terminal —
1409
+ * no longer predict future ones (spec §2.3).
1410
+ *
1411
+ * Same discipline as markEmbeddingFailure: no `updated_at` touch and no
1412
+ * outbox event, because embedding lifecycle is local state, not replicated.
1413
+ * Returns the number of rows cleared.
1414
+ */
1415
+ async clearEmbeddingFailureMarkers(tx) {
1416
+ const executor = this.getExecutor(tx);
1417
+ const result = await executor.runAsync(
1418
+ `UPDATE ${this.prefix}entries
1419
+ SET embedding_failed_at = NULL,
1420
+ embedding_failure_kind = NULL,
1421
+ embedding_attempts = 0
1422
+ WHERE embedding_failed_at IS NOT NULL`
1423
+ );
1424
+ return result.changes;
1425
+ }
1344
1426
  async hasLegacySourceTypes(tx) {
1345
1427
  const executor = this.getExecutor(tx);
1346
1428
  const row = await executor.getFirstAsync(
@@ -4814,6 +4896,23 @@ var HEAL_ANCHOR_SEARCH_OVERFETCH = 4;
4814
4896
  var HEAL_MAX_PROMPT_CHARS = 4e4;
4815
4897
  var HEAL_BATCH_SIZE = 25;
4816
4898
  var HEAL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
4899
+ var MAX_EMBED_ATTEMPTS = 5;
4900
+ var EMBED_RETRY_BASE_MS = 6e4;
4901
+ var EMBED_RETRY_CAP_MS = 24 * 60 * 60 * 1e3;
4902
+ function embedRetryDelayMs(attempts) {
4903
+ const n = Math.max(1, Math.trunc(attempts || 0));
4904
+ const delay = EMBED_RETRY_BASE_MS * Math.pow(2, n - 1);
4905
+ return Math.min(delay, EMBED_RETRY_CAP_MS);
4906
+ }
4907
+ function classifyReembedRow(row, now, force) {
4908
+ if (force) return "attempt";
4909
+ const failedAt = row.embedding_failed_at;
4910
+ if (failedAt === null || failedAt === void 0) return "attempt";
4911
+ if (row.embedding_failure_kind === "float32_overflow") return "permanent";
4912
+ const attempts = row.embedding_attempts ?? 0;
4913
+ if (attempts >= MAX_EMBED_ATTEMPTS) return "permanent";
4914
+ return now - failedAt >= embedRetryDelayMs(attempts) ? "attempt" : "defer";
4915
+ }
4817
4916
  var SKIP_ERROR_LOG_CHARS = 4096;
4818
4917
  var formatSkipError = (err) => {
4819
4918
  let base;
@@ -4952,9 +5051,24 @@ var MaintenanceService = class {
4952
5051
  this.jobManager.releaseLock("ontologyBackfill", entityId);
4953
5052
  }
4954
5053
  }
5054
+ /**
5055
+ * Re-embed facts, honouring per-row failure markers and exponential backoff.
5056
+ *
5057
+ * Marker lifecycle (spec §2): markers are cleared by a successful embed, by
5058
+ * an upsert carrying a valid blob, and by an embedding-dimension promotion.
5059
+ * A promotion revives rows for the NEXT sweep, not this one — reconciliation
5060
+ * runs after candidates were already classified.
5061
+ *
5062
+ * Residual, by design: swapping to a provider with the SAME dimension never
5063
+ * sets embedding_dimension_mismatch, so promotion never fires and markers
5064
+ * survive the swap. Use `runReembed({ force: true })` to clear that state —
5065
+ * `force` bypasses classification entirely and retries every row.
5066
+ */
4955
5067
  async runReembed(entityId, opts) {
4956
5068
  const embedFn = this.options.llmProvider.embed;
4957
- if (!embedFn) return { embedded: 0, skipped: 0, failed: 0 };
5069
+ if (typeof embedFn !== "function") {
5070
+ return { embedded: 0, skipped: 0, failed: 0, deferred: 0, permanentlyFailed: 0 };
5071
+ }
4958
5072
  const op = entityId ? "reembed" : "global_reembed";
4959
5073
  this.jobManager.acquireLock(op, entityId ?? "*");
4960
5074
  try {
@@ -4977,6 +5091,10 @@ var MaintenanceService = class {
4977
5091
  let embedded = 0;
4978
5092
  let skipped = 0;
4979
5093
  let failed = 0;
5094
+ let deferred = 0;
5095
+ let permanentlyFailed = 0;
5096
+ const force = opts?.force ?? false;
5097
+ const now = Date.now();
4980
5098
  try {
4981
5099
  for (const row of rows) {
4982
5100
  const existingBlob = row.embedding_blob;
@@ -4988,8 +5106,17 @@ var MaintenanceService = class {
4988
5106
  continue;
4989
5107
  }
4990
5108
  }
4991
- const success = await this.embeddingService.embedFact(row);
4992
- if (success) embedded++;
5109
+ const disposition = classifyReembedRow(row, now, force);
5110
+ if (disposition === "defer") {
5111
+ deferred++;
5112
+ continue;
5113
+ }
5114
+ if (disposition === "permanent") {
5115
+ permanentlyFailed++;
5116
+ continue;
5117
+ }
5118
+ const result = await this.embeddingService.tryEmbedFact(row);
5119
+ if (result.ok) embedded++;
4993
5120
  else failed++;
4994
5121
  }
4995
5122
  if (embedded > 0) {
@@ -4998,7 +5125,7 @@ var MaintenanceService = class {
4998
5125
  } finally {
4999
5126
  this.searchService.evictCache(entityId);
5000
5127
  }
5001
- return { embedded, skipped, failed };
5128
+ return { embedded, skipped, failed, deferred, permanentlyFailed };
5002
5129
  } finally {
5003
5130
  this.jobManager.releaseLock(op, entityId ?? "*");
5004
5131
  }
@@ -6176,20 +6303,37 @@ var EmbeddingService = class {
6176
6303
  await this.metadataRepo.setMeta("embedding_dimension", String(dim), this.db);
6177
6304
  }
6178
6305
  }
6179
- /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
6306
+ /**
6307
+ * Promotes embedding_dimension_mismatch to canonical embedding_dimension when
6308
+ * safe, and clears embedding failure markers as part of the same event.
6309
+ *
6310
+ * Marker reset (spec §2.3): a promoted dimension means a different model is
6311
+ * producing the vectors, so prior failures no longer predict future ones.
6312
+ * `float32_overflow` is cleared too — it is terminal only because retrying is
6313
+ * the same arithmetic on the same vector, and after a model change it is not.
6314
+ *
6315
+ * All three writes commit atomically; see the transaction note inline.
6316
+ *
6317
+ * Revived rows are NOT embedded here. They become eligible and are picked up
6318
+ * by the NEXT sweep, because runReembed calls this after its candidates were
6319
+ * already classified; same-sweep revival would be re-entrant.
6320
+ */
6180
6321
  async reconcileEmbeddingDimension() {
6181
6322
  const mismatchValue = await this.metadataRepo.getMeta("embedding_dimension_mismatch");
6182
6323
  if (!mismatchValue) return;
6183
6324
  const newDim = parseInt(mismatchValue, 10);
6184
6325
  const residualCount = await this.entryRepo.countStaleEmbeddings(newDim);
6185
6326
  if (residualCount === 0) {
6186
- await this.metadataRepo.setMeta("embedding_dimension", mismatchValue, this.db);
6187
- await this.metadataRepo.clearDimensionMismatch(this.db);
6327
+ await this.db.withTransactionAsync(async (tx) => {
6328
+ await this.metadataRepo.setMeta("embedding_dimension", mismatchValue, tx);
6329
+ await this.metadataRepo.clearDimensionMismatch(tx);
6330
+ await this.entryRepo.clearEmbeddingFailureMarkers(tx);
6331
+ });
6188
6332
  }
6189
6333
  }
6190
- async embedFact(fact) {
6334
+ async tryEmbedFact(fact) {
6191
6335
  const embedFn = this.options.llmProvider.embed;
6192
- if (!embedFn) return false;
6336
+ if (typeof embedFn !== "function") return { ok: false, kind: "no_provider" };
6193
6337
  let tagsStr;
6194
6338
  if (Array.isArray(fact.tags)) {
6195
6339
  tagsStr = fact.tags.join(" ");
@@ -6204,13 +6348,15 @@ var EmbeddingService = class {
6204
6348
  const configuredMaxEmbedChars = this.options.config?.maxEmbedChars;
6205
6349
  const maxEmbedChars = Number.isFinite(configuredMaxEmbedChars) ? Math.min(Math.max(0, Math.trunc(configuredMaxEmbedChars)), EMBED_CHARS_CEILING) : DEFAULT_MAX_EMBED_CHARS;
6206
6350
  const text = clip(`${fact.title} ${fact.body} ${tagsStr}`.trim(), maxEmbedChars);
6351
+ let float32Vector;
6207
6352
  try {
6208
6353
  const vector = await embedFn(text);
6209
6354
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
6210
6355
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
6211
- return false;
6356
+ await this.markFailure(fact.id, "invalid_vector");
6357
+ return { ok: false, kind: "invalid_vector" };
6212
6358
  }
6213
- const float32Vector = new Float32Array(vector);
6359
+ float32Vector = new Float32Array(vector);
6214
6360
  let hasNonFinite = false;
6215
6361
  for (let i = 0; i < float32Vector.length; i++) {
6216
6362
  if (!isFinite(float32Vector[i])) {
@@ -6220,21 +6366,45 @@ var EmbeddingService = class {
6220
6366
  }
6221
6367
  if (hasNonFinite) {
6222
6368
  console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
6223
- return false;
6369
+ await this.markFailure(fact.id, "float32_overflow");
6370
+ return { ok: false, kind: "float32_overflow" };
6224
6371
  }
6372
+ } catch (err) {
6373
+ console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
6374
+ await this.markFailure(fact.id, "provider_error");
6375
+ return { ok: false, kind: "provider_error" };
6376
+ }
6377
+ try {
6225
6378
  await this.storeEmbeddingDimension(float32Vector.length);
6226
6379
  const blob = new Uint8Array(float32Vector.buffer);
6227
6380
  await this.entryRepo.updateEmbeddingBlob(fact.id, blob);
6228
- try {
6229
- await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
6230
- } catch (hookErr) {
6231
- console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
6232
- }
6233
- return true;
6234
6381
  } catch (err) {
6235
- console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
6236
- return false;
6382
+ console.warn(`[WikiMemory] embedFact: persisting embedding failed for ${fact.id}:`, err);
6383
+ return { ok: false, kind: "storage_error" };
6384
+ }
6385
+ try {
6386
+ await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
6387
+ } catch (hookErr) {
6388
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
6237
6389
  }
6390
+ return { ok: true, dimension: float32Vector.length };
6391
+ }
6392
+ /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
6393
+ async markFailure(id, kind) {
6394
+ try {
6395
+ await this.entryRepo.markEmbeddingFailure(id, kind, Date.now());
6396
+ } catch (err) {
6397
+ console.warn(`[WikiMemory] failed to record embedding failure for ${id}:`, err);
6398
+ }
6399
+ }
6400
+ /**
6401
+ * Back-compatible boolean form. Kept because callers such as
6402
+ * ImportExportService branch on `if (!embedded)`; a discriminated result
6403
+ * object is always truthy and would silently disable that branch.
6404
+ */
6405
+ async embedFact(fact) {
6406
+ const result = await this.tryEmbedFact(fact);
6407
+ return result.ok;
6238
6408
  }
6239
6409
  async notifyEmbeddingPersisted(entityId, factId, vector) {
6240
6410
  if (!this.options.vectorRanker?.onEmbeddingPersisted) return;