hippo-memory 1.68.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/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/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/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/memories.js +9 -6
- package/dist/server-detect.js +3 -2
- package/dist/server.d.ts +14 -1
- package/dist/server.js +16 -6
- 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/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-port.d.ts +71 -0
- package/dist/store-port.js +23 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/server-detect.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { envHealthProbeMs } from './env.js';
|
|
3
4
|
// Pidfile sits directly inside hippoRoot. `hippoRoot` is the `.hippo`
|
|
4
5
|
// directory itself (the same convention used by api.ts / store.ts /
|
|
5
6
|
// openHippoDb), so this resolves to `${hippoRoot}/server.pid`.
|
|
@@ -8,7 +9,7 @@ const PIDFILE = 'server.pid';
|
|
|
8
9
|
* How long detectServer waits for the `/health` liveness probe before
|
|
9
10
|
* treating the pidfile as stale. Short by design: the probe only fires on
|
|
10
11
|
* the rare path where a pidfile exists and its pid is live, and the target
|
|
11
|
-
* is always loopback, so a healthy server answers well within this bound.
|
|
12
|
+
* is always loopback, so a healthy server answers well within this bound. HIPPO_HEALTH_PROBE_MS overrides it.
|
|
12
13
|
*/
|
|
13
14
|
const HEALTH_PROBE_TIMEOUT_MS = 300;
|
|
14
15
|
/**
|
|
@@ -84,7 +85,7 @@ async function healthMatchesPidfile(hippoRoot, info) {
|
|
|
84
85
|
// returns null WITHOUT unlinking. The pidfile survives for the next probe.
|
|
85
86
|
try {
|
|
86
87
|
const res = await fetch(`${info.url}/health`, {
|
|
87
|
-
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),
|
|
88
|
+
signal: AbortSignal.timeout(envHealthProbeMs() ?? HEALTH_PROBE_TIMEOUT_MS),
|
|
88
89
|
});
|
|
89
90
|
if (!res.ok || !res.body) {
|
|
90
91
|
removePidfile(hippoRoot);
|
package/dist/server.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ export { HttpError } from './http-util.js';
|
|
|
11
11
|
export { promptHookContext, type CallerProject } from './prompt-hook.js';
|
|
12
12
|
export type { JsonValue } from './json.js';
|
|
13
13
|
export { captureSessionTexts, type SessionCaptureRequest, type SessionCaptureResult } from './capture/session-texts.js';
|
|
14
|
-
export { hasGroup, sqliteStore, type HippoStore, type KeyAudit, type KeyListQuery, type KeyMint, type KeyRevoke, type KeyWrites, type RecallSearchArgs, type RecallWrites, type SelfKeyMint, type StoreGroup, type StoreGroups, type VectorReads, type VectorBackfillQuery, type VectorRowWrite, type VectorWrite, type VectorWriteResult, type VectorWrites, } from './store-port.js';
|
|
14
|
+
export { hasGroup, sqliteStore, type AmbientCandidateRequest, type ContextReads, type EntryRemoval, type EntryTarget, type EntryWrite, type EntryWrites, type HippoStore, type KeyAudit, type KeyListQuery, type KeyMint, type KeyRevoke, type KeyWrites, type OutcomeWrite, type RawArchive, type RecallSearchArgs, type RecallWrites, type SelfKeyMint, type StoreGroup, type StoreGroups, type SupersedeWrite, type VectorReads, type VectorBackfillQuery, type VectorRowWrite, type VectorWrite, type VectorWriteResult, type VectorWrites, } from './store-port.js';
|
|
15
|
+
export { entryAfterOutcome } from './memory.js';
|
|
16
|
+
export { rejectionDigest, RejectedValueError } from './rejection.js';
|
|
17
|
+
export { ownScopeTouches } from './recall-scope.js';
|
|
18
|
+
export { BadRequestError, ConflictError } from './api-errors.js';
|
|
15
19
|
export type { HippoDbContext, StoreReply } from './api/types.js';
|
|
16
20
|
export type { ApiKeyListItem, ApiKeyListRow, ApiKeyRecord, ListApiKeysOpts, NewApiKey } from './auth.js';
|
|
17
21
|
export type { KeysetPosition } from './keyset.js';
|
|
@@ -25,6 +29,11 @@ export type { RecallTraceInput } from './recall-trace.js';
|
|
|
25
29
|
export type { StrengthenOptions } from './store/entry-writes.js';
|
|
26
30
|
export type { OriginFilter, RecallScopeFilter, VectorCandidateSpec } from './store/search-rows.js';
|
|
27
31
|
export type { ContinuityKey } from './store/sessions.js';
|
|
32
|
+
export type { SessionEvent, TaskSnapshot } from './store/rows.js';
|
|
33
|
+
export type { SessionHandoff } from './handoff.js';
|
|
34
|
+
export type { AmbientLoadResult, AmbientRecallRequest, ContextCandidateFilter, RecentOrigins } from './store/candidates.js';
|
|
35
|
+
export type { AmbientStoreFilter } from './ambient-store.js';
|
|
36
|
+
export type { AmbientTallies } from './ambient.js';
|
|
28
37
|
export type { TokenUse } from './token-ledger.js';
|
|
29
38
|
export type { EmbeddingIndexState } from './embeddings.js';
|
|
30
39
|
export type { PhysicsParticle } from './physics.js';
|
|
@@ -32,6 +41,10 @@ export { StoreBusyError } from './db.js';
|
|
|
32
41
|
export { decodeVector, EMBEDDING_MODEL_META_KEY, encodeVector, rankVectorRows, type VectorMatch, type VectorRow } from './vector-store.js';
|
|
33
42
|
export { bufferToFloat32, float32ToBuffer } from './physics-state.js';
|
|
34
43
|
export { replacesIndex } from './embeddings.js';
|
|
44
|
+
export { passesScopeFilterForRecall, RECALL_DEFAULT_DENY_SCOPES } from './recall-scope.js';
|
|
45
|
+
export { SECRET_TAGS } from './secret-detect.js';
|
|
46
|
+
export { tallyAmbientEntries } from './ambient.js';
|
|
47
|
+
export { ftsTermParts, rarestFtsQuery } from './prompt-recall.js';
|
|
35
48
|
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
36
49
|
export { authCreate, type AuthCreateOpts, type AuthCreateResult } from './api.js';
|
|
37
50
|
export { isSharedStore } from './config.js';
|
package/dist/server.js
CHANGED
|
@@ -47,12 +47,22 @@ export { promptHookContext } from './prompt-hook.js';
|
|
|
47
47
|
export { captureSessionTexts } from './capture/session-texts.js';
|
|
48
48
|
// An add-on serves from another database by passing serve() its own HippoStore.
|
|
49
49
|
export { hasGroup, sqliteStore, } from './store-port.js';
|
|
50
|
+
// An add-on store's entry writes apply an outcome, guard tombstones and check reach exactly as hippo.db does.
|
|
51
|
+
export { entryAfterOutcome } from './memory.js';
|
|
52
|
+
export { rejectionDigest, RejectedValueError } from './rejection.js';
|
|
53
|
+
export { ownScopeTouches } from './recall-scope.js';
|
|
54
|
+
export { BadRequestError, ConflictError } from './api-errors.js';
|
|
50
55
|
export { StoreBusyError } from './db.js';
|
|
51
56
|
// An add-on store encodes, decodes and ranks vectors and particles with hippo.db's own code, and drops the index by its rule,
|
|
52
57
|
// so both stores keep the same bytes and return the same ids in the same order.
|
|
53
58
|
export { decodeVector, EMBEDDING_MODEL_META_KEY, encodeVector, rankVectorRows } from './vector-store.js';
|
|
54
59
|
export { bufferToFloat32, float32ToBuffer } from './physics-state.js';
|
|
55
60
|
export { replacesIndex } from './embeddings.js';
|
|
61
|
+
// An add-on's ContextReads applies hippo.db's scope, secret, tally and rarest-term rules with the same code.
|
|
62
|
+
export { passesScopeFilterForRecall, RECALL_DEFAULT_DENY_SCOPES } from './recall-scope.js';
|
|
63
|
+
export { SECRET_TAGS } from './secret-detect.js';
|
|
64
|
+
export { tallyAmbientEntries } from './ambient.js';
|
|
65
|
+
export { ftsTermParts, rarestFtsQuery } from './prompt-recall.js';
|
|
56
66
|
// store copy --db writes the marker and reads the old hippo.db under the waiver.
|
|
57
67
|
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
58
68
|
// An add-on's install step mints the first admin key into a store folder it names, which `hippo auth create` cannot reach.
|
|
@@ -98,17 +108,17 @@ const VERSION = PACKAGE_VERSION;
|
|
|
98
108
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
99
109
|
/** The /v1 routes in dispatch order; the first entry whose method and path match handles the request. */
|
|
100
110
|
const V1_ROUTES = [
|
|
101
|
-
{ method: 'POST', path: '/v1/memories', handler: handleCreateMemory },
|
|
111
|
+
{ method: 'POST', path: '/v1/memories', storeReady: 'entryWrites', handler: handleCreateMemory },
|
|
102
112
|
{ method: 'GET', path: '/v1/graph', handler: handleGetGraph },
|
|
103
113
|
{ method: 'GET', path: '/v1/memories', storeReady: 'base', handler: handleRecallMemories },
|
|
104
114
|
{ method: 'GET', pattern: '/v1/sessions/:id/assemble', handler: handleAssembleSession },
|
|
105
115
|
{ method: 'GET', pattern: '/v1/recall/drill/:id', handler: handleDrillRecall },
|
|
106
|
-
{ method: 'POST', pattern: '/v1/memories/:id/archive', handler: handleArchiveMemory },
|
|
107
|
-
{ method: 'POST', pattern: '/v1/memories/:id/supersede', handler: handleSupersedeMemory },
|
|
116
|
+
{ method: 'POST', pattern: '/v1/memories/:id/archive', storeReady: 'entryWrites', handler: handleArchiveMemory },
|
|
117
|
+
{ method: 'POST', pattern: '/v1/memories/:id/supersede', storeReady: 'entryWrites', handler: handleSupersedeMemory },
|
|
108
118
|
{ method: 'POST', pattern: '/v1/memories/:id/promote', handler: handlePromoteMemory },
|
|
109
|
-
{ method: 'DELETE', pattern: '/v1/memories/:id', handler: handleForgetMemory },
|
|
110
|
-
{ method: 'POST', path: '/v1/outcome', handler: handleApplyOutcome },
|
|
111
|
-
{ method: 'GET', path: '/v1/context', handler: handleGetContext },
|
|
119
|
+
{ method: 'DELETE', pattern: '/v1/memories/:id', storeReady: 'entryWrites', handler: handleForgetMemory },
|
|
120
|
+
{ method: 'POST', path: '/v1/outcome', storeReady: 'entryWrites', handler: handleApplyOutcome },
|
|
121
|
+
{ method: 'GET', path: '/v1/context', storeReady: 'contextReads', handler: handleGetContext },
|
|
112
122
|
{ method: 'POST', path: '/v1/sleep', handler: handleSleep },
|
|
113
123
|
{ method: 'POST', path: '/v1/auth/keys', storeReady: 'keyWrites', handler: handleCreateAuthKey },
|
|
114
124
|
{ method: 'GET', path: '/v1/auth/keys', storeReady: 'keyWrites', handler: handleListAuthKeys },
|
|
@@ -11,7 +11,7 @@ export declare function audit(db: ReturnType<typeof openHippoDb>, op: AuditOp, t
|
|
|
11
11
|
/**
|
|
12
12
|
* Refusal audit for the rejected-value guard. Written by the
|
|
13
13
|
* transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
|
|
14
|
-
* there, so this lands in a fresh implicit transaction) and
|
|
14
|
+
* there, so this lands in a fresh implicit transaction) and commitSupersede's
|
|
15
15
|
* catch (after its own ROLLBACK) — never inside a scope the caller's own
|
|
16
16
|
* rollback could claw back. Best-effort `audit()` semantics: never throws.
|
|
17
17
|
*/
|
|
@@ -23,7 +23,7 @@ export function audit(db, op, targetId, metadata, actor = 'cli', tenantId) {
|
|
|
23
23
|
/**
|
|
24
24
|
* Refusal audit for the rejected-value guard. Written by the
|
|
25
25
|
* transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
|
|
26
|
-
* there, so this lands in a fresh implicit transaction) and
|
|
26
|
+
* there, so this lands in a fresh implicit transaction) and commitSupersede's
|
|
27
27
|
* catch (after its own ROLLBACK) — never inside a scope the caller's own
|
|
28
28
|
* rollback could claw back. Best-effort `audit()` semantics: never throws.
|
|
29
29
|
*/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
2
|
+
import type { EntryWrites, SupersedeWrite } from '../store-port.js';
|
|
3
|
+
/** Each call on its own handle; mirrors and the forgotten counter follow the commit, so a rolled-back write leaves neither. */
|
|
4
|
+
export declare function sqliteEntryWrites(hippoRoot: string): EntryWrites;
|
|
5
|
+
/** 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. */
|
|
6
|
+
export declare function commitSupersede(db: DatabaseSyncLike, write: SupersedeWrite): void;
|
|
7
|
+
/** 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. */
|
|
8
|
+
export declare function cleanArchivedMirrors(db: DatabaseSyncLike, hippoRoot: string, id: string): void;
|
|
9
|
+
//# sourceMappingURL=entry-writes-group.d.ts.map
|
|
@@ -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);
|
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;
|
package/dist/store-port.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import type { AmbientTallies } from './ambient.js';
|
|
2
|
+
import { type AmbientStoreFilter } from './ambient-store.js';
|
|
1
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';
|
|
6
9
|
import type { JsonValue } from './json.js';
|
|
7
10
|
import type { MemoryEntry } from './memory.js';
|
|
8
11
|
import type { PhysicsParticle } from './physics.js';
|
|
9
12
|
import { type PlanningFallacyEvidence } from './predictions/planning-fallacy.js';
|
|
10
13
|
import { type RecallTraceInput } from './recall-trace.js';
|
|
14
|
+
import { type AmbientLoadResult, type AmbientRecallRequest, type ContextCandidateFilter, type RecentOrigins } from './store/candidates.js';
|
|
11
15
|
import { type StrengthenOptions } from './store/entry-writes.js';
|
|
12
16
|
import { type OriginFilter, type VectorCandidateSpec } from './store/search-rows.js';
|
|
13
17
|
import { type ContinuityKey } from './store/sessions.js';
|
|
@@ -102,6 +106,71 @@ export interface KeyAudit {
|
|
|
102
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. */
|
|
103
107
|
auditHighId(): Promise<number>;
|
|
104
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
|
+
}
|
|
105
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. */
|
|
106
175
|
export interface KeyMint {
|
|
107
176
|
readonly key: NewApiKey;
|
|
@@ -139,6 +208,8 @@ export interface StoreGroups {
|
|
|
139
208
|
readonly keyWrites: KeyWrites;
|
|
140
209
|
/** embedMemory and embedAll with a store need it and `vectors`. */
|
|
141
210
|
readonly vectorWrites: VectorWrites;
|
|
211
|
+
readonly entryWrites: EntryWrites;
|
|
212
|
+
readonly contextReads: ContextReads;
|
|
142
213
|
}
|
|
143
214
|
export type StoreGroup = 'base' | keyof StoreGroups;
|
|
144
215
|
export declare function hasGroup(store: HippoStore, group: StoreGroup): boolean;
|