hippo-memory 1.58.0 → 1.60.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/api.d.ts +2 -0
- package/dist/api.js +25 -9
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +3 -0
- package/dist/autolearn.js +5 -2
- package/dist/capture.js +8 -6
- 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 +356 -0
- package/dist/cli.js +1587 -1404
- package/dist/compaction-record.js +7 -6
- package/dist/config.js +12 -11
- package/dist/connectors/github/cli-impl.js +1 -0
- package/dist/consolidate.js +16 -4
- package/dist/dag.js +11 -9
- package/dist/dashboard.js +4 -2
- package/dist/db.js +16 -3
- package/dist/dedupe.js +1 -1
- package/dist/delivery-recorder.js +1 -0
- package/dist/doctor.js +24 -0
- package/dist/dormant.d.ts +2 -2
- package/dist/dormant.js +1 -0
- package/dist/embedding-provider.js +3 -2
- package/dist/embeddings.js +10 -7
- package/dist/extract.js +20 -19
- package/dist/graph.js +3 -8
- package/dist/handoff.js +3 -0
- package/dist/hooks.js +3 -0
- package/dist/importers.js +6 -3
- package/dist/incidents.js +1 -0
- package/dist/judgment.js +5 -2
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +32 -13
- package/dist/memory.d.ts +5 -3
- package/dist/processes.js +1 -0
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +9 -4
- package/dist/project-merge.d.ts +52 -0
- package/dist/project-merge.js +168 -0
- package/dist/raw-archive-mirror-cleanup.js +2 -1
- package/dist/recall-scope.d.ts +3 -3
- package/dist/recall-scope.js +5 -4
- package/dist/recall-trace.d.ts +2 -2
- package/dist/recall-trace.js +10 -14
- package/dist/refine-llm.js +18 -10
- package/dist/rerankers/clef.d.ts +29 -0
- package/dist/rerankers/clef.js +223 -0
- package/dist/rerankers/cross-encoder.js +6 -4
- package/dist/rerankers/index.js +3 -0
- package/dist/rerankers/jev.d.ts +14 -1
- package/dist/rerankers/jev.js +30 -20
- package/dist/rerankers/llm.js +3 -3
- package/dist/rerankers/types.d.ts +16 -0
- package/dist/same-text.d.ts +2 -0
- package/dist/same-text.js +4 -0
- package/dist/scheduler.js +1 -0
- package/dist/search.js +4 -2
- package/dist/secret-detect.js +1 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +38 -15
- package/dist/shared.js +21 -19
- package/dist/stdin.js +1 -0
- package/dist/store.d.ts +33 -1
- package/dist/store.js +123 -22
- package/dist/token-ledger.js +1 -0
- 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 +6 -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/api.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ export { classifyOriginProject } from './project-identity.js';
|
|
|
95
95
|
* flagged rows with no project origin never ambient-inject.
|
|
96
96
|
*/
|
|
97
97
|
export declare function ambientSecretAdmit(e: MemoryEntry, currentProjectName: string): boolean;
|
|
98
|
+
/** Most rows per store a no-query context reads; past it, ranking and ambientState see the strongest by decay. */
|
|
99
|
+
export declare const CONTEXT_CANDIDATE_CAP = 2000;
|
|
98
100
|
export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
|
|
99
101
|
export interface RememberOpts {
|
|
100
102
|
content: string;
|
package/dist/api.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
10
10
|
import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from './api-errors.js';
|
|
11
11
|
export { ApiError, BadRequestError, ConflictError, ForbiddenError, NotFoundError } from './api-errors.js';
|
|
12
|
-
import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, memoriesBackingObjects, } from './store.js';
|
|
12
|
+
import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, loadContextCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, memoriesBackingObjects, } from './store.js';
|
|
13
13
|
import { RejectedValueError } from './rejection.js';
|
|
14
14
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
15
15
|
import { listDormantRows, readDormantSnapshot, deleteDormantRow, hasDormantRow, } from './dormant.js';
|
|
@@ -17,6 +17,7 @@ import { recordTokenUse, summarizeTokenUse } from './token-ledger.js';
|
|
|
17
17
|
import { detectInstruction } from './instruction-detect.js';
|
|
18
18
|
import { quarantineScopeFor, recordQuarantine, getQuarantineRow, listQuarantineRows, approveQuarantineRow, rejectQuarantineRow, } from './quarantine.js';
|
|
19
19
|
import { summarizeFailures } from './failure-log.js';
|
|
20
|
+
import { log } from './log.js';
|
|
20
21
|
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
21
22
|
import { createMemory, createSuccessor, applyOutcome, calculateStrength, markRetrieved, CHURN_STALE_TAG, COMPACTION_MEMORY_TAG, } from './memory.js';
|
|
22
23
|
import { appendAuditEvent, reportAuditWriteFailure, auditQueryFields, queryAuditEvents, auditMemories, isContentWorthStoring, } from './audit.js';
|
|
@@ -128,10 +129,15 @@ export function ambientSecretAdmit(e, currentProjectName) {
|
|
|
128
129
|
return false;
|
|
129
130
|
return origin === currentProjectName;
|
|
130
131
|
}
|
|
132
|
+
/** Most rows per store a no-query context reads; past it, ranking and ambientState see the strongest by decay. */
|
|
133
|
+
export const CONTEXT_CANDIDATE_CAP = 2000;
|
|
131
134
|
// The pinned-only branch needs pins and recent-N candidates, not the corpus; `recall` applies there only.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
// Without `window` the whole store loads: a local-only query searches every local row.
|
|
136
|
+
function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admit, recall, onQualityDrop, window) {
|
|
137
|
+
if (!pinnedOnly) {
|
|
138
|
+
const rows = window ? loadContextCandidates(hippoRoot, tenantId, window) : loadAllEntries(hippoRoot, tenantId);
|
|
139
|
+
return { entries: rows.filter(admit) };
|
|
140
|
+
}
|
|
135
141
|
// DF3's quality floor runs on the recent-N slice AFTER this load, so the load
|
|
136
142
|
// counts by it too, or it stops short of a store whose newest rows are junk.
|
|
137
143
|
const admitAmbient = (e) => {
|
|
@@ -1314,7 +1320,7 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
1314
1320
|
writeEntryMirrors(ctx.hippoRoot, newEntry);
|
|
1315
1321
|
}
|
|
1316
1322
|
catch (mirrorErr) {
|
|
1317
|
-
|
|
1323
|
+
log.error(`supersede: mirror write failed (non-fatal, will self-heal): ${mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)}`);
|
|
1318
1324
|
}
|
|
1319
1325
|
}
|
|
1320
1326
|
finally {
|
|
@@ -1359,7 +1365,7 @@ export function archiveRaw(ctx, id, reason, opts = {}) {
|
|
|
1359
1365
|
mirrorOk = true;
|
|
1360
1366
|
}
|
|
1361
1367
|
catch (mirrorErr) {
|
|
1362
|
-
|
|
1368
|
+
log.error(`archiveRaw: mirror cleanup failed for ${id} (will retry via reaper on next openHippoDb): ${mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)}`);
|
|
1363
1369
|
}
|
|
1364
1370
|
if (mirrorOk) {
|
|
1365
1371
|
// Stamp mirror_cleaned_at now so the next openHippoDb reaper SELECT
|
|
@@ -1694,12 +1700,22 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1694
1700
|
const admit = (e) => !e.superseded_by && !isOwnCompactionItem(e) && ambientAdmit(e);
|
|
1695
1701
|
const loadAdmit = obs ? obs.watchAdmit(admit) : admit;
|
|
1696
1702
|
const qualityDrop = (isGlobal) => obs && !promptRecallPending ? (e) => obs.qualityDropped(e, isGlobal) : undefined;
|
|
1703
|
+
// The window's predicates are ones admit applies anyway, so below the cap the admitted rows are unchanged.
|
|
1704
|
+
const searchesLocalRows = query !== '*' && !(hasGlobal && !primaryIsGlobal);
|
|
1705
|
+
const window = pinnedOnly || searchesLocalRows
|
|
1706
|
+
? undefined
|
|
1707
|
+
: {
|
|
1708
|
+
exactScope,
|
|
1709
|
+
project: includeCrossProject || currentProjectName === '' ? undefined : currentProjectName,
|
|
1710
|
+
cap: CONTEXT_CANDIDATE_CAP,
|
|
1711
|
+
now: evalNow(),
|
|
1712
|
+
};
|
|
1697
1713
|
// Tenant-scoped loads (v1.11.1 lesson: NEVER resolveTenantId({}) here).
|
|
1698
1714
|
const localLoad = hasLocal
|
|
1699
|
-
? loadAmbientEntries(ctx.hippoRoot, ctx.tenantId, pinnedOnly, includeRecent, loadAdmit, recallRequest, qualityDrop(primaryIsGlobal))
|
|
1715
|
+
? loadAmbientEntries(ctx.hippoRoot, ctx.tenantId, pinnedOnly, includeRecent, loadAdmit, recallRequest, qualityDrop(primaryIsGlobal), window)
|
|
1700
1716
|
: { entries: [] };
|
|
1701
1717
|
const globalLoad = hasGlobal && !primaryIsGlobal
|
|
1702
|
-
? loadAmbientEntries(globalRoot, ctx.tenantId, pinnedOnly, includeRecent, loadAdmit, recallRequest, qualityDrop(true))
|
|
1718
|
+
? loadAmbientEntries(globalRoot, ctx.tenantId, pinnedOnly, includeRecent, loadAdmit, recallRequest, qualityDrop(true), window)
|
|
1703
1719
|
: { entries: [] };
|
|
1704
1720
|
let localEntries = localLoad.entries;
|
|
1705
1721
|
let globalEntries = globalLoad.entries;
|
|
@@ -2402,7 +2418,7 @@ export function quarantineApprove(ctx, id) {
|
|
|
2402
2418
|
writeEntryMirrors(ctx.hippoRoot, restored);
|
|
2403
2419
|
}
|
|
2404
2420
|
catch (err) {
|
|
2405
|
-
|
|
2421
|
+
log.error(`quarantine: mirror rewrite failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2406
2422
|
}
|
|
2407
2423
|
}
|
|
2408
2424
|
/** Keep a quarantined memory hidden for good. Admin only; the raw row is untouched (append-only). */
|
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';
|
|
@@ -328,6 +330,7 @@ function safeJsonParse(raw) {
|
|
|
328
330
|
return v instanceof Object ? v : {};
|
|
329
331
|
}
|
|
330
332
|
catch {
|
|
333
|
+
// Malformed metadata reads as empty so the audit row itself stays listable.
|
|
331
334
|
return {};
|
|
332
335
|
}
|
|
333
336
|
}
|
package/dist/autolearn.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadAllEntries } from './store.js';
|
|
|
8
8
|
import { textOverlap } from './search.js';
|
|
9
9
|
import { isContentWorthStoring } from './audit.js';
|
|
10
10
|
import { redactSecretsStrict } from './secret-detect.js';
|
|
11
|
+
import { log } from './log.js';
|
|
11
12
|
/** A memory of a failed command, "Command '<cmd>' failed: <truncated stderr>"; no store is in reach, so `hippo watch` re-derives its half-life from the store's config. */
|
|
12
13
|
export function captureError(exitCode, stderr, command, tenantId) {
|
|
13
14
|
// Truncate to first 500 chars to avoid storing megabytes of build logs
|
|
@@ -154,7 +155,8 @@ export function isGitRepo(cwd) {
|
|
|
154
155
|
});
|
|
155
156
|
return raw.trim() === 'true';
|
|
156
157
|
}
|
|
157
|
-
catch {
|
|
158
|
+
catch (err) {
|
|
159
|
+
log.debug(`autolearn: not a git repo: ${err instanceof Error ? err.message : String(err)}`);
|
|
158
160
|
return false;
|
|
159
161
|
}
|
|
160
162
|
}
|
|
@@ -169,7 +171,8 @@ export function fetchGitLog(cwd, days) {
|
|
|
169
171
|
], { encoding: 'utf8', cwd, timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
170
172
|
return raw;
|
|
171
173
|
}
|
|
172
|
-
catch {
|
|
174
|
+
catch (err) {
|
|
175
|
+
log.debug(`autolearn: git log unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
173
176
|
return '';
|
|
174
177
|
}
|
|
175
178
|
}
|
package/dist/capture.js
CHANGED
|
@@ -26,6 +26,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
26
26
|
import { loadConfig } from './config.js';
|
|
27
27
|
import { classifyOriginProject } from './project-identity.js';
|
|
28
28
|
import { isObjectLike, isStringValue, readClaudeCodePreCompact } from './capture-contract.js';
|
|
29
|
+
import { log } from './log.js';
|
|
29
30
|
// Sentence-level patterns
|
|
30
31
|
//
|
|
31
32
|
// T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
|
|
@@ -521,7 +522,7 @@ export function collectSessionTurns(jsonl, visit) {
|
|
|
521
522
|
entry = JSON.parse(line);
|
|
522
523
|
}
|
|
523
524
|
catch {
|
|
524
|
-
continue;
|
|
525
|
+
continue; // a torn or partial transcript line carries no turn
|
|
525
526
|
}
|
|
526
527
|
if (!isObjectLike(entry) || !('type' in entry))
|
|
527
528
|
continue;
|
|
@@ -660,7 +661,7 @@ export function resolveLastSessionTranscript(explicit, stdinText, opts) {
|
|
|
660
661
|
}
|
|
661
662
|
}
|
|
662
663
|
catch {
|
|
663
|
-
return null;
|
|
664
|
+
return null; // an unlistable projects dir means no transcript to find; the caller prints that
|
|
664
665
|
}
|
|
665
666
|
return newest?.path ?? null;
|
|
666
667
|
}
|
|
@@ -698,7 +699,7 @@ function beginLogTee(logFile) {
|
|
|
698
699
|
fs.appendFileSync(logFile, `[hippo] ${new Date().toISOString()} capturing session...\n`, 'utf8');
|
|
699
700
|
}
|
|
700
701
|
catch (err) {
|
|
701
|
-
|
|
702
|
+
log.warn(`could not open log file ${logFile}: ${errorMessage(err)}`);
|
|
702
703
|
return () => { };
|
|
703
704
|
}
|
|
704
705
|
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
@@ -734,6 +735,7 @@ function beginLogTee(logFile) {
|
|
|
734
735
|
process.stderr.write = origStderrWrite;
|
|
735
736
|
};
|
|
736
737
|
}
|
|
738
|
+
// Console lines here are the `hippo capture` command's printed result, so they stay off the logger.
|
|
737
739
|
function cmdCaptureCore(hippoRoot, options) {
|
|
738
740
|
const useGlobal = options.global;
|
|
739
741
|
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
@@ -993,7 +995,7 @@ function lastPlainUserMessage(jsonl) {
|
|
|
993
995
|
entry = JSON.parse(lines[i]);
|
|
994
996
|
}
|
|
995
997
|
catch {
|
|
996
|
-
continue;
|
|
998
|
+
continue; // the tail read can start mid-line; skip the torn fragment
|
|
997
999
|
}
|
|
998
1000
|
if (!isObjectLike(entry))
|
|
999
1001
|
continue;
|
|
@@ -1023,7 +1025,7 @@ function lastAssistantTextBlock(jsonl) {
|
|
|
1023
1025
|
entry = JSON.parse(lines[i]);
|
|
1024
1026
|
}
|
|
1025
1027
|
catch {
|
|
1026
|
-
continue;
|
|
1028
|
+
continue; // the tail read can start mid-line; skip the torn fragment
|
|
1027
1029
|
}
|
|
1028
1030
|
if (!isObjectLike(entry))
|
|
1029
1031
|
continue;
|
|
@@ -1085,7 +1087,7 @@ function isReadableFile(filePath) {
|
|
|
1085
1087
|
return true;
|
|
1086
1088
|
}
|
|
1087
1089
|
catch {
|
|
1088
|
-
return false;
|
|
1090
|
+
return false; // missing and unreadable both mean "no file" to the caller
|
|
1089
1091
|
}
|
|
1090
1092
|
}
|
|
1091
1093
|
/** PreCompact stdout is the summariser's instructions; sent before the snapshot work because a locked store can run the hook past its 30 s limit, and via writeSync because process.exit drops buffered pipe output. */
|
|
@@ -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
|
}
|