hippo-memory 1.72.0 → 2.0.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 +6 -4
- package/dist/agent-memories/apply.js +5 -5
- package/dist/api/assemble.js +2 -1
- package/dist/api/audit.d.ts +7 -7
- package/dist/api/audit.js +19 -17
- package/dist/api/auth.d.ts +5 -6
- package/dist/api/auth.js +28 -33
- package/dist/api/conflicts.js +1 -1
- package/dist/api/context-auto.js +4 -2
- package/dist/api/context-select.js +8 -20
- package/dist/api/context.js +12 -10
- package/dist/api/drill-down.js +2 -1
- package/dist/api/forget.d.ts +2 -2
- package/dist/api/forget.js +6 -7
- package/dist/api/graph.js +2 -1
- package/dist/api/learn.d.ts +1 -1
- package/dist/api/learn.js +45 -44
- package/dist/api/memories.d.ts +4 -2
- package/dist/api/memories.js +16 -5
- package/dist/api/objects.js +2 -1
- package/dist/api/outcome.d.ts +5 -5
- package/dist/api/outcome.js +16 -15
- package/dist/api/predictions.js +2 -1
- package/dist/api/promote.d.ts +3 -3
- package/dist/api/promote.js +25 -27
- package/dist/api/prompt-hook.d.ts +3 -1
- package/dist/api/prompt-hook.js +1 -0
- package/dist/api/quarantine.js +2 -1
- package/dist/api/recall-core.js +8 -3
- package/dist/api/recall-pipeline.js +17 -17
- package/dist/api/recall-types.d.ts +3 -1
- package/dist/api/recall.js +32 -29
- package/dist/api/remember-local.d.ts +5 -2
- package/dist/api/remember-local.js +14 -6
- package/dist/api/remember.d.ts +5 -2
- package/dist/api/remember.js +16 -14
- package/dist/api/session-end.d.ts +24 -0
- package/dist/api/session-end.js +31 -1
- package/dist/api/sleep-run.js +5 -4
- package/dist/api/sleep.js +4 -3
- package/dist/api/tokens.js +1 -1
- package/dist/api/types.d.ts +1 -7
- package/dist/capture/command.js +1 -1
- package/dist/capture/compaction-spool.d.ts +3 -5
- package/dist/capture/compaction-spool.js +41 -46
- package/dist/capture/session-end-caller.js +1 -1
- package/dist/capture/session-worker.js +19 -6
- package/dist/cli/audit.js +10 -10
- package/dist/cli/auth.js +4 -4
- package/dist/cli/client.d.ts +1 -2
- package/dist/cli/client.js +13 -13
- package/dist/cli/context.js +1 -1
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +9 -9
- package/dist/cli/decisions.js +4 -5
- package/dist/cli/hook-runtime.d.ts +3 -1
- package/dist/cli/hook-runtime.js +2 -2
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +9 -9
- package/dist/cli/object-verbs.d.ts +2 -2
- package/dist/cli/object-verbs.js +4 -4
- package/dist/cli/predictions.js +2 -3
- package/dist/cli/quality-repair-once.d.ts +1 -1
- package/dist/cli/quality-repair-once.js +2 -2
- package/dist/cli/refine-llm.js +2 -1
- package/dist/cli/remember.js +5 -5
- package/dist/cli/serve.js +9 -4
- package/dist/cli/session-hooks.d.ts +0 -4
- package/dist/cli/session-hooks.js +57 -66
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +2 -2
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli/sleep.js +2 -2
- package/dist/cli/transfer.d.ts +3 -3
- package/dist/cli/transfer.js +34 -15
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +1 -1
- package/dist/cli/verb-row.d.ts +2 -0
- package/dist/cli/verbs/health.js +2 -0
- package/dist/cli/verbs/memory.js +43 -51
- package/dist/cli/verbs/objects.js +6 -3
- package/dist/cli/verbs/sessions.js +4 -2
- package/dist/cli.js +1 -2
- package/dist/connectors/dlq.js +2 -1
- package/dist/connectors/github/deletion.js +2 -1
- package/dist/connectors/github/ingest.js +2 -1
- package/dist/connectors/github/tenant-routing.js +2 -1
- package/dist/connectors/github/webhook.js +4 -2
- package/dist/connectors/ingest.js +2 -1
- package/dist/connectors/slack/deletion.js +2 -1
- package/dist/connectors/slack/ingest.js +2 -1
- package/dist/connectors/slack/tenant-routing.js +2 -1
- package/dist/consolidate/conflicts.js +1 -1
- package/dist/consolidate/dag.js +14 -9
- package/dist/consolidate/dedupe.js +1 -1
- package/dist/consolidate/memory-value.js +1 -1
- package/dist/consolidate/merge.js +6 -6
- package/dist/consolidate/sleep.js +1 -1
- package/dist/consolidate/traces.js +2 -2
- package/dist/core/config.js +15 -16
- package/dist/core/markdown.js +2 -2
- package/dist/core/memory.d.ts +4 -4
- package/dist/core/memory.js +10 -5
- package/dist/core/physics.d.ts +3 -1
- package/dist/core/physics.js +17 -2
- package/dist/core/project-identity.js +2 -1
- package/dist/core/search-types.d.ts +1 -1
- package/dist/dashboard/dashboard-actions.d.ts +2 -2
- package/dist/dashboard/dashboard-actions.js +5 -5
- package/dist/dashboard/dashboard-queries.js +2 -7
- package/dist/dashboard/dashboard-snapshot.js +1 -3
- package/dist/dashboard/dashboard.js +16 -5
- package/dist/db/connect.d.ts +2 -1
- package/dist/db/connect.js +2 -1
- package/dist/db/migrate.js +14 -7
- package/dist/db/migrations/v11.js +1 -0
- package/dist/db/migrations/v14.js +1 -0
- package/dist/db/physics-state.d.ts +0 -4
- package/dist/db/physics-state.js +5 -31
- package/dist/db/request-stores.d.ts +1 -0
- package/dist/db/request-stores.js +2 -1
- package/dist/db/vector-store.js +2 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +7 -7
- package/dist/embeddings/local.js +2 -2
- package/dist/embeddings/provider.js +1 -10
- package/dist/entry/store-kit.d.ts +37 -0
- package/dist/entry/store-kit.js +25 -0
- package/dist/entry/testing.d.ts +6 -0
- package/dist/entry/testing.js +7 -0
- package/dist/eval/judgment.js +1 -1
- package/dist/hooks/json-hooks.js +6 -2
- package/dist/importers/core.js +3 -3
- package/dist/importers/vault.d.ts +1 -1
- package/dist/importers/vault.js +18 -18
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/learn/autolearn.d.ts +6 -2
- package/dist/learn/autolearn.js +8 -6
- package/dist/learn/extract.js +1 -1
- package/dist/learn/invalidation.d.ts +2 -1
- package/dist/learn/invalidation.js +10 -5
- package/dist/mcp/memory-tools.d.ts +1 -1
- package/dist/mcp/memory-tools.js +6 -9
- package/dist/mcp/request.js +20 -19
- package/dist/mcp/stdio.js +13 -8
- package/dist/rerankers/cross-encoder.js +10 -3
- package/dist/search/embed-memories.d.ts +5 -2
- package/dist/search/embed-memories.js +40 -28
- package/dist/search/engine.d.ts +8 -0
- package/dist/search/engine.js +10 -0
- package/dist/search/vector.js +5 -5
- package/dist/server/auth.d.ts +1 -1
- package/dist/server/auth.js +7 -8
- package/dist/server/boot.d.ts +2 -1
- package/dist/server/boot.js +28 -19
- package/dist/server/client-ip.d.ts +9 -4
- package/dist/server/client-ip.js +35 -17
- package/dist/server/route-table.js +5 -6
- package/dist/server/routes/memories.js +4 -2
- package/dist/server/sleep-offload.js +3 -3
- package/dist/server/tls.d.ts +7 -2
- package/dist/server/tls.js +17 -5
- package/dist/server/types.d.ts +7 -2
- package/dist/server.d.ts +4 -41
- package/dist/server.js +3 -30
- package/dist/sharing/global-store.d.ts +4 -5
- package/dist/sharing/global-store.js +6 -11
- package/dist/sharing/global-sync.js +1 -1
- package/dist/sharing/search-both.js +9 -2
- package/dist/sharing/share.js +2 -4
- package/dist/store/ambient.js +2 -1
- package/dist/store/candidates.js +18 -8
- package/dist/store/cards.js +5 -4
- package/dist/store/compactions-record.js +8 -7
- package/dist/store/conflicts.d.ts +7 -1
- package/dist/store/conflicts.js +6 -5
- package/dist/store/connectors/dlq.js +2 -1
- package/dist/store/connectors/event-log.d.ts +13 -0
- package/dist/store/connectors/event-log.js +24 -0
- package/dist/store/connectors/github.js +6 -8
- package/dist/store/connectors/slack.js +6 -10
- package/dist/store/dashboard-reads.js +1 -0
- package/dist/store/delete-and-batch.js +5 -4
- package/dist/store/delivery-recorder.d.ts +2 -2
- package/dist/store/delivery-recorder.js +1 -6
- package/dist/store/descendant-page.js +2 -1
- package/dist/store/dormant.js +7 -3
- package/dist/store/entry-reads.d.ts +25 -3
- package/dist/store/entry-reads.js +43 -12
- package/dist/store/entry-row.js +9 -2
- package/dist/store/entry-writes.d.ts +10 -7
- package/dist/store/entry-writes.js +23 -11
- package/dist/store/goals.js +3 -2
- package/dist/store/graph-reads.js +5 -4
- package/dist/store/graph-view-rows.js +2 -1
- package/dist/store/half-life.js +2 -2
- package/dist/store/handoffs.d.ts +9 -3
- package/dist/store/handoffs.js +4 -3
- package/dist/store/index.d.ts +2 -2
- package/dist/store/index.js +1 -1
- package/dist/store/mirrors.js +9 -4
- package/dist/store/open.d.ts +2 -1
- package/dist/store/open.js +2 -1
- package/dist/store/port.d.ts +59 -3
- package/dist/store/quality-repair.js +1 -1
- package/dist/store/recall-trace.js +2 -1
- package/dist/store/rejected-values.js +14 -9
- package/dist/store/rows.js +2 -2
- package/dist/store/rule-sql.d.ts +2 -0
- package/dist/store/rule-sql.js +12 -6
- package/dist/store/search-rows.d.ts +5 -5
- package/dist/store/search-rows.js +9 -12
- package/dist/store/set-aside.js +1 -1
- package/dist/store/sqlite/connector-writes-group.js +3 -1
- package/dist/store/sqlite/entry-writes-group.d.ts +3 -3
- package/dist/store/sqlite/entry-writes-group.js +1 -1
- package/dist/store/sqlite/held-connection.d.ts +2 -0
- package/dist/store/sqlite/held-connection.js +1 -1
- package/dist/store/sqlite/last-recall-group.d.ts +13 -0
- package/dist/store/sqlite/last-recall-group.js +57 -0
- package/dist/store/sqlite/local.d.ts +0 -17
- package/dist/store/sqlite/local.js +0 -31
- package/dist/store/sqlite/maintenance-groups.d.ts +6 -0
- package/dist/store/sqlite/maintenance-groups.js +60 -0
- package/dist/store/sqlite/objects-group.js +14 -6
- package/dist/store/sqlite/store.d.ts +1 -8
- package/dist/store/sqlite/store.js +13 -24
- package/dist/store/summaries.d.ts +7 -0
- package/dist/store/summaries.js +49 -7
- package/dist/store/token-ledger-rows.js +3 -2
- package/dist/store/vector-index.js +5 -2
- package/dist/util/anthropic-messages.d.ts +3 -1
- package/dist/util/anthropic-messages.js +4 -2
- package/dist/util/atomic-write.d.ts +5 -2
- package/dist/util/atomic-write.js +12 -8
- package/dist/util/env.d.ts +10 -1
- package/dist/util/env.js +10 -1
- package/dist/util/format.d.ts +2 -0
- package/dist/util/format.js +4 -0
- package/dist/util/request-scope.d.ts +6 -2
- package/dist/util/request-scope.js +7 -3
- package/dist/util/spawn-argv.d.ts +9 -0
- package/dist/util/spawn-argv.js +57 -0
- package/dist/util/sql.d.ts +3 -0
- package/dist/util/sql.js +6 -0
- package/dist/util/vector-codec.d.ts +4 -0
- package/dist/util/vector-codec.js +15 -0
- package/dist/util/version.d.ts +1 -1
- package/dist/util/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 +3 -1
- package/dist/api/on-store.d.ts +0 -16
- package/dist/api/on-store.js +0 -38
- package/dist/cli/lenient-id.d.ts +0 -2
- package/dist/cli/lenient-id.js +0 -13
|
@@ -2,6 +2,7 @@ import { strengthAblation } from '../core/ablation.js';
|
|
|
2
2
|
import { markRetrieved } from '../core/memory.js';
|
|
3
3
|
import { closeHippoDb, openHippoDb, withWriteScope } from '../db/index.js';
|
|
4
4
|
import { rethrowIfSqliteBlocked } from '../util/sqlite-blocked.js';
|
|
5
|
+
import { chunked } from '../util/chunked.js';
|
|
5
6
|
import { RejectedValueError } from '../core/api-errors.js';
|
|
6
7
|
import { markSummaryDirtyInTx } from './summary-dirty.js';
|
|
7
8
|
import { errorMessage, log } from '../util/log.js';
|
|
@@ -10,9 +11,13 @@ import { selectEntriesByIds } from './entry-reads.js';
|
|
|
10
11
|
import { stampOriginProject, upsertEntryRow, syncFtsRow, deleteFtsRow } from './entry-row.js';
|
|
11
12
|
import { mirrorBestEffort, writeMarkdownMirror } from './mirrors.js';
|
|
12
13
|
import { onHandle, openStore } from './open.js';
|
|
13
|
-
/** Write an entry to SQLite and refresh mirrors
|
|
14
|
-
*
|
|
14
|
+
/** Write an entry to SQLite and refresh mirrors. `opts.actor` defaults to 'cli'; the api layer passes the resolved actor,
|
|
15
|
+
* so the audit log gets one row per write. */
|
|
15
16
|
export function writeEntry(hippoRoot, entry, opts) {
|
|
17
|
+
writeEntryWithHook(hippoRoot, entry, opts);
|
|
18
|
+
}
|
|
19
|
+
/** writeEntry for store code that needs the write's own handle: `opts.afterWrite` shares the INSERT's SAVEPOINT. */
|
|
20
|
+
export function writeEntryWithHook(hippoRoot, entry, opts) {
|
|
16
21
|
onHandle(hippoRoot, (db) => {
|
|
17
22
|
writeEntryOn(db, hippoRoot, entry, opts);
|
|
18
23
|
}, openStore);
|
|
@@ -117,11 +122,11 @@ export function auditEntryWrite(db, entry, actor) {
|
|
|
117
122
|
audit(db, 'remember', { targetId: entry.id, metadata: {
|
|
118
123
|
kind: entry.kind ?? 'distilled',
|
|
119
124
|
scope: entry.scope ?? null,
|
|
120
|
-
}, actor, tenantId: entry.
|
|
125
|
+
}, actor, tenantId: entry.tenant_id });
|
|
121
126
|
// A child write marks its summary parent dirty for the sleep-cycle rebuild; most writes
|
|
122
127
|
// have no parent, so the hot path pays one null check.
|
|
123
128
|
if (entry.dag_parent_id) {
|
|
124
|
-
markSummaryDirtyInTx(db, entry.dag_parent_id, entry.
|
|
129
|
+
markSummaryDirtyInTx(db, entry.dag_parent_id, entry.tenant_id, actor);
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
/** Markdown mirror path, invoked AFTER commit (a rolled-back tx must leave no orphan markdown). */
|
|
@@ -164,6 +169,8 @@ export function strengthenRetrievedInOwnTx(db, ids, opts) {
|
|
|
164
169
|
function warnStrengthenFailed(error) {
|
|
165
170
|
log.warn(`retrieval stats not saved (${errorMessage(error)})`);
|
|
166
171
|
}
|
|
172
|
+
/** Rows per recall-write statement: five bound values each, far under SQLite's parameter limit. */
|
|
173
|
+
const RETRIEVAL_UPDATE_CHUNK = 100;
|
|
167
174
|
/** strengthenRetrieved on the caller's handle, inside the caller's transaction. Throws; the caller decides. */
|
|
168
175
|
export function strengthenRetrievedOn(db, ids, opts) {
|
|
169
176
|
const found = new Set();
|
|
@@ -171,25 +178,30 @@ export function strengthenRetrievedOn(db, ids, opts) {
|
|
|
171
178
|
return found;
|
|
172
179
|
const byId = selectEntriesByIds(db, ids, opts.tenantId);
|
|
173
180
|
const live = [...new Set(ids)].flatMap((id) => byId.get(id) ?? []);
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
const updated = markRetrieved(live, undefined, { ablation: strengthAblation() });
|
|
182
|
+
// One UPDATE ... FROM per chunk of rows instead of one statement per row.
|
|
183
|
+
for (const chunk of chunked(updated, RETRIEVAL_UPDATE_CHUNK)) {
|
|
184
|
+
const values = chunk.map(() => '(?, ?, ?, ?, ?)').join(', ');
|
|
185
|
+
db.prepare(`WITH v(id, rc, lr, hl, st) AS (VALUES ${values})
|
|
186
|
+
UPDATE memories SET retrieval_count = v.rc, last_retrieved = v.lr, half_life_days = v.hl, strength = v.st
|
|
187
|
+
FROM v WHERE memories.id = v.id`).run(...chunk.flatMap((e) => [e.id, e.retrieval_count, e.last_retrieved, e.half_life_days, e.strength]));
|
|
188
|
+
for (const e of chunk)
|
|
189
|
+
found.add(e.id);
|
|
178
190
|
}
|
|
179
191
|
return found;
|
|
180
192
|
}
|
|
181
193
|
/** Rewrites a live row's tags and its full-text row on the caller's transaction, with no audit row. */
|
|
182
194
|
export function setEntryTagsInTx(db, entry) {
|
|
183
195
|
db.prepare(`UPDATE memories SET tags_json = ?, updated_at = datetime('now') WHERE id = ? AND tenant_id = ?`)
|
|
184
|
-
.run(JSON.stringify(entry.tags), entry.id, entry.
|
|
196
|
+
.run(JSON.stringify(entry.tags), entry.id, entry.tenant_id);
|
|
185
197
|
syncFtsRow(db, entry);
|
|
186
198
|
}
|
|
187
199
|
/** Removes a row from `memories` and full-text search on the caller's transaction, as sleep's dormant move does, and marks its summary parent dirty. */
|
|
188
200
|
export function deleteEntryRowInTx(db, entry, actor) {
|
|
189
|
-
db.prepare('DELETE FROM memories WHERE id = ? AND tenant_id = ?').run(entry.id, entry.
|
|
201
|
+
db.prepare('DELETE FROM memories WHERE id = ? AND tenant_id = ?').run(entry.id, entry.tenant_id);
|
|
190
202
|
deleteFtsRow(db, entry.id);
|
|
191
203
|
if (entry.dag_parent_id)
|
|
192
|
-
markSummaryDirtyInTx(db, entry.dag_parent_id, entry.
|
|
204
|
+
markSummaryDirtyInTx(db, entry.dag_parent_id, entry.tenant_id, actor);
|
|
193
205
|
}
|
|
194
206
|
/** One half-life per memory id, on the caller's transaction. */
|
|
195
207
|
export function setHalfLivesAt(db, rows) {
|
package/dist/store/goals.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { withWriteScope } from '../db/index.js';
|
|
4
4
|
import { onHandle } from './open.js';
|
|
5
|
+
import { placeholders } from '../util/sql.js';
|
|
5
6
|
const MAX_ACTIVE_GOAL_DEPTH = 3;
|
|
6
7
|
export function rowToGoal(row) {
|
|
7
8
|
return {
|
|
@@ -152,7 +153,7 @@ function loadGoalPolicies(db, active) {
|
|
|
152
153
|
// SAFETY: the SELECT projects exactly these seven retrieval_policy columns; MAX_ACTIVE_GOAL_DEPTH bounds the IN list.
|
|
153
154
|
const rows = db.prepare(`
|
|
154
155
|
SELECT id, goal_id, policy_type, weight_schema_fit, weight_recency, weight_outcome, error_priority
|
|
155
|
-
FROM retrieval_policy WHERE id IN (${policyIds.
|
|
156
|
+
FROM retrieval_policy WHERE id IN (${placeholders(policyIds.length)})
|
|
156
157
|
`).all(...policyIds);
|
|
157
158
|
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
158
159
|
for (const g of active) {
|
|
@@ -178,7 +179,7 @@ export function localGoalRecallRows(db, rows) {
|
|
|
178
179
|
return [];
|
|
179
180
|
const ids = [...new Set(rows.map((r) => r.memoryId))];
|
|
180
181
|
// SAFETY: the SELECT projects exactly one column, `id`, from memories.
|
|
181
|
-
const local = db.prepare(`SELECT id FROM memories WHERE id IN (${ids.
|
|
182
|
+
const local = db.prepare(`SELECT id FROM memories WHERE id IN (${placeholders(ids.length)})`).all(...ids);
|
|
182
183
|
const localIds = new Set(local.map((r) => r.id));
|
|
183
184
|
return rows.filter((r) => localIds.has(r.memoryId));
|
|
184
185
|
}
|
|
@@ -4,6 +4,7 @@ import { openHippoDb, closeHippoDb, withReadSnapshot } from '../db/index.js';
|
|
|
4
4
|
import { onHandle } from './open.js';
|
|
5
5
|
import { assertTenantId } from './tenant.js';
|
|
6
6
|
import { GRAPH_ENTITY_TYPES, rowToEntity, rowToRelation, ENTITY_COLS, RELATION_COLS } from './graph-rows.js';
|
|
7
|
+
import { placeholders } from '../util/sql.js';
|
|
7
8
|
const DEFAULT_GRAPH_PAGE_SIZE = DEFAULT_LIST_LIMIT;
|
|
8
9
|
const DEFAULT_GRAPH_SCAN_LIMIT = 1000;
|
|
9
10
|
/** Entities with an exact `name`, bounded by `limit` in SQL with a deterministic order, so graph-view focus finds `--entity NAME` directly
|
|
@@ -100,7 +101,7 @@ export function loadEntitiesByIds(hippoRoot, tenantId, ids, txDb) {
|
|
|
100
101
|
const out = [];
|
|
101
102
|
for (let i = 0; i < ids.length; i += IN_LIST_CHUNK) {
|
|
102
103
|
const slice = ids.slice(i, i + IN_LIST_CHUNK);
|
|
103
|
-
const ph = slice.
|
|
104
|
+
const ph = placeholders(slice.length);
|
|
104
105
|
// SAFETY: rows' shape matches the columns named in ENTITY_COLS above.
|
|
105
106
|
const rows = db.prepare(`
|
|
106
107
|
SELECT ${ENTITY_COLS} FROM entities
|
|
@@ -133,7 +134,7 @@ export function loadNeighborRelations(hippoRoot, tenantId, entityIds, opts = {},
|
|
|
133
134
|
const byId = new Map();
|
|
134
135
|
for (let i = 0; i < entityIds.length; i += IN_LIST_CHUNK) {
|
|
135
136
|
const slice = entityIds.slice(i, i + IN_LIST_CHUNK);
|
|
136
|
-
const ph = slice.
|
|
137
|
+
const ph = placeholders(slice.length);
|
|
137
138
|
// SAFETY: rows' shape matches the columns named in RELATION_COLS above.
|
|
138
139
|
const rows = db.prepare(`
|
|
139
140
|
SELECT ${RELATION_COLS} FROM relations
|
|
@@ -164,7 +165,7 @@ export function loadRelationsAmong(hippoRoot, tenantId, entityIds, opts = {}, tx
|
|
|
164
165
|
const ownDb = txDb ? null : openHippoDb(hippoRoot);
|
|
165
166
|
const db = txDb ?? ownDb;
|
|
166
167
|
try {
|
|
167
|
-
const ph = entityIds.
|
|
168
|
+
const ph = placeholders(entityIds.length);
|
|
168
169
|
// SAFETY: rows' shape matches the columns named in RELATION_COLS above.
|
|
169
170
|
const rows = db.prepare(`
|
|
170
171
|
SELECT ${RELATION_COLS} FROM relations
|
|
@@ -189,7 +190,7 @@ export function loadEntitiesByMemoryId(hippoRoot, tenantId, memoryIds) {
|
|
|
189
190
|
const out = [];
|
|
190
191
|
for (let i = 0; i < memoryIds.length; i += IN_LIST_CHUNK) {
|
|
191
192
|
const slice = memoryIds.slice(i, i + IN_LIST_CHUNK);
|
|
192
|
-
const ph = slice.
|
|
193
|
+
const ph = placeholders(slice.length);
|
|
193
194
|
// id ASC so chunk-local scan order never decides ties (id is an autoincrement PK).
|
|
194
195
|
// SAFETY: rows' shape matches the columns named in ENTITY_COLS above.
|
|
195
196
|
const rows = db.prepare(`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IN_LIST_CHUNK, loadEntities, loadEntitiesByIds, loadEntitiesByName, loadNeighborRelations, loadRelations, loadRelationsAmong } from './graph-reads.js';
|
|
2
|
+
import { placeholders } from '../util/sql.js';
|
|
2
3
|
/** The items whose source memory `canRead` admits: a NULL memoryId has no scope to hide, and a missing memory fails closed. */
|
|
3
4
|
function readableOnly(db, tenantId, items, canRead) {
|
|
4
5
|
if (!canRead)
|
|
@@ -8,7 +9,7 @@ function readableOnly(db, tenantId, items, canRead) {
|
|
|
8
9
|
for (let i = 0; i < ids.length; i += IN_LIST_CHUNK) {
|
|
9
10
|
const slice = ids.slice(i, i + IN_LIST_CHUNK);
|
|
10
11
|
// SAFETY: rows' shape matches the id and scope columns named in the SELECT.
|
|
11
|
-
const rows = db.prepare(`SELECT id, scope FROM memories WHERE +tenant_id = ? AND id IN (${slice.
|
|
12
|
+
const rows = db.prepare(`SELECT id, scope FROM memories WHERE +tenant_id = ? AND id IN (${placeholders(slice.length)})`)
|
|
12
13
|
.all(tenantId, ...slice);
|
|
13
14
|
for (const r of rows)
|
|
14
15
|
if (canRead(r.scope))
|
package/dist/store/half-life.js
CHANGED
|
@@ -34,9 +34,9 @@ function writeMove(db, move, old, actor) {
|
|
|
34
34
|
const byTenant = new Map();
|
|
35
35
|
for (const e of move.entries) {
|
|
36
36
|
// One record per tenant, filled in place: copying it per row made the write grow with the square of the store.
|
|
37
|
-
let record = byTenant.get(e.
|
|
37
|
+
let record = byTenant.get(e.tenant_id);
|
|
38
38
|
if (!record)
|
|
39
|
-
byTenant.set(e.
|
|
39
|
+
byTenant.set(e.tenant_id, (record = {}));
|
|
40
40
|
record[e.id] = old.get(e.id);
|
|
41
41
|
}
|
|
42
42
|
for (const [tenantId, oldHalfLives] of byTenant) {
|
package/dist/store/handoffs.d.ts
CHANGED
|
@@ -17,10 +17,16 @@ export declare function loadLatestHandoff(hippoRoot: string, tenantId: string, s
|
|
|
17
17
|
export declare function loadHandoffById(hippoRoot: string, tenantId: string, id: number): SessionHandoff | null;
|
|
18
18
|
/** Stamp the outcome on a session's newest handoff, only if it has none yet. Returns rows changed. */
|
|
19
19
|
export declare function stampHandoffOutcome(hippoRoot: string, tenantId: string, sessionId: string, outcome: HandoffOutcome): number;
|
|
20
|
+
export interface SessionEndHandoffRequest {
|
|
21
|
+
readonly tenantId: string;
|
|
22
|
+
readonly sessionId: string;
|
|
23
|
+
readonly evidence: HandoffEvidence | null;
|
|
24
|
+
readonly derived?: Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null;
|
|
25
|
+
readonly key?: ContinuityKey;
|
|
26
|
+
readonly inPlace?: boolean;
|
|
27
|
+
}
|
|
20
28
|
/** Session-end handoff from the active snapshot, else `derived` transcript state; `inPlace` rewrites an earlier transcript read instead of adding a revision.
|
|
21
29
|
* Returns null when no source is the session's, a newer handoff covers it, or the latest one is not transcript-derived or already holds `derived`. */
|
|
22
|
-
export declare function writeSessionEndHandoff(hippoRoot: string,
|
|
23
|
-
inPlace?: boolean;
|
|
24
|
-
}): SessionHandoff | null;
|
|
30
|
+
export declare function writeSessionEndHandoff(hippoRoot: string, request: SessionEndHandoffRequest): SessionHandoff | null;
|
|
25
31
|
export {};
|
|
26
32
|
//# sourceMappingURL=handoffs.d.ts.map
|
package/dist/store/handoffs.js
CHANGED
|
@@ -169,7 +169,8 @@ function buildSessionEndHandoff(sessionId, snapshot, existing, handoffEvidence,
|
|
|
169
169
|
}
|
|
170
170
|
/** Session-end handoff from the active snapshot, else `derived` transcript state; `inPlace` rewrites an earlier transcript read instead of adding a revision.
|
|
171
171
|
* Returns null when no source is the session's, a newer handoff covers it, or the latest one is not transcript-derived or already holds `derived`. */
|
|
172
|
-
export function writeSessionEndHandoff(hippoRoot,
|
|
172
|
+
export function writeSessionEndHandoff(hippoRoot, request) {
|
|
173
|
+
const { tenantId, sessionId, evidence, derived = null, key, inPlace } = request;
|
|
173
174
|
assertTenantId('writeSessionEndHandoff', tenantId);
|
|
174
175
|
if (key)
|
|
175
176
|
continuityStamp(key); // a partial key is a caller bug, so fail loud rather than write nothing
|
|
@@ -179,7 +180,7 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence,
|
|
|
179
180
|
let handoffEvidence = evidence;
|
|
180
181
|
let fromTranscript = false;
|
|
181
182
|
// After every reply the transcript is newer than the last compaction's snapshot, which the open chat keeps for the next one.
|
|
182
|
-
const snapshotGivesWay =
|
|
183
|
+
const snapshotGivesWay = inPlace === true && derived !== null && active?.source === 'pre-compact';
|
|
183
184
|
if (active && active.session_id === sessionId && !snapshotGivesWay) {
|
|
184
185
|
// Strict '>': a same-millisecond tie must not swallow the session's only write (test 6e).
|
|
185
186
|
if (existing && existing.updatedAt > active.updated_at)
|
|
@@ -197,7 +198,7 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence,
|
|
|
197
198
|
const outcome = sessionOutcome(hippoRoot, tenantId, sessionId);
|
|
198
199
|
const handoff = buildSessionEndHandoff(sessionId, snapshot, existing, handoffEvidence, outcome);
|
|
199
200
|
// A miss means a handoff landed since the read, and a new transcript row would hide it.
|
|
200
|
-
if (fromTranscript &&
|
|
201
|
+
if (fromTranscript && inPlace && existing)
|
|
201
202
|
return replaceTranscriptHandoff(hippoRoot, tenantId, handoff, key);
|
|
202
203
|
return saveSessionHandoff(hippoRoot, tenantId, handoff, key);
|
|
203
204
|
}
|
package/dist/store/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { AmbientCandidateRequest, ContextReads, EntryRemoval, EntryTarget, EntryWrite, EntryWrites, HippoStore, KeyAudit, KeyListQuery, KeyMint, KeyRevoke, KeyWrites, OutcomeWrite, RawArchive, RecallSearchArgs, RecallWrites, SelfKeyMint, StoreGroup, StoreGroups, SupersedeWrite, VectorBackfillQuery, VectorReads, VectorRowWrite, VectorWrite, VectorWriteResult, VectorWrites, } from './port.js';
|
|
1
|
+
export type { AmbientCandidateRequest, ContextReads, EntryRemoval, EntryTarget, EntryWrite, EntryWrites, HippoStore, KeyAudit, KeyListQuery, KeyMint, KeyRevoke, KeyWrites, LastRecall, LastRecallWrites, OutcomeWrite, RawArchive, RecallSearchArgs, RecallWrites, SelfKeyMint, StoreGroup, StoreGroups, SupersedeWrite, VectorBackfillQuery, VectorReads, VectorRowWrite, VectorWrite, VectorWriteResult, VectorWrites, } from './port.js';
|
|
2
2
|
export { hasGroup, requireGroup } from './port.js';
|
|
3
|
-
export
|
|
3
|
+
export { sqliteStore } from './sqlite/store.js';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/store/index.js
CHANGED
package/dist/store/mirrors.js
CHANGED
|
@@ -8,7 +8,12 @@ import { oncePerStore } from '../db/connect.js';
|
|
|
8
8
|
import { errorFields, errorMessage, log } from '../util/log.js';
|
|
9
9
|
import { parseJsonArray, INDEX_VERSION, parseLastTraceId, MEMORY_SELECT_COLUMNS, rowToEntry, rowToMemoryConflict, } from './rows.js';
|
|
10
10
|
import { serializeEntry } from '../core/markdown.js';
|
|
11
|
+
import { writeFileAtomic } from '../util/atomic-write.js';
|
|
11
12
|
const SCORE_DECIMALS = 3;
|
|
13
|
+
// Mirrors are derived and rebuildable from the store, so they skip the fsyncs (the cost on Windows) and keep the temp-and-rename atomicity.
|
|
14
|
+
function writeMirror(file, text) {
|
|
15
|
+
writeFileAtomic(file, text, { durable: false });
|
|
16
|
+
}
|
|
12
17
|
export function layerDir(root, layer) {
|
|
13
18
|
return path.join(root, layer);
|
|
14
19
|
}
|
|
@@ -61,7 +66,7 @@ export function writeActiveTaskMirror(hippoRoot, tenantId, snapshot) {
|
|
|
61
66
|
body.push(`## Session`, snapshot.session_id, '');
|
|
62
67
|
}
|
|
63
68
|
fs.mkdirSync(path.dirname(filePath), MIRROR_FOLDER);
|
|
64
|
-
|
|
69
|
+
writeMirror(filePath, `${fm}\n\n${body.join('\n')}`);
|
|
65
70
|
}
|
|
66
71
|
export function removeActiveTaskMirror(hippoRoot, tenantId) {
|
|
67
72
|
const filePath = activeTaskMirrorPath(hippoRoot, tenantId);
|
|
@@ -99,7 +104,7 @@ export function writeRecentSessionMirror(hippoRoot, tenantId, events) {
|
|
|
99
104
|
}
|
|
100
105
|
lines.push('');
|
|
101
106
|
fs.mkdirSync(path.dirname(filePath), MIRROR_FOLDER);
|
|
102
|
-
|
|
107
|
+
writeMirror(filePath, `${fm}\n\n${lines.join('\n')}`);
|
|
103
108
|
}
|
|
104
109
|
function writeConflictMirrors(hippoRoot, conflicts) {
|
|
105
110
|
const conflictDir = path.join(hippoRoot, 'conflicts');
|
|
@@ -128,7 +133,7 @@ function writeConflictMirrors(hippoRoot, conflicts) {
|
|
|
128
133
|
`- Status: ${conflict.status}`,
|
|
129
134
|
'',
|
|
130
135
|
].join('\n');
|
|
131
|
-
|
|
136
|
+
writeMirror(path.join(conflictDir, filename), `${fm}\n\n${body}`);
|
|
132
137
|
}
|
|
133
138
|
for (const existing of fs.readdirSync(conflictDir)) {
|
|
134
139
|
if (existing === '.gitkeep')
|
|
@@ -142,7 +147,7 @@ export function writeMarkdownMirror(hippoRoot, entry) {
|
|
|
142
147
|
removeEntryMirrors(hippoRoot, entry.id);
|
|
143
148
|
const dir = layerDir(hippoRoot, entry.layer);
|
|
144
149
|
fs.mkdirSync(dir, MIRROR_FOLDER);
|
|
145
|
-
|
|
150
|
+
writeMirror(path.join(dir, `${entry.id}.md`), serializeEntry(entry));
|
|
146
151
|
}
|
|
147
152
|
// Every layer, Trace included: `writeMarkdownMirror` writes any layer, and a stale trace mirror
|
|
148
153
|
// is the resurrection channel bootstrapLegacyStore/rebuildIndex guard against.
|
package/dist/store/open.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export declare function initStore(hippoRoot: string): void;
|
|
|
5
5
|
export declare function openStore(hippoRoot: string, opts?: {
|
|
6
6
|
busyWaitMs?: number;
|
|
7
7
|
}): DatabaseSyncLike;
|
|
8
|
-
/** One call on a handle of its own, closed after
|
|
8
|
+
/** One call on a handle of its own, closed after (a request scope's handle stays open);
|
|
9
|
+
* pass openStore where the call also sets up mirror folders and legacy rows. */
|
|
9
10
|
export declare function onHandle<T>(hippoRoot: string, fn: (db: DatabaseSyncLike) => T, open?: (hippoRoot: string) => DatabaseSyncLike): T;
|
|
10
11
|
/** `meta` key holding the default half-life base a store's memories are on (src/consolidate/half-life-migration.ts). */
|
|
11
12
|
export declare const HALF_LIFE_BASE_META_KEY = "default_half_life_base";
|
package/dist/store/open.js
CHANGED
|
@@ -44,7 +44,8 @@ export function openStore(hippoRoot, opts) {
|
|
|
44
44
|
throw error;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
/** One call on a handle of its own, closed after
|
|
47
|
+
/** One call on a handle of its own, closed after (a request scope's handle stays open);
|
|
48
|
+
* pass openStore where the call also sets up mirror folders and legacy rows. */
|
|
48
49
|
export function onHandle(hippoRoot, fn, open = openHippoDb) {
|
|
49
50
|
const db = open(hippoRoot);
|
|
50
51
|
try {
|
package/dist/store/port.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AmbientTallies } from '../core/ambient.js';
|
|
2
2
|
import type { AmbientStoreFilter } from './ambient.js';
|
|
3
3
|
import type { ApiKeyListRow, ApiKeyRecord, ListApiKeysOpts, NewApiKey } from './auth.js';
|
|
4
|
-
import type { AppendAuditOpts, AuditEvent, ListAuditAfterOpts, QueryAuditOpts } from './audit.js';
|
|
4
|
+
import type { AppendAuditOpts, AuditEvent, AuditPrune, ListAuditAfterOpts, QueryAuditOpts } from './audit.js';
|
|
5
|
+
import type { QualityRepairResult } from './quality-repair.js';
|
|
5
6
|
import type { EmbeddingIndexState } from './vector-index.js';
|
|
6
7
|
import type { Entity, Relation } from './graph-rows.js';
|
|
7
8
|
import type { ActiveGoals, GetActiveGoalsOpts, GoalRecallLogRow } from './goals.js';
|
|
@@ -17,7 +18,7 @@ import type { ClosureState } from '../core/prediction.js';
|
|
|
17
18
|
import type { QuarantineRow, QuarantineStatus } from './quarantine.js';
|
|
18
19
|
import type { ScopeActor } from '../core/recall-scope.js';
|
|
19
20
|
import type { RecallTraceInput } from './recall-trace.js';
|
|
20
|
-
import type { AmbientLoadResult, AmbientRecallRequest, ContextCandidateFilter, RecentOrigins } from './candidates.js';
|
|
21
|
+
import type { AmbientLoadResult, AmbientRecallRequest, ContextCandidateFilter, HeldText, RecentOrigins } from './candidates.js';
|
|
21
22
|
import type { GitHubDlqInsert, GitHubRouting } from './connectors/github.js';
|
|
22
23
|
import type { SlackDlqInsert, SlackTeamRoute } from './connectors/slack.js';
|
|
23
24
|
import type { StrengthenOptions } from './entry-writes.js';
|
|
@@ -411,6 +412,18 @@ export interface Readiness {
|
|
|
411
412
|
/** Resolves once one cheap read has answered; rejects with the store's own error when it cannot. */
|
|
412
413
|
ping(): Promise<void>;
|
|
413
414
|
}
|
|
415
|
+
/** A recall's writes when the store keeps it as its last one: `strengthen.ids` are the ids it returned. */
|
|
416
|
+
export type LastRecallWrites = RecallWrites & Required<Pick<RecallWrites, 'strengthen'>>;
|
|
417
|
+
/** The last recall a store keeps, so a later outcome can name it, and the outcome linked to a recall trace. */
|
|
418
|
+
export interface LastRecall {
|
|
419
|
+
/** entryWrites.applyOutcome, then a link from recall trace `traceId` to the ids applied; resolves to those ids. */
|
|
420
|
+
applyTracedOutcome(outcome: OutcomeWrite, traceId: number): Promise<string[]>;
|
|
421
|
+
/** applyTracedOutcome on the ids and trace of the last recall, read together so an outcome never pairs them with an older trace. */
|
|
422
|
+
applyOutcomeToLast(target: EntryTarget, good: boolean): Promise<string[]>;
|
|
423
|
+
/** finishRecall, then the ids this store lacks strengthened under `globalRoot`, then the ids and their trace saved as the last
|
|
424
|
+
* recall in one write; resolves to the trace id, null when none was asked or its write failed. */
|
|
425
|
+
finishRecall(writes: LastRecallWrites, globalRoot: string | undefined): Promise<number | null>;
|
|
426
|
+
}
|
|
414
427
|
/** Which of a tenant's quarantine records a list reads; `limit` is 100 when unset. */
|
|
415
428
|
export interface QuarantineListQuery {
|
|
416
429
|
readonly status: QuarantineStatus | 'all';
|
|
@@ -575,8 +588,49 @@ export interface VectorViews {
|
|
|
575
588
|
/** The same ids and values as `VectorReads.storedVectors`, each value a Float32 view. */
|
|
576
589
|
storedVectorViews(ids: readonly string[]): Promise<Map<string, Float32Array>>;
|
|
577
590
|
}
|
|
591
|
+
/** The store is hippo.db and its files under `hippoRoot`, so a path that reopens them by folder (sleep, its child process, the global store) may run. */
|
|
592
|
+
export interface LocalFiles {
|
|
593
|
+
readonly hippoRoot: string;
|
|
594
|
+
}
|
|
595
|
+
/** The reads and writes behind the memory-quality audit, its repair and audit-log retention. */
|
|
596
|
+
export interface AuditMaintenance {
|
|
597
|
+
/** Every memory of the tenant, in `loadAllEntries` order. */
|
|
598
|
+
tenantEntries(tenantId: string): Promise<MemoryEntry[]>;
|
|
599
|
+
/** The ids of every memory a typed object row names, in any tenant; an automatic pass never deletes one. */
|
|
600
|
+
objectBackedIds(): Promise<Set<string>>;
|
|
601
|
+
/** Deletes each removal's row with its own forget row, leaving a pinned, raw, kept or object-backing row; resolves to the ids deleted. */
|
|
602
|
+
deleteAutomatically(removals: readonly {
|
|
603
|
+
readonly id: string;
|
|
604
|
+
readonly reason: string;
|
|
605
|
+
}[], actor: string): Promise<string[]>;
|
|
606
|
+
/** Counts (dryRun) or deletes the tenant's audit rows before `cutoff`, then appends one audit_prune row; resolves to the count. */
|
|
607
|
+
pruneAuditRows(prune: AuditPrune): Promise<number>;
|
|
608
|
+
/** A preview, or with `apply` the moves of certain defects to dormant storage behind a backup. */
|
|
609
|
+
repairQuality(tenantId: string, apply: boolean): Promise<QualityRepairResult>;
|
|
610
|
+
/** repairQuality applied once per store; null once it ran there. */
|
|
611
|
+
repairQualityOnce(tenantId: string): Promise<QualityRepairResult | null>;
|
|
612
|
+
}
|
|
613
|
+
/** The reads and writes behind git auto-learn's batch. */
|
|
614
|
+
export interface LessonWrites {
|
|
615
|
+
/** Every memory of the tenant, in `loadAllEntries` order. */
|
|
616
|
+
tenantEntries(tenantId: string): Promise<MemoryEntry[]>;
|
|
617
|
+
/** Content, source and origin of the tenant's rows holding any of `words`, among the rows a team member or `ownScope`'s owner may touch. */
|
|
618
|
+
textsHoldingWords(tenantId: string, words: readonly string[], ownScope: string | null): Promise<HeldText[]>;
|
|
619
|
+
/** Writes each entry in one transaction; a rejected value writes its refusal row and is skipped. Resolves to the ids written. */
|
|
620
|
+
writeSkippingRejected(entries: readonly MemoryEntry[], actor: string): Promise<string[]>;
|
|
621
|
+
/** Rewrites each entry in one transaction, all or none. */
|
|
622
|
+
writeTogether(entries: readonly MemoryEntry[]): Promise<void>;
|
|
623
|
+
/** Adds `count` to the store-wide remembered counter. */
|
|
624
|
+
bumpRemembered(count: number): Promise<void>;
|
|
625
|
+
}
|
|
578
626
|
/** The optional groups: a store sets each one whole or leaves it unset, and a route or MCP tool names the one it needs. */
|
|
579
627
|
export interface StoreGroups {
|
|
628
|
+
/** Unset on any store but hippo.db; a request then runs fenced from hippo.db, and a route or tool that names no group answers 501. */
|
|
629
|
+
readonly localFiles: LocalFiles;
|
|
630
|
+
/** Unset on a store without it, where the quality audit, its repair and audit prune answer 501. */
|
|
631
|
+
readonly auditMaintenance: AuditMaintenance;
|
|
632
|
+
/** Unset on a store without it, where git learn answers 501. */
|
|
633
|
+
readonly lessonWrites: LessonWrites;
|
|
580
634
|
/** Unset on a store built before them, where hybrid and physics recall under an embedding provider answer 501. */
|
|
581
635
|
readonly vectors: VectorReads;
|
|
582
636
|
/** Unset on a store without them, where recall scores the number[] copies `vectors` returns, to the same ranking. */
|
|
@@ -599,6 +653,8 @@ export interface StoreGroups {
|
|
|
599
653
|
readonly objects: Objects;
|
|
600
654
|
/** Unset on a store built before it, where GET /ready answers 200 with `store: "unchecked"`. */
|
|
601
655
|
readonly readiness: Readiness;
|
|
656
|
+
/** Unset on a store that keeps no last recall, where outcomeForLastRecall and a traced outcome answer 501 and context writes no last recall. */
|
|
657
|
+
readonly lastRecall: LastRecall;
|
|
602
658
|
}
|
|
603
659
|
export type StoreGroup = 'base' | keyof StoreGroups;
|
|
604
660
|
export declare function hasGroup(store: HippoStore, group: StoreGroup): boolean;
|
|
@@ -612,7 +668,7 @@ export declare class StoreBusyError extends Error {
|
|
|
612
668
|
/** What `serve()` reads and writes through. Each method is atomic and no transaction spans an
|
|
613
669
|
* await, since SQLite's lock wait blocks the event loop; a lock timeout throws `StoreBusyError`. */
|
|
614
670
|
export interface HippoStore extends Partial<StoreGroups> {
|
|
615
|
-
/**
|
|
671
|
+
/** A name for logs and errors; what a store can do is the groups it sets, `localFiles` above all. */
|
|
616
672
|
readonly kind: string;
|
|
617
673
|
/** The api_keys row for `keyId` with its scope grants, revoked or not; null when no row matches. */
|
|
618
674
|
findApiKey(keyId: string): Promise<ApiKeyRecord | null>;
|
|
@@ -133,7 +133,7 @@ function setAsideIssue(db, entry, reason, now, backup) {
|
|
|
133
133
|
if (getMeta(db, 'fts5_available', '0') === '1' && ftsRowExists(db, entry.id)) {
|
|
134
134
|
throw new Error(`Quality repair could not remove the full-text row for ${entry.id}`);
|
|
135
135
|
}
|
|
136
|
-
appendAuditEvent(db, { tenantId: entry.
|
|
136
|
+
appendAuditEvent(db, { tenantId: entry.tenant_id, actor, op: 'quality_repair', targetId: entry.id, metadata: { reason, backup } });
|
|
137
137
|
return true;
|
|
138
138
|
}
|
|
139
139
|
function applyPlan(db, root, tenantId, backup, doneKey) {
|
|
@@ -7,6 +7,7 @@ import { DELIVERY_LEDGER_VERSION, isBoundaryEvent } from './delivery-recorder.js
|
|
|
7
7
|
import { errorFields, errorMessage, log } from '../util/log.js';
|
|
8
8
|
import { DAY_MS } from '../util/time.js';
|
|
9
9
|
import { blockHash } from '../util/token-text.js';
|
|
10
|
+
import { placeholders } from '../util/sql.js';
|
|
10
11
|
/** Strip a RerankStep to {stage, multiplier, scoreBefore, scoreAfter}: `note` is free-form (the CLI goal-boost step embeds goal tag text) and would leak
|
|
11
12
|
* user content into `rerank_json`; an allowlist, so any future free-form field is dropped too. */
|
|
12
13
|
function sanitizeRerankSteps(steps) {
|
|
@@ -184,7 +185,7 @@ export function writeDeliveryEvent(db, input) {
|
|
|
184
185
|
];
|
|
185
186
|
const eventId = Number(db.prepare(`
|
|
186
187
|
INSERT INTO delivery_events (${DELIVERY_EVENT_COLUMNS.join(', ')})
|
|
187
|
-
VALUES (${DELIVERY_EVENT_COLUMNS.
|
|
188
|
+
VALUES (${placeholders(DELIVERY_EVENT_COLUMNS.length)})
|
|
188
189
|
`).run(...values).lastInsertRowid);
|
|
189
190
|
const insertCandidate = db.prepare(`
|
|
190
191
|
INSERT INTO delivery_candidates (event_id, tenant_id, memory_id, source_store, pool, stage, outcome, reason, cand_rank, score, tokens)
|
|
@@ -5,7 +5,7 @@ import { heldTexts } from '../util/same-text.js';
|
|
|
5
5
|
import { audit } from './audit-event.js';
|
|
6
6
|
import { deleteEntryCore } from './delete-and-batch.js';
|
|
7
7
|
import { deleteDormantRow, listDormantSnapshots, purgeDormantByDigest, replaceDormantEntry } from './dormant.js';
|
|
8
|
-
import { entryRejectRowAt,
|
|
8
|
+
import { entryRejectRowAt, selectEntriesByIds, selectTenantTexts } from './entry-reads.js';
|
|
9
9
|
import { stampOriginProject } from './entry-row.js';
|
|
10
10
|
import { writeEntryDbOnly, writeEntryMirrors } from './entry-writes.js';
|
|
11
11
|
import { purgeMirrorBestEffort } from './mirrors.js';
|
|
@@ -16,15 +16,20 @@ import { RejectedValueError } from '../core/api-errors.js';
|
|
|
16
16
|
function removeLiveRows(db, hippoRoot, rejection, holdsValue, removal) {
|
|
17
17
|
const { tenantId, actor, reason } = rejection;
|
|
18
18
|
const { removedIds, removedRawIds, successors } = removal;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Text columns decide which rows hold the value; only those rows are read whole.
|
|
20
|
+
const directIds = [];
|
|
21
|
+
const mergedIds = [];
|
|
22
|
+
for (const row of selectTenantTexts(db, tenantId)) {
|
|
21
23
|
if (!rejection.inReach(row.scope))
|
|
22
24
|
continue;
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
if (holdsValue(row.content))
|
|
26
|
+
directIds.push(row.id);
|
|
27
|
+
else if (heldTexts(row).some(holdsValue))
|
|
28
|
+
mergedIds.push(row.id);
|
|
29
|
+
}
|
|
30
|
+
const held = selectEntriesByIds(db, [...directIds, ...mergedIds], tenantId);
|
|
31
|
+
const merged = mergedIds.flatMap((id) => held.get(id) ?? []);
|
|
32
|
+
for (const row of directIds.flatMap((id) => held.get(id) ?? [])) {
|
|
28
33
|
if (row.kind === 'raw') {
|
|
29
34
|
// A raw row is append-only: archiving is its one removal path, and its savepoint nests in this write scope.
|
|
30
35
|
archiveRawMemory(db, row.id, { reason, who: actor });
|
|
@@ -97,7 +102,7 @@ export function applyRejection(hippoRoot, rejection) {
|
|
|
97
102
|
sourceMemoryId: memoryId ?? null,
|
|
98
103
|
normalizedChars: normalizeValueForRejection(content).length,
|
|
99
104
|
});
|
|
100
|
-
// SHORTCUT: scans
|
|
105
|
+
// SHORTCUT: scans the tenant's text columns, fine for a human-run command; a digest column needs a schema migration, held until approved.
|
|
101
106
|
const holdsValue = (text) => rejectionDigest(text) === digest;
|
|
102
107
|
removeLiveRows(db, hippoRoot, rejection, holdsValue, removal);
|
|
103
108
|
removeDormantCopies(db, rejection, digest, holdsValue, removal);
|
package/dist/store/rows.js
CHANGED
|
@@ -61,12 +61,12 @@ function rowToPlacementFields(row) {
|
|
|
61
61
|
scope: row.scope ?? null,
|
|
62
62
|
owner: row.owner ?? null,
|
|
63
63
|
artifact_ref: row.artifact_ref ?? null,
|
|
64
|
-
|
|
64
|
+
tenant_id: row.tenant_id ?? DEFAULT_TENANT_ID,
|
|
65
65
|
origin_project: row.origin_project ?? null,
|
|
66
66
|
descendant_count: Number(row.descendant_count ?? 0),
|
|
67
67
|
earliest_at: row.earliest_at ?? null,
|
|
68
68
|
latest_at: row.latest_at ?? null,
|
|
69
|
-
summary_dirty:
|
|
69
|
+
summary_dirty: Number(row.summary_dirty) === 1,
|
|
70
70
|
last_rebuilt_at: row.last_rebuilt_at ?? null,
|
|
71
71
|
rebuild_count: Number(row.rebuild_count ?? 0),
|
|
72
72
|
dag_level_3_built_at: row.dag_level_3_built_at ?? null,
|
package/dist/store/rule-sql.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** calculateRewardFactor as SQL over `memories` columns; strengthSql and the context-candidate rank both build on it, so the slope has one source. */
|
|
2
|
+
export declare function rewardFactorSql(): string;
|
|
1
3
|
/** calculateStrength's clock-basis formula as SQL over `memories` columns, flags and multipliers baked in; keep in step.
|
|
2
4
|
* An unparseable date scores NULL here and 0 in JS, so sums agree. */
|
|
3
5
|
export declare function strengthSql(now: Date): string;
|
package/dist/store/rule-sql.js
CHANGED
|
@@ -3,7 +3,16 @@ import { DAY_MS } from '../util/time.js';
|
|
|
3
3
|
import { isDecayAblated, isOutcomeSlowAblated, isRecallBoostAblated } from '../core/ablation.js';
|
|
4
4
|
import { DECAY_BASE, EMOTIONAL_MULTIPLIERS, FALLBACK_HALF_LIFE_DAYS, KEEP_PAIRS, MAX_WRONG_HALVINGS, RETRIEVAL_BOOST_SLOPE, REWARD_SLOPE, applyLossAversionRatio, } from '../core/memory.js';
|
|
5
5
|
import { PERSONAL_SCOPE_PREFIX, RECALL_DEFAULT_DENY_SCOPES } from '../core/recall-scope.js';
|
|
6
|
+
import { placeholders } from '../util/sql.js';
|
|
6
7
|
const UNIX_EPOCH_JULIAN_DAY = 2440587.5;
|
|
8
|
+
/** calculateRewardFactor as SQL over `memories` columns; strengthSql and the context-candidate rank both build on it, so the slope has one source. */
|
|
9
|
+
export function rewardFactorSql() {
|
|
10
|
+
if (isOutcomeSlowAblated())
|
|
11
|
+
return '1.0';
|
|
12
|
+
const pos = 'COALESCE(outcome_positive, 0)';
|
|
13
|
+
const neg = 'COALESCE(outcome_negative, 0)';
|
|
14
|
+
return `(CASE WHEN ${pos} = 0 AND ${neg} = 0 THEN 1.0 ELSE 1.0 + ${REWARD_SLOPE} * (${pos} - ${neg}) / (${pos} + ${neg} + 1.0) END)`;
|
|
15
|
+
}
|
|
7
16
|
/** calculateStrength's clock-basis formula as SQL over `memories` columns, flags and multipliers baked in; keep in step.
|
|
8
17
|
* An unparseable date scores NULL here and 0 in JS, so sums agree. */
|
|
9
18
|
export function strengthSql(now) {
|
|
@@ -11,10 +20,7 @@ export function strengthSql(now) {
|
|
|
11
20
|
const pos = 'COALESCE(outcome_positive, 0)';
|
|
12
21
|
const neg = 'COALESCE(outcome_negative, 0)';
|
|
13
22
|
const wrong = isOutcomeSlowAblated() || isDecayAblated() ? '0' : `MAX(0, ${neg} - ${pos})`;
|
|
14
|
-
const
|
|
15
|
-
? '1.0'
|
|
16
|
-
: `(CASE WHEN ${pos} = 0 AND ${neg} = 0 THEN 1.0 ELSE 1.0 + ${REWARD_SLOPE} * (${pos} - ${neg}) / (${pos} + ${neg} + 1.0) END)`;
|
|
17
|
-
const halfLife = `(COALESCE(half_life_days, ${FALLBACK_HALF_LIFE_DAYS}) * ${reward})`;
|
|
23
|
+
const halfLife = `(COALESCE(half_life_days, ${FALLBACK_HALF_LIFE_DAYS}) * ${rewardFactorSql()})`;
|
|
18
24
|
const anchor = isRecallBoostAblated() ? 'created' : 'last_retrieved';
|
|
19
25
|
const nowJulian = num(now.getTime() / DAY_MS + UNIX_EPOCH_JULIAN_DAY);
|
|
20
26
|
const decay = isDecayAblated() ? '1.0' : `pow(${DECAY_BASE}, (${nowJulian} - julianday(${anchor})) / ${halfLife})`;
|
|
@@ -38,8 +44,8 @@ const keepPairSql = (p) => `(COALESCE(superseded_by, '') = '' AND EXISTS (SELECT
|
|
|
38
44
|
export const AUTO_DELETABLE_SQL = `pinned = 0 AND kind != 'raw'${KEEP_PAIRS.map((p) => ` AND NOT ${keepPairSql(p)}`).join('')}`;
|
|
39
45
|
/** SQL twin of the no-request arm of passesScopeFilterForRecall; `ownScope` is bound and compared with `=`, so `%` or `_` in an owner match nothing extra. */
|
|
40
46
|
export function scopeAdmitSql(col, ownScope) {
|
|
41
|
-
const
|
|
42
|
-
const admitted = `${col}scope IS NULL OR (${col}scope NOT IN (${
|
|
47
|
+
const marks = placeholders(RECALL_DEFAULT_DENY_SCOPES.length);
|
|
48
|
+
const admitted = `${col}scope IS NULL OR (${col}scope NOT IN (${marks}) AND ${col}scope NOT LIKE '%:private:%')`;
|
|
43
49
|
if (ownScope == null)
|
|
44
50
|
return { sql: `(${admitted})`, params: [...RECALL_DEFAULT_DENY_SCOPES] };
|
|
45
51
|
// The own arm sits inside the outer parentheses so a caller's `AND ${sql}` cannot split it off.
|
|
@@ -18,13 +18,8 @@ export type OriginFilter = string | readonly string[];
|
|
|
18
18
|
/** Scope rule for recall: none requested is default-deny; 'exact' narrows to the request; 'additive' adds it to the default set, unless it is personal,
|
|
19
19
|
* which only `ownScope` opens. */
|
|
20
20
|
export declare function recallScopeFilter(requestedScope: string | undefined, mode: 'exact' | 'additive', ownScope?: string): RecallScopeFilter;
|
|
21
|
-
/** Sends term queries down the LIKE path on a build that has FTS. Only tests call it. */
|
|
22
|
-
export declare function _forceLikePathForTests(on: boolean): void;
|
|
23
21
|
/** Load likely search candidates: FTS5 when available, else LIKE, then full-store fallback. With `tenantId` every SELECT filters by it; omitted = no filter. */
|
|
24
22
|
export declare function loadSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string): MemoryEntry[];
|
|
25
|
-
/** Recall-mode loader: scope predicate in SQL so `unknown:legacy` cannot leak. Empty `requestedScope` default-denies (admits `ownScope`); else exact match.
|
|
26
|
-
* SQL `NOT LIKE '%:private:%'` runs before the LIMIT so private rows cannot starve admitted ones; `passesScopeFilterForRecall` is the exact post-filter. */
|
|
27
|
-
export declare function loadRecallSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean, originProjects?: OriginFilter, ownScope?: string): MemoryEntry[];
|
|
28
23
|
export interface RecallSearchEntriesOptions {
|
|
29
24
|
readonly limit?: number;
|
|
30
25
|
readonly tenantId?: string;
|
|
@@ -34,6 +29,11 @@ export interface RecallSearchEntriesOptions {
|
|
|
34
29
|
readonly originProjects?: OriginFilter;
|
|
35
30
|
readonly ownScope?: string;
|
|
36
31
|
}
|
|
32
|
+
/** Recall-mode loader: scope predicate in SQL so `unknown:legacy` cannot leak. Empty `requestedScope` default-denies (admits `ownScope`); else exact match.
|
|
33
|
+
* SQL `NOT LIKE '%:private:%'` runs before the LIMIT so private rows cannot starve admitted ones; `passesScopeFilterForRecall` is the exact post-filter. */
|
|
34
|
+
export declare function loadRecallEntries(hippoRoot: string, query: string, options?: RecallSearchEntriesOptions): MemoryEntry[];
|
|
35
|
+
/** Positional form of `loadRecallEntries`, kept internal because the frozen q3b-ranker-floor benchmark imports it from dist. */
|
|
36
|
+
export declare function loadRecallSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean, originProjects?: OriginFilter, ownScope?: string): MemoryEntry[];
|
|
37
37
|
export declare function loadRecallSearchEntriesFromDb(db: DatabaseSyncLike, query: string, options?: RecallSearchEntriesOptions): MemoryEntry[];
|
|
38
38
|
/** Which rows the vector arm of hybrid search may add: the same tenant, scope and superseded rules as the lexical load. */
|
|
39
39
|
export interface VectorCandidateSpec {
|