hippo-memory 1.45.0 → 1.47.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 +58 -15
- package/bin/hippo.js +0 -0
- package/dist/ablation.d.ts +10 -1
- package/dist/ablation.js +17 -1
- package/dist/api.d.ts +66 -1
- package/dist/api.js +202 -7
- package/dist/audit.d.ts +1 -1
- package/dist/capture-error.d.ts +26 -0
- package/dist/capture-error.js +110 -0
- package/dist/capture.d.ts +25 -8
- package/dist/capture.js +100 -5
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +432 -48
- package/dist/config.d.ts +20 -0
- package/dist/config.js +35 -0
- package/dist/consolidate.d.ts +6 -0
- package/dist/consolidate.js +98 -13
- package/dist/db.js +81 -1
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.js +183 -0
- package/dist/dormant.d.ts +91 -0
- package/dist/dormant.js +121 -0
- package/dist/eval-stats.d.ts +123 -0
- package/dist/eval-stats.js +187 -0
- package/dist/failure-log.d.ts +49 -0
- package/dist/failure-log.js +58 -0
- package/dist/half-life-migration.d.ts +55 -0
- package/dist/half-life-migration.js +111 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/hooks.js +47 -0
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.js +70 -13
- package/dist/memory.d.ts +16 -2
- package/dist/memory.js +27 -5
- package/dist/physics-config.js +5 -1
- package/dist/recall-scope.d.ts +24 -0
- package/dist/recall-scope.js +41 -0
- package/dist/reject-flow.d.ts +3 -3
- package/dist/reject-flow.js +10 -3
- package/dist/search.d.ts +4 -4
- package/dist/search.js +23 -18
- package/dist/server.js +11 -1
- package/dist/store.d.ts +12 -1
- package/dist/store.js +58 -12
- package/dist/token-ledger.d.ts +119 -0
- package/dist/token-ledger.js +181 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🦛 Hippo
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Hippo learns what is wrong and stops repeating it.** Good memory is knowing what to forget: what turned out wrong, what got replaced, what nobody used.
|
|
4
4
|
|
|
5
5
|
[](https://npmjs.com/package/hippo-memory)
|
|
6
6
|
[](./LICENSE)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<img src="./assets/hippo-init.svg" alt="hippo init --scan ~ — initializing memory across all repos" width="720">
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
A memory layer for AI agents.
|
|
13
|
+
A memory layer for AI agents. Mark a memory wrong and it stops coming back. A newer fact replaces the old one. Memories you use get stronger. Provenance on every memory. SQLite under the hood, zero runtime deps, works with every CLI agent you have.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm install -g hippo-memory && hippo init --scan ~
|
|
@@ -18,6 +18,8 @@ npm install -g hippo-memory && hippo init --scan ~
|
|
|
18
18
|
|
|
19
19
|
One command. Every git repo on your machine gets memory.
|
|
20
20
|
|
|
21
|
+
Having an AI agent install it? Point it at [llms-install.md](llms-install.md): it installs, wires hippo into the agents it finds, and verifies with `hippo doctor`.
|
|
22
|
+
|
|
21
23
|
```
|
|
22
24
|
Works with: Claude Code, Codex, Cursor, OpenClaw, OpenCode, Pi, any MCP client
|
|
23
25
|
Imports from: ChatGPT, Claude (CLAUDE.md), Cursor (.cursorrules), Slack, markdown
|
|
@@ -31,7 +33,7 @@ Dependencies: Zero runtime deps. Node.js 22.16+. Optional embeddings: bring-you
|
|
|
31
33
|
|
|
32
34
|
Most "AI memory" systems save everything and search later. That's storage with semantic search bolted on. It's why your agent kept hitting the same deploy bug last week. And the week before. The system saw the failure four times. It had no way to know it should remember.
|
|
33
35
|
|
|
34
|
-
Hippo
|
|
36
|
+
Hippo learns from outcomes. When a recalled memory turns out wrong, mark it bad and it drops out of the top results. When a fact changes, the new version supersedes the old one. Memories you keep using get stronger. Those are the parts we measured helping ([mechanism audit, round 2](https://github.com/kitfunso/hippo-memory/pull/232)). The design borrows from the hippocampus (decay, three layers, sleep consolidation), but that is inspiration. We have not measured decay or sleep making recall better.
|
|
35
37
|
|
|
36
38
|
It also fixes the portability problem. Your ChatGPT memories don't travel to Claude. Your `.cursorrules` don't travel to Codex. Hippo is one process behind every agent. CLAUDE.md, Cursor rules, ChatGPT exports, Slack history, all in one SQLite store, all queryable from any tool that speaks MCP or HTTP.
|
|
37
39
|
|
|
@@ -297,16 +299,15 @@ sequenceDiagram
|
|
|
297
299
|
|
|
298
300
|
### Decay by default
|
|
299
301
|
|
|
300
|
-
Every memory has a half-life. 7 days
|
|
302
|
+
Every memory has a half-life: 365 days by default. Until 1.46.0 the default was 7 days. A pre-registered evaluation found 7 days lost the current version of a fact far more often: it was in the top five 29% of the time at 7 days and 75% at 365 ([result](docs/evals/2026-09-24-decay-default-result.md)). 730 days and decay off both tied with 365. So 365 was not tuned: it is the tested value that tied with the others, and on that test decay made no measurable difference to recall. `hippo sleep` moves memories still on the old 7-day base to the new one, once, and records each move in the audit log. Set `defaultHalfLifeDays` in `.hippo/config.json` to choose your own.
|
|
301
303
|
|
|
302
304
|
```bash
|
|
303
305
|
hippo remember "always check cache contents after refresh"
|
|
304
|
-
# stored with half_life:
|
|
306
|
+
# stored with half_life: 365d, strength: 1.0
|
|
305
307
|
|
|
306
|
-
#
|
|
308
|
+
# two years later with no retrieval:
|
|
307
309
|
hippo inspect mem_a1b2c3
|
|
308
310
|
# strength: 0.25 (decayed by 2 half-lives)
|
|
309
|
-
# at risk of removal on next sleep
|
|
310
311
|
```
|
|
311
312
|
|
|
312
313
|
---
|
|
@@ -318,12 +319,12 @@ Use it or lose it. Each recall boosts the half-life by 2 days.
|
|
|
318
319
|
```bash
|
|
319
320
|
hippo recall "cache issues"
|
|
320
321
|
# finds mem_a1b2c3, retrieval_count: 1 -> 2
|
|
321
|
-
# half_life extended:
|
|
322
|
+
# half_life extended: 365d -> 367d
|
|
322
323
|
# strength recalculated from retrieval timestamp
|
|
323
324
|
|
|
324
325
|
hippo recall "cache issues" # again next week
|
|
325
326
|
# retrieval_count: 2 -> 3
|
|
326
|
-
# half_life:
|
|
327
|
+
# half_life: 367d -> 369d
|
|
327
328
|
# this memory is learning to survive
|
|
328
329
|
```
|
|
329
330
|
|
|
@@ -454,6 +455,8 @@ hippo sleep
|
|
|
454
455
|
|
|
455
456
|
Three or more related episodes get merged into a single semantic memory. The originals decay. The pattern survives.
|
|
456
457
|
|
|
458
|
+
Sleep keeps the store tidy. It has not been shown to improve recall. In round 2 of the mechanism audit, a slept LongMemEval store scored 3.6 points lower at hit@5 than the same store never slept, and no scorer showed sleep helping ([PR #232](https://github.com/kitfunso/hippo-memory/pull/232)).
|
|
459
|
+
|
|
457
460
|
**Experimental: learned memory-value rescue (opt-in, default off).** With
|
|
458
461
|
`{"memoryValue":{"enabled":true}}` in `.hippo/config.json`, sleep consults a learned
|
|
459
462
|
linear memory-value scorer before deleting a decayed memory: a memory that scores in the
|
|
@@ -467,6 +470,34 @@ usage, NOT real usage value — treat the flag as an experiment, not a recommend
|
|
|
467
470
|
Tenants with fewer than 10 non-pinned memories never rescue (rank statistics are noise at
|
|
468
471
|
tiny scale).
|
|
469
472
|
|
|
473
|
+
**Faded memories go dormant, not gone (on by default).** Sleep moves a memory that faded
|
|
474
|
+
below the decay threshold into a dormant store instead of deleting it. A dormant memory
|
|
475
|
+
leaves recall and context exactly like a deleted one and sits out every later sleep, so
|
|
476
|
+
your agent's context stays as lean as before, but nothing is lost:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
hippo dormant # list, newest first (--json, --limit <n>)
|
|
480
|
+
hippo dormant "staging hostname" # search: every term must match
|
|
481
|
+
hippo dormant restore mem_a1b2c3 # back to active memory, as if just recalled
|
|
482
|
+
hippo dormant forget mem_a1b2c3 # delete for good
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
A restored memory comes back with a fresh recall clock, so it gets a full half-life before
|
|
486
|
+
it can fade again, and every restore is logged (`hippo audit list --op dormant_restore`) as
|
|
487
|
+
a "forgot it, then needed it" signal. Two guardrails: a faded memory that the secret
|
|
488
|
+
detector flags is deleted, never kept dormant, and a dormant memory nobody restores within
|
|
489
|
+
`dormant.retentionDays` (default 180, `0` keeps them forever) is deleted for good. Rejecting
|
|
490
|
+
a value (`hippo reject`) removes its dormant copies too. To delete faded memories straight
|
|
491
|
+
away as before, set `{"dormant":{"enabled":false}}` in `.hippo/config.json`. Sleep never
|
|
492
|
+
removes pinned memories or raw receipts (Slack, GitHub, vault imports) either way, and
|
|
493
|
+
duplicate removal and junk cleanup still delete.
|
|
494
|
+
|
|
495
|
+
**See what memory costs in tokens.** Every block of memory text hippo hands an agent (the
|
|
496
|
+
per-prompt hook, `hippo context`, `hippo recall`, the MCP tools, the HTTP API) is recorded
|
|
497
|
+
in a token ledger: counts, surface and session, never the text. `hippo tokens` shows the
|
|
498
|
+
totals for the last 30 days (`--days`, `--json`). Counts are estimates (characters / 4), the
|
|
499
|
+
same estimate every budget uses. Rows older than 90 days are pruned.
|
|
500
|
+
|
|
470
501
|
---
|
|
471
502
|
|
|
472
503
|
### Outcome feedback
|
|
@@ -488,6 +519,8 @@ hippo outcome --bad
|
|
|
488
519
|
|
|
489
520
|
Outcomes are cumulative. A memory with 5 positive outcomes and 0 negative has a reward factor of ~1.42, making its effective half-life 42% longer. A memory with 0 positive and 3 negative has a factor of ~0.63, decaying nearly twice as fast. Mixed outcomes converge toward neutral (1.0).
|
|
490
521
|
|
|
522
|
+
This is the mechanism with the clearest measured win. On the synthetic E1 test, plain BM25 plus the outcome nudge cut how often a marked-bad memory stayed in the top five from 71.9% to 0.0% ([mechanism audit, round 2](https://github.com/kitfunso/hippo-memory/pull/232)). Every mark in E1 is correct; real marks are noisier, since `--bad` marks the whole recall batch.
|
|
523
|
+
|
|
491
524
|
---
|
|
492
525
|
|
|
493
526
|
### Token budgets
|
|
@@ -592,6 +625,12 @@ hippo watch "npm run build"
|
|
|
592
625
|
| `hippo outcome --id <id> --good` | Target a specific memory |
|
|
593
626
|
| `hippo inspect <id>` | Full detail on one memory |
|
|
594
627
|
| `hippo forget <id>` | Force remove a memory |
|
|
628
|
+
| `hippo dormant [<query>]` | List faded memories sleep kept instead of deleting |
|
|
629
|
+
| `hippo dormant restore <id>` | Bring a dormant memory back to active memory |
|
|
630
|
+
| `hippo dormant forget <id>` | Delete a dormant memory permanently |
|
|
631
|
+
| `hippo doctor [--json]` | Check the install: Node, store, schema, sleep, agent hooks; each problem names its fix |
|
|
632
|
+
| `hippo tokens [--days n]` | Estimated tokens of memory text handed to agents, per surface, and what skipping unchanged hook blocks saved |
|
|
633
|
+
| `hippo failures [--days n]` | Failed tool calls the capture-error hook saw, by outcome, and how many errors first happened in another session |
|
|
595
634
|
| `hippo embed` | Embed all memories for semantic search |
|
|
596
635
|
| `hippo embed --status` | Show embedding coverage |
|
|
597
636
|
| `hippo watch "<command>"` | Run command, auto-learn from failures |
|
|
@@ -683,9 +722,11 @@ This adds a `<!-- hippo:start -->` ... `<!-- hippo:end -->` block that tells the
|
|
|
683
722
|
For Claude Code, it also adds:
|
|
684
723
|
- a `SessionEnd` hook so `hippo sleep` runs automatically when the session exits
|
|
685
724
|
- a `SessionStart` hook that prints the previous session's consolidation output
|
|
686
|
-
- a `UserPromptSubmit` hook that runs `hippo context --pinned-only --include-recent 5 --format additional-context` every turn. It re-injects pinned memories (`hippo remember <text> --pin`) plus the last 5 writes, so fresh same-session lessons appear on the next prompt before you pin them. Opt out with `{"pinnedInject":{"enabled":false}}` in `.hippo/config.json`.
|
|
725
|
+
- a `UserPromptSubmit` hook that runs `hippo context --pinned-only --include-recent 5 --format additional-context` every turn. It re-injects pinned memories (`hippo remember <text> --pin`) plus the last 5 writes, so fresh same-session lessons appear on the next prompt before you pin them. The block is rendered without live strength percentages, so it stays byte-identical while its memories do not change, and it is sent only when it changed since the session's last prompt: an unchanged block is skipped, resent every 10 skips (`pinnedInject.refreshTurns`, `0` never resends) and resent after compaction. `{"pinnedInject":{"skipUnchanged":false}}` sends it every turn as before. Opt out entirely with `{"pinnedInject":{"enabled":false}}` in `.hippo/config.json`.
|
|
687
726
|
- a `PreCompact` hook that runs `hippo pre-compact` before the transcript gets summarized. It saves a working-state snapshot (task/summary/next step) and extracts durable memories from the tail, so mid-session compaction can't drop them.
|
|
688
727
|
- a second `SessionStart` hook (matcher `compact`) that runs `hippo compact-resume`, printing that snapshot plus the recent session trail back into context right after compaction.
|
|
728
|
+
- a `PostCompact` hook that runs `hippo post-compact`, which tells you what was saved ("Hippo saved your task snapshot and 2 new memories before compacting"). It prints nothing when nothing was saved.
|
|
729
|
+
- a `PostToolUseFailure` hook that runs `hippo capture-error`, which stores a failed tool call as an error memory. It skips interrupts, declined permissions and searches that found nothing, and stores a repeated failure once. It also logs every failure, stored or not, for `hippo failures`: the session, the tool and hashes of the error, never its text. A hash is not anonymous, since anyone who guesses an error's text can check it against the hash. The log keeps 90 days.
|
|
689
730
|
|
|
690
731
|
To remove: `hippo hook uninstall claude-code`
|
|
691
732
|
|
|
@@ -725,7 +766,9 @@ Add to your MCP config (e.g. `.cursor/mcp.json` or `claude_desktop_config.json`)
|
|
|
725
766
|
}
|
|
726
767
|
```
|
|
727
768
|
|
|
728
|
-
|
|
769
|
+
No global install needed: `"command": "npx", "args": ["-y", "hippo-memory", "mcp"]` works too. With no store anywhere, the first tool call creates the global store (`~/.hippo`); `hippo init` in a project adds a project store. Check any install with `hippo doctor`.
|
|
770
|
+
|
|
771
|
+
Exposes 13 tools: `hippo_recall`, `hippo_assemble`, `hippo_drill`, `hippo_remember`, `hippo_outcome`, `hippo_context`, `hippo_status`, `hippo_learn`, `hippo_conflicts`, `hippo_resolve`, `hippo_share`, `hippo_peers`, `hippo_predict_baserate`.
|
|
729
772
|
|
|
730
773
|
### OpenClaw Plugin
|
|
731
774
|
|
|
@@ -755,11 +798,11 @@ Full integration details: [integrations/](integrations/)
|
|
|
755
798
|
|
|
756
799
|
## The Neuroscience
|
|
757
800
|
|
|
758
|
-
Hippo
|
|
801
|
+
Hippo's design borrows seven properties of the human hippocampus. This section is design inspiration, not measured benefit. For what we measured, see the Receipts and [PR #232](https://github.com/kitfunso/hippo-memory/pull/232).
|
|
759
802
|
|
|
760
803
|
**Why two stores?** The brain uses a fast hippocampal buffer + a slow neocortical store (Complementary Learning Systems theory, McClelland et al. 1995). If the neocortex learned fast, new information would overwrite old knowledge. The buffer absorbs new episodes; the neocortex extracts patterns over time.
|
|
761
804
|
|
|
762
|
-
**Why
|
|
805
|
+
**Why decay at all?** In the brain, new neurons born in the dentate gyrus disrupt old memory traces (Frankland et al. 2013), which may reduce interference from outdated information. That is why hippo has decay. In hippo's own tests, age-based decay made no measurable difference to recall: at the 365-day default it tied with decay switched off. The forgetting that measured helpful is by evidence: a bad outcome mark, or a newer fact superseding an old one.
|
|
763
806
|
|
|
764
807
|
**Why do errors stick?** The amygdala modulates hippocampal consolidation based on emotional significance. Fear and error signals boost encoding. Your first production incident is burned into memory. Your 200th uneventful deploy isn't.
|
|
765
808
|
|
|
@@ -810,7 +853,7 @@ The AI-memory category matured fast in 2026. Hippo's specific take — bio-decay
|
|
|
810
853
|
|
|
811
854
|
\*\* Different metric: Memoria's 88.78% and EverMind's 83% are reported as overall accuracy with a reader LLM, not retrieval R@5. Higher denominator + LLM helps. Not directly comparable to retrieval-only R@5 numbers above.
|
|
812
855
|
|
|
813
|
-
Different tools answer different questions. Mem0 and Basic Memory implement "save everything, search later." MemPalace implements "store everything, organize spatially for retrieval." gbrain, Zep, and Cognee implement "extract typed entities and relationships into a knowledge graph." Letta implements "the agent edits its own memory blocks." Memoria implements "Git-style version control over the memory state itself." EverMind implements "self-evolving Skill Memory + multi-modal retrieval over hierarchical scopes." Hippo implements "
|
|
856
|
+
Different tools answer different questions. Mem0 and Basic Memory implement "save everything, search later." MemPalace implements "store everything, organize spatially for retrieval." gbrain, Zep, and Cognee implement "extract typed entities and relationships into a knowledge graph." Letta implements "the agent edits its own memory blocks." Memoria implements "Git-style version control over the memory state itself." EverMind implements "self-evolving Skill Memory + multi-modal retrieval over hierarchical scopes." Hippo implements "learn what is wrong and stop repeating it." These are complementary takes, not a single-axis ranking: bio-lifecycle (Hippo) + GraphRAG (gbrain/Cognee/Zep) + agent-self-edit (Letta) + memory-VCS (Memoria) + skill-distillation (EverMind) cover different parts of the same problem.
|
|
814
857
|
|
|
815
858
|
---
|
|
816
859
|
|
|
@@ -831,7 +874,7 @@ Three benchmarks testing three different things. Full details in [`benchmarks/`]
|
|
|
831
874
|
|
|
832
875
|
gbrain reports 97.6 R@5 on this split with a paid frontier embedder. Hippo reaches 98.0 with a free local embedder, a tie at 500 questions. These numbers come from the scripts in `benchmarks/longmemeval/`, which index every turn and fuse BM25 with dense ranks; they are not `hippo recall`, and a default install has no embedder. Re-measure: [`docs/evals/2026-09-23-longmemeval-reproduction.md`](docs/evals/2026-09-23-longmemeval-reproduction.md). Retrieval recall on the standard task is effectively saturated, so the embedder is a swappable commodity, not the differentiator. Method and the global-pool comparison: [`docs/evals/2026-06-09-longmemeval-per-haystack-dual.md`](docs/evals/2026-06-09-longmemeval-per-haystack-dual.md).
|
|
833
876
|
|
|
834
|
-
The differentiator is what happens as one store grows. Point retrieval at a single unified memory of tens of thousands of sessions, with no pre-scoped haystack, and recall stops being free (MiniLM 47, voyage 56 on the 19,195-session `_s` store, June 2026). That is where
|
|
877
|
+
The differentiator is what happens as one store grows. Point retrieval at a single unified memory of tens of thousands of sessions, with no pre-scoped haystack, and recall stops being free (MiniLM 47, voyage 56 on the 19,195-session `_s` store, June 2026). That is where we expect the memory lifecycle to matter, and it is what hippo measures next (see ROADMAP Part III). It is not shown yet: in our tests so far, decay made no measurable difference and sleep cost recall. Outcome marks and supersession are what measured helpful.
|
|
835
878
|
|
|
836
879
|
**Hippo v0.28.0 oracle-split results (hybrid BM25 + cosine, full 500 questions, pooled retrieval):**
|
|
837
880
|
|
package/bin/hippo.js
CHANGED
|
File without changes
|
package/dist/ablation.d.ts
CHANGED
|
@@ -50,7 +50,13 @@
|
|
|
50
50
|
* arms must not pass it.
|
|
51
51
|
* HIPPO_ABLATE_OUTCOME_SLOW rewardFactor := 1 (no half-life modulation).
|
|
52
52
|
* HIPPO_ABLATE_OUTCOME_FAST hybridSearch outcomeBoost := 1.
|
|
53
|
-
*
|
|
53
|
+
* HIPPO_ABLATE_RECENCY search recency factor := 1 (the creation-age
|
|
54
|
+
* multiplier 0.8 + 0.2*exp(-age/30d)); decay,
|
|
55
|
+
* strengthening and outcomes stay live.
|
|
56
|
+
* HIPPO_EVAL_RECENCY_DAYS a positive number replaces the 30-day scale of
|
|
57
|
+
* that recency factor (tuning grids only). Unset,
|
|
58
|
+
* zero, negative or junk values keep 30.
|
|
59
|
+
* HIPPO_FAKE_NOW timestamp injected as the default `now` for
|
|
54
60
|
* strength computation and retrieval stamping
|
|
55
61
|
* (simulated-time protocols). MUST be the exact
|
|
56
62
|
* Date.toISOString() form
|
|
@@ -82,6 +88,9 @@ export declare function isDecayAblated(): boolean;
|
|
|
82
88
|
export declare function isRecallBoostAblated(): boolean;
|
|
83
89
|
export declare function isOutcomeSlowAblated(): boolean;
|
|
84
90
|
export declare function isOutcomeFastAblated(): boolean;
|
|
91
|
+
export declare function isRecencyAblated(): boolean;
|
|
92
|
+
/** HIPPO_EVAL_RECENCY_DAYS when it is a positive number, else null (callers keep their default). */
|
|
93
|
+
export declare function evalRecencyScaleDays(): number | null;
|
|
85
94
|
/**
|
|
86
95
|
* The default `now` for lifecycle computations: HIPPO_FAKE_NOW when set and
|
|
87
96
|
* parseable, else the real clock. Callers that already take an explicit `now`
|
package/dist/ablation.js
CHANGED
|
@@ -50,7 +50,13 @@
|
|
|
50
50
|
* arms must not pass it.
|
|
51
51
|
* HIPPO_ABLATE_OUTCOME_SLOW rewardFactor := 1 (no half-life modulation).
|
|
52
52
|
* HIPPO_ABLATE_OUTCOME_FAST hybridSearch outcomeBoost := 1.
|
|
53
|
-
*
|
|
53
|
+
* HIPPO_ABLATE_RECENCY search recency factor := 1 (the creation-age
|
|
54
|
+
* multiplier 0.8 + 0.2*exp(-age/30d)); decay,
|
|
55
|
+
* strengthening and outcomes stay live.
|
|
56
|
+
* HIPPO_EVAL_RECENCY_DAYS a positive number replaces the 30-day scale of
|
|
57
|
+
* that recency factor (tuning grids only). Unset,
|
|
58
|
+
* zero, negative or junk values keep 30.
|
|
59
|
+
* HIPPO_FAKE_NOW timestamp injected as the default `now` for
|
|
54
60
|
* strength computation and retrieval stamping
|
|
55
61
|
* (simulated-time protocols). MUST be the exact
|
|
56
62
|
* Date.toISOString() form
|
|
@@ -101,11 +107,14 @@ function readFlags() {
|
|
|
101
107
|
fakeNowMs = parsed;
|
|
102
108
|
}
|
|
103
109
|
}
|
|
110
|
+
const recencyDays = Number(process.env.HIPPO_EVAL_RECENCY_DAYS);
|
|
104
111
|
_cache = {
|
|
105
112
|
decay: isTruthy(process.env.HIPPO_ABLATE_DECAY),
|
|
106
113
|
recallBoost: isTruthy(process.env.HIPPO_ABLATE_RECALL_BOOST),
|
|
107
114
|
outcomeSlow: outcomeBoth || isTruthy(process.env.HIPPO_ABLATE_OUTCOME_SLOW),
|
|
108
115
|
outcomeFast: outcomeBoth || isTruthy(process.env.HIPPO_ABLATE_OUTCOME_FAST),
|
|
116
|
+
recency: isTruthy(process.env.HIPPO_ABLATE_RECENCY),
|
|
117
|
+
recencyDays: Number.isFinite(recencyDays) && recencyDays > 0 ? recencyDays : null,
|
|
109
118
|
fakeNowMs,
|
|
110
119
|
};
|
|
111
120
|
return _cache;
|
|
@@ -122,6 +131,13 @@ export function isOutcomeSlowAblated() {
|
|
|
122
131
|
export function isOutcomeFastAblated() {
|
|
123
132
|
return readFlags().outcomeFast;
|
|
124
133
|
}
|
|
134
|
+
export function isRecencyAblated() {
|
|
135
|
+
return readFlags().recency;
|
|
136
|
+
}
|
|
137
|
+
/** HIPPO_EVAL_RECENCY_DAYS when it is a positive number, else null (callers keep their default). */
|
|
138
|
+
export function evalRecencyScaleDays() {
|
|
139
|
+
return readFlags().recencyDays;
|
|
140
|
+
}
|
|
125
141
|
/**
|
|
126
142
|
* The default `now` for lifecycle computations: HIPPO_FAKE_NOW when set and
|
|
127
143
|
* parseable, else the real clock. Callers that already take an explicit `now`
|
package/dist/api.d.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import { type DatabaseSyncLike } from './db.js';
|
|
10
10
|
import { deleteEntry, loadAllEntries, type TaskSnapshot, type SessionEvent } from './store.js';
|
|
11
11
|
import { type RejectedValueRow } from './rejection.js';
|
|
12
|
+
import { type DormantMemory, type ListDormantOpts } from './dormant.js';
|
|
13
|
+
import { type TokenSummary, type TokenSurface } from './token-ledger.js';
|
|
14
|
+
import { type FailureSummary } from './failure-log.js';
|
|
12
15
|
import { type SessionHandoff } from './handoff.js';
|
|
13
16
|
import { type MemoryKind, type MemoryEntry } from './memory.js';
|
|
14
17
|
import { auditMemories, type AuditEvent, type AuditOp } from './audit.js';
|
|
@@ -78,7 +81,9 @@ export declare class ForbiddenError extends Error {
|
|
|
78
81
|
}
|
|
79
82
|
import { isPrivateScope, passesScopeFilterForRecall } from './recall-scope.js';
|
|
80
83
|
export { isPrivateScope, passesScopeFilterForRecall };
|
|
81
|
-
export { passesCliRecallScopeFilter } from './recall-scope.js';
|
|
84
|
+
export { passesCliRecallScopeFilter, ScopeForbiddenError } from './recall-scope.js';
|
|
85
|
+
export type { TokenSummary, TokenSurface, TokenSurfaceSummary } from './token-ledger.js';
|
|
86
|
+
export type { FailureSummary } from './failure-log.js';
|
|
82
87
|
export { classifyOriginProject } from './project-identity.js';
|
|
83
88
|
/**
|
|
84
89
|
* v39 S4: the secret half of the ambient policy on its own, for surfaces
|
|
@@ -998,9 +1003,69 @@ export interface SleepOpts {
|
|
|
998
1003
|
*/
|
|
999
1004
|
__phases?: Partial<SleepPhases>;
|
|
1000
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Record memory text handed to an agent in the token ledger (ROADMAP TE0).
|
|
1008
|
+
* Best-effort: never throws, because a ledger failure must not fail the
|
|
1009
|
+
* recall or context call that produced the text.
|
|
1010
|
+
*/
|
|
1011
|
+
export declare function recordTokens(ctx: Context, surface: TokenSurface, use: {
|
|
1012
|
+
items: number;
|
|
1013
|
+
tokens: number;
|
|
1014
|
+
sessionId?: string | null;
|
|
1015
|
+
}): void;
|
|
1016
|
+
/**
|
|
1017
|
+
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
1018
|
+
* tokens sent per surface, blocks skipped as unchanged and the tokens that
|
|
1019
|
+
* saved, and mean tokens per session.
|
|
1020
|
+
*/
|
|
1021
|
+
export declare function tokenSummary(ctx: Context, opts?: {
|
|
1022
|
+
days?: number;
|
|
1023
|
+
}): TokenSummary;
|
|
1024
|
+
/** Failed tool calls by outcome, and repeats across sessions, over the last `days` days (default 30); ROADMAP CD13. */
|
|
1025
|
+
export declare function failureSummary(ctx: Context, opts?: {
|
|
1026
|
+
days?: number;
|
|
1027
|
+
}): FailureSummary;
|
|
1028
|
+
/**
|
|
1029
|
+
* A tenant's dormant memories (src/dormant.ts): what sleep moved out of
|
|
1030
|
+
* active memory instead of deleting, when `dormant.enabled` is on. Newest
|
|
1031
|
+
* first; `opts.query` keeps rows containing every term (case-insensitive).
|
|
1032
|
+
*/
|
|
1033
|
+
export declare function listDormant(ctx: Context, opts?: ListDormantOpts): DormantMemory[];
|
|
1034
|
+
/**
|
|
1035
|
+
* Bring a dormant memory back into active memory. It returns as if just
|
|
1036
|
+
* recalled: `last_retrieved` is now, so it gets a full half-life before it
|
|
1037
|
+
* can fade again. Every other field is the snapshot taken when it went
|
|
1038
|
+
* dormant.
|
|
1039
|
+
*
|
|
1040
|
+
* Throws when the tenant has no dormant memory with that id (another
|
|
1041
|
+
* tenant's id reads the same way), when a live memory already holds the id,
|
|
1042
|
+
* and RejectedValueError when the value has been rejected since. On any
|
|
1043
|
+
* throw the dormant copy stays where it is.
|
|
1044
|
+
*/
|
|
1045
|
+
export declare function restoreDormant(ctx: Context, id: string): MemoryEntry;
|
|
1046
|
+
/**
|
|
1047
|
+
* Permanently delete a dormant memory: the explicit "forget it for good"
|
|
1048
|
+
* that dormant storage leaves to the user. Throws when the tenant has no
|
|
1049
|
+
* dormant memory with that id.
|
|
1050
|
+
*/
|
|
1051
|
+
export declare function forgetDormant(ctx: Context, id: string): void;
|
|
1052
|
+
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
1053
|
+
export declare function isDormant(ctx: Context, id: string): boolean;
|
|
1001
1054
|
export interface SleepResult {
|
|
1002
1055
|
active: number;
|
|
1003
1056
|
removed: number;
|
|
1057
|
+
/**
|
|
1058
|
+
* Faded memories the decay pass moved to the dormant store instead of
|
|
1059
|
+
* deleting (config `dormant.enabled`). Absent when 0. Per-invocation
|
|
1060
|
+
* activity counter, same class as `removed`.
|
|
1061
|
+
*/
|
|
1062
|
+
dormant?: number;
|
|
1063
|
+
/**
|
|
1064
|
+
* Dormant memories deleted for good this sleep because they outlived
|
|
1065
|
+
* `dormant.retentionDays`. Absent when 0. Same per-invocation class as
|
|
1066
|
+
* `removed`.
|
|
1067
|
+
*/
|
|
1068
|
+
dormantExpired?: number;
|
|
1004
1069
|
mergedEpisodic: number;
|
|
1005
1070
|
newSemantic: number;
|
|
1006
1071
|
dryRun: boolean;
|
package/dist/api.js
CHANGED
|
@@ -11,6 +11,9 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
11
11
|
import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, } from './store.js';
|
|
12
12
|
import { RejectedValueError } from './rejection.js';
|
|
13
13
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
14
|
+
import { listDormantRows, readDormantSnapshot, deleteDormantRow, hasDormantRow, } from './dormant.js';
|
|
15
|
+
import { recordTokenUse, summarizeTokenUse } from './token-ledger.js';
|
|
16
|
+
import { summarizeFailures } from './failure-log.js';
|
|
14
17
|
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
15
18
|
import { createMemory, applyOutcome, calculateStrength, Layer, } from './memory.js';
|
|
16
19
|
import { appendAuditEvent, queryAuditEvents, auditMemories, isContentWorthStoring, } from './audit.js';
|
|
@@ -84,9 +87,9 @@ export class ForbiddenError extends Error {
|
|
|
84
87
|
// back-compat (`api.isPrivateScope`, test imports). NOTE: the import statement
|
|
85
88
|
// is required — a bare `export { x } from` re-export does not bind the local
|
|
86
89
|
// names this module's ~9 call sites use.
|
|
87
|
-
import { isPrivateScope, passesScopeFilterForRecall } from './recall-scope.js';
|
|
90
|
+
import { isPrivateScope, passesScopeFilterForRecall, assertScopeRequestAllowed } from './recall-scope.js';
|
|
88
91
|
export { isPrivateScope, passesScopeFilterForRecall };
|
|
89
|
-
export { passesCliRecallScopeFilter } from './recall-scope.js';
|
|
92
|
+
export { passesCliRecallScopeFilter, ScopeForbiddenError } from './recall-scope.js';
|
|
90
93
|
// v39: classifyOriginProject lives in project-identity.ts (leaf) so
|
|
91
94
|
// shared.ts can use it without an api.ts import cycle. Re-exported here for
|
|
92
95
|
// callers that already import the api surface.
|
|
@@ -184,11 +187,14 @@ export function buildSuppressionSummary(counts) {
|
|
|
184
187
|
* `tests/api-recall-no-side-effects.test.ts`.
|
|
185
188
|
*/
|
|
186
189
|
export function recall(ctx, opts) {
|
|
190
|
+
// A member key may not unlock a private or quarantined scope by naming it.
|
|
191
|
+
assertScopeRequestAllowed(ctx.actor.role, opts.scope);
|
|
187
192
|
const windowSize = recallWindowSize(opts);
|
|
188
193
|
return recallFrom(ctx, opts, windowSize, loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false));
|
|
189
194
|
}
|
|
190
195
|
/** Mode-aware recall that strengthens each returned row; never writes last_retrieval_ids (v1.11.5 lock). */
|
|
191
196
|
export async function retrieve(ctx, opts) {
|
|
197
|
+
assertScopeRequestAllowed(ctx.actor.role, opts.scope);
|
|
192
198
|
const windowSize = recallWindowSize(opts);
|
|
193
199
|
let candidates = loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false);
|
|
194
200
|
if (opts.mode === 'hybrid' || opts.mode === 'physics') {
|
|
@@ -471,7 +477,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
471
477
|
// prepended (NOT rows already in baseRanked that got tagged isFreshTail).
|
|
472
478
|
freshTailAddedCount = freshRanked.length;
|
|
473
479
|
rankedOut = [...freshRanked, ...baseRanked, ...summaryRanked];
|
|
474
|
-
tokensOut = rankedOut.reduce((acc, r) => acc +
|
|
480
|
+
tokensOut = rankedOut.reduce((acc, r) => acc + estimateTokens(r.content), 0);
|
|
475
481
|
totalOut = entries.length;
|
|
476
482
|
// TODO(a1-task-4): emit via the shared audit hook in store.ts so we don't
|
|
477
483
|
// double-emit. Recall does not currently write through writeEntry, so no
|
|
@@ -553,7 +559,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
553
559
|
sessionHandoff: filteredHandoff,
|
|
554
560
|
recentSessionEvents: filteredEvents,
|
|
555
561
|
};
|
|
556
|
-
const tokenize = (s) => s ?
|
|
562
|
+
const tokenize = (s) => s ? estimateTokens(s) : 0;
|
|
557
563
|
continuityTokens =
|
|
558
564
|
tokenize(filteredSnapshot?.task) +
|
|
559
565
|
tokenize(filteredSnapshot?.summary) +
|
|
@@ -719,6 +725,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
719
725
|
* - all rows fail the scope/tenant filter
|
|
720
726
|
*/
|
|
721
727
|
export function assemble(ctx, sessionId, opts = {}) {
|
|
728
|
+
assertScopeRequestAllowed(ctx.actor.role, opts.scope);
|
|
722
729
|
const budget = opts.budget ?? 4000;
|
|
723
730
|
const freshTailCount = opts.freshTailCount ?? 10;
|
|
724
731
|
const summarizeOlder = opts.summarizeOlder ?? true;
|
|
@@ -818,7 +825,7 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
818
825
|
olderItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
819
826
|
tailItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
820
827
|
let items = [...olderItems, ...tailItems];
|
|
821
|
-
let tokens = items.reduce((acc, it) => acc +
|
|
828
|
+
let tokens = items.reduce((acc, it) => acc + estimateTokens(it.content), 0);
|
|
822
829
|
let evicted = 0;
|
|
823
830
|
while (tokens > budget && items.length > 0) {
|
|
824
831
|
let worstIdx = -1;
|
|
@@ -833,7 +840,7 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
833
840
|
}
|
|
834
841
|
if (worstIdx === -1)
|
|
835
842
|
break;
|
|
836
|
-
const cost =
|
|
843
|
+
const cost = estimateTokens(items[worstIdx].content);
|
|
837
844
|
items = items.filter((_, i) => i !== worstIdx);
|
|
838
845
|
tokens -= cost;
|
|
839
846
|
evicted++;
|
|
@@ -917,7 +924,7 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
917
924
|
const out = [];
|
|
918
925
|
let used = 0;
|
|
919
926
|
for (const c of collected) {
|
|
920
|
-
const t =
|
|
927
|
+
const t = estimateTokens(c.content);
|
|
921
928
|
if (out.length > 0 && used + t > opts.budget) {
|
|
922
929
|
truncated = true;
|
|
923
930
|
break;
|
|
@@ -1858,6 +1865,186 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1858
1865
|
ambientState,
|
|
1859
1866
|
};
|
|
1860
1867
|
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Record memory text handed to an agent in the token ledger (ROADMAP TE0).
|
|
1870
|
+
* Best-effort: never throws, because a ledger failure must not fail the
|
|
1871
|
+
* recall or context call that produced the text.
|
|
1872
|
+
*/
|
|
1873
|
+
export function recordTokens(ctx, surface, use) {
|
|
1874
|
+
try {
|
|
1875
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1876
|
+
try {
|
|
1877
|
+
recordTokenUse(db, {
|
|
1878
|
+
tenantId: ctx.tenantId,
|
|
1879
|
+
sessionId: use.sessionId ?? null,
|
|
1880
|
+
surface,
|
|
1881
|
+
event: 'inject',
|
|
1882
|
+
items: use.items,
|
|
1883
|
+
tokens: use.tokens,
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
finally {
|
|
1887
|
+
closeHippoDb(db);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
catch {
|
|
1891
|
+
// Ledger is best-effort.
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
1896
|
+
* tokens sent per surface, blocks skipped as unchanged and the tokens that
|
|
1897
|
+
* saved, and mean tokens per session.
|
|
1898
|
+
*/
|
|
1899
|
+
export function tokenSummary(ctx, opts = {}) {
|
|
1900
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1901
|
+
try {
|
|
1902
|
+
return summarizeTokenUse(db, ctx.tenantId, reportWindowStart(opts.days));
|
|
1903
|
+
}
|
|
1904
|
+
finally {
|
|
1905
|
+
closeHippoDb(db);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/** Failed tool calls by outcome, and repeats across sessions, over the last `days` days (default 30); ROADMAP CD13. */
|
|
1909
|
+
export function failureSummary(ctx, opts = {}) {
|
|
1910
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1911
|
+
try {
|
|
1912
|
+
return summarizeFailures(db, ctx.tenantId, reportWindowStart(opts.days));
|
|
1913
|
+
}
|
|
1914
|
+
finally {
|
|
1915
|
+
closeHippoDb(db);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function reportWindowStart(days) {
|
|
1919
|
+
const span = days !== undefined && Number.isFinite(days) && days > 0 ? days : 30;
|
|
1920
|
+
return new Date(Date.now() - span * 86_400_000).toISOString();
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* A tenant's dormant memories (src/dormant.ts): what sleep moved out of
|
|
1924
|
+
* active memory instead of deleting, when `dormant.enabled` is on. Newest
|
|
1925
|
+
* first; `opts.query` keeps rows containing every term (case-insensitive).
|
|
1926
|
+
*/
|
|
1927
|
+
export function listDormant(ctx, opts = {}) {
|
|
1928
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1929
|
+
try {
|
|
1930
|
+
return listDormantRows(db, ctx.tenantId, opts);
|
|
1931
|
+
}
|
|
1932
|
+
finally {
|
|
1933
|
+
closeHippoDb(db);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Bring a dormant memory back into active memory. It returns as if just
|
|
1938
|
+
* recalled: `last_retrieved` is now, so it gets a full half-life before it
|
|
1939
|
+
* can fade again. Every other field is the snapshot taken when it went
|
|
1940
|
+
* dormant.
|
|
1941
|
+
*
|
|
1942
|
+
* Throws when the tenant has no dormant memory with that id (another
|
|
1943
|
+
* tenant's id reads the same way), when a live memory already holds the id,
|
|
1944
|
+
* and RejectedValueError when the value has been rejected since. On any
|
|
1945
|
+
* throw the dormant copy stays where it is.
|
|
1946
|
+
*/
|
|
1947
|
+
export function restoreDormant(ctx, id) {
|
|
1948
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1949
|
+
try {
|
|
1950
|
+
let restored;
|
|
1951
|
+
db.exec('BEGIN IMMEDIATE');
|
|
1952
|
+
try {
|
|
1953
|
+
const dormant = readDormantSnapshot(db, ctx.tenantId, id);
|
|
1954
|
+
if (!dormant) {
|
|
1955
|
+
throw new Error(`dormant memory not found: ${id}`);
|
|
1956
|
+
}
|
|
1957
|
+
if (db.prepare(`SELECT 1 FROM memories WHERE id = ?`).get(id) !== undefined) {
|
|
1958
|
+
throw new Error(`memory ${id} is already active; forget it before restoring its dormant copy`);
|
|
1959
|
+
}
|
|
1960
|
+
const now = new Date();
|
|
1961
|
+
// Dormant rows are long-lived, so a snapshot can predate a field added
|
|
1962
|
+
// later: createMemory supplies a default for anything it lacks, then
|
|
1963
|
+
// the snapshot overrides every field it does carry, content included.
|
|
1964
|
+
// (The placeholder only satisfies createMemory's minimum length, so a
|
|
1965
|
+
// legacy row shorter than 3 chars can still be restored.)
|
|
1966
|
+
const revived = {
|
|
1967
|
+
...createMemory('dormant snapshot defaults'),
|
|
1968
|
+
...dormant.entry,
|
|
1969
|
+
last_retrieved: now.toISOString(),
|
|
1970
|
+
};
|
|
1971
|
+
restored = stampOriginProject(ctx.hippoRoot, { ...revived, strength: calculateStrength(revived, now) });
|
|
1972
|
+
writeEntryDbOnly(db, restored, { actor: ctx.actor.subject });
|
|
1973
|
+
deleteDormantRow(db, ctx.tenantId, id);
|
|
1974
|
+
// A restore is a labelled "forgot it, then needed it" event: the
|
|
1975
|
+
// signal a learned lifecycle (ROADMAP LC3) trains on. Same transaction
|
|
1976
|
+
// as the restore, so the label exists exactly when the restore does.
|
|
1977
|
+
appendAuditEvent(db, {
|
|
1978
|
+
tenantId: ctx.tenantId,
|
|
1979
|
+
actor: ctx.actor.subject,
|
|
1980
|
+
op: 'dormant_restore',
|
|
1981
|
+
targetId: id,
|
|
1982
|
+
metadata: {
|
|
1983
|
+
reason: dormant.reason,
|
|
1984
|
+
strengthAtDormancy: dormant.strength,
|
|
1985
|
+
dormantAt: dormant.dormantAt,
|
|
1986
|
+
daysDormant: Math.max(0, (now.getTime() - Date.parse(dormant.dormantAt)) / (24 * 60 * 60 * 1000)),
|
|
1987
|
+
},
|
|
1988
|
+
});
|
|
1989
|
+
db.exec('COMMIT');
|
|
1990
|
+
}
|
|
1991
|
+
catch (err) {
|
|
1992
|
+
try {
|
|
1993
|
+
db.exec('ROLLBACK');
|
|
1994
|
+
}
|
|
1995
|
+
catch { /* already rolled back */ }
|
|
1996
|
+
if (err instanceof RejectedValueError) {
|
|
1997
|
+
auditRejectionRefusal(db, err, ctx.actor.subject);
|
|
1998
|
+
}
|
|
1999
|
+
throw err;
|
|
2000
|
+
}
|
|
2001
|
+
writeEntryMirrors(ctx.hippoRoot, db, restored);
|
|
2002
|
+
return restored;
|
|
2003
|
+
}
|
|
2004
|
+
finally {
|
|
2005
|
+
closeHippoDb(db);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Permanently delete a dormant memory: the explicit "forget it for good"
|
|
2010
|
+
* that dormant storage leaves to the user. Throws when the tenant has no
|
|
2011
|
+
* dormant memory with that id.
|
|
2012
|
+
*/
|
|
2013
|
+
export function forgetDormant(ctx, id) {
|
|
2014
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2015
|
+
try {
|
|
2016
|
+
if (!deleteDormantRow(db, ctx.tenantId, id)) {
|
|
2017
|
+
throw new Error(`dormant memory not found: ${id}`);
|
|
2018
|
+
}
|
|
2019
|
+
try {
|
|
2020
|
+
appendAuditEvent(db, {
|
|
2021
|
+
tenantId: ctx.tenantId,
|
|
2022
|
+
actor: ctx.actor.subject,
|
|
2023
|
+
op: 'forget',
|
|
2024
|
+
targetId: id,
|
|
2025
|
+
metadata: { dormant: true },
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
catch {
|
|
2029
|
+
// Best-effort, like every other forget audit row: the delete stands.
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
finally {
|
|
2033
|
+
closeHippoDb(db);
|
|
2034
|
+
}
|
|
2035
|
+
// Counted like every other permanent removal (forget, archiveRaw).
|
|
2036
|
+
updateStats(ctx.hippoRoot, { forgotten: 1 });
|
|
2037
|
+
}
|
|
2038
|
+
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
2039
|
+
export function isDormant(ctx, id) {
|
|
2040
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2041
|
+
try {
|
|
2042
|
+
return hasDormantRow(db, ctx.tenantId, id);
|
|
2043
|
+
}
|
|
2044
|
+
finally {
|
|
2045
|
+
closeHippoDb(db);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
1861
2048
|
const DEFAULT_SLEEP_PHASES = {
|
|
1862
2049
|
consolidate,
|
|
1863
2050
|
deduplicateStore,
|
|
@@ -1921,6 +2108,14 @@ export async function sleep(ctx, opts = {}) {
|
|
|
1921
2108
|
dryRun,
|
|
1922
2109
|
details: consolidateResult.details,
|
|
1923
2110
|
};
|
|
2111
|
+
// Set only when non-zero, so a store without dormant memories gets a
|
|
2112
|
+
// byte-identical result (HTTP /v1/sleep, the CLI render snapshot).
|
|
2113
|
+
if (consolidateResult.dormant > 0) {
|
|
2114
|
+
result.dormant = consolidateResult.dormant;
|
|
2115
|
+
}
|
|
2116
|
+
if (consolidateResult.dormantExpired > 0) {
|
|
2117
|
+
result.dormantExpired = consolidateResult.dormantExpired;
|
|
2118
|
+
}
|
|
1924
2119
|
// Phase 2: Dedup (post-consolidate near-duplicate cleanup).
|
|
1925
2120
|
const dedupResult = phases.deduplicateStore(ctx.hippoRoot, { dryRun, actor: ctx.actor.subject });
|
|
1926
2121
|
dedupCount = dedupResult.removed;
|