hippo-memory 1.63.2 → 1.64.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 +10 -4
- 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/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/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/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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Components that share no id, ordered by each one's first op across writes, then deletes, then dormant moves. */
|
|
2
|
+
export function groupFlush(writes, deletes, dormant, units) {
|
|
3
|
+
const find = unionFind(units);
|
|
4
|
+
const byRoot = new Map();
|
|
5
|
+
const componentOf = (id) => {
|
|
6
|
+
const root = find(id);
|
|
7
|
+
let component = byRoot.get(root);
|
|
8
|
+
if (!component)
|
|
9
|
+
byRoot.set(root, (component = { writes: [], deletes: [], dormant: [] }));
|
|
10
|
+
return component;
|
|
11
|
+
};
|
|
12
|
+
for (const entry of writes)
|
|
13
|
+
componentOf(entry.id).writes.push(entry);
|
|
14
|
+
for (const id of deletes)
|
|
15
|
+
componentOf(id).deletes.push(id);
|
|
16
|
+
for (const move of dormant)
|
|
17
|
+
componentOf(move.entry.id).dormant.push(move);
|
|
18
|
+
// A row queued twice keeps its first place and its last version, as one batch always did.
|
|
19
|
+
return [...byRoot.values()].map((c) => ({ ...c, writes: [...new Map(c.writes.map((e) => [e.id, e])).values()] }));
|
|
20
|
+
}
|
|
21
|
+
/** Maps each id to its unit's representative; an id in no unit is its own. */
|
|
22
|
+
function unionFind(units) {
|
|
23
|
+
const up = new Map();
|
|
24
|
+
const find = (id) => {
|
|
25
|
+
let root = id;
|
|
26
|
+
for (let next = up.get(root); next !== undefined && next !== root; next = up.get(root))
|
|
27
|
+
root = next;
|
|
28
|
+
if (root !== id)
|
|
29
|
+
up.set(id, root);
|
|
30
|
+
return root;
|
|
31
|
+
};
|
|
32
|
+
for (const unit of units) {
|
|
33
|
+
const head = unit[0];
|
|
34
|
+
if (head === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
for (const id of unit.slice(1)) {
|
|
37
|
+
const a = find(head);
|
|
38
|
+
const b = find(id);
|
|
39
|
+
if (a !== b)
|
|
40
|
+
up.set(b, a);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return find;
|
|
44
|
+
}
|
|
45
|
+
/** [child, parent] for each queued op on a child whose DAG parent the run removes: a child's change marks the parent
|
|
46
|
+
* dirty, and one transaction always applied that mark after the parent was gone, so it audited nothing. */
|
|
47
|
+
// SHORTCUT: a removed parent and all its changed children commit as one component, so one hold can pass holdMs by a family's size; split by child if a family grows large.
|
|
48
|
+
export function familyUnits(writes, deletes, dormant, snapshot) {
|
|
49
|
+
const removing = new Set([...deletes, ...dormant.map((m) => m.entry.id)]);
|
|
50
|
+
const units = [];
|
|
51
|
+
const link = (id, parentId) => {
|
|
52
|
+
if (parentId && removing.has(parentId))
|
|
53
|
+
units.push([id, parentId]);
|
|
54
|
+
};
|
|
55
|
+
for (const entry of writes)
|
|
56
|
+
link(entry.id, entry.dag_parent_id);
|
|
57
|
+
for (const id of deletes)
|
|
58
|
+
link(id, snapshot.get(id)?.dag_parent_id);
|
|
59
|
+
for (const move of dormant)
|
|
60
|
+
link(move.entry.id, move.entry.dag_parent_id);
|
|
61
|
+
return units;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=flush-units.js.map
|
|
@@ -2,6 +2,7 @@ import { envAnthropicApiKey, envDagRebuildCap } from '../env.js';
|
|
|
2
2
|
import { Layer } from '../memory.js';
|
|
3
3
|
import { log } from '../log.js';
|
|
4
4
|
import { keptAsWritten } from './run.js';
|
|
5
|
+
import { isReusable } from '../memory-quality.js';
|
|
5
6
|
/** The key, model options and once-per-line error reporter every LLM phase shares. */
|
|
6
7
|
function sleepLlm(run, fetcher) {
|
|
7
8
|
const { config, result } = run;
|
|
@@ -24,7 +25,8 @@ export async function llmPasses(run, fetcher) {
|
|
|
24
25
|
// 1.6. Batch extraction — extract facts from episodic memories missing them
|
|
25
26
|
// -------------------------------------------------------------------------
|
|
26
27
|
const extractedFromIds = new Set(run.survivors.filter((e) => e.extracted_from).map((e) => e.extracted_from));
|
|
27
|
-
const extractionCandidates = run.survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !extractedFromIds.has(e.id) && !keptAsWritten(e)
|
|
28
|
+
const extractionCandidates = run.survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !extractedFromIds.has(e.id) && !keptAsWritten(e)
|
|
29
|
+
&& e.trace_outcome === null && e.source !== 'auto-promote' && isReusable(e));
|
|
28
30
|
run.result.extractionCandidates = extractionCandidates.length;
|
|
29
31
|
const llm = sleepLlm(run, fetcher);
|
|
30
32
|
if (llm.apiKey && extractionCandidates.length > 0 && !run.dryRun) {
|
|
@@ -35,8 +37,7 @@ export async function llmPasses(run, fetcher) {
|
|
|
35
37
|
try {
|
|
36
38
|
const facts = await extractFacts(candidate.content, { ...llm.llmOpts, onError: llm.llmError('extraction') });
|
|
37
39
|
if (facts.length > 0) {
|
|
38
|
-
storeExtractedFacts(run.hippoRoot, candidate, facts);
|
|
39
|
-
extractedCount += facts.length;
|
|
40
|
+
extractedCount += storeExtractedFacts(run.hippoRoot, candidate, facts).length;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
catch (err) {
|
|
@@ -55,7 +56,7 @@ export async function llmPasses(run, fetcher) {
|
|
|
55
56
|
// 1.7. DAG summarization — cluster extracted facts and generate summaries
|
|
56
57
|
// -------------------------------------------------------------------------
|
|
57
58
|
async function dagBuildPass(run, { apiKey, llmError, llmOpts }) {
|
|
58
|
-
const extractedFacts = run.survivors.filter((e) => e.tags.includes('extracted') && e.dag_level === 1 && !e.superseded_by);
|
|
59
|
+
const extractedFacts = run.survivors.filter((e) => e.tags.includes('extracted') && e.dag_level === 1 && !e.superseded_by && isReusable(e));
|
|
59
60
|
if (!(apiKey && extractedFacts.length >= 3 && !run.dryRun))
|
|
60
61
|
return;
|
|
61
62
|
try {
|
|
@@ -9,6 +9,7 @@ import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
|
9
9
|
import { derivationScope, derivationPartitionKey } from '../recall-scope.js';
|
|
10
10
|
import { jaccardSets } from './conflicts.js';
|
|
11
11
|
import { keptAsWritten } from './run.js';
|
|
12
|
+
import { isReusable } from '../memory-quality.js';
|
|
12
13
|
const MERGE_OVERLAP_THRESHOLD = 0.35; // Jaccard similarity for "related"
|
|
13
14
|
const MERGE_MIN_CLUSTER = 2; // minimum cluster size to merge
|
|
14
15
|
const MERGE_MAX_SOURCES = 5; // with MERGE_MAX_CHARS, keeps a merged row near 500 tokens, a third of the 1,500-token context budget
|
|
@@ -35,6 +36,7 @@ export function retireHeldTexts(run) {
|
|
|
35
36
|
if (run.dryRun)
|
|
36
37
|
continue;
|
|
37
38
|
run.pendingDeletes.push(row.id);
|
|
39
|
+
run.units.push(successor ? [row.id, successor.id] : [row.id]);
|
|
38
40
|
if (successor) {
|
|
39
41
|
run.pendingWrites.push(successor);
|
|
40
42
|
survivors[i] = successor;
|
|
@@ -52,7 +54,8 @@ export function mergePass(run) {
|
|
|
52
54
|
const alreadyMergedIds = new Set(run.survivors.flatMap((e) => e.parents));
|
|
53
55
|
const mergeCandidates = run.survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !keptAsWritten(e) && !alreadyMergedIds.has(e.id)
|
|
54
56
|
&& !e.pinned // a pin merged with a look-alike would read as one of two values
|
|
55
|
-
&& tokenize(e.content).length > 0
|
|
57
|
+
&& tokenize(e.content).length > 0 // two empty token sets overlap 1, so tokenless text would merge with any other
|
|
58
|
+
&& isReusable(e));
|
|
56
59
|
const used = new Set();
|
|
57
60
|
// Partition BEFORE the overlap loop so a cluster can never span tenants and merge cross-tenant content.
|
|
58
61
|
// Map keeps insertion order, so a single-tenant store gets one partition in the original order.
|
|
@@ -135,6 +138,7 @@ function mergeCluster(run, partition, cluster, related, used) {
|
|
|
135
138
|
result.details.push(` 🔀 merged ${cluster.length} episodic entries into semantic: "${mergedContent.slice(0, 60)}..."`);
|
|
136
139
|
if (!dryRun && semantic) {
|
|
137
140
|
run.pendingWrites.push(semantic);
|
|
141
|
+
run.units.push([semantic.id, ...cluster.map((e) => e.id)]);
|
|
138
142
|
result.semanticCreated++;
|
|
139
143
|
// Demote source episodics (they've been compressed into neocortex):
|
|
140
144
|
// scale half_life_days so they decay sooner while staying recoverable.
|
|
@@ -43,7 +43,7 @@ interface LazyDb {
|
|
|
43
43
|
export declare function lazyConsolidateDb(hippoRoot: string, dryRun: boolean): LazyDb;
|
|
44
44
|
/** Re-syncs the full-text index with `memories`; a store open that is already current no longer counts the two. */
|
|
45
45
|
export declare function syncFtsIndex(hippoRoot: string, dryRun: boolean, result: ConsolidationResult): void;
|
|
46
|
-
/** State every sleep stage reads or appends to; the pending lists are flushed in one transaction
|
|
46
|
+
/** State every sleep stage reads or appends to; the pending lists are flushed at the end, each of `units` whole in one transaction. */
|
|
47
47
|
export interface SleepRun {
|
|
48
48
|
hippoRoot: string;
|
|
49
49
|
now: Date;
|
|
@@ -58,6 +58,7 @@ export interface SleepRun {
|
|
|
58
58
|
pendingWrites: MemoryEntry[];
|
|
59
59
|
pendingDeletes: string[];
|
|
60
60
|
pendingDormant: DormantMove[];
|
|
61
|
+
units: string[][];
|
|
61
62
|
}
|
|
62
63
|
export declare function newConsolidationResult(dryRun: boolean): ConsolidationResult;
|
|
63
64
|
export {};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 2. Merge pass - find episodic entries with high text overlap, create semantic summaries
|
|
7
7
|
* 3. Stats tracking
|
|
8
8
|
*/
|
|
9
|
+
import { type WriteBudget } from '../write-budget.js';
|
|
9
10
|
import { type ConsolidationResult } from './run.js';
|
|
10
11
|
/**
|
|
11
12
|
* Run a full consolidation pass.
|
|
@@ -14,5 +15,6 @@ export declare function consolidate(hippoRoot: string, options?: {
|
|
|
14
15
|
dryRun?: boolean;
|
|
15
16
|
now?: Date;
|
|
16
17
|
fetcher?: typeof fetch;
|
|
18
|
+
budget?: WriteBudget;
|
|
17
19
|
}): Promise<ConsolidationResult>;
|
|
18
20
|
//# sourceMappingURL=sleep.d.ts.map
|
|
@@ -9,16 +9,19 @@
|
|
|
9
9
|
import { evalNow } from '../ablation.js';
|
|
10
10
|
import { canAutoDelete } from '../memory.js';
|
|
11
11
|
import { loadAllEntries } from '../store/entry-reads.js';
|
|
12
|
-
import {
|
|
12
|
+
import { batchWriteAndDeleteOn, memoriesBackingObjects, noteFailedUnit } from '../store/delete-and-batch.js';
|
|
13
|
+
import { openStore } from '../store/open.js';
|
|
13
14
|
import { appendConsolidationRun, loadSessionDecayContext, incrementSleepCount } from '../store/index-and-stats.js';
|
|
14
15
|
import { replaceDetectedConflicts } from '../store/conflicts.js';
|
|
15
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
16
|
-
import {
|
|
16
|
+
import { openHippoDb, closeHippoDb, SLEEP_DB_WAIT_MS } from '../db.js';
|
|
17
|
+
import { deleteExpiredDormantRow, expiredDormantKeys } from '../dormant.js';
|
|
17
18
|
import { loadConfig } from '../config.js';
|
|
18
19
|
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
19
20
|
import { migrateDefaultHalfLife, LEGACY_TYPED_HALF_LIFE } from '../half-life-migration.js';
|
|
20
21
|
import { log } from '../log.js';
|
|
22
|
+
import { WRITE_BUDGET } from '../write-budget.js';
|
|
21
23
|
import { decayPass } from './decay.js';
|
|
24
|
+
import { familyUnits, groupFlush } from './flush-units.js';
|
|
22
25
|
import { retireHeldTexts, mergePass } from './merge.js';
|
|
23
26
|
import { detectConflicts } from './conflicts.js';
|
|
24
27
|
import { lazyConsolidateDb, newConsolidationResult, syncFtsIndex } from './run.js';
|
|
@@ -61,6 +64,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
61
64
|
pendingWrites: [],
|
|
62
65
|
pendingDeletes: [],
|
|
63
66
|
pendingDormant: [],
|
|
67
|
+
units: [],
|
|
64
68
|
};
|
|
65
69
|
const decay = decayPass(run);
|
|
66
70
|
let mergesSkippedRejected = 0;
|
|
@@ -78,8 +82,9 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
78
82
|
if (mergesSkippedRejected > 0) {
|
|
79
83
|
log.warn(`consolidate: skipped ${mergesSkippedRejected} merge(s) whose content matches a rejected value`);
|
|
80
84
|
}
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
const budget = options.budget ?? WRITE_BUDGET;
|
|
86
|
+
await flushPending(run, snapshot, budget);
|
|
87
|
+
await expireDormant(run, budget);
|
|
83
88
|
if (!dryRun)
|
|
84
89
|
logRun(run, decay);
|
|
85
90
|
return result;
|
|
@@ -96,14 +101,14 @@ function migrateHalfLives(hippoRoot, dryRun, result) {
|
|
|
96
101
|
}
|
|
97
102
|
return halfLife;
|
|
98
103
|
}
|
|
99
|
-
function flushPending(run, snapshot) {
|
|
100
|
-
const { result, pendingDeletes, pendingDormant } = run;
|
|
104
|
+
async function flushPending(run, snapshot, budget) {
|
|
105
|
+
const { result, pendingWrites, pendingDeletes, pendingDormant } = run;
|
|
101
106
|
result.removedIds = pendingDeletes;
|
|
102
|
-
// One transaction; the snapshot keeps what the DAG passes and other writers changed while sleep ran.
|
|
103
|
-
// Dormant moves ride in the same transaction (src/dormant.ts).
|
|
104
107
|
if (run.dryRun)
|
|
105
108
|
return;
|
|
106
|
-
const
|
|
109
|
+
const units = [...run.units, ...familyUnits(pendingWrites, pendingDeletes, pendingDormant, snapshot)];
|
|
110
|
+
const components = groupFlush(pendingWrites, pendingDeletes, pendingDormant, units);
|
|
111
|
+
const left = new Set(await commitInChunks(run.hippoRoot, components, snapshot, budget));
|
|
107
112
|
for (const id of [...pendingDeletes, ...pendingDormant.map((m) => m.entry.id)]) {
|
|
108
113
|
if (!left.has(id))
|
|
109
114
|
result.details.push(` ↩ ${id} not removed: pinned or already gone before sleep saved`);
|
|
@@ -112,18 +117,81 @@ function flushPending(run, snapshot) {
|
|
|
112
117
|
result.removed = result.removedIds.length;
|
|
113
118
|
result.dormant = pendingDormant.filter((m) => left.has(m.entry.id)).length;
|
|
114
119
|
}
|
|
120
|
+
/** Commits whole components in transactions of about `budget.holdMs`, letting other writers in between; returns the ids that left `memories`.
|
|
121
|
+
* The snapshot keeps what the DAG passes and other writers changed while sleep ran. */
|
|
122
|
+
async function commitInChunks(hippoRoot, components, snapshot, budget) {
|
|
123
|
+
if (components.length === 0)
|
|
124
|
+
return [];
|
|
125
|
+
const removed = [];
|
|
126
|
+
// Its own wait, not a server request's 250 ms, and an option rather than a PRAGMA so a shared hook handle keeps its own.
|
|
127
|
+
const db = openStore(hippoRoot, { busyWaitMs: SLEEP_DB_WAIT_MS });
|
|
128
|
+
let next = 0;
|
|
129
|
+
try {
|
|
130
|
+
let committedAt = 0;
|
|
131
|
+
while (next < components.length) {
|
|
132
|
+
if (next > 0)
|
|
133
|
+
await budget.pause(committedAt);
|
|
134
|
+
const chunk = batchWriteAndDeleteOn(db, hippoRoot, components, next, { snapshot, holdMs: budget.holdMs, clock: budget.clock });
|
|
135
|
+
committedAt = budget.clock();
|
|
136
|
+
next = chunk.next;
|
|
137
|
+
for (const id of chunk.removedIds)
|
|
138
|
+
removed.push(id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// A unit that threw is already tagged; a throw outside one (a pause, BEGIN or COMMIT) names the chunk's first unit.
|
|
143
|
+
if (err instanceof Error)
|
|
144
|
+
noteFailedUnit(err, components[next]);
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
closeHippoDb(db);
|
|
149
|
+
}
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
152
|
+
/** Deletes `keys` in transactions of about `budget.holdMs`, letting other writers in between; returns how many went. */
|
|
153
|
+
async function expireInChunks(db, keys, cutoff, budget) {
|
|
154
|
+
let gone = 0;
|
|
155
|
+
let next = 0;
|
|
156
|
+
let committedAt = 0;
|
|
157
|
+
while (next < keys.length) {
|
|
158
|
+
if (next > 0)
|
|
159
|
+
await budget.pause(committedAt);
|
|
160
|
+
db.exec('BEGIN IMMEDIATE');
|
|
161
|
+
const begunAt = budget.clock();
|
|
162
|
+
try {
|
|
163
|
+
do
|
|
164
|
+
gone += deleteExpiredDormantRow(db, keys[next++], cutoff);
|
|
165
|
+
while (next < keys.length && budget.clock() - begunAt < budget.holdMs);
|
|
166
|
+
db.exec('COMMIT');
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
if (db.isTransaction !== false) {
|
|
170
|
+
try {
|
|
171
|
+
db.exec('ROLLBACK');
|
|
172
|
+
}
|
|
173
|
+
catch { /* preserve the original throw */ }
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
committedAt = budget.clock();
|
|
178
|
+
}
|
|
179
|
+
return gone;
|
|
180
|
+
}
|
|
115
181
|
// Dormant retention: a dormant memory nobody restored within
|
|
116
182
|
// dormant.retentionDays is deleted for good (0 keeps them forever). Runs
|
|
117
183
|
// even when dormant.enabled is off, so turning it off still ages out what
|
|
118
184
|
// earlier sleeps kept.
|
|
119
|
-
function expireDormant(run) {
|
|
185
|
+
async function expireDormant(run, budget) {
|
|
120
186
|
const { config, result, dryRun } = run;
|
|
121
187
|
if (!(config.dormant.retentionDays > 0))
|
|
122
188
|
return;
|
|
123
189
|
const cutoff = new Date(run.now.getTime() - config.dormant.retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
124
|
-
const db = openHippoDb(run.hippoRoot);
|
|
190
|
+
const db = openHippoDb(run.hippoRoot, { busyWaitMs: SLEEP_DB_WAIT_MS });
|
|
125
191
|
try {
|
|
126
|
-
|
|
192
|
+
// The keys come from a read, so a sleep with nothing to expire never takes the write lock; one DELETE scanned every stored entry under it.
|
|
193
|
+
const keys = expiredDormantKeys(db, cutoff);
|
|
194
|
+
result.dormantExpired = dryRun ? keys.length : await expireInChunks(db, keys, cutoff, budget);
|
|
127
195
|
}
|
|
128
196
|
finally {
|
|
129
197
|
closeHippoDb(db);
|
|
@@ -152,8 +220,7 @@ function logRun(run, decay) {
|
|
|
152
220
|
if (decay.rescuedEntries.length > 0)
|
|
153
221
|
auditRescues(run, decay);
|
|
154
222
|
}
|
|
155
|
-
// One audit row per rescue, written only after
|
|
156
|
-
// would assert rescues for a cycle whose effects never landed if a later phase threw.
|
|
223
|
+
// One audit row per rescue, written only after the last flush chunk commits, so a run stopped mid-flush asserts no rescue.
|
|
157
224
|
function auditRescues(run, { rescuedEntries, rankById }) {
|
|
158
225
|
const { result } = run;
|
|
159
226
|
try {
|
package/dist/context-render.d.ts
CHANGED
|
@@ -6,13 +6,27 @@ export declare function snapshotText(s: TaskSnapshot): string;
|
|
|
6
6
|
export declare function handoffText(h: SessionHandoff): string;
|
|
7
7
|
/** Needs at least one event; the header reads the latest. */
|
|
8
8
|
export declare function sessionTrailText(events: SessionEvent[]): string;
|
|
9
|
+
export declare const COMPACT_RESUME_EVENT_CONTENT_CAP = 400;
|
|
10
|
+
export declare const COMPACT_RESUME_MAX_AGE_MS: number;
|
|
11
|
+
/** The block compact-resume hands the model; events arrive already capped. */
|
|
12
|
+
export declare function compactResumeText(snapshot: TaskSnapshot, events: SessionEvent[]): string;
|
|
9
13
|
export declare function contextHeading(heading: string, entries: number, tokens: number): string;
|
|
10
14
|
export declare function contextLine(item: {
|
|
11
15
|
entry: MemoryEntry;
|
|
12
16
|
isGlobal: boolean;
|
|
13
17
|
}, framing: string, showStrength: boolean, now: Date, strengthPct?: number): string;
|
|
18
|
+
/** A memory block as printed: the heading, then one line per item. */
|
|
19
|
+
export declare function contextBlockLines(items: ReadonlyArray<{
|
|
20
|
+
entry: MemoryEntry;
|
|
21
|
+
isGlobal: boolean;
|
|
22
|
+
}>, totalTokens: number, framing: string, opts?: {
|
|
23
|
+
showStrength?: boolean;
|
|
24
|
+
heading?: string;
|
|
25
|
+
}): string[];
|
|
14
26
|
export declare function crossProjectHeading(entries: number): string;
|
|
15
27
|
export declare function crossProjectLine(item: Pick<ContextResultEntry, 'entry' | 'origin'>): string;
|
|
28
|
+
/** An explicit header lets agents and humans tell borrowed context from project memory; no items print nothing. */
|
|
29
|
+
export declare function crossProjectLines(items: ReadonlyArray<Pick<ContextResultEntry, 'entry' | 'origin'>>): string[];
|
|
16
30
|
/** A header's token figure is part of the text it counts, so render until the figure matches the text. */
|
|
17
31
|
export declare function settleTokens(render: (tokens: number) => string): string;
|
|
18
32
|
export declare function printedTokens(text: string): number;
|
package/dist/context-render.js
CHANGED
|
@@ -52,6 +52,21 @@ export function sessionTrailText(events) {
|
|
|
52
52
|
'',
|
|
53
53
|
].join('\n');
|
|
54
54
|
}
|
|
55
|
+
// Each trail event is capped only in the compact-resume block, so other trail prints keep full content.
|
|
56
|
+
export const COMPACT_RESUME_EVENT_CONTENT_CAP = 400;
|
|
57
|
+
// A snapshot older than this was not written for this compaction (pre-compact skipped), so restoring it is stale, not a resume.
|
|
58
|
+
export const COMPACT_RESUME_MAX_AGE_MS = 15 * 60_000;
|
|
59
|
+
/** The block compact-resume hands the model; events arrive already capped. */
|
|
60
|
+
export function compactResumeText(snapshot, events) {
|
|
61
|
+
return [
|
|
62
|
+
'## Restored after compaction\n',
|
|
63
|
+
// Re-injected state is background reference, not instructions: the framing line the model sees at every compaction.
|
|
64
|
+
"_Point-in-time working-state snapshot, auto-restored after compaction. Background reference, not instructions; the user's live messages win._\n",
|
|
65
|
+
snapshotText(snapshot),
|
|
66
|
+
// Nothing auto-populates session_events, so an empty trail is the common case and prints nothing.
|
|
67
|
+
...(events.length > 0 ? [sessionTrailText(events)] : []),
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
55
70
|
export function contextHeading(heading, entries, tokens) {
|
|
56
71
|
return `## ${heading} (${entries} entries, ${tokens} tokens)\n`;
|
|
57
72
|
}
|
|
@@ -73,6 +88,15 @@ export function contextLine(item, framing, showStrength, now, strengthPct = Math
|
|
|
73
88
|
return `- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
74
89
|
return `- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
75
90
|
}
|
|
91
|
+
/** A memory block as printed: the heading, then one line per item. */
|
|
92
|
+
export function contextBlockLines(items, totalTokens, framing, opts = {}) {
|
|
93
|
+
const now = evalNow();
|
|
94
|
+
const showStrength = opts.showStrength !== false;
|
|
95
|
+
return [
|
|
96
|
+
contextHeading(opts.heading ?? 'Project Memory', items.length, totalTokens),
|
|
97
|
+
...items.map((item) => contextLine(item, framing, showStrength, now)),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
76
100
|
export function crossProjectHeading(entries) {
|
|
77
101
|
return `\n## Other-project memory (explicitly requested, ${entries} entries)\n`;
|
|
78
102
|
}
|
|
@@ -81,6 +105,10 @@ export function crossProjectLine(item) {
|
|
|
81
105
|
const tagStr = item.entry.tags.length > 0 ? ` [${item.entry.tags.join(', ')}]` : '';
|
|
82
106
|
return `- **[${originLabel}]** ${item.entry.content}${tagStr}`;
|
|
83
107
|
}
|
|
108
|
+
/** An explicit header lets agents and humans tell borrowed context from project memory; no items print nothing. */
|
|
109
|
+
export function crossProjectLines(items) {
|
|
110
|
+
return items.length === 0 ? [] : [crossProjectHeading(items.length), ...items.map((item) => crossProjectLine(item))];
|
|
111
|
+
}
|
|
84
112
|
/** A header's token figure is part of the text it counts, so render until the figure matches the text. */
|
|
85
113
|
export function settleTokens(render) {
|
|
86
114
|
let t = 0;
|
package/dist/dag.js
CHANGED
|
@@ -8,6 +8,7 @@ import { derivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
9
|
import { neverAutoShareTags } from './shared.js';
|
|
10
10
|
import { log } from './log.js';
|
|
11
|
+
import { certainDefect } from './memory-quality.js';
|
|
11
12
|
export function clusterFacts(facts) {
|
|
12
13
|
if (facts.length === 0)
|
|
13
14
|
return [];
|
|
@@ -80,6 +81,11 @@ export async function generateDagSummary(label, factContents, opts) {
|
|
|
80
81
|
try {
|
|
81
82
|
const data = await res.json();
|
|
82
83
|
const text = data.content?.[0]?.text?.trim() ?? '';
|
|
84
|
+
const defect = certainDefect(text);
|
|
85
|
+
if (defect !== null) {
|
|
86
|
+
opts.onError?.(`summary quality refused: ${defect}`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
83
89
|
return text.length >= 20 ? text : null;
|
|
84
90
|
}
|
|
85
91
|
catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The four dashboard writes, each a thin call into an existing store or api function; the router owns HTTP and the cache.
|
|
2
2
|
import * as api from './api.js';
|
|
3
3
|
import { NotFoundError } from './api-errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import { listTouchableConflicts, resolveConflict } from './store/conflicts.js';
|
|
5
5
|
import { readEntry } from './store/entry-reads.js';
|
|
6
6
|
import { writeEntry } from './store/entry-writes.js';
|
|
7
7
|
import { ParamError, parseMemoryId } from './dashboard-params.js';
|
|
@@ -66,7 +66,8 @@ export function resolveOpenConflict(hippoRoot, tenantId, conflictId, body) {
|
|
|
66
66
|
if (keep === undefined)
|
|
67
67
|
throw new ParamError('keep must be a memory id');
|
|
68
68
|
parseMemoryId(keep);
|
|
69
|
-
|
|
69
|
+
// The dashboard acts as an unowned admin, so a pair holding a personal row reads as missing, open or resolved.
|
|
70
|
+
const conflict = listTouchableConflicts(hippoRoot, '*', tenantId, api.adminActor(ACTOR)).find((c) => c.id === conflictId);
|
|
70
71
|
if (!conflict)
|
|
71
72
|
return notFound();
|
|
72
73
|
const already = () => ({ status: 409, body: { error: 'This conflict is already resolved' }, changed: false });
|
package/dist/dashboard.js
CHANGED
|
@@ -8,7 +8,7 @@ import { readEntry } from './store/entry-reads.js';
|
|
|
8
8
|
import { listCards } from './store-cards.js';
|
|
9
9
|
import { resolveTenantId } from './tenant.js';
|
|
10
10
|
import { loadCardDetail } from './card-detail.js';
|
|
11
|
-
import { isCrossSite, LOOPBACK_HOST_HEADER } from './http-util.js';
|
|
11
|
+
import { closeAfterReply, isCrossSite, LOOPBACK_HOST_HEADER } from './http-util.js';
|
|
12
12
|
import { log } from './log.js';
|
|
13
13
|
import { createSnapshotService, isLiveMemory } from './dashboard-snapshot.js';
|
|
14
14
|
import { buildMemoryDetail, buildMemoryPage, buildOverview, buildProjectDetail, buildSearch, } from './dashboard-queries.js';
|
|
@@ -269,8 +269,9 @@ export function serveDashboard(hippoRoot, port = 3333, token = randomBytes(32).t
|
|
|
269
269
|
return;
|
|
270
270
|
}
|
|
271
271
|
if (err instanceof BodyDrainExceeded) {
|
|
272
|
-
|
|
273
|
-
res.
|
|
272
|
+
// No `Connection: close` header: Node then destroys the socket as soon as the reply is written.
|
|
273
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
274
|
+
res.end(JSON.stringify({ error: err.message }), () => closeAfterReply(req));
|
|
274
275
|
return;
|
|
275
276
|
}
|
|
276
277
|
if (err instanceof ParamError)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DatabaseSyncLike } from './sqlite.js';
|
|
2
|
+
/** Copies the database before a repair writes, so the audit ids plus this file are the way back. */
|
|
3
|
+
export declare function backupStore(db: DatabaseSyncLike, hippoRoot: string, label: string, now?: Date): string;
|
|
4
|
+
/** Backs the store up, then runs one transaction; a transaction that throws rolled back, so its backup goes too. */
|
|
5
|
+
export declare function withBackup<T>(db: DatabaseSyncLike, hippoRoot: string, label: string, transaction: (backup: string) => T): T;
|
|
6
|
+
//# sourceMappingURL=backup.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { log } from '../log.js';
|
|
4
|
+
/** Copies the database before a repair writes, so the audit ids plus this file are the way back. */
|
|
5
|
+
export function backupStore(db, hippoRoot, label, now = new Date()) {
|
|
6
|
+
const dir = path.join(hippoRoot, 'backups');
|
|
7
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
8
|
+
const file = path.join(dir, `hippo-${label}-${now.toISOString().replace(/[:.]/g, '-')}.db`);
|
|
9
|
+
db.prepare('VACUUM INTO ?').run(file);
|
|
10
|
+
return file;
|
|
11
|
+
}
|
|
12
|
+
/** Backs the store up, then runs one transaction; a transaction that throws rolled back, so its backup goes too. */
|
|
13
|
+
export function withBackup(db, hippoRoot, label, transaction) {
|
|
14
|
+
const backup = backupStore(db, hippoRoot, label);
|
|
15
|
+
try {
|
|
16
|
+
return transaction(backup);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
// A locked file on Windows must not hide why the transaction failed.
|
|
20
|
+
try {
|
|
21
|
+
fs.rmSync(backup, { force: true });
|
|
22
|
+
}
|
|
23
|
+
catch (cleanupError) {
|
|
24
|
+
log.warn(`Could not delete the unused backup ${backup}: ${String(cleanupError)}`);
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=backup.js.map
|
package/dist/db/busy.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from './sqlite.js';
|
|
2
2
|
export declare function isSqliteBusy(error: unknown): boolean;
|
|
3
3
|
export declare const STORE_BUSY_MESSAGE = "store busy (another hippo process holds the write lock); retry shortly";
|
|
4
|
+
/** A store behind the port throws this when its lock wait runs out, so the server answers 503 as it does for a busy hippo.db. */
|
|
5
|
+
export declare class StoreBusyError extends Error {
|
|
6
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
7
|
+
}
|
|
8
|
+
/** A held lock in any store: SQLite's busy codes or a port's StoreBusyError. */
|
|
9
|
+
export declare function isStoreBusy<E>(error: E): boolean;
|
|
4
10
|
export declare function execWithBusyRetry(db: DatabaseSyncLike, sql: string, timeoutMs?: number): void;
|
|
11
|
+
/** Runs `fn` as one write: BEGIN IMMEDIATE on an idle handle, else SAVEPOINT `name` inside the caller's transaction.
|
|
12
|
+
* A deferred scope that reads first cannot wait out another writer, while BEGIN IMMEDIATE waits the open's busy_timeout. */
|
|
13
|
+
export declare function withWriteScope<T>(db: DatabaseSyncLike, name: string, fn: () => T): T;
|
|
5
14
|
//# sourceMappingURL=busy.d.ts.map
|
package/dist/db/busy.js
CHANGED
|
@@ -3,6 +3,17 @@ export function isSqliteBusy(error) {
|
|
|
3
3
|
return code === 5 || code === 6 || code === 517;
|
|
4
4
|
}
|
|
5
5
|
export const STORE_BUSY_MESSAGE = 'store busy (another hippo process holds the write lock); retry shortly';
|
|
6
|
+
/** A store behind the port throws this when its lock wait runs out, so the server answers 503 as it does for a busy hippo.db. */
|
|
7
|
+
export class StoreBusyError extends Error {
|
|
8
|
+
constructor(message = STORE_BUSY_MESSAGE, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.name = 'StoreBusyError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** A held lock in any store: SQLite's busy codes or a port's StoreBusyError. */
|
|
14
|
+
export function isStoreBusy(error) {
|
|
15
|
+
return error instanceof StoreBusyError || isSqliteBusy(error);
|
|
16
|
+
}
|
|
6
17
|
// busy_timeout covers neither of this file's two contended statements: SQLite
|
|
7
18
|
// skips the busy handler for `PRAGMA journal_mode` and for a write that upgrades
|
|
8
19
|
// a deferred read snapshot. Both need an explicit wait instead.
|
|
@@ -21,4 +32,28 @@ export function execWithBusyRetry(db, sql, timeoutMs = 30000) {
|
|
|
21
32
|
}
|
|
22
33
|
}
|
|
23
34
|
}
|
|
35
|
+
/** Runs `fn` as one write: BEGIN IMMEDIATE on an idle handle, else SAVEPOINT `name` inside the caller's transaction.
|
|
36
|
+
* A deferred scope that reads first cannot wait out another writer, while BEGIN IMMEDIATE waits the open's busy_timeout. */
|
|
37
|
+
export function withWriteScope(db, name, fn) {
|
|
38
|
+
const top = db.isTransaction === false;
|
|
39
|
+
db.exec(top ? 'BEGIN IMMEDIATE' : `SAVEPOINT ${name}`);
|
|
40
|
+
try {
|
|
41
|
+
const result = fn();
|
|
42
|
+
db.exec(top ? 'COMMIT' : `RELEASE SAVEPOINT ${name}`);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
try {
|
|
47
|
+
if (top) {
|
|
48
|
+
db.exec('ROLLBACK');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
52
|
+
db.exec(`RELEASE SAVEPOINT ${name}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* already rolled back; keep the original error */ }
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
24
59
|
//# sourceMappingURL=busy.js.map
|
package/dist/db/continuity.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from './sqlite.js';
|
|
2
2
|
export declare const MEMORY_QUARANTINE_DDL = "\n CREATE TABLE IF NOT EXISTS memory_quarantine (\n tenant_id TEXT NOT NULL DEFAULT 'default',\n memory_id TEXT NOT NULL,\n original_scope TEXT,\n reason TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),\n quarantined_at TEXT NOT NULL,\n decided_at TEXT,\n decided_by TEXT,\n PRIMARY KEY (tenant_id, memory_id)\n ) WITHOUT ROWID;\n CREATE INDEX IF NOT EXISTS idx_memory_quarantine_status\n ON memory_quarantine(tenant_id, status, quarantined_at DESC);\n";
|
|
3
|
+
export declare const SESSION_OWNERS_DDL = "\n CREATE TABLE IF NOT EXISTS session_owners (\n tenant_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n owner_subject TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (tenant_id, session_id)\n )\n ";
|
|
3
4
|
export declare function ensureContinuityTables(db: DatabaseSyncLike): void;
|
|
4
5
|
export declare const MEMORIES_FTS_DDL = "CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(id UNINDEXED, content, tags)";
|
|
5
6
|
/** Names of every table and index a `CREATE ... IF NOT EXISTS` in `ddl` makes. */
|