hippo-memory 1.58.0 → 1.59.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 +13 -1
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/legacy.js +4 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +2 -0
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +7 -0
- package/dist/cli/projects.d.ts +4 -0
- package/dist/cli/projects.js +90 -0
- package/dist/cli/shared.js +14 -8
- package/dist/cli/sleep.js +5 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +389 -361
- package/dist/consolidate.js +12 -1
- package/dist/dag.js +4 -2
- package/dist/dedupe.js +1 -1
- package/dist/doctor.js +23 -0
- package/dist/dormant.d.ts +2 -2
- package/dist/extract.js +16 -16
- package/dist/project-merge.d.ts +52 -0
- package/dist/project-merge.js +168 -0
- package/dist/recall-scope.d.ts +3 -3
- package/dist/recall-scope.js +5 -4
- package/dist/rerankers/clef.d.ts +29 -0
- package/dist/rerankers/clef.js +182 -0
- package/dist/rerankers/index.js +3 -0
- package/dist/rerankers/jev.d.ts +11 -0
- package/dist/rerankers/jev.js +10 -5
- package/dist/rerankers/types.d.ts +16 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +9 -3
- package/dist/store.js +6 -6
- 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 +5 -2
package/README.md
CHANGED
|
@@ -535,6 +535,18 @@ removes pinned memories, raw receipts (Slack, GitHub, vault imports) or the memo
|
|
|
535
535
|
Claude Code compaction saved either way, and duplicate removal and junk cleanup still
|
|
536
536
|
delete other memories. `hippo forget` still deletes a compaction memory.
|
|
537
537
|
|
|
538
|
+
**Clean up project names left by older versions.** Older versions tagged memories saved in
|
|
539
|
+
a git worktree with the worktree's folder name, and older sleep saved merged memories as
|
|
540
|
+
user-global, so every project could see them. Upgrading stops new damage; these commands
|
|
541
|
+
repair old rows. Each is a dry run until you add `--apply`. With `--apply`, it backs up the
|
|
542
|
+
database to `.hippo/backups/` first and logs every id it touched in the audit log:
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
hippo projects --global # names, counts, live worktrees of this repo
|
|
546
|
+
hippo projects merge hippo-wt-fix hippo --global # fold an old worktree name into its repo
|
|
547
|
+
hippo projects repair --global # re-tag user-global merges by their parents
|
|
548
|
+
```
|
|
549
|
+
|
|
538
550
|
**See what memory costs in tokens.** Every block of memory text hippo hands an agent (the
|
|
539
551
|
per-prompt hook, the block `hippo compact-resume` restores after compaction, `hippo context`,
|
|
540
552
|
`hippo recall`, the MCP tools, the HTTP API) is recorded in a token ledger: counts, surface
|
|
@@ -1126,7 +1138,7 @@ Mark it, and it drops out of the top results. `hippo outcome --bad` weakens the
|
|
|
1126
1138
|
|
|
1127
1139
|
### Where does hippo keep my data?
|
|
1128
1140
|
|
|
1129
|
-
On your machine, in SQLite: `.hippo/hippo.db` in each project, plus a global store in `~/.hippo/` for lessons shared across projects, with markdown mirrors you can read and commit. Recall makes no network call by default. Text goes to an outside provider only through features that use one: an API embedder, the Jev or LLM reranker, `hippo refine`, and the fact extraction `hippo sleep` runs through Anthropic's API whenever `ANTHROPIC_API_KEY` is set in its environment. To turn that last one off, set `{"extraction":{"enabled":false}}` in `.hippo/config.json`.
|
|
1141
|
+
On your machine, in SQLite: `.hippo/hippo.db` in each project, plus a global store in `~/.hippo/` for lessons shared across projects, with markdown mirrors you can read and commit. Recall makes no network call by default. Text goes to an outside provider only through features that use one: an API embedder, the Jev, CLEF or LLM reranker, `hippo refine`, and the fact extraction `hippo sleep` runs through Anthropic's API whenever `ANTHROPIC_API_KEY` is set in its environment. To turn that last one off, set `{"extraction":{"enabled":false}}` in `.hippo/config.json`.
|
|
1130
1142
|
|
|
1131
1143
|
### What does hippo cost?
|
|
1132
1144
|
|
|
@@ -34,7 +34,7 @@ export interface ContainerOutcome {
|
|
|
34
34
|
}
|
|
35
35
|
/** Throws SQLITE_BUSY when another writer holds the store past its busy timeout; nothing is written then. */
|
|
36
36
|
export declare function syncContainer(s: StoreSession, work: ContainerWork): ContainerOutcome;
|
|
37
|
-
export type SetAsideWhy = 'note-gone' | 'note-changed' | 'handover';
|
|
37
|
+
export type SetAsideWhy = 'note-gone' | 'note-changed' | 'handover' | 'project-merge';
|
|
38
38
|
export type SetAsideResult = {
|
|
39
39
|
readonly kind: 'untagged';
|
|
40
40
|
readonly entry: MemoryEntry;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Rows the old Claude import wrote (`claude-memory:<file>`), taken over by the notes they came from (plan design 10).
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { duplicateKey } from '../same-text.js';
|
|
4
|
+
import { maskEmails } from '../secret-detect.js';
|
|
4
5
|
import { selectLiveEntriesBySourcePrefix } from '../store.js';
|
|
5
6
|
import { matchLegacy } from './plan.js';
|
|
6
7
|
import { MIN_ITEM_CHARS, storedText } from './source.js';
|
|
@@ -25,7 +26,9 @@ export function legacyWork(db, tenantId, listings) {
|
|
|
25
26
|
refs.push({ dir: container.path, key: item.key });
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
const match = matchLegacy(
|
|
29
|
+
const match = matchLegacy(
|
|
30
|
+
// The old import kept emails in clear, and targets are masked; without the mask those rows never matched and imported twice.
|
|
31
|
+
rows.map((r) => ({ id: r.id, file: r.source.slice(LEGACY_SOURCE_PREFIX.length), textKey: duplicateKey(maskEmails(r.content)) })), targets);
|
|
29
32
|
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
30
33
|
const add = (pick, ref, id) => {
|
|
31
34
|
const { dir, key } = refs[Number(ref)];
|
package/dist/audit.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function auditMemory(entry: MemoryEntry, backsObject?: boolean):
|
|
|
18
18
|
/** `backing`: ids of memories that back an object (store.memoriesBackingObjects). */
|
|
19
19
|
export declare function auditMemories(entries: MemoryEntry[], backing: ReadonlySet<string>): AuditResult;
|
|
20
20
|
export declare function isContentWorthStoring(content: string): boolean;
|
|
21
|
-
export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject", "agent_memory_restore", "agent_memory_set_aside"];
|
|
21
|
+
export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject", "agent_memory_restore", "agent_memory_set_aside", "project_merge", "project_repair"];
|
|
22
22
|
export type AuditOp = (typeof AUDIT_OPS)[number];
|
|
23
23
|
export interface AppendAuditOpts {
|
|
24
24
|
tenantId: string;
|
package/dist/audit.js
CHANGED
|
@@ -235,6 +235,8 @@ export const AUDIT_OPS = [
|
|
|
235
235
|
'quarantine_reject', // emitted by api.quarantineReject
|
|
236
236
|
'agent_memory_restore', // emitted by the agent memory sync when a deleted note comes back
|
|
237
237
|
'agent_memory_set_aside', // emitted by the agent memory sync when a note is deleted or refused
|
|
238
|
+
'project_merge', // emitted by `hippo projects merge --apply` with every id it touched
|
|
239
|
+
'project_repair', // emitted by `hippo projects repair --apply` with every id it touched
|
|
238
240
|
];
|
|
239
241
|
function isBigIntValue(value) {
|
|
240
242
|
return typeof value === 'bigint';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// User-facing CLI messages (usage, not-found, refusals) print at every HIPPO_LOG level; diagnostics go to `log`.
|
|
2
|
+
// Both forward to console.error so the bytes match the old calls and console spies in tests still see them.
|
|
3
|
+
/** Writes a message the user asked for, or must act on, to stderr. */
|
|
4
|
+
export function printError(...args) {
|
|
5
|
+
console.error(...args);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The `hippo projects` verb: list a store's project names, fold an old worktree name into its repo, repair sleep's user-global merges.
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { closeHippoDb, openHippoDb } from '../db.js';
|
|
5
|
+
import { listProjects, mergeProjects, repairUserGlobalMerges } from '../project-merge.js';
|
|
6
|
+
import { resolveTenantId } from '../tenant.js';
|
|
7
|
+
import { resolveAuthRoot } from './shared.js';
|
|
8
|
+
import { printError } from './output.js';
|
|
9
|
+
/** Old per-worktree project names of the repo at cwd, mapped to the repo's main checkout name; empty outside git. */
|
|
10
|
+
function worktreeNames() {
|
|
11
|
+
try {
|
|
12
|
+
const out = execFileSync('git', ['worktree', 'list', '--porcelain'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
13
|
+
const paths = out.split(/\r?\n/).filter((l) => l.startsWith('worktree ')).map((l) => path.basename(l.slice('worktree '.length)));
|
|
14
|
+
return new Map(paths.slice(1).map((name) => [name, paths[0]]));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Outside a git checkout, or no git on PATH: the list just has no worktree hints.
|
|
18
|
+
return new Map();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function label(origin) {
|
|
22
|
+
return origin === null ? '(unknown)' : origin === '' ? '(user-global)' : origin;
|
|
23
|
+
}
|
|
24
|
+
function hint(p, worktrees) {
|
|
25
|
+
const main = p.origin ? worktrees.get(p.origin) : undefined;
|
|
26
|
+
if (main)
|
|
27
|
+
return `\n a worktree of ${main}: hippo projects merge ${p.origin} ${main}`;
|
|
28
|
+
return p.copiesElsewhere > 0 ? `\n ${p.copiesElsewhere} of its imported notes are copies also held under another name` : '';
|
|
29
|
+
}
|
|
30
|
+
function count(n, one, many = `${one}s`) {
|
|
31
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
32
|
+
}
|
|
33
|
+
export function cmdProjects(hippoRoot, args, flags) {
|
|
34
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
35
|
+
const tenantId = resolveTenantId({});
|
|
36
|
+
const apply = flags['apply'] === true;
|
|
37
|
+
const sub = args[0] ?? 'list';
|
|
38
|
+
const db = openHippoDb(root);
|
|
39
|
+
try {
|
|
40
|
+
if (sub === 'list') {
|
|
41
|
+
const projects = listProjects(db, tenantId);
|
|
42
|
+
if (flags['json']) {
|
|
43
|
+
console.log(JSON.stringify({ store: root, projects }, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const worktrees = worktreeNames();
|
|
47
|
+
console.log(`${count(projects.length, 'project name')} in ${root} (newest write first):\n`);
|
|
48
|
+
for (const p of projects) {
|
|
49
|
+
console.log(`${label(p.origin)} ${count(p.live, 'memory', 'memories')}, ${p.imported} imported from agent notes, newest ${p.newest.slice(0, 10)}${hint(p, worktrees)}`);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (sub === 'merge') {
|
|
54
|
+
const [from, into] = [args[1] ?? '', args[2] ?? ''];
|
|
55
|
+
const r = mergeProjects(db, root, { tenantId, from, into, dryRun: !apply });
|
|
56
|
+
if (flags['json']) {
|
|
57
|
+
console.log(JSON.stringify(r, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(`${apply ? 'Merged' : 'Dry run: would merge'} ${from} into ${into}:`);
|
|
61
|
+
console.log(` ${count(r.setAside.length, 'imported note copy', 'imported note copies')} set aside (dormant; the next sync under ${into} imports the notes still on disk)`);
|
|
62
|
+
console.log(` ${count(r.restamped.length, 'memory', 'memories')} re-tagged, plus ${r.dormantRestamped.length} dormant and ${count(r.compactions, 'compaction record')}`);
|
|
63
|
+
console.log(apply ? `Backup: ${r.backup}\nEvery id is in the audit log: hippo audit list --op project_merge` : 'Nothing written. Add --apply to run it.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (sub === 'repair') {
|
|
67
|
+
const r = repairUserGlobalMerges(db, root, { tenantId, dryRun: !apply });
|
|
68
|
+
if (flags['json']) {
|
|
69
|
+
console.log(JSON.stringify(r, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(`${apply ? 'Repaired' : 'Dry run: would repair'} sleep's user-global merged rows:`);
|
|
73
|
+
console.log(` ${r.toProject.length} re-tagged to their parents' project`);
|
|
74
|
+
console.log(` ${r.setAside.length} set aside (parents in two projects; sleep re-merges them per project)`);
|
|
75
|
+
console.log(` ${r.untraced.length} left as they are (no parent left to show which project; check them with hippo inspect <id>)`);
|
|
76
|
+
console.log(apply ? `Backup: ${r.backup}\nEvery id is in the audit log: hippo audit list --op project_repair` : 'Nothing written. Add --apply to run it.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
printError('Usage: hippo projects [list] [--json] | merge <from> <into> [--apply] | repair [--apply] [--global]');
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
closeHippoDb(db);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=projects.js.map
|
package/dist/cli/shared.js
CHANGED
|
@@ -28,6 +28,8 @@ import * as client from '../client.js';
|
|
|
28
28
|
import { detectServer, removePidfileIfOwned } from '../server-detect.js';
|
|
29
29
|
import { resolveTenantId } from '../tenant.js';
|
|
30
30
|
import { snapshotText, sessionTrailText, handoffText } from '../context-render.js';
|
|
31
|
+
import { log } from '../log.js';
|
|
32
|
+
import { printError } from './output.js';
|
|
31
33
|
export function parseLimitFlag(value) {
|
|
32
34
|
if (!value)
|
|
33
35
|
return Infinity;
|
|
@@ -45,13 +47,13 @@ export function parseBudgetFlag(value, fallback) {
|
|
|
45
47
|
return fallback;
|
|
46
48
|
// A value-less flag and a junk value are different typos; the --hops guard already splits them.
|
|
47
49
|
if (typeof value !== 'string') {
|
|
48
|
-
|
|
50
|
+
printError('--budget requires an integer value (e.g. --budget 1500).');
|
|
49
51
|
process.exit(1);
|
|
50
52
|
}
|
|
51
53
|
// Number(), like the --hops guard: parseInt('12abc') is 12, silently accepting what this message rejects.
|
|
52
54
|
const parsed = Number(value);
|
|
53
55
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
54
|
-
|
|
56
|
+
printError(`Invalid --budget: "${value}". Must be a non-negative integer.`);
|
|
55
57
|
process.exit(1);
|
|
56
58
|
}
|
|
57
59
|
return parsed;
|
|
@@ -84,7 +86,7 @@ export function emitCliAudit(hippoRoot, op, targetId, metadata) {
|
|
|
84
86
|
}
|
|
85
87
|
export function requireInit(hippoRoot) {
|
|
86
88
|
if (!isInitialized(hippoRoot)) {
|
|
87
|
-
|
|
89
|
+
printError(`No hippo store at ${hippoRoot} (searched ${process.cwd()} and its parents up to your home directory). Run \`hippo init\` first.`);
|
|
88
90
|
process.exit(1);
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -149,7 +151,7 @@ export async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
149
151
|
const failure = client.classifyTransportFailure(err);
|
|
150
152
|
if (failure === 'never-sent') {
|
|
151
153
|
failIfServerRequired('the server pidfile was stale (connection refused)');
|
|
152
|
-
|
|
154
|
+
log.warn('stale server pidfile detected, falling back to direct mode');
|
|
153
155
|
// Clear the pidfile only if it still names the dead server we just
|
|
154
156
|
// probed — a newer server may have rewritten it (removePidfileIfOwned).
|
|
155
157
|
removePidfileIfOwned(hippoRoot, { pid: info.pid, startedAt: info.started_at });
|
|
@@ -159,7 +161,7 @@ export async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
159
161
|
// Every caller of this helper is a non-idempotent write, so replaying on
|
|
160
162
|
// the direct path would store a row the server may already have committed.
|
|
161
163
|
// Leave the pidfile alone: the next command's connect-phase failure heals it.
|
|
162
|
-
|
|
164
|
+
printError(`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
165
|
process.exit(1);
|
|
164
166
|
}
|
|
165
167
|
throw err;
|
|
@@ -216,7 +218,7 @@ export function printAgentImport(report, indent = ' ') {
|
|
|
216
218
|
if (line !== null)
|
|
217
219
|
console.log(`${indent}${line}`);
|
|
218
220
|
for (const warning of report.warnings)
|
|
219
|
-
|
|
221
|
+
printError(`hippo: agent memories: ${warning}`);
|
|
220
222
|
}
|
|
221
223
|
/** The first hippo block in `text` and the agent whose current or shipped text it is; `owner` is undefined for an edited block. */
|
|
222
224
|
export function hippoBlock(text) {
|
|
@@ -301,6 +303,7 @@ export function setupDailySchedule(globalRoot) {
|
|
|
301
303
|
console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
|
|
302
304
|
}
|
|
303
305
|
catch {
|
|
306
|
+
// No crontab or no permission: print the line for the user to add by hand.
|
|
304
307
|
const cronLine = `15 6 * * * ${cmd}`;
|
|
305
308
|
console.log(` To schedule the machine-level daily runner, add to crontab (crontab -e):`);
|
|
306
309
|
console.log(` ${cronLine}`);
|
|
@@ -310,7 +313,7 @@ export function setupDailySchedule(globalRoot) {
|
|
|
310
313
|
export function parseAsOfFlag(flags) {
|
|
311
314
|
const asOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
|
|
312
315
|
if (asOf !== undefined && Number.isNaN(new Date(asOf).getTime())) {
|
|
313
|
-
|
|
316
|
+
printError(`Error: --as-of value "${asOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
314
317
|
process.exit(1);
|
|
315
318
|
}
|
|
316
319
|
return asOf;
|
|
@@ -342,6 +345,7 @@ export function collectHandoffEvidence(cwd, testStatus) {
|
|
|
342
345
|
}).trim() || null;
|
|
343
346
|
}
|
|
344
347
|
catch {
|
|
348
|
+
// No git, not a repo, or timed out: evidence is optional, so the field stays null.
|
|
345
349
|
gitRef = null;
|
|
346
350
|
}
|
|
347
351
|
let dirtyTree = null;
|
|
@@ -352,6 +356,7 @@ export function collectHandoffEvidence(cwd, testStatus) {
|
|
|
352
356
|
dirtyTree = status.trim().length > 0;
|
|
353
357
|
}
|
|
354
358
|
catch {
|
|
359
|
+
// Same as gitRef: unknown tree state is reported as null, never as an error.
|
|
355
360
|
dirtyTree = null;
|
|
356
361
|
}
|
|
357
362
|
return { gitRef, dirtyTree, testStatus };
|
|
@@ -408,7 +413,7 @@ export function cardStringFlag(flags, key) {
|
|
|
408
413
|
if (v === undefined)
|
|
409
414
|
return undefined;
|
|
410
415
|
if (v === true || v === false || Array.isArray(v)) {
|
|
411
|
-
|
|
416
|
+
printError(`--${key} requires a value`);
|
|
412
417
|
process.exit(1);
|
|
413
418
|
}
|
|
414
419
|
return v.trim();
|
|
@@ -477,6 +482,7 @@ export function withLedgerDb(hippoRoot, fn) {
|
|
|
477
482
|
root = getGlobalRoot();
|
|
478
483
|
}
|
|
479
484
|
catch {
|
|
485
|
+
// An unreadable store root means no ledger write; the ledger must never break context or recall.
|
|
480
486
|
return undefined;
|
|
481
487
|
}
|
|
482
488
|
if (root === null)
|
package/dist/cli/sleep.js
CHANGED
|
@@ -8,7 +8,9 @@ import { replayCompactionsAt } from '../compaction-record.js';
|
|
|
8
8
|
import * as api from '../api.js';
|
|
9
9
|
import { resolveTenantId } from '../tenant.js';
|
|
10
10
|
import { renderAmbientSummary } from '../ambient.js';
|
|
11
|
+
import { log } from '../log.js';
|
|
11
12
|
import { requireInit, learnFromRepo, runChurnStaleForRepo, printAgentImport } from './shared.js';
|
|
13
|
+
import { printError } from './output.js';
|
|
12
14
|
/** Runs `hippo sleep`; with `--log-file` it also tees its output to that file. */
|
|
13
15
|
export async function cmdSleep(hippoRoot, flags) {
|
|
14
16
|
// Tee stdout/stderr to a log file when --log-file is set. The SessionEnd
|
|
@@ -45,7 +47,7 @@ export async function cmdSleep(hippoRoot, flags) {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
catch (err) {
|
|
48
|
-
|
|
50
|
+
log.warn(`could not open log file ${logFile}: ${err.message}`);
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
try {
|
|
@@ -145,14 +147,14 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
145
147
|
if (result.marked > 0)
|
|
146
148
|
console.log(`Tagged ${result.marked} memories churn-stale in ${root}.`);
|
|
147
149
|
if (result.error)
|
|
148
|
-
|
|
150
|
+
printError(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
printAgentImport(importForStore(hippoRoot, { machine: currentMachine() }), '');
|
|
152
154
|
}
|
|
153
155
|
// Finishes compactions a killed or busy post-compact hook left; never throws, and a dry run writes nothing.
|
|
154
156
|
if (!flags['dry-run']) {
|
|
155
|
-
const finished = replayCompactionsAt(hippoRoot, (message) =>
|
|
157
|
+
const finished = replayCompactionsAt(hippoRoot, (message) => log.warn(`compaction replay: ${message}`));
|
|
156
158
|
if (finished > 0)
|
|
157
159
|
console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over from earlier sessions.`);
|
|
158
160
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* hippo rejections
|
|
21
21
|
* hippo unreject <digest-prefix>
|
|
22
22
|
* hippo dormant [<query>] [--limit <n>] [--json] | restore <id> | forget <id>
|
|
23
|
+
* hippo projects [--json] | merge <from> <into> [--apply] | repair [--apply]
|
|
23
24
|
* hippo tokens [--days <n>] [--json] [--global]
|
|
24
25
|
* hippo doctor [--json]
|
|
25
26
|
* hippo inspect <id>
|