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
|
@@ -4,9 +4,11 @@ import { tokenize } from '../tokenize.js';
|
|
|
4
4
|
import { RECALL_DEFAULT_DENY_SCOPES } from '../recall-scope.js';
|
|
5
5
|
import { RAREST_TERM_COUNT, rarestPromptTerms } from '../prompt-recall.js';
|
|
6
6
|
import { log } from '../log.js';
|
|
7
|
+
import { originInSql } from '../project-identity.js';
|
|
7
8
|
import { topVectorMatches } from '../vector-store.js';
|
|
8
9
|
import { MEMORY_SELECT_COLUMNS, MEMORY_SEARCH_COLUMNS, DEFAULT_SEARCH_CANDIDATE_LIMIT, rowToEntry, } from './rows.js';
|
|
9
10
|
import { openStore } from './open.js';
|
|
11
|
+
import { escapeLike } from '../escape.js';
|
|
10
12
|
/** The recall scope rule for a table column prefix (`m.` or none); `passesScopeFilterForRecall` in recall-scope.ts is its JS twin. */
|
|
11
13
|
function recallScopeClause(col, scopeFilter) {
|
|
12
14
|
if (scopeFilter === undefined)
|
|
@@ -23,10 +25,12 @@ function recallScopeClause(col, scopeFilter) {
|
|
|
23
25
|
return { sql: ` AND (${admitted} OR ${col}scope = ?)`, params: [...RECALL_DEFAULT_DENY_SCOPES, scopeFilter.value] };
|
|
24
26
|
}
|
|
25
27
|
// In SQL, not after the window cut, so other projects' matches cannot crowd the project's own rows out of the LIMIT.
|
|
26
|
-
function withProject(scope, col,
|
|
27
|
-
if (
|
|
28
|
+
function withProject(scope, col, origin) {
|
|
29
|
+
if (origin === undefined)
|
|
28
30
|
return scope;
|
|
29
|
-
|
|
31
|
+
// A string is the shape published callers passed before a project could carry several names.
|
|
32
|
+
const originProjects = [origin].flat();
|
|
33
|
+
return { sql: `${scope.sql} AND (${col}origin_project = '' OR ${originInSql(originProjects, `${col}origin_project`)})`, params: [...scope.params, ...originProjects] };
|
|
30
34
|
}
|
|
31
35
|
/** Scope rule for recall: none requested is default-deny; 'exact' narrows to the request; 'additive' adds it to the default set. */
|
|
32
36
|
export function recallScopeFilter(requestedScope, mode) {
|
|
@@ -35,22 +39,15 @@ export function recallScopeFilter(requestedScope, mode) {
|
|
|
35
39
|
return mode === 'additive' ? { mode: 'default-deny-or-exact', value: requestedScope } : { mode: 'exact', value: requestedScope };
|
|
36
40
|
}
|
|
37
41
|
const FTS_QUERY_SYNTAX_RE = /fts5: syntax error|unterminated string/i;
|
|
38
|
-
function loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded = true,
|
|
39
|
-
const p = searchPredicates(tenantId, scopeFilter, includeSuperseded,
|
|
42
|
+
function loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded = true, originProjects) {
|
|
43
|
+
const p = searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjects);
|
|
40
44
|
const terms = Array.from(new Set(tokenize(query)));
|
|
41
45
|
if (terms.length === 0) {
|
|
42
|
-
//
|
|
43
|
-
// path (codex diff-pass caught the full-store fallback at the bottom;
|
|
44
|
-
// this no-terms path had the same shape). Apply LIMIT so all four
|
|
45
|
-
// candidate paths honour the caller's cap when set.
|
|
46
|
+
// LIMIT here too, so every candidate path honours the caller's cap.
|
|
46
47
|
return selectAllCandidates(db, p, limit);
|
|
47
48
|
}
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// (`syncFtsRow`, `deleteFtsRow`, `raw-archive.ts::archiveRaw`) keep using
|
|
51
|
-
// `isFtsAvailable` honestly and never silently skip FTS index sync.
|
|
52
|
-
// Lets tests exercise the LIKE branch deterministically without
|
|
53
|
-
// poisoning the on-disk FTS state.
|
|
49
|
+
// `HIPPO_FORCE_LIKE_PATH=1` forces the LIKE path for tests; gated at this read site so
|
|
50
|
+
// writes keep using `isFtsAvailable` and never silently skip FTS index sync.
|
|
54
51
|
const forceLikePath = envForceLikePath();
|
|
55
52
|
if (!forceLikePath && isFtsAvailable(db)) {
|
|
56
53
|
const rows = selectFtsCandidates(db, terms, p, limit);
|
|
@@ -60,14 +57,11 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSupersed
|
|
|
60
57
|
const rows = selectLikeCandidates(db, terms, p, limit);
|
|
61
58
|
if (rows.length > 0)
|
|
62
59
|
return rows;
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// now reported on RecallResult, an unbounded fallback would lie about
|
|
66
|
-
// candidate-pool size. Apply LIMIT here so all four paths honour the
|
|
67
|
-
// caller's cap.
|
|
60
|
+
// LIMIT the full-store fallback too: RecallResult reports the scorer window, so an
|
|
61
|
+
// unbounded fallback would misstate the candidate-pool size.
|
|
68
62
|
return selectAllCandidates(db, p, limit);
|
|
69
63
|
}
|
|
70
|
-
function searchPredicates(tenantId, scopeFilter, includeSuperseded,
|
|
64
|
+
function searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjects) {
|
|
71
65
|
// tenantId undefined = no tenant filter (legacy callers / cross-deployment
|
|
72
66
|
// helpers). tenantId set = strict tenant isolation, leveraging the composite
|
|
73
67
|
// idx_memories_tenant_created (leading column tenant_id, O(log n) lookup).
|
|
@@ -75,19 +69,8 @@ function searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjec
|
|
|
75
69
|
const tenantPredicateNoAlias = tenantId !== undefined ? ` AND tenant_id = ?` : '';
|
|
76
70
|
const tenantOnlyPredicate = tenantId !== undefined ? ` WHERE tenant_id = ?` : '';
|
|
77
71
|
const tenantParams = tenantId !== undefined ? [tenantId] : [];
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// SAVEPOINT (src/raw-archive.ts:56): UPDATE kind = 'archived' immediately
|
|
81
|
-
// followed by DELETE, both inside one savepoint that commits or rolls back
|
|
82
|
-
// atomically. SQLite atomicity guarantees no concurrent reader sees the
|
|
83
|
-
// intermediate state. This filter is defensive-only against:
|
|
84
|
-
// (a) future bugs that drop the SAVEPOINT,
|
|
85
|
-
// (b) future bugs that introduce kind='archived' as a persisted state,
|
|
86
|
-
// (c) external direct-SQL writes that bypass archiveRawMemory.
|
|
87
|
-
// tenantOnlyPredicate starts with " WHERE tenant_id = ?" when tenant is set;
|
|
88
|
-
// when unset, we have no WHERE yet, so the archived clause needs both AND
|
|
89
|
-
// and WHERE forms. The "tenant-only" path always has WHERE (from tenant or
|
|
90
|
-
// we synthesize one).
|
|
72
|
+
// Defensive: kind='archived' is a transient sentinel inside archiveRawMemory's SAVEPOINT, so this only
|
|
73
|
+
// guards against a dropped SAVEPOINT, a persisted 'archived' state, or direct-SQL writes.
|
|
91
74
|
const archivedClauseAlias = ` AND m.kind != 'archived'`;
|
|
92
75
|
const archivedClauseNoAlias = ` AND kind != 'archived'`;
|
|
93
76
|
// For the "tenant-only" path: if no tenant set, tenantOnlyPredicate is '',
|
|
@@ -95,8 +78,8 @@ function searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjec
|
|
|
95
78
|
// by always joining `tenantOnlyPredicate + archivedClauseTenantOnly` where
|
|
96
79
|
// the latter switches between " AND" and " WHERE" based on caller context.
|
|
97
80
|
const archivedClauseTenantOnly = tenantId !== undefined ? ` AND kind != 'archived'` : ` WHERE kind != 'archived'`;
|
|
98
|
-
const aliasScope = withProject(recallScopeClause('m.', scopeFilter), 'm.',
|
|
99
|
-
const plainScope = withProject(recallScopeClause('', scopeFilter), '',
|
|
81
|
+
const aliasScope = withProject(recallScopeClause('m.', scopeFilter), 'm.', originProjects);
|
|
82
|
+
const plainScope = withProject(recallScopeClause('', scopeFilter), '', originProjects);
|
|
100
83
|
const scopeParams = aliasScope.params;
|
|
101
84
|
const currentAlias = includeSuperseded ? '' : ' AND m.superseded_by IS NULL';
|
|
102
85
|
const currentNoAlias = includeSuperseded ? '' : ' AND superseded_by IS NULL';
|
|
@@ -120,9 +103,6 @@ function selectFtsCandidates(db, terms, p, limit) {
|
|
|
120
103
|
// memories_fts virtual table has no tenant_id column; filter via the
|
|
121
104
|
// joined memories row (cheap with idx_memories_tenant_created leading
|
|
122
105
|
// on tenant_id).
|
|
123
|
-
// F1 (v1.7.0): MEMORY_SEARCH_COLUMNS adds bm25_score as the trailing
|
|
124
|
-
// result column. Every other column is m.<col> AS <col> so rowToEntry
|
|
125
|
-
// sees the same shape it always has.
|
|
126
106
|
// SAFETY: MEMORY_SEARCH_COLUMNS aliases every column to the same name
|
|
127
107
|
// MEMORY_SELECT_COLUMNS uses (plus bm25_score), matching MemoryRow.
|
|
128
108
|
return db.prepare(`
|
|
@@ -143,7 +123,6 @@ function selectFtsCandidates(db, terms, p, limit) {
|
|
|
143
123
|
}
|
|
144
124
|
}
|
|
145
125
|
function selectLikeCandidates(db, terms, p, limit) {
|
|
146
|
-
const escapeLike = (term) => term.replace(/[%_\\]/g, '\\$&');
|
|
147
126
|
const where = terms.map(() => `(LOWER(content) LIKE ? ESCAPE '\\' OR LOWER(tags_json) LIKE ? ESCAPE '\\')`).join(' OR ');
|
|
148
127
|
const params = terms.flatMap((term) => {
|
|
149
128
|
const like = `%${escapeLike(term)}%`;
|
|
@@ -176,42 +155,35 @@ export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDI
|
|
|
176
155
|
}
|
|
177
156
|
}
|
|
178
157
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* to re-filter (root-cause-over-patches: codex flagged this on v1.6.5 review).
|
|
158
|
+
* Recall-mode loader. Pushes the recall-side scope predicate into SQL so
|
|
159
|
+
* `unknown:legacy` cannot leak via any consumer that hasn't remembered to re-filter.
|
|
182
160
|
*
|
|
183
161
|
* - `requestedScope` undefined / '': default-deny on `unknown:legacy`.
|
|
184
162
|
* - `requestedScope` non-empty string: exact match on `m.scope = requestedScope`.
|
|
185
163
|
*
|
|
186
|
-
* Private
|
|
187
|
-
*
|
|
188
|
-
* post-window-only filtering let private rows starve admitted candidates out
|
|
189
|
-
* of the LIMIT window); the exact anchored regex
|
|
190
|
-
* (`passesScopeFilterForRecall`) remains the authoritative JS post-filter in
|
|
191
|
-
* the recall consumers.
|
|
164
|
+
* Private scopes: SQL applies a conservative `NOT LIKE '%:private:%'` before the LIMIT window so private
|
|
165
|
+
* rows cannot starve admitted ones; `passesScopeFilterForRecall` stays the exact JS post-filter.
|
|
192
166
|
*
|
|
193
|
-
* Consumers: `api.recall
|
|
194
|
-
* and `searchBothHybrid` recall mode
|
|
167
|
+
* Consumers: `api.recall`, `cmdRecall`/`cmdExplain` direct CLI paths
|
|
168
|
+
* and `searchBothHybrid` recall mode. Background pipelines
|
|
195
169
|
* (`consolidate`, `embeddings`, `refine-llm`, ...) keep using
|
|
196
170
|
* `loadSearchEntries` so they can see quarantined rows when needed.
|
|
197
171
|
*
|
|
198
|
-
* `tenantId`
|
|
199
|
-
* mode (its `tenantId` option is optional); `loadSearchRows` already treats
|
|
200
|
-
* undefined as "no tenant filter" for legacy callers.
|
|
172
|
+
* `tenantId` is optional because searchBothHybrid's is; undefined means no tenant filter.
|
|
201
173
|
*/
|
|
202
|
-
export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true,
|
|
174
|
+
export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProjects) {
|
|
203
175
|
const db = openStore(hippoRoot);
|
|
204
176
|
try {
|
|
205
|
-
return loadRecallSearchEntriesFromDb(db, query, limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded,
|
|
177
|
+
return loadRecallSearchEntriesFromDb(db, query, limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded, originProjects);
|
|
206
178
|
}
|
|
207
179
|
finally {
|
|
208
180
|
closeHippoDb(db);
|
|
209
181
|
}
|
|
210
182
|
}
|
|
211
|
-
// Split out so callers with an already-open db (
|
|
183
|
+
// Split out so callers with an already-open db (the prompt-recall path) skip
|
|
212
184
|
// the initStore+open/close cycle per store per call.
|
|
213
|
-
export function loadRecallSearchEntriesFromDb(db, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true,
|
|
214
|
-
return loadSearchRows(db, query, limit, tenantId, recallScopeFilter(requestedScope, explicitScopeMode), includeSuperseded,
|
|
185
|
+
export function loadRecallSearchEntriesFromDb(db, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProjects) {
|
|
186
|
+
return loadSearchRows(db, query, limit, tenantId, recallScopeFilter(requestedScope, explicitScopeMode), includeSuperseded, originProjects).map(rowToEntry);
|
|
215
187
|
}
|
|
216
188
|
/** The rows nearest `queryVector` that pass `spec`, nearest first. */
|
|
217
189
|
export function loadVectorCandidateEntries(hippoRoot, queryVector, spec) {
|
package/dist/store/sessions.d.ts
CHANGED
|
@@ -9,8 +9,7 @@ export declare function saveActiveTaskSnapshot(hippoRoot: string, tenantId: stri
|
|
|
9
9
|
}): TaskSnapshot;
|
|
10
10
|
export declare function loadActiveTaskSnapshot(hippoRoot: string, tenantId: string): TaskSnapshot | null;
|
|
11
11
|
/**
|
|
12
|
-
* Default freshness bound for AMBIENT active-task-snapshot reads
|
|
13
|
-
* docs/plans/2026-08-23-df1-snapshot-lifecycle.md): 72h, chosen over 48h so
|
|
12
|
+
* Default freshness bound for AMBIENT active-task-snapshot reads: 72h, chosen over 48h so
|
|
14
13
|
* a Friday-evening orphan still offers continuity on Monday morning.
|
|
15
14
|
* Exported so callers can override via `loadFreshActiveTaskSnapshot`'s
|
|
16
15
|
* `opts.maxAgeMs`; deliberately no env knob (Simplicity First).
|
|
@@ -18,7 +17,7 @@ export declare function loadActiveTaskSnapshot(hippoRoot: string, tenantId: stri
|
|
|
18
17
|
export declare const SNAPSHOT_AMBIENT_MAX_AGE_MS: number;
|
|
19
18
|
/**
|
|
20
19
|
* Bounded read for AMBIENT active-task-snapshot surfaces (UserPromptSubmit
|
|
21
|
-
* hook context, MCP recall block)
|
|
20
|
+
* hook context, MCP recall block), so snapshots expire. A
|
|
22
21
|
* snapshot written by `hippo pre-compact` has no death path tied to the
|
|
23
22
|
* session that owns it, so an orphaned row would otherwise inject into
|
|
24
23
|
* every prompt of every later session forever. Wraps `loadActiveTaskSnapshot`
|
|
@@ -45,8 +44,8 @@ export declare function loadFreshActiveTaskSnapshot(hippoRoot: string, tenantId:
|
|
|
45
44
|
}): TaskSnapshot | null;
|
|
46
45
|
export declare function clearActiveTaskSnapshot(hippoRoot: string, tenantId: string, clearedStatus?: string): boolean;
|
|
47
46
|
/**
|
|
48
|
-
* Close the `active` task snapshot(s) owned by `sessionId`, for the
|
|
49
|
-
* session-end death path
|
|
47
|
+
* Close the `active` task snapshot(s) owned by `sessionId`, for the
|
|
48
|
+
* session-end death path.
|
|
50
49
|
* Only one `active` row exists per tenant in practice (supersession happens
|
|
51
50
|
* at save), but the WHERE clause scopes on `session_id` too — not just
|
|
52
51
|
* `status='active' AND tenant_id=?` — so an ending session can never close a
|
package/dist/store/sessions.js
CHANGED
|
@@ -68,8 +68,7 @@ export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Default freshness bound for AMBIENT active-task-snapshot reads
|
|
72
|
-
* docs/plans/2026-08-23-df1-snapshot-lifecycle.md): 72h, chosen over 48h so
|
|
71
|
+
* Default freshness bound for AMBIENT active-task-snapshot reads: 72h, chosen over 48h so
|
|
73
72
|
* a Friday-evening orphan still offers continuity on Monday morning.
|
|
74
73
|
* Exported so callers can override via `loadFreshActiveTaskSnapshot`'s
|
|
75
74
|
* `opts.maxAgeMs`; deliberately no env knob (Simplicity First).
|
|
@@ -83,7 +82,7 @@ function isNonEmptySessionId(value) {
|
|
|
83
82
|
}
|
|
84
83
|
/**
|
|
85
84
|
* Bounded read for AMBIENT active-task-snapshot surfaces (UserPromptSubmit
|
|
86
|
-
* hook context, MCP recall block)
|
|
85
|
+
* hook context, MCP recall block), so snapshots expire. A
|
|
87
86
|
* snapshot written by `hippo pre-compact` has no death path tied to the
|
|
88
87
|
* session that owns it, so an orphaned row would otherwise inject into
|
|
89
88
|
* every prompt of every later session forever. Wraps `loadActiveTaskSnapshot`
|
|
@@ -138,8 +137,8 @@ export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cl
|
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
/**
|
|
141
|
-
* Close the `active` task snapshot(s) owned by `sessionId`, for the
|
|
142
|
-
* session-end death path
|
|
140
|
+
* Close the `active` task snapshot(s) owned by `sessionId`, for the
|
|
141
|
+
* session-end death path.
|
|
143
142
|
* Only one `active` row exists per tenant in practice (supersession happens
|
|
144
143
|
* at save), but the WHERE clause scopes on `session_id` too — not just
|
|
145
144
|
* `status='active' AND tenant_id=?` — so an ending session can never close a
|
|
@@ -162,7 +161,7 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
|
|
|
162
161
|
assertTenantId('appendSessionEvent', tenantId);
|
|
163
162
|
const db = openStore(hippoRoot);
|
|
164
163
|
const now = new Date().toISOString();
|
|
165
|
-
//
|
|
164
|
+
// Scope is stored as given; default-deny in api.recall + cmdRecall
|
|
166
165
|
// continuity reads applies to slack:private:* and 'unknown:legacy' rows.
|
|
167
166
|
try {
|
|
168
167
|
const result = db.prepare(`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MemoryEntry } from '../memory.js';
|
|
2
2
|
/**
|
|
3
3
|
* Load summaries flagged dirty for the given tenant. Sorted by latest_at
|
|
4
|
-
* DESC (NULLS LAST) so
|
|
4
|
+
* DESC (NULLS LAST) so the rebuild cap (HIPPO_DAG_REBUILD_CAP, default 20)
|
|
5
5
|
* takes the most-recently-changed summaries first.
|
|
6
6
|
*
|
|
7
7
|
* Returns full MemoryEntry shape via MEMORY_SELECT_COLUMNS + rowToEntry
|
|
@@ -11,21 +11,20 @@ export declare function loadDirtySummaries(hippoRoot: string, tenantId: string):
|
|
|
11
11
|
/**
|
|
12
12
|
* Mark a summary as dirty. Idempotent (re-marking dirty is a no-op + no
|
|
13
13
|
* second audit row). Tenant-scoped to prevent cross-tenant writes via
|
|
14
|
-
* parent-lookup. Called
|
|
14
|
+
* parent-lookup. Called from invalidation.ts / writeEntry /
|
|
15
15
|
* forgetMemory / archiveRawMemory whenever a child is invalidated,
|
|
16
16
|
* superseded, forgotten, or archived.
|
|
17
17
|
*
|
|
18
|
-
* Quietly no-ops if the target row doesn't exist or isn't a level-2
|
|
19
|
-
* summary
|
|
20
|
-
* build path lands). Emits a 'summary_marked_dirty' audit row on actual
|
|
18
|
+
* Quietly no-ops if the target row doesn't exist or isn't a level-2/3
|
|
19
|
+
* summary. Emits a 'summary_marked_dirty' audit row on actual
|
|
21
20
|
* state transitions (0 -> 1) via the audit() helper, which try/catches
|
|
22
21
|
* for missing audit_log (the v27 self-heal scenario).
|
|
23
22
|
*/
|
|
24
23
|
export declare function markSummaryDirty(hippoRoot: string, summaryId: string, tenantId: string, actor?: string): void;
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
25
|
+
* Host-wide loader for L2 topic summaries without an L3 parent.
|
|
27
26
|
* Used by consolidate phase 1.9 (buildEntityProfiles) to cluster L2s into
|
|
28
|
-
* L3 entity profiles. Mirrors loadAllDirtySummaries
|
|
27
|
+
* L3 entity profiles. Mirrors loadAllDirtySummaries: SQL-level
|
|
29
28
|
* filter is cheaper than reusing in-memory `survivors` (which doesn't
|
|
30
29
|
* contain L2s freshly created by phase 1.7 buildDag).
|
|
31
30
|
*
|
|
@@ -34,7 +33,7 @@ export declare function markSummaryDirty(hippoRoot: string, summaryId: string, t
|
|
|
34
33
|
*/
|
|
35
34
|
export declare function loadAllL2Summaries(hippoRoot: string): MemoryEntry[];
|
|
36
35
|
/**
|
|
37
|
-
*
|
|
36
|
+
* Host-wide variant of loadDirtySummaries. Iterates all tenants
|
|
38
37
|
* in one query so consolidate.ts (host-wide per L106-109) does not need a
|
|
39
38
|
* per-tenant loop. Each returned MemoryEntry carries its own tenantId (via
|
|
40
39
|
* rowToEntry), so per-summary children + rebuild UPDATE stay tenant-scoped.
|
|
@@ -44,7 +43,7 @@ export declare function loadAllL2Summaries(hippoRoot: string): MemoryEntry[];
|
|
|
44
43
|
*/
|
|
45
44
|
export declare function loadAllDirtySummaries(hippoRoot: string): MemoryEntry[];
|
|
46
45
|
/**
|
|
47
|
-
*
|
|
46
|
+
* Load live children of a DAG summary. Used by
|
|
48
47
|
* rebuildDirtySummaries to regenerate content from the CURRENT child set
|
|
49
48
|
* (not the children at create-time). Skips archived. Tenant-scoped
|
|
50
49
|
* (defence in depth — dag_parent_id is unique-ish but tenant guard is
|
|
@@ -52,7 +51,7 @@ export declare function loadAllDirtySummaries(hippoRoot: string): MemoryEntry[];
|
|
|
52
51
|
*/
|
|
53
52
|
export declare function loadChildrenOfSummary(hippoRoot: string, summaryId: string, tenantId: string): MemoryEntry[];
|
|
54
53
|
/**
|
|
55
|
-
*
|
|
54
|
+
* Patch applied by applyRebuildResult. Two-branch shape
|
|
56
55
|
* (bumpRebuildCount false for zero-child case, true for normal rebuild).
|
|
57
56
|
*/
|
|
58
57
|
export interface RebuildPatch {
|
|
@@ -65,7 +64,7 @@ export interface RebuildPatch {
|
|
|
65
64
|
actor: string;
|
|
66
65
|
}
|
|
67
66
|
/**
|
|
68
|
-
*
|
|
67
|
+
* Apply a rebuild result to a dirty summary. Atomic: one
|
|
69
68
|
* prepared UPDATE statement plus syncFtsRow inside one SAVEPOINT.
|
|
70
69
|
* WHERE includes `AND summary_dirty = 1` so concurrent sleep's race-loser
|
|
71
70
|
* becomes a no-op (no rebuild_count bump, no audit row).
|
|
@@ -81,14 +80,11 @@ export declare function applyRebuildResult(hippoRoot: string, summary: MemoryEnt
|
|
|
81
80
|
refused: boolean;
|
|
82
81
|
};
|
|
83
82
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* each member's writeEntry call fires markSummaryDirtyInTx on the just-
|
|
87
|
-
* created parent (E2 hook at store.ts:1214), and the same sleep cycle's
|
|
88
|
-
* E3 rebuild phase would re-rebuild every new summary (2x LLM cost).
|
|
83
|
+
* Clear summary_dirty on a freshly-built summary. Called by buildDag right after the child-link loop:
|
|
84
|
+
* each member's writeEntry marks the new parent dirty, so the same cycle's rebuild would redo every new summary.
|
|
89
85
|
*
|
|
90
86
|
* Idempotent: no-op + no audit if summary isn't dirty. Audit
|
|
91
|
-
* source='buildDag-clean' distinguishes from
|
|
87
|
+
* source='buildDag-clean' distinguishes it from the rebuild's source.
|
|
92
88
|
*/
|
|
93
89
|
export declare function clearSummaryDirtyAfterBuild(hippoRoot: string, summaryId: string, tenantId: string, actor?: string, source?: string): void;
|
|
94
90
|
//# sourceMappingURL=summaries.d.ts.map
|
package/dist/store/summaries.js
CHANGED
|
@@ -7,17 +7,12 @@ import { audit } from './audit-event.js';
|
|
|
7
7
|
import { syncFtsRow } from './entry-row.js';
|
|
8
8
|
import { openStore } from './open.js';
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// Used by E2 (child-write propagation in invalidation.ts / writeEntry /
|
|
14
|
-
// forgetMemory / archiveRawMemory) to mark a summary dirty when one of its
|
|
15
|
-
// children changes, and by E3's sleep-cycle rebuildDirtySummaries phase to
|
|
16
|
-
// enumerate candidates without scanning every memory row.
|
|
10
|
+
// Dirty-flag helpers for DAG summaries: child writes mark a summary dirty, and the
|
|
11
|
+
// sleep-cycle rebuild enumerates candidates without scanning every memory row.
|
|
17
12
|
// ---------------------------------------------------------------------------
|
|
18
13
|
/**
|
|
19
14
|
* Load summaries flagged dirty for the given tenant. Sorted by latest_at
|
|
20
|
-
* DESC (NULLS LAST) so
|
|
15
|
+
* DESC (NULLS LAST) so the rebuild cap (HIPPO_DAG_REBUILD_CAP, default 20)
|
|
21
16
|
* takes the most-recently-changed summaries first.
|
|
22
17
|
*
|
|
23
18
|
* Returns full MemoryEntry shape via MEMORY_SELECT_COLUMNS + rowToEntry
|
|
@@ -46,13 +41,12 @@ export function loadDirtySummaries(hippoRoot, tenantId) {
|
|
|
46
41
|
/**
|
|
47
42
|
* Mark a summary as dirty. Idempotent (re-marking dirty is a no-op + no
|
|
48
43
|
* second audit row). Tenant-scoped to prevent cross-tenant writes via
|
|
49
|
-
* parent-lookup. Called
|
|
44
|
+
* parent-lookup. Called from invalidation.ts / writeEntry /
|
|
50
45
|
* forgetMemory / archiveRawMemory whenever a child is invalidated,
|
|
51
46
|
* superseded, forgotten, or archived.
|
|
52
47
|
*
|
|
53
|
-
* Quietly no-ops if the target row doesn't exist or isn't a level-2
|
|
54
|
-
* summary
|
|
55
|
-
* build path lands). Emits a 'summary_marked_dirty' audit row on actual
|
|
48
|
+
* Quietly no-ops if the target row doesn't exist or isn't a level-2/3
|
|
49
|
+
* summary. Emits a 'summary_marked_dirty' audit row on actual
|
|
56
50
|
* state transitions (0 -> 1) via the audit() helper, which try/catches
|
|
57
51
|
* for missing audit_log (the v27 self-heal scenario).
|
|
58
52
|
*/
|
|
@@ -60,8 +54,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
|
|
|
60
54
|
assertTenantId('markSummaryDirty', tenantId);
|
|
61
55
|
const db = openStore(hippoRoot);
|
|
62
56
|
try {
|
|
63
|
-
//
|
|
64
|
-
// actual level in same round trip.
|
|
57
|
+
// RETURNING dag_level reads the actual level in the same round trip.
|
|
65
58
|
// SAFETY: result's shape matches the single `dag_level` column returned
|
|
66
59
|
// above.
|
|
67
60
|
const result = db.prepare(`
|
|
@@ -76,8 +69,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
|
|
|
76
69
|
`).get(summaryId, tenantId);
|
|
77
70
|
if (result) {
|
|
78
71
|
// audit() wraps appendAuditEvent in try/catch (v27 heal scenario).
|
|
79
|
-
// metadata.source
|
|
80
|
-
// distinguish dirty-marks across the arc's wiring layers.
|
|
72
|
+
// metadata.source tells this dirty-mark apart from the other wiring layers' marks.
|
|
81
73
|
audit(db, 'summary_marked_dirty', summaryId, { dag_level: result.dag_level, source: 'E1' }, actor, tenantId);
|
|
82
74
|
}
|
|
83
75
|
}
|
|
@@ -86,7 +78,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
|
|
|
86
78
|
}
|
|
87
79
|
}
|
|
88
80
|
// ---------------------------------------------------------------------------
|
|
89
|
-
//
|
|
81
|
+
// Sleep-cycle rebuild surface.
|
|
90
82
|
//
|
|
91
83
|
// loadAllDirtySummaries / loadChildrenOfSummary / applyRebuildResult /
|
|
92
84
|
// clearSummaryDirtyAfterBuild live HERE (not in dag.ts) because they need
|
|
@@ -95,9 +87,9 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
|
|
|
95
87
|
// rebuildDirtySummaries() that calls into these.
|
|
96
88
|
// ---------------------------------------------------------------------------
|
|
97
89
|
/**
|
|
98
|
-
*
|
|
90
|
+
* Host-wide loader for L2 topic summaries without an L3 parent.
|
|
99
91
|
* Used by consolidate phase 1.9 (buildEntityProfiles) to cluster L2s into
|
|
100
|
-
* L3 entity profiles. Mirrors loadAllDirtySummaries
|
|
92
|
+
* L3 entity profiles. Mirrors loadAllDirtySummaries: SQL-level
|
|
101
93
|
* filter is cheaper than reusing in-memory `survivors` (which doesn't
|
|
102
94
|
* contain L2s freshly created by phase 1.7 buildDag).
|
|
103
95
|
*
|
|
@@ -125,7 +117,7 @@ export function loadAllL2Summaries(hippoRoot) {
|
|
|
125
117
|
}
|
|
126
118
|
}
|
|
127
119
|
/**
|
|
128
|
-
*
|
|
120
|
+
* Host-wide variant of loadDirtySummaries. Iterates all tenants
|
|
129
121
|
* in one query so consolidate.ts (host-wide per L106-109) does not need a
|
|
130
122
|
* per-tenant loop. Each returned MemoryEntry carries its own tenantId (via
|
|
131
123
|
* rowToEntry), so per-summary children + rebuild UPDATE stay tenant-scoped.
|
|
@@ -152,7 +144,7 @@ export function loadAllDirtySummaries(hippoRoot) {
|
|
|
152
144
|
}
|
|
153
145
|
}
|
|
154
146
|
/**
|
|
155
|
-
*
|
|
147
|
+
* Load live children of a DAG summary. Used by
|
|
156
148
|
* rebuildDirtySummaries to regenerate content from the CURRENT child set
|
|
157
149
|
* (not the children at create-time). Skips archived. Tenant-scoped
|
|
158
150
|
* (defence in depth — dag_parent_id is unique-ish but tenant guard is
|
|
@@ -180,7 +172,7 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
|
|
|
180
172
|
}
|
|
181
173
|
}
|
|
182
174
|
/**
|
|
183
|
-
*
|
|
175
|
+
* Apply a rebuild result to a dirty summary. Atomic: one
|
|
184
176
|
* prepared UPDATE statement plus syncFtsRow inside one SAVEPOINT.
|
|
185
177
|
* WHERE includes `AND summary_dirty = 1` so concurrent sleep's race-loser
|
|
186
178
|
* becomes a no-op (no rebuild_count bump, no audit row).
|
|
@@ -217,7 +209,6 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
|
|
|
217
209
|
}
|
|
218
210
|
}
|
|
219
211
|
// ONE prepared UPDATE per branch. Test #8 inspects the SQL string.
|
|
220
|
-
// v0.30 / E5: widened dag_level=2 -> IN (2, 3) on both branches.
|
|
221
212
|
const REBUILD_CONTENT_SQL = `UPDATE memories
|
|
222
213
|
SET content = ?,
|
|
223
214
|
descendant_count = ?,
|
|
@@ -243,17 +234,8 @@ const REBUILD_METADATA_SQL = `UPDATE memories
|
|
|
243
234
|
AND kind != 'archived'`;
|
|
244
235
|
function applyRebuildInSavepoint(db, summary, patch) {
|
|
245
236
|
const nowIso = new Date().toISOString();
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
// a direct UPDATE, bypassing the rejection guard entirely (the guard
|
|
249
|
-
// lives in upsertEntryRow's INSERT path, which this function never
|
|
250
|
-
// calls). A rebuild that regenerates byte-identical content to an
|
|
251
|
-
// already-rejected value (e.g. deterministic summarization of an
|
|
252
|
-
// unchanged child set) would silently re-assert it every sleep cycle.
|
|
253
|
-
// Check BEFORE choosing which UPDATE to run — only the
|
|
254
|
-
// bumpRebuildCount branch ever writes content, so a miss or a
|
|
255
|
-
// zero-child call is a no-op here (one indexed point query, guarded
|
|
256
|
-
// path only).
|
|
237
|
+
// Check tombstones before choosing the UPDATE: this direct UPDATE bypasses upsertEntryRow's guard, and
|
|
238
|
+
// a deterministic rebuild would otherwise re-assert a rejected value every sleep cycle.
|
|
257
239
|
const tombstone = patch.bumpRebuildCount
|
|
258
240
|
? findRejectedValue(db, summary.tenantId, rejectionDigest(patch.content))
|
|
259
241
|
: null;
|
|
@@ -268,23 +250,8 @@ function applyRebuildInSavepoint(db, summary, patch) {
|
|
|
268
250
|
const result = applyContentWrite
|
|
269
251
|
? db.prepare(REBUILD_CONTENT_SQL).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.tenantId)
|
|
270
252
|
: db.prepare(REBUILD_METADATA_SQL).run(patch.descendant_count, patch.earliest_at, patch.latest_at, summary.id, summary.tenantId);
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
// whether patch.content specifically landed. On a refusal, metadata
|
|
274
|
-
// still applies, so changed=true even though content did not change.
|
|
275
|
-
// This preserves the pre-T4 no-infinite-retry choice: the caller
|
|
276
|
-
// (dag.ts rebuildDirtySummaries) treats changed=false as "race lost,
|
|
277
|
-
// silently retry next cycle" — returning false on a refusal would
|
|
278
|
-
// retry the same doomed LLM rebuild forever, so changed=true settles
|
|
279
|
-
// this cycle (dirty cleared) regardless of refusal.
|
|
280
|
-
// `refused` is the T4 addition: true only when a tombstone hit AND
|
|
281
|
-
// the metadata UPDATE landed (changed=true) — a refusal that loses
|
|
282
|
-
// the race to a concurrent writer reports refused=false too, since
|
|
283
|
-
// nothing from this call took effect. Before T4, a refusal also
|
|
284
|
-
// counted toward the caller's `rebuilt` stat because `changed` alone
|
|
285
|
-
// could not distinguish it; the caller now increments `refused`
|
|
286
|
-
// instead of `rebuilt` when this is true, so the stat reflects what
|
|
287
|
-
// happened without changing dirty-clearing or retry behavior.
|
|
253
|
+
// `changed` means THIS call's UPDATE hit a row, so a refusal still settles the cycle (false would retry
|
|
254
|
+
// the doomed rebuild forever); `refused` lets the caller count it as refused rather than rebuilt.
|
|
288
255
|
const changed = (result.changes ?? 0) > 0;
|
|
289
256
|
const refused = Boolean(tombstone) && changed;
|
|
290
257
|
if (tombstone && changed)
|
|
@@ -307,13 +274,8 @@ function auditRefusedRebuild(db, summary, patch, tombstone) {
|
|
|
307
274
|
`(digest ${tombstone.digest.slice(0, 12)}...); metadata updated, content unchanged`);
|
|
308
275
|
}
|
|
309
276
|
function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// memory and reuse the existing syncFtsRow helper (delete-then-insert).
|
|
313
|
-
// earliest_at/latest_at preserve null semantics (R2 must-fix).
|
|
314
|
-
// AT1: content stays summary.content (unchanged) when the write was
|
|
315
|
-
// refused — applyContentWrite is false, so patch.content was never
|
|
316
|
-
// written to the row FTS must mirror.
|
|
277
|
+
// A bare UPDATE on memories does NOT update memories_fts, so resync from the patched entry;
|
|
278
|
+
// content stays summary.content when the write was refused, since FTS must mirror the row.
|
|
317
279
|
const patchedEntry = {
|
|
318
280
|
...summary,
|
|
319
281
|
content: applyContentWrite ? patch.content : summary.content,
|
|
@@ -328,8 +290,7 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
|
|
|
328
290
|
};
|
|
329
291
|
syncFtsRow(db, patchedEntry);
|
|
330
292
|
audit(db, 'summary_rebuilt', summary.id, {
|
|
331
|
-
//
|
|
332
|
-
// (NOT hardcoded 2). L2 -> 2, L3 -> 3.
|
|
293
|
+
// Actual level from the summary in scope, never hardcoded: L2 -> 2, L3 -> 3.
|
|
333
294
|
dag_level: summary.dag_level,
|
|
334
295
|
source: 'E3-rebuild',
|
|
335
296
|
zero_children: patch.zeroChildren,
|
|
@@ -337,21 +298,17 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
|
|
|
337
298
|
}, patch.actor, summary.tenantId);
|
|
338
299
|
}
|
|
339
300
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* each member's writeEntry call fires markSummaryDirtyInTx on the just-
|
|
343
|
-
* created parent (E2 hook at store.ts:1214), and the same sleep cycle's
|
|
344
|
-
* E3 rebuild phase would re-rebuild every new summary (2x LLM cost).
|
|
301
|
+
* Clear summary_dirty on a freshly-built summary. Called by buildDag right after the child-link loop:
|
|
302
|
+
* each member's writeEntry marks the new parent dirty, so the same cycle's rebuild would redo every new summary.
|
|
345
303
|
*
|
|
346
304
|
* Idempotent: no-op + no audit if summary isn't dirty. Audit
|
|
347
|
-
* source='buildDag-clean' distinguishes from
|
|
305
|
+
* source='buildDag-clean' distinguishes it from the rebuild's source.
|
|
348
306
|
*/
|
|
349
307
|
export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, actor = 'cli', source = 'buildDag-clean') {
|
|
350
308
|
assertTenantId('clearSummaryDirtyAfterBuild', tenantId);
|
|
351
309
|
const db = openStore(hippoRoot);
|
|
352
310
|
try {
|
|
353
|
-
//
|
|
354
|
-
// actual level so audit metadata stays accurate without an extra SELECT.
|
|
311
|
+
// RETURNING dag_level reads the actual level so audit metadata stays accurate without an extra SELECT.
|
|
355
312
|
// SAFETY: result's shape matches the single `dag_level` column returned
|
|
356
313
|
// below.
|
|
357
314
|
const result = db.prepare(`
|
|
@@ -365,8 +322,7 @@ export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, acto
|
|
|
365
322
|
RETURNING dag_level
|
|
366
323
|
`).get(summaryId, tenantId);
|
|
367
324
|
if (result) {
|
|
368
|
-
//
|
|
369
|
-
// buildEntityProfiles-clean (L3) and any future build path.
|
|
325
|
+
// source tells buildDag-clean (L2) from buildEntityProfiles-clean (L3) and any future build path.
|
|
370
326
|
audit(db, 'summary_marked_clean', summaryId, { dag_level: result.dag_level, source }, actor, tenantId);
|
|
371
327
|
}
|
|
372
328
|
}
|
package/dist/support-bundle.js
CHANGED
|
@@ -11,6 +11,8 @@ import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getMeta, countTabl
|
|
|
11
11
|
import { runDoctor } from './doctor.js';
|
|
12
12
|
import { loadConfig } from './config.js';
|
|
13
13
|
import { redactSecretsStrict } from './secret-detect.js';
|
|
14
|
+
import { isJsonString } from './json.js';
|
|
15
|
+
import { escapeRegex } from './escape.js';
|
|
14
16
|
const TAIL_MAX_BYTES = 256 * 1024;
|
|
15
17
|
export const TAIL_MAX_LINES = 200;
|
|
16
18
|
const TAIL_MAX_LINE_CHARS = 2000;
|
|
@@ -28,9 +30,6 @@ const CONFIG_SECRET_KEY_RE = /key|token|secret|passw|credential|auth|cookie|bear
|
|
|
28
30
|
function isJsonObject(v) {
|
|
29
31
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
30
32
|
}
|
|
31
|
-
function isJsonString(v) {
|
|
32
|
-
return typeof v === 'string';
|
|
33
|
-
}
|
|
34
33
|
function buildRuntime() {
|
|
35
34
|
return {
|
|
36
35
|
node: process.versions.node,
|
|
@@ -177,9 +176,6 @@ function buildLogsSection(opts) {
|
|
|
177
176
|
}
|
|
178
177
|
return logs;
|
|
179
178
|
}
|
|
180
|
-
function escapeRegExp(s) {
|
|
181
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
182
|
-
}
|
|
183
179
|
// The native realpath is the only form that exposes a short-name (8.3) or symlinked alias for what it is.
|
|
184
180
|
function nativeRealPathKey(p) {
|
|
185
181
|
try {
|
|
@@ -232,12 +228,12 @@ function mountedDrive(letter) {
|
|
|
232
228
|
/** Regex source for one spelling of the home, or null when too little of it is left to swap safely. */
|
|
233
229
|
function spellingPattern(spelling, deep) {
|
|
234
230
|
if (process.platform !== 'win32')
|
|
235
|
-
return spelling.length >= 3 ?
|
|
231
|
+
return spelling.length >= 3 ? escapeRegex(spelling) : null;
|
|
236
232
|
// Each tool that mounts a drive writes it its own way, so a home two or more folders below its drive (\Users\<name>)
|
|
237
233
|
// matches after any prefix, with \, / or JSON's \\ between folders. A drive written a known way goes into the swap with it.
|
|
238
234
|
const drive = /^([A-Za-z])[:-]/.exec(spelling);
|
|
239
235
|
const below = drive === null ? spelling : spelling.slice(2);
|
|
240
|
-
const body = below.split(/[\\/]+/).map(
|
|
236
|
+
const body = below.split(/[\\/]+/).map(escapeRegex).join('[\\\\/]+');
|
|
241
237
|
if (drive === null) {
|
|
242
238
|
if (spelling.length < 3)
|
|
243
239
|
return null;
|
package/dist/tenant.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from './db.js';
|
|
2
|
+
import type { JsonValue } from './json.js';
|
|
2
3
|
export interface ResolveOpts {
|
|
3
4
|
db?: DatabaseSyncLike;
|
|
4
5
|
apiKey?: string;
|
|
5
6
|
}
|
|
6
7
|
export declare function resolveTenantId(opts: ResolveOpts): string;
|
|
7
|
-
/** A value that round-trips through JSON.stringify/JSON.parse unchanged. */
|
|
8
|
-
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
9
|
-
[key: string]: JsonValue;
|
|
10
|
-
};
|
|
11
8
|
/**
|
|
12
9
|
* Defensive runtime guard for tenant id arguments.
|
|
13
10
|
*
|
|
@@ -25,5 +22,4 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
25
22
|
* Acceptable tradeoff for catching the silent-leak class.
|
|
26
23
|
*/
|
|
27
24
|
export declare function assertTenantId(fnName: string, value: JsonValue): asserts value is string;
|
|
28
|
-
export {};
|
|
29
25
|
//# sourceMappingURL=tenant.d.ts.map
|
package/dist/token-ledger.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { DatabaseSyncLike } from './db.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Where a block of memory text was sent.
|
|
4
4
|
* - `hook`: the per-prompt `UserPromptSubmit` hook (`hippo context --pinned-only`).
|
|
5
|
-
* - `hook_recall`: the same hook's
|
|
5
|
+
* - `hook_recall`: the same hook's prompt-recall section.
|
|
6
6
|
* - `compact_resume`: the snapshot the SessionStart(compact) hook prints (`hippo compact-resume`).
|
|
7
7
|
* - `context`, `recall`: the CLI commands.
|
|
8
8
|
* - `mcp_recall`, `mcp_context`: the MCP tools.
|