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/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 = [];
|
|
@@ -148,19 +145,13 @@ export function searchBoth(query, localRoot, globalRoot, options = {}) {
|
|
|
148
145
|
* Async version of searchBoth that calls hybridSearch instead of search.
|
|
149
146
|
*/
|
|
150
147
|
export async function searchBothHybrid(query, localRoot, globalRoot, options = {}) {
|
|
151
|
-
const {
|
|
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
|
-
// interactive - never the per-turn pinned-only hook - so ranking the full
|
|
157
|
-
// match set is acceptable.
|
|
158
|
-
// 5000 = 25x the default 200-row window: large enough that exclusion
|
|
159
|
-
// crowding is a non-issue on real stores, bounded so a common query term
|
|
160
|
-
// on a 100k-row store cannot stall an interactive call by ranking every
|
|
161
|
-
// 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.
|
|
162
153
|
const searchWindow = entryFilter ? 5000 : undefined;
|
|
163
|
-
//
|
|
154
|
+
// Recall mode: push the scope predicate into SQL exactly like
|
|
164
155
|
// api.recall (loadRecallSearchEntries), so quarantine/private rows never
|
|
165
156
|
// enter the candidate set, never shadow admitted duplicates in the dedupe
|
|
166
157
|
// pass, and never consume budget. The JS post-filter below is the
|
|
@@ -179,8 +170,6 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
|
|
|
179
170
|
const admit = (e) => passesScope(e) && (!entryFilter || entryFilter(e));
|
|
180
171
|
const localEntries = loadEntries(localRoot).filter(admit);
|
|
181
172
|
const globalEntries = loadEntries(globalRoot).filter(admit);
|
|
182
|
-
if (localEntries.length === 0 && globalEntries.length === 0)
|
|
183
|
-
return [];
|
|
184
173
|
// The vector arm loads under the same SQL rules as loadEntries, then the same JS admission.
|
|
185
174
|
const vectorCandidates = {
|
|
186
175
|
tenantId,
|
|
@@ -188,9 +177,16 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
|
|
|
188
177
|
includeSuperseded: !recallScope || Boolean(includeSuperseded) || Boolean(asOf),
|
|
189
178
|
admit,
|
|
190
179
|
};
|
|
180
|
+
return rankBothStores(query, { local: localRoot, global: globalRoot }, { local: localEntries, global: globalEntries }, vectorCandidates, options);
|
|
181
|
+
}
|
|
182
|
+
/** Hybrid ranking of rows already loaded from each store: the local bump, one copy per text, then the shared budget. */
|
|
183
|
+
export async function rankBothStores(query, roots, entries, vectorCandidates, options = {}) {
|
|
184
|
+
const { budget = DEFAULT_RECALL_BUDGET, now = evalNow(), embeddingWeight, explain, mmr, mmrLambda, localBump = DEFAULT_LOCAL_BUMP, minResults, cost, scope, includeSuperseded, asOf, summaryDeboost, summaryFreshness } = options;
|
|
185
|
+
if (entries.local.length === 0 && entries.global.length === 0)
|
|
186
|
+
return [];
|
|
191
187
|
const shared = { budget, now, embeddingWeight, explain, mmr, mmrLambda, minResults, cost, scope, includeSuperseded, asOf, summaryDeboost, summaryFreshness, vectorCandidates };
|
|
192
|
-
const localResults = await hybridSearch(query,
|
|
193
|
-
const globalResults = await hybridSearch(query,
|
|
188
|
+
const localResults = await hybridSearch(query, entries.local, { ...shared, hippoRoot: roots.local });
|
|
189
|
+
const globalResults = await hybridSearch(query, entries.global, { ...shared, hippoRoot: roots.global });
|
|
194
190
|
// Tag global results. Local memories get a configurable priority bump.
|
|
195
191
|
const tagged = [
|
|
196
192
|
...localResults.map((r) => ({
|
|
@@ -212,7 +208,7 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
|
|
|
212
208
|
seen.add(key);
|
|
213
209
|
return true;
|
|
214
210
|
});
|
|
215
|
-
//
|
|
211
|
+
// PLAIN stable score sort on purpose -- see searchBoth above;
|
|
216
212
|
// same rationale (deterministic inputs + stability; local-first on ties).
|
|
217
213
|
deduped.sort((a, b) => b.score - a.score);
|
|
218
214
|
return fitBudget(deduped, budget, minResults ?? 1, cost);
|
|
@@ -284,7 +280,7 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
284
280
|
const entry = readEntry(localRoot, id, options.tenantId);
|
|
285
281
|
if (!entry)
|
|
286
282
|
throw new NotFoundError(`Memory not found: ${id}`);
|
|
287
|
-
//
|
|
283
|
+
// Secret producer veto: secrets never go to the global store, not even
|
|
288
284
|
// with --force. Explicit and loud - a silent null would read as "low
|
|
289
285
|
// transfer score" and invite retries.
|
|
290
286
|
const secret = detectSecret(entry);
|
|
@@ -292,7 +288,7 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
292
288
|
throw new BadRequestError(`Refusing to share ${id} to the global store: content matches secret material (${secret.reason}). ` +
|
|
293
289
|
`Secrets stay in their owning project's store.`);
|
|
294
290
|
}
|
|
295
|
-
//
|
|
291
|
+
// A quarantined row is unreviewed input, not a lesson; sharing it would spread poison globally.
|
|
296
292
|
if (isQuarantineScope(entry.scope)) {
|
|
297
293
|
throw new BadRequestError(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
298
294
|
}
|
|
@@ -326,7 +322,7 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
326
322
|
* List all projects that have contributed memories to the global store.
|
|
327
323
|
* Parses the source field for 'shared:<project>:' or 'promoted:<path>' patterns.
|
|
328
324
|
*
|
|
329
|
-
*
|
|
325
|
+
* `tenantId` is optional. When provided, the global entries
|
|
330
326
|
* are filtered to that tenant before aggregation — matches every other
|
|
331
327
|
* read path's default-safe behaviour. When undefined, host-wide (back-compat
|
|
332
328
|
* for legacy callers like CLI standalone + dashboard internal use). Operators
|
|
@@ -337,7 +333,7 @@ export function listPeers(globalRoot, tenantId) {
|
|
|
337
333
|
const root = globalRoot ?? getGlobalRoot();
|
|
338
334
|
if (!fs.existsSync(root))
|
|
339
335
|
return [];
|
|
340
|
-
//
|
|
336
|
+
// Tenant-scoped by default when tenantId provided. Host-wide when
|
|
341
337
|
// undefined (preserves back-compat).
|
|
342
338
|
const tallies = tallySources(root, tenantId).sort((a, b) => (a.first < b.first ? -1 : a.first > b.first ? 1 : 0));
|
|
343
339
|
const peerMap = new Map();
|
|
@@ -368,25 +364,85 @@ export function listPeers(globalRoot, tenantId) {
|
|
|
368
364
|
.map(([project, data]) => ({ project, ...data }))
|
|
369
365
|
.sort((a, b) => b.count - a.count);
|
|
370
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
|
+
}
|
|
371
429
|
/**
|
|
372
430
|
* Auto-share: local memories with high transfer scores, not already global, no NEVER_AUTO_SHARE_TAGS tag.
|
|
373
431
|
* Returns the list of shared entries.
|
|
374
432
|
*
|
|
375
|
-
*
|
|
433
|
+
* `options.tenantId` is opt-in. When provided, the LOCAL-entries read is
|
|
376
434
|
* scoped to that tenant. When undefined, the local read is host-wide (current
|
|
377
435
|
* behaviour). The GLOBAL-entries read is always unioned — the global root IS
|
|
378
|
-
* the cross-tenant aggregate by design.
|
|
379
|
-
*
|
|
380
|
-
* passes options without tenantId because `sleep` is host-wide by intent;
|
|
381
|
-
* 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.
|
|
382
438
|
*
|
|
383
|
-
*
|
|
439
|
+
* `options.stats` is an opt-in out-param. When provided,
|
|
384
440
|
* `stats.secretSkipped` is incremented once per row that passed every OTHER
|
|
385
441
|
* admission gate (transfer score, not-already-global) and was withheld SOLELY
|
|
386
442
|
* by the secret veto — i.e. it counts shares actually prevented, not secret
|
|
387
443
|
* rows merely present. Filled identically under `dryRun`.
|
|
388
444
|
*
|
|
389
|
-
*
|
|
445
|
+
* `stats.rejectedSkipped` (optional) is incremented once per candidate
|
|
390
446
|
* refused by the GLOBAL store's rejection tombstone (RejectedValueError from
|
|
391
447
|
* shareMemory -> writeEntry). Unlike secretSkipped, this can only be
|
|
392
448
|
* detected by attempting the write — `dryRun` returns candidates before the
|
|
@@ -399,73 +455,15 @@ export function autoShare(localRoot, options = {}) {
|
|
|
399
455
|
const localEntries = loadAllEntries(localRoot, options.tenantId);
|
|
400
456
|
initGlobal();
|
|
401
457
|
const globalRoot = getGlobalRoot();
|
|
402
|
-
//
|
|
458
|
+
// Host-wide read. The global store IS the union across all tenants;
|
|
403
459
|
// per-tenant filtering on the global root would defeat the purpose.
|
|
404
460
|
const globalEntries = loadAllEntries(globalRoot);
|
|
405
461
|
// Build set of global content hashes to avoid duplicates
|
|
406
462
|
const globalContentSet = storedTextKeys(globalEntries);
|
|
407
|
-
const candidates = localEntries.filter((entry) =>
|
|
408
|
-
// CD5: shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
|
|
409
|
-
if (isQuarantineScope(entry.scope ?? null))
|
|
410
|
-
return false;
|
|
411
|
-
// Before the score: these rows describe one project only, and a git seed's 'error' tag clears the bar.
|
|
412
|
-
if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t)) || entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX)) {
|
|
413
|
-
if (options.stats)
|
|
414
|
-
options.stats.neverAutoShareSkipped = (options.stats.neverAutoShareSkipped ?? 0) + 1;
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
const score = transferScore(entry);
|
|
418
|
-
if (score < minScore)
|
|
419
|
-
return false;
|
|
420
|
-
// Skip if already shared (same text apart from spacing)
|
|
421
|
-
if (globalContentSet.has(duplicateKey(entry.content)))
|
|
422
|
-
return false;
|
|
423
|
-
// v39 S4 producer veto: secret rows never auto-share, regardless of
|
|
424
|
-
// transfer score. (shareMemory would throw; filtering here keeps the
|
|
425
|
-
// sleep pipeline fail-safe.) Checked LAST (v1.25.0) so the stats counter
|
|
426
|
-
// only counts rows the veto actually withheld — a row failing the score
|
|
427
|
-
// or dedupe gates was never going to share, secret or not.
|
|
428
|
-
if (detectSecret(entry).flagged) {
|
|
429
|
-
if (options.stats)
|
|
430
|
-
options.stats.secretSkipped++;
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
return true;
|
|
434
|
-
});
|
|
463
|
+
const candidates = localEntries.filter((entry) => isAutoShareCandidate(entry, globalContentSet, minScore, options.stats));
|
|
435
464
|
if (dryRun)
|
|
436
465
|
return candidates;
|
|
437
|
-
const shared =
|
|
438
|
-
// AT1 containment (docs/plans/2026-08-15-at1-rejected-value-tombstone.md
|
|
439
|
-
// plan §3 — sync/promote/share copy paths must not let ONE rejected
|
|
440
|
-
// candidate kill the batch): shareMemory -> writeEntry hits the LIVE guard
|
|
441
|
-
// against the GLOBAL store's tombstones. A matching candidate throws
|
|
442
|
-
// RejectedValueError, which (uncaught) would abort this whole loop and,
|
|
443
|
-
// via api.ts's sleep pipeline, the entire autoShare sleep phase. Mirrors
|
|
444
|
-
// syncGlobalToLocal's per-item catch just above in this file. writeEntry's
|
|
445
|
-
// own catch already writes the reject_refusal audit before rethrowing
|
|
446
|
-
// (plan §3) — do not double-audit here, just count and continue.
|
|
447
|
-
let rejectedSkipped = 0;
|
|
448
|
-
for (const entry of candidates) {
|
|
449
|
-
try {
|
|
450
|
-
// skipEmbed: batching invariant, this is a batch producer, so it embeds
|
|
451
|
-
// once via embedAll() below rather than once per row inside shareMemory.
|
|
452
|
-
const result = shareMemory(localRoot, entry.id, { force: true, skipEmbed: true });
|
|
453
|
-
if (result)
|
|
454
|
-
shared.push(result);
|
|
455
|
-
}
|
|
456
|
-
catch (err) {
|
|
457
|
-
if (err instanceof RejectedValueError) {
|
|
458
|
-
rejectedSkipped++;
|
|
459
|
-
if (options.stats)
|
|
460
|
-
options.stats.rejectedSkipped = (options.stats.rejectedSkipped ?? 0) + 1;
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
throw err;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (rejectedSkipped > 0) {
|
|
467
|
-
log.warn(`autoShare: skipped ${rejectedSkipped} candidate(s) refused by the global store's rejection tombstone`);
|
|
468
|
-
}
|
|
466
|
+
const shared = shareCandidates(localRoot, candidates, options.stats);
|
|
469
467
|
if (shared.length > 0) {
|
|
470
468
|
void embedAll(globalRoot).catch((err) => logEmbedAllFailure('autoShare', err));
|
|
471
469
|
}
|
|
@@ -479,18 +477,18 @@ export function autoShare(localRoot, options = {}) {
|
|
|
479
477
|
export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
480
478
|
if (!fs.existsSync(globalRoot))
|
|
481
479
|
return 0;
|
|
482
|
-
//
|
|
480
|
+
// Host-wide read. syncGlobalToLocal copies the global union into a
|
|
483
481
|
// tenant-scoped local store; writeEntry on each row carries the tenant if
|
|
484
482
|
// the local-root context provides one.
|
|
485
483
|
const globalEntries = loadAllEntries(globalRoot);
|
|
486
484
|
const localIndex = loadIndex(localRoot);
|
|
487
485
|
const textKey = (e) => `${e.tenantId}\n${e.content}`;
|
|
488
486
|
const localText = new Set(loadAllEntries(localRoot).map(textKey));
|
|
489
|
-
//
|
|
487
|
+
// Syncing down must not re-import what ambient context
|
|
490
488
|
// excludes - other-project rows are skipped by default and secret rows
|
|
491
489
|
// are never copied. origin_project is preserved on the copy (writeEntry
|
|
492
490
|
// only stamps when the field is missing).
|
|
493
|
-
const
|
|
491
|
+
const currentProject = resolveProjectIdentity(path.dirname(path.resolve(localRoot)));
|
|
494
492
|
let count = 0;
|
|
495
493
|
// A locally rejected value must not come back through sync down: caught per item, printed as one line.
|
|
496
494
|
let rejected = 0;
|
|
@@ -506,7 +504,7 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
|
506
504
|
if (detectSecret(entry).flagged)
|
|
507
505
|
continue;
|
|
508
506
|
if (!opts.includeCrossProject &&
|
|
509
|
-
classifyOriginProject(entry.origin_project,
|
|
507
|
+
classifyOriginProject(entry.origin_project, currentProject) === 'cross-project')
|
|
510
508
|
continue;
|
|
511
509
|
try {
|
|
512
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
|
}
|
|
@@ -111,9 +111,76 @@ function buildSkillContent(skillName, instructions, trigger) {
|
|
|
111
111
|
content += `\n\n${instructions}`;
|
|
112
112
|
return content;
|
|
113
113
|
}
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
114
|
+
// Preflight the supersede target BEFORE inserting the new row (so the new
|
|
115
|
+
// autoincrement id can never be its own supersede target); read the
|
|
116
|
+
// predecessor version in the same SELECT for server-derived versioning.
|
|
117
|
+
// Mirrors saveProcess / savePolicy.
|
|
118
|
+
function preflightSkillSupersede(db, tenantId, supersedesId) {
|
|
119
|
+
// SAFETY: row shape matches the `status, version` columns named in the SELECT below.
|
|
120
|
+
const pred = db.prepare(`SELECT status, version FROM skills WHERE id = ? AND tenant_id = ?`).get(supersedesId, tenantId);
|
|
121
|
+
if (!pred) {
|
|
122
|
+
throw new NotFoundError(`saveSkill: skill ${supersedesId} to supersede not found for tenant ${tenantId}`);
|
|
123
|
+
}
|
|
124
|
+
if (pred.status !== 'active') {
|
|
125
|
+
throw new ConflictError(`saveSkill: skill ${supersedesId} is not active (status='${pred.status}'); only active skills can be superseded.`);
|
|
126
|
+
}
|
|
127
|
+
return pred.version + 1;
|
|
128
|
+
}
|
|
129
|
+
function insertSkillRow(db, memoryId, w, version) {
|
|
130
|
+
const result = db.prepare(`
|
|
131
|
+
INSERT INTO skills(
|
|
132
|
+
memory_id, tenant_id, skill_name, instructions, trigger_text, version,
|
|
133
|
+
status, superseded_by, superseded_at, change_summary, closed_at, created_at
|
|
134
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'active', NULL, NULL, ?, NULL, ?)
|
|
135
|
+
`).run(memoryId, w.tenantId, w.name, w.instructions, w.trigger, version, w.changeSummary, w.now);
|
|
136
|
+
return Number(result.lastInsertRowid ?? 0);
|
|
137
|
+
}
|
|
138
|
+
function supersedeSkillRow(db, w, supersedesId, skillId, version) {
|
|
139
|
+
const sup = db.prepare(`
|
|
140
|
+
UPDATE skills
|
|
141
|
+
SET status = 'superseded', superseded_by = ?, superseded_at = ?
|
|
142
|
+
WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
|
|
143
|
+
`).run(skillId, w.now, supersedesId, w.tenantId, skillId);
|
|
144
|
+
if (sup.changes === 0) {
|
|
145
|
+
throw new ConflictError(`saveSkill: skill ${supersedesId} could not be superseded (no longer active or self-reference).`);
|
|
146
|
+
}
|
|
147
|
+
appendAuditEvent(db, {
|
|
148
|
+
tenantId: w.tenantId,
|
|
149
|
+
actor: w.actor,
|
|
150
|
+
op: 'skill_supersede',
|
|
151
|
+
targetId: String(supersedesId),
|
|
152
|
+
metadata: {
|
|
153
|
+
skill_id: supersedesId,
|
|
154
|
+
superseded_by: skillId,
|
|
155
|
+
new_version: version,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** The afterWrite body: preflight, INSERT, supersede, reload, create audit, all in one SAVEPOINT. */
|
|
160
|
+
function writeSkillRow(db, memoryId, w) {
|
|
161
|
+
const version = w.supersedesId !== undefined ? preflightSkillSupersede(db, w.tenantId, w.supersedesId) : 1;
|
|
162
|
+
const skillId = insertSkillRow(db, memoryId, w, version);
|
|
163
|
+
if (w.supersedesId !== undefined)
|
|
164
|
+
supersedeSkillRow(db, w, w.supersedesId, skillId, version);
|
|
165
|
+
// SAFETY: row's shape matches the columns named in SKILL_COLS above.
|
|
166
|
+
const row = db.prepare(`SELECT ${SKILL_COLS} FROM skills WHERE id = ?`)
|
|
167
|
+
.get(skillId);
|
|
168
|
+
if (!row)
|
|
169
|
+
throw new Error('saveSkill: failed to reload saved skill row');
|
|
170
|
+
// GDPR-light metadata: ids + flags only, no skill text.
|
|
171
|
+
appendAuditEvent(db, {
|
|
172
|
+
tenantId: w.tenantId,
|
|
173
|
+
actor: w.actor,
|
|
174
|
+
op: 'skill_create',
|
|
175
|
+
targetId: String(skillId),
|
|
176
|
+
metadata: {
|
|
177
|
+
skill_id: skillId,
|
|
178
|
+
version,
|
|
179
|
+
has_trigger: w.trigger !== null,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
return row;
|
|
183
|
+
}
|
|
117
184
|
/**
|
|
118
185
|
* Create a skill (or a new version that supersedes an existing one). Writes the
|
|
119
186
|
* memory mirror + the skills row atomically inside writeEntry's SAVEPOINT. When
|
|
@@ -137,72 +204,21 @@ export function saveSkill(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
|
137
204
|
baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
|
|
138
205
|
tenantId,
|
|
139
206
|
});
|
|
207
|
+
const w = {
|
|
208
|
+
tenantId,
|
|
209
|
+
actor,
|
|
210
|
+
name,
|
|
211
|
+
instructions: opts.instructions,
|
|
212
|
+
trigger,
|
|
213
|
+
changeSummary,
|
|
214
|
+
supersedesId: opts.supersedesSkillId,
|
|
215
|
+
now,
|
|
216
|
+
};
|
|
140
217
|
let savedRow;
|
|
141
218
|
writeEntry(hippoRoot, mem, {
|
|
142
219
|
actor,
|
|
143
220
|
afterWrite: (db, memoryId) => {
|
|
144
|
-
|
|
145
|
-
// autoincrement id can never be its own supersede target); read the
|
|
146
|
-
// predecessor version in the same SELECT for server-derived versioning.
|
|
147
|
-
// Mirrors saveProcess / savePolicy (codex P1 2026-05-28).
|
|
148
|
-
let version = 1;
|
|
149
|
-
if (opts.supersedesSkillId !== undefined) {
|
|
150
|
-
// SAFETY: row shape matches the `status, version` columns named in the SELECT below.
|
|
151
|
-
const pred = db.prepare(`SELECT status, version FROM skills WHERE id = ? AND tenant_id = ?`).get(opts.supersedesSkillId, tenantId);
|
|
152
|
-
if (!pred) {
|
|
153
|
-
throw new NotFoundError(`saveSkill: skill ${opts.supersedesSkillId} to supersede not found for tenant ${tenantId}`);
|
|
154
|
-
}
|
|
155
|
-
if (pred.status !== 'active') {
|
|
156
|
-
throw new ConflictError(`saveSkill: skill ${opts.supersedesSkillId} is not active (status='${pred.status}'); only active skills can be superseded.`);
|
|
157
|
-
}
|
|
158
|
-
version = pred.version + 1;
|
|
159
|
-
}
|
|
160
|
-
const result = db.prepare(`
|
|
161
|
-
INSERT INTO skills(
|
|
162
|
-
memory_id, tenant_id, skill_name, instructions, trigger_text, version,
|
|
163
|
-
status, superseded_by, superseded_at, change_summary, closed_at, created_at
|
|
164
|
-
) VALUES (?, ?, ?, ?, ?, ?, 'active', NULL, NULL, ?, NULL, ?)
|
|
165
|
-
`).run(memoryId, tenantId, name, opts.instructions, trigger, version, changeSummary, now);
|
|
166
|
-
const skillId = Number(result.lastInsertRowid ?? 0);
|
|
167
|
-
if (opts.supersedesSkillId !== undefined) {
|
|
168
|
-
const sup = db.prepare(`
|
|
169
|
-
UPDATE skills
|
|
170
|
-
SET status = 'superseded', superseded_by = ?, superseded_at = ?
|
|
171
|
-
WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
|
|
172
|
-
`).run(skillId, now, opts.supersedesSkillId, tenantId, skillId);
|
|
173
|
-
if (sup.changes === 0) {
|
|
174
|
-
throw new ConflictError(`saveSkill: skill ${opts.supersedesSkillId} could not be superseded (no longer active or self-reference).`);
|
|
175
|
-
}
|
|
176
|
-
appendAuditEvent(db, {
|
|
177
|
-
tenantId,
|
|
178
|
-
actor,
|
|
179
|
-
op: 'skill_supersede',
|
|
180
|
-
targetId: String(opts.supersedesSkillId),
|
|
181
|
-
metadata: {
|
|
182
|
-
skill_id: opts.supersedesSkillId,
|
|
183
|
-
superseded_by: skillId,
|
|
184
|
-
new_version: version,
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
// SAFETY: row's shape matches the columns named in SKILL_COLS above.
|
|
189
|
-
const row = db.prepare(`SELECT ${SKILL_COLS} FROM skills WHERE id = ?`)
|
|
190
|
-
.get(skillId);
|
|
191
|
-
if (!row)
|
|
192
|
-
throw new Error('saveSkill: failed to reload saved skill row');
|
|
193
|
-
savedRow = row;
|
|
194
|
-
// GDPR-light metadata: ids + flags only, no skill text.
|
|
195
|
-
appendAuditEvent(db, {
|
|
196
|
-
tenantId,
|
|
197
|
-
actor,
|
|
198
|
-
op: 'skill_create',
|
|
199
|
-
targetId: String(skillId),
|
|
200
|
-
metadata: {
|
|
201
|
-
skill_id: skillId,
|
|
202
|
-
version,
|
|
203
|
-
has_trigger: trigger !== null,
|
|
204
|
-
},
|
|
205
|
-
});
|
|
221
|
+
savedRow = writeSkillRow(db, memoryId, w);
|
|
206
222
|
},
|
|
207
223
|
});
|
|
208
224
|
if (!savedRow) {
|
|
@@ -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 {
|