hippo-memory 1.52.9 → 1.53.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 +47 -13
- 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 +28 -29
- package/dist/audit.d.ts +4 -3
- package/dist/audit.js +10 -6
- package/dist/capture.d.ts +11 -22
- package/dist/capture.js +76 -81
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +133 -170
- 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 +573 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +13 -0
- package/dist/consolidate.d.ts +0 -2
- package/dist/consolidate.js +3 -36
- package/dist/db.d.ts +5 -1
- package/dist/db.js +40 -8
- package/dist/dedupe.js +3 -2
- 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 +32 -3
- package/dist/shared.js +10 -7
- package/dist/store.d.ts +14 -2
- package/dist/store.js +75 -19
- 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.d.ts
CHANGED
|
@@ -37,8 +37,6 @@ export interface ConsolidationResult {
|
|
|
37
37
|
/** Ids the decay pass removes (or would remove, under dryRun). */
|
|
38
38
|
removedIds?: string[];
|
|
39
39
|
}
|
|
40
|
-
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
|
|
41
|
-
export declare const MEMORY_BACKED_TABLES: readonly ["predictions", "decisions", "incidents", "processes", "policies", "skills", "project_briefs", "customer_notes"];
|
|
42
40
|
/**
|
|
43
41
|
* Run a full consolidation pass.
|
|
44
42
|
*/
|
package/dist/consolidate.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { evalNow, isRecallBoostAblated } from './ablation.js';
|
|
10
10
|
import { Layer, calculateStrength, canAutoDelete, createMemory } from './memory.js';
|
|
11
|
-
import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, } from './store.js';
|
|
11
|
+
import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, memoriesBackingObjects, } from './store.js';
|
|
12
12
|
import { textOverlap, markRetrieved, tokenize } from './search.js';
|
|
13
13
|
import { compareEntryIdentity } from './compare.js';
|
|
14
14
|
import { duplicateKey, mergedText } from './same-text.js';
|
|
@@ -72,39 +72,6 @@ function keptAsWritten(entry) {
|
|
|
72
72
|
function isJsonString(value) {
|
|
73
73
|
return typeof value === 'string';
|
|
74
74
|
}
|
|
75
|
-
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
|
|
76
|
-
export const MEMORY_BACKED_TABLES = ['predictions', 'decisions', 'incidents', 'processes', 'policies', 'skills', 'project_briefs', 'customer_notes'];
|
|
77
|
-
/**
|
|
78
|
-
* Ids of memories that back a first-class object (a decision, incident, prediction,
|
|
79
|
-
* process, policy, skill, project brief or customer note). Sleep never
|
|
80
|
-
* retires these: deleting or moving one to dormant storage fires the
|
|
81
|
-
* object's ON DELETE SET NULL and a restore cannot repair the link. Their
|
|
82
|
-
* lifecycle belongs to the object. A table missing from an older schema is
|
|
83
|
-
* skipped.
|
|
84
|
-
*/
|
|
85
|
-
function memoriesBackingObjects(hippoRoot) {
|
|
86
|
-
const ids = new Set();
|
|
87
|
-
const db = openHippoDb(hippoRoot);
|
|
88
|
-
try {
|
|
89
|
-
for (const table of MEMORY_BACKED_TABLES) {
|
|
90
|
-
try {
|
|
91
|
-
// SAFETY: SELECT of one nullable TEXT column, filtered to non-null.
|
|
92
|
-
const rows = db.prepare(`SELECT memory_id FROM ${table} WHERE memory_id IS NOT NULL`).all();
|
|
93
|
-
for (const r of rows)
|
|
94
|
-
ids.add(r.memory_id);
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
// A missing table is an older schema; any other error could hide a backing memory, so sleep stops.
|
|
98
|
-
if (!(err instanceof Error && err.message.includes('no such table')))
|
|
99
|
-
throw err;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
finally {
|
|
104
|
-
closeHippoDb(db);
|
|
105
|
-
}
|
|
106
|
-
return ids;
|
|
107
|
-
}
|
|
108
75
|
/**
|
|
109
76
|
* Run a full consolidation pass.
|
|
110
77
|
*/
|
|
@@ -153,7 +120,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
153
120
|
for (const e of all)
|
|
154
121
|
e.half_life_days = halfLife.halfLives.get(e.id) ?? e.half_life_days;
|
|
155
122
|
const backingObjects = memoriesBackingObjects(hippoRoot);
|
|
156
|
-
// Retirable: auto-deletable (never pinned,
|
|
123
|
+
// Retirable: auto-deletable (never pinned, raw or kept for good) and not backing a first-class object.
|
|
157
124
|
const retirable = (entry) => canAutoDelete(entry) && !backingObjects.has(entry.id);
|
|
158
125
|
const snapshot = new Map(structuredClone(all).map((e) => [e.id, e]));
|
|
159
126
|
// Load decay options from config + session context
|
|
@@ -174,7 +141,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
174
141
|
// so it stays where it is (stored strength refreshed) but sits out the
|
|
175
142
|
// rest of this cycle the way a deleted row would. Anything else goes
|
|
176
143
|
// dormant when config.dormant is on, and is deleted otherwise.
|
|
177
|
-
// Only called for rows `retirable` allows (never pinned,
|
|
144
|
+
// Only called for rows `retirable` allows (never pinned, raw, kept for good or backing a first-class object).
|
|
178
145
|
const retireFaded = (entry, strength) => {
|
|
179
146
|
const why = `(strength ${strength.toFixed(4)} < ${DECAY_THRESHOLD})`;
|
|
180
147
|
// 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/dedupe.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* superseded rows are not candidates at all (v1.38.1).
|
|
22
22
|
*/
|
|
23
23
|
import { textOverlap } from './search.js';
|
|
24
|
-
import { loadAllEntries, deleteEntry } from './store.js';
|
|
24
|
+
import { loadAllEntries, deleteEntry, memoriesBackingObjects } from './store.js';
|
|
25
25
|
import { compareEntryIdentity } from './compare.js';
|
|
26
26
|
import { canAutoDelete } from './memory.js';
|
|
27
27
|
import { derivationPartitionKey } from './recall-scope.js';
|
|
@@ -97,6 +97,7 @@ export function deduplicateStore(hippoRoot, options = {}) {
|
|
|
97
97
|
// primary-key id alone.
|
|
98
98
|
const removed = new Set();
|
|
99
99
|
const pairs = [];
|
|
100
|
+
const backing = memoriesBackingObjects(hippoRoot);
|
|
100
101
|
for (const tenantEntries of entriesByTenant.values()) {
|
|
101
102
|
// The v1.26.3 survivor total order (see the file-level docstring:
|
|
102
103
|
// strength bucket desc -> retrieval_count desc -> compareEntryIdentity),
|
|
@@ -116,7 +117,7 @@ export function deduplicateStore(hippoRoot, options = {}) {
|
|
|
116
117
|
if (removed.has(tenantEntries[i].id))
|
|
117
118
|
continue;
|
|
118
119
|
for (let j = i + 1; j < tenantEntries.length; j++) {
|
|
119
|
-
if (removed.has(tenantEntries[j].id) || !canAutoDelete(tenantEntries[j]))
|
|
120
|
+
if (removed.has(tenantEntries[j].id) || !canAutoDelete(tenantEntries[j]) || backing.has(tenantEntries[j].id))
|
|
120
121
|
continue;
|
|
121
122
|
if (texts[j] !== texts[i])
|
|
122
123
|
continue;
|
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
|
+
/** An imported agent memory (its tool's tag, and a source starting that tool's prefix) is kept for good: the agent's note file is its record. 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,19 @@ 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 = AGENT_MEMORY_TOOLS.map((t) => ({ tag: t.tag, sourcePrefix: toolSourcePrefix(t.id) }));
|
|
321
|
+
const sqlText = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
322
|
+
// json_each matches the tag as a whole element; substr, not LIKE, keeps the prefix case-sensitive like startsWith.
|
|
323
|
+
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)})`;
|
|
324
|
+
// 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.
|
|
325
|
+
export const AUTO_DELETABLE_SQL = `pinned = 0 AND kind != 'raw'${KEEP_PAIRS.map((p) => ` AND NOT ${keepPairSql(p)}`).join('')}`;
|
|
326
|
+
export function isKeptForGood(entry) {
|
|
327
|
+
return !entry.superseded_by && KEEP_PAIRS.some((p) => entry.tags.includes(p.tag) && entry.source.startsWith(p.sourcePrefix));
|
|
328
|
+
}
|
|
319
329
|
export function canAutoDelete(entry) {
|
|
320
|
-
return !entry.pinned && entry.kind !== 'raw';
|
|
330
|
+
return !entry.pinned && entry.kind !== 'raw' && !isKeptForGood(entry);
|
|
321
331
|
}
|
|
322
332
|
export function createMemory(content, options = {}) {
|
|
323
333
|
const trimmed = content.trim();
|
|
@@ -373,6 +383,25 @@ export function createMemory(content, options = {}) {
|
|
|
373
383
|
entry.strength = calculateStrength(entry);
|
|
374
384
|
return entry;
|
|
375
385
|
}
|
|
386
|
+
/** The row that replaces `old`: a supersede never changes where a memory belongs, so source, scope, session and a stamped origin carry over. */
|
|
387
|
+
export function createSuccessor(old, content, opts) {
|
|
388
|
+
const next = createMemory(content, {
|
|
389
|
+
layer: opts.layer ?? old.layer,
|
|
390
|
+
tags: opts.tags ?? [...old.tags],
|
|
391
|
+
pinned: opts.pinned ?? old.pinned,
|
|
392
|
+
source: old.source,
|
|
393
|
+
confidence: 'verified',
|
|
394
|
+
tenantId: opts.tenantId,
|
|
395
|
+
scope: old.scope,
|
|
396
|
+
source_session_id: old.source_session_id,
|
|
397
|
+
baseHalfLifeDays: opts.baseHalfLifeDays,
|
|
398
|
+
});
|
|
399
|
+
// A legacy null origin has nothing to carry, so the store stamps it from its own location.
|
|
400
|
+
if (typeof old.origin_project === 'string') {
|
|
401
|
+
next.origin_project = old.origin_project;
|
|
402
|
+
}
|
|
403
|
+
return next;
|
|
404
|
+
}
|
|
376
405
|
/**
|
|
377
406
|
* Compute how well new content fits existing knowledge patterns.
|
|
378
407
|
* 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)
|