hippo-memory 1.63.2 → 1.64.1
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 +11 -5
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/maintenance.d.ts +2 -1
- package/dist/cli/maintenance.js +17 -11
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/embedding-provider.js +4 -3
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/local-embedding.d.ts +2 -0
- package/dist/local-embedding.js +20 -4
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -0
- 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 +8 -4
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// One record per Claude Code compaction, and what turns its summary into kept memories.
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { ConflictError } from './api-errors.js';
|
|
4
5
|
import { isObjectLike, isStringValue } from './capture-contract.js';
|
|
5
|
-
import { compactSummaryBody, parseCompactionItems, selectItemRows } from './compaction-items.js';
|
|
6
|
-
import {
|
|
6
|
+
import { COMPACTION_ITEM_MAX_CHARS, compactSummaryBody, parseCompactionItems, selectItemRows } from './compaction-items.js';
|
|
7
|
+
import { importSpool, spool } from './compaction-spool.js';
|
|
8
|
+
import { isSharedStore, loadConfig } from './config.js';
|
|
7
9
|
import { closeHippoDb, isSqliteBusy, openHippoDb } from './db.js';
|
|
8
10
|
import { gatedWrite } from './gated-write.js';
|
|
9
11
|
import { COMPACTION_MEMORY_TAG, COMPACTION_SOURCE_PREFIX, Layer, createMemory, generateId } from './memory.js';
|
|
10
|
-
import { isGlobalStoreRoot, originInSql, projectId, projectNames, resolveProjectIdentity } from './project-identity.js';
|
|
12
|
+
import { fallbackOrigin, isGlobalStoreRoot, originInSql, projectId, projectNames, resolveProjectIdentity } from './project-identity.js';
|
|
13
|
+
import { scopeAdmitSql } from './recall-scope.js';
|
|
11
14
|
import { maskEmails, redactSecretsStrict } from './secret-detect.js';
|
|
12
15
|
import { strengthenRetrievedOn, writeEntryMirrors } from './store/entry-writes.js';
|
|
13
16
|
import { isRecallBoostAblated } from './ablation.js';
|
|
@@ -25,17 +28,19 @@ export const REPLAY_AFTER_MS = 10 * 60_000;
|
|
|
25
28
|
/** Claude Code deletes transcripts after 30 days, so an older gap can never be filled. */
|
|
26
29
|
export const TRANSCRIPT_FILL_WINDOW_MS = 30 * 24 * 60 * 60_000;
|
|
27
30
|
const TRANSCRIPT_TAIL_CAPS = [1 << 20, 8 << 20, 64 << 20];
|
|
28
|
-
const SPOOL_DIR = 'compactions-spool';
|
|
29
|
-
const CLAIMED_SUFFIX = '.claimed';
|
|
30
31
|
/** The marker redactSecretsStrict writes; an item holding it was a secret before it was stored. */
|
|
31
32
|
const REDACTED = '[REDACTED]';
|
|
32
|
-
/** Where the session ran: rows written through the global store keep the project the session was in. */
|
|
33
|
+
/** Where the session ran: rows written through the global store or a shared store keep the project the session was in. */
|
|
33
34
|
function compactionProject(hippoRoot, cwd) {
|
|
34
|
-
if (!isGlobalStoreRoot(hippoRoot))
|
|
35
|
+
if (!isGlobalStoreRoot(hippoRoot) && !isSharedStore(hippoRoot))
|
|
35
36
|
return resolveProjectIdentity(path.dirname(hippoRoot));
|
|
36
37
|
// No cwd means user-global, as stampOriginProject gives the global store; undefined would fall back to the hook's own cwd.
|
|
37
38
|
return cwd === null ? '' : resolveProjectIdentity(cwd);
|
|
38
39
|
}
|
|
40
|
+
/** The record column cannot hold NULL, but an item from an unknown folder on a shared store must not read as user-global. */
|
|
41
|
+
function itemOrigin(hippoRoot, ctx) {
|
|
42
|
+
return ctx.cwd === null && isSharedStore(hippoRoot) ? fallbackOrigin(hippoRoot) : ctx.originProject;
|
|
43
|
+
}
|
|
39
44
|
function compactionOrigin(hippoRoot, cwd) {
|
|
40
45
|
return projectId(compactionProject(hippoRoot, cwd));
|
|
41
46
|
}
|
|
@@ -53,6 +58,10 @@ export function readCompactionText(compactSummary) {
|
|
|
53
58
|
const parsed = parseCompactionItems(body);
|
|
54
59
|
return { summary: truncateCodePointSafe(scrub(body), SUMMARY_MAX_CHARS), items: parsed.items.map(scrub), found: parsed.found };
|
|
55
60
|
}
|
|
61
|
+
/** A caller's items scrubbed as readCompactionText scrubs a summary's, since another machine's scrub is not trusted; cut after it, as a mask can run longer than what it hides. */
|
|
62
|
+
export function scrubCompactionItems(items) {
|
|
63
|
+
return items.map((item) => truncateCodePointSafe(scrub(item), COMPACTION_ITEM_MAX_CHARS));
|
|
64
|
+
}
|
|
56
65
|
function toRecord(row) {
|
|
57
66
|
const listed = row.items_json === null ? [] : JSON.parse(row.items_json);
|
|
58
67
|
return {
|
|
@@ -88,13 +97,24 @@ export function startCompaction(db, tenantId, start, at = new Date()) {
|
|
|
88
97
|
export function latestCompaction(db, tenantId, sessionId) {
|
|
89
98
|
return selectRecords(db, 'tenant_id = ? AND session_id = ? ORDER BY started_at DESC, id DESC LIMIT 1', tenantId, sessionId)[0] ?? null;
|
|
90
99
|
}
|
|
100
|
+
/** The record a caller's request made, whatever state it reached, so a retry neither writes its items twice nor starts a second record; another session's id is a ConflictError. */
|
|
101
|
+
export function compactionByRequest(db, tenantId, requestId, sessionId) {
|
|
102
|
+
const record = selectRecords(db, 'tenant_id = ? AND request_id = ?', tenantId, requestId)[0] ?? null;
|
|
103
|
+
// Sessions are owner-bound, so this also keeps one owner from reading or finishing another's record.
|
|
104
|
+
if (record !== null && record.sessionId !== sessionId)
|
|
105
|
+
throw new ConflictError('request id belongs to another session');
|
|
106
|
+
return record;
|
|
107
|
+
}
|
|
91
108
|
/** Pre-compact's record for the compaction that is ending: the session's newest `started` one within REPLAY_AFTER_MS before `at`, so an older one is left for the transcript fill. */
|
|
92
109
|
function latestStarted(db, tenantId, sessionId, at) {
|
|
93
110
|
return selectRecords(db, `tenant_id = ? AND session_id = ? AND status = 'started' AND started_at <= ? AND started_at >= ? ORDER BY started_at DESC, id DESC LIMIT 1`, tenantId, sessionId, at.toISOString(), new Date(at.getTime() - REPLAY_AFTER_MS).toISOString())[0] ?? null;
|
|
94
111
|
}
|
|
95
|
-
/** Moves a `started` record to `summarised`; false when another process already moved it. */
|
|
96
|
-
function markSummarised(db, tenantId, id, text, summarisedAt) {
|
|
97
|
-
|
|
112
|
+
/** Moves a `started` record to `summarised`; false when another process already moved it. The request id lands in the same statement, so no crash leaves the record unfindable by its retry. */
|
|
113
|
+
function markSummarised(db, tenantId, id, text, summarisedAt, requestId) {
|
|
114
|
+
// Only a caller names the column, so a store from before it was added still takes local writes.
|
|
115
|
+
const stamp = requestId === undefined ? [] : [requestId];
|
|
116
|
+
const result = db.prepare(`UPDATE compactions SET summary = ?, items_json = ?, summarised_at = ?, status = 'summarised'${stamp.length === 0 ? '' : ', request_id = ?'}
|
|
117
|
+
WHERE tenant_id = ? AND id = ? AND status = 'started'`).run(text.summary, JSON.stringify(text.items), summarisedAt, ...stamp, tenantId, id);
|
|
98
118
|
return (result.changes ?? 0) > 0;
|
|
99
119
|
}
|
|
100
120
|
/** Best effort, never throws: a compaction must not fail because its record could not be written. */
|
|
@@ -113,11 +133,15 @@ export function recordCompactionStart(hippoRoot, start, log) {
|
|
|
113
133
|
closeHippoDb(db);
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
|
-
|
|
136
|
+
/** On the caller's handle and tenant, so a server marks the record under the caller's tenant with its own wait. */
|
|
137
|
+
export function markSnapshotSaved(db, tenantId, recordId) {
|
|
138
|
+
db.prepare(`UPDATE compactions SET snapshot_saved = 1 WHERE tenant_id = ? AND id = ?`).run(tenantId, recordId);
|
|
139
|
+
}
|
|
140
|
+
export function recordSnapshotSaved(hippoRoot, tenantId, recordId, log) {
|
|
117
141
|
let db;
|
|
118
142
|
try {
|
|
119
143
|
db = openHippoDb(hippoRoot, { busyWaitMs: COMPACTION_DB_WAIT_MS });
|
|
120
|
-
db
|
|
144
|
+
markSnapshotSaved(db, tenantId, recordId);
|
|
121
145
|
}
|
|
122
146
|
catch (err) {
|
|
123
147
|
log(`compaction record not marked with its snapshot: ${errorMessage(err)}`);
|
|
@@ -128,17 +152,21 @@ export function recordSnapshotSaved(hippoRoot, recordId, log) {
|
|
|
128
152
|
}
|
|
129
153
|
}
|
|
130
154
|
/** Puts the summary on the session's `started` record, or inserts a `summarised` one when pre-compact wrote none. One statement each. */
|
|
131
|
-
function recordSummary(db, hippoRoot, tenantId, meta, text, at) {
|
|
155
|
+
export function recordSummary(db, hippoRoot, tenantId, meta, text, at, caller = {}) {
|
|
132
156
|
const now = new Date().toISOString();
|
|
133
157
|
const itemsJson = JSON.stringify(text.items);
|
|
158
|
+
const { requestId } = caller;
|
|
134
159
|
const started = latestStarted(db, tenantId, meta.sessionId, at);
|
|
135
|
-
if (started && markSummarised(db, tenantId, started.id, text, now)) {
|
|
160
|
+
if (started && markSummarised(db, tenantId, started.id, text, now, requestId)) {
|
|
136
161
|
return { ...started, summary: text.summary, items: text.items, summarisedAt: now, status: 'summarised' };
|
|
137
162
|
}
|
|
138
|
-
const originProject = compactionOrigin(hippoRoot, meta.cwd);
|
|
163
|
+
const originProject = caller.originProject ?? compactionOrigin(hippoRoot, meta.cwd);
|
|
139
164
|
const id = generateId('cmp');
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
// Only a caller names the column, so a store from before it was added still takes local writes.
|
|
166
|
+
const stamp = requestId === undefined ? [] : [requestId];
|
|
167
|
+
const [col, mark] = stamp.length === 0 ? ['', ''] : [', request_id', ', ?'];
|
|
168
|
+
db.prepare(`INSERT INTO compactions(tenant_id, id, session_id, origin_project, compact_trigger, cwd, transcript_path, started_at, summarised_at, summary, items_json, status${col})
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'summarised'${mark})`).run(tenantId, id, meta.sessionId, originProject, meta.trigger, meta.cwd, meta.transcriptPath, at.toISOString(), now, text.summary, itemsJson, ...stamp);
|
|
142
170
|
return {
|
|
143
171
|
id, tenantId, sessionId: meta.sessionId, originProject, trigger: meta.trigger, cwd: meta.cwd, transcriptPath: meta.transcriptPath,
|
|
144
172
|
snapshotSaved: false, startedAt: at.toISOString(), summarisedAt: now, summary: text.summary, items: text.items, itemsWritten: 0, status: 'summarised',
|
|
@@ -167,12 +195,13 @@ function restates(item, held) {
|
|
|
167
195
|
}
|
|
168
196
|
/** Live rows of one tenant and origin that default recall shows: a compaction often restates what an earlier one, or the user, already saved. */
|
|
169
197
|
function heldRows(db, tenantId, origins) {
|
|
198
|
+
const deny = scopeAdmitSql('');
|
|
170
199
|
// SAFETY: the SELECT names the id, source_session_id and content columns.
|
|
171
200
|
const rows = db.prepare(`SELECT id, source_session_id, content FROM memories WHERE tenant_id = ? AND ${originInSql(origins)} AND superseded_by IS NULL AND kind != 'raw'
|
|
172
|
-
AND
|
|
201
|
+
AND ${deny.sql}`).all(tenantId, ...origins, ...deny.params);
|
|
173
202
|
return rows.map((r) => ({ id: r.id, sessionId: r.source_session_id, words: words(r.content) }));
|
|
174
203
|
}
|
|
175
|
-
function compactionEntry(text, ctx, baseHalfLifeDays) {
|
|
204
|
+
function compactionEntry(text, ctx, origin, baseHalfLifeDays) {
|
|
176
205
|
return {
|
|
177
206
|
...createMemory(text, {
|
|
178
207
|
layer: Layer.Episodic,
|
|
@@ -184,13 +213,14 @@ function compactionEntry(text, ctx, baseHalfLifeDays) {
|
|
|
184
213
|
tenantId: ctx.tenantId,
|
|
185
214
|
baseHalfLifeDays,
|
|
186
215
|
}),
|
|
187
|
-
origin_project:
|
|
216
|
+
origin_project: origin,
|
|
188
217
|
};
|
|
189
218
|
}
|
|
190
219
|
/** Runs inside saveItems' transaction: restatements of held rows are counted, the rest go through the write gate. */
|
|
191
220
|
function writeItemRows(db, hippoRoot, ctx, rows, baseHalfLifeDays) {
|
|
192
221
|
const out = { written: [], repeats: 0, refused: 0, restated: [] };
|
|
193
|
-
const held = heldRows(db, ctx.tenantId, heldOrigins(hippoRoot, ctx.cwd, ctx.originProject));
|
|
222
|
+
const held = heldRows(db, ctx.tenantId, ctx.caller?.origins ?? heldOrigins(hippoRoot, ctx.cwd, ctx.originProject));
|
|
223
|
+
const origin = ctx.caller === undefined ? itemOrigin(hippoRoot, ctx) : ctx.originProject;
|
|
194
224
|
const restated = new Set();
|
|
195
225
|
for (const text of rows) {
|
|
196
226
|
const itemWords = words(text);
|
|
@@ -206,8 +236,8 @@ function writeItemRows(db, hippoRoot, ctx, rows, baseHalfLifeDays) {
|
|
|
206
236
|
out.refused++;
|
|
207
237
|
continue;
|
|
208
238
|
}
|
|
209
|
-
const entry = compactionEntry(text, ctx, baseHalfLifeDays);
|
|
210
|
-
if (gatedWrite(db, hippoRoot, entry, { actor: 'post-compact' }) === 'written') {
|
|
239
|
+
const entry = compactionEntry(text, ctx, origin, baseHalfLifeDays);
|
|
240
|
+
if (gatedWrite(db, hippoRoot, entry, { actor: ctx.caller?.actor ?? 'post-compact' }) === 'written') {
|
|
211
241
|
out.written.push(entry);
|
|
212
242
|
held.push({ id: null, sessionId: ctx.sessionId, words: itemWords });
|
|
213
243
|
}
|
|
@@ -297,17 +327,6 @@ export function parsePostCompactPayload(stdinText) {
|
|
|
297
327
|
compactSummary: 'compact_summary' in raw && isStringValue(raw.compact_summary) ? raw.compact_summary : null,
|
|
298
328
|
};
|
|
299
329
|
}
|
|
300
|
-
function spoolFile(hippoRoot, sessionId) {
|
|
301
|
-
return path.join(hippoRoot, SPOOL_DIR, `${sessionId.replace(/[^\w-]/g, '_').slice(0, 80)}-${Date.now()}.json`);
|
|
302
|
-
}
|
|
303
|
-
function spool(hippoRoot, tenantId, payload, text, at) {
|
|
304
|
-
const file = spoolFile(hippoRoot, payload.sessionId);
|
|
305
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
306
|
-
const body = { tenantId, sessionId: payload.sessionId, trigger: payload.trigger, cwd: payload.cwd, transcriptPath: payload.transcriptPath, at: at.toISOString(), summary: text.summary, items: text.items };
|
|
307
|
-
// Renamed into place so a replayer listing `.json` files never reads half a file.
|
|
308
|
-
fs.writeFileSync(`${file}.tmp`, JSON.stringify(body), 'utf8');
|
|
309
|
-
fs.renameSync(`${file}.tmp`, file);
|
|
310
|
-
}
|
|
311
330
|
/** The PostCompact work: record the summary, then write its items. Each step is independent; a busy store spools or defers. Never throws. */
|
|
312
331
|
export function saveCompaction(hippoRoot, payload, log) {
|
|
313
332
|
const result = { written: null, deferred: false, snapshotSaved: false };
|
|
@@ -427,105 +446,15 @@ function nextStartedAt(db, record) {
|
|
|
427
446
|
.get(record.tenantId, record.sessionId, record.startedAt);
|
|
428
447
|
return row?.at ?? null;
|
|
429
448
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
if (!isObjectLike(raw) || !('sessionId' in raw) || !isStringValue(raw.sessionId) || !('summary' in raw) || !isStringValue(raw.summary))
|
|
439
|
-
return null;
|
|
440
|
-
if (!('at' in raw) || !isStringValue(raw.at) || Number.isNaN(Date.parse(raw.at)))
|
|
441
|
-
return null;
|
|
442
|
-
const items = 'items' in raw && Array.isArray(raw.items) ? raw.items.filter(isStringValue) : [];
|
|
443
|
-
return {
|
|
444
|
-
tenantId: 'tenantId' in raw && isStringValue(raw.tenantId) && raw.tenantId !== '' ? raw.tenantId : fallbackTenantId,
|
|
445
|
-
payload: {
|
|
446
|
-
sessionId: raw.sessionId,
|
|
447
|
-
trigger: 'trigger' in raw && isStringValue(raw.trigger) ? raw.trigger : null,
|
|
448
|
-
cwd: 'cwd' in raw && isStringValue(raw.cwd) ? raw.cwd : null,
|
|
449
|
-
transcriptPath: 'transcriptPath' in raw && isStringValue(raw.transcriptPath) ? raw.transcriptPath : null,
|
|
450
|
-
compactSummary: null,
|
|
451
|
-
},
|
|
452
|
-
text: { summary: raw.summary, items },
|
|
453
|
-
at: new Date(raw.at),
|
|
449
|
+
/** Records a spooled summary, then writes its items. */
|
|
450
|
+
function spoolImporter(db, hippoRoot, log) {
|
|
451
|
+
return (spooled, recorded) => {
|
|
452
|
+
const record = recordSummary(db, hippoRoot, spooled.tenantId, spooled.payload, spooled.text, spooled.at);
|
|
453
|
+
// The record holds the items now, so the file is done even if the write below fails.
|
|
454
|
+
recorded();
|
|
455
|
+
saveItems(db, hippoRoot, { tenantId: spooled.tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, cwd: record.cwd, items: record.items }, log);
|
|
454
456
|
};
|
|
455
457
|
}
|
|
456
|
-
function isMissingFile(cause) {
|
|
457
|
-
return cause instanceof Error && 'code' in cause && cause.code === 'ENOENT';
|
|
458
|
-
}
|
|
459
|
-
/** A live replayer refreshes its claim's mtime when it takes it, so an old claim means the replayer died. */
|
|
460
|
-
function recoverStaleClaims(dir, log) {
|
|
461
|
-
const staleBefore = Date.now() - REPLAY_AFTER_MS;
|
|
462
|
-
for (const name of fs.readdirSync(dir).filter((n) => n.endsWith(`.json${CLAIMED_SUFFIX}`))) {
|
|
463
|
-
const claimed = path.join(dir, name);
|
|
464
|
-
try {
|
|
465
|
-
if (fs.statSync(claimed).mtimeMs >= staleBefore)
|
|
466
|
-
continue;
|
|
467
|
-
fs.renameSync(claimed, claimed.slice(0, -CLAIMED_SUFFIX.length));
|
|
468
|
-
log(`spool file ${name} was claimed by a replayer that never finished, put back`);
|
|
469
|
-
}
|
|
470
|
-
catch (err) {
|
|
471
|
-
if (!isMissingFile(err))
|
|
472
|
-
log(`spool file ${name} not recovered: ${errorMessage(err)}`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
function releaseClaim(claimed, file, log) {
|
|
477
|
-
try {
|
|
478
|
-
fs.renameSync(claimed, file);
|
|
479
|
-
}
|
|
480
|
-
catch (err) {
|
|
481
|
-
log(`spool file ${path.basename(file)} could not be put back: ${errorMessage(err)}`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
/** Each file is claimed by rename before it is read, so two replayers never import the same one. */
|
|
485
|
-
function importSpool(db, hippoRoot, tenantId, log, deadline) {
|
|
486
|
-
const dir = path.join(hippoRoot, SPOOL_DIR);
|
|
487
|
-
if (!fs.existsSync(dir))
|
|
488
|
-
return 0;
|
|
489
|
-
recoverStaleClaims(dir, log);
|
|
490
|
-
let finished = 0;
|
|
491
|
-
for (const name of fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort()) {
|
|
492
|
-
if (Date.now() > deadline)
|
|
493
|
-
break;
|
|
494
|
-
const file = path.join(dir, name);
|
|
495
|
-
const claimed = `${file}${CLAIMED_SUFFIX}`;
|
|
496
|
-
try {
|
|
497
|
-
fs.renameSync(file, claimed);
|
|
498
|
-
const now = new Date();
|
|
499
|
-
fs.utimesSync(claimed, now, now);
|
|
500
|
-
}
|
|
501
|
-
catch (err) {
|
|
502
|
-
if (!isMissingFile(err))
|
|
503
|
-
log(`spool file ${name} not claimed: ${errorMessage(err)}`);
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
const spooled = readSpooled(claimed, tenantId);
|
|
507
|
-
if (!spooled) {
|
|
508
|
-
log(`spool file ${name} is not readable, set aside`);
|
|
509
|
-
fs.renameSync(claimed, `${file}.bad`);
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
let removed = false;
|
|
513
|
-
try {
|
|
514
|
-
const record = recordSummary(db, hippoRoot, spooled.tenantId, spooled.payload, spooled.text, spooled.at);
|
|
515
|
-
// The record holds the items now, so the file is done even if the write below fails.
|
|
516
|
-
fs.rmSync(claimed, { force: true });
|
|
517
|
-
removed = true;
|
|
518
|
-
saveItems(db, hippoRoot, { tenantId: spooled.tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, cwd: record.cwd, items: record.items }, log);
|
|
519
|
-
finished++;
|
|
520
|
-
}
|
|
521
|
-
catch (err) {
|
|
522
|
-
log(`spool file ${name} not imported: ${errorMessage(err)}`);
|
|
523
|
-
if (!removed)
|
|
524
|
-
releaseClaim(claimed, file, log);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return finished;
|
|
528
|
-
}
|
|
529
458
|
/** Finishes what a killed hook or a busy store left: `summarised` records, spool files, and `started` records the transcript can fill. Returns how many compactions it saved. */
|
|
530
459
|
export function replayCompactions(db, hippoRoot, log, deadline = Number.POSITIVE_INFINITY) {
|
|
531
460
|
const tenantId = resolveTenantId({});
|
|
@@ -544,7 +473,7 @@ export function replayCompactions(db, hippoRoot, log, deadline = Number.POSITIVE
|
|
|
544
473
|
}
|
|
545
474
|
}
|
|
546
475
|
try {
|
|
547
|
-
finished += importSpool(
|
|
476
|
+
finished += importSpool(hippoRoot, tenantId, log, deadline, spoolImporter(db, hippoRoot, log));
|
|
548
477
|
}
|
|
549
478
|
catch (err) {
|
|
550
479
|
log(`spool import failed: ${errorMessage(err)}`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CompactionText, Log, PostCompactPayload } from './compaction-record.js';
|
|
2
|
+
export declare const SPOOL_DIR = "compactions-spool";
|
|
3
|
+
/** Starts the log line of a step that broke with an unexpected error, so the session-start banner counts it; a set-aside file is counted by its `.bad` name instead. */
|
|
4
|
+
export declare const SPOOL_PROBLEM = "spool problem: ";
|
|
5
|
+
/** Only the fs calls the spool makes, so a test can stand in for one of them. */
|
|
6
|
+
export interface SpoolFs {
|
|
7
|
+
existsSync(file: string): boolean;
|
|
8
|
+
mkdirSync(dir: string, options: {
|
|
9
|
+
recursive: true;
|
|
10
|
+
}): void;
|
|
11
|
+
readdirSync(dir: string): string[];
|
|
12
|
+
readFileSync(file: string, encoding: 'utf8'): string;
|
|
13
|
+
renameSync(from: string, to: string): void;
|
|
14
|
+
statSync(file: string): {
|
|
15
|
+
mtimeMs: number;
|
|
16
|
+
};
|
|
17
|
+
unlinkSync(file: string): void;
|
|
18
|
+
writeFileSync(file: string, data: string, options: {
|
|
19
|
+
encoding: 'utf8';
|
|
20
|
+
flag?: 'wx';
|
|
21
|
+
}): void;
|
|
22
|
+
}
|
|
23
|
+
/** Test-only seam, so a test can make one fs call fail; null restores the real fs. */
|
|
24
|
+
export declare function __setSpoolFs(next: SpoolFs | null): void;
|
|
25
|
+
export interface SpooledCompaction {
|
|
26
|
+
tenantId: string;
|
|
27
|
+
payload: PostCompactPayload;
|
|
28
|
+
text: CompactionText;
|
|
29
|
+
at: Date;
|
|
30
|
+
}
|
|
31
|
+
/** Records one spooled compaction; it calls `recorded` once the store holds the summary, so the file is done even if a later step throws. */
|
|
32
|
+
export type SpoolImporter = (spooled: SpooledCompaction, recorded: () => void) => void;
|
|
33
|
+
export declare function spool(hippoRoot: string, tenantId: string, payload: PostCompactPayload, text: CompactionText, at: Date): void;
|
|
34
|
+
/** null when the text is not a spooled compaction. */
|
|
35
|
+
export declare function parseSpooled(text: string, fallbackTenantId: string): SpooledCompaction | null;
|
|
36
|
+
export interface SpoolCounts {
|
|
37
|
+
waiting: number;
|
|
38
|
+
stale: number;
|
|
39
|
+
bad: number;
|
|
40
|
+
}
|
|
41
|
+
/** What doctor reports, read only: files a replay should have taken by now, claims and tmp files a dead process left, and `.bad` files. */
|
|
42
|
+
export declare function spoolCounts(hippoRoot: string, now: Date, waitingAfterMs: number): SpoolCounts;
|
|
43
|
+
/** One replayer at a time holds `replay.lock`, so only one process claims a file even where Windows lets two renames of it both land. */
|
|
44
|
+
export declare function importSpool(hippoRoot: string, tenantId: string, log: Log, deadline: number, importOne: SpoolImporter): number;
|
|
45
|
+
//# sourceMappingURL=compaction-spool.d.ts.map
|