hippo-memory 1.63.2 → 1.64.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 +10 -4
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -0
- 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 +8 -4
package/README.md
CHANGED
|
@@ -827,11 +827,11 @@ The block asks for nothing a hook already does, because each extra tool call re-
|
|
|
827
827
|
|
|
828
828
|
For Claude Code, it also adds 7 hook entries to `~/.claude/settings.json`:
|
|
829
829
|
- a `SessionEnd` hook that runs `hippo sleep` and then `hippo capture` when the session exits. Capture matches the last 20 user and 10 assistant messages of the transcript against word patterns for decisions, rules, errors and preferences. It uses no model and does not read earlier turns, so record lessons with `hippo remember` as you go.
|
|
830
|
-
- a `SessionStart` hook that prints the previous session's consolidation
|
|
830
|
+
- a `SessionStart` hook that prints the previous session's consolidation log on stderr, where `claude --debug` shows it. When the last sleep failed or a compaction summary was set aside, it shows you one line that points to `hippo doctor`.
|
|
831
831
|
- 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 up to 5 memories that share words with your prompt. When nothing matches, it adds only the pinned ones. Since 1.55.0 this replaces the 5 newest memories, which cut the median block from 847 to 533 tokens in our eval. `{"pinnedInject":{"promptRecall":false}}` brings back the 5 newest, so fresh same-session lessons appear on the next prompt whatever you ask. 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. The prompt-matched memories go in a separate block that is never skipped, so they are sent on every prompt they match. `{"pinnedInject":{"skipUnchanged":false}}` sends the pinned block every turn as before. Opt out entirely with `{"pinnedInject":{"enabled":false}}` in `.hippo/config.json`.
|
|
832
832
|
- a `PreCompact` hook that runs `hippo pre-compact` before the transcript gets summarized. It records the compaction in the store, saves a working-state snapshot (task/summary/next step) so mid-session compaction can't drop it, and asks the summariser to end its summary with a "Memories for hippo" list: the lessons, decisions and corrections from the session that should outlive it. The `SessionEnd` hook still owns extracting durable memories from the transcript.
|
|
833
833
|
- a second `SessionStart` hook (matcher `compact`) that runs `hippo compact-resume`, printing that snapshot back into context right after compaction, if it is under 15 minutes old.
|
|
834
|
-
- a `PostCompact` hook that runs `hippo post-compact`. It keeps the summary in the store with secrets scrubbed, and saves each item of that list as a memory that sleep never deletes: at most 10 per compaction, skipping an item an earlier compaction already saved and any item that looks like a secret. An item over 500 characters stays in the compaction's record only. It then prints one line, such as "Hippo saved 3 memories from this compaction and restored your task snapshot." If the store is busy, the summary waits in the store's `compactions-spool/` folder and `hippo sleep` finishes the save; `hippo doctor` names any compaction left unfinished for over 10 minutes. The session that compacted does not have those memories injected back into its own prompts, since it just read them in the summary; `hippo recall` still finds them. The hook prints nothing when there is no store to save to.
|
|
834
|
+
- a `PostCompact` hook that runs `hippo post-compact`. It keeps the summary in the store with secrets scrubbed, and saves each item of that list as a memory that sleep never deletes: at most 10 per compaction, skipping an item an earlier compaction already saved and any item that looks like a secret. An item over 500 characters stays in the compaction's record only. It then prints one line, such as "Hippo saved 3 memories from this compaction and restored your task snapshot." If the store is busy, the summary waits in the store's `compactions-spool/` folder and `hippo sleep` finishes the save; `hippo doctor` names any compaction left unfinished for over 10 minutes. It also counts spool files that are waiting, stale (left by a replay that stopped) or set aside as `.bad`. The session that compacted does not have those memories injected back into its own prompts, since it just read them in the summary; `hippo recall` still finds them. The hook prints nothing when there is no store to save to.
|
|
835
835
|
- 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.
|
|
836
836
|
|
|
837
837
|
Only Claude Code saves memories at a compaction: hippo installs no `PreCompact` or `PostCompact` hook for Codex, Cursor, OpenCode, OpenClaw or Pi.
|
|
@@ -893,6 +893,12 @@ No global install needed: `"command": "npx", "args": ["-y", "hippo-memory", "mcp
|
|
|
893
893
|
|
|
894
894
|
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`.
|
|
895
895
|
|
|
896
|
+
On a store whose `config.json` sets `"sharedStore": true`, `hippo serve` answers MCP at `POST /mcp`, and each request names the caller's repo in two headers: `X-Hippo-Project` and an optional comma-separated `X-Hippo-Project-Aliases` (at most 10). The client percent-encodes each name, so any lowercase Unicode name fits. The server answers 400 for either header sent twice, aliases with no project, a blank, padded or uppercase name, a colon, a name over 256 characters, or a bad percent escape; a bad key still gets 401 first. With a project, `hippo_remember` stamps it on the row, and recall, assemble, drill, context and conflicts show only that project's rows and user-global ones; `hippo_outcome` rates only the recall made under that project. Without one, every tool returns an `isError` refusal that names the header, except `hippo_predict_baserate`. `hippo_learn`, `hippo_share`, `hippo_resolve` and `hippo_peers` are off on a shared store whatever the headers say; the admin resolves conflicts with the CLI. All headers must fit Node's 16 KB limit, and a larger request gets 431. Other stores ignore both headers. Every `/mcp` reply from any store, errors included, carries `X-Hippo-Project-Scoped: 1`, so a client can tell this server reads the project headers where an older one silently ignores them; `hippo-memory/project-identity` exports the name as `MCP_PROJECT_SCOPED_HEADER`.
|
|
897
|
+
|
|
898
|
+
The headers are a filter the caller picks, not access control: any member key can name another project. The tenant is the security boundary, and binding a key to its projects is a backlog item.
|
|
899
|
+
|
|
900
|
+
`hippo_peers` reads the server user's own global store, never the repos that share this one, which is why a shared store turns it off.
|
|
901
|
+
|
|
896
902
|
### OpenClaw Plugin
|
|
897
903
|
|
|
898
904
|
Native plugin with auto-context injection, workspace-aware memory lookup, and
|
|
@@ -967,7 +973,7 @@ The AI-memory category matured fast in 2026. Hippo's specific take (bio-decay, s
|
|
|
967
973
|
| Auto-hook install | Yes | No | No | No | No | No | No | No | No | No |
|
|
968
974
|
| MCP server | Yes | Yes | Yes (hosted, needs an account) | Yes | Yes (stdio + HTTP/OAuth) | Yes (hosted, needs an account) | Yes (hosted, needs an API key) | Yes (first-party Claude/LangGraph) | Yes | ? |
|
|
969
975
|
| Zero runtime deps | Yes | No (ChromaDB) | No | No | No (PGLite or PG+pgvector) | No (managed service) | No (npm deps) | No (Python deps) | Yes (single Rust binary) | No (managed + OSS) |
|
|
970
|
-
| LongMemEval (best published) | 98.0%
|
|
976
|
+
| LongMemEval (best published) | 98.0% any / 88.5% all R@5\* (local MiniLM; 99.8% any-evidence with voyage-3-large; s_cleaned, per-haystack) | 96.6% raw / 100% reranked R@5 | 94.4 (hosted platform)\*\* | N/A | 95.53% all R@5\* (paid reranker; 93.19% without; s_cleaned) | 90.2% accuracy\*\* (LoCoMo 94.7%) | N/A | N/A | 88.78% accuracy\*\* (overall, with a reader) | 83.00% accuracy\*\* (overall; LoCoMo 93.05%, HaluMem 93.04%) |
|
|
971
977
|
| Git-friendly | Yes | No | No | Yes | Yes | No | Yes (memory tracked in git) | No | Yes (Git is the model) | ? |
|
|
972
978
|
| Framework agnostic | Yes | Yes | Partial | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
|
973
979
|
| License | MIT | (open) | Apache-2.0 | (open) | MIT | Proprietary cloud (Graphiti: Apache-2.0) | Apache-2.0 | MIT (core) | Apache-2.0 | Apache-2.0 (OSS) + cloud |
|
|
@@ -1157,7 +1163,7 @@ The interesting problems:
|
|
|
1157
1163
|
|
|
1158
1164
|
Hippo is open core. This repository provides the MIT core for individual developers and self-hosted teams: the CLI, the MCP server, supported hooks and adapters, connectors, the dashboard, tenants, API keys, admin/member roles, per-key scope grants and the audit log. Automatic capture and delivery depend on the configured integration; the all-agent low-touch acceptance work remains planned. Code published here stays MIT and stays here.
|
|
1159
1165
|
|
|
1160
|
-
The commercial edition is planned; its private repository is a scaffold, not a released enterprise product. It is intended for larger companies as a separate package under a commercial licence from KITFUNSO LTD. Planned capabilities include SSO (OIDC and SAML sign-in), SCIM, organisation/team/project policy, an org admin view, the pilot report and telemetry join, SIEM export of the audit log, offline licence keys, hosted SaaS, and support with an SLA. Pull requests implementing those commercial features belong there; see [CONTRIBUTING.md](CONTRIBUTING.md). Release availability and independently verified benefit are separate milestones in the [roadmap](https://github.com/kitfunso/hippo-memory/blob/master/ROADMAP.md#current-execution-index).
|
|
1166
|
+
The commercial edition is planned; its private repository is a scaffold, not a released enterprise product. It is intended for larger companies as a separate package under a commercial licence from KITFUNSO LTD. Planned capabilities include SSO (OIDC and SAML sign-in), SCIM, organisation/team/project policy, an org admin view, the pilot report and telemetry join, SIEM export of the audit log, offline licence keys, serving agent hooks to laptops that keep no store of their own, hosted SaaS, and support with an SLA. Pull requests implementing those commercial features belong there; see [CONTRIBUTING.md](CONTRIBUTING.md). Release availability and independently verified benefit are separate milestones in the [roadmap](https://github.com/kitfunso/hippo-memory/blob/master/ROADMAP.md#current-execution-index).
|
|
1161
1167
|
|
|
1162
1168
|
## License
|
|
1163
1169
|
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { realpathOrResolve, resolveProjectIdentity } from '../project-identity.js';
|
|
5
5
|
import { isStringValue } from '../capture-contract.js';
|
|
6
|
-
import { isJsonObject } from '../hooks/shared.js';
|
|
6
|
+
import { claudeConfigDir, isJsonObject } from '../hooks/shared.js';
|
|
7
7
|
import { expandHome, frontmatterField, itemTime, readTextFile, splitFrontmatter } from './files.js';
|
|
8
8
|
import { markdownNotes, readFolderStore, uniqueFolders } from './folder-store.js';
|
|
9
9
|
import { gitLayout } from './git.js';
|
|
@@ -39,7 +39,7 @@ export function claudeFolderName(root) {
|
|
|
39
39
|
export const claudeCodeAdapter = {
|
|
40
40
|
tool: 'claude-code',
|
|
41
41
|
list(ctx, scope) {
|
|
42
|
-
const config =
|
|
42
|
+
const config = claudeConfigDir(ctx.home, ctx.env);
|
|
43
43
|
const warnings = [];
|
|
44
44
|
const folders = scope === 'project' ? projectFolders(ctx, config) : userFolders(ctx, config, warnings);
|
|
45
45
|
return { tool: 'claude-code', home: config, containers: readFolders(folders, scope, ctx.platform), warnings };
|
|
@@ -47,7 +47,7 @@ export const claudeCodeAdapter = {
|
|
|
47
47
|
};
|
|
48
48
|
/** A session's own notes folder and nothing else, with no git call, so post-compact can read it inside the hook's time limit. */
|
|
49
49
|
export function claudeTranscriptListing(ctx, transcriptPath) {
|
|
50
|
-
const config =
|
|
50
|
+
const config = claudeConfigDir(ctx.home, ctx.env);
|
|
51
51
|
const folder = path.join(path.dirname(transcriptPath), 'memory');
|
|
52
52
|
return { tool: 'claude-code', home: config, containers: readFolders([folder], 'project', ctx.platform), warnings: [] };
|
|
53
53
|
}
|
package/dist/ambient-store.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type AmbientTallies } from './ambient.js';
|
|
|
2
2
|
/** The rows an ambient summary describes: a context read's envelope, origin partition and tag secret veto. */
|
|
3
3
|
export interface AmbientStoreFilter {
|
|
4
4
|
exactScope?: string;
|
|
5
|
+
/** The reader's personal scope, which the default deny admits. */
|
|
6
|
+
ownScope?: string;
|
|
5
7
|
/** Rows carrying one of these project names, and user-global rows, pass; absent admits every origin. */
|
|
6
8
|
project?: readonly string[];
|
|
7
9
|
/** The reader's project names: a secret-tagged row counts only inside its own non-empty origin project. */
|
package/dist/ambient-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { closeHippoDb } from './db.js';
|
|
2
2
|
import { strengthSql } from './memory.js';
|
|
3
|
-
import {
|
|
3
|
+
import { scopeAdmitSql } from './recall-scope.js';
|
|
4
4
|
import { SECRET_TAGS } from './secret-detect.js';
|
|
5
5
|
import { openStore } from './store/open.js';
|
|
6
6
|
import { originInSql } from './project-identity.js';
|
|
@@ -16,8 +16,9 @@ function contextRowsWhere(tenantId, filter) {
|
|
|
16
16
|
params.push(filter.exactScope);
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const admit = scopeAdmitSql('', filter.ownScope);
|
|
20
|
+
where.push(admit.sql);
|
|
21
|
+
params.push(...admit.params);
|
|
21
22
|
}
|
|
22
23
|
if (filter.project !== undefined) {
|
|
23
24
|
where.push(`(origin_project = '' OR ${originInSql(filter.project)})`);
|
package/dist/api/assemble.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CallerProject } from '../prompt-hook.js';
|
|
1
2
|
import type { Context } from './types.js';
|
|
2
3
|
export declare const DEFAULT_ASSEMBLE_BUDGET = 4000;
|
|
3
4
|
export interface AssembleOpts {
|
|
@@ -23,6 +24,7 @@ export interface AssembleOpts {
|
|
|
23
24
|
*/
|
|
24
25
|
rowCap?: number;
|
|
25
26
|
cost?: AssembleCost;
|
|
27
|
+
project?: CallerProject;
|
|
26
28
|
}
|
|
27
29
|
export interface AssembleCost {
|
|
28
30
|
item: (it: AssembledContextItem) => number;
|
package/dist/api/assemble.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Session context assembly under a token budget.
|
|
2
2
|
import { loadEntriesByIds, loadSessionRawMemories, countSessionRawMemories } from '../store/entry-reads.js';
|
|
3
3
|
import { estimateTokens } from '../token-ledger.js';
|
|
4
|
-
import { passesScopeFilterForRecall, assertScopeRequestAllowed } from '../recall-scope.js';
|
|
4
|
+
import { passesScopeFilterForRecall, assertScopeRequestAllowed, personalScopeOf } from '../recall-scope.js';
|
|
5
|
+
import { classifyOriginProject, projectNames } from '../project-identity.js';
|
|
5
6
|
export const DEFAULT_ASSEMBLE_BUDGET = 4000;
|
|
6
7
|
/**
|
|
7
8
|
* Build a chronologically-ordered context window for a session. Adapts the
|
|
@@ -31,18 +32,20 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
31
32
|
const freshTailCount = opts.freshTailCount ?? 10;
|
|
32
33
|
const summarizeOlder = opts.summarizeOlder ?? true;
|
|
33
34
|
const rowCap = opts.rowCap ?? 5000;
|
|
35
|
+
const own = personalScopeOf(ctx.actor) ?? undefined;
|
|
34
36
|
if (!sessionId) {
|
|
35
37
|
return { sessionId, items: [], tokens: 0, totalRaw: 0, summarized: 0, evicted: 0, truncated: false };
|
|
36
38
|
}
|
|
37
|
-
const
|
|
39
|
+
const origins = opts.project ? projectNames(opts.project) : undefined;
|
|
40
|
+
const rows = loadSessionRawMemories(ctx.hippoRoot, sessionId, ctx.tenantId, rowCap, origins);
|
|
38
41
|
const truncated = rows.length === rowCap;
|
|
39
42
|
// `scoped.length` under-counts a capped session, so totalRaw falls back to a COUNT below.
|
|
40
|
-
const scoped = rows.filter((r) => passesScopeFilterForRecall(r.scope ?? null, opts.scope));
|
|
43
|
+
const scoped = rows.filter((r) => passesScopeFilterForRecall(r.scope ?? null, opts.scope, own));
|
|
41
44
|
let totalRaw;
|
|
42
45
|
if (truncated) {
|
|
43
46
|
// The COUNT applies the same default-deny scope rule in SQL, so a no-scope
|
|
44
47
|
// caller cannot infer private rows by comparing totalRaw to items.length.
|
|
45
|
-
totalRaw = countSessionRawMemories(ctx.hippoRoot, sessionId, ctx.tenantId, opts.scope);
|
|
48
|
+
totalRaw = countSessionRawMemories(ctx.hippoRoot, sessionId, ctx.tenantId, opts.scope, own, origins);
|
|
46
49
|
}
|
|
47
50
|
else {
|
|
48
51
|
totalRaw = scoped.length;
|
|
@@ -56,7 +59,7 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
56
59
|
const tailRows = scoped.slice(tailStartIdx);
|
|
57
60
|
// Substitute parent summaries for older rows that share one.
|
|
58
61
|
const { olderItems, summarized } = summarizeOlder && olderRows.length > 0
|
|
59
|
-
? substituteSummaries(ctx, olderRows, opts.scope)
|
|
62
|
+
? substituteSummaries(ctx, olderRows, opts.scope, own, opts.project)
|
|
60
63
|
: { olderItems: olderRows.map(rawItem), summarized: 0 };
|
|
61
64
|
const tailItems = tailRows.map((r) => ({
|
|
62
65
|
id: r.id,
|
|
@@ -83,7 +86,7 @@ function rawItem(r) {
|
|
|
83
86
|
strength: r.strength,
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
|
-
function substituteSummaries(ctx, olderRows, scope) {
|
|
89
|
+
function substituteSummaries(ctx, olderRows, scope, own, project) {
|
|
87
90
|
const olderItems = [];
|
|
88
91
|
let summarized = 0;
|
|
89
92
|
const olderByParent = new Map();
|
|
@@ -98,7 +101,8 @@ function substituteSummaries(ctx, olderRows, scope) {
|
|
|
98
101
|
const parents = eligibleParentIds.length > 0
|
|
99
102
|
? loadEntriesByIds(ctx.hippoRoot, eligibleParentIds, ctx.tenantId)
|
|
100
103
|
.filter((p) => (p.dag_level ?? 0) === 2 && !p.superseded_by)
|
|
101
|
-
.filter((p) => passesScopeFilterForRecall(p.scope ?? null, scope))
|
|
104
|
+
.filter((p) => passesScopeFilterForRecall(p.scope ?? null, scope, own))
|
|
105
|
+
.filter((p) => !project || classifyOriginProject(p.origin_project, project) !== 'cross-project')
|
|
102
106
|
: [];
|
|
103
107
|
const claimedRawIds = new Set();
|
|
104
108
|
for (const parent of parents) {
|
package/dist/api/auth.d.ts
CHANGED
|
@@ -30,6 +30,19 @@ export interface AuthCreateResult {
|
|
|
30
30
|
* outranks its minter: a resolver admin is tenant-only, so it mints members.
|
|
31
31
|
*/
|
|
32
32
|
export declare function authCreate(ctx: Context, opts: AuthCreateOpts): AuthCreateResult;
|
|
33
|
+
export interface AuthCreateSelfOpts {
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Days until the new key expires. */
|
|
36
|
+
ttlDays: number;
|
|
37
|
+
/** Live self-minted keys one subject may hold; minting past it revokes the oldest. */
|
|
38
|
+
perSubject: number;
|
|
39
|
+
}
|
|
40
|
+
export interface AuthCreateSelfResult extends AuthCreateResult {
|
|
41
|
+
/** ISO time the key stops working. */
|
|
42
|
+
expiresAt: string;
|
|
43
|
+
}
|
|
44
|
+
/** Mint a member key for the caller an auth resolver vouched for, whatever its role; the binary floor, the cap's revokes, the mint and its audit rows commit or fail together. */
|
|
45
|
+
export declare function authCreateSelf(ctx: Context, opts: AuthCreateSelfOpts): AuthCreateSelfResult;
|
|
33
46
|
/**
|
|
34
47
|
* List API keys visible to the calling tenant.
|
|
35
48
|
*
|
|
@@ -47,21 +60,12 @@ export declare function authListRows(ctx: Context, opts: {
|
|
|
47
60
|
limit?: number;
|
|
48
61
|
after?: KeysetPosition;
|
|
49
62
|
}): ApiKeyListRow[];
|
|
50
|
-
/**
|
|
51
|
-
* Revoke an API key.
|
|
52
|
-
*
|
|
53
|
-
* Security: the key must belong to `ctx.tenantId`. Cross-tenant revoke is
|
|
54
|
-
* rejected with the "not found" message used for missing keys, and a member may
|
|
55
|
-
* revoke only its own key (checked first), so no caller can probe other key_ids.
|
|
56
|
-
*
|
|
57
|
-
* Audit: emits 'auth_revoke' with `tenantId` set to the KEY ROW's tenant_id
|
|
58
|
-
* (mirrors src/cli.ts:cmdAuthRevoke). Skipped on no-op
|
|
59
|
-
* revoke (already revoked) so re-running doesn't pad the audit log.
|
|
60
|
-
*/
|
|
61
63
|
export interface AuthRevokeResult {
|
|
62
64
|
ok: true;
|
|
63
65
|
revokedAt: string;
|
|
64
66
|
}
|
|
67
|
+
/** Revoke a key in the caller's tenant: a member API key may revoke only itself, a resolver member only the keys it minted.
|
|
68
|
+
* The auth_revoke row carries the KEY ROW's tenant, as cmdAuthRevoke does, and is skipped for an already-revoked key. */
|
|
65
69
|
export declare function authRevoke(ctx: Context, keyId: string): AuthRevokeResult;
|
|
66
70
|
/**
|
|
67
71
|
* The tenant that owns `keyId`, or undefined for an unknown key. Host admin only: it reads across tenants,
|
package/dist/api/auth.js
CHANGED
|
@@ -2,9 +2,30 @@
|
|
|
2
2
|
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
3
3
|
import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from '../api-errors.js';
|
|
4
4
|
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
5
|
-
import { createApiKey, listApiKeyRows, revokeApiKey, grantScope, ungrantScope } from '../auth.js';
|
|
5
|
+
import { createApiKey, listApiKeyRows, listLiveOwnedKeyIds, revokeApiKey, grantScope, ungrantScope } from '../auth.js';
|
|
6
|
+
import { DAY_MS } from '../dashboard-snapshot.js';
|
|
6
7
|
import { isRestrictedScope } from '../recall-scope.js';
|
|
7
8
|
import { selectApiKeyOwner } from '../store/tenant-lookup.js';
|
|
9
|
+
const API_KEY_SUBJECT = 'api_key:';
|
|
10
|
+
/** The key id an API-key actor's subject names, or null for any other actor. */
|
|
11
|
+
function keyIdOfSubject(subject) {
|
|
12
|
+
return subject.startsWith(API_KEY_SUBJECT) ? subject.slice(API_KEY_SUBJECT.length) : null;
|
|
13
|
+
}
|
|
14
|
+
function inTransaction(db, fn) {
|
|
15
|
+
db.exec('BEGIN IMMEDIATE');
|
|
16
|
+
try {
|
|
17
|
+
const out = fn();
|
|
18
|
+
db.exec('COMMIT');
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
try {
|
|
23
|
+
db.exec('ROLLBACK');
|
|
24
|
+
}
|
|
25
|
+
catch { /* keep the original error */ }
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
/**
|
|
9
30
|
* Mint a new API key. The new key is ALWAYS bound to `ctx.tenantId`. Callers
|
|
10
31
|
* cannot override the tenant via the opts bag — a previous `tenantId` field
|
|
@@ -26,20 +47,23 @@ export function authCreate(ctx, opts) {
|
|
|
26
47
|
const db = openHippoDb(ctx.hippoRoot);
|
|
27
48
|
try {
|
|
28
49
|
const role = opts.role ?? (ctx.actor.viaAuthResolver ? 'member' : 'admin');
|
|
29
|
-
const
|
|
30
|
-
// Same try/catch as authRevoke so audit failure can't crash a successful mint.
|
|
50
|
+
const mint = () => createApiKey(db, { tenantId: ctx.tenantId, label: opts.label, role });
|
|
31
51
|
// The plaintext is NEVER logged; metadata carries label + role, keyId is non-secret.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
52
|
+
const audit = (keyId) => appendAuditEvent(db, {
|
|
53
|
+
tenantId: ctx.tenantId, actor: ctx.actor.subject, op: 'auth_create', targetId: keyId, metadata: { label: opts.label ?? null, role },
|
|
54
|
+
});
|
|
55
|
+
// A resolver admin's keys are found again only through their audit rows, so the key and its row commit together.
|
|
56
|
+
if (ctx.actor.viaAuthResolver) {
|
|
57
|
+
const result = inTransaction(db, () => {
|
|
58
|
+
const minted = mint();
|
|
59
|
+
audit(minted.keyId);
|
|
60
|
+
return minted;
|
|
42
61
|
});
|
|
62
|
+
return { keyId: result.keyId, plaintext: result.plaintext, tenantId: ctx.tenantId, role };
|
|
63
|
+
}
|
|
64
|
+
const result = mint();
|
|
65
|
+
try {
|
|
66
|
+
audit(result.keyId);
|
|
43
67
|
}
|
|
44
68
|
catch (error) {
|
|
45
69
|
// Audit must not crash a successful mint.
|
|
@@ -51,6 +75,49 @@ export function authCreate(ctx, opts) {
|
|
|
51
75
|
closeHippoDb(db);
|
|
52
76
|
}
|
|
53
77
|
}
|
|
78
|
+
const MAX_TTL_DAYS = 3650;
|
|
79
|
+
/** A RangeError, not a 4xx, since the operator's config sets these; the ttl ceiling keeps toISOString in range and a departed user's key short-lived. */
|
|
80
|
+
function assertSelfMintOpts({ ttlDays, perSubject }) {
|
|
81
|
+
if (!Number.isFinite(ttlDays) || ttlDays <= 0 || ttlDays > MAX_TTL_DAYS) {
|
|
82
|
+
throw new RangeError(`authCreateSelf: ttlDays must be above 0 and at most ${MAX_TTL_DAYS}, got ${String(ttlDays)}`);
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isInteger(perSubject) || perSubject < 1) {
|
|
85
|
+
throw new RangeError(`authCreateSelf: perSubject must be a whole number of at least 1, got ${String(perSubject)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Mint a member key for the caller an auth resolver vouched for, whatever its role; the binary floor, the cap's revokes, the mint and its audit rows commit or fail together. */
|
|
89
|
+
export function authCreateSelf(ctx, opts) {
|
|
90
|
+
if (!ctx.actor.viaAuthResolver) {
|
|
91
|
+
throw new ForbiddenError('Only a caller signed in through the auth resolver can mint its own key');
|
|
92
|
+
}
|
|
93
|
+
assertSelfMintOpts(opts);
|
|
94
|
+
const { tenantId, actor: { subject } } = ctx;
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const expiresAt = new Date(now + opts.ttlDays * DAY_MS).toISOString();
|
|
97
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
98
|
+
try {
|
|
99
|
+
// IMMEDIATE takes the write lock before the count, so two mints for one subject cannot both see room under the cap.
|
|
100
|
+
const result = inTransaction(db, () => {
|
|
101
|
+
const live = listLiveOwnedKeyIds(db, tenantId, subject, now);
|
|
102
|
+
const replaced = live.slice(0, Math.max(0, live.length - opts.perSubject + 1));
|
|
103
|
+
const minted = createApiKey(db, { tenantId, label: opts.label, role: 'member', ownerSubject: subject, expiresAt });
|
|
104
|
+
for (const keyId of replaced) {
|
|
105
|
+
revokeApiKey(db, keyId);
|
|
106
|
+
appendAuditEvent(db, { tenantId, actor: subject, op: 'auth_revoke', targetId: keyId, metadata: { replacedBy: minted.keyId } });
|
|
107
|
+
}
|
|
108
|
+
// Same op and actor as an admin mint, so a lookup by audit row finds this key too.
|
|
109
|
+
appendAuditEvent(db, {
|
|
110
|
+
tenantId, actor: subject, op: 'auth_create', targetId: minted.keyId,
|
|
111
|
+
metadata: { label: opts.label ?? null, role: 'member', self: true, expiresAt },
|
|
112
|
+
});
|
|
113
|
+
return minted;
|
|
114
|
+
});
|
|
115
|
+
return { keyId: result.keyId, plaintext: result.plaintext, tenantId, role: 'member', expiresAt };
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
closeHippoDb(db);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
54
121
|
/**
|
|
55
122
|
* List API keys visible to the calling tenant.
|
|
56
123
|
*
|
|
@@ -66,26 +133,50 @@ export function authList(ctx, opts) {
|
|
|
66
133
|
export function authListRows(ctx, opts) {
|
|
67
134
|
const db = openHippoDb(ctx.hippoRoot);
|
|
68
135
|
try {
|
|
69
|
-
|
|
136
|
+
const filter = memberListFilter(db, ctx);
|
|
137
|
+
return filter === null ? [] : listApiKeyRows(db, { ...opts, tenantId: ctx.tenantId, ...filter });
|
|
70
138
|
}
|
|
71
139
|
finally {
|
|
72
140
|
closeHippoDb(db);
|
|
73
141
|
}
|
|
74
142
|
}
|
|
143
|
+
/** A member sees only the keys its person minted, or just its own key when that has no owner; an admin sees the whole tenant; null means no keys. */
|
|
144
|
+
function memberListFilter(db, ctx) {
|
|
145
|
+
const { actor } = ctx;
|
|
146
|
+
if (actor.role === 'admin')
|
|
147
|
+
return {};
|
|
148
|
+
if (actor.viaAuthResolver)
|
|
149
|
+
return { ownerSubject: actor.subject };
|
|
150
|
+
const keyId = keyIdOfSubject(actor.subject);
|
|
151
|
+
if (keyId === null)
|
|
152
|
+
return null;
|
|
153
|
+
const owner = selectApiKeyOwner(db, keyId)?.ownerSubject;
|
|
154
|
+
return owner ? { ownerSubject: owner } : { keyId };
|
|
155
|
+
}
|
|
156
|
+
/** Throws unless `ctx` may revoke `keyId`; a member gets one answer for a missing, foreign or unowned key, so it cannot probe key ids. */
|
|
157
|
+
function assertMayRevoke(ctx, keyId, row) {
|
|
158
|
+
const { actor } = ctx;
|
|
159
|
+
if (actor.role !== 'admin' && actor.viaAuthResolver && (row?.tenantId !== ctx.tenantId || row.ownerSubject !== actor.subject)) {
|
|
160
|
+
throw new ForbiddenError('A member can revoke only the keys it minted');
|
|
161
|
+
}
|
|
162
|
+
// Cross-tenant access denied: same message as missing key, no info leak.
|
|
163
|
+
if (!row || row.tenantId !== ctx.tenantId) {
|
|
164
|
+
throw new NotFoundError(`Unknown key_id: ${keyId}`);
|
|
165
|
+
}
|
|
166
|
+
if (actor.viaAuthResolver && row.role === 'admin') {
|
|
167
|
+
throw new ForbiddenError('An auth resolver admin cannot revoke an admin key, which outranks it');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Revoke a key in the caller's tenant: a member API key may revoke only itself, a resolver member only the keys it minted.
|
|
171
|
+
* The auth_revoke row carries the KEY ROW's tenant, as cmdAuthRevoke does, and is skipped for an already-revoked key. */
|
|
75
172
|
export function authRevoke(ctx, keyId) {
|
|
76
|
-
if (ctx.actor.role !== 'admin' && ctx.actor.subject !==
|
|
173
|
+
if (ctx.actor.role !== 'admin' && !ctx.actor.viaAuthResolver && keyIdOfSubject(ctx.actor.subject) !== keyId) {
|
|
77
174
|
throw new ForbiddenError('A member key can revoke only itself');
|
|
78
175
|
}
|
|
79
176
|
const db = openHippoDb(ctx.hippoRoot);
|
|
80
177
|
try {
|
|
81
178
|
const row = selectApiKeyOwner(db, keyId);
|
|
82
|
-
|
|
83
|
-
if (!row || row.tenantId !== ctx.tenantId) {
|
|
84
|
-
throw new NotFoundError(`Unknown key_id: ${keyId}`);
|
|
85
|
-
}
|
|
86
|
-
if (ctx.actor.viaAuthResolver && row.role === 'admin') {
|
|
87
|
-
throw new ForbiddenError('An auth resolver admin cannot revoke an admin key, which outranks it');
|
|
88
|
-
}
|
|
179
|
+
assertMayRevoke(ctx, keyId, row);
|
|
89
180
|
let revokedAt;
|
|
90
181
|
let alreadyRevoked = false;
|
|
91
182
|
if (row.revokedAt) {
|
|
@@ -153,6 +244,9 @@ function changeScopeGrant(ctx, keyId, scope, op) {
|
|
|
153
244
|
if (op === 'auth_grant' && row.revokedAt) {
|
|
154
245
|
throw new ConflictError(`${keyId} is revoked; a grant on it would never apply`);
|
|
155
246
|
}
|
|
247
|
+
if (op === 'auth_grant' && /^personal:/i.test(scope)) {
|
|
248
|
+
throw new BadRequestError(`${scope} is a personal scope: only its owner reads it, and no grant can change that`);
|
|
249
|
+
}
|
|
156
250
|
if (!isRestrictedScope(scope)) {
|
|
157
251
|
throw new BadRequestError(`${scope} is not a restricted scope; it is already readable by default`);
|
|
158
252
|
}
|
|
@@ -12,12 +12,15 @@ export interface ContextPlan {
|
|
|
12
12
|
includeRecent: number;
|
|
13
13
|
activeScope: string;
|
|
14
14
|
exactScope: string | undefined;
|
|
15
|
+
/** The caller's personal scope, from its authenticated owner; the default deny admits it. */
|
|
16
|
+
ownScope: string | undefined;
|
|
15
17
|
query: string;
|
|
16
18
|
hasLocal: boolean;
|
|
17
19
|
hasGlobal: boolean;
|
|
18
20
|
globalRoot: string;
|
|
19
21
|
primaryIsGlobal: boolean;
|
|
20
22
|
hasLocalTaskState: boolean;
|
|
23
|
+
sharedStore: boolean;
|
|
21
24
|
config: HippoConfig;
|
|
22
25
|
currentProject: ProjectRef;
|
|
23
26
|
includeCrossProject: boolean;
|
|
@@ -27,13 +30,12 @@ export interface ContextPlan {
|
|
|
27
30
|
price: (entry: MemoryEntry, isGlobal: boolean, promptRecall?: boolean) => number;
|
|
28
31
|
obs: DeliveryObserver | undefined;
|
|
29
32
|
}
|
|
30
|
-
/** The ambient admit rules
|
|
33
|
+
/** The ambient admit rules. */
|
|
31
34
|
export interface ContextAdmission {
|
|
32
35
|
ambientAdmit: (e: MemoryEntry) => boolean;
|
|
33
36
|
admit: (e: MemoryEntry) => boolean;
|
|
34
37
|
/** What the two-store search admits: `admit` without the own-session compaction rule. */
|
|
35
38
|
bothStoresAdmit: (e: MemoryEntry) => boolean;
|
|
36
|
-
digestHidden: () => boolean;
|
|
37
39
|
}
|
|
38
40
|
export interface ContextPools {
|
|
39
41
|
local: AmbientLoadResult;
|
|
@@ -41,7 +43,7 @@ export interface ContextPools {
|
|
|
41
43
|
}
|
|
42
44
|
export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
|
|
43
45
|
export declare const finiteOr: (v: number, dflt: number, min: number) => number;
|
|
44
|
-
/** Pins plus the prompt-recall or recent-N backfill; null means the block is
|
|
46
|
+
/** Pins plus the prompt-recall or recent-N backfill; null means the hook block is turned off, so task state stays out too. */
|
|
45
47
|
export declare function selectPinned(opts: ContextOpts, plan: ContextPlan, left: number, pools: ContextPools, admission: ContextAdmission): ContextResultEntry[] | null;
|
|
46
48
|
/** No query: the strongest memories by strength, up to budget. */
|
|
47
49
|
export declare function selectStrongest(plan: ContextPlan, left: number, pools: ContextPools): ContextResultEntry[];
|
|
@@ -3,7 +3,8 @@ import { openHippoDb, closeHippoDb } from '../db.js';
|
|
|
3
3
|
import { recallScopeFilter } from '../store/search-rows.js';
|
|
4
4
|
import { loadIndex } from '../store/index-and-stats.js';
|
|
5
5
|
import { calculateStrength } from '../memory.js';
|
|
6
|
-
import { appendAuditEvent, auditQueryFields
|
|
6
|
+
import { appendAuditEvent, auditQueryFields } from '../audit.js';
|
|
7
|
+
import { isWorthSurfacing } from '../memory-quality.js';
|
|
7
8
|
import { rankBothStores } from '../shared.js';
|
|
8
9
|
import { evalNow } from '../ablation.js';
|
|
9
10
|
import { hybridSearch } from '../search/hybrid.js';
|
|
@@ -34,7 +35,7 @@ export function oneCopyPerMemory(local, global, now) {
|
|
|
34
35
|
return [local.filter((e) => kept.has(e)), global.filter((e) => kept.has(e))];
|
|
35
36
|
}
|
|
36
37
|
export const finiteOr = (v, dflt, min) => Number.isFinite(v) && v >= min ? v : dflt;
|
|
37
|
-
/** Pins plus the prompt-recall or recent-N backfill; null means the block is
|
|
38
|
+
/** Pins plus the prompt-recall or recent-N backfill; null means the hook block is turned off, so task state stays out too. */
|
|
38
39
|
export function selectPinned(opts, plan, left, pools, admission) {
|
|
39
40
|
const { obs, primaryIsGlobal, config: pinnedCfg } = plan;
|
|
40
41
|
if (!pinnedCfg.pinnedInject.enabled) {
|
|
@@ -65,12 +66,6 @@ export function selectPinned(opts, plan, left, pools, admission) {
|
|
|
65
66
|
else if (plan.includeRecent > 0) {
|
|
66
67
|
backfillRecent(plan, localPool, globalPool, picked, recentBudget, nowP);
|
|
67
68
|
}
|
|
68
|
-
if (pinnedLocal.length === 0 &&
|
|
69
|
-
pinnedGlobal.length === 0 &&
|
|
70
|
-
picked.items.length === 0 &&
|
|
71
|
-
!admission.digestHidden()) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
69
|
admitWithinBudget(rankedPinned, picked, effBudget, obs);
|
|
75
70
|
return picked.items;
|
|
76
71
|
}
|
|
@@ -155,7 +150,7 @@ function promptRecallCandidates(plan, pools, admit, rankedPinned, nowP) {
|
|
|
155
150
|
const pinnedText = new Set(rankedPinned.map((r) => r.entry.content));
|
|
156
151
|
const ineligibleReason = (e) => !admit(e) ? 'scope'
|
|
157
152
|
: e.pinned ? 'pinned'
|
|
158
|
-
: !
|
|
153
|
+
: !isWorthSurfacing(e) ? 'quality'
|
|
159
154
|
: pinnedText.has(e.content) ? 'duplicate'
|
|
160
155
|
: null;
|
|
161
156
|
const eligible = (e) => {
|
|
@@ -199,7 +194,7 @@ function backfillRecent(plan, localPool, globalPool, picked, recentBudget, nowP)
|
|
|
199
194
|
})
|
|
200
195
|
// Filter before slice so a junk row is backfilled past, not counted against N. Pins bypass the floor: a dropped
|
|
201
196
|
// pin's share of the shared budget would go to a backfilled row, and the pin loop could not win it back.
|
|
202
|
-
.filter(({ entry }) => entry.pinned ||
|
|
197
|
+
.filter(({ entry }) => entry.pinned || isWorthSurfacing(entry))
|
|
203
198
|
.slice(0, plan.includeRecent)
|
|
204
199
|
.map(({ entry, isGlobal }) => ({
|
|
205
200
|
entry,
|
|
@@ -271,7 +266,7 @@ async function searchBothStores(ctx, plan, left, minResults, pools, admit) {
|
|
|
271
266
|
}
|
|
272
267
|
/** The vector arm under the lexical window's own tenant, scope and current-row rules. */
|
|
273
268
|
function contextVectorSpec(ctx, plan, admit) {
|
|
274
|
-
return { tenantId: ctx.tenantId, scope: recallScopeFilter(plan.exactScope, 'exact'), includeSuperseded: false, admit };
|
|
269
|
+
return { tenantId: ctx.tenantId, scope: recallScopeFilter(plan.exactScope, 'exact', plan.ownScope), includeSuperseded: false, admit };
|
|
275
270
|
}
|
|
276
271
|
async function searchLocalRows(ctx, plan, left, minResults, localEntries, admit) {
|
|
277
272
|
const { cost, price, primaryIsGlobal, query, config: ctxConfig } = plan;
|
|
@@ -25,7 +25,7 @@ export interface ContextOpts {
|
|
|
25
25
|
/** Envelope scope to match exactly, as in `recall`: admits that scope even when private, after the actor's scope check. */
|
|
26
26
|
exactScope?: string;
|
|
27
27
|
/** With `pinnedOnly`, also inject the N most recent writes that pass the
|
|
28
|
-
* quality floor (`
|
|
28
|
+
* quality floor (`isWorthSurfacing`). Filtering happens BEFORE
|
|
29
29
|
* the take-N, so a caller asking for 5 gets 5 qualifying entries rather
|
|
30
30
|
* than 5-minus-junk; pinned entries bypass the floor. Entries are only
|
|
31
31
|
* skipped for this read, never mutated or deleted. Ignored when
|
|
@@ -55,6 +55,7 @@ export interface ContextOpts {
|
|
|
55
55
|
cost?: ContextCost;
|
|
56
56
|
/** @internal The CLI's delivery-ledger observer; it only reads, so selection is the same with or without it. */
|
|
57
57
|
deliveryObserver?: DeliveryObserver;
|
|
58
|
+
sharedStore?: true;
|
|
58
59
|
}
|
|
59
60
|
/** Budget prices in the text a caller prints, so the budget bounds what reaches the model. */
|
|
60
61
|
export interface ContextCost {
|
package/dist/api/context.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type MemoryEntry } from '../memory.js';
|
|
2
2
|
import { type ProjectRef } from '../project-identity.js';
|
|
3
3
|
import type { ContextOpts, ContextResult } from './context-types.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type Context } from './types.js';
|
|
5
5
|
export { oneCopyPerMemory } from './context-select.js';
|
|
6
6
|
/**
|
|
7
7
|
* The secret half of the ambient policy on its own, for callers
|