hippo-memory 1.67.0 → 1.69.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 +1 -1
- package/dist/agent-memories/copilot.d.ts +3 -1
- package/dist/agent-memories/copilot.js +2 -2
- package/dist/api/auth.d.ts +14 -18
- package/dist/api/auth.js +93 -60
- package/dist/api/context-select.d.ts +10 -0
- package/dist/api/context-select.js +16 -18
- package/dist/api/context.js +101 -60
- package/dist/api/forget.d.ts +2 -2
- package/dist/api/forget.js +11 -0
- package/dist/api/outcome.d.ts +4 -4
- package/dist/api/outcome.js +29 -6
- package/dist/api/promote.d.ts +3 -3
- package/dist/api/promote.js +45 -90
- package/dist/api/remember.d.ts +3 -2
- package/dist/api/remember.js +38 -11
- package/dist/api/types.d.ts +9 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +14 -6
- package/dist/capture/compact.js +21 -2
- package/dist/capture/copilot-transcript.d.ts +3 -0
- package/dist/capture/copilot-transcript.js +8 -1
- package/dist/capture/session-worker.d.ts +13 -0
- package/dist/capture/session-worker.js +194 -0
- package/dist/capture-contract.d.ts +3 -1
- package/dist/capture-contract.js +36 -0
- package/dist/cli/session-hooks.js +90 -22
- package/dist/cli/setup.js +70 -24
- package/dist/cli/usage.js +4 -1
- package/dist/cli.js +1 -1
- package/dist/connectors/github/ingest.js +2 -1
- package/dist/connectors/slack/deletion.js +2 -1
- package/dist/connectors/slack/ingest.js +2 -1
- package/dist/embeddings.d.ts +10 -10
- package/dist/embeddings.js +103 -18
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -0
- package/dist/hooks/copilot.d.ts +50 -8
- package/dist/hooks/copilot.js +116 -30
- package/dist/hooks/json-hooks.d.ts +2 -0
- package/dist/hooks/json-hooks.js +11 -2
- package/dist/hooks/shared.d.ts +7 -1
- package/dist/hooks/shared.js +18 -3
- package/dist/mcp/memory-tools.d.ts +2 -2
- package/dist/mcp/memory-tools.js +9 -8
- package/dist/mcp/request.js +3 -3
- package/dist/memory.d.ts +2 -0
- package/dist/memory.js +7 -0
- package/dist/prompt-recall.d.ts +5 -0
- package/dist/prompt-recall.js +13 -0
- package/dist/raw-archive.d.ts +2 -2
- package/dist/raw-archive.js +5 -3
- package/dist/recall-scope.d.ts +2 -5
- package/dist/recall-scope.js +5 -6
- package/dist/server/routes/admin.js +2 -2
- package/dist/server/routes/memories.js +9 -6
- package/dist/server-detect.js +3 -2
- package/dist/server.d.ts +20 -4
- package/dist/server.js +23 -11
- package/dist/store/audit-event.d.ts +1 -1
- package/dist/store/audit-event.js +1 -1
- package/dist/store/entry-writes-group.d.ts +9 -0
- package/dist/store/entry-writes-group.js +157 -0
- package/dist/store/handoffs.d.ts +5 -2
- package/dist/store/handoffs.js +35 -5
- package/dist/store/key-writes.d.ts +9 -0
- package/dist/store/key-writes.js +30 -0
- package/dist/store/open.d.ts +2 -0
- package/dist/store/open.js +10 -0
- package/dist/store/search-rows.js +3 -9
- package/dist/store/sessions.d.ts +5 -0
- package/dist/store/sessions.js +4 -1
- package/dist/store/vector-writes.d.ts +6 -0
- package/dist/store/vector-writes.js +59 -0
- package/dist/store-port.d.ts +143 -1
- package/dist/store-port.js +48 -12
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// hippo.db's half of the EntryWrites store group: the queries remember, outcome, supersede, archive and forget run today.
|
|
2
|
+
import { ConflictError, NotFoundError } from '../api-errors.js';
|
|
3
|
+
import { appendAuditEvent } from '../audit.js';
|
|
4
|
+
import { isSqliteBusy, withWriteScope } from '../db.js';
|
|
5
|
+
import { log } from '../log.js';
|
|
6
|
+
import { entryAfterOutcome } from '../memory.js';
|
|
7
|
+
import { archiveRawMemory } from '../raw-archive.js';
|
|
8
|
+
import { ownScopeTouches } from '../recall-scope.js';
|
|
9
|
+
import { RejectedValueError } from '../rejection.js';
|
|
10
|
+
import { markSummaryDirtyInTx } from '../summary-dirty.js';
|
|
11
|
+
import { auditRejectionRefusal } from './audit-event.js';
|
|
12
|
+
import { deleteEntryCore } from './delete-and-batch.js';
|
|
13
|
+
import { selectEntriesByIds } from './entry-reads.js';
|
|
14
|
+
import { stampOriginProject } from './entry-row.js';
|
|
15
|
+
import { writeEntryDbOnly, writeEntryMirrors } from './entry-writes.js';
|
|
16
|
+
import { updateStatsUnlessBusy } from './index-and-stats.js';
|
|
17
|
+
import { purgeMirrorBestEffort, removeEntryMirrors } from './mirrors.js';
|
|
18
|
+
import { onHandle, openStore } from './open.js';
|
|
19
|
+
import { selectMemoryReach } from './tenant-lookup.js';
|
|
20
|
+
/** Each call on its own handle; mirrors and the forgotten counter follow the commit, so a rolled-back write leaves neither. */
|
|
21
|
+
export function sqliteEntryWrites(hippoRoot) {
|
|
22
|
+
return {
|
|
23
|
+
async writeEntry({ entry, actor }) {
|
|
24
|
+
const stamped = stampOriginProject(hippoRoot, entry);
|
|
25
|
+
onHandle(hippoRoot, (db) => writeInOwnTenant(db, stamped, actor), openStore);
|
|
26
|
+
writeEntryMirrors(hippoRoot, stamped);
|
|
27
|
+
},
|
|
28
|
+
async applyOutcome(outcome) {
|
|
29
|
+
const applied = onHandle(hippoRoot, (db) => applyOutcomeOn(db, outcome), openStore);
|
|
30
|
+
for (const entry of applied)
|
|
31
|
+
writeEntryMirrors(hippoRoot, entry);
|
|
32
|
+
return applied.map((entry) => entry.id);
|
|
33
|
+
},
|
|
34
|
+
async supersede(write) {
|
|
35
|
+
const successor = stampOriginProject(hippoRoot, write.successor);
|
|
36
|
+
onHandle(hippoRoot, (db) => commitSupersede(db, { ...write, successor }));
|
|
37
|
+
writeEntryMirrors(hippoRoot, successor);
|
|
38
|
+
},
|
|
39
|
+
async archiveRaw(archive) {
|
|
40
|
+
const archivedAt = onHandle(hippoRoot, (db) => {
|
|
41
|
+
const at = withWriteScope(db, 'archive_raw_in_reach', () => {
|
|
42
|
+
assertInReach(db, archive, archive.id);
|
|
43
|
+
return archiveRawMemory(db, archive.id, { reason: archive.reason, who: archive.actor });
|
|
44
|
+
});
|
|
45
|
+
cleanArchivedMirrors(db, hippoRoot, archive.id);
|
|
46
|
+
return at;
|
|
47
|
+
});
|
|
48
|
+
updateStatsUnlessBusy(hippoRoot, { forgotten: 1 }, `removed ${archive.id}`);
|
|
49
|
+
return archivedAt;
|
|
50
|
+
},
|
|
51
|
+
async forget(removal) {
|
|
52
|
+
onHandle(hippoRoot, (db) => withWriteScope(db, 'forget_in_reach', () => {
|
|
53
|
+
assertInReach(db, removal, removal.id);
|
|
54
|
+
if (!deleteEntryCore(db, removal.id, { actor: removal.actor }))
|
|
55
|
+
throw notFound(removal.id);
|
|
56
|
+
}), openStore);
|
|
57
|
+
purgeMirrorBestEffort(hippoRoot, removal.id, false, 'deleteEntry');
|
|
58
|
+
updateStatsUnlessBusy(hippoRoot, { forgotten: 1 }, `removed ${removal.id}`);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function notFound(id) {
|
|
63
|
+
return new NotFoundError(`memory not found: ${id}`);
|
|
64
|
+
}
|
|
65
|
+
/** Another tenant's row, or another person's personal row, answers as missing, so a caller learns nothing about it. */
|
|
66
|
+
function assertInReach(db, target, id) {
|
|
67
|
+
const reach = selectMemoryReach(db, id);
|
|
68
|
+
if (reach?.tenantId !== target.tenantId || !ownScopeTouches(target.ownScope, reach.scope))
|
|
69
|
+
throw notFound(id);
|
|
70
|
+
}
|
|
71
|
+
/** hippo.db's upsert would move the row into the writer's tenant, so a store write refuses an id another tenant holds. */
|
|
72
|
+
function assertIdInTenant(db, entry) {
|
|
73
|
+
const holder = selectMemoryReach(db, entry.id)?.tenantId;
|
|
74
|
+
if (holder !== undefined && holder !== entry.tenantId)
|
|
75
|
+
throw new ConflictError(`Memory ${entry.id} belongs to another tenant`);
|
|
76
|
+
}
|
|
77
|
+
/** The refusal row lands after the rollback, in a fresh implicit transaction the aborted one cannot undo. */
|
|
78
|
+
function writeInOwnTenant(db, entry, actor) {
|
|
79
|
+
try {
|
|
80
|
+
withWriteScope(db, 'write_entry_in_tenant', () => {
|
|
81
|
+
assertIdInTenant(db, entry);
|
|
82
|
+
writeEntryDbOnly(db, entry, { actor });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (err instanceof RejectedValueError)
|
|
87
|
+
auditRejectionRefusal(db, err, actor);
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The reads, rewrites and outcome rows share one BEGIN IMMEDIATE transaction, hippo.db's form of the port's row lock; the caller writes the mirrors after. */
|
|
92
|
+
function applyOutcomeOn(db, outcome) {
|
|
93
|
+
return withWriteScope(db, 'apply_outcome', () => {
|
|
94
|
+
const live = selectEntriesByIds(db, outcome.ids, outcome.tenantId);
|
|
95
|
+
const applied = [];
|
|
96
|
+
for (const id of outcome.ids) {
|
|
97
|
+
const entry = live.get(id);
|
|
98
|
+
if (!entry || !ownScopeTouches(outcome.ownScope, entry.scope))
|
|
99
|
+
continue;
|
|
100
|
+
const updated = entryAfterOutcome(entry, outcome.good);
|
|
101
|
+
writeEntryDbOnly(db, updated, { actor: outcome.actor });
|
|
102
|
+
live.set(id, updated);
|
|
103
|
+
appendAuditEvent(db, { tenantId: outcome.tenantId, actor: outcome.actor, op: 'outcome', targetId: id, metadata: { good: outcome.good } });
|
|
104
|
+
applied.push(updated);
|
|
105
|
+
}
|
|
106
|
+
return applied;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Reach, the CAS on the old row, the successor's insert and the supersede row in one BEGIN IMMEDIATE transaction. Two racing supersedes: one CAS wins. */
|
|
110
|
+
export function commitSupersede(db, write) {
|
|
111
|
+
const { tenantId, actor, oldId, successor } = write;
|
|
112
|
+
db.exec('BEGIN IMMEDIATE');
|
|
113
|
+
try {
|
|
114
|
+
assertInReach(db, write, oldId);
|
|
115
|
+
// SAFETY: RETURNING names one column, dag_parent_id; no row means another writer got there first.
|
|
116
|
+
const won = db.prepare('UPDATE memories SET superseded_by = ? WHERE id = ? AND tenant_id = ? AND superseded_by IS NULL RETURNING dag_parent_id')
|
|
117
|
+
.get(successor.id, oldId, tenantId);
|
|
118
|
+
if (!won)
|
|
119
|
+
throw new ConflictError(`Memory ${oldId} already superseded by another writer`);
|
|
120
|
+
// After the CAS, so a lost race marks no parent for rebuild.
|
|
121
|
+
if (won.dag_parent_id)
|
|
122
|
+
markSummaryDirtyInTx(db, won.dag_parent_id, tenantId, actor);
|
|
123
|
+
assertIdInTenant(db, successor);
|
|
124
|
+
writeEntryDbOnly(db, successor, { actor });
|
|
125
|
+
appendAuditEvent(db, { tenantId, actor, op: 'supersede', targetId: oldId, metadata: { newId: successor.id } });
|
|
126
|
+
db.exec('COMMIT');
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
try {
|
|
130
|
+
db.exec('ROLLBACK');
|
|
131
|
+
}
|
|
132
|
+
catch { /* already rolled back */ }
|
|
133
|
+
// The refusal row lands after ROLLBACK, in a fresh implicit transaction the aborted one cannot undo.
|
|
134
|
+
if (err instanceof RejectedValueError)
|
|
135
|
+
auditRejectionRefusal(db, err, actor);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** A mirror left on disk would bring the archived row back on the next import; on failure the reaper retries, as mirror_cleaned_at stays NULL. */
|
|
140
|
+
export function cleanArchivedMirrors(db, hippoRoot, id) {
|
|
141
|
+
try {
|
|
142
|
+
removeEntryMirrors(hippoRoot, id);
|
|
143
|
+
}
|
|
144
|
+
catch (mirrorErr) {
|
|
145
|
+
log.error(`archiveRaw: mirror cleanup failed for ${id} (will retry via reaper on next openHippoDb): ${mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
db.prepare(`UPDATE raw_archive SET mirror_cleaned_at = ? WHERE memory_id = ?`).run(new Date().toISOString(), id);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (!isSqliteBusy(err))
|
|
153
|
+
throw err;
|
|
154
|
+
log.warnThenDebug('archive-mirror-stamp-busy', `archived ${id}; the store was busy, so the mirror reaper will re-check it on the next open`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=entry-writes-group.js.map
|
package/dist/store/handoffs.d.ts
CHANGED
|
@@ -23,7 +23,10 @@ export declare function loadHandoffById(hippoRoot: string, tenantId: string, id:
|
|
|
23
23
|
export declare function stampHandoffOutcome(hippoRoot: string, tenantId: string, sessionId: string, outcome: HandoffOutcome): number;
|
|
24
24
|
/** Auto-write a handoff at session-end from the session's active snapshot, else from `derived`, its transcript state.
|
|
25
25
|
* @param evidence best-effort git state; outcome comes from the newest session_complete event.
|
|
26
|
-
* @
|
|
27
|
-
|
|
26
|
+
* @param options.inPlace rewrite an earlier transcript read instead of adding a revision, for a close that runs after every reply.
|
|
27
|
+
* @returns null when neither source is the session's, a newer handoff covers the snapshot, the session's latest handoff was not read off its transcript or already holds `derived`, or, in place, a handoff landed after the read. */
|
|
28
|
+
export declare function writeSessionEndHandoff(hippoRoot: string, tenantId: string, sessionId: string, evidence: HandoffEvidence | null, derived?: Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null, key?: ContinuityKey, options?: {
|
|
29
|
+
inPlace?: boolean;
|
|
30
|
+
}): SessionHandoff | null;
|
|
28
31
|
export {};
|
|
29
32
|
//# sourceMappingURL=handoffs.d.ts.map
|
package/dist/store/handoffs.js
CHANGED
|
@@ -128,10 +128,32 @@ export function stampHandoffOutcome(hippoRoot, tenantId, sessionId, outcome) {
|
|
|
128
128
|
closeHippoDb(db);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
/** Rewrites the session's newest handoff when it is still a transcript read, in one statement so a handoff written meanwhile is never overwritten; null when none was rewritten. */
|
|
132
|
+
function replaceTranscriptHandoff(hippoRoot, tenantId, handoff, key) {
|
|
133
|
+
const { conditions, params } = handoffConditions(tenantId, handoff.sessionId, {}, key);
|
|
134
|
+
const newest = `SELECT id FROM session_handoffs WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC, id DESC LIMIT 1`;
|
|
135
|
+
const db = openStore(hippoRoot);
|
|
136
|
+
try {
|
|
137
|
+
const result = db.prepare(`
|
|
138
|
+
UPDATE session_handoffs SET repo_root = ?, task_id = ?, summary = ?, next_action = ?, artifacts_json = ?, scope = ?, created_at = ?,
|
|
139
|
+
constraints_json = ?, evidence_json = ?, outcome = ?, target_runtime = ?, card_id = ?
|
|
140
|
+
WHERE id = (${newest}) AND json_valid(evidence_json) AND json_extract(evidence_json, '$.derivedFrom') = 'transcript'
|
|
141
|
+
`).run(handoff.repoRoot ?? null, handoff.taskId ?? null, handoff.summary, handoff.nextAction ?? null, JSON.stringify(handoff.artifacts ?? []), handoff.scope ?? null, new Date().toISOString(), JSON.stringify(handoff.constraints ?? []), handoff.evidence ? JSON.stringify(handoff.evidence) : null, handoff.outcome ?? null, handoff.targetRuntime ?? null, handoff.cardId ?? null, ...params);
|
|
142
|
+
if (Number(result.changes ?? 0) === 0)
|
|
143
|
+
return null;
|
|
144
|
+
// SAFETY: row's shape matches HANDOFF_COLUMNS.
|
|
145
|
+
const row = db.prepare(`SELECT ${HANDOFF_COLUMNS} FROM session_handoffs WHERE id = (${newest})`).get(...params);
|
|
146
|
+
return row ? rowToSessionHandoff(row) : null;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
closeHippoDb(db);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
131
152
|
/** Auto-write a handoff at session-end from the session's active snapshot, else from `derived`, its transcript state.
|
|
132
153
|
* @param evidence best-effort git state; outcome comes from the newest session_complete event.
|
|
133
|
-
* @
|
|
134
|
-
|
|
154
|
+
* @param options.inPlace rewrite an earlier transcript read instead of adding a revision, for a close that runs after every reply.
|
|
155
|
+
* @returns null when neither source is the session's, a newer handoff covers the snapshot, the session's latest handoff was not read off its transcript or already holds `derived`, or, in place, a handoff landed after the read. */
|
|
156
|
+
export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence, derived = null, key, options = {}) {
|
|
135
157
|
assertTenantId('writeSessionEndHandoff', tenantId);
|
|
136
158
|
if (key)
|
|
137
159
|
continuityStamp(key); // a partial key is a caller bug, so fail loud rather than write nothing
|
|
@@ -139,7 +161,10 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence,
|
|
|
139
161
|
const existing = loadLatestHandoff(hippoRoot, tenantId, sessionId, {}, key);
|
|
140
162
|
let snapshot;
|
|
141
163
|
let handoffEvidence = evidence;
|
|
142
|
-
|
|
164
|
+
let fromTranscript = false;
|
|
165
|
+
// After every reply the transcript is newer than the last compaction's snapshot, which the open chat keeps for the next one.
|
|
166
|
+
const snapshotGivesWay = options.inPlace === true && derived !== null && active?.source === 'pre-compact';
|
|
167
|
+
if (active && active.session_id === sessionId && !snapshotGivesWay) {
|
|
143
168
|
// Strict '>': a same-millisecond tie must not swallow the session's only write (test 6e).
|
|
144
169
|
if (existing && existing.updatedAt > active.updated_at)
|
|
145
170
|
return null;
|
|
@@ -154,13 +179,14 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence,
|
|
|
154
179
|
return null;
|
|
155
180
|
snapshot = { ...derived, scope: null };
|
|
156
181
|
handoffEvidence = { ...evidence, derivedFrom: 'transcript' };
|
|
182
|
+
fromTranscript = true;
|
|
157
183
|
}
|
|
158
184
|
const outcome = sessionOutcome(hippoRoot, tenantId, sessionId);
|
|
159
185
|
// A same-task refresh carries forward envelope fields nobody cleared; a scope
|
|
160
186
|
// mismatch must not leak private metadata into an unscoped envelope.
|
|
161
187
|
const carryForward = existing != null && existing.taskId === snapshot.task
|
|
162
188
|
&& (existing.scope ?? null) === (snapshot.scope ?? null);
|
|
163
|
-
|
|
189
|
+
const handoff = {
|
|
164
190
|
version: 1,
|
|
165
191
|
sessionId,
|
|
166
192
|
repoRoot: carryForward ? existing.repoRoot : undefined,
|
|
@@ -174,7 +200,11 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence,
|
|
|
174
200
|
constraints: carryForward ? existing.constraints : undefined,
|
|
175
201
|
targetRuntime: carryForward ? existing.targetRuntime : undefined,
|
|
176
202
|
cardId: carryForward ? existing.cardId : undefined,
|
|
177
|
-
}
|
|
203
|
+
};
|
|
204
|
+
// A miss means a handoff landed since the read, and a new transcript row would hide it.
|
|
205
|
+
if (fromTranscript && options.inPlace && existing)
|
|
206
|
+
return replaceTranscriptHandoff(hippoRoot, tenantId, handoff, key);
|
|
207
|
+
return saveSessionHandoff(hippoRoot, tenantId, handoff, key);
|
|
178
208
|
}
|
|
179
209
|
function sessionOutcome(hippoRoot, tenantId, sessionId) {
|
|
180
210
|
const db = openHippoDb(hippoRoot);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AppendAuditOpts } from '../audit.js';
|
|
2
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
3
|
+
import type { KeyMint, SelfKeyMint } from '../store-port.js';
|
|
4
|
+
/** The auth_create row a mint writes; the plaintext never reaches it. */
|
|
5
|
+
export declare function createAuditOf({ key, actor, metadata }: KeyMint): AppendAuditOpts;
|
|
6
|
+
export declare function createKeyAt(db: DatabaseSyncLike, mint: KeyMint): void;
|
|
7
|
+
/** IMMEDIATE takes the write lock before the count, so two mints for one owner cannot both see room under the cap. */
|
|
8
|
+
export declare function createSelfKeyAt(db: DatabaseSyncLike, mint: SelfKeyMint): string[];
|
|
9
|
+
//# sourceMappingURL=key-writes.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// hippo.db's half of the KeyWrites store group.
|
|
2
|
+
import { appendAuditEvent } from '../audit.js';
|
|
3
|
+
import { insertApiKey, listLiveOwnedKeyIds, revokeApiKey } from '../auth.js';
|
|
4
|
+
import { withWriteScope } from '../db.js';
|
|
5
|
+
/** The auth_create row a mint writes; the plaintext never reaches it. */
|
|
6
|
+
export function createAuditOf({ key, actor, metadata }) {
|
|
7
|
+
return { tenantId: key.tenantId, actor, op: 'auth_create', targetId: key.keyId, metadata };
|
|
8
|
+
}
|
|
9
|
+
export function createKeyAt(db, mint) {
|
|
10
|
+
withWriteScope(db, 'create_api_key', () => {
|
|
11
|
+
insertApiKey(db, mint.key);
|
|
12
|
+
appendAuditEvent(db, createAuditOf(mint));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** IMMEDIATE takes the write lock before the count, so two mints for one owner cannot both see room under the cap. */
|
|
16
|
+
export function createSelfKeyAt(db, mint) {
|
|
17
|
+
const { key, actor, perSubject } = mint;
|
|
18
|
+
return withWriteScope(db, 'create_self_api_key', () => {
|
|
19
|
+
const live = listLiveOwnedKeyIds(db, key.tenantId, key.ownerSubject, Date.parse(key.createdAt));
|
|
20
|
+
const replaced = live.slice(0, Math.max(0, live.length - perSubject + 1));
|
|
21
|
+
insertApiKey(db, key);
|
|
22
|
+
for (const keyId of replaced) {
|
|
23
|
+
revokeApiKey(db, keyId, key.createdAt);
|
|
24
|
+
appendAuditEvent(db, { tenantId: key.tenantId, actor, op: 'auth_revoke', targetId: keyId, metadata: { replacedBy: key.keyId } });
|
|
25
|
+
}
|
|
26
|
+
appendAuditEvent(db, createAuditOf(mint));
|
|
27
|
+
return replaced;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=key-writes.js.map
|
package/dist/store/open.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare function initStore(hippoRoot: string): void;
|
|
|
9
9
|
export declare function openStore(hippoRoot: string, opts?: {
|
|
10
10
|
busyWaitMs?: number;
|
|
11
11
|
}): DatabaseSyncLike;
|
|
12
|
+
/** One call on a handle of its own, closed after; pass openStore where the call also sets up mirror folders and legacy rows. */
|
|
13
|
+
export declare function onHandle<T>(hippoRoot: string, fn: (db: DatabaseSyncLike) => T, open?: (hippoRoot: string) => DatabaseSyncLike): T;
|
|
12
14
|
/** `meta` key holding the default half-life base a store's memories are on (src/half-life-migration.ts). */
|
|
13
15
|
export declare const HALF_LIFE_BASE_META_KEY = "default_half_life_base";
|
|
14
16
|
/** `meta` key set once no memory of a decision, incident or other object sits on the old flat 90 days. */
|
package/dist/store/open.js
CHANGED
|
@@ -46,6 +46,16 @@ export function openStore(hippoRoot, opts) {
|
|
|
46
46
|
throw error;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
/** One call on a handle of its own, closed after; pass openStore where the call also sets up mirror folders and legacy rows. */
|
|
50
|
+
export function onHandle(hippoRoot, fn, open = openHippoDb) {
|
|
51
|
+
const db = open(hippoRoot);
|
|
52
|
+
try {
|
|
53
|
+
return fn(db);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
closeHippoDb(db);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
49
59
|
/** `meta` key holding the default half-life base a store's memories are on (src/half-life-migration.ts). */
|
|
50
60
|
export const HALF_LIFE_BASE_META_KEY = 'default_half_life_base';
|
|
51
61
|
/** `meta` key set once no memory of a decision, incident or other object sits on the old flat 90 days. */
|
|
@@ -2,7 +2,7 @@ import { envForceLikePath } from '../env.js';
|
|
|
2
2
|
import { isFtsAvailable, closeHippoDb } from '../db.js';
|
|
3
3
|
import { tokenize } from '../tokenize.js';
|
|
4
4
|
import { isPersonalScope, scopeAdmitSql } from '../recall-scope.js';
|
|
5
|
-
import { RAREST_TERM_COUNT,
|
|
5
|
+
import { ftsTermParts, RAREST_TERM_COUNT, rarestFtsQuery } from '../prompt-recall.js';
|
|
6
6
|
import { log } from '../log.js';
|
|
7
7
|
import { originInSql } from '../project-identity.js';
|
|
8
8
|
import { topVectorMatches } from '../vector-store.js';
|
|
@@ -210,9 +210,7 @@ export function pickRarestFtsQuery(db, terms, maxTerms = RAREST_TERM_COUNT) {
|
|
|
210
210
|
if (!isFtsAvailable(db))
|
|
211
211
|
return terms.slice(0, 32).join(' ');
|
|
212
212
|
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS temp.z1_rarest_vocab USING fts5vocab(main, 'memories_fts', 'row')`);
|
|
213
|
-
|
|
214
|
-
const partsOf = (t) => t.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
215
|
-
const vocab = Array.from(new Set(terms.flatMap(partsOf)));
|
|
213
|
+
const vocab = Array.from(new Set(terms.flatMap(ftsTermParts)));
|
|
216
214
|
if (vocab.length === 0)
|
|
217
215
|
return '';
|
|
218
216
|
// SAFETY: rows' shape matches the two columns named in the SELECT.
|
|
@@ -220,10 +218,6 @@ export function pickRarestFtsQuery(db, terms, maxTerms = RAREST_TERM_COUNT) {
|
|
|
220
218
|
.prepare(`SELECT term, doc FROM temp.z1_rarest_vocab WHERE term IN (${vocab.map(() => '?').join(', ')})`)
|
|
221
219
|
.all(...vocab);
|
|
222
220
|
const counts = new Map(rows.map((r) => [r.term, r.doc]));
|
|
223
|
-
|
|
224
|
-
const parts = partsOf(t);
|
|
225
|
-
return parts.length === 0 ? 0 : Math.min(...parts.map((x) => counts.get(x) ?? 0));
|
|
226
|
-
};
|
|
227
|
-
return rarestPromptTerms(terms, docCount, maxTerms).join(' ');
|
|
221
|
+
return rarestFtsQuery(terms, (part) => counts.get(part) ?? 0, maxTerms);
|
|
228
222
|
}
|
|
229
223
|
//# sourceMappingURL=search-rows.js.map
|
package/dist/store/sessions.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ export declare function loadFreshActiveTaskSnapshot(hippoRoot: string, tenantId:
|
|
|
60
60
|
maxAgeMs?: number;
|
|
61
61
|
sessionId?: string | null;
|
|
62
62
|
}, key?: ContinuityKey): TaskSnapshot | null;
|
|
63
|
+
/** `loadFreshActiveTaskSnapshot`'s owner-match-or-age rule, for a snapshot a store read. */
|
|
64
|
+
export declare function freshActiveSnapshot(snapshot: TaskSnapshot | null, opts?: {
|
|
65
|
+
maxAgeMs?: number;
|
|
66
|
+
sessionId?: string | null;
|
|
67
|
+
}): TaskSnapshot | null;
|
|
63
68
|
export declare function clearActiveTaskSnapshot(hippoRoot: string, tenantId: string, clearedStatus?: string): boolean;
|
|
64
69
|
/**
|
|
65
70
|
* Close the `active` task snapshot(s) owned by `sessionId`, for the
|
package/dist/store/sessions.js
CHANGED
|
@@ -142,7 +142,10 @@ function isNonEmptySessionId(value) {
|
|
|
142
142
|
* No SQL change — age derives from the existing `updated_at` column.
|
|
143
143
|
*/
|
|
144
144
|
export function loadFreshActiveTaskSnapshot(hippoRoot, tenantId, opts = {}, key) {
|
|
145
|
-
|
|
145
|
+
return freshActiveSnapshot(loadActiveTaskSnapshot(hippoRoot, tenantId, key), opts);
|
|
146
|
+
}
|
|
147
|
+
/** `loadFreshActiveTaskSnapshot`'s owner-match-or-age rule, for a snapshot a store read. */
|
|
148
|
+
export function freshActiveSnapshot(snapshot, opts = {}) {
|
|
146
149
|
if (!snapshot)
|
|
147
150
|
return null;
|
|
148
151
|
const callerSessionId = opts.sessionId;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
2
|
+
import type { MemoryEntry } from '../memory.js';
|
|
3
|
+
import type { VectorBackfillQuery, VectorWrite, VectorWriteResult } from '../store-port.js';
|
|
4
|
+
export declare function entriesWithoutVectorAt(db: DatabaseSyncLike, query: VectorBackfillQuery): MemoryEntry[];
|
|
5
|
+
export declare function writeVectorsAt(db: DatabaseSyncLike, write: VectorWrite): VectorWriteResult;
|
|
6
|
+
//# sourceMappingURL=vector-writes.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// hippo.db's half of the VectorWrites store group.
|
|
2
|
+
import { setMeta, withWriteScope } from '../db.js';
|
|
3
|
+
import { embeddingIndexStateOn, replacesIndex } from '../embeddings.js';
|
|
4
|
+
import { float32ToBuffer } from '../physics-state.js';
|
|
5
|
+
import { EMBEDDING_MODEL_META_KEY, upsertVectors } from '../vector-store.js';
|
|
6
|
+
import { chunked } from './entry-reads.js';
|
|
7
|
+
import { MEMORY_SELECT_COLUMNS, rowToEntry } from './rows.js';
|
|
8
|
+
const MAX_BACKFILL_PAGE = 500;
|
|
9
|
+
export function entriesWithoutVectorAt(db, query) {
|
|
10
|
+
if (!Number.isInteger(query.limit))
|
|
11
|
+
throw new RangeError('limit must be an integer');
|
|
12
|
+
const limit = Math.max(1, Math.min(query.limit, MAX_BACKFILL_PAGE));
|
|
13
|
+
const tenantClause = query.tenantId === undefined ? '' : ' AND tenant_id = ?';
|
|
14
|
+
const tenantArgs = query.tenantId === undefined ? [] : [query.tenantId];
|
|
15
|
+
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
16
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id > ?${tenantClause}
|
|
17
|
+
AND NOT EXISTS (SELECT 1 FROM memory_vectors v WHERE v.memory_id = memories.id AND v.model = ?)
|
|
18
|
+
ORDER BY id LIMIT ?`).all(query.afterId ?? '', ...tenantArgs, query.model, limit);
|
|
19
|
+
return rows.map(rowToEntry);
|
|
20
|
+
}
|
|
21
|
+
function ownedIds(db, ids, tenantId) {
|
|
22
|
+
const owned = new Set();
|
|
23
|
+
for (const chunk of chunked([...new Set(ids)])) {
|
|
24
|
+
// SAFETY: the SELECT names one column, id.
|
|
25
|
+
const rows = db.prepare(`SELECT id FROM memories WHERE tenant_id = ? AND id IN (${chunk.map(() => '?').join(', ')})`).all(tenantId, ...chunk);
|
|
26
|
+
for (const row of rows)
|
|
27
|
+
owned.add(row.id);
|
|
28
|
+
}
|
|
29
|
+
return owned;
|
|
30
|
+
}
|
|
31
|
+
// savePhysicsState opens its own transaction and overwrites, so the first particle for a memory is inserted here instead.
|
|
32
|
+
function insertNewParticles(db, rows) {
|
|
33
|
+
const stmt = db.prepare(`
|
|
34
|
+
INSERT INTO memory_physics (memory_id, position_blob, velocity_blob, mass, charge, temperature, last_simulation)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(memory_id) DO NOTHING
|
|
36
|
+
`);
|
|
37
|
+
for (const { memoryId, particle: p } of rows) {
|
|
38
|
+
if (p)
|
|
39
|
+
stmt.run(memoryId, float32ToBuffer(p.position), float32ToBuffer(p.velocity), p.mass, p.charge, p.temperature, p.lastSimulation);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function writeVectorsAt(db, write) {
|
|
43
|
+
return withWriteScope(db, 'write_vectors', () => {
|
|
44
|
+
const replaces = replacesIndex(embeddingIndexStateOn(db), write.model);
|
|
45
|
+
if (replaces && !write.replaceIndex)
|
|
46
|
+
return { written: 0, modelMismatch: true };
|
|
47
|
+
const owned = ownedIds(db, write.rows.map((r) => r.memoryId), write.tenantId);
|
|
48
|
+
const rows = write.rows.filter((r) => owned.has(r.memoryId) && r.vector.length > 0 && r.vector.every(Number.isFinite));
|
|
49
|
+
if (rows.length === 0)
|
|
50
|
+
return { written: 0, modelMismatch: false };
|
|
51
|
+
if (replaces)
|
|
52
|
+
db.exec('DELETE FROM memory_vectors; DELETE FROM memory_physics;');
|
|
53
|
+
const written = upsertVectors(db, rows.map((r) => [r.memoryId, r.vector]), write.model);
|
|
54
|
+
insertNewParticles(db, rows);
|
|
55
|
+
setMeta(db, EMBEDDING_MODEL_META_KEY, write.model);
|
|
56
|
+
return { written, modelMismatch: false };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=vector-writes.js.map
|
package/dist/store-port.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AmbientTallies } from './ambient.js';
|
|
2
|
+
import { type AmbientStoreFilter } from './ambient-store.js';
|
|
3
|
+
import { type ApiKeyListRow, type ApiKeyRecord, type ListApiKeysOpts, type NewApiKey } from './auth.js';
|
|
2
4
|
import { type AppendAuditOpts, type AuditEvent, type ListAuditAfterOpts } from './audit.js';
|
|
3
5
|
import type { ContinuityBlock } from './api/recall-types.js';
|
|
4
6
|
import { type EmbeddingIndexState } from './embeddings.js';
|
|
5
7
|
import { type ActiveGoals, type GetActiveGoalsOpts, type GoalRecallLogRow } from './goals.js';
|
|
8
|
+
import type { SessionHandoff } from './handoff.js';
|
|
9
|
+
import type { JsonValue } from './json.js';
|
|
6
10
|
import type { MemoryEntry } from './memory.js';
|
|
7
11
|
import type { PhysicsParticle } from './physics.js';
|
|
8
12
|
import { type PlanningFallacyEvidence } from './predictions/planning-fallacy.js';
|
|
9
13
|
import { type RecallTraceInput } from './recall-trace.js';
|
|
14
|
+
import { type AmbientLoadResult, type AmbientRecallRequest, type ContextCandidateFilter, type RecentOrigins } from './store/candidates.js';
|
|
10
15
|
import { type StrengthenOptions } from './store/entry-writes.js';
|
|
11
16
|
import { type OriginFilter, type VectorCandidateSpec } from './store/search-rows.js';
|
|
12
17
|
import { type ContinuityKey } from './store/sessions.js';
|
|
@@ -48,6 +53,44 @@ export interface VectorReads {
|
|
|
48
53
|
/** The particles of `ids` only; an empty list reads nothing. */
|
|
49
54
|
physicsParticles(ids: readonly string[]): Promise<Map<string, PhysicsParticle>>;
|
|
50
55
|
}
|
|
56
|
+
/** One memory's vector, computed by the caller's embedding provider. */
|
|
57
|
+
export interface VectorRowWrite {
|
|
58
|
+
readonly memoryId: string;
|
|
59
|
+
readonly vector: readonly number[];
|
|
60
|
+
/** Kept only when the memory has no particle yet, under `memoryId` whatever its own memoryId says. */
|
|
61
|
+
readonly particle?: PhysicsParticle;
|
|
62
|
+
}
|
|
63
|
+
export interface VectorWrite {
|
|
64
|
+
readonly tenantId: string;
|
|
65
|
+
/** The index identity the vectors were built by, `embeddingIndexIdentity(provider.id)`. */
|
|
66
|
+
readonly model: string;
|
|
67
|
+
/** Set only by embedAll's rebuild, so a write that read the index state before another process rebuilt it cannot drop that rebuild. */
|
|
68
|
+
readonly replaceIndex: boolean;
|
|
69
|
+
readonly rows: readonly VectorRowWrite[];
|
|
70
|
+
}
|
|
71
|
+
export interface VectorWriteResult {
|
|
72
|
+
readonly written: number;
|
|
73
|
+
/** True when another model built the index and `replaceIndex` is false; the store then keeps nothing and `written` is 0. */
|
|
74
|
+
readonly modelMismatch: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface VectorBackfillQuery {
|
|
77
|
+
readonly model: string;
|
|
78
|
+
/** Only ids above this one in byte order; unset starts at the first. */
|
|
79
|
+
readonly afterId?: string;
|
|
80
|
+
/** Clamped to 1..500; a non-integer rejects with RangeError `limit must be an integer`. */
|
|
81
|
+
readonly limit: number;
|
|
82
|
+
/** Every tenant when unset, since the backfill covers the whole store. */
|
|
83
|
+
readonly tenantId?: string;
|
|
84
|
+
}
|
|
85
|
+
/** The writes behind embed on write and the backfill: core computes each vector, the store only keeps it.
|
|
86
|
+
* A store deletes a memory's vector and particle when the memory is deleted (hippo.db does it by trigger and foreign-key cascade). */
|
|
87
|
+
export interface VectorWrites {
|
|
88
|
+
/** Memories of any kind with no vector stored under `query.model`, by id ascending in byte order. */
|
|
89
|
+
entriesWithoutVector(query: VectorBackfillQuery): Promise<MemoryEntry[]>;
|
|
90
|
+
/** In one transaction: refuses with `modelMismatch` if `replacesIndex` and not `replaceIndex`; else, when a row is writable (memory in `tenantId`, vector non-empty
|
|
91
|
+
* and finite), drops every vector and particle if `replacesIndex`, keeps each writable row under `model` with its particle where none exists, sets `model`. No audit row. */
|
|
92
|
+
writeVectors(write: VectorWrite): Promise<VectorWriteResult>;
|
|
93
|
+
}
|
|
51
94
|
export interface KeyRevoke {
|
|
52
95
|
readonly tenantId: string;
|
|
53
96
|
readonly keyId: string;
|
|
@@ -63,11 +106,110 @@ export interface KeyAudit {
|
|
|
63
106
|
/** The highest audit id ever assigned, pruned rows included, 0 before the first; a tail that sees it drop knows the log was restored. */
|
|
64
107
|
auditHighId(): Promise<number>;
|
|
65
108
|
}
|
|
109
|
+
/** One ambient load: the pins, the `recentNeeded` newest rows `admit` keeps, and the prompt-recall candidates. */
|
|
110
|
+
export interface AmbientCandidateRequest {
|
|
111
|
+
readonly recentNeeded: number;
|
|
112
|
+
/** Sees each row in the order `loadAmbientCandidates` reads them, since the delivery ledger counts its refusals. */
|
|
113
|
+
readonly admit: (e: MemoryEntry) => boolean;
|
|
114
|
+
readonly recall?: AmbientRecallRequest;
|
|
115
|
+
readonly origins?: RecentOrigins;
|
|
116
|
+
}
|
|
117
|
+
/** The reads behind getContext beyond the base `continuity`: its fallback handoff, candidate rows and ambient tallies.
|
|
118
|
+
* getContext with a query under an embedding provider needs `vectors` too, and answers 501 without it. */
|
|
119
|
+
export interface ContextReads {
|
|
120
|
+
/** The newest handoff of the last `maxAgeMs` that is its session's highest id, has no outcome or a partial or failed one, and
|
|
121
|
+
* passes recall's default scope deny, as `loadLatestHandoff` with `unfinishedOnly` and `scopeFilter: 'default-deny'`. Rejects an empty tenant id. */
|
|
122
|
+
unfinishedHandoff(tenantId: string, maxAgeMs: number, key: ContinuityKey | null): Promise<SessionHandoff | null>;
|
|
123
|
+
/** As `loadAmbientCandidates`; `recall` searches with the query `rarestFtsQuery` builds from FTS document counts over every tenant's rows,
|
|
124
|
+
* and finds nothing when no term is indexed. Core keeps only `tenantId`'s rows that its own admit passes, whatever the store returns. */
|
|
125
|
+
ambientCandidates(tenantId: string, request: AmbientCandidateRequest): Promise<AmbientLoadResult>;
|
|
126
|
+
/** As `loadContextCandidates`: at most `filter.cap` live rows, pins and then the least decayed first, returned by created, then id. */
|
|
127
|
+
contextCandidates(tenantId: string, filter: ContextCandidateFilter): Promise<MemoryEntry[]>;
|
|
128
|
+
/** As `loadAmbientTallies`: one aggregate over the live, unarchived rows `filter` admits, less other projects' secret-tagged rows. */
|
|
129
|
+
ambientTallies(tenantId: string, filter: AmbientStoreFilter): Promise<AmbientTallies>;
|
|
130
|
+
}
|
|
131
|
+
export interface EntryWrite {
|
|
132
|
+
readonly entry: MemoryEntry;
|
|
133
|
+
readonly actor: string;
|
|
134
|
+
}
|
|
135
|
+
/** Who acts on which of a tenant's rows; `ownScope` is the caller's personal scope, so every other personal row is out of reach. */
|
|
136
|
+
export interface EntryTarget {
|
|
137
|
+
readonly tenantId: string;
|
|
138
|
+
readonly actor: string;
|
|
139
|
+
readonly ownScope: string | null;
|
|
140
|
+
}
|
|
141
|
+
export interface OutcomeWrite extends EntryTarget {
|
|
142
|
+
readonly ids: readonly string[];
|
|
143
|
+
readonly good: boolean;
|
|
144
|
+
}
|
|
145
|
+
export interface SupersedeWrite extends EntryTarget {
|
|
146
|
+
readonly oldId: string;
|
|
147
|
+
readonly successor: MemoryEntry;
|
|
148
|
+
}
|
|
149
|
+
export interface EntryRemoval extends EntryTarget {
|
|
150
|
+
readonly id: string;
|
|
151
|
+
}
|
|
152
|
+
export interface RawArchive extends EntryRemoval {
|
|
153
|
+
readonly reason: string;
|
|
154
|
+
}
|
|
155
|
+
/** The memory-row writes behind remember, outcome, supersede, archive and forget. Each writes its audit rows in the write's own transaction,
|
|
156
|
+
* a child row's change marks its level 2 or 3 summary parent dirty (one summary_marked_dirty row on the flip), and the schema's rules hold. */
|
|
157
|
+
export interface EntryWrites {
|
|
158
|
+
/** Upserts the row, its full-text row and one remember row ({kind, scope}) in the entry's tenant; an id another tenant holds rejects in the transaction with ConflictError `Memory <id>
|
|
159
|
+
* belongs to another tenant`. Content the write brings into a tenant that tombstoned it rejects with RejectedValueError and writes one reject_refusal row after the rollback, best effort. */
|
|
160
|
+
writeEntry(write: EntryWrite): Promise<void>;
|
|
161
|
+
/** For each id in order, a row of the tenant within reach gets `entryAfterOutcome`, a rewrite as writeEntry does and one outcome row ({good}); other ids are skipped, a repeated id builds
|
|
162
|
+
* on its first outcome. Each read holds the row's lock to commit (SELECT ... FOR UPDATE), so a supersede between read and rewrite is not undone. Resolves to the ids applied, repeats kept. */
|
|
163
|
+
applyOutcome(outcome: OutcomeWrite): Promise<string[]>;
|
|
164
|
+
/** Sets the old row's superseded_by where it is within reach and not yet superseded, both checked in the transaction, else rejects with NotFoundError `memory not found: <oldId>` or
|
|
165
|
+
* ConflictError `Memory <oldId> already superseded by another writer`. Then writes the successor as writeEntry does and one supersede row ({newId}), all or nothing. */
|
|
166
|
+
supersede(write: SupersedeWrite): Promise<void>;
|
|
167
|
+
/** Moves a raw row to raw_archive (metadata only, no content), deletes it and its full-text row and writes one archive_raw row ({reason});
|
|
168
|
+
* resolves to the archived_at written. A row out of reach rejects with NotFoundError `memory not found: <id>`, any other kind with BadRequestError. */
|
|
169
|
+
archiveRaw(archive: RawArchive): Promise<string>;
|
|
170
|
+
/** Deletes the row and its full-text row and writes one forget row; a raw row rejects, being append-only. A row out of reach rejects with
|
|
171
|
+
* NotFoundError `memory not found: <id>`. Archive and forget then add one to the store-wide forgotten counter, best effort. */
|
|
172
|
+
forget(removal: EntryRemoval): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
/** A key to insert and the actor and metadata of its auth_create row, whose tenant and target are the key's. Neither holds the plaintext. */
|
|
175
|
+
export interface KeyMint {
|
|
176
|
+
readonly key: NewApiKey;
|
|
177
|
+
readonly actor: string;
|
|
178
|
+
readonly metadata: Readonly<Record<string, JsonValue>>;
|
|
179
|
+
}
|
|
180
|
+
export interface SelfKeyMint extends KeyMint {
|
|
181
|
+
readonly key: NewApiKey & {
|
|
182
|
+
readonly ownerSubject: string;
|
|
183
|
+
readonly expiresAt: string;
|
|
184
|
+
};
|
|
185
|
+
/** Live keys the owner may hold in the tenant once this one is in. */
|
|
186
|
+
readonly perSubject: number;
|
|
187
|
+
}
|
|
188
|
+
/** `listApiKeyRows`'s filters, always inside one tenant. */
|
|
189
|
+
export interface KeyListQuery extends Omit<ListApiKeysOpts, 'tenantId'> {
|
|
190
|
+
readonly tenantId: string;
|
|
191
|
+
}
|
|
192
|
+
export interface KeyWrites {
|
|
193
|
+
/** Inserts the key and appends its auth_create row in one transaction; with either one failing, neither is written. A key id taken in any tenant
|
|
194
|
+
* rejects, since keys are looked up by id alone. */
|
|
195
|
+
createApiKey(mint: KeyMint): Promise<void>;
|
|
196
|
+
/** One transaction, serialized per (tenant, owner) before the count (hippo.db BEGIN IMMEDIATE, Postgres pg_advisory_xact_lock on the pair): revoke at `key.createdAt`
|
|
197
|
+
* the owner's oldest live keys in the key's tenant down to `perSubject - 1`, insert the key, append an auth_revoke row `{ replacedBy: <new key id> }` per revoked key,
|
|
198
|
+
* then the auth_create row; resolves to the revoked ids. Live: unrevoked, expires_at null or Date.parse(expires_at) after createdAt (NaN counts as expired). */
|
|
199
|
+
createSelfApiKey(mint: SelfKeyMint): Promise<string[]>;
|
|
200
|
+
/** As `listApiKeyRows`: newest inserted first, each with its scope grants; `active` drops revoked rows and an expires_at not above now's toISOString. */
|
|
201
|
+
listApiKeys(query: KeyListQuery): Promise<ApiKeyListRow[]>;
|
|
202
|
+
}
|
|
66
203
|
/** The optional groups: a store sets each one whole or leaves it unset, and a route or MCP tool names the one it needs. */
|
|
67
204
|
export interface StoreGroups {
|
|
68
205
|
/** Unset on a store built before them, where hybrid and physics recall under an embedding provider answer 501. */
|
|
69
206
|
readonly vectors: VectorReads;
|
|
70
207
|
readonly keyAudit: KeyAudit;
|
|
208
|
+
readonly keyWrites: KeyWrites;
|
|
209
|
+
/** embedMemory and embedAll with a store need it and `vectors`. */
|
|
210
|
+
readonly vectorWrites: VectorWrites;
|
|
211
|
+
readonly entryWrites: EntryWrites;
|
|
212
|
+
readonly contextReads: ContextReads;
|
|
71
213
|
}
|
|
72
214
|
export type StoreGroup = 'base' | keyof StoreGroups;
|
|
73
215
|
export declare function hasGroup(store: HippoStore, group: StoreGroup): boolean;
|