@equationalapplications/core-llm-wiki 5.5.1 → 6.0.0

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.
@@ -101,6 +101,20 @@ interface OntologyBackfillResult {
101
101
  * Stamped with the recheck cooldown, so they reappear as `deferred` next pass. */
102
102
  skipped: number;
103
103
  }
104
+ /**
105
+ * L3 heal-success record: a fact the model emitted a verdict for under a
106
+ * truncated view of its own body. `originalBodyChars` lets an operator flag
107
+ * the fact for re-inspection; `truncatedBodyChars` is the L3 cap actually
108
+ * applied. Emitted by `PromptService.buildHealPrompt`, surfaced via
109
+ * `HealResult.degraded`, mutually exclusive with `HealResult.skipped` on
110
+ * `id` (a fact can be either healed-under-degradation or dropped — never
111
+ * both).
112
+ */
113
+ interface DegradedRecord {
114
+ id: string;
115
+ originalBodyChars: number;
116
+ truncatedBodyChars: number;
117
+ }
104
118
  /** Result of a single heal run. */
105
119
  interface HealResult {
106
120
  /**
@@ -118,8 +132,32 @@ interface HealResult {
118
132
  deleted: number;
119
133
  /** New facts synthesized by heal. */
120
134
  newFactsCreated: number;
121
- /** Candidates a batch could not process even alone, skipped so the pass could finish. */
122
- skipped: number;
135
+ /**
136
+ * Candidates that could not converge after the helper's full escalation
137
+ * path. `reason` distinguishes genuine non-convergence
138
+ * (`'non_convergent'` — terminal give-up at attemptLevel 3, parse error,
139
+ * or model-config error) from transient provider failures
140
+ * (`'call_error'` — non-truncation error originating in `call()` at
141
+ * `batch.length === 1`). `doRunHeal` excludes `'call_error'` ids from
142
+ * `markHealChecked`'s cooldown stamp so a momentary provider hiccup does
143
+ * not lock the fact out for `HEAL_RECHECK_MS`. Mutually exclusive with
144
+ * `degraded` on `id`.
145
+ */
146
+ skipped: Array<{
147
+ id: string;
148
+ reason: 'non_convergent' | 'call_error';
149
+ }>;
150
+ /**
151
+ * L3 heal successes: facts the model emitted a verdict for under a
152
+ * truncated view of their own body. Each entry carries the truncation
153
+ * magnitude so an operator can flag the fact for re-inspection. The
154
+ * corresponding log line `[WikiMemory] heal healed under degraded
155
+ * context ...` fires for each entry. Mutually exclusive with `skipped`
156
+ * on `id` — the reconciliation step in doRunHeal drops any record
157
+ * whose id also appears in `skipped` (a contradiction: degraded means
158
+ * healed, skipped means dropped).
159
+ */
160
+ degraded: Array<DegradedRecord>;
123
161
  /**
124
162
  * Heal candidates still eligible after this run — convergence signal: loop while > 0.
125
163
  *
@@ -1530,9 +1568,34 @@ declare class PromptService {
1530
1568
  systemPrompt: string;
1531
1569
  userPrompt: string;
1532
1570
  };
1533
- buildHealPrompt(healCandidates: unknown[], documentAnchors: unknown[], allTasks: unknown[], recentEvents: unknown[], runtimeOverride?: string): {
1534
- systemPrompt: string;
1535
- userPrompt: string;
1571
+ /**
1572
+ * Heal-prompt level interpretation for the `attemptLevel` ladder.
1573
+ *
1574
+ * Caller contract: `documentAnchors` may be a slice sized for `batch.length`
1575
+ * or a larger set (e.g. a cache hit from `_selectHealAnchors`). This function
1576
+ * applies the prompt-side anchor cap `min(HEAL_MAX_ANCHORS=50, batch.length
1577
+ * * HEAL_ANCHORS_PER_CANDIDATE=4)` so the rendered prompt is bounded
1578
+ * regardless of caller input. `HEAL_MAX_ANCHORS` and
1579
+ * `HEAL_ANCHORS_PER_CANDIDATE` live here too — keeping the formula
1580
+ * co-located with its application avoids a "MaintenanceService policy"
1581
+ * import cycle (`PromptService` is constructed before `MaintenanceService`
1582
+ * exists) and makes the cap testable without a `MaintenanceService`
1583
+ * instance. Task 3 exports the same two constants from `MaintenanceService`
1584
+ * for caller-side overfetch sizing; the values must match.
1585
+ *
1586
+ * Level semantics:
1587
+ * - L0: allTasks + recentEvents + full candidate bodies; anchors re-capped
1588
+ * - L1: drop allTasks; recentEvents present; candidate bodies full
1589
+ * - L2: drop allTasks and recentEvents; candidate bodies full
1590
+ * - L3: drop allTasks and recentEvents; truncate each candidate body to
1591
+ * `bodyTruncationChars` and emit a `degraded` record per truncated fact
1592
+ */
1593
+ buildHealPrompt(healCandidates: unknown[], documentAnchors: unknown[], allTasks: unknown[], recentEvents: unknown[], runtimeOverride: string | undefined, attemptLevel: 0 | 1 | 2 | 3, bodyTruncationChars?: number): {
1594
+ prompts: {
1595
+ systemPrompt: string;
1596
+ userPrompt: string;
1597
+ };
1598
+ degraded: DegradedRecord[];
1536
1599
  };
1537
1600
  buildOntologyBackfillPrompt(facts: unknown[], runtimeOverride?: string, ontologyContext?: OntologyPromptContext | null): {
1538
1601
  systemPrompt: string;
@@ -1748,6 +1811,7 @@ declare class EventRepository extends BaseRepository {
1748
1811
  declare const ONTOLOGY_BACKFILL_BATCH_SIZE = 25;
1749
1812
  declare const ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS = 40000;
1750
1813
  declare const ONTOLOGY_BACKFILL_RECHECK_MS: number;
1814
+
1751
1815
  /**
1752
1816
  * Heal candidates fetched per pass. Bounds the pool runBatched draws from, not
1753
1817
  * the size of an individual LLM call — runBatched still packs ~10 candidates per
@@ -1787,6 +1851,7 @@ declare class MaintenanceService {
1787
1851
  runHeal(entityId: string, options?: {
1788
1852
  promptOverride?: string;
1789
1853
  batchSize?: number;
1854
+ bodyTruncationChars?: number;
1790
1855
  }): Promise<HealResult>;
1791
1856
  runOntologyBackfill(entityId: string, options?: {
1792
1857
  promptOverride?: string;
@@ -1840,6 +1905,7 @@ declare class MaintenanceService {
1840
1905
  doRunHeal(entityId: string, options?: {
1841
1906
  promptOverride?: string;
1842
1907
  batchSize?: number;
1908
+ bodyTruncationChars?: number;
1843
1909
  }): Promise<HealResult>;
1844
1910
  /** Core ontology backfill pass (locks handled by {@link runOntologyBackfill}). Package-internal orchestration hook. */
1845
1911
  doRunOntologyBackfill(entityId: string, options?: {
@@ -2304,4 +2370,4 @@ declare class WikiMemory {
2304
2370
  setGeneratedByTask(taskId: string, entityId: string, actor: string): Promise<void>;
2305
2371
  }
2306
2372
 
2307
- export { WikiStrictOntologyViolation as $, WikiBusyError as A, type WikiBusyOperation as B, type ChunkFailure as C, type WikiCheckpoint as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type IngestDocumentResult as I, type WikiConfig as J, WikiDuplicateHashError as K, type LLMProvider as L, type MemoryBundle as M, type WikiEdge as N, type OntologyManifest as O, type PromptOverrides as P, type WikiEvent as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiFact as T, WikiIngestEmptyError as U, type VectorRanker as V, type WikiOptions as W, type WikiMemoryTestAccess as X, type WikiOutboxEvent as Y, WikiParseError as Z, WikiSourceRefHashCollision as _, type MemoryDump as a, type WikiTask as a0, WikiTransactionError as a1, EmbeddingService as a2, ImportExportService as a3, IngestionService as a4, JobManager as a5, MaintenanceService as a6, RetrievalService as a7, SearchService as a8, WriteService as a9, 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 };
2373
+ export { WikiSourceRefHashCollision 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, type WikiMemoryTestAccess as Y, type WikiOutboxEvent as Z, WikiParseError as _, type MemoryDump as a, WikiStrictOntologyViolation as a0, type WikiTask as a1, WikiTransactionError as a2, EmbeddingService as a3, ImportExportService as a4, IngestionService as a5, JobManager as a6, MaintenanceService as a7, RetrievalService as a8, SearchService as a9, WriteService as aa, 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 };
@@ -101,6 +101,20 @@ interface OntologyBackfillResult {
101
101
  * Stamped with the recheck cooldown, so they reappear as `deferred` next pass. */
102
102
  skipped: number;
103
103
  }
104
+ /**
105
+ * L3 heal-success record: a fact the model emitted a verdict for under a
106
+ * truncated view of its own body. `originalBodyChars` lets an operator flag
107
+ * the fact for re-inspection; `truncatedBodyChars` is the L3 cap actually
108
+ * applied. Emitted by `PromptService.buildHealPrompt`, surfaced via
109
+ * `HealResult.degraded`, mutually exclusive with `HealResult.skipped` on
110
+ * `id` (a fact can be either healed-under-degradation or dropped — never
111
+ * both).
112
+ */
113
+ interface DegradedRecord {
114
+ id: string;
115
+ originalBodyChars: number;
116
+ truncatedBodyChars: number;
117
+ }
104
118
  /** Result of a single heal run. */
105
119
  interface HealResult {
106
120
  /**
@@ -118,8 +132,32 @@ interface HealResult {
118
132
  deleted: number;
119
133
  /** New facts synthesized by heal. */
120
134
  newFactsCreated: number;
121
- /** Candidates a batch could not process even alone, skipped so the pass could finish. */
122
- skipped: number;
135
+ /**
136
+ * Candidates that could not converge after the helper's full escalation
137
+ * path. `reason` distinguishes genuine non-convergence
138
+ * (`'non_convergent'` — terminal give-up at attemptLevel 3, parse error,
139
+ * or model-config error) from transient provider failures
140
+ * (`'call_error'` — non-truncation error originating in `call()` at
141
+ * `batch.length === 1`). `doRunHeal` excludes `'call_error'` ids from
142
+ * `markHealChecked`'s cooldown stamp so a momentary provider hiccup does
143
+ * not lock the fact out for `HEAL_RECHECK_MS`. Mutually exclusive with
144
+ * `degraded` on `id`.
145
+ */
146
+ skipped: Array<{
147
+ id: string;
148
+ reason: 'non_convergent' | 'call_error';
149
+ }>;
150
+ /**
151
+ * L3 heal successes: facts the model emitted a verdict for under a
152
+ * truncated view of their own body. Each entry carries the truncation
153
+ * magnitude so an operator can flag the fact for re-inspection. The
154
+ * corresponding log line `[WikiMemory] heal healed under degraded
155
+ * context ...` fires for each entry. Mutually exclusive with `skipped`
156
+ * on `id` — the reconciliation step in doRunHeal drops any record
157
+ * whose id also appears in `skipped` (a contradiction: degraded means
158
+ * healed, skipped means dropped).
159
+ */
160
+ degraded: Array<DegradedRecord>;
123
161
  /**
124
162
  * Heal candidates still eligible after this run — convergence signal: loop while > 0.
125
163
  *
@@ -1530,9 +1568,34 @@ declare class PromptService {
1530
1568
  systemPrompt: string;
1531
1569
  userPrompt: string;
1532
1570
  };
1533
- buildHealPrompt(healCandidates: unknown[], documentAnchors: unknown[], allTasks: unknown[], recentEvents: unknown[], runtimeOverride?: string): {
1534
- systemPrompt: string;
1535
- userPrompt: string;
1571
+ /**
1572
+ * Heal-prompt level interpretation for the `attemptLevel` ladder.
1573
+ *
1574
+ * Caller contract: `documentAnchors` may be a slice sized for `batch.length`
1575
+ * or a larger set (e.g. a cache hit from `_selectHealAnchors`). This function
1576
+ * applies the prompt-side anchor cap `min(HEAL_MAX_ANCHORS=50, batch.length
1577
+ * * HEAL_ANCHORS_PER_CANDIDATE=4)` so the rendered prompt is bounded
1578
+ * regardless of caller input. `HEAL_MAX_ANCHORS` and
1579
+ * `HEAL_ANCHORS_PER_CANDIDATE` live here too — keeping the formula
1580
+ * co-located with its application avoids a "MaintenanceService policy"
1581
+ * import cycle (`PromptService` is constructed before `MaintenanceService`
1582
+ * exists) and makes the cap testable without a `MaintenanceService`
1583
+ * instance. Task 3 exports the same two constants from `MaintenanceService`
1584
+ * for caller-side overfetch sizing; the values must match.
1585
+ *
1586
+ * Level semantics:
1587
+ * - L0: allTasks + recentEvents + full candidate bodies; anchors re-capped
1588
+ * - L1: drop allTasks; recentEvents present; candidate bodies full
1589
+ * - L2: drop allTasks and recentEvents; candidate bodies full
1590
+ * - L3: drop allTasks and recentEvents; truncate each candidate body to
1591
+ * `bodyTruncationChars` and emit a `degraded` record per truncated fact
1592
+ */
1593
+ buildHealPrompt(healCandidates: unknown[], documentAnchors: unknown[], allTasks: unknown[], recentEvents: unknown[], runtimeOverride: string | undefined, attemptLevel: 0 | 1 | 2 | 3, bodyTruncationChars?: number): {
1594
+ prompts: {
1595
+ systemPrompt: string;
1596
+ userPrompt: string;
1597
+ };
1598
+ degraded: DegradedRecord[];
1536
1599
  };
1537
1600
  buildOntologyBackfillPrompt(facts: unknown[], runtimeOverride?: string, ontologyContext?: OntologyPromptContext | null): {
1538
1601
  systemPrompt: string;
@@ -1748,6 +1811,7 @@ declare class EventRepository extends BaseRepository {
1748
1811
  declare const ONTOLOGY_BACKFILL_BATCH_SIZE = 25;
1749
1812
  declare const ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS = 40000;
1750
1813
  declare const ONTOLOGY_BACKFILL_RECHECK_MS: number;
1814
+
1751
1815
  /**
1752
1816
  * Heal candidates fetched per pass. Bounds the pool runBatched draws from, not
1753
1817
  * the size of an individual LLM call — runBatched still packs ~10 candidates per
@@ -1787,6 +1851,7 @@ declare class MaintenanceService {
1787
1851
  runHeal(entityId: string, options?: {
1788
1852
  promptOverride?: string;
1789
1853
  batchSize?: number;
1854
+ bodyTruncationChars?: number;
1790
1855
  }): Promise<HealResult>;
1791
1856
  runOntologyBackfill(entityId: string, options?: {
1792
1857
  promptOverride?: string;
@@ -1840,6 +1905,7 @@ declare class MaintenanceService {
1840
1905
  doRunHeal(entityId: string, options?: {
1841
1906
  promptOverride?: string;
1842
1907
  batchSize?: number;
1908
+ bodyTruncationChars?: number;
1843
1909
  }): Promise<HealResult>;
1844
1910
  /** Core ontology backfill pass (locks handled by {@link runOntologyBackfill}). Package-internal orchestration hook. */
1845
1911
  doRunOntologyBackfill(entityId: string, options?: {
@@ -2304,4 +2370,4 @@ declare class WikiMemory {
2304
2370
  setGeneratedByTask(taskId: string, entityId: string, actor: string): Promise<void>;
2305
2371
  }
2306
2372
 
2307
- export { WikiStrictOntologyViolation as $, WikiBusyError as A, type WikiBusyOperation as B, type ChunkFailure as C, type WikiCheckpoint as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type IngestDocumentResult as I, type WikiConfig as J, WikiDuplicateHashError as K, type LLMProvider as L, type MemoryBundle as M, type WikiEdge as N, type OntologyManifest as O, type PromptOverrides as P, type WikiEvent as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiFact as T, WikiIngestEmptyError as U, type VectorRanker as V, type WikiOptions as W, type WikiMemoryTestAccess as X, type WikiOutboxEvent as Y, WikiParseError as Z, WikiSourceRefHashCollision as _, type MemoryDump as a, type WikiTask as a0, WikiTransactionError as a1, EmbeddingService as a2, ImportExportService as a3, IngestionService as a4, JobManager as a5, MaintenanceService as a6, RetrievalService as a7, SearchService as a8, WriteService as a9, 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 };
2373
+ export { WikiSourceRefHashCollision 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, type WikiMemoryTestAccess as Y, type WikiOutboxEvent as Z, WikiParseError as _, type MemoryDump as a, WikiStrictOntologyViolation as a0, type WikiTask as a1, WikiTransactionError as a2, EmbeddingService as a3, ImportExportService as a4, IngestionService as a5, JobManager as a6, MaintenanceService as a7, RetrievalService as a8, SearchService as a9, WriteService as aa, 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 };
@@ -1,3 +1,3 @@
1
- export { a2 as EmbeddingService, a3 as ImportExportService, a4 as IngestionService, a5 as JobManager, a5 as JobManagerType, a6 as MaintenanceService, a7 as RetrievalService, a8 as SearchService, a8 as SearchServiceType, X as WikiMemoryTestAccess, a9 as WriteService } from './testing-D0RnZjyW.mjs';
1
+ export { a3 as EmbeddingService, a4 as ImportExportService, a5 as IngestionService, a6 as JobManager, a6 as JobManagerType, a7 as MaintenanceService, a8 as RetrievalService, a9 as SearchService, a9 as SearchServiceType, Y as WikiMemoryTestAccess, aa as WriteService } from './testing-DgqVB29I.mjs';
2
2
  import '@equationalapplications/core-okf';
3
3
  import 'minisearch';
package/dist/testing.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { a2 as EmbeddingService, a3 as ImportExportService, a4 as IngestionService, a5 as JobManager, a5 as JobManagerType, a6 as MaintenanceService, a7 as RetrievalService, a8 as SearchService, a8 as SearchServiceType, X as WikiMemoryTestAccess, a9 as WriteService } from './testing-D0RnZjyW.js';
1
+ export { a3 as EmbeddingService, a4 as ImportExportService, a5 as IngestionService, a6 as JobManager, a6 as JobManagerType, a7 as MaintenanceService, a8 as RetrievalService, a9 as SearchService, a9 as SearchServiceType, Y as WikiMemoryTestAccess, aa as WriteService } from './testing-DgqVB29I.js';
2
2
  import '@equationalapplications/core-okf';
3
3
  import 'minisearch';
package/dist/testing.js CHANGED
@@ -1270,6 +1270,12 @@ Return ONLY a valid JSON object matching this schema:
1270
1270
  If no manifest type fits a fact, omit that fact from "classifications" entirely \u2014 do not guess.
1271
1271
  When echoing an existing fact's title verbatim into "target_title", preserve every JSON escape sequence (\\", \\n, \\\\, \\/) exactly as it appeared in the input body \u2014 do not strip backslashes, do not add unescaped quotes. Do not return markdown, just raw JSON.`;
1272
1272
 
1273
+ // src/utils/healConstants.ts
1274
+ var HEAL_MAX_ANCHORS = 50;
1275
+ var HEAL_ANCHORS_PER_CANDIDATE = 4;
1276
+ var HEAL_MAX_FACT_BODY_CHARS_L3 = 4e3;
1277
+ var HEAL_MAX_TASKS = 50;
1278
+
1273
1279
  // src/services/PromptService.ts
1274
1280
  var PromptService = class {
1275
1281
  constructor(globalOverrides) {
@@ -1337,25 +1343,67 @@ Current Facts:
1337
1343
  ${JSON.stringify(currentFacts, null, 2)}`
1338
1344
  };
1339
1345
  }
1340
- buildHealPrompt(healCandidates, documentAnchors, allTasks, recentEvents, runtimeOverride) {
1346
+ /**
1347
+ * Heal-prompt level interpretation for the `attemptLevel` ladder.
1348
+ *
1349
+ * Caller contract: `documentAnchors` may be a slice sized for `batch.length`
1350
+ * or a larger set (e.g. a cache hit from `_selectHealAnchors`). This function
1351
+ * applies the prompt-side anchor cap `min(HEAL_MAX_ANCHORS=50, batch.length
1352
+ * * HEAL_ANCHORS_PER_CANDIDATE=4)` so the rendered prompt is bounded
1353
+ * regardless of caller input. `HEAL_MAX_ANCHORS` and
1354
+ * `HEAL_ANCHORS_PER_CANDIDATE` live here too — keeping the formula
1355
+ * co-located with its application avoids a "MaintenanceService policy"
1356
+ * import cycle (`PromptService` is constructed before `MaintenanceService`
1357
+ * exists) and makes the cap testable without a `MaintenanceService`
1358
+ * instance. Task 3 exports the same two constants from `MaintenanceService`
1359
+ * for caller-side overfetch sizing; the values must match.
1360
+ *
1361
+ * Level semantics:
1362
+ * - L0: allTasks + recentEvents + full candidate bodies; anchors re-capped
1363
+ * - L1: drop allTasks; recentEvents present; candidate bodies full
1364
+ * - L2: drop allTasks and recentEvents; candidate bodies full
1365
+ * - L3: drop allTasks and recentEvents; truncate each candidate body to
1366
+ * `bodyTruncationChars` and emit a `degraded` record per truncated fact
1367
+ */
1368
+ buildHealPrompt(healCandidates, documentAnchors, allTasks, recentEvents, runtimeOverride, attemptLevel, bodyTruncationChars = HEAL_MAX_FACT_BODY_CHARS_L3) {
1369
+ const effectiveTasks = attemptLevel >= 1 ? [] : allTasks;
1370
+ const effectiveEvents = attemptLevel >= 2 ? [] : recentEvents;
1371
+ const maxAnchors = Math.max(1, Math.min(HEAL_MAX_ANCHORS, healCandidates.length * HEAL_ANCHORS_PER_CANDIDATE));
1372
+ const effectiveAnchors = documentAnchors.slice(0, maxAnchors);
1373
+ const { shapedCandidates, degraded } = applyBodyTruncation(
1374
+ healCandidates,
1375
+ attemptLevel,
1376
+ bodyTruncationChars
1377
+ );
1341
1378
  const template = runtimeOverride ?? this.globalOverrides?.healSystemPrompt ?? HEAL_SYSTEM_PROMPT;
1342
1379
  if (/\{\{\s*healCandidates\s*\}\}/.test(template) || /\{\{\s*documentAnchors\s*\}\}/.test(template) || /\{\{\s*allTasks\s*\}\}/.test(template) || /\{\{\s*recentEvents\s*\}\}/.test(template)) {
1343
1380
  return {
1344
- systemPrompt: this.hydrate(template, { healCandidates, documentAnchors, allTasks, recentEvents }),
1345
- userPrompt: "Please heal the memory graph."
1381
+ prompts: {
1382
+ systemPrompt: this.hydrate(template, {
1383
+ healCandidates: shapedCandidates,
1384
+ documentAnchors: effectiveAnchors,
1385
+ allTasks: effectiveTasks,
1386
+ recentEvents: effectiveEvents
1387
+ }),
1388
+ userPrompt: "Please heal the memory graph."
1389
+ },
1390
+ degraded
1346
1391
  };
1347
1392
  }
1348
1393
  return {
1349
- systemPrompt: template,
1350
- userPrompt: `Heal Candidates:
1351
- ${JSON.stringify(healCandidates, null, 2)}
1394
+ prompts: {
1395
+ systemPrompt: template,
1396
+ userPrompt: `Heal Candidates:
1397
+ ${JSON.stringify(shapedCandidates, null, 2)}
1352
1398
  Document Anchors (DO NOT MODIFY OR DELETE):
1353
- ${JSON.stringify(documentAnchors, null, 2)}
1399
+ ${JSON.stringify(effectiveAnchors, null, 2)}
1354
1400
  All Tasks:
1355
- ${JSON.stringify(allTasks, null, 2)}
1401
+ ${JSON.stringify(effectiveTasks, null, 2)}
1356
1402
  Recent Events:
1357
- ${JSON.stringify(recentEvents, null, 2)}
1403
+ ${JSON.stringify(effectiveEvents, null, 2)}
1358
1404
  The following document anchors are provided for contradiction detection only. Do not include them in \`downgraded\`, \`deleted\`, or \`newFacts\`.`
1405
+ },
1406
+ degraded
1359
1407
  };
1360
1408
  }
1361
1409
  buildOntologyBackfillPrompt(facts, runtimeOverride, ontologyContext) {
@@ -1375,6 +1423,36 @@ ${JSON.stringify(facts, null, 2)}`
1375
1423
  };
1376
1424
  }
1377
1425
  };
1426
+ function applyBodyTruncation(candidates, attemptLevel, bodyTruncationChars) {
1427
+ if (attemptLevel < 3) {
1428
+ return { shapedCandidates: candidates, degraded: [] };
1429
+ }
1430
+ const shapedCandidates = [];
1431
+ const degraded = [];
1432
+ for (const c of candidates) {
1433
+ if (typeof c !== "object" || c === null) {
1434
+ shapedCandidates.push(c);
1435
+ continue;
1436
+ }
1437
+ const fact = c;
1438
+ const body = typeof fact.body === "string" ? fact.body : "";
1439
+ if (body.length <= bodyTruncationChars) {
1440
+ shapedCandidates.push(c);
1441
+ continue;
1442
+ }
1443
+ if (typeof fact.id !== "string") {
1444
+ shapedCandidates.push(c);
1445
+ continue;
1446
+ }
1447
+ const originalBodyChars = body.length;
1448
+ const prefix = safeSlice(body, 0, bodyTruncationChars);
1449
+ const truncatedBodyChars = prefix.length;
1450
+ const truncated = `${prefix}\u2026[truncated at ${truncatedBodyChars} chars, original was ${originalBodyChars}]`;
1451
+ shapedCandidates.push({ ...fact, body: truncated });
1452
+ degraded.push({ id: fact.id, originalBodyChars, truncatedBodyChars });
1453
+ }
1454
+ return { shapedCandidates, degraded };
1455
+ }
1378
1456
 
1379
1457
  // src/utils/chunkingDefaults.ts
1380
1458
  var DEFAULT_MAX_CHUNK_LENGTH = 12e3;
@@ -1885,16 +1963,24 @@ var TRUNCATION_PATTERNS = [
1885
1963
  /finish[_ ]?reason/i
1886
1964
  ];
1887
1965
  var EXCEEDS_LIMIT_PATTERN = /exceed[a-z]*[^.]{0,40}\b(model|context)?[ _-]?limit/i;
1888
- function isTruncationError(err) {
1889
- let message;
1966
+ function getErrorMessage(err) {
1890
1967
  try {
1891
- message = err instanceof Error ? err.message : String(err ?? "");
1968
+ return err instanceof Error ? err.message : String(err ?? "");
1892
1969
  } catch {
1893
- return false;
1970
+ return void 0;
1894
1971
  }
1972
+ }
1973
+ function isTruncationError(err) {
1974
+ const message = getErrorMessage(err);
1975
+ if (message === void 0) return false;
1895
1976
  if (EXCEEDS_LIMIT_PATTERN.test(message)) return false;
1896
1977
  return TRUNCATION_PATTERNS.some((pattern) => pattern.test(message));
1897
1978
  }
1979
+ function isConfigError(err) {
1980
+ const message = getErrorMessage(err);
1981
+ if (message === void 0) return false;
1982
+ return EXCEEDS_LIMIT_PATTERN.test(message);
1983
+ }
1898
1984
  function initialBatchSize(maxOutputTokens) {
1899
1985
  if (!maxOutputTokens || !Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) {
1900
1986
  return DEFAULT_BATCH_SIZE;
@@ -1936,14 +2022,18 @@ async function runBatched(args) {
1936
2022
  const single = candidate.slice(0, 1);
1937
2023
  return { batch: single, prompts: await buildPrompt(single) };
1938
2024
  };
1939
- const onFailure = async (batch, err) => {
1940
- if (batch.length <= 1) {
1941
- if (batch.length === 1) {
1942
- skipped.push(batch[0]);
2025
+ const onFailure = async (batch, err, attemptLevel, fromCall) => {
2026
+ if (batch.length === 1) {
2027
+ if (fromCall && isTruncationError(err) && attemptLevel < 3) {
2028
+ await attempt(batch, void 0, attemptLevel + 1);
2029
+ } else {
2030
+ const reason = fromCall && !isTruncationError(err) && !isConfigError(err) ? "call_error" : "non_convergent";
2031
+ skipped.push({ item: batch[0], reason });
1943
2032
  onSkip?.(batch[0], err);
1944
2033
  }
1945
2034
  return;
1946
2035
  }
2036
+ if (fromCall && !isTruncationError(err)) throw err;
1947
2037
  const mid = Math.ceil(batch.length / 2);
1948
2038
  if (mid < batchSize) batchSize = mid;
1949
2039
  let i = 0;
@@ -1954,23 +2044,22 @@ async function runBatched(args) {
1954
2044
  i += trimmed.batch.length;
1955
2045
  }
1956
2046
  };
1957
- const attempt = async (batch, prebuilt) => {
2047
+ const attempt = async (batch, prebuilt, attemptLevel = 0) => {
1958
2048
  if (batch.length === 0) return;
1959
- const prompts = prebuilt ?? await buildPrompt(batch);
2049
+ const prompts = prebuilt && attemptLevel === 0 ? prebuilt : await buildPrompt(batch, attemptLevel);
1960
2050
  batches++;
1961
2051
  let responseText;
1962
2052
  try {
1963
2053
  responseText = await call(prompts);
1964
2054
  } catch (err) {
1965
- if (!isTruncationError(err)) throw err;
1966
- await onFailure(batch, err);
2055
+ await onFailure(batch, err, attemptLevel, true);
1967
2056
  return;
1968
2057
  }
1969
2058
  let result;
1970
2059
  try {
1971
2060
  result = parse(responseText, batch);
1972
2061
  } catch (err) {
1973
- await onFailure(batch, err);
2062
+ await onFailure(batch, err, attemptLevel, false);
1974
2063
  return;
1975
2064
  }
1976
2065
  results.push(result);
@@ -1979,7 +2068,7 @@ async function runBatched(args) {
1979
2068
  while (index < items.length) {
1980
2069
  const { batch, prompts } = await trim(items.slice(index, index + batchSize));
1981
2070
  index += batch.length;
1982
- await attempt(batch, prompts);
2071
+ await attempt(batch, prompts, 0);
1983
2072
  }
1984
2073
  return { results, skipped, batches };
1985
2074
  }
@@ -1990,7 +2079,6 @@ var MIN_TOKENS_TO_QUALIFY = 3;
1990
2079
  var ONTOLOGY_BACKFILL_BATCH_SIZE = 25;
1991
2080
  var ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS = 4e4;
1992
2081
  var ONTOLOGY_BACKFILL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
1993
- var HEAL_MAX_ANCHORS = 50;
1994
2082
  var HEAL_ANCHOR_SEARCH_OVERFETCH = 4;
1995
2083
  var HEAL_MAX_PROMPT_CHARS = 4e4;
1996
2084
  var HEAL_BATCH_SIZE = 25;
@@ -2421,6 +2509,10 @@ var MaintenanceService = class {
2421
2509
  if (!Number.isInteger(batchSize) || batchSize < 1) {
2422
2510
  throw new Error("Invalid batchSize: must be an integer >= 1");
2423
2511
  }
2512
+ const bodyTruncationChars = options?.bodyTruncationChars ?? HEAL_MAX_FACT_BODY_CHARS_L3;
2513
+ if (!Number.isInteger(bodyTruncationChars) || bodyTruncationChars < 1) {
2514
+ throw new Error("Invalid bodyTruncationChars: must be an integer >= 1");
2515
+ }
2424
2516
  const now = Date.now();
2425
2517
  const recheckCutoff = now - HEAL_RECHECK_MS;
2426
2518
  const orphanAfterDays = this.options.config?.orphanAfterDays !== void 0 ? this.options.config?.orphanAfterDays : 30;
@@ -2461,29 +2553,44 @@ var MaintenanceService = class {
2461
2553
  downgraded: staleDowngradedIds.length,
2462
2554
  deleted: orphanedIds.length,
2463
2555
  newFactsCreated: 0,
2464
- skipped: 0,
2556
+ skipped: [],
2557
+ degraded: [],
2465
2558
  remaining: counts2.eligible,
2466
2559
  deferred: counts2.deferred
2467
2560
  };
2468
2561
  }
2469
- const allTasks = await this.taskRepo.findAllPending([entityId]);
2562
+ const allTasks = await this.taskRepo.findAllPending([entityId], HEAL_MAX_TASKS);
2470
2563
  const recentEvents = await this.eventRepo.getRecent(entityId, 20);
2471
2564
  const toPromptShape = (f) => {
2472
2565
  const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
2473
2566
  return { ...rest, tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags };
2474
2567
  };
2475
2568
  const anchorCache = /* @__PURE__ */ new Map();
2569
+ const degraded = [];
2476
2570
  const outcome = await runBatched({
2477
2571
  items: healCandidates,
2478
- buildPrompt: async (batch) => {
2479
- const documentAnchors = await this._selectHealAnchors(entityId, batch, anchorCache);
2480
- return this.promptService.buildHealPrompt(
2572
+ buildPrompt: async (batch, attemptLevel = 0) => {
2573
+ const documentAnchors = await this._selectHealAnchors(
2574
+ entityId,
2575
+ batch,
2576
+ // Per-batch anchor cap: `batch.length * HEAL_ANCHORS_PER_CANDIDATE` (capped at
2577
+ // HEAL_MAX_ANCHORS) right-sizes the anchor lookup so a 1-fact batch does
2578
+ // not overfetch the same 200 keyword hits a 25-fact batch once did.
2579
+ // buildHealPrompt applies the matching cap on its side — values must match.
2580
+ Math.min(HEAL_MAX_ANCHORS, batch.length * HEAL_ANCHORS_PER_CANDIDATE),
2581
+ anchorCache
2582
+ );
2583
+ const { prompts, degraded: batchDegraded } = await this.promptService.buildHealPrompt(
2481
2584
  batch.map(toPromptShape),
2482
2585
  documentAnchors,
2483
2586
  allTasks,
2484
2587
  recentEvents,
2485
- promptOverride
2588
+ promptOverride,
2589
+ attemptLevel,
2590
+ bodyTruncationChars
2486
2591
  );
2592
+ degraded.push(...batchDegraded);
2593
+ return prompts;
2487
2594
  },
2488
2595
  call: (prompts) => this.options.llmProvider.generateText(prompts),
2489
2596
  parse: (responseText, batch) => {
@@ -2557,8 +2664,14 @@ var MaintenanceService = class {
2557
2664
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
2558
2665
  healFactsForDedupe.push({ id, title: fact.title });
2559
2666
  }
2667
+ const callErrorIds = new Set(
2668
+ outcome.skipped.filter((s) => s.reason === "call_error").map((s) => s.item.id)
2669
+ );
2560
2670
  await this.entryRepo.markHealChecked(
2561
- [...healCandidates.map((f) => f.id), ...insertedFacts.map((f) => f.id)],
2671
+ [
2672
+ ...healCandidates.map((f) => f.id).filter((id) => !callErrorIds.has(id)),
2673
+ ...insertedFacts.map((f) => f.id)
2674
+ ],
2562
2675
  entityId,
2563
2676
  now,
2564
2677
  tx
@@ -2576,6 +2689,13 @@ var MaintenanceService = class {
2576
2689
  await this.embeddingService.embedFact(fact);
2577
2690
  }
2578
2691
  this.searchService.evictCache(entityId);
2692
+ const skippedIds = new Set(outcome.skipped.map(({ item }) => item.id));
2693
+ const healedDegraded = degraded.filter((d) => !skippedIds.has(d.id));
2694
+ for (const d of healedDegraded) {
2695
+ console.warn(
2696
+ `[WikiMemory] heal healed under degraded context ${entityId}/${d.id}: body truncated from ${d.originalBodyChars} to ${d.truncatedBodyChars} chars`
2697
+ );
2698
+ }
2579
2699
  let scanned = outcome.skipped.length;
2580
2700
  for (const batchResult of outcome.results) scanned += batchResult.batch.length;
2581
2701
  const allDowngraded = /* @__PURE__ */ new Set([...staleDowngradedIds, ...safeDowngraded]);
@@ -2586,7 +2706,8 @@ var MaintenanceService = class {
2586
2706
  downgraded: allDowngraded.size,
2587
2707
  deleted: allDeleted.size,
2588
2708
  newFactsCreated: insertedFacts.length,
2589
- skipped: outcome.skipped.length,
2709
+ skipped: outcome.skipped.map(({ item, reason }) => ({ id: item.id, reason })),
2710
+ degraded: healedDegraded,
2590
2711
  remaining: counts.eligible,
2591
2712
  deferred: counts.deferred
2592
2713
  };
@@ -2670,7 +2791,15 @@ var MaintenanceService = class {
2670
2791
  };
2671
2792
  }
2672
2793
  if (outcome.skipped.length > 0) {
2673
- await this.entryRepo.markOntologyChecked(outcome.skipped.map((f) => f.id), entityId, now, this.db);
2794
+ const callErrorIds = new Set(
2795
+ outcome.skipped.filter((s) => s.reason === "call_error").map((s) => s.item.id)
2796
+ );
2797
+ await this.entryRepo.markOntologyChecked(
2798
+ outcome.skipped.filter(({ item }) => !callErrorIds.has(item.id)).map(({ item }) => item.id),
2799
+ entityId,
2800
+ now,
2801
+ this.db
2802
+ );
2674
2803
  }
2675
2804
  this.searchService.evictCache(entityId);
2676
2805
  const counts = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
@@ -2786,7 +2915,7 @@ var MaintenanceService = class {
2786
2915
  * batches that reduce to the same query share one lookup. Caller-owned and
2787
2916
  * per-pass — see the call site in doRunHeal.
2788
2917
  */
2789
- async _selectHealAnchors(entityId, batch, cache) {
2918
+ async _selectHealAnchors(entityId, batch, cap = HEAL_MAX_ANCHORS, cache) {
2790
2919
  const query = batch.map((f) => f.title).join(" ").trim();
2791
2920
  if (!query) return [];
2792
2921
  const cached = cache?.get(query);
@@ -2794,7 +2923,7 @@ var MaintenanceService = class {
2794
2923
  const hits = this.searchService.searchKeyword(
2795
2924
  query,
2796
2925
  [entityId],
2797
- HEAL_MAX_ANCHORS * HEAL_ANCHOR_SEARCH_OVERFETCH
2926
+ cap * HEAL_ANCHOR_SEARCH_OVERFETCH
2798
2927
  );
2799
2928
  const hitIds = hits.map((h) => h.id);
2800
2929
  const anchors = [];
@@ -2805,7 +2934,7 @@ var MaintenanceService = class {
2805
2934
  const row = byId.get(id);
2806
2935
  if (!row) continue;
2807
2936
  anchors.push(row);
2808
- if (anchors.length >= HEAL_MAX_ANCHORS) break;
2937
+ if (anchors.length >= cap) break;
2809
2938
  }
2810
2939
  }
2811
2940
  cache?.set(query, anchors);