hippo-memory 1.45.0 → 1.46.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 +57 -15
- package/dist/ablation.d.ts +10 -1
- package/dist/ablation.js +17 -1
- package/dist/api.d.ts +60 -1
- package/dist/api.js +189 -7
- package/dist/audit.d.ts +1 -1
- package/dist/capture-error.d.ts +20 -0
- package/dist/capture-error.js +82 -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 +381 -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 +57 -1
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.js +174 -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/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,11 @@ 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 |
|
|
595
633
|
| `hippo embed` | Embed all memories for semantic search |
|
|
596
634
|
| `hippo embed --status` | Show embedding coverage |
|
|
597
635
|
| `hippo watch "<command>"` | Run command, auto-learn from failures |
|
|
@@ -683,9 +721,11 @@ This adds a `<!-- hippo:start -->` ... `<!-- hippo:end -->` block that tells the
|
|
|
683
721
|
For Claude Code, it also adds:
|
|
684
722
|
- a `SessionEnd` hook so `hippo sleep` runs automatically when the session exits
|
|
685
723
|
- 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`.
|
|
724
|
+
- 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
725
|
- 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
726
|
- 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.
|
|
727
|
+
- 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.
|
|
728
|
+
- 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.
|
|
689
729
|
|
|
690
730
|
To remove: `hippo hook uninstall claude-code`
|
|
691
731
|
|
|
@@ -725,7 +765,9 @@ Add to your MCP config (e.g. `.cursor/mcp.json` or `claude_desktop_config.json`)
|
|
|
725
765
|
}
|
|
726
766
|
```
|
|
727
767
|
|
|
728
|
-
|
|
768
|
+
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`.
|
|
769
|
+
|
|
770
|
+
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
771
|
|
|
730
772
|
### OpenClaw Plugin
|
|
731
773
|
|
|
@@ -755,11 +797,11 @@ Full integration details: [integrations/](integrations/)
|
|
|
755
797
|
|
|
756
798
|
## The Neuroscience
|
|
757
799
|
|
|
758
|
-
Hippo
|
|
800
|
+
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
801
|
|
|
760
802
|
**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
803
|
|
|
762
|
-
**Why
|
|
804
|
+
**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
805
|
|
|
764
806
|
**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
807
|
|
|
@@ -810,7 +852,7 @@ The AI-memory category matured fast in 2026. Hippo's specific take — bio-decay
|
|
|
810
852
|
|
|
811
853
|
\*\* 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
854
|
|
|
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 "
|
|
855
|
+
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
856
|
|
|
815
857
|
---
|
|
816
858
|
|
|
@@ -831,7 +873,7 @@ Three benchmarks testing three different things. Full details in [`benchmarks/`]
|
|
|
831
873
|
|
|
832
874
|
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
875
|
|
|
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
|
|
876
|
+
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
877
|
|
|
836
878
|
**Hippo v0.28.0 oracle-split results (hybrid BM25 + cosine, full 500 questions, pooled retrieval):**
|
|
837
879
|
|
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,8 @@
|
|
|
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';
|
|
12
14
|
import { type SessionHandoff } from './handoff.js';
|
|
13
15
|
import { type MemoryKind, type MemoryEntry } from './memory.js';
|
|
14
16
|
import { auditMemories, type AuditEvent, type AuditOp } from './audit.js';
|
|
@@ -78,7 +80,8 @@ export declare class ForbiddenError extends Error {
|
|
|
78
80
|
}
|
|
79
81
|
import { isPrivateScope, passesScopeFilterForRecall } from './recall-scope.js';
|
|
80
82
|
export { isPrivateScope, passesScopeFilterForRecall };
|
|
81
|
-
export { passesCliRecallScopeFilter } from './recall-scope.js';
|
|
83
|
+
export { passesCliRecallScopeFilter, ScopeForbiddenError } from './recall-scope.js';
|
|
84
|
+
export type { TokenSummary, TokenSurface, TokenSurfaceSummary } from './token-ledger.js';
|
|
82
85
|
export { classifyOriginProject } from './project-identity.js';
|
|
83
86
|
/**
|
|
84
87
|
* v39 S4: the secret half of the ambient policy on its own, for surfaces
|
|
@@ -998,9 +1001,65 @@ export interface SleepOpts {
|
|
|
998
1001
|
*/
|
|
999
1002
|
__phases?: Partial<SleepPhases>;
|
|
1000
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Record memory text handed to an agent in the token ledger (ROADMAP TE0).
|
|
1006
|
+
* Best-effort: never throws, because a ledger failure must not fail the
|
|
1007
|
+
* recall or context call that produced the text.
|
|
1008
|
+
*/
|
|
1009
|
+
export declare function recordTokens(ctx: Context, surface: TokenSurface, use: {
|
|
1010
|
+
items: number;
|
|
1011
|
+
tokens: number;
|
|
1012
|
+
sessionId?: string | null;
|
|
1013
|
+
}): void;
|
|
1014
|
+
/**
|
|
1015
|
+
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
1016
|
+
* tokens sent per surface, blocks skipped as unchanged and the tokens that
|
|
1017
|
+
* saved, and mean tokens per session.
|
|
1018
|
+
*/
|
|
1019
|
+
export declare function tokenSummary(ctx: Context, opts?: {
|
|
1020
|
+
days?: number;
|
|
1021
|
+
}): TokenSummary;
|
|
1022
|
+
/**
|
|
1023
|
+
* A tenant's dormant memories (src/dormant.ts): what sleep moved out of
|
|
1024
|
+
* active memory instead of deleting, when `dormant.enabled` is on. Newest
|
|
1025
|
+
* first; `opts.query` keeps rows containing every term (case-insensitive).
|
|
1026
|
+
*/
|
|
1027
|
+
export declare function listDormant(ctx: Context, opts?: ListDormantOpts): DormantMemory[];
|
|
1028
|
+
/**
|
|
1029
|
+
* Bring a dormant memory back into active memory. It returns as if just
|
|
1030
|
+
* recalled: `last_retrieved` is now, so it gets a full half-life before it
|
|
1031
|
+
* can fade again. Every other field is the snapshot taken when it went
|
|
1032
|
+
* dormant.
|
|
1033
|
+
*
|
|
1034
|
+
* Throws when the tenant has no dormant memory with that id (another
|
|
1035
|
+
* tenant's id reads the same way), when a live memory already holds the id,
|
|
1036
|
+
* and RejectedValueError when the value has been rejected since. On any
|
|
1037
|
+
* throw the dormant copy stays where it is.
|
|
1038
|
+
*/
|
|
1039
|
+
export declare function restoreDormant(ctx: Context, id: string): MemoryEntry;
|
|
1040
|
+
/**
|
|
1041
|
+
* Permanently delete a dormant memory: the explicit "forget it for good"
|
|
1042
|
+
* that dormant storage leaves to the user. Throws when the tenant has no
|
|
1043
|
+
* dormant memory with that id.
|
|
1044
|
+
*/
|
|
1045
|
+
export declare function forgetDormant(ctx: Context, id: string): void;
|
|
1046
|
+
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
1047
|
+
export declare function isDormant(ctx: Context, id: string): boolean;
|
|
1001
1048
|
export interface SleepResult {
|
|
1002
1049
|
active: number;
|
|
1003
1050
|
removed: number;
|
|
1051
|
+
/**
|
|
1052
|
+
* Faded memories the decay pass moved to the dormant store instead of
|
|
1053
|
+
* deleting (config `dormant.enabled`). Absent when 0. Per-invocation
|
|
1054
|
+
* activity counter, same class as `removed`.
|
|
1055
|
+
*/
|
|
1056
|
+
dormant?: number;
|
|
1057
|
+
/**
|
|
1058
|
+
* Dormant memories deleted for good this sleep because they outlived
|
|
1059
|
+
* `dormant.retentionDays`. Absent when 0. Same per-invocation class as
|
|
1060
|
+
* `removed`.
|
|
1061
|
+
*/
|
|
1062
|
+
dormantExpired?: number;
|
|
1004
1063
|
mergedEpisodic: number;
|
|
1005
1064
|
newSemantic: number;
|
|
1006
1065
|
dryRun: boolean;
|
package/dist/api.js
CHANGED
|
@@ -11,6 +11,8 @@ 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';
|
|
14
16
|
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
15
17
|
import { createMemory, applyOutcome, calculateStrength, Layer, } from './memory.js';
|
|
16
18
|
import { appendAuditEvent, queryAuditEvents, auditMemories, isContentWorthStoring, } from './audit.js';
|
|
@@ -84,9 +86,9 @@ export class ForbiddenError extends Error {
|
|
|
84
86
|
// back-compat (`api.isPrivateScope`, test imports). NOTE: the import statement
|
|
85
87
|
// is required — a bare `export { x } from` re-export does not bind the local
|
|
86
88
|
// names this module's ~9 call sites use.
|
|
87
|
-
import { isPrivateScope, passesScopeFilterForRecall } from './recall-scope.js';
|
|
89
|
+
import { isPrivateScope, passesScopeFilterForRecall, assertScopeRequestAllowed } from './recall-scope.js';
|
|
88
90
|
export { isPrivateScope, passesScopeFilterForRecall };
|
|
89
|
-
export { passesCliRecallScopeFilter } from './recall-scope.js';
|
|
91
|
+
export { passesCliRecallScopeFilter, ScopeForbiddenError } from './recall-scope.js';
|
|
90
92
|
// v39: classifyOriginProject lives in project-identity.ts (leaf) so
|
|
91
93
|
// shared.ts can use it without an api.ts import cycle. Re-exported here for
|
|
92
94
|
// callers that already import the api surface.
|
|
@@ -184,11 +186,14 @@ export function buildSuppressionSummary(counts) {
|
|
|
184
186
|
* `tests/api-recall-no-side-effects.test.ts`.
|
|
185
187
|
*/
|
|
186
188
|
export function recall(ctx, opts) {
|
|
189
|
+
// A member key may not unlock a private or quarantined scope by naming it.
|
|
190
|
+
assertScopeRequestAllowed(ctx.actor.role, opts.scope);
|
|
187
191
|
const windowSize = recallWindowSize(opts);
|
|
188
192
|
return recallFrom(ctx, opts, windowSize, loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false));
|
|
189
193
|
}
|
|
190
194
|
/** Mode-aware recall that strengthens each returned row; never writes last_retrieval_ids (v1.11.5 lock). */
|
|
191
195
|
export async function retrieve(ctx, opts) {
|
|
196
|
+
assertScopeRequestAllowed(ctx.actor.role, opts.scope);
|
|
192
197
|
const windowSize = recallWindowSize(opts);
|
|
193
198
|
let candidates = loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false);
|
|
194
199
|
if (opts.mode === 'hybrid' || opts.mode === 'physics') {
|
|
@@ -471,7 +476,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
471
476
|
// prepended (NOT rows already in baseRanked that got tagged isFreshTail).
|
|
472
477
|
freshTailAddedCount = freshRanked.length;
|
|
473
478
|
rankedOut = [...freshRanked, ...baseRanked, ...summaryRanked];
|
|
474
|
-
tokensOut = rankedOut.reduce((acc, r) => acc +
|
|
479
|
+
tokensOut = rankedOut.reduce((acc, r) => acc + estimateTokens(r.content), 0);
|
|
475
480
|
totalOut = entries.length;
|
|
476
481
|
// TODO(a1-task-4): emit via the shared audit hook in store.ts so we don't
|
|
477
482
|
// double-emit. Recall does not currently write through writeEntry, so no
|
|
@@ -553,7 +558,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
553
558
|
sessionHandoff: filteredHandoff,
|
|
554
559
|
recentSessionEvents: filteredEvents,
|
|
555
560
|
};
|
|
556
|
-
const tokenize = (s) => s ?
|
|
561
|
+
const tokenize = (s) => s ? estimateTokens(s) : 0;
|
|
557
562
|
continuityTokens =
|
|
558
563
|
tokenize(filteredSnapshot?.task) +
|
|
559
564
|
tokenize(filteredSnapshot?.summary) +
|
|
@@ -719,6 +724,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
719
724
|
* - all rows fail the scope/tenant filter
|
|
720
725
|
*/
|
|
721
726
|
export function assemble(ctx, sessionId, opts = {}) {
|
|
727
|
+
assertScopeRequestAllowed(ctx.actor.role, opts.scope);
|
|
722
728
|
const budget = opts.budget ?? 4000;
|
|
723
729
|
const freshTailCount = opts.freshTailCount ?? 10;
|
|
724
730
|
const summarizeOlder = opts.summarizeOlder ?? true;
|
|
@@ -818,7 +824,7 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
818
824
|
olderItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
819
825
|
tailItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
820
826
|
let items = [...olderItems, ...tailItems];
|
|
821
|
-
let tokens = items.reduce((acc, it) => acc +
|
|
827
|
+
let tokens = items.reduce((acc, it) => acc + estimateTokens(it.content), 0);
|
|
822
828
|
let evicted = 0;
|
|
823
829
|
while (tokens > budget && items.length > 0) {
|
|
824
830
|
let worstIdx = -1;
|
|
@@ -833,7 +839,7 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
833
839
|
}
|
|
834
840
|
if (worstIdx === -1)
|
|
835
841
|
break;
|
|
836
|
-
const cost =
|
|
842
|
+
const cost = estimateTokens(items[worstIdx].content);
|
|
837
843
|
items = items.filter((_, i) => i !== worstIdx);
|
|
838
844
|
tokens -= cost;
|
|
839
845
|
evicted++;
|
|
@@ -917,7 +923,7 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
917
923
|
const out = [];
|
|
918
924
|
let used = 0;
|
|
919
925
|
for (const c of collected) {
|
|
920
|
-
const t =
|
|
926
|
+
const t = estimateTokens(c.content);
|
|
921
927
|
if (out.length > 0 && used + t > opts.budget) {
|
|
922
928
|
truncated = true;
|
|
923
929
|
break;
|
|
@@ -1858,6 +1864,174 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1858
1864
|
ambientState,
|
|
1859
1865
|
};
|
|
1860
1866
|
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Record memory text handed to an agent in the token ledger (ROADMAP TE0).
|
|
1869
|
+
* Best-effort: never throws, because a ledger failure must not fail the
|
|
1870
|
+
* recall or context call that produced the text.
|
|
1871
|
+
*/
|
|
1872
|
+
export function recordTokens(ctx, surface, use) {
|
|
1873
|
+
try {
|
|
1874
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1875
|
+
try {
|
|
1876
|
+
recordTokenUse(db, {
|
|
1877
|
+
tenantId: ctx.tenantId,
|
|
1878
|
+
sessionId: use.sessionId ?? null,
|
|
1879
|
+
surface,
|
|
1880
|
+
event: 'inject',
|
|
1881
|
+
items: use.items,
|
|
1882
|
+
tokens: use.tokens,
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
finally {
|
|
1886
|
+
closeHippoDb(db);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
catch {
|
|
1890
|
+
// Ledger is best-effort.
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
1895
|
+
* tokens sent per surface, blocks skipped as unchanged and the tokens that
|
|
1896
|
+
* saved, and mean tokens per session.
|
|
1897
|
+
*/
|
|
1898
|
+
export function tokenSummary(ctx, opts = {}) {
|
|
1899
|
+
const days = opts.days !== undefined && Number.isFinite(opts.days) && opts.days > 0 ? opts.days : 30;
|
|
1900
|
+
const since = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
1901
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1902
|
+
try {
|
|
1903
|
+
return summarizeTokenUse(db, ctx.tenantId, since);
|
|
1904
|
+
}
|
|
1905
|
+
finally {
|
|
1906
|
+
closeHippoDb(db);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* A tenant's dormant memories (src/dormant.ts): what sleep moved out of
|
|
1911
|
+
* active memory instead of deleting, when `dormant.enabled` is on. Newest
|
|
1912
|
+
* first; `opts.query` keeps rows containing every term (case-insensitive).
|
|
1913
|
+
*/
|
|
1914
|
+
export function listDormant(ctx, opts = {}) {
|
|
1915
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1916
|
+
try {
|
|
1917
|
+
return listDormantRows(db, ctx.tenantId, opts);
|
|
1918
|
+
}
|
|
1919
|
+
finally {
|
|
1920
|
+
closeHippoDb(db);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Bring a dormant memory back into active memory. It returns as if just
|
|
1925
|
+
* recalled: `last_retrieved` is now, so it gets a full half-life before it
|
|
1926
|
+
* can fade again. Every other field is the snapshot taken when it went
|
|
1927
|
+
* dormant.
|
|
1928
|
+
*
|
|
1929
|
+
* Throws when the tenant has no dormant memory with that id (another
|
|
1930
|
+
* tenant's id reads the same way), when a live memory already holds the id,
|
|
1931
|
+
* and RejectedValueError when the value has been rejected since. On any
|
|
1932
|
+
* throw the dormant copy stays where it is.
|
|
1933
|
+
*/
|
|
1934
|
+
export function restoreDormant(ctx, id) {
|
|
1935
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1936
|
+
try {
|
|
1937
|
+
let restored;
|
|
1938
|
+
db.exec('BEGIN IMMEDIATE');
|
|
1939
|
+
try {
|
|
1940
|
+
const dormant = readDormantSnapshot(db, ctx.tenantId, id);
|
|
1941
|
+
if (!dormant) {
|
|
1942
|
+
throw new Error(`dormant memory not found: ${id}`);
|
|
1943
|
+
}
|
|
1944
|
+
if (db.prepare(`SELECT 1 FROM memories WHERE id = ?`).get(id) !== undefined) {
|
|
1945
|
+
throw new Error(`memory ${id} is already active; forget it before restoring its dormant copy`);
|
|
1946
|
+
}
|
|
1947
|
+
const now = new Date();
|
|
1948
|
+
// Dormant rows are long-lived, so a snapshot can predate a field added
|
|
1949
|
+
// later: createMemory supplies a default for anything it lacks, then
|
|
1950
|
+
// the snapshot overrides every field it does carry, content included.
|
|
1951
|
+
// (The placeholder only satisfies createMemory's minimum length, so a
|
|
1952
|
+
// legacy row shorter than 3 chars can still be restored.)
|
|
1953
|
+
const revived = {
|
|
1954
|
+
...createMemory('dormant snapshot defaults'),
|
|
1955
|
+
...dormant.entry,
|
|
1956
|
+
last_retrieved: now.toISOString(),
|
|
1957
|
+
};
|
|
1958
|
+
restored = stampOriginProject(ctx.hippoRoot, { ...revived, strength: calculateStrength(revived, now) });
|
|
1959
|
+
writeEntryDbOnly(db, restored, { actor: ctx.actor.subject });
|
|
1960
|
+
deleteDormantRow(db, ctx.tenantId, id);
|
|
1961
|
+
// A restore is a labelled "forgot it, then needed it" event: the
|
|
1962
|
+
// signal a learned lifecycle (ROADMAP LC3) trains on. Same transaction
|
|
1963
|
+
// as the restore, so the label exists exactly when the restore does.
|
|
1964
|
+
appendAuditEvent(db, {
|
|
1965
|
+
tenantId: ctx.tenantId,
|
|
1966
|
+
actor: ctx.actor.subject,
|
|
1967
|
+
op: 'dormant_restore',
|
|
1968
|
+
targetId: id,
|
|
1969
|
+
metadata: {
|
|
1970
|
+
reason: dormant.reason,
|
|
1971
|
+
strengthAtDormancy: dormant.strength,
|
|
1972
|
+
dormantAt: dormant.dormantAt,
|
|
1973
|
+
daysDormant: Math.max(0, (now.getTime() - Date.parse(dormant.dormantAt)) / (24 * 60 * 60 * 1000)),
|
|
1974
|
+
},
|
|
1975
|
+
});
|
|
1976
|
+
db.exec('COMMIT');
|
|
1977
|
+
}
|
|
1978
|
+
catch (err) {
|
|
1979
|
+
try {
|
|
1980
|
+
db.exec('ROLLBACK');
|
|
1981
|
+
}
|
|
1982
|
+
catch { /* already rolled back */ }
|
|
1983
|
+
if (err instanceof RejectedValueError) {
|
|
1984
|
+
auditRejectionRefusal(db, err, ctx.actor.subject);
|
|
1985
|
+
}
|
|
1986
|
+
throw err;
|
|
1987
|
+
}
|
|
1988
|
+
writeEntryMirrors(ctx.hippoRoot, db, restored);
|
|
1989
|
+
return restored;
|
|
1990
|
+
}
|
|
1991
|
+
finally {
|
|
1992
|
+
closeHippoDb(db);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Permanently delete a dormant memory: the explicit "forget it for good"
|
|
1997
|
+
* that dormant storage leaves to the user. Throws when the tenant has no
|
|
1998
|
+
* dormant memory with that id.
|
|
1999
|
+
*/
|
|
2000
|
+
export function forgetDormant(ctx, id) {
|
|
2001
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2002
|
+
try {
|
|
2003
|
+
if (!deleteDormantRow(db, ctx.tenantId, id)) {
|
|
2004
|
+
throw new Error(`dormant memory not found: ${id}`);
|
|
2005
|
+
}
|
|
2006
|
+
try {
|
|
2007
|
+
appendAuditEvent(db, {
|
|
2008
|
+
tenantId: ctx.tenantId,
|
|
2009
|
+
actor: ctx.actor.subject,
|
|
2010
|
+
op: 'forget',
|
|
2011
|
+
targetId: id,
|
|
2012
|
+
metadata: { dormant: true },
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
catch {
|
|
2016
|
+
// Best-effort, like every other forget audit row: the delete stands.
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
finally {
|
|
2020
|
+
closeHippoDb(db);
|
|
2021
|
+
}
|
|
2022
|
+
// Counted like every other permanent removal (forget, archiveRaw).
|
|
2023
|
+
updateStats(ctx.hippoRoot, { forgotten: 1 });
|
|
2024
|
+
}
|
|
2025
|
+
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
2026
|
+
export function isDormant(ctx, id) {
|
|
2027
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2028
|
+
try {
|
|
2029
|
+
return hasDormantRow(db, ctx.tenantId, id);
|
|
2030
|
+
}
|
|
2031
|
+
finally {
|
|
2032
|
+
closeHippoDb(db);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1861
2035
|
const DEFAULT_SLEEP_PHASES = {
|
|
1862
2036
|
consolidate,
|
|
1863
2037
|
deduplicateStore,
|
|
@@ -1921,6 +2095,14 @@ export async function sleep(ctx, opts = {}) {
|
|
|
1921
2095
|
dryRun,
|
|
1922
2096
|
details: consolidateResult.details,
|
|
1923
2097
|
};
|
|
2098
|
+
// Set only when non-zero, so a store without dormant memories gets a
|
|
2099
|
+
// byte-identical result (HTTP /v1/sleep, the CLI render snapshot).
|
|
2100
|
+
if (consolidateResult.dormant > 0) {
|
|
2101
|
+
result.dormant = consolidateResult.dormant;
|
|
2102
|
+
}
|
|
2103
|
+
if (consolidateResult.dormantExpired > 0) {
|
|
2104
|
+
result.dormantExpired = consolidateResult.dormantExpired;
|
|
2105
|
+
}
|
|
1924
2106
|
// Phase 2: Dedup (post-consolidate near-duplicate cleanup).
|
|
1925
2107
|
const dedupResult = phases.deduplicateStore(ctx.hippoRoot, { dryRun, actor: ctx.actor.subject });
|
|
1926
2108
|
dedupCount = dedupResult.removed;
|
package/dist/audit.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface AuditResult {
|
|
|
16
16
|
export declare function auditMemory(entry: MemoryEntry): AuditIssue | null;
|
|
17
17
|
export declare function auditMemories(entries: MemoryEntry[]): AuditResult;
|
|
18
18
|
export declare function isContentWorthStoring(content: string): boolean;
|
|
19
|
-
export type AuditOp = 'remember' | 'recall' | 'promote' | 'supersede' | 'forget' | 'archive_raw' | 'auth_revoke' | 'auth_create' | 'outcome' | 'consolidate' | 'audit_prune' | 'summary_marked_dirty' | 'summary_marked_clean' | 'summary_rebuilt' | 'predict_create' | 'predict_close' | 'predict_baserate' | 'recall_autodebias_hint' | 'recall_autodebias_hint_no_class_match' | 'recall_autodebias_hint_tiebreak' | 'recall_anchor_detected_query_repeat' | 'recall_anchor_detected_memory_dominance' | 'recall_anchor_skipped_no_session' | 'recall_availability_detected' | 'decision_create' | 'decision_supersede' | 'decision_close' | 'incident_open' | 'incident_resolve' | 'incident_close' | 'process_create' | 'process_supersede' | 'process_close' | 'policy_create' | 'policy_supersede' | 'policy_close' | 'skill_create' | 'skill_supersede' | 'skill_close' | 'project_brief_create' | 'project_brief_supersede' | 'project_brief_close' | 'customer_note_create' | 'customer_note_supersede' | 'customer_note_close' | 'mv_rescue' | 'reject_value' | 'reject_refusal' | 'unreject_value' | 'conflict_resolve';
|
|
19
|
+
export type AuditOp = 'remember' | 'recall' | 'promote' | 'supersede' | 'forget' | 'archive_raw' | 'auth_revoke' | 'auth_create' | 'outcome' | 'consolidate' | 'audit_prune' | 'summary_marked_dirty' | 'summary_marked_clean' | 'summary_rebuilt' | 'predict_create' | 'predict_close' | 'predict_baserate' | 'recall_autodebias_hint' | 'recall_autodebias_hint_no_class_match' | 'recall_autodebias_hint_tiebreak' | 'recall_anchor_detected_query_repeat' | 'recall_anchor_detected_memory_dominance' | 'recall_anchor_skipped_no_session' | 'recall_availability_detected' | 'decision_create' | 'decision_supersede' | 'decision_close' | 'incident_open' | 'incident_resolve' | 'incident_close' | 'process_create' | 'process_supersede' | 'process_close' | 'policy_create' | 'policy_supersede' | 'policy_close' | 'skill_create' | 'skill_supersede' | 'skill_close' | 'project_brief_create' | 'project_brief_supersede' | 'project_brief_close' | 'customer_note_create' | 'customer_note_supersede' | 'customer_note_close' | 'mv_rescue' | 'reject_value' | 'reject_refusal' | 'unreject_value' | 'half_life_migrate' | 'dormant_restore' | 'conflict_resolve';
|
|
20
20
|
export interface AppendAuditOpts {
|
|
21
21
|
tenantId: string;
|
|
22
22
|
actor: string;
|