hippo-memory 1.56.0 → 1.58.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.
Files changed (129) hide show
  1. package/README.md +11 -0
  2. package/dist/agent-memories/claude-code.js +1 -1
  3. package/dist/agent-memories/gemini.js +1 -1
  4. package/dist/api-errors.d.ts +27 -0
  5. package/dist/api-errors.js +37 -0
  6. package/dist/api.d.ts +21 -14
  7. package/dist/api.js +97 -71
  8. package/dist/audit.d.ts +4 -0
  9. package/dist/audit.js +11 -0
  10. package/dist/autolearn.d.ts +1 -1
  11. package/dist/autolearn.js +7 -5
  12. package/dist/capture-contract.d.ts +47 -0
  13. package/dist/capture-contract.js +49 -0
  14. package/dist/capture-error.js +2 -1
  15. package/dist/capture.d.ts +0 -13
  16. package/dist/capture.js +5 -66
  17. package/dist/card-detail.d.ts +1 -1
  18. package/dist/card-detail.js +1 -1
  19. package/dist/cli/shared.d.ts +137 -0
  20. package/dist/cli/shared.js +834 -0
  21. package/dist/cli/sleep.d.ts +10 -0
  22. package/dist/cli/sleep.js +171 -0
  23. package/dist/cli.d.ts +0 -7
  24. package/dist/cli.js +322 -1827
  25. package/dist/client.js +9 -0
  26. package/dist/codex-patch.js +1 -1
  27. package/dist/compaction-record.d.ts +1 -1
  28. package/dist/compaction-record.js +3 -2
  29. package/dist/config.d.ts +5 -0
  30. package/dist/config.js +17 -0
  31. package/dist/connectors/github/dlq.js +5 -2
  32. package/dist/connectors/github/octokit-client.js +4 -2
  33. package/dist/connectors/github/webhook.d.ts +19 -0
  34. package/dist/connectors/github/webhook.js +313 -0
  35. package/dist/connectors/slack/dlq.js +6 -2
  36. package/dist/connectors/slack/web-client.js +7 -5
  37. package/dist/connectors/slack/webhook.d.ts +22 -0
  38. package/dist/connectors/slack/webhook.js +203 -0
  39. package/dist/consolidate.d.ts +10 -0
  40. package/dist/consolidate.js +38 -35
  41. package/dist/context-auto.d.ts +3 -0
  42. package/dist/context-auto.js +34 -0
  43. package/dist/customer-notes.js +16 -14
  44. package/dist/dag.js +3 -2
  45. package/dist/dashboard.js +3 -2
  46. package/dist/db.d.ts +12 -0
  47. package/dist/db.js +62 -1
  48. package/dist/decisions.js +11 -9
  49. package/dist/doctor.js +5 -0
  50. package/dist/embedding-provider.js +3 -3
  51. package/dist/embeddings.d.ts +4 -4
  52. package/dist/embeddings.js +72 -16
  53. package/dist/eval-stats.d.ts +58 -0
  54. package/dist/eval-stats.js +111 -0
  55. package/dist/extract.js +3 -2
  56. package/dist/goals.d.ts +49 -25
  57. package/dist/goals.js +39 -22
  58. package/dist/graph-extract.js +1 -1
  59. package/dist/graph-recall.d.ts +1 -1
  60. package/dist/graph-recall.js +1 -1
  61. package/dist/graph.js +1 -1
  62. package/dist/hooks.d.ts +1 -3
  63. package/dist/hooks.js +2 -4
  64. package/dist/http-retry.d.ts +21 -0
  65. package/dist/http-retry.js +50 -0
  66. package/dist/http-util.d.ts +39 -0
  67. package/dist/http-util.js +56 -0
  68. package/dist/importers.d.ts +2 -0
  69. package/dist/importers.js +16 -5
  70. package/dist/incidents.js +13 -11
  71. package/dist/index.d.ts +5 -2
  72. package/dist/index.js +5 -2
  73. package/dist/judgment.js +10 -17
  74. package/dist/log.d.ts +25 -0
  75. package/dist/log.js +48 -0
  76. package/dist/mcp/server.js +224 -308
  77. package/dist/mcp/tool-args.d.ts +21 -0
  78. package/dist/mcp/tool-args.js +80 -0
  79. package/dist/memory.d.ts +19 -0
  80. package/dist/memory.js +41 -2
  81. package/dist/overlap-index.d.ts +7 -0
  82. package/dist/overlap-index.js +38 -0
  83. package/dist/pilot-arm.d.ts +9 -0
  84. package/dist/pilot-arm.js +47 -0
  85. package/dist/policies.js +14 -12
  86. package/dist/predictions.js +11 -9
  87. package/dist/processes.js +16 -14
  88. package/dist/project-briefs.js +19 -16
  89. package/dist/project-identity.d.ts +1 -1
  90. package/dist/project-identity.js +25 -1
  91. package/dist/prompt-recall.js +1 -1
  92. package/dist/raw-archive.js +7 -6
  93. package/dist/recall-history.d.ts +5 -0
  94. package/dist/recall-history.js +9 -0
  95. package/dist/recall-pipeline.d.ts +101 -0
  96. package/dist/recall-pipeline.js +313 -0
  97. package/dist/recall-scope.d.ts +24 -1
  98. package/dist/recall-scope.js +29 -2
  99. package/dist/refine-llm.js +3 -2
  100. package/dist/reject-flow.js +6 -9
  101. package/dist/rejection.d.ts +2 -1
  102. package/dist/rejection.js +2 -1
  103. package/dist/search.d.ts +0 -20
  104. package/dist/search.js +16 -51
  105. package/dist/secret-detect.d.ts +13 -1
  106. package/dist/secret-detect.js +33 -1
  107. package/dist/server.d.ts +3 -1
  108. package/dist/server.js +1854 -2566
  109. package/dist/session-digest.js +2 -1
  110. package/dist/shared.js +7 -6
  111. package/dist/skills.js +17 -15
  112. package/dist/store-cards.d.ts +53 -0
  113. package/dist/store-cards.js +512 -0
  114. package/dist/store.d.ts +2 -89
  115. package/dist/store.js +10 -566
  116. package/dist/tenant.d.ts +22 -0
  117. package/dist/tenant.js +26 -0
  118. package/dist/token-ledger.d.ts +4 -2
  119. package/dist/token-ledger.js +2 -2
  120. package/dist/tokenize.d.ts +2 -0
  121. package/dist/tokenize.js +8 -0
  122. package/dist/version.d.ts +1 -1
  123. package/dist/version.js +1 -1
  124. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  125. package/extensions/openclaw-plugin/package.json +1 -1
  126. package/openclaw.plugin.json +1 -1
  127. package/package.json +1 -1
  128. package/dist/connectors/slack/ratelimit.d.ts +0 -9
  129. package/dist/connectors/slack/ratelimit.js +0 -18
@@ -0,0 +1,834 @@
1
+ // Helpers two or more CLI verbs use, split from cli.ts so a verb can move to its own file without importing cli.ts.
2
+ // This module must never import cli.ts.
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { execFileSync, execSync } from 'child_process';
6
+ import { installJsonHooks, CODEX_TRUST_LINE } from '../hooks.js';
7
+ import { confidenceLabel, computeSchemaFit, createMemory, Layer } from '../memory.js';
8
+ import { isInitialized, loadAllEntries, writeEntry, updateStats } from '../store.js';
9
+ import { RejectedValueError } from '../rejection.js';
10
+ import { explainMatch } from '../search.js';
11
+ import { embedMemory } from '../embeddings.js';
12
+ import { loadConfig } from '../config.js';
13
+ import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy } from '../db.js';
14
+ import { hookPayloadSessionId, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
15
+ import { isGitRepo, fetchGitLog, extractLessons, partitionLessons } from '../autolearn.js';
16
+ import { storedTextKeys, duplicateKey } from '../same-text.js';
17
+ import { importAtSessionEnd, currentMachine } from '../agent-memories/sync.js';
18
+ import { summaryLine } from '../agent-memories/report.js';
19
+ import { detectChurnStale, extractInvalidationTarget, invalidateMatching } from '../invalidation.js';
20
+ import { resolveProjectIdentity } from '../project-identity.js';
21
+ import { extractPathTags } from '../path-context.js';
22
+ import { getGlobalRoot, initGlobal } from '../shared.js';
23
+ import { DAILY_TASK_NAME, buildDailyRunnerCommand, buildSchtasksCreateArgs, buildWindowsTaskRun } from '../scheduler.js';
24
+ import { sanitizeLogMessage } from '../capture.js';
25
+ import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
26
+ import { createHash } from 'node:crypto';
27
+ import * as client from '../client.js';
28
+ import { detectServer, removePidfileIfOwned } from '../server-detect.js';
29
+ import { resolveTenantId } from '../tenant.js';
30
+ import { snapshotText, sessionTrailText, handoffText } from '../context-render.js';
31
+ export function parseLimitFlag(value) {
32
+ if (!value)
33
+ return Infinity;
34
+ const parsed = parseInt(String(value), 10);
35
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : Infinity;
36
+ }
37
+ export function parseCountFlag(value) {
38
+ if (!value || value === true || Array.isArray(value))
39
+ return 0;
40
+ const parsed = parseInt(String(value), 10);
41
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : 0;
42
+ }
43
+ export function parseBudgetFlag(value, fallback) {
44
+ if (value === undefined)
45
+ return fallback;
46
+ // A value-less flag and a junk value are different typos; the --hops guard already splits them.
47
+ if (typeof value !== 'string') {
48
+ console.error('--budget requires an integer value (e.g. --budget 1500).');
49
+ process.exit(1);
50
+ }
51
+ // Number(), like the --hops guard: parseInt('12abc') is 12, silently accepting what this message rejects.
52
+ const parsed = Number(value);
53
+ if (!Number.isInteger(parsed) || parsed < 0) {
54
+ console.error(`Invalid --budget: "${value}". Must be a non-negative integer.`);
55
+ process.exit(1);
56
+ }
57
+ return parsed;
58
+ }
59
+ /**
60
+ * Emit an audit event against `hippoRoot`'s db. Opens its own short-lived
61
+ * connection so callers don't have to thread a db handle. Swallows all errors
62
+ * — audit must never crash a CLI command.
63
+ */
64
+ export function emitCliAudit(hippoRoot, op, targetId, metadata) {
65
+ try {
66
+ const db = openHippoDb(hippoRoot);
67
+ try {
68
+ appendAuditEvent(db, {
69
+ tenantId: resolveTenantId({}),
70
+ actor: 'cli',
71
+ op,
72
+ targetId,
73
+ metadata,
74
+ });
75
+ }
76
+ finally {
77
+ closeHippoDb(db);
78
+ }
79
+ }
80
+ catch (error) {
81
+ // Best effort: the command already did its work.
82
+ reportAuditWriteFailure(op, String(error), targetId);
83
+ }
84
+ }
85
+ export function requireInit(hippoRoot) {
86
+ if (!isInitialized(hippoRoot)) {
87
+ console.error(`No hippo store at ${hippoRoot} (searched ${process.cwd()} and its parents up to your home directory). Run \`hippo init\` first.`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+ /** Runs detectChurnStale against every store this repo's memories can live in. */
92
+ export function runChurnStaleForRepo(hippoRoot, dryRun) {
93
+ const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8', windowsHide: true }).trim();
94
+ const projectName = resolveProjectIdentity(process.cwd()).name;
95
+ const globalRoot = getGlobalRoot();
96
+ const roots = globalRoot !== hippoRoot && isInitialized(globalRoot) ? [hippoRoot, globalRoot] : [hippoRoot];
97
+ const tenantId = resolveTenantId({});
98
+ return roots.map((root) => {
99
+ // One store failing must not abort sleep's later phases or skip the other store.
100
+ try {
101
+ return { root, result: detectChurnStale(root, repoRoot, { tenantId, projectName, dryRun }) };
102
+ }
103
+ catch (err) {
104
+ const message = err instanceof Error ? err.message : String(err);
105
+ return { root, result: { checked: 0, marked: 0, alreadyMarked: 0, skippedPinned: [], dryRun, preview: [], error: message } };
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * When HIPPO_REQUIRE_SERVER is set, the CLI must not silently fall back to
111
+ * direct DB mode — a missing server then masks a real misconfiguration (the
112
+ * configured HIPPO_API_KEY is also silently discarded on fallback). Throws a
113
+ * clear error then. It guards only the routed writes (remember, forget, archive,
114
+ * promote); every other command opens the store directly, knob or not.
115
+ */
116
+ function failIfServerRequired(reason) {
117
+ if (process.env['HIPPO_REQUIRE_SERVER']) {
118
+ throw new Error(`hippo: HIPPO_REQUIRE_SERVER is set but ${reason}. ` +
119
+ `Start \`hippo serve\`, or unset HIPPO_REQUIRE_SERVER to allow direct-mode fallback.`);
120
+ }
121
+ }
122
+ /**
123
+ * Run an HTTP-routed command if a `hippo serve` instance is detected for
124
+ * `hippoRoot`. Returns:
125
+ * - true if the HTTP path ran (success OR a structured server error that
126
+ * was already surfaced to stdout/stderr by `httpFn`),
127
+ * - false if no server was detected, or if the detected pidfile turned out
128
+ * to be stale (connection refused). On stale, the pidfile is removed
129
+ * if it still names that dead server (a newer one may have replaced
130
+ * it) and the caller should fall back to the direct path.
131
+ *
132
+ * Stale pidfiles must self-heal, not crash.
133
+ * When HIPPO_REQUIRE_SERVER is set, both fallback paths throw instead of
134
+ * returning false, so a missing server fails loudly rather than silently
135
+ * degrading to direct mode.
136
+ */
137
+ export async function runViaServerIfAvailable(hippoRoot, httpFn) {
138
+ const info = await detectServer(hippoRoot);
139
+ if (!info) {
140
+ failIfServerRequired('no running server was detected for this hippoRoot');
141
+ return false;
142
+ }
143
+ const apiKey = process.env['HIPPO_API_KEY'];
144
+ try {
145
+ await httpFn(info, apiKey);
146
+ return true;
147
+ }
148
+ catch (err) {
149
+ const failure = client.classifyTransportFailure(err);
150
+ if (failure === 'never-sent') {
151
+ failIfServerRequired('the server pidfile was stale (connection refused)');
152
+ console.error('hippo: stale server pidfile detected, falling back to direct mode');
153
+ // Clear the pidfile only if it still names the dead server we just
154
+ // probed — a newer server may have rewritten it (removePidfileIfOwned).
155
+ removePidfileIfOwned(hippoRoot, { pid: info.pid, startedAt: info.started_at });
156
+ return false;
157
+ }
158
+ if (failure === 'delivery-unknown') {
159
+ // Every caller of this helper is a non-idempotent write, so replaying on
160
+ // the direct path would store a row the server may already have committed.
161
+ // Leave the pidfile alone: the next command's connect-phase failure heals it.
162
+ console.error(`hippo: the connection to ${info.url} dropped or timed out mid-request, so the write may already have been applied. Not retrying locally. Check with \`hippo recall\` before running this again.`);
163
+ process.exit(1);
164
+ }
165
+ throw err;
166
+ }
167
+ }
168
+ export function fmt(n, digits = 2) {
169
+ return n.toFixed(digits);
170
+ }
171
+ // What `hippo recall` prints for one result; the budget prices this same text.
172
+ export function recallEntryText(r, query, showWhy, isGlobal) {
173
+ const e = r.entry;
174
+ const label = confidenceLabel(e);
175
+ const confLabel = label.warn ? `[${label.text}] ⚠️` : `[${label.text}]`;
176
+ const bars = Math.round(e.strength * 10);
177
+ const graphMark = r.graphVia ? ` [graph: ${r.graphVia.hops}hop ${r.graphVia.relType}]` : '';
178
+ const lines = [
179
+ `--- ${e.id} [${e.layer}] ${confLabel}${isGlobal ? ' [global]' : ''}${e.superseded_by ? ' [superseded]' : ''}${graphMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`,
180
+ ` [${'█'.repeat(bars)}${'░'.repeat(10 - bars)}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`,
181
+ ];
182
+ if (showWhy) {
183
+ const explanation = explainMatch(query, r);
184
+ lines.push(` source:${isGlobal ? ' [global]' : ' [local]'} | layer: [${e.layer}] | confidence: [${label.text}]`, ` reason: ${explanation.reason}`);
185
+ const env = explanation.envelope;
186
+ if (env) {
187
+ lines.push(` kind: ${env.kind}`);
188
+ if (env.scope)
189
+ lines.push(` scope: ${env.scope}`);
190
+ if (env.owner)
191
+ lines.push(` owner: ${env.owner}`);
192
+ if (env.artifact_ref)
193
+ lines.push(` artifact_ref: ${env.artifact_ref}`);
194
+ if (env.session_id)
195
+ lines.push(` session_id: ${env.session_id}`);
196
+ lines.push(` confidence: ${env.confidence}`);
197
+ }
198
+ // The recall trace, e.g. "ranking: base 0.420 -> interference x0.30 -> 0.126 -> goal-boost x1.50 -> 0.189".
199
+ if (r.rerankTrace && r.rerankTrace.length > 0) {
200
+ const parts = [`base ${fmt(r.rerankTrace[0].scoreBefore, 3)}`];
201
+ for (const step of r.rerankTrace) {
202
+ parts.push(`${step.stage}${step.multiplier !== undefined ? ` x${fmt(step.multiplier, 2)}` : ''}`, fmt(step.scoreAfter, 3));
203
+ }
204
+ lines.push(` ranking: ${parts.join(' -> ')}`);
205
+ }
206
+ }
207
+ lines.push('', e.content, '');
208
+ return lines.join('\n');
209
+ }
210
+ export function recallHeading(entries, tokens, query) {
211
+ return `Found ${entries} memories (${tokens} tokens) for: "${query}"\n`;
212
+ }
213
+ /** One line when an agent memory import moved anything; its warnings go to stderr. */
214
+ export function printAgentImport(report, indent = ' ') {
215
+ const line = summaryLine(report);
216
+ if (line !== null)
217
+ console.log(`${indent}${line}`);
218
+ for (const warning of report.warnings)
219
+ console.error(`hippo: agent memories: ${warning}`);
220
+ }
221
+ /** The first hippo block in `text` and the agent whose current or shipped text it is; `owner` is undefined for an edited block. */
222
+ export function hippoBlock(text) {
223
+ const at = text.indexOf(HOOK_MARKERS.start);
224
+ const start = at + HOOK_MARKERS.start.length;
225
+ const end = text.indexOf(HOOK_MARKERS.end, start);
226
+ if (at < 0 || end < 0)
227
+ return null;
228
+ // git autocrlf checks these files out with CRLF: match as LF, write back in the file's own ending.
229
+ const raw = text.slice(start, end);
230
+ const inner = raw.replace(/\r\n/g, '\n').trim();
231
+ const owner = Object.keys(HOOKS).find((k) => HOOKS[k].content === inner) ?? SHIPPED_HOOK_HASHES.get(createHash('sha256').update(inner).digest('hex'));
232
+ return { start, end, eol: raw.includes('\r\n') ? '\r\n' : '\n', inner, owner };
233
+ }
234
+ /** Adds hippo's two Codex hooks and says what changed; each install ends on the trust reminder, since Codex skips an untrusted hook. */
235
+ export function installCodexMemoryHooks(indent) {
236
+ const result = installJsonHooks('codex');
237
+ if (result.invalidJson) {
238
+ console.log(`${indent}WARNING: ${result.settingsPath} is not a hooks file hippo can merge into; fix it, then run \`hippo hook install codex\`.`);
239
+ return;
240
+ }
241
+ const added = [
242
+ result.installedUserPromptSubmit ? 'UserPromptSubmit' : '',
243
+ result.installedCompactResume ? 'SessionStart(compact)' : '',
244
+ ].filter(Boolean);
245
+ console.log(added.length > 0
246
+ ? `${indent}Installed hippo's Codex memory hooks (${added.join(', ')}) in ${result.settingsPath}`
247
+ : `${indent}hippo's Codex memory hooks already in ${result.settingsPath}`);
248
+ console.log(`${indent}${CODEX_TRUST_LINE}`);
249
+ }
250
+ /**
251
+ * Set up a machine-level daily runner that sweeps all registered Hippo
252
+ * workspaces.
253
+ * Linux/macOS: writes to user crontab.
254
+ * Windows: creates a scheduled task.
255
+ * Skips if already installed.
256
+ */
257
+ export function setupDailySchedule(globalRoot) {
258
+ const runnerDir = path.resolve(globalRoot);
259
+ // Reject paths with characters that could break shell/crontab quoting
260
+ // (backslash is normal on Windows, only dangerous in Unix shell/crontab)
261
+ const unsafeChars = process.platform === 'win32' ? /["`$%\n\r]/ : /["`$\n\r\\]/;
262
+ if (unsafeChars.test(runnerDir)) {
263
+ console.log(` Skipping schedule: runner path contains unsafe characters.`);
264
+ return;
265
+ }
266
+ const isWindows = process.platform === 'win32';
267
+ const taskName = DAILY_TASK_NAME;
268
+ const cmd = buildDailyRunnerCommand(runnerDir);
269
+ if (isWindows) {
270
+ // Check if task already exists
271
+ try {
272
+ const existing = execSync(`schtasks /query /tn "${taskName}" 2>nul`, { encoding: 'utf-8', windowsHide: true });
273
+ if (existing.includes(taskName)) {
274
+ return; // already scheduled
275
+ }
276
+ }
277
+ catch {
278
+ // Task doesn't exist, create it
279
+ }
280
+ try {
281
+ execFileSync('schtasks', buildSchtasksCreateArgs(taskName, cmd), { stdio: 'pipe', windowsHide: true });
282
+ console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
283
+ }
284
+ catch {
285
+ // No admin rights or schtasks unavailable, fall back to printing instructions
286
+ console.log(` To schedule the machine-level daily runner, run:`);
287
+ console.log(` schtasks /create /tn "${taskName}" /tr "${buildWindowsTaskRun(cmd).replace(/"/g, '\\"')}" /sc daily /st 06:15`);
288
+ }
289
+ }
290
+ else {
291
+ // Unix: check crontab for existing entry
292
+ const marker = `# hippo:${taskName}`;
293
+ try {
294
+ const existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8', windowsHide: true });
295
+ if (existing.includes(marker)) {
296
+ return; // already scheduled
297
+ }
298
+ const cronLine = `15 6 * * * ${cmd} ${marker}`;
299
+ const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
300
+ execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
301
+ console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
302
+ }
303
+ catch {
304
+ const cronLine = `15 6 * * * ${cmd}`;
305
+ console.log(` To schedule the machine-level daily runner, add to crontab (crontab -e):`);
306
+ console.log(` ${cronLine}`);
307
+ }
308
+ }
309
+ }
310
+ export function parseAsOfFlag(flags) {
311
+ const asOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
312
+ if (asOf !== undefined && Number.isNaN(new Date(asOf).getTime())) {
313
+ console.error(`Error: --as-of value "${asOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
314
+ process.exit(1);
315
+ }
316
+ return asOf;
317
+ }
318
+ /** --physics forces physics, --classic forces BM25+cosine, else physics unless the config turns it off. */
319
+ export function engineFlags(flags, config) {
320
+ return {
321
+ usePhysics: Boolean(flags['physics']) || (!flags['classic'] && config.physics.enabled !== false),
322
+ physicsConfig: config.physics,
323
+ mmr: !flags['no-mmr'] && config.mmr.enabled,
324
+ mmrLambda: flags['mmr-lambda'] !== undefined ? parseFloat(String(flags['mmr-lambda'])) : config.mmr.lambda,
325
+ localBump: flags['equal-sources']
326
+ ? 1.0
327
+ : flags['local-bump'] !== undefined ? parseFloat(String(flags['local-bump'])) : config.search.localBump,
328
+ };
329
+ }
330
+ /**
331
+ * Detached worker that counts re-reads, runs sleep, then capture. Invoked via the internal
332
+ * `__session-end-worker` subcommand (not user-facing). Failures in one stage
333
+ * do not block the other.
334
+ */
335
+ // Best-effort git state; a missing git, non-repo cwd, or the timeout all
336
+ // yield null fields rather than throw (autolearn.ts execFileSync shape).
337
+ export function collectHandoffEvidence(cwd, testStatus) {
338
+ let gitRef = null;
339
+ try {
340
+ gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
341
+ cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
342
+ }).trim() || null;
343
+ }
344
+ catch {
345
+ gitRef = null;
346
+ }
347
+ let dirtyTree = null;
348
+ try {
349
+ const status = execFileSync('git', ['status', '--porcelain'], {
350
+ cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
351
+ });
352
+ dirtyTree = status.trim().length > 0;
353
+ }
354
+ catch {
355
+ dirtyTree = null;
356
+ }
357
+ return { gitRef, dirtyTree, testStatus };
358
+ }
359
+ /** A folder without its own store never sleeps at session end, so its project's agent notes go to the global store here. */
360
+ export function logSessionEndImport(logFile, transcriptPath) {
361
+ try {
362
+ const report = importAtSessionEnd(process.cwd(), transcriptPath, { machine: currentMachine() });
363
+ const line = summaryLine(report);
364
+ if (line !== null)
365
+ appendSessionEndCloseLog(logFile, line);
366
+ for (const warning of report.warnings)
367
+ appendSessionEndCloseLog(logFile, `agent memories: ${warning}`);
368
+ }
369
+ catch (err) {
370
+ appendSessionEndCloseLog(logFile, `agent memory import failed: ${err instanceof Error ? err.message : String(err)}`);
371
+ }
372
+ }
373
+ /**
374
+ * Best-effort log line for the snapshot-close step in
375
+ * `cmdSessionEndWorker`. `cmdSleep`/`cmdCapture` each tee console output to
376
+ * `logFile` only for their own duration (the tee is restored before this
377
+ * runs), so a plain `console.log` here would be silently discarded under
378
+ * the detached worker's `stdio: 'ignore'` — write straight to the file
379
+ * instead, matching capture.ts's `appendPreCompactLog` convention.
380
+ */
381
+ export function appendSessionEndCloseLog(logFile, message, opts = {}) {
382
+ if (!logFile)
383
+ return;
384
+ try {
385
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
386
+ // sanitizeLogMessage: `message` interpolates the payload-controlled
387
+ // session_id — same log-forgery guard appendPreCompactLog applies.
388
+ const write = opts.startFresh ? fs.writeFileSync : fs.appendFileSync;
389
+ write(logFile, `[hippo] ${new Date().toISOString()} ${sanitizeLogMessage(message)}\n`, 'utf8');
390
+ }
391
+ catch {
392
+ // Best-effort only — never let a log-write failure surface as an error.
393
+ }
394
+ }
395
+ export function printActiveTaskSnapshot(snapshot) {
396
+ console.log(snapshotText(snapshot));
397
+ }
398
+ export function printSessionEvents(events) {
399
+ console.log(events.length === 0 ? 'No session events found.' : sessionTrailText(events));
400
+ }
401
+ export function printHandoff(handoff) {
402
+ console.log(handoffText(handoff));
403
+ }
404
+ // parseArgs turns a value-less flag into `true`; refuse rather than silently
405
+ // stringifying it (String(true) === 'true'), mirroring cmdHandoff's guard.
406
+ export function cardStringFlag(flags, key) {
407
+ const v = flags[key];
408
+ if (v === undefined)
409
+ return undefined;
410
+ if (v === true || v === false || Array.isArray(v)) {
411
+ console.error(`--${key} requires a value`);
412
+ process.exit(1);
413
+ }
414
+ return v.trim();
415
+ }
416
+ // Claude Code exports its own session var, not ours; without the fallback agent-run recalls trace with no session.
417
+ export function hostSessionId() {
418
+ return process.env.HIPPO_SESSION_ID?.trim() || process.env.CLAUDE_CODE_SESSION_ID?.trim() || undefined;
419
+ }
420
+ /**
421
+ * Compaction drops the pinned blocks the per-prompt hook injected
422
+ * earlier, so record a `reset` for the payload's session and the next prompt
423
+ * injects again even if nothing changed. `requiredSource` limits it to hook
424
+ * payloads with that `source` (SessionStart fires for other reasons too).
425
+ * Best-effort and silent: a malformed payload records nothing.
426
+ */
427
+ export function resetHookInjection(hippoRoot, stdinText, requiredSource) {
428
+ const sessionId = hookPayloadSessionId(stdinText, requiredSource);
429
+ // A sub-agent's compaction leaves its parent's context, and the blocks in it, as they were.
430
+ if (sessionId === null || isSubagentPayload(stdinText))
431
+ return;
432
+ withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
433
+ tenantId: resolveTenantId({}), sessionId, surface: 'hook', event: 'reset', items: 0, tokens: 0,
434
+ }));
435
+ }
436
+ /**
437
+ * Run `fn` with console.log captured; returns the captured lines joined by
438
+ * newlines (what the same calls would have printed, minus the final newline).
439
+ */
440
+ export function captureConsole(fn) {
441
+ const lines = [];
442
+ const realLog = console.log;
443
+ console.log = (...parts) => { lines.push(parts.map(String).join(' ')); };
444
+ try {
445
+ fn();
446
+ }
447
+ finally {
448
+ console.log = realLog;
449
+ }
450
+ return lines.join('\n');
451
+ }
452
+ /**
453
+ * The store a Claude Code hook writes to: the project store when there is
454
+ * one, else an existing global store, else the project path (which the hook
455
+ * then skips, since hooks fire in every directory and must not create one).
456
+ * Pre-compact and compact-resume must agree, or a snapshot saved to one store
457
+ * is looked for in the other.
458
+ */
459
+ export function hookStoreRoot(hippoRoot) {
460
+ if (isInitialized(hippoRoot))
461
+ return hippoRoot;
462
+ const globalRoot = getGlobalRoot();
463
+ return isInitialized(globalRoot) ? globalRoot : hippoRoot;
464
+ }
465
+ /**
466
+ * Run `fn` against the token ledger's store: the local store when it is
467
+ * initialized, else the global one (the per-prompt hook runs in directories
468
+ * without a local store). Best-effort: returns undefined and never throws,
469
+ * because a ledger failure must not break context or recall.
470
+ */
471
+ export function withLedgerDb(hippoRoot, fn) {
472
+ let root = null;
473
+ try {
474
+ if (isInitialized(hippoRoot))
475
+ root = hippoRoot;
476
+ else if (isInitialized(getGlobalRoot()))
477
+ root = getGlobalRoot();
478
+ }
479
+ catch {
480
+ return undefined;
481
+ }
482
+ if (root === null)
483
+ return undefined;
484
+ let db;
485
+ try {
486
+ db = openHippoDb(root);
487
+ return fn(db);
488
+ }
489
+ catch (error) {
490
+ // Best effort, but a busy store is the one failure an operator can act on, so it warns once.
491
+ if (isSqliteBusy(error))
492
+ noteStoreBusy('token ledger row skipped');
493
+ return undefined;
494
+ }
495
+ finally {
496
+ if (db)
497
+ closeHippoDb(db);
498
+ }
499
+ }
500
+ export function learnFromRepo(hippoRoot, repoPath, days, label) {
501
+ const prefix = label ? `[${label}] ` : '';
502
+ if (!isGitRepo(repoPath)) {
503
+ console.log(`${prefix}No git history found (or not a git repository).`);
504
+ return { added: 0, skipped: 0, lowInfo: 0 };
505
+ }
506
+ const gitLog = fetchGitLog(repoPath, days);
507
+ if (!gitLog.trim()) {
508
+ console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
509
+ return { added: 0, skipped: 0, lowInfo: 0 };
510
+ }
511
+ // Same patterns as MCP hippo_learn: config.gitLearnPatterns (whose default
512
+ // equals extractLessons' built-in list) so a custom list applies everywhere.
513
+ const config = loadConfig(hippoRoot);
514
+ const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
515
+ if (parsedLessons.length === 0) {
516
+ console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
517
+ return { added: 0, skipped: 0, lowInfo: 0 };
518
+ }
519
+ // The admission gate lives at the write path, not in extractLessons
520
+ // (a published API surface that only parses). Bare subjects like "fixed
521
+ // signals" are dropped here, before they ever become a memory.
522
+ // The gate filters the loop INPUT, so a dropped lesson neither stores nor
523
+ // invalidates. That is deliberate, and it was argued both ways.
524
+ //
525
+ // One review called the lost invalidation serious: a migration subject
526
+ // too thin to store ("replace webpack with vite") would stop weakening
527
+ // stale webpack memories. True. So the loop was widened to walk every
528
+ // parsed lesson with the gate on the write alone.
529
+ //
530
+ // A second review found the cure was worse. STORAGE is what makes invalidation
531
+ // idempotent here: a stored lesson is recognised by its same-text key on
532
+ // the next scan and short-circuits before invalidating again. A lesson that
533
+ // invalidates but is never stored has no such record, so every rescan
534
+ // re-invalidates, and invalidateMatching halves half_life_days each time.
535
+ // Measured: 7 -> 3 -> 1 over two runs. That is compounding data damage.
536
+ //
537
+ // Measured frequency decided it. Across 413 real auto-learn rows in 4
538
+ // stores, 24 are gated and ZERO of those carry an invalidation target; the
539
+ // 45 lessons that do carry targets all pass the gate and are unaffected
540
+ // either way. Both failure modes are empty on real data, so the tie breaks
541
+ // on which one is benign if it ever fires: not invalidating is a missed
542
+ // improvement, re-invalidating forever is damage.
543
+ //
544
+ // Documented limitation, pinned by test: a migration subject too thin to
545
+ // store also does not invalidate. Making invalidateMatching idempotent
546
+ // would allow both, and is backlogged - it is a latent issue for the manual
547
+ // `hippo invalidate` path too, not just this one.
548
+ const { kept: lessons, dropped } = partitionLessons(parsedLessons);
549
+ const lowInfo = dropped.length;
550
+ let added = 0;
551
+ let skipped = 0;
552
+ // Containment: per-lesson refusal must not abort the rest
553
+ // of the git-log scan. No signature change (added/skipped return shape
554
+ // used by cmdLearn + cmdSleepCore callers) — counted locally, folded into
555
+ // the existing summary line.
556
+ let rejected = 0;
557
+ const gitLearnTags = ['error', 'git-learned'];
558
+ const existingForSchema = loadAllEntries(hippoRoot, resolveTenantId({}));
559
+ const keys = storedTextKeys(existingForSchema);
560
+ for (const lesson of lessons) {
561
+ if (keys.has(duplicateKey(lesson))) {
562
+ skipped++;
563
+ continue;
564
+ }
565
+ const target = extractInvalidationTarget(lesson);
566
+ if (target) {
567
+ const invResult = invalidateMatching(hippoRoot, target, resolveTenantId({}));
568
+ if (invResult.invalidated > 0) {
569
+ console.log(`${prefix} Invalidated ${invResult.invalidated} memories referencing "${target.from}"`);
570
+ }
571
+ }
572
+ const schemaFitVal = computeSchemaFit(lesson, gitLearnTags, existingForSchema);
573
+ const entry = createMemory(lesson, {
574
+ layer: Layer.Episodic,
575
+ tags: [...gitLearnTags],
576
+ source: 'git-learn',
577
+ confidence: 'observed',
578
+ schema_fit: schemaFitVal,
579
+ tenantId: resolveTenantId({}),
580
+ baseHalfLifeDays: config.defaultHalfLifeDays,
581
+ });
582
+ // Auto-tag with path context from the repo being learned
583
+ const learnPathTags = extractPathTags(repoPath);
584
+ for (const pt of learnPathTags) {
585
+ if (!entry.tags.includes(pt))
586
+ entry.tags.push(pt);
587
+ }
588
+ try {
589
+ writeEntry(hippoRoot, entry);
590
+ }
591
+ catch (err) {
592
+ if (err instanceof RejectedValueError) {
593
+ rejected++;
594
+ continue;
595
+ }
596
+ throw err;
597
+ }
598
+ updateStats(hippoRoot, { remembered: 1 });
599
+ keys.add(duplicateKey(lesson));
600
+ void embedMemory(hippoRoot, entry);
601
+ added++;
602
+ }
603
+ console.log(`${prefix}${added} new lessons added, ${skipped} duplicates skipped` +
604
+ (rejected > 0 ? `, ${rejected} rejected value(s) skipped` : '') +
605
+ (lowInfo > 0 ? `, ${lowInfo} low-information subject(s) dropped` : '') +
606
+ '.');
607
+ return { added, skipped, lowInfo };
608
+ }
609
+ export const HOOK_MARKERS = {
610
+ start: '<!-- hippo:start -->',
611
+ end: '<!-- hippo:end -->',
612
+ };
613
+ export const HOOKS = {
614
+ 'claude-code': {
615
+ file: 'CLAUDE.md',
616
+ description: 'Claude Code',
617
+ content: `
618
+ ## Project Memory (Hippo)
619
+
620
+ Pinned rules and recent writes auto-inject at every prompt via the installed
621
+ UserPromptSubmit hook; never re-run that part manually. At the START of a
622
+ task (not per prompt), additionally load task-specific context: git-aware
623
+ recall over the full store that per-prompt injection does not cover. Also
624
+ run it if the hook is not installed:
625
+ \`\`\`bash
626
+ hippo context --auto --budget 1500
627
+ \`\`\`
628
+
629
+ When you find out why something failed, record it right then, while you
630
+ work, never as a closing step:
631
+ \`\`\`bash
632
+ hippo remember "<what went wrong and why>" --error
633
+ \`\`\`
634
+
635
+ The installed hooks store failed tool calls and capture the session when it
636
+ ends, so there is nothing to run before you finish.
637
+ `.trim(),
638
+ },
639
+ 'codex': {
640
+ file: 'AGENTS.md',
641
+ description: 'OpenAI Codex',
642
+ content: `
643
+ ## Project Memory (Hippo)
644
+
645
+ At the start of every task, run:
646
+ \`\`\`bash
647
+ hippo context --auto --budget 1500
648
+ \`\`\`
649
+ Read the output before writing any code.
650
+
651
+ On errors or unexpected behaviour, record it right then, while you work,
652
+ never as a closing step:
653
+ \`\`\`bash
654
+ hippo remember "<description of what went wrong>" --error
655
+ \`\`\`
656
+
657
+ When you learn something that should outlive this session (a decision and
658
+ its reason, a user preference, a lesson), record it right then, while you
659
+ work, never as a closing step. Leave out secrets and personal details:
660
+ \`\`\`bash
661
+ hippo remember "<what you learned and why>"
662
+ \`\`\`
663
+
664
+ When Hippo's Codex wrapper is installed, session-end capture runs automatically.
665
+ If the wrapper is not installed, capture a brief summary manually:
666
+ \`\`\`bash
667
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
668
+ \`\`\`
669
+ `.trim(),
670
+ },
671
+ 'cursor': {
672
+ file: 'AGENTS.md',
673
+ description: 'Cursor',
674
+ content: `
675
+ ## Project Memory (Hippo)
676
+
677
+ At the start of every task, run:
678
+ \`\`\`bash
679
+ hippo context --auto --budget 1500
680
+ \`\`\`
681
+ Read the output before writing any code.
682
+
683
+ On errors or unexpected behaviour, record it right then, while you work,
684
+ never as a closing step:
685
+ \`\`\`bash
686
+ hippo remember "<description of what went wrong>" --error
687
+ \`\`\`
688
+
689
+ When you learn something that should outlive this session (a decision and
690
+ its reason, a user preference, a lesson), record it right then, while you
691
+ work, never as a closing step. Leave out secrets and personal details:
692
+ \`\`\`bash
693
+ hippo remember "<what you learned and why>"
694
+ \`\`\`
695
+
696
+ When ending a session, capture a brief summary:
697
+ \`\`\`bash
698
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
699
+ \`\`\`
700
+ `.trim(),
701
+ },
702
+ 'openclaw': {
703
+ file: 'AGENTS.md',
704
+ description: 'OpenClaw',
705
+ content: `
706
+ ## Project Memory (Hippo)
707
+
708
+ At the start of every session, run:
709
+ \`\`\`bash
710
+ hippo context --auto --budget 1500
711
+ \`\`\`
712
+ Read the output before writing any code.
713
+
714
+ On errors or unexpected behaviour, record it right then, while you work,
715
+ never as a closing step:
716
+ \`\`\`bash
717
+ hippo remember "<description of what went wrong>" --error
718
+ \`\`\`
719
+
720
+ When you learn something that should outlive this session (a decision and
721
+ its reason, a user preference, a lesson), record it right then, while you
722
+ work, never as a closing step. Leave out secrets and personal details:
723
+ \`\`\`bash
724
+ hippo remember "<what you learned and why>"
725
+ \`\`\`
726
+
727
+ When ending a session, capture a brief summary:
728
+ \`\`\`bash
729
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
730
+ \`\`\`
731
+ `.trim(),
732
+ },
733
+ 'opencode': {
734
+ file: 'AGENTS.md',
735
+ description: 'OpenCode',
736
+ content: `
737
+ ## Project Memory (Hippo)
738
+
739
+ At the start of every task, run:
740
+ \`\`\`bash
741
+ hippo context --auto --budget 1500
742
+ \`\`\`
743
+ Read the output before writing any code.
744
+
745
+ On errors or unexpected behaviour, record it right then, while you work,
746
+ never as a closing step:
747
+ \`\`\`bash
748
+ hippo remember "<description of what went wrong>" --error
749
+ \`\`\`
750
+
751
+ When you learn something that should outlive this session (a decision and
752
+ its reason, a user preference, a lesson), record it right then, while you
753
+ work, never as a closing step. Leave out secrets and personal details:
754
+ \`\`\`bash
755
+ hippo remember "<what you learned and why>"
756
+ \`\`\`
757
+
758
+ When stuck or repeating yourself, check if this happened before:
759
+ \`\`\`bash
760
+ hippo recall "<what's going wrong>" --budget 2000
761
+ \`\`\`
762
+
763
+ When ending a session, capture a brief summary:
764
+ \`\`\`bash
765
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
766
+ \`\`\`
767
+ `.trim(),
768
+ },
769
+ 'pi': {
770
+ file: 'AGENTS.md',
771
+ description: 'Pi',
772
+ content: `
773
+ ## Project Memory (Hippo)
774
+
775
+ At the start of every session, run:
776
+ \`\`\`bash
777
+ hippo context --auto --budget 1500
778
+ \`\`\`
779
+ Read the output before writing any code.
780
+
781
+ On errors or unexpected behaviour, record it right then, while you work,
782
+ never as a closing step:
783
+ \`\`\`bash
784
+ hippo remember "<description of what went wrong>" --error
785
+ \`\`\`
786
+
787
+ When you learn something that should outlive this session (a decision and
788
+ its reason, a user preference, a lesson), record it right then, while you
789
+ work, never as a closing step. Leave out secrets and personal details:
790
+ \`\`\`bash
791
+ hippo remember "<what you learned and why>"
792
+ \`\`\`
793
+
794
+ When ending a session, capture a brief summary:
795
+ \`\`\`bash
796
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
797
+ \`\`\`
798
+
799
+ For full integration, copy the hippo-memory Pi extension to \`~/.pi/agent/extensions/hippo-memory/\`.
800
+ `.trim(),
801
+ },
802
+ };
803
+ // sha256 of each trimmed block an earlier hippo wrote, so init refreshes only blocks nobody edited. Add the old hash when a block changes.
804
+ const SHIPPED_HOOK_HASHES = new Map([
805
+ ['c04e48f2896a4fee9ae98f8f832e2d26a3910269df3beb5bcd6baee3cd9db68e', 'claude-code'],
806
+ ['e6b12bd8983c032e5ca8e95a97aeff4178a5a05026d10acad5b2e1b25d5656dd', 'claude-code'],
807
+ ['4c64e11d3e5be68fa547c9248d7553feb645a02f7cf13ba02f7275e1854baf44', 'claude-code'],
808
+ ['293bd319bbc86225a0ee027490a3322a0336257f5832f7fade65e4ffb2530654', 'claude-code'],
809
+ ['15abcece9712279fb4721f7a8f0ba117457400278977beb5cf5b5d7ba49f7b1a', 'codex'],
810
+ ['0c81a6b2c21473313001f624b80ea870e661aecbfda9bfe8503febc0d5f34533', 'codex'],
811
+ ['88e45358aba4f17912f113221c991dc758275991335d1daa4aa1974a69c46769', 'codex'],
812
+ ['e61632fe177450a06541c148a9a4f9182530d8df667806927a99792825903298', 'codex'],
813
+ ['a1415ecda9b2f8f317c233738e4a5ac16e6b2cc385a017c0c8ecfbfacbcab6a3', 'cursor'],
814
+ ['a38c428bbdfc14ec50f6f7b9183785170a4eae1ce9cde60257cca6efc7206b3a', 'cursor'],
815
+ ['0ec9f556abfd55e94f9e6fb47ece0fc5acb841977d144b35a2371e03645d8636', 'cursor'],
816
+ ['40524c3bd5a2eb04036567cc761451961d950995768bccd93a9900b0f75eafea', 'openclaw'],
817
+ ['7b3518e8c0feaa7b8b454cde7743f7598ad14cd9979e1680d0954484e2464aae', 'openclaw'],
818
+ ['1137dcf04568caf011e41db77bc55324faee88bc29c3a5fcc98ab687cd952a16', 'openclaw'],
819
+ ['4601c67c31f41cd5b1324cfccdb1afc66872b7fb0bc1e7c5789ecabb1f6bd942', 'opencode'],
820
+ ['90d9e21d8d1ecbe99a0fc7b7f2d9f8af7b5315a6b4b0203df4f7a9bdc0699b98', 'opencode'],
821
+ ['ca4e00284f1397ed2f2fcc53210c27f63b90edf6b37fd66dad5ee58b94ea3eee', 'opencode'],
822
+ ['8b8f5986d7f7ed15f06e68720d8913c3cab23d94366b411935ca2bbaa334553b', 'pi'],
823
+ ['37767b355e18beac726b05b9e2b898dab8c6135fd7b98f3aa52edc734d5dd283', 'pi'],
824
+ ['6e85a5cccb3cfeaa9a080713754936db730f96376f94cc9a9888a746149c7268', 'pi'],
825
+ ]);
826
+ export function resolveAuthRoot(hippoRoot, flags) {
827
+ if (flags['global']) {
828
+ initGlobal();
829
+ return getGlobalRoot();
830
+ }
831
+ requireInit(hippoRoot);
832
+ return hippoRoot;
833
+ }
834
+ //# sourceMappingURL=shared.js.map