hippo-memory 1.64.1 → 1.65.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/dist/agent-memories/sync.js +3 -2
- package/dist/api/recall-record.d.ts +46 -0
- package/dist/api/recall-record.js +98 -0
- package/dist/api/recall-request.d.ts +32 -0
- package/dist/api/recall-request.js +121 -0
- package/dist/api/recall-types.d.ts +7 -1
- package/dist/api/recall.d.ts +3 -2
- package/dist/api/recall.js +174 -181
- package/dist/api/tokens.d.ts +1 -1
- package/dist/api/tokens.js +13 -17
- package/dist/cli/recall.d.ts +1 -1
- package/dist/cli/recall.js +18 -49
- package/dist/cli/shared.js +2 -2
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +18 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +4 -0
- package/dist/db/connect.d.ts +5 -0
- package/dist/db/connect.js +52 -0
- package/dist/db/open.d.ts +9 -11
- package/dist/db/open.js +23 -116
- package/dist/db/request-stores.d.ts +30 -0
- package/dist/db/request-stores.js +100 -0
- package/dist/db/sqlite-blocked.d.ts +6 -0
- package/dist/db/sqlite-blocked.js +11 -0
- package/dist/db.d.ts +3 -1
- package/dist/db.js +3 -1
- package/dist/embeddings.js +2 -1
- package/dist/goals.d.ts +18 -11
- package/dist/goals.js +38 -36
- package/dist/http-util.d.ts +2 -0
- package/dist/http-util.js +5 -0
- package/dist/mcp/memory-tools.js +4 -1
- package/dist/mcp/recall-tools.js +28 -133
- package/dist/mcp/request.d.ts +7 -0
- package/dist/mcp/request.js +63 -41
- package/dist/mcp/session-state.d.ts +1 -3
- package/dist/mcp/session-state.js +3 -5
- package/dist/predictions/planning-fallacy.d.ts +52 -0
- package/dist/predictions/planning-fallacy.js +70 -55
- package/dist/recall-trace.d.ts +1 -1
- package/dist/recall-trace.js +5 -4
- package/dist/search/physics-search.js +4 -1
- package/dist/search/vector.js +2 -0
- package/dist/server/routes/recall.d.ts +1 -1
- package/dist/server/routes/recall.js +30 -218
- package/dist/server.d.ts +12 -1
- package/dist/server.js +33 -22
- package/dist/store/entry-writes.d.ts +2 -0
- package/dist/store/entry-writes.js +24 -16
- package/dist/store-port.d.ts +70 -1
- package/dist/store-port.js +87 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +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/mcp/request.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type McpContext, type McpRequest, type McpResponse } from './protocol.js';
|
|
2
|
+
/**
|
|
3
|
+
* Record the memory text a recall or context tool returned. Best-effort: a
|
|
4
|
+
* ledger failure never fails the tool call. Other tools are not recorded.
|
|
5
|
+
*/
|
|
6
|
+
export declare function recordMcpTokens(toolName: string, output: string, ctx?: McpContext): Promise<void>;
|
|
7
|
+
/** The tools listed and run under a store other than hippo.db. */
|
|
8
|
+
export declare const STORE_READY_TOOLS: ReadonlySet<string>;
|
|
2
9
|
/**
|
|
3
10
|
* Transport-agnostic MCP dispatcher. Both the stdio loop (below) and the
|
|
4
11
|
* HTTP/SSE transport in src/server.ts route every incoming JSON-RPC message
|
package/dist/mcp/request.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// Transport-agnostic request handling: tool dispatch table, tool execution and the JSON-RPC method switch.
|
|
2
2
|
import { log } from '../log.js';
|
|
3
|
+
import { STORE_NOT_PORTED_MESSAGE } from '../http-util.js';
|
|
3
4
|
import { getGlobalRoot, initGlobal } from '../shared.js';
|
|
4
5
|
import { loadConfig } from '../config.js';
|
|
5
6
|
import { resolveTenantId } from '../tenant.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { rethrowIfSqliteBlocked, runWithRequestStores } from '../db.js';
|
|
8
|
+
import { storeFor } from '../store-port.js';
|
|
9
|
+
import { estimateTokens } from '../token-ledger.js';
|
|
8
10
|
import { PACKAGE_VERSION } from '../version.js';
|
|
9
11
|
import { validateToolArgs } from './tool-args.js';
|
|
12
|
+
import { RecallRequestError } from '../api/recall-request.js';
|
|
10
13
|
import { findHippoRoot, isJsonObjectRecord } from './protocol.js';
|
|
11
14
|
import { TOOLS, TOOLS_BY_NAME, ARGS_CHECKED_BY_API } from './tools.js';
|
|
12
15
|
import { runRecallTool, runAssembleTool, runDrillTool, runContextTool } from './recall-tools.js';
|
|
@@ -35,7 +38,7 @@ const MCP_TOKEN_SURFACES = new Map([
|
|
|
35
38
|
* Record the memory text a recall or context tool returned. Best-effort: a
|
|
36
39
|
* ledger failure never fails the tool call. Other tools are not recorded.
|
|
37
40
|
*/
|
|
38
|
-
function recordMcpTokens(toolName, output, ctx) {
|
|
41
|
+
export async function recordMcpTokens(toolName, output, ctx) {
|
|
39
42
|
const surface = MCP_TOKEN_SURFACES.get(toolName);
|
|
40
43
|
if (!surface || !output)
|
|
41
44
|
return;
|
|
@@ -43,40 +46,41 @@ function recordMcpTokens(toolName, output, ctx) {
|
|
|
43
46
|
const hippoRoot = ctx?.hippoRoot ?? findHippoRoot();
|
|
44
47
|
if (!hippoRoot)
|
|
45
48
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
tokens: estimateTokens(output),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
finally {
|
|
57
|
-
closeHippoDb(db);
|
|
58
|
-
}
|
|
49
|
+
await storeFor({ hippoRoot, store: ctx?.store }).recordTokens({
|
|
50
|
+
tenantId: ctx?.tenantId ?? resolveTenantId({}),
|
|
51
|
+
surface,
|
|
52
|
+
event: 'inject',
|
|
53
|
+
items: 0,
|
|
54
|
+
tokens: estimateTokens(output),
|
|
55
|
+
});
|
|
59
56
|
}
|
|
60
57
|
catch (err) {
|
|
58
|
+
rethrowIfSqliteBlocked(err);
|
|
61
59
|
log.warnThenDebug('mcp-token-ledger', `token ledger write failed; the tool reply is unaffected: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
60
|
}
|
|
63
61
|
}
|
|
64
|
-
// ── Tool execution ──
|
|
65
62
|
const TOOL_HANDLERS = new Map([
|
|
66
|
-
['hippo_recall', runRecallTool],
|
|
67
|
-
['hippo_assemble', runAssembleTool],
|
|
68
|
-
['hippo_drill', runDrillTool],
|
|
69
|
-
['hippo_predict_baserate', runPredictBaserateTool],
|
|
70
|
-
['hippo_remember', runRememberTool],
|
|
71
|
-
['hippo_outcome', runOutcomeTool],
|
|
72
|
-
['hippo_context', runContextTool],
|
|
73
|
-
['hippo_status', runStatusTool],
|
|
74
|
-
['hippo_learn', runLearnTool],
|
|
75
|
-
['hippo_conflicts', runConflictsTool],
|
|
76
|
-
['hippo_resolve', runResolveTool],
|
|
77
|
-
['hippo_share', runShareTool],
|
|
78
|
-
['hippo_peers', runPeersTool],
|
|
63
|
+
['hippo_recall', { handler: runRecallTool, storeReady: true }],
|
|
64
|
+
['hippo_assemble', { handler: runAssembleTool }],
|
|
65
|
+
['hippo_drill', { handler: runDrillTool }],
|
|
66
|
+
['hippo_predict_baserate', { handler: runPredictBaserateTool }],
|
|
67
|
+
['hippo_remember', { handler: runRememberTool }],
|
|
68
|
+
['hippo_outcome', { handler: runOutcomeTool }],
|
|
69
|
+
['hippo_context', { handler: runContextTool }],
|
|
70
|
+
['hippo_status', { handler: runStatusTool }],
|
|
71
|
+
['hippo_learn', { handler: runLearnTool }],
|
|
72
|
+
['hippo_conflicts', { handler: runConflictsTool }],
|
|
73
|
+
['hippo_resolve', { handler: runResolveTool }],
|
|
74
|
+
['hippo_share', { handler: runShareTool }],
|
|
75
|
+
['hippo_peers', { handler: runPeersTool }],
|
|
79
76
|
]);
|
|
77
|
+
/** The tools listed and run under a store other than hippo.db. */
|
|
78
|
+
export const STORE_READY_TOOLS = new Set([...TOOL_HANDLERS].filter(([, entry]) => entry.storeReady).map(([name]) => name));
|
|
79
|
+
/** The served store's kind when it is not hippo.db, else null. */
|
|
80
|
+
function otherStoreKind(ctx) {
|
|
81
|
+
const kind = ctx?.store?.kind;
|
|
82
|
+
return kind !== undefined && kind !== 'sqlite' ? kind : null;
|
|
83
|
+
}
|
|
80
84
|
async function executeTool(name, args, ctx) {
|
|
81
85
|
// When a transport hands us a context (HTTP path), trust it: the HTTP
|
|
82
86
|
// server already resolved hippoRoot from its bound opts and tenantId
|
|
@@ -89,13 +93,21 @@ async function executeTool(name, args, ctx) {
|
|
|
89
93
|
// tenant-isolated. Resolved once per tool call: prefer the transport's
|
|
90
94
|
// ctx.tenantId so an HTTP Bearer for tenant B doesn't drop to HIPPO_TENANT.
|
|
91
95
|
const tenantId = ctx?.tenantId ?? resolveTenantId({});
|
|
92
|
-
const handler = TOOL_HANDLERS.get(name);
|
|
96
|
+
const handler = TOOL_HANDLERS.get(name)?.handler;
|
|
93
97
|
// handleMcpRequest rejects names missing from TOOLS, so reaching here means TOOLS and this table drifted apart.
|
|
94
98
|
if (!handler)
|
|
95
99
|
throw new Error(`hippo-mcp: tool ${name} is declared but has no handler`);
|
|
96
100
|
return handler({ args, ctx, hippoRoot, config, tenantId });
|
|
97
101
|
}
|
|
98
102
|
// ── Request handling ──
|
|
103
|
+
// The MCP spec reports input validation as a tool result with isError, so the model can read it and retry.
|
|
104
|
+
function invalidArgs(id, toolName, problems) {
|
|
105
|
+
return {
|
|
106
|
+
jsonrpc: '2.0',
|
|
107
|
+
id,
|
|
108
|
+
result: { content: [{ type: 'text', text: `Invalid arguments for ${toolName}: ${problems.join('; ')}` }], isError: true },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
99
111
|
/**
|
|
100
112
|
* Transport-agnostic MCP dispatcher. Both the stdio loop (below) and the
|
|
101
113
|
* HTTP/SSE transport in src/server.ts route every incoming JSON-RPC message
|
|
@@ -119,7 +131,7 @@ export async function handleMcpRequest(req, ctx) {
|
|
|
119
131
|
case 'notifications/initialized':
|
|
120
132
|
return null;
|
|
121
133
|
case 'tools/list':
|
|
122
|
-
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
134
|
+
return { jsonrpc: '2.0', id, result: { tools: otherStoreKind(ctx) ? TOOLS.filter((t) => STORE_READY_TOOLS.has(t.name)) : TOOLS } };
|
|
123
135
|
case 'tools/call': {
|
|
124
136
|
const nameValue = params?.name;
|
|
125
137
|
const toolName = isJsonString(nameValue) ? nameValue : '';
|
|
@@ -127,6 +139,10 @@ export async function handleMcpRequest(req, ctx) {
|
|
|
127
139
|
if (!tool) {
|
|
128
140
|
return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${toolName.slice(0, 128)}` } };
|
|
129
141
|
}
|
|
142
|
+
// The same refusal a ported tool gives when it reaches hippo.db, so a client handles one shape.
|
|
143
|
+
if (otherStoreKind(ctx) && !STORE_READY_TOOLS.has(toolName)) {
|
|
144
|
+
return { jsonrpc: '2.0', id, error: { code: -32603, message: STORE_NOT_PORTED_MESSAGE } };
|
|
145
|
+
}
|
|
130
146
|
const refusal = sharedStoreRefusal(toolName, ctx);
|
|
131
147
|
if (refusal !== undefined)
|
|
132
148
|
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: refusal }], isError: true } };
|
|
@@ -135,17 +151,23 @@ export async function handleMcpRequest(req, ctx) {
|
|
|
135
151
|
return { jsonrpc: '2.0', id, error: { code: -32602, message: `${toolName}: arguments must be an object` } };
|
|
136
152
|
}
|
|
137
153
|
const toolArgs = isJsonObjectRecord(argumentsValue) ? argumentsValue : {};
|
|
138
|
-
// The MCP spec reports input validation as a tool result with isError, so the model can read it and retry.
|
|
139
154
|
const problems = validateToolArgs(tool.inputSchema, toolArgs, ARGS_CHECKED_BY_API.get(toolName));
|
|
140
|
-
if (problems.length > 0)
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
if (problems.length > 0)
|
|
156
|
+
return invalidArgs(id, toolName, problems);
|
|
157
|
+
let output;
|
|
158
|
+
try {
|
|
159
|
+
// One handle per store for the tool and its ledger row; stdio interleaves calls, so each gets its own scope.
|
|
160
|
+
output = await runWithRequestStores(async () => {
|
|
161
|
+
const text = await executeTool(toolName, toolArgs, ctx);
|
|
162
|
+
await recordMcpTokens(toolName, text, ctx);
|
|
163
|
+
return text;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (!(err instanceof RecallRequestError))
|
|
168
|
+
throw err;
|
|
169
|
+
return invalidArgs(id, toolName, [err.message]);
|
|
146
170
|
}
|
|
147
|
-
const output = await executeTool(toolName, toolArgs, ctx);
|
|
148
|
-
recordMcpTokens(toolName, output, ctx);
|
|
149
171
|
return {
|
|
150
172
|
jsonrpc: '2.0',
|
|
151
173
|
id,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const sessionRecallHistoryMcp: Map<string, RingBuffer>;
|
|
3
|
-
/** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
|
|
1
|
+
/** Test-only: reset the MCP recall rings. Call from beforeEach. */
|
|
4
2
|
export declare function __resetSessionRecallHistoryMcp(): void;
|
|
5
3
|
export declare const MAX_RECALL_CLIENTS = 4096;
|
|
6
4
|
export declare const lastRecalledIds: Map<string, string[]>;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// Per-process state the MCP tools share across calls: recall rings, last recalled ids, auto-sleep runs.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const sessionRecallHistoryMcp = new Map();
|
|
5
|
-
/** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
|
|
2
|
+
import { resetSessionRings } from '../api/recall-record.js';
|
|
3
|
+
/** Test-only: reset the MCP recall rings. Call from beforeEach. */
|
|
6
4
|
export function __resetSessionRecallHistoryMcp() {
|
|
7
|
-
|
|
5
|
+
resetSessionRings('mcp');
|
|
8
6
|
}
|
|
9
7
|
// ── Track last recalled IDs for outcome feedback ──
|
|
10
8
|
//
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type AppendAuditOpts } from '../audit.js';
|
|
2
|
+
import { type ForwardClaimMatch } from '../forward-claim-detector.js';
|
|
3
|
+
import { type PredictionBaserate } from './store.js';
|
|
1
4
|
/**
|
|
2
5
|
* Surface delivered on `RecallResult.planningFallacyHint` when an
|
|
3
6
|
* agent's recall query carries a forward-prediction phrase AND the closest
|
|
@@ -66,6 +69,49 @@ export interface ComputePlanningFallacyHintOpts {
|
|
|
66
69
|
* carry the right attribution instead of the 'cli' default. */
|
|
67
70
|
actor?: string;
|
|
68
71
|
}
|
|
72
|
+
export interface ClassResolution {
|
|
73
|
+
classTag: string | null;
|
|
74
|
+
/** True when ≥2 classes tied at the best overlap score AND best ≥ 1.
|
|
75
|
+
* Caller emits `recall_autodebias_hint_tiebreak` audit and returns
|
|
76
|
+
* null hint (silent — prevents the "show wrong class half the time"
|
|
77
|
+
* failure mode the alphabetical-tiebreak alternative would create). */
|
|
78
|
+
tiebreak: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** The prediction class a forward claim resolves to, and that class's closed-prediction stats when one resolved. */
|
|
81
|
+
export interface PlanningFallacyEvidence extends ClassResolution {
|
|
82
|
+
readonly baserate: PredictionBaserate | null;
|
|
83
|
+
}
|
|
84
|
+
/** The output a claim earns from its evidence, and the audit row that records the decision when there is one. */
|
|
85
|
+
export interface PlanningFallacyDecision {
|
|
86
|
+
output: PlanningFallacyOutput;
|
|
87
|
+
audit?: AppendAuditOpts;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a query-token set to a unique best-matching class_tag for the
|
|
91
|
+
* tenant. Scores by lower-cased token overlap; requires best score ≥ 1
|
|
92
|
+
* AND strictly greater than the 2nd-best score.
|
|
93
|
+
*
|
|
94
|
+
* Indexed via idx_predictions_tenant_class (db.ts:1015) → O(log n) seek
|
|
95
|
+
* plus a small DISTINCT scan over the per-tenant class-tag set.
|
|
96
|
+
*
|
|
97
|
+
* Scope behaviour (deliberate): class_tag selection is TENANT-GLOBAL,
|
|
98
|
+
* NOT scope-filtered against
|
|
99
|
+
* the recall's opts.scope. The class_tag is an aggregator label across
|
|
100
|
+
* historical predictions in the class, not a per-memory scope-bound
|
|
101
|
+
* property. A no-scope recall CAN surface a class_tag from a privately-
|
|
102
|
+
* scoped prediction's class in PlanningFallacyHint.classTag — by design,
|
|
103
|
+
* because base-rate reasoning needs the full historical sample.
|
|
104
|
+
* Implications:
|
|
105
|
+
* - The hint payload itself carries no memory content (only the aggregate
|
|
106
|
+
* summary string + numeric stats), so memory bodies do not leak.
|
|
107
|
+
* - The class_tag NAME is the side-channel. If sensitive labels are a
|
|
108
|
+
* concern, callers should either use opaque class names (e.g. hashes
|
|
109
|
+
* or numeric tokens) or set HIPPO_AUTODEBIAS=off.
|
|
110
|
+
* - tests/api-recall-autodebias.test.ts locks this with an explicit
|
|
111
|
+
* test asserting that scope-set predictions surface via no-scope
|
|
112
|
+
* recalls (so future "fix" attempts that scope-filter trip CI).
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolveClassFromTokens(hippoRoot: string, tenantId: string, queryTokens: readonly string[]): ClassResolution;
|
|
69
115
|
/**
|
|
70
116
|
* Planning-fallacy orchestrator.
|
|
71
117
|
*
|
|
@@ -92,4 +138,10 @@ export interface ComputePlanningFallacyHintOpts {
|
|
|
92
138
|
* Latency budget: well under 50ms; a miss pays only the regex.
|
|
93
139
|
*/
|
|
94
140
|
export declare function computePlanningFallacyOutput(hippoRoot: string, tenantId: string, queryText: string, opts?: ComputePlanningFallacyHintOpts): PlanningFallacyOutput;
|
|
141
|
+
/** The forward claim in a recall query; null when HIPPO_AUTODEBIAS=off, the query is empty or no claim matches. */
|
|
142
|
+
export declare function detectPlanningClaim(queryText: string, opts?: ComputePlanningFallacyHintOpts): ForwardClaimMatch | null;
|
|
143
|
+
/** Resolves a claim's tokens to one class and reads its baserate, writing no audit row. */
|
|
144
|
+
export declare function planningFallacyEvidenceAt(hippoRoot: string, tenantId: string, classQueryTokens: readonly string[]): PlanningFallacyEvidence;
|
|
145
|
+
/** The hint, the watching variant or nothing for a claim and its class evidence; reads and writes nothing itself. */
|
|
146
|
+
export declare function decidePlanningFallacy(match: ForwardClaimMatch, evidence: PlanningFallacyEvidence, tenantId: string, actor: string): PlanningFallacyDecision;
|
|
95
147
|
//# sourceMappingURL=planning-fallacy.d.ts.map
|
|
@@ -28,7 +28,7 @@ import { computePredictionBaserate } from './store.js';
|
|
|
28
28
|
* test asserting that scope-set predictions surface via no-scope
|
|
29
29
|
* recalls (so future "fix" attempts that scope-filter trip CI).
|
|
30
30
|
*/
|
|
31
|
-
function resolveClassFromTokens(hippoRoot, tenantId, queryTokens) {
|
|
31
|
+
export function resolveClassFromTokens(hippoRoot, tenantId, queryTokens) {
|
|
32
32
|
if (queryTokens.length === 0)
|
|
33
33
|
return { classTag: null, tiebreak: false };
|
|
34
34
|
const db = openHippoDb(hippoRoot);
|
|
@@ -100,56 +100,69 @@ function resolveClassFromTokens(hippoRoot, tenantId, queryTokens) {
|
|
|
100
100
|
* Latency budget: well under 50ms; a miss pays only the regex.
|
|
101
101
|
*/
|
|
102
102
|
export function computePlanningFallacyOutput(hippoRoot, tenantId, queryText, opts = {}) {
|
|
103
|
-
|
|
104
|
-
// (rather than module-load cache) is deliberate: tests env-toggle this
|
|
105
|
-
// via process.env mutation without module reload.
|
|
106
|
-
const mode = opts.mode ?? (envAutodebiasOff() ? 'off' : 'regex');
|
|
107
|
-
if (mode === 'off')
|
|
108
|
-
return {};
|
|
109
|
-
if (!queryText)
|
|
110
|
-
return {};
|
|
111
|
-
const match = detectForwardClaim(queryText);
|
|
103
|
+
const match = detectPlanningClaim(queryText, opts);
|
|
112
104
|
if (!match)
|
|
113
105
|
return {};
|
|
114
106
|
const actor = opts.actor ?? 'recall';
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
107
|
+
const { output, audit } = decidePlanningFallacy(match, planningFallacyEvidenceAt(hippoRoot, tenantId, match.classQueryTokens), tenantId, actor);
|
|
108
|
+
if (audit)
|
|
109
|
+
appendAuditEventOnce(hippoRoot, audit);
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
/** The forward claim in a recall query; null when HIPPO_AUTODEBIAS=off, the query is empty or no claim matches. */
|
|
113
|
+
export function detectPlanningClaim(queryText, opts = {}) {
|
|
114
|
+
// Env read FIRST so AUTODEBIAS=off pays zero regex cost; read per call so tests can toggle it without a module reload.
|
|
115
|
+
const mode = opts.mode ?? (envAutodebiasOff() ? 'off' : 'regex');
|
|
116
|
+
if (mode === 'off')
|
|
117
|
+
return null;
|
|
118
|
+
if (!queryText)
|
|
119
|
+
return null;
|
|
120
|
+
return detectForwardClaim(queryText);
|
|
121
|
+
}
|
|
122
|
+
/** Resolves a claim's tokens to one class and reads its baserate, writing no audit row. */
|
|
123
|
+
export function planningFallacyEvidenceAt(hippoRoot, tenantId, classQueryTokens) {
|
|
124
|
+
const resolution = resolveClassFromTokens(hippoRoot, tenantId, classQueryTokens);
|
|
125
|
+
// emitAudit=false: the recall_autodebias_hint row carries n_closed and mean_ratio, so predict_baserate stays off.
|
|
126
|
+
const baserate = resolution.classTag ? computePredictionBaserate(hippoRoot, tenantId, resolution.classTag, 'recall', false) : null;
|
|
127
|
+
return { ...resolution, baserate };
|
|
128
|
+
}
|
|
129
|
+
/** The hint, the watching variant or nothing for a claim and its class evidence; reads and writes nothing itself. */
|
|
130
|
+
export function decidePlanningFallacy(match, evidence, tenantId, actor) {
|
|
131
|
+
if (evidence.tiebreak) {
|
|
117
132
|
// Telemetry: forward-claim detected, ≥2 classes tied at best overlap. The watching variant lets the
|
|
118
133
|
// caller render "watching but no baserate (tiebreak)"; the audit channel stays the source of truth.
|
|
119
|
-
return watchingWithAudit(
|
|
134
|
+
return watchingWithAudit(tenantId, actor, match, 'tiebreak', TIEBREAK_SUGGESTION);
|
|
120
135
|
}
|
|
121
|
-
if (!
|
|
122
|
-
// Telemetry:
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
return watchingWithAudit(hippoRoot, tenantId, actor, match, 'no_class_match', NO_CLASS_MATCH_SUGGESTION);
|
|
136
|
+
if (!evidence.classTag) {
|
|
137
|
+
// Telemetry: no class scored ≥ 1. High volume here means regex+token-overlap misses real forward-claims
|
|
138
|
+
// with no class signal, the case for an embedding fallback.
|
|
139
|
+
return watchingWithAudit(tenantId, actor, match, 'no_class_match', NO_CLASS_MATCH_SUGGESTION);
|
|
126
140
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/*emitAudit=*/ false);
|
|
131
|
-
if (baserate.nClosed === 0)
|
|
132
|
-
return {}; // Silent — wait for closed data.
|
|
133
|
-
appendAuditEventOnce(hippoRoot, {
|
|
134
|
-
tenantId,
|
|
135
|
-
actor,
|
|
136
|
-
op: 'recall_autodebias_hint',
|
|
137
|
-
targetId: resolution.classTag,
|
|
138
|
-
metadata: {
|
|
139
|
-
class_tag: resolution.classTag,
|
|
140
|
-
detected_phrase: match.phrase,
|
|
141
|
-
n_closed: baserate.nClosed,
|
|
142
|
-
mean_ratio: baserate.meanRatio,
|
|
143
|
-
},
|
|
144
|
-
});
|
|
141
|
+
const baserate = evidence.baserate;
|
|
142
|
+
if (!baserate || baserate.nClosed === 0)
|
|
143
|
+
return { output: {} }; // Silent — wait for closed data.
|
|
145
144
|
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
output: {
|
|
146
|
+
hint: {
|
|
147
|
+
classTag: evidence.classTag,
|
|
148
|
+
baserateSummary: baserate.summary,
|
|
149
|
+
source: 'j3.2-auto',
|
|
150
|
+
detectedPhrase: match.phrase,
|
|
151
|
+
nClosed: baserate.nClosed,
|
|
152
|
+
meanRatio: baserate.meanRatio,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
audit: {
|
|
156
|
+
tenantId,
|
|
157
|
+
actor,
|
|
158
|
+
op: 'recall_autodebias_hint',
|
|
159
|
+
targetId: evidence.classTag,
|
|
160
|
+
metadata: {
|
|
161
|
+
class_tag: evidence.classTag,
|
|
162
|
+
detected_phrase: match.phrase,
|
|
163
|
+
n_closed: baserate.nClosed,
|
|
164
|
+
mean_ratio: baserate.meanRatio,
|
|
165
|
+
},
|
|
153
166
|
},
|
|
154
167
|
};
|
|
155
168
|
}
|
|
@@ -165,19 +178,21 @@ function appendAuditEventOnce(hippoRoot, event) {
|
|
|
165
178
|
closeHippoDb(db);
|
|
166
179
|
}
|
|
167
180
|
}
|
|
168
|
-
function watchingWithAudit(
|
|
169
|
-
appendAuditEventOnce(hippoRoot, {
|
|
170
|
-
tenantId,
|
|
171
|
-
actor,
|
|
172
|
-
op: reason === 'tiebreak' ? 'recall_autodebias_hint_tiebreak' : 'recall_autodebias_hint_no_class_match',
|
|
173
|
-
targetId: match.phrase.slice(0, 100),
|
|
174
|
-
metadata: { detected_phrase: match.phrase, token_count: match.classQueryTokens.length },
|
|
175
|
-
});
|
|
181
|
+
function watchingWithAudit(tenantId, actor, match, reason, suggestion) {
|
|
176
182
|
return {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
output: {
|
|
184
|
+
watching: {
|
|
185
|
+
detectedPhrase: match.phrase,
|
|
186
|
+
reason,
|
|
187
|
+
suggestion,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
audit: {
|
|
191
|
+
tenantId,
|
|
192
|
+
actor,
|
|
193
|
+
op: reason === 'tiebreak' ? 'recall_autodebias_hint_tiebreak' : 'recall_autodebias_hint_no_class_match',
|
|
194
|
+
targetId: match.phrase.slice(0, 100),
|
|
195
|
+
metadata: { detected_phrase: match.phrase, token_count: match.classQueryTokens.length },
|
|
181
196
|
},
|
|
182
197
|
};
|
|
183
198
|
}
|
package/dist/recall-trace.d.ts
CHANGED
|
@@ -173,7 +173,7 @@ export interface DeliveryEventRow {
|
|
|
173
173
|
}
|
|
174
174
|
/** One event plus its candidates in one write transaction, then prune; fail-soft. The caller must not hold a transaction on `db`. */
|
|
175
175
|
export declare function writeDeliveryEvent(db: DatabaseSyncLike, input: DeliveryEventInput): number | null;
|
|
176
|
-
/** Write on a short-lived
|
|
176
|
+
/** Write on the request's handle, or a short-lived one, waiting at most {@link DELIVERY_LEDGER_WAIT_MS} for the lock. Fail-soft. */
|
|
177
177
|
export declare function writeDeliveryEventAtRoot(root: string, input: DeliveryEventInput): number | null;
|
|
178
178
|
/** On a caller's open handle, which saves a second open and close per turn; the handle's own lock wait comes back after. */
|
|
179
179
|
export declare function writeDeliveryEventOnHandle(db: DatabaseSyncLike, input: DeliveryEventInput): number | null;
|
package/dist/recall-trace.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* new debug env var).
|
|
16
16
|
*/
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
|
-
import { openHippoDb, closeHippoDb } from './db.js';
|
|
18
|
+
import { openHippoDb, closeHippoDb, rethrowIfSqliteBlocked } from './db.js';
|
|
19
19
|
import { DELIVERY_LEDGER_VERSION } from './delivery-recorder.js';
|
|
20
20
|
import { log } from './log.js';
|
|
21
21
|
/**
|
|
@@ -109,6 +109,7 @@ export function writeRecallTraceAtRoot(root, input) {
|
|
|
109
109
|
db = openHippoDb(root);
|
|
110
110
|
}
|
|
111
111
|
catch (error) {
|
|
112
|
+
rethrowIfSqliteBlocked(error);
|
|
112
113
|
log.error(`recall trace connection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
114
|
return null;
|
|
114
115
|
}
|
|
@@ -267,11 +268,11 @@ export function writeDeliveryEvent(db, input) {
|
|
|
267
268
|
return null;
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
|
-
/** Write on a short-lived
|
|
271
|
+
/** Write on the request's handle, or a short-lived one, waiting at most {@link DELIVERY_LEDGER_WAIT_MS} for the lock. Fail-soft. */
|
|
271
272
|
export function writeDeliveryEventAtRoot(root, input) {
|
|
272
273
|
let db;
|
|
273
274
|
try {
|
|
274
|
-
db = openHippoDb(root
|
|
275
|
+
db = openHippoDb(root);
|
|
275
276
|
}
|
|
276
277
|
catch (error) {
|
|
277
278
|
// Same hook stderr line as writeDeliveryEvent above.
|
|
@@ -279,7 +280,7 @@ export function writeDeliveryEventAtRoot(root, input) {
|
|
|
279
280
|
return null;
|
|
280
281
|
}
|
|
281
282
|
try {
|
|
282
|
-
return
|
|
283
|
+
return writeDeliveryEventOnHandle(db, input);
|
|
283
284
|
}
|
|
284
285
|
finally {
|
|
285
286
|
closeHippoDb(db);
|
|
@@ -6,7 +6,7 @@ import { resolveEmbeddingProvider } from '../embedding-provider.js';
|
|
|
6
6
|
import { physicsScore as computePhysicsScores, computeMass } from '../physics.js';
|
|
7
7
|
import { DEFAULT_PHYSICS_CONFIG } from '../physics-config.js';
|
|
8
8
|
import { loadPhysicsState } from '../physics-state.js';
|
|
9
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
9
|
+
import { openHippoDb, closeHippoDb, rethrowIfSqliteBlocked } from '../db.js';
|
|
10
10
|
import { compareScoredResults } from '../compare.js';
|
|
11
11
|
import { log } from '../log.js';
|
|
12
12
|
import { churnStaleFactor, summaryMultipliers, summaryScoring } from './boosts.js';
|
|
@@ -57,6 +57,7 @@ async function physicsQueryVector(query, root, given) {
|
|
|
57
57
|
return vec && vec.length > 0 ? vec : null;
|
|
58
58
|
}
|
|
59
59
|
catch (err) {
|
|
60
|
+
rethrowIfSqliteBlocked(err);
|
|
60
61
|
log.debug(`physics search: query embed failed, using hybrid: ${err instanceof Error ? err.message : String(err)}`);
|
|
61
62
|
return null;
|
|
62
63
|
}
|
|
@@ -68,6 +69,7 @@ function withVectorCandidates(root, entries, queryVector, spec) {
|
|
|
68
69
|
return [...entries, ...vectorCandidatesOutside(root, entries, queryVector, spec)];
|
|
69
70
|
}
|
|
70
71
|
catch (err) {
|
|
72
|
+
rethrowIfSqliteBlocked(err);
|
|
71
73
|
log.warn(`physics search ranked the lexical pool only; the vector lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
74
|
return entries;
|
|
73
75
|
}
|
|
@@ -84,6 +86,7 @@ function loadCandidateParticles(root, pool) {
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
catch (err) {
|
|
89
|
+
rethrowIfSqliteBlocked(err);
|
|
87
90
|
log.debug(`physics search: state load failed, using hybrid: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
91
|
return null;
|
|
89
92
|
}
|
package/dist/search/vector.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cosineSimilarity, embeddingModelRequiresReindex, hasEmbeddings, loadStoredVectors } from '../embeddings.js';
|
|
2
2
|
import { loadVectorCandidateEntries } from '../store/search-rows.js';
|
|
3
3
|
import { resolveEmbeddingProvider } from '../embedding-provider.js';
|
|
4
|
+
import { rethrowIfSqliteBlocked } from '../db.js';
|
|
4
5
|
import { log } from '../log.js';
|
|
5
6
|
import { redactSecretsStrict } from '../secret-detect.js';
|
|
6
7
|
import { currentEntries } from './as-of.js';
|
|
@@ -22,6 +23,7 @@ export async function resolveVectorArm(query, entries, options) {
|
|
|
22
23
|
await fillVectorArm(arm, query, options.hippoRoot, options);
|
|
23
24
|
}
|
|
24
25
|
catch (err) {
|
|
26
|
+
rethrowIfSqliteBlocked(err);
|
|
25
27
|
warnBm25Fallback('error', err instanceof Error ? err.message : String(err));
|
|
26
28
|
}
|
|
27
29
|
return arm;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RouteRequest } from '../types.js';
|
|
2
|
-
/** Test-only: reset the
|
|
2
|
+
/** Test-only: reset the HTTP recall rings. Call from beforeEach. */
|
|
3
3
|
export declare function __resetSessionRecallHistoryHttp(): void;
|
|
4
4
|
export declare function handleRecallMemories({ req, res, opts, query }: RouteRequest): Promise<void>;
|
|
5
5
|
export declare function handleAssembleSession({ req, res, opts, query }: RouteRequest, assembleMatch: Record<string, string>): Promise<void>;
|