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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// A checkout's `origin` remote, read from its git config file and normalised into a project id; no git process, so the prompt hook can call it.
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { isObjectLike, isStringValue } from './capture-contract.js';
|
|
5
|
+
import { log } from './log.js';
|
|
6
|
+
/** The config file git reads for a checkout: `.git/config`, a linked worktree's `<commondir>/config`, a submodule's `<gitdir>/config`. */
|
|
7
|
+
export function gitConfigPath(gitRoot) {
|
|
8
|
+
const marker = path.join(gitRoot, '.git');
|
|
9
|
+
try {
|
|
10
|
+
if (fs.statSync(marker).isDirectory())
|
|
11
|
+
return path.join(marker, 'config');
|
|
12
|
+
const gitDir = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(marker, 'utf8'))?.[1]?.trim();
|
|
13
|
+
if (!gitDir)
|
|
14
|
+
return null;
|
|
15
|
+
const linkDir = path.resolve(gitRoot, gitDir);
|
|
16
|
+
const commondir = path.join(linkDir, 'commondir');
|
|
17
|
+
if (!fs.existsSync(commondir))
|
|
18
|
+
return path.join(linkDir, 'config');
|
|
19
|
+
return path.join(path.resolve(linkDir, fs.readFileSync(commondir, 'utf8').trim()), 'config');
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
log.debug(`project remote: no git config for ${gitRoot}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** One config value with git's quoting: `"` toggles quoting, backslash escapes, and an unquoted `#` or `;` starts a comment. */
|
|
27
|
+
function configValue(raw) {
|
|
28
|
+
let out = '';
|
|
29
|
+
let quoted = false;
|
|
30
|
+
for (let i = 0; i < raw.length; i++) {
|
|
31
|
+
const c = raw[i];
|
|
32
|
+
if (c === '\\' && i + 1 < raw.length) {
|
|
33
|
+
const next = raw[++i];
|
|
34
|
+
out += next === 'n' ? '\n' : next === 't' ? '\t' : next === 'b' ? '' : next;
|
|
35
|
+
}
|
|
36
|
+
else if (c === '"') {
|
|
37
|
+
quoted = !quoted;
|
|
38
|
+
}
|
|
39
|
+
else if (!quoted && (c === '#' || c === ';')) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
out += c;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out.trim();
|
|
47
|
+
}
|
|
48
|
+
const SECTION = /^\[\s*([A-Za-z0-9.-]+)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\](.*)$/;
|
|
49
|
+
const ENTRY = /^([A-Za-z][A-Za-z0-9-]*)\s*(?:=(.*))?$/;
|
|
50
|
+
/** `remote.origin.url` from a config file's text, the first value as git fetch uses; the subsection name is case-sensitive. */
|
|
51
|
+
export function originUrlFromConfig(text) {
|
|
52
|
+
let inOrigin = false;
|
|
53
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
54
|
+
let line = raw.trim();
|
|
55
|
+
const section = SECTION.exec(line);
|
|
56
|
+
if (section) {
|
|
57
|
+
const [, name, sub, rest] = section;
|
|
58
|
+
inOrigin = sub === undefined
|
|
59
|
+
? name.toLowerCase() === 'remote.origin'
|
|
60
|
+
: name.toLowerCase() === 'remote' && sub.replace(/\\(.)/g, '$1') === 'origin';
|
|
61
|
+
line = rest.trim();
|
|
62
|
+
}
|
|
63
|
+
const entry = inOrigin ? ENTRY.exec(line) : null;
|
|
64
|
+
if (entry && entry[1].toLowerCase() === 'url' && entry[2] !== undefined) {
|
|
65
|
+
const value = configValue(entry[2]);
|
|
66
|
+
if (value !== '')
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/** Azure DevOps names one repo three ways; all become `dev.azure.com/org/project/repo`. */
|
|
73
|
+
function azureDevOps(hostPath) {
|
|
74
|
+
const ssh = /^(?:ssh\.dev\.azure\.com|vs-ssh\.visualstudio\.com)\/v3\/(.+)$/.exec(hostPath);
|
|
75
|
+
if (ssh)
|
|
76
|
+
return `dev.azure.com/${ssh[1]}`;
|
|
77
|
+
const legacy = /^([^/.]+)\.visualstudio\.com\/(?:defaultcollection\/)?(.+)$/.exec(hostPath);
|
|
78
|
+
const https = legacy ? `dev.azure.com/${legacy[1]}/${legacy[2]}` : hostPath;
|
|
79
|
+
return https.startsWith('dev.azure.com/') ? https.replace('/_git/', '/') : https;
|
|
80
|
+
}
|
|
81
|
+
/** `host/path` for a network remote, so ssh, scp and https forms agree; null for a local path, which would put a user path into rows. */
|
|
82
|
+
export function normaliseRemote(url) {
|
|
83
|
+
// A query or fragment can carry a token (`?private_token=`) and never names the repo.
|
|
84
|
+
const u = url.trim().replace(/[?#].*$/, '');
|
|
85
|
+
if (u === '' || u.includes('::') || /^file:/i.test(u) || /^[A-Za-z]:[\\/]/.test(u) || /^[./\\~]/.test(u))
|
|
86
|
+
return null;
|
|
87
|
+
let hostPath;
|
|
88
|
+
const withScheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/]*)(.*)$/.exec(u);
|
|
89
|
+
if (withScheme) {
|
|
90
|
+
// Userinfo can hold a token, and a port differs between ssh and https.
|
|
91
|
+
hostPath = withScheme[1].replace(/^.*@/, '').replace(/:\d*$/, '') + withScheme[2];
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(.*)$/.exec(u);
|
|
95
|
+
if (!scp)
|
|
96
|
+
return null;
|
|
97
|
+
hostPath = `${scp[1]}/${scp[2]}`;
|
|
98
|
+
}
|
|
99
|
+
const tidy = hostPath.toLowerCase().replace(/\\/g, '/').replace(/\/{2,}/g, '/').replace(/\/+$/, '').replace(/\.git$/, '').replace(/\/+$/, '');
|
|
100
|
+
const id = azureDevOps(tidy);
|
|
101
|
+
// A colon would break the `shared:<project>:` source format; whitespace means this was no URL; an `@` left in a path may be a credential.
|
|
102
|
+
return /^[^/]+\/./.test(id) && !/[:\s@]/.test(id) ? id : null;
|
|
103
|
+
}
|
|
104
|
+
/** The normalised origin of the checkout rooted at gitRoot, or null when it has none a project can be named by. */
|
|
105
|
+
export function originRemoteId(gitRoot) {
|
|
106
|
+
const file = gitConfigPath(gitRoot);
|
|
107
|
+
if (file === null)
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
const url = originUrlFromConfig(fs.readFileSync(file, 'utf8'));
|
|
111
|
+
return url === null ? null : normaliseRemote(url);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
log.debug(`project remote: ${file} not read: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export const PROJECT_FILE = '.hippo-project.json';
|
|
119
|
+
/** The `id` a team committed in `.hippo-project.json` at root, lowercased like every project name; null when absent or unusable. */
|
|
120
|
+
export function projectFileId(root) {
|
|
121
|
+
const file = path.join(root, PROJECT_FILE);
|
|
122
|
+
if (!fs.existsSync(file))
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
126
|
+
const raw = isObjectLike(parsed) && 'id' in parsed ? parsed.id : undefined;
|
|
127
|
+
const id = isStringValue(raw) ? raw.trim().toLowerCase() : '';
|
|
128
|
+
// No slash: a committed file in an untrusted clone must not claim a remote-shaped id like `github.com/acme/api`.
|
|
129
|
+
if (id !== '' && id.length <= 200 && !/[:\s/\\]/.test(id))
|
|
130
|
+
return id;
|
|
131
|
+
log.warn(`${file}: "id" must be a non-empty string with no colon, slash or spaces; using the remote or folder name instead`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
log.warn(`${file} not read: ${err instanceof Error ? err.message : String(err)}; using the remote or folder name instead`);
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=project-remote.js.map
|
package/dist/prompt-recall.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* See docs/plans/2026-09-26-z1-prompt-recall.md. */
|
|
1
|
+
/** Recall gated on the hook prompt, not the five newest memories (pure, no I/O). */
|
|
3
2
|
import { tokenize } from './tokenize.js';
|
|
4
3
|
import { STOP_WORDS } from './audit.js';
|
|
5
4
|
// Latency bound, not tuned: fixed in the prereg regardless of gate config.
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-key token-bucket rate limiter for inbound /v1/* requests.
|
|
3
3
|
*
|
|
4
|
-
* Bounds api-key-id enumeration
|
|
4
|
+
* Bounds api-key-id enumeration: a
|
|
5
5
|
* client that drains its bucket is denied until it refills. Dependency-free
|
|
6
6
|
* and unit-testable in isolation via the injectable `now`.
|
|
7
7
|
*/
|
package/dist/rate-limit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-key token-bucket rate limiter for inbound /v1/* requests.
|
|
3
3
|
*
|
|
4
|
-
* Bounds api-key-id enumeration
|
|
4
|
+
* Bounds api-key-id enumeration: a
|
|
5
5
|
* client that drains its bucket is denied until it refills. Dependency-free
|
|
6
6
|
* and unit-testable in isolation via the injectable `now`.
|
|
7
7
|
*/
|
package/dist/raw-archive.d.ts
CHANGED
|
@@ -11,5 +11,14 @@ export interface ArchiveOpts {
|
|
|
11
11
|
*/
|
|
12
12
|
afterArchive?: (db: DatabaseSyncLike, archivedMemoryId: string) => void;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The only legitimate path to remove a `kind='raw'` row from `memories`.
|
|
16
|
+
*
|
|
17
|
+
* Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
|
|
18
|
+
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
19
|
+
* SAVEPOINT so it can be nested inside an outer transaction (e.g. batchWriteAndDelete).
|
|
20
|
+
*
|
|
21
|
+
* Throws if the row does not exist or is not `kind='raw'`.
|
|
22
|
+
*/
|
|
14
23
|
export declare function archiveRawMemory(db: DatabaseSyncLike, id: string, opts: ArchiveOpts): void;
|
|
15
24
|
//# sourceMappingURL=raw-archive.d.ts.map
|
package/dist/raw-archive.js
CHANGED
|
@@ -2,7 +2,7 @@ import { BadRequestError, NotFoundError } from './api-errors.js';
|
|
|
2
2
|
import { isFtsAvailable } from './db.js';
|
|
3
3
|
import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
|
|
4
4
|
import { markSummaryDirtyInTx } from './summary-dirty.js';
|
|
5
|
-
|
|
5
|
+
function loadRawRow(db, id) {
|
|
6
6
|
// SAFETY: SELECT * FROM memories returns every column of the memories table; only
|
|
7
7
|
// kind, tenant_id, and dag_parent_id are read below, all guaranteed present (possibly
|
|
8
8
|
// null) by the memories schema.
|
|
@@ -12,65 +12,82 @@ export function archiveRawMemory(db, id, opts) {
|
|
|
12
12
|
if (row.kind !== 'raw') {
|
|
13
13
|
throw new BadRequestError(`memory ${id} is not raw (kind=${String(row.kind)})`);
|
|
14
14
|
}
|
|
15
|
+
return row;
|
|
16
|
+
}
|
|
17
|
+
function moveRowToArchive(db, id, row, opts) {
|
|
18
|
+
// GDPR: raw_archive stores ONLY metadata, not the original
|
|
19
|
+
// memory content. The audit_log row appended below carries op='archive_raw'
|
|
20
|
+
// for the compliance audit trail. True right-to-be-forgotten — the original
|
|
21
|
+
// content is unrecoverable from raw_archive after this point.
|
|
22
|
+
const archivedAt = new Date().toISOString();
|
|
23
|
+
const redactedPayload = JSON.stringify({
|
|
24
|
+
redacted: true,
|
|
25
|
+
archived_at: archivedAt,
|
|
26
|
+
tenant_id: row.tenant_id ?? 'default',
|
|
27
|
+
kind: row.kind,
|
|
28
|
+
reason: opts.reason,
|
|
29
|
+
});
|
|
30
|
+
db.prepare(`INSERT INTO raw_archive (memory_id, archived_at, reason, archived_by, payload_json) VALUES (?, ?, ?, ?, ?)`).run(id, archivedAt, opts.reason, opts.who, redactedPayload);
|
|
31
|
+
// Flip kind to 'archived' so the BEFORE DELETE trigger no longer fires, then delete.
|
|
32
|
+
db.prepare(`UPDATE memories SET kind = 'archived' WHERE id = ?`).run(id);
|
|
33
|
+
db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
|
34
|
+
// FTS5 is a virtual table — no FK CASCADE applies. Purge the FTS row so the
|
|
35
|
+
// archived content is not searchable after archive. Without this the original
|
|
36
|
+
// raw text remains in memories_fts until the next DB-open backfill, defeating
|
|
37
|
+
// GDPR right-to-be-forgotten.
|
|
38
|
+
if (isFtsAvailable(db)) {
|
|
39
|
+
try {
|
|
40
|
+
db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(id);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Best effort only. The DELETE on memories already succeeded; FTS will
|
|
44
|
+
// self-heal on next DB open via backfillFtsIndex.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Emit the archive_raw audit event inside the SAVEPOINT so the audit row is
|
|
49
|
+
// committed atomically with the row deletion. Use the row's own tenant_id
|
|
50
|
+
// (fetched above as part of SELECT *), not the env. Archives must be
|
|
51
|
+
// attributed to the tenant that owns the row, not whatever HIPPO_TENANT
|
|
52
|
+
// happens to be set to in the calling shell.
|
|
53
|
+
function auditArchive(db, id, row, opts) {
|
|
54
|
+
try {
|
|
55
|
+
appendAuditEvent(db, {
|
|
56
|
+
tenantId: String(row.tenant_id ?? 'default'),
|
|
57
|
+
actor: opts.who || 'cli',
|
|
58
|
+
op: 'archive_raw',
|
|
59
|
+
targetId: id,
|
|
60
|
+
metadata: { reason: opts.reason },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// The archive itself has already succeeded; an unwritable audit table must not undo it.
|
|
65
|
+
reportAuditWriteFailure('archive_raw', String(error), id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The only legitimate path to remove a `kind='raw'` row from `memories`.
|
|
70
|
+
*
|
|
71
|
+
* Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
|
|
72
|
+
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
73
|
+
* SAVEPOINT so it can be nested inside an outer transaction (e.g. batchWriteAndDelete).
|
|
74
|
+
*
|
|
75
|
+
* Throws if the row does not exist or is not `kind='raw'`.
|
|
76
|
+
*/
|
|
77
|
+
export function archiveRawMemory(db, id, opts) {
|
|
78
|
+
const row = loadRawRow(db, id);
|
|
15
79
|
// SAVEPOINT (not BEGIN) so this works whether or not we're already inside a
|
|
16
80
|
// transaction. SQLite refuses BEGIN within a transaction; SAVEPOINT nests safely.
|
|
17
81
|
db.exec('SAVEPOINT archive_raw');
|
|
18
82
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const archivedAt = new Date().toISOString();
|
|
24
|
-
const redactedPayload = JSON.stringify({
|
|
25
|
-
redacted: true,
|
|
26
|
-
archived_at: archivedAt,
|
|
27
|
-
tenant_id: row.tenant_id ?? 'default',
|
|
28
|
-
kind: row.kind,
|
|
29
|
-
reason: opts.reason,
|
|
30
|
-
});
|
|
31
|
-
db.prepare(`INSERT INTO raw_archive (memory_id, archived_at, reason, archived_by, payload_json) VALUES (?, ?, ?, ?, ?)`).run(id, archivedAt, opts.reason, opts.who, redactedPayload);
|
|
32
|
-
// Flip kind to 'archived' so the BEFORE DELETE trigger no longer fires, then delete.
|
|
33
|
-
db.prepare(`UPDATE memories SET kind = 'archived' WHERE id = ?`).run(id);
|
|
34
|
-
db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
|
35
|
-
// FTS5 is a virtual table — no FK CASCADE applies. Purge the FTS row so the
|
|
36
|
-
// archived content is not searchable after archive. Without this the original
|
|
37
|
-
// raw text remains in memories_fts until the next DB-open backfill, defeating
|
|
38
|
-
// GDPR right-to-be-forgotten.
|
|
39
|
-
if (isFtsAvailable(db)) {
|
|
40
|
-
try {
|
|
41
|
-
db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(id);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Best effort only. The DELETE on memories already succeeded; FTS will
|
|
45
|
-
// self-heal on next DB open via backfillFtsIndex.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// A5 audit: emit archive_raw event inside the SAVEPOINT so the audit row is
|
|
49
|
-
// committed atomically with the row deletion. Use the row's own tenant_id
|
|
50
|
-
// (fetched above as part of SELECT *), not the env. Archives must be
|
|
51
|
-
// attributed to the tenant that owns the row, not whatever HIPPO_TENANT
|
|
52
|
-
// happens to be set to in the calling shell.
|
|
53
|
-
try {
|
|
54
|
-
appendAuditEvent(db, {
|
|
55
|
-
tenantId: String(row.tenant_id ?? 'default'),
|
|
56
|
-
actor: opts.who || 'cli',
|
|
57
|
-
op: 'archive_raw',
|
|
58
|
-
targetId: id,
|
|
59
|
-
metadata: { reason: opts.reason },
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
// The archive itself has already succeeded; an unwritable audit table must not undo it.
|
|
64
|
-
reportAuditWriteFailure('archive_raw', String(error), id);
|
|
65
|
-
}
|
|
66
|
-
// v0.30 / E2 — DAG live-coupling: archive of a child under a level-2
|
|
67
|
-
// summary marks parent dirty. Inside the SAVEPOINT so the dirty-mark
|
|
68
|
-
// commits atomically with the archive. row.dag_parent_id was fetched
|
|
69
|
-
// via SELECT * at L28 (schema v28 includes it).
|
|
83
|
+
moveRowToArchive(db, id, row, opts);
|
|
84
|
+
auditArchive(db, id, row, opts);
|
|
85
|
+
// Archiving a child under a level-2 summary marks the parent dirty, inside the
|
|
86
|
+
// SAVEPOINT so the dirty-mark commits atomically with the archive.
|
|
70
87
|
if (row.dag_parent_id) {
|
|
71
88
|
markSummaryDirtyInTx(db, String(row.dag_parent_id), String(row.tenant_id ?? 'default'), opts.who || 'cli');
|
|
72
89
|
}
|
|
73
|
-
// afterArchive hook
|
|
90
|
+
// afterArchive hook: connector-level idempotency markers
|
|
74
91
|
// (e.g. slack_event_log) must commit atomically with the archive itself.
|
|
75
92
|
// Throwing here rolls back the entire SAVEPOINT — both the archive and any
|
|
76
93
|
// hook side effects. The hook runs INSIDE the SAVEPOINT so its writes
|
package/dist/recall-history.d.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Anchoring detector (recall-recurrence), pure module.
|
|
3
3
|
*
|
|
4
|
-
* Implements two detection rules
|
|
5
|
-
*
|
|
4
|
+
* Implements two detection rules:
|
|
5
|
+
* query_repeat: same queryHash within recentRepeatWindow returned
|
|
6
6
|
* same topMemoryId (caller is re-asking the same question).
|
|
7
|
-
*
|
|
7
|
+
* memory_dominance: same topMemoryId across >= minDominance distinct
|
|
8
8
|
* queryHashes (memory acts as a fixed-point anchor regardless of what
|
|
9
9
|
* the agent asks).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Each pipeline (api.recall via
|
|
12
12
|
* HTTP, cmdRecall, MCP hippo_recall) owns its OWN ring buffer Map keyed
|
|
13
13
|
* by (tenant, session). No cross-pipeline sharing (the typical multi-
|
|
14
14
|
* process deployment makes IPC ring-sharing impractical; per-pipeline
|
|
15
15
|
* is correct because each pipeline has its own top-1 ranking anyway).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* Composes with J3.2: AnchoringHint + PlanningFallacyHint are independent
|
|
17
|
+
* AnchoringHint + PlanningFallacyHint are independent
|
|
19
18
|
* signals; both can fire on the same recall.
|
|
20
19
|
*/
|
|
21
20
|
export type AnchoringReason = 'query_repeat' | 'memory_dominance';
|
|
@@ -36,20 +35,20 @@ export interface RecallHistoryEntry {
|
|
|
36
35
|
queryHash: number;
|
|
37
36
|
/** Top-1 memory id this recall surfaced; null if zero results. */
|
|
38
37
|
topMemoryId: string | null;
|
|
39
|
-
/** ISO-8601 timestamp; advisory, not used by
|
|
38
|
+
/** ISO-8601 timestamp; advisory, not used by the detection rules. */
|
|
40
39
|
ts: string;
|
|
41
40
|
/** Memory id of the AnchoringHint that fired on this recall, if any.
|
|
42
|
-
* Used by
|
|
41
|
+
* Used by the cooldown logic to prevent re-emitting the same hint on
|
|
43
42
|
* consecutive recalls within the dominance window. Caller-written
|
|
44
43
|
* AFTER detectAnchoring returns; reads next time detectAnchoring runs. */
|
|
45
44
|
anchoredOn?: string;
|
|
46
45
|
}
|
|
47
46
|
export type RecallHistorySnapshot = readonly RecallHistoryEntry[];
|
|
48
47
|
export interface DetectAnchoringOpts {
|
|
49
|
-
/**
|
|
48
|
+
/** memory_dominance threshold: number of distinct queryHashes that must have returned
|
|
50
49
|
* the same topMemoryId. Default 3. */
|
|
51
50
|
minDominance?: number;
|
|
52
|
-
/**
|
|
51
|
+
/** query_repeat window: how many recent history entries to scan for query repeat.
|
|
53
52
|
* Default 5. */
|
|
54
53
|
recentRepeatWindow?: number;
|
|
55
54
|
/** Cooldown: if the immediately-prior fire (per `anchoredOn`) was for
|
|
@@ -63,8 +62,8 @@ export interface DetectAnchoringOpts {
|
|
|
63
62
|
* sort tokens → join → FNV-1a 32-bit.
|
|
64
63
|
*
|
|
65
64
|
* Token sort + dedup means semantically-equivalent queries with reordered
|
|
66
|
-
* words collide intentionally (
|
|
67
|
-
*
|
|
65
|
+
* words collide intentionally ("semantically-distinct" is approximated by
|
|
66
|
+
* textual normalization, not embeddings).
|
|
68
67
|
*
|
|
69
68
|
* Deterministic across processes; stable across Node + V8 versions.
|
|
70
69
|
*/
|
|
@@ -73,17 +72,17 @@ export declare function hashQueryText(query: string): number;
|
|
|
73
72
|
* Detect anchoring patterns in the recall history against the current
|
|
74
73
|
* recall's (queryHash, topMemoryId).
|
|
75
74
|
*
|
|
76
|
-
* Rule precedence:
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
75
|
+
* Rule precedence: memory_dominance wins on tie. When both rules fire on the
|
|
76
|
+
* same recall, return only memory_dominance, the stronger signal (a memory
|
|
77
|
+
* dominating multiple DIFFERENT queries is a fixed-point anchor; query_repeat
|
|
78
|
+
* alone is just a literal re-ask).
|
|
80
79
|
*
|
|
81
80
|
* Cooldown: if the immediately-prior recall fired a hint on the SAME
|
|
82
81
|
* topMemoryId within `cooldown=3` history entries, suppress. Prevents
|
|
83
82
|
* spam when the agent repeatedly recalls within the dominance window.
|
|
84
|
-
* Cooldown is per-memory, not per-rule: if
|
|
85
|
-
* engaged for M), and the next recall has top=N + repeated query
|
|
86
|
-
*
|
|
83
|
+
* Cooldown is per-memory, not per-rule: if memory_dominance fired on M
|
|
84
|
+
* (cooldown engaged for M), and the next recall has top=N + repeated query,
|
|
85
|
+
* query_repeat fires on N (different memory, not in cooldown).
|
|
87
86
|
*
|
|
88
87
|
* @returns AnchoringHint when a pattern fires; null otherwise.
|
|
89
88
|
*/
|
package/dist/recall-history.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Anchoring detector (recall-recurrence), pure module.
|
|
3
3
|
*
|
|
4
|
-
* Implements two detection rules
|
|
5
|
-
*
|
|
4
|
+
* Implements two detection rules:
|
|
5
|
+
* query_repeat: same queryHash within recentRepeatWindow returned
|
|
6
6
|
* same topMemoryId (caller is re-asking the same question).
|
|
7
|
-
*
|
|
7
|
+
* memory_dominance: same topMemoryId across >= minDominance distinct
|
|
8
8
|
* queryHashes (memory acts as a fixed-point anchor regardless of what
|
|
9
9
|
* the agent asks).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Each pipeline (api.recall via
|
|
12
12
|
* HTTP, cmdRecall, MCP hippo_recall) owns its OWN ring buffer Map keyed
|
|
13
13
|
* by (tenant, session). No cross-pipeline sharing (the typical multi-
|
|
14
14
|
* process deployment makes IPC ring-sharing impractical; per-pipeline
|
|
15
15
|
* is correct because each pipeline has its own top-1 ranking anyway).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* Composes with J3.2: AnchoringHint + PlanningFallacyHint are independent
|
|
17
|
+
* AnchoringHint + PlanningFallacyHint are independent
|
|
19
18
|
* signals; both can fire on the same recall.
|
|
20
19
|
*/
|
|
21
20
|
import { envAnchoringOff, envAvailabilityOff } from './env.js';
|
|
@@ -31,44 +30,27 @@ const DEFAULT_COOLDOWN = 3;
|
|
|
31
30
|
* sort tokens → join → FNV-1a 32-bit.
|
|
32
31
|
*
|
|
33
32
|
* Token sort + dedup means semantically-equivalent queries with reordered
|
|
34
|
-
* words collide intentionally (
|
|
35
|
-
*
|
|
33
|
+
* words collide intentionally ("semantically-distinct" is approximated by
|
|
34
|
+
* textual normalization, not embeddings).
|
|
36
35
|
*
|
|
37
36
|
* Deterministic across processes; stable across Node + V8 versions.
|
|
38
37
|
*/
|
|
39
38
|
export function hashQueryText(query) {
|
|
40
39
|
if (!query)
|
|
41
40
|
return 0;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// variations (typos, doubled tokens, intensifiers) would inflate
|
|
46
|
-
// distinct-query counts and trip R2 on essentially the same question.
|
|
47
|
-
// Codex round-1 catch.
|
|
48
|
-
// Unicode-aware tokenization (codex round-3 P2 catch): the prior
|
|
49
|
-
// ASCII-only [^a-z0-9\s] pattern stripped every non-Latin letter, so
|
|
50
|
-
// Japanese, Arabic, Cyrillic, accented-Latin etc. queries collapsed
|
|
51
|
-
// to empty token set -> hash 0 -> false R1 collisions across distinct
|
|
52
|
-
// non-English queries. \p{L} = any Unicode letter, \p{N} = any Unicode
|
|
53
|
-
// number, \p{M} = combining marks (preserve composed accented chars).
|
|
54
|
-
// Requires the /u flag and Node >= 12.
|
|
41
|
+
// Tokens are deduped before join so `foo foo bar` and `foo bar` count as one query for memory_dominance.
|
|
42
|
+
// Unicode classes (\p{L} letter, \p{N} number, \p{M} combining mark; needs /u) keep
|
|
43
|
+
// non-Latin queries from collapsing to an empty token set and colliding at hash 0.
|
|
55
44
|
const normalized = query
|
|
56
45
|
.toLowerCase()
|
|
57
46
|
.replace(/[^\p{L}\p{N}\p{M}\s]/gu, ' ')
|
|
58
47
|
.split(/\s+/)
|
|
59
48
|
.filter((t) => t.length > 0);
|
|
60
|
-
// Drop tokens
|
|
61
|
-
//
|
|
62
|
-
// hash differently and inflate R2 distinct-query counts, firing
|
|
63
|
-
// memory_dominance on repeated phrasings of the same question.
|
|
64
|
-
// Codex round-4 P2 catch. Matches the same >=3 filter in
|
|
65
|
-
// src/forward-claim-detector.ts for class-resolver tokens.
|
|
49
|
+
// Drop tokens under 3 chars (filler words) so `a login bug` and `login bug` hash
|
|
50
|
+
// alike; matches the >=3 filter in src/forward-claim-detector.ts.
|
|
66
51
|
const filtered = normalized.filter((t) => t.length >= 3);
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// acronyms like `AI` / `UI` are 2 chars. Without this fallback they
|
|
70
|
-
// all hash to fnv1a32(''), producing false R1 collisions across
|
|
71
|
-
// distinct short-token queries. Codex round-5 P2 catch.
|
|
52
|
+
// Fall back to all tokens when the filter empties the query (CJK 2-char words,
|
|
53
|
+
// acronyms like `AI`) so distinct short queries do not all hash to fnv1a32('').
|
|
72
54
|
const tokens = filtered.length > 0 ? filtered : normalized;
|
|
73
55
|
const deduped = Array.from(new Set(tokens)).sort();
|
|
74
56
|
return fnv1a32(deduped.join(' '));
|
|
@@ -91,17 +73,17 @@ function fnv1a32(text) {
|
|
|
91
73
|
* Detect anchoring patterns in the recall history against the current
|
|
92
74
|
* recall's (queryHash, topMemoryId).
|
|
93
75
|
*
|
|
94
|
-
* Rule precedence:
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
76
|
+
* Rule precedence: memory_dominance wins on tie. When both rules fire on the
|
|
77
|
+
* same recall, return only memory_dominance, the stronger signal (a memory
|
|
78
|
+
* dominating multiple DIFFERENT queries is a fixed-point anchor; query_repeat
|
|
79
|
+
* alone is just a literal re-ask).
|
|
98
80
|
*
|
|
99
81
|
* Cooldown: if the immediately-prior recall fired a hint on the SAME
|
|
100
82
|
* topMemoryId within `cooldown=3` history entries, suppress. Prevents
|
|
101
83
|
* spam when the agent repeatedly recalls within the dominance window.
|
|
102
|
-
* Cooldown is per-memory, not per-rule: if
|
|
103
|
-
* engaged for M), and the next recall has top=N + repeated query
|
|
104
|
-
*
|
|
84
|
+
* Cooldown is per-memory, not per-rule: if memory_dominance fired on M
|
|
85
|
+
* (cooldown engaged for M), and the next recall has top=N + repeated query,
|
|
86
|
+
* query_repeat fires on N (different memory, not in cooldown).
|
|
105
87
|
*
|
|
106
88
|
* @returns AnchoringHint when a pattern fires; null otherwise.
|
|
107
89
|
*/
|
|
@@ -119,7 +101,7 @@ export function detectAnchoring(history, currentQueryHash, currentTopMemoryId, o
|
|
|
119
101
|
return null;
|
|
120
102
|
}
|
|
121
103
|
}
|
|
122
|
-
//
|
|
104
|
+
// memory_dominance check FIRST (wins on tie). Count distinct queryHashes in history
|
|
123
105
|
// where topMemoryId === currentTopMemoryId (excluding null tops).
|
|
124
106
|
const matchingQueryHashes = new Set();
|
|
125
107
|
for (const entry of history) {
|
|
@@ -139,7 +121,7 @@ export function detectAnchoring(history, currentQueryHash, currentTopMemoryId, o
|
|
|
139
121
|
source: 'j1-recurrence',
|
|
140
122
|
};
|
|
141
123
|
}
|
|
142
|
-
//
|
|
124
|
+
// query_repeat check: is currentQueryHash present in the last `recentRepeatWindow`
|
|
143
125
|
// entries AND was that entry's topMemoryId === currentTopMemoryId?
|
|
144
126
|
const r1Slice = history.slice(-recentRepeatWindow);
|
|
145
127
|
for (const entry of r1Slice) {
|
package/dist/recall-pipeline.js
CHANGED
|
@@ -11,6 +11,7 @@ import { DEFAULT_GRAPH_STREAM_WEIGHT } from './graph-stream.js';
|
|
|
11
11
|
import { Layer } from './memory.js';
|
|
12
12
|
import { multihopSearch } from './multihop.js';
|
|
13
13
|
import { passesCliRecallScopeFilter } from './recall-scope.js';
|
|
14
|
+
import { currentEntries } from './search/as-of.js';
|
|
14
15
|
import { hybridSearch } from './search/hybrid.js';
|
|
15
16
|
import { physicsSearch } from './search/physics-search.js';
|
|
16
17
|
import { searchBothHybrid } from './shared.js';
|
|
@@ -72,36 +73,11 @@ function loadRecallPool(ctx, opts) {
|
|
|
72
73
|
const passes = (e) => passesCliRecallScopeFilter(e.scope ?? null, requested);
|
|
73
74
|
local = local.filter(passes);
|
|
74
75
|
global = global.filter(passes);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
else if (!opts.includeSuperseded) {
|
|
80
|
-
local = local.filter((e) => !e.superseded_by);
|
|
81
|
-
global = global.filter((e) => !e.superseded_by);
|
|
82
|
-
}
|
|
76
|
+
const currentness = { asOf: opts.asOf, includeSuperseded: opts.includeSuperseded };
|
|
77
|
+
local = currentEntries(local, currentness);
|
|
78
|
+
global = currentEntries(global, currentness);
|
|
83
79
|
return { local, global, total, dropped: total - (local.length + global.length) };
|
|
84
80
|
}
|
|
85
|
-
/** Rows that were true at `asOf`: valid by then, and not yet replaced by a successor in the same pool. */
|
|
86
|
-
function currentAsOf(entries, asOf) {
|
|
87
|
-
const asOfDate = new Date(asOf);
|
|
88
|
-
const successorValidFrom = new Map();
|
|
89
|
-
for (const e of entries) {
|
|
90
|
-
if (e.superseded_by) {
|
|
91
|
-
const successor = entries.find(s => s.id === e.superseded_by);
|
|
92
|
-
if (successor)
|
|
93
|
-
successorValidFrom.set(e.id, successor.valid_from);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return entries.filter(e => {
|
|
97
|
-
if (new Date(e.valid_from) > asOfDate)
|
|
98
|
-
return false;
|
|
99
|
-
if (!e.superseded_by)
|
|
100
|
-
return true;
|
|
101
|
-
const succVf = successorValidFrom.get(e.id);
|
|
102
|
-
return succVf ? new Date(succVf) > asOfDate : true;
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
81
|
async function searchPool(ctx, opts, pool) {
|
|
106
82
|
const { query, budget, cost, includeSuperseded, asOf, activeScope: scope, search } = opts;
|
|
107
83
|
const { mmr, mmrLambda, minResults, explain } = search;
|
package/dist/recall-scope.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* cycle. Mirrors the v39 `project-identity.ts` precedent. api.ts imports
|
|
2
|
+
* Recall-side scope predicates, in a leaf module so shared.ts (which api.ts
|
|
3
|
+
* imports) can apply the same default-deny rule to searchBothHybrid's internal
|
|
4
|
+
* candidate loads without an import cycle. api.ts imports
|
|
6
5
|
* these for its own call sites AND re-exports them for back-compat
|
|
7
6
|
* (`api.isPrivateScope`, test imports of `passesScopeFilterForRecall`).
|
|
8
7
|
*/
|
|
@@ -30,7 +29,7 @@ export declare const RECALL_DEFAULT_DENY_SCOPES: readonly ["unknown:legacy"];
|
|
|
30
29
|
*/
|
|
31
30
|
export declare function assertNonEmpty<T>(arr: readonly T[], name: string): void;
|
|
32
31
|
/**
|
|
33
|
-
*
|
|
32
|
+
* Source-agnostic private-scope detector. A scope string is treated
|
|
34
33
|
* as private when it has the shape `<lowercase-source>:private:<rest>`.
|
|
35
34
|
*
|
|
36
35
|
* Examples that match:
|
|
@@ -56,14 +55,14 @@ export declare function isPrivateScope(scope: string | null | undefined): boolea
|
|
|
56
55
|
* `<source>:private:*` scope and on the `RECALL_DEFAULT_DENY_SCOPES`
|
|
57
56
|
* quarantine buckets. `null` and public scopes pass.
|
|
58
57
|
*
|
|
59
|
-
* @internal
|
|
58
|
+
* @internal Exported for test parity with
|
|
60
59
|
* `RECALL_DEFAULT_DENY_SCOPES` (single-source-of-truth verification). NOT part
|
|
61
60
|
* of the public API surface; not re-exported from `src/index.ts`. Subject to
|
|
62
61
|
* change without semver bump.
|
|
63
62
|
*/
|
|
64
63
|
export declare function passesScopeFilterForRecall(scope: string | null, requested: string | undefined): boolean;
|
|
65
64
|
/**
|
|
66
|
-
*
|
|
65
|
+
* The CLI `--scope` variant of the recall filter (JS half of the
|
|
67
66
|
* SQL 'default-deny-or-exact' mode in loadSearchRows).
|
|
68
67
|
*
|
|
69
68
|
* The CLI flag predates the envelope column as a TAG-boost ranking hint
|
|
@@ -101,7 +100,7 @@ export interface ScopeActor {
|
|
|
101
100
|
}
|
|
102
101
|
/** True when `actor` may read `scope`: admin always; member needs an exact grant on a restricted scope. */
|
|
103
102
|
export declare function canReadScope(actor: ScopeActor, scope: string): boolean;
|
|
104
|
-
/** Authorize an explicitly requested scope before any read honours it (
|
|
103
|
+
/** Authorize an explicitly requested scope before any read honours it (member scope grants). */
|
|
105
104
|
export declare function assertScopeRequestAllowed(actor: ScopeActor, requested: string | undefined): void;
|
|
106
105
|
/** Scope a derived row keeps from one source: the restricted scope itself, else null. */
|
|
107
106
|
export declare function derivationScope(scope: string | null | undefined): string | null;
|