hippo-memory 1.62.0 → 1.63.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 +9 -0
- package/dist/agent-memories/apply.d.ts +2 -0
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +2 -1
- package/dist/agent-memories/claude-code.js +4 -4
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.js +37 -23
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +7 -12
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +4 -3
- package/dist/api/context-select.js +9 -11
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +3 -2
- package/dist/api/context.js +20 -20
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +22 -10
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +7 -18
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +3 -2
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +1 -1
- package/dist/capture/compact.js +9 -13
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +8 -24
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +10 -21
- package/dist/cli/explain.js +2 -1
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/playbooks.js +4 -9
- package/dist/cli/projects.js +3 -1
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +6 -18
- package/dist/cli/session-hooks.js +21 -40
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +2 -6
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +7 -12
- package/dist/cli/transfer.js +8 -13
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -0
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +6 -9
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +23 -73
- package/dist/db/continuity.js +2 -2
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +6 -6
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +119 -104
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +5 -6
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +1 -1
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +12 -14
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +3 -3
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +9 -13
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +17 -2
- package/dist/project-merge.js +109 -27
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +70 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +17 -17
- package/dist/shared.js +93 -97
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +8 -8
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +9 -31
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +31 -59
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +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 +1 -1
package/README.md
CHANGED
|
@@ -547,6 +547,15 @@ hippo projects merge hippo-wt-fix hippo --global # fold an old worktree name i
|
|
|
547
547
|
hippo projects repair --global # set aside note copies, fold old worktree names, re-tag merges
|
|
548
548
|
```
|
|
549
549
|
|
|
550
|
+
A project's name is the `id` in a committed `.hippo-project.json`, else its `origin` remote
|
|
551
|
+
(`github.com/acme/api`), else its folder name. So two repos both called `api` no longer share
|
|
552
|
+
memories in the global store. Rows saved under the old folder name stay visible to the
|
|
553
|
+
project, and `hippo projects repair` folds that name into the id unless two projects claim it.
|
|
554
|
+
`hippo sleep` runs that repair once per store after an upgrade, with a backup; in the global store
|
|
555
|
+
it leaves the name folds to you, since only `hippo projects repair --global` lists them for review.
|
|
556
|
+
Set `{"projectIdentity":{"remote":false}}` in the global `config.json` to keep folder names.
|
|
557
|
+
A long-running MCP or HTTP server reads a new project file or remote after a restart.
|
|
558
|
+
|
|
550
559
|
**See what memory costs in tokens.** Every block of memory text hippo hands an agent (the
|
|
551
560
|
per-prompt hook, the block `hippo compact-resume` restores after compaction, `hippo context`,
|
|
552
561
|
`hippo recall`, the MCP tools, the HTTP API) is recorded in a token ledger: counts, surface
|
|
@@ -24,6 +24,8 @@ export interface ContainerWork {
|
|
|
24
24
|
readonly adopt: ReadonlyMap<string, readonly MemoryEntry[]>;
|
|
25
25
|
/** Legacy rows a new row of the key supersedes, by key (second round). */
|
|
26
26
|
readonly replace: ReadonlyMap<string, readonly MemoryEntry[]>;
|
|
27
|
+
/** This container's prefixes under the project's earlier names; their rows move here, so a new id imports nothing twice. */
|
|
28
|
+
readonly legacyPrefixes: readonly string[];
|
|
27
29
|
}
|
|
28
30
|
export interface ContainerOutcome {
|
|
29
31
|
readonly tally: Tally;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// One container's sync in one transaction on the caller's handle: lookup, plan, then every write (plan designs 6 to 8).
|
|
2
2
|
import { appendAuditEvent } from '../audit.js';
|
|
3
|
-
import { deleteDormantRow, dormantSnapshotsBySourcePrefix, insertDormantRow, readDormantSnapshot } from '../dormant.js';
|
|
3
|
+
import { deleteDormantRow, dormantSnapshotsBySourcePrefix, insertDormantRow, readDormantSnapshot, replaceDormantEntry } from '../dormant.js';
|
|
4
4
|
import { gatedWrite } from '../gated-write.js';
|
|
5
5
|
import { Layer, calculateStrength, createMemory } from '../memory.js';
|
|
6
6
|
import { findRejectedValue, rejectionDigest } from '../rejection.js';
|
|
@@ -65,6 +65,8 @@ class ContainerRun {
|
|
|
65
65
|
}
|
|
66
66
|
run() {
|
|
67
67
|
this.adoptLegacy();
|
|
68
|
+
for (const old of this.w.legacyPrefixes)
|
|
69
|
+
this.adoptPrefix(old);
|
|
68
70
|
const live = selectLiveEntriesBySourcePrefix(this.s.db, this.s.tenantId, this.w.prefix);
|
|
69
71
|
for (const row of [...live, ...[...this.w.replace.values()].flat()])
|
|
70
72
|
this.rows.set(row.id, row);
|
|
@@ -116,6 +118,26 @@ class ContainerRun {
|
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
}
|
|
121
|
+
/** Rows filed under an earlier project name keep their id and history; dormant ones move too, scanned only until this prefix holds live rows. */
|
|
122
|
+
adoptPrefix(old) {
|
|
123
|
+
const origin = this.s.originProject ?? null;
|
|
124
|
+
let moved = 0;
|
|
125
|
+
for (const row of selectLiveEntriesBySourcePrefix(this.s.db, this.s.tenantId, old)) {
|
|
126
|
+
const source = this.w.prefix + row.source.slice(old.length);
|
|
127
|
+
const done = this.s.db.prepare(`UPDATE memories SET source = ?, origin_project = COALESCE(?, origin_project) WHERE id = ? AND tenant_id = ? AND source = ?`).run(source, origin, row.id, row.tenantId, row.source);
|
|
128
|
+
if (Number(done.changes ?? 0) === 0)
|
|
129
|
+
continue;
|
|
130
|
+
moved++;
|
|
131
|
+
this.mirror.push({ ...row, source, origin_project: origin ?? row.origin_project });
|
|
132
|
+
}
|
|
133
|
+
this.tally.renamed += moved;
|
|
134
|
+
if (moved === 0 && (this.w.container.items.length === 0 || selectLiveEntriesBySourcePrefix(this.s.db, this.s.tenantId, this.w.prefix).length > 0))
|
|
135
|
+
return;
|
|
136
|
+
for (const snap of dormantSnapshotsBySourcePrefix(this.s.db, this.s.tenantId, old)) {
|
|
137
|
+
const source = this.w.prefix + snap.entry.source.slice(old.length);
|
|
138
|
+
replaceDormantEntry(this.s.db, this.s.tenantId, snap.entry.id, { ...snap.entry, source, origin_project: origin ?? snap.entry.origin_project });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
119
141
|
refusals() {
|
|
120
142
|
const out = new Map();
|
|
121
143
|
for (const item of this.w.container.items) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProjectIdentity } from '../project-identity.js';
|
|
1
2
|
import type { Adapter, AdapterContext, Listing } from './types.js';
|
|
2
3
|
/** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
|
|
3
4
|
export declare function claudeMemoryFolderNames(projectRoot: string, platform: NodeJS.Platform): Set<string>;
|
|
@@ -9,5 +10,5 @@ export declare const claudeCodeAdapter: Adapter;
|
|
|
9
10
|
/** 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. */
|
|
10
11
|
export declare function claudeTranscriptListing(ctx: AdapterContext, transcriptPath: string): Listing;
|
|
11
12
|
/** The project a session folder's notes belong to: the one Claude named the folder for, the session's start folder, else cwd or a parent; null when none matches. */
|
|
12
|
-
export declare function
|
|
13
|
+
export declare function transcriptNotesProject(transcriptPath: string, cwd: string | null, machine: Pick<AdapterContext, 'platform' | 'env'>): ProjectIdentity | null;
|
|
13
14
|
//# sourceMappingURL=claude-code.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Claude Code's auto memory: frontmatter `.md` notes in a per-project folder, plus the `autoMemoryDirectory` user folder.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { realpathOrResolve, resolveProjectIdentity } from '../project-identity.js';
|
|
5
5
|
import { isStringValue } from '../capture-contract.js';
|
|
6
6
|
import { isJsonObject } from '../hooks/shared.js';
|
|
7
7
|
import { expandHome, frontmatterField, itemTime, readTextFile, splitFrontmatter } from './files.js';
|
|
@@ -52,7 +52,7 @@ export function claudeTranscriptListing(ctx, transcriptPath) {
|
|
|
52
52
|
return { tool: 'claude-code', home: config, containers: readFolders([folder], 'project', ctx.platform), warnings: [] };
|
|
53
53
|
}
|
|
54
54
|
/** The project a session folder's notes belong to: the one Claude named the folder for, the session's start folder, else cwd or a parent; null when none matches. */
|
|
55
|
-
export function
|
|
55
|
+
export function transcriptNotesProject(transcriptPath, cwd, machine) {
|
|
56
56
|
const fold = (name) => (machine.platform === 'win32' ? name.toLowerCase() : name);
|
|
57
57
|
const folder = fold(path.basename(path.dirname(transcriptPath)));
|
|
58
58
|
const start = transcriptStartCwd(transcriptPath);
|
|
@@ -60,13 +60,13 @@ export function transcriptNotesOrigin(transcriptPath, cwd, machine) {
|
|
|
60
60
|
if (pinned !== null && fold(pinned) === folder) {
|
|
61
61
|
// A pinned name stands for whatever project the session ran in, so its start folder decides.
|
|
62
62
|
const from = start ?? cwd;
|
|
63
|
-
return from !== null && fs.existsSync(from) ?
|
|
63
|
+
return from !== null && fs.existsSync(from) ? resolveProjectIdentity(from) : null;
|
|
64
64
|
}
|
|
65
65
|
for (const from of [start, cwd]) {
|
|
66
66
|
for (let dir = from === null ? null : path.resolve(from); dir !== null; dir = path.dirname(dir) === dir ? null : path.dirname(dir)) {
|
|
67
67
|
// A folder gone from disk resolves to its bare name, never the project it was in, so it decides nothing.
|
|
68
68
|
if ([dir, realpathOrResolve(dir)].some((d) => fold(claudeFolderName(d)) === folder))
|
|
69
|
-
return fs.existsSync(dir) ?
|
|
69
|
+
return fs.existsSync(dir) ? resolveProjectIdentity(dir) : null;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
return null;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { AGENT_MEMORY_TOOLS } from './tools.js';
|
|
3
3
|
export function emptyTally() {
|
|
4
4
|
return {
|
|
5
|
-
imported: 0, replaced: 0, restored: 0, setAside: 0, untagged: 0, unchanged: 0, adopted: 0, collapsed: 0, retagged: 0,
|
|
5
|
+
imported: 0, replaced: 0, restored: 0, setAside: 0, untagged: 0, unchanged: 0, adopted: 0, renamed: 0, collapsed: 0, retagged: 0,
|
|
6
6
|
handedOver: 0, duplicate: 0, secret: 0, short: 0, rejected: 0, unread: 0, unreadable: 0, busy: 0,
|
|
7
7
|
};
|
|
8
8
|
}
|
|
@@ -47,7 +47,7 @@ export function totalTally(report) {
|
|
|
47
47
|
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
48
48
|
const TALLY_WORDS = {
|
|
49
49
|
imported: 'new', replaced: 'replaced', restored: 'brought back', setAside: 'set aside', untagged: 'pinned and kept',
|
|
50
|
-
unchanged: 'unchanged', adopted: 'taken over from the old Claude import', collapsed: 'duplicates folded', retagged: 'retagged',
|
|
50
|
+
unchanged: 'unchanged', adopted: 'taken over from the old Claude import', renamed: 'moved to the project id', collapsed: 'duplicates folded', retagged: 'retagged',
|
|
51
51
|
handedOver: 'handed over from the global store', duplicate: 'already stored', secret: 'skipped for a secret', short: 'too short',
|
|
52
52
|
rejected: 'skipped as rejected', unread: 'files skipped', unreadable: 'unreadable folders', busy: 'busy folders',
|
|
53
53
|
};
|
|
@@ -6,7 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import { errorMessage } from '../log.js';
|
|
7
7
|
import { loadConfig } from '../config.js';
|
|
8
8
|
import { closeHippoDb, isSqliteBusy, openHippoDb } from '../db.js';
|
|
9
|
-
import {
|
|
9
|
+
import { namesFoldedInto } from '../project-merge.js';
|
|
10
|
+
import { isGlobalStoreRoot, originInSql, projectNames, resolveGlobalRootDir, resolveProjectIdentity } from '../project-identity.js';
|
|
10
11
|
import { duplicateKey, heldTextKeys } from '../same-text.js';
|
|
11
12
|
import { removeEntryMirrors } from '../store/mirrors.js';
|
|
12
13
|
import { initStore, isInitialized } from '../store/open.js';
|
|
@@ -15,7 +16,7 @@ import { selectLiveEntriesBySourcePrefix } from '../store/entry-reads.js';
|
|
|
15
16
|
import { updateStats } from '../store/index-and-stats.js';
|
|
16
17
|
import { resolveTenantId } from '../tenant.js';
|
|
17
18
|
import { setAsideRow, syncContainer } from './apply.js';
|
|
18
|
-
import { claudeCodeAdapter, claudeTranscriptListing,
|
|
19
|
+
import { claudeCodeAdapter, claudeTranscriptListing, transcriptNotesProject } from './claude-code.js';
|
|
19
20
|
import { codexAdapter } from './codex.js';
|
|
20
21
|
import { copilotAdapter } from './copilot.js';
|
|
21
22
|
import { geminiAdapter } from './gemini.js';
|
|
@@ -43,14 +44,14 @@ export function importForStore(hippoRoot, opts) {
|
|
|
43
44
|
export function importProjectMemories(hippoRoot, opts) {
|
|
44
45
|
const ctx = context(opts.machine, { projectRoot: path.dirname(hippoRoot) });
|
|
45
46
|
return runPass({
|
|
46
|
-
scope: 'project', target: hippoRoot, invoking: hippoRoot, list: (a) => a.list(ctx, 'project'), legacy: true, originProject: undefined, handover: true,
|
|
47
|
+
scope: 'project', target: hippoRoot, invoking: hippoRoot, list: (a) => a.list(ctx, 'project'), legacy: true, originProject: undefined, legacyOrigins: [], handover: true,
|
|
47
48
|
}, opts);
|
|
48
49
|
}
|
|
49
50
|
/** Every tool's user-level memory into the global store, created on demand, with no origin. */
|
|
50
51
|
export function importUserMemories(invokingRoot, opts) {
|
|
51
52
|
const ctx = context(opts.machine, {});
|
|
52
53
|
return runPass({
|
|
53
|
-
scope: 'user', target: resolveGlobalRootDir(), invoking: invokingRoot, list: (a) => a.list(ctx, 'user'), legacy: false, originProject: '', handover: false,
|
|
54
|
+
scope: 'user', target: resolveGlobalRootDir(), invoking: invokingRoot, list: (a) => a.list(ctx, 'user'), legacy: false, originProject: '', legacyOrigins: [], handover: false,
|
|
54
55
|
}, opts);
|
|
55
56
|
}
|
|
56
57
|
/** Session end in a folder with no store of its own: the session's project into the global store with its origin, the session folder's notes, then the user pass. */
|
|
@@ -58,7 +59,7 @@ export function importAtSessionEnd(cwd, transcriptPath, opts) {
|
|
|
58
59
|
const globalRoot = resolveGlobalRootDir();
|
|
59
60
|
const ctx = context(opts.machine, { projectRoot: cwd });
|
|
60
61
|
const report = runPass({
|
|
61
|
-
scope: 'project', target: globalRoot, invoking: globalRoot, list: (a) => a.list(ctx, 'project'), legacy: false,
|
|
62
|
+
scope: 'project', target: globalRoot, invoking: globalRoot, list: (a) => a.list(ctx, 'project'), legacy: false, ...projectOrigins(resolveProjectIdentity(cwd)), handover: false,
|
|
62
63
|
}, opts);
|
|
63
64
|
if (transcriptPath !== undefined)
|
|
64
65
|
mergeReports(report, importSessionFolder(globalRoot, transcriptPath, cwd, opts));
|
|
@@ -67,18 +68,21 @@ export function importAtSessionEnd(cwd, transcriptPath, opts) {
|
|
|
67
68
|
}
|
|
68
69
|
/** The session folder's notes under the project Claude filed them for, never cwd's: a session begun at home keeps its home notes user-global wherever it ends. No git call, so post-compact can run it. */
|
|
69
70
|
export function importSessionFolder(hippoRoot, transcriptPath, cwd, opts) {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
71
|
+
const project = transcriptNotesProject(transcriptPath, cwd, opts.machine);
|
|
72
|
+
if (project === null)
|
|
72
73
|
return emptyReport();
|
|
73
74
|
// A project store takes its own project's notes; another project's, or home's, go to the global store, which parts them by origin.
|
|
74
|
-
const own = isGlobalStoreRoot(hippoRoot) ||
|
|
75
|
+
const own = isGlobalStoreRoot(hippoRoot) || project.name === resolveProjectIdentity(path.dirname(hippoRoot)).name;
|
|
75
76
|
const target = own ? hippoRoot : resolveGlobalRootDir();
|
|
76
77
|
const ctx = context(opts.machine, {});
|
|
77
78
|
return runPass({
|
|
78
|
-
scope: 'project', target, invoking: hippoRoot, legacy: false,
|
|
79
|
+
scope: 'project', target, invoking: hippoRoot, legacy: false, ...projectOrigins(project), handover: false,
|
|
79
80
|
list: (a) => (a.tool === 'claude-code' ? claudeTranscriptListing(ctx, transcriptPath) : null),
|
|
80
81
|
}, opts);
|
|
81
82
|
}
|
|
83
|
+
function projectOrigins(project) {
|
|
84
|
+
return { originProject: project.name, legacyOrigins: projectNames(project).slice(1) };
|
|
85
|
+
}
|
|
82
86
|
function context(machine, extra) {
|
|
83
87
|
return { home: machine.home, env: machine.env, platform: machine.platform, ...extra };
|
|
84
88
|
}
|
|
@@ -183,11 +187,12 @@ function syncStore(pass, listings, store, opts, report) {
|
|
|
183
187
|
tenantId,
|
|
184
188
|
baseHalfLifeDays: loadConfig(store.root).defaultHalfLifeDays,
|
|
185
189
|
originProject: pass.originProject,
|
|
186
|
-
isDuplicate: duplicateCheck(store, tenantId, pass.originProject, legacy?.adopted ?? new Set()),
|
|
190
|
+
isDuplicate: duplicateCheck(store, tenantId, [pass.originProject ?? '', ...pass.legacyOrigins], legacy?.adopted ?? new Set()),
|
|
187
191
|
dryRun: opts.dryRun === true,
|
|
188
192
|
};
|
|
189
193
|
// A project's rows in the global store are parted by origin: a worktree and its main checkout share a Claude folder.
|
|
190
194
|
const partition = store.global && pass.scope === 'project' ? pass.originProject ?? '' : null;
|
|
195
|
+
const earlier = partition === null || partition === '' ? [] : earlierNames(store.db, tenantId, partition, pass.legacyOrigins);
|
|
191
196
|
const synced = [];
|
|
192
197
|
let remembered = 0;
|
|
193
198
|
for (const listing of listings) {
|
|
@@ -198,7 +203,7 @@ function syncStore(pass, listings, store, opts, report) {
|
|
|
198
203
|
out.tally.unreadable++;
|
|
199
204
|
continue;
|
|
200
205
|
}
|
|
201
|
-
const work = containerWork(tool, container, opts.machine.platform, legacy, partition ?? '');
|
|
206
|
+
const work = containerWork(tool, container, opts.machine.platform, legacy, partition ?? '', earlier);
|
|
202
207
|
const outcome = syncOne(session, work, out, report);
|
|
203
208
|
if (outcome === null)
|
|
204
209
|
continue;
|
|
@@ -208,21 +213,28 @@ function syncStore(pass, listings, store, opts, report) {
|
|
|
208
213
|
afterCommit(store.root, outcome, report);
|
|
209
214
|
}
|
|
210
215
|
if (session.dryRun)
|
|
211
|
-
out.unlisted += unlistedRows(store.db, tenantId, tool, pass.scope, listing, opts.machine.platform, partition);
|
|
216
|
+
out.unlisted += unlistedRows(store.db, tenantId, tool, pass.scope, listing, opts.machine.platform, partition === null ? null : [partition, ...earlier]);
|
|
212
217
|
}
|
|
213
218
|
if (remembered > 0 && !session.dryRun)
|
|
214
219
|
bumpRemembered(store.root, remembered, report);
|
|
215
220
|
return synced;
|
|
216
221
|
}
|
|
217
|
-
|
|
222
|
+
/** The legacy names plus every name a merge or repair folded into the project; the audit read runs once per pass. */
|
|
223
|
+
function earlierNames(db, tenantId, id, legacyOrigins) {
|
|
224
|
+
const names = [id, ...legacyOrigins];
|
|
225
|
+
return [...legacyOrigins, ...namesFoldedInto(db, tenantId, names)].filter((n) => n !== '' && n !== id);
|
|
226
|
+
}
|
|
227
|
+
function containerWork(tool, container, platform, legacy, origin, earlier) {
|
|
218
228
|
const none = new Map();
|
|
219
229
|
const own = tool.id === 'claude-code' ? legacy?.byContainer.get(container.path) : undefined;
|
|
230
|
+
const prefixOf = (name) => containerPrefix(tool.id, containerId(container.path, container.scope, platform, name));
|
|
220
231
|
return {
|
|
221
232
|
tool,
|
|
222
233
|
container,
|
|
223
|
-
prefix:
|
|
234
|
+
prefix: prefixOf(origin),
|
|
224
235
|
adopt: own?.adopt ?? none,
|
|
225
236
|
replace: own?.replace ?? none,
|
|
237
|
+
legacyPrefixes: earlier.map(prefixOf),
|
|
226
238
|
};
|
|
227
239
|
}
|
|
228
240
|
/** Null when the container was skipped: a busy store waits for the next sync, any other failure is a warning and the sync goes on. */
|
|
@@ -265,27 +277,27 @@ function bumpRemembered(root, remembered, report) {
|
|
|
265
277
|
}
|
|
266
278
|
}
|
|
267
279
|
/** Design 6: only text stored by another path counts, and in the global store only rows visible where the new row goes. */
|
|
268
|
-
function duplicateCheck(store, tenantId,
|
|
280
|
+
function duplicateCheck(store, tenantId, origins, adopted) {
|
|
269
281
|
let keys = null;
|
|
270
282
|
return (text) => {
|
|
271
|
-
keys ??= otherPathKeys(store, tenantId,
|
|
283
|
+
keys ??= otherPathKeys(store, tenantId, origins, adopted);
|
|
272
284
|
return keys.has(duplicateKey(text));
|
|
273
285
|
};
|
|
274
286
|
}
|
|
275
|
-
function otherPathKeys(store, tenantId,
|
|
276
|
-
const visible = store.global ? ` AND (origin_project = '' OR
|
|
277
|
-
const params = store.global ? [tenantId, AGENT_MEMORY_SOURCE_PREFIX,
|
|
287
|
+
function otherPathKeys(store, tenantId, origins, adopted) {
|
|
288
|
+
const visible = store.global ? ` AND (origin_project = '' OR ${originInSql(origins)})` : '';
|
|
289
|
+
const params = store.global ? [tenantId, AGENT_MEMORY_SOURCE_PREFIX, ...origins] : [tenantId, AGENT_MEMORY_SOURCE_PREFIX];
|
|
278
290
|
// SAFETY: the SELECT names the three columns of the row type.
|
|
279
291
|
const rows = store.db.prepare(`SELECT id, content, source FROM memories
|
|
280
292
|
WHERE tenant_id = ? AND superseded_by IS NULL AND substr(source, 1, ${AGENT_MEMORY_SOURCE_PREFIX.length}) != ?${visible}`).all(...params);
|
|
281
293
|
return new Set(rows.filter((r) => !adopted.has(r.id)).flatMap(heldTextKeys));
|
|
282
294
|
}
|
|
283
|
-
/** Dry run only: kept rows of this scope in containers this run did not list (a moved project's old folder); `partition` limits it to one
|
|
295
|
+
/** Dry run only: kept rows of this scope in containers this run did not list (a moved project's old folder); `partition` limits it to one project's names. */
|
|
284
296
|
function unlistedRows(db, tenantId, tool, scope, listing, platform, partition) {
|
|
285
|
-
const listed = listing.containers.
|
|
297
|
+
const listed = listing.containers.flatMap((c) => (partition ?? ['']).map((name) => containerPrefix(tool.id, containerId(c.path, c.scope, platform, name))));
|
|
286
298
|
return selectLiveEntriesBySourcePrefix(db, tenantId, `${toolSourcePrefix(tool.id)}${scope === 'project' ? 'p' : 'u'}-`)
|
|
287
299
|
.filter((row) => row.tags.includes(tool.tag) && !listed.some((p) => row.source.startsWith(p)))
|
|
288
|
-
.filter((row) => partition === null || (row.origin_project ?? '')
|
|
300
|
+
.filter((row) => partition === null || partition.includes(row.origin_project ?? '')).length;
|
|
289
301
|
}
|
|
290
302
|
/** Design 2's handover: rows the store-less hook path left in the global store, under this project's origin, for containers its store now syncs. */
|
|
291
303
|
function handOver(synced, projectRoot, opts, report) {
|
|
@@ -297,7 +309,9 @@ function handOver(synced, projectRoot, opts, report) {
|
|
|
297
309
|
db = openHippoDb(globalRoot, { busyWaitMs: opts.busyWaitMs });
|
|
298
310
|
const tenantId = resolveTenantId({});
|
|
299
311
|
// A folder with no git and no marker wrote as '' before its store existed, and as its own name after.
|
|
300
|
-
|
|
312
|
+
// Names folded into this project's in the global store were its rows too.
|
|
313
|
+
const names = projectNames(resolveProjectIdentity(projectRoot));
|
|
314
|
+
const origins = [...new Set([...names, ...namesFoldedInto(db, tenantId, names), ''])];
|
|
301
315
|
for (const work of synced)
|
|
302
316
|
handOverContainer(db, globalRoot, tenantId, work, origins, opts.machine.platform, report);
|
|
303
317
|
}
|
package/dist/ambient-store.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ 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
|
-
/** Rows of
|
|
6
|
-
project?: string;
|
|
7
|
-
/** The reader's project: a secret-tagged row counts only inside its own non-empty origin project. */
|
|
8
|
-
currentProject: string;
|
|
5
|
+
/** Rows carrying one of these project names, and user-global rows, pass; absent admits every origin. */
|
|
6
|
+
project?: readonly string[];
|
|
7
|
+
/** The reader's project names: a secret-tagged row counts only inside its own non-empty origin project. */
|
|
8
|
+
currentProject: readonly string[];
|
|
9
9
|
now: Date;
|
|
10
10
|
}
|
|
11
11
|
/** Whole-store ambient tallies in one aggregate pass, no row loaded. Content-pattern secrets and session-local
|
package/dist/ambient-store.js
CHANGED
|
@@ -3,6 +3,7 @@ import { strengthSql } from './memory.js';
|
|
|
3
3
|
import { RECALL_DEFAULT_DENY_SCOPES } from './recall-scope.js';
|
|
4
4
|
import { SECRET_TAGS } from './secret-detect.js';
|
|
5
5
|
import { openStore } from './store/open.js';
|
|
6
|
+
import { originInSql } from './project-identity.js';
|
|
6
7
|
import { isErrorTagged } from './ambient.js';
|
|
7
8
|
// A malformed or non-array JSON list reads as empty, as parseJsonArray does, instead of failing json_each.
|
|
8
9
|
const jsonList = (column) => `(CASE WHEN json_valid(${column}) AND json_type(${column}) = 'array' THEN ${column} ELSE '[]' END)`;
|
|
@@ -19,8 +20,8 @@ function contextRowsWhere(tenantId, filter) {
|
|
|
19
20
|
params.push(...RECALL_DEFAULT_DENY_SCOPES);
|
|
20
21
|
}
|
|
21
22
|
if (filter.project !== undefined) {
|
|
22
|
-
where.push(`(origin_project = '' OR
|
|
23
|
-
params.push(filter.project);
|
|
23
|
+
where.push(`(origin_project = '' OR ${originInSql(filter.project)})`);
|
|
24
|
+
params.push(...filter.project);
|
|
24
25
|
}
|
|
25
26
|
return { where, params };
|
|
26
27
|
}
|
|
@@ -57,7 +58,7 @@ export function loadAmbientTallies(hippoRoot, tenantId, filter) {
|
|
|
57
58
|
'[' || COALESCE(group_concat(${jsonList('tags_json')}, ','), '') || ']' AS tagLists
|
|
58
59
|
FROM memories
|
|
59
60
|
WHERE ${where.join(' AND ')}
|
|
60
|
-
AND ((
|
|
61
|
+
AND ((${originInSql(filter.currentProject)} AND origin_project != '') OR NOT ${secret.sql})`).get(sevenDaysAgo, ...params, ...filter.currentProject, ...secret.params);
|
|
61
62
|
const tagCounts = new Map();
|
|
62
63
|
let errors = 0;
|
|
63
64
|
const lists = /* SAFETY: jsonList yields an array per row, joined into one array */ JSON.parse(row.tagLists);
|
package/dist/api/assemble.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context } from './types.js';
|
|
2
|
+
export declare const DEFAULT_ASSEMBLE_BUDGET = 4000;
|
|
2
3
|
export interface AssembleOpts {
|
|
3
|
-
/** Token budget. Default
|
|
4
|
+
/** Token budget. Default DEFAULT_ASSEMBLE_BUDGET. */
|
|
4
5
|
budget?: number;
|
|
5
6
|
/** Recent raw rows always kept verbatim. Default 10. */
|
|
6
7
|
freshTailCount?: number;
|
|
@@ -8,7 +9,7 @@ export interface AssembleOpts {
|
|
|
8
9
|
* ancestor. Default true. */
|
|
9
10
|
summarizeOlder?: boolean;
|
|
10
11
|
/**
|
|
11
|
-
* Restrict to a specific scope
|
|
12
|
+
* Restrict to a specific scope, same rule as
|
|
12
13
|
* `recall`: when set, exact match required (so an authorised caller can
|
|
13
14
|
* assemble a `slack:private:CSEC` session by passing scope explicitly).
|
|
14
15
|
* When undefined, default-deny applies to ANY `<source>:private:*` and
|
|
@@ -49,19 +50,15 @@ export interface AssembleResult {
|
|
|
49
50
|
items: AssembledContextItem[];
|
|
50
51
|
tokens: number;
|
|
51
52
|
/**
|
|
52
|
-
* Tenant + scope-filtered raw row count for the session
|
|
53
|
-
*
|
|
54
|
-
* for all-private sessions); pre-v1.6.3 was capped (under-reported on
|
|
55
|
-
* sessions > rowCap). v1.6.3 reports the FULL post-filter count via a
|
|
56
|
-
* separate COUNT(*) query so consumers can render "session has N msgs"
|
|
57
|
-
* accurately even when items[] is the windowed view.
|
|
53
|
+
* Tenant + scope-filtered raw row count for the session, uncapped by `rowCap`, so
|
|
54
|
+
* "session has N msgs" stays accurate when items[] is the windowed view.
|
|
58
55
|
*/
|
|
59
56
|
totalRaw: number;
|
|
60
57
|
summarized: number;
|
|
61
58
|
evicted: number;
|
|
62
59
|
/**
|
|
63
|
-
* True when `rowCap` truncated the loaded window.
|
|
64
|
-
*
|
|
60
|
+
* True when `rowCap` truncated the loaded window. The cap keeps the NEWEST
|
|
61
|
+
* rows, so the items[] array represents the freshest tail of the session;
|
|
65
62
|
* older rows beyond the cap are silently absent. Use `totalRaw - items.length
|
|
66
63
|
* - summarized + ...` to estimate how much you didn't see, or widen `rowCap`.
|
|
67
64
|
*/
|
package/dist/api/assemble.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { loadEntriesByIds, loadSessionRawMemories, countSessionRawMemories } from '../store/entry-reads.js';
|
|
3
3
|
import { estimateTokens } from '../token-ledger.js';
|
|
4
4
|
import { passesScopeFilterForRecall, assertScopeRequestAllowed } from '../recall-scope.js';
|
|
5
|
+
export const DEFAULT_ASSEMBLE_BUDGET = 4000;
|
|
5
6
|
/**
|
|
6
7
|
* Build a chronologically-ordered context window for a session. Adapts the
|
|
7
8
|
* lossless-claw context-engine pattern to Hippo's score-ranked memory store.
|
|
@@ -26,7 +27,7 @@ import { passesScopeFilterForRecall, assertScopeRequestAllowed } from '../recall
|
|
|
26
27
|
*/
|
|
27
28
|
export function assemble(ctx, sessionId, opts = {}) {
|
|
28
29
|
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
29
|
-
const budget = opts.budget ??
|
|
30
|
+
const budget = opts.budget ?? DEFAULT_ASSEMBLE_BUDGET;
|
|
30
31
|
const freshTailCount = opts.freshTailCount ?? 10;
|
|
31
32
|
const summarizeOlder = opts.summarizeOlder ?? true;
|
|
32
33
|
const rowCap = opts.rowCap ?? 5000;
|
|
@@ -35,17 +36,12 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
35
36
|
}
|
|
36
37
|
const rows = loadSessionRawMemories(ctx.hippoRoot, sessionId, ctx.tenantId, rowCap);
|
|
37
38
|
const truncated = rows.length === rowCap;
|
|
38
|
-
//
|
|
39
|
-
// when the cap windows the loaded set. Pre-v1.6.3 used `scoped.length`
|
|
40
|
-
// which under-reported on long sessions and made consumers render
|
|
41
|
-
// wrong "session has N msgs" UX.
|
|
39
|
+
// `scoped.length` under-counts a capped session, so totalRaw falls back to a COUNT below.
|
|
42
40
|
const scoped = rows.filter((r) => passesScopeFilterForRecall(r.scope ?? null, opts.scope));
|
|
43
41
|
let totalRaw;
|
|
44
42
|
if (truncated) {
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// applies in TS, so a no-scope caller cannot infer private rows by
|
|
48
|
-
// comparing totalRaw to items.length on a truncated session.
|
|
43
|
+
// The COUNT applies the same default-deny scope rule in SQL, so a no-scope
|
|
44
|
+
// caller cannot infer private rows by comparing totalRaw to items.length.
|
|
49
45
|
totalRaw = countSessionRawMemories(ctx.hippoRoot, sessionId, ctx.tenantId, opts.scope);
|
|
50
46
|
}
|
|
51
47
|
else {
|
|
@@ -69,9 +65,8 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
69
65
|
isFreshTail: true,
|
|
70
66
|
strength: r.strength,
|
|
71
67
|
}));
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// documented in src/memory.ts above MemoryEntry.
|
|
68
|
+
// Byte compare is chronological for the fixed-form UTC ISO timestamps
|
|
69
|
+
// (invariant documented in src/memory.ts above MemoryEntry).
|
|
75
70
|
const cmpIso = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
76
71
|
olderItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
77
72
|
tailItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
package/dist/api/audit.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export interface AuditListOpts {
|
|
|
10
10
|
after?: KeysetPosition;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Read audit events scoped to `ctx.tenantId`. Read-only
|
|
14
|
-
*
|
|
13
|
+
* Read audit events scoped to `ctx.tenantId`. Read-only, no audit emit (matches
|
|
14
|
+
* cmdAuditList, which does not record a 'recall'-style read event).
|
|
15
15
|
*/
|
|
16
16
|
export declare function auditList(ctx: Context, opts: AuditListOpts): AuditEvent[];
|
|
17
17
|
//# sourceMappingURL=audit.d.ts.map
|
package/dist/api/audit.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
3
3
|
import { queryAuditEvents } from '../audit.js';
|
|
4
4
|
/**
|
|
5
|
-
* Read audit events scoped to `ctx.tenantId`. Read-only
|
|
6
|
-
*
|
|
5
|
+
* Read audit events scoped to `ctx.tenantId`. Read-only, no audit emit (matches
|
|
6
|
+
* cmdAuditList, which does not record a 'recall'-style read event).
|
|
7
7
|
*/
|
|
8
8
|
export function auditList(ctx, opts) {
|
|
9
9
|
const db = openHippoDb(ctx.hippoRoot);
|
package/dist/api/auth.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import type { Context } from './types.js';
|
|
|
4
4
|
export interface AuthCreateOpts {
|
|
5
5
|
label?: string;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* back-compat with
|
|
7
|
+
* Authorization role for the new key. Defaults to `'admin'` for
|
|
8
|
+
* back-compat with keys minted before roles existed (the api_keys.role column DEFAULT also
|
|
9
9
|
* resolves to 'admin' if omitted from the INSERT). Member keys are
|
|
10
10
|
* 403-blocked from admin-gated routes (e.g. `POST /v1/sleep`).
|
|
11
11
|
*/
|
|
@@ -15,7 +15,7 @@ export interface AuthCreateResult {
|
|
|
15
15
|
keyId: string;
|
|
16
16
|
plaintext: string;
|
|
17
17
|
tenantId: string;
|
|
18
|
-
/**
|
|
18
|
+
/** The role bound to the new key (admin | member). */
|
|
19
19
|
role: 'admin' | 'member';
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -36,7 +36,7 @@ export declare function authCreate(ctx: Context, opts: AuthCreateOpts): AuthCrea
|
|
|
36
36
|
* Divergence from `cmdAuthList` in src/cli.ts: the CLI today returns ALL keys
|
|
37
37
|
* regardless of tenant (single-tenant deployments). The API surface is tenant-
|
|
38
38
|
* scoped because future multi-tenant deployments will share a hippoRoot, and
|
|
39
|
-
* tenant A must not see tenant B's keys. Read-only
|
|
39
|
+
* tenant A must not see tenant B's keys. Read-only, so no audit emit.
|
|
40
40
|
*/
|
|
41
41
|
export declare function authList(ctx: Context, opts: {
|
|
42
42
|
active: boolean;
|
|
@@ -55,7 +55,7 @@ export declare function authListRows(ctx: Context, opts: {
|
|
|
55
55
|
* revoke only its own key (checked first), so no caller can probe other key_ids.
|
|
56
56
|
*
|
|
57
57
|
* Audit: emits 'auth_revoke' with `tenantId` set to the KEY ROW's tenant_id
|
|
58
|
-
* (
|
|
58
|
+
* (mirrors src/cli.ts:cmdAuthRevoke). Skipped on no-op
|
|
59
59
|
* revoke (already revoked) so re-running doesn't pad the audit log.
|
|
60
60
|
*/
|
|
61
61
|
export interface AuthRevokeResult {
|
|
@@ -72,7 +72,7 @@ export declare function authKeyTenant(ctx: Context, keyId: string): string | und
|
|
|
72
72
|
export interface AuthGrantResult {
|
|
73
73
|
ok: true;
|
|
74
74
|
}
|
|
75
|
-
/** Grant `keyId` read access to one restricted `scope
|
|
75
|
+
/** Grant `keyId` read access to one restricted `scope`. Admin only. */
|
|
76
76
|
export declare function authGrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
|
77
77
|
/** Revoke `keyId`'s grant on `scope`. Same authorization and lookup rules as authGrant. */
|
|
78
78
|
export declare function authUngrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
package/dist/api/auth.js
CHANGED
|
@@ -27,10 +27,8 @@ export function authCreate(ctx, opts) {
|
|
|
27
27
|
try {
|
|
28
28
|
const role = opts.role ?? (ctx.actor.viaAuthResolver ? 'member' : 'admin');
|
|
29
29
|
const result = createApiKey(db, { tenantId: ctx.tenantId, label: opts.label, role });
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// failure can't crash a successful mint. The plaintext is NEVER logged;
|
|
33
|
-
// metadata carries label + role + the keyId (which is non-secret).
|
|
30
|
+
// Same try/catch as authRevoke so audit failure can't crash a successful mint.
|
|
31
|
+
// The plaintext is NEVER logged; metadata carries label + role, keyId is non-secret.
|
|
34
32
|
try {
|
|
35
33
|
appendAuditEvent(db, {
|
|
36
34
|
tenantId: ctx.tenantId,
|
|
@@ -59,7 +57,7 @@ export function authCreate(ctx, opts) {
|
|
|
59
57
|
* Divergence from `cmdAuthList` in src/cli.ts: the CLI today returns ALL keys
|
|
60
58
|
* regardless of tenant (single-tenant deployments). The API surface is tenant-
|
|
61
59
|
* scoped because future multi-tenant deployments will share a hippoRoot, and
|
|
62
|
-
* tenant A must not see tenant B's keys. Read-only
|
|
60
|
+
* tenant A must not see tenant B's keys. Read-only, so no audit emit.
|
|
63
61
|
*/
|
|
64
62
|
export function authList(ctx, opts) {
|
|
65
63
|
return authListRows(ctx, opts).map((r) => r.key);
|
|
@@ -101,7 +99,7 @@ export function authRevoke(ctx, keyId) {
|
|
|
101
99
|
if (!alreadyRevoked) {
|
|
102
100
|
try {
|
|
103
101
|
appendAuditEvent(db, {
|
|
104
|
-
tenantId: row.tenantId, //
|
|
102
|
+
tenantId: row.tenantId, // KEY's tenant, not ctx.tenantId.
|
|
105
103
|
actor: ctx.actor.subject,
|
|
106
104
|
op: 'auth_revoke',
|
|
107
105
|
targetId: keyId,
|
|
@@ -134,7 +132,7 @@ export function authKeyTenant(ctx, keyId) {
|
|
|
134
132
|
closeHippoDb(db);
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
|
-
/** Grant `keyId` read access to one restricted `scope
|
|
135
|
+
/** Grant `keyId` read access to one restricted `scope`. Admin only. */
|
|
138
136
|
export function authGrant(ctx, keyId, scope) {
|
|
139
137
|
return changeScopeGrant(ctx, keyId, scope, 'auth_grant');
|
|
140
138
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AmbientLoadResult } from '../store/candidates.js';
|
|
2
2
|
import { type MemoryEntry } from '../memory.js';
|
|
3
3
|
import { type HippoConfig } from '../config.js';
|
|
4
|
+
import type { ProjectRef } from '../project-identity.js';
|
|
4
5
|
import type { DeliveryObserver } from '../delivery-recorder.js';
|
|
5
6
|
import type { ContextCost, ContextOpts, ContextResultEntry } from './context-types.js';
|
|
6
7
|
import type { Context } from './types.js';
|
|
@@ -18,9 +19,9 @@ export interface ContextPlan {
|
|
|
18
19
|
primaryIsGlobal: boolean;
|
|
19
20
|
hasLocalTaskState: boolean;
|
|
20
21
|
config: HippoConfig;
|
|
21
|
-
|
|
22
|
+
currentProject: ProjectRef;
|
|
22
23
|
includeCrossProject: boolean;
|
|
23
|
-
originProject: string | undefined;
|
|
24
|
+
originProject: readonly string[] | undefined;
|
|
24
25
|
promptRecallPending: boolean;
|
|
25
26
|
cost: ContextCost | undefined;
|
|
26
27
|
price: (entry: MemoryEntry, isGlobal: boolean, promptRecall?: boolean) => number;
|
|
@@ -41,7 +42,7 @@ export interface ContextPools {
|
|
|
41
42
|
export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
|
|
42
43
|
export declare const finiteOr: (v: number, dflt: number, min: number) => number;
|
|
43
44
|
/** Pins plus the prompt-recall or recent-N backfill; null means the block is empty. */
|
|
44
|
-
export declare function selectPinned(
|
|
45
|
+
export declare function selectPinned(opts: ContextOpts, plan: ContextPlan, left: number, pools: ContextPools, admission: ContextAdmission): ContextResultEntry[] | null;
|
|
45
46
|
/** No query: the strongest memories by strength, up to budget. */
|
|
46
47
|
export declare function selectStrongest(plan: ContextPlan, left: number, pools: ContextPools): ContextResultEntry[];
|
|
47
48
|
/** Real query: hybrid search over both stores, or physics/hybrid over the local rows; emits the 'recall' audit row. */
|