hippo-memory 1.52.8 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -101
- package/dist/agent-memories/apply.d.ts +47 -0
- package/dist/agent-memories/apply.js +253 -0
- package/dist/agent-memories/claude-code.d.ts +11 -0
- package/dist/agent-memories/claude-code.js +113 -0
- package/dist/agent-memories/codex.d.ts +3 -0
- package/dist/agent-memories/codex.js +47 -0
- package/dist/agent-memories/copilot.d.ts +3 -0
- package/dist/agent-memories/copilot.js +125 -0
- package/dist/agent-memories/files.d.ts +37 -0
- package/dist/agent-memories/files.js +77 -0
- package/dist/agent-memories/folder-store.d.ts +17 -0
- package/dist/agent-memories/folder-store.js +44 -0
- package/dist/agent-memories/gemini.d.ts +3 -0
- package/dist/agent-memories/gemini.js +103 -0
- package/dist/agent-memories/git.d.ts +8 -0
- package/dist/agent-memories/git.js +11 -0
- package/dist/agent-memories/keys.d.ts +9 -0
- package/dist/agent-memories/keys.js +20 -0
- package/dist/agent-memories/legacy.d.ts +17 -0
- package/dist/agent-memories/legacy.js +45 -0
- package/dist/agent-memories/markdown.d.ts +13 -0
- package/dist/agent-memories/markdown.js +123 -0
- package/dist/agent-memories/openclaw.d.ts +3 -0
- package/dist/agent-memories/openclaw.js +42 -0
- package/dist/agent-memories/plan.d.ts +78 -0
- package/dist/agent-memories/plan.js +123 -0
- package/dist/agent-memories/qwen-code.d.ts +5 -0
- package/dist/agent-memories/qwen-code.js +50 -0
- package/dist/agent-memories/report.d.ts +52 -0
- package/dist/agent-memories/report.js +88 -0
- package/dist/agent-memories/source.d.ts +16 -0
- package/dist/agent-memories/source.js +32 -0
- package/dist/agent-memories/sync.d.ts +33 -0
- package/dist/agent-memories/sync.js +336 -0
- package/dist/agent-memories/tools.d.ts +33 -0
- package/dist/agent-memories/tools.js +19 -0
- package/dist/agent-memories/types.d.ts +42 -0
- package/dist/agent-memories/types.js +2 -0
- package/dist/api.d.ts +53 -20
- package/dist/api.js +141 -97
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +68 -2
- package/dist/capture.d.ts +48 -22
- package/dist/capture.js +186 -161
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +750 -797
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/compaction-items.d.ts +18 -0
- package/dist/compaction-items.js +60 -0
- package/dist/compaction-record.d.ts +94 -0
- package/dist/compaction-record.js +546 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +13 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +87 -34
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/db.d.ts +5 -1
- package/dist/db.js +46 -14
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +69 -4
- package/dist/dormant.d.ts +9 -3
- package/dist/dormant.js +26 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/gated-write.d.ts +9 -0
- package/dist/gated-write.js +24 -0
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +5 -3
- package/dist/hooks.d.ts +18 -2
- package/dist/hooks.js +128 -32
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +171 -210
- package/dist/memory.d.ts +19 -2
- package/dist/memory.js +35 -3
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +6 -0
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +10 -2
- package/dist/shared.js +44 -36
- package/dist/store.d.ts +9 -2
- package/dist/store.js +25 -2
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -37,24 +37,24 @@ import * as path from 'path';
|
|
|
37
37
|
import * as fs from 'fs';
|
|
38
38
|
import * as os from 'os';
|
|
39
39
|
import { fileURLToPath } from 'node:url';
|
|
40
|
-
import { execFileSync, execSync, spawn
|
|
41
|
-
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
42
|
-
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
|
|
43
|
-
import { detectSecret } from './secret-detect.js';
|
|
40
|
+
import { execFileSync, execSync, spawn } from 'child_process';
|
|
41
|
+
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, detectRealCodexPath, isCodexPresent, isCodexWrapperInstalled, CODEX_TRUST_LINE, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
42
|
+
import { createMemory, createSuccessor, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
|
|
44
43
|
import { getHippoRoot, isInitialized, initStore, writeEntry, strengthenRetrieved, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardRuns, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, } from './store.js';
|
|
45
44
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
46
45
|
import { RejectedValueError } from './rejection.js';
|
|
47
46
|
import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
|
|
47
|
+
import { readSessionScan, recordSessionDigest } from './session-digest.js';
|
|
48
48
|
import { isCardStatus } from './card.js';
|
|
49
49
|
import { loadCardDetail } from './card-detail.js';
|
|
50
50
|
import { passesScopeFilterForRecall } from './recall-scope.js';
|
|
51
|
-
import { estimateTokens, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
51
|
+
import { estimateTokens, fitBudget, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
52
52
|
import { compareEntryIdentity } from './compare.js';
|
|
53
53
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
54
54
|
import { writeRecallTraceAtRoot } from './recall-trace.js';
|
|
55
55
|
import { deduplicateStore } from './dedupe.js';
|
|
56
56
|
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, resolveEmbeddingModel, embeddingModelRequiresReindex, } from './embeddings.js';
|
|
57
|
-
import {
|
|
57
|
+
import { resolveEmbeddingProvider } from './embedding-provider.js';
|
|
58
58
|
import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
59
59
|
import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
60
60
|
import { loadConfig } from './config.js';
|
|
@@ -63,21 +63,25 @@ import { runDoctor, formatDoctor } from './doctor.js';
|
|
|
63
63
|
import { buildSupportBundle, TAIL_MAX_LINES } from './support-bundle.js';
|
|
64
64
|
import { PACKAGE_VERSION } from './version.js';
|
|
65
65
|
import { captureToolFailure } from './capture-error.js';
|
|
66
|
-
import { blockHash, hookPayloadSessionId, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
66
|
+
import { blockHash, hookPayloadSessionId, isSubagentPayload, lastSentState, readApiCalls, recordRereads, recordTokenUse, shouldSkipUnchanged, } from './token-ledger.js';
|
|
67
67
|
import { FAILURE_LOG_RETENTION_DAYS } from './failure-log.js';
|
|
68
68
|
import { pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal, applyGoalStackBoost } from './goals.js';
|
|
69
69
|
import { rowToGoal } from './goals.js';
|
|
70
|
-
import { captureError, extractLessons, partitionLessons,
|
|
70
|
+
import { captureError, extractLessons, partitionLessons, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
|
|
71
|
+
import { dropHeldCopies, duplicateKey, storedTextKeys } from './same-text.js';
|
|
72
|
+
import { currentMachine, importAtCompaction, importAtSessionEnd, importForStore, importProjectMemories, importUserMemories, } from './agent-memories/sync.js';
|
|
73
|
+
import { detailLines, emptyReport, mergeReports, summaryLine } from './agent-memories/report.js';
|
|
71
74
|
import { extractInvalidationTarget, invalidateMatching, detectChurnStale } from './invalidation.js';
|
|
72
|
-
import {
|
|
75
|
+
import { deriveOriginProject, isGlobalStoreRoot, resolveProjectIdentity } from './project-identity.js';
|
|
73
76
|
import { extractPathTags } from './path-context.js';
|
|
74
77
|
import { detectScope } from './scope.js';
|
|
75
78
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
76
|
-
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
79
|
+
import { DAILY_TASK_NAME, buildDailyRunnerCommand, buildSchtasksCreateArgs, buildWindowsTaskRun, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
77
80
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
|
|
78
|
-
import { cmdCapture, cmdPreCompact,
|
|
81
|
+
import { cmdCapture, cmdPreCompact, cmdPostCompact, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage, transcriptWorkingState } from './capture.js';
|
|
82
|
+
import { COMPACTION_DB_WAIT_MS, replayCompactionsAt } from './compaction-record.js';
|
|
79
83
|
import { readStdinBounded } from './stdin.js';
|
|
80
|
-
import { auditMemories, appendAuditEvent, } from './audit.js';
|
|
84
|
+
import { auditMemories, appendAuditEvent, AUDIT_OPS, } from './audit.js';
|
|
81
85
|
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
82
86
|
import { buildProvenanceCoverage } from './provenance-coverage.js';
|
|
83
87
|
import { buildCorrectionLatency } from './correction-latency.js';
|
|
@@ -132,6 +136,7 @@ import { getReranker } from './rerankers/index.js';
|
|
|
132
136
|
import { JEV_DEFAULT_TOP_K } from './rerankers/jev.js';
|
|
133
137
|
import { computeSalience } from './salience.js';
|
|
134
138
|
import { renderAmbientSummary } from './ambient.js';
|
|
139
|
+
import { assembleCost, assembleHeading, contextCost, contextHeading, contextLine, crossProjectHeading, crossProjectLine, drillCost, handoffText, printedTokens, sessionTrailText, settleTokens, snapshotText, } from './context-render.js';
|
|
135
140
|
import { validateOwner, isStrictOwnerEnv } from './owner-validation.js';
|
|
136
141
|
import { pruneAuditLog, parseOlderThanFlag } from './audit-prune.js';
|
|
137
142
|
import { listDlq, replayDlqEntry } from './connectors/slack/dlq.js';
|
|
@@ -282,7 +287,7 @@ async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
282
287
|
// and as off under === true (`--pin=true` would not pin), so parseArgs and main() refuse one.
|
|
283
288
|
// tests/cli-parse-flag-equals.test.ts fails when a switch read is missing from this set.
|
|
284
289
|
export const BOOLEAN_FLAGS = new Set([
|
|
285
|
-
'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
|
|
290
|
+
'agents', 'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
|
|
286
291
|
'cross-project', 'dry-run', 'equal-sources', 'error', 'evc-adaptive', 'extract',
|
|
287
292
|
'filter-conflicts', 'fix', 'force', 'forget', 'git', 'global', 'good', 'graph-stream',
|
|
288
293
|
'help', 'include-logs', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
@@ -407,6 +412,71 @@ export function parseArgs(argv) {
|
|
|
407
412
|
function fmt(n, digits = 2) {
|
|
408
413
|
return n.toFixed(digits);
|
|
409
414
|
}
|
|
415
|
+
// What `hippo recall` prints for one result; the budget prices this same text.
|
|
416
|
+
function recallEntryText(r, query, showWhy, isGlobal) {
|
|
417
|
+
const e = r.entry;
|
|
418
|
+
const label = confidenceLabel(e);
|
|
419
|
+
const confLabel = label.warn ? `[${label.text}] ⚠️` : `[${label.text}]`;
|
|
420
|
+
const bars = Math.round(e.strength * 10);
|
|
421
|
+
const graphMark = r.graphVia ? ` [graph: ${r.graphVia.hops}hop ${r.graphVia.relType}]` : '';
|
|
422
|
+
const lines = [
|
|
423
|
+
`--- ${e.id} [${e.layer}] ${confLabel}${isGlobal ? ' [global]' : ''}${e.superseded_by ? ' [superseded]' : ''}${graphMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`,
|
|
424
|
+
` [${'█'.repeat(bars)}${'░'.repeat(10 - bars)}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`,
|
|
425
|
+
];
|
|
426
|
+
if (showWhy) {
|
|
427
|
+
const explanation = explainMatch(query, r);
|
|
428
|
+
lines.push(` source:${isGlobal ? ' [global]' : ' [local]'} | layer: [${e.layer}] | confidence: [${label.text}]`, ` reason: ${explanation.reason}`);
|
|
429
|
+
const env = explanation.envelope;
|
|
430
|
+
if (env) {
|
|
431
|
+
lines.push(` kind: ${env.kind}`);
|
|
432
|
+
if (env.scope)
|
|
433
|
+
lines.push(` scope: ${env.scope}`);
|
|
434
|
+
if (env.owner)
|
|
435
|
+
lines.push(` owner: ${env.owner}`);
|
|
436
|
+
if (env.artifact_ref)
|
|
437
|
+
lines.push(` artifact_ref: ${env.artifact_ref}`);
|
|
438
|
+
if (env.session_id)
|
|
439
|
+
lines.push(` session_id: ${env.session_id}`);
|
|
440
|
+
lines.push(` confidence: ${env.confidence}`);
|
|
441
|
+
}
|
|
442
|
+
// A7 recall-trace, e.g. "ranking: base 0.420 -> interference x0.30 -> 0.126 -> goal-boost x1.50 -> 0.189".
|
|
443
|
+
if (r.rerankTrace && r.rerankTrace.length > 0) {
|
|
444
|
+
const parts = [`base ${fmt(r.rerankTrace[0].scoreBefore, 3)}`];
|
|
445
|
+
for (const step of r.rerankTrace) {
|
|
446
|
+
parts.push(`${step.stage}${step.multiplier !== undefined ? ` x${fmt(step.multiplier, 2)}` : ''}`, fmt(step.scoreAfter, 3));
|
|
447
|
+
}
|
|
448
|
+
lines.push(` ranking: ${parts.join(' -> ')}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
lines.push('', e.content, '');
|
|
452
|
+
return lines.join('\n');
|
|
453
|
+
}
|
|
454
|
+
function recallHeading(entries, tokens, query) {
|
|
455
|
+
return `Found ${entries} memories (${tokens} tokens) for: "${query}"\n`;
|
|
456
|
+
}
|
|
457
|
+
// JSON.stringify keeps quotes or parens in the matched phrase from blurring the line.
|
|
458
|
+
function planningLine(p) {
|
|
459
|
+
if (p.hint)
|
|
460
|
+
return `Planning fallacy hint (class: ${p.hint.classTag}): ${p.hint.baserateSummary} [detected: ${JSON.stringify(p.hint.detectedPhrase)}]`;
|
|
461
|
+
if (p.watching)
|
|
462
|
+
return `Planning fallacy: watching this query (reason: ${p.watching.reason}). ${p.watching.suggestion} [detected: ${JSON.stringify(p.watching.detectedPhrase)}]`;
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
function cutoffLine(shown, s) {
|
|
466
|
+
const clauses = [];
|
|
467
|
+
// The residual covers rank, budget and limit drops alike, and fires with no --limit at all.
|
|
468
|
+
if (s.droppedByBudget > 0)
|
|
469
|
+
clauses.push(`${s.droppedByBudget} not shown (rank, budget or limit)`);
|
|
470
|
+
if (s.droppedPreRank > 0)
|
|
471
|
+
clauses.push(`${s.droppedPreRank} filtered pre-rank`);
|
|
472
|
+
if (s.summarySubstitutionsAdded > 0)
|
|
473
|
+
clauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
|
|
474
|
+
if (s.freshTailAdded > 0)
|
|
475
|
+
clauses.push(`${s.freshTailAdded} fresh-tail added`);
|
|
476
|
+
if (s.suppressedByInterference > 0)
|
|
477
|
+
clauses.push(`${s.suppressedByInterference} suppressed by interference`);
|
|
478
|
+
return clauses.length > 0 ? `Cutoff: showing ${shown} of ${s.totalCandidates} candidates; ${clauses.join('; ')}.` : null;
|
|
479
|
+
}
|
|
410
480
|
// ---------------------------------------------------------------------------
|
|
411
481
|
// Commands
|
|
412
482
|
// ---------------------------------------------------------------------------
|
|
@@ -460,6 +530,8 @@ function cmdInitScan(scanDir, flags) {
|
|
|
460
530
|
// that a dropped subject is never invisible.
|
|
461
531
|
let totalLowInfo = 0;
|
|
462
532
|
const seedDays = parseInt(String(flags['days'] ?? '365'), 10);
|
|
533
|
+
const machine = currentMachine();
|
|
534
|
+
const agentImport = emptyReport();
|
|
463
535
|
for (const repo of repos) {
|
|
464
536
|
const name = path.basename(repo);
|
|
465
537
|
const repoHippo = path.join(repo, '.hippo');
|
|
@@ -476,6 +548,8 @@ function cmdInitScan(scanDir, flags) {
|
|
|
476
548
|
totalLessons += added;
|
|
477
549
|
totalLowInfo += result.lowInfo;
|
|
478
550
|
}
|
|
551
|
+
if (!flags['no-learn'])
|
|
552
|
+
mergeReports(agentImport, importProjectMemories(repoHippo, { machine }));
|
|
479
553
|
const status = alreadyExists ? 'existing' : 'new';
|
|
480
554
|
const entries = loadAllEntries(repoHippo);
|
|
481
555
|
console.log(` ${name.padEnd(25)} ${status.padEnd(10)} ${entries.length} memories${added > 0 ? ` (+${added} from git)` : ''}`);
|
|
@@ -483,8 +557,12 @@ function cmdInitScan(scanDir, flags) {
|
|
|
483
557
|
console.log(`\n${repos.length} repositories, ${totalLessons} new lessons learned` +
|
|
484
558
|
(totalLowInfo > 0 ? `, ${totalLowInfo} low-information subject(s) dropped` : '') +
|
|
485
559
|
'.');
|
|
560
|
+
if (!flags['no-learn']) {
|
|
561
|
+
mergeReports(agentImport, importUserMemories(globalRoot, { machine }));
|
|
562
|
+
printAgentImport(agentImport, '');
|
|
563
|
+
}
|
|
486
564
|
console.log(`Global store: ${globalRoot}`);
|
|
487
|
-
if (
|
|
565
|
+
if (initInstallsIntegrations(flags)) {
|
|
488
566
|
// User-level hooks only: a hippo block in each repo's CLAUDE.md or AGENTS.md would leave a diff in every repo.
|
|
489
567
|
const agents = detectAgentHooks(repos);
|
|
490
568
|
if (agents.length === 0) {
|
|
@@ -508,10 +586,13 @@ function cmdInit(hippoRoot, flags) {
|
|
|
508
586
|
const globalRoot = getGlobalRoot();
|
|
509
587
|
if (isInitialized(globalRoot)) {
|
|
510
588
|
console.log('Already initialized global store at', globalRoot);
|
|
511
|
-
return;
|
|
512
589
|
}
|
|
513
|
-
|
|
514
|
-
|
|
590
|
+
else {
|
|
591
|
+
initGlobal();
|
|
592
|
+
console.log('Initialized global Hippo store at', globalRoot);
|
|
593
|
+
}
|
|
594
|
+
if (!flags['no-learn'])
|
|
595
|
+
printAgentImport(importUserMemories(globalRoot, { machine: currentMachine() }));
|
|
515
596
|
return;
|
|
516
597
|
}
|
|
517
598
|
const alreadyExists = isInitialized(hippoRoot);
|
|
@@ -526,8 +607,7 @@ function cmdInit(hippoRoot, flags) {
|
|
|
526
607
|
}
|
|
527
608
|
const globalRoot = getGlobalRoot();
|
|
528
609
|
registerWorkspace(globalRoot, path.dirname(hippoRoot));
|
|
529
|
-
|
|
530
|
-
if (!flags['no-hooks']) {
|
|
610
|
+
if (initInstallsIntegrations(flags)) {
|
|
531
611
|
autoInstallHooks();
|
|
532
612
|
}
|
|
533
613
|
// Auto-setup daily schedule (unless --no-schedule)
|
|
@@ -547,11 +627,27 @@ function cmdInit(hippoRoot, flags) {
|
|
|
547
627
|
console.log(` No matching commits found in git history.`);
|
|
548
628
|
}
|
|
549
629
|
}
|
|
550
|
-
const memImported = learnFromMemoryMd(hippoRoot);
|
|
551
|
-
if (memImported > 0) {
|
|
552
|
-
console.log(` Imported ${memImported} memories from this project's Claude Code auto memory.`);
|
|
553
|
-
}
|
|
554
630
|
}
|
|
631
|
+
// Every run, not only the first: an agent's notes change between inits.
|
|
632
|
+
if (!flags['no-learn'])
|
|
633
|
+
printAgentImport(importForStore(hippoRoot, { machine: currentMachine() }));
|
|
634
|
+
}
|
|
635
|
+
/** One line when an agent memory import moved anything; its warnings go to stderr. */
|
|
636
|
+
function printAgentImport(report, indent = ' ') {
|
|
637
|
+
const line = summaryLine(report);
|
|
638
|
+
if (line !== null)
|
|
639
|
+
console.log(`${indent}${line}`);
|
|
640
|
+
for (const warning of report.warnings)
|
|
641
|
+
console.error(`hippo: agent memories: ${warning}`);
|
|
642
|
+
}
|
|
643
|
+
/** Every write init makes into agent config (instruction blocks, hooks, plugins) is an automatic integration, so one switch skips them all. */
|
|
644
|
+
function initInstallsIntegrations(flags) {
|
|
645
|
+
if (flags['no-hooks'])
|
|
646
|
+
return false;
|
|
647
|
+
if (process.env.HIPPO_SKIP_AUTO_INTEGRATIONS !== '1')
|
|
648
|
+
return true;
|
|
649
|
+
console.log(' HIPPO_SKIP_AUTO_INTEGRATIONS=1, so init left agent instruction files and hooks alone.');
|
|
650
|
+
return false;
|
|
555
651
|
}
|
|
556
652
|
/** Plain init: patch the detected agents' instruction files in cwd, then install their user-level hooks. */
|
|
557
653
|
function autoInstallHooks() {
|
|
@@ -628,13 +724,32 @@ function refreshShippedBlock(filePath, text, hook) {
|
|
|
628
724
|
fs.writeFileSync(filePath, `${text.slice(0, start)}${eol}${HOOKS[owner].content.replace(/\n/g, eol)}${eol}${text.slice(end)}`, 'utf8');
|
|
629
725
|
console.log(` Refreshed the ${owner} hippo block in ${name}`);
|
|
630
726
|
}
|
|
631
|
-
/**
|
|
727
|
+
/** Adds hippo's two Codex hooks and says what changed; each install ends on the trust reminder, since Codex skips an untrusted hook. */
|
|
728
|
+
function installCodexMemoryHooks(indent) {
|
|
729
|
+
const result = installJsonHooks('codex');
|
|
730
|
+
if (result.invalidJson) {
|
|
731
|
+
console.log(`${indent}WARNING: ${result.settingsPath} is not a hooks file hippo can merge into; fix it, then run \`hippo hook install codex\`.`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const added = [
|
|
735
|
+
result.installedUserPromptSubmit ? 'UserPromptSubmit' : '',
|
|
736
|
+
result.installedCompactResume ? 'SessionStart(compact)' : '',
|
|
737
|
+
].filter(Boolean);
|
|
738
|
+
console.log(added.length > 0
|
|
739
|
+
? `${indent}Installed hippo's Codex memory hooks (${added.join(', ')}) in ${result.settingsPath}`
|
|
740
|
+
: `${indent}hippo's Codex memory hooks already in ${result.settingsPath}`);
|
|
741
|
+
console.log(`${indent}${CODEX_TRUST_LINE}`);
|
|
742
|
+
}
|
|
743
|
+
/** Claude Code settings hooks, Codex's hooks.json and the OpenCode plugin, under the home directory; idempotent, so re-running init adds newer hooks. */
|
|
632
744
|
function installUserLevelHooks(agents, codexHint) {
|
|
633
745
|
for (const hook of agents) {
|
|
634
746
|
// The Codex capture wrapper swaps the codex launcher binary, so init only points at the opt-in (issue #133).
|
|
635
747
|
if (hook === 'codex' && codexHint && !isCodexWrapperInstalled()) {
|
|
636
748
|
console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
|
|
637
749
|
}
|
|
750
|
+
// Checked first so init never creates ~/.codex on a machine without Codex.
|
|
751
|
+
if (hook === 'codex' && isCodexPresent())
|
|
752
|
+
installCodexMemoryHooks(' ');
|
|
638
753
|
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
639
754
|
// settings.json. Keeps `hippo init` in lockstep with `hippo hook install
|
|
640
755
|
// claude-code` and `hippo setup`.
|
|
@@ -719,13 +834,13 @@ function setupDailySchedule(globalRoot) {
|
|
|
719
834
|
// Task doesn't exist, create it
|
|
720
835
|
}
|
|
721
836
|
try {
|
|
722
|
-
|
|
837
|
+
execFileSync('schtasks', buildSchtasksCreateArgs(taskName, cmd), { stdio: 'pipe', windowsHide: true });
|
|
723
838
|
console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
|
|
724
839
|
}
|
|
725
840
|
catch {
|
|
726
841
|
// No admin rights or schtasks unavailable, fall back to printing instructions
|
|
727
842
|
console.log(` To schedule the machine-level daily runner, run:`);
|
|
728
|
-
console.log(` schtasks /create /tn "${taskName}" /tr "cmd
|
|
843
|
+
console.log(` schtasks /create /tn "${taskName}" /tr "${buildWindowsTaskRun(cmd).replace(/"/g, '\\"')}" /sc daily /st 06:15`);
|
|
729
844
|
}
|
|
730
845
|
}
|
|
731
846
|
else {
|
|
@@ -748,6 +863,25 @@ function setupDailySchedule(globalRoot) {
|
|
|
748
863
|
}
|
|
749
864
|
}
|
|
750
865
|
}
|
|
866
|
+
// Shared by the direct write and the routed request so both store the same tags.
|
|
867
|
+
function rememberTags(flags, cwd) {
|
|
868
|
+
const requested = Array.isArray(flags['tag']) ? [...flags['tag']] : [];
|
|
869
|
+
if (flags['error'])
|
|
870
|
+
requested.push('error');
|
|
871
|
+
const all = [...requested];
|
|
872
|
+
for (const pt of extractPathTags(cwd)) {
|
|
873
|
+
if (!all.includes(pt))
|
|
874
|
+
all.push(pt);
|
|
875
|
+
}
|
|
876
|
+
const explicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
877
|
+
const activeScope = explicitScope || detectScope();
|
|
878
|
+
if (activeScope) {
|
|
879
|
+
const scopeTag = `scope:${activeScope}`;
|
|
880
|
+
if (!all.includes(scopeTag))
|
|
881
|
+
all.push(scopeTag);
|
|
882
|
+
}
|
|
883
|
+
return { requested, all };
|
|
884
|
+
}
|
|
751
885
|
async function cmdRemember(hippoRoot, text, flags) {
|
|
752
886
|
const useGlobal = Boolean(flags['global']);
|
|
753
887
|
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
@@ -757,9 +891,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
757
891
|
else {
|
|
758
892
|
requireInit(hippoRoot);
|
|
759
893
|
}
|
|
760
|
-
const
|
|
761
|
-
if (flags['error'])
|
|
762
|
-
rawTags.push('error');
|
|
894
|
+
const { requested: requestedTags, all: allTags } = rememberTags(flags, process.cwd());
|
|
763
895
|
// Resolve explicit confidence flag (default: 'verified' for manual remember)
|
|
764
896
|
let confidence = 'verified';
|
|
765
897
|
if (flags['observed'])
|
|
@@ -768,9 +900,9 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
768
900
|
confidence = 'inferred';
|
|
769
901
|
if (flags['verified'])
|
|
770
902
|
confidence = 'verified';
|
|
771
|
-
//
|
|
903
|
+
// Schema fit needs the store, which the routed request has no access to, so it stays here.
|
|
772
904
|
const existing = loadAllEntries(targetRoot, resolveTenantId({}));
|
|
773
|
-
const schemaFit = computeSchemaFit(text,
|
|
905
|
+
const schemaFit = computeSchemaFit(text, requestedTags, existing);
|
|
774
906
|
// A3 envelope flags
|
|
775
907
|
const kindFlagRaw = typeof flags['kind'] === 'string' ? flags['kind'] : undefined;
|
|
776
908
|
const kindFlag = kindFlagRaw === undefined ? undefined : kindFlagRaw.toLowerCase();
|
|
@@ -802,7 +934,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
802
934
|
const rememberConfig = loadConfig(targetRoot);
|
|
803
935
|
const entry = createMemory(text, {
|
|
804
936
|
layer: Layer.Episodic,
|
|
805
|
-
tags:
|
|
937
|
+
tags: allTags,
|
|
806
938
|
pinned: Boolean(flags['pin']),
|
|
807
939
|
source: useGlobal ? 'cli-global' : 'cli',
|
|
808
940
|
confidence,
|
|
@@ -814,20 +946,6 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
814
946
|
tenantId,
|
|
815
947
|
baseHalfLifeDays: rememberConfig.defaultHalfLifeDays,
|
|
816
948
|
});
|
|
817
|
-
// Auto-tag with path context
|
|
818
|
-
const pathTags = extractPathTags(process.cwd());
|
|
819
|
-
for (const pt of pathTags) {
|
|
820
|
-
if (!entry.tags.includes(pt))
|
|
821
|
-
entry.tags.push(pt);
|
|
822
|
-
}
|
|
823
|
-
// Scope tagging: explicit --scope or auto-detected
|
|
824
|
-
const explicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
825
|
-
const activeScope = explicitScope || detectScope();
|
|
826
|
-
if (activeScope) {
|
|
827
|
-
const scopeTag = `scope:${activeScope}`;
|
|
828
|
-
if (!entry.tags.includes(scopeTag))
|
|
829
|
-
entry.tags.push(scopeTag);
|
|
830
|
-
}
|
|
831
949
|
// Salience gate: decide if this memory is worth storing
|
|
832
950
|
if (rememberConfig.salience.enabled && !Boolean(flags['pin']) && !Boolean(flags['force'])) {
|
|
833
951
|
const salienceResult = computeSalience(text, entry.tags, existing, {
|
|
@@ -855,12 +973,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
855
973
|
console.log(` Tags: ${entry.tags.join(', ')}`);
|
|
856
974
|
if (entry.pinned)
|
|
857
975
|
console.log(' Pinned (no decay)');
|
|
858
|
-
|
|
859
|
-
if (isEmbeddingConfigured(targetRoot)) {
|
|
860
|
-
embedMemory(targetRoot, entry).catch(() => {
|
|
861
|
-
// Silently ignore embedding errors
|
|
862
|
-
});
|
|
863
|
-
}
|
|
976
|
+
void embedMemory(targetRoot, entry);
|
|
864
977
|
const config = loadConfig(targetRoot);
|
|
865
978
|
const shouldExtract = flags['extract'] || config.extraction.enabled === true;
|
|
866
979
|
const apiKey = process.env.ANTHROPIC_API_KEY ?? '';
|
|
@@ -897,23 +1010,20 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
897
1010
|
console.error(`Error: memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
898
1011
|
process.exit(1);
|
|
899
1012
|
}
|
|
900
|
-
const layer =
|
|
1013
|
+
const layer = typeof flags['layer'] === 'string' ? flags['layer'] : undefined;
|
|
901
1014
|
const rawTags = flags['tag'];
|
|
902
1015
|
const tags = Array.isArray(rawTags)
|
|
903
1016
|
? rawTags.map((t) => String(t))
|
|
904
1017
|
: typeof rawTags === 'string'
|
|
905
1018
|
? rawTags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
906
|
-
:
|
|
1019
|
+
: undefined;
|
|
907
1020
|
const pinned = flags['pin'] === true || old.pinned;
|
|
908
|
-
const newEntry =
|
|
1021
|
+
const newEntry = createSuccessor(old, newContent, {
|
|
1022
|
+
tenantId: old.tenantId,
|
|
1023
|
+
baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
|
|
909
1024
|
layer,
|
|
910
1025
|
tags,
|
|
911
1026
|
pinned,
|
|
912
|
-
source: old.source,
|
|
913
|
-
confidence: 'verified',
|
|
914
|
-
tenantId: old.tenantId,
|
|
915
|
-
scope: old.scope,
|
|
916
|
-
baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
|
|
917
1027
|
});
|
|
918
1028
|
// AT1: write the SUCCESSOR first. The rejection guard fires on the new
|
|
919
1029
|
// content — if it refuses, nothing has been mutated yet (the old ordering
|
|
@@ -954,6 +1064,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
954
1064
|
process.exit(1);
|
|
955
1065
|
}
|
|
956
1066
|
const globalRoot = getGlobalRoot();
|
|
1067
|
+
const primaryIsGlobal = isGlobalStoreRoot(hippoRoot);
|
|
957
1068
|
// A5 stub auth: resolve the active tenant once and thread it through every
|
|
958
1069
|
// recall-time SELECT against `memories`. Cross-tenant rows must never surface.
|
|
959
1070
|
const tenantId = resolveTenantId({});
|
|
@@ -973,7 +1084,9 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
973
1084
|
const requestedScopeForFilter = recallExplicitScope || undefined;
|
|
974
1085
|
const loadSuperseded = includeSuperseded || Boolean(asOf);
|
|
975
1086
|
let localEntries = loadRecallSearchEntries(hippoRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive', loadSuperseded);
|
|
976
|
-
let globalEntries =
|
|
1087
|
+
let globalEntries = globalRoot !== hippoRoot && isInitialized(globalRoot)
|
|
1088
|
+
? loadRecallSearchEntries(globalRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive', loadSuperseded)
|
|
1089
|
+
: [];
|
|
977
1090
|
// v1.12.13 / C5 — WYSIATI counters. Track filter activity per the plan v3
|
|
978
1091
|
// Task 3 mapping table. dropped_pre_rank is the SUM of all non-budget
|
|
979
1092
|
// filter drops (pre-rank AND post-rank); its meaning is unchanged by C5.
|
|
@@ -1096,6 +1209,12 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1096
1209
|
}
|
|
1097
1210
|
graphStreamSeeds = s;
|
|
1098
1211
|
}
|
|
1212
|
+
// Engines spend the budget on the text each result prints as, less the header, so selection and print agree.
|
|
1213
|
+
const localIndex = loadIndex(hippoRoot);
|
|
1214
|
+
const globalOn = isInitialized(globalRoot);
|
|
1215
|
+
const entryText = (r) => recallEntryText(r, query, showWhy, primaryIsGlobal || (globalOn && !localIndex.entries[r.entry.id]));
|
|
1216
|
+
const printCost = (r) => printedTokens(entryText(r));
|
|
1217
|
+
const entryBudget = Math.max(0, budget - printedTokens(recallHeading(budget, budget, query)));
|
|
1099
1218
|
let results;
|
|
1100
1219
|
if (useGraphStream) {
|
|
1101
1220
|
if (!isEmbeddingAvailable()) {
|
|
@@ -1111,7 +1230,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1111
1230
|
// tail; on a pool with <= seedCount candidates EVERY candidate is a seed and the stream
|
|
1112
1231
|
// is inert (it degrades to the 2-list fusion). Tune the anchor count with --graph-seeds.
|
|
1113
1232
|
results = await hybridSearch(query, localEntries, {
|
|
1114
|
-
budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1233
|
+
budget: entryBudget, cost: printCost, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1115
1234
|
includeSuperseded, asOf,
|
|
1116
1235
|
scoring: 'rrf',
|
|
1117
1236
|
graphStream: { weight: DEFAULT_GRAPH_STREAM_WEIGHT, tenantId, hops: graphStreamHops, seedCount: graphStreamSeeds },
|
|
@@ -1121,7 +1240,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1121
1240
|
// Unlike searchBothHybrid below, multihop ranks one pooled list, so a shared memory's two copies both compete.
|
|
1122
1241
|
const allEntries = api.oneCopyPerMemory(localEntries, globalEntries, evalNow()).flat();
|
|
1123
1242
|
results = multihopSearch(query, allEntries, {
|
|
1124
|
-
budget,
|
|
1243
|
+
budget: entryBudget,
|
|
1244
|
+
cost: printCost,
|
|
1125
1245
|
hippoRoot,
|
|
1126
1246
|
minResults,
|
|
1127
1247
|
includeSuperseded,
|
|
@@ -1130,7 +1250,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1130
1250
|
}
|
|
1131
1251
|
else if (usePhysics && !hasGlobal) {
|
|
1132
1252
|
results = await physicsSearch(query, localEntries, {
|
|
1133
|
-
budget,
|
|
1253
|
+
budget: entryBudget,
|
|
1254
|
+
cost: printCost,
|
|
1134
1255
|
hippoRoot,
|
|
1135
1256
|
physicsConfig: config.physics,
|
|
1136
1257
|
minResults,
|
|
@@ -1145,7 +1266,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1145
1266
|
// so the scope rule must be plumbed in — the filtered localEntries /
|
|
1146
1267
|
// globalEntries above are NOT what this path ranks.
|
|
1147
1268
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
1148
|
-
budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope, tenantId,
|
|
1269
|
+
budget: entryBudget, cost: printCost, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope, tenantId,
|
|
1149
1270
|
includeSuperseded, asOf,
|
|
1150
1271
|
recallScope: recallExplicitScope
|
|
1151
1272
|
? { requested: recallExplicitScope, additive: true }
|
|
@@ -1154,7 +1275,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1154
1275
|
}
|
|
1155
1276
|
else {
|
|
1156
1277
|
results = await hybridSearch(query, localEntries, {
|
|
1157
|
-
budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1278
|
+
budget: entryBudget, cost: printCost, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1158
1279
|
includeSuperseded, asOf,
|
|
1159
1280
|
});
|
|
1160
1281
|
}
|
|
@@ -1211,7 +1332,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1211
1332
|
tenantId,
|
|
1212
1333
|
includeSuperseded,
|
|
1213
1334
|
asOf,
|
|
1214
|
-
budget,
|
|
1335
|
+
budget: entryBudget,
|
|
1336
|
+
cost: printCost,
|
|
1215
1337
|
minResults: minResults ?? 1,
|
|
1216
1338
|
recallScope: recallExplicitScope
|
|
1217
1339
|
? { requested: recallExplicitScope, additive: true }
|
|
@@ -1590,133 +1712,14 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1590
1712
|
if (limit < results.length) {
|
|
1591
1713
|
results = results.slice(0, limit);
|
|
1592
1714
|
}
|
|
1593
|
-
const droppedByBudgetCountCmd = Math.max(0, totalCandidatesCountCmd + graphAddedCountCmd - droppedPreRankCountCmd - results.length);
|
|
1594
|
-
// v0.33 / J1 — CLI per-pipeline anchoring detector. Each pipeline (api.recall,
|
|
1595
|
-
// cmdRecall, MCP) computes its own AnchoringHint via the shared detectAnchoring
|
|
1596
|
-
// helper against its own top-1 + its own per-(tenant, session) ring buffer.
|
|
1597
|
-
// HIPPO_ANCHORING=off short-circuits BEFORE both the ring lookup and the
|
|
1598
|
-
// detect call so disabled tenants pay truly zero work. When sessionId is
|
|
1599
|
-
// absent we emit recall_anchor_skipped_no_session for J1-v2 telemetry.
|
|
1600
|
-
let cmdAnchoringHint = null;
|
|
1601
|
-
if (process.env.HIPPO_ANCHORING !== 'off') {
|
|
1602
|
-
if (sessionId) {
|
|
1603
|
-
const ringKey = buildSessionKey(tenantId, sessionId);
|
|
1604
|
-
const ring = getOrCreateRing(sessionRecallHistoryCli, ringKey);
|
|
1605
|
-
const queryHash = hashQueryText(query);
|
|
1606
|
-
const topId = results[0]?.entry.id ?? null;
|
|
1607
|
-
cmdAnchoringHint = detectAnchoring(snapshotRing(ring), queryHash, topId);
|
|
1608
|
-
// Append AFTER detect (snapshot was taken before). anchoredOn carries
|
|
1609
|
-
// the memoryId of any hint that fired, feeding the cooldown logic for
|
|
1610
|
-
// the NEXT cmdRecall on this session.
|
|
1611
|
-
appendRecall(ring, queryHash, topId, cmdAnchoringHint?.memoryId);
|
|
1612
|
-
}
|
|
1613
|
-
else {
|
|
1614
|
-
// Telemetry: caller had no sessionId so ring tracking is skipped.
|
|
1615
|
-
// Per the recall-audit convention at api.ts:854, use SHA-256/16
|
|
1616
|
-
// for prompt hashing (NOT hashQueryText which is FNV-1a 32-bit
|
|
1617
|
-
// for recall matching; brute-force trivial for low-entropy
|
|
1618
|
-
// queries). Codex round-1 P1 / round-2 P2 catch.
|
|
1619
|
-
emitCliAudit(hippoRoot, 'recall_anchor_skipped_no_session', undefined, {
|
|
1620
|
-
query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
|
|
1621
|
-
query_length: query.length,
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
// v1.12.13 / C5 — Build suppressionSummary for cmdRecall pipeline. Surfaced
|
|
1626
|
-
// in --why text output and in the --json JSON output. cmdRecall does not
|
|
1627
|
-
// run the summarizeOverflow path (api.recall does) and does not currently
|
|
1628
|
-
// expose fresh-tail in the CLI, so those two counters are 0 here.
|
|
1629
|
-
// v0.33 / J1: suppressedByInterference is bumped by 1 when cmdAnchoringHint
|
|
1630
|
-
// fires with reason='memory_dominance' (the only reason that counts as
|
|
1631
|
-
// interference; query_repeat is a re-ask, not memory competition).
|
|
1632
|
-
const cmdSuppressedByInterference = cmdAnchoringHint?.reason === 'memory_dominance' ? 1 : 0;
|
|
1633
|
-
const cmdSuppressionSummary = api.buildSuppressionSummary({
|
|
1634
|
-
// PUBLISHED total includes graph-surfaced rows. Folding graphAdded into
|
|
1635
|
-
// the derivation but not into the reported total made the invariant hold
|
|
1636
|
-
// internally and break externally by exactly that count: JSON consumers
|
|
1637
|
-
// saw total != preRank + byBudget + returned, and the text line could
|
|
1638
|
-
// read "showing 8 of 10" having actually considered 12. The number a
|
|
1639
|
-
// caller sees must be the number the arithmetic used.
|
|
1640
|
-
totalCandidates: totalCandidatesCountCmd + graphAddedCountCmd,
|
|
1641
|
-
droppedPreRank: droppedPreRankCountCmd,
|
|
1642
|
-
droppedByBudget: droppedByBudgetCountCmd,
|
|
1643
|
-
summarySubstitutionsAdded: 0,
|
|
1644
|
-
freshTailAdded: 0,
|
|
1645
|
-
suppressedByInterference: cmdSuppressedByInterference,
|
|
1646
|
-
});
|
|
1647
|
-
// v0.33 / J1 — emit pipeline-local audit op when a hint fires (lockstep
|
|
1648
|
-
// with api.recall's audit pattern; each pipeline emits for its own hits).
|
|
1649
|
-
if (cmdAnchoringHint?.reason === 'memory_dominance') {
|
|
1650
|
-
emitCliAudit(hippoRoot, 'recall_anchor_detected_memory_dominance', cmdAnchoringHint.memoryId, {
|
|
1651
|
-
memory_id: cmdAnchoringHint.memoryId,
|
|
1652
|
-
query_count: cmdAnchoringHint.queryCount ?? null,
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
else if (cmdAnchoringHint?.reason === 'query_repeat') {
|
|
1656
|
-
emitCliAudit(hippoRoot, 'recall_anchor_detected_query_repeat', cmdAnchoringHint.memoryId, {
|
|
1657
|
-
memory_id: cmdAnchoringHint.memoryId,
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
// v1.13.x / J2 — CLI per-pipeline availability/recency-bias detector. Each
|
|
1661
|
-
// pipeline computes its own hint (this one against the CLI's returned top-K
|
|
1662
|
-
// and the full local+global candidate pool). Soft warning only. Gated by
|
|
1663
|
-
// HIPPO_AVAILABILITY=off, which short-circuits BEFORE the detect call so
|
|
1664
|
-
// disabled tenants pay zero work. Audit emission is pipeline-local, lockstep
|
|
1665
|
-
// with the anchoring emitCliAudit calls above. cmdAvailabilityHint is null on
|
|
1666
|
-
// the zero-result branch (topK < minReturned), so the splat is a no-op there.
|
|
1667
|
-
let cmdAvailabilityHint = null;
|
|
1668
|
-
if (process.env.HIPPO_AVAILABILITY !== 'off') {
|
|
1669
|
-
cmdAvailabilityHint = detectAvailabilityBias({
|
|
1670
|
-
topK: results.map((r) => ({ id: r.entry.id, created: r.entry.created })),
|
|
1671
|
-
pool: [...localEntries, ...globalEntries].map((e) => ({ id: e.id, created: e.created })),
|
|
1672
|
-
});
|
|
1673
|
-
if (cmdAvailabilityHint) {
|
|
1674
|
-
emitCliAudit(hippoRoot, 'recall_availability_detected', undefined, {
|
|
1675
|
-
recent_fraction: cmdAvailabilityHint.recentFraction,
|
|
1676
|
-
older_passed_over: cmdAvailabilityHint.olderCandidatesPassedOver,
|
|
1677
|
-
returned_count: cmdAvailabilityHint.returnedCount,
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
// v0.32 / J3.2 — auto-injection of reference-class baserate when the
|
|
1682
|
-
// CLI query carries a forward-prediction phrase AND a class matches.
|
|
1683
|
-
// cmdRecall runs its own pipeline (doesn't go through api.recall for
|
|
1684
|
-
// the memory list), so it computes the hint here. The hint VALUE is
|
|
1685
|
-
// pipeline-invariant — same (hippoRoot, tenantId, query) inputs would
|
|
1686
|
-
// produce the same hint in api.recall — but the audit emission is
|
|
1687
|
-
// pipeline-local (one audit row per actual call, actor='cli' here).
|
|
1688
|
-
// computePlanningFallacyOutput short-circuits BEFORE the regex gate
|
|
1689
|
-
// when HIPPO_AUTODEBIAS=off so the no-match path is effectively free.
|
|
1690
|
-
// v1.13.4: switched to the richer Output type so the watching variant
|
|
1691
|
-
// (regex fired, no class matched OR tiebreak) can also surface.
|
|
1692
|
-
const cmdPlanningFallacyOutput = computePlanningFallacyOutput(hippoRoot, tenantId, query, { actor: 'cli' });
|
|
1693
|
-
const cmdPlanningFallacyHint = cmdPlanningFallacyOutput.hint ?? null;
|
|
1694
|
-
const cmdPlanningFallacyWatching = cmdPlanningFallacyOutput.watching ?? null;
|
|
1695
|
-
// A5 audit: emit one 'recall' event per query, capturing the (truncated)
|
|
1696
|
-
// query text and the post-filter result count. Tenant resolved by emitCliAudit.
|
|
1697
|
-
// Emit before the early-empty return so zero-result recalls are still logged.
|
|
1698
|
-
// recall reads from BOTH local and global stores when both are initialized;
|
|
1699
|
-
// log against every participating store so the audit trail in either db
|
|
1700
|
-
// shows the read access (no false negatives across --global flows).
|
|
1701
|
-
const recallMetadata = {
|
|
1702
|
-
query: query.slice(0, 200),
|
|
1703
|
-
results: results.length,
|
|
1704
|
-
};
|
|
1705
|
-
emitCliAudit(hippoRoot, 'recall', undefined, recallMetadata);
|
|
1706
|
-
if (isInitialized(globalRoot) && globalRoot !== hippoRoot) {
|
|
1707
|
-
emitCliAudit(globalRoot, 'recall', undefined, recallMetadata);
|
|
1708
|
-
}
|
|
1709
1715
|
// Continuity assembly (--continuity). Lives BEFORE the zero-result branch
|
|
1710
1716
|
// so a no-match query with active continuity state still returns a useful
|
|
1711
|
-
// resume packet. Same three tenant-scoped store helpers as api.recall
|
|
1712
|
-
// continuityTokens uses the same Math.ceil(len/4) rule as the search-path
|
|
1713
|
-
// estimateTokens() in src/search.ts.
|
|
1717
|
+
// resume packet. Same three tenant-scoped store helpers as api.recall.
|
|
1714
1718
|
const includeContinuity = Boolean(flags['continuity']);
|
|
1715
1719
|
let activeSnapshot = null;
|
|
1716
1720
|
let sessionHandoff = null;
|
|
1717
1721
|
let recentSessionEvents = [];
|
|
1718
|
-
|
|
1719
|
-
if (includeContinuity) {
|
|
1722
|
+
if (includeContinuity && !primaryIsGlobal) {
|
|
1720
1723
|
const rawSnapshot = loadActiveTaskSnapshot(hippoRoot, tenantId);
|
|
1721
1724
|
const sessionId = rawSnapshot?.session_id ?? undefined;
|
|
1722
1725
|
const rawHandoff = sessionId
|
|
@@ -1735,34 +1738,165 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1735
1738
|
sessionHandoff =
|
|
1736
1739
|
rawHandoff && passesScopeFilterForRecall(rowScope(rawHandoff), effectiveScope) ? rawHandoff : null;
|
|
1737
1740
|
recentSessionEvents = rawEvents.filter((e) => passesScopeFilterForRecall(rowScope(e), effectiveScope));
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1741
|
+
}
|
|
1742
|
+
// Sections print ahead of the memories, so they are paid first, after the header; one that does not fit is dropped.
|
|
1743
|
+
const sectionBudget = budget - printedTokens(recallHeading(budget, budget, query));
|
|
1744
|
+
let left = sectionBudget;
|
|
1745
|
+
const pays = (tokens) => { if (tokens > left)
|
|
1746
|
+
return false; left -= tokens; return true; };
|
|
1747
|
+
if (activeSnapshot && !pays(printedTokens(snapshotText(activeSnapshot))))
|
|
1748
|
+
activeSnapshot = null;
|
|
1749
|
+
if (sessionHandoff && !pays(printedTokens(handoffText(sessionHandoff))))
|
|
1750
|
+
sessionHandoff = null;
|
|
1751
|
+
if (recentSessionEvents.length > 0 && !pays(printedTokens(sessionTrailText(recentSessionEvents))))
|
|
1752
|
+
recentSessionEvents = [];
|
|
1753
|
+
const continuityTokens = sectionBudget - left;
|
|
1754
|
+
const hasContinuity = activeSnapshot !== null || sessionHandoff !== null || recentSessionEvents.length > 0;
|
|
1755
|
+
// J3.2: the baserate hint depends on the query alone; its audit is pipeline-local (actor 'cli').
|
|
1756
|
+
const cmdPlanningFallacyOutput = computePlanningFallacyOutput(hippoRoot, tenantId, query, { actor: 'cli' });
|
|
1757
|
+
const planText = planningLine(cmdPlanningFallacyOutput);
|
|
1758
|
+
const showPlan = planText !== null && pays(printedTokens(`${planText}\n`));
|
|
1759
|
+
const cmdPlanningFallacyHint = showPlan ? cmdPlanningFallacyOutput.hint ?? null : null;
|
|
1760
|
+
const cmdPlanningFallacyWatching = showPlan ? cmdPlanningFallacyOutput.watching ?? null : null;
|
|
1761
|
+
// The first --min-results are kept whatever they cost (the documented exception); the rest skip and continue.
|
|
1762
|
+
const floor = minResults ?? 1;
|
|
1763
|
+
const fitted = fitBudget(results, left, floor, printCost);
|
|
1764
|
+
// Copies go after every cut, so a merged row the budget drops never hides its sources.
|
|
1765
|
+
const shown = (n) => dropHeldCopies(fitted.slice(0, n), (r) => r.entry);
|
|
1766
|
+
let kept = fitted.length;
|
|
1767
|
+
results = shown(kept);
|
|
1768
|
+
// J1, J2 and C5: each pipeline computes its hints over the list it returns, so they follow the list as it shrinks.
|
|
1769
|
+
// HIPPO_ANCHORING=off and HIPPO_AVAILABILITY=off skip the work entirely.
|
|
1770
|
+
const anchorRing = process.env.HIPPO_ANCHORING !== 'off' && sessionId
|
|
1771
|
+
? getOrCreateRing(sessionRecallHistoryCli, buildSessionKey(tenantId, sessionId))
|
|
1772
|
+
: null;
|
|
1773
|
+
const queryHash = hashQueryText(query);
|
|
1774
|
+
const availabilityPool = process.env.HIPPO_AVAILABILITY !== 'off'
|
|
1775
|
+
? [...localEntries, ...globalEntries].map((e) => ({ id: e.id, created: e.created }))
|
|
1776
|
+
: null;
|
|
1777
|
+
const hintsFor = (list, held) => {
|
|
1778
|
+
const anchoring = anchorRing ? detectAnchoring(snapshotRing(anchorRing), queryHash, list[0]?.entry.id ?? null) : null;
|
|
1779
|
+
const availability = availabilityPool
|
|
1780
|
+
? detectAvailabilityBias({ topK: list.map((r) => ({ id: r.entry.id, created: r.entry.created })), pool: availabilityPool })
|
|
1781
|
+
: null;
|
|
1782
|
+
const summary = api.buildSuppressionSummary({
|
|
1783
|
+
// The published total includes graph-surfaced rows, so total == preRank + byBudget + returned holds for callers.
|
|
1784
|
+
totalCandidates: totalCandidatesCountCmd + graphAddedCountCmd,
|
|
1785
|
+
droppedPreRank: droppedPreRankCountCmd + held,
|
|
1786
|
+
droppedByBudget: Math.max(0, totalCandidatesCountCmd + graphAddedCountCmd - droppedPreRankCountCmd - held - list.length),
|
|
1787
|
+
summarySubstitutionsAdded: 0,
|
|
1788
|
+
freshTailAdded: 0,
|
|
1789
|
+
suppressedByInterference: anchoring?.reason === 'memory_dominance' ? 1 : 0, // a query_repeat is a re-ask, not competition
|
|
1790
|
+
});
|
|
1791
|
+
return { anchoring, availability, summary };
|
|
1792
|
+
};
|
|
1793
|
+
const printContinuity = () => {
|
|
1794
|
+
if (activeSnapshot)
|
|
1795
|
+
printActiveTaskSnapshot(activeSnapshot);
|
|
1796
|
+
if (sessionHandoff)
|
|
1797
|
+
printHandoff(sessionHandoff);
|
|
1798
|
+
if (recentSessionEvents.length > 0)
|
|
1799
|
+
printSessionEvents(recentSessionEvents);
|
|
1800
|
+
};
|
|
1801
|
+
const renderRecall = (list, h) => settleTokens((t) => captureConsole(() => {
|
|
1802
|
+
if (list.length === 0) {
|
|
1803
|
+
// The hint still prints when nothing matched, so the agent sees its track record.
|
|
1804
|
+
if (showPlan) {
|
|
1805
|
+
console.log(planText);
|
|
1806
|
+
console.log();
|
|
1807
|
+
}
|
|
1808
|
+
if (hasContinuity) {
|
|
1809
|
+
printContinuity();
|
|
1810
|
+
console.log(`(no memories matched "${query}")`);
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
console.log('No memories found for:', query);
|
|
1814
|
+
}
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
printContinuity();
|
|
1818
|
+
// Anchoring is the stronger pull, so it prints first; the Cutoff line sits above the list, where a reader sees it.
|
|
1819
|
+
if (h.anchoring) {
|
|
1820
|
+
console.log(`[anchored_on: ${h.anchoring.memoryId}] ${h.anchoring.summary}`);
|
|
1821
|
+
console.log();
|
|
1822
|
+
}
|
|
1823
|
+
if (h.availability) {
|
|
1824
|
+
console.log(`Availability bias (${h.availability.recentCount}/${h.availability.returnedCount} recent): ${h.availability.summary}`);
|
|
1825
|
+
console.log();
|
|
1826
|
+
}
|
|
1827
|
+
if (showPlan) {
|
|
1828
|
+
console.log(planText);
|
|
1829
|
+
console.log();
|
|
1830
|
+
}
|
|
1831
|
+
const cutoff = showWhy ? cutoffLine(list.length, h.summary) : null;
|
|
1832
|
+
if (cutoff) {
|
|
1833
|
+
console.log(cutoff);
|
|
1834
|
+
console.log();
|
|
1835
|
+
}
|
|
1836
|
+
console.log(recallHeading(list.length, t, query));
|
|
1837
|
+
for (const r of list)
|
|
1838
|
+
console.log(entryText(r));
|
|
1765
1839
|
}));
|
|
1840
|
+
let hints = hintsFor(results, kept - results.length);
|
|
1841
|
+
let recallText = renderRecall(results, hints);
|
|
1842
|
+
// The hints, Cutoff line and header vary with the list, so the lowest-ranked entry goes until the whole block fits.
|
|
1843
|
+
while (kept > floor && estimateTokens(recallText) > budget) {
|
|
1844
|
+
kept--;
|
|
1845
|
+
results = shown(kept);
|
|
1846
|
+
hints = hintsFor(results, kept - results.length);
|
|
1847
|
+
recallText = renderRecall(results, hints);
|
|
1848
|
+
}
|
|
1849
|
+
const { anchoring: cmdAnchoringHint, availability: cmdAvailabilityHint, summary: cmdSuppressionSummary } = hints;
|
|
1850
|
+
if (anchorRing) {
|
|
1851
|
+
// Appended after every detect: anchoredOn feeds the cooldown for the next recall on this session.
|
|
1852
|
+
appendRecall(anchorRing, queryHash, results[0]?.entry.id ?? null, cmdAnchoringHint?.memoryId);
|
|
1853
|
+
}
|
|
1854
|
+
else if (process.env.HIPPO_ANCHORING !== 'off') {
|
|
1855
|
+
// SHA-256/16 per the recall-audit convention; hashQueryText is FNV-1a and brute-forceable on short queries.
|
|
1856
|
+
emitCliAudit(hippoRoot, 'recall_anchor_skipped_no_session', undefined, {
|
|
1857
|
+
query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
|
|
1858
|
+
query_length: query.length,
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
if (cmdAnchoringHint?.reason === 'memory_dominance') {
|
|
1862
|
+
emitCliAudit(hippoRoot, 'recall_anchor_detected_memory_dominance', cmdAnchoringHint.memoryId, {
|
|
1863
|
+
memory_id: cmdAnchoringHint.memoryId,
|
|
1864
|
+
query_count: cmdAnchoringHint.queryCount ?? null,
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
else if (cmdAnchoringHint?.reason === 'query_repeat') {
|
|
1868
|
+
emitCliAudit(hippoRoot, 'recall_anchor_detected_query_repeat', cmdAnchoringHint.memoryId, {
|
|
1869
|
+
memory_id: cmdAnchoringHint.memoryId,
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
if (cmdAvailabilityHint) {
|
|
1873
|
+
emitCliAudit(hippoRoot, 'recall_availability_detected', undefined, {
|
|
1874
|
+
recent_fraction: cmdAvailabilityHint.recentFraction,
|
|
1875
|
+
older_passed_over: cmdAvailabilityHint.olderCandidatesPassedOver,
|
|
1876
|
+
returned_count: cmdAvailabilityHint.returnedCount,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
// A5 audit: one 'recall' event per query, before the early-empty return, in every participating store.
|
|
1880
|
+
const recallMetadata = {
|
|
1881
|
+
query: query.slice(0, 200),
|
|
1882
|
+
results: results.length,
|
|
1883
|
+
};
|
|
1884
|
+
emitCliAudit(hippoRoot, 'recall', undefined, recallMetadata);
|
|
1885
|
+
if (isInitialized(globalRoot) && globalRoot !== hippoRoot) {
|
|
1886
|
+
emitCliAudit(globalRoot, 'recall', undefined, recallMetadata);
|
|
1887
|
+
}
|
|
1888
|
+
// TE0 token ledger: books the block this recall prints, on whichever exit it takes.
|
|
1889
|
+
const emit = (text) => {
|
|
1890
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
1891
|
+
tenantId,
|
|
1892
|
+
sessionId: hostSessionId() ?? null,
|
|
1893
|
+
surface: 'recall',
|
|
1894
|
+
event: 'inject',
|
|
1895
|
+
items: results.length,
|
|
1896
|
+
tokens: estimateTokens(text),
|
|
1897
|
+
}));
|
|
1898
|
+
console.log(text);
|
|
1899
|
+
};
|
|
1766
1900
|
if (results.length === 0) {
|
|
1767
1901
|
// LC1 F1 structural fix (docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
|
|
1768
1902
|
// trace the zero-result recall too (result_count 0, no result rows) so
|
|
@@ -1807,55 +1941,13 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1807
1941
|
};
|
|
1808
1942
|
out.continuityTokens = continuityTokens;
|
|
1809
1943
|
}
|
|
1810
|
-
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
// v0.33 / J1 — render anchoring hint above planning hint (anchoring
|
|
1814
|
-
// is the stronger cognitive-pull warning so it gets first position).
|
|
1815
|
-
if (cmdAnchoringHint) {
|
|
1816
|
-
console.log(`[anchored_on: ${cmdAnchoringHint.memoryId}] ${cmdAnchoringHint.summary}`);
|
|
1817
|
-
}
|
|
1818
|
-
// v1.13.x / J2 — render availability/recency-bias hint below anchoring and
|
|
1819
|
-
// above the planning-fallacy hint. Soft warning; absent (env disabled or no
|
|
1820
|
-
// bias detected) is silent. Null on this zero-result branch anyway since
|
|
1821
|
-
// topK < minReturned, so this is effectively a no-op here; wired for parity.
|
|
1822
|
-
if (cmdAvailabilityHint) {
|
|
1823
|
-
console.log(`Availability bias (${cmdAvailabilityHint.recentCount}/${cmdAvailabilityHint.returnedCount} recent): ${cmdAvailabilityHint.summary}`);
|
|
1824
|
-
}
|
|
1825
|
-
// v0.32 / J3.2 — render hint BEFORE the no-memories message so the
|
|
1826
|
-
// calling agent sees its track record even when the query missed
|
|
1827
|
-
// every memory. Same single-line shape + JSON.stringify-safe phrase
|
|
1828
|
-
// as the populated-results path below.
|
|
1829
|
-
if (cmdPlanningFallacyHint) {
|
|
1830
|
-
const safePhrase = JSON.stringify(cmdPlanningFallacyHint.detectedPhrase);
|
|
1831
|
-
console.log(`Planning fallacy hint (class: ${cmdPlanningFallacyHint.classTag}): ${cmdPlanningFallacyHint.baserateSummary} [detected: ${safePhrase}]`);
|
|
1832
|
-
console.log();
|
|
1833
|
-
}
|
|
1834
|
-
else if (cmdPlanningFallacyWatching) {
|
|
1835
|
-
// v1.13.4: render the watching variant when the regex matched but no
|
|
1836
|
-
// baserate could be produced. Suggestion text directs the user to
|
|
1837
|
-
// tag a prediction class so future queries can produce a baserate.
|
|
1838
|
-
const safePhrase = JSON.stringify(cmdPlanningFallacyWatching.detectedPhrase);
|
|
1839
|
-
console.log(`Planning fallacy: watching this query (reason: ${cmdPlanningFallacyWatching.reason}). ${cmdPlanningFallacyWatching.suggestion} [detected: ${safePhrase}]`);
|
|
1840
|
-
console.log();
|
|
1841
|
-
}
|
|
1842
|
-
if (hasContinuity) {
|
|
1843
|
-
// Print continuity even when no memories matched. The resume packet
|
|
1844
|
-
// is the whole point of `--continuity` and must not be dropped here.
|
|
1845
|
-
if (activeSnapshot)
|
|
1846
|
-
printActiveTaskSnapshot(activeSnapshot);
|
|
1847
|
-
if (sessionHandoff)
|
|
1848
|
-
printHandoff(sessionHandoff);
|
|
1849
|
-
if (recentSessionEvents.length > 0)
|
|
1850
|
-
printSessionEvents(recentSessionEvents);
|
|
1851
|
-
console.log(`(no memories matched "${query}")`);
|
|
1944
|
+
emit(JSON.stringify(out));
|
|
1852
1945
|
return;
|
|
1853
1946
|
}
|
|
1854
|
-
|
|
1947
|
+
emit(recallText);
|
|
1855
1948
|
return;
|
|
1856
1949
|
}
|
|
1857
1950
|
const retrievedIds = results.map((r) => r.entry.id);
|
|
1858
|
-
const localIndex = loadIndex(hippoRoot);
|
|
1859
1951
|
const strengthenedHere = strengthenRetrieved(hippoRoot, retrievedIds);
|
|
1860
1952
|
if (isInitialized(globalRoot))
|
|
1861
1953
|
strengthenRetrieved(globalRoot, retrievedIds.filter((id) => !strengthenedHere.has(id)));
|
|
@@ -1886,7 +1978,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1886
1978
|
updateStats(hippoRoot, { recalled: results.length });
|
|
1887
1979
|
if (asJson) {
|
|
1888
1980
|
const output = results.map((r) => {
|
|
1889
|
-
const isGlobal = isInitialized(globalRoot) && !localIndex.entries[r.entry.id];
|
|
1981
|
+
const isGlobal = primaryIsGlobal || (isInitialized(globalRoot) && !localIndex.entries[r.entry.id]);
|
|
1890
1982
|
const base = {
|
|
1891
1983
|
id: r.entry.id,
|
|
1892
1984
|
score: r.score,
|
|
@@ -1944,131 +2036,10 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1944
2036
|
};
|
|
1945
2037
|
jsonOut.continuityTokens = continuityTokens;
|
|
1946
2038
|
}
|
|
1947
|
-
|
|
2039
|
+
emit(JSON.stringify(jsonOut));
|
|
1948
2040
|
return;
|
|
1949
2041
|
}
|
|
1950
|
-
|
|
1951
|
-
if (includeContinuity && hasContinuity) {
|
|
1952
|
-
if (activeSnapshot)
|
|
1953
|
-
printActiveTaskSnapshot(activeSnapshot);
|
|
1954
|
-
if (sessionHandoff)
|
|
1955
|
-
printHandoff(sessionHandoff);
|
|
1956
|
-
if (recentSessionEvents.length > 0)
|
|
1957
|
-
printSessionEvents(recentSessionEvents);
|
|
1958
|
-
}
|
|
1959
|
-
// v0.33 / J1 — render anchoring hint above planning-fallacy hint
|
|
1960
|
-
// (anchoring is the stronger cognitive-pull warning so it gets first
|
|
1961
|
-
// position). Hint absent (env disabled, no sessionId, or no R1/R2)
|
|
1962
|
-
// is silent.
|
|
1963
|
-
if (cmdAnchoringHint) {
|
|
1964
|
-
console.log(`[anchored_on: ${cmdAnchoringHint.memoryId}] ${cmdAnchoringHint.summary}`);
|
|
1965
|
-
console.log();
|
|
1966
|
-
}
|
|
1967
|
-
// v1.13.x / J2 — render availability/recency-bias hint below anchoring and
|
|
1968
|
-
// above the planning-fallacy hint. Soft warning; absent (env disabled or no
|
|
1969
|
-
// bias detected) is silent.
|
|
1970
|
-
if (cmdAvailabilityHint) {
|
|
1971
|
-
console.log(`Availability bias (${cmdAvailabilityHint.recentCount}/${cmdAvailabilityHint.returnedCount} recent): ${cmdAvailabilityHint.summary}`);
|
|
1972
|
-
console.log();
|
|
1973
|
-
}
|
|
1974
|
-
// v0.32 / J3.2 — render planning-fallacy hint ABOVE the result list so
|
|
1975
|
-
// the agent sees its track record before scrolling. Hint absent (env
|
|
1976
|
-
// disabled or no forward-claim match) is silent. detectedPhrase is
|
|
1977
|
-
// sanitised against control chars and ASCII quotes via JSON.stringify
|
|
1978
|
-
// to head off rendering ambiguity when a regex match contains quotes
|
|
1979
|
-
// or parens (plan-eng-critic round 2 LOW).
|
|
1980
|
-
if (cmdPlanningFallacyHint) {
|
|
1981
|
-
const safePhrase = JSON.stringify(cmdPlanningFallacyHint.detectedPhrase);
|
|
1982
|
-
console.log(`Planning fallacy hint (class: ${cmdPlanningFallacyHint.classTag}): ${cmdPlanningFallacyHint.baserateSummary} [detected: ${safePhrase}]`);
|
|
1983
|
-
console.log();
|
|
1984
|
-
}
|
|
1985
|
-
else if (cmdPlanningFallacyWatching) {
|
|
1986
|
-
// v1.13.4: render the watching variant when the regex matched but no
|
|
1987
|
-
// baserate could be produced (no_class_match / tiebreak). Suggestion
|
|
1988
|
-
// text directs the user toward an action that would unblock the
|
|
1989
|
-
// hint next time (typically: tag a prediction class).
|
|
1990
|
-
const safePhrase = JSON.stringify(cmdPlanningFallacyWatching.detectedPhrase);
|
|
1991
|
-
console.log(`Planning fallacy: watching this query (reason: ${cmdPlanningFallacyWatching.reason}). ${cmdPlanningFallacyWatching.suggestion} [detected: ${safePhrase}]`);
|
|
1992
|
-
console.log();
|
|
1993
|
-
}
|
|
1994
|
-
// v1.13.3 / C5 follow-up — Cutoff line ABOVE the result list (was a
|
|
1995
|
-
// "WYSIATI:" line BELOW the result list in v1.12.13-v1.13.2). Dogfood
|
|
1996
|
-
// proof at docs/dogfood/2026-05-27-track-j-warnings.md: a fresh sub-agent
|
|
1997
|
-
// reading the v1.13.2 bottom-placed line ignored it entirely and
|
|
1998
|
-
// summarised the visible memories as if the dropped pool didn't exist
|
|
1999
|
-
// (the exact WYSIATI failure mode C5 is supposed to flag). Top placement
|
|
2000
|
-
// + plain English ("Cutoff:" not "WYSIATI:") closes the read gap.
|
|
2001
|
-
if (showWhy) {
|
|
2002
|
-
const s = cmdSuppressionSummary;
|
|
2003
|
-
const clauses = [];
|
|
2004
|
-
// "dropped to fit limit" pointed at the wrong control: the residual covers
|
|
2005
|
-
// search-ranking and token-budget drops too, and fires even when --limit
|
|
2006
|
-
// was never passed. Measured: `recall --budget 20 --why` printed "39
|
|
2007
|
-
// dropped to fit limit" with no --limit flag in the command at all.
|
|
2008
|
-
if (s.droppedByBudget > 0)
|
|
2009
|
-
clauses.push(`${s.droppedByBudget} not shown (rank, budget or limit)`);
|
|
2010
|
-
if (s.droppedPreRank > 0)
|
|
2011
|
-
clauses.push(`${s.droppedPreRank} filtered pre-rank`);
|
|
2012
|
-
if (s.summarySubstitutionsAdded > 0)
|
|
2013
|
-
clauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
|
|
2014
|
-
if (s.freshTailAdded > 0)
|
|
2015
|
-
clauses.push(`${s.freshTailAdded} fresh-tail added`);
|
|
2016
|
-
if (s.suppressedByInterference > 0)
|
|
2017
|
-
clauses.push(`${s.suppressedByInterference} suppressed by interference`);
|
|
2018
|
-
if (clauses.length > 0) {
|
|
2019
|
-
console.log(`Cutoff: showing ${results.length} of ${s.totalCandidates} candidates; ${clauses.join('; ')}.`);
|
|
2020
|
-
console.log();
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
console.log(`Found ${results.length} memories (${totalTokens} tokens) for: "${query}"\n`);
|
|
2024
|
-
for (const r of results) {
|
|
2025
|
-
const e = r.entry;
|
|
2026
|
-
const label = confidenceLabel(e);
|
|
2027
|
-
const confLabel = label.warn ? `[${label.text}] \u26A0\uFE0F` : `[${label.text}]`;
|
|
2028
|
-
const strengthBar = '\u2588'.repeat(Math.round(e.strength * 10)) + '\u2591'.repeat(10 - Math.round(e.strength * 10));
|
|
2029
|
-
const isGlobal = isInitialized(globalRoot) && !localIndex.entries[e.id];
|
|
2030
|
-
const globalMark = isGlobal ? ' [global]' : '';
|
|
2031
|
-
const supersededMark = e.superseded_by ? ' [superseded]' : '';
|
|
2032
|
-
const graphMark = r.graphVia ? ` [graph: ${r.graphVia.hops}hop ${r.graphVia.relType}]` : '';
|
|
2033
|
-
const sourceMark = isGlobal ? ' [global]' : ' [local]';
|
|
2034
|
-
console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark}${supersededMark}${graphMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
|
|
2035
|
-
console.log(` [${strengthBar}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`);
|
|
2036
|
-
if (showWhy) {
|
|
2037
|
-
const explanation = explainMatch(query, r);
|
|
2038
|
-
console.log(` source:${sourceMark} | layer: [${e.layer}] | confidence: [${label.text}]`);
|
|
2039
|
-
console.log(` reason: ${explanation.reason}`);
|
|
2040
|
-
if (explanation.envelope) {
|
|
2041
|
-
const env = explanation.envelope;
|
|
2042
|
-
console.log(` kind: ${env.kind}`);
|
|
2043
|
-
if (env.scope)
|
|
2044
|
-
console.log(` scope: ${env.scope}`);
|
|
2045
|
-
if (env.owner)
|
|
2046
|
-
console.log(` owner: ${env.owner}`);
|
|
2047
|
-
if (env.artifact_ref)
|
|
2048
|
-
console.log(` artifact_ref: ${env.artifact_ref}`);
|
|
2049
|
-
if (env.session_id)
|
|
2050
|
-
console.log(` session_id: ${env.session_id}`);
|
|
2051
|
-
console.log(` confidence: ${env.confidence}`);
|
|
2052
|
-
}
|
|
2053
|
-
// A7 recall-trace: render the ordered lifecycle re-ranking chain, e.g.
|
|
2054
|
-
// "ranking: base 0.420 -> interference x0.3 -> 0.126 -> goal-boost x1.5 -> 0.189".
|
|
2055
|
-
if (r.rerankTrace && r.rerankTrace.length > 0) {
|
|
2056
|
-
const parts = [`base ${fmt(r.rerankTrace[0].scoreBefore, 3)}`];
|
|
2057
|
-
for (const step of r.rerankTrace) {
|
|
2058
|
-
const mult = step.multiplier !== undefined ? ` x${fmt(step.multiplier, 2)}` : '';
|
|
2059
|
-
parts.push(`${step.stage}${mult}`);
|
|
2060
|
-
parts.push(fmt(step.scoreAfter, 3));
|
|
2061
|
-
}
|
|
2062
|
-
console.log(` ranking: ${parts.join(' -> ')}`);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
console.log();
|
|
2066
|
-
console.log(e.content);
|
|
2067
|
-
console.log();
|
|
2068
|
-
}
|
|
2069
|
-
// v1.12.13 / C5 -> v1.13.3 follow-up: the WYSIATI bottom block was
|
|
2070
|
-
// moved above the result list (see comment near the Cutoff render
|
|
2071
|
-
// above). Function ends here.
|
|
2042
|
+
emit(recallText);
|
|
2072
2043
|
}
|
|
2073
2044
|
async function cmdExplain(hippoRoot, query, flags) {
|
|
2074
2045
|
requireInit(hippoRoot);
|
|
@@ -2155,11 +2126,17 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2155
2126
|
: config.search.localBump;
|
|
2156
2127
|
// explainExplicitScope hoisted above the candidate loads (v1.25.0).
|
|
2157
2128
|
const explainActiveScope = explainExplicitScope || detectScope();
|
|
2129
|
+
// Priced as recall prints each result, so explain returns what recall's engines would.
|
|
2130
|
+
const explainIndex = loadIndex(hippoRoot);
|
|
2131
|
+
const explainGlobalOn = isInitialized(globalRoot);
|
|
2132
|
+
const cost = (r) => printedTokens(recallEntryText(r, query, false, explainGlobalOn && !explainIndex.entries[r.entry.id]));
|
|
2133
|
+
const entryBudget = Math.max(0, budget - printedTokens(recallHeading(budget, budget, query)));
|
|
2158
2134
|
let results;
|
|
2159
2135
|
let modeUsed;
|
|
2160
2136
|
if (usePhysics && !hasGlobal) {
|
|
2161
2137
|
results = await physicsSearch(query, explainLocalEntries, {
|
|
2162
|
-
budget,
|
|
2138
|
+
budget: entryBudget,
|
|
2139
|
+
cost,
|
|
2163
2140
|
hippoRoot,
|
|
2164
2141
|
physicsConfig: config.physics,
|
|
2165
2142
|
explain: true,
|
|
@@ -2169,7 +2146,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2169
2146
|
}
|
|
2170
2147
|
else if (hasGlobal) {
|
|
2171
2148
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
2172
|
-
budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
2149
|
+
budget: entryBudget, cost, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
2173
2150
|
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf, tenantId,
|
|
2174
2151
|
recallScope: explainExplicitScope
|
|
2175
2152
|
? { requested: explainExplicitScope, additive: true }
|
|
@@ -2179,7 +2156,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2179
2156
|
}
|
|
2180
2157
|
else {
|
|
2181
2158
|
results = await hybridSearch(query, explainLocalEntries, {
|
|
2182
|
-
budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
|
|
2159
|
+
budget: entryBudget, cost, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
|
|
2183
2160
|
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
2184
2161
|
});
|
|
2185
2162
|
modeUsed = 'hybrid';
|
|
@@ -2187,6 +2164,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2187
2164
|
if (limit < results.length) {
|
|
2188
2165
|
results = results.slice(0, limit);
|
|
2189
2166
|
}
|
|
2167
|
+
results = dropHeldCopies(results, (r) => r.entry);
|
|
2190
2168
|
const candidates = explainLocalEntries.length + explainGlobalEntries.length;
|
|
2191
2169
|
if (asJson) {
|
|
2192
2170
|
const output = results.map((r, rank) => ({
|
|
@@ -2313,7 +2291,9 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
|
2313
2291
|
try {
|
|
2314
2292
|
baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
2315
2293
|
}
|
|
2316
|
-
catch {
|
|
2294
|
+
catch {
|
|
2295
|
+
console.error(`Warning: eval baseline ${baselinePath} is unreadable; running without it.`);
|
|
2296
|
+
}
|
|
2317
2297
|
}
|
|
2318
2298
|
const result = await runFeatureEval(version);
|
|
2319
2299
|
if (asJson) {
|
|
@@ -2645,123 +2625,15 @@ async function cmdRefine(hippoRoot, flags) {
|
|
|
2645
2625
|
}
|
|
2646
2626
|
}
|
|
2647
2627
|
}
|
|
2648
|
-
/** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
|
|
2649
|
-
function claudeMemoryFolderNames(projectRoot) {
|
|
2650
|
-
const roots = [projectRoot, realpathOrResolve(projectRoot)];
|
|
2651
|
-
const git = spawnSync('git', ['rev-parse', '--path-format=absolute', '--show-toplevel', '--absolute-git-dir', '--git-common-dir'], { cwd: projectRoot, encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
2652
|
-
if (git.status === 0) {
|
|
2653
|
-
const [top, gitDir, common] = git.stdout.trim().split(/\r?\n/);
|
|
2654
|
-
roots.push(claudeCheckoutRoot(top, gitDir, common));
|
|
2655
|
-
}
|
|
2656
|
-
return new Set(roots.map((root) => (process.platform === 'win32' ? claudeFolderName(root).toLowerCase() : claudeFolderName(root))));
|
|
2657
|
-
}
|
|
2658
|
-
/** Claude Code's rule: a linked worktree shares its main checkout's folder, or the git folder's when that sits outside a checkout (a bare repository, or --separate-git-dir); any other checkout, a submodule included, keeps its own. */
|
|
2659
|
-
function claudeCheckoutRoot(top, gitDir, common) {
|
|
2660
|
-
if (gitDir === common)
|
|
2661
|
-
return top;
|
|
2662
|
-
if (path.basename(common) === '.git')
|
|
2663
|
-
return path.dirname(common);
|
|
2664
|
-
return fs.existsSync(path.join(common, '.git')) ? top : common;
|
|
2665
|
-
}
|
|
2666
|
-
/** Claude Code's folder name for a path: non-alphanumerics made '-', and a name over 200 characters cut to 200 plus a base-36 hash of the whole path. */
|
|
2667
|
-
function claudeFolderName(root) {
|
|
2668
|
-
const full = path.resolve(root);
|
|
2669
|
-
const name = full.replace(/[^a-zA-Z0-9]/g, '-');
|
|
2670
|
-
if (name.length <= 200)
|
|
2671
|
-
return name;
|
|
2672
|
-
let hash = 0;
|
|
2673
|
-
for (let i = 0; i < full.length; i++)
|
|
2674
|
-
hash = ((hash << 5) - hash + full.charCodeAt(i)) | 0;
|
|
2675
|
-
return `${name.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
|
|
2676
|
-
}
|
|
2677
|
-
/** Import new entries from the Claude Code auto memory of the store's project, the frontmatter .md files in ~/.claude/projects/<project>/memory/. */
|
|
2678
|
-
export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
2679
|
-
// Only this project's folder, because every entry written here counts as this project's memory.
|
|
2680
|
-
// SHORTCUT: an autoMemoryDirectory or CLAUDE_CODE_PROJECT_DIR_NAME setting imports nothing; read Claude's settings if users ask.
|
|
2681
|
-
const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
|
|
2682
|
-
const memoryDirs = [...claudeMemoryFolderNames(path.dirname(hippoRoot))]
|
|
2683
|
-
.map((folder) => path.join(claudeProjectsDir, folder, 'memory'))
|
|
2684
|
-
.filter((memDir) => fs.existsSync(memDir));
|
|
2685
|
-
if (memoryDirs.length === 0)
|
|
2686
|
-
return 0;
|
|
2687
|
-
const existing = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
2688
|
-
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
2689
|
-
let imported = 0;
|
|
2690
|
-
let skippedSecret = 0;
|
|
2691
|
-
// AT1 (plan §3 containment): a rejection guard refusal is per-VALUE — one
|
|
2692
|
-
// tombstoned memory file must not abort the whole directory scan. No
|
|
2693
|
-
// signature change (bare number return, cli.ts:540 + cli.ts:2903 callers
|
|
2694
|
-
// unchanged) — counted internally and printed as one summary line.
|
|
2695
|
-
let rejected = 0;
|
|
2696
|
-
for (const memDir of memoryDirs) {
|
|
2697
|
-
try {
|
|
2698
|
-
const files = fs.readdirSync(memDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
|
|
2699
|
-
for (const file of files) {
|
|
2700
|
-
const raw = fs.readFileSync(path.join(memDir, file), 'utf8');
|
|
2701
|
-
// Parse YAML frontmatter
|
|
2702
|
-
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
2703
|
-
if (!fmMatch)
|
|
2704
|
-
continue;
|
|
2705
|
-
const body = fmMatch[2].trim();
|
|
2706
|
-
if (!body || body.length < 10)
|
|
2707
|
-
continue;
|
|
2708
|
-
// Truncate to reasonable size
|
|
2709
|
-
const content = body.length > 1500 ? body.slice(0, 1500) + ' [truncated]' : body;
|
|
2710
|
-
// Never ingest secret-bearing memory files. Some Claude Code memory
|
|
2711
|
-
// files exist purely to hold a live credential (e.g. an API-key
|
|
2712
|
-
// reference). The scope-isolation secret veto (v1.24.0) gated
|
|
2713
|
-
// share/promote/sync/ambient but missed this import path, so a live
|
|
2714
|
-
// key could land in the store here. Veto it at ingest. (v1.24.1)
|
|
2715
|
-
if (detectSecret({ content, tags: ['claude-code-memory'] }).flagged) {
|
|
2716
|
-
skippedSecret++;
|
|
2717
|
-
continue;
|
|
2718
|
-
}
|
|
2719
|
-
// Dedup: check if substantially similar content already exists
|
|
2720
|
-
const isDup = existing.some(e => {
|
|
2721
|
-
const overlap = textOverlap(content.slice(0, 200), e.content.slice(0, 200));
|
|
2722
|
-
return overlap > 0.6;
|
|
2723
|
-
});
|
|
2724
|
-
if (isDup)
|
|
2725
|
-
continue;
|
|
2726
|
-
const entry = createMemory(content, {
|
|
2727
|
-
layer: Layer.Episodic,
|
|
2728
|
-
tags: ['claude-code-memory'],
|
|
2729
|
-
source: `claude-memory:${file}`,
|
|
2730
|
-
confidence: 'observed',
|
|
2731
|
-
tenantId: resolveTenantId({}),
|
|
2732
|
-
baseHalfLifeDays,
|
|
2733
|
-
});
|
|
2734
|
-
try {
|
|
2735
|
-
writeEntry(hippoRoot, entry);
|
|
2736
|
-
}
|
|
2737
|
-
catch (err) {
|
|
2738
|
-
if (err instanceof RejectedValueError) {
|
|
2739
|
-
rejected++;
|
|
2740
|
-
continue;
|
|
2741
|
-
}
|
|
2742
|
-
throw err;
|
|
2743
|
-
}
|
|
2744
|
-
existing.push(entry); // prevent self-dedup within batch
|
|
2745
|
-
imported++;
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
catch { /* skip broken dirs */ }
|
|
2749
|
-
}
|
|
2750
|
-
if (skippedSecret > 0) {
|
|
2751
|
-
console.log(`Skipped ${skippedSecret} secret-bearing memory file${skippedSecret === 1 ? '' : 's'} (not ingested).`);
|
|
2752
|
-
}
|
|
2753
|
-
if (rejected > 0) {
|
|
2754
|
-
console.log(`Skipped ${rejected} rejected value${rejected === 1 ? '' : 's'} (run \`hippo unreject\` to allow).`);
|
|
2755
|
-
}
|
|
2756
|
-
return imported;
|
|
2757
|
-
}
|
|
2758
2628
|
function cmdDedup(hippoRoot, flags) {
|
|
2759
2629
|
requireInit(hippoRoot);
|
|
2760
2630
|
const dryRun = Boolean(flags['dry-run']);
|
|
2761
|
-
|
|
2631
|
+
if (flags['threshold'] !== undefined) {
|
|
2632
|
+
console.error('hippo dedup: --threshold is ignored; a duplicate is the same text apart from spacing.');
|
|
2633
|
+
}
|
|
2762
2634
|
const entries = loadAllEntries(hippoRoot);
|
|
2763
|
-
console.log(`Scanning ${entries.length} memories for duplicates (
|
|
2764
|
-
const result = deduplicateStore(hippoRoot, {
|
|
2635
|
+
console.log(`Scanning ${entries.length} memories for duplicates (same text apart from spacing)${dryRun ? ' (dry run)' : ''}...\n`);
|
|
2636
|
+
const result = deduplicateStore(hippoRoot, { dryRun });
|
|
2765
2637
|
if (result.removed === 0) {
|
|
2766
2638
|
console.log('No duplicates found.');
|
|
2767
2639
|
return;
|
|
@@ -2913,10 +2785,10 @@ export function renderSleepResult(result) {
|
|
|
2913
2785
|
}
|
|
2914
2786
|
async function cmdSleepCore(hippoRoot, flags) {
|
|
2915
2787
|
requireInit(hippoRoot);
|
|
2916
|
-
// Phase 1: Auto-learn from git
|
|
2788
|
+
// Phase 1: Auto-learn from git and every coding agent's own memories (CLI-only, uses process.cwd() / os.homedir()).
|
|
2917
2789
|
// Stays in cli.ts; api.sleep covers Phase 2-6 only.
|
|
2918
2790
|
if (!flags['no-learn'] && flags['dry-run']) {
|
|
2919
|
-
console.log("Dry run: skipped learning from git commits and
|
|
2791
|
+
console.log("Dry run: skipped learning from git commits and coding agents' own memories (`hippo import --agents --dry-run` previews those).");
|
|
2920
2792
|
}
|
|
2921
2793
|
else if (!flags['no-learn']) {
|
|
2922
2794
|
const config = loadConfig(hippoRoot);
|
|
@@ -2934,9 +2806,13 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
2934
2806
|
console.error(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
2935
2807
|
}
|
|
2936
2808
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2809
|
+
printAgentImport(importForStore(hippoRoot, { machine: currentMachine() }), '');
|
|
2810
|
+
}
|
|
2811
|
+
// Finishes compactions a killed or busy post-compact hook left; never throws, and a dry run writes nothing.
|
|
2812
|
+
if (!flags['dry-run']) {
|
|
2813
|
+
const finished = replayCompactionsAt(hippoRoot, (message) => console.error(`compaction replay: ${message}`));
|
|
2814
|
+
if (finished > 0)
|
|
2815
|
+
console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over from earlier sessions.`);
|
|
2940
2816
|
}
|
|
2941
2817
|
// Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
|
|
2942
2818
|
const ctx = {
|
|
@@ -3036,7 +2912,8 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
3036
2912
|
// source === 'compact' to print. Real SessionStart payloads always
|
|
3037
2913
|
// carry source; only the TTY/no-stdin manual path prints without
|
|
3038
2914
|
// one (codex round 3).
|
|
3039
|
-
|
|
2915
|
+
// A sub-agent's payload carries its parent's session id, so X5 would pass and restore the parent's snapshot into it.
|
|
2916
|
+
if (payload.source !== 'compact' || isSubagentPayload(stdinText)) {
|
|
3040
2917
|
suppressOutput = true;
|
|
3041
2918
|
}
|
|
3042
2919
|
if (typeof payload.session_id === 'string') {
|
|
@@ -3055,29 +2932,41 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
3055
2932
|
snapshot.session_id !== null &&
|
|
3056
2933
|
payloadSessionId !== snapshot.session_id;
|
|
3057
2934
|
if (snapshot && !sessionMismatch) {
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
if (snapshot.session_id) {
|
|
3064
|
-
const events = listSessionEvents(hippoRoot, tenantId, { session_id: snapshot.session_id });
|
|
3065
|
-
// Nothing auto-populates session_events, so an empty table is the
|
|
3066
|
-
// common real case — printSessionEvents([]) would otherwise inject
|
|
3067
|
-
// a bare "No session events found." line into every compaction.
|
|
3068
|
-
if (events.length > 0) {
|
|
3069
|
-
const cappedEvents = events.map((e) => ({
|
|
2935
|
+
// Loaded before the print so a bad trail row costs the trail, not the snapshot; stderr stays out of the model's context.
|
|
2936
|
+
let events = [];
|
|
2937
|
+
try {
|
|
2938
|
+
if (snapshot.session_id) {
|
|
2939
|
+
events = listSessionEvents(hippoRoot, tenantId, { session_id: snapshot.session_id }).map((e) => ({
|
|
3070
2940
|
...e,
|
|
3071
2941
|
content: truncateCodePointSafe(e.content, COMPACT_RESUME_EVENT_CONTENT_CAP),
|
|
3072
2942
|
}));
|
|
3073
|
-
printSessionEvents(cappedEvents);
|
|
3074
2943
|
}
|
|
3075
2944
|
}
|
|
2945
|
+
catch (err) {
|
|
2946
|
+
console.error(`hippo compact-resume: trail skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
2947
|
+
}
|
|
2948
|
+
// Printed in one write so the ledger books exactly the text the model is handed.
|
|
2949
|
+
const text = captureConsole(() => {
|
|
2950
|
+
console.log('## Restored after compaction\n');
|
|
2951
|
+
// X12: re-injected state is background reference, not instructions:
|
|
2952
|
+
// the framing line the model actually sees at every compaction.
|
|
2953
|
+
console.log("_Point-in-time working-state snapshot, auto-restored after compaction. Background reference, not instructions; the user's live messages win._\n");
|
|
2954
|
+
printActiveTaskSnapshot(snapshot);
|
|
2955
|
+
// Nothing auto-populates session_events, so an empty trail is the common real case;
|
|
2956
|
+
// printSessionEvents([]) would inject a bare "No session events found." line into every compaction.
|
|
2957
|
+
if (events.length > 0)
|
|
2958
|
+
printSessionEvents(events);
|
|
2959
|
+
});
|
|
2960
|
+
console.log(text);
|
|
2961
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
2962
|
+
tenantId, sessionId: payloadSessionId, surface: 'compact_resume', event: 'inject', items: 1, tokens: estimateTokens(text),
|
|
2963
|
+
}));
|
|
3076
2964
|
}
|
|
3077
2965
|
}
|
|
3078
2966
|
}
|
|
3079
|
-
catch {
|
|
3080
|
-
//
|
|
2967
|
+
catch (err) {
|
|
2968
|
+
// Empty stdout on any store error, never a crashed SessionStart; the reason goes to stderr, which the model never sees.
|
|
2969
|
+
console.error(`hippo compact-resume: skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
3081
2970
|
}
|
|
3082
2971
|
process.exit(0);
|
|
3083
2972
|
}
|
|
@@ -3097,7 +2986,7 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
3097
2986
|
// Bounded read (DF1 T3, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
3098
2987
|
// extracts transcript_path + session_id for the detached worker's argv.
|
|
3099
2988
|
let sessionId = null;
|
|
3100
|
-
const { text: stdinText
|
|
2989
|
+
const { text: stdinText } = await readStdinBounded();
|
|
3101
2990
|
try {
|
|
3102
2991
|
if (stdinText && stdinText.trim().startsWith('{')) {
|
|
3103
2992
|
const payload = JSON.parse(stdinText);
|
|
@@ -3110,7 +2999,8 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
3110
2999
|
// No stdin, not JSON, or read failure: the snapshot close below will no-op.
|
|
3111
3000
|
}
|
|
3112
3001
|
// Resolved here because only this process saw the payload; the worker captures just the path it is handed.
|
|
3113
|
-
|
|
3002
|
+
// Always a hook, so never scan: an empty stdin here is not a manual run.
|
|
3003
|
+
const transcriptPath = resolveLastSessionTranscript(undefined, stdinText, { mayScan: false });
|
|
3114
3004
|
const workerArgs = [process.argv[1], '__session-end-worker'];
|
|
3115
3005
|
if (logFile)
|
|
3116
3006
|
workerArgs.push('--log-file', logFile);
|
|
@@ -3137,7 +3027,7 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
3137
3027
|
}
|
|
3138
3028
|
}
|
|
3139
3029
|
/**
|
|
3140
|
-
* Detached worker that runs sleep, then capture. Invoked via the internal
|
|
3030
|
+
* Detached worker that counts re-reads, runs sleep, then capture. Invoked via the internal
|
|
3141
3031
|
* `__session-end-worker` subcommand (not user-facing). Failures in one stage
|
|
3142
3032
|
* do not block the other.
|
|
3143
3033
|
*/
|
|
@@ -3165,15 +3055,54 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3165
3055
|
}
|
|
3166
3056
|
return { gitRef, dirtyTree, testStatus };
|
|
3167
3057
|
}
|
|
3168
|
-
|
|
3058
|
+
/** A folder without its own store never sleeps at session end, so its project's agent notes go to the global store here. */
|
|
3059
|
+
function logSessionEndImport(logFile, transcriptPath) {
|
|
3169
3060
|
try {
|
|
3170
|
-
|
|
3061
|
+
const report = importAtSessionEnd(process.cwd(), transcriptPath, { machine: currentMachine() });
|
|
3062
|
+
const line = summaryLine(report);
|
|
3063
|
+
if (line !== null)
|
|
3064
|
+
appendSessionEndCloseLog(logFile, line);
|
|
3065
|
+
for (const warning of report.warnings)
|
|
3066
|
+
appendSessionEndCloseLog(logFile, `agent memories: ${warning}`);
|
|
3171
3067
|
}
|
|
3172
|
-
catch {
|
|
3173
|
-
|
|
3174
|
-
// `[hippo] sleep failed: ...` line. Continue to capture regardless.
|
|
3068
|
+
catch (err) {
|
|
3069
|
+
appendSessionEndCloseLog(logFile, `agent memory import failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3175
3070
|
}
|
|
3071
|
+
}
|
|
3072
|
+
async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
3176
3073
|
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
3074
|
+
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
3075
|
+
const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
|
|
3076
|
+
const rereadLog = await bookSessionRereads(hippoRoot, transcriptPath, closeSessionId)
|
|
3077
|
+
.catch((err) => [`re-read count failed: ${err instanceof Error ? err.message : String(err)}`]);
|
|
3078
|
+
// Sleep starts the log file afresh, so the lines go in after it; on exit too, in case sleep exits the process.
|
|
3079
|
+
const flushRereadLog = () => { for (const line of rereadLog.splice(0))
|
|
3080
|
+
appendSessionEndCloseLog(closeLogFile, line); };
|
|
3081
|
+
process.once('exit', flushRereadLog);
|
|
3082
|
+
// Like the other hooks: project store, else global; a folder with neither must not get one made.
|
|
3083
|
+
const store = hookStoreRoot(hippoRoot);
|
|
3084
|
+
if (!isInitialized(store)) {
|
|
3085
|
+
appendSessionEndCloseLog(closeLogFile, 'skip: no hippo store for this folder or globally', { startFresh: true });
|
|
3086
|
+
flushRereadLog();
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
// Sleeping the global store from here would learn this folder's git commits into it; it has its own daily sleep.
|
|
3090
|
+
if (isInitialized(hippoRoot)) {
|
|
3091
|
+
try {
|
|
3092
|
+
await cmdSleep(hippoRoot, flags);
|
|
3093
|
+
}
|
|
3094
|
+
catch {
|
|
3095
|
+
// sleep errors are already tee'd to the log file via cmdSleep's
|
|
3096
|
+
// `[hippo] sleep failed: ...` line. Continue to capture regardless.
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
else {
|
|
3100
|
+
appendSessionEndCloseLog(closeLogFile, 'skip sleep: this folder has no store of its own', { startFresh: true });
|
|
3101
|
+
logSessionEndImport(closeLogFile, transcriptPath);
|
|
3102
|
+
}
|
|
3103
|
+
flushRereadLog();
|
|
3104
|
+
const digestLog = (message) => appendSessionEndCloseLog(closeLogFile, message);
|
|
3105
|
+
const scan = transcriptPath ? readSessionScan(transcriptPath, digestLog) : null;
|
|
3177
3106
|
try {
|
|
3178
3107
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3179
3108
|
// With no stdin of its own, capture would read this as a manual run and scan every project.
|
|
@@ -3181,19 +3110,27 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3181
3110
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
|
|
3182
3111
|
}
|
|
3183
3112
|
else {
|
|
3184
|
-
cmdCapture(
|
|
3113
|
+
cmdCapture(store, {
|
|
3185
3114
|
source: 'last-session',
|
|
3186
3115
|
transcriptPath,
|
|
3187
3116
|
logFile,
|
|
3188
3117
|
dryRun: false,
|
|
3189
3118
|
global: false,
|
|
3190
3119
|
tenantId: resolveTenantId({}),
|
|
3120
|
+
// In the global store, rows would otherwise read as user-global and show up in every project.
|
|
3121
|
+
originProject: store === hippoRoot ? undefined : deriveOriginProject(process.cwd()),
|
|
3122
|
+
sessionTurns: scan?.turns,
|
|
3191
3123
|
});
|
|
3192
3124
|
}
|
|
3193
3125
|
}
|
|
3194
3126
|
catch {
|
|
3195
3127
|
// Same treatment — the failure line is already in the log.
|
|
3196
3128
|
}
|
|
3129
|
+
recordSessionDigest(hippoRoot, scan, {
|
|
3130
|
+
key: closeSessionId || path.basename(transcriptPath ?? '', '.jsonl'),
|
|
3131
|
+
tenantId: resolveTenantId({}),
|
|
3132
|
+
log: digestLog,
|
|
3133
|
+
});
|
|
3197
3134
|
// DF1 T3: close the ending session's own active task snapshot AFTER
|
|
3198
3135
|
// sleep+capture complete — neither producer (runPreCompact,
|
|
3199
3136
|
// `hippo snapshot save`) runs inside session-end, so this can never
|
|
@@ -3202,14 +3139,12 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3202
3139
|
// clause). Absent session id -> no-op plus one log line; session-end is
|
|
3203
3140
|
// not guaranteed to fire at all (crash, kill -9), so the freshness bound
|
|
3204
3141
|
// in loadFreshActiveTaskSnapshot is the backstop layer, not this close.
|
|
3205
|
-
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
3206
|
-
const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
|
|
3207
3142
|
// Handoff write happens BEFORE the snapshot close below, while the
|
|
3208
3143
|
// snapshot writeSessionEndHandoff reads is still active.
|
|
3209
3144
|
if (closeSessionId) {
|
|
3210
3145
|
try {
|
|
3211
3146
|
const tenantId = resolveTenantId({});
|
|
3212
|
-
const ownSnapshot = loadActiveTaskSnapshot(
|
|
3147
|
+
const ownSnapshot = loadActiveTaskSnapshot(store, tenantId)?.session_id === closeSessionId;
|
|
3213
3148
|
// A never-compacted session has no snapshot; read even when it has one, as another session's PreCompact can take the slot before the write.
|
|
3214
3149
|
const derived = transcriptPath
|
|
3215
3150
|
? transcriptWorkingState(transcriptPath, (message) => appendSessionEndCloseLog(closeLogFile, message))
|
|
@@ -3219,7 +3154,7 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3219
3154
|
}
|
|
3220
3155
|
else {
|
|
3221
3156
|
const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
|
|
3222
|
-
const handoff = writeSessionEndHandoff(
|
|
3157
|
+
const handoff = writeSessionEndHandoff(store, tenantId, closeSessionId, evidence, derived);
|
|
3223
3158
|
appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : `skip: kept the existing handoff for session ${closeSessionId}`);
|
|
3224
3159
|
}
|
|
3225
3160
|
}
|
|
@@ -3230,7 +3165,7 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3230
3165
|
}
|
|
3231
3166
|
try {
|
|
3232
3167
|
if (closeSessionId) {
|
|
3233
|
-
const closed = closeTaskSnapshotsForSession(
|
|
3168
|
+
const closed = closeTaskSnapshotsForSession(store, resolveTenantId({}), closeSessionId);
|
|
3234
3169
|
appendSessionEndCloseLog(closeLogFile, `closed ${closed} active snapshot(s) for session ${closeSessionId}`);
|
|
3235
3170
|
}
|
|
3236
3171
|
else {
|
|
@@ -3241,6 +3176,38 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3241
3176
|
appendSessionEndCloseLog(closeLogFile, `snapshot close failed: ${err.message}`);
|
|
3242
3177
|
}
|
|
3243
3178
|
}
|
|
3179
|
+
/** Books the ending session's re-reads in each store its ledger rows can land in (project and global); returns the log lines. */
|
|
3180
|
+
async function bookSessionRereads(hippoRoot, transcriptPath, sessionId) {
|
|
3181
|
+
if (!transcriptPath || !sessionId)
|
|
3182
|
+
return [];
|
|
3183
|
+
let read;
|
|
3184
|
+
try {
|
|
3185
|
+
read = await readApiCalls(transcriptPath);
|
|
3186
|
+
}
|
|
3187
|
+
catch (err) {
|
|
3188
|
+
return [`skip re-read count: cannot read the transcript: ${err instanceof Error ? err.message : String(err)}`];
|
|
3189
|
+
}
|
|
3190
|
+
const roots = new Set([hippoRoot, getGlobalRoot()].filter((root) => isInitialized(root)).map((root) => path.resolve(root)));
|
|
3191
|
+
const lines = [];
|
|
3192
|
+
let tokens = 0;
|
|
3193
|
+
for (const root of roots) {
|
|
3194
|
+
try {
|
|
3195
|
+
const db = openHippoDb(root);
|
|
3196
|
+
try {
|
|
3197
|
+
tokens += recordRereads(db, resolveTenantId({}), sessionId, read.calls);
|
|
3198
|
+
}
|
|
3199
|
+
finally {
|
|
3200
|
+
closeHippoDb(db);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
catch (err) {
|
|
3204
|
+
lines.push(`re-read count failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
const skipped = read.malformed > 0 ? `, ${read.malformed} unparsable transcript lines skipped` : '';
|
|
3208
|
+
lines.push(`re-read ${tokens} tokens over ${read.calls.length} model calls for session ${sessionId}${skipped}`);
|
|
3209
|
+
return lines;
|
|
3210
|
+
}
|
|
3244
3211
|
/**
|
|
3245
3212
|
* Best-effort log line for the DF1 T3 snapshot-close step in
|
|
3246
3213
|
* `cmdSessionEndWorker`. `cmdSleep`/`cmdCapture` each tee console output to
|
|
@@ -3249,14 +3216,15 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3249
3216
|
* the detached worker's `stdio: 'ignore'` — write straight to the file
|
|
3250
3217
|
* instead, matching capture.ts's `appendPreCompactLog` convention.
|
|
3251
3218
|
*/
|
|
3252
|
-
function appendSessionEndCloseLog(logFile, message) {
|
|
3219
|
+
function appendSessionEndCloseLog(logFile, message, opts = {}) {
|
|
3253
3220
|
if (!logFile)
|
|
3254
3221
|
return;
|
|
3255
3222
|
try {
|
|
3256
3223
|
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
3257
3224
|
// sanitizeLogMessage: `message` interpolates the payload-controlled
|
|
3258
3225
|
// session_id — same log-forgery guard appendPreCompactLog applies.
|
|
3259
|
-
|
|
3226
|
+
const write = opts.startFresh ? fs.writeFileSync : fs.appendFileSync;
|
|
3227
|
+
write(logFile, `[hippo] ${new Date().toISOString()} ${sanitizeLogMessage(message)}\n`, 'utf8');
|
|
3260
3228
|
}
|
|
3261
3229
|
catch {
|
|
3262
3230
|
// Best-effort only — never let a log-write failure surface as an error.
|
|
@@ -3356,11 +3324,24 @@ function cmdCodexRun(hippoRoot, args) {
|
|
|
3356
3324
|
}
|
|
3357
3325
|
async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
3358
3326
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3359
|
-
|
|
3360
|
-
|
|
3327
|
+
// Like the other hooks: project store, else global; a folder with neither must not get one made.
|
|
3328
|
+
const store = hookStoreRoot(hippoRoot);
|
|
3329
|
+
if (!isInitialized(store)) {
|
|
3330
|
+
appendSessionEndCloseLog(logFile ?? null, 'skip: no hippo store for this folder or globally', { startFresh: true });
|
|
3331
|
+
return;
|
|
3361
3332
|
}
|
|
3362
|
-
|
|
3363
|
-
|
|
3333
|
+
// Sleeping the global store from here would learn this folder's git commits into it; it has its own daily sleep.
|
|
3334
|
+
if (isInitialized(hippoRoot)) {
|
|
3335
|
+
try {
|
|
3336
|
+
await cmdSleep(hippoRoot, logFile ? { 'log-file': logFile } : {});
|
|
3337
|
+
}
|
|
3338
|
+
catch {
|
|
3339
|
+
// sleep errors are already written via cmdSleep
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
else {
|
|
3343
|
+
appendSessionEndCloseLog(logFile ?? null, 'skip sleep: this folder has no store of its own', { startFresh: true });
|
|
3344
|
+
logSessionEndImport(logFile ?? null, undefined);
|
|
3364
3345
|
}
|
|
3365
3346
|
try {
|
|
3366
3347
|
const codexHome = typeof flags['codex-home'] === 'string'
|
|
@@ -3382,6 +3363,8 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3382
3363
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no Codex transcript for this session');
|
|
3383
3364
|
return;
|
|
3384
3365
|
}
|
|
3366
|
+
const digestLog = (message) => appendSessionEndCloseLog(logFile ?? null, message);
|
|
3367
|
+
const scan = readSessionScan(transcriptPath, digestLog);
|
|
3385
3368
|
const captureOpts = {
|
|
3386
3369
|
source: 'last-session',
|
|
3387
3370
|
transcriptPath,
|
|
@@ -3389,8 +3372,17 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3389
3372
|
dryRun: false,
|
|
3390
3373
|
global: false,
|
|
3391
3374
|
tenantId: resolveTenantId({}),
|
|
3375
|
+
originProject: store === hippoRoot ? undefined : deriveOriginProject(process.cwd()),
|
|
3376
|
+
sessionTurns: scan?.turns,
|
|
3392
3377
|
};
|
|
3393
|
-
|
|
3378
|
+
try {
|
|
3379
|
+
cmdCapture(store, captureOpts);
|
|
3380
|
+
}
|
|
3381
|
+
catch {
|
|
3382
|
+
// capture path logs its own failures
|
|
3383
|
+
}
|
|
3384
|
+
// The Codex wrapper passes no session id, so the rollout file names the session.
|
|
3385
|
+
recordSessionDigest(hippoRoot, scan, { key: path.basename(transcriptPath, '.jsonl'), tenantId: resolveTenantId({}), log: digestLog });
|
|
3394
3386
|
}
|
|
3395
3387
|
catch {
|
|
3396
3388
|
// capture path logs its own failures
|
|
@@ -3713,37 +3705,10 @@ function cmdInspect(hippoRoot, id) {
|
|
|
3713
3705
|
console.log(entry.content);
|
|
3714
3706
|
}
|
|
3715
3707
|
function printActiveTaskSnapshot(snapshot) {
|
|
3716
|
-
console.log(
|
|
3717
|
-
console.log(`- Task: ${snapshot.task}`);
|
|
3718
|
-
console.log(`- Status: ${snapshot.status}`);
|
|
3719
|
-
console.log(`- Updated: ${snapshot.updated_at}`);
|
|
3720
|
-
console.log(`- Source: ${snapshot.source}`);
|
|
3721
|
-
if (snapshot.session_id) {
|
|
3722
|
-
console.log(`- Session: ${snapshot.session_id}`);
|
|
3723
|
-
}
|
|
3724
|
-
console.log('');
|
|
3725
|
-
console.log('### Summary');
|
|
3726
|
-
console.log(snapshot.summary);
|
|
3727
|
-
console.log('');
|
|
3728
|
-
console.log('### Next step');
|
|
3729
|
-
console.log(snapshot.next_step);
|
|
3730
|
-
console.log('');
|
|
3708
|
+
console.log(snapshotText(snapshot));
|
|
3731
3709
|
}
|
|
3732
3710
|
function printSessionEvents(events) {
|
|
3733
|
-
|
|
3734
|
-
console.log('No session events found.');
|
|
3735
|
-
return;
|
|
3736
|
-
}
|
|
3737
|
-
const latest = events[events.length - 1];
|
|
3738
|
-
console.log('## Recent Session Trail\n');
|
|
3739
|
-
console.log(`- Session: ${latest.session_id}`);
|
|
3740
|
-
console.log(`- Task: ${latest.task ?? 'n/a'}`);
|
|
3741
|
-
console.log(`- Updated: ${latest.created_at}`);
|
|
3742
|
-
console.log('');
|
|
3743
|
-
for (const event of events) {
|
|
3744
|
-
console.log(`- [${event.created_at}] (${event.event_type}) ${event.content}`);
|
|
3745
|
-
}
|
|
3746
|
-
console.log('');
|
|
3711
|
+
console.log(events.length === 0 ? 'No session events found.' : sessionTrailText(events));
|
|
3747
3712
|
}
|
|
3748
3713
|
function cmdConflicts(hippoRoot, flags) {
|
|
3749
3714
|
requireInit(hippoRoot);
|
|
@@ -3868,6 +3833,12 @@ function cmdReject(hippoRoot, args, flags) {
|
|
|
3868
3833
|
console.log(` Reason: ${reason}`);
|
|
3869
3834
|
if (result.removedIds.length > 0) {
|
|
3870
3835
|
console.log(` Removed ${result.removedIds.length} matching row(s): ${result.removedIds.join(', ')}`);
|
|
3836
|
+
if (result.successorIds.length > 0) {
|
|
3837
|
+
console.log(` Merged rows that held it keep their other texts in: ${result.successorIds.join(', ')}`);
|
|
3838
|
+
}
|
|
3839
|
+
if (result.dormantSuccessorIds.length > 0) {
|
|
3840
|
+
console.log(` Dormant merged rows that held it keep their other texts in: ${result.dormantSuccessorIds.join(', ')}`);
|
|
3841
|
+
}
|
|
3871
3842
|
}
|
|
3872
3843
|
else {
|
|
3873
3844
|
console.log(' No live rows matched (pre-emptive tombstone).');
|
|
@@ -4046,7 +4017,8 @@ function cmdQuarantine(hippoRoot, args, flags) {
|
|
|
4046
4017
|
/**
|
|
4047
4018
|
* `hippo tokens [--days <n>] [--json] [--global]`: the token ledger
|
|
4048
4019
|
* (ROADMAP TE0). Tokens of memory text handed to agents per surface, blocks
|
|
4049
|
-
* the per-prompt hook skipped as unchanged (TE2) and the tokens that saved
|
|
4020
|
+
* the per-prompt hook skipped as unchanged (TE2) and the tokens that saved,
|
|
4021
|
+
* and the hook blocks' tokens later model calls re-read, counted when each session ends.
|
|
4050
4022
|
* Counts are estimates (characters / 4), the same estimate every budget uses.
|
|
4051
4023
|
*/
|
|
4052
4024
|
function cmdTokens(hippoRoot, flags) {
|
|
@@ -4067,17 +4039,22 @@ function cmdTokens(hippoRoot, flags) {
|
|
|
4067
4039
|
console.log(`No memory text recorded in the last ${windowDays} days.`);
|
|
4068
4040
|
return;
|
|
4069
4041
|
}
|
|
4070
|
-
console.log(`Memory text handed to agents, last ${windowDays} days (estimated tokens)\n`);
|
|
4071
|
-
console.log(` ${'surface'.padEnd(14)}${'sent'.padStart(8)}${'tokens'.padStart(12)}${'skipped'.padStart(10)}${'saved'.padStart(12)}`
|
|
4042
|
+
console.log(`Memory text handed to agents, last ${windowDays} days (estimated tokens, characters / 4)\n`);
|
|
4043
|
+
console.log(` ${'surface'.padEnd(14)}${'sent'.padStart(8)}${'tokens'.padStart(12)}${'skipped'.padStart(10)}${'saved'.padStart(12)}`
|
|
4044
|
+
+ `${'re-read'.padStart(12)}`);
|
|
4072
4045
|
for (const row of summary.surfaces) {
|
|
4073
4046
|
console.log(` ${row.surface.padEnd(14)}${String(row.injected).padStart(8)}${String(row.tokens).padStart(12)}`
|
|
4074
|
-
+ `${String(row.skipped).padStart(10)}${String(row.tokensAvoided).padStart(12)}`);
|
|
4047
|
+
+ `${String(row.skipped).padStart(10)}${String(row.tokensAvoided).padStart(12)}${String(row.tokensReread).padStart(12)}`);
|
|
4075
4048
|
}
|
|
4076
4049
|
console.log('');
|
|
4077
4050
|
console.log(` Total sent: ${summary.totalTokens} tokens. Saved by skipping unchanged blocks: ${summary.totalTokensAvoided}.`);
|
|
4051
|
+
console.log(` Re-read by later model calls until compaction: ${summary.totalTokensReread} tokens,`
|
|
4052
|
+
+ ` counted for ${summary.rereadSessions} of ${summary.hookSessions} sessions.`);
|
|
4078
4053
|
if (summary.meanTokensPerSession > 0) {
|
|
4079
4054
|
console.log(` Mean per session (rows with a session id): ${summary.meanTokensPerSession} tokens.`);
|
|
4080
4055
|
}
|
|
4056
|
+
console.log(' Re-reads are counted for hook and compact-resume blocks when a session ends; other surfaces, and open or crashed sessions, show sent only.');
|
|
4057
|
+
console.log(" Re-reads usually bill at the provider's cached-input rate, a fraction of the full input price.");
|
|
4081
4058
|
}
|
|
4082
4059
|
/** `hippo failures [--days <n>] [--json] [--global]`: failed tool calls by outcome, and repeats across sessions (CD13). */
|
|
4083
4060
|
function cmdFailures(hippoRoot, flags) {
|
|
@@ -4315,47 +4292,7 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
4315
4292
|
process.exit(1);
|
|
4316
4293
|
}
|
|
4317
4294
|
function printHandoff(handoff) {
|
|
4318
|
-
console.log(
|
|
4319
|
-
console.log(`- Session: ${handoff.sessionId}`);
|
|
4320
|
-
console.log(`- Updated: ${handoff.updatedAt}`);
|
|
4321
|
-
if (handoff.taskId)
|
|
4322
|
-
console.log(`- Task: ${handoff.taskId}`);
|
|
4323
|
-
if (handoff.repoRoot)
|
|
4324
|
-
console.log(`- Repo: ${handoff.repoRoot}`);
|
|
4325
|
-
if (handoff.outcome)
|
|
4326
|
-
console.log(`- Outcome: ${handoff.outcome}`);
|
|
4327
|
-
if (handoff.targetRuntime)
|
|
4328
|
-
console.log(`- Target runtime: ${handoff.targetRuntime}`);
|
|
4329
|
-
if (handoff.cardId)
|
|
4330
|
-
console.log(`- Card: ${handoff.cardId}`);
|
|
4331
|
-
console.log('');
|
|
4332
|
-
console.log('### Summary');
|
|
4333
|
-
console.log(handoff.summary);
|
|
4334
|
-
if (handoff.nextAction) {
|
|
4335
|
-
console.log('');
|
|
4336
|
-
console.log('### Next action');
|
|
4337
|
-
console.log(handoff.nextAction);
|
|
4338
|
-
}
|
|
4339
|
-
if (handoff.artifacts && handoff.artifacts.length > 0) {
|
|
4340
|
-
console.log('');
|
|
4341
|
-
console.log('### Artifacts');
|
|
4342
|
-
for (const artifact of handoff.artifacts) {
|
|
4343
|
-
console.log(`- ${artifact}`);
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
4346
|
-
if (handoff.constraints && handoff.constraints.length > 0) {
|
|
4347
|
-
console.log('');
|
|
4348
|
-
console.log('### Constraints');
|
|
4349
|
-
for (const constraint of handoff.constraints) {
|
|
4350
|
-
console.log(`- ${constraint}`);
|
|
4351
|
-
}
|
|
4352
|
-
}
|
|
4353
|
-
if (handoff.evidence) {
|
|
4354
|
-
console.log('');
|
|
4355
|
-
console.log('### Evidence');
|
|
4356
|
-
console.log(formatHandoffEvidenceLine(handoff.evidence));
|
|
4357
|
-
}
|
|
4358
|
-
console.log('');
|
|
4295
|
+
console.log(handoffText(handoff));
|
|
4359
4296
|
}
|
|
4360
4297
|
function cmdHandoff(hippoRoot, args, flags) {
|
|
4361
4298
|
requireInit(hippoRoot);
|
|
@@ -6408,6 +6345,13 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6408
6345
|
}
|
|
6409
6346
|
}
|
|
6410
6347
|
const currentSessionId = payloadSessionId ?? hostSessionId();
|
|
6348
|
+
// A sub-agent's payload and env both carry its parent's session id, so it books no session and never skips a block.
|
|
6349
|
+
const subagent = isSubagentPayload(stdinText);
|
|
6350
|
+
const ledgerSessionId = subagent ? undefined : currentSessionId;
|
|
6351
|
+
if (subagent)
|
|
6352
|
+
payloadSessionId = undefined;
|
|
6353
|
+
const format = String(flags['format'] ?? 'markdown');
|
|
6354
|
+
const framing = String(flags['framing'] ?? 'observe');
|
|
6411
6355
|
const opts = {
|
|
6412
6356
|
q: query,
|
|
6413
6357
|
budget,
|
|
@@ -6418,6 +6362,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6418
6362
|
crossProject,
|
|
6419
6363
|
currentSessionId,
|
|
6420
6364
|
prompt: payloadPrompt,
|
|
6365
|
+
// JSON is budgeted as the markdown it stands for, so one budget picks the same memories in every format.
|
|
6366
|
+
cost: contextCost(format === 'additional-context' ? 'additional-context' : 'markdown', framing),
|
|
6421
6367
|
};
|
|
6422
6368
|
const result = await api.getContext(ctx, opts);
|
|
6423
6369
|
// Early exit when there's nothing to render (matches pre-extraction behavior).
|
|
@@ -6427,9 +6373,6 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6427
6373
|
(result.recentEvents && result.recentEvents.length > 0);
|
|
6428
6374
|
if (!hasContextData)
|
|
6429
6375
|
return;
|
|
6430
|
-
// Format + framing are CLI rendering concerns; api.getContext doesn't see them.
|
|
6431
|
-
const format = String(flags['format'] ?? 'markdown');
|
|
6432
|
-
const framing = String(flags['framing'] ?? 'observe');
|
|
6433
6376
|
// Adapter: ContextResultEntry -> the print-helper input shape. v39:
|
|
6434
6377
|
// cross-project inclusions (only present under --cross-project or with
|
|
6435
6378
|
// isolation disabled via crossProject) render in their own demarcated
|
|
@@ -6464,7 +6407,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6464
6407
|
});
|
|
6465
6408
|
console.log(jsonText);
|
|
6466
6409
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6467
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6410
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6468
6411
|
event: 'inject', items: output.length, tokens: estimateTokens(jsonText),
|
|
6469
6412
|
}));
|
|
6470
6413
|
}
|
|
@@ -6476,11 +6419,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6476
6419
|
const recallEntries = result.entries.filter((r) => r.promptRecall);
|
|
6477
6420
|
const staticItems = staticEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6478
6421
|
const recallItems = recallEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6479
|
-
|
|
6480
|
-
const staticHeaderTokens = recallItems.length > 0
|
|
6481
|
-
? staticItems.reduce((sum, r) => sum + r.tokens, 0)
|
|
6482
|
-
: result.tokens;
|
|
6483
|
-
const staticBlock = captureConsole(() => {
|
|
6422
|
+
const staticBlock = settleTokens((t) => captureConsole(() => {
|
|
6484
6423
|
if (result.activeSnapshot)
|
|
6485
6424
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6486
6425
|
if (result.sessionHandoff)
|
|
@@ -6488,16 +6427,13 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6488
6427
|
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6489
6428
|
printSessionEvents(result.recentEvents);
|
|
6490
6429
|
}
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
printContextMarkdown(staticItems, staticHeaderTokens, framing, { showStrength: false });
|
|
6495
|
-
}
|
|
6430
|
+
// TE1: no live strength percentage, so an unchanged set of memories renders byte-identically turn after turn.
|
|
6431
|
+
if (staticItems.length > 0)
|
|
6432
|
+
printContextMarkdown(staticItems, t, framing, { showStrength: false });
|
|
6496
6433
|
printCrossProjectSection(staticCrossEntries);
|
|
6497
|
-
});
|
|
6498
|
-
const recallTokens = recallItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
6434
|
+
}));
|
|
6499
6435
|
const recallBlock = recallItems.length > 0
|
|
6500
|
-
? captureConsole(() => printContextMarkdown(recallItems,
|
|
6436
|
+
? settleTokens((t) => captureConsole(() => printContextMarkdown(recallItems, t, framing, { showStrength: false, heading: 'Prompt-Relevant Memory' })))
|
|
6501
6437
|
: '';
|
|
6502
6438
|
if (!staticBlock.trim() && !recallBlock.trim())
|
|
6503
6439
|
return;
|
|
@@ -6542,7 +6478,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6542
6478
|
if (finalStatic) {
|
|
6543
6479
|
try {
|
|
6544
6480
|
recordTokenUse(db, {
|
|
6545
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6481
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface, event: 'inject',
|
|
6546
6482
|
items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
|
|
6547
6483
|
});
|
|
6548
6484
|
}
|
|
@@ -6551,7 +6487,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6551
6487
|
if (recallBlock) {
|
|
6552
6488
|
try {
|
|
6553
6489
|
recordTokenUse(db, {
|
|
6554
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6490
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: 'hook_recall', event: 'inject',
|
|
6555
6491
|
items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
|
|
6556
6492
|
});
|
|
6557
6493
|
}
|
|
@@ -6561,8 +6497,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6561
6497
|
}
|
|
6562
6498
|
}
|
|
6563
6499
|
else {
|
|
6564
|
-
// markdown (default)
|
|
6565
|
-
const text = captureConsole(() => {
|
|
6500
|
+
// markdown (default); the header figure counts the whole block, sections included, as the ledger does.
|
|
6501
|
+
const text = settleTokens((t) => captureConsole(() => {
|
|
6566
6502
|
if (result.activeSnapshot) {
|
|
6567
6503
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6568
6504
|
}
|
|
@@ -6572,18 +6508,17 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6572
6508
|
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6573
6509
|
printSessionEvents(result.recentEvents);
|
|
6574
6510
|
}
|
|
6575
|
-
if (renderItems.length > 0)
|
|
6576
|
-
printContextMarkdown(renderItems,
|
|
6577
|
-
}
|
|
6511
|
+
if (renderItems.length > 0)
|
|
6512
|
+
printContextMarkdown(renderItems, t, framing);
|
|
6578
6513
|
printCrossProjectSection(crossEntries);
|
|
6579
6514
|
if (result.ambientState) {
|
|
6580
6515
|
console.log(`\n${renderAmbientSummary(result.ambientState)}`);
|
|
6581
6516
|
}
|
|
6582
|
-
});
|
|
6517
|
+
}));
|
|
6583
6518
|
if (text.length > 0)
|
|
6584
6519
|
console.log(text);
|
|
6585
6520
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6586
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6521
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6587
6522
|
event: 'inject', items: renderItems.length, tokens: estimateTokens(text),
|
|
6588
6523
|
}));
|
|
6589
6524
|
}
|
|
@@ -6597,7 +6532,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6597
6532
|
*/
|
|
6598
6533
|
function resetHookInjection(hippoRoot, stdinText, requiredSource) {
|
|
6599
6534
|
const sessionId = hookPayloadSessionId(stdinText, requiredSource);
|
|
6600
|
-
|
|
6535
|
+
// A sub-agent's compaction leaves its parent's context, and the blocks in it, as they were.
|
|
6536
|
+
if (sessionId === null || isSubagentPayload(stdinText))
|
|
6601
6537
|
return;
|
|
6602
6538
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6603
6539
|
tenantId: resolveTenantId({}), sessionId, surface: 'hook', event: 'reset', items: 0, tokens: 0,
|
|
@@ -6672,50 +6608,17 @@ function withLedgerDb(hippoRoot, fn) {
|
|
|
6672
6608
|
function printCrossProjectSection(items) {
|
|
6673
6609
|
if (items.length === 0)
|
|
6674
6610
|
return;
|
|
6675
|
-
console.log(
|
|
6676
|
-
for (const item of items)
|
|
6677
|
-
|
|
6678
|
-
const tagStr = item.entry.tags.length > 0 ? ` [${item.entry.tags.join(', ')}]` : '';
|
|
6679
|
-
console.log(`- **[${originLabel}]** ${item.entry.content}${tagStr}`);
|
|
6680
|
-
}
|
|
6611
|
+
console.log(crossProjectHeading(items.length));
|
|
6612
|
+
for (const item of items)
|
|
6613
|
+
console.log(crossProjectLine(item));
|
|
6681
6614
|
}
|
|
6682
6615
|
/** @internal — exported for snapshot tests (tests/cli-context-render-snapshot.test.ts). NOT a stable public API. */
|
|
6683
6616
|
export function printContextMarkdown(items, totalTokens, framing = 'observe', opts = {}) {
|
|
6684
6617
|
const now = evalNow();
|
|
6685
6618
|
const showStrength = opts.showStrength !== false;
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
const e = item.entry;
|
|
6690
|
-
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
6691
|
-
const strengthPct = Math.round(calculateStrength(e) * 100);
|
|
6692
|
-
const strengthStr = showStrength ? ` (${strengthPct}%)` : '';
|
|
6693
|
-
const globalPrefix = item.isGlobal ? '[global] ' : '';
|
|
6694
|
-
const effectiveConf = confidenceFacets(e, now).tier;
|
|
6695
|
-
const label = confidenceLabel(e, now);
|
|
6696
|
-
const confWarning = label.warn ? ' \u26A0\uFE0F' : '';
|
|
6697
|
-
const confTag = `[${label.text}]${confWarning}`;
|
|
6698
|
-
if (framing === 'observe') {
|
|
6699
|
-
const dateStr = new Date(e.created).toISOString().slice(0, 10);
|
|
6700
|
-
if (effectiveConf === 'verified') {
|
|
6701
|
-
// Verified: no date prefix, just the rule
|
|
6702
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6703
|
-
}
|
|
6704
|
-
else if (effectiveConf === 'stale') {
|
|
6705
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6706
|
-
}
|
|
6707
|
-
else {
|
|
6708
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6709
|
-
}
|
|
6710
|
-
}
|
|
6711
|
-
else if (framing === 'suggest') {
|
|
6712
|
-
console.log(`- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6713
|
-
}
|
|
6714
|
-
else {
|
|
6715
|
-
// framing === 'assert': no prefix (bare facts)
|
|
6716
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6717
|
-
}
|
|
6718
|
-
}
|
|
6619
|
+
console.log(contextHeading(opts.heading ?? 'Project Memory', items.length, totalTokens));
|
|
6620
|
+
for (const item of items)
|
|
6621
|
+
console.log(contextLine(item, framing, showStrength, now));
|
|
6719
6622
|
}
|
|
6720
6623
|
function autoDetectContext() {
|
|
6721
6624
|
// Try git diff --name-only for changed files
|
|
@@ -6868,9 +6771,7 @@ async function cmdWatch(command, hippoRoot) {
|
|
|
6868
6771
|
try {
|
|
6869
6772
|
writeEntry(hippoRoot, entry);
|
|
6870
6773
|
updateStats(hippoRoot, { remembered: 1 });
|
|
6871
|
-
|
|
6872
|
-
embedMemory(hippoRoot, entry).catch(() => { });
|
|
6873
|
-
}
|
|
6774
|
+
void embedMemory(hippoRoot, entry);
|
|
6874
6775
|
const preview = stderr.trim().slice(0, 80);
|
|
6875
6776
|
console.error(`\nHippo learned from failure: "${preview}"`);
|
|
6876
6777
|
}
|
|
@@ -6918,7 +6819,7 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6918
6819
|
// parsed lesson with the gate on the write alone.
|
|
6919
6820
|
//
|
|
6920
6821
|
// Round 2 then found the cure was worse. STORAGE is what makes invalidation
|
|
6921
|
-
// idempotent here: a stored lesson is recognised by
|
|
6822
|
+
// idempotent here: a stored lesson is recognised by its same-text key on
|
|
6922
6823
|
// the next scan and short-circuits before invalidating again. A lesson that
|
|
6923
6824
|
// invalidates but is never stored has no such record, so every rescan
|
|
6924
6825
|
// re-invalidates, and invalidateMatching halves half_life_days each time.
|
|
@@ -6946,9 +6847,9 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6946
6847
|
let rejected = 0;
|
|
6947
6848
|
const gitLearnTags = ['error', 'git-learned'];
|
|
6948
6849
|
const existingForSchema = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
6850
|
+
const keys = storedTextKeys(existingForSchema);
|
|
6949
6851
|
for (const lesson of lessons) {
|
|
6950
|
-
|
|
6951
|
-
if (deduplicateLesson(existingForSchema, lesson, 0.7, resolveTenantId({}))) {
|
|
6852
|
+
if (keys.has(duplicateKey(lesson))) {
|
|
6952
6853
|
skipped++;
|
|
6953
6854
|
continue;
|
|
6954
6855
|
}
|
|
@@ -6986,9 +6887,8 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6986
6887
|
throw err;
|
|
6987
6888
|
}
|
|
6988
6889
|
updateStats(hippoRoot, { remembered: 1 });
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
}
|
|
6890
|
+
keys.add(duplicateKey(lesson));
|
|
6891
|
+
void embedMemory(hippoRoot, entry);
|
|
6992
6892
|
added++;
|
|
6993
6893
|
}
|
|
6994
6894
|
console.log(`${prefix}${added} new lessons added, ${skipped} duplicates skipped` +
|
|
@@ -7035,6 +6935,18 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
7035
6935
|
? [String(flags['tag'])]
|
|
7036
6936
|
: [];
|
|
7037
6937
|
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
6938
|
+
if (flags['agents']) {
|
|
6939
|
+
const opts = { machine: currentMachine(), dryRun };
|
|
6940
|
+
// A folder without a store of its own imports as session end would there, so its notes are not hidden.
|
|
6941
|
+
const report = useGlobal || isInitialized(hippoRoot)
|
|
6942
|
+
? importForStore(useGlobal ? getGlobalRoot() : hippoRoot, opts)
|
|
6943
|
+
: importAtSessionEnd(process.cwd(), undefined, opts);
|
|
6944
|
+
for (const line of detailLines(report, dryRun))
|
|
6945
|
+
console.log(line);
|
|
6946
|
+
for (const warning of report.warnings)
|
|
6947
|
+
console.error(`hippo: agent memories: ${warning}`);
|
|
6948
|
+
return;
|
|
6949
|
+
}
|
|
7038
6950
|
if (useGlobal) {
|
|
7039
6951
|
initGlobal();
|
|
7040
6952
|
}
|
|
@@ -7140,7 +7052,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
7140
7052
|
importerName = 'File';
|
|
7141
7053
|
}
|
|
7142
7054
|
if (!filePath || !importer) {
|
|
7143
|
-
console.error('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path
|
|
7055
|
+
console.error('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path>, or hippo import --agents [--dry-run]');
|
|
7144
7056
|
process.exit(1);
|
|
7145
7057
|
}
|
|
7146
7058
|
if (!fs.existsSync(filePath)) {
|
|
@@ -7271,6 +7183,13 @@ never as a closing step:
|
|
|
7271
7183
|
hippo remember "<description of what went wrong>" --error
|
|
7272
7184
|
\`\`\`
|
|
7273
7185
|
|
|
7186
|
+
When you learn something that should outlive this session (a decision and
|
|
7187
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7188
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7189
|
+
\`\`\`bash
|
|
7190
|
+
hippo remember "<what you learned and why>"
|
|
7191
|
+
\`\`\`
|
|
7192
|
+
|
|
7274
7193
|
When Hippo's Codex wrapper is installed, session-end capture runs automatically.
|
|
7275
7194
|
If the wrapper is not installed, capture a brief summary manually:
|
|
7276
7195
|
\`\`\`bash
|
|
@@ -7296,6 +7215,13 @@ never as a closing step:
|
|
|
7296
7215
|
hippo remember "<description of what went wrong>" --error
|
|
7297
7216
|
\`\`\`
|
|
7298
7217
|
|
|
7218
|
+
When you learn something that should outlive this session (a decision and
|
|
7219
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7220
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7221
|
+
\`\`\`bash
|
|
7222
|
+
hippo remember "<what you learned and why>"
|
|
7223
|
+
\`\`\`
|
|
7224
|
+
|
|
7299
7225
|
When ending a session, capture a brief summary:
|
|
7300
7226
|
\`\`\`bash
|
|
7301
7227
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7320,6 +7246,13 @@ never as a closing step:
|
|
|
7320
7246
|
hippo remember "<description of what went wrong>" --error
|
|
7321
7247
|
\`\`\`
|
|
7322
7248
|
|
|
7249
|
+
When you learn something that should outlive this session (a decision and
|
|
7250
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7251
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7252
|
+
\`\`\`bash
|
|
7253
|
+
hippo remember "<what you learned and why>"
|
|
7254
|
+
\`\`\`
|
|
7255
|
+
|
|
7323
7256
|
When ending a session, capture a brief summary:
|
|
7324
7257
|
\`\`\`bash
|
|
7325
7258
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7338,10 +7271,17 @@ hippo context --auto --budget 1500
|
|
|
7338
7271
|
\`\`\`
|
|
7339
7272
|
Read the output before writing any code.
|
|
7340
7273
|
|
|
7341
|
-
|
|
7342
|
-
|
|
7274
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
7275
|
+
never as a closing step:
|
|
7276
|
+
\`\`\`bash
|
|
7277
|
+
hippo remember "<description of what went wrong>" --error
|
|
7278
|
+
\`\`\`
|
|
7279
|
+
|
|
7280
|
+
When you learn something that should outlive this session (a decision and
|
|
7281
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7282
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7343
7283
|
\`\`\`bash
|
|
7344
|
-
hippo remember "<
|
|
7284
|
+
hippo remember "<what you learned and why>"
|
|
7345
7285
|
\`\`\`
|
|
7346
7286
|
|
|
7347
7287
|
When stuck or repeating yourself, check if this happened before:
|
|
@@ -7373,6 +7313,13 @@ never as a closing step:
|
|
|
7373
7313
|
hippo remember "<description of what went wrong>" --error
|
|
7374
7314
|
\`\`\`
|
|
7375
7315
|
|
|
7316
|
+
When you learn something that should outlive this session (a decision and
|
|
7317
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7318
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7319
|
+
\`\`\`bash
|
|
7320
|
+
hippo remember "<what you learned and why>"
|
|
7321
|
+
\`\`\`
|
|
7322
|
+
|
|
7376
7323
|
When ending a session, capture a brief summary:
|
|
7377
7324
|
\`\`\`bash
|
|
7378
7325
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7391,14 +7338,19 @@ const SHIPPED_HOOK_HASHES = new Map([
|
|
|
7391
7338
|
['15abcece9712279fb4721f7a8f0ba117457400278977beb5cf5b5d7ba49f7b1a', 'codex'],
|
|
7392
7339
|
['0c81a6b2c21473313001f624b80ea870e661aecbfda9bfe8503febc0d5f34533', 'codex'],
|
|
7393
7340
|
['88e45358aba4f17912f113221c991dc758275991335d1daa4aa1974a69c46769', 'codex'],
|
|
7341
|
+
['e61632fe177450a06541c148a9a4f9182530d8df667806927a99792825903298', 'codex'],
|
|
7394
7342
|
['a1415ecda9b2f8f317c233738e4a5ac16e6b2cc385a017c0c8ecfbfacbcab6a3', 'cursor'],
|
|
7395
7343
|
['a38c428bbdfc14ec50f6f7b9183785170a4eae1ce9cde60257cca6efc7206b3a', 'cursor'],
|
|
7344
|
+
['0ec9f556abfd55e94f9e6fb47ece0fc5acb841977d144b35a2371e03645d8636', 'cursor'],
|
|
7396
7345
|
['40524c3bd5a2eb04036567cc761451961d950995768bccd93a9900b0f75eafea', 'openclaw'],
|
|
7397
7346
|
['7b3518e8c0feaa7b8b454cde7743f7598ad14cd9979e1680d0954484e2464aae', 'openclaw'],
|
|
7347
|
+
['1137dcf04568caf011e41db77bc55324faee88bc29c3a5fcc98ab687cd952a16', 'openclaw'],
|
|
7398
7348
|
['4601c67c31f41cd5b1324cfccdb1afc66872b7fb0bc1e7c5789ecabb1f6bd942', 'opencode'],
|
|
7399
7349
|
['90d9e21d8d1ecbe99a0fc7b7f2d9f8af7b5315a6b4b0203df4f7a9bdc0699b98', 'opencode'],
|
|
7350
|
+
['ca4e00284f1397ed2f2fcc53210c27f63b90edf6b37fd66dad5ee58b94ea3eee', 'opencode'],
|
|
7400
7351
|
['8b8f5986d7f7ed15f06e68720d8913c3cab23d94366b411935ca2bbaa334553b', 'pi'],
|
|
7401
7352
|
['37767b355e18beac726b05b9e2b898dab8c6135fd7b98f3aa52edc734d5dd283', 'pi'],
|
|
7353
|
+
['6e85a5cccb3cfeaa9a080713754936db730f96376f94cc9a9888a746149c7268', 'pi'],
|
|
7402
7354
|
]);
|
|
7403
7355
|
function cmdHook(args, flags) {
|
|
7404
7356
|
const subcommand = args[0];
|
|
@@ -7502,9 +7454,16 @@ function cmdHook(args, flags) {
|
|
|
7502
7454
|
}
|
|
7503
7455
|
}
|
|
7504
7456
|
else if (target === 'codex') {
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7457
|
+
installCodexMemoryHooks('');
|
|
7458
|
+
// The wrapper stays the capture path; the hooks above work without it, so a missing launcher is not an error.
|
|
7459
|
+
if (detectRealCodexPath()) {
|
|
7460
|
+
const result = installCodexWrapper();
|
|
7461
|
+
console.log(`Installed Codex session-end integration -> ${result.metadataPath}`);
|
|
7462
|
+
console.log(` Wrapped detected Codex launcher at ${result.commandPath}`);
|
|
7463
|
+
}
|
|
7464
|
+
else {
|
|
7465
|
+
console.log('No codex launcher on PATH, so session-end capture was not set up; re-run once `codex` is on PATH.');
|
|
7466
|
+
}
|
|
7508
7467
|
}
|
|
7509
7468
|
return;
|
|
7510
7469
|
}
|
|
@@ -7548,6 +7507,9 @@ function cmdHook(args, flags) {
|
|
|
7548
7507
|
}
|
|
7549
7508
|
}
|
|
7550
7509
|
else if (target === 'codex') {
|
|
7510
|
+
if (uninstallJsonHooks('codex')) {
|
|
7511
|
+
console.log(`Removed hippo's Codex memory hooks from ${resolveJsonHookPaths('codex').settings}`);
|
|
7512
|
+
}
|
|
7551
7513
|
if (uninstallCodexWrapper()) {
|
|
7552
7514
|
console.log('Removed Codex wrapper integration');
|
|
7553
7515
|
}
|
|
@@ -7637,10 +7599,13 @@ function cmdSetup(flags) {
|
|
|
7637
7599
|
}
|
|
7638
7600
|
for (const tool of wrapperTools) {
|
|
7639
7601
|
if (dryRun) {
|
|
7602
|
+
if (tool.name === 'codex')
|
|
7603
|
+
console.log(`[dry-run] would install Codex memory hooks in ${resolveJsonHookPaths('codex').settings}`);
|
|
7640
7604
|
console.log(`[dry-run] would wrap the detected ${tool.name} launcher in place`);
|
|
7641
7605
|
continue;
|
|
7642
7606
|
}
|
|
7643
7607
|
if (tool.name === 'codex') {
|
|
7608
|
+
installCodexMemoryHooks(` ${tool.name.padEnd(14)} `);
|
|
7644
7609
|
const result = ensureCodexWrapperInstalled();
|
|
7645
7610
|
if (result.status === 'installed') {
|
|
7646
7611
|
console.log(` ${tool.name.padEnd(14)} wrapped launcher -> ${result.commandPath}`);
|
|
@@ -7700,11 +7665,22 @@ function cmdSetup(flags) {
|
|
|
7700
7665
|
setupDailySchedule(globalRoot);
|
|
7701
7666
|
}
|
|
7702
7667
|
}
|
|
7668
|
+
if (!flags['no-learn']) {
|
|
7669
|
+
console.log('');
|
|
7670
|
+
printAgentImport(importUserMemories(globalRoot, { machine: currentMachine(), dryRun }), dryRun ? '[dry-run] ' : '');
|
|
7671
|
+
}
|
|
7703
7672
|
console.log('');
|
|
7704
7673
|
console.log('Done. Restart your AI tool to activate the hooks.');
|
|
7705
7674
|
}
|
|
7706
7675
|
function cmdDailyRunner() {
|
|
7707
7676
|
const globalRoot = getGlobalRoot();
|
|
7677
|
+
// No workspace sleep ever opens the global store, yet hooks in folders without a store compact into it.
|
|
7678
|
+
if (isInitialized(globalRoot)) {
|
|
7679
|
+
const finished = replayCompactionsAt(globalRoot, (message) => console.error(`compaction replay: ${message}`));
|
|
7680
|
+
if (finished > 0)
|
|
7681
|
+
console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over in the global store.`);
|
|
7682
|
+
}
|
|
7683
|
+
printAgentImport(importUserMemories(globalRoot, { machine: currentMachine() }), '');
|
|
7708
7684
|
const workspaces = listRegisteredWorkspaces(globalRoot);
|
|
7709
7685
|
if (workspaces.length === 0) {
|
|
7710
7686
|
console.log('No registered Hippo workspaces found. Run `hippo init` inside a project first.');
|
|
@@ -7903,17 +7879,20 @@ function cmdAssemble(hippoRoot, sessionId, flags) {
|
|
|
7903
7879
|
...(Number.isFinite(freshTailCount) && freshTailCount >= 0 ? { freshTailCount } : {}),
|
|
7904
7880
|
summarizeOlder,
|
|
7905
7881
|
...(scope !== undefined ? { scope } : {}),
|
|
7882
|
+
cost: assembleCost(sessionId),
|
|
7906
7883
|
});
|
|
7907
7884
|
if (flags['json']) {
|
|
7908
7885
|
console.log(JSON.stringify(r, null, 2));
|
|
7909
7886
|
return;
|
|
7910
7887
|
}
|
|
7911
|
-
console.log(
|
|
7912
|
-
|
|
7913
|
-
const
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7888
|
+
console.log(settleTokens((t) => captureConsole(() => {
|
|
7889
|
+
console.log(assembleHeading({ ...r, items: r.items.length, tokens: t }));
|
|
7890
|
+
for (const it of r.items) {
|
|
7891
|
+
const prefix = it.isSummary ? '[summary]' : it.isFreshTail ? '[tail]' : '[older]';
|
|
7892
|
+
const head = it.content.slice(0, 120);
|
|
7893
|
+
console.log(` ${prefix} ${it.createdAt} ${it.id} \u2014 ${head}${it.content.length > 120 ? '…' : ''}`);
|
|
7894
|
+
}
|
|
7895
|
+
})));
|
|
7917
7896
|
}
|
|
7918
7897
|
function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
7919
7898
|
requireInit(hippoRoot);
|
|
@@ -7940,6 +7919,7 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
|
7940
7919
|
...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
|
|
7941
7920
|
...(Number.isFinite(budget) && budget > 0 ? { budget } : {}),
|
|
7942
7921
|
...(depth !== undefined ? { depth } : {}),
|
|
7922
|
+
cost: drillCost,
|
|
7943
7923
|
});
|
|
7944
7924
|
if ('failure' in r) {
|
|
7945
7925
|
// v1.6.4: only `not_drillable` is caller-actionable. `not_found`
|
|
@@ -8144,65 +8124,7 @@ function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
|
|
|
8144
8124
|
// ---------------------------------------------------------------------------
|
|
8145
8125
|
// Audit log subcommands (A5 stub auth — `hippo audit list`)
|
|
8146
8126
|
// ---------------------------------------------------------------------------
|
|
8147
|
-
const VALID_AUDIT_OPS = new Set(
|
|
8148
|
-
'remember',
|
|
8149
|
-
'recall',
|
|
8150
|
-
'promote',
|
|
8151
|
-
'supersede',
|
|
8152
|
-
'forget',
|
|
8153
|
-
'archive_raw',
|
|
8154
|
-
'auth_revoke',
|
|
8155
|
-
'auth_create', // v1.12.4: emitted by api.authCreate
|
|
8156
|
-
'outcome', // v1.11.5: pre-existing drift — emitted today but rejected by old Set
|
|
8157
|
-
'consolidate', // v1.11.5: emitted by api.sleep / hippo sleep
|
|
8158
|
-
'audit_prune', // v1.12.9: emitted by pruneAuditLog
|
|
8159
|
-
'summary_marked_dirty', // v0.30 / E1 — lockstep with AuditOp union + server.ts VALID_AUDIT_OPS (v1.11.5 CRIT A institutional rule)
|
|
8160
|
-
'summary_marked_clean', // v0.30 / E3 — buildDag post-link clean op; lockstep
|
|
8161
|
-
'summary_rebuilt', // v0.30 / E3 — sleep-cycle rebuild op; lockstep
|
|
8162
|
-
'predict_create', // v0.31 / E2 prediction first-class object — emitted by savePrediction
|
|
8163
|
-
'predict_close', // v0.31 / E2 — emitted by closePrediction
|
|
8164
|
-
'predict_baserate', // v0.31 / J3 — emitted by computePredictionBaserate
|
|
8165
|
-
'recall_autodebias_hint', // v0.32 / J3.2 — emitted by computePlanningFallacyHint on success
|
|
8166
|
-
'recall_autodebias_hint_no_class_match', // v0.32 / J3.2 — telemetry: forward-claim, no class scored
|
|
8167
|
-
'recall_autodebias_hint_tiebreak', // v0.32 / J3.2 — telemetry: forward-claim, >=2 classes tied
|
|
8168
|
-
'recall_anchor_detected_query_repeat', // v0.33 / J1 — emitted by detector on R1 fire
|
|
8169
|
-
'recall_anchor_detected_memory_dominance', // v0.33 / J1 — emitted by detector on R2 fire
|
|
8170
|
-
'recall_anchor_skipped_no_session', // v0.33 / J1 — telemetry: no sessionId, ring skipped
|
|
8171
|
-
'recall_availability_detected', // v1.13.x / J2 - emitted when availability/recency-bias hint fires
|
|
8172
|
-
'decision_create', // E2 decision first-class object — emitted by saveDecision
|
|
8173
|
-
'decision_supersede', // E2 — emitted by saveDecision when --supersedes resolves to an active decision row
|
|
8174
|
-
'decision_close', // E2 — emitted by closeDecision
|
|
8175
|
-
'incident_open', // E2 incident first-class object — emitted by saveIncident
|
|
8176
|
-
'incident_resolve', // E2 — emitted by resolveIncident (open -> resolved)
|
|
8177
|
-
'incident_close', // E2 — emitted by closeIncident (open|resolved -> closed)
|
|
8178
|
-
'process_create', // E2 process first-class object — emitted by saveProcess
|
|
8179
|
-
'process_supersede', // E2 — emitted by saveProcess on a supersession
|
|
8180
|
-
'process_close', // E2 — emitted by closeProcess
|
|
8181
|
-
'policy_create', // E2 policy first-class object — emitted by savePolicy
|
|
8182
|
-
'policy_supersede', // E2 — emitted by savePolicy on a supersession
|
|
8183
|
-
'policy_close', // E2 — emitted by closePolicy
|
|
8184
|
-
'skill_create', // E2 skill first-class object — emitted by saveSkill
|
|
8185
|
-
'skill_supersede', // E2 — emitted by saveSkill on a supersession
|
|
8186
|
-
'skill_close', // E2 — emitted by closeSkill
|
|
8187
|
-
'project_brief_create', // E2 project_brief first-class object — emitted by saveProjectBrief
|
|
8188
|
-
'project_brief_supersede', // E2 — emitted by saveProjectBrief on a supersession (incl. refresh)
|
|
8189
|
-
'project_brief_close', // E2 — emitted by closeProjectBrief
|
|
8190
|
-
'customer_note_create', // E2 customer_note first-class object — emitted by saveCustomerNote
|
|
8191
|
-
'customer_note_supersede', // E2 — emitted by saveCustomerNote on a supersession
|
|
8192
|
-
'customer_note_close', // E2 — emitted by closeCustomerNote
|
|
8193
|
-
'mv_rescue', // LC2-E3 — emitted by consolidate() per rescue; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8194
|
-
'reject_value', // AT1 — emitted by `hippo reject`; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8195
|
-
'reject_refusal', // AT1 — emitted when the rejection guard refuses a write; lockstep
|
|
8196
|
-
'unreject_value', // AT1 — emitted by `hippo unreject`; lockstep
|
|
8197
|
-
'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
|
|
8198
|
-
'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
|
|
8199
|
-
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8200
|
-
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8201
|
-
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8202
|
-
'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8203
|
-
'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
|
|
8204
|
-
'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
|
|
8205
|
-
]);
|
|
8127
|
+
const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
|
|
8206
8128
|
function formatAuditRow(ev) {
|
|
8207
8129
|
const target = ev.targetId ?? '-';
|
|
8208
8130
|
const meta = JSON.stringify(ev.metadata ?? {});
|
|
@@ -8728,7 +8650,7 @@ function cmdSlack(hippoRoot, args, flags) {
|
|
|
8728
8650
|
}
|
|
8729
8651
|
export function usageText() {
|
|
8730
8652
|
return `
|
|
8731
|
-
Hippo - memory for AI agents that learns what is wrong and
|
|
8653
|
+
Hippo - memory for AI agents that learns what is wrong and ranks it down
|
|
8732
8654
|
|
|
8733
8655
|
Usage: hippo <command> [options]
|
|
8734
8656
|
|
|
@@ -8738,8 +8660,10 @@ Commands:
|
|
|
8738
8660
|
--days <n> Days of git history to seed (default: 365 for --scan, 30 for single)
|
|
8739
8661
|
--global Init the global store ($HIPPO_HOME or ~/.hippo/)
|
|
8740
8662
|
--no-hooks Skip auto-detecting and installing agent hooks
|
|
8663
|
+
(HIPPO_SKIP_AUTO_INTEGRATIONS=1 does the same)
|
|
8741
8664
|
--no-schedule Skip auto-creating the machine-level daily runner
|
|
8742
|
-
--no-learn Skip seeding memories from git history
|
|
8665
|
+
--no-learn Skip seeding memories from git history and importing
|
|
8666
|
+
coding agents' own memories (every init imports those)
|
|
8743
8667
|
remember <text> Store a memory
|
|
8744
8668
|
--tag <tag> Add a tag (repeatable)
|
|
8745
8669
|
--error Tag as error (boosts retention)
|
|
@@ -8753,7 +8677,7 @@ Commands:
|
|
|
8753
8677
|
--tag <tag> Tag for the new memory (repeatable; default: the old memory's tags)
|
|
8754
8678
|
--pin Pin the new memory (default: pinned if the old one was)
|
|
8755
8679
|
recall <query> Search and retrieve memories (local + global)
|
|
8756
|
-
--budget <n> Token budget (default: 4000)
|
|
8680
|
+
--budget <n> Token budget for the whole printed block (default: 4000)
|
|
8757
8681
|
--min-results <n> Minimum results regardless of budget (default: 1)
|
|
8758
8682
|
--json Output as JSON
|
|
8759
8683
|
--why Show match reasons and source annotations
|
|
@@ -8834,7 +8758,7 @@ Commands:
|
|
|
8834
8758
|
handoff/events (use 'hippo session resume' for
|
|
8835
8759
|
the explicit handoff-without-snapshot path).
|
|
8836
8760
|
explain <query> Show full score breakdown for each retrieved memory
|
|
8837
|
-
--budget <n> Token budget (default: 4000)
|
|
8761
|
+
--budget <n> Token budget, counted as recall prints (default: 4000)
|
|
8838
8762
|
--limit <n> Cap the number of results displayed
|
|
8839
8763
|
--json Output as JSON
|
|
8840
8764
|
--physics | --classic Force search mode (default: from config)
|
|
@@ -8865,20 +8789,23 @@ Commands:
|
|
|
8865
8789
|
--json Output full summary as JSON
|
|
8866
8790
|
context Smart context injection for AI agents
|
|
8867
8791
|
--auto Auto-detect task from git state
|
|
8868
|
-
--budget <n> Token budget (default: 1500)
|
|
8792
|
+
--budget <n> Token budget for the whole printed block (default: 1500)
|
|
8869
8793
|
--pinned-only Only inject pinned memories (used by UserPromptSubmit hook)
|
|
8870
8794
|
--include-recent <n> With --pinned-only, also inject the last N writes regardless of pinning
|
|
8871
8795
|
(the hook payload's "prompt" drives prompt recall instead of --include-recent when pinnedInject.promptRecall is on)
|
|
8872
8796
|
--format <fmt> Output format: markdown (default), json, or additional-context (Claude Code hook JSON)
|
|
8873
8797
|
--framing <mode> Framing: observe (default), suggest, assert
|
|
8874
8798
|
sleep Run consolidation pass (auto-learns + dedup + auto-shares)
|
|
8799
|
+
Runs at Claude Code and OpenCode session end and in the daily job.
|
|
8800
|
+
With ANTHROPIC_API_KEY set it sends memory text to Anthropic for
|
|
8801
|
+
fact extraction; {"extraction":{"enabled":false}} turns that off
|
|
8875
8802
|
--dry-run Preview without writing
|
|
8876
|
-
--no-learn Skip auto git-learn before consolidation
|
|
8803
|
+
--no-learn Skip auto git-learn and the agent memory import before consolidation
|
|
8877
8804
|
--no-share Skip auto-sharing to global store
|
|
8878
8805
|
daily-runner Sweep registered workspaces and run daily learn+sleep
|
|
8879
8806
|
dedup Remove duplicate memories (keeps stronger copy)
|
|
8880
8807
|
--dry-run Preview without removing
|
|
8881
|
-
--threshold <n>
|
|
8808
|
+
--threshold <n> Ignored, kept for old scripts: a duplicate is the same text apart from spacing
|
|
8882
8809
|
status Show memory health stats
|
|
8883
8810
|
audit [--fix] Check memory quality (--fix removes junk)
|
|
8884
8811
|
github GitHub connector subcommands (backfill, dlq)
|
|
@@ -8902,10 +8829,10 @@ Commands:
|
|
|
8902
8829
|
--stats Count memories per DAG level instead
|
|
8903
8830
|
drill <summary-id> Walk down a DAG level-2 summary to its children
|
|
8904
8831
|
--limit N Cap children list (default 50)
|
|
8905
|
-
--budget N
|
|
8832
|
+
--budget N Token budget for the printed children (≈ chars/4)
|
|
8906
8833
|
--json Output as JSON
|
|
8907
8834
|
assemble --session <id> Build a session's chronological context window
|
|
8908
|
-
--budget N Token budget (default 4000)
|
|
8835
|
+
--budget N Token budget for the printed window (default 4000)
|
|
8909
8836
|
--fresh-tail N Recent rows always kept verbatim (default 10)
|
|
8910
8837
|
--no-summarize-older Disable older-row summary substitution
|
|
8911
8838
|
--scope <s> Restrict to exact scope (default: deny *:private:*)
|
|
@@ -8956,8 +8883,10 @@ Commands:
|
|
|
8956
8883
|
--out <file> Where to write it (default: hippo-support-<time>.json here)
|
|
8957
8884
|
--include-logs Add the last ${TAIL_MAX_LINES} lines of each hippo log, known secret shapes removed
|
|
8958
8885
|
tokens Tokens of memory text hippo handed agents, per surface
|
|
8959
|
-
(hook, context, recall, MCP, HTTP),
|
|
8960
|
-
unchanged hook blocks saved
|
|
8886
|
+
(hook, compact-resume, context, recall, MCP, HTTP), what
|
|
8887
|
+
skipping unchanged hook blocks saved, and how much of the
|
|
8888
|
+
hook and compact-resume blocks later model calls re-read,
|
|
8889
|
+
counted when a session ends. Estimates (characters / 4)
|
|
8961
8890
|
--days <n> Window in days (default: 30)
|
|
8962
8891
|
--json Output as JSON
|
|
8963
8892
|
--global Operate on the global store
|
|
@@ -9069,6 +8998,11 @@ Commands:
|
|
|
9069
8998
|
--vault <path> Import a markdown-vault FOLDER as kind='raw' notes
|
|
9070
8999
|
(Obsidian/Foam/Dendron). Requires --name <vault>.
|
|
9071
9000
|
[--scope <scope>]
|
|
9001
|
+
--agents Sync every coding agent's own memories (Claude Code, Codex,
|
|
9002
|
+
Gemini CLI, Copilot, OpenClaw, Qwen Code) now; with
|
|
9003
|
+
--dry-run, print each tool's folders and what would change.
|
|
9004
|
+
HIPPO_AGENT_MEMORY_TOOLS=<ids> or config agentMemories.tools
|
|
9005
|
+
picks the tools; "none" or [] turns the import off
|
|
9072
9006
|
--dry-run Preview without writing
|
|
9073
9007
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
9074
9008
|
--tag <tag> Add extra tag (repeatable)
|
|
@@ -9077,34 +9011,44 @@ Commands:
|
|
|
9077
9011
|
capture Extract memories from conversation text
|
|
9078
9012
|
--stdin Read from piped input
|
|
9079
9013
|
--file <path> Read from a file
|
|
9080
|
-
--last-session Read
|
|
9014
|
+
--last-session Read the transcript a hook names on stdin, else the newest
|
|
9015
|
+
Claude Code one from any project
|
|
9081
9016
|
--transcript <path> Explicit transcript path (implies --last-session)
|
|
9082
9017
|
--log-file <path> Tee output to a log file (paired with 'hippo last-sleep')
|
|
9083
9018
|
--dry-run Preview without writing
|
|
9084
9019
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
9085
|
-
setup One-shot: detect installed AI tools and install
|
|
9086
|
-
|
|
9020
|
+
setup One-shot: detect installed AI tools and install their hooks:
|
|
9021
|
+
claude-code gets 7 hooks in ~/.claude/settings.json, opencode
|
|
9022
|
+
a plugin, codex 2 hooks in its hooks.json plus a launcher
|
|
9023
|
+
wrapper; other tools get a hint. Then imports each agent's
|
|
9024
|
+
user-level memories into the global store
|
|
9087
9025
|
--all Install for every JSON-hook tool, even if not detected
|
|
9088
9026
|
--dry-run Show what would be installed without writing
|
|
9089
9027
|
--no-schedule Skip installing or repairing the daily runner
|
|
9028
|
+
--no-learn Skip the agent memory import
|
|
9090
9029
|
last-sleep Print the last 'hippo sleep --log-file' output to stderr and clear it
|
|
9091
9030
|
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
9092
9031
|
--keep Print without clearing
|
|
9093
|
-
session-end SessionEnd hook:
|
|
9032
|
+
session-end SessionEnd hook: count this session's re-read tokens, run sleep, then
|
|
9033
|
+
capture from the session's last 20 user and 10 assistant messages,
|
|
9034
|
+
in a detached worker
|
|
9094
9035
|
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
|
|
9095
|
-
pre-compact PreCompact hook: save a working-state snapshot
|
|
9036
|
+
pre-compact PreCompact hook: record the compaction, save a working-state snapshot, and
|
|
9037
|
+
ask the summariser to end with a "Memories for hippo" list
|
|
9096
9038
|
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
9097
|
-
compact-resume SessionStart(compact) hook: re-print the snapshot
|
|
9098
|
-
post-compact PostCompact hook:
|
|
9039
|
+
compact-resume SessionStart(compact) hook: re-print the snapshot, if under 15 minutes old
|
|
9040
|
+
post-compact PostCompact hook: keep that list as memories (a busy store leaves the save to
|
|
9041
|
+
the next hippo sleep) and print one line saying how many
|
|
9099
9042
|
--log-file <p> Same log path as pre-compact (default: ~/.hippo/logs/pre-compact.log)
|
|
9100
9043
|
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
9101
9044
|
hook <sub> [target] Manage framework integrations
|
|
9102
9045
|
hook list Show available hooks
|
|
9103
9046
|
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
9104
|
-
claude-code
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9047
|
+
claude-code adds 7 hooks to ~/.claude/settings.json;
|
|
9048
|
+
opencode installs a plugin; codex adds 2 hooks to
|
|
9049
|
+
$CODEX_HOME/hooks.json (trust them once in /hooks) and
|
|
9050
|
+
wraps the detected launcher in place; all but claude-code
|
|
9051
|
+
also patch an existing AGENTS.md
|
|
9108
9052
|
hook uninstall <target> Remove hook
|
|
9109
9053
|
predict "<claim>" Record a prediction to score against the actual outcome later
|
|
9110
9054
|
--class <c> Reference class (required)
|
|
@@ -9225,7 +9169,7 @@ Commands:
|
|
|
9225
9169
|
wm clear Clear working memory entries
|
|
9226
9170
|
--scope <scope> Filter by scope
|
|
9227
9171
|
--session <id> Filter by session
|
|
9228
|
-
wm flush
|
|
9172
|
+
wm flush Same as clear; nothing runs it at session end
|
|
9229
9173
|
--scope <scope> Filter by scope
|
|
9230
9174
|
--session <id> Filter by session
|
|
9231
9175
|
dashboard Open web dashboard for memory health
|
|
@@ -9467,10 +9411,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9467
9411
|
const rememberKindRaw = typeof flags['kind'] === 'string' ? flags['kind'].toLowerCase() : undefined;
|
|
9468
9412
|
const rememberKindAllowed = ['distilled', 'superseded'];
|
|
9469
9413
|
if (rememberKindRaw === undefined || rememberKindAllowed.includes(rememberKindRaw)) {
|
|
9470
|
-
const
|
|
9471
|
-
const tags = Array.isArray(tagsRaw)
|
|
9472
|
-
? tagsRaw.map(String)
|
|
9473
|
-
: typeof tagsRaw === 'string' ? [tagsRaw] : undefined;
|
|
9414
|
+
const tags = rememberTags(flags, process.cwd()).all;
|
|
9474
9415
|
// B2 v1.12.6 — validate --owner on the thin-client path too.
|
|
9475
9416
|
// Failure on this path exits early so the user gets the same
|
|
9476
9417
|
// validation experience whether or not a server is up.
|
|
@@ -9507,7 +9448,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9507
9448
|
console.error('Please provide a search query.');
|
|
9508
9449
|
process.exit(1);
|
|
9509
9450
|
}
|
|
9510
|
-
await cmdRecall(hippoRoot, query, flags);
|
|
9451
|
+
await cmdRecall(hookStoreRoot(hippoRoot), query, flags);
|
|
9511
9452
|
break;
|
|
9512
9453
|
}
|
|
9513
9454
|
case 'drill': {
|
|
@@ -9592,13 +9533,25 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9592
9533
|
break;
|
|
9593
9534
|
}
|
|
9594
9535
|
case 'post-compact': {
|
|
9595
|
-
// PostCompact hook:
|
|
9596
|
-
// because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
9536
|
+
// PostCompact hook: saves the compaction summary and its memories, then prints one plain line, because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
9597
9537
|
const { text } = await readStdinBounded();
|
|
9598
9538
|
const logFlag = flags['log-file'];
|
|
9599
|
-
const
|
|
9600
|
-
|
|
9601
|
-
|
|
9539
|
+
const store = hookStoreRoot(hippoRoot);
|
|
9540
|
+
const line = cmdPostCompact(store, {
|
|
9541
|
+
stdinText: text,
|
|
9542
|
+
logFile: logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag,
|
|
9543
|
+
// Passed in, since capture.ts importing the sync would close an import cycle.
|
|
9544
|
+
afterSave: (transcriptPath, originProject, log) => {
|
|
9545
|
+
const report = importAtCompaction(store, transcriptPath, originProject, { machine: currentMachine(), busyWaitMs: COMPACTION_DB_WAIT_MS });
|
|
9546
|
+
const summary = summaryLine(report);
|
|
9547
|
+
if (summary !== null)
|
|
9548
|
+
log(summary);
|
|
9549
|
+
for (const warning of report.warnings)
|
|
9550
|
+
log(`agent memories: ${warning}`);
|
|
9551
|
+
},
|
|
9552
|
+
});
|
|
9553
|
+
if (line !== null)
|
|
9554
|
+
console.log(line);
|
|
9602
9555
|
break;
|
|
9603
9556
|
}
|
|
9604
9557
|
case 'capture-error': {
|
|
@@ -9896,7 +9849,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9896
9849
|
// Bounded, not a TTY guard (DF1 T2, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
9897
9850
|
// the hot stdin path and a manual run share this one command.
|
|
9898
9851
|
const { text: stdinText } = await readStdinBounded();
|
|
9899
|
-
await cmdContext(hippoRoot, args, flags, stdinText);
|
|
9852
|
+
await cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText);
|
|
9900
9853
|
break;
|
|
9901
9854
|
}
|
|
9902
9855
|
case 'hook':
|