hippo-memory 1.62.0 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/agent-memories/apply.d.ts +2 -0
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +2 -1
- package/dist/agent-memories/claude-code.js +4 -4
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.js +37 -23
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +7 -12
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +4 -3
- package/dist/api/context-select.js +9 -11
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +3 -2
- package/dist/api/context.js +20 -20
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +22 -10
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +7 -18
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +3 -2
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +1 -1
- package/dist/capture/compact.js +9 -13
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +8 -24
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +10 -21
- package/dist/cli/explain.js +2 -1
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/playbooks.js +4 -9
- package/dist/cli/projects.js +3 -1
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +6 -18
- package/dist/cli/session-hooks.js +21 -40
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +2 -6
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +7 -12
- package/dist/cli/transfer.js +8 -13
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -0
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +6 -9
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +23 -73
- package/dist/db/continuity.js +2 -2
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +6 -6
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +119 -104
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +5 -6
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +1 -1
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +12 -14
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +3 -3
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +9 -13
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +17 -2
- package/dist/project-merge.js +109 -27
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +70 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +17 -17
- package/dist/shared.js +93 -97
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +8 -8
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +9 -31
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +31 -59
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/shared.js
CHANGED
|
@@ -19,8 +19,9 @@ import { passesScopeFilterForRecall, passesCliRecallScopeFilter } from './recall
|
|
|
19
19
|
import { search } from './search/bm25-search.js';
|
|
20
20
|
import { hybridSearch } from './search/hybrid.js';
|
|
21
21
|
import { fitBudget } from './search/finalize.js';
|
|
22
|
+
import { DEFAULT_LOCAL_BUMP, DEFAULT_RECALL_BUDGET } from './search/types.js';
|
|
22
23
|
import { evalNow } from './ablation.js';
|
|
23
|
-
import { deriveOriginProject, classifyOriginProject, resolveGlobalRootDir } from './project-identity.js';
|
|
24
|
+
import { deriveOriginProject, classifyOriginProject, resolveGlobalRootDir, resolveProjectIdentity } from './project-identity.js';
|
|
24
25
|
import { detectSecret } from './secret-detect.js';
|
|
25
26
|
import { isQuarantineScope } from './quarantine.js';
|
|
26
27
|
import { RejectedValueError } from './rejection.js';
|
|
@@ -62,12 +63,12 @@ export function promoteToGlobal(localRoot, id, opts) {
|
|
|
62
63
|
const entry = readEntry(localRoot, id, opts?.tenantId);
|
|
63
64
|
if (!entry)
|
|
64
65
|
throw new NotFoundError(`Memory not found: ${id}`);
|
|
65
|
-
//
|
|
66
|
+
// Same quarantine veto as shareMemory; a promoted copy would have no quarantine record to review.
|
|
66
67
|
if (isQuarantineScope(entry.scope)) {
|
|
67
68
|
throw new BadRequestError(`Refusing to promote ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
68
69
|
}
|
|
69
|
-
//
|
|
70
|
-
// exactly like shareMemory - same hard rule
|
|
70
|
+
// Secret producer veto: promote is a producer path to the global store
|
|
71
|
+
// exactly like shareMemory - same hard rule.
|
|
71
72
|
const promoteSecret = detectSecret(entry);
|
|
72
73
|
if (promoteSecret.flagged) {
|
|
73
74
|
throw new BadRequestError(`Refusing to promote ${id} to the global store: content matches secret material (${promoteSecret.reason}). ` +
|
|
@@ -95,7 +96,7 @@ export function promoteToGlobal(localRoot, id, opts) {
|
|
|
95
96
|
* Returns results sorted by adjusted score, within combined token budget.
|
|
96
97
|
*/
|
|
97
98
|
export function searchBoth(query, localRoot, globalRoot, options = {}) {
|
|
98
|
-
const { budget =
|
|
99
|
+
const { budget = DEFAULT_RECALL_BUDGET, now = evalNow(), minResults, tenantId } = options;
|
|
99
100
|
const effectiveMin = minResults ?? 1;
|
|
100
101
|
const localEntries = fs.existsSync(localRoot) ? loadSearchEntries(localRoot, query, undefined, tenantId) : [];
|
|
101
102
|
const globalEntries = fs.existsSync(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
|
|
@@ -105,14 +106,13 @@ export function searchBoth(query, localRoot, globalRoot, options = {}) {
|
|
|
105
106
|
const localResults = search(query, localEntries, { budget, now, minResults });
|
|
106
107
|
const globalResults = search(query, globalEntries, { budget, now, minResults });
|
|
107
108
|
// Tag global results. Local memories get a configurable priority bump.
|
|
108
|
-
const syncLocalBump = 1.2;
|
|
109
109
|
const tagged = [
|
|
110
110
|
...localResults.map((r) => ({
|
|
111
111
|
...r,
|
|
112
112
|
isGlobal: false,
|
|
113
|
-
score: r.score *
|
|
113
|
+
score: r.score * DEFAULT_LOCAL_BUMP,
|
|
114
114
|
breakdown: r.breakdown
|
|
115
|
-
? { ...r.breakdown, sourceBump:
|
|
115
|
+
? { ...r.breakdown, sourceBump: DEFAULT_LOCAL_BUMP, final: r.breakdown.final * DEFAULT_LOCAL_BUMP }
|
|
116
116
|
: undefined,
|
|
117
117
|
})),
|
|
118
118
|
...globalResults.map((r) => ({ ...r, isGlobal: true })),
|
|
@@ -126,11 +126,8 @@ export function searchBoth(query, localRoot, globalRoot, options = {}) {
|
|
|
126
126
|
seen.add(key);
|
|
127
127
|
return true;
|
|
128
128
|
});
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// search), stability inherits that, and an exact post-bump tie keeps the
|
|
132
|
-
// LOCAL result ahead of the global one (the concat order), preserving the
|
|
133
|
-
// pre-T2 semantics.
|
|
129
|
+
// PLAIN stable score sort on purpose: both inputs are deterministically ordered,
|
|
130
|
+
// and an exact tie keeps the LOCAL result ahead of the global one (concat order).
|
|
134
131
|
deduped.sort((a, b) => b.score - a.score);
|
|
135
132
|
// Apply combined token budget (guarantee at least minResults items)
|
|
136
133
|
const results = [];
|
|
@@ -151,13 +148,10 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
|
|
|
151
148
|
const { includeSuperseded, asOf, tenantId, entryFilter, recallScope } = options;
|
|
152
149
|
// When an admission filter is active, lift the per-store candidate cap
|
|
153
150
|
// (default 200): excluded rows matching the query could otherwise fill the
|
|
154
|
-
// window before any admitted row is even loaded
|
|
155
|
-
//
|
|
156
|
-
// crowding is a non-issue on real stores, bounded so a common query term
|
|
157
|
-
// on a 100k-row store cannot stall an interactive call by ranking every
|
|
158
|
-
// match (post-merge adversarial review, 2026-07-02).
|
|
151
|
+
// window before any admitted row is even loaded. 5000 is bounded so a common
|
|
152
|
+
// term on a large store cannot stall an interactive call by ranking every match.
|
|
159
153
|
const searchWindow = entryFilter ? 5000 : undefined;
|
|
160
|
-
//
|
|
154
|
+
// Recall mode: push the scope predicate into SQL exactly like
|
|
161
155
|
// api.recall (loadRecallSearchEntries), so quarantine/private rows never
|
|
162
156
|
// enter the candidate set, never shadow admitted duplicates in the dedupe
|
|
163
157
|
// pass, and never consume budget. The JS post-filter below is the
|
|
@@ -187,7 +181,7 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
|
|
|
187
181
|
}
|
|
188
182
|
/** Hybrid ranking of rows already loaded from each store: the local bump, one copy per text, then the shared budget. */
|
|
189
183
|
export async function rankBothStores(query, roots, entries, vectorCandidates, options = {}) {
|
|
190
|
-
const { budget =
|
|
184
|
+
const { budget = DEFAULT_RECALL_BUDGET, now = evalNow(), embeddingWeight, explain, mmr, mmrLambda, localBump = DEFAULT_LOCAL_BUMP, minResults, cost, scope, includeSuperseded, asOf, summaryDeboost, summaryFreshness } = options;
|
|
191
185
|
if (entries.local.length === 0 && entries.global.length === 0)
|
|
192
186
|
return [];
|
|
193
187
|
const shared = { budget, now, embeddingWeight, explain, mmr, mmrLambda, minResults, cost, scope, includeSuperseded, asOf, summaryDeboost, summaryFreshness, vectorCandidates };
|
|
@@ -214,7 +208,7 @@ export async function rankBothStores(query, roots, entries, vectorCandidates, op
|
|
|
214
208
|
seen.add(key);
|
|
215
209
|
return true;
|
|
216
210
|
});
|
|
217
|
-
//
|
|
211
|
+
// PLAIN stable score sort on purpose -- see searchBoth above;
|
|
218
212
|
// same rationale (deterministic inputs + stability; local-first on ties).
|
|
219
213
|
deduped.sort((a, b) => b.score - a.score);
|
|
220
214
|
return fitBudget(deduped, budget, minResults ?? 1, cost);
|
|
@@ -286,7 +280,7 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
286
280
|
const entry = readEntry(localRoot, id, options.tenantId);
|
|
287
281
|
if (!entry)
|
|
288
282
|
throw new NotFoundError(`Memory not found: ${id}`);
|
|
289
|
-
//
|
|
283
|
+
// Secret producer veto: secrets never go to the global store, not even
|
|
290
284
|
// with --force. Explicit and loud - a silent null would read as "low
|
|
291
285
|
// transfer score" and invite retries.
|
|
292
286
|
const secret = detectSecret(entry);
|
|
@@ -294,7 +288,7 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
294
288
|
throw new BadRequestError(`Refusing to share ${id} to the global store: content matches secret material (${secret.reason}). ` +
|
|
295
289
|
`Secrets stay in their owning project's store.`);
|
|
296
290
|
}
|
|
297
|
-
//
|
|
291
|
+
// A quarantined row is unreviewed input, not a lesson; sharing it would spread poison globally.
|
|
298
292
|
if (isQuarantineScope(entry.scope)) {
|
|
299
293
|
throw new BadRequestError(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
300
294
|
}
|
|
@@ -328,7 +322,7 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
328
322
|
* List all projects that have contributed memories to the global store.
|
|
329
323
|
* Parses the source field for 'shared:<project>:' or 'promoted:<path>' patterns.
|
|
330
324
|
*
|
|
331
|
-
*
|
|
325
|
+
* `tenantId` is optional. When provided, the global entries
|
|
332
326
|
* are filtered to that tenant before aggregation — matches every other
|
|
333
327
|
* read path's default-safe behaviour. When undefined, host-wide (back-compat
|
|
334
328
|
* for legacy callers like CLI standalone + dashboard internal use). Operators
|
|
@@ -339,7 +333,7 @@ export function listPeers(globalRoot, tenantId) {
|
|
|
339
333
|
const root = globalRoot ?? getGlobalRoot();
|
|
340
334
|
if (!fs.existsSync(root))
|
|
341
335
|
return [];
|
|
342
|
-
//
|
|
336
|
+
// Tenant-scoped by default when tenantId provided. Host-wide when
|
|
343
337
|
// undefined (preserves back-compat).
|
|
344
338
|
const tallies = tallySources(root, tenantId).sort((a, b) => (a.first < b.first ? -1 : a.first > b.first ? 1 : 0));
|
|
345
339
|
const peerMap = new Map();
|
|
@@ -370,25 +364,85 @@ export function listPeers(globalRoot, tenantId) {
|
|
|
370
364
|
.map(([project, data]) => ({ project, ...data }))
|
|
371
365
|
.sort((a, b) => b.count - a.count);
|
|
372
366
|
}
|
|
367
|
+
function isAutoShareCandidate(entry, globalContentSet, minScore, stats) {
|
|
368
|
+
// shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
|
|
369
|
+
if (isQuarantineScope(entry.scope ?? null))
|
|
370
|
+
return false;
|
|
371
|
+
// Before the score: these rows describe one project only, and a git seed's 'error' tag clears the bar.
|
|
372
|
+
if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t)) || entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX)) {
|
|
373
|
+
if (stats)
|
|
374
|
+
stats.neverAutoShareSkipped = (stats.neverAutoShareSkipped ?? 0) + 1;
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const score = transferScore(entry);
|
|
378
|
+
if (score < minScore)
|
|
379
|
+
return false;
|
|
380
|
+
// Skip if already shared (same text apart from spacing)
|
|
381
|
+
if (globalContentSet.has(duplicateKey(entry.content)))
|
|
382
|
+
return false;
|
|
383
|
+
// Secret producer veto: secret rows never auto-share, regardless of
|
|
384
|
+
// transfer score. (shareMemory would throw; filtering here keeps the
|
|
385
|
+
// sleep pipeline fail-safe.) Checked LAST so the stats counter
|
|
386
|
+
// only counts rows the veto actually withheld — a row failing the score
|
|
387
|
+
// or dedupe gates was never going to share, secret or not.
|
|
388
|
+
if (detectSecret(entry).flagged) {
|
|
389
|
+
if (stats)
|
|
390
|
+
stats.secretSkipped++;
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
// Rejection containment (sync/promote/share copy paths must not let ONE rejected
|
|
396
|
+
// candidate kill the batch): shareMemory -> writeEntry hits the LIVE guard
|
|
397
|
+
// against the GLOBAL store's tombstones. A matching candidate throws
|
|
398
|
+
// RejectedValueError, which (uncaught) would abort this whole loop and,
|
|
399
|
+
// via api.ts's sleep pipeline, the entire autoShare sleep phase. Mirrors
|
|
400
|
+
// syncGlobalToLocal's per-item catch just above in this file. writeEntry's
|
|
401
|
+
// own catch already writes the reject_refusal audit before rethrowing,
|
|
402
|
+
// so do not double-audit here, just count and continue.
|
|
403
|
+
function shareCandidates(localRoot, candidates, stats) {
|
|
404
|
+
const shared = [];
|
|
405
|
+
let rejectedSkipped = 0;
|
|
406
|
+
for (const entry of candidates) {
|
|
407
|
+
try {
|
|
408
|
+
// skipEmbed: batching invariant, this is a batch producer, so it embeds
|
|
409
|
+
// once via embedAll() below rather than once per row inside shareMemory.
|
|
410
|
+
const result = shareMemory(localRoot, entry.id, { force: true, skipEmbed: true });
|
|
411
|
+
if (result)
|
|
412
|
+
shared.push(result);
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
if (err instanceof RejectedValueError) {
|
|
416
|
+
rejectedSkipped++;
|
|
417
|
+
if (stats)
|
|
418
|
+
stats.rejectedSkipped = (stats.rejectedSkipped ?? 0) + 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (rejectedSkipped > 0) {
|
|
425
|
+
log.warn(`autoShare: skipped ${rejectedSkipped} candidate(s) refused by the global store's rejection tombstone`);
|
|
426
|
+
}
|
|
427
|
+
return shared;
|
|
428
|
+
}
|
|
373
429
|
/**
|
|
374
430
|
* Auto-share: local memories with high transfer scores, not already global, no NEVER_AUTO_SHARE_TAGS tag.
|
|
375
431
|
* Returns the list of shared entries.
|
|
376
432
|
*
|
|
377
|
-
*
|
|
433
|
+
* `options.tenantId` is opt-in. When provided, the LOCAL-entries read is
|
|
378
434
|
* scoped to that tenant. When undefined, the local read is host-wide (current
|
|
379
435
|
* behaviour). The GLOBAL-entries read is always unioned — the global root IS
|
|
380
|
-
* the cross-tenant aggregate by design.
|
|
381
|
-
*
|
|
382
|
-
* passes options without tenantId because `sleep` is host-wide by intent;
|
|
383
|
-
* see `src/api.ts:2073-2077` for the cross-tenant dedup rationale.
|
|
436
|
+
* the cross-tenant aggregate by design. `api.sleep` passes no tenantId
|
|
437
|
+
* because `sleep` is host-wide by intent.
|
|
384
438
|
*
|
|
385
|
-
*
|
|
439
|
+
* `options.stats` is an opt-in out-param. When provided,
|
|
386
440
|
* `stats.secretSkipped` is incremented once per row that passed every OTHER
|
|
387
441
|
* admission gate (transfer score, not-already-global) and was withheld SOLELY
|
|
388
442
|
* by the secret veto — i.e. it counts shares actually prevented, not secret
|
|
389
443
|
* rows merely present. Filled identically under `dryRun`.
|
|
390
444
|
*
|
|
391
|
-
*
|
|
445
|
+
* `stats.rejectedSkipped` (optional) is incremented once per candidate
|
|
392
446
|
* refused by the GLOBAL store's rejection tombstone (RejectedValueError from
|
|
393
447
|
* shareMemory -> writeEntry). Unlike secretSkipped, this can only be
|
|
394
448
|
* detected by attempting the write — `dryRun` returns candidates before the
|
|
@@ -401,73 +455,15 @@ export function autoShare(localRoot, options = {}) {
|
|
|
401
455
|
const localEntries = loadAllEntries(localRoot, options.tenantId);
|
|
402
456
|
initGlobal();
|
|
403
457
|
const globalRoot = getGlobalRoot();
|
|
404
|
-
//
|
|
458
|
+
// Host-wide read. The global store IS the union across all tenants;
|
|
405
459
|
// per-tenant filtering on the global root would defeat the purpose.
|
|
406
460
|
const globalEntries = loadAllEntries(globalRoot);
|
|
407
461
|
// Build set of global content hashes to avoid duplicates
|
|
408
462
|
const globalContentSet = storedTextKeys(globalEntries);
|
|
409
|
-
const candidates = localEntries.filter((entry) =>
|
|
410
|
-
// CD5: shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
|
|
411
|
-
if (isQuarantineScope(entry.scope ?? null))
|
|
412
|
-
return false;
|
|
413
|
-
// Before the score: these rows describe one project only, and a git seed's 'error' tag clears the bar.
|
|
414
|
-
if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t)) || entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX)) {
|
|
415
|
-
if (options.stats)
|
|
416
|
-
options.stats.neverAutoShareSkipped = (options.stats.neverAutoShareSkipped ?? 0) + 1;
|
|
417
|
-
return false;
|
|
418
|
-
}
|
|
419
|
-
const score = transferScore(entry);
|
|
420
|
-
if (score < minScore)
|
|
421
|
-
return false;
|
|
422
|
-
// Skip if already shared (same text apart from spacing)
|
|
423
|
-
if (globalContentSet.has(duplicateKey(entry.content)))
|
|
424
|
-
return false;
|
|
425
|
-
// v39 S4 producer veto: secret rows never auto-share, regardless of
|
|
426
|
-
// transfer score. (shareMemory would throw; filtering here keeps the
|
|
427
|
-
// sleep pipeline fail-safe.) Checked LAST (v1.25.0) so the stats counter
|
|
428
|
-
// only counts rows the veto actually withheld — a row failing the score
|
|
429
|
-
// or dedupe gates was never going to share, secret or not.
|
|
430
|
-
if (detectSecret(entry).flagged) {
|
|
431
|
-
if (options.stats)
|
|
432
|
-
options.stats.secretSkipped++;
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
return true;
|
|
436
|
-
});
|
|
463
|
+
const candidates = localEntries.filter((entry) => isAutoShareCandidate(entry, globalContentSet, minScore, options.stats));
|
|
437
464
|
if (dryRun)
|
|
438
465
|
return candidates;
|
|
439
|
-
const shared =
|
|
440
|
-
// AT1 containment (docs/plans/2026-08-15-at1-rejected-value-tombstone.md
|
|
441
|
-
// plan §3 — sync/promote/share copy paths must not let ONE rejected
|
|
442
|
-
// candidate kill the batch): shareMemory -> writeEntry hits the LIVE guard
|
|
443
|
-
// against the GLOBAL store's tombstones. A matching candidate throws
|
|
444
|
-
// RejectedValueError, which (uncaught) would abort this whole loop and,
|
|
445
|
-
// via api.ts's sleep pipeline, the entire autoShare sleep phase. Mirrors
|
|
446
|
-
// syncGlobalToLocal's per-item catch just above in this file. writeEntry's
|
|
447
|
-
// own catch already writes the reject_refusal audit before rethrowing
|
|
448
|
-
// (plan §3) — do not double-audit here, just count and continue.
|
|
449
|
-
let rejectedSkipped = 0;
|
|
450
|
-
for (const entry of candidates) {
|
|
451
|
-
try {
|
|
452
|
-
// skipEmbed: batching invariant, this is a batch producer, so it embeds
|
|
453
|
-
// once via embedAll() below rather than once per row inside shareMemory.
|
|
454
|
-
const result = shareMemory(localRoot, entry.id, { force: true, skipEmbed: true });
|
|
455
|
-
if (result)
|
|
456
|
-
shared.push(result);
|
|
457
|
-
}
|
|
458
|
-
catch (err) {
|
|
459
|
-
if (err instanceof RejectedValueError) {
|
|
460
|
-
rejectedSkipped++;
|
|
461
|
-
if (options.stats)
|
|
462
|
-
options.stats.rejectedSkipped = (options.stats.rejectedSkipped ?? 0) + 1;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
throw err;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (rejectedSkipped > 0) {
|
|
469
|
-
log.warn(`autoShare: skipped ${rejectedSkipped} candidate(s) refused by the global store's rejection tombstone`);
|
|
470
|
-
}
|
|
466
|
+
const shared = shareCandidates(localRoot, candidates, options.stats);
|
|
471
467
|
if (shared.length > 0) {
|
|
472
468
|
void embedAll(globalRoot).catch((err) => logEmbedAllFailure('autoShare', err));
|
|
473
469
|
}
|
|
@@ -481,18 +477,18 @@ export function autoShare(localRoot, options = {}) {
|
|
|
481
477
|
export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
482
478
|
if (!fs.existsSync(globalRoot))
|
|
483
479
|
return 0;
|
|
484
|
-
//
|
|
480
|
+
// Host-wide read. syncGlobalToLocal copies the global union into a
|
|
485
481
|
// tenant-scoped local store; writeEntry on each row carries the tenant if
|
|
486
482
|
// the local-root context provides one.
|
|
487
483
|
const globalEntries = loadAllEntries(globalRoot);
|
|
488
484
|
const localIndex = loadIndex(localRoot);
|
|
489
485
|
const textKey = (e) => `${e.tenantId}\n${e.content}`;
|
|
490
486
|
const localText = new Set(loadAllEntries(localRoot).map(textKey));
|
|
491
|
-
//
|
|
487
|
+
// Syncing down must not re-import what ambient context
|
|
492
488
|
// excludes - other-project rows are skipped by default and secret rows
|
|
493
489
|
// are never copied. origin_project is preserved on the copy (writeEntry
|
|
494
490
|
// only stamps when the field is missing).
|
|
495
|
-
const
|
|
491
|
+
const currentProject = resolveProjectIdentity(path.dirname(path.resolve(localRoot)));
|
|
496
492
|
let count = 0;
|
|
497
493
|
// A locally rejected value must not come back through sync down: caught per item, printed as one line.
|
|
498
494
|
let rejected = 0;
|
|
@@ -508,7 +504,7 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
|
508
504
|
if (detectSecret(entry).flagged)
|
|
509
505
|
continue;
|
|
510
506
|
if (!opts.includeCrossProject &&
|
|
511
|
-
classifyOriginProject(entry.origin_project,
|
|
507
|
+
classifyOriginProject(entry.origin_project, currentProject) === 'cross-project')
|
|
512
508
|
continue;
|
|
513
509
|
try {
|
|
514
510
|
writeEntry(localRoot, entry);
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Skills as a first-class, versioned object.
|
|
3
3
|
*
|
|
4
4
|
* A `skill` is a reusable, agent-followable capability: an `instructions` body
|
|
5
5
|
* plus an optional `trigger` ("when to apply"), evolving via the supersede delta
|
|
@@ -29,8 +29,8 @@ export declare const VALID_SKILL_STATES: ReadonlySet<SkillStatus>;
|
|
|
29
29
|
export declare const MAX_SKILL_NAME_LEN = 256;
|
|
30
30
|
export declare const MAX_SKILL_INSTRUCTIONS_LEN = 8192;
|
|
31
31
|
export declare const MAX_SKILL_TRIGGER_LEN = 1024;
|
|
32
|
-
/** Aggregate bound on a single export render
|
|
33
|
-
*
|
|
32
|
+
/** Aggregate bound on a single export render so the export body is never unbounded.
|
|
33
|
+
* Realistic active-skill counts are tens; 1000 is a generous bound. */
|
|
34
34
|
export declare const MAX_EXPORT_SKILLS = 1000;
|
|
35
35
|
export interface Skill {
|
|
36
36
|
id: number;
|
package/dist/skills.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Skills as a first-class, versioned object.
|
|
3
3
|
*
|
|
4
4
|
* A `skill` is a reusable, agent-followable capability: an `instructions` body
|
|
5
5
|
* plus an optional `trigger` ("when to apply"), evolving via the supersede delta
|
|
@@ -39,13 +39,13 @@ export const VALID_SKILL_STATES = new Set([
|
|
|
39
39
|
export const MAX_SKILL_NAME_LEN = 256;
|
|
40
40
|
export const MAX_SKILL_INSTRUCTIONS_LEN = 8192;
|
|
41
41
|
export const MAX_SKILL_TRIGGER_LEN = 1024;
|
|
42
|
-
/** Aggregate bound on a single export render
|
|
43
|
-
*
|
|
42
|
+
/** Aggregate bound on a single export render so the export body is never unbounded.
|
|
43
|
+
* Realistic active-skill counts are tens; 1000 is a generous bound. */
|
|
44
44
|
export const MAX_EXPORT_SKILLS = 1000;
|
|
45
45
|
/**
|
|
46
46
|
* Validate + normalise skill fields. skill_name is trimmed and MUST be a single
|
|
47
|
-
* line (no newlines) so it cannot break the H2 header in the export render
|
|
48
|
-
*
|
|
47
|
+
* line (no newlines) so it cannot break the H2 header in the export render.
|
|
48
|
+
* instructions are kept verbatim (operator content) but capped.
|
|
49
49
|
* Returns the normalised name + trigger (null when absent/empty).
|
|
50
50
|
*/
|
|
51
51
|
function validateSkillFields(skillName, instructions, trigger) {
|
|
@@ -69,8 +69,8 @@ function validateSkillFields(skillName, instructions, trigger) {
|
|
|
69
69
|
throw new BadRequestError(`saveSkill: trigger exceeds the ${MAX_SKILL_TRIGGER_LEN}-char cap`);
|
|
70
70
|
}
|
|
71
71
|
// Single-line, like skill_name: a trigger is a short "when to apply" phrase,
|
|
72
|
-
// and a newline would let it forge a heading inside the export **When:** line
|
|
73
|
-
//
|
|
72
|
+
// and a newline would let it forge a heading inside the export **When:** line.
|
|
73
|
+
// Reject rather than emit a multi-line trigger.
|
|
74
74
|
if (/[\r\n]/.test(trigger)) {
|
|
75
75
|
throw new BadRequestError('saveSkill: trigger must be a single line (no newlines)');
|
|
76
76
|
}
|
|
@@ -114,7 +114,7 @@ function buildSkillContent(skillName, instructions, trigger) {
|
|
|
114
114
|
// Preflight the supersede target BEFORE inserting the new row (so the new
|
|
115
115
|
// autoincrement id can never be its own supersede target); read the
|
|
116
116
|
// predecessor version in the same SELECT for server-derived versioning.
|
|
117
|
-
// Mirrors saveProcess / savePolicy
|
|
117
|
+
// Mirrors saveProcess / savePolicy.
|
|
118
118
|
function preflightSkillSupersede(db, tenantId, supersedesId) {
|
|
119
119
|
// SAFETY: row shape matches the `status, version` columns named in the SELECT below.
|
|
120
120
|
const pred = db.prepare(`SELECT status, version FROM skills WHERE id = ? AND tenant_id = ?`).get(supersedesId, tenantId);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { openHippoDb } from '../db.js';
|
|
2
2
|
import { type AuditOp } from '../audit.js';
|
|
3
3
|
import { RejectedValueError } from '../rejection.js';
|
|
4
|
-
import type { JsonValue } from '
|
|
4
|
+
import type { JsonValue } from '../json.js';
|
|
5
5
|
/**
|
|
6
6
|
* Emit an audit event for a mutation against `db`. Wrapped so a broken audit
|
|
7
7
|
* log can never crash the surrounding mutation — the SQLite store is still the
|
|
@@ -9,7 +9,7 @@ import type { JsonValue } from './rows.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function audit(db: ReturnType<typeof openHippoDb>, op: AuditOp, targetId?: string, metadata?: Record<string, JsonValue>, actor?: string, tenantId?: string): void;
|
|
11
11
|
/**
|
|
12
|
-
* Refusal audit for the
|
|
12
|
+
* Refusal audit for the rejected-value guard. Written by the
|
|
13
13
|
* transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
|
|
14
14
|
* there, so this lands in a fresh implicit transaction) and api.supersede's
|
|
15
15
|
* catch (after its own ROLLBACK) — never inside a scope the caller's own
|
|
@@ -21,7 +21,7 @@ export function audit(db, op, targetId, metadata, actor = 'cli', tenantId) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Refusal audit for the
|
|
24
|
+
* Refusal audit for the rejected-value guard. Written by the
|
|
25
25
|
* transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
|
|
26
26
|
* there, so this lands in a fresh implicit transaction) and api.supersede's
|
|
27
27
|
* catch (after its own ROLLBACK) — never inside a scope the caller's own
|
|
@@ -16,8 +16,8 @@ export declare function loadAmbientCandidates(hippoRoot: string, tenantId: strin
|
|
|
16
16
|
export interface ContextCandidateFilter {
|
|
17
17
|
/** Envelope scope asked for by name; absent applies recall's default deny. */
|
|
18
18
|
exactScope?: string;
|
|
19
|
-
/** Rows of
|
|
20
|
-
project?: string;
|
|
19
|
+
/** Rows carrying one of these project names, and user-global rows, pass; absent admits every origin. */
|
|
20
|
+
project?: readonly string[];
|
|
21
21
|
/** Most rows returned; past it, the rows decay has worn least win. */
|
|
22
22
|
cap: number;
|
|
23
23
|
now: Date;
|
package/dist/store/candidates.js
CHANGED
|
@@ -2,6 +2,7 @@ import { closeHippoDb } from '../db.js';
|
|
|
2
2
|
import { RECALL_DEFAULT_DENY_SCOPES } from '../recall-scope.js';
|
|
3
3
|
import { MEMORY_SELECT_COLUMNS, rowToEntry, parseJsonArray } from './rows.js';
|
|
4
4
|
import { openStore } from './open.js';
|
|
5
|
+
import { originInSql } from '../project-identity.js';
|
|
5
6
|
import { pickRarestFtsQuery, loadRecallSearchEntriesFromDb } from './search-rows.js';
|
|
6
7
|
const AMBIENT_SCOPED = 'superseded_by IS NULL AND tenant_id = ?';
|
|
7
8
|
/** Exported so the plan test runs the exact SQL; idx_memories_pinned (db.ts v51) serves it. */
|
|
@@ -9,7 +10,7 @@ export const AMBIENT_PINNED_WHERE = `pinned = 1 AND ${AMBIENT_SCOPED} ORDER BY c
|
|
|
9
10
|
/** Exported so the plan test runs the exact SQL; idx_memories_created_drift (db.ts v51) serves it. */
|
|
10
11
|
export const AMBIENT_DRIFT_SQL = `SELECT 1 FROM memories WHERE ${AMBIENT_SCOPED} AND (length(created) <> 24 OR created NOT LIKE '%Z') LIMIT 1`;
|
|
11
12
|
// The pins plus the `recentNeeded` newest rows that pass `admit`, for ambient
|
|
12
|
-
// injection. One connection; `recall` piggybacks the
|
|
13
|
+
// injection. One connection; `recall` piggybacks the prompt-recall FTS query on it too.
|
|
13
14
|
export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit, recall) {
|
|
14
15
|
// A SQL LIMIT takes an integer; the Array.slice this replaced truncated one,
|
|
15
16
|
// and include_recent is any non-negative finite number at the HTTP edge.
|
|
@@ -78,8 +79,8 @@ export function loadContextCandidates(hippoRoot, tenantId, filter) {
|
|
|
78
79
|
params.push(...RECALL_DEFAULT_DENY_SCOPES);
|
|
79
80
|
}
|
|
80
81
|
if (filter.project !== undefined) {
|
|
81
|
-
where.push(`(origin_project = '' OR
|
|
82
|
-
params.push(filter.project);
|
|
82
|
+
where.push(`(origin_project = '' OR ${originInSql(filter.project)})`);
|
|
83
|
+
params.push(...filter.project);
|
|
83
84
|
}
|
|
84
85
|
const db = openStore(hippoRoot);
|
|
85
86
|
try {
|
package/dist/store/conflicts.js
CHANGED
|
@@ -3,7 +3,8 @@ import { rejectionDigest, insertRejectedValue, normalizeValueForRejection } from
|
|
|
3
3
|
import { archiveRawMemory } from '../raw-archive.js';
|
|
4
4
|
import { rowToMemoryConflict } from './rows.js';
|
|
5
5
|
import { audit } from './audit-event.js';
|
|
6
|
-
import {
|
|
6
|
+
import { syncChangedMirrors, purgeMirrorBestEffort } from './mirrors.js';
|
|
7
|
+
import { selectEntriesByIds } from './entry-reads.js';
|
|
7
8
|
import { openStore } from './open.js';
|
|
8
9
|
import { deleteEntryCore } from './delete-and-batch.js';
|
|
9
10
|
function canonicalConflictPair(aId, bId) {
|
|
@@ -81,9 +82,9 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
|
|
|
81
82
|
}));
|
|
82
83
|
resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt);
|
|
83
84
|
upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt);
|
|
84
|
-
rebuildConflictsWithJson(db, sameTenant);
|
|
85
|
+
const changedIds = rebuildConflictsWithJson(db, sameTenant);
|
|
85
86
|
db.exec('COMMIT');
|
|
86
|
-
|
|
87
|
+
syncChangedMirrors(hippoRoot, db, [...selectEntriesByIds(db, changedIds).values()]);
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
90
|
try {
|
|
@@ -123,36 +124,38 @@ function resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt
|
|
|
123
124
|
FROM memory_conflicts
|
|
124
125
|
WHERE status = 'open'
|
|
125
126
|
`).all();
|
|
127
|
+
const resolve = db.prepare(`UPDATE memory_conflicts SET status = 'resolved', updated_at = ? WHERE id = ?`);
|
|
126
128
|
for (const row of openRows) {
|
|
127
129
|
const key = `${row.memory_a_id}::${row.memory_b_id}`;
|
|
128
130
|
const stale = !detectedKeys.has(key);
|
|
129
131
|
// v1.11.0 residue: auto-resolve any open cross-tenant row. The insert
|
|
130
|
-
// loop
|
|
132
|
+
// loop in upsertDetectedConflicts and the refMap rebuild skip
|
|
131
133
|
// cross-tenant pairs, but the resolve-stale loop previously left
|
|
132
134
|
// re-detected cross-tenant rows lingering status='open'. The
|
|
133
135
|
// sameTenant() helper is already built one block up; no extra query.
|
|
134
136
|
const crossTenant = !sameTenant(row.memory_a_id, row.memory_b_id);
|
|
135
|
-
if (stale || crossTenant)
|
|
136
|
-
|
|
137
|
-
}
|
|
137
|
+
if (stale || crossTenant)
|
|
138
|
+
resolve.run(detectedAt, row.id);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
function upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt) {
|
|
142
|
+
const upsert = db.prepare(`
|
|
143
|
+
INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
|
|
144
|
+
VALUES (?, ?, ?, ?, 'open', ?, ?)
|
|
145
|
+
ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
|
|
146
|
+
reason = excluded.reason,
|
|
147
|
+
score = excluded.score,
|
|
148
|
+
status = 'open',
|
|
149
|
+
updated_at = excluded.updated_at
|
|
150
|
+
`);
|
|
141
151
|
for (const conflict of canonicalDetected) {
|
|
142
152
|
// Skip cross-tenant pairs — never persist a conflict spanning tenants.
|
|
143
153
|
if (!sameTenant(conflict.memory_a_id, conflict.memory_b_id))
|
|
144
154
|
continue;
|
|
145
|
-
|
|
146
|
-
INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
|
|
147
|
-
VALUES (?, ?, ?, ?, 'open', ?, ?)
|
|
148
|
-
ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
|
|
149
|
-
reason = excluded.reason,
|
|
150
|
-
score = excluded.score,
|
|
151
|
-
status = 'open',
|
|
152
|
-
updated_at = excluded.updated_at
|
|
153
|
-
`).run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
|
|
155
|
+
upsert.run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
158
|
+
/** Rewrites only the rows whose conflicts_with_json changes, and returns their ids. */
|
|
156
159
|
function rebuildConflictsWithJson(db, sameTenant) {
|
|
157
160
|
// SAFETY: openConflicts' shape matches the two columns named above.
|
|
158
161
|
const openConflicts = db.prepare(`
|
|
@@ -173,13 +176,18 @@ function rebuildConflictsWithJson(db, sameTenant) {
|
|
|
173
176
|
refMap.get(row.memory_a_id).add(row.memory_b_id);
|
|
174
177
|
refMap.get(row.memory_b_id).add(row.memory_a_id);
|
|
175
178
|
}
|
|
176
|
-
// SAFETY: memoryRows' shape matches the
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
+
// SAFETY: memoryRows' shape matches the two columns selected below.
|
|
180
|
+
const memoryRows = db.prepare(`SELECT id, conflicts_with_json FROM memories`).all();
|
|
181
|
+
const update = db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?`);
|
|
182
|
+
const changedIds = [];
|
|
179
183
|
for (const memory of memoryRows) {
|
|
180
|
-
const
|
|
181
|
-
|
|
184
|
+
const refsJson = JSON.stringify(Array.from(refMap.get(memory.id) ?? []).sort());
|
|
185
|
+
if (memory.conflicts_with_json === refsJson)
|
|
186
|
+
continue;
|
|
187
|
+
update.run(refsJson, memory.id);
|
|
188
|
+
changedIds.push(memory.id);
|
|
182
189
|
}
|
|
190
|
+
return changedIds;
|
|
183
191
|
}
|
|
184
192
|
/**
|
|
185
193
|
* Resolve a conflict by keeping one memory and weakening the other.
|
|
@@ -230,7 +238,7 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
230
238
|
stripConflictRefs(db, target, removal.loserRemoved);
|
|
231
239
|
auditConflictResolve(db, target, removal, tenantId);
|
|
232
240
|
db.exec('COMMIT');
|
|
233
|
-
|
|
241
|
+
syncChangedMirrors(hippoRoot, db, [...selectEntriesByIds(db, [keepId, loserId]).values()]);
|
|
234
242
|
if (removal.loserRemoved)
|
|
235
243
|
purgeRemovedLoserMirrors(hippoRoot, db, loserId, removal);
|
|
236
244
|
return { conflict: { ...conflict, status: 'resolved' }, loserId };
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import { type MemoryEntry } from '../memory.js';
|
|
2
|
-
import { openHippoDb } from '../db.js';
|
|
2
|
+
import { openHippoDb, type DatabaseSyncLike } from '../db.js';
|
|
3
3
|
import { type DormantMove } from '../dormant.js';
|
|
4
4
|
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
|
|
5
5
|
export declare const MEMORY_BACKED_TABLES: readonly ["predictions", "decisions", "incidents", "processes", "policies", "skills", "project_briefs", "customer_notes"];
|
|
6
6
|
/** Ids of memories that back a first-class object, for passes that plan deletes before making them. A table missing from an older schema is skipped. */
|
|
7
7
|
export declare function memoriesBackingObjects(hippoRoot: string): Set<string>;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* never compose inside a caller's transaction (unlike writeEntry/
|
|
12
|
-
* writeEntryDbOnly, which already split this way). Split identically: row-
|
|
13
|
-
* meta SELECT, `DELETE FROM memories`, FTS delete, `forget` audit, DAG
|
|
14
|
-
* dirty-mark. NO filesystem I/O — the caller's own transaction may still be
|
|
15
|
-
* rolled back, and mirror writes must only happen post-commit.
|
|
9
|
+
* db-scoped delete core, so a delete can compose inside a caller's transaction.
|
|
10
|
+
* NO filesystem I/O: the caller's transaction may still roll back, and mirrors are written post-commit.
|
|
16
11
|
*
|
|
17
|
-
* `opts.suppressForgetAudit` (default false
|
|
18
|
-
* so a removed
|
|
19
|
-
* already writes its own aggregate audit trail — `src/reject-flow.ts`'s
|
|
20
|
-
* `rejectValue` (single `reject_value` row covering every same-digest row
|
|
21
|
-
* removed) and `resolveConflict` (`conflict_resolve` row per resolution).
|
|
22
|
-
* Default keeps `deleteEntry` byte-identical to its pre-split behavior.
|
|
12
|
+
* `opts.suppressForgetAudit` (default false): `rejectValue` and `resolveConflict` set it because each
|
|
13
|
+
* writes its own aggregate audit row, so a removed row must not ALSO emit a `forget` row.
|
|
23
14
|
*
|
|
24
15
|
* Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
|
|
25
16
|
* existed or `automatic` refused it (pinned, raw, kept for good or backing an object at DELETE time, so a late pin wins).
|
|
@@ -49,6 +40,12 @@ export declare function deleteEntry(hippoRoot: string, id: string, opts?: {
|
|
|
49
40
|
reason?: string;
|
|
50
41
|
automatic?: boolean;
|
|
51
42
|
}): boolean;
|
|
43
|
+
/** deleteEntry on the caller's open store, so a loop of deletes opens the store once; each delete still commits alone. */
|
|
44
|
+
export declare function deleteEntryOn(db: DatabaseSyncLike, hippoRoot: string, id: string, opts?: {
|
|
45
|
+
actor?: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
automatic?: boolean;
|
|
48
|
+
}): boolean;
|
|
52
49
|
/** Consolidation's flush, one transaction. With `snapshot` (rows as the caller loaded them), a write keeps only
|
|
53
50
|
* the fields the caller changed, takes the rest from the live row, and never resurrects a row that is gone.
|
|
54
51
|
*
|