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
package/dist/graph-extract.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (docs/plans/2026-06-01-e3-deterministic-extraction.md).
|
|
2
|
+
* Deterministic entity extraction.
|
|
4
3
|
*
|
|
5
|
-
* Populates the
|
|
4
|
+
* Populates the graph from the already-structured consolidated first-class object tables -
|
|
6
5
|
* NO NLP, no precision gate for entities + supersedes. The graph is a pure derived
|
|
7
|
-
* function of the current
|
|
6
|
+
* function of the current object state, so `extractGraph` is an idempotent REBUILD: clear
|
|
8
7
|
* the tenant's graph, then re-derive entities + `supersedes` relations from decisions /
|
|
9
|
-
* policies / customer_notes / project_briefs (the four
|
|
8
|
+
* policies / customer_notes / project_briefs (the four object types whose kind maps to the
|
|
10
9
|
* `entity_type` enum). All writes go through the src/graph.ts consolidated-source guard
|
|
11
10
|
* (insertEntity / insertRelation / clearGraph); this module issues no raw SQL.
|
|
12
11
|
*
|
|
13
|
-
* Pass 3
|
|
12
|
+
* Pass 3 adds the
|
|
14
13
|
* first CROSS-OBJECT relations: a deterministic NAME-MATCH heuristic that emits a
|
|
15
14
|
* `references` edge when one consolidated object's text contains another entity's name.
|
|
16
15
|
* It is conservative (word-boundary, length-bounded, ambiguity-guarded, per-source
|
|
@@ -27,6 +26,7 @@ import { loadPolicies } from './policies.js';
|
|
|
27
26
|
import { loadCustomerNotes } from './customer-notes.js';
|
|
28
27
|
import { loadProjectBriefs } from './project-briefs.js';
|
|
29
28
|
import { assertTenantId } from './tenant.js';
|
|
29
|
+
import { escapeRegex } from './escape.js';
|
|
30
30
|
/** Per-type load cap (the loaders default to 100). A type whose active or superseded
|
|
31
31
|
* set exceeds this is truncated; `ExtractResult.truncated` records it so the
|
|
32
32
|
* incompleteness is observable rather than silent. */
|
|
@@ -46,8 +46,8 @@ export const MAX_TARGET_NAMES = 5000;
|
|
|
46
46
|
function keyOf(entityType, e2Id) {
|
|
47
47
|
return `${entityType}:${e2Id}`;
|
|
48
48
|
}
|
|
49
|
-
/** The four
|
|
50
|
-
* two EntityType members ('person', 'system') have no
|
|
49
|
+
/** The four object-derived extraction entity types map 1:1 to source_object_type; the other
|
|
50
|
+
* two EntityType members ('person', 'system') have no object table and stay unmapped. */
|
|
51
51
|
const ENTITY_TYPE_TO_SOURCE_OBJECT = {
|
|
52
52
|
decision: 'decision',
|
|
53
53
|
policy: 'policy',
|
|
@@ -56,18 +56,14 @@ const ENTITY_TYPE_TO_SOURCE_OBJECT = {
|
|
|
56
56
|
person: undefined,
|
|
57
57
|
system: undefined,
|
|
58
58
|
};
|
|
59
|
-
/** The
|
|
60
|
-
*
|
|
59
|
+
/** The source-object ref for an extraction row (always set: every extracted row is a
|
|
60
|
+
* first-class object). Throws on an unmappable entityType (a graph invariant violation). */
|
|
61
61
|
function sourceObjectOf(entityType, e2Id) {
|
|
62
62
|
const type = ENTITY_TYPE_TO_SOURCE_OBJECT[entityType];
|
|
63
63
|
if (!type)
|
|
64
64
|
throw new Error(`graph-extract: entityType '${entityType}' has no source_object_type mapping`);
|
|
65
65
|
return { type, id: e2Id };
|
|
66
66
|
}
|
|
67
|
-
/** Escape a string for safe use as a literal inside a RegExp alternation. */
|
|
68
|
-
function escapeRegex(s) {
|
|
69
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
70
|
-
}
|
|
71
67
|
/** Unordered entity-id pair key, so a relation between a,b is found in either direction. */
|
|
72
68
|
function pairKey(a, b) {
|
|
73
69
|
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
@@ -95,7 +91,7 @@ textOf) {
|
|
|
95
91
|
// loadProjectBriefs (wired at each extractGraph call site); every one of their row
|
|
96
92
|
// types declares id: number, memoryId: string | null, supersededBy: number | null.
|
|
97
93
|
// `any` here is the deliberate type-erasure boundary (see eslint-disable above)
|
|
98
|
-
// that lets one loop handle all four
|
|
94
|
+
// that lets one loop handle all four object row shapes.
|
|
99
95
|
rows.push({
|
|
100
96
|
entityType,
|
|
101
97
|
e2Id: r.id,
|
|
@@ -109,9 +105,9 @@ textOf) {
|
|
|
109
105
|
return { rows, hitCap };
|
|
110
106
|
}
|
|
111
107
|
/**
|
|
112
|
-
* Idempotent rebuild of the tenant's deterministic graph from its consolidated
|
|
108
|
+
* Idempotent rebuild of the tenant's deterministic graph from its consolidated first-class
|
|
113
109
|
* objects. Returns the entity/relation counts (+ which types were truncated at the
|
|
114
|
-
* per-type cap). Safe to re-run: output is a pure function of the current
|
|
110
|
+
* per-type cap). Safe to re-run: output is a pure function of the current object state.
|
|
115
111
|
*/
|
|
116
112
|
export function extractGraph(hippoRoot, tenantId) {
|
|
117
113
|
assertTenantId('extractGraph', tenantId);
|
|
@@ -129,50 +125,27 @@ export function extractGraph(hippoRoot, tenantId) {
|
|
|
129
125
|
const { rows, hitCap } = loadType(hippoRoot, tenantId, src.entityType, src.loadFn, src.nameOf, src.textOf);
|
|
130
126
|
return { entityType: src.entityType, rows, hitCap };
|
|
131
127
|
});
|
|
132
|
-
// WRITE PHASE
|
|
128
|
+
// WRITE PHASE: clear + every insert run in ONE transaction, so two
|
|
133
129
|
// concurrent rebuilds serialize on the SQLite write lock (no duplicate rows)
|
|
134
130
|
// and a throw mid-rebuild rolls back the clear (no bricked graph). No second
|
|
135
131
|
// connection is opened inside.
|
|
136
132
|
return runGraphRebuildTransaction(hippoRoot, tenantId, (txDb) => rebuildGraphRows(txDb, hippoRoot, tenantId, loaded));
|
|
137
133
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
function rebuildGraphRows(txDb, hippoRoot, tenantId, loaded) {
|
|
145
|
-
// Rebuild from scratch: the graph is derived, so clear then re-derive.
|
|
146
|
-
clearGraph(hippoRoot, tenantId, txDb);
|
|
147
|
-
const byType = {};
|
|
148
|
-
const truncated = [];
|
|
149
|
-
const allRows = [];
|
|
150
|
-
const entityIdByKey = new Map();
|
|
151
|
-
// The mirror memory per extracted key, null once forgotten/pruned (so a supersedes
|
|
152
|
-
// edge anchors to the successor's object when the mirror is gone but still passes the
|
|
153
|
-
// memory through when it lives).
|
|
154
|
-
const memoryIdByKey = new Map();
|
|
155
|
-
// Created entities ONLY (drives Pass 3 sources + targets), in stable insertion order.
|
|
156
|
-
const created = [];
|
|
157
|
-
// Pass 1: entities. Every ACTIVE/SUPERSEDED E2 row becomes an entity ANCHORED to its
|
|
158
|
-
// authoritative E2 object (source_object_type/id) - it survives a forgotten mirror
|
|
159
|
-
// (memory_id NULL). The mirror memory is passed through only when it still exists
|
|
160
|
-
// (it remains a recall pointer until forgotten/pruned).
|
|
134
|
+
// Pass 1: entities. Every ACTIVE/SUPERSEDED object row becomes an entity ANCHORED to its
|
|
135
|
+
// authoritative object (source_object_type/id) - it survives a forgotten mirror
|
|
136
|
+
// (memory_id NULL). The mirror memory is passed through only when it still exists
|
|
137
|
+
// (it remains a recall pointer until forgotten/pruned).
|
|
138
|
+
function insertEntityRows(txDb, hippoRoot, tenantId, loaded) {
|
|
139
|
+
const pass = { byType: {}, truncated: [], allRows: [], entityIdByKey: new Map(), memoryIdByKey: new Map(), created: [] };
|
|
161
140
|
for (const { entityType, rows, hitCap } of loaded) {
|
|
162
141
|
if (hitCap)
|
|
163
|
-
truncated.push(entityType);
|
|
164
|
-
byType[entityType] = 0;
|
|
142
|
+
pass.truncated.push(entityType);
|
|
143
|
+
pass.byType[entityType] = 0;
|
|
165
144
|
for (const row of rows) {
|
|
166
|
-
allRows.push(row);
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// source, and insertEntity REJECTS (not truncates) both an over-cap name AND an
|
|
171
|
-
// empty one. So: TRIM FIRST (codex 2026-06-01: >512 leading-whitespace chars
|
|
172
|
-
// would otherwise slice to a whitespace-only string -> trimmed to '' ->
|
|
173
|
-
// 'name is required' throw), THEN cap to MAX_ENTITY_NAME_LEN; if the normalised
|
|
174
|
-
// label is empty (the E2 save APIs forbid this, but be defensive) skip the row
|
|
175
|
-
// rather than throw. This closes the entire name-brick class.
|
|
145
|
+
pass.allRows.push(row);
|
|
146
|
+
// Name fields are uncapped at source and insertEntity rejects an over-cap or empty name,
|
|
147
|
+
// which after clearGraph would brick the rebuild. Trim before capping so leading
|
|
148
|
+
// whitespace cannot slice to ''; skip a still-empty label rather than throw.
|
|
176
149
|
const name = (row.name ?? '').trim().slice(0, MAX_ENTITY_NAME_LEN);
|
|
177
150
|
if (name.length === 0)
|
|
178
151
|
continue;
|
|
@@ -184,33 +157,35 @@ function rebuildGraphRows(txDb, hippoRoot, tenantId, loaded) {
|
|
|
184
157
|
sourceObject,
|
|
185
158
|
}, txDb);
|
|
186
159
|
const k = keyOf(row.entityType, row.e2Id);
|
|
187
|
-
entityIdByKey.set(k, entity.id);
|
|
188
|
-
memoryIdByKey.set(k, row.memoryId);
|
|
189
|
-
created.push({ entityId: entity.id, entityType: row.entityType, memoryId: row.memoryId, sourceObject, name, searchText: row.searchText ?? '', superseded: row.supersededBy !== null });
|
|
190
|
-
byType[entityType] += 1;
|
|
160
|
+
pass.entityIdByKey.set(k, entity.id);
|
|
161
|
+
pass.memoryIdByKey.set(k, row.memoryId);
|
|
162
|
+
pass.created.push({ entityId: entity.id, entityType: row.entityType, memoryId: row.memoryId, sourceObject, name, searchText: row.searchText ?? '', superseded: row.supersededBy !== null });
|
|
163
|
+
pass.byType[entityType] += 1;
|
|
191
164
|
}
|
|
192
165
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
166
|
+
return pass;
|
|
167
|
+
}
|
|
168
|
+
// "Y supersedes X" - but only when BOTH X and Y were EXTRACTED (e.g. Y may be closed
|
|
169
|
+
// and absent). The emit guard is ENTITY presence (entityIdByKey), not memory presence:
|
|
170
|
+
// a forgotten successor mirror must still emit the edge. The relation is anchored to Y's
|
|
171
|
+
// authoritative object; Y's mirror memory is passed only when it still lives.
|
|
172
|
+
function insertSupersedesRelations(txDb, hippoRoot, tenantId, pass) {
|
|
198
173
|
let relations = 0;
|
|
199
174
|
// Entity-id pairs already related by supersedes (unordered). Pass 3 skips a references
|
|
200
175
|
// edge for such a pair: a version-extends-its-predecessor's-name containment (e.g.
|
|
201
176
|
// "Adopt X (managed)" contains "Adopt X") is a name artifact, not a cross-reference,
|
|
202
177
|
// and supersedes already captures their relationship.
|
|
203
178
|
const supersededPairs = new Set();
|
|
204
|
-
for (const row of allRows) {
|
|
179
|
+
for (const row of pass.allRows) {
|
|
205
180
|
if (row.supersededBy === null)
|
|
206
181
|
continue;
|
|
207
182
|
const xKey = keyOf(row.entityType, row.e2Id);
|
|
208
183
|
const yKey = keyOf(row.entityType, row.supersededBy);
|
|
209
|
-
const fromId = entityIdByKey.get(yKey); // successor Y
|
|
210
|
-
const toId = entityIdByKey.get(xKey); // superseded X
|
|
184
|
+
const fromId = pass.entityIdByKey.get(yKey); // successor Y
|
|
185
|
+
const toId = pass.entityIdByKey.get(xKey); // superseded X
|
|
211
186
|
if (fromId === undefined || toId === undefined)
|
|
212
187
|
continue;
|
|
213
|
-
const yMemoryId = memoryIdByKey.get(yKey) ?? null; // successor's mirror, null if gone
|
|
188
|
+
const yMemoryId = pass.memoryIdByKey.get(yKey) ?? null; // successor's mirror, null if gone
|
|
214
189
|
insertRelation(hippoRoot, tenantId, {
|
|
215
190
|
fromEntityId: fromId,
|
|
216
191
|
toEntityId: toId,
|
|
@@ -221,11 +196,26 @@ function rebuildGraphRows(txDb, hippoRoot, tenantId, loaded) {
|
|
|
221
196
|
supersededPairs.add(pairKey(fromId, toId));
|
|
222
197
|
relations += 1;
|
|
223
198
|
}
|
|
199
|
+
return { relations, supersededPairs };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The deterministic rebuild WRITES, run inside `runGraphRebuildTransaction`'s
|
|
203
|
+
* transaction (`txDb` is its connection). Clears the tenant's graph then
|
|
204
|
+
* re-derives entities + `supersedes` + `references` from the preloaded rows. All
|
|
205
|
+
* DB access here is on `txDb` (or in-memory) — no other connection is opened.
|
|
206
|
+
*/
|
|
207
|
+
function rebuildGraphRows(txDb, hippoRoot, tenantId, loaded) {
|
|
208
|
+
// Rebuild from scratch: the graph is derived, so clear then re-derive.
|
|
209
|
+
clearGraph(hippoRoot, tenantId, txDb);
|
|
210
|
+
const pass = insertEntityRows(txDb, hippoRoot, tenantId, loaded);
|
|
211
|
+
const { byType, truncated, created } = pass;
|
|
212
|
+
const supersedes = insertSupersedesRelations(txDb, hippoRoot, tenantId, pass);
|
|
213
|
+
let relations = supersedes.relations;
|
|
224
214
|
// Pass 3: cross-object `references` edges via conservative name matching. A source's
|
|
225
215
|
// text containing a target entity's name -> "source references target". Sources +
|
|
226
|
-
// targets are CREATED entities only, each anchored to its
|
|
216
|
+
// targets are CREATED entities only, each anchored to its source object (so the
|
|
227
217
|
// edge survives a forgotten source mirror).
|
|
228
|
-
const references = extractReferences(hippoRoot, tenantId, created, supersededPairs, truncated, txDb);
|
|
218
|
+
const references = extractReferences(hippoRoot, tenantId, created, supersedes.supersededPairs, truncated, txDb);
|
|
229
219
|
relations += references;
|
|
230
220
|
const entities = created.length;
|
|
231
221
|
return { entities, relations, references, byType, truncated };
|
|
@@ -234,7 +224,7 @@ function rebuildGraphRows(txDb, hippoRoot, tenantId, loaded) {
|
|
|
234
224
|
* Pass 3. Build a target-name index from the created entities (names within the length
|
|
235
225
|
* bounds, ambiguous names dropped), scan each created entity's text once with one
|
|
236
226
|
* combined word-boundary regex, and emit `references` edges (self-skipped, deduped,
|
|
237
|
-
* per-source capped). Each edge is anchored to the source entity's
|
|
227
|
+
* per-source capped). Each edge is anchored to the source entity's object (memoryId
|
|
238
228
|
* passed through only when the mirror lives). Returns the number of references edges written.
|
|
239
229
|
*/
|
|
240
230
|
function extractReferences(hippoRoot, tenantId, created, supersededPairs, truncated, txDb) {
|
|
@@ -244,7 +234,7 @@ function extractReferences(hippoRoot, tenantId, created, supersededPairs, trunca
|
|
|
244
234
|
const ambiguous = new Set();
|
|
245
235
|
for (const e of created) {
|
|
246
236
|
// References are among ACTIVE entities only: a superseded (outdated) row is not a
|
|
247
|
-
// current cross-reference target
|
|
237
|
+
// current cross-reference target.
|
|
248
238
|
if (e.superseded)
|
|
249
239
|
continue;
|
|
250
240
|
// Decisions are SOURCE-only: their name is decision prose, referenced by supersedes,
|
|
@@ -275,7 +265,7 @@ function extractReferences(hippoRoot, tenantId, created, supersededPairs, trunca
|
|
|
275
265
|
truncated.push('references-targets');
|
|
276
266
|
// LONGEST name first, then alphabetical: JS regex alternation is leftmost-first, so
|
|
277
267
|
// ordering longer names before their prefixes makes the match longest-at-position
|
|
278
|
-
// (`postgres pro` wins over `postgres
|
|
268
|
+
// (`postgres pro` wins over `postgres`). Deterministic, so truncation is stable.
|
|
279
269
|
const targetNames = [...nameToId.keys()]
|
|
280
270
|
.sort((a, b) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0))
|
|
281
271
|
.slice(0, MAX_TARGET_NAMES);
|
|
@@ -284,7 +274,7 @@ function extractReferences(hippoRoot, tenantId, created, supersededPairs, trunca
|
|
|
284
274
|
let references = 0;
|
|
285
275
|
for (const src of created) {
|
|
286
276
|
if (src.superseded)
|
|
287
|
-
continue; // superseded sources hold only stale references
|
|
277
|
+
continue; // superseded sources hold only stale references
|
|
288
278
|
if (!src.searchText)
|
|
289
279
|
continue;
|
|
290
280
|
const targets = new Set();
|
package/dist/graph-recall.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ResultCost, type SearchResult } from './search/types.js';
|
|
2
2
|
/** Hard cap on `--hops` (a higher value just walks more of a finite graph; this bounds
|
|
3
3
|
* worst-case work and keeps the flag honest). */
|
|
4
4
|
export declare const MAX_HOPS = 3;
|
|
@@ -21,7 +21,7 @@ export interface GraphExpandOpts {
|
|
|
21
21
|
* cmdRecall: a row is visible if valid_from <= asOf AND, when superseded, its successor
|
|
22
22
|
* was not yet valid at asOf). */
|
|
23
23
|
asOf?: string;
|
|
24
|
-
/** Token budget for the augmented set (defaults to
|
|
24
|
+
/** Token budget for the augmented set (defaults to DEFAULT_RECALL_BUDGET, matching recall). */
|
|
25
25
|
budget?: number;
|
|
26
26
|
/** Budget cost per result; defaults to the memory text. */
|
|
27
27
|
cost?: ResultCost;
|
package/dist/graph-recall.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Multi-hop graph recall.
|
|
3
3
|
*
|
|
4
|
-
* READ-ONLY consumer of the
|
|
5
|
-
* guarded by
|
|
4
|
+
* READ-ONLY consumer of the graph substrate (entities/relations built by graph-extract,
|
|
5
|
+
* guarded by the graph-on-consolidated triggers). Given the lexical recall seeds, walk the relations graph up to N
|
|
6
6
|
* hops and surface the memories of reached entities that the lexical search did not
|
|
7
7
|
* already return.
|
|
8
8
|
*
|
|
9
9
|
* Relation-type-AGNOSTIC: it walks whatever edges exist. Today the graph holds only
|
|
10
10
|
* `supersedes` edges (so a 1-hop walk surfaces a supersession-linked predecessor/
|
|
11
|
-
* successor a lexical search may miss); the moment
|
|
11
|
+
* successor a lexical search may miss); the moment extraction emits cross-object edges
|
|
12
12
|
* (owns/depends-on/blocked-by/references) the SAME traversal lights up cross-entity
|
|
13
13
|
* multi-hop with zero rework here.
|
|
14
14
|
*
|
|
15
|
-
* Design points
|
|
16
|
-
* codex review — all root-cause, not patches):
|
|
15
|
+
* Design points:
|
|
17
16
|
* 1. Graph-reached memories are loaded DIRECTLY by id (tenant-scoped PK fetch), NOT
|
|
18
17
|
* intersected with the recall handler's candidate set — that set is lexically
|
|
19
18
|
* prefiltered (loadSearchRows filters by query tokens), so intersecting would exclude
|
|
@@ -29,14 +28,15 @@
|
|
|
29
28
|
* nothing at realistic budgets.
|
|
30
29
|
* 3. BOTH the local and global stores are expanded — a global seed's entities/relations
|
|
31
30
|
* live under the global root, so graph recall must traverse each seed in the store its
|
|
32
|
-
* graph lives in
|
|
31
|
+
* graph lives in.
|
|
33
32
|
* 4. By-id loads are chunked at 500 (loadEntriesByIds caps at 500/call), so a high-fanout
|
|
34
|
-
* traversal (--hops 3 --max-neighbors 200 -> up to 600 ids) loses none
|
|
33
|
+
* traversal (--hops 3 --max-neighbors 200 -> up to 600 ids) loses none.
|
|
35
34
|
*
|
|
36
|
-
* No graph writes (only SELECTs via graph.ts read helpers + store reads), so the
|
|
35
|
+
* No graph writes (only SELECTs via graph.ts read helpers + store reads), so the
|
|
37
36
|
* check-graph-writes lint permits this module living outside graph.ts.
|
|
38
37
|
*/
|
|
39
38
|
import { loadEntriesByIds } from './store/entry-reads.js';
|
|
39
|
+
import { DEFAULT_RECALL_BUDGET } from './search/types.js';
|
|
40
40
|
import { estimateTokens } from './token-ledger.js';
|
|
41
41
|
import { compareEntryIdentity } from './compare.js';
|
|
42
42
|
import { loadEntitiesByMemoryId, loadEntitiesByIds, loadNeighborRelations } from './graph/read.js';
|
|
@@ -61,15 +61,9 @@ function loadByIdsChunked(root, tenantId, ids) {
|
|
|
61
61
|
}
|
|
62
62
|
return out;
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const { hops, maxNeighbors, tenantId, includeSuperseded, asOfDate, recallScope } = opts;
|
|
68
|
-
// Seeds = graph entities (in THIS store) whose source memory is a base result.
|
|
69
|
-
const seedEntities = loadEntitiesByMemoryId(root, tenantId, baseResults.map((r) => r.entry.id));
|
|
70
|
-
if (seedEntities.length === 0)
|
|
71
|
-
return;
|
|
72
|
-
// BFS, both directions, up to `hops`. `visited` prevents re-expansion (cycle-safe).
|
|
64
|
+
/** BFS, both directions, up to `hops`: each reached entity's via, and the base memory it descends from. */
|
|
65
|
+
function walkRelations(root, tenantId, seedEntities, hops, maxNeighbors) {
|
|
66
|
+
// `visited` prevents re-expansion (cycle-safe).
|
|
73
67
|
// `originMemByEntityId` propagates the base-result memory id each reached node descends
|
|
74
68
|
// from (for adjacency placement + score inheritance).
|
|
75
69
|
const visitedEntityIds = new Set(seedEntities.map((e) => e.id));
|
|
@@ -118,6 +112,43 @@ function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds,
|
|
|
118
112
|
}
|
|
119
113
|
frontier = nextFrontier;
|
|
120
114
|
}
|
|
115
|
+
return { reached, originMemByEntityId };
|
|
116
|
+
}
|
|
117
|
+
/** The recall hard filters (as-of, superseded, scope) re-applied to a directly loaded graph-reached row. */
|
|
118
|
+
function passesRecallFilters(mem, via, successorValidFrom, opts) {
|
|
119
|
+
const { includeSuperseded, asOfDate, recallScope } = opts;
|
|
120
|
+
// A node reached as the `to` endpoint of a `supersedes` edge IS the superseded
|
|
121
|
+
// (older) version — the graph is the authoritative signal (the memory mirror's
|
|
122
|
+
// `superseded_by` is NOT set by `hippo decide`, only the decisions table is). By
|
|
123
|
+
// default recall shows current truth, so drop it unless --include-superseded; the
|
|
124
|
+
// `from` endpoint (the newer successor) is always kept.
|
|
125
|
+
const isSupersededEndpoint = via.relType === 'supersedes' && via.direction === 'to';
|
|
126
|
+
if (asOfDate) {
|
|
127
|
+
if (new Date(mem.valid_from) > asOfDate)
|
|
128
|
+
return false; // not yet valid at asOf
|
|
129
|
+
if (mem.superseded_by) {
|
|
130
|
+
const succVf = successorValidFrom.get(mem.superseded_by);
|
|
131
|
+
// Visible only while its successor was NOT yet valid at asOf (matches cmdRecall).
|
|
132
|
+
if (succVf && new Date(succVf) <= asOfDate)
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (!includeSuperseded && (mem.superseded_by || isSupersededEndpoint)) {
|
|
137
|
+
return false; // default recall drops superseded
|
|
138
|
+
}
|
|
139
|
+
return recallScope.additive
|
|
140
|
+
? passesCliRecallScopeFilter(mem.scope ?? null, recallScope.requested)
|
|
141
|
+
: passesScopeFilterForRecall(mem.scope ?? null, recallScope.requested);
|
|
142
|
+
}
|
|
143
|
+
/** Traverse one store's graph from its seeds into `hitsByOrigin`. Pure reads; mutates `seenMemoryIds`
|
|
144
|
+
* and `seenContent` so a memory, or a share/promote copy of it, surfaces at most once across stores. */
|
|
145
|
+
function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds, seenContent, hitsByOrigin, opts) {
|
|
146
|
+
const { hops, maxNeighbors, tenantId, asOfDate } = opts;
|
|
147
|
+
// Seeds = graph entities (in THIS store) whose source memory is a base result.
|
|
148
|
+
const seedEntities = loadEntitiesByMemoryId(root, tenantId, baseResults.map((r) => r.entry.id));
|
|
149
|
+
if (seedEntities.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
const { reached, originMemByEntityId } = walkRelations(root, tenantId, seedEntities, hops, maxNeighbors);
|
|
121
152
|
if (reached.size === 0)
|
|
122
153
|
return;
|
|
123
154
|
// Reached entities -> source memory ids -> load DIRECTLY by id (chunked), not lexical.
|
|
@@ -147,29 +178,7 @@ function produceHitsForRoot(root, baseResults, baseScoreByMemId, seenMemoryIds,
|
|
|
147
178
|
if (seenContent.has(mem.content))
|
|
148
179
|
continue; // share/promote copy: same text, another id
|
|
149
180
|
const via = reached.get(ent.id);
|
|
150
|
-
|
|
151
|
-
// (older) version — the graph is the authoritative signal (the memory mirror's
|
|
152
|
-
// `superseded_by` is NOT set by `hippo decide`, only the decisions table is). By
|
|
153
|
-
// default recall shows current truth, so drop it unless --include-superseded; the
|
|
154
|
-
// `from` endpoint (the newer successor) is always kept.
|
|
155
|
-
const isSupersededEndpoint = via.relType === 'supersedes' && via.direction === 'to';
|
|
156
|
-
if (asOfDate) {
|
|
157
|
-
if (new Date(mem.valid_from) > asOfDate)
|
|
158
|
-
continue; // not yet valid at asOf
|
|
159
|
-
if (mem.superseded_by) {
|
|
160
|
-
const succVf = successorValidFrom.get(mem.superseded_by);
|
|
161
|
-
// Visible only while its successor was NOT yet valid at asOf (matches cmdRecall).
|
|
162
|
-
if (succVf && new Date(succVf) <= asOfDate)
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
else if (!includeSuperseded && (mem.superseded_by || isSupersededEndpoint)) {
|
|
167
|
-
continue; // default recall drops superseded
|
|
168
|
-
}
|
|
169
|
-
const scopeOk = recallScope.additive
|
|
170
|
-
? passesCliRecallScopeFilter(mem.scope ?? null, recallScope.requested)
|
|
171
|
-
: passesScopeFilterForRecall(mem.scope ?? null, recallScope.requested);
|
|
172
|
-
if (!scopeOk)
|
|
181
|
+
if (!passesRecallFilters(mem, via, successorValidFrom, opts))
|
|
173
182
|
continue;
|
|
174
183
|
const origin = originMemByEntityId.get(ent.id) ?? baseResults[0].entry.id;
|
|
175
184
|
const originScore = baseScoreByMemId.get(origin) ?? baseResults[baseResults.length - 1].score;
|
|
@@ -202,7 +211,7 @@ export function graphExpandRecall(baseResults, opts) {
|
|
|
202
211
|
if (hops <= 0 || baseResults.length === 0)
|
|
203
212
|
return baseResults;
|
|
204
213
|
const maxNeighbors = opts.maxNeighbors ?? DEFAULT_MAX_NEIGHBORS;
|
|
205
|
-
const budget = opts.budget ??
|
|
214
|
+
const budget = opts.budget ?? DEFAULT_RECALL_BUDGET;
|
|
206
215
|
const includeSuperseded = opts.includeSuperseded ?? false;
|
|
207
216
|
const asOfDate = opts.asOf ? new Date(opts.asOf) : null;
|
|
208
217
|
const minResults = opts.minResults ?? 1;
|
|
@@ -222,8 +231,7 @@ export function graphExpandRecall(baseResults, opts) {
|
|
|
222
231
|
return baseResults;
|
|
223
232
|
// Closer hops first within each origin group, then by inherited score.
|
|
224
233
|
// Hops asc and score desc are both true primary keys (unchanged);
|
|
225
|
-
// compareEntryIdentity is only the TAIL for a same-hop, same-score tie
|
|
226
|
-
// (T2, deterministic tie keys).
|
|
234
|
+
// compareEntryIdentity is only the TAIL for a same-hop, same-score tie.
|
|
227
235
|
for (const hits of hitsByOrigin.values()) {
|
|
228
236
|
hits.sort((a, b) => {
|
|
229
237
|
const byHops = a.graphVia.hops - b.graphVia.hops;
|
|
@@ -243,16 +251,14 @@ export function graphExpandRecall(baseResults, opts) {
|
|
|
243
251
|
// (NOTE: at a tight budget a new graph hit can displace a weakly-scored base result;
|
|
244
252
|
// aggregate recall stays >= baseline, the displaced item is the lowest-value one.)
|
|
245
253
|
// Protect the top --min-results base rows from eviction (graph expansion must not
|
|
246
|
-
// violate the recall min-results floor
|
|
254
|
+
// violate the recall min-results floor). They are kept regardless of budget;
|
|
247
255
|
// baseResults is score-ordered, so slice(0, N) is the top N.
|
|
248
256
|
const protectedCount = Math.min(Math.max(minResults, 1), baseResults.length);
|
|
249
257
|
const keep = new Set(baseResults.slice(0, protectedCount));
|
|
250
258
|
const price = opts.cost ?? ((r) => r.tokens);
|
|
251
259
|
let usedTokens = [...keep].reduce((s, r) => s + price(r), 0);
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// base-vs-graph-hit tie keeps the BASE result first (the concat order),
|
|
255
|
-
// preserving pre-T2 semantics.
|
|
260
|
+
// PLAIN stable score sort on purpose: both input lists are already deterministically
|
|
261
|
+
// ordered, and a base-vs-graph-hit tie keeps the BASE result first (the concat order).
|
|
256
262
|
for (const r of [...baseResults.slice(protectedCount), ...allHits].sort((a, b) => b.score - a.score)) {
|
|
257
263
|
const tokens = price(r);
|
|
258
264
|
if (usedTokens + tokens > budget)
|
package/dist/graph-stream.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (docs/plans/2026-06-02-l1-graph-rrf-stream.md).
|
|
2
|
+
* Graph-retrieval ranked-list stream for RRF fusion.
|
|
4
3
|
*
|
|
5
|
-
* READ-ONLY consumer of the
|
|
6
|
-
*
|
|
4
|
+
* READ-ONLY consumer of the entity/relation graph. Produces a ranked list of
|
|
5
|
+
* `entries[]` indices ordered by graph
|
|
7
6
|
* proximity to the strong lexical seeds, for use as a 3rd fusion input to `rrfFuse`
|
|
8
7
|
* beside BM25 + dense (src/search.ts hybridSearch, scoring:'rrf').
|
|
9
8
|
*
|
|
@@ -13,10 +12,10 @@
|
|
|
13
12
|
* themselves are never scored (they already rank via BM25/dense; scoring them would
|
|
14
13
|
* double-count and dilute the orthogonal graph signal).
|
|
15
14
|
*
|
|
16
|
-
* Reuses the
|
|
15
|
+
* Reuses the BFS traversal shape from graph-recall.ts (loadEntitiesByMemoryId
|
|
17
16
|
* seeds -> loadNeighborRelations BFS both directions, per-hop fanout cap, visited set
|
|
18
17
|
* -> loadEntitiesByIds to resolve reached -> memoryId). Expands across the local AND
|
|
19
|
-
* global stores. Pure reads (SELECTs only via graph.ts helpers), so the
|
|
18
|
+
* global stores. Pure reads (SELECTs only via graph.ts helpers), so the
|
|
20
19
|
* check-graph-writes lint permits this module living outside graph.ts.
|
|
21
20
|
*
|
|
22
21
|
* The graph stream's score scale (1/lexRank seed strength x decay^hops) only sets the
|
package/dist/graph-stream.js
CHANGED
|
@@ -33,39 +33,40 @@ export function selectGraphSeeds(bm25Ranked, cosineRanked, seedCount) {
|
|
|
33
33
|
.slice(0, seedCount)
|
|
34
34
|
.map(([index, best]) => ({ index, strength: 1 / (best + 1) }));
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// loaded by memory id, so its memoryId is non-null here; the guard keeps the widened
|
|
58
|
-
// (string | null) type honest (a null-memory entity is not a lexical seed).
|
|
59
|
-
const originStrength = new Map();
|
|
60
|
-
for (const e of seedEntities) {
|
|
61
|
-
if (e.memoryId === null)
|
|
36
|
+
// Pass 1: accumulate the STRONGEST reaching-seed strength per new neighbour across ALL
|
|
37
|
+
// relations at this depth BEFORE committing any to `visited`. Marking a node
|
|
38
|
+
// visited mid-loop would lock it to whichever relation SQLite returned first, so a later
|
|
39
|
+
// edge from a STRONGER lexical seed would be dropped and the neighbour mis-scored. A node
|
|
40
|
+
// already in `visited` was committed at an earlier (shorter) depth and keeps that score.
|
|
41
|
+
function bestStrengthAtDepth(rels, frontierSet, visited, frontierStrength, originStrength) {
|
|
42
|
+
const bestStrengthThisDepth = new Map();
|
|
43
|
+
for (const rel of rels) {
|
|
44
|
+
const fromIn = frontierSet.has(rel.fromEntityId);
|
|
45
|
+
const toIn = frontierSet.has(rel.toEntityId);
|
|
46
|
+
let neighborId;
|
|
47
|
+
let reacherId;
|
|
48
|
+
if (fromIn && !toIn) {
|
|
49
|
+
neighborId = rel.toEntityId;
|
|
50
|
+
reacherId = rel.fromEntityId;
|
|
51
|
+
}
|
|
52
|
+
else if (toIn && !fromIn) {
|
|
53
|
+
neighborId = rel.fromEntityId;
|
|
54
|
+
reacherId = rel.toEntityId;
|
|
55
|
+
}
|
|
56
|
+
else
|
|
62
57
|
continue;
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
if (visited.has(neighborId))
|
|
59
|
+
continue;
|
|
60
|
+
const seedStrength = frontierStrength.get(reacherId) ?? originStrength.get(reacherId) ?? 0;
|
|
61
|
+
bestStrengthThisDepth.set(neighborId, Math.max(bestStrengthThisDepth.get(neighborId) ?? 0, seedStrength));
|
|
65
62
|
}
|
|
66
|
-
|
|
63
|
+
return bestStrengthThisDepth;
|
|
64
|
+
}
|
|
65
|
+
/** BFS from the seed entities: entityId -> best originSeedStrength x decay^depth over every reached entity. */
|
|
66
|
+
function reachedEntityScores(root, tenantId, seedIds, originStrength, hops, decay, maxNeighbors) {
|
|
67
|
+
const visited = new Set(seedIds); // seeds never re-reached
|
|
67
68
|
const reachedScore = new Map(); // entityId -> best score
|
|
68
|
-
let frontier =
|
|
69
|
+
let frontier = seedIds;
|
|
69
70
|
let frontierStrength = new Map(originStrength); // entityId -> seed strength
|
|
70
71
|
for (let depth = 1; depth <= hops && frontier.length > 0; depth++) {
|
|
71
72
|
const frontierSet = new Set(frontier);
|
|
@@ -73,32 +74,7 @@ function accumulateForRoot(root, seeds, entries, memIdToIndex, graphScore, hops,
|
|
|
73
74
|
limit: Math.max(maxNeighbors, maxNeighbors * frontier.length),
|
|
74
75
|
});
|
|
75
76
|
const hopFactor = Math.pow(decay, depth);
|
|
76
|
-
|
|
77
|
-
// relations at this depth BEFORE committing any to `visited` (codex P2). Marking a node
|
|
78
|
-
// visited mid-loop would lock it to whichever relation SQLite returned first, so a later
|
|
79
|
-
// edge from a STRONGER lexical seed would be dropped and the neighbour mis-scored. A node
|
|
80
|
-
// already in `visited` was committed at an earlier (shorter) depth and keeps that score.
|
|
81
|
-
const bestStrengthThisDepth = new Map();
|
|
82
|
-
for (const rel of rels) {
|
|
83
|
-
const fromIn = frontierSet.has(rel.fromEntityId);
|
|
84
|
-
const toIn = frontierSet.has(rel.toEntityId);
|
|
85
|
-
let neighborId;
|
|
86
|
-
let reacherId;
|
|
87
|
-
if (fromIn && !toIn) {
|
|
88
|
-
neighborId = rel.toEntityId;
|
|
89
|
-
reacherId = rel.fromEntityId;
|
|
90
|
-
}
|
|
91
|
-
else if (toIn && !fromIn) {
|
|
92
|
-
neighborId = rel.fromEntityId;
|
|
93
|
-
reacherId = rel.toEntityId;
|
|
94
|
-
}
|
|
95
|
-
else
|
|
96
|
-
continue;
|
|
97
|
-
if (visited.has(neighborId))
|
|
98
|
-
continue;
|
|
99
|
-
const seedStrength = frontierStrength.get(reacherId) ?? originStrength.get(reacherId) ?? 0;
|
|
100
|
-
bestStrengthThisDepth.set(neighborId, Math.max(bestStrengthThisDepth.get(neighborId) ?? 0, seedStrength));
|
|
101
|
-
}
|
|
77
|
+
const bestStrengthThisDepth = bestStrengthAtDepth(rels, frontierSet, visited, frontierStrength, originStrength);
|
|
102
78
|
// Pass 2: commit strongest-first (then id asc — deterministic), so the per-hop fanout cap
|
|
103
79
|
// keeps the highest-scoring neighbours rather than whichever SQLite happened to return.
|
|
104
80
|
const nextFrontier = [];
|
|
@@ -119,6 +95,39 @@ function accumulateForRoot(root, seeds, entries, memIdToIndex, graphScore, hops,
|
|
|
119
95
|
frontier = nextFrontier;
|
|
120
96
|
frontierStrength = nextStrength;
|
|
121
97
|
}
|
|
98
|
+
return reachedScore;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Accumulate per-entryIndex graph-proximity scores from ONE store's graph into
|
|
102
|
+
* `graphScore`. Pure reads. `seeds` are the lexical seeds (index + strength); only the
|
|
103
|
+
* seeds whose entities live in THIS store are expanded. The origin seed strength is
|
|
104
|
+
* carried UNCHANGED along each BFS path; the per-hop decay is applied as decay^depth so
|
|
105
|
+
* a neighbour's score = originSeedStrength x decay^(graph distance).
|
|
106
|
+
*/
|
|
107
|
+
function accumulateForRoot(root, seeds, entries, memIdToIndex, graphScore, hops, decay, maxNeighbors, tenantId) {
|
|
108
|
+
if (seeds.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
// The strongest seed strength per source memory id (a memId could appear once, but
|
|
111
|
+
// guard against dup indices mapping to the same memId).
|
|
112
|
+
const strengthByMemId = new Map();
|
|
113
|
+
for (const s of seeds) {
|
|
114
|
+
const memId = entries[s.index].id;
|
|
115
|
+
strengthByMemId.set(memId, Math.max(strengthByMemId.get(memId) ?? 0, s.strength));
|
|
116
|
+
}
|
|
117
|
+
const seedEntities = loadEntitiesByMemoryId(root, tenantId, [...strengthByMemId.keys()]);
|
|
118
|
+
if (seedEntities.length === 0)
|
|
119
|
+
return;
|
|
120
|
+
// entityId -> origin seed strength (carried unchanged along the path). A seed entity is
|
|
121
|
+
// loaded by memory id, so its memoryId is non-null here; the guard keeps the widened
|
|
122
|
+
// (string | null) type honest (a null-memory entity is not a lexical seed).
|
|
123
|
+
const originStrength = new Map();
|
|
124
|
+
for (const e of seedEntities) {
|
|
125
|
+
if (e.memoryId === null)
|
|
126
|
+
continue;
|
|
127
|
+
const st = strengthByMemId.get(e.memoryId) ?? 0;
|
|
128
|
+
originStrength.set(e.id, Math.max(originStrength.get(e.id) ?? 0, st));
|
|
129
|
+
}
|
|
130
|
+
const reachedScore = reachedEntityScores(root, tenantId, seedEntities.map((e) => e.id), originStrength, hops, decay, maxNeighbors);
|
|
122
131
|
if (reachedScore.size === 0)
|
|
123
132
|
return;
|
|
124
133
|
// Reached entity ids -> source memory ids -> in-pool entry indices.
|
|
@@ -159,7 +168,7 @@ export function graphRankStream(entries, seeds, opts) {
|
|
|
159
168
|
for (const root of roots) {
|
|
160
169
|
accumulateForRoot(root, seeds, entries, memIdToIndex, graphScore, hops, decay, maxNeighbors, opts.tenantId);
|
|
161
170
|
}
|
|
162
|
-
// Seed-exclusion guard
|
|
171
|
+
// Seed-exclusion guard: graphScore is keyed by entryIndex
|
|
163
172
|
// GLOBALLY across roots, but each root's BFS visited-set is per-root, so a memory that
|
|
164
173
|
// is a seed in one store could be reached as a neighbour in the other store and pick up
|
|
165
174
|
// a score via max(). Drop every seed index so the "seeds are never scored by the graph
|