@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.
@@ -104,6 +104,26 @@ interface OntologyBackfillResult {
104
104
  * Stamped with the recheck cooldown, so they reappear as `deferred` next pass. */
105
105
  skipped: number;
106
106
  }
107
+ /** Result of a single `runReembed` sweep. */
108
+ interface ReembedResult {
109
+ /** Facts successfully (re-)embedded this sweep. */
110
+ embedded: number;
111
+ /** Facts with a valid existing embedding left untouched because
112
+ * `skipExisting` was in effect (and no dimension mismatch forced a re-embed). */
113
+ skipped: number;
114
+ /** Facts whose embed attempt ran and failed this sweep. Includes
115
+ * `storage_error` failures, which are not backoff-tracked (a marker is
116
+ * itself a DB write — see the reembed README section). Convergence loops
117
+ * should test this counter, never `deferred`. */
118
+ failed: number;
119
+ /** Previously failed facts still inside their exponential backoff window
120
+ * (60s doubling, capped at 24h), not attempted this sweep. Not an error;
121
+ * a later sweep retries them once the window elapses. */
122
+ deferred: number;
123
+ /** Facts excluded for good: a `float32_overflow` failure or 5 failed
124
+ * lifetime attempts. `{ force: true }` retries these anyway. */
125
+ permanentlyFailed: number;
126
+ }
107
127
  /**
108
128
  * L3 heal-success record: a fact the model emitted a verdict for under a
109
129
  * truncated view of its own body. `originalBodyChars` lets an operator flag
@@ -743,6 +763,14 @@ declare class PrunePartialFailureError extends Error {
743
763
  constructor(deleted: number, failedAt: string, remaining: number, cause: Error, deletedTasks?: number, deletedEvents?: number);
744
764
  }
745
765
  declare const HOOK_TIMEOUT_MARKER: unique symbol;
766
+ /**
767
+ * Failure kinds eligible for a persisted embedding failure marker (spec §3.3).
768
+ * `no_provider` never marks (nothing to retry against), and `storage_error`
769
+ * never marks (D3 — a marker is itself a DB write). Defined here, neutral
770
+ * between repositories and services, so EntryRepository can type-narrow the
771
+ * marker kind without importing from services.
772
+ */
773
+ type EmbeddingMarkerKind = 'invalid_vector' | 'float32_overflow' | 'provider_error';
746
774
  /**
747
775
  * Failure record for a single chunk that could not be ingested. Produced by
748
776
  * `IngestionService.ingestDocument` and surfaced on the result's `parseFailures`
@@ -890,6 +918,18 @@ type EntryRowWithEmbeddings = EntryRowMetadata & {
890
918
  embedding_blob: Uint8Array | null;
891
919
  embedding: string | null;
892
920
  };
921
+ /**
922
+ * One row of the reembed candidate scan. `findAllForReembed` does `SELECT *`,
923
+ * so the marker columns come back at runtime; naming them here makes that
924
+ * dependency visible to the compiler instead of hiding it behind a cast in
925
+ * MaintenanceService.runReembed (spec §4.4).
926
+ */
927
+ type ReembedCandidateRow = WikiFact & {
928
+ embedding_blob?: Uint8Array | null;
929
+ embedding_failed_at?: number | null;
930
+ embedding_failure_kind?: string | null;
931
+ embedding_attempts?: number | null;
932
+ };
893
933
  declare class EntryRepository extends BaseRepository {
894
934
  private outbox;
895
935
  private chunkSize;
@@ -918,6 +958,13 @@ declare class EntryRepository extends BaseRepository {
918
958
  * New-row INSERTs still bind a value so the column is populated; if the
919
959
  * caller supplied nothing, the schema DEFAULT 'stable' applies when the
920
960
  * bind is bound as 'stable' explicitly here.
961
+ *
962
+ * Marker discipline (spec §4.1): a conflict carrying a real
963
+ * `embedding_blob` clears the embedding failure markers, because the row now
964
+ * HAS a valid embedding and a surviving marker would be a self-contradictory
965
+ * diagnostic that inflates runReembed's permanentlyFailed counter. A conflict
966
+ * with no blob leaves marker state alone, matching the "absent means don't
967
+ * touch" semantics used for embedding_blob itself.
921
968
  */
922
969
  upsert(fact: WikiFact, tx: SQLiteAdapter): Promise<{
923
970
  changes: number;
@@ -1066,11 +1113,31 @@ declare class EntryRepository extends BaseRepository {
1066
1113
  tags: string;
1067
1114
  }>>;
1068
1115
  updateEmbeddingBlob(id: string, blob: Uint8Array, tx?: SQLiteAdapter): Promise<void>;
1116
+ /**
1117
+ * Record a failed embedding attempt. Deliberately does NOT touch updated_at
1118
+ * (import merge is last-write-wins on it) and pushes no outbox event —
1119
+ * embedding lifecycle is local state, not replicated. Same discipline as
1120
+ * updateEmbeddingBlob. See spec §3.5.
1121
+ *
1122
+ * Only marker-eligible kinds (spec §3.3) are accepted: `no_provider` never
1123
+ * marks and `storage_error` never marks, so retry/permanent-failure policy
1124
+ * built on these rows only ever sees real embed failures.
1125
+ */
1126
+ markEmbeddingFailure(id: string, kind: EmbeddingMarkerKind, now: number, tx?: SQLiteAdapter): Promise<void>;
1127
+ /**
1128
+ * Clear marker state for every marked row. Called when the embedding
1129
+ * dimension is promoted: a new model produces different vectors, so past
1130
+ * failures — including `float32_overflow`, which is otherwise terminal —
1131
+ * no longer predict future ones (spec §2.3).
1132
+ *
1133
+ * Same discipline as markEmbeddingFailure: no `updated_at` touch and no
1134
+ * outbox event, because embedding lifecycle is local state, not replicated.
1135
+ * Returns the number of rows cleared.
1136
+ */
1137
+ clearEmbeddingFailureMarkers(tx?: SQLiteAdapter): Promise<number>;
1069
1138
  hasLegacySourceTypes(tx?: SQLiteAdapter): Promise<boolean>;
1070
1139
  countLegacySourceTypes(tx?: SQLiteAdapter): Promise<number>;
1071
- findAllForReembed(entityId?: string, tx?: SQLiteAdapter): Promise<Array<WikiFact & {
1072
- embedding_blob?: Uint8Array | null;
1073
- }>>;
1140
+ findAllForReembed(entityId?: string, tx?: SQLiteAdapter): Promise<ReembedCandidateRow[]>;
1074
1141
  findRowsForSourceRefMigration(tx?: SQLiteAdapter): Promise<Array<{
1075
1142
  rowid: number;
1076
1143
  source_ref: string;
@@ -1540,6 +1607,14 @@ declare class EdgeRepository extends BaseRepository {
1540
1607
  }>;
1541
1608
  }
1542
1609
 
1610
+ type EmbedFailureKind = EmbeddingMarkerKind | 'no_provider' | 'storage_error';
1611
+ type EmbedFactResult = {
1612
+ ok: true;
1613
+ dimension: number;
1614
+ } | {
1615
+ ok: false;
1616
+ kind: EmbedFailureKind;
1617
+ };
1543
1618
  declare class EmbeddingService {
1544
1619
  private db;
1545
1620
  private options;
@@ -1547,8 +1622,36 @@ declare class EmbeddingService {
1547
1622
  private metadataRepo;
1548
1623
  constructor(db: SQLiteAdapter, options: WikiOptions, entryRepo: EntryRepository, metadataRepo: MetadataRepository);
1549
1624
  storeEmbeddingDimension(dim: number): Promise<void>;
1550
- /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
1625
+ /**
1626
+ * Promotes embedding_dimension_mismatch to canonical embedding_dimension when
1627
+ * safe, and clears embedding failure markers as part of the same event.
1628
+ *
1629
+ * Marker reset (spec §2.3): a promoted dimension means a different model is
1630
+ * producing the vectors, so prior failures no longer predict future ones.
1631
+ * `float32_overflow` is cleared too — it is terminal only because retrying is
1632
+ * the same arithmetic on the same vector, and after a model change it is not.
1633
+ *
1634
+ * All three writes commit atomically; see the transaction note inline.
1635
+ *
1636
+ * Revived rows are NOT embedded here. They become eligible and are picked up
1637
+ * by the NEXT sweep, because runReembed calls this after its candidates were
1638
+ * already classified; same-sweep revival would be re-entrant.
1639
+ */
1551
1640
  reconcileEmbeddingDimension(): Promise<void>;
1641
+ tryEmbedFact(fact: {
1642
+ id: string;
1643
+ entity_id: string;
1644
+ title: string;
1645
+ body: string;
1646
+ tags: string | string[];
1647
+ }): Promise<EmbedFactResult>;
1648
+ /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
1649
+ private markFailure;
1650
+ /**
1651
+ * Back-compatible boolean form. Kept because callers such as
1652
+ * ImportExportService branch on `if (!embedded)`; a discriminated result
1653
+ * object is always truthy and would silently disable that branch.
1654
+ */
1552
1655
  embedFact(fact: {
1553
1656
  id: string;
1554
1657
  entity_id: string;
@@ -1911,14 +2014,23 @@ declare class MaintenanceService {
1911
2014
  promptOverride?: string;
1912
2015
  batchSize?: number;
1913
2016
  }): Promise<OntologyBackfillResult>;
2017
+ /**
2018
+ * Re-embed facts, honouring per-row failure markers and exponential backoff.
2019
+ *
2020
+ * Marker lifecycle (spec §2): markers are cleared by a successful embed, by
2021
+ * an upsert carrying a valid blob, and by an embedding-dimension promotion.
2022
+ * A promotion revives rows for the NEXT sweep, not this one — reconciliation
2023
+ * runs after candidates were already classified.
2024
+ *
2025
+ * Residual, by design: swapping to a provider with the SAME dimension never
2026
+ * sets embedding_dimension_mismatch, so promotion never fires and markers
2027
+ * survive the swap. Use `runReembed({ force: true })` to clear that state —
2028
+ * `force` bypasses classification entirely and retries every row.
2029
+ */
1914
2030
  runReembed(entityId?: string, opts?: {
1915
2031
  force?: boolean;
1916
2032
  skipExisting?: boolean;
1917
- }): Promise<{
1918
- embedded: number;
1919
- skipped: number;
1920
- failed: number;
1921
- }>;
2033
+ }): Promise<ReembedResult>;
1922
2034
  forget(entityId: string, params: {
1923
2035
  entryId?: string;
1924
2036
  taskId?: string;
@@ -2275,11 +2387,7 @@ declare class WikiMemory {
2275
2387
  runReembed(entityId?: string, opts?: {
2276
2388
  force?: boolean;
2277
2389
  skipExisting?: boolean;
2278
- }): Promise<{
2279
- embedded: number;
2280
- skipped: number;
2281
- failed: number;
2282
- }>;
2390
+ }): Promise<ReembedResult>;
2283
2391
  getEntityStatus(entityId: string): EntityStatus;
2284
2392
  subscribeEntityStatus(entityId: string, callback: (status: EntityStatus) => void): () => void;
2285
2393
  clearVectorCache(): void;
@@ -2458,4 +2566,4 @@ declare class WikiMemory {
2458
2566
  setGeneratedByTask(taskId: string, entityId: string, actor: string): Promise<void>;
2459
2567
  }
2460
2568
 
2461
- export { WikiParseError as $, WikiBusyError as A, type WikiBusyOperation as B, type ChunkFailure as C, type DegradedRecord as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type IngestDocumentResult as I, type WikiCheckpoint as J, type WikiConfig as K, type LLMProvider as L, type MemoryBundle as M, WikiDuplicateHashError as N, type OntologyManifest as O, type PromptOverrides as P, type WikiEdge as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiEvent as T, type WikiFact as U, type VectorRanker as V, type WikiOptions as W, WikiIngestEmptyError as X, WikiInvalidReadOptions as Y, type WikiMemoryTestAccess as Z, type WikiOutboxEvent as _, type MemoryDump as a, WikiSourceRefHashCollision as a0, WikiStrictOntologyViolation as a1, type WikiTask as a2, WikiTransactionError as a3, EmbeddingService as a4, ImportExportService as a5, IngestionService as a6, JobManager as a7, MaintenanceService as a8, RetrievalService as a9, SearchService as aa, WriteService as ab, type FormattedMemoryDump as b, WikiMemory as c, type ExtractedFact as d, type ExtractedFactEdge as e, type ExtractedFactWithOntology as f, type ExtractedTask as g, type GraphTraversalOptions as h, HEAL_RECHECK_MS as i, HOOK_TIMEOUT_MARKER as j, type HealResult as k, ONTOLOGY_BACKFILL_BATCH_SIZE as l, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as m, ONTOLOGY_BACKFILL_RECHECK_MS as n, type OntologyBackfillResult as o, type OntologyConfig as p, type OntologyEdgeType as q, type OntologyMode as r, type OntologyNodeType as s, type OntologyPromptContext as t, type OntologyUpdates as u, PromptService as v, PrunePartialFailureError as w, type VectorRankerFallback as x, type VectorRankerRankArgs as y, type VectorRankerSemanticResult as z };
2569
+ export { WikiIngestEmptyError as $, type ReembedResult as A, type VectorRankerFallback as B, type ChunkFailure as C, type DegradedRecord as D, type EmbedFactResult as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type IngestDocumentResult as I, type VectorRankerRankArgs as J, type VectorRankerSemanticResult as K, type LLMProvider as L, type MemoryBundle as M, WikiBusyError as N, type OntologyManifest as O, type PromptOverrides as P, type WikiBusyOperation as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiCheckpoint as T, type WikiConfig as U, type VectorRanker as V, type WikiOptions as W, WikiDuplicateHashError as X, type WikiEdge as Y, type WikiEvent as Z, type WikiFact as _, type MemoryDump as a, WikiInvalidReadOptions as a0, type WikiMemoryTestAccess as a1, type WikiOutboxEvent as a2, WikiParseError as a3, WikiSourceRefHashCollision as a4, WikiStrictOntologyViolation as a5, type WikiTask as a6, WikiTransactionError as a7, EmbeddingService as a8, ImportExportService as a9, IngestionService as aa, JobManager as ab, MaintenanceService as ac, RetrievalService as ad, SearchService as ae, WriteService as af, type FormattedMemoryDump as b, WikiMemory as c, type EmbedFailureKind as d, type EmbeddingMarkerKind as e, type EntityStatus as f, type ExtractedFact as g, type ExtractedFactEdge as h, type ExtractedFactWithOntology as i, type ExtractedTask as j, type GraphTraversalOptions as k, HEAL_RECHECK_MS as l, HOOK_TIMEOUT_MARKER as m, type HealResult as n, ONTOLOGY_BACKFILL_BATCH_SIZE as o, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as p, ONTOLOGY_BACKFILL_RECHECK_MS as q, type OntologyBackfillResult as r, type OntologyConfig as s, type OntologyEdgeType as t, type OntologyMode as u, type OntologyNodeType as v, type OntologyPromptContext as w, type OntologyUpdates as x, PromptService as y, PrunePartialFailureError as z };
@@ -104,6 +104,26 @@ interface OntologyBackfillResult {
104
104
  * Stamped with the recheck cooldown, so they reappear as `deferred` next pass. */
105
105
  skipped: number;
106
106
  }
107
+ /** Result of a single `runReembed` sweep. */
108
+ interface ReembedResult {
109
+ /** Facts successfully (re-)embedded this sweep. */
110
+ embedded: number;
111
+ /** Facts with a valid existing embedding left untouched because
112
+ * `skipExisting` was in effect (and no dimension mismatch forced a re-embed). */
113
+ skipped: number;
114
+ /** Facts whose embed attempt ran and failed this sweep. Includes
115
+ * `storage_error` failures, which are not backoff-tracked (a marker is
116
+ * itself a DB write — see the reembed README section). Convergence loops
117
+ * should test this counter, never `deferred`. */
118
+ failed: number;
119
+ /** Previously failed facts still inside their exponential backoff window
120
+ * (60s doubling, capped at 24h), not attempted this sweep. Not an error;
121
+ * a later sweep retries them once the window elapses. */
122
+ deferred: number;
123
+ /** Facts excluded for good: a `float32_overflow` failure or 5 failed
124
+ * lifetime attempts. `{ force: true }` retries these anyway. */
125
+ permanentlyFailed: number;
126
+ }
107
127
  /**
108
128
  * L3 heal-success record: a fact the model emitted a verdict for under a
109
129
  * truncated view of its own body. `originalBodyChars` lets an operator flag
@@ -743,6 +763,14 @@ declare class PrunePartialFailureError extends Error {
743
763
  constructor(deleted: number, failedAt: string, remaining: number, cause: Error, deletedTasks?: number, deletedEvents?: number);
744
764
  }
745
765
  declare const HOOK_TIMEOUT_MARKER: unique symbol;
766
+ /**
767
+ * Failure kinds eligible for a persisted embedding failure marker (spec §3.3).
768
+ * `no_provider` never marks (nothing to retry against), and `storage_error`
769
+ * never marks (D3 — a marker is itself a DB write). Defined here, neutral
770
+ * between repositories and services, so EntryRepository can type-narrow the
771
+ * marker kind without importing from services.
772
+ */
773
+ type EmbeddingMarkerKind = 'invalid_vector' | 'float32_overflow' | 'provider_error';
746
774
  /**
747
775
  * Failure record for a single chunk that could not be ingested. Produced by
748
776
  * `IngestionService.ingestDocument` and surfaced on the result's `parseFailures`
@@ -890,6 +918,18 @@ type EntryRowWithEmbeddings = EntryRowMetadata & {
890
918
  embedding_blob: Uint8Array | null;
891
919
  embedding: string | null;
892
920
  };
921
+ /**
922
+ * One row of the reembed candidate scan. `findAllForReembed` does `SELECT *`,
923
+ * so the marker columns come back at runtime; naming them here makes that
924
+ * dependency visible to the compiler instead of hiding it behind a cast in
925
+ * MaintenanceService.runReembed (spec §4.4).
926
+ */
927
+ type ReembedCandidateRow = WikiFact & {
928
+ embedding_blob?: Uint8Array | null;
929
+ embedding_failed_at?: number | null;
930
+ embedding_failure_kind?: string | null;
931
+ embedding_attempts?: number | null;
932
+ };
893
933
  declare class EntryRepository extends BaseRepository {
894
934
  private outbox;
895
935
  private chunkSize;
@@ -918,6 +958,13 @@ declare class EntryRepository extends BaseRepository {
918
958
  * New-row INSERTs still bind a value so the column is populated; if the
919
959
  * caller supplied nothing, the schema DEFAULT 'stable' applies when the
920
960
  * bind is bound as 'stable' explicitly here.
961
+ *
962
+ * Marker discipline (spec §4.1): a conflict carrying a real
963
+ * `embedding_blob` clears the embedding failure markers, because the row now
964
+ * HAS a valid embedding and a surviving marker would be a self-contradictory
965
+ * diagnostic that inflates runReembed's permanentlyFailed counter. A conflict
966
+ * with no blob leaves marker state alone, matching the "absent means don't
967
+ * touch" semantics used for embedding_blob itself.
921
968
  */
922
969
  upsert(fact: WikiFact, tx: SQLiteAdapter): Promise<{
923
970
  changes: number;
@@ -1066,11 +1113,31 @@ declare class EntryRepository extends BaseRepository {
1066
1113
  tags: string;
1067
1114
  }>>;
1068
1115
  updateEmbeddingBlob(id: string, blob: Uint8Array, tx?: SQLiteAdapter): Promise<void>;
1116
+ /**
1117
+ * Record a failed embedding attempt. Deliberately does NOT touch updated_at
1118
+ * (import merge is last-write-wins on it) and pushes no outbox event —
1119
+ * embedding lifecycle is local state, not replicated. Same discipline as
1120
+ * updateEmbeddingBlob. See spec §3.5.
1121
+ *
1122
+ * Only marker-eligible kinds (spec §3.3) are accepted: `no_provider` never
1123
+ * marks and `storage_error` never marks, so retry/permanent-failure policy
1124
+ * built on these rows only ever sees real embed failures.
1125
+ */
1126
+ markEmbeddingFailure(id: string, kind: EmbeddingMarkerKind, now: number, tx?: SQLiteAdapter): Promise<void>;
1127
+ /**
1128
+ * Clear marker state for every marked row. Called when the embedding
1129
+ * dimension is promoted: a new model produces different vectors, so past
1130
+ * failures — including `float32_overflow`, which is otherwise terminal —
1131
+ * no longer predict future ones (spec §2.3).
1132
+ *
1133
+ * Same discipline as markEmbeddingFailure: no `updated_at` touch and no
1134
+ * outbox event, because embedding lifecycle is local state, not replicated.
1135
+ * Returns the number of rows cleared.
1136
+ */
1137
+ clearEmbeddingFailureMarkers(tx?: SQLiteAdapter): Promise<number>;
1069
1138
  hasLegacySourceTypes(tx?: SQLiteAdapter): Promise<boolean>;
1070
1139
  countLegacySourceTypes(tx?: SQLiteAdapter): Promise<number>;
1071
- findAllForReembed(entityId?: string, tx?: SQLiteAdapter): Promise<Array<WikiFact & {
1072
- embedding_blob?: Uint8Array | null;
1073
- }>>;
1140
+ findAllForReembed(entityId?: string, tx?: SQLiteAdapter): Promise<ReembedCandidateRow[]>;
1074
1141
  findRowsForSourceRefMigration(tx?: SQLiteAdapter): Promise<Array<{
1075
1142
  rowid: number;
1076
1143
  source_ref: string;
@@ -1540,6 +1607,14 @@ declare class EdgeRepository extends BaseRepository {
1540
1607
  }>;
1541
1608
  }
1542
1609
 
1610
+ type EmbedFailureKind = EmbeddingMarkerKind | 'no_provider' | 'storage_error';
1611
+ type EmbedFactResult = {
1612
+ ok: true;
1613
+ dimension: number;
1614
+ } | {
1615
+ ok: false;
1616
+ kind: EmbedFailureKind;
1617
+ };
1543
1618
  declare class EmbeddingService {
1544
1619
  private db;
1545
1620
  private options;
@@ -1547,8 +1622,36 @@ declare class EmbeddingService {
1547
1622
  private metadataRepo;
1548
1623
  constructor(db: SQLiteAdapter, options: WikiOptions, entryRepo: EntryRepository, metadataRepo: MetadataRepository);
1549
1624
  storeEmbeddingDimension(dim: number): Promise<void>;
1550
- /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
1625
+ /**
1626
+ * Promotes embedding_dimension_mismatch to canonical embedding_dimension when
1627
+ * safe, and clears embedding failure markers as part of the same event.
1628
+ *
1629
+ * Marker reset (spec §2.3): a promoted dimension means a different model is
1630
+ * producing the vectors, so prior failures no longer predict future ones.
1631
+ * `float32_overflow` is cleared too — it is terminal only because retrying is
1632
+ * the same arithmetic on the same vector, and after a model change it is not.
1633
+ *
1634
+ * All three writes commit atomically; see the transaction note inline.
1635
+ *
1636
+ * Revived rows are NOT embedded here. They become eligible and are picked up
1637
+ * by the NEXT sweep, because runReembed calls this after its candidates were
1638
+ * already classified; same-sweep revival would be re-entrant.
1639
+ */
1551
1640
  reconcileEmbeddingDimension(): Promise<void>;
1641
+ tryEmbedFact(fact: {
1642
+ id: string;
1643
+ entity_id: string;
1644
+ title: string;
1645
+ body: string;
1646
+ tags: string | string[];
1647
+ }): Promise<EmbedFactResult>;
1648
+ /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
1649
+ private markFailure;
1650
+ /**
1651
+ * Back-compatible boolean form. Kept because callers such as
1652
+ * ImportExportService branch on `if (!embedded)`; a discriminated result
1653
+ * object is always truthy and would silently disable that branch.
1654
+ */
1552
1655
  embedFact(fact: {
1553
1656
  id: string;
1554
1657
  entity_id: string;
@@ -1911,14 +2014,23 @@ declare class MaintenanceService {
1911
2014
  promptOverride?: string;
1912
2015
  batchSize?: number;
1913
2016
  }): Promise<OntologyBackfillResult>;
2017
+ /**
2018
+ * Re-embed facts, honouring per-row failure markers and exponential backoff.
2019
+ *
2020
+ * Marker lifecycle (spec §2): markers are cleared by a successful embed, by
2021
+ * an upsert carrying a valid blob, and by an embedding-dimension promotion.
2022
+ * A promotion revives rows for the NEXT sweep, not this one — reconciliation
2023
+ * runs after candidates were already classified.
2024
+ *
2025
+ * Residual, by design: swapping to a provider with the SAME dimension never
2026
+ * sets embedding_dimension_mismatch, so promotion never fires and markers
2027
+ * survive the swap. Use `runReembed({ force: true })` to clear that state —
2028
+ * `force` bypasses classification entirely and retries every row.
2029
+ */
1914
2030
  runReembed(entityId?: string, opts?: {
1915
2031
  force?: boolean;
1916
2032
  skipExisting?: boolean;
1917
- }): Promise<{
1918
- embedded: number;
1919
- skipped: number;
1920
- failed: number;
1921
- }>;
2033
+ }): Promise<ReembedResult>;
1922
2034
  forget(entityId: string, params: {
1923
2035
  entryId?: string;
1924
2036
  taskId?: string;
@@ -2275,11 +2387,7 @@ declare class WikiMemory {
2275
2387
  runReembed(entityId?: string, opts?: {
2276
2388
  force?: boolean;
2277
2389
  skipExisting?: boolean;
2278
- }): Promise<{
2279
- embedded: number;
2280
- skipped: number;
2281
- failed: number;
2282
- }>;
2390
+ }): Promise<ReembedResult>;
2283
2391
  getEntityStatus(entityId: string): EntityStatus;
2284
2392
  subscribeEntityStatus(entityId: string, callback: (status: EntityStatus) => void): () => void;
2285
2393
  clearVectorCache(): void;
@@ -2458,4 +2566,4 @@ declare class WikiMemory {
2458
2566
  setGeneratedByTask(taskId: string, entityId: string, actor: string): Promise<void>;
2459
2567
  }
2460
2568
 
2461
- export { WikiParseError as $, WikiBusyError as A, type WikiBusyOperation as B, type ChunkFailure as C, type DegradedRecord as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type IngestDocumentResult as I, type WikiCheckpoint as J, type WikiConfig as K, type LLMProvider as L, type MemoryBundle as M, WikiDuplicateHashError as N, type OntologyManifest as O, type PromptOverrides as P, type WikiEdge as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiEvent as T, type WikiFact as U, type VectorRanker as V, type WikiOptions as W, WikiIngestEmptyError as X, WikiInvalidReadOptions as Y, type WikiMemoryTestAccess as Z, type WikiOutboxEvent as _, type MemoryDump as a, WikiSourceRefHashCollision as a0, WikiStrictOntologyViolation as a1, type WikiTask as a2, WikiTransactionError as a3, EmbeddingService as a4, ImportExportService as a5, IngestionService as a6, JobManager as a7, MaintenanceService as a8, RetrievalService as a9, SearchService as aa, WriteService as ab, type FormattedMemoryDump as b, WikiMemory as c, type ExtractedFact as d, type ExtractedFactEdge as e, type ExtractedFactWithOntology as f, type ExtractedTask as g, type GraphTraversalOptions as h, HEAL_RECHECK_MS as i, HOOK_TIMEOUT_MARKER as j, type HealResult as k, ONTOLOGY_BACKFILL_BATCH_SIZE as l, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as m, ONTOLOGY_BACKFILL_RECHECK_MS as n, type OntologyBackfillResult as o, type OntologyConfig as p, type OntologyEdgeType as q, type OntologyMode as r, type OntologyNodeType as s, type OntologyPromptContext as t, type OntologyUpdates as u, PromptService as v, PrunePartialFailureError as w, type VectorRankerFallback as x, type VectorRankerRankArgs as y, type VectorRankerSemanticResult as z };
2569
+ export { WikiIngestEmptyError as $, type ReembedResult as A, type VectorRankerFallback as B, type ChunkFailure as C, type DegradedRecord as D, type EmbedFactResult as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type IngestDocumentResult as I, type VectorRankerRankArgs as J, type VectorRankerSemanticResult as K, type LLMProvider as L, type MemoryBundle as M, WikiBusyError as N, type OntologyManifest as O, type PromptOverrides as P, type WikiBusyOperation as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiCheckpoint as T, type WikiConfig as U, type VectorRanker as V, type WikiOptions as W, WikiDuplicateHashError as X, type WikiEdge as Y, type WikiEvent as Z, type WikiFact as _, type MemoryDump as a, WikiInvalidReadOptions as a0, type WikiMemoryTestAccess as a1, type WikiOutboxEvent as a2, WikiParseError as a3, WikiSourceRefHashCollision as a4, WikiStrictOntologyViolation as a5, type WikiTask as a6, WikiTransactionError as a7, EmbeddingService as a8, ImportExportService as a9, IngestionService as aa, JobManager as ab, MaintenanceService as ac, RetrievalService as ad, SearchService as ae, WriteService as af, type FormattedMemoryDump as b, WikiMemory as c, type EmbedFailureKind as d, type EmbeddingMarkerKind as e, type EntityStatus as f, type ExtractedFact as g, type ExtractedFactEdge as h, type ExtractedFactWithOntology as i, type ExtractedTask as j, type GraphTraversalOptions as k, HEAL_RECHECK_MS as l, HOOK_TIMEOUT_MARKER as m, type HealResult as n, ONTOLOGY_BACKFILL_BATCH_SIZE as o, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as p, ONTOLOGY_BACKFILL_RECHECK_MS as q, type OntologyBackfillResult as r, type OntologyConfig as s, type OntologyEdgeType as t, type OntologyMode as u, type OntologyNodeType as v, type OntologyPromptContext as w, type OntologyUpdates as x, PromptService as y, PrunePartialFailureError as z };
@@ -1,3 +1,3 @@
1
- export { a4 as EmbeddingService, a5 as ImportExportService, a6 as IngestionService, a7 as JobManager, a7 as JobManagerType, a8 as MaintenanceService, a9 as RetrievalService, aa as SearchService, aa as SearchServiceType, Z as WikiMemoryTestAccess, ab as WriteService } from './testing-DWpAgNLt.mjs';
1
+ export { a8 as EmbeddingService, a9 as ImportExportService, aa as IngestionService, ab as JobManager, ab as JobManagerType, ac as MaintenanceService, ad as RetrievalService, ae as SearchService, ae as SearchServiceType, a1 as WikiMemoryTestAccess, af as WriteService } from './testing-BtsN1pTK.mjs';
2
2
  import '@equationalapplications/core-okf';
3
3
  import 'minisearch';
package/dist/testing.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { a4 as EmbeddingService, a5 as ImportExportService, a6 as IngestionService, a7 as JobManager, a7 as JobManagerType, a8 as MaintenanceService, a9 as RetrievalService, aa as SearchService, aa as SearchServiceType, Z as WikiMemoryTestAccess, ab as WriteService } from './testing-DWpAgNLt.js';
1
+ export { a8 as EmbeddingService, a9 as ImportExportService, aa as IngestionService, ab as JobManager, ab as JobManagerType, ac as MaintenanceService, ad as RetrievalService, ae as SearchService, ae as SearchServiceType, a1 as WikiMemoryTestAccess, af as WriteService } from './testing-BtsN1pTK.js';
2
2
  import '@equationalapplications/core-okf';
3
3
  import 'minisearch';
package/dist/testing.js CHANGED
@@ -651,20 +651,37 @@ var EmbeddingService = class {
651
651
  await this.metadataRepo.setMeta("embedding_dimension", String(dim), this.db);
652
652
  }
653
653
  }
654
- /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
654
+ /**
655
+ * Promotes embedding_dimension_mismatch to canonical embedding_dimension when
656
+ * safe, and clears embedding failure markers as part of the same event.
657
+ *
658
+ * Marker reset (spec §2.3): a promoted dimension means a different model is
659
+ * producing the vectors, so prior failures no longer predict future ones.
660
+ * `float32_overflow` is cleared too — it is terminal only because retrying is
661
+ * the same arithmetic on the same vector, and after a model change it is not.
662
+ *
663
+ * All three writes commit atomically; see the transaction note inline.
664
+ *
665
+ * Revived rows are NOT embedded here. They become eligible and are picked up
666
+ * by the NEXT sweep, because runReembed calls this after its candidates were
667
+ * already classified; same-sweep revival would be re-entrant.
668
+ */
655
669
  async reconcileEmbeddingDimension() {
656
670
  const mismatchValue = await this.metadataRepo.getMeta("embedding_dimension_mismatch");
657
671
  if (!mismatchValue) return;
658
672
  const newDim = parseInt(mismatchValue, 10);
659
673
  const residualCount = await this.entryRepo.countStaleEmbeddings(newDim);
660
674
  if (residualCount === 0) {
661
- await this.metadataRepo.setMeta("embedding_dimension", mismatchValue, this.db);
662
- await this.metadataRepo.clearDimensionMismatch(this.db);
675
+ await this.db.withTransactionAsync(async (tx) => {
676
+ await this.metadataRepo.setMeta("embedding_dimension", mismatchValue, tx);
677
+ await this.metadataRepo.clearDimensionMismatch(tx);
678
+ await this.entryRepo.clearEmbeddingFailureMarkers(tx);
679
+ });
663
680
  }
664
681
  }
665
- async embedFact(fact) {
682
+ async tryEmbedFact(fact) {
666
683
  const embedFn = this.options.llmProvider.embed;
667
- if (!embedFn) return false;
684
+ if (typeof embedFn !== "function") return { ok: false, kind: "no_provider" };
668
685
  let tagsStr;
669
686
  if (Array.isArray(fact.tags)) {
670
687
  tagsStr = fact.tags.join(" ");
@@ -679,13 +696,15 @@ var EmbeddingService = class {
679
696
  const configuredMaxEmbedChars = this.options.config?.maxEmbedChars;
680
697
  const maxEmbedChars = Number.isFinite(configuredMaxEmbedChars) ? Math.min(Math.max(0, Math.trunc(configuredMaxEmbedChars)), EMBED_CHARS_CEILING) : DEFAULT_MAX_EMBED_CHARS;
681
698
  const text = clip(`${fact.title} ${fact.body} ${tagsStr}`.trim(), maxEmbedChars);
699
+ let float32Vector;
682
700
  try {
683
701
  const vector = await embedFn(text);
684
702
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
685
703
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
686
- return false;
704
+ await this.markFailure(fact.id, "invalid_vector");
705
+ return { ok: false, kind: "invalid_vector" };
687
706
  }
688
- const float32Vector = new Float32Array(vector);
707
+ float32Vector = new Float32Array(vector);
689
708
  let hasNonFinite = false;
690
709
  for (let i = 0; i < float32Vector.length; i++) {
691
710
  if (!isFinite(float32Vector[i])) {
@@ -695,22 +714,46 @@ var EmbeddingService = class {
695
714
  }
696
715
  if (hasNonFinite) {
697
716
  console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
698
- return false;
717
+ await this.markFailure(fact.id, "float32_overflow");
718
+ return { ok: false, kind: "float32_overflow" };
699
719
  }
720
+ } catch (err) {
721
+ console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
722
+ await this.markFailure(fact.id, "provider_error");
723
+ return { ok: false, kind: "provider_error" };
724
+ }
725
+ try {
700
726
  await this.storeEmbeddingDimension(float32Vector.length);
701
727
  const blob = new Uint8Array(float32Vector.buffer);
702
728
  await this.entryRepo.updateEmbeddingBlob(fact.id, blob);
703
- try {
704
- await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
705
- } catch (hookErr) {
706
- console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
707
- }
708
- return true;
709
729
  } catch (err) {
710
- console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
711
- return false;
730
+ console.warn(`[WikiMemory] embedFact: persisting embedding failed for ${fact.id}:`, err);
731
+ return { ok: false, kind: "storage_error" };
732
+ }
733
+ try {
734
+ await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
735
+ } catch (hookErr) {
736
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
737
+ }
738
+ return { ok: true, dimension: float32Vector.length };
739
+ }
740
+ /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
741
+ async markFailure(id, kind) {
742
+ try {
743
+ await this.entryRepo.markEmbeddingFailure(id, kind, Date.now());
744
+ } catch (err) {
745
+ console.warn(`[WikiMemory] failed to record embedding failure for ${id}:`, err);
712
746
  }
713
747
  }
748
+ /**
749
+ * Back-compatible boolean form. Kept because callers such as
750
+ * ImportExportService branch on `if (!embedded)`; a discriminated result
751
+ * object is always truthy and would silently disable that branch.
752
+ */
753
+ async embedFact(fact) {
754
+ const result = await this.tryEmbedFact(fact);
755
+ return result.ok;
756
+ }
714
757
  async notifyEmbeddingPersisted(entityId, factId, vector) {
715
758
  if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
716
759
  const vectorCopy = vector ? vector.slice() : null;
@@ -2109,6 +2152,23 @@ var HEAL_ANCHOR_SEARCH_OVERFETCH = 4;
2109
2152
  var HEAL_MAX_PROMPT_CHARS = 4e4;
2110
2153
  var HEAL_BATCH_SIZE = 25;
2111
2154
  var HEAL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
2155
+ var MAX_EMBED_ATTEMPTS = 5;
2156
+ var EMBED_RETRY_BASE_MS = 6e4;
2157
+ var EMBED_RETRY_CAP_MS = 24 * 60 * 60 * 1e3;
2158
+ function embedRetryDelayMs(attempts) {
2159
+ const n = Math.max(1, Math.trunc(attempts || 0));
2160
+ const delay = EMBED_RETRY_BASE_MS * Math.pow(2, n - 1);
2161
+ return Math.min(delay, EMBED_RETRY_CAP_MS);
2162
+ }
2163
+ function classifyReembedRow(row, now, force) {
2164
+ if (force) return "attempt";
2165
+ const failedAt = row.embedding_failed_at;
2166
+ if (failedAt === null || failedAt === void 0) return "attempt";
2167
+ if (row.embedding_failure_kind === "float32_overflow") return "permanent";
2168
+ const attempts = row.embedding_attempts ?? 0;
2169
+ if (attempts >= MAX_EMBED_ATTEMPTS) return "permanent";
2170
+ return now - failedAt >= embedRetryDelayMs(attempts) ? "attempt" : "defer";
2171
+ }
2112
2172
  var SKIP_ERROR_LOG_CHARS = 4096;
2113
2173
  var formatSkipError = (err) => {
2114
2174
  let base;
@@ -2247,9 +2307,24 @@ var MaintenanceService = class {
2247
2307
  this.jobManager.releaseLock("ontologyBackfill", entityId);
2248
2308
  }
2249
2309
  }
2310
+ /**
2311
+ * Re-embed facts, honouring per-row failure markers and exponential backoff.
2312
+ *
2313
+ * Marker lifecycle (spec §2): markers are cleared by a successful embed, by
2314
+ * an upsert carrying a valid blob, and by an embedding-dimension promotion.
2315
+ * A promotion revives rows for the NEXT sweep, not this one — reconciliation
2316
+ * runs after candidates were already classified.
2317
+ *
2318
+ * Residual, by design: swapping to a provider with the SAME dimension never
2319
+ * sets embedding_dimension_mismatch, so promotion never fires and markers
2320
+ * survive the swap. Use `runReembed({ force: true })` to clear that state —
2321
+ * `force` bypasses classification entirely and retries every row.
2322
+ */
2250
2323
  async runReembed(entityId, opts) {
2251
2324
  const embedFn = this.options.llmProvider.embed;
2252
- if (!embedFn) return { embedded: 0, skipped: 0, failed: 0 };
2325
+ if (typeof embedFn !== "function") {
2326
+ return { embedded: 0, skipped: 0, failed: 0, deferred: 0, permanentlyFailed: 0 };
2327
+ }
2253
2328
  const op = entityId ? "reembed" : "global_reembed";
2254
2329
  this.jobManager.acquireLock(op, entityId ?? "*");
2255
2330
  try {
@@ -2272,6 +2347,10 @@ var MaintenanceService = class {
2272
2347
  let embedded = 0;
2273
2348
  let skipped = 0;
2274
2349
  let failed = 0;
2350
+ let deferred = 0;
2351
+ let permanentlyFailed = 0;
2352
+ const force = opts?.force ?? false;
2353
+ const now = Date.now();
2275
2354
  try {
2276
2355
  for (const row of rows) {
2277
2356
  const existingBlob = row.embedding_blob;
@@ -2283,8 +2362,17 @@ var MaintenanceService = class {
2283
2362
  continue;
2284
2363
  }
2285
2364
  }
2286
- const success = await this.embeddingService.embedFact(row);
2287
- if (success) embedded++;
2365
+ const disposition = classifyReembedRow(row, now, force);
2366
+ if (disposition === "defer") {
2367
+ deferred++;
2368
+ continue;
2369
+ }
2370
+ if (disposition === "permanent") {
2371
+ permanentlyFailed++;
2372
+ continue;
2373
+ }
2374
+ const result = await this.embeddingService.tryEmbedFact(row);
2375
+ if (result.ok) embedded++;
2288
2376
  else failed++;
2289
2377
  }
2290
2378
  if (embedded > 0) {
@@ -2293,7 +2381,7 @@ var MaintenanceService = class {
2293
2381
  } finally {
2294
2382
  this.searchService.evictCache(entityId);
2295
2383
  }
2296
- return { embedded, skipped, failed };
2384
+ return { embedded, skipped, failed, deferred, permanentlyFailed };
2297
2385
  } finally {
2298
2386
  this.jobManager.releaseLock(op, entityId ?? "*");
2299
2387
  }