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
|
@@ -38,9 +38,15 @@ export interface ResolveConflictOpts {
|
|
|
38
38
|
* loser is kind='raw'). Defaults to a conflict-context string. */
|
|
39
39
|
reason?: string;
|
|
40
40
|
}
|
|
41
|
+
export interface ResolveConflictRequest {
|
|
42
|
+
readonly conflictId: number;
|
|
43
|
+
readonly keepId: string;
|
|
44
|
+
readonly forgetLoser?: boolean;
|
|
45
|
+
readonly tenantId?: string;
|
|
46
|
+
}
|
|
41
47
|
/** Resolve a conflict by keeping one memory and halving the loser's half-life; `--forget` removes the loser, `opts.rejectLoserValue` also tombstones its
|
|
42
48
|
* digest. Every path emits a `conflict_resolve` audit row. Returns the resolved conflict, or null if not found. */
|
|
43
|
-
export declare function resolveConflict(hippoRoot: string, conflictId
|
|
49
|
+
export declare function resolveConflict(hippoRoot: string, { conflictId, keepId, forgetLoser, tenantId }: ResolveConflictRequest, opts?: ResolveConflictOpts): {
|
|
44
50
|
conflict: MemoryConflict;
|
|
45
51
|
loserId: string;
|
|
46
52
|
} | null;
|
package/dist/store/conflicts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { DEFAULT_TENANT_ID } from '../util/env.js';
|
|
|
2
2
|
import { closeHippoDb, withWriteScope } from '../db/index.js';
|
|
3
3
|
import { rejectionDigest, insertRejectedValue, normalizeValueForRejection } from './rejection.js';
|
|
4
4
|
import { archiveRawMemory } from './raw-archive.js';
|
|
5
|
-
import { rowToMemoryConflict } from './rows.js';
|
|
5
|
+
import { parseJsonArray, rowToMemoryConflict } from './rows.js';
|
|
6
6
|
import { audit } from './audit-event.js';
|
|
7
7
|
import { syncChangedMirrors, purgeMirrorBestEffort } from './mirrors.js';
|
|
8
8
|
import { selectEntriesByIds } from './entry-reads.js';
|
|
@@ -12,6 +12,7 @@ import { deleteEntryCore } from './delete-and-batch.js';
|
|
|
12
12
|
import { BadRequestError } from '../core/api-errors.js';
|
|
13
13
|
import { canTouchScope, isPersonalScope } from '../core/recall-scope.js';
|
|
14
14
|
import { selectMemoryReach } from './tenant-lookup.js';
|
|
15
|
+
import { placeholders } from '../util/sql.js';
|
|
15
16
|
// The one place MemoryConflictRow's columns are listed; CONFLICT_COLS_MC is the same list under the `mc` alias.
|
|
16
17
|
const CONFLICT_COLS = 'id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at';
|
|
17
18
|
const CONFLICT_COLS_MC = CONFLICT_COLS.split(', ').map((c) => `mc.${c}`).join(', ');
|
|
@@ -132,7 +133,7 @@ function readConflictRefresh(db, detected) {
|
|
|
132
133
|
}
|
|
133
134
|
const given = [...new Set(detected.flatMap((c) => [c.memory_a_id, c.memory_b_id]))].filter((id) => !tenantById.has(id));
|
|
134
135
|
for (const ids of chunked(given)) {
|
|
135
|
-
const marks = ids.
|
|
136
|
+
const marks = placeholders(ids.length);
|
|
136
137
|
// SAFETY: rows' shape matches the two columns named in the SELECT.
|
|
137
138
|
const found = db.prepare(`SELECT id, tenant_id FROM memories WHERE id IN (${marks})`).all(...ids);
|
|
138
139
|
for (const r of found)
|
|
@@ -253,7 +254,7 @@ function findResolvableConflict(db, conflictId, keepId, tenantId) {
|
|
|
253
254
|
}
|
|
254
255
|
/** Resolve a conflict by keeping one memory and halving the loser's half-life; `--forget` removes the loser, `opts.rejectLoserValue` also tombstones its
|
|
255
256
|
* digest. Every path emits a `conflict_resolve` audit row. Returns the resolved conflict, or null if not found. */
|
|
256
|
-
export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = false, tenantId, opts) {
|
|
257
|
+
export function resolveConflict(hippoRoot, { conflictId, keepId, forgetLoser = false, tenantId }, opts) {
|
|
257
258
|
const db = openStore(hippoRoot);
|
|
258
259
|
const scope = conflictMemScope(tenantId);
|
|
259
260
|
try {
|
|
@@ -389,7 +390,7 @@ function stripConflictRefs(db, t, loserRemoved) {
|
|
|
389
390
|
// SAFETY: keepRow's shape matches the single `conflicts_with_json` column selected above.
|
|
390
391
|
const keepRow = db.prepare(`SELECT conflicts_with_json FROM memories WHERE id = ?${memScope}`).get(keepId, ...memArgs);
|
|
391
392
|
if (keepRow) {
|
|
392
|
-
const refs =
|
|
393
|
+
const refs = parseJsonArray(keepRow.conflicts_with_json, { table: 'memories', id: keepId, column: 'conflicts_with_json' });
|
|
393
394
|
const cleaned = refs.filter((r) => r !== loserId);
|
|
394
395
|
db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?${memScope}`)
|
|
395
396
|
.run(JSON.stringify(cleaned), keepId, ...memArgs);
|
|
@@ -399,7 +400,7 @@ function stripConflictRefs(db, t, loserRemoved) {
|
|
|
399
400
|
// column named in the SELECT below.
|
|
400
401
|
const loserRow = db.prepare(`SELECT conflicts_with_json FROM memories WHERE id = ?${memScope}`).get(loserId, ...memArgs);
|
|
401
402
|
if (loserRow) {
|
|
402
|
-
const refs =
|
|
403
|
+
const refs = parseJsonArray(loserRow.conflicts_with_json, { table: 'memories', id: loserId, column: 'conflicts_with_json' });
|
|
403
404
|
const cleaned = refs.filter((r) => r !== keepId);
|
|
404
405
|
db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?${memScope}`)
|
|
405
406
|
.run(JSON.stringify(cleaned), loserId, ...memArgs);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// The dead-letter statements both connectors share; each connector names its table and columns and maps rows to its own item type.
|
|
2
|
+
import { placeholders } from '../../util/sql.js';
|
|
2
3
|
/** Inserts one row, stamping received_at; `columns` pairs each column name with its value, in insert order. */
|
|
3
4
|
export function insertDlqRow(db, table, columns) {
|
|
4
5
|
const names = [...columns.map(([name]) => name), 'received_at'];
|
|
5
6
|
const values = [...columns.map(([, value]) => value), new Date().toISOString()];
|
|
6
7
|
const result = db
|
|
7
|
-
.prepare(`INSERT INTO ${table} (${names.join(', ')}) VALUES (${names.
|
|
8
|
+
.prepare(`INSERT INTO ${table} (${names.join(', ')}) VALUES (${placeholders(names.length)})`)
|
|
8
9
|
.run(...values);
|
|
9
10
|
return Number(result.lastInsertRowid);
|
|
10
11
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DatabaseSyncLike } from '../../db/index.js';
|
|
2
|
+
import type { ConnectorEventRecord } from '../port.js';
|
|
3
|
+
/** Which log table a connector writes and the column its dedupe key lives in. */
|
|
4
|
+
export interface EventLogSpec {
|
|
5
|
+
readonly table: 'slack_event_log' | 'github_event_log';
|
|
6
|
+
readonly keyColumn: 'event_id' | 'idempotency_key';
|
|
7
|
+
}
|
|
8
|
+
export declare function eventSeenAt(db: DatabaseSyncLike, log: EventLogSpec, key: string): boolean;
|
|
9
|
+
export declare function eventMemoryAt(db: DatabaseSyncLike, log: EventLogSpec, key: string): string | null;
|
|
10
|
+
/** Records the key unless a row holds it already; false means another writer got there first. `extra` pairs connector-only columns with values. */
|
|
11
|
+
export declare function markEventSeenAt(db: DatabaseSyncLike, log: EventLogSpec, key: string, extra: ReadonlyArray<readonly [string, string]>, memoryId: string | null): boolean;
|
|
12
|
+
export declare function eventRecord(hippoRoot: string, log: EventLogSpec, key: string): ConnectorEventRecord;
|
|
13
|
+
//# sourceMappingURL=event-log.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The event-log statements both connectors share; each connector names its table, key column and extra columns.
|
|
2
|
+
import { placeholders } from '../../util/sql.js';
|
|
3
|
+
import { onHandle } from '../open.js';
|
|
4
|
+
export function eventSeenAt(db, log, key) {
|
|
5
|
+
return !!db.prepare(`SELECT 1 FROM ${log.table} WHERE ${log.keyColumn} = ?`).get(key);
|
|
6
|
+
}
|
|
7
|
+
export function eventMemoryAt(db, log, key) {
|
|
8
|
+
// SAFETY: the SELECT projects only `memory_id`, a nullable column; .get() returns undefined when no row matches.
|
|
9
|
+
const row = db.prepare(`SELECT memory_id FROM ${log.table} WHERE ${log.keyColumn} = ?`).get(key);
|
|
10
|
+
return row?.memory_id ?? null;
|
|
11
|
+
}
|
|
12
|
+
/** Records the key unless a row holds it already; false means another writer got there first. `extra` pairs connector-only columns with values. */
|
|
13
|
+
export function markEventSeenAt(db, log, key, extra, memoryId) {
|
|
14
|
+
const names = [log.keyColumn, ...extra.map(([name]) => name), 'ingested_at', 'memory_id'];
|
|
15
|
+
const values = [key, ...extra.map(([, value]) => value), new Date().toISOString(), memoryId];
|
|
16
|
+
const inserted = db
|
|
17
|
+
.prepare(`INSERT OR IGNORE INTO ${log.table} (${names.join(', ')}) VALUES (${placeholders(names.length)})`)
|
|
18
|
+
.run(...values);
|
|
19
|
+
return Number(inserted.changes ?? 0) !== 0;
|
|
20
|
+
}
|
|
21
|
+
export function eventRecord(hippoRoot, log, key) {
|
|
22
|
+
return onHandle(hippoRoot, (db) => eventSeenAt(db, log, key) ? { seen: true, memoryId: eventMemoryAt(db, log, key) } : { seen: false });
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=event-log.js.map
|
|
@@ -2,23 +2,21 @@
|
|
|
2
2
|
import { withWriteScope } from '../../db/index.js';
|
|
3
3
|
import { archiveRawMemory } from '../raw-archive.js';
|
|
4
4
|
import { onHandle } from '../open.js';
|
|
5
|
+
import { eventMemoryAt, eventRecord, eventSeenAt, markEventSeenAt } from './event-log.js';
|
|
5
6
|
import { getDlqRow, insertDlqRow, listDlqRows, markDlqRetried } from './dlq.js';
|
|
7
|
+
const GITHUB_LOG = { table: 'github_event_log', keyColumn: 'idempotency_key' };
|
|
6
8
|
export function githubEventSeenAt(db, idempotencyKey) {
|
|
7
|
-
|
|
8
|
-
return !!row;
|
|
9
|
+
return eventSeenAt(db, GITHUB_LOG, idempotencyKey);
|
|
9
10
|
}
|
|
10
11
|
export function githubEventMemoryAt(db, idempotencyKey) {
|
|
11
|
-
|
|
12
|
-
const row = db.prepare(`SELECT memory_id FROM github_event_log WHERE idempotency_key = ?`).get(idempotencyKey);
|
|
13
|
-
return row?.memory_id ?? null;
|
|
12
|
+
return eventMemoryAt(db, GITHUB_LOG, idempotencyKey);
|
|
14
13
|
}
|
|
15
14
|
/** False when the key was already logged, so a caller inside a write scope can roll its own row back. */
|
|
16
15
|
export function markGitHubEventSeenAt(db, entry) {
|
|
17
|
-
|
|
18
|
-
return Number(inserted.changes ?? 0) !== 0;
|
|
16
|
+
return markEventSeenAt(db, GITHUB_LOG, entry.idempotencyKey, [['delivery_id', entry.deliveryId], ['event_name', entry.eventName]], entry.memoryId);
|
|
19
17
|
}
|
|
20
18
|
export function githubEventRecord(hippoRoot, idempotencyKey) {
|
|
21
|
-
return
|
|
19
|
+
return eventRecord(hippoRoot, GITHUB_LOG, idempotencyKey);
|
|
22
20
|
}
|
|
23
21
|
export function markGitHubEventSeen(hippoRoot, entry) {
|
|
24
22
|
onHandle(hippoRoot, (db) => { markGitHubEventSeenAt(db, entry); });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Every statement the Slack connector runs on hippo.db: backfill cursors, the event log, the dead-letter queue and workspace routing.
|
|
2
2
|
// `xAt(db, ...)` runs on the caller's handle; the same name without `At` opens hippo.db for that one call.
|
|
3
3
|
import { onHandle } from '../open.js';
|
|
4
|
+
import { eventMemoryAt, eventRecord, eventSeenAt, markEventSeenAt } from './event-log.js';
|
|
4
5
|
import { getDlqRow, insertDlqRow, listDlqRows, markDlqRetried } from './dlq.js';
|
|
5
6
|
// Backfill cursors
|
|
6
7
|
export function slackCursorAt(db, tenantId, channelId) {
|
|
@@ -21,24 +22,19 @@ export function saveSlackCursor(hippoRoot, tenantId, channelId, latestTs) {
|
|
|
21
22
|
onHandle(hippoRoot, (db) => saveSlackCursorAt(db, tenantId, channelId, latestTs));
|
|
22
23
|
}
|
|
23
24
|
// Event log
|
|
25
|
+
const SLACK_LOG = { table: 'slack_event_log', keyColumn: 'event_id' };
|
|
24
26
|
export function slackEventSeenAt(db, eventId) {
|
|
25
|
-
|
|
26
|
-
return !!row;
|
|
27
|
+
return eventSeenAt(db, SLACK_LOG, eventId);
|
|
27
28
|
}
|
|
28
29
|
/** Records the event unless a row already holds its id; false means another writer got there first. */
|
|
29
30
|
export function markSlackEventSeenAt(db, eventId, memoryId) {
|
|
30
|
-
|
|
31
|
-
.prepare(`INSERT OR IGNORE INTO slack_event_log (event_id, ingested_at, memory_id) VALUES (?, ?, ?)`)
|
|
32
|
-
.run(eventId, new Date().toISOString(), memoryId);
|
|
33
|
-
return Number(inserted.changes ?? 0) !== 0;
|
|
31
|
+
return markEventSeenAt(db, SLACK_LOG, eventId, [], memoryId);
|
|
34
32
|
}
|
|
35
33
|
export function slackEventMemoryAt(db, eventId) {
|
|
36
|
-
|
|
37
|
-
const row = db.prepare(`SELECT memory_id FROM slack_event_log WHERE event_id = ?`).get(eventId);
|
|
38
|
-
return row?.memory_id ?? null;
|
|
34
|
+
return eventMemoryAt(db, SLACK_LOG, eventId);
|
|
39
35
|
}
|
|
40
36
|
export function slackEventRecord(hippoRoot, eventId) {
|
|
41
|
-
return
|
|
37
|
+
return eventRecord(hippoRoot, SLACK_LOG, eventId);
|
|
42
38
|
}
|
|
43
39
|
export function markSlackEventSeen(hippoRoot, eventId, memoryId) {
|
|
44
40
|
onHandle(hippoRoot, (db) => markSlackEventSeenAt(db, eventId, memoryId));
|
|
@@ -8,6 +8,7 @@ import { storedVectorIds } from '../db/vector-store.js';
|
|
|
8
8
|
import { onHandle, openStore } from './open.js';
|
|
9
9
|
import { QUARANTINE_SCOPE_PREFIX } from './quarantine.js';
|
|
10
10
|
import { parseJsonArray } from './rows.js';
|
|
11
|
+
// Full content stays: dashboard-snapshot's Fact.lower serves search over the whole text, so a preview would change which rows match.
|
|
11
12
|
const LIVE_COLUMNS = 'id, created, last_retrieved, retrieval_count, half_life_days, layer, tags_json, emotional_valence, outcome_positive, ' +
|
|
12
13
|
'outcome_negative, pinned, confidence, content, superseded_by, kind, scope, origin_project';
|
|
13
14
|
// SQL twins of isQuarantineScope and of the two halves of isLiveMemory; substr compares bytes, as startsWith does, where LIKE would fold case.
|
|
@@ -14,6 +14,7 @@ import { purgeMirrorBestEffort, mirrorBestEffort, writeMarkdownMirror } from './
|
|
|
14
14
|
import { onHandle, openStore } from './open.js';
|
|
15
15
|
import { clock } from '../util/write-budget.js';
|
|
16
16
|
import { chunked } from '../util/chunked.js';
|
|
17
|
+
import { placeholders } from '../util/sql.js';
|
|
17
18
|
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
|
|
18
19
|
export const MEMORY_BACKED_TABLES = ['predictions', 'decisions', 'incidents', 'processes', 'policies', 'skills', 'project_briefs', 'customer_notes'];
|
|
19
20
|
/** Deleting a memory that backs an object nulls the object's link, and no restore can repair it, so no automatic pass may. */
|
|
@@ -118,7 +119,7 @@ function selectDeleteTargets(db, ids) {
|
|
|
118
119
|
const rows = new Map();
|
|
119
120
|
for (const chunk of chunked([...new Set(ids)])) {
|
|
120
121
|
// SAFETY: rows' shape matches the three columns named in the SELECT.
|
|
121
|
-
const found = db.prepare(`SELECT id, tenant_id, dag_parent_id FROM memories WHERE id IN (${chunk.
|
|
122
|
+
const found = db.prepare(`SELECT id, tenant_id, dag_parent_id FROM memories WHERE id IN (${placeholders(chunk.length)})`)
|
|
122
123
|
.all(...chunk);
|
|
123
124
|
for (const row of found)
|
|
124
125
|
rows.set(row.id, row);
|
|
@@ -248,7 +249,7 @@ function moveDormantAndDelete(db, dormantMoves, deletableIds, out) {
|
|
|
248
249
|
/** The still auto-deletable rows among `ids`, recording each one's DAG parent as dirty. Placeholders come from `ids` itself. */
|
|
249
250
|
function selectAutoDeletableRows(db, ids, dirty) {
|
|
250
251
|
// SAFETY: rows' shape matches the three columns named in the SELECT.
|
|
251
|
-
const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${ids.
|
|
252
|
+
const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${placeholders(ids.length)}) AND ${AUTOMATIC_DELETE_SQL}`).all(...ids);
|
|
252
253
|
for (const row of rows) {
|
|
253
254
|
if (row.dag_parent_id) {
|
|
254
255
|
dirty.parents.add(row.dag_parent_id);
|
|
@@ -287,13 +288,13 @@ function applyBatchWrites(db, stampedWrites, snapshot, out) {
|
|
|
287
288
|
// Hook for writes: a new child, or a change to what its summary reads, marks the parent dirty; decay alone does not.
|
|
288
289
|
if (row.dag_parent_id && (!live || SUMMARY_INPUTS.some((k) => row[k] !== live[k]))) {
|
|
289
290
|
dirty.parents.add(row.dag_parent_id);
|
|
290
|
-
dirty.tenantById.set(row.dag_parent_id, row.
|
|
291
|
+
dirty.tenantById.set(row.dag_parent_id, row.tenant_id);
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
/** True, after auditing the refusal, when the write would introduce a rejected value. */
|
|
295
296
|
function isRejectedBatchWrite(db, row) {
|
|
296
|
-
const entryTenantId = row.
|
|
297
|
+
const entryTenantId = row.tenant_id ?? DEFAULT_TENANT_ID;
|
|
297
298
|
// checkRejectionGuard, not a bare tombstone probe: a tombstone can coexist with a live same-content row,
|
|
298
299
|
// and skipping every re-persist would starve it of decay/replay updates; only new or changed content is refused.
|
|
299
300
|
try {
|
|
@@ -138,10 +138,10 @@ export interface DeliveryRecorderInit {
|
|
|
138
138
|
runtime?: DeliveryRuntime;
|
|
139
139
|
/** Set by hooks that are not prompt or context calls; without it the payload's hook event decides. */
|
|
140
140
|
eventType?: DeliveryEventType;
|
|
141
|
+
/** Makes this recorder throw at one stage; only tests set it, and no CLI flag or environment variable reaches it. */
|
|
142
|
+
fault?: DeliveryFault;
|
|
141
143
|
}
|
|
142
144
|
export type DeliveryFault = 'observe' | 'build' | 'flush';
|
|
143
|
-
/** Makes every recorder created afterwards throw at one stage. Only tests call it; the shipped CLI and server have no route here. */
|
|
144
|
-
export declare function _setDeliveryFaultForTests(fault: DeliveryFault | null): void;
|
|
145
145
|
/** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
|
|
146
146
|
export declare function createDeliveryRecorder(init: DeliveryRecorderInit): DeliveryRecorder;
|
|
147
147
|
//# sourceMappingURL=delivery-recorder.d.ts.map
|
|
@@ -198,16 +198,11 @@ function candidateMethods(state, guard) {
|
|
|
198
198
|
}),
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
|
-
let injectedFault = null;
|
|
202
|
-
/** Makes every recorder created afterwards throw at one stage. Only tests call it; the shipped CLI and server have no route here. */
|
|
203
|
-
export function _setDeliveryFaultForTests(fault) {
|
|
204
|
-
injectedFault = fault;
|
|
205
|
-
}
|
|
206
201
|
/** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
|
|
207
202
|
export function createDeliveryRecorder(init) {
|
|
208
203
|
const startedMs = Date.now();
|
|
209
204
|
const ts = evalNow().toISOString();
|
|
210
|
-
const fault =
|
|
205
|
+
const fault = init.fault ?? null;
|
|
211
206
|
const payload = readPayload(init);
|
|
212
207
|
const state = {
|
|
213
208
|
candidates: new Map(), picked: new Map(), filtered: new Set(), facts: null, shown: 0, dropped: 0,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { selectEntriesByIds, TENANT_IS } from './entry-reads.js';
|
|
2
2
|
import { chunked } from '../util/chunked.js';
|
|
3
3
|
import { MEMORY_SELECT_COLUMNS, rowToEntry } from './rows.js';
|
|
4
|
-
|
|
4
|
+
import { placeholders } from '../util/sql.js';
|
|
5
|
+
const marks = (ids) => placeholders(ids.length);
|
|
5
6
|
/** The walk `summaryWithDescendants` documents, with `levels` cut to the first `page.rows` rows and each level's full count in `sizes`. */
|
|
6
7
|
export function pagedDescendants(db, walk, summaryAdmitted) {
|
|
7
8
|
const sizes = [];
|
package/dist/store/dormant.js
CHANGED
|
@@ -24,15 +24,19 @@ export function insertDormantRow(db, move) {
|
|
|
24
24
|
reason = excluded.reason,
|
|
25
25
|
strength = excluded.strength,
|
|
26
26
|
dormant_at = excluded.dormant_at
|
|
27
|
-
`).run(move.entry.
|
|
27
|
+
`).run(move.entry.tenant_id, move.entry.id, move.entry.content, JSON.stringify(move.entry), move.reason, move.strength, move.dormantAt);
|
|
28
|
+
}
|
|
29
|
+
function toCurrentEntry({ tenantId, summary_dirty, tenant_id, ...rest }) {
|
|
30
|
+
return { ...rest, tenant_id: tenant_id ?? tenantId ?? '', summary_dirty: summary_dirty === true || summary_dirty === 1 };
|
|
28
31
|
}
|
|
29
32
|
/** Parse a stored snapshot back into a MemoryEntry, or null when the row no longer matches it (edited by hand, or truncated). */
|
|
30
33
|
function parseSnapshot(row) {
|
|
31
34
|
try {
|
|
32
35
|
// SAFETY: entry_json is only written by insertDormantRow from a MemoryEntry;
|
|
33
36
|
// the id / tenant / content checks below reject a row edited out of shape.
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
// SAFETY: entry_json came from JSON.stringify of a MemoryEntry (2.0) or a 1.x one; the checks below reject a row edited out of shape.
|
|
38
|
+
const entry = toCurrentEntry(JSON.parse(row.entry_json));
|
|
39
|
+
if (entry.id !== row.id || entry.tenant_id !== row.tenant_id || entry.content !== row.content) {
|
|
36
40
|
warnDamagedColumn({ table: 'dormant_memories', id: row.id, column: 'entry_json' }, 'wrong shape');
|
|
37
41
|
return null;
|
|
38
42
|
}
|
|
@@ -31,12 +31,12 @@ export declare function countAllEntries(hippoRoot: string): number;
|
|
|
31
31
|
export declare function loadEntriesMentioning(hippoRoot: string, tokens: readonly string[], tag: string, tenantId?: string): MemoryEntry[];
|
|
32
32
|
export interface EntryTextKey {
|
|
33
33
|
id: string;
|
|
34
|
-
|
|
34
|
+
tenant_id: string;
|
|
35
35
|
content: string;
|
|
36
36
|
source: string;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
export declare function loadEntryTextKeys(hippoRoot: string): EntryTextKey[];
|
|
38
|
+
/** The rows in loadAllEntries' order (every tenant unless one is named), with only the columns that say whether another store already holds each. */
|
|
39
|
+
export declare function loadEntryTextKeys(hippoRoot: string, tenantId?: string): EntryTextKey[];
|
|
40
40
|
export interface EntriesWithBase {
|
|
41
41
|
entries: MemoryEntry[];
|
|
42
42
|
base: {
|
|
@@ -45,8 +45,30 @@ export interface EntriesWithBase {
|
|
|
45
45
|
}
|
|
46
46
|
/** Every entry plus a by-id lookup of each as loaded, rebuilt from its row on first use, so no deep copy of the store is kept. */
|
|
47
47
|
export declare function loadAllEntriesWithBase(hippoRoot: string): EntriesWithBase;
|
|
48
|
+
/** Where a page stopped: the `created`, `id` of its last row, in loadAllEntries' order. */
|
|
49
|
+
export interface EntryCursor {
|
|
50
|
+
created: string;
|
|
51
|
+
id: string;
|
|
52
|
+
}
|
|
53
|
+
export interface EntryPage {
|
|
54
|
+
entries: MemoryEntry[];
|
|
55
|
+
/** The cursor that reads the page after this one, or null when this was the last. */
|
|
56
|
+
next: EntryCursor | null;
|
|
57
|
+
}
|
|
58
|
+
/** Up to `limit` rows in loadAllEntries' order, starting after `after`; omitting `tenantId` reads every tenant. */
|
|
59
|
+
export declare function loadEntriesPage(hippoRoot: string, tenantId: string | undefined, page: {
|
|
60
|
+
limit: number;
|
|
61
|
+
after?: EntryCursor;
|
|
62
|
+
}): EntryPage;
|
|
48
63
|
/** Every memory row on an open connection, so a caller can read inside its own transaction. */
|
|
49
64
|
export declare function selectAllEntries(db: DatabaseSyncLike, tenantId?: string): MemoryEntry[];
|
|
65
|
+
/** The columns a text match needs, for every row of a tenant in loadAllEntries' order: callers read whole rows only for the ids that match. */
|
|
66
|
+
export declare function selectTenantTexts(db: DatabaseSyncLike, tenantId: string): Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
content: string;
|
|
69
|
+
source: string;
|
|
70
|
+
scope: string | null;
|
|
71
|
+
}>;
|
|
50
72
|
/** Live rows whose source starts with `prefix`, on the caller's handle; LIKE folds case, so the prefix is checked again exactly. */
|
|
51
73
|
export declare function selectLiveEntriesBySourcePrefix(db: DatabaseSyncLike, tenantId: string, prefix: string): MemoryEntry[];
|
|
52
74
|
export declare function loadContentsWithTag(hippoRoot: string, tenantId: string, tag: string, origins?: readonly string[]): string[];
|
|
@@ -6,6 +6,7 @@ import { originInSql } from '../core/project-identity.js';
|
|
|
6
6
|
import { scopeAdmitSql } from './rule-sql.js';
|
|
7
7
|
import { chunked } from '../util/chunked.js';
|
|
8
8
|
import { DEFAULT_TENANT_ID } from '../util/env.js';
|
|
9
|
+
import { placeholders } from '../util/sql.js';
|
|
9
10
|
const MAX_IDS_PER_READ = 500;
|
|
10
11
|
// The plus keeps SQLite on the primary key for an id list: with a bare tenant_id it walks every row of the tenant instead.
|
|
11
12
|
export const TENANT_IS = '+tenant_id = ?';
|
|
@@ -29,7 +30,7 @@ export function heldIdLookup(hippoRoot, tenantId, ids) {
|
|
|
29
30
|
for (const id of chunk)
|
|
30
31
|
held.set(id, false);
|
|
31
32
|
// SAFETY: rows' shape matches the single `id` column selected.
|
|
32
|
-
const rows = db.prepare(`SELECT id FROM memories WHERE id IN (${chunk.
|
|
33
|
+
const rows = db.prepare(`SELECT id FROM memories WHERE id IN (${placeholders(chunk.length)}) AND ${TENANT_IS}`).all(...chunk, tenantId);
|
|
33
34
|
for (const row of rows)
|
|
34
35
|
held.set(row.id, true);
|
|
35
36
|
}
|
|
@@ -49,9 +50,9 @@ export function selectEntriesByIds(db, ids, tenantId) {
|
|
|
49
50
|
const tenantClause = tenantId !== undefined ? ` AND ${TENANT_IS}` : '';
|
|
50
51
|
const tenantArgs = tenantId !== undefined ? [tenantId] : [];
|
|
51
52
|
for (const chunk of chunked([...new Set(ids)])) {
|
|
52
|
-
const
|
|
53
|
+
const marks = placeholders(chunk.length);
|
|
53
54
|
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
54
|
-
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${
|
|
55
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${marks})${tenantClause}`).all(...chunk, ...tenantArgs);
|
|
55
56
|
for (const row of rows)
|
|
56
57
|
byId.set(row.id, rowToEntry(row));
|
|
57
58
|
}
|
|
@@ -63,9 +64,9 @@ export function selectChildrenByParent(db, parentIds, tenantId) {
|
|
|
63
64
|
const tenantClause = tenantId !== undefined ? ` AND ${TENANT_IS}` : '';
|
|
64
65
|
const tenantArgs = tenantId !== undefined ? [tenantId] : [];
|
|
65
66
|
for (const chunk of chunked([...new Set(parentIds)])) {
|
|
66
|
-
const
|
|
67
|
+
const marks = placeholders(chunk.length);
|
|
67
68
|
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
68
|
-
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id IN (${
|
|
69
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id IN (${marks})${tenantClause} ORDER BY created ASC, id ASC`).all(...chunk, ...tenantArgs);
|
|
69
70
|
for (const row of rows) {
|
|
70
71
|
const entry = rowToEntry(row);
|
|
71
72
|
const parentId = entry.dag_parent_id ?? '';
|
|
@@ -84,12 +85,12 @@ export function loadEntriesByIds(hippoRoot, ids, tenantId) {
|
|
|
84
85
|
return [];
|
|
85
86
|
const capped = ids.slice(0, MAX_IDS_PER_READ);
|
|
86
87
|
return onHandle(hippoRoot, (db) => {
|
|
87
|
-
const
|
|
88
|
+
const marks = placeholders(capped.length);
|
|
88
89
|
// Without ORDER BY, rows follow SQLite's IN(...) scan order, undefined w.r.t. `ids`.
|
|
89
90
|
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
90
91
|
const rows = tenantId !== undefined
|
|
91
|
-
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${
|
|
92
|
-
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${
|
|
92
|
+
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${marks}) AND ${TENANT_IS} ORDER BY created ASC, content ASC, id ASC`).all(...capped, tenantId)
|
|
93
|
+
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${marks}) ORDER BY created ASC, content ASC, id ASC`).all(...capped);
|
|
93
94
|
return rows.map(rowToEntry);
|
|
94
95
|
}, openStore);
|
|
95
96
|
}
|
|
@@ -228,12 +229,14 @@ export function loadEntriesMentioning(hippoRoot, tokens, tag, tenantId) {
|
|
|
228
229
|
return rows.map(rowToEntry);
|
|
229
230
|
}, openStore);
|
|
230
231
|
}
|
|
231
|
-
/**
|
|
232
|
-
export function loadEntryTextKeys(hippoRoot) {
|
|
232
|
+
/** The rows in loadAllEntries' order (every tenant unless one is named), with only the columns that say whether another store already holds each. */
|
|
233
|
+
export function loadEntryTextKeys(hippoRoot, tenantId) {
|
|
233
234
|
return onHandle(hippoRoot, (db) => {
|
|
235
|
+
const where = tenantId !== undefined ? 'WHERE tenant_id = ? ' : '';
|
|
234
236
|
// SAFETY: rows' shape matches the four columns named in the SELECT.
|
|
235
|
-
const rows = db.prepare(
|
|
236
|
-
|
|
237
|
+
const rows = db.prepare(`SELECT id, tenant_id, content, source FROM memories ${where}ORDER BY created ASC, id ASC`)
|
|
238
|
+
.all(...(tenantId !== undefined ? [tenantId] : []));
|
|
239
|
+
return rows.map((row) => ({ id: row.id, tenant_id: row.tenant_id ?? DEFAULT_TENANT_ID, content: row.content, source: row.source }));
|
|
237
240
|
}, openStore);
|
|
238
241
|
}
|
|
239
242
|
/** Every entry plus a by-id lookup of each as loaded, rebuilt from its row on first use, so no deep copy of the store is kept. */
|
|
@@ -264,10 +267,38 @@ function selectAllRows(db, tenantId) {
|
|
|
264
267
|
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE tenant_id = ? ORDER BY created ASC, id ASC`).all(tenantId)
|
|
265
268
|
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
|
|
266
269
|
}
|
|
270
|
+
/** Up to `limit` rows in loadAllEntries' order, starting after `after`; omitting `tenantId` reads every tenant. */
|
|
271
|
+
export function loadEntriesPage(hippoRoot, tenantId, page) {
|
|
272
|
+
const where = [];
|
|
273
|
+
const params = [];
|
|
274
|
+
if (tenantId !== undefined) {
|
|
275
|
+
where.push('tenant_id = ?');
|
|
276
|
+
params.push(tenantId);
|
|
277
|
+
}
|
|
278
|
+
if (page.after) {
|
|
279
|
+
where.push('(created > ? OR (created = ? AND id > ?))');
|
|
280
|
+
params.push(page.after.created, page.after.created, page.after.id);
|
|
281
|
+
}
|
|
282
|
+
const clause = where.length > 0 ? `WHERE ${where.join(' AND ')} ` : '';
|
|
283
|
+
// One extra row says whether another page follows without a count.
|
|
284
|
+
params.push(page.limit + 1);
|
|
285
|
+
return onHandle(hippoRoot, (db) => {
|
|
286
|
+
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
287
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ${clause}ORDER BY created ASC, id ASC LIMIT ?`).all(...params);
|
|
288
|
+
const entries = rows.slice(0, page.limit).map(rowToEntry);
|
|
289
|
+
const last = entries[entries.length - 1];
|
|
290
|
+
return { entries, next: rows.length > page.limit && last ? { created: last.created, id: last.id } : null };
|
|
291
|
+
}, openStore);
|
|
292
|
+
}
|
|
267
293
|
/** Every memory row on an open connection, so a caller can read inside its own transaction. */
|
|
268
294
|
export function selectAllEntries(db, tenantId) {
|
|
269
295
|
return selectAllRows(db, tenantId).map(rowToEntry);
|
|
270
296
|
}
|
|
297
|
+
/** The columns a text match needs, for every row of a tenant in loadAllEntries' order: callers read whole rows only for the ids that match. */
|
|
298
|
+
export function selectTenantTexts(db, tenantId) {
|
|
299
|
+
// SAFETY: rows' shape matches the four columns named in the SELECT.
|
|
300
|
+
return db.prepare('SELECT id, content, source, scope FROM memories WHERE tenant_id = ? ORDER BY created ASC, id ASC').all(tenantId);
|
|
301
|
+
}
|
|
271
302
|
/** Live rows whose source starts with `prefix`, on the caller's handle; LIKE folds case, so the prefix is checked again exactly. */
|
|
272
303
|
export function selectLiveEntriesBySourcePrefix(db, tenantId, prefix) {
|
|
273
304
|
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
package/dist/store/entry-row.js
CHANGED
|
@@ -3,8 +3,15 @@ import { closeHippoDb, ftsRowCounts, openHippoDb, isFtsAvailable, repairFtsDrift
|
|
|
3
3
|
import { fallbackOrigin, originFromSource } from '../core/project-identity.js';
|
|
4
4
|
import { checkRejectionGuard } from './rejection.js';
|
|
5
5
|
import { errorMessage, log } from '../util/log.js';
|
|
6
|
+
/** A 1.x JS caller's `tenantId` would otherwise fall through to the default tenant without a sound. */
|
|
7
|
+
function rejectLegacyTenantKey(entry) {
|
|
8
|
+
if (Object.hasOwn(entry, 'tenantId') && entry.tenant_id === undefined) {
|
|
9
|
+
throw new TypeError('tenantId was renamed to tenant_id in 2.0');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
6
12
|
export function upsertEntryRow(db, entry) {
|
|
7
|
-
|
|
13
|
+
rejectLegacyTenantKey(entry);
|
|
14
|
+
checkRejectionGuard(db, entry.tenant_id ?? DEFAULT_TENANT_ID, entry.id, entry.content);
|
|
8
15
|
syncFtsRow(db, entry, upsertMemoryRow(db, entry));
|
|
9
16
|
}
|
|
10
17
|
/** The row alone, with no rejection guard or full-text row, for `upsertEntryRow` and for
|
|
@@ -104,7 +111,7 @@ function memoryRowValues(entry) {
|
|
|
104
111
|
entry.scope ?? null,
|
|
105
112
|
entry.owner ?? null,
|
|
106
113
|
entry.artifact_ref ?? null,
|
|
107
|
-
entry.
|
|
114
|
+
entry.tenant_id ?? DEFAULT_TENANT_ID,
|
|
108
115
|
entry.origin_project ?? null,
|
|
109
116
|
entry.descendant_count ?? 0,
|
|
110
117
|
entry.earliest_at ?? null,
|
|
@@ -2,13 +2,19 @@ import { type MemoryEntry } from '../core/memory.js';
|
|
|
2
2
|
import { type DatabaseSyncLike } from '../db/index.js';
|
|
3
3
|
export interface WriteEntryOptions {
|
|
4
4
|
actor?: string;
|
|
5
|
-
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
6
5
|
/** Runs after the row commits and before the mirrors, on an idle connection; keep it best-effort. */
|
|
7
6
|
afterCommit?: () => void;
|
|
8
7
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
8
|
+
/** Store-side only, never exported from a package entry: `afterWrite` hands the write's own db handle to the hook. */
|
|
9
|
+
export interface WriteEntryInternalOptions extends WriteEntryOptions {
|
|
10
|
+
/** Runs in the same SAVEPOINT as the INSERT, so a throw rolls the row back. */
|
|
11
|
+
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/** Write an entry to SQLite and refresh mirrors. `opts.actor` defaults to 'cli'; the api layer passes the resolved actor,
|
|
14
|
+
* so the audit log gets one row per write. */
|
|
11
15
|
export declare function writeEntry(hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
|
|
16
|
+
/** writeEntry for store code that needs the write's own handle: `opts.afterWrite` shares the INSERT's SAVEPOINT. */
|
|
17
|
+
export declare function writeEntryWithHook(hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryInternalOptions): void;
|
|
12
18
|
/** writeEntry for each of `entries` in one transaction, so all land or none; each
|
|
13
19
|
* keeps its audit row, and mirrors follow the commit. Returns how many were written; an empty list opens no store. */
|
|
14
20
|
export declare function writeEntriesTogether(hippoRoot: string, entries: readonly MemoryEntry[]): number;
|
|
@@ -20,10 +26,7 @@ export declare function writeEntriesSkippingRejected(hippoRoot: string, fill: (p
|
|
|
20
26
|
export declare function addTagToEntries(hippoRoot: string, tenantId: string, ids: readonly string[], tag: string): void;
|
|
21
27
|
/** DB-only write path: upsert, afterWrite hook and audit row inside one withWriteScope; the caller owns `db` and must call `writeEntryMirrors` after any
|
|
22
28
|
* outer tx commits, so a rolled-back tx never leaves orphan markdown. The audit row commits atomically with the INSERT. */
|
|
23
|
-
export declare function writeEntryDbOnly(db: DatabaseSyncLike, entry: MemoryEntry, opts?:
|
|
24
|
-
actor?: string;
|
|
25
|
-
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
26
|
-
}): void;
|
|
29
|
+
export declare function writeEntryDbOnly(db: DatabaseSyncLike, entry: MemoryEntry, opts?: Pick<WriteEntryInternalOptions, 'actor' | 'afterWrite'>): void;
|
|
27
30
|
/** What follows a row's upsert inside its write scope: the remember row, last of the write's audit rows, then the summary parent's dirty mark. */
|
|
28
31
|
export declare function auditEntryWrite(db: DatabaseSyncLike, entry: MemoryEntry, actor: string): void;
|
|
29
32
|
/** Markdown mirror path, invoked AFTER commit (a rolled-back tx must leave no orphan markdown). */
|