hippo-memory 1.61.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 +37 -53
- package/dist/agent-memories/apply.d.ts +3 -1
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +4 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +46 -24
- package/dist/agent-memories/types.d.ts +0 -2
- 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 +62 -66
- 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 +50 -0
- package/dist/api/context-select.js +342 -0
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +5 -4
- package/dist/api/context.js +208 -535
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +56 -41
- 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/promote.js +55 -66
- 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 +310 -456
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +206 -220
- 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 +2 -2
- package/dist/capture/compact.js +11 -15
- 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/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +43 -58
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +334 -345
- package/dist/cli/explain.js +68 -61
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +391 -379
- package/dist/cli/projects.js +11 -6
- package/dist/cli/recall.js +30 -44
- package/dist/cli/remember.js +118 -87
- package/dist/cli/session-hooks.js +106 -115
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +267 -250
- 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 +80 -76
- package/dist/cli/transfer.js +88 -107
- 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 -2
- 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 +82 -76
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +189 -203
- 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 +97 -80
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +129 -110
- 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 +103 -91
- 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 +46 -39
- 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 +96 -81
- 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 +88 -72
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +141 -118
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +28 -5
- package/dist/project-merge.js +213 -46
- 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 +26 -17
- package/dist/shared.js +102 -104
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +88 -72
- 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 +34 -56
- 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
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
import type { MemoryEntry } from '../memory.js';
|
|
2
2
|
import { openHippoDb } from '../db.js';
|
|
3
3
|
/**
|
|
4
|
-
* `bypassRejectionGuard
|
|
5
|
-
*
|
|
6
|
-
* (mergeContents, consolidate.ts:736-751), not LLM paraphrase — the bypass
|
|
7
|
-
* is safe because the producer (consolidate.ts's merge pass) now checks the
|
|
8
|
-
* merged content's rejection digest against the tenant's tombstones BEFORE
|
|
9
|
-
* ever assembling a batch to write, and skips the merge entirely on a hit.
|
|
10
|
-
* Every other caller (writeEntryDbOnly, bootstrapLegacyStore, rebuildIndex)
|
|
11
|
-
* leaves this false and the guard runs live.
|
|
12
|
-
*
|
|
13
|
-
* AT1 P1 fix (codex, batch-transaction rejection race): the producer check
|
|
14
|
-
* above runs on a DIFFERENT connection BEFORE this transaction opens — a
|
|
15
|
-
* `hippo reject X` that commits in that window is invisible to it. This
|
|
16
|
-
* parameter's contract is UNCHANGED (still the sole bypass, still trusted
|
|
17
|
-
* by the producer-side check for the common case); what changed is that
|
|
18
|
-
* `batchWriteAndDelete` no longer trusts it BLINDLY. It now runs its own
|
|
19
|
-
* in-transaction point-probe (same connection, same digest lookup this
|
|
20
|
-
* function's guard would have done) immediately before each upsert and
|
|
21
|
-
* skips — rather than writes — any entry whose content matches a tombstone
|
|
22
|
-
* that landed after the producer's check. See batchWriteAndDelete for the
|
|
23
|
-
* skip logic.
|
|
4
|
+
* `bypassRejectionGuard`: ONLY `batchWriteAndDelete` passes `true`; its merges concatenate
|
|
5
|
+
* already-guarded facts, and it re-probes tombstones in-transaction before each upsert.
|
|
24
6
|
*/
|
|
25
7
|
export declare function upsertEntryRow(db: ReturnType<typeof openHippoDb>, entry: MemoryEntry, bypassRejectionGuard?: boolean): void;
|
|
26
8
|
export declare function syncFtsRow(db: ReturnType<typeof openHippoDb>, entry: MemoryEntry, isNewRow?: boolean): void;
|
|
@@ -29,14 +11,14 @@ export declare function deleteFtsRow(db: ReturnType<typeof openHippoDb>, id: str
|
|
|
29
11
|
* Write a memory entry to SQLite and refresh compatibility mirrors.
|
|
30
12
|
*
|
|
31
13
|
* `opts.actor` defaults to 'cli' so unauthenticated direct-CLI callers still
|
|
32
|
-
* get the right audit attribution. The HTTP server
|
|
14
|
+
* get the right audit attribution. The HTTP server and api.* layer pass
|
|
33
15
|
* the resolved actor (`api_key:<key_id>` / `localhost:cli`) so audit events
|
|
34
16
|
* land with one row per write, no double-emit.
|
|
35
17
|
*
|
|
36
18
|
* `opts.afterWrite` is invoked inside the same SAVEPOINT as the memories
|
|
37
19
|
* INSERT (mirrors archiveRawMemory's shape in raw-archive.ts). On callback
|
|
38
20
|
* throw, the SAVEPOINT rolls back — the memory row never lands, and the
|
|
39
|
-
* filesystem mirrors / audit emit never run. Used by
|
|
21
|
+
* filesystem mirrors / audit emit never run. Used by connectors to
|
|
40
22
|
* stamp idempotency rows atomically with the memory write.
|
|
41
23
|
*/
|
|
42
24
|
/**
|
|
@@ -51,7 +33,7 @@ export declare function deleteFtsRow(db: ReturnType<typeof openHippoDb>, id: str
|
|
|
51
33
|
* v39 migration found no evidence for" and is deny-by-default in ambient
|
|
52
34
|
* context. A writeback (e.g. markRetrieved on a crossProject-included row)
|
|
53
35
|
* must not launder it into an injectable origin - the migration is the only
|
|
54
|
-
* evidence-based NULL converter
|
|
36
|
+
* evidence-based NULL converter.
|
|
55
37
|
*/
|
|
56
38
|
export declare function stampOriginProject(hippoRoot: string, entry: MemoryEntry): MemoryEntry;
|
|
57
39
|
/**
|
|
@@ -61,7 +43,7 @@ export declare function stampOriginProject(hippoRoot: string, entry: MemoryEntry
|
|
|
61
43
|
* backfill. Same evidence order as the migration: the provenance source
|
|
62
44
|
* (`shared:<project>:` / `promoted:<localRoot>`) wins over the destination
|
|
63
45
|
* store's location, so a shared row imported into the global store keeps its
|
|
64
|
-
* owning project instead of becoming user-global
|
|
46
|
+
* owning project instead of becoming user-global.
|
|
65
47
|
*/
|
|
66
48
|
export declare function stampOriginProjectForImport(hippoRoot: string, entry: MemoryEntry): MemoryEntry;
|
|
67
49
|
//# sourceMappingURL=entry-row.d.ts.map
|
package/dist/store/entry-row.js
CHANGED
|
@@ -4,26 +4,8 @@ import { deriveOriginProject, originFromSource } from '../project-identity.js';
|
|
|
4
4
|
import { checkRejectionGuard } from '../rejection.js';
|
|
5
5
|
import { log } from '../log.js';
|
|
6
6
|
/**
|
|
7
|
-
* `bypassRejectionGuard
|
|
8
|
-
*
|
|
9
|
-
* (mergeContents, consolidate.ts:736-751), not LLM paraphrase — the bypass
|
|
10
|
-
* is safe because the producer (consolidate.ts's merge pass) now checks the
|
|
11
|
-
* merged content's rejection digest against the tenant's tombstones BEFORE
|
|
12
|
-
* ever assembling a batch to write, and skips the merge entirely on a hit.
|
|
13
|
-
* Every other caller (writeEntryDbOnly, bootstrapLegacyStore, rebuildIndex)
|
|
14
|
-
* leaves this false and the guard runs live.
|
|
15
|
-
*
|
|
16
|
-
* AT1 P1 fix (codex, batch-transaction rejection race): the producer check
|
|
17
|
-
* above runs on a DIFFERENT connection BEFORE this transaction opens — a
|
|
18
|
-
* `hippo reject X` that commits in that window is invisible to it. This
|
|
19
|
-
* parameter's contract is UNCHANGED (still the sole bypass, still trusted
|
|
20
|
-
* by the producer-side check for the common case); what changed is that
|
|
21
|
-
* `batchWriteAndDelete` no longer trusts it BLINDLY. It now runs its own
|
|
22
|
-
* in-transaction point-probe (same connection, same digest lookup this
|
|
23
|
-
* function's guard would have done) immediately before each upsert and
|
|
24
|
-
* skips — rather than writes — any entry whose content matches a tombstone
|
|
25
|
-
* that landed after the producer's check. See batchWriteAndDelete for the
|
|
26
|
-
* skip logic.
|
|
7
|
+
* `bypassRejectionGuard`: ONLY `batchWriteAndDelete` passes `true`; its merges concatenate
|
|
8
|
+
* already-guarded facts, and it re-probes tombstones in-transaction before each upsert.
|
|
27
9
|
*/
|
|
28
10
|
export function upsertEntryRow(db, entry, bypassRejectionGuard = false) {
|
|
29
11
|
if (!bypassRejectionGuard) {
|
|
@@ -158,14 +140,14 @@ export function deleteFtsRow(db, id) {
|
|
|
158
140
|
* Write a memory entry to SQLite and refresh compatibility mirrors.
|
|
159
141
|
*
|
|
160
142
|
* `opts.actor` defaults to 'cli' so unauthenticated direct-CLI callers still
|
|
161
|
-
* get the right audit attribution. The HTTP server
|
|
143
|
+
* get the right audit attribution. The HTTP server and api.* layer pass
|
|
162
144
|
* the resolved actor (`api_key:<key_id>` / `localhost:cli`) so audit events
|
|
163
145
|
* land with one row per write, no double-emit.
|
|
164
146
|
*
|
|
165
147
|
* `opts.afterWrite` is invoked inside the same SAVEPOINT as the memories
|
|
166
148
|
* INSERT (mirrors archiveRawMemory's shape in raw-archive.ts). On callback
|
|
167
149
|
* throw, the SAVEPOINT rolls back — the memory row never lands, and the
|
|
168
|
-
* filesystem mirrors / audit emit never run. Used by
|
|
150
|
+
* filesystem mirrors / audit emit never run. Used by connectors to
|
|
169
151
|
* stamp idempotency rows atomically with the memory write.
|
|
170
152
|
*/
|
|
171
153
|
/**
|
|
@@ -180,7 +162,7 @@ export function deleteFtsRow(db, id) {
|
|
|
180
162
|
* v39 migration found no evidence for" and is deny-by-default in ambient
|
|
181
163
|
* context. A writeback (e.g. markRetrieved on a crossProject-included row)
|
|
182
164
|
* must not launder it into an injectable origin - the migration is the only
|
|
183
|
-
* evidence-based NULL converter
|
|
165
|
+
* evidence-based NULL converter.
|
|
184
166
|
*/
|
|
185
167
|
export function stampOriginProject(hippoRoot, entry) {
|
|
186
168
|
if (entry.origin_project !== undefined)
|
|
@@ -194,7 +176,7 @@ export function stampOriginProject(hippoRoot, entry) {
|
|
|
194
176
|
* backfill. Same evidence order as the migration: the provenance source
|
|
195
177
|
* (`shared:<project>:` / `promoted:<localRoot>`) wins over the destination
|
|
196
178
|
* store's location, so a shared row imported into the global store keeps its
|
|
197
|
-
* owning project instead of becoming user-global
|
|
179
|
+
* owning project instead of becoming user-global.
|
|
198
180
|
*/
|
|
199
181
|
export function stampOriginProjectForImport(hippoRoot, entry) {
|
|
200
182
|
if (entry.origin_project !== undefined)
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { type MemoryEntry } from '../memory.js';
|
|
2
2
|
import { type DatabaseSyncLike } from '../db.js';
|
|
3
|
-
export
|
|
3
|
+
export interface WriteEntryOptions {
|
|
4
4
|
actor?: string;
|
|
5
5
|
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
6
|
-
/** Runs
|
|
7
|
-
* BEFORE the markdown mirrors are written. Lets a caller perform a post-commit
|
|
8
|
-
* side effect (e.g. mark the graph dirty) that must still happen even if a
|
|
9
|
-
* mirror write then throws. Keep it best-effort — it runs on a committed,
|
|
10
|
-
* idle connection, so opening another handle inside it is safe. */
|
|
6
|
+
/** Runs after the row commits and before the mirrors, on an idle connection; keep it best-effort. */
|
|
11
7
|
afterCommit?: () => void;
|
|
12
|
-
}
|
|
8
|
+
}
|
|
9
|
+
export declare function writeEntry(hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
|
|
10
|
+
/** writeEntry on the caller's open store, so a loop of writes opens the store once; each row still commits alone. */
|
|
11
|
+
export declare function writeEntryOn(db: DatabaseSyncLike, hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
|
|
13
12
|
/**
|
|
14
13
|
* DB-only write path. Caller owns the open `db` handle. Runs SAVEPOINT +
|
|
15
14
|
* upsert + afterWrite hook + audit row inside the SAVEPOINT scope. Caller
|
|
@@ -10,6 +10,15 @@ import { mirrorBestEffort, writeMarkdownMirror } from './mirrors.js';
|
|
|
10
10
|
import { openStore } from './open.js';
|
|
11
11
|
export function writeEntry(hippoRoot, entry, opts) {
|
|
12
12
|
const db = openStore(hippoRoot);
|
|
13
|
+
try {
|
|
14
|
+
writeEntryOn(db, hippoRoot, entry, opts);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
closeHippoDb(db);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** writeEntry on the caller's open store, so a loop of writes opens the store once; each row still commits alone. */
|
|
21
|
+
export function writeEntryOn(db, hippoRoot, entry, opts) {
|
|
13
22
|
try {
|
|
14
23
|
const stamped = stampOriginProject(hippoRoot, entry);
|
|
15
24
|
writeEntryDbOnly(db, stamped, opts);
|
|
@@ -17,18 +26,13 @@ export function writeEntry(hippoRoot, entry, opts) {
|
|
|
17
26
|
writeEntryMirrors(hippoRoot, stamped);
|
|
18
27
|
}
|
|
19
28
|
catch (error) {
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// a fresh implicit transaction — then rethrow so the caller sees the
|
|
23
|
-
// refusal.
|
|
29
|
+
// writeEntryDbOnly's SAVEPOINT has already unwound here, so the refusal audit lands
|
|
30
|
+
// post-rollback in a fresh implicit transaction; then rethrow so the caller sees it.
|
|
24
31
|
if (error instanceof RejectedValueError) {
|
|
25
32
|
auditRejectionRefusal(db, error, opts?.actor ?? 'cli');
|
|
26
33
|
}
|
|
27
34
|
throw error;
|
|
28
35
|
}
|
|
29
|
-
finally {
|
|
30
|
-
closeHippoDb(db);
|
|
31
|
-
}
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* DB-only write path. Caller owns the open `db` handle. Runs SAVEPOINT +
|
|
@@ -58,10 +62,8 @@ export function writeEntryDbOnly(db, entry, opts) {
|
|
|
58
62
|
kind: entry.kind ?? 'distilled',
|
|
59
63
|
scope: entry.scope ?? null,
|
|
60
64
|
}, opts?.actor ?? 'cli', entry.tenantId);
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// null dag_parent_id (vast majority of writes); cost is one null check
|
|
64
|
-
// on the hot path.
|
|
65
|
+
// A child write marks its summary parent dirty for the sleep-cycle rebuild; most writes
|
|
66
|
+
// have no parent, so the hot path pays one null check.
|
|
65
67
|
if (entry.dag_parent_id) {
|
|
66
68
|
markSummaryDirtyInTx(db, entry.dag_parent_id, entry.tenantId, opts?.actor ?? 'cli');
|
|
67
69
|
}
|
package/dist/store/handoffs.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare function loadLatestHandoff(hippoRoot: string, tenantId: string, s
|
|
|
19
19
|
export declare function loadHandoffById(hippoRoot: string, tenantId: string, id: number): SessionHandoff | null;
|
|
20
20
|
/** Stamp the outcome on a session's newest handoff, only if it has none yet. Returns rows changed. */
|
|
21
21
|
export declare function stampHandoffOutcome(hippoRoot: string, tenantId: string, sessionId: string, outcome: HandoffOutcome): number;
|
|
22
|
-
/** Auto-write a handoff at session-end
|
|
22
|
+
/** Auto-write a handoff at session-end from the session's active snapshot, else from `derived`, its transcript state.
|
|
23
23
|
* @param evidence best-effort git state; outcome comes from the newest session_complete event.
|
|
24
24
|
* @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
|
|
25
25
|
export declare function writeSessionEndHandoff(hippoRoot: string, tenantId: string, sessionId: string, evidence: HandoffEvidence | null, derived?: Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null): SessionHandoff | null;
|
package/dist/store/handoffs.js
CHANGED
|
@@ -4,8 +4,6 @@ import { RECALL_DEFAULT_DENY_SCOPES } from '../recall-scope.js';
|
|
|
4
4
|
import { assertTenantId } from '../tenant.js';
|
|
5
5
|
import { openStore } from './open.js';
|
|
6
6
|
import { loadActiveTaskSnapshot } from './sessions.js';
|
|
7
|
-
// W1: the nine-column SELECT was cloned four times (plan rule 8); one
|
|
8
|
-
// definition so a sixth caller can't drift from the other five.
|
|
9
7
|
/** Column list shared by every session_handoffs SELECT; store-cards.ts reuses it for the card handoff lookup. */
|
|
10
8
|
export const HANDOFF_COLUMNS = 'id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at, constraints_json, evidence_json, outcome, target_runtime, card_id';
|
|
11
9
|
/**
|
|
@@ -15,8 +13,8 @@ export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
|
|
|
15
13
|
assertTenantId('saveSessionHandoff', tenantId);
|
|
16
14
|
const db = openStore(hippoRoot);
|
|
17
15
|
const now = new Date().toISOString();
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// Scope is stored as given; read-side default-deny in api.recall + cmdRecall
|
|
17
|
+
// continuity excludes slack:private:* and 'unknown:legacy'.
|
|
20
18
|
try {
|
|
21
19
|
const result = db.prepare(`
|
|
22
20
|
INSERT INTO session_handoffs(session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, tenant_id, created_at, constraints_json, evidence_json, outcome, target_runtime, card_id)
|
|
@@ -54,7 +52,7 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
|
|
|
54
52
|
params.push(opts.excludeSessionId);
|
|
55
53
|
}
|
|
56
54
|
if (opts.unfinishedOnly) {
|
|
57
|
-
//
|
|
55
|
+
// Restrict to each session's newest revision first: stampHandoffOutcome
|
|
58
56
|
// only stamps the newest row, so an older null-outcome revision must not resurrect.
|
|
59
57
|
conditions.push(`id IN (SELECT MAX(id) FROM session_handoffs WHERE tenant_id = ? GROUP BY session_id)`);
|
|
60
58
|
params.push(tenantId);
|
|
@@ -65,7 +63,7 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
|
|
|
65
63
|
params.push(new Date(Date.now() - opts.maxAgeMs).toISOString());
|
|
66
64
|
}
|
|
67
65
|
if (opts.scopeFilter === 'default-deny') {
|
|
68
|
-
//
|
|
66
|
+
// Admit scope before LIMIT 1, else a newer denied row hides an older eligible one.
|
|
69
67
|
const placeholders = RECALL_DEFAULT_DENY_SCOPES.map(() => '?').join(', ');
|
|
70
68
|
conditions.push(`(scope IS NULL OR (scope NOT IN (${placeholders}) AND scope NOT LIKE '%:private:%'))`);
|
|
71
69
|
params.push(...RECALL_DEFAULT_DENY_SCOPES);
|
|
@@ -123,7 +121,7 @@ export function stampHandoffOutcome(hippoRoot, tenantId, sessionId, outcome) {
|
|
|
123
121
|
closeHippoDb(db);
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
|
-
/** Auto-write a handoff at session-end
|
|
124
|
+
/** Auto-write a handoff at session-end from the session's active snapshot, else from `derived`, its transcript state.
|
|
127
125
|
* @param evidence best-effort git state; outcome comes from the newest session_complete event.
|
|
128
126
|
* @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
|
|
129
127
|
export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence, derived = null) {
|
|
@@ -160,9 +158,8 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence,
|
|
|
160
158
|
finally {
|
|
161
159
|
closeHippoDb(db);
|
|
162
160
|
}
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// codex P1: a scope mismatch must not leak private metadata into an unscoped envelope.
|
|
161
|
+
// A same-task refresh carries forward envelope fields nobody cleared; a scope
|
|
162
|
+
// mismatch must not leak private metadata into an unscoped envelope.
|
|
166
163
|
const carryForward = existing != null && existing.taskId === snapshot.task
|
|
167
164
|
&& (existing.scope ?? null) === (snapshot.scope ?? null);
|
|
168
165
|
return saveSessionHandoff(hippoRoot, tenantId, {
|
|
@@ -4,12 +4,8 @@ export declare function loadIndex(hippoRoot: string): HippoIndex;
|
|
|
4
4
|
/**
|
|
5
5
|
* Persist mutable index metadata. Entry rows themselves are derived from SQLite.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* trace id into `index.last_trace_id` before calling this, relying on BOTH
|
|
10
|
-
* meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
|
|
11
|
-
* mid-write failure can never advance one key without the other. index.json
|
|
12
|
-
* is left untouched; only `rebuildIndex` writes it.
|
|
7
|
+
* `last_retrieval_ids` and `last_trace_id` commit in one transaction: callers fold a fresh trace id
|
|
8
|
+
* into the index and rely on both keys moving together. index.json is left to `rebuildIndex`.
|
|
13
9
|
*/
|
|
14
10
|
export declare function saveIndex(hippoRoot: string, index: HippoIndex): void;
|
|
15
11
|
/**
|
|
@@ -18,12 +18,8 @@ export function loadIndex(hippoRoot) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Persist mutable index metadata. Entry rows themselves are derived from SQLite.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* trace id into `index.last_trace_id` before calling this, relying on BOTH
|
|
24
|
-
* meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
|
|
25
|
-
* mid-write failure can never advance one key without the other. index.json
|
|
26
|
-
* is left untouched; only `rebuildIndex` writes it.
|
|
21
|
+
* `last_retrieval_ids` and `last_trace_id` commit in one transaction: callers fold a fresh trace id
|
|
22
|
+
* into the index and rely on both keys moving together. index.json is left to `rebuildIndex`.
|
|
27
23
|
*/
|
|
28
24
|
export function saveIndex(hippoRoot, index) {
|
|
29
25
|
const db = openStore(hippoRoot);
|
|
@@ -58,10 +54,8 @@ export function rebuildIndex(hippoRoot) {
|
|
|
58
54
|
if (legacyEntries.length > 0) {
|
|
59
55
|
db.exec('BEGIN');
|
|
60
56
|
try {
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// which a stale markdown mirror could resurrect a rejected value.
|
|
64
|
-
// Refusal audit written INLINE (nothing rolls back on a skip).
|
|
57
|
+
// Guard with per-row skip, like bootstrapLegacyStore: a stale markdown mirror could resurrect a
|
|
58
|
+
// rejected value here. Refusal audit is written inline because nothing rolls back on a skip.
|
|
65
59
|
let rejectedCount = 0;
|
|
66
60
|
for (const entry of legacyEntries) {
|
|
67
61
|
// v39: same store-derived origin stamp as bootstrapLegacyStore.
|
package/dist/store/mirrors.d.ts
CHANGED
|
@@ -9,28 +9,13 @@ export declare function writeRecentSessionMirror(hippoRoot: string, tenantId: st
|
|
|
9
9
|
export declare function writeMarkdownMirror(hippoRoot: string, entry: MemoryEntry): void;
|
|
10
10
|
export declare function removeEntryMirrors(hippoRoot: string, id: string): void;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* same way `removeEntryMirrors` walks them (one per layer: buffer/episodic/
|
|
15
|
-
* semantic), filtered to the ones that still `fs.existsSync`. Used to report
|
|
16
|
-
* an EXPLICIT path when a best-effort purge fails and no reaper exists to
|
|
17
|
-
* retry it — plain `removeEntryMirrors` returns void, giving no way to name
|
|
18
|
-
* which file is stuck.
|
|
12
|
+
* Mirror paths for `id` still on disk, walked like `removeEntryMirrors`, so a failed purge
|
|
13
|
+
* with no reaper to retry it can name the stuck file.
|
|
19
14
|
*/
|
|
20
15
|
export declare function getExistingEntryMirrorPaths(hippoRoot: string, id: string): string[];
|
|
21
16
|
/**
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
* "will retry via reaper on next open" for EVERY failure, but the reaper
|
|
25
|
-
* (`cleanupArchivedMirrors`, raw-archive-mirror-cleanup.ts) only scans
|
|
26
|
-
* `raw_archive` — that message was false for a non-raw id, which has no
|
|
27
|
-
* reaper at all.
|
|
28
|
-
*
|
|
29
|
-
* Retries the unlink once synchronously (the common real-world failure is a
|
|
30
|
-
* transient lock/AV-scanner false positive, not a permanent one). On a
|
|
31
|
-
* second failure: raw ids still get the honest reaper message (true); non-raw
|
|
32
|
-
* ids get the EXPLICIT leftover file path(s) and a manual-delete instruction,
|
|
33
|
-
* since nothing will ever retry them automatically.
|
|
17
|
+
* Best-effort mirror purge. Retries the unlink once (most failures are transient locks); the reaper only
|
|
18
|
+
* scans `raw_archive`, so a non-raw id's second failure logs the leftover path(s) to delete by hand.
|
|
34
19
|
*
|
|
35
20
|
* Returns true if the mirror ended up purged (first or second attempt).
|
|
36
21
|
*/
|
|
@@ -46,4 +31,6 @@ export declare function writeStatsMirror(hippoRoot: string, stats: LegacyStats):
|
|
|
46
31
|
/** Mirrors are derived from SQLite and written after COMMIT, so a failed write warns instead of failing a committed change. */
|
|
47
32
|
export declare function mirrorBestEffort(what: string, write: () => void): void;
|
|
48
33
|
export declare function syncMirrorFiles(hippoRoot: string, db: ReturnType<typeof openHippoDb>): void;
|
|
34
|
+
/** syncMirrorFiles for a pass that changed only `changed`: the other rows' markdown is already current. */
|
|
35
|
+
export declare function syncChangedMirrors(hippoRoot: string, db: ReturnType<typeof openHippoDb>, changed: readonly MemoryEntry[]): void;
|
|
49
36
|
//# sourceMappingURL=mirrors.d.ts.map
|
package/dist/store/mirrors.js
CHANGED
|
@@ -145,15 +145,8 @@ export function writeMarkdownMirror(hippoRoot, entry) {
|
|
|
145
145
|
fs.mkdirSync(dir, { recursive: true });
|
|
146
146
|
fs.writeFileSync(path.join(dir, `${entry.id}.md`), serializeEntry(entry), 'utf8');
|
|
147
147
|
}
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// consolidate.ts) — but this enumeration only walked
|
|
151
|
-
// Buffer/Episodic/Semantic. A rejected/forgotten trace row's markdown
|
|
152
|
-
// content survived on disk while the purge (and `hippo reject`/plain
|
|
153
|
-
// `forget`) reported success, and a stale trace mirror is exactly the
|
|
154
|
-
// resurrection channel bootstrapLegacyStore/rebuildIndex guard against.
|
|
155
|
-
// Fixes BOTH the AT1 reject-flow purge and the pre-existing plain-`forget`
|
|
156
|
-
// gap for trace rows (deleteEntry has always called this same function).
|
|
148
|
+
// Every layer, Trace included: `writeMarkdownMirror` writes any layer, and a stale trace mirror
|
|
149
|
+
// is the resurrection channel bootstrapLegacyStore/rebuildIndex guard against.
|
|
157
150
|
export function removeEntryMirrors(hippoRoot, id) {
|
|
158
151
|
for (const layer of [Layer.Buffer, Layer.Episodic, Layer.Semantic, Layer.Trace]) {
|
|
159
152
|
const file = path.join(layerDir(hippoRoot, layer), `${id}.md`);
|
|
@@ -163,36 +156,18 @@ export function removeEntryMirrors(hippoRoot, id) {
|
|
|
163
156
|
}
|
|
164
157
|
}
|
|
165
158
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* same way `removeEntryMirrors` walks them (one per layer: buffer/episodic/
|
|
169
|
-
* semantic), filtered to the ones that still `fs.existsSync`. Used to report
|
|
170
|
-
* an EXPLICIT path when a best-effort purge fails and no reaper exists to
|
|
171
|
-
* retry it — plain `removeEntryMirrors` returns void, giving no way to name
|
|
172
|
-
* which file is stuck.
|
|
159
|
+
* Mirror paths for `id` still on disk, walked like `removeEntryMirrors`, so a failed purge
|
|
160
|
+
* with no reaper to retry it can name the stuck file.
|
|
173
161
|
*/
|
|
174
162
|
export function getExistingEntryMirrorPaths(hippoRoot, id) {
|
|
175
|
-
//
|
|
176
|
-
// — kept in lockstep with it since this function's whole purpose is
|
|
177
|
-
// walking the mirror paths "the same way removeEntryMirrors walks them"
|
|
178
|
-
// (see its own doc comment).
|
|
163
|
+
// Layer list kept in lockstep with removeEntryMirrors.
|
|
179
164
|
return [Layer.Buffer, Layer.Episodic, Layer.Semantic, Layer.Trace]
|
|
180
165
|
.map((layer) => path.join(layerDir(hippoRoot, layer), `${id}.md`))
|
|
181
166
|
.filter((file) => fs.existsSync(file));
|
|
182
167
|
}
|
|
183
168
|
/**
|
|
184
|
-
*
|
|
185
|
-
* `
|
|
186
|
-
* "will retry via reaper on next open" for EVERY failure, but the reaper
|
|
187
|
-
* (`cleanupArchivedMirrors`, raw-archive-mirror-cleanup.ts) only scans
|
|
188
|
-
* `raw_archive` — that message was false for a non-raw id, which has no
|
|
189
|
-
* reaper at all.
|
|
190
|
-
*
|
|
191
|
-
* Retries the unlink once synchronously (the common real-world failure is a
|
|
192
|
-
* transient lock/AV-scanner false positive, not a permanent one). On a
|
|
193
|
-
* second failure: raw ids still get the honest reaper message (true); non-raw
|
|
194
|
-
* ids get the EXPLICIT leftover file path(s) and a manual-delete instruction,
|
|
195
|
-
* since nothing will ever retry them automatically.
|
|
169
|
+
* Best-effort mirror purge. Retries the unlink once (most failures are transient locks); the reaper only
|
|
170
|
+
* scans `raw_archive`, so a non-raw id's second failure logs the leftover path(s) to delete by hand.
|
|
196
171
|
*
|
|
197
172
|
* Returns true if the mirror ended up purged (first or second attempt).
|
|
198
173
|
*/
|
|
@@ -242,12 +217,8 @@ export function buildIndexFromDb(db) {
|
|
|
242
217
|
pinned: Boolean(row.pinned),
|
|
243
218
|
};
|
|
244
219
|
}
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// saveIndex (which commits both keys in one transaction) lands between
|
|
248
|
-
// them, handing the reader mismatched last_retrieval_ids / last_trace_id
|
|
249
|
-
// and re-opening the mislinkage hole saveIndex's BEGIN/COMMIT closed on
|
|
250
|
-
// the write side. One SELECT = one SQLite read snapshot.
|
|
220
|
+
// Read both lockstep keys in ONE statement: two autocommit SELECTs could straddle a concurrent
|
|
221
|
+
// saveIndex and hand back a mismatched last_retrieval_ids / last_trace_id pair.
|
|
251
222
|
// SAFETY: lockstepRows' shape matches the key/value columns named above.
|
|
252
223
|
const lockstepRows = db.prepare(`SELECT key, value FROM meta WHERE key IN ('last_retrieval_ids', 'last_trace_id')`).all();
|
|
253
224
|
const lockstep = new Map(lockstepRows.map((r) => [r.key, r.value]));
|
|
@@ -294,8 +265,12 @@ export function syncMirrorFiles(hippoRoot, db) {
|
|
|
294
265
|
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
295
266
|
// MemoryRow's field set.
|
|
296
267
|
const entries = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
|
|
268
|
+
syncChangedMirrors(hippoRoot, db, entries.map(rowToEntry));
|
|
269
|
+
}
|
|
270
|
+
/** syncMirrorFiles for a pass that changed only `changed`: the other rows' markdown is already current. */
|
|
271
|
+
export function syncChangedMirrors(hippoRoot, db, changed) {
|
|
297
272
|
mirrorBestEffort('markdown mirrors', () => {
|
|
298
|
-
for (const entry of
|
|
273
|
+
for (const entry of changed)
|
|
299
274
|
writeMarkdownMirror(hippoRoot, entry);
|
|
300
275
|
});
|
|
301
276
|
// SAFETY: conflicts' shape matches the eight columns named in the SELECT
|
package/dist/store/open.js
CHANGED
|
@@ -16,13 +16,8 @@ export function getHippoRoot(cwd = process.cwd(), opts) {
|
|
|
16
16
|
return findHippoStoreDir(cwd, opts) ?? path.join(realpathOrResolve(cwd), '.hippo');
|
|
17
17
|
}
|
|
18
18
|
export function isInitialized(hippoRoot) {
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// leaving a partial directory (integrations/, logs/, runs/) with no
|
|
22
|
-
// hippo.db. Returning true in that state caused `hippo init` to skip
|
|
23
|
-
// initStore and `hippo recall` to silently fall back to an empty store
|
|
24
|
-
// (incident 2026-04-26: ingest_direct.py against a bare .hippo).
|
|
25
|
-
// Treat the store as initialized only if hippo.db actually exists.
|
|
19
|
+
// autoInstallHooks / setupDailySchedule can create a bare .hippo with no hippo.db; counting
|
|
20
|
+
// that as initialized makes `hippo init` skip initStore, so only hippo.db counts.
|
|
26
21
|
return fs.existsSync(path.join(hippoRoot, 'hippo.db'));
|
|
27
22
|
}
|
|
28
23
|
export function initStore(hippoRoot) {
|
|
@@ -75,15 +70,8 @@ function bootstrapLegacyStore(db, hippoRoot) {
|
|
|
75
70
|
const memoryCount = Number(countRow?.count ?? 0);
|
|
76
71
|
if (memoryCount > 0)
|
|
77
72
|
return false;
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// rejected, memories stays at 0 rows even after a successful bootstrap
|
|
81
|
-
// pass, so the memoryCount>0 gate above never trips — every subsequent
|
|
82
|
-
// initStore() call would re-run this whole function: re-scan the legacy
|
|
83
|
-
// mirrors, re-attempt (and re-refuse, re-auditing) every row, and
|
|
84
|
-
// re-INSERT the legacy consolidation_runs rows with no dedup, duplicating
|
|
85
|
-
// them on each open. A dedicated meta flag marks bootstrap as
|
|
86
|
-
// attempted-and-settled regardless of how many rows actually landed.
|
|
73
|
+
// memoryCount misses an all-rejected bootstrap (memories stays empty), which would re-run the
|
|
74
|
+
// import on every open and duplicate consolidation_runs; this meta flag settles it.
|
|
87
75
|
if (getMeta(db, 'legacy_bootstrap_completed', '0') === '1')
|
|
88
76
|
return false;
|
|
89
77
|
const legacyEntries = loadLegacyEntriesFromMarkdown(hippoRoot);
|
|
@@ -93,8 +81,7 @@ function bootstrapLegacyStore(db, hippoRoot) {
|
|
|
93
81
|
try {
|
|
94
82
|
importLegacyEntries(db, hippoRoot, legacyEntries);
|
|
95
83
|
importLegacyIndexAndStats(db, hippoRoot);
|
|
96
|
-
//
|
|
97
|
-
// landed (all-rejected included) — see the gate comment above.
|
|
84
|
+
// Stamp completion even when every row was rejected; see the gate above.
|
|
98
85
|
setMeta(db, 'legacy_bootstrap_completed', '1');
|
|
99
86
|
db.exec('COMMIT');
|
|
100
87
|
}
|
|
@@ -108,14 +95,8 @@ function bootstrapLegacyStore(db, hippoRoot) {
|
|
|
108
95
|
return true;
|
|
109
96
|
}
|
|
110
97
|
function importLegacyEntries(db, hippoRoot, legacyEntries) {
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// which a stale/never-purged markdown mirror could resurrect a rejected
|
|
114
|
-
// value; a skip-and-count here closes that structurally, independent of
|
|
115
|
-
// mirror state. The refusal audit is written INLINE inside this
|
|
116
|
-
// still-open loop transaction (plain audit() — nothing is rolled back
|
|
117
|
-
// on a per-row skip, so the post-rollback auditRejectionRefusal helper
|
|
118
|
-
// is the wrong tool here).
|
|
98
|
+
// Guard live per row: a stale markdown mirror could resurrect a rejected value. Plain audit()
|
|
99
|
+
// inline, because nothing rolls back on a per-row skip.
|
|
119
100
|
let rejectedCount = 0;
|
|
120
101
|
for (const entry of legacyEntries) {
|
|
121
102
|
// v39: legacy markdown carries no origin_project; stamp from the store
|
|
@@ -140,11 +121,8 @@ function importLegacyEntries(db, hippoRoot, legacyEntries) {
|
|
|
140
121
|
function importLegacyIndexAndStats(db, hippoRoot) {
|
|
141
122
|
const legacyIndex = loadLegacyIndexFile(hippoRoot);
|
|
142
123
|
setMeta(db, 'last_retrieval_ids', JSON.stringify(legacyIndex.last_retrieval_ids ?? []));
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// Coerce like its neighbors below coerce theirs (independent-review-critic
|
|
146
|
-
// LOW finding): accept only a clean digit string, else fall back to ''
|
|
147
|
-
// rather than trusting whatever a hand-edited/corrupt index.json carries.
|
|
124
|
+
// Legacy index.json predates last_trace_id, so '' is normal; accept only a clean digit
|
|
125
|
+
// string rather than trusting a hand-edited or corrupt index.json.
|
|
148
126
|
const legacyTraceId = String(legacyIndex.last_trace_id ?? '');
|
|
149
127
|
setMeta(db, 'last_trace_id', /^\d+$/.test(legacyTraceId) ? legacyTraceId : '');
|
|
150
128
|
const legacyStats = loadLegacyStatsFile(hippoRoot);
|
package/dist/store/rows.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Layer, type MemoryEntry, type ConfidenceLevel } from '../memory.js';
|
|
2
|
-
|
|
3
|
-
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
4
|
-
[key: string]: JsonValue;
|
|
5
|
-
};
|
|
2
|
+
import type { JsonValue } from '../json.js';
|
|
6
3
|
export interface IndexEntry {
|
|
7
4
|
id: string;
|
|
8
5
|
file: string;
|
|
@@ -17,11 +14,8 @@ export interface HippoIndex {
|
|
|
17
14
|
version: number;
|
|
18
15
|
entries: Record<string, IndexEntry>;
|
|
19
16
|
last_retrieval_ids: string[];
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
* from the `last_trace_id` meta key exactly like last_retrieval_ids. null
|
|
23
|
-
* when no trace has been written yet (fresh store, pre-v40 flow, or
|
|
24
|
-
* api.recall-only usage — api.recall never sets this). */
|
|
17
|
+
/** Id of the most recent recall_traces row written by getContext/cmdRecall, mirrored from the
|
|
18
|
+
* `last_trace_id` meta key like last_retrieval_ids. null when none was written (api.recall never sets it). */
|
|
25
19
|
last_trace_id: string | null;
|
|
26
20
|
}
|
|
27
21
|
export interface MemoryRow {
|
|
@@ -151,8 +145,8 @@ export declare const DEFAULT_SEARCH_CANDIDATE_LIMIT = 200;
|
|
|
151
145
|
export declare function rowToEntry(row: MemoryRow): MemoryEntry;
|
|
152
146
|
export declare function parseJsonArray(raw: string | null | undefined): string[];
|
|
153
147
|
/**
|
|
154
|
-
* Strict parse for the `last_trace_id` meta value
|
|
155
|
-
*
|
|
148
|
+
* Strict parse for the `last_trace_id` meta value.
|
|
149
|
+
* A bare Number(raw) would turn '', whitespace, or garbage into a
|
|
156
150
|
* usable-looking 0/NaN — a consumer INSERTing recall_trace_outcomes with
|
|
157
151
|
* trace_id=0 would hit a masked FK violation (row id 0 never exists).
|
|
158
152
|
* Require a clean positive integer string; anything else is treated as
|
package/dist/store/rows.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { log } from '../log.js';
|
|
2
2
|
export const INDEX_VERSION = 3;
|
|
3
3
|
export const MEMORY_SELECT_COLUMNS = `id, created, last_retrieved, retrieval_count, strength, half_life_days, layer, tags_json, emotional_valence, schema_fit, source, outcome_score, outcome_positive, outcome_negative, conflicts_with_json, pinned, confidence, content, parents_json, starred, trace_outcome, source_session_id, valid_from, superseded_by, extracted_from, dag_level, dag_parent_id, kind, scope, owner, artifact_ref, tenant_id, origin_project, descendant_count, earliest_at, latest_at, summary_dirty, last_rebuilt_at, rebuild_count, dag_level_3_built_at`;
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// unqualified field reads keep working unchanged. The trailing
|
|
7
|
-
// bm25(memories_fts) AS bm25_score adds the FTS rank as a result column.
|
|
8
|
-
// Only used inside the FTS path; non-FTS paths keep MEMORY_SELECT_COLUMNS.
|
|
4
|
+
// FTS-join columns for loadSearchRows: each is `m.<col> AS <col>` so rowToEntry's unqualified
|
|
5
|
+
// reads still work, and the trailing bm25(memories_fts) AS bm25_score carries the FTS rank.
|
|
9
6
|
export const MEMORY_SEARCH_COLUMNS = `m.id AS id, m.created AS created, m.last_retrieved AS last_retrieved, m.retrieval_count AS retrieval_count, m.strength AS strength, m.half_life_days AS half_life_days, m.layer AS layer, m.tags_json AS tags_json, m.emotional_valence AS emotional_valence, m.schema_fit AS schema_fit, m.source AS source, m.outcome_score AS outcome_score, m.outcome_positive AS outcome_positive, m.outcome_negative AS outcome_negative, m.conflicts_with_json AS conflicts_with_json, m.pinned AS pinned, m.confidence AS confidence, m.content AS content, m.parents_json AS parents_json, m.starred AS starred, m.trace_outcome AS trace_outcome, m.source_session_id AS source_session_id, m.valid_from AS valid_from, m.superseded_by AS superseded_by, m.extracted_from AS extracted_from, m.dag_level AS dag_level, m.dag_parent_id AS dag_parent_id, m.kind AS kind, m.scope AS scope, m.owner AS owner, m.artifact_ref AS artifact_ref, m.tenant_id AS tenant_id, m.origin_project AS origin_project, m.descendant_count AS descendant_count, m.earliest_at AS earliest_at, m.latest_at AS latest_at, m.summary_dirty AS summary_dirty, m.last_rebuilt_at AS last_rebuilt_at, m.rebuild_count AS rebuild_count, m.dag_level_3_built_at AS dag_level_3_built_at, bm25(memories_fts) AS bm25_score`;
|
|
10
7
|
/**
|
|
11
8
|
* Default candidate-pool size for `loadSearchEntries` when called with
|
|
@@ -55,15 +52,13 @@ export function rowToEntry(row) {
|
|
|
55
52
|
descendant_count: Number(row.descendant_count ?? 0),
|
|
56
53
|
earliest_at: row.earliest_at ?? null,
|
|
57
54
|
latest_at: row.latest_at ?? null,
|
|
58
|
-
// v0.30 / E1 of DAG live-coupling (schema v28). Symmetric with v25 cache.
|
|
59
55
|
summary_dirty: (Number(row.summary_dirty ?? 0) === 1 ? 1 : 0),
|
|
60
56
|
last_rebuilt_at: row.last_rebuilt_at ?? null,
|
|
61
57
|
rebuild_count: Number(row.rebuild_count ?? 0),
|
|
62
58
|
dag_level_3_built_at: row.dag_level_3_built_at ?? null,
|
|
63
59
|
};
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// value 0" — though FTS5 bm25() never returns 0, this is defensive.
|
|
60
|
+
// Preserve bm25_score from the FTS path; `'bm25_score' in row` tells an absent column
|
|
61
|
+
// (non-FTS path) from a present one.
|
|
67
62
|
if ('bm25_score' in row && row.bm25_score !== undefined && row.bm25_score !== null) {
|
|
68
63
|
entry.bm25_score = Number(row.bm25_score);
|
|
69
64
|
}
|
|
@@ -82,8 +77,8 @@ export function parseJsonArray(raw) {
|
|
|
82
77
|
}
|
|
83
78
|
}
|
|
84
79
|
/**
|
|
85
|
-
* Strict parse for the `last_trace_id` meta value
|
|
86
|
-
*
|
|
80
|
+
* Strict parse for the `last_trace_id` meta value.
|
|
81
|
+
* A bare Number(raw) would turn '', whitespace, or garbage into a
|
|
87
82
|
* usable-looking 0/NaN — a consumer INSERTing recall_trace_outcomes with
|
|
88
83
|
* trace_id=0 would hit a masked FK violation (row id 0 never exists).
|
|
89
84
|
* Require a clean positive integer string; anything else is treated as
|