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
|
@@ -9,6 +9,7 @@ import { topVectorMatches } from '../db/vector-store.js';
|
|
|
9
9
|
import { MEMORY_SELECT_COLUMNS, MEMORY_SEARCH_COLUMNS, DEFAULT_SEARCH_CANDIDATE_LIMIT, rowToEntry, } from './rows.js';
|
|
10
10
|
import { onHandle, openStore } from './open.js';
|
|
11
11
|
import { escapeLike } from '../util/escape.js';
|
|
12
|
+
import { placeholders } from '../util/sql.js';
|
|
12
13
|
/** The recall scope rule for a table column prefix (`m.` or none); `passesScopeFilterForRecall` in recall-scope.ts is its JS twin. */
|
|
13
14
|
function recallScopeClause(col, scopeFilter) {
|
|
14
15
|
if (scopeFilter === undefined)
|
|
@@ -40,11 +41,6 @@ export function recallScopeFilter(requestedScope, mode, ownScope) {
|
|
|
40
41
|
return mode === 'additive' ? { mode: 'default-deny-or-exact', value: requestedScope, ownScope } : { mode: 'exact', value: requestedScope };
|
|
41
42
|
}
|
|
42
43
|
const FTS_QUERY_SYNTAX_RE = /fts5: syntax error|unterminated string/i;
|
|
43
|
-
let forceLikePath = false;
|
|
44
|
-
/** Sends term queries down the LIKE path on a build that has FTS. Only tests call it. */
|
|
45
|
-
export function _forceLikePathForTests(on) {
|
|
46
|
-
forceLikePath = on;
|
|
47
|
-
}
|
|
48
44
|
function loadSearchRows(db, query, limit, tenantId, options = {}) {
|
|
49
45
|
const { scopeFilter, includeSuperseded = true, originProjects } = options;
|
|
50
46
|
const p = searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjects);
|
|
@@ -53,8 +49,7 @@ function loadSearchRows(db, query, limit, tenantId, options = {}) {
|
|
|
53
49
|
// LIMIT here too, so every candidate path honours the caller's cap.
|
|
54
50
|
return selectAllCandidates(db, p, limit);
|
|
55
51
|
}
|
|
56
|
-
|
|
57
|
-
if (!forceLikePath && isFtsAvailable(db)) {
|
|
52
|
+
if (isFtsAvailable(db)) {
|
|
58
53
|
const rows = selectFtsCandidates(db, terms, p, limit);
|
|
59
54
|
if (rows.length > 0)
|
|
60
55
|
return rows;
|
|
@@ -152,10 +147,12 @@ export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDI
|
|
|
152
147
|
}
|
|
153
148
|
/** Recall-mode loader: scope predicate in SQL so `unknown:legacy` cannot leak. Empty `requestedScope` default-denies (admits `ownScope`); else exact match.
|
|
154
149
|
* SQL `NOT LIKE '%:private:%'` runs before the LIMIT so private rows cannot starve admitted ones; `passesScopeFilterForRecall` is the exact post-filter. */
|
|
150
|
+
export function loadRecallEntries(hippoRoot, query, options = {}) {
|
|
151
|
+
return onHandle(hippoRoot, (db) => loadRecallSearchEntriesFromDb(db, query, options), openStore);
|
|
152
|
+
}
|
|
153
|
+
/** Positional form of `loadRecallEntries`, kept internal because the frozen q3b-ranker-floor benchmark imports it from dist. */
|
|
155
154
|
export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProjects, ownScope) {
|
|
156
|
-
return
|
|
157
|
-
return loadRecallSearchEntriesFromDb(db, query, { limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded, originProjects, ownScope });
|
|
158
|
-
}, openStore);
|
|
155
|
+
return loadRecallEntries(hippoRoot, query, { limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded, originProjects, ownScope });
|
|
159
156
|
}
|
|
160
157
|
// Split out so callers with an already-open db (the prompt-recall path) skip
|
|
161
158
|
// the initStore+open/close cycle per store per call.
|
|
@@ -176,7 +173,7 @@ export async function loadVectorCandidateEntries(hippoRoot, queryVector, spec) {
|
|
|
176
173
|
if (matches.length === 0)
|
|
177
174
|
return [];
|
|
178
175
|
// SAFETY: the SELECT names exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
179
|
-
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${matches.
|
|
176
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders(matches.length)})`)
|
|
180
177
|
.all(...matches.map((m) => m.id));
|
|
181
178
|
const byId = new Map(rows.map((r) => [r.id, rowToEntry(r)]));
|
|
182
179
|
return matches.flatMap((m) => byId.get(m.id) ?? []);
|
|
@@ -197,7 +194,7 @@ export function pickRarestFtsQuery(db, terms, maxTerms = RAREST_TERM_COUNT) {
|
|
|
197
194
|
return '';
|
|
198
195
|
// SAFETY: rows' shape matches the two columns named in the SELECT.
|
|
199
196
|
const rows = db
|
|
200
|
-
.prepare(`SELECT term, doc FROM temp.z1_rarest_vocab WHERE term IN (${vocab.
|
|
197
|
+
.prepare(`SELECT term, doc FROM temp.z1_rarest_vocab WHERE term IN (${placeholders(vocab.length)})`)
|
|
201
198
|
.all(...vocab);
|
|
202
199
|
const counts = new Map(rows.map((r) => [r.term, r.doc]));
|
|
203
200
|
return rarestFtsQuery(terms, (part) => counts.get(part) ?? 0, maxTerms);
|
package/dist/store/set-aside.js
CHANGED
|
@@ -7,7 +7,7 @@ export const SYNC_ACTOR = 'agent-memories';
|
|
|
7
7
|
/** Design 6's set-aside on the caller's transaction: a pinned row only loses the tag, any other goes dormant, restorable. */
|
|
8
8
|
export function setAsideRow(db, tag, row, why) {
|
|
9
9
|
const untagged = { ...row, tags: row.tags.filter((t) => t !== tag) };
|
|
10
|
-
const audit = (metadata) => appendAuditEvent(db, { tenantId: row.
|
|
10
|
+
const audit = (metadata) => appendAuditEvent(db, { tenantId: row.tenant_id, actor: SYNC_ACTOR, op: 'agent_memory_set_aside', targetId: row.id, metadata });
|
|
11
11
|
if (row.pinned) {
|
|
12
12
|
setEntryTagsInTx(db, untagged);
|
|
13
13
|
audit({ why, untagged: true });
|
|
@@ -22,7 +22,9 @@ function loggedMemory(db, event) {
|
|
|
22
22
|
function writeWithCompanions(db, { entry, actor, event, quarantine }) {
|
|
23
23
|
writeInOwnTenant(db, entry, actor, (handle, memoryId) => {
|
|
24
24
|
if (quarantine)
|
|
25
|
-
recordQuarantine(handle, {
|
|
25
|
+
recordQuarantine(handle, {
|
|
26
|
+
tenantId: entry.tenant_id, memoryId, originalScope: quarantine.originalScope, reason: quarantine.reason, actor,
|
|
27
|
+
});
|
|
26
28
|
if (event && !logEvent(handle, event, memoryId))
|
|
27
29
|
throw new EventAlreadyLogged();
|
|
28
30
|
});
|
|
@@ -2,11 +2,11 @@ import { type DatabaseSyncLike } from '../../db/index.js';
|
|
|
2
2
|
import { type MemoryEntry } from '../../core/memory.js';
|
|
3
3
|
import { type ArchiveOpts } from '../raw-archive.js';
|
|
4
4
|
import type { EntryWrite, EntryWrites, OutcomeWrite, RawArchive, Sync } from '../port.js';
|
|
5
|
-
import { type
|
|
5
|
+
import { type WriteEntryInternalOptions } from '../entry-writes.js';
|
|
6
6
|
/** Each call on its own handle; mirrors and the forgotten counter follow the commit, so a rolled-back write leaves neither. */
|
|
7
7
|
export declare function sqliteEntryWrites(hippoRoot: string): Sync<EntryWrites>;
|
|
8
8
|
/** A store-side hook writes on the row's handle inside its write scope, so its throw undoes the row. */
|
|
9
|
-
export declare function writeEntryAt(hippoRoot: string, { entry, actor }: EntryWrite, afterWrite?:
|
|
9
|
+
export declare function writeEntryAt(hippoRoot: string, { entry, actor }: EntryWrite, afterWrite?: WriteEntryInternalOptions['afterWrite']): void;
|
|
10
10
|
/** A trace link follows the commit on the same handle and names only the ids applied, so a rolled-back outcome leaves none. */
|
|
11
11
|
export declare function applyOutcomeAt(hippoRoot: string, outcome: OutcomeWrite, traceId?: number): string[];
|
|
12
12
|
/** Reach is checked inside the archive's write scope. A store-side hook writes on the same handle inside that scope, so its throw undoes the archive. */
|
|
@@ -14,5 +14,5 @@ export declare function archiveRawAt(hippoRoot: string, archive: RawArchive, aft
|
|
|
14
14
|
/** The refusal row lands after `write` has rolled back, in a fresh implicit transaction the aborted one cannot undo. */
|
|
15
15
|
export declare function auditingRefusal<T>(db: DatabaseSyncLike, actor: string, write: () => T): T;
|
|
16
16
|
/** writeEntryAt on the caller's handle, so a sibling group can read on it after the scope has unwound. */
|
|
17
|
-
export declare function writeInOwnTenant(db: DatabaseSyncLike, entry: MemoryEntry, actor: string, afterWrite?:
|
|
17
|
+
export declare function writeInOwnTenant(db: DatabaseSyncLike, entry: MemoryEntry, actor: string, afterWrite?: WriteEntryInternalOptions['afterWrite']): void;
|
|
18
18
|
//# sourceMappingURL=entry-writes-group.d.ts.map
|
|
@@ -91,7 +91,7 @@ function assertInReach(db, target, id) {
|
|
|
91
91
|
/** hippo.db's upsert would move the row into the writer's tenant, so a store write refuses an id another tenant holds. */
|
|
92
92
|
function assertIdInTenant(db, entry) {
|
|
93
93
|
const holder = selectMemoryReach(db, entry.id)?.tenantId;
|
|
94
|
-
if (holder !== undefined && holder !== entry.
|
|
94
|
+
if (holder !== undefined && holder !== entry.tenant_id)
|
|
95
95
|
throw new ConflictError(`Memory ${entry.id} belongs to another tenant`);
|
|
96
96
|
}
|
|
97
97
|
/** The refusal row lands after `write` has rolled back, in a fresh implicit transaction the aborted one cannot undo. */
|
|
@@ -3,6 +3,8 @@ export interface StoreHolder {
|
|
|
3
3
|
hold: () => void;
|
|
4
4
|
afterResponse: () => void;
|
|
5
5
|
release: () => Promise<void>;
|
|
6
|
+
/** Runs one request, inside the fence that keeps hippo.db shut when the store is not it. */
|
|
7
|
+
runRequest: (run: () => Promise<void>) => Promise<void>;
|
|
6
8
|
}
|
|
7
9
|
/** Holds a connection to the hippo.db under `hippoRoot` once it exists, so serving never creates one. */
|
|
8
10
|
export declare function holdSqliteConnection(hippoRoot: string): StoreHolder;
|
|
@@ -36,6 +36,6 @@ export function holdSqliteConnection(hippoRoot) {
|
|
|
36
36
|
closeHippoDb(heldDb);
|
|
37
37
|
heldDb = undefined;
|
|
38
38
|
};
|
|
39
|
-
return { hold, afterResponse, release };
|
|
39
|
+
return { hold, afterResponse, release, runRequest: (run) => run() };
|
|
40
40
|
}
|
|
41
41
|
//# sourceMappingURL=held-connection.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LastRecall, RecallWrites, Sync } from '../port.js';
|
|
2
|
+
/** What a finished recall wrote after its audit rows: the trace's id, null when none was asked or its write failed, and the ids strengthened. */
|
|
3
|
+
interface RecallFinish {
|
|
4
|
+
readonly traceId: number | null;
|
|
5
|
+
readonly strengthened: ReadonlySet<string>;
|
|
6
|
+
}
|
|
7
|
+
/** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
|
|
8
|
+
export declare function finishRecallAt(hippoRoot: string, writes: RecallWrites): RecallFinish;
|
|
9
|
+
export declare function sqliteLastRecall(hippoRoot: string): Sync<LastRecall>;
|
|
10
|
+
/** The group as a served store answers it: the call runs at once and resolves through a Promise, so a throw rejects as another store's would. */
|
|
11
|
+
export declare function servedLastRecall(sync: Sync<LastRecall>): LastRecall;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=last-recall-group.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// hippo.db's LastRecall group: the last recall sits in its meta table, and a traced outcome links on the outcome's own handle.
|
|
2
|
+
import { withWriteScope } from '../../db/index.js';
|
|
3
|
+
import { appendAuditEvent } from '../audit.js';
|
|
4
|
+
import { strengthenRetrieved, strengthenRetrievedInOwnTx } from '../entry-writes.js';
|
|
5
|
+
import { localGoalRecallRows, writeGoalRecallLog } from '../goals.js';
|
|
6
|
+
import { loadLastRecall, saveIndex } from '../index-and-stats.js';
|
|
7
|
+
import { onHandle } from '../open.js';
|
|
8
|
+
import { writeRecallTrace } from '../recall-trace.js';
|
|
9
|
+
import { applyOutcomeAt } from './entry-writes-group.js';
|
|
10
|
+
/** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
|
|
11
|
+
export function finishRecallAt(hippoRoot, writes) {
|
|
12
|
+
return onHandle(hippoRoot, (db) => {
|
|
13
|
+
const traceAndStrengthen = () => ({
|
|
14
|
+
traceId: writes.trace ? writeRecallTrace(db, writes.trace) : null,
|
|
15
|
+
strengthened: writes.strengthen ? strengthenRetrievedInOwnTx(db, writes.strengthen.ids, writes.strengthen.opts) : new Set(),
|
|
16
|
+
});
|
|
17
|
+
// Alone, the fail-soft trace and strengthen keep their own scopes so a busy lock loses them, never the read.
|
|
18
|
+
if (writes.goalLog.length === 0 && writes.audit.length === 0)
|
|
19
|
+
return traceAndStrengthen();
|
|
20
|
+
// Inside this scope both run as savepoints, so one recall commits once and a failed trace or strengthen still unwinds alone.
|
|
21
|
+
return withWriteScope(db, 'finish_recall', () => {
|
|
22
|
+
writeGoalRecallLog(db, localGoalRecallRows(db, writes.goalLog));
|
|
23
|
+
for (const event of writes.audit)
|
|
24
|
+
appendAuditEvent(db, event);
|
|
25
|
+
return traceAndStrengthen();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function sqliteLastRecall(hippoRoot) {
|
|
30
|
+
return {
|
|
31
|
+
applyTracedOutcome: (outcome, traceId) => applyOutcomeAt(hippoRoot, outcome, traceId),
|
|
32
|
+
applyOutcomeToLast(target, good) {
|
|
33
|
+
const { last_retrieval_ids: ids, last_trace_id: trace } = loadLastRecall(hippoRoot);
|
|
34
|
+
if (ids.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
return applyOutcomeAt(hippoRoot, { ...target, ids, good }, trace === null ? undefined : Number(trace));
|
|
37
|
+
},
|
|
38
|
+
finishRecall(writes, globalRoot) {
|
|
39
|
+
const { ids, opts } = writes.strengthen;
|
|
40
|
+
const { traceId, strengthened } = finishRecallAt(hippoRoot, writes);
|
|
41
|
+
if (globalRoot !== undefined)
|
|
42
|
+
strengthenRetrieved(globalRoot, ids.filter((id) => !strengthened.has(id)), opts);
|
|
43
|
+
// One write for both keys, so an outcome never pairs these ids with an older trace; a lost trace saves null.
|
|
44
|
+
saveIndex(hippoRoot, { last_retrieval_ids: [...ids], last_trace_id: traceId === null ? null : String(traceId) });
|
|
45
|
+
return traceId;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The group as a served store answers it: the call runs at once and resolves through a Promise, so a throw rejects as another store's would. */
|
|
50
|
+
export function servedLastRecall(sync) {
|
|
51
|
+
return {
|
|
52
|
+
applyTracedOutcome: async (outcome, traceId) => sync.applyTracedOutcome(outcome, traceId),
|
|
53
|
+
applyOutcomeToLast: async (target, good) => sync.applyOutcomeToLast(target, good),
|
|
54
|
+
finishRecall: async (writes, globalRoot) => sync.finishRecall(writes, globalRoot),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=last-recall-group.js.map
|
|
@@ -1,21 +1,4 @@
|
|
|
1
1
|
import { type ScopeGrantChange } from '../key-writes.js';
|
|
2
|
-
import type { EntryTarget, OutcomeWrite, RecallWrites } from '../port.js';
|
|
3
|
-
/** A recall's writes when hippo.db keeps it as its last one: `strengthen.ids` are the ids it returned. */
|
|
4
|
-
type LastRecallWrites = RecallWrites & Required<Pick<RecallWrites, 'strengthen'>>;
|
|
5
|
-
/** The port's writes with a part that needs hippo.db's own handle or its meta table, which no other store has. */
|
|
6
|
-
export interface SqliteLocal {
|
|
7
|
-
/** entryWrites.applyOutcome, then a link from the recall trace to the ids applied, on the same handle after the commit. */
|
|
8
|
-
applyOutcome(outcome: OutcomeWrite, traceId: number): string[];
|
|
9
|
-
/** applyOutcome on the ids of the last recall, which hippo.db's meta table holds with that recall's trace; answers the ids applied. */
|
|
10
|
-
applyOutcomeToLastRecall(target: EntryTarget, good: boolean): string[];
|
|
11
|
-
/** finishRecall, then the ids this root lacks strengthened under `globalRoot`, then the ids and their trace saved as the last recall;
|
|
12
|
-
* answers the trace id. A store of another kind keeps no last recall, so its stand-in is its own finishRecall, answering null. */
|
|
13
|
-
finishLastRecall(writes: LastRecallWrites, globalRoot: string | undefined): number | null | Promise<number | null>;
|
|
14
|
-
}
|
|
15
|
-
export declare function sqliteLocal(hippoRoot: string): SqliteLocal;
|
|
16
|
-
/** What a served store answers for each write that needs hippo.db's own handle: a refusal. */
|
|
17
|
-
export declare const REFUSED_ON_A_STORE: Pick<SqliteLocal, 'applyOutcome'>;
|
|
18
2
|
/** Off the port: only the local CLI changes grants, through synchronous published functions a store's Promise cannot answer. */
|
|
19
3
|
export declare function changeScopeGrant(hippoRoot: string, change: ScopeGrantChange): void;
|
|
20
|
-
export {};
|
|
21
4
|
//# sourceMappingURL=local.d.ts.map
|
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
// What only hippo.db does: operations no port method covers, so they never run on another store.
|
|
2
|
-
import { strengthenRetrieved } from '../entry-writes.js';
|
|
3
|
-
import { loadLastRecall, saveIndex } from '../index-and-stats.js';
|
|
4
2
|
import { changeScopeGrantAt } from '../key-writes.js';
|
|
5
3
|
import { onHandle } from '../open.js';
|
|
6
|
-
import { applyOutcomeAt } from './entry-writes-group.js';
|
|
7
|
-
import { finishRecallAt } from './store.js';
|
|
8
|
-
export function sqliteLocal(hippoRoot) {
|
|
9
|
-
return {
|
|
10
|
-
applyOutcome: (outcome, traceId) => applyOutcomeAt(hippoRoot, outcome, traceId),
|
|
11
|
-
// The ids and the trace come from one statement, so the outcome is linked to the recall that returned those ids.
|
|
12
|
-
applyOutcomeToLastRecall(target, good) {
|
|
13
|
-
const { last_retrieval_ids: ids, last_trace_id: trace } = loadLastRecall(hippoRoot);
|
|
14
|
-
if (ids.length === 0)
|
|
15
|
-
return [];
|
|
16
|
-
return applyOutcomeAt(hippoRoot, { ...target, ids, good }, trace === null ? undefined : Number(trace));
|
|
17
|
-
},
|
|
18
|
-
finishLastRecall(writes, globalRoot) {
|
|
19
|
-
const { ids, opts } = writes.strengthen;
|
|
20
|
-
const { traceId, strengthened } = finishRecallAt(hippoRoot, writes);
|
|
21
|
-
if (globalRoot !== undefined)
|
|
22
|
-
strengthenRetrieved(globalRoot, ids.filter((id) => !strengthened.has(id)), opts);
|
|
23
|
-
// One write for both keys, so an outcome never pairs these ids with an older trace; a lost trace saves null.
|
|
24
|
-
saveIndex(hippoRoot, { last_retrieval_ids: [...ids], last_trace_id: traceId === null ? null : String(traceId) });
|
|
25
|
-
return traceId;
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/** What a served store answers for each write that needs hippo.db's own handle: a refusal. */
|
|
30
|
-
export const REFUSED_ON_A_STORE = {
|
|
31
|
-
applyOutcome() {
|
|
32
|
-
throw new Error('an outcome links its recall trace on hippo.db only, never through a store');
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
4
|
/** Off the port: only the local CLI changes grants, through synchronous published functions a store's Promise cannot answer. */
|
|
36
5
|
export function changeScopeGrant(hippoRoot, change) {
|
|
37
6
|
onHandle(hippoRoot, (db) => changeScopeGrantAt(db, change));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AuditMaintenance, LessonWrites, Sync } from '../port.js';
|
|
2
|
+
export declare function sqliteAuditMaintenance(hippoRoot: string): Sync<AuditMaintenance>;
|
|
3
|
+
export declare function servedAuditMaintenance(sync: Sync<AuditMaintenance>): AuditMaintenance;
|
|
4
|
+
export declare function sqliteLessonWrites(hippoRoot: string): Sync<LessonWrites>;
|
|
5
|
+
export declare function servedLessonWrites(sync: Sync<LessonWrites>): LessonWrites;
|
|
6
|
+
//# sourceMappingURL=maintenance-groups.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// hippo.db's half of the AuditMaintenance and LessonWrites store groups: the quality audit, audit prune and git learn's batch.
|
|
2
|
+
import { pruneAuditRows } from '../audit.js';
|
|
3
|
+
import { loadTextsHoldingWords } from '../candidates.js';
|
|
4
|
+
import { deleteEntriesAutomatically, memoriesBackingObjects } from '../delete-and-batch.js';
|
|
5
|
+
import { loadAllEntries } from '../entry-reads.js';
|
|
6
|
+
import { writeEntriesSkippingRejected, writeEntriesTogether } from '../entry-writes.js';
|
|
7
|
+
import { updateStats } from '../index-and-stats.js';
|
|
8
|
+
import { repairAutomaticMemories, repairQualityOnce } from '../quality-repair.js';
|
|
9
|
+
import { touchableScopeSql } from '../rule-sql.js';
|
|
10
|
+
export function sqliteAuditMaintenance(hippoRoot) {
|
|
11
|
+
return {
|
|
12
|
+
tenantEntries: (tenantId) => loadAllEntries(hippoRoot, tenantId),
|
|
13
|
+
objectBackedIds: () => memoriesBackingObjects(hippoRoot),
|
|
14
|
+
deleteAutomatically: (removals, actor) => deleteEntriesAutomatically(hippoRoot, removals, actor),
|
|
15
|
+
pruneAuditRows: (prune) => pruneAuditRows(hippoRoot, prune),
|
|
16
|
+
repairQuality: (tenantId, apply) => repairAutomaticMemories(hippoRoot, { tenantId, apply }),
|
|
17
|
+
repairQualityOnce: (tenantId) => repairQualityOnce(hippoRoot, tenantId),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function servedAuditMaintenance(sync) {
|
|
21
|
+
return {
|
|
22
|
+
tenantEntries: async (tenantId) => sync.tenantEntries(tenantId),
|
|
23
|
+
objectBackedIds: async () => sync.objectBackedIds(),
|
|
24
|
+
deleteAutomatically: async (removals, actor) => sync.deleteAutomatically(removals, actor),
|
|
25
|
+
pruneAuditRows: async (prune) => sync.pruneAuditRows(prune),
|
|
26
|
+
repairQuality: async (tenantId, apply) => sync.repairQuality(tenantId, apply),
|
|
27
|
+
repairQualityOnce: async (tenantId) => sync.repairQualityOnce(tenantId),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function sqliteLessonWrites(hippoRoot) {
|
|
31
|
+
return {
|
|
32
|
+
tenantEntries: (tenantId) => loadAllEntries(hippoRoot, tenantId),
|
|
33
|
+
textsHoldingWords: (tenantId, words, ownScope) => loadTextsHoldingWords(hippoRoot, tenantId, words, undefined, touchableScopeSql('', ownScope)),
|
|
34
|
+
writeSkippingRejected(entries, actor) {
|
|
35
|
+
const written = [];
|
|
36
|
+
writeEntriesSkippingRejected(hippoRoot, (put) => {
|
|
37
|
+
for (const entry of entries)
|
|
38
|
+
if (put(entry))
|
|
39
|
+
written.push(entry.id);
|
|
40
|
+
}, actor);
|
|
41
|
+
return written;
|
|
42
|
+
},
|
|
43
|
+
writeTogether(entries) {
|
|
44
|
+
writeEntriesTogether(hippoRoot, entries);
|
|
45
|
+
},
|
|
46
|
+
bumpRemembered(count) {
|
|
47
|
+
updateStats(hippoRoot, { remembered: count });
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function servedLessonWrites(sync) {
|
|
52
|
+
return {
|
|
53
|
+
tenantEntries: async (tenantId) => sync.tenantEntries(tenantId),
|
|
54
|
+
textsHoldingWords: async (tenantId, words, ownScope) => sync.textsHoldingWords(tenantId, words, ownScope),
|
|
55
|
+
writeSkippingRejected: async (entries, actor) => sync.writeSkippingRejected(entries, actor),
|
|
56
|
+
writeTogether: async (entries) => sync.writeTogether(entries),
|
|
57
|
+
bumpRemembered: async (count) => sync.bumpRemembered(count),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=maintenance-groups.js.map
|
|
@@ -3,6 +3,7 @@ import { appendAuditEvent } from '../audit.js';
|
|
|
3
3
|
import { getMeta, withWriteScopeOr } from '../../db/index.js';
|
|
4
4
|
import { escapeLike } from '../../util/escape.js';
|
|
5
5
|
import { keysetAfter } from '../../util/keyset.js';
|
|
6
|
+
import { chunked } from '../../util/chunked.js';
|
|
6
7
|
import { calculateStrength, deriveHalfLife } from '../../core/memory.js';
|
|
7
8
|
import { scopeAdmitSql } from '../rule-sql.js';
|
|
8
9
|
import { stampOriginProject, upsertEntryRow } from '../entry-row.js';
|
|
@@ -13,6 +14,7 @@ import { LEGACY_TYPED_HALF_LIFE, onHandle, openStore, TYPED_HALF_LIFE_META_KEY }
|
|
|
13
14
|
import { isObjectRefusal, } from '../port.js';
|
|
14
15
|
import { auditingRefusal } from './entry-writes-group.js';
|
|
15
16
|
import { insertSpec, rowSpec } from './object-rows.js';
|
|
17
|
+
import { placeholders } from '../../util/sql.js';
|
|
16
18
|
export function sqliteObjects(hippoRoot) {
|
|
17
19
|
return {
|
|
18
20
|
listObjects: (tenantId, kind, query) => onHandle(hippoRoot, (db) => listRows(db, tenantId, kind, query)),
|
|
@@ -80,7 +82,7 @@ function listRows(db, tenantId, kind, query) {
|
|
|
80
82
|
function closeRow(db, tenantId, kind, id, close) {
|
|
81
83
|
const spec = rowSpec(kind);
|
|
82
84
|
return withWriteScopeOr(db, 'close_object', (rollback) => {
|
|
83
|
-
const marks = close.from.
|
|
85
|
+
const marks = placeholders(close.from.length);
|
|
84
86
|
const updated = db.prepare(`UPDATE ${spec.table} SET status = 'closed', closed_at = ? WHERE id = ? AND tenant_id = ? AND status IN (${marks})`).run(close.at, id, tenantId, ...close.from);
|
|
85
87
|
if (updated.changes === 0) {
|
|
86
88
|
const existing = db.prepare(`SELECT status FROM ${spec.table} WHERE id = ? AND tenant_id = ?`)
|
|
@@ -126,7 +128,7 @@ function insertRow(db, row) {
|
|
|
126
128
|
}
|
|
127
129
|
const result = db.prepare(`
|
|
128
130
|
INSERT INTO ${rowSpec(row.kind).table}(${cols.join(', ')}, status, superseded_by, superseded_at, closed_at, created_at)
|
|
129
|
-
VALUES (${cols.
|
|
131
|
+
VALUES (${placeholders(cols.length)}, 'active', NULL, NULL, NULL, ?)
|
|
130
132
|
`).run(...values, row.save.at);
|
|
131
133
|
return Number(result.lastInsertRowid ?? 0);
|
|
132
134
|
}
|
|
@@ -217,11 +219,17 @@ function saveAt(hippoRoot, tenantId, kind, save) {
|
|
|
217
219
|
/** The linked ids are checked once the mirror is in and before the INSERT, so a refusal undoes the mirror with it. */
|
|
218
220
|
function openIncidentRow(db, tenantId, memoryId, open) {
|
|
219
221
|
const { incidentText, context, linkedMemoryIds } = open.fields;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const held = new Set();
|
|
223
|
+
for (const ids of chunked(linkedMemoryIds)) {
|
|
224
|
+
const marks = placeholders(ids.length);
|
|
225
|
+
// SAFETY: rows' shape matches the one column named in the SELECT.
|
|
226
|
+
const found = db.prepare(`SELECT id FROM memories WHERE tenant_id = ? AND id IN (${marks})`).all(tenantId, ...ids);
|
|
227
|
+
for (const r of found)
|
|
228
|
+
held.add(r.id);
|
|
224
229
|
}
|
|
230
|
+
const missing = linkedMemoryIds.find((linkId) => !held.has(linkId));
|
|
231
|
+
if (missing !== undefined)
|
|
232
|
+
return { refused: 'unlinked', memoryId: missing };
|
|
225
233
|
const result = db.prepare(`
|
|
226
234
|
INSERT INTO incidents(
|
|
227
235
|
memory_id, tenant_id, incident_text, context,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HippoStore,
|
|
1
|
+
import type { HippoStore, StoreGroups, Sync } from '../port.js';
|
|
2
2
|
/** The store a request runs on: the served one, else hippo.db under its root, as the CLI and SDK callers have it. */
|
|
3
3
|
export declare function storeFor(ctx: {
|
|
4
4
|
readonly hippoRoot: string;
|
|
@@ -12,12 +12,5 @@ export type SqliteSyncStore = Sync<Omit<HippoStore, AsyncOnly>>;
|
|
|
12
12
|
export declare function sqliteSyncStore(hippoRoot: string): SqliteSyncStore & Sync<Omit<StoreGroups, AsyncOnly>>;
|
|
13
13
|
/** `sqliteSyncStore` as a served store: each method runs at once and answers through a Promise, so a throw rejects as another store's would. */
|
|
14
14
|
export declare function sqliteStore(hippoRoot: string): HippoStore & StoreGroups;
|
|
15
|
-
/** What a finished recall wrote after its audit rows: the trace's id, null when none was asked or its write failed, and the ids strengthened. */
|
|
16
|
-
interface RecallFinish {
|
|
17
|
-
readonly traceId: number | null;
|
|
18
|
-
readonly strengthened: ReadonlySet<string>;
|
|
19
|
-
}
|
|
20
|
-
/** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
|
|
21
|
-
export declare function finishRecallAt(hippoRoot: string, writes: RecallWrites): RecallFinish;
|
|
22
15
|
export {};
|
|
23
16
|
//# sourceMappingURL=store.d.ts.map
|
|
@@ -4,17 +4,17 @@ import { listApiKeyRows, readApiKeyRecord } from '../auth.js';
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { appendAuditEvent, listAuditEventsAfter, queryAuditEvents } from '../audit.js';
|
|
6
6
|
import { getHippoDbPath, withWriteScope } from '../../db/index.js';
|
|
7
|
-
import { activeGoalsWithPolicies
|
|
7
|
+
import { activeGoalsWithPolicies } from '../goals.js';
|
|
8
8
|
import { planningFallacyEvidenceAt } from '../planning-fallacy-evidence.js';
|
|
9
|
-
import { writeRecallTrace } from '../recall-trace.js';
|
|
10
9
|
import { recordTokenUse } from '../token-ledger.js';
|
|
11
10
|
import { loadAmbientCandidates, loadContextCandidates } from '../candidates.js';
|
|
12
11
|
import { loadEntriesByIds, loadFreshRawMemories } from '../entry-reads.js';
|
|
13
|
-
import { strengthenRetrievedInOwnTx } from '../entry-writes.js';
|
|
14
12
|
import { servedConnectorEvents, sqliteConnectorEvents } from './connector-events-group.js';
|
|
15
13
|
import { servedConnectorWrites, sqliteConnectorWrites } from './connector-writes-group.js';
|
|
16
14
|
import { sqliteDagReads } from './dag-reads-group.js';
|
|
17
15
|
import { sqliteEntryWrites } from './entry-writes-group.js';
|
|
16
|
+
import { servedAuditMaintenance, servedLessonWrites, sqliteAuditMaintenance, sqliteLessonWrites } from './maintenance-groups.js';
|
|
17
|
+
import { finishRecallAt, servedLastRecall, sqliteLastRecall } from './last-recall-group.js';
|
|
18
18
|
import { servedGraphReads, sqliteGraphReads } from './graph-reads-group.js';
|
|
19
19
|
import { servedObjects, sqliteObjects } from './objects-group.js';
|
|
20
20
|
import { servedPredictions, sqlitePredictions } from './predictions-group.js';
|
|
@@ -25,7 +25,7 @@ import { updateStats } from '../index-and-stats.js';
|
|
|
25
25
|
import { auditHighIdAt, revokeKeyAt } from '../key-audit.js';
|
|
26
26
|
import { createKeyAt, createSelfKeyAt } from '../key-writes.js';
|
|
27
27
|
import { onHandle } from '../open.js';
|
|
28
|
-
import {
|
|
28
|
+
import { loadRecallEntries } from '../search-rows.js';
|
|
29
29
|
import { listSessionEvents, loadActiveTaskSnapshot } from '../sessions.js';
|
|
30
30
|
import { entriesWithoutVectorAt, loadStoredVectorViews, writeVectorsAt } from '../vector-index.js';
|
|
31
31
|
/** The store a request runs on: the served one, else hippo.db under its root, as the CLI and SDK callers have it. */
|
|
@@ -36,6 +36,7 @@ export function storeFor(ctx) {
|
|
|
36
36
|
export function sqliteSyncStore(hippoRoot) {
|
|
37
37
|
return {
|
|
38
38
|
kind: 'sqlite',
|
|
39
|
+
localFiles: { hippoRoot },
|
|
39
40
|
...sqliteBaseReads(hippoRoot),
|
|
40
41
|
...sqliteRecallWrites(hippoRoot),
|
|
41
42
|
keyAudit: sqliteKeyAudit(hippoRoot),
|
|
@@ -53,6 +54,9 @@ export function sqliteSyncStore(hippoRoot) {
|
|
|
53
54
|
connectorEvents: sqliteConnectorEvents(hippoRoot),
|
|
54
55
|
objects: sqliteObjects(hippoRoot),
|
|
55
56
|
readiness: sqliteReadiness(hippoRoot),
|
|
57
|
+
lastRecall: sqliteLastRecall(hippoRoot),
|
|
58
|
+
auditMaintenance: sqliteAuditMaintenance(hippoRoot),
|
|
59
|
+
lessonWrites: sqliteLessonWrites(hippoRoot),
|
|
56
60
|
close() { },
|
|
57
61
|
};
|
|
58
62
|
}
|
|
@@ -62,7 +66,7 @@ function sqliteBaseReads(hippoRoot) {
|
|
|
62
66
|
return onHandle(hippoRoot, (db) => readApiKeyRecord(db, keyId));
|
|
63
67
|
},
|
|
64
68
|
searchRecallEntries(query, args) {
|
|
65
|
-
return
|
|
69
|
+
return loadRecallEntries(hippoRoot, query, args);
|
|
66
70
|
},
|
|
67
71
|
entriesByIds(ids, tenantId) {
|
|
68
72
|
return loadEntriesByIds(hippoRoot, ids, tenantId);
|
|
@@ -141,6 +145,7 @@ export function sqliteStore(hippoRoot) {
|
|
|
141
145
|
const sync = sqliteSyncStore(hippoRoot);
|
|
142
146
|
return {
|
|
143
147
|
...servedBaseMethods(sync),
|
|
148
|
+
localFiles: sync.localFiles,
|
|
144
149
|
vectors: sqliteVectorReads(hippoRoot),
|
|
145
150
|
vectorViews: { storedVectorViews: async (ids) => sync.vectorViews.storedVectorViews(ids) },
|
|
146
151
|
...servedKeyGroups(sync),
|
|
@@ -153,6 +158,9 @@ export function sqliteStore(hippoRoot) {
|
|
|
153
158
|
connectorEvents: servedConnectorEvents(sync.connectorEvents),
|
|
154
159
|
objects: servedObjects(sqliteObjects(hippoRoot)),
|
|
155
160
|
readiness: { ping: async () => sync.readiness.ping() },
|
|
161
|
+
lastRecall: servedLastRecall(sync.lastRecall),
|
|
162
|
+
auditMaintenance: servedAuditMaintenance(sync.auditMaintenance),
|
|
163
|
+
lessonWrites: servedLessonWrites(sync.lessonWrites),
|
|
156
164
|
close: async () => sync.close(),
|
|
157
165
|
};
|
|
158
166
|
}
|
|
@@ -274,23 +282,4 @@ function continuityAt(hippoRoot, tenantId, eventLimit, key) {
|
|
|
274
282
|
recentSessionEvents: sessionId ? listSessionEvents(hippoRoot, tenantId, { session_id: sessionId, limit: eventLimit }) : [],
|
|
275
283
|
};
|
|
276
284
|
}
|
|
277
|
-
/** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
|
|
278
|
-
export function finishRecallAt(hippoRoot, writes) {
|
|
279
|
-
return onHandle(hippoRoot, (db) => {
|
|
280
|
-
const traceAndStrengthen = () => ({
|
|
281
|
-
traceId: writes.trace ? writeRecallTrace(db, writes.trace) : null,
|
|
282
|
-
strengthened: writes.strengthen ? strengthenRetrievedInOwnTx(db, writes.strengthen.ids, writes.strengthen.opts) : new Set(),
|
|
283
|
-
});
|
|
284
|
-
// Alone, the fail-soft trace and strengthen keep their own scopes so a busy lock loses them, never the read.
|
|
285
|
-
if (writes.goalLog.length === 0 && writes.audit.length === 0)
|
|
286
|
-
return traceAndStrengthen();
|
|
287
|
-
// Inside this scope both run as savepoints, so one recall commits once and a failed trace or strengthen still unwinds alone.
|
|
288
|
-
return withWriteScope(db, 'finish_recall', () => {
|
|
289
|
-
writeGoalRecallLog(db, localGoalRecallRows(db, writes.goalLog));
|
|
290
|
-
for (const event of writes.audit)
|
|
291
|
-
appendAuditEvent(db, event);
|
|
292
|
-
return traceAndStrengthen();
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
285
|
//# sourceMappingURL=store.js.map
|
|
@@ -7,6 +7,13 @@ export declare function loadAllL2Summaries(hippoRoot: string): MemoryEntry[];
|
|
|
7
7
|
export declare function loadAllDirtySummaries(hippoRoot: string, limit?: number): MemoryEntry[];
|
|
8
8
|
/** Live (non-archived) children of a DAG summary, so a rebuild regenerates from the CURRENT child set; tenant-scoped as defence in depth. */
|
|
9
9
|
export declare function loadChildrenOfSummary(hippoRoot: string, summaryId: string, tenantId: string): MemoryEntry[];
|
|
10
|
+
/** loadChildrenOfSummary for many summaries in one IN query per tenant chunk; the map key is `${tenantId}\u0000${summaryId}`, each list in created order. */
|
|
11
|
+
export declare function loadChildrenOfSummaries(hippoRoot: string, summaries: ReadonlyArray<{
|
|
12
|
+
id: string;
|
|
13
|
+
tenant_id: string;
|
|
14
|
+
}>): Map<string, MemoryEntry[]>;
|
|
15
|
+
/** Key into the map loadChildrenOfSummaries returns. */
|
|
16
|
+
export declare function childrenKey(tenantId: string, summaryId: string): string;
|
|
10
17
|
/** Patch applied by applyRebuildResult; `bumpRebuildCount` is false for the zero-child case, true for a normal rebuild. */
|
|
11
18
|
export interface RebuildPatch {
|
|
12
19
|
content: string;
|
package/dist/store/summaries.js
CHANGED
|
@@ -6,7 +6,9 @@ import { MEMORY_SELECT_COLUMNS, rowToEntry } from './rows.js';
|
|
|
6
6
|
import { audit } from './audit-event.js';
|
|
7
7
|
import { syncFtsRow } from './entry-row.js';
|
|
8
8
|
import { onHandle, openStore } from './open.js';
|
|
9
|
+
import { chunked } from '../util/chunked.js';
|
|
9
10
|
import { DIGEST_DISPLAY_CHARS } from '../util/token-text.js';
|
|
11
|
+
import { placeholders } from '../util/sql.js';
|
|
10
12
|
// Sleep-cycle rebuild surface: lives here, not in src/consolidate/dag.ts, because it needs module-private MEMORY_SELECT_COLUMNS, MemoryRow, rowToEntry,
|
|
11
13
|
// audit, syncFtsRow and assertTenantId. dag.ts keeps only the thin orchestrator rebuildDirtySummaries().
|
|
12
14
|
/** Host-wide loader for L2 topic summaries without an L3 parent, for consolidate phase 1.9 (buildEntityProfiles); SQL-level filter, since in-memory
|
|
@@ -62,10 +64,50 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
|
|
|
62
64
|
return rows.map(rowToEntry);
|
|
63
65
|
}, openStore);
|
|
64
66
|
}
|
|
67
|
+
/** loadChildrenOfSummary for many summaries in one IN query per tenant chunk; the map key is `${tenantId}\u0000${summaryId}`, each list in created order. */
|
|
68
|
+
export function loadChildrenOfSummaries(hippoRoot, summaries) {
|
|
69
|
+
const byTenant = new Map();
|
|
70
|
+
for (const s of summaries) {
|
|
71
|
+
assertTenantId('loadChildrenOfSummaries', s.tenant_id);
|
|
72
|
+
const ids = byTenant.get(s.tenant_id) ?? [];
|
|
73
|
+
ids.push(s.id);
|
|
74
|
+
byTenant.set(s.tenant_id, ids);
|
|
75
|
+
}
|
|
76
|
+
const out = new Map();
|
|
77
|
+
onHandle(hippoRoot, (db) => {
|
|
78
|
+
for (const [tenantId, ids] of byTenant) {
|
|
79
|
+
for (const slice of chunked(ids)) {
|
|
80
|
+
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
81
|
+
const rows = db.prepare(`
|
|
82
|
+
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
83
|
+
FROM memories
|
|
84
|
+
WHERE dag_parent_id IN (${placeholders(slice.length)})
|
|
85
|
+
AND tenant_id = ?
|
|
86
|
+
AND kind != 'archived'
|
|
87
|
+
AND superseded_by IS NULL
|
|
88
|
+
ORDER BY created ASC
|
|
89
|
+
`).all(...slice, tenantId);
|
|
90
|
+
for (const entry of rows.map(rowToEntry)) {
|
|
91
|
+
const key = childrenKey(tenantId, entry.dag_parent_id ?? '');
|
|
92
|
+
const list = out.get(key);
|
|
93
|
+
if (list)
|
|
94
|
+
list.push(entry);
|
|
95
|
+
else
|
|
96
|
+
out.set(key, [entry]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, openStore);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Key into the map loadChildrenOfSummaries returns. */
|
|
104
|
+
export function childrenKey(tenantId, summaryId) {
|
|
105
|
+
return `${tenantId}\u0000${summaryId}`;
|
|
106
|
+
}
|
|
65
107
|
/** Apply a rebuild result to a dirty summary in one write scope (UPDATE plus syncFtsRow); `AND summary_dirty = 1` makes a concurrent race-loser a no-op.
|
|
66
108
|
* Returns `{ changed, refused }`: `refused` only when a tombstone hit suppressed the content write while the metadata UPDATE still landed. */
|
|
67
109
|
export function applyRebuildResult(hippoRoot, summary, patch) {
|
|
68
|
-
assertTenantId('applyRebuildResult', summary.
|
|
110
|
+
assertTenantId('applyRebuildResult', summary.tenant_id);
|
|
69
111
|
return onHandle(hippoRoot, (db) => {
|
|
70
112
|
return withWriteScope(db, 'rebuild_summary', () => applyRebuildInSavepoint(db, summary, patch));
|
|
71
113
|
}, openStore);
|
|
@@ -99,14 +141,14 @@ function applyRebuildInSavepoint(db, summary, patch) {
|
|
|
99
141
|
// Check tombstones before choosing the UPDATE: this direct UPDATE bypasses upsertEntryRow's guard, and
|
|
100
142
|
// a deterministic rebuild would otherwise re-assert a rejected value every sleep cycle.
|
|
101
143
|
const tombstone = patch.bumpRebuildCount
|
|
102
|
-
? findRejectedValue(db, summary.
|
|
144
|
+
? findRejectedValue(db, summary.tenant_id, rejectionDigest(patch.content))
|
|
103
145
|
: null;
|
|
104
146
|
// On a tombstone hit, write no content: take the zero-child metadata-only path (descendant_count/earliest_at/latest_at, summary_dirty cleared, no
|
|
105
147
|
// rebuild_count bump). Clearing dirty is deliberate: leaving it set would make every later sleep re-attempt and re-refuse the same rebuild forever.
|
|
106
148
|
const applyContentWrite = patch.bumpRebuildCount && !tombstone;
|
|
107
149
|
const result = applyContentWrite
|
|
108
|
-
? db.prepare(REBUILD_CONTENT_SQL).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.
|
|
109
|
-
: db.prepare(REBUILD_METADATA_SQL).run(patch.descendant_count, patch.earliest_at, patch.latest_at, summary.id, summary.
|
|
150
|
+
? db.prepare(REBUILD_CONTENT_SQL).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.tenant_id)
|
|
151
|
+
: db.prepare(REBUILD_METADATA_SQL).run(patch.descendant_count, patch.earliest_at, patch.latest_at, summary.id, summary.tenant_id);
|
|
110
152
|
// `changed` means THIS call's UPDATE hit a row, so a refusal still settles the cycle (false would retry
|
|
111
153
|
// the doomed rebuild forever); `refused` lets the caller count it as refused rather than rebuilt.
|
|
112
154
|
const changed = (result.changes ?? 0) > 0;
|
|
@@ -125,7 +167,7 @@ function auditRefusedRebuild(db, summary, patch, tombstone) {
|
|
|
125
167
|
targetId: summary.id,
|
|
126
168
|
metadata: { digest: tombstone.digest, reason: tombstone.reason },
|
|
127
169
|
actor: patch.actor,
|
|
128
|
-
tenantId: summary.
|
|
170
|
+
tenantId: summary.tenant_id
|
|
129
171
|
});
|
|
130
172
|
log.warn(`applyRebuildResult: refused rebuild content for ${summary.id} — matches a rejected value ` +
|
|
131
173
|
`(digest ${tombstone.digest.slice(0, DIGEST_DISPLAY_CHARS)}...); metadata updated, content unchanged`);
|
|
@@ -139,7 +181,7 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
|
|
|
139
181
|
descendant_count: patch.descendant_count,
|
|
140
182
|
earliest_at: patch.earliest_at,
|
|
141
183
|
latest_at: patch.latest_at,
|
|
142
|
-
summary_dirty:
|
|
184
|
+
summary_dirty: false,
|
|
143
185
|
last_rebuilt_at: applyContentWrite ? nowIso : summary.last_rebuilt_at,
|
|
144
186
|
rebuild_count: applyContentWrite
|
|
145
187
|
? (summary.rebuild_count ?? 0) + 1
|
|
@@ -152,7 +194,7 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
|
|
|
152
194
|
source: 'E3-rebuild',
|
|
153
195
|
zero_children: patch.zeroChildren,
|
|
154
196
|
descendant_count: patch.descendant_count,
|
|
155
|
-
}, actor: patch.actor, tenantId: summary.
|
|
197
|
+
}, actor: patch.actor, tenantId: summary.tenant_id });
|
|
156
198
|
}
|
|
157
199
|
/** Clear summary_dirty on a new summary after buildDag's child-link loop (each member's writeEntry marks the parent dirty, so the same cycle would rebuild it).
|
|
158
200
|
* Idempotent (no-op, no audit if not dirty); audit source='buildDag-clean' distinguishes it from the rebuild's. */
|