hippo-memory 1.52.8 → 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 +185 -101
- 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 +53 -20
- package/dist/api.js +141 -97
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +68 -2
- package/dist/capture.d.ts +48 -22
- package/dist/capture.js +186 -161
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +750 -797
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- 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 -1
- package/dist/config.js +13 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +87 -34
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/db.d.ts +5 -1
- package/dist/db.js +46 -14
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +69 -4
- package/dist/dormant.d.ts +9 -3
- package/dist/dormant.js +26 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/gated-write.d.ts +9 -0
- package/dist/gated-write.js +24 -0
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +5 -3
- package/dist/hooks.d.ts +18 -2
- package/dist/hooks.js +128 -32
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +171 -210
- package/dist/memory.d.ts +19 -2
- package/dist/memory.js +35 -3
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +6 -0
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +10 -2
- package/dist/shared.js +44 -36
- package/dist/store.d.ts +9 -2
- package/dist/store.js +25 -2
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// Runs the adapters and routes each container to its store: the project pass, the user pass and their call sites (plan designs 2, 8, 11).
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { errorMessage } from '../capture.js';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { closeHippoDb, isSqliteBusy, openHippoDb } from '../db.js';
|
|
8
|
+
import { deriveOriginProject, isGlobalStoreRoot, resolveGlobalRootDir } from '../project-identity.js';
|
|
9
|
+
import { duplicateKey, heldTextKeys } from '../same-text.js';
|
|
10
|
+
import { initStore, isInitialized, removeEntryMirrors, selectLiveEntriesBySourcePrefix, updateStats, writeEntryMirrors } from '../store.js';
|
|
11
|
+
import { resolveTenantId } from '../tenant.js';
|
|
12
|
+
import { setAsideRow, syncContainer } from './apply.js';
|
|
13
|
+
import { claudeCodeAdapter, claudeTranscriptListing } from './claude-code.js';
|
|
14
|
+
import { codexAdapter } from './codex.js';
|
|
15
|
+
import { copilotAdapter } from './copilot.js';
|
|
16
|
+
import { geminiAdapter } from './gemini.js';
|
|
17
|
+
import { legacyWork } from './legacy.js';
|
|
18
|
+
import { openclawAdapter } from './openclaw.js';
|
|
19
|
+
import { qwenCodeAdapter } from './qwen-code.js';
|
|
20
|
+
import { addTally, emptyReport, mergeReports, toolReport } from './report.js';
|
|
21
|
+
import { containerId, containerPrefix, splitSource } from './source.js';
|
|
22
|
+
import { AGENT_MEMORY_SOURCE_PREFIX, AGENT_MEMORY_TOOLS, isToolId, toolSourcePrefix } from './tools.js';
|
|
23
|
+
export const ADAPTERS = [claudeCodeAdapter, codexAdapter, geminiAdapter, copilotAdapter, openclawAdapter, qwenCodeAdapter];
|
|
24
|
+
/** Overrides config because `hippo init` creates the store in the same command that imports (plan design 11). */
|
|
25
|
+
export const TOOLS_ENV = 'HIPPO_AGENT_MEMORY_TOOLS';
|
|
26
|
+
export function currentMachine() {
|
|
27
|
+
return { home: os.homedir(), env: process.env, platform: process.platform };
|
|
28
|
+
}
|
|
29
|
+
/** init, sleep, `import --agents` and the daily runner: a local store gets its project pass then the user pass; the global store the user pass only. */
|
|
30
|
+
export function importForStore(hippoRoot, opts) {
|
|
31
|
+
if (isGlobalStoreRoot(hippoRoot))
|
|
32
|
+
return importUserMemories(hippoRoot, opts);
|
|
33
|
+
const report = importProjectMemories(hippoRoot, opts);
|
|
34
|
+
mergeReports(report, importUserMemories(hippoRoot, opts));
|
|
35
|
+
return report;
|
|
36
|
+
}
|
|
37
|
+
/** A local store's project pass with legacy adoption; `init --scan` runs it per repository and the user pass once. */
|
|
38
|
+
export function importProjectMemories(hippoRoot, opts) {
|
|
39
|
+
const ctx = context(opts.machine, { projectRoot: path.dirname(hippoRoot) });
|
|
40
|
+
return runPass({
|
|
41
|
+
scope: 'project', target: hippoRoot, invoking: hippoRoot, list: (a) => a.list(ctx, 'project'), legacy: true, originProject: undefined, handover: true,
|
|
42
|
+
}, opts);
|
|
43
|
+
}
|
|
44
|
+
/** Every tool's user-level memory into the global store, created on demand, with no origin. */
|
|
45
|
+
export function importUserMemories(invokingRoot, opts) {
|
|
46
|
+
const ctx = context(opts.machine, {});
|
|
47
|
+
return runPass({
|
|
48
|
+
scope: 'user', target: resolveGlobalRootDir(), invoking: invokingRoot, list: (a) => a.list(ctx, 'user'), legacy: false, originProject: '', handover: false,
|
|
49
|
+
}, opts);
|
|
50
|
+
}
|
|
51
|
+
/** Session end in a folder with no store of its own: the session's project into the global store with its origin, then the user pass. */
|
|
52
|
+
export function importAtSessionEnd(cwd, transcriptPath, opts) {
|
|
53
|
+
const globalRoot = resolveGlobalRootDir();
|
|
54
|
+
const ctx = context(opts.machine, { projectRoot: cwd, transcriptPath });
|
|
55
|
+
const report = runPass({
|
|
56
|
+
scope: 'project', target: globalRoot, invoking: globalRoot, list: (a) => a.list(ctx, 'project'), legacy: false, originProject: deriveOriginProject(cwd), handover: false,
|
|
57
|
+
}, opts);
|
|
58
|
+
mergeReports(report, importUserMemories(globalRoot, opts));
|
|
59
|
+
return report;
|
|
60
|
+
}
|
|
61
|
+
/** Post-compact: the transcript folder's notes only, with no git call, no legacy adoption and no user pass, as the hook has 10 seconds. */
|
|
62
|
+
export function importAtCompaction(hippoRoot, transcriptPath, originProject, opts) {
|
|
63
|
+
const ctx = context(opts.machine, {});
|
|
64
|
+
return runPass({
|
|
65
|
+
scope: 'project', target: hippoRoot, invoking: hippoRoot, legacy: false, originProject, handover: false,
|
|
66
|
+
list: (a) => (a.tool === 'claude-code' ? claudeTranscriptListing(ctx, transcriptPath) : null),
|
|
67
|
+
}, opts);
|
|
68
|
+
}
|
|
69
|
+
function context(machine, extra) {
|
|
70
|
+
return { home: machine.home, env: machine.env, platform: machine.platform, ...extra };
|
|
71
|
+
}
|
|
72
|
+
/** The variable when set (empty or `none` is off); else the tools both the invoking and the target store's config allow. */
|
|
73
|
+
export function allowedTools(env, invoking, target, warnings) {
|
|
74
|
+
const fromEnv = env[TOOLS_ENV];
|
|
75
|
+
if (fromEnv !== undefined) {
|
|
76
|
+
const listed = fromEnv.trim().toLowerCase() === 'none' ? [] : fromEnv.split(',').map((t) => t.trim()).filter((t) => t !== '');
|
|
77
|
+
return knownTools(listed, TOOLS_ENV, warnings);
|
|
78
|
+
}
|
|
79
|
+
const mine = configuredTools(invoking, warnings);
|
|
80
|
+
const theirs = configuredTools(target, warnings);
|
|
81
|
+
return new Set(AGENT_MEMORY_TOOLS.map((t) => t.id).filter((id) => (mine === null || mine.has(id)) && (theirs === null || theirs.has(id))));
|
|
82
|
+
}
|
|
83
|
+
function configuredTools(root, warnings) {
|
|
84
|
+
const tools = loadConfig(root).agentMemories.tools;
|
|
85
|
+
return tools === null ? null : knownTools(tools, 'config agentMemories.tools', warnings);
|
|
86
|
+
}
|
|
87
|
+
function knownTools(ids, where, warnings) {
|
|
88
|
+
const out = new Set();
|
|
89
|
+
for (const id of ids) {
|
|
90
|
+
if (isToolId(id))
|
|
91
|
+
out.add(id);
|
|
92
|
+
else
|
|
93
|
+
warnings.push(`${where}: unknown agent memory tool "${id}" ignored (known: ${AGENT_MEMORY_TOOLS.map((t) => t.id).join(', ')})`);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function runPass(pass, opts) {
|
|
98
|
+
const report = emptyReport();
|
|
99
|
+
const allowed = allowedTools(opts.machine.env, pass.invoking, pass.target, report.warnings);
|
|
100
|
+
const listings = ADAPTERS.filter((a) => allowed.has(a.tool)).flatMap((a) => listSafely(a, pass, report));
|
|
101
|
+
const hasItems = listings.some((l) => l.containers.some((c) => c.items.length > 0));
|
|
102
|
+
let store = null;
|
|
103
|
+
let synced = [];
|
|
104
|
+
try {
|
|
105
|
+
store = openTarget(pass.target, hasItems, opts);
|
|
106
|
+
if (store !== null)
|
|
107
|
+
synced = syncStore(pass, listings, store, opts, report);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
report.warnings.push(`agent memories not synced into ${pass.target}: ${isSqliteBusy(err) ? 'the store was busy' : errorMessage(err)}`);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
store?.close();
|
|
114
|
+
}
|
|
115
|
+
if (pass.handover && !opts.dryRun)
|
|
116
|
+
handOver(synced, path.dirname(pass.target), opts, report);
|
|
117
|
+
return report;
|
|
118
|
+
}
|
|
119
|
+
function listSafely(adapter, pass, report) {
|
|
120
|
+
const tool = toolReport(report, adapter.tool);
|
|
121
|
+
let listing;
|
|
122
|
+
try {
|
|
123
|
+
listing = pass.list(adapter);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
report.warnings.push(`${tool.label}: ${errorMessage(err)}`);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
if (listing === null)
|
|
130
|
+
return [];
|
|
131
|
+
if (!tool.homes.includes(listing.home))
|
|
132
|
+
tool.homes.push(listing.home);
|
|
133
|
+
report.warnings.push(...listing.warnings.map((w) => `${tool.label}: ${w}`));
|
|
134
|
+
for (const c of listing.containers) {
|
|
135
|
+
tool.containers.push({ scope: c.scope, path: c.path, store: pass.target, items: c.items.length, readable: c.readable });
|
|
136
|
+
report.warnings.push(...c.warnings.map((w) => `${tool.label}: ${w}`));
|
|
137
|
+
}
|
|
138
|
+
return [listing];
|
|
139
|
+
}
|
|
140
|
+
/** A store that does not exist yet is created only when there is something to put in it; a dry run plans against an empty stand-in. */
|
|
141
|
+
function openTarget(target, hasItems, opts) {
|
|
142
|
+
const global = isGlobalStoreRoot(target);
|
|
143
|
+
if (!isInitialized(target)) {
|
|
144
|
+
if (opts.dryRun)
|
|
145
|
+
return emptyStandIn(global);
|
|
146
|
+
if (!hasItems)
|
|
147
|
+
return null;
|
|
148
|
+
initStore(target);
|
|
149
|
+
}
|
|
150
|
+
const db = openHippoDb(target, { busyWaitMs: opts.busyWaitMs });
|
|
151
|
+
return { db, root: target, global, close: () => closeHippoDb(db) };
|
|
152
|
+
}
|
|
153
|
+
function emptyStandIn(global) {
|
|
154
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hippo-agent-memories-'));
|
|
155
|
+
const db = openHippoDb(root);
|
|
156
|
+
return {
|
|
157
|
+
db, root, global,
|
|
158
|
+
close: () => {
|
|
159
|
+
closeHippoDb(db);
|
|
160
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function syncStore(pass, listings, store, opts, report) {
|
|
165
|
+
const tenantId = resolveTenantId({});
|
|
166
|
+
const legacy = pass.legacy ? legacyWork(store.db, tenantId, listings) : null;
|
|
167
|
+
const session = {
|
|
168
|
+
db: store.db,
|
|
169
|
+
hippoRoot: store.root,
|
|
170
|
+
tenantId,
|
|
171
|
+
baseHalfLifeDays: loadConfig(store.root).defaultHalfLifeDays,
|
|
172
|
+
originProject: pass.originProject,
|
|
173
|
+
isDuplicate: duplicateCheck(store, tenantId, pass.originProject, legacy?.adopted ?? new Set()),
|
|
174
|
+
dryRun: opts.dryRun === true,
|
|
175
|
+
};
|
|
176
|
+
// A project's rows in the global store are parted by origin: a worktree and its main checkout share a Claude folder.
|
|
177
|
+
const partition = store.global && pass.scope === 'project' ? pass.originProject ?? '' : null;
|
|
178
|
+
const synced = [];
|
|
179
|
+
let remembered = 0;
|
|
180
|
+
for (const listing of listings) {
|
|
181
|
+
const tool = toolOf(listing.tool);
|
|
182
|
+
const out = toolReport(report, tool.id);
|
|
183
|
+
for (const container of listing.containers) {
|
|
184
|
+
if (!container.readable) {
|
|
185
|
+
out.tally.unreadable++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const work = containerWork(tool, container, opts.machine.platform, legacy, partition ?? '');
|
|
189
|
+
const outcome = syncOne(session, work, out, report);
|
|
190
|
+
if (outcome === null)
|
|
191
|
+
continue;
|
|
192
|
+
synced.push(work);
|
|
193
|
+
remembered += outcome.tally.imported + outcome.tally.replaced;
|
|
194
|
+
if (!session.dryRun)
|
|
195
|
+
afterCommit(store.root, outcome, report);
|
|
196
|
+
}
|
|
197
|
+
if (session.dryRun)
|
|
198
|
+
out.unlisted += unlistedRows(store.db, tenantId, tool, pass.scope, listing, opts.machine.platform, partition);
|
|
199
|
+
}
|
|
200
|
+
if (remembered > 0 && !session.dryRun)
|
|
201
|
+
bumpRemembered(store.root, remembered, report);
|
|
202
|
+
return synced;
|
|
203
|
+
}
|
|
204
|
+
function containerWork(tool, container, platform, legacy, origin) {
|
|
205
|
+
const none = new Map();
|
|
206
|
+
const own = tool.id === 'claude-code' ? legacy?.byContainer.get(container.path) : undefined;
|
|
207
|
+
return {
|
|
208
|
+
tool,
|
|
209
|
+
container,
|
|
210
|
+
prefix: containerPrefix(tool.id, containerId(container.path, container.scope, platform, origin)),
|
|
211
|
+
adopt: own?.adopt ?? none,
|
|
212
|
+
replace: own?.replace ?? none,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Null when the container was skipped: a busy store waits for the next sync, any other failure is a warning and the sync goes on. */
|
|
216
|
+
function syncOne(session, work, out, report) {
|
|
217
|
+
try {
|
|
218
|
+
const outcome = syncContainer(session, work);
|
|
219
|
+
addTally(out.tally, outcome.tally);
|
|
220
|
+
return outcome;
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (isSqliteBusy(err)) {
|
|
224
|
+
out.tally.busy++;
|
|
225
|
+
report.warnings.push(`${out.label}: ${work.container.path} skipped, the store was busy`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
report.warnings.push(`${out.label}: ${work.container.path} not synced: ${errorMessage(err)}`);
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function afterCommit(root, outcome, report) {
|
|
234
|
+
for (const entry of outcome.mirror)
|
|
235
|
+
writeEntryMirrors(root, entry);
|
|
236
|
+
for (const id of outcome.purge) {
|
|
237
|
+
try {
|
|
238
|
+
removeEntryMirrors(root, id);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
// rebuildIndex would bring the row back live, and the next sync sets it aside again.
|
|
242
|
+
report.warnings.push(`mirror of ${id} not removed: ${errorMessage(err)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function bumpRemembered(root, remembered, report) {
|
|
247
|
+
try {
|
|
248
|
+
updateStats(root, { remembered });
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
report.warnings.push(`remembered counter not updated: ${errorMessage(err)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Design 6: only text stored by another path counts, and in the global store only rows visible where the new row goes. */
|
|
255
|
+
function duplicateCheck(store, tenantId, origin, adopted) {
|
|
256
|
+
let keys = null;
|
|
257
|
+
return (text) => {
|
|
258
|
+
keys ??= otherPathKeys(store, tenantId, origin ?? '', adopted);
|
|
259
|
+
return keys.has(duplicateKey(text));
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function otherPathKeys(store, tenantId, origin, adopted) {
|
|
263
|
+
const visible = store.global ? ` AND (origin_project = '' OR origin_project = ?)` : '';
|
|
264
|
+
const params = store.global ? [tenantId, AGENT_MEMORY_SOURCE_PREFIX, origin] : [tenantId, AGENT_MEMORY_SOURCE_PREFIX];
|
|
265
|
+
// SAFETY: the SELECT names the three columns of the row type.
|
|
266
|
+
const rows = store.db.prepare(`SELECT id, content, source FROM memories
|
|
267
|
+
WHERE tenant_id = ? AND superseded_by IS NULL AND substr(source, 1, ${AGENT_MEMORY_SOURCE_PREFIX.length}) != ?${visible}`).all(...params);
|
|
268
|
+
return new Set(rows.filter((r) => !adopted.has(r.id)).flatMap(heldTextKeys));
|
|
269
|
+
}
|
|
270
|
+
/** Dry run only: kept rows of this scope in containers this run did not list (a moved project's old folder); `partition` limits it to one origin. */
|
|
271
|
+
function unlistedRows(db, tenantId, tool, scope, listing, platform, partition) {
|
|
272
|
+
const listed = listing.containers.map((c) => containerPrefix(tool.id, containerId(c.path, c.scope, platform, partition ?? '')));
|
|
273
|
+
return selectLiveEntriesBySourcePrefix(db, tenantId, `${toolSourcePrefix(tool.id)}${scope === 'project' ? 'p' : 'u'}-`)
|
|
274
|
+
.filter((row) => row.tags.includes(tool.tag) && !listed.some((p) => row.source.startsWith(p)))
|
|
275
|
+
.filter((row) => partition === null || (row.origin_project ?? '') === partition).length;
|
|
276
|
+
}
|
|
277
|
+
/** Design 2's handover: rows the store-less hook path left in the global store, under this project's origin, for containers its store now syncs. */
|
|
278
|
+
function handOver(synced, projectRoot, opts, report) {
|
|
279
|
+
const globalRoot = resolveGlobalRootDir();
|
|
280
|
+
if (synced.length === 0 || !isInitialized(globalRoot))
|
|
281
|
+
return;
|
|
282
|
+
let db;
|
|
283
|
+
try {
|
|
284
|
+
db = openHippoDb(globalRoot, { busyWaitMs: opts.busyWaitMs });
|
|
285
|
+
const tenantId = resolveTenantId({});
|
|
286
|
+
// A folder with no git and no marker wrote as '' before its store existed, and as its own name after.
|
|
287
|
+
const origins = [...new Set([deriveOriginProject(projectRoot), ''])];
|
|
288
|
+
for (const work of synced)
|
|
289
|
+
handOverContainer(db, globalRoot, tenantId, work, origins, opts.machine.platform, report);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
report.warnings.push(`global copies not handed over: ${isSqliteBusy(err) ? 'the global store was busy' : errorMessage(err)}`);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
if (db)
|
|
296
|
+
closeHippoDb(db);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function handOverContainer(db, root, tenantId, work, origins, platform, report) {
|
|
300
|
+
// A note the local pass could not read has no local row yet, so its global copy stays until it does.
|
|
301
|
+
const unread = new Set(work.container.skipped);
|
|
302
|
+
const mirror = [];
|
|
303
|
+
const purge = [];
|
|
304
|
+
db.exec('BEGIN IMMEDIATE');
|
|
305
|
+
try {
|
|
306
|
+
for (const origin of origins) {
|
|
307
|
+
const prefix = containerPrefix(work.tool.id, containerId(work.container.path, work.container.scope, platform, origin));
|
|
308
|
+
for (const row of selectLiveEntriesBySourcePrefix(db, tenantId, prefix)) {
|
|
309
|
+
if (!row.tags.includes(work.tool.tag) || unread.has(splitSource(row.source, prefix).key))
|
|
310
|
+
continue;
|
|
311
|
+
const result = setAsideRow(db, work.tool.tag, row, 'handover');
|
|
312
|
+
if (result.kind === 'untagged')
|
|
313
|
+
mirror.push(result.entry);
|
|
314
|
+
else
|
|
315
|
+
purge.push(result.id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
db.exec('COMMIT');
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
try {
|
|
322
|
+
db.exec('ROLLBACK');
|
|
323
|
+
}
|
|
324
|
+
catch { /* already rolled back; keep the original error */ }
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
toolReport(report, work.tool.id).tally.handedOver += mirror.length + purge.length;
|
|
328
|
+
afterCommit(root, { mirror, purge }, report);
|
|
329
|
+
}
|
|
330
|
+
function toolOf(id) {
|
|
331
|
+
const tool = AGENT_MEMORY_TOOLS.find((t) => t.id === id);
|
|
332
|
+
if (tool === undefined)
|
|
333
|
+
throw new Error(`agent memory sync: no tool ${id}`);
|
|
334
|
+
return tool;
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const AGENT_MEMORY_TOOLS: readonly [{
|
|
2
|
+
readonly id: "claude-code";
|
|
3
|
+
readonly tag: "claude-code-memory";
|
|
4
|
+
readonly label: "Claude Code";
|
|
5
|
+
}, {
|
|
6
|
+
readonly id: "codex";
|
|
7
|
+
readonly tag: "codex-memory";
|
|
8
|
+
readonly label: "Codex";
|
|
9
|
+
}, {
|
|
10
|
+
readonly id: "gemini";
|
|
11
|
+
readonly tag: "gemini-memory";
|
|
12
|
+
readonly label: "Gemini CLI";
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "copilot";
|
|
15
|
+
readonly tag: "copilot-memory";
|
|
16
|
+
readonly label: "Copilot";
|
|
17
|
+
}, {
|
|
18
|
+
readonly id: "openclaw";
|
|
19
|
+
readonly tag: "openclaw-memory";
|
|
20
|
+
readonly label: "OpenClaw";
|
|
21
|
+
}, {
|
|
22
|
+
readonly id: "qwen-code";
|
|
23
|
+
readonly tag: "qwen-code-memory";
|
|
24
|
+
readonly label: "Qwen Code";
|
|
25
|
+
}];
|
|
26
|
+
export type AgentMemoryTool = (typeof AGENT_MEMORY_TOOLS)[number];
|
|
27
|
+
export type ToolId = AgentMemoryTool['id'];
|
|
28
|
+
export declare const AGENT_MEMORY_SOURCE_PREFIX = "agent-memory:";
|
|
29
|
+
/** Every imported row of a tool has a source starting with this. */
|
|
30
|
+
export declare function toolSourcePrefix(id: ToolId): string;
|
|
31
|
+
export declare const AGENT_MEMORY_TAGS: readonly string[];
|
|
32
|
+
export declare function isToolId(value: string): value is ToolId;
|
|
33
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The agents whose own memories hippo imports. A leaf module: the keep rule, merge and share lists are built from it.
|
|
2
|
+
export const AGENT_MEMORY_TOOLS = [
|
|
3
|
+
{ id: 'claude-code', tag: 'claude-code-memory', label: 'Claude Code' },
|
|
4
|
+
{ id: 'codex', tag: 'codex-memory', label: 'Codex' },
|
|
5
|
+
{ id: 'gemini', tag: 'gemini-memory', label: 'Gemini CLI' },
|
|
6
|
+
{ id: 'copilot', tag: 'copilot-memory', label: 'Copilot' },
|
|
7
|
+
{ id: 'openclaw', tag: 'openclaw-memory', label: 'OpenClaw' },
|
|
8
|
+
{ id: 'qwen-code', tag: 'qwen-code-memory', label: 'Qwen Code' },
|
|
9
|
+
];
|
|
10
|
+
export const AGENT_MEMORY_SOURCE_PREFIX = 'agent-memory:';
|
|
11
|
+
/** Every imported row of a tool has a source starting with this. */
|
|
12
|
+
export function toolSourcePrefix(id) {
|
|
13
|
+
return `${AGENT_MEMORY_SOURCE_PREFIX}${id}:`;
|
|
14
|
+
}
|
|
15
|
+
export const AGENT_MEMORY_TAGS = AGENT_MEMORY_TOOLS.map((t) => t.tag);
|
|
16
|
+
export function isToolId(value) {
|
|
17
|
+
return AGENT_MEMORY_TOOLS.some((t) => t.id === value);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ToolId } from './tools.js';
|
|
2
|
+
export type Scope = 'project' | 'user';
|
|
3
|
+
/** All an adapter may know about the machine; it reads nothing else from the process, so tests can fake it all. */
|
|
4
|
+
export interface AdapterContext {
|
|
5
|
+
readonly home: string;
|
|
6
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
7
|
+
readonly platform: NodeJS.Platform;
|
|
8
|
+
readonly projectRoot?: string;
|
|
9
|
+
/** Claude Code's transcript at a hook: its folder's `memory/` holds that session's own notes. */
|
|
10
|
+
readonly transcriptPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MemoryItem {
|
|
13
|
+
/** A path inside the container with '/' separators, or `<heading slug>/<text hash>` in a single-file store. */
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly text: string;
|
|
16
|
+
readonly updatedAt: number;
|
|
17
|
+
}
|
|
18
|
+
/** A folder, or one file's memory section, that exists on disk. */
|
|
19
|
+
export interface Container {
|
|
20
|
+
readonly scope: Scope;
|
|
21
|
+
readonly path: string;
|
|
22
|
+
/** False when it exists but could not be read or failed its shape check, so nothing in it is set aside. */
|
|
23
|
+
readonly readable: boolean;
|
|
24
|
+
readonly items: readonly MemoryItem[];
|
|
25
|
+
/** Keys of items on disk that were not read (too big, not text, a failed read); their rows are left alone. */
|
|
26
|
+
readonly skipped: readonly string[];
|
|
27
|
+
readonly warnings: readonly string[];
|
|
28
|
+
/** Keys made from text, so an edit is matched by heading rather than by key. */
|
|
29
|
+
readonly textKeyed: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface Listing {
|
|
32
|
+
readonly tool: ToolId;
|
|
33
|
+
readonly home: string;
|
|
34
|
+
readonly containers: readonly Container[];
|
|
35
|
+
/** Problems finding containers, such as a malformed index file; an unlisted container is left alone. */
|
|
36
|
+
readonly warnings: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
export interface Adapter {
|
|
39
|
+
readonly tool: ToolId;
|
|
40
|
+
list(ctx: AdapterContext, scope: Scope): Listing;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/api.d.ts
CHANGED
|
@@ -274,6 +274,9 @@ export interface RecallOpts {
|
|
|
274
274
|
* callers leave this unset and get the trace.
|
|
275
275
|
*/
|
|
276
276
|
suppressRecallTrace?: boolean;
|
|
277
|
+
/** Set only by the MCP recall tool, which ranks with its own scorer and drops copies from its own final list: this call
|
|
278
|
+
* then keeps a memory that a merged row in the same result holds word for word. Other callers leave it unset. */
|
|
279
|
+
keepHeldCopies?: boolean;
|
|
277
280
|
}
|
|
278
281
|
export interface ContinuityBlock {
|
|
279
282
|
activeSnapshot: TaskSnapshot | null;
|
|
@@ -373,7 +376,7 @@ export interface RecallResult {
|
|
|
373
376
|
* the calling agent sees its track record at the moment of forecasting
|
|
374
377
|
* (Lovallo-Kahneman 2003 inside-vs-outside view).
|
|
375
378
|
*
|
|
376
|
-
* Populated by `api.recall` itself via `
|
|
379
|
+
* Populated by `api.recall` itself via `computePlanningFallacyOutput`.
|
|
377
380
|
* Pipeline-invariant: the value depends only on (queryText, tenantId,
|
|
378
381
|
* predictions table state) — all three are identical regardless of
|
|
379
382
|
* which downstream search pipeline produces the memory list, so MCP
|
|
@@ -553,6 +556,11 @@ export interface AssembleOpts {
|
|
|
553
556
|
* is set on the result so the caller knows to widen.
|
|
554
557
|
*/
|
|
555
558
|
rowCap?: number;
|
|
559
|
+
cost?: AssembleCost;
|
|
560
|
+
}
|
|
561
|
+
export interface AssembleCost {
|
|
562
|
+
item: (it: AssembledContextItem) => number;
|
|
563
|
+
fixed: (widest: number) => number;
|
|
556
564
|
}
|
|
557
565
|
export interface AssembledContextItem {
|
|
558
566
|
id: string;
|
|
@@ -623,7 +631,7 @@ export interface DrillDownOpts {
|
|
|
623
631
|
/**
|
|
624
632
|
* Optional token budget. When set, children are appended in chronological
|
|
625
633
|
* order (created ASC) until adding the next child would exceed the budget.
|
|
626
|
-
* Token cost = ceil(content.length / 4)
|
|
634
|
+
* Token cost = the child's printed line under `cost`, else ceil(content.length / 4).
|
|
627
635
|
*
|
|
628
636
|
* For depth > 1, the budget is GLOBAL cumulative (NOT per-level).
|
|
629
637
|
*/
|
|
@@ -636,22 +644,29 @@ export interface DrillDownOpts {
|
|
|
636
644
|
* construction).
|
|
637
645
|
*/
|
|
638
646
|
depth?: number;
|
|
647
|
+
cost?: DrillDownCost;
|
|
648
|
+
}
|
|
649
|
+
export interface DrillDownSummary {
|
|
650
|
+
id: string;
|
|
651
|
+
content: string;
|
|
652
|
+
descendantCount: number;
|
|
653
|
+
earliestAt: string | null;
|
|
654
|
+
latestAt: string | null;
|
|
655
|
+
}
|
|
656
|
+
export interface DrillDownChild {
|
|
657
|
+
id: string;
|
|
658
|
+
content: string;
|
|
659
|
+
layer: string;
|
|
660
|
+
dagLevel: number;
|
|
661
|
+
created: string;
|
|
662
|
+
}
|
|
663
|
+
export interface DrillDownCost {
|
|
664
|
+
child: (c: DrillDownChild) => number;
|
|
665
|
+
fixed: (summary: DrillDownSummary, widest: number) => number;
|
|
639
666
|
}
|
|
640
667
|
export interface DrillDownResult {
|
|
641
|
-
summary:
|
|
642
|
-
|
|
643
|
-
content: string;
|
|
644
|
-
descendantCount: number;
|
|
645
|
-
earliestAt: string | null;
|
|
646
|
-
latestAt: string | null;
|
|
647
|
-
};
|
|
648
|
-
children: Array<{
|
|
649
|
-
id: string;
|
|
650
|
-
content: string;
|
|
651
|
-
layer: string;
|
|
652
|
-
dagLevel: number;
|
|
653
|
-
created: string;
|
|
654
|
-
}>;
|
|
668
|
+
summary: DrillDownSummary;
|
|
669
|
+
children: DrillDownChild[];
|
|
655
670
|
totalChildren: number;
|
|
656
671
|
truncated: boolean;
|
|
657
672
|
}
|
|
@@ -955,10 +970,28 @@ export interface ContextOpts {
|
|
|
955
970
|
currentSessionId?: string | null;
|
|
956
971
|
/** Z1: raw hook-payload prompt; only the pinned-only branch reads it, gated on `pinnedInject.promptRecall`. */
|
|
957
972
|
prompt?: string;
|
|
973
|
+
/** What the budget pays for, from the caller that renders the block. Absent = the memory text alone. */
|
|
974
|
+
cost?: ContextCost;
|
|
975
|
+
}
|
|
976
|
+
/** Budget prices in the text a caller prints, so the budget bounds what reaches the model. */
|
|
977
|
+
export interface ContextCost {
|
|
978
|
+
/** Tokens of one entry as printed. */
|
|
979
|
+
entry: (item: Pick<ContextResultEntry, 'entry' | 'isGlobal' | 'promptRecall' | 'origin' | 'category'>) => number;
|
|
980
|
+
/** Tokens of the headers and footer the block can print at this budget, reserved before any entry. */
|
|
981
|
+
fixed: (budget: number, can: {
|
|
982
|
+
cross: boolean;
|
|
983
|
+
promptRecall: boolean;
|
|
984
|
+
ambient: boolean;
|
|
985
|
+
}) => number;
|
|
986
|
+
/** Tokens of the sections printed ahead of the memories, each as printed. */
|
|
987
|
+
snapshot: (s: TaskSnapshot) => number;
|
|
988
|
+
handoff: (h: SessionHandoff) => number;
|
|
989
|
+
trail: (events: SessionEvent[]) => number;
|
|
958
990
|
}
|
|
959
991
|
export interface ContextResultEntry {
|
|
960
992
|
entry: MemoryEntry;
|
|
961
993
|
score: number;
|
|
994
|
+
/** What this entry cost the budget: its printed line under `ContextOpts.cost`, else its memory text. */
|
|
962
995
|
tokens: number;
|
|
963
996
|
isGlobal?: boolean;
|
|
964
997
|
isFreshTail?: boolean;
|
|
@@ -1005,7 +1038,7 @@ export declare function getContext(ctx: Context, opts?: ContextOpts): Promise<Co
|
|
|
1005
1038
|
* (consolidate + dedup + audit + share + ambient) and return structured counts.
|
|
1006
1039
|
*
|
|
1007
1040
|
* Extracted from `cmdSleepCore` Phase 2-6 in Episode A. NOT covered by api.sleep:
|
|
1008
|
-
* the cli-only auto-learn phase (Phase 1: learnFromRepo +
|
|
1041
|
+
* the cli-only auto-learn phase (Phase 1: learnFromRepo + the agent memory import),
|
|
1009
1042
|
* which is intrinsically host-bound (uses `process.cwd()` / `os.homedir()`).
|
|
1010
1043
|
* Auto-learn stays in cli.ts cmdSleepCore as a pre-api block.
|
|
1011
1044
|
*
|
|
@@ -1037,8 +1070,8 @@ export declare function recordTokens(ctx: Context, surface: TokenSurface, use: {
|
|
|
1037
1070
|
}): void;
|
|
1038
1071
|
/**
|
|
1039
1072
|
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
1040
|
-
* tokens sent
|
|
1041
|
-
*
|
|
1073
|
+
* tokens sent, skipped as unchanged and re-read by later model calls, per
|
|
1074
|
+
* surface, with session counts and mean tokens per session.
|
|
1042
1075
|
*/
|
|
1043
1076
|
export declare function tokenSummary(ctx: Context, opts?: {
|
|
1044
1077
|
days?: number;
|
|
@@ -1162,7 +1195,7 @@ export interface SleepResult {
|
|
|
1162
1195
|
* api.sleep itself will need to scope dedup / audit / delete by ctx.tenantId.
|
|
1163
1196
|
*
|
|
1164
1197
|
* Dedup and audit deletes each log a `forget` row with the ctx actor and a
|
|
1165
|
-
* `metadata.reason`. Pinned and
|
|
1198
|
+
* `metadata.reason`. Pinned, raw and kept compaction-memory rows are never auto-deleted (canAutoDelete).
|
|
1166
1199
|
* dryRun previews consolidate, dedup and audit, then returns before share/ambient.
|
|
1167
1200
|
*/
|
|
1168
1201
|
/**
|