hippo-memory 1.52.9 → 1.53.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 +34 -10
- package/dist/agent-memories/apply.d.ts +47 -0
- package/dist/agent-memories/apply.js +253 -0
- package/dist/agent-memories/claude-code.d.ts +11 -0
- package/dist/agent-memories/claude-code.js +113 -0
- package/dist/agent-memories/codex.d.ts +3 -0
- package/dist/agent-memories/codex.js +47 -0
- package/dist/agent-memories/copilot.d.ts +3 -0
- package/dist/agent-memories/copilot.js +125 -0
- package/dist/agent-memories/files.d.ts +37 -0
- package/dist/agent-memories/files.js +77 -0
- package/dist/agent-memories/folder-store.d.ts +17 -0
- package/dist/agent-memories/folder-store.js +44 -0
- package/dist/agent-memories/gemini.d.ts +3 -0
- package/dist/agent-memories/gemini.js +103 -0
- package/dist/agent-memories/git.d.ts +8 -0
- package/dist/agent-memories/git.js +11 -0
- package/dist/agent-memories/keys.d.ts +9 -0
- package/dist/agent-memories/keys.js +20 -0
- package/dist/agent-memories/legacy.d.ts +17 -0
- package/dist/agent-memories/legacy.js +45 -0
- package/dist/agent-memories/markdown.d.ts +13 -0
- package/dist/agent-memories/markdown.js +123 -0
- package/dist/agent-memories/openclaw.d.ts +3 -0
- package/dist/agent-memories/openclaw.js +42 -0
- package/dist/agent-memories/plan.d.ts +78 -0
- package/dist/agent-memories/plan.js +123 -0
- package/dist/agent-memories/qwen-code.d.ts +5 -0
- package/dist/agent-memories/qwen-code.js +50 -0
- package/dist/agent-memories/report.d.ts +52 -0
- package/dist/agent-memories/report.js +88 -0
- package/dist/agent-memories/source.d.ts +16 -0
- package/dist/agent-memories/source.js +32 -0
- package/dist/agent-memories/sync.d.ts +33 -0
- package/dist/agent-memories/sync.js +336 -0
- package/dist/agent-memories/tools.d.ts +33 -0
- package/dist/agent-memories/tools.js +19 -0
- package/dist/agent-memories/types.d.ts +42 -0
- package/dist/agent-memories/types.js +2 -0
- package/dist/api.d.ts +2 -2
- package/dist/api.js +26 -27
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +5 -2
- package/dist/capture.d.ts +11 -22
- package/dist/capture.js +76 -81
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +131 -151
- package/dist/compaction-items.d.ts +18 -0
- package/dist/compaction-items.js +60 -0
- package/dist/compaction-record.d.ts +94 -0
- package/dist/compaction-record.js +546 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +13 -0
- package/dist/consolidate.js +2 -2
- package/dist/db.d.ts +5 -1
- package/dist/db.js +40 -8
- package/dist/doctor.js +34 -2
- package/dist/dormant.d.ts +5 -3
- package/dist/dormant.js +9 -0
- package/dist/gated-write.d.ts +9 -0
- package/dist/gated-write.js +24 -0
- package/dist/hooks.d.ts +4 -2
- package/dist/hooks.js +8 -7
- package/dist/memory.d.ts +19 -2
- package/dist/memory.js +35 -3
- package/dist/shared.js +10 -7
- package/dist/store.d.ts +8 -2
- package/dist/store.js +21 -2
- 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/config.js
CHANGED
|
@@ -82,6 +82,9 @@ const DEFAULT_CONFIG = {
|
|
|
82
82
|
churnStaleness: {
|
|
83
83
|
enabled: false,
|
|
84
84
|
},
|
|
85
|
+
agentMemories: {
|
|
86
|
+
tools: null,
|
|
87
|
+
},
|
|
85
88
|
};
|
|
86
89
|
function isMemoryValueConfig(value) {
|
|
87
90
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -92,6 +95,15 @@ function isDormantConfig(value) {
|
|
|
92
95
|
function isChurnStalenessConfig(value) {
|
|
93
96
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
94
97
|
}
|
|
98
|
+
function agentMemoryTools(value) {
|
|
99
|
+
if (value === undefined || value === null)
|
|
100
|
+
return null;
|
|
101
|
+
if (Array.isArray(value) && value.every((t) => String(t) === t))
|
|
102
|
+
return value;
|
|
103
|
+
console.error(`Warning: config.json's "agentMemories.tools" must be a list of tool ids like ["claude-code", "codex"] ` +
|
|
104
|
+
`(got ${JSON.stringify(value)}) - importing none.`);
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
95
107
|
export function loadConfig(hippoRoot) {
|
|
96
108
|
const configPath = path.join(hippoRoot, 'config.json');
|
|
97
109
|
if (!fs.existsSync(configPath))
|
|
@@ -192,6 +204,7 @@ export function loadConfig(hippoRoot) {
|
|
|
192
204
|
churnStaleness: {
|
|
193
205
|
enabled: churnStalenessEnabled,
|
|
194
206
|
},
|
|
207
|
+
agentMemories: { tools: agentMemoryTools(raw.agentMemories?.tools) },
|
|
195
208
|
};
|
|
196
209
|
}
|
|
197
210
|
catch (err) {
|
package/dist/consolidate.js
CHANGED
|
@@ -153,7 +153,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
153
153
|
for (const e of all)
|
|
154
154
|
e.half_life_days = halfLife.halfLives.get(e.id) ?? e.half_life_days;
|
|
155
155
|
const backingObjects = memoriesBackingObjects(hippoRoot);
|
|
156
|
-
// Retirable: auto-deletable (never pinned,
|
|
156
|
+
// Retirable: auto-deletable (never pinned, raw or kept for good) and not backing a first-class object.
|
|
157
157
|
const retirable = (entry) => canAutoDelete(entry) && !backingObjects.has(entry.id);
|
|
158
158
|
const snapshot = new Map(structuredClone(all).map((e) => [e.id, e]));
|
|
159
159
|
// Load decay options from config + session context
|
|
@@ -174,7 +174,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
174
174
|
// so it stays where it is (stored strength refreshed) but sits out the
|
|
175
175
|
// rest of this cycle the way a deleted row would. Anything else goes
|
|
176
176
|
// dormant when config.dormant is on, and is deleted otherwise.
|
|
177
|
-
// Only called for rows `retirable` allows (never pinned,
|
|
177
|
+
// Only called for rows `retirable` allows (never pinned, raw, kept for good or backing a first-class object).
|
|
178
178
|
const retireFaded = (entry, strength) => {
|
|
179
179
|
const why = `(strength ${strength.toFixed(4)} < ${DECAY_THRESHOLD})`;
|
|
180
180
|
// A faded secret is deleted, never kept dormant: keeping it would hold a
|
package/dist/db.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ export declare function getCurrentSchemaVersion(): number;
|
|
|
16
16
|
/** Thrown by {@link assertBinaryCompatible}; doctor uses it to pick the upgrade fix over a generic permissions fix. */
|
|
17
17
|
export declare class IncompatibleBinaryError extends Error {
|
|
18
18
|
}
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function isSqliteBusy(error: unknown): boolean;
|
|
20
|
+
/** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
|
|
21
|
+
export declare function openHippoDb(hippoRoot: string, opts?: {
|
|
22
|
+
busyWaitMs?: number;
|
|
23
|
+
}): DatabaseSyncLike;
|
|
20
24
|
/** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
|
|
21
25
|
export declare function openHippoDbReadOnly(hippoRoot: string): DatabaseSyncLike;
|
|
22
26
|
export declare function getSchemaVersion(db: DatabaseSyncLike): number;
|
package/dist/db.js
CHANGED
|
@@ -11,7 +11,7 @@ const require = createRequire(import.meta.url);
|
|
|
11
11
|
// runtime (Node's built-in synchronous SQLite module); there are no bundled
|
|
12
12
|
// types for it here, so this require + cast is the module's documented boundary.
|
|
13
13
|
const { DatabaseSync } = require('node:sqlite');
|
|
14
|
-
const CURRENT_SCHEMA_VERSION =
|
|
14
|
+
const CURRENT_SCHEMA_VERSION = 49;
|
|
15
15
|
const MIGRATIONS = [
|
|
16
16
|
{
|
|
17
17
|
version: 1,
|
|
@@ -2471,6 +2471,36 @@ const MIGRATIONS = [
|
|
|
2471
2471
|
db.exec(MEMORY_QUARANTINE_DDL);
|
|
2472
2472
|
},
|
|
2473
2473
|
},
|
|
2474
|
+
{
|
|
2475
|
+
version: 49,
|
|
2476
|
+
up: (db) => {
|
|
2477
|
+
// Compaction record (src/compaction-record.ts): one row per Claude Code compaction, written before it (started)
|
|
2478
|
+
// and after it (summarised, done). Not a memory row. Additive only: no min_compatible_binary bump.
|
|
2479
|
+
db.exec(`
|
|
2480
|
+
CREATE TABLE IF NOT EXISTS compactions (
|
|
2481
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2482
|
+
id TEXT NOT NULL,
|
|
2483
|
+
session_id TEXT NOT NULL,
|
|
2484
|
+
origin_project TEXT NOT NULL DEFAULT '',
|
|
2485
|
+
compact_trigger TEXT,
|
|
2486
|
+
cwd TEXT,
|
|
2487
|
+
transcript_path TEXT,
|
|
2488
|
+
snapshot_saved INTEGER NOT NULL DEFAULT 0,
|
|
2489
|
+
started_at TEXT NOT NULL,
|
|
2490
|
+
summarised_at TEXT,
|
|
2491
|
+
summary TEXT,
|
|
2492
|
+
items_json TEXT,
|
|
2493
|
+
items_written INTEGER NOT NULL DEFAULT 0,
|
|
2494
|
+
status TEXT NOT NULL DEFAULT 'started' CHECK (status IN ('started','summarised','done','no-summary')),
|
|
2495
|
+
PRIMARY KEY (tenant_id, id)
|
|
2496
|
+
);
|
|
2497
|
+
CREATE INDEX IF NOT EXISTS idx_compactions_session
|
|
2498
|
+
ON compactions(tenant_id, session_id, started_at);
|
|
2499
|
+
CREATE INDEX IF NOT EXISTS idx_compactions_status
|
|
2500
|
+
ON compactions(tenant_id, status);
|
|
2501
|
+
`);
|
|
2502
|
+
},
|
|
2503
|
+
},
|
|
2474
2504
|
];
|
|
2475
2505
|
function tableHasColumn(db, tableName, columnName) {
|
|
2476
2506
|
if (!/^[a-z_]+$/i.test(tableName))
|
|
@@ -2504,7 +2534,7 @@ function assertBinaryCompatible(db) {
|
|
|
2504
2534
|
`Upgrade hippo-memory to open it; an older binary does not know this schema and could expose private rows or damage the store.`);
|
|
2505
2535
|
}
|
|
2506
2536
|
}
|
|
2507
|
-
function isSqliteBusy(error) {
|
|
2537
|
+
export function isSqliteBusy(error) {
|
|
2508
2538
|
const code = error?.errcode;
|
|
2509
2539
|
return code === 5 || code === 6 || code === 517;
|
|
2510
2540
|
}
|
|
@@ -2526,16 +2556,18 @@ function execWithBusyRetry(db, sql, timeoutMs = 30000) {
|
|
|
2526
2556
|
}
|
|
2527
2557
|
}
|
|
2528
2558
|
}
|
|
2529
|
-
|
|
2559
|
+
/** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
|
|
2560
|
+
export function openHippoDb(hippoRoot, opts) {
|
|
2530
2561
|
fs.mkdirSync(hippoRoot, { recursive: true });
|
|
2531
2562
|
const db = new DatabaseSync(getHippoDbPath(hippoRoot));
|
|
2563
|
+
const busyWaitMs = opts?.busyWaitMs;
|
|
2532
2564
|
try {
|
|
2533
|
-
db.exec(
|
|
2534
|
-
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL');
|
|
2565
|
+
db.exec(`PRAGMA busy_timeout = ${busyWaitMs ?? 5000}`);
|
|
2566
|
+
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL', busyWaitMs);
|
|
2535
2567
|
db.exec('PRAGMA synchronous = NORMAL');
|
|
2536
2568
|
db.exec('PRAGMA wal_autocheckpoint = 100');
|
|
2537
2569
|
db.exec('PRAGMA foreign_keys = ON');
|
|
2538
|
-
runMigrations(db, hippoRoot);
|
|
2570
|
+
runMigrations(db, hippoRoot, busyWaitMs);
|
|
2539
2571
|
// Path A backfill: delete any orphan markdown mirrors for already-archived
|
|
2540
2572
|
// raw_archive rows. Idempotent via per-row raw_archive.mirror_cleaned_at
|
|
2541
2573
|
// (v21). Wrapped in try/catch — a filesystem failure must not prevent DB open.
|
|
@@ -2576,7 +2608,7 @@ export function openHippoDbReadOnly(hippoRoot) {
|
|
|
2576
2608
|
throw error;
|
|
2577
2609
|
}
|
|
2578
2610
|
}
|
|
2579
|
-
function runMigrations(db, hippoRoot) {
|
|
2611
|
+
function runMigrations(db, hippoRoot, busyWaitMs) {
|
|
2580
2612
|
ensureMetaTable(db);
|
|
2581
2613
|
// Before anything writes, so a stale binary never repairs or migrates a store it does not understand.
|
|
2582
2614
|
assertBinaryCompatible(db);
|
|
@@ -2586,7 +2618,7 @@ function runMigrations(db, hippoRoot) {
|
|
|
2586
2618
|
for (const migration of MIGRATIONS) {
|
|
2587
2619
|
if (migration.version <= currentVersion)
|
|
2588
2620
|
continue;
|
|
2589
|
-
execWithBusyRetry(db, 'BEGIN IMMEDIATE');
|
|
2621
|
+
execWithBusyRetry(db, 'BEGIN IMMEDIATE', busyWaitMs);
|
|
2590
2622
|
try {
|
|
2591
2623
|
// A newer binary may have migrated and raised the minimum while we waited for the lock.
|
|
2592
2624
|
assertBinaryCompatible(db);
|
package/dist/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { findHippoStoreDir } from './project-identity.js';
|
|
|
11
11
|
import { getGlobalRoot } from './shared.js';
|
|
12
12
|
import { isInitialized } from './store.js';
|
|
13
13
|
import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getCurrentSchemaVersion, countTableRows, IncompatibleBinaryError } from './db.js';
|
|
14
|
+
import { REPLAY_AFTER_MS, TRANSCRIPT_FILL_WINDOW_MS } from './compaction-record.js';
|
|
14
15
|
import { isEmbeddingAvailable } from './embeddings.js';
|
|
15
16
|
import { CODEX_TRUST_LINE, codexHomeDir, isCodexPresent, isJsonObject } from './hooks.js';
|
|
16
17
|
/** Minimum Node.js version hippo supports (package.json engines). */
|
|
@@ -96,6 +97,36 @@ function sleepCheck(db, now) {
|
|
|
96
97
|
return { id: 'sleep', status: 'info', detail: `sleep history unavailable (${message})` };
|
|
97
98
|
}
|
|
98
99
|
}
|
|
100
|
+
/** Compaction records the PostCompact hook left unfinished, which `hippo sleep` replays. */
|
|
101
|
+
function compactionsCheck(db, now) {
|
|
102
|
+
const stuckBefore = new Date(now.getTime() - REPLAY_AFTER_MS).toISOString();
|
|
103
|
+
const transcriptFloor = new Date(now.getTime() - TRANSCRIPT_FILL_WINDOW_MS).toISOString();
|
|
104
|
+
try {
|
|
105
|
+
// SAFETY: COUNT aggregate row.
|
|
106
|
+
const row = db.prepare(`SELECT COUNT(*) AS total,
|
|
107
|
+
COUNT(CASE WHEN status = 'summarised' AND summarised_at < ? THEN 1 END) AS summarised,
|
|
108
|
+
COUNT(CASE WHEN status = 'started' AND started_at < ? AND started_at > ? AND transcript_path IS NOT NULL THEN 1 END) AS started
|
|
109
|
+
FROM compactions`).get(stuckBefore, stuckBefore, transcriptFloor);
|
|
110
|
+
const total = Number(row?.total ?? 0);
|
|
111
|
+
const summarised = Number(row?.summarised ?? 0);
|
|
112
|
+
const started = Number(row?.started ?? 0);
|
|
113
|
+
const stuck = summarised + started;
|
|
114
|
+
if (stuck === 0)
|
|
115
|
+
return { id: 'compactions', status: 'pass', detail: `${total} compaction${total === 1 ? '' : 's'} recorded, none stuck` };
|
|
116
|
+
return {
|
|
117
|
+
id: 'compactions',
|
|
118
|
+
status: 'warn',
|
|
119
|
+
detail: `${stuck} compaction${stuck === 1 ? '' : 's'} unfinished after 10 minutes (${summarised} with a summary whose memories are not saved yet, ${started} with no summary yet)`,
|
|
120
|
+
fix: 'hippo sleep (replays them)',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
+
return message.includes('no such table')
|
|
126
|
+
? { id: 'compactions', status: 'info', detail: 'no compaction records yet (hippo creates them on the next write)' }
|
|
127
|
+
: { id: 'compactions', status: 'warn', detail: `cannot read the compaction records: ${message}` };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
99
130
|
/** Run every check. Never throws for a broken install; broken parts become failed checks. */
|
|
100
131
|
export function runDoctor(opts) {
|
|
101
132
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -162,6 +193,7 @@ export function runDoctor(opts) {
|
|
|
162
193
|
}
|
|
163
194
|
checks.push(failuresCheck(db, since, have));
|
|
164
195
|
checks.push(sleepCheck(db, now));
|
|
196
|
+
checks.push(compactionsCheck(db, now));
|
|
165
197
|
}
|
|
166
198
|
catch (err) {
|
|
167
199
|
checks.push({
|
|
@@ -185,9 +217,9 @@ export function runDoctor(opts) {
|
|
|
185
217
|
const hooks = [
|
|
186
218
|
['hippo context --pinned-only', 'per-prompt memory'],
|
|
187
219
|
['hippo session-end', 'session-end capture and sleep'],
|
|
188
|
-
['hippo pre-compact', 'compaction snapshot and
|
|
220
|
+
['hippo pre-compact', 'compaction snapshot and memories request'],
|
|
189
221
|
['hippo compact-resume', 'resume after compaction'],
|
|
190
|
-
['hippo post-compact', 'the
|
|
222
|
+
['hippo post-compact', 'saving the memories a compaction lists'],
|
|
191
223
|
['hippo capture-error', 'failed-tool capture'],
|
|
192
224
|
];
|
|
193
225
|
const missing = hooks.filter(([marker]) => !text.includes(marker)).map(([, what]) => what);
|
package/dist/dormant.d.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { DatabaseSyncLike } from './db.js';
|
|
21
21
|
import type { MemoryEntry } from './memory.js';
|
|
22
|
-
/** Why
|
|
23
|
-
export type DormantReason = 'decay';
|
|
22
|
+
/** Why a memory went dormant: sleep's decay pass, or an imported agent memory whose note was deleted. */
|
|
23
|
+
export type DormantReason = 'decay' | 'source-deleted';
|
|
24
24
|
/** One memory that sleep is moving out of active memory into the dormant store. */
|
|
25
25
|
export interface DormantMove {
|
|
26
26
|
/** The memory as it stood when it faded; restored verbatim apart from its recall clock. */
|
|
@@ -39,7 +39,7 @@ export interface DormantMemory {
|
|
|
39
39
|
tags: string[];
|
|
40
40
|
/** Live strength when it went dormant. */
|
|
41
41
|
strength: number;
|
|
42
|
-
/** Why it went dormant (`decay`). */
|
|
42
|
+
/** Why it went dormant (`decay` or `source-deleted`). */
|
|
43
43
|
reason: string;
|
|
44
44
|
/** ISO time it went dormant. */
|
|
45
45
|
dormantAt: string;
|
|
@@ -73,6 +73,8 @@ export interface DormantSnapshot {
|
|
|
73
73
|
export declare function readDormantSnapshot(db: DatabaseSyncLike, tenantId: string, id: string): DormantSnapshot | null;
|
|
74
74
|
/** Every dormant memory of a tenant whose snapshot still reads back. */
|
|
75
75
|
export declare function listDormantSnapshots(db: DatabaseSyncLike, tenantId: string): DormantSnapshot[];
|
|
76
|
+
/** Readable snapshots whose entry's source starts with `prefix`; a malformed snapshot is passed over, not an error. */
|
|
77
|
+
export declare function dormantSnapshotsBySourcePrefix(db: DatabaseSyncLike, tenantId: string, prefix: string): DormantSnapshot[];
|
|
76
78
|
/** Put `entry` in place of a tenant's dormant memory `id`, keeping when and why that one went dormant. */
|
|
77
79
|
export declare function replaceDormantEntry(db: DatabaseSyncLike, tenantId: string, id: string, entry: MemoryEntry): void;
|
|
78
80
|
/** Whether a tenant has a dormant memory with this id (snapshot readable or not). */
|
package/dist/dormant.js
CHANGED
|
@@ -83,6 +83,15 @@ export function listDormantSnapshots(db, tenantId) {
|
|
|
83
83
|
FROM dormant_memories WHERE tenant_id = ?`).all(tenantId);
|
|
84
84
|
return rows.flatMap((row) => toSnapshot(row) ?? []);
|
|
85
85
|
}
|
|
86
|
+
/** Readable snapshots whose entry's source starts with `prefix`; a malformed snapshot is passed over, not an error. */
|
|
87
|
+
export function dormantSnapshotsBySourcePrefix(db, tenantId, prefix) {
|
|
88
|
+
// SAFETY: rows' shape matches the seven columns named in the SELECT.
|
|
89
|
+
const rows = db.prepare(`SELECT tenant_id, id, content, entry_json, reason, strength, dormant_at
|
|
90
|
+
FROM dormant_memories
|
|
91
|
+
WHERE tenant_id = ?
|
|
92
|
+
AND CASE WHEN json_valid(entry_json) THEN json_extract(entry_json, '$.source') END LIKE ? ESCAPE '\\'`).all(tenantId, `${escapeLike(prefix)}%`);
|
|
93
|
+
return rows.flatMap((row) => toSnapshot(row) ?? []).filter((s) => String(s.entry.source).startsWith(prefix));
|
|
94
|
+
}
|
|
86
95
|
function toSnapshot(row) {
|
|
87
96
|
const entry = parseSnapshot(row);
|
|
88
97
|
return entry ? { entry, reason: row.reason, strength: row.strength, dormantAt: row.dormant_at } : null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DatabaseSyncLike } from './db.js';
|
|
2
|
+
import type { MemoryEntry } from './memory.js';
|
|
3
|
+
export type GatedWriteResult = 'written' | 'skipped:not-worth-storing' | 'skipped:secret' | 'skipped:rejected';
|
|
4
|
+
/** Runs on the caller's handle so it nests in the caller's transaction (writeEntry would open a second handle and wait on that lock); the caller mirrors after commit with an entry it stamped itself. The rejection audit lands inside that transaction, which is safe because a batch that rolls back stays `summarised` and replay writes the audit again. */
|
|
5
|
+
export declare function gatedWrite(db: DatabaseSyncLike, hippoRoot: string, entry: MemoryEntry, opts?: {
|
|
6
|
+
actor?: string;
|
|
7
|
+
worthCheck?: boolean;
|
|
8
|
+
}): GatedWriteResult;
|
|
9
|
+
//# sourceMappingURL=gated-write.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The one write path for text no person typed into hippo: capture, compaction items and imported agent memories.
|
|
2
|
+
import { isContentWorthStoring } from './audit.js';
|
|
3
|
+
import { RejectedValueError } from './rejection.js';
|
|
4
|
+
import { detectSecret } from './secret-detect.js';
|
|
5
|
+
import { auditRejectionRefusal, stampOriginProject, writeEntryDbOnly } from './store.js';
|
|
6
|
+
/** Runs on the caller's handle so it nests in the caller's transaction (writeEntry would open a second handle and wait on that lock); the caller mirrors after commit with an entry it stamped itself. The rejection audit lands inside that transaction, which is safe because a batch that rolls back stays `summarised` and replay writes the audit again. */
|
|
7
|
+
export function gatedWrite(db, hippoRoot, entry, opts) {
|
|
8
|
+
// Off for imported agent memories: a person wrote those notes, and a one-line preference fails the check.
|
|
9
|
+
if (opts?.worthCheck !== false && !isContentWorthStoring(entry.content))
|
|
10
|
+
return 'skipped:not-worth-storing';
|
|
11
|
+
if (detectSecret(entry).flagged)
|
|
12
|
+
return 'skipped:secret';
|
|
13
|
+
try {
|
|
14
|
+
writeEntryDbOnly(db, stampOriginProject(hippoRoot, entry), opts);
|
|
15
|
+
return 'written';
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (!(err instanceof RejectedValueError))
|
|
19
|
+
throw err;
|
|
20
|
+
auditRejectionRefusal(db, err, opts?.actor ?? 'cli');
|
|
21
|
+
return 'skipped:rejected';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=gated-write.js.map
|
package/dist/hooks.d.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
|
|
17
17
|
* - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
|
|
18
18
|
* - 0.22.x: separate sleep + capture SessionEnd entries.
|
|
19
|
+
* PreCompact and PostCompact entries go in too: the first records the compaction and
|
|
20
|
+
* asks the summariser for a "Memories for hippo" list, the second saves that list.
|
|
19
21
|
* Codex's hooks.json gets only two groups (per-prompt memory and
|
|
20
22
|
* compact-resume); see installCodexHooks.
|
|
21
23
|
*
|
|
@@ -91,7 +93,7 @@ export interface InstallResult {
|
|
|
91
93
|
installedUserPromptSubmit: boolean;
|
|
92
94
|
installedPreCompact: boolean;
|
|
93
95
|
installedCompactResume: boolean;
|
|
94
|
-
/** PostCompact -> `hippo post-compact` (tells the user
|
|
96
|
+
/** PostCompact -> `hippo post-compact` (saves the summary's memories, tells the user how many). */
|
|
95
97
|
installedPostCompact: boolean;
|
|
96
98
|
/** PostToolUseFailure -> `hippo capture-error` (failed tool calls become error memories). */
|
|
97
99
|
installedCaptureError: boolean;
|
|
@@ -150,7 +152,7 @@ declare const HIPPO_OPENCODE_PLUGIN_MARKER = "HIPPO_OPENCODE_PLUGIN_V1";
|
|
|
150
152
|
export declare const OPENCODE_PLUGIN_SOURCE = "// HIPPO_OPENCODE_PLUGIN_V1\n// hippo-memory opencode plugin. DO NOT EDIT \u2014 regenerated on every\n// `hippo hook install opencode` from src/hooks.ts OPENCODE_PLUGIN_SOURCE\n// in https://github.com/kitfunso/hippo-memory. Local changes will be lost.\n\nexport const HippoPlugin = async ({ $ }) => {\n return {\n event: async ({ event }) => {\n // Defense in depth: opencode currently runs in Bun where $ is the shell\n // template helper. A non-Bun runtime would have $ as undefined; fail\n // closed instead of crashing the host session.\n if (typeof $ !== \"function\") return;\n try {\n if (event.type === \"session.idle\") {\n await $`hippo session-end`.quiet().nothrow();\n } else if (event.type === \"session.created\") {\n await $`hippo last-sleep`.quiet().nothrow();\n }\n } catch {\n // hippo CLI not on PATH or other failure \u2014 never crash the host session.\n }\n },\n };\n};\n";
|
|
151
153
|
export { HIPPO_OPENCODE_PLUGIN_MARKER };
|
|
152
154
|
/** Codex's config folder: $CODEX_HOME, else ~/.codex, as the Codex hooks docs describe. */
|
|
153
|
-
export declare function codexHomeDir(home?: string): string;
|
|
155
|
+
export declare function codexHomeDir(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
154
156
|
/** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
|
|
155
157
|
export declare function isCodexPresent(home?: string): boolean;
|
|
156
158
|
/** Codex hashes each hook and skips new or changed ones until the user reviews them in `/hooks`, so the reminder says what they would trust. */
|
package/dist/hooks.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
|
|
17
17
|
* - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
|
|
18
18
|
* - 0.22.x: separate sleep + capture SessionEnd entries.
|
|
19
|
+
* PreCompact and PostCompact entries go in too: the first records the compaction and
|
|
20
|
+
* asks the summariser for a "Memories for hippo" list, the second saves that list.
|
|
19
21
|
* Codex's hooks.json gets only two groups (per-prompt memory and
|
|
20
22
|
* compact-resume); see installCodexHooks.
|
|
21
23
|
*
|
|
@@ -124,8 +126,8 @@ function homeDir() {
|
|
|
124
126
|
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
125
127
|
}
|
|
126
128
|
/** Codex's config folder: $CODEX_HOME, else ~/.codex, as the Codex hooks docs describe. */
|
|
127
|
-
export function codexHomeDir(home = homeDir()) {
|
|
128
|
-
return
|
|
129
|
+
export function codexHomeDir(home = homeDir(), env = process.env) {
|
|
130
|
+
return env.CODEX_HOME || path.join(home, '.codex');
|
|
129
131
|
}
|
|
130
132
|
/** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
|
|
131
133
|
export function isCodexPresent(home = homeDir()) {
|
|
@@ -731,8 +733,8 @@ export function installJsonHooks(target) {
|
|
|
731
733
|
});
|
|
732
734
|
installedUserPromptSubmit = true;
|
|
733
735
|
}
|
|
734
|
-
// PreCompact: fires on manual AND auto compaction (no matcher).
|
|
735
|
-
// working-state snapshot before the
|
|
736
|
+
// PreCompact: fires on manual AND auto compaction (no matcher). Records the compaction, asks the
|
|
737
|
+
// summariser for a "Memories for hippo" list and saves a working-state snapshot before the summary drops detail.
|
|
736
738
|
// Exit-0 contract lives in the verb itself (src/capture.ts cmdPreCompact),
|
|
737
739
|
// not here — this is install-time wiring only.
|
|
738
740
|
let installedPreCompact = false;
|
|
@@ -772,9 +774,8 @@ export function installJsonHooks(target) {
|
|
|
772
774
|
});
|
|
773
775
|
installedCompactResume = true;
|
|
774
776
|
}
|
|
775
|
-
// PostCompact:
|
|
776
|
-
//
|
|
777
|
-
// summarising model as instructions; PostCompact stdout is only shown.
|
|
777
|
+
// PostCompact: saves the memories the summariser listed and prints one line, which Claude Code only shows.
|
|
778
|
+
// PreCompact stdout, by contrast, is handed to the summariser as instructions, so pre-compact prints just the request.
|
|
778
779
|
let installedPostCompact = false;
|
|
779
780
|
if (!hookArrayContains(hooks.PostCompact, HIPPO_POST_COMPACT_MARKER)) {
|
|
780
781
|
if (!Array.isArray(hooks.PostCompact))
|
package/dist/memory.d.ts
CHANGED
|
@@ -213,8 +213,17 @@ export declare function resolveConfidence(entry: MemoryEntry, now?: Date): Confi
|
|
|
213
213
|
* on an older base (src/half-life-migration.ts).
|
|
214
214
|
*/
|
|
215
215
|
export declare const DEFAULT_HALF_LIFE_DAYS = 365;
|
|
216
|
-
export declare const
|
|
217
|
-
export declare
|
|
216
|
+
export declare const COMPACTION_MEMORY_TAG = "compaction-memory";
|
|
217
|
+
export declare const COMPACTION_SOURCE_PREFIX = "compaction:";
|
|
218
|
+
/** A row with `tag` and a source starting `sourcePrefix` is kept for good. Both, since merge copies source tags onto rows whose source is 'consolidation'. */
|
|
219
|
+
export interface KeepPair {
|
|
220
|
+
readonly tag: string;
|
|
221
|
+
readonly sourcePrefix: string;
|
|
222
|
+
}
|
|
223
|
+
export declare const KEEP_PAIRS: readonly KeepPair[];
|
|
224
|
+
export declare const AUTO_DELETABLE_SQL: string;
|
|
225
|
+
export declare function isKeptForGood(entry: Pick<MemoryEntry, 'tags' | 'source' | 'superseded_by'>): boolean;
|
|
226
|
+
export declare function canAutoDelete(entry: Pick<MemoryEntry, 'pinned' | 'kind' | 'tags' | 'source' | 'superseded_by'>): boolean;
|
|
218
227
|
export interface CreateMemoryOptions {
|
|
219
228
|
layer?: Layer;
|
|
220
229
|
tags?: string[];
|
|
@@ -239,6 +248,14 @@ export interface CreateMemoryOptions {
|
|
|
239
248
|
}
|
|
240
249
|
/** Create a new memory entry with defaults. Untyped JavaScript callers that omit the options get the compiled default half-life. */
|
|
241
250
|
export declare function createMemory(content: string, options: CreateMemoryOptions): MemoryEntry;
|
|
251
|
+
/** The row that replaces `old`: a supersede never changes where a memory belongs, so source, scope, session and a stamped origin carry over. */
|
|
252
|
+
export declare function createSuccessor(old: MemoryEntry, content: string, opts: {
|
|
253
|
+
tenantId: string;
|
|
254
|
+
baseHalfLifeDays: number;
|
|
255
|
+
layer?: Layer;
|
|
256
|
+
tags?: string[];
|
|
257
|
+
pinned?: boolean;
|
|
258
|
+
}): MemoryEntry;
|
|
242
259
|
/**
|
|
243
260
|
* Compute how well new content fits existing knowledge patterns.
|
|
244
261
|
* Returns 0..1 where:
|
package/dist/memory.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { isDecayAblated, isOutcomeSlowAblated, isRecallBoostAblated, evalNow, } from './ablation.js';
|
|
7
|
+
import { AGENT_MEMORY_TOOLS, toolSourcePrefix } from './agent-memories/tools.js';
|
|
7
8
|
export var Layer;
|
|
8
9
|
(function (Layer) {
|
|
9
10
|
Layer["Buffer"] = "buffer";
|
|
@@ -314,10 +315,22 @@ export function resolveConfidence(entry, now = evalNow()) {
|
|
|
314
315
|
* on an older base (src/half-life-migration.ts).
|
|
315
316
|
*/
|
|
316
317
|
export const DEFAULT_HALF_LIFE_DAYS = 365;
|
|
317
|
-
|
|
318
|
-
export const
|
|
318
|
+
export const COMPACTION_MEMORY_TAG = 'compaction-memory';
|
|
319
|
+
export const COMPACTION_SOURCE_PREFIX = 'compaction:';
|
|
320
|
+
export const KEEP_PAIRS = [
|
|
321
|
+
{ tag: COMPACTION_MEMORY_TAG, sourcePrefix: COMPACTION_SOURCE_PREFIX },
|
|
322
|
+
...AGENT_MEMORY_TOOLS.map((t) => ({ tag: t.tag, sourcePrefix: toolSourcePrefix(t.id) })),
|
|
323
|
+
];
|
|
324
|
+
const sqlText = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
325
|
+
// json_each matches the tag as a whole element; substr, not LIKE, keeps the prefix case-sensitive like startsWith.
|
|
326
|
+
const keepPairSql = (p) => `(COALESCE(superseded_by, '') = '' AND EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(tags_json) THEN tags_json ELSE '[]' END) WHERE value = ${sqlText(p.tag)}) AND substr(source, 1, ${p.sourcePrefix.length}) = ${sqlText(p.sourcePrefix)})`;
|
|
327
|
+
// Pinned and kept rows stay (a superseded row is not kept: its successor carries the tag and source); raw rows leave only through archiveRawMemory. The SQL twin guards the DELETE itself.
|
|
328
|
+
export const AUTO_DELETABLE_SQL = `pinned = 0 AND kind != 'raw'${KEEP_PAIRS.map((p) => ` AND NOT ${keepPairSql(p)}`).join('')}`;
|
|
329
|
+
export function isKeptForGood(entry) {
|
|
330
|
+
return !entry.superseded_by && KEEP_PAIRS.some((p) => entry.tags.includes(p.tag) && entry.source.startsWith(p.sourcePrefix));
|
|
331
|
+
}
|
|
319
332
|
export function canAutoDelete(entry) {
|
|
320
|
-
return !entry.pinned && entry.kind !== 'raw';
|
|
333
|
+
return !entry.pinned && entry.kind !== 'raw' && !isKeptForGood(entry);
|
|
321
334
|
}
|
|
322
335
|
export function createMemory(content, options = {}) {
|
|
323
336
|
const trimmed = content.trim();
|
|
@@ -373,6 +386,25 @@ export function createMemory(content, options = {}) {
|
|
|
373
386
|
entry.strength = calculateStrength(entry);
|
|
374
387
|
return entry;
|
|
375
388
|
}
|
|
389
|
+
/** The row that replaces `old`: a supersede never changes where a memory belongs, so source, scope, session and a stamped origin carry over. */
|
|
390
|
+
export function createSuccessor(old, content, opts) {
|
|
391
|
+
const next = createMemory(content, {
|
|
392
|
+
layer: opts.layer ?? old.layer,
|
|
393
|
+
tags: opts.tags ?? [...old.tags],
|
|
394
|
+
pinned: opts.pinned ?? old.pinned,
|
|
395
|
+
source: old.source,
|
|
396
|
+
confidence: 'verified',
|
|
397
|
+
tenantId: opts.tenantId,
|
|
398
|
+
scope: old.scope,
|
|
399
|
+
source_session_id: old.source_session_id,
|
|
400
|
+
baseHalfLifeDays: opts.baseHalfLifeDays,
|
|
401
|
+
});
|
|
402
|
+
// A legacy null origin has nothing to carry, so the store stamps it from its own location.
|
|
403
|
+
if (typeof old.origin_project === 'string') {
|
|
404
|
+
next.origin_project = old.origin_project;
|
|
405
|
+
}
|
|
406
|
+
return next;
|
|
407
|
+
}
|
|
376
408
|
/**
|
|
377
409
|
* Compute how well new content fits existing knowledge patterns.
|
|
378
410
|
* Returns 0..1 where:
|
package/dist/shared.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import { generateId } from './memory.js';
|
|
9
|
+
import { generateId, COMPACTION_MEMORY_TAG } from './memory.js';
|
|
10
|
+
import { AGENT_MEMORY_SOURCE_PREFIX, AGENT_MEMORY_TAGS } from './agent-memories/tools.js';
|
|
10
11
|
import { initStore, loadAllEntries, loadIndex, loadSearchEntries, loadRecallSearchEntries, writeEntry, readEntry, } from './store.js';
|
|
11
12
|
import { passesScopeFilterForRecall, passesCliRecallScopeFilter } from './recall-scope.js';
|
|
12
13
|
import { search, hybridSearch, fitBudget } from './search.js';
|
|
@@ -224,6 +225,7 @@ const TRANSFERABLE_TAGS = new Set([
|
|
|
224
225
|
export const NEVER_AUTO_SHARE_TAGS = new Set([
|
|
225
226
|
'git-learned',
|
|
226
227
|
'session-digest',
|
|
228
|
+
...AGENT_MEMORY_TAGS,
|
|
227
229
|
]);
|
|
228
230
|
export function neverAutoShareTags(sources) {
|
|
229
231
|
return [...NEVER_AUTO_SHARE_TAGS].filter((tag) => sources.some((s) => s.tags.includes(tag)));
|
|
@@ -232,6 +234,8 @@ export function neverAutoShareTags(sources) {
|
|
|
232
234
|
export const NO_MERGE_TAGS = new Set([
|
|
233
235
|
'extracted',
|
|
234
236
|
'session-digest',
|
|
237
|
+
COMPACTION_MEMORY_TAG,
|
|
238
|
+
...AGENT_MEMORY_TAGS,
|
|
235
239
|
]);
|
|
236
240
|
/**
|
|
237
241
|
* Estimate how well a memory would transfer to other projects.
|
|
@@ -398,7 +402,7 @@ export function autoShare(localRoot, options = {}) {
|
|
|
398
402
|
if (isQuarantineScope(entry.scope ?? null))
|
|
399
403
|
return false;
|
|
400
404
|
// Before the score: these rows describe one project only, and a git seed's 'error' tag clears the bar.
|
|
401
|
-
if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t))) {
|
|
405
|
+
if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t)) || entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX)) {
|
|
402
406
|
if (options.stats)
|
|
403
407
|
options.stats.neverAutoShareSkipped = (options.stats.neverAutoShareSkipped ?? 0) + 1;
|
|
404
408
|
return false;
|
|
@@ -481,16 +485,15 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
|
481
485
|
// only stamps when the field is missing).
|
|
482
486
|
const currentName = deriveOriginProject(path.dirname(path.resolve(localRoot)));
|
|
483
487
|
let count = 0;
|
|
484
|
-
//
|
|
485
|
-
// — a locally-rejected value must not silently resurrect via sync down
|
|
486
|
-
// from global): per-item catch, no signature change (bare number return;
|
|
487
|
-
// see the syncGlobalToLocal callers in cli.ts + tests). Counted locally
|
|
488
|
-
// and printed as one summary line, same pattern as learnFromMemoryMd.
|
|
488
|
+
// A locally rejected value must not come back through sync down: caught per item, printed as one line.
|
|
489
489
|
let rejected = 0;
|
|
490
490
|
for (const entry of globalEntries) {
|
|
491
491
|
// Skip if already present by ID
|
|
492
492
|
if (localIndex.entries[entry.id])
|
|
493
493
|
continue;
|
|
494
|
+
// Only the global store's user pass sets an imported note's row aside, so a copy would outlive the note.
|
|
495
|
+
if (entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX))
|
|
496
|
+
continue;
|
|
494
497
|
if (localText.has(textKey(entry)))
|
|
495
498
|
continue;
|
|
496
499
|
if (detectSecret(entry).flagged)
|
package/dist/store.d.ts
CHANGED
|
@@ -367,7 +367,7 @@ export declare function loadChildrenOf(hippoRoot: string, parentId: string, tena
|
|
|
367
367
|
* Default keeps `deleteEntry` byte-identical to its pre-split behavior.
|
|
368
368
|
*
|
|
369
369
|
* Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
|
|
370
|
-
* existed or `automatic` refused it (pinned or
|
|
370
|
+
* existed or `automatic` refused it (pinned, raw or kept for good at DELETE time, so a late pin wins).
|
|
371
371
|
*/
|
|
372
372
|
export declare function deleteEntryCore(db: ReturnType<typeof openHippoDb>, id: string, opts?: {
|
|
373
373
|
actor?: string;
|
|
@@ -400,7 +400,7 @@ export declare function deleteEntry(hippoRoot: string, id: string, opts?: {
|
|
|
400
400
|
* `dormant` (src/dormant.ts): each move's snapshot is inserted into `dormant_memories` and its `memories` row
|
|
401
401
|
* leaves exactly like a delete (FTS row, DAG parent dirty-mark, mirrors), in the same transaction, so a memory
|
|
402
402
|
* is never in both places or in neither. Deletes and moves both skip rows that are no longer auto-deletable
|
|
403
|
-
* (pinned or
|
|
403
|
+
* (pinned, raw or kept for good since the caller decided). Returns the ids that left `memories`, deleted or moved. */
|
|
404
404
|
export declare function batchWriteAndDelete(hippoRoot: string, toWrite: MemoryEntry[], toDeleteIds: string[], opts?: {
|
|
405
405
|
snapshot?: ReadonlyMap<string, MemoryEntry>;
|
|
406
406
|
dormant?: DormantMove[];
|
|
@@ -415,6 +415,12 @@ export declare function batchWriteAndDelete(hippoRoot: string, toWrite: MemoryEn
|
|
|
415
415
|
export declare function loadAllEntries(hippoRoot: string, tenantId?: string): MemoryEntry[];
|
|
416
416
|
/** Every memory row on an open connection, so a caller can read inside its own transaction. */
|
|
417
417
|
export declare function selectAllEntries(db: DatabaseSyncLike, tenantId?: string): MemoryEntry[];
|
|
418
|
+
/** Live rows whose source starts with `prefix`, on the caller's handle; LIKE folds case, so the prefix is checked again exactly. */
|
|
419
|
+
export declare function selectLiveEntriesBySourcePrefix(db: DatabaseSyncLike, tenantId: string, prefix: string): MemoryEntry[];
|
|
420
|
+
/** Rewrites a live row's tags and its full-text row on the caller's transaction, with no audit row. */
|
|
421
|
+
export declare function setEntryTagsInTx(db: DatabaseSyncLike, entry: MemoryEntry): void;
|
|
422
|
+
/** Removes a row from `memories` and full-text search on the caller's transaction, as sleep's dormant move does, and marks its summary parent dirty. */
|
|
423
|
+
export declare function deleteEntryRowInTx(db: DatabaseSyncLike, entry: MemoryEntry, actor: string): void;
|
|
418
424
|
export declare function loadContentsWithTag(hippoRoot: string, tenantId: string, tag: string): string[];
|
|
419
425
|
export interface AmbientRecallRequest {
|
|
420
426
|
terms: string[];
|
package/dist/store.js
CHANGED
|
@@ -1555,7 +1555,7 @@ export function loadChildrenOf(hippoRoot, parentId, tenantId) {
|
|
|
1555
1555
|
* Default keeps `deleteEntry` byte-identical to its pre-split behavior.
|
|
1556
1556
|
*
|
|
1557
1557
|
* Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
|
|
1558
|
-
* existed or `automatic` refused it (pinned or
|
|
1558
|
+
* existed or `automatic` refused it (pinned, raw or kept for good at DELETE time, so a late pin wins).
|
|
1559
1559
|
*/
|
|
1560
1560
|
export function deleteEntryCore(db, id, opts) {
|
|
1561
1561
|
// SAFETY: row's shape matches the three columns named in the SELECT above.
|
|
@@ -1622,7 +1622,7 @@ function mergeOwnChanges(base, ours, live) {
|
|
|
1622
1622
|
* `dormant` (src/dormant.ts): each move's snapshot is inserted into `dormant_memories` and its `memories` row
|
|
1623
1623
|
* leaves exactly like a delete (FTS row, DAG parent dirty-mark, mirrors), in the same transaction, so a memory
|
|
1624
1624
|
* is never in both places or in neither. Deletes and moves both skip rows that are no longer auto-deletable
|
|
1625
|
-
* (pinned or
|
|
1625
|
+
* (pinned, raw or kept for good since the caller decided). Returns the ids that left `memories`, deleted or moved. */
|
|
1626
1626
|
export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
|
|
1627
1627
|
const dormantMoves = opts?.dormant ?? [];
|
|
1628
1628
|
if (toWrite.length === 0 && toDeleteIds.length === 0 && dormantMoves.length === 0)
|
|
@@ -1810,6 +1810,25 @@ export function selectAllEntries(db, tenantId) {
|
|
|
1810
1810
|
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
|
|
1811
1811
|
return rows.map(rowToEntry);
|
|
1812
1812
|
}
|
|
1813
|
+
/** Live rows whose source starts with `prefix`, on the caller's handle; LIKE folds case, so the prefix is checked again exactly. */
|
|
1814
|
+
export function selectLiveEntriesBySourcePrefix(db, tenantId, prefix) {
|
|
1815
|
+
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
1816
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE tenant_id = ? AND superseded_by IS NULL AND source LIKE ? ESCAPE '\\'`).all(tenantId, `${prefix.replace(/[%_\\]/g, '\\$&')}%`);
|
|
1817
|
+
return rows.map(rowToEntry).filter((entry) => entry.source.startsWith(prefix));
|
|
1818
|
+
}
|
|
1819
|
+
/** Rewrites a live row's tags and its full-text row on the caller's transaction, with no audit row. */
|
|
1820
|
+
export function setEntryTagsInTx(db, entry) {
|
|
1821
|
+
db.prepare(`UPDATE memories SET tags_json = ?, updated_at = datetime('now') WHERE id = ? AND tenant_id = ?`)
|
|
1822
|
+
.run(JSON.stringify(entry.tags), entry.id, entry.tenantId);
|
|
1823
|
+
syncFtsRow(db, entry);
|
|
1824
|
+
}
|
|
1825
|
+
/** Removes a row from `memories` and full-text search on the caller's transaction, as sleep's dormant move does, and marks its summary parent dirty. */
|
|
1826
|
+
export function deleteEntryRowInTx(db, entry, actor) {
|
|
1827
|
+
db.prepare('DELETE FROM memories WHERE id = ? AND tenant_id = ?').run(entry.id, entry.tenantId);
|
|
1828
|
+
deleteFtsRow(db, entry.id);
|
|
1829
|
+
if (entry.dag_parent_id)
|
|
1830
|
+
markSummaryDirtyInTx(db, entry.dag_parent_id, entry.tenantId, actor);
|
|
1831
|
+
}
|
|
1813
1832
|
// Content of every tenant row tagged `tag`, without reading the rest of the store.
|
|
1814
1833
|
// `instr` is a substring prefilter over the raw JSON; `includes` below re-checks exactly.
|
|
1815
1834
|
export function loadContentsWithTag(hippoRoot, tenantId, tag) {
|
package/dist/version.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export declare const PACKAGE_VERSION = "1.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.53.0";
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export declare function compareSemver(a: string, b: string): number;
|
|
22
22
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export const PACKAGE_VERSION = '1.
|
|
19
|
+
export const PACKAGE_VERSION = '1.53.0';
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export function compareSemver(a, b) {
|
|
22
22
|
const parse = (v) => {
|