@equationalapplications/core-llm-wiki 7.7.7 → 7.8.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.
package/README.md CHANGED
@@ -67,6 +67,82 @@ For programmatic pipelines — importing pre-classified data, building a GraphRA
67
67
 
68
68
  This is the GraphRAG seed path: load a corpus, walk it.
69
69
 
70
+ ## Temporal facts
71
+
72
+ Facts carry an optional validity window. Nothing changes until you use it: facts without validity data are always current.
73
+
74
+ ```typescript
75
+ // The user tells you they moved; the move happened on March 1.
76
+ await wiki.supersede('user-123', seattleFactId, { title: 'Lives in San Francisco', body: 'Moved in March.' }, {
77
+ validFrom: Date.parse('2026-03-01'),
78
+ });
79
+
80
+ await wiki.read('user-123', 'where do I live'); // current: San Francisco
81
+ await wiki.read('user-123', 'where do I live', { asOf: Date.parse('2026-02-01') }); // then: Seattle
82
+ await wiki.history('user-123', seattleFactId); // [Seattle, San Francisco]
83
+
84
+ // Record when an event really happened (defaults to ingestion time):
85
+ await wiki.write('user-123', { event_type: 'observation', summary: 'Moved to SF', occurred_at: Date.parse('2026-03-01') });
86
+ ```
87
+
88
+ - Validity is half-open: `[valid_from, valid_to)`. At the switch instant only the new fact matches.
89
+ - `asOf` answers "what do we now know was true at T". Learning late about a past change still answers correctly for the past.
90
+ - `supersede()` refuses `immutable_document` facts (re-ingest the document instead), cycles, facts owned by another entity, and facts already superseded. It throws `WikiSupersedeError` with a `reason`.
91
+ - `traverseGraph({ asOf })` only walks edges whose two facts are both live at that instant.
92
+ - History is kept. Set `config.pruneSupersededAfter` (days) to let `runPrune` retire expired facts.
93
+ - `getMemoryBundle()` and `exportDump()` return every non-deleted fact, including history.
94
+
95
+ ## Librarian strategies
96
+
97
+ `config.librarian.strategy` selects how events become facts. The default, `'legacy'`, is unchanged from 7.x.
98
+
99
+ ```typescript
100
+ new WikiMemory(db, {
101
+ llmProvider,
102
+ config: { librarian: { strategy: 'ops', gate: { k: 5, dupThreshold: 0.89, novelThreshold: 0.30 } } },
103
+ });
104
+ ```
105
+
106
+ `'ops'` works in three steps:
107
+
108
+ 1. **Extract.** Candidate facts come from the events since the last pass. Existing facts are not sent.
109
+ 2. **Gate, with no LLM call.** Each candidate is compared with its closest current facts. An identical fact is a no-op, and a candidate with no close match is added. Only candidates that are similar but different continue to step 3. Similar is not the same as identical: "lives in Seattle" and "moved to San Francisco" score as similar, so they go to the model.
110
+ 3. **Resolve, in one batched call.** The model returns `ADD`, `UPDATE`, `SUPERSEDE` or `NOOP` for each remaining candidate. `SUPERSEDE` uses `supersede()`, so history is kept and `read({ asOf })` still sees the old fact.
111
+
112
+ - The thresholds depend on your embedding model. The defaults are provisional.
113
+ - Without `embed`, the gate uses keyword search. Every keyword match then goes to the resolve step.
114
+ - Under `'ops'`, `runLibrarian`'s `promptOverride` and `prompts.librarianSystemPrompt` are ignored: the ops prompts use a different schema.
115
+ - A model answer that can't be used never loses a fact. It is added, or stored as a draft, and a diagnostic is emitted.
116
+
117
+ ## Deferred maintenance
118
+
119
+ By default, `write()` starts the librarian and heal in the background when thresholds are crossed. On serverless or request-scoped hosts those background promises can be frozen or killed. Set `maintenance: 'deferred'` to decide when maintenance runs:
120
+
121
+ ```typescript
122
+ const wiki = new WikiMemory(db, {
123
+ llmProvider,
124
+ config: { maintenance: 'deferred', librarian: { strategy: 'ops' } },
125
+ });
126
+
127
+ await wiki.write(entityId, { event_type: 'observation', summary, occurred_at });
128
+
129
+ const report = await wiki.runPendingMaintenance({ tokenBudget: 20_000, deadlineMs: 8_000 });
130
+ // report.stoppedReason: complete | deadline | budget_exhausted | budget_too_small
131
+ // Nothing is still running here, so it is safe to persist the database.
132
+ ```
133
+
134
+ - `getPendingMaintenance()` shows the backlog per entity without calling a model.
135
+ - The budget covers text-generation tokens only; embedding calls are not counted. Figures are provider-reported when `generateTextWithUsage` exists, otherwise estimated at characters ÷ 4.
136
+ - Entities share the budget fairly: each round runs one batch per entity.
137
+ - `budget_too_small` names the first job and entity whose smallest step needed more than the whole budget; other entities may have hit the same limit in that run.
138
+ - A crash mid-batch loses nothing. The librarian's progress marker only advances after a batch commits.
139
+ - In `'auto'` mode, call `drain()` before the app is suspended or closed. `autoLibrarianTokenThreshold` also triggers the librarian on the size of the pending text.
140
+ - Use `'deferred'` with the ops strategy. The legacy strategy reads only the newest 50 events per pass and marks them processed, so a backlog larger than that is skipped.
141
+ - Under `'deferred'` or the ops strategy, `runPrune` never deletes events the librarian has not processed.
142
+ - `JobManager` locks only work within one process. Guarantee a single writer per database file across processes.
143
+
144
+ See the [SynapseTree integration guide](../../docs/synapsetree-integration.md) for the end-to-end pattern on tenant-aware hosts (S3-backed SQLite, request lifecycle, `entity_id` scheme, MCP surface, costs). Benchmarks live at [docs/benchmarks.md](../../docs/benchmarks.md).
145
+
70
146
  ## Installation
71
147
 
72
148
  ```bash
@@ -124,6 +200,15 @@ const wikiMemory = new WikiMemory(db, {
124
200
  // read() returns MiniSearch results, onRetrievalFallback called (embed threw)
125
201
  ```
126
202
 
203
+ ### Token budgets
204
+
205
+ ```typescript
206
+ const bundle = await wiki.read('user-123', 'what should I cook?', { tokenBudget: 800 });
207
+ const prompt = formatContext(bundle, { compact: true });
208
+ ```
209
+
210
+ `tokenBudget` keeps the most relevant facts per token that fit within about 800 tokens. Tokens are estimated as characters ÷ 4. The top fact is always included, truncated if it alone is too big. Tasks and events are unaffected. `traverseGraph` accepts the same option and keeps nodes in breadth-first order. A `read_budget` diagnostic reports how many facts were packed.
211
+
127
212
  ## Configuration
128
213
 
129
214
  All `WikiConfig` fields are optional: