@psiclawops/hypermem 0.5.5 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +108 -62
  2. package/dist/background-indexer.d.ts +18 -0
  3. package/dist/background-indexer.d.ts.map +1 -1
  4. package/dist/background-indexer.js +131 -20
  5. package/dist/cache.d.ts +24 -1
  6. package/dist/cache.d.ts.map +1 -1
  7. package/dist/cache.js +77 -3
  8. package/dist/compositor.d.ts +6 -0
  9. package/dist/compositor.d.ts.map +1 -1
  10. package/dist/compositor.js +471 -129
  11. package/dist/context-backfill.d.ts +46 -0
  12. package/dist/context-backfill.d.ts.map +1 -0
  13. package/dist/context-backfill.js +113 -0
  14. package/dist/context-store.d.ts +77 -0
  15. package/dist/context-store.d.ts.map +1 -0
  16. package/dist/context-store.js +177 -0
  17. package/dist/cross-agent.d.ts +12 -0
  18. package/dist/cross-agent.d.ts.map +1 -1
  19. package/dist/cross-agent.js +31 -19
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +8 -0
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +26 -7
  25. package/dist/knowledge-lint.js +4 -4
  26. package/dist/message-store.d.ts +31 -2
  27. package/dist/message-store.d.ts.map +1 -1
  28. package/dist/message-store.js +131 -17
  29. package/dist/preference-store.d.ts +1 -1
  30. package/dist/preference-store.js +1 -1
  31. package/dist/profiles.d.ts +4 -2
  32. package/dist/profiles.d.ts.map +1 -1
  33. package/dist/profiles.js +72 -37
  34. package/dist/repair-tool-pairs.d.ts.map +1 -1
  35. package/dist/repair-tool-pairs.js +73 -2
  36. package/dist/schema.d.ts +1 -1
  37. package/dist/schema.d.ts.map +1 -1
  38. package/dist/schema.js +27 -1
  39. package/dist/seed.d.ts +1 -1
  40. package/dist/seed.js +1 -1
  41. package/dist/session-flusher.d.ts +2 -2
  42. package/dist/session-flusher.js +2 -2
  43. package/dist/spawn-context.d.ts +1 -1
  44. package/dist/spawn-context.js +1 -1
  45. package/dist/topic-synthesizer.d.ts.map +1 -1
  46. package/dist/topic-synthesizer.js +4 -3
  47. package/dist/trigger-registry.d.ts +1 -1
  48. package/dist/trigger-registry.js +4 -4
  49. package/dist/types.d.ts +74 -32
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/vector-store.d.ts +10 -1
  52. package/dist/vector-store.d.ts.map +1 -1
  53. package/dist/vector-store.js +353 -0
  54. package/dist/version.d.ts +5 -5
  55. package/dist/version.js +5 -5
  56. package/package.json +4 -2
package/README.md CHANGED
@@ -116,7 +116,7 @@ send transcript to model tool compression by turn age
116
116
  model responds → append again keystone guard + hyperform profile
117
117
  composed prompt → model
118
118
  ┌──────────────────┐ model responds → afterTurn ingest
119
- loop until full │ → write back to all 4 layers
119
+ loop until full │ → write back to all 4 layers
120
120
  └──────────────────┘
121
121
 
122
122
  When it fills: When budget is exceeded:
@@ -127,7 +127,7 @@ When it fills: When budget is exceeded:
127
127
 
128
128
  | | Standard | hypercompositor |
129
129
  |---|---|---|
130
- | Context source | Growing transcript | 4 independent storage layers |
130
+ | Context source | Growing transcript only | Transcript + 3 additional storage layers |
131
131
  | When context fills | Trim + summarize (lossy) | Budget allocation (lossless storage) |
132
132
  | Old decisions | Lost after compaction | Retrievable via keystones + semantic recall |
133
133
  | Topic changes | All history competes equally | Scoped retrieval by active topic |
@@ -136,21 +136,45 @@ When it fills: When budget is exceeded:
136
136
 
137
137
  High-signal turns are marked as keystones and survive pressure trimming ahead of ordinary history.
138
138
 
139
+ The compositor fills 9 slots in priority order (system prompt → identity → hyperform → history → facts → wiki → semantic recall → cross-session → action summary). Each slot consumes tokens from the remaining budget before the next slot runs. Slots that don't fit this turn stay in storage, not destroyed.
140
+
141
+ For the full fill order, budget formula, and all configuration knobs, see **[Tuning](#tuning)** below and **[docs/TUNING.md](./docs/TUNING.md)**.
142
+
139
143
  ---
140
144
 
141
145
  ## hyperform
142
146
 
143
147
  Raw model output has two problems. It drifts from your standards (sycophancy, hedging, pagination, formatting) and it drifts from your facts (confabulation, contradiction, stale claims). hyperform handles both: normalization enforces consistency, confabulation resistance checks output against what's actually stored.
144
148
 
145
- **Normalization** shapes output to match a profile you define. Three presets ship with hypermem:
149
+ Consistent output isn't just aesthetic. A model that paginates short answers, preambles with filler, or inflates lists uses more output tokens per turn. Over hundreds of turns, that compounds into real cost. hyperform directives compress output at the source: fewer tokens generated means lower API spend per session, and less context pressure for subsequent turns.
150
+
151
+ ### Behavior standards
146
152
 
147
- | Profile | Tokens | Covers |
153
+ Behavior standards define how your agents write. Anti-sycophancy rules prevent filler openings. Density targets compress answers. Anti-pattern bans remove common AI markers (em dashes, AI vocabulary, inflated significance). These rules apply to all models equally.
154
+
155
+ | Tier | Tokens | What it injects |
148
156
  |---|---|---|
149
- | `light` | ~100 | Anti-sycophancy, em dash ban, AI vocab ban, length targets, evidence calibration |
150
- | `standard` | ~250 | Full directive set plus pagination rules and hedging policy |
151
- | `full` | ~400 | Complete normalization with full directive set and model-specific calibration |
157
+ | `light` | ~100 | 9 standalone directives: lead with answer, no sycophancy, no em dashes, AI vocab ban, length targets (simple/analysis/code), filler ban, no pagination of short answers, evidence calibration, numbers over adjectives. No database required. |
158
+ | `standard` | ~250 | Full directive set from the `fleet_output_standard` table: structural rules, density targets per task type, anti-patterns, format rules, compression ratios, voice directives, and task-context overrides. Falls back to `light` directives if no record exists. |
159
+ | `full` | ~250 + adaptation | Same directives as `standard`, plus model adaptation (see below). |
160
+
161
+ ### Model adaptation
162
+
163
+ Different models have different default behaviors. GPT-5.4 tends toward 2x verbosity and long lists. Claude Opus defaults to hedging and preambles. Gemini produces bulleted summaries where prose would be more direct. Model adaptation corrects for these tendencies per model.
164
+
165
+ Adaptation entries are stored in the `model_output_directives` table and matched by model ID using exact match, then glob pattern (longest wins), then wildcard fallback. Each entry contains:
166
+
167
+ - **Calibration** — known model tendencies and specific adjustments (e.g., "2x verbosity: cut first drafts in half")
168
+ - **Corrections** — hard/medium/soft severity rules applied in order (e.g., "No preamble before the answer")
169
+ - **Task overrides** — per-task-type adjustments
170
+
171
+ Model adaptation is only active at the `full` tier. At `light` and `standard`, model-specific corrections are suppressed.
172
+
173
+ The `model_output_directives` table starts empty. You populate it with corrections for the models you run. See [docs/TUNING.md](./docs/TUNING.md#creating-custom-entries) for the schema and SQL examples.
152
174
 
153
- The same prompt, GPT-5.4, with and without `outputProfile: "light"`:
175
+ ### Before and after
176
+
177
+ The same prompt, GPT-5.4, with and without `hyperformProfile: "light"`:
154
178
 
155
179
  ```
156
180
  Prompt: "How should I size my context window budget for a long-running agent session?"
@@ -172,11 +196,13 @@ Would you like me to go deeper on any of these?
172
196
  WITH outputProfile: "light":
173
197
  For a 128k window: reserve 14k for identity/system, target 46k for history, 10k for recent
174
198
  tool context, and leave ~30k as allocator reserve. hypermem handles slot competition
175
- automatically -- set contextWindowReserve to your preferred floor and let the compositor fill.
199
+ automatically set `reserveFraction` to your preferred floor and let the compositor fill.
176
200
  ```
177
201
 
178
202
  **Confabulation resistance** checks output against stored facts before claims are recorded. No LLM call. Pattern matching against the fact corpus, with confidence scoring and contradiction detection. Unsupported claims are flagged, contradictions surface in diagnostics, and a confabulation risk score is attached to the stored episode.
179
203
 
204
+ Set `compositor.hyperformProfile` to `light`, `standard`, or `full`. For tier selection guidance, configuration details, and custom entry creation, see **[Tuning](#tuning)** below and **[docs/TUNING.md](./docs/TUNING.md)**.
205
+
180
206
  ---
181
207
 
182
208
  ## What it solves
@@ -197,17 +223,32 @@ OpenClaw 2026.4.7 ships memory wiki for structured storage. hypermem goes furthe
197
223
 
198
224
  Spawned subagents inherit a bounded context block: recent parent turns, session-scoped documents, and relevant facts. Scope is isolated from the shared library. Documents are cleaned up on completion.
199
225
 
226
+ ### Context that doesn't repeat itself
227
+
228
+ Retrieval paths pull from four layers, trigger shortcuts, temporal indexes, open-domain FTS5, semantic recall, and cross-session summaries. Without dedup, the same fact surfaces through multiple paths and wastes budget on repetition.
229
+
230
+ hypermem runs content fingerprint dedup across all compose-time retrieval. Every fact, temporal result, open-domain hit, and semantic recall entry is normalized and fingerprinted on a 120-char prefix. O(1) lookup in a shared set catches duplicates regardless of which retrieval path produced them, including rephrased near-duplicates that substring matching missed. Diagnostics track dedup counts and fingerprint collisions per compose call.
231
+
232
+ Identity content (SOUL.md, USER.md, IDENTITY.md) and doc chunks already injected by OpenClaw's bootstrap are fingerprinted before retrieval runs, so the compositor never double-injects content the runtime already placed in the prompt.
233
+
234
+ ### Integrity under failure
235
+
236
+ The background indexer runs a startup integrity check against `library.db` on every boot. If the schema is corrupt, tables are missing, or critical indexes are damaged, the indexer enters circuit-breaker mode: it logs the failure, skips indexing for the session, and avoids cascading writes into a broken database. The agent still runs with cached and in-memory data while the operator is notified.
237
+
238
+ SQL queries that interpolate datetime values are fully parameterized. FTS5 trigger terms are quoted to prevent injection through crafted content. These aren't theoretical: agentic sessions ingest arbitrary user and tool output into the fact store, and unparameterized queries on that path were a real attack surface.
239
+
200
240
  ---
201
241
 
202
242
  ## Pressure management
203
243
 
204
- hypermem composes context fresh on every turn, but a long-running session still accumulates history in its JSONL transcript. When that grows large enough, incoming tool results have nowhere to land and get silently stripped. Three automatic paths handle this:
244
+ hypermem composes context fresh on every turn, but a long-running session still accumulates history in its JSONL transcript. When that grows large enough, incoming tool results have nowhere to land and get silently stripped. Four automatic paths handle this:
205
245
 
206
246
  | Path | Trigger | Action |
207
247
  |---|---|---|
208
248
  | **Pressure-tiered tool-loop trim** | Any tool-loop turn | Measures projected occupancy before results land; trims large results at 80%+ and truncates the messages[] array for the current turn |
209
249
  | **AfterTurn trim** | Every turn at >80% | Pre-emptive headroom cut after the assistant replies, before the next turn arrives |
210
250
  | **Deep compaction** | compact() at >85% | Cuts in-memory cache to 25% budget and truncates JSONL to ~20% depth. Bypasses the normal reshape guard |
251
+ | **Reshape guard** | Structured tool history on downshift | `canPersistReshapedHistory()` blocks a lower-context snapshot from overwriting the full JSONL history |
211
252
 
212
253
  **The one thing these paths cannot fix:** a session whose JSONL transcript on disk is already at 98% when the gateway restarts. The JSONL loads into runtime context before any compaction runs. Check `session_status` on startup. If you're above 85%, start a fresh session.
213
254
 
@@ -281,6 +322,8 @@ Retrieval follows a fixed pipeline on every compose call:
281
322
 
282
323
  FTS5 queries use compound indexes on `agentId + sort key` and prefix optimization (3+ chars, capped at 8 terms, OR queries). These indexes yielded a 25% read improvement over baseline despite a 47% increase in stored data.
283
324
 
325
+ ### Retrieval pipeline
326
+
284
327
  **L4: Library DB.** Per-agent storage can't hold shared knowledge. Facts established by one agent, wiki pages synthesized from cross-agent topics, shared registry state: these belong to the system, not one agent. One shared SQLite database:
285
328
 
286
329
  | Collection | What it holds |
@@ -311,17 +354,17 @@ Facts are ranked by `confidence × recencyDecay`, where decay is exponential wit
311
354
 
312
355
  topic detection ──► scope retrieval to active thread
313
356
 
314
- ┌────┴────────────────────────────────────────────┐
315
- │ query 4 layers (parallel)
316
-
317
- │ L1 in-memory L2 History L3 Vectors L4 Library │
357
+ ┌────┴───────────────────────────────────────────────┐
358
+ │ query 4 layers (parallel)
359
+
360
+ │ L1 in-memory L2 History L3 Vectors L4 Library │
318
361
  │ hot state durable semantic facts/wiki │
319
362
  │ 0.1ms 0.16ms 0.29ms 0.08ms │
320
- └────┬────────────────────────────────────────────┘
363
+ └────┬───────────────────────────────────────────────┘
321
364
 
322
365
  budget allocator ──► 10 slots, fixed token cap
323
366
 
324
- tool compression ──► clusters by age, T0 3 turns full → T1 6k → T2 800 → T3 150-char stub
367
+ tool compression ──► clusterNeutralMessages() T0 full → T1 6k → T2 800 → T3 150-char stub
325
368
 
326
369
  keystone guard ──► high-signal turns survive pressure
327
370
 
@@ -340,14 +383,7 @@ Slot-level budget allocation is shown in the [hypercompositor diagram](#what-the
340
383
 
341
384
  ## Requirements
342
385
 
343
- **Current release: hypermem 0.5.4.** Topic-aware memory and compiled-knowledge system, optimized to run light by default and scale up when operators need richer context.
344
-
345
- What 0.5.4 includes:
346
- - Topic-aware context tracking
347
- - Compiled knowledge / wiki-like synthesis and recall
348
- - Metrics dashboard primitives
349
- - Obsidian import and export
350
- - Aligned runtime profiles: `light`, `standard`, `full`
386
+ **Current release: hypermem 0.5.6.** Changelog: [CHANGELOG.md](./CHANGELOG.md)
351
387
 
352
388
  | Requirement | Version | Notes |
353
389
  |---|---|---|
@@ -360,9 +396,8 @@ SQLite is a library, not a service. All four layers run in-process with no exter
360
396
  **Runtime version constants** (importable from the package):
361
397
  ```typescript
362
398
  import {
363
- ENGINE_VERSION, // '0.5.4'
399
+ ENGINE_VERSION, // '0.5.6'
364
400
  MIN_NODE_VERSION, // '22.0.0'
365
- MIN_SQLITE_VERSION, // '3.35.0'
366
401
  SQLITE_VEC_VERSION, // '0.1.9'
367
402
  MAIN_SCHEMA_VERSION, // 6 (hypermem.db)
368
403
  LIBRARY_SCHEMA_VERSION_EXPORT, // 12 (library.db)
@@ -407,54 +442,59 @@ If you prefer, hand the install to your OpenClaw agent:
407
442
 
408
443
  ### Tuning
409
444
 
410
- hypermem ships three aligned operating profiles: `light`, `standard`, and `full`. Pick one and set `outputProfile` in your config. Everything else follows.
411
-
412
- | Profile | Context window | Budget fraction | Best for |
413
- |---|---|---|---|
414
- | `light` | 64k | 0.50 | Single-agent installs, minimal parallel work |
415
- | `standard` | 128k | 0.65 | Normal OpenClaw deployments |
416
- | `full` | 200k+ | 0.55 | Large-context or multi-agent installs, maximum richness |
445
+ Two independent surfaces: **context assembly** (what fills the context window) and **output shaping** (how the model writes). Pick a profile first most deployments adjust one or two settings on top.
417
446
 
418
- **Start with `light`** on 64k models or single-agent systems. Move to `standard` once the system has stable latency and headroom. Use `full` only when you want maximum context richness and have the budget for it.
447
+ | Profile | Target window | Best for |
448
+ |---|---|---|
449
+ | `light` | 64k | Single agent, small models, constrained resources |
450
+ | `standard` | 128k | Normal deployments, small fleets |
451
+ | `full` | 200k+ | Multi-agent fleets, large-context models |
419
452
 
420
- Primary tuning knobs:
453
+ Start with `light`. Use `mergeProfile()` to adjust individual settings:
421
454
 
422
- - **`targetBudgetFraction`**: caps total non-history context weight. Lower values force lighter composition.
423
- - **`wikiTokenCap`**: caps compiled-knowledge/wiki contribution.
424
- - **`outputProfile`**: `light`, `standard`, or `full`. Controls how much hyperform guidance is injected per turn.
455
+ ```typescript
456
+ import { mergeProfile } from '@psiclawops/hypermem';
457
+ const config = mergeProfile('standard', { compositor: { maxFacts: 40 } });
458
+ ```
425
459
 
426
- Drop a `~/.openclaw/hypermem/config.json` to override compositor defaults. Takes effect on gateway restart:
460
+ Drop a `~/.openclaw/hypermem/config.json` to override defaults (takes effect on gateway restart):
427
461
 
428
462
  ```json
429
463
  {
430
- "deferToolPruning": true,
431
464
  "compositor": {
432
- "defaultTokenBudget": 60000,
433
- "maxFacts": 18,
434
- "contextWindowReserve": 0.25,
435
- "outputProfile": "standard"
465
+ "budgetFraction": 0.70,
466
+ "hyperformProfile": "standard"
436
467
  }
437
468
  }
438
469
  ```
439
470
 
440
- Additional compositor knobs: `maxCrossSessionContext`, `maxRecentToolPairs`, `maxProseToolPairs`, see INSTALL.md for full descriptions.
441
-
442
- `deferToolPruning: true` tells hypermem to skip its own T0/T1/T2/T3 gradient when OpenClaw's native `contextPruning` extension is active (Anthropic and Google providers). On those providers, OpenClaw's pruner handles tool result trimming: ratio-driven at >30% context fill, soft-trim head+tail for results over 4,000 chars, hard-clear above 50k total, with the last 3 assistant turns always protected. hypermem's gradient remains active as fallback for other providers (GPT-5.4, etc.). Default: `true` for Anthropic installs.
443
-
444
- `outputProfile` valid values: `"light"` (~100 tokens: anti-sycophancy, em dash ban, AI vocab ban, length targets, evidence calibration), `"standard"` (~250 tokens: full directive set plus pagination and hedging rules), `"full"` (~400 tokens: complete normalization with full directive set and model-specific calibration). Default: `"standard"`.
471
+ Or configure through `openclaw.json` (preferred for managed deployments):
445
472
 
446
- Context presets ship as named profiles importable from the package:
447
-
448
- ```typescript
449
- import { lightProfile, standardProfile, fullProfile } from '@psiclawops/hypermem';
473
+ ```json
474
+ {
475
+ "plugins": {
476
+ "entries": {
477
+ "hypercompositor": {
478
+ "config": {
479
+ "compositor": { "budgetFraction": 0.70 },
480
+ "hyperformProfile": "standard"
481
+ }
482
+ }
483
+ }
484
+ }
485
+ }
450
486
  ```
451
487
 
452
- Pass to `HyperMem.create()` as the base config. Full tuning notes are in INSTALL.md.
488
+ Plugin config in `openclaw.json` takes precedence over `config.json`. Both sources are merged, with plugin config winning on overlap. The config schema is validated on gateway start and visible via `openclaw config get plugins.entries.hypercompositor.config`.
489
+
490
+ Full reference: **[docs/TUNING.md](./docs/TUNING.md)**
453
491
 
454
492
  ---
455
493
 
456
494
  ## API
457
495
 
496
+ > **Note:** The examples below use placeholder agent names (`my-agent`, `agent1`, etc.). Replace these with your actual agent IDs from your OpenClaw config. Single-agent installs typically use `main`. Multi-agent fleets use whatever IDs you've configured. See [INSTALL.md § "Configure your fleet"](./INSTALL.md#step-5--configure-your-fleet-multi-agent-only) for details.
497
+
458
498
  ```typescript
459
499
  import { HyperMem } from '@psiclawops/hypermem';
460
500
 
@@ -468,18 +508,18 @@ const hm = await HyperMem.create({
468
508
  });
469
509
 
470
510
  // Record and compose
471
- await hm.recordUserMessage('forge', 'agent:forge:webchat:main', 'How does drift detection work?');
511
+ await hm.recordUserMessage('my-agent', 'agent:my-agent:webchat:main', 'How does drift detection work?');
472
512
 
473
513
  const composed = await hm.compose({
474
- agentId: 'forge',
475
- sessionKey: 'agent:forge:webchat:main',
514
+ agentId: 'my-agent',
515
+ sessionKey: 'agent:my-agent:webchat:main',
476
516
  prompt: 'How does drift detection work?',
477
517
  tokenBudget: 4000,
478
518
  provider: 'anthropic',
479
519
  });
480
520
 
481
521
  // Refresh tool compression after each turn
482
- await hm.refreshCacheGradient('forge', 'agent:forge:webchat:main');
522
+ await hm.refreshCacheGradient('my-agent', 'agent:my-agent:webchat:main');
483
523
  ```
484
524
 
485
525
  Spawning a subagent with parent context:
@@ -488,10 +528,10 @@ Spawning a subagent with parent context:
488
528
  import { buildSpawnContext, MessageStore, DocChunkStore } from '@psiclawops/hypermem';
489
529
 
490
530
  const spawn = await buildSpawnContext(
491
- new MessageStore(hm.dbManager.getMessageDb('forge')),
531
+ new MessageStore(hm.dbManager.getMessageDb('my-agent')),
492
532
  new DocChunkStore(hm.dbManager.getLibraryDb()),
493
- 'forge',
494
- { parentSessionKey: 'agent:forge:webchat:main', workingSnapshot: 12 }
533
+ 'my-agent',
534
+ { parentSessionKey: 'agent:my-agent:webchat:main', workingSnapshot: 12 }
495
535
  );
496
536
  ```
497
537
 
@@ -503,7 +543,7 @@ const spawn = await buildSpawnContext(
503
543
 
504
544
  ```bash
505
545
  node bin/hypermem-status.mjs # full dashboard
506
- node bin/hypermem-status.mjs --agent forge # scoped to one agent
546
+ node bin/hypermem-status.mjs --agent my-agent # scoped to one agent
507
547
  node bin/hypermem-status.mjs --json # machine-readable output
508
548
  node bin/hypermem-status.mjs --health # health checks only (exit 1 on failure)
509
549
  ```
@@ -545,6 +585,12 @@ Design guide: [PsiClawOps/AgenticCognitiveArchitecture](https://github.com/PsiCl
545
585
 
546
586
  ---
547
587
 
588
+ ## Acknowledgments
589
+
590
+ The embedding-space fidelity threshold used in compaction validation was informed by the geometric preservation mathematics published by the [libravdb](https://github.com/xDarkicex/openclaw-memory-libravdb) project.
591
+
592
+ ---
593
+
548
594
  ## License
549
595
 
550
596
  Apache-2.0, [PsiClawOps](https://github.com/PsiClawOps)
@@ -56,6 +56,10 @@ export declare class BackgroundIndexer {
56
56
  private vectorStore;
57
57
  private synthesizer;
58
58
  private tickCount;
59
+ /** Circuit breaker: consecutive tick failure count. Resets on success. */
60
+ private consecutiveFailures;
61
+ /** True when the indexer is running in backoff mode due to repeated failures. */
62
+ private inBackoff;
59
63
  constructor(config?: Partial<IndexerConfig>, getMessageDb?: ((agentId: string) => DatabaseSync) | undefined, getLibraryDb?: (() => DatabaseSync) | undefined, listAgents?: (() => string[]) | undefined, getCursor?: CursorFetcher | undefined, dreamerConfig?: Partial<DreamerConfig>);
60
64
  /**
61
65
  * Set the vector store for embedding new facts/episodes at index time.
@@ -66,6 +70,20 @@ export declare class BackgroundIndexer {
66
70
  * Start periodic indexing.
67
71
  */
68
72
  start(): void;
73
+ /**
74
+ * Circuit breaker for tick failures.
75
+ *
76
+ * - Tracks consecutive failures.
77
+ * - After 3 failures, logs actionable recovery guidance once, then switches
78
+ * the indexer to 10× backoff interval so it stops spamming the log.
79
+ * - On the next successful tick, resets state and restores normal interval.
80
+ */
81
+ private _handleTickError;
82
+ /**
83
+ * Reset the circuit breaker and restore normal interval after a successful tick.
84
+ * Called at the end of a successful tick().
85
+ */
86
+ private _resetCircuitBreaker;
69
87
  /**
70
88
  * Stop periodic indexing.
71
89
  */
@@ -1 +1 @@
1
- {"version":3,"file":"background-indexer.d.ts","sourceRoot":"","sources":["../src/background-indexer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"background-indexer.d.ts","sourceRoot":"","sources":["../src/background-indexer.ts"],"names":[],"mappings":"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"}
@@ -32,23 +32,31 @@ import { isSafeForSharedVisibility } from './secret-scanner.js';
32
32
  // Used to populate the `domain` column on extracted facts so that
33
33
  // domain-scoped retrieval (e.g. getActiveFacts({ domain: 'infrastructure' }))
34
34
  // returns results. New agents default to 'general'.
35
+ //
36
+ // ── EXAMPLE DATA ──────────────────────────────────────────────────
37
+ // The agent names below (agent1, director1, etc.) are PLACEHOLDERS.
38
+ // Replace them with your own agent IDs and domain labels to match
39
+ // your fleet. Single-agent installs don't need to edit this:
40
+ // unknown agents fall through to 'general' automatically.
41
+ // See INSTALL.md § "Configure your fleet" for details.
42
+ // ─────────────────────────────────────────────────────────────────
35
43
  const AGENT_DOMAIN_MAP = {
36
- forge: 'infrastructure',
37
- vigil: 'infrastructure',
38
- pylon: 'infrastructure',
39
- plane: 'infrastructure',
40
- compass: 'product',
41
- helm: 'product',
42
- chisel: 'product',
43
- facet: 'product',
44
- sentinel: 'security',
45
- bastion: 'security',
46
- gauge: 'security',
47
- clarity: 'ux',
48
- anvil: 'governance',
49
- vanguard: 'strategy',
50
- crucible: 'development',
51
- relay: 'communications',
44
+ agent1: 'infrastructure',
45
+ director2: 'infrastructure',
46
+ director1: 'infrastructure',
47
+ director3: 'infrastructure',
48
+ agent2: 'product',
49
+ director4: 'product',
50
+ director5: 'product',
51
+ director6: 'product',
52
+ agent3: 'security',
53
+ director7: 'security',
54
+ director8: 'security',
55
+ agent4: 'ux',
56
+ agent6: 'governance',
57
+ agent5: 'strategy',
58
+ specialist1: 'development',
59
+ specialist2: 'communications',
52
60
  main: 'general',
53
61
  'channel-mini': 'general',
54
62
  };
@@ -83,7 +91,7 @@ function extractFactCandidates(content) {
83
91
  // Preference patterns — medium confidence (0.60)
84
92
  const preferencePatterns = [
85
93
  /(?:prefer|always use|never use|don't use|avoid) (.{10,150})/gi,
86
- /(?:ragesaq|operator) (?:wants|prefers|likes|hates|dislikes) (.{10,150})/gi,
94
+ /(?:operator|operator) (?:wants|prefers|likes|hates|dislikes) (.{10,150})/gi,
87
95
  ];
88
96
  // Operational patterns: deployments, incidents, fixes — high confidence (0.70)
89
97
  const operationalPatterns = [
@@ -137,7 +145,7 @@ const OPERATIONAL_BOILERPLATE = [
137
145
  /still\s*waiting/i,
138
146
  /will\s*pick\s*(it\s*)?up\s*(on\s*(next|the))?/i,
139
147
  /message\s*is\s*in\s*(his|her|their|the)\s*queue/i,
140
- /sent\s+to\s+(anvil|compass|clarity|sentinel|vanguard|forge)/i,
148
+ /sent\s+to\s+(agent6|agent2|agent4|agent3|agent5|agent1)/i,
141
149
  /dispatched\s+(it\s+)?to/i,
142
150
  /timed\s*out\s*after/i,
143
151
  /\bNO_REPLY\b/,
@@ -408,6 +416,10 @@ export class BackgroundIndexer {
408
416
  vectorStore = null;
409
417
  synthesizer = null;
410
418
  tickCount = 0;
419
+ /** Circuit breaker: consecutive tick failure count. Resets on success. */
420
+ consecutiveFailures = 0;
421
+ /** True when the indexer is running in backoff mode due to repeated failures. */
422
+ inBackoff = false;
411
423
  constructor(config, getMessageDb, getLibraryDb, listAgents, getCursor, dreamerConfig) {
412
424
  this.getMessageDb = getMessageDb;
413
425
  this.getLibraryDb = getLibraryDb;
@@ -457,9 +469,31 @@ export class BackgroundIndexer {
457
469
  return;
458
470
  if (this.intervalHandle)
459
471
  return;
472
+ // Startup integrity check — catch corruption before the first tick writes anything.
473
+ if (this.getLibraryDb) {
474
+ try {
475
+ const libDb = this.getLibraryDb();
476
+ if (libDb) {
477
+ const row = libDb.prepare('PRAGMA quick_check').get();
478
+ if (row?.integrity_check && row.integrity_check !== 'ok') {
479
+ console.error('[indexer] ⚠️ library.db integrity check failed: ' + row.integrity_check + '\n' +
480
+ '[indexer] Recovery: stop OpenClaw, run ' +
481
+ '`sqlite3 ~/.openclaw/hypermem/library.db ".recover" | sqlite3 ~/.openclaw/hypermem/library_recovered.db`' +
482
+ ', swap the files, and restart. If recovery fails, delete library.db — the indexer rebuilds from message history.');
483
+ // Don't start the interval — nothing will succeed with a corrupt DB.
484
+ return;
485
+ }
486
+ }
487
+ }
488
+ catch (err) {
489
+ // If we can't even open the DB, log and bail — don't start the interval.
490
+ console.error('[indexer] Could not open library.db for integrity check:', err.message);
491
+ return;
492
+ }
493
+ }
460
494
  // Run once immediately
461
495
  this.tick().catch(err => {
462
- console.error('[indexer] Initial tick failed:', err);
496
+ this._handleTickError(err, 'initial');
463
497
  });
464
498
  // Run episode vector backfill once at startup (no-op if already done)
465
499
  if (this.vectorStore && this.getLibraryDb) {
@@ -470,11 +504,83 @@ export class BackgroundIndexer {
470
504
  // Then periodically
471
505
  this.intervalHandle = setInterval(() => {
472
506
  this.tick().catch(err => {
473
- console.error('[indexer] Periodic tick failed:', err);
507
+ this._handleTickError(err, 'periodic');
474
508
  });
475
509
  }, this.config.periodicInterval);
476
510
  console.log(`[indexer] Started with interval ${this.config.periodicInterval}ms, batchSize ${this.config.batchSize}, maxPerTick ${this.config.maxMessagesPerTick}`);
477
511
  }
512
+ /**
513
+ * Circuit breaker for tick failures.
514
+ *
515
+ * - Tracks consecutive failures.
516
+ * - After 3 failures, logs actionable recovery guidance once, then switches
517
+ * the indexer to 10× backoff interval so it stops spamming the log.
518
+ * - On the next successful tick, resets state and restores normal interval.
519
+ */
520
+ _handleTickError(err, phase) {
521
+ this.consecutiveFailures++;
522
+ const msg = err instanceof Error ? err.message : String(err);
523
+ const isSqliteCorrupt = msg.includes('database disk image is malformed') ||
524
+ msg.includes('SQLITE_CORRUPT') ||
525
+ (err instanceof Error && 'code' in err && err.code === 'ERR_SQLITE_ERROR');
526
+ if (this.consecutiveFailures < 3) {
527
+ // First 1–2 failures: log normally.
528
+ console.error(`[indexer] ${phase === 'initial' ? 'Initial' : 'Periodic'} tick failed (attempt ${this.consecutiveFailures}/3):`, err);
529
+ return;
530
+ }
531
+ if (this.consecutiveFailures === 3) {
532
+ // Third failure: log once with recovery instructions, then enter backoff.
533
+ if (isSqliteCorrupt) {
534
+ console.error(`[indexer] ⛔ Tick failed 3 times consecutively — library.db appears corrupted. Entering backoff mode.\n` +
535
+ `[indexer] Recovery steps:\n` +
536
+ `[indexer] 1. Stop OpenClaw: openclaw gateway stop\n` +
537
+ `[indexer] 2. Check damage: sqlite3 ~/.openclaw/hypermem/library.db "PRAGMA integrity_check"\n` +
538
+ `[indexer] 3. Attempt recovery: sqlite3 ~/.openclaw/hypermem/library.db ".recover" | sqlite3 ~/.openclaw/hypermem/library_recovered.db\n` +
539
+ `[indexer] 4. Swap: mv library.db library_corrupt.bak && mv library_recovered.db library.db\n` +
540
+ `[indexer] 5. If recovery fails, delete library.db — the indexer rebuilds from message history on next start.\n` +
541
+ `[indexer] 6. Restart: openclaw gateway start\n` +
542
+ `[indexer] Indexer will retry every ${(this.config.periodicInterval * 10) / 60000} minutes until then.`);
543
+ }
544
+ else {
545
+ console.error(`[indexer] ⛔ Tick failed 3 times consecutively (${msg}). Entering backoff mode. ` +
546
+ `Will retry every ${(this.config.periodicInterval * 10) / 60000} minutes.`);
547
+ }
548
+ // Switch to backoff interval.
549
+ this.inBackoff = true;
550
+ if (this.intervalHandle) {
551
+ clearInterval(this.intervalHandle);
552
+ }
553
+ this.intervalHandle = setInterval(() => {
554
+ this.tick().catch(backoffErr => {
555
+ this._handleTickError(backoffErr, 'periodic');
556
+ });
557
+ }, this.config.periodicInterval * 10);
558
+ return;
559
+ }
560
+ // Beyond 3: silent (already logged, in backoff — don't spam).
561
+ }
562
+ /**
563
+ * Reset the circuit breaker and restore normal interval after a successful tick.
564
+ * Called at the end of a successful tick().
565
+ */
566
+ _resetCircuitBreaker() {
567
+ if (this.consecutiveFailures === 0)
568
+ return;
569
+ const wasInBackoff = this.inBackoff;
570
+ this.consecutiveFailures = 0;
571
+ this.inBackoff = false;
572
+ if (wasInBackoff) {
573
+ // Restore normal interval.
574
+ if (this.intervalHandle)
575
+ clearInterval(this.intervalHandle);
576
+ this.intervalHandle = setInterval(() => {
577
+ this.tick().catch(err => {
578
+ this._handleTickError(err, 'periodic');
579
+ });
580
+ }, this.config.periodicInterval);
581
+ console.log('[indexer] Circuit breaker reset — tick succeeded, restored normal interval.');
582
+ }
583
+ }
478
584
  /**
479
585
  * Stop periodic indexing.
480
586
  */
@@ -494,6 +600,7 @@ export class BackgroundIndexer {
494
600
  }
495
601
  this.running = true;
496
602
  const results = [];
603
+ let tickSucceeded = false;
497
604
  try {
498
605
  if (!this.listAgents || !this.getMessageDb || !this.getLibraryDb) {
499
606
  console.warn('[indexer] Missing database accessors — skipping');
@@ -601,8 +708,12 @@ export class BackgroundIndexer {
601
708
  }
602
709
  }
603
710
  }
711
+ // If we reach here, the tick completed without throwing.
712
+ tickSucceeded = true;
604
713
  }
605
714
  finally {
715
+ if (tickSucceeded)
716
+ this._resetCircuitBreaker();
606
717
  this.running = false;
607
718
  }
608
719
  return results;
package/dist/cache.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Same public interface, zero external dependencies, zero TCP overhead.
6
6
  */
7
7
  import { DatabaseSync } from 'node:sqlite';
8
- import type { CacheConfig, SessionMeta, SessionCursor, StoredMessage, NeutralMessage } from './types.js';
8
+ import type { CacheConfig, ComposeDiagnostics, SessionMeta, SessionCursor, StoredMessage, NeutralMessage } from './types.js';
9
9
  export interface ModelState {
10
10
  model: string;
11
11
  tokenBudget: number;
@@ -13,6 +13,13 @@ export interface ModelState {
13
13
  historyDepth: number;
14
14
  reshapedAt?: string;
15
15
  }
16
+ export interface WindowCacheMeta {
17
+ slots: Record<string, number>;
18
+ totalTokens: number;
19
+ warnings: string[];
20
+ diagnostics: ComposeDiagnostics;
21
+ composedAt: string;
22
+ }
16
23
  export declare class CacheLayer {
17
24
  private db;
18
25
  private readonly config;
@@ -41,6 +48,7 @@ export declare class CacheLayer {
41
48
  private stmtEvictHistory;
42
49
  private stmtSetWindow;
43
50
  private stmtGetWindow;
51
+ private stmtGetFreshWindowBundle;
44
52
  private stmtDeleteWindow;
45
53
  private stmtEvictWindows;
46
54
  private stmtSetKv;
@@ -67,6 +75,21 @@ export declare class CacheLayer {
67
75
  setWindow(agentId: string, sessionKey: string, messages: NeutralMessage[], ttlSeconds?: number): Promise<void>;
68
76
  getWindow(agentId: string, sessionKey: string): Promise<NeutralMessage[] | null>;
69
77
  invalidateWindow(agentId: string, sessionKey: string): Promise<void>;
78
+ /**
79
+ * Returns the cached window + metadata only if a single read shows the cache
80
+ * and cursor still refer to the same composed window.
81
+ * Used for C4 window cache fast-exit in compositor.ts.
82
+ */
83
+ getFreshWindowBundle(agentId: string, sessionKey: string, lastMessageId: number): Promise<{
84
+ messages: NeutralMessage[];
85
+ meta: WindowCacheMeta;
86
+ } | null>;
87
+ /**
88
+ * Store compose result metadata alongside the window cache.
89
+ * Enables the C4 fast-exit to return a complete ComposeResult without re-running.
90
+ */
91
+ setWindowMeta(agentId: string, sessionKey: string, meta: WindowCacheMeta, ttl: number): Promise<void>;
92
+ getWindowMeta(agentId: string, sessionKey: string): Promise<WindowCacheMeta | null>;
70
93
  setCursor(agentId: string, sessionKey: string, cursor: SessionCursor): Promise<void>;
71
94
  getCursor(agentId: string, sessionKey: string): Promise<SessionCursor | null>;
72
95
  warmSession(agentId: string, sessionKey: string, slots: {