hippo-memory 1.52.7 → 1.52.9
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 +159 -99
- package/dist/api.d.ts +52 -18
- package/dist/api.js +155 -86
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/autolearn.js +3 -2
- package/dist/capture.d.ts +40 -0
- package/dist/capture.js +141 -119
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +695 -702
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +85 -32
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +4 -0
- package/dist/db.js +6 -6
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +35 -2
- package/dist/dormant.d.ts +4 -0
- package/dist/dormant.js +17 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- 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/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +13 -10
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +19 -5
- package/dist/hooks.js +131 -30
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +174 -213
- 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 +9 -3
- 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 +11 -3
- package/dist/shared.js +41 -31
- package/dist/store.d.ts +4 -5
- package/dist/store.js +26 -13
- 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/dist-ui/assets/index-BhT8RvO6.js +61 -0
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/index.ts +1 -0
- 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-ui/assets/index-BgmA7Hwe.js +0 -61
package/dist/cli.js
CHANGED
|
@@ -37,24 +37,25 @@ 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 } from 'child_process';
|
|
41
|
-
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
40
|
+
import { execFileSync, execSync, spawn, spawnSync } 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
42
|
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
|
|
43
43
|
import { detectSecret } from './secret-detect.js';
|
|
44
44
|
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
45
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
46
46
|
import { RejectedValueError } from './rejection.js';
|
|
47
47
|
import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
|
|
48
|
+
import { readSessionScan, recordSessionDigest } from './session-digest.js';
|
|
48
49
|
import { isCardStatus } from './card.js';
|
|
49
50
|
import { loadCardDetail } from './card-detail.js';
|
|
50
51
|
import { passesScopeFilterForRecall } from './recall-scope.js';
|
|
51
|
-
import { estimateTokens, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
52
|
+
import { estimateTokens, fitBudget, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
52
53
|
import { compareEntryIdentity } from './compare.js';
|
|
53
54
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
54
55
|
import { writeRecallTraceAtRoot } from './recall-trace.js';
|
|
55
56
|
import { deduplicateStore } from './dedupe.js';
|
|
56
57
|
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, resolveEmbeddingModel, embeddingModelRequiresReindex, } from './embeddings.js';
|
|
57
|
-
import {
|
|
58
|
+
import { resolveEmbeddingProvider } from './embedding-provider.js';
|
|
58
59
|
import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
59
60
|
import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
60
61
|
import { loadConfig } from './config.js';
|
|
@@ -63,21 +64,22 @@ import { runDoctor, formatDoctor } from './doctor.js';
|
|
|
63
64
|
import { buildSupportBundle, TAIL_MAX_LINES } from './support-bundle.js';
|
|
64
65
|
import { PACKAGE_VERSION } from './version.js';
|
|
65
66
|
import { captureToolFailure } from './capture-error.js';
|
|
66
|
-
import { blockHash, hookPayloadSessionId, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
67
|
+
import { blockHash, hookPayloadSessionId, isSubagentPayload, lastSentState, readApiCalls, recordRereads, recordTokenUse, shouldSkipUnchanged, } from './token-ledger.js';
|
|
67
68
|
import { FAILURE_LOG_RETENTION_DAYS } from './failure-log.js';
|
|
68
69
|
import { pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal, applyGoalStackBoost } from './goals.js';
|
|
69
70
|
import { rowToGoal } from './goals.js';
|
|
70
|
-
import { captureError, extractLessons, partitionLessons,
|
|
71
|
+
import { captureError, extractLessons, partitionLessons, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
|
|
72
|
+
import { dropHeldCopies, duplicateKey, storedTextKeys } from './same-text.js';
|
|
71
73
|
import { extractInvalidationTarget, invalidateMatching, detectChurnStale } from './invalidation.js';
|
|
72
|
-
import { resolveProjectIdentity } from './project-identity.js';
|
|
74
|
+
import { deriveOriginProject, realpathOrResolve, resolveProjectIdentity } from './project-identity.js';
|
|
73
75
|
import { extractPathTags } from './path-context.js';
|
|
74
76
|
import { detectScope } from './scope.js';
|
|
75
77
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
76
|
-
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
78
|
+
import { DAILY_TASK_NAME, buildDailyRunnerCommand, buildSchtasksCreateArgs, buildWindowsTaskRun, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
77
79
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
|
|
78
|
-
import { cmdCapture, cmdPreCompact, postCompactMessage, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage } from './capture.js';
|
|
80
|
+
import { cmdCapture, cmdPreCompact, postCompactMessage, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage, transcriptWorkingState } from './capture.js';
|
|
79
81
|
import { readStdinBounded } from './stdin.js';
|
|
80
|
-
import { auditMemories, appendAuditEvent, } from './audit.js';
|
|
82
|
+
import { auditMemories, appendAuditEvent, AUDIT_OPS, } from './audit.js';
|
|
81
83
|
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
82
84
|
import { buildProvenanceCoverage } from './provenance-coverage.js';
|
|
83
85
|
import { buildCorrectionLatency } from './correction-latency.js';
|
|
@@ -132,6 +134,7 @@ import { getReranker } from './rerankers/index.js';
|
|
|
132
134
|
import { JEV_DEFAULT_TOP_K } from './rerankers/jev.js';
|
|
133
135
|
import { computeSalience } from './salience.js';
|
|
134
136
|
import { renderAmbientSummary } from './ambient.js';
|
|
137
|
+
import { assembleCost, assembleHeading, contextCost, contextHeading, contextLine, crossProjectHeading, crossProjectLine, drillCost, handoffText, printedTokens, sessionTrailText, settleTokens, snapshotText, } from './context-render.js';
|
|
135
138
|
import { validateOwner, isStrictOwnerEnv } from './owner-validation.js';
|
|
136
139
|
import { pruneAuditLog, parseOlderThanFlag } from './audit-prune.js';
|
|
137
140
|
import { listDlq, replayDlqEntry } from './connectors/slack/dlq.js';
|
|
@@ -203,7 +206,7 @@ function requireInit(hippoRoot) {
|
|
|
203
206
|
}
|
|
204
207
|
/** FE2: run detectChurnStale against every store this repo's memories can live in. */
|
|
205
208
|
function runChurnStaleForRepo(hippoRoot, dryRun) {
|
|
206
|
-
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8' }).trim();
|
|
209
|
+
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8', windowsHide: true }).trim();
|
|
207
210
|
const projectName = resolveProjectIdentity(process.cwd()).name;
|
|
208
211
|
const globalRoot = getGlobalRoot();
|
|
209
212
|
const roots = globalRoot !== hippoRoot && isInitialized(globalRoot) ? [hippoRoot, globalRoot] : [hippoRoot];
|
|
@@ -407,6 +410,71 @@ export function parseArgs(argv) {
|
|
|
407
410
|
function fmt(n, digits = 2) {
|
|
408
411
|
return n.toFixed(digits);
|
|
409
412
|
}
|
|
413
|
+
// What `hippo recall` prints for one result; the budget prices this same text.
|
|
414
|
+
function recallEntryText(r, query, showWhy, isGlobal) {
|
|
415
|
+
const e = r.entry;
|
|
416
|
+
const label = confidenceLabel(e);
|
|
417
|
+
const confLabel = label.warn ? `[${label.text}] ⚠️` : `[${label.text}]`;
|
|
418
|
+
const bars = Math.round(e.strength * 10);
|
|
419
|
+
const graphMark = r.graphVia ? ` [graph: ${r.graphVia.hops}hop ${r.graphVia.relType}]` : '';
|
|
420
|
+
const lines = [
|
|
421
|
+
`--- ${e.id} [${e.layer}] ${confLabel}${isGlobal ? ' [global]' : ''}${e.superseded_by ? ' [superseded]' : ''}${graphMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`,
|
|
422
|
+
` [${'█'.repeat(bars)}${'░'.repeat(10 - bars)}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`,
|
|
423
|
+
];
|
|
424
|
+
if (showWhy) {
|
|
425
|
+
const explanation = explainMatch(query, r);
|
|
426
|
+
lines.push(` source:${isGlobal ? ' [global]' : ' [local]'} | layer: [${e.layer}] | confidence: [${label.text}]`, ` reason: ${explanation.reason}`);
|
|
427
|
+
const env = explanation.envelope;
|
|
428
|
+
if (env) {
|
|
429
|
+
lines.push(` kind: ${env.kind}`);
|
|
430
|
+
if (env.scope)
|
|
431
|
+
lines.push(` scope: ${env.scope}`);
|
|
432
|
+
if (env.owner)
|
|
433
|
+
lines.push(` owner: ${env.owner}`);
|
|
434
|
+
if (env.artifact_ref)
|
|
435
|
+
lines.push(` artifact_ref: ${env.artifact_ref}`);
|
|
436
|
+
if (env.session_id)
|
|
437
|
+
lines.push(` session_id: ${env.session_id}`);
|
|
438
|
+
lines.push(` confidence: ${env.confidence}`);
|
|
439
|
+
}
|
|
440
|
+
// A7 recall-trace, e.g. "ranking: base 0.420 -> interference x0.30 -> 0.126 -> goal-boost x1.50 -> 0.189".
|
|
441
|
+
if (r.rerankTrace && r.rerankTrace.length > 0) {
|
|
442
|
+
const parts = [`base ${fmt(r.rerankTrace[0].scoreBefore, 3)}`];
|
|
443
|
+
for (const step of r.rerankTrace) {
|
|
444
|
+
parts.push(`${step.stage}${step.multiplier !== undefined ? ` x${fmt(step.multiplier, 2)}` : ''}`, fmt(step.scoreAfter, 3));
|
|
445
|
+
}
|
|
446
|
+
lines.push(` ranking: ${parts.join(' -> ')}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
lines.push('', e.content, '');
|
|
450
|
+
return lines.join('\n');
|
|
451
|
+
}
|
|
452
|
+
function recallHeading(entries, tokens, query) {
|
|
453
|
+
return `Found ${entries} memories (${tokens} tokens) for: "${query}"\n`;
|
|
454
|
+
}
|
|
455
|
+
// JSON.stringify keeps quotes or parens in the matched phrase from blurring the line.
|
|
456
|
+
function planningLine(p) {
|
|
457
|
+
if (p.hint)
|
|
458
|
+
return `Planning fallacy hint (class: ${p.hint.classTag}): ${p.hint.baserateSummary} [detected: ${JSON.stringify(p.hint.detectedPhrase)}]`;
|
|
459
|
+
if (p.watching)
|
|
460
|
+
return `Planning fallacy: watching this query (reason: ${p.watching.reason}). ${p.watching.suggestion} [detected: ${JSON.stringify(p.watching.detectedPhrase)}]`;
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
function cutoffLine(shown, s) {
|
|
464
|
+
const clauses = [];
|
|
465
|
+
// The residual covers rank, budget and limit drops alike, and fires with no --limit at all.
|
|
466
|
+
if (s.droppedByBudget > 0)
|
|
467
|
+
clauses.push(`${s.droppedByBudget} not shown (rank, budget or limit)`);
|
|
468
|
+
if (s.droppedPreRank > 0)
|
|
469
|
+
clauses.push(`${s.droppedPreRank} filtered pre-rank`);
|
|
470
|
+
if (s.summarySubstitutionsAdded > 0)
|
|
471
|
+
clauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
|
|
472
|
+
if (s.freshTailAdded > 0)
|
|
473
|
+
clauses.push(`${s.freshTailAdded} fresh-tail added`);
|
|
474
|
+
if (s.suppressedByInterference > 0)
|
|
475
|
+
clauses.push(`${s.suppressedByInterference} suppressed by interference`);
|
|
476
|
+
return clauses.length > 0 ? `Cutoff: showing ${shown} of ${s.totalCandidates} candidates; ${clauses.join('; ')}.` : null;
|
|
477
|
+
}
|
|
410
478
|
// ---------------------------------------------------------------------------
|
|
411
479
|
// Commands
|
|
412
480
|
// ---------------------------------------------------------------------------
|
|
@@ -484,7 +552,7 @@ function cmdInitScan(scanDir, flags) {
|
|
|
484
552
|
(totalLowInfo > 0 ? `, ${totalLowInfo} low-information subject(s) dropped` : '') +
|
|
485
553
|
'.');
|
|
486
554
|
console.log(`Global store: ${globalRoot}`);
|
|
487
|
-
if (
|
|
555
|
+
if (initInstallsIntegrations(flags)) {
|
|
488
556
|
// User-level hooks only: a hippo block in each repo's CLAUDE.md or AGENTS.md would leave a diff in every repo.
|
|
489
557
|
const agents = detectAgentHooks(repos);
|
|
490
558
|
if (agents.length === 0) {
|
|
@@ -526,8 +594,7 @@ function cmdInit(hippoRoot, flags) {
|
|
|
526
594
|
}
|
|
527
595
|
const globalRoot = getGlobalRoot();
|
|
528
596
|
registerWorkspace(globalRoot, path.dirname(hippoRoot));
|
|
529
|
-
|
|
530
|
-
if (!flags['no-hooks']) {
|
|
597
|
+
if (initInstallsIntegrations(flags)) {
|
|
531
598
|
autoInstallHooks();
|
|
532
599
|
}
|
|
533
600
|
// Auto-setup daily schedule (unless --no-schedule)
|
|
@@ -547,13 +614,21 @@ function cmdInit(hippoRoot, flags) {
|
|
|
547
614
|
console.log(` No matching commits found in git history.`);
|
|
548
615
|
}
|
|
549
616
|
}
|
|
550
|
-
// Also import from Claude Code / agent MEMORY.md files
|
|
551
617
|
const memImported = learnFromMemoryMd(hippoRoot);
|
|
552
618
|
if (memImported > 0) {
|
|
553
|
-
console.log(` Imported ${memImported} memories from
|
|
619
|
+
console.log(` Imported ${memImported} memories from this project's Claude Code auto memory.`);
|
|
554
620
|
}
|
|
555
621
|
}
|
|
556
622
|
}
|
|
623
|
+
/** Every write init makes into agent config (instruction blocks, hooks, plugins) is an automatic integration, so one switch skips them all. */
|
|
624
|
+
function initInstallsIntegrations(flags) {
|
|
625
|
+
if (flags['no-hooks'])
|
|
626
|
+
return false;
|
|
627
|
+
if (process.env.HIPPO_SKIP_AUTO_INTEGRATIONS !== '1')
|
|
628
|
+
return true;
|
|
629
|
+
console.log(' HIPPO_SKIP_AUTO_INTEGRATIONS=1, so init left agent instruction files and hooks alone.');
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
557
632
|
/** Plain init: patch the detected agents' instruction files in cwd, then install their user-level hooks. */
|
|
558
633
|
function autoInstallHooks() {
|
|
559
634
|
const cwd = process.cwd();
|
|
@@ -629,13 +704,32 @@ function refreshShippedBlock(filePath, text, hook) {
|
|
|
629
704
|
fs.writeFileSync(filePath, `${text.slice(0, start)}${eol}${HOOKS[owner].content.replace(/\n/g, eol)}${eol}${text.slice(end)}`, 'utf8');
|
|
630
705
|
console.log(` Refreshed the ${owner} hippo block in ${name}`);
|
|
631
706
|
}
|
|
632
|
-
/**
|
|
707
|
+
/** Adds hippo's two Codex hooks and says what changed; each install ends on the trust reminder, since Codex skips an untrusted hook. */
|
|
708
|
+
function installCodexMemoryHooks(indent) {
|
|
709
|
+
const result = installJsonHooks('codex');
|
|
710
|
+
if (result.invalidJson) {
|
|
711
|
+
console.log(`${indent}WARNING: ${result.settingsPath} is not a hooks file hippo can merge into; fix it, then run \`hippo hook install codex\`.`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const added = [
|
|
715
|
+
result.installedUserPromptSubmit ? 'UserPromptSubmit' : '',
|
|
716
|
+
result.installedCompactResume ? 'SessionStart(compact)' : '',
|
|
717
|
+
].filter(Boolean);
|
|
718
|
+
console.log(added.length > 0
|
|
719
|
+
? `${indent}Installed hippo's Codex memory hooks (${added.join(', ')}) in ${result.settingsPath}`
|
|
720
|
+
: `${indent}hippo's Codex memory hooks already in ${result.settingsPath}`);
|
|
721
|
+
console.log(`${indent}${CODEX_TRUST_LINE}`);
|
|
722
|
+
}
|
|
723
|
+
/** Claude Code settings hooks, Codex's hooks.json and the OpenCode plugin, under the home directory; idempotent, so re-running init adds newer hooks. */
|
|
633
724
|
function installUserLevelHooks(agents, codexHint) {
|
|
634
725
|
for (const hook of agents) {
|
|
635
726
|
// The Codex capture wrapper swaps the codex launcher binary, so init only points at the opt-in (issue #133).
|
|
636
727
|
if (hook === 'codex' && codexHint && !isCodexWrapperInstalled()) {
|
|
637
728
|
console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
|
|
638
729
|
}
|
|
730
|
+
// Checked first so init never creates ~/.codex on a machine without Codex.
|
|
731
|
+
if (hook === 'codex' && isCodexPresent())
|
|
732
|
+
installCodexMemoryHooks(' ');
|
|
639
733
|
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
640
734
|
// settings.json. Keeps `hippo init` in lockstep with `hippo hook install
|
|
641
735
|
// claude-code` and `hippo setup`.
|
|
@@ -711,7 +805,7 @@ function setupDailySchedule(globalRoot) {
|
|
|
711
805
|
if (isWindows) {
|
|
712
806
|
// Check if task already exists
|
|
713
807
|
try {
|
|
714
|
-
const existing = execSync(`schtasks /query /tn "${taskName}" 2>nul`, { encoding: 'utf-8' });
|
|
808
|
+
const existing = execSync(`schtasks /query /tn "${taskName}" 2>nul`, { encoding: 'utf-8', windowsHide: true });
|
|
715
809
|
if (existing.includes(taskName)) {
|
|
716
810
|
return; // already scheduled
|
|
717
811
|
}
|
|
@@ -720,26 +814,26 @@ function setupDailySchedule(globalRoot) {
|
|
|
720
814
|
// Task doesn't exist, create it
|
|
721
815
|
}
|
|
722
816
|
try {
|
|
723
|
-
|
|
817
|
+
execFileSync('schtasks', buildSchtasksCreateArgs(taskName, cmd), { stdio: 'pipe', windowsHide: true });
|
|
724
818
|
console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
|
|
725
819
|
}
|
|
726
820
|
catch {
|
|
727
821
|
// No admin rights or schtasks unavailable, fall back to printing instructions
|
|
728
822
|
console.log(` To schedule the machine-level daily runner, run:`);
|
|
729
|
-
console.log(` schtasks /create /tn "${taskName}" /tr "cmd
|
|
823
|
+
console.log(` schtasks /create /tn "${taskName}" /tr "${buildWindowsTaskRun(cmd).replace(/"/g, '\\"')}" /sc daily /st 06:15`);
|
|
730
824
|
}
|
|
731
825
|
}
|
|
732
826
|
else {
|
|
733
827
|
// Unix: check crontab for existing entry
|
|
734
828
|
const marker = `# hippo:${taskName}`;
|
|
735
829
|
try {
|
|
736
|
-
const existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
830
|
+
const existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8', windowsHide: true });
|
|
737
831
|
if (existing.includes(marker)) {
|
|
738
832
|
return; // already scheduled
|
|
739
833
|
}
|
|
740
834
|
const cronLine = `15 6 * * * ${cmd} ${marker}`;
|
|
741
835
|
const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
|
|
742
|
-
execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
836
|
+
execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
743
837
|
console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
|
|
744
838
|
}
|
|
745
839
|
catch {
|
|
@@ -749,6 +843,25 @@ function setupDailySchedule(globalRoot) {
|
|
|
749
843
|
}
|
|
750
844
|
}
|
|
751
845
|
}
|
|
846
|
+
// Shared by the direct write and the routed request so both store the same tags.
|
|
847
|
+
function rememberTags(flags, cwd) {
|
|
848
|
+
const requested = Array.isArray(flags['tag']) ? [...flags['tag']] : [];
|
|
849
|
+
if (flags['error'])
|
|
850
|
+
requested.push('error');
|
|
851
|
+
const all = [...requested];
|
|
852
|
+
for (const pt of extractPathTags(cwd)) {
|
|
853
|
+
if (!all.includes(pt))
|
|
854
|
+
all.push(pt);
|
|
855
|
+
}
|
|
856
|
+
const explicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
857
|
+
const activeScope = explicitScope || detectScope();
|
|
858
|
+
if (activeScope) {
|
|
859
|
+
const scopeTag = `scope:${activeScope}`;
|
|
860
|
+
if (!all.includes(scopeTag))
|
|
861
|
+
all.push(scopeTag);
|
|
862
|
+
}
|
|
863
|
+
return { requested, all };
|
|
864
|
+
}
|
|
752
865
|
async function cmdRemember(hippoRoot, text, flags) {
|
|
753
866
|
const useGlobal = Boolean(flags['global']);
|
|
754
867
|
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
@@ -758,9 +871,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
758
871
|
else {
|
|
759
872
|
requireInit(hippoRoot);
|
|
760
873
|
}
|
|
761
|
-
const
|
|
762
|
-
if (flags['error'])
|
|
763
|
-
rawTags.push('error');
|
|
874
|
+
const { requested: requestedTags, all: allTags } = rememberTags(flags, process.cwd());
|
|
764
875
|
// Resolve explicit confidence flag (default: 'verified' for manual remember)
|
|
765
876
|
let confidence = 'verified';
|
|
766
877
|
if (flags['observed'])
|
|
@@ -769,9 +880,9 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
769
880
|
confidence = 'inferred';
|
|
770
881
|
if (flags['verified'])
|
|
771
882
|
confidence = 'verified';
|
|
772
|
-
//
|
|
883
|
+
// Schema fit needs the store, which the routed request has no access to, so it stays here.
|
|
773
884
|
const existing = loadAllEntries(targetRoot, resolveTenantId({}));
|
|
774
|
-
const schemaFit = computeSchemaFit(text,
|
|
885
|
+
const schemaFit = computeSchemaFit(text, requestedTags, existing);
|
|
775
886
|
// A3 envelope flags
|
|
776
887
|
const kindFlagRaw = typeof flags['kind'] === 'string' ? flags['kind'] : undefined;
|
|
777
888
|
const kindFlag = kindFlagRaw === undefined ? undefined : kindFlagRaw.toLowerCase();
|
|
@@ -803,7 +914,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
803
914
|
const rememberConfig = loadConfig(targetRoot);
|
|
804
915
|
const entry = createMemory(text, {
|
|
805
916
|
layer: Layer.Episodic,
|
|
806
|
-
tags:
|
|
917
|
+
tags: allTags,
|
|
807
918
|
pinned: Boolean(flags['pin']),
|
|
808
919
|
source: useGlobal ? 'cli-global' : 'cli',
|
|
809
920
|
confidence,
|
|
@@ -815,20 +926,6 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
815
926
|
tenantId,
|
|
816
927
|
baseHalfLifeDays: rememberConfig.defaultHalfLifeDays,
|
|
817
928
|
});
|
|
818
|
-
// Auto-tag with path context
|
|
819
|
-
const pathTags = extractPathTags(process.cwd());
|
|
820
|
-
for (const pt of pathTags) {
|
|
821
|
-
if (!entry.tags.includes(pt))
|
|
822
|
-
entry.tags.push(pt);
|
|
823
|
-
}
|
|
824
|
-
// Scope tagging: explicit --scope or auto-detected
|
|
825
|
-
const explicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
826
|
-
const activeScope = explicitScope || detectScope();
|
|
827
|
-
if (activeScope) {
|
|
828
|
-
const scopeTag = `scope:${activeScope}`;
|
|
829
|
-
if (!entry.tags.includes(scopeTag))
|
|
830
|
-
entry.tags.push(scopeTag);
|
|
831
|
-
}
|
|
832
929
|
// Salience gate: decide if this memory is worth storing
|
|
833
930
|
if (rememberConfig.salience.enabled && !Boolean(flags['pin']) && !Boolean(flags['force'])) {
|
|
834
931
|
const salienceResult = computeSalience(text, entry.tags, existing, {
|
|
@@ -856,12 +953,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
856
953
|
console.log(` Tags: ${entry.tags.join(', ')}`);
|
|
857
954
|
if (entry.pinned)
|
|
858
955
|
console.log(' Pinned (no decay)');
|
|
859
|
-
|
|
860
|
-
if (isEmbeddingConfigured(targetRoot)) {
|
|
861
|
-
embedMemory(targetRoot, entry).catch(() => {
|
|
862
|
-
// Silently ignore embedding errors
|
|
863
|
-
});
|
|
864
|
-
}
|
|
956
|
+
void embedMemory(targetRoot, entry);
|
|
865
957
|
const config = loadConfig(targetRoot);
|
|
866
958
|
const shouldExtract = flags['extract'] || config.extraction.enabled === true;
|
|
867
959
|
const apiKey = process.env.ANTHROPIC_API_KEY ?? '';
|
|
@@ -1097,6 +1189,12 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1097
1189
|
}
|
|
1098
1190
|
graphStreamSeeds = s;
|
|
1099
1191
|
}
|
|
1192
|
+
// Engines spend the budget on the text each result prints as, less the header, so selection and print agree.
|
|
1193
|
+
const localIndex = loadIndex(hippoRoot);
|
|
1194
|
+
const globalOn = isInitialized(globalRoot);
|
|
1195
|
+
const entryText = (r) => recallEntryText(r, query, showWhy, globalOn && !localIndex.entries[r.entry.id]);
|
|
1196
|
+
const printCost = (r) => printedTokens(entryText(r));
|
|
1197
|
+
const entryBudget = Math.max(0, budget - printedTokens(recallHeading(budget, budget, query)));
|
|
1100
1198
|
let results;
|
|
1101
1199
|
if (useGraphStream) {
|
|
1102
1200
|
if (!isEmbeddingAvailable()) {
|
|
@@ -1112,16 +1210,18 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1112
1210
|
// tail; on a pool with <= seedCount candidates EVERY candidate is a seed and the stream
|
|
1113
1211
|
// is inert (it degrades to the 2-list fusion). Tune the anchor count with --graph-seeds.
|
|
1114
1212
|
results = await hybridSearch(query, localEntries, {
|
|
1115
|
-
budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1213
|
+
budget: entryBudget, cost: printCost, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1116
1214
|
includeSuperseded, asOf,
|
|
1117
1215
|
scoring: 'rrf',
|
|
1118
1216
|
graphStream: { weight: DEFAULT_GRAPH_STREAM_WEIGHT, tenantId, hops: graphStreamHops, seedCount: graphStreamSeeds },
|
|
1119
1217
|
});
|
|
1120
1218
|
}
|
|
1121
1219
|
else if (useMultihop) {
|
|
1122
|
-
|
|
1220
|
+
// Unlike searchBothHybrid below, multihop ranks one pooled list, so a shared memory's two copies both compete.
|
|
1221
|
+
const allEntries = api.oneCopyPerMemory(localEntries, globalEntries, evalNow()).flat();
|
|
1123
1222
|
results = multihopSearch(query, allEntries, {
|
|
1124
|
-
budget,
|
|
1223
|
+
budget: entryBudget,
|
|
1224
|
+
cost: printCost,
|
|
1125
1225
|
hippoRoot,
|
|
1126
1226
|
minResults,
|
|
1127
1227
|
includeSuperseded,
|
|
@@ -1130,7 +1230,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1130
1230
|
}
|
|
1131
1231
|
else if (usePhysics && !hasGlobal) {
|
|
1132
1232
|
results = await physicsSearch(query, localEntries, {
|
|
1133
|
-
budget,
|
|
1233
|
+
budget: entryBudget,
|
|
1234
|
+
cost: printCost,
|
|
1134
1235
|
hippoRoot,
|
|
1135
1236
|
physicsConfig: config.physics,
|
|
1136
1237
|
minResults,
|
|
@@ -1145,7 +1246,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1145
1246
|
// so the scope rule must be plumbed in — the filtered localEntries /
|
|
1146
1247
|
// globalEntries above are NOT what this path ranks.
|
|
1147
1248
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
1148
|
-
budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope, tenantId,
|
|
1249
|
+
budget: entryBudget, cost: printCost, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope, tenantId,
|
|
1149
1250
|
includeSuperseded, asOf,
|
|
1150
1251
|
recallScope: recallExplicitScope
|
|
1151
1252
|
? { requested: recallExplicitScope, additive: true }
|
|
@@ -1154,7 +1255,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1154
1255
|
}
|
|
1155
1256
|
else {
|
|
1156
1257
|
results = await hybridSearch(query, localEntries, {
|
|
1157
|
-
budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1258
|
+
budget: entryBudget, cost: printCost, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1158
1259
|
includeSuperseded, asOf,
|
|
1159
1260
|
});
|
|
1160
1261
|
}
|
|
@@ -1211,7 +1312,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1211
1312
|
tenantId,
|
|
1212
1313
|
includeSuperseded,
|
|
1213
1314
|
asOf,
|
|
1214
|
-
budget,
|
|
1315
|
+
budget: entryBudget,
|
|
1316
|
+
cost: printCost,
|
|
1215
1317
|
minResults: minResults ?? 1,
|
|
1216
1318
|
recallScope: recallExplicitScope
|
|
1217
1319
|
? { requested: recallExplicitScope, additive: true }
|
|
@@ -1590,132 +1692,13 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1590
1692
|
if (limit < results.length) {
|
|
1591
1693
|
results = results.slice(0, limit);
|
|
1592
1694
|
}
|
|
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
1695
|
// Continuity assembly (--continuity). Lives BEFORE the zero-result branch
|
|
1710
1696
|
// 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.
|
|
1697
|
+
// resume packet. Same three tenant-scoped store helpers as api.recall.
|
|
1714
1698
|
const includeContinuity = Boolean(flags['continuity']);
|
|
1715
1699
|
let activeSnapshot = null;
|
|
1716
1700
|
let sessionHandoff = null;
|
|
1717
1701
|
let recentSessionEvents = [];
|
|
1718
|
-
let continuityTokens = 0;
|
|
1719
1702
|
if (includeContinuity) {
|
|
1720
1703
|
const rawSnapshot = loadActiveTaskSnapshot(hippoRoot, tenantId);
|
|
1721
1704
|
const sessionId = rawSnapshot?.session_id ?? undefined;
|
|
@@ -1735,34 +1718,165 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1735
1718
|
sessionHandoff =
|
|
1736
1719
|
rawHandoff && passesScopeFilterForRecall(rowScope(rawHandoff), effectiveScope) ? rawHandoff : null;
|
|
1737
1720
|
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
|
-
|
|
1721
|
+
}
|
|
1722
|
+
// Sections print ahead of the memories, so they are paid first, after the header; one that does not fit is dropped.
|
|
1723
|
+
const sectionBudget = budget - printedTokens(recallHeading(budget, budget, query));
|
|
1724
|
+
let left = sectionBudget;
|
|
1725
|
+
const pays = (tokens) => { if (tokens > left)
|
|
1726
|
+
return false; left -= tokens; return true; };
|
|
1727
|
+
if (activeSnapshot && !pays(printedTokens(snapshotText(activeSnapshot))))
|
|
1728
|
+
activeSnapshot = null;
|
|
1729
|
+
if (sessionHandoff && !pays(printedTokens(handoffText(sessionHandoff))))
|
|
1730
|
+
sessionHandoff = null;
|
|
1731
|
+
if (recentSessionEvents.length > 0 && !pays(printedTokens(sessionTrailText(recentSessionEvents))))
|
|
1732
|
+
recentSessionEvents = [];
|
|
1733
|
+
const continuityTokens = sectionBudget - left;
|
|
1734
|
+
const hasContinuity = activeSnapshot !== null || sessionHandoff !== null || recentSessionEvents.length > 0;
|
|
1735
|
+
// J3.2: the baserate hint depends on the query alone; its audit is pipeline-local (actor 'cli').
|
|
1736
|
+
const cmdPlanningFallacyOutput = computePlanningFallacyOutput(hippoRoot, tenantId, query, { actor: 'cli' });
|
|
1737
|
+
const planText = planningLine(cmdPlanningFallacyOutput);
|
|
1738
|
+
const showPlan = planText !== null && pays(printedTokens(`${planText}\n`));
|
|
1739
|
+
const cmdPlanningFallacyHint = showPlan ? cmdPlanningFallacyOutput.hint ?? null : null;
|
|
1740
|
+
const cmdPlanningFallacyWatching = showPlan ? cmdPlanningFallacyOutput.watching ?? null : null;
|
|
1741
|
+
// The first --min-results are kept whatever they cost (the documented exception); the rest skip and continue.
|
|
1742
|
+
const floor = minResults ?? 1;
|
|
1743
|
+
const fitted = fitBudget(results, left, floor, printCost);
|
|
1744
|
+
// Copies go after every cut, so a merged row the budget drops never hides its sources.
|
|
1745
|
+
const shown = (n) => dropHeldCopies(fitted.slice(0, n), (r) => r.entry);
|
|
1746
|
+
let kept = fitted.length;
|
|
1747
|
+
results = shown(kept);
|
|
1748
|
+
// J1, J2 and C5: each pipeline computes its hints over the list it returns, so they follow the list as it shrinks.
|
|
1749
|
+
// HIPPO_ANCHORING=off and HIPPO_AVAILABILITY=off skip the work entirely.
|
|
1750
|
+
const anchorRing = process.env.HIPPO_ANCHORING !== 'off' && sessionId
|
|
1751
|
+
? getOrCreateRing(sessionRecallHistoryCli, buildSessionKey(tenantId, sessionId))
|
|
1752
|
+
: null;
|
|
1753
|
+
const queryHash = hashQueryText(query);
|
|
1754
|
+
const availabilityPool = process.env.HIPPO_AVAILABILITY !== 'off'
|
|
1755
|
+
? [...localEntries, ...globalEntries].map((e) => ({ id: e.id, created: e.created }))
|
|
1756
|
+
: null;
|
|
1757
|
+
const hintsFor = (list, held) => {
|
|
1758
|
+
const anchoring = anchorRing ? detectAnchoring(snapshotRing(anchorRing), queryHash, list[0]?.entry.id ?? null) : null;
|
|
1759
|
+
const availability = availabilityPool
|
|
1760
|
+
? detectAvailabilityBias({ topK: list.map((r) => ({ id: r.entry.id, created: r.entry.created })), pool: availabilityPool })
|
|
1761
|
+
: null;
|
|
1762
|
+
const summary = api.buildSuppressionSummary({
|
|
1763
|
+
// The published total includes graph-surfaced rows, so total == preRank + byBudget + returned holds for callers.
|
|
1764
|
+
totalCandidates: totalCandidatesCountCmd + graphAddedCountCmd,
|
|
1765
|
+
droppedPreRank: droppedPreRankCountCmd + held,
|
|
1766
|
+
droppedByBudget: Math.max(0, totalCandidatesCountCmd + graphAddedCountCmd - droppedPreRankCountCmd - held - list.length),
|
|
1767
|
+
summarySubstitutionsAdded: 0,
|
|
1768
|
+
freshTailAdded: 0,
|
|
1769
|
+
suppressedByInterference: anchoring?.reason === 'memory_dominance' ? 1 : 0, // a query_repeat is a re-ask, not competition
|
|
1770
|
+
});
|
|
1771
|
+
return { anchoring, availability, summary };
|
|
1772
|
+
};
|
|
1773
|
+
const printContinuity = () => {
|
|
1774
|
+
if (activeSnapshot)
|
|
1775
|
+
printActiveTaskSnapshot(activeSnapshot);
|
|
1776
|
+
if (sessionHandoff)
|
|
1777
|
+
printHandoff(sessionHandoff);
|
|
1778
|
+
if (recentSessionEvents.length > 0)
|
|
1779
|
+
printSessionEvents(recentSessionEvents);
|
|
1780
|
+
};
|
|
1781
|
+
const renderRecall = (list, h) => settleTokens((t) => captureConsole(() => {
|
|
1782
|
+
if (list.length === 0) {
|
|
1783
|
+
// The hint still prints when nothing matched, so the agent sees its track record.
|
|
1784
|
+
if (showPlan) {
|
|
1785
|
+
console.log(planText);
|
|
1786
|
+
console.log();
|
|
1787
|
+
}
|
|
1788
|
+
if (hasContinuity) {
|
|
1789
|
+
printContinuity();
|
|
1790
|
+
console.log(`(no memories matched "${query}")`);
|
|
1791
|
+
}
|
|
1792
|
+
else {
|
|
1793
|
+
console.log('No memories found for:', query);
|
|
1794
|
+
}
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
printContinuity();
|
|
1798
|
+
// Anchoring is the stronger pull, so it prints first; the Cutoff line sits above the list, where a reader sees it.
|
|
1799
|
+
if (h.anchoring) {
|
|
1800
|
+
console.log(`[anchored_on: ${h.anchoring.memoryId}] ${h.anchoring.summary}`);
|
|
1801
|
+
console.log();
|
|
1802
|
+
}
|
|
1803
|
+
if (h.availability) {
|
|
1804
|
+
console.log(`Availability bias (${h.availability.recentCount}/${h.availability.returnedCount} recent): ${h.availability.summary}`);
|
|
1805
|
+
console.log();
|
|
1806
|
+
}
|
|
1807
|
+
if (showPlan) {
|
|
1808
|
+
console.log(planText);
|
|
1809
|
+
console.log();
|
|
1810
|
+
}
|
|
1811
|
+
const cutoff = showWhy ? cutoffLine(list.length, h.summary) : null;
|
|
1812
|
+
if (cutoff) {
|
|
1813
|
+
console.log(cutoff);
|
|
1814
|
+
console.log();
|
|
1815
|
+
}
|
|
1816
|
+
console.log(recallHeading(list.length, t, query));
|
|
1817
|
+
for (const r of list)
|
|
1818
|
+
console.log(entryText(r));
|
|
1765
1819
|
}));
|
|
1820
|
+
let hints = hintsFor(results, kept - results.length);
|
|
1821
|
+
let recallText = renderRecall(results, hints);
|
|
1822
|
+
// The hints, Cutoff line and header vary with the list, so the lowest-ranked entry goes until the whole block fits.
|
|
1823
|
+
while (kept > floor && estimateTokens(recallText) > budget) {
|
|
1824
|
+
kept--;
|
|
1825
|
+
results = shown(kept);
|
|
1826
|
+
hints = hintsFor(results, kept - results.length);
|
|
1827
|
+
recallText = renderRecall(results, hints);
|
|
1828
|
+
}
|
|
1829
|
+
const { anchoring: cmdAnchoringHint, availability: cmdAvailabilityHint, summary: cmdSuppressionSummary } = hints;
|
|
1830
|
+
if (anchorRing) {
|
|
1831
|
+
// Appended after every detect: anchoredOn feeds the cooldown for the next recall on this session.
|
|
1832
|
+
appendRecall(anchorRing, queryHash, results[0]?.entry.id ?? null, cmdAnchoringHint?.memoryId);
|
|
1833
|
+
}
|
|
1834
|
+
else if (process.env.HIPPO_ANCHORING !== 'off') {
|
|
1835
|
+
// SHA-256/16 per the recall-audit convention; hashQueryText is FNV-1a and brute-forceable on short queries.
|
|
1836
|
+
emitCliAudit(hippoRoot, 'recall_anchor_skipped_no_session', undefined, {
|
|
1837
|
+
query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
|
|
1838
|
+
query_length: query.length,
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
if (cmdAnchoringHint?.reason === 'memory_dominance') {
|
|
1842
|
+
emitCliAudit(hippoRoot, 'recall_anchor_detected_memory_dominance', cmdAnchoringHint.memoryId, {
|
|
1843
|
+
memory_id: cmdAnchoringHint.memoryId,
|
|
1844
|
+
query_count: cmdAnchoringHint.queryCount ?? null,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
else if (cmdAnchoringHint?.reason === 'query_repeat') {
|
|
1848
|
+
emitCliAudit(hippoRoot, 'recall_anchor_detected_query_repeat', cmdAnchoringHint.memoryId, {
|
|
1849
|
+
memory_id: cmdAnchoringHint.memoryId,
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
if (cmdAvailabilityHint) {
|
|
1853
|
+
emitCliAudit(hippoRoot, 'recall_availability_detected', undefined, {
|
|
1854
|
+
recent_fraction: cmdAvailabilityHint.recentFraction,
|
|
1855
|
+
older_passed_over: cmdAvailabilityHint.olderCandidatesPassedOver,
|
|
1856
|
+
returned_count: cmdAvailabilityHint.returnedCount,
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
// A5 audit: one 'recall' event per query, before the early-empty return, in every participating store.
|
|
1860
|
+
const recallMetadata = {
|
|
1861
|
+
query: query.slice(0, 200),
|
|
1862
|
+
results: results.length,
|
|
1863
|
+
};
|
|
1864
|
+
emitCliAudit(hippoRoot, 'recall', undefined, recallMetadata);
|
|
1865
|
+
if (isInitialized(globalRoot) && globalRoot !== hippoRoot) {
|
|
1866
|
+
emitCliAudit(globalRoot, 'recall', undefined, recallMetadata);
|
|
1867
|
+
}
|
|
1868
|
+
// TE0 token ledger: books the block this recall prints, on whichever exit it takes.
|
|
1869
|
+
const emit = (text) => {
|
|
1870
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
1871
|
+
tenantId,
|
|
1872
|
+
sessionId: hostSessionId() ?? null,
|
|
1873
|
+
surface: 'recall',
|
|
1874
|
+
event: 'inject',
|
|
1875
|
+
items: results.length,
|
|
1876
|
+
tokens: estimateTokens(text),
|
|
1877
|
+
}));
|
|
1878
|
+
console.log(text);
|
|
1879
|
+
};
|
|
1766
1880
|
if (results.length === 0) {
|
|
1767
1881
|
// LC1 F1 structural fix (docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
|
|
1768
1882
|
// trace the zero-result recall too (result_count 0, no result rows) so
|
|
@@ -1807,55 +1921,13 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1807
1921
|
};
|
|
1808
1922
|
out.continuityTokens = continuityTokens;
|
|
1809
1923
|
}
|
|
1810
|
-
|
|
1924
|
+
emit(JSON.stringify(out));
|
|
1811
1925
|
return;
|
|
1812
1926
|
}
|
|
1813
|
-
|
|
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}")`);
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
console.log('No memories found for:', query);
|
|
1927
|
+
emit(recallText);
|
|
1855
1928
|
return;
|
|
1856
1929
|
}
|
|
1857
1930
|
const retrievedIds = results.map((r) => r.entry.id);
|
|
1858
|
-
const localIndex = loadIndex(hippoRoot);
|
|
1859
1931
|
const strengthenedHere = strengthenRetrieved(hippoRoot, retrievedIds);
|
|
1860
1932
|
if (isInitialized(globalRoot))
|
|
1861
1933
|
strengthenRetrieved(globalRoot, retrievedIds.filter((id) => !strengthenedHere.has(id)));
|
|
@@ -1944,131 +2016,10 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1944
2016
|
};
|
|
1945
2017
|
jsonOut.continuityTokens = continuityTokens;
|
|
1946
2018
|
}
|
|
1947
|
-
|
|
2019
|
+
emit(JSON.stringify(jsonOut));
|
|
1948
2020
|
return;
|
|
1949
2021
|
}
|
|
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.
|
|
2022
|
+
emit(recallText);
|
|
2072
2023
|
}
|
|
2073
2024
|
async function cmdExplain(hippoRoot, query, flags) {
|
|
2074
2025
|
requireInit(hippoRoot);
|
|
@@ -2155,11 +2106,17 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2155
2106
|
: config.search.localBump;
|
|
2156
2107
|
// explainExplicitScope hoisted above the candidate loads (v1.25.0).
|
|
2157
2108
|
const explainActiveScope = explainExplicitScope || detectScope();
|
|
2109
|
+
// Priced as recall prints each result, so explain returns what recall's engines would.
|
|
2110
|
+
const explainIndex = loadIndex(hippoRoot);
|
|
2111
|
+
const explainGlobalOn = isInitialized(globalRoot);
|
|
2112
|
+
const cost = (r) => printedTokens(recallEntryText(r, query, false, explainGlobalOn && !explainIndex.entries[r.entry.id]));
|
|
2113
|
+
const entryBudget = Math.max(0, budget - printedTokens(recallHeading(budget, budget, query)));
|
|
2158
2114
|
let results;
|
|
2159
2115
|
let modeUsed;
|
|
2160
2116
|
if (usePhysics && !hasGlobal) {
|
|
2161
2117
|
results = await physicsSearch(query, explainLocalEntries, {
|
|
2162
|
-
budget,
|
|
2118
|
+
budget: entryBudget,
|
|
2119
|
+
cost,
|
|
2163
2120
|
hippoRoot,
|
|
2164
2121
|
physicsConfig: config.physics,
|
|
2165
2122
|
explain: true,
|
|
@@ -2169,7 +2126,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2169
2126
|
}
|
|
2170
2127
|
else if (hasGlobal) {
|
|
2171
2128
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
2172
|
-
budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
2129
|
+
budget: entryBudget, cost, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
2173
2130
|
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf, tenantId,
|
|
2174
2131
|
recallScope: explainExplicitScope
|
|
2175
2132
|
? { requested: explainExplicitScope, additive: true }
|
|
@@ -2179,7 +2136,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2179
2136
|
}
|
|
2180
2137
|
else {
|
|
2181
2138
|
results = await hybridSearch(query, explainLocalEntries, {
|
|
2182
|
-
budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
|
|
2139
|
+
budget: entryBudget, cost, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
|
|
2183
2140
|
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
2184
2141
|
});
|
|
2185
2142
|
modeUsed = 'hybrid';
|
|
@@ -2187,6 +2144,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
2187
2144
|
if (limit < results.length) {
|
|
2188
2145
|
results = results.slice(0, limit);
|
|
2189
2146
|
}
|
|
2147
|
+
results = dropHeldCopies(results, (r) => r.entry);
|
|
2190
2148
|
const candidates = explainLocalEntries.length + explainGlobalEntries.length;
|
|
2191
2149
|
if (asJson) {
|
|
2192
2150
|
const output = results.map((r, rank) => ({
|
|
@@ -2313,7 +2271,9 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
|
2313
2271
|
try {
|
|
2314
2272
|
baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
2315
2273
|
}
|
|
2316
|
-
catch {
|
|
2274
|
+
catch {
|
|
2275
|
+
console.error(`Warning: eval baseline ${baselinePath} is unreadable; running without it.`);
|
|
2276
|
+
}
|
|
2317
2277
|
}
|
|
2318
2278
|
const result = await runFeatureEval(version);
|
|
2319
2279
|
if (asJson) {
|
|
@@ -2645,28 +2605,46 @@ async function cmdRefine(hippoRoot, flags) {
|
|
|
2645
2605
|
}
|
|
2646
2606
|
}
|
|
2647
2607
|
}
|
|
2648
|
-
/**
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2608
|
+
/** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
|
|
2609
|
+
function claudeMemoryFolderNames(projectRoot) {
|
|
2610
|
+
const roots = [projectRoot, realpathOrResolve(projectRoot)];
|
|
2611
|
+
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 });
|
|
2612
|
+
if (git.status === 0) {
|
|
2613
|
+
const [top, gitDir, common] = git.stdout.trim().split(/\r?\n/);
|
|
2614
|
+
roots.push(claudeCheckoutRoot(top, gitDir, common));
|
|
2615
|
+
}
|
|
2616
|
+
return new Set(roots.map((root) => (process.platform === 'win32' ? claudeFolderName(root).toLowerCase() : claudeFolderName(root))));
|
|
2617
|
+
}
|
|
2618
|
+
/** 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. */
|
|
2619
|
+
function claudeCheckoutRoot(top, gitDir, common) {
|
|
2620
|
+
if (gitDir === common)
|
|
2621
|
+
return top;
|
|
2622
|
+
if (path.basename(common) === '.git')
|
|
2623
|
+
return path.dirname(common);
|
|
2624
|
+
return fs.existsSync(path.join(common, '.git')) ? top : common;
|
|
2625
|
+
}
|
|
2626
|
+
/** 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. */
|
|
2627
|
+
function claudeFolderName(root) {
|
|
2628
|
+
const full = path.resolve(root);
|
|
2629
|
+
const name = full.replace(/[^a-zA-Z0-9]/g, '-');
|
|
2630
|
+
if (name.length <= 200)
|
|
2631
|
+
return name;
|
|
2632
|
+
let hash = 0;
|
|
2633
|
+
for (let i = 0; i < full.length; i++)
|
|
2634
|
+
hash = ((hash << 5) - hash + full.charCodeAt(i)) | 0;
|
|
2635
|
+
return `${name.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
|
|
2636
|
+
}
|
|
2637
|
+
/** Import new entries from the Claude Code auto memory of the store's project, the frontmatter .md files in ~/.claude/projects/<project>/memory/. */
|
|
2652
2638
|
export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
for (const project of fs.readdirSync(claudeProjectsDir)) {
|
|
2660
|
-
const memDir = path.join(claudeProjectsDir, project, 'memory');
|
|
2661
|
-
if (fs.existsSync(memDir))
|
|
2662
|
-
memoryDirs.push(memDir);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
catch { /* permission denied */ }
|
|
2666
|
-
}
|
|
2639
|
+
// Only this project's folder, because every entry written here counts as this project's memory.
|
|
2640
|
+
// SHORTCUT: an autoMemoryDirectory or CLAUDE_CODE_PROJECT_DIR_NAME setting imports nothing; read Claude's settings if users ask.
|
|
2641
|
+
const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
|
|
2642
|
+
const memoryDirs = [...claudeMemoryFolderNames(path.dirname(hippoRoot))]
|
|
2643
|
+
.map((folder) => path.join(claudeProjectsDir, folder, 'memory'))
|
|
2644
|
+
.filter((memDir) => fs.existsSync(memDir));
|
|
2667
2645
|
if (memoryDirs.length === 0)
|
|
2668
2646
|
return 0;
|
|
2669
|
-
const
|
|
2647
|
+
const keys = storedTextKeys(loadAllEntries(hippoRoot, resolveTenantId({})));
|
|
2670
2648
|
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
2671
2649
|
let imported = 0;
|
|
2672
2650
|
let skippedSecret = 0;
|
|
@@ -2698,12 +2676,8 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2698
2676
|
skippedSecret++;
|
|
2699
2677
|
continue;
|
|
2700
2678
|
}
|
|
2701
|
-
// Dedup:
|
|
2702
|
-
|
|
2703
|
-
const overlap = textOverlap(content.slice(0, 200), e.content.slice(0, 200));
|
|
2704
|
-
return overlap > 0.6;
|
|
2705
|
-
});
|
|
2706
|
-
if (isDup)
|
|
2679
|
+
// Dedup: skip only when the same text is already stored
|
|
2680
|
+
if (keys.has(duplicateKey(content)))
|
|
2707
2681
|
continue;
|
|
2708
2682
|
const entry = createMemory(content, {
|
|
2709
2683
|
layer: Layer.Episodic,
|
|
@@ -2723,7 +2697,7 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2723
2697
|
}
|
|
2724
2698
|
throw err;
|
|
2725
2699
|
}
|
|
2726
|
-
|
|
2700
|
+
keys.add(duplicateKey(content)); // prevent self-dedup within batch
|
|
2727
2701
|
imported++;
|
|
2728
2702
|
}
|
|
2729
2703
|
}
|
|
@@ -2740,10 +2714,12 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2740
2714
|
function cmdDedup(hippoRoot, flags) {
|
|
2741
2715
|
requireInit(hippoRoot);
|
|
2742
2716
|
const dryRun = Boolean(flags['dry-run']);
|
|
2743
|
-
|
|
2717
|
+
if (flags['threshold'] !== undefined) {
|
|
2718
|
+
console.error('hippo dedup: --threshold is ignored; a duplicate is the same text apart from spacing.');
|
|
2719
|
+
}
|
|
2744
2720
|
const entries = loadAllEntries(hippoRoot);
|
|
2745
|
-
console.log(`Scanning ${entries.length} memories for duplicates (
|
|
2746
|
-
const result = deduplicateStore(hippoRoot, {
|
|
2721
|
+
console.log(`Scanning ${entries.length} memories for duplicates (same text apart from spacing)${dryRun ? ' (dry run)' : ''}...\n`);
|
|
2722
|
+
const result = deduplicateStore(hippoRoot, { dryRun });
|
|
2747
2723
|
if (result.removed === 0) {
|
|
2748
2724
|
console.log('No duplicates found.');
|
|
2749
2725
|
return;
|
|
@@ -2895,10 +2871,10 @@ export function renderSleepResult(result) {
|
|
|
2895
2871
|
}
|
|
2896
2872
|
async function cmdSleepCore(hippoRoot, flags) {
|
|
2897
2873
|
requireInit(hippoRoot);
|
|
2898
|
-
// Phase 1: Auto-learn from git +
|
|
2874
|
+
// Phase 1: Auto-learn from git + Claude Code auto memory (CLI-only, uses process.cwd() / os.homedir()).
|
|
2899
2875
|
// Stays in cli.ts; api.sleep covers Phase 2-6 only.
|
|
2900
2876
|
if (!flags['no-learn'] && flags['dry-run']) {
|
|
2901
|
-
console.log(
|
|
2877
|
+
console.log("Dry run: skipped learning from git commits and this project's Claude Code auto memory.");
|
|
2902
2878
|
}
|
|
2903
2879
|
else if (!flags['no-learn']) {
|
|
2904
2880
|
const config = loadConfig(hippoRoot);
|
|
@@ -2916,10 +2892,9 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
2916
2892
|
console.error(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
2917
2893
|
}
|
|
2918
2894
|
}
|
|
2919
|
-
// Also learn from Claude Code MEMORY.md files
|
|
2920
2895
|
const memImported = learnFromMemoryMd(hippoRoot);
|
|
2921
2896
|
if (memImported > 0)
|
|
2922
|
-
console.log(`Imported ${memImported} memories from Claude Code
|
|
2897
|
+
console.log(`Imported ${memImported} memories from this project's Claude Code auto memory.`);
|
|
2923
2898
|
}
|
|
2924
2899
|
// Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
|
|
2925
2900
|
const ctx = {
|
|
@@ -2933,12 +2908,8 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
2933
2908
|
});
|
|
2934
2909
|
renderSleepResult(result);
|
|
2935
2910
|
}
|
|
2936
|
-
/**
|
|
2937
|
-
*
|
|
2938
|
-
* Called from SessionStart hooks so the user sees the previous session's
|
|
2939
|
-
* consolidation output (SessionEnd hook output is invisible because the TUI
|
|
2940
|
-
* is tearing down when it runs).
|
|
2941
|
-
*/
|
|
2911
|
+
/** Prints the SessionEnd sleep log, then clears it. Stderr, because Claude Code adds
|
|
2912
|
+
* SessionStart stdout to the model's context and this log is for the user. */
|
|
2942
2913
|
function cmdLastSleep(flags) {
|
|
2943
2914
|
const logPath = typeof flags['path'] === 'string'
|
|
2944
2915
|
? flags['path']
|
|
@@ -2953,11 +2924,11 @@ function cmdLastSleep(flags) {
|
|
|
2953
2924
|
return;
|
|
2954
2925
|
}
|
|
2955
2926
|
if (content.trim().length > 0) {
|
|
2956
|
-
console.
|
|
2957
|
-
process.
|
|
2927
|
+
console.error('=== Previous session hippo consolidation ===');
|
|
2928
|
+
process.stderr.write(content);
|
|
2958
2929
|
if (!content.endsWith('\n'))
|
|
2959
|
-
console.
|
|
2960
|
-
console.
|
|
2930
|
+
console.error();
|
|
2931
|
+
console.error('===========================================');
|
|
2961
2932
|
}
|
|
2962
2933
|
if (!flags['keep']) {
|
|
2963
2934
|
try {
|
|
@@ -3023,7 +2994,8 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
3023
2994
|
// source === 'compact' to print. Real SessionStart payloads always
|
|
3024
2995
|
// carry source; only the TTY/no-stdin manual path prints without
|
|
3025
2996
|
// one (codex round 3).
|
|
3026
|
-
|
|
2997
|
+
// A sub-agent's payload carries its parent's session id, so X5 would pass and restore the parent's snapshot into it.
|
|
2998
|
+
if (payload.source !== 'compact' || isSubagentPayload(stdinText)) {
|
|
3027
2999
|
suppressOutput = true;
|
|
3028
3000
|
}
|
|
3029
3001
|
if (typeof payload.session_id === 'string') {
|
|
@@ -3042,29 +3014,41 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
3042
3014
|
snapshot.session_id !== null &&
|
|
3043
3015
|
payloadSessionId !== snapshot.session_id;
|
|
3044
3016
|
if (snapshot && !sessionMismatch) {
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
if (snapshot.session_id) {
|
|
3051
|
-
const events = listSessionEvents(hippoRoot, tenantId, { session_id: snapshot.session_id });
|
|
3052
|
-
// Nothing auto-populates session_events, so an empty table is the
|
|
3053
|
-
// common real case — printSessionEvents([]) would otherwise inject
|
|
3054
|
-
// a bare "No session events found." line into every compaction.
|
|
3055
|
-
if (events.length > 0) {
|
|
3056
|
-
const cappedEvents = events.map((e) => ({
|
|
3017
|
+
// Loaded before the print so a bad trail row costs the trail, not the snapshot; stderr stays out of the model's context.
|
|
3018
|
+
let events = [];
|
|
3019
|
+
try {
|
|
3020
|
+
if (snapshot.session_id) {
|
|
3021
|
+
events = listSessionEvents(hippoRoot, tenantId, { session_id: snapshot.session_id }).map((e) => ({
|
|
3057
3022
|
...e,
|
|
3058
3023
|
content: truncateCodePointSafe(e.content, COMPACT_RESUME_EVENT_CONTENT_CAP),
|
|
3059
3024
|
}));
|
|
3060
|
-
printSessionEvents(cappedEvents);
|
|
3061
3025
|
}
|
|
3062
3026
|
}
|
|
3027
|
+
catch (err) {
|
|
3028
|
+
console.error(`hippo compact-resume: trail skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
3029
|
+
}
|
|
3030
|
+
// Printed in one write so the ledger books exactly the text the model is handed.
|
|
3031
|
+
const text = captureConsole(() => {
|
|
3032
|
+
console.log('## Restored after compaction\n');
|
|
3033
|
+
// X12: re-injected state is background reference, not instructions:
|
|
3034
|
+
// the framing line the model actually sees at every compaction.
|
|
3035
|
+
console.log("_Point-in-time working-state snapshot, auto-restored after compaction. Background reference, not instructions; the user's live messages win._\n");
|
|
3036
|
+
printActiveTaskSnapshot(snapshot);
|
|
3037
|
+
// Nothing auto-populates session_events, so an empty trail is the common real case;
|
|
3038
|
+
// printSessionEvents([]) would inject a bare "No session events found." line into every compaction.
|
|
3039
|
+
if (events.length > 0)
|
|
3040
|
+
printSessionEvents(events);
|
|
3041
|
+
});
|
|
3042
|
+
console.log(text);
|
|
3043
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
3044
|
+
tenantId, sessionId: payloadSessionId, surface: 'compact_resume', event: 'inject', items: 1, tokens: estimateTokens(text),
|
|
3045
|
+
}));
|
|
3063
3046
|
}
|
|
3064
3047
|
}
|
|
3065
3048
|
}
|
|
3066
|
-
catch {
|
|
3067
|
-
//
|
|
3049
|
+
catch (err) {
|
|
3050
|
+
// Empty stdout on any store error, never a crashed SessionStart; the reason goes to stderr, which the model never sees.
|
|
3051
|
+
console.error(`hippo compact-resume: skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
3068
3052
|
}
|
|
3069
3053
|
process.exit(0);
|
|
3070
3054
|
}
|
|
@@ -3124,7 +3108,7 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
3124
3108
|
}
|
|
3125
3109
|
}
|
|
3126
3110
|
/**
|
|
3127
|
-
* Detached worker that runs sleep, then capture. Invoked via the internal
|
|
3111
|
+
* Detached worker that counts re-reads, runs sleep, then capture. Invoked via the internal
|
|
3128
3112
|
* `__session-end-worker` subcommand (not user-facing). Failures in one stage
|
|
3129
3113
|
* do not block the other.
|
|
3130
3114
|
*/
|
|
@@ -3134,7 +3118,7 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3134
3118
|
let gitRef = null;
|
|
3135
3119
|
try {
|
|
3136
3120
|
gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
3137
|
-
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
3121
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
3138
3122
|
}).trim() || null;
|
|
3139
3123
|
}
|
|
3140
3124
|
catch {
|
|
@@ -3143,7 +3127,7 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3143
3127
|
let dirtyTree = null;
|
|
3144
3128
|
try {
|
|
3145
3129
|
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
3146
|
-
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
3130
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
3147
3131
|
});
|
|
3148
3132
|
dirtyTree = status.trim().length > 0;
|
|
3149
3133
|
}
|
|
@@ -3153,34 +3137,66 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3153
3137
|
return { gitRef, dirtyTree, testStatus };
|
|
3154
3138
|
}
|
|
3155
3139
|
async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
3156
|
-
|
|
3157
|
-
|
|
3140
|
+
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
3141
|
+
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
3142
|
+
const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
|
|
3143
|
+
const rereadLog = await bookSessionRereads(hippoRoot, transcriptPath, closeSessionId)
|
|
3144
|
+
.catch((err) => [`re-read count failed: ${err instanceof Error ? err.message : String(err)}`]);
|
|
3145
|
+
// Sleep starts the log file afresh, so the lines go in after it; on exit too, in case sleep exits the process.
|
|
3146
|
+
const flushRereadLog = () => { for (const line of rereadLog.splice(0))
|
|
3147
|
+
appendSessionEndCloseLog(closeLogFile, line); };
|
|
3148
|
+
process.once('exit', flushRereadLog);
|
|
3149
|
+
// Like the other hooks: project store, else global; a folder with neither must not get one made.
|
|
3150
|
+
const store = hookStoreRoot(hippoRoot);
|
|
3151
|
+
if (!isInitialized(store)) {
|
|
3152
|
+
appendSessionEndCloseLog(closeLogFile, 'skip: no hippo store for this folder or globally', { startFresh: true });
|
|
3153
|
+
flushRereadLog();
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
// Sleeping the global store from here would learn this folder's git commits into it; it has its own daily sleep.
|
|
3157
|
+
if (isInitialized(hippoRoot)) {
|
|
3158
|
+
try {
|
|
3159
|
+
await cmdSleep(hippoRoot, flags);
|
|
3160
|
+
}
|
|
3161
|
+
catch {
|
|
3162
|
+
// sleep errors are already tee'd to the log file via cmdSleep's
|
|
3163
|
+
// `[hippo] sleep failed: ...` line. Continue to capture regardless.
|
|
3164
|
+
}
|
|
3158
3165
|
}
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
// `[hippo] sleep failed: ...` line. Continue to capture regardless.
|
|
3166
|
+
else {
|
|
3167
|
+
appendSessionEndCloseLog(closeLogFile, 'skip sleep: this folder has no store of its own', { startFresh: true });
|
|
3162
3168
|
}
|
|
3169
|
+
flushRereadLog();
|
|
3170
|
+
const digestLog = (message) => appendSessionEndCloseLog(closeLogFile, message);
|
|
3171
|
+
const scan = transcriptPath ? readSessionScan(transcriptPath, digestLog) : null;
|
|
3163
3172
|
try {
|
|
3164
3173
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3165
|
-
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
3166
3174
|
// With no stdin of its own, capture would read this as a manual run and scan every project.
|
|
3167
3175
|
if (!transcriptPath) {
|
|
3168
3176
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
|
|
3169
3177
|
}
|
|
3170
3178
|
else {
|
|
3171
|
-
cmdCapture(
|
|
3179
|
+
cmdCapture(store, {
|
|
3172
3180
|
source: 'last-session',
|
|
3173
3181
|
transcriptPath,
|
|
3174
3182
|
logFile,
|
|
3175
3183
|
dryRun: false,
|
|
3176
3184
|
global: false,
|
|
3177
3185
|
tenantId: resolveTenantId({}),
|
|
3186
|
+
// In the global store, rows would otherwise read as user-global and show up in every project.
|
|
3187
|
+
originProject: store === hippoRoot ? undefined : deriveOriginProject(process.cwd()),
|
|
3188
|
+
sessionTurns: scan?.turns,
|
|
3178
3189
|
});
|
|
3179
3190
|
}
|
|
3180
3191
|
}
|
|
3181
3192
|
catch {
|
|
3182
3193
|
// Same treatment — the failure line is already in the log.
|
|
3183
3194
|
}
|
|
3195
|
+
recordSessionDigest(hippoRoot, scan, {
|
|
3196
|
+
key: closeSessionId || path.basename(transcriptPath ?? '', '.jsonl'),
|
|
3197
|
+
tenantId: resolveTenantId({}),
|
|
3198
|
+
log: digestLog,
|
|
3199
|
+
});
|
|
3184
3200
|
// DF1 T3: close the ending session's own active task snapshot AFTER
|
|
3185
3201
|
// sleep+capture complete — neither producer (runPreCompact,
|
|
3186
3202
|
// `hippo snapshot save`) runs inside session-end, so this can never
|
|
@@ -3189,20 +3205,23 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3189
3205
|
// clause). Absent session id -> no-op plus one log line; session-end is
|
|
3190
3206
|
// not guaranteed to fire at all (crash, kill -9), so the freshness bound
|
|
3191
3207
|
// in loadFreshActiveTaskSnapshot is the backstop layer, not this close.
|
|
3192
|
-
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
3193
|
-
const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
|
|
3194
3208
|
// Handoff write happens BEFORE the snapshot close below, while the
|
|
3195
3209
|
// snapshot writeSessionEndHandoff reads is still active.
|
|
3196
3210
|
if (closeSessionId) {
|
|
3197
3211
|
try {
|
|
3198
|
-
const
|
|
3199
|
-
|
|
3200
|
-
|
|
3212
|
+
const tenantId = resolveTenantId({});
|
|
3213
|
+
const ownSnapshot = loadActiveTaskSnapshot(store, tenantId)?.session_id === closeSessionId;
|
|
3214
|
+
// 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.
|
|
3215
|
+
const derived = transcriptPath
|
|
3216
|
+
? transcriptWorkingState(transcriptPath, (message) => appendSessionEndCloseLog(closeLogFile, message))
|
|
3217
|
+
: null;
|
|
3218
|
+
if (!ownSnapshot && !derived) {
|
|
3219
|
+
appendSessionEndCloseLog(closeLogFile, 'skip: no snapshot or transcript for session');
|
|
3201
3220
|
}
|
|
3202
3221
|
else {
|
|
3203
3222
|
const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
|
|
3204
|
-
const handoff = writeSessionEndHandoff(
|
|
3205
|
-
appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` :
|
|
3223
|
+
const handoff = writeSessionEndHandoff(store, tenantId, closeSessionId, evidence, derived);
|
|
3224
|
+
appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : `skip: kept the existing handoff for session ${closeSessionId}`);
|
|
3206
3225
|
}
|
|
3207
3226
|
}
|
|
3208
3227
|
catch (err) {
|
|
@@ -3212,7 +3231,7 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3212
3231
|
}
|
|
3213
3232
|
try {
|
|
3214
3233
|
if (closeSessionId) {
|
|
3215
|
-
const closed = closeTaskSnapshotsForSession(
|
|
3234
|
+
const closed = closeTaskSnapshotsForSession(store, resolveTenantId({}), closeSessionId);
|
|
3216
3235
|
appendSessionEndCloseLog(closeLogFile, `closed ${closed} active snapshot(s) for session ${closeSessionId}`);
|
|
3217
3236
|
}
|
|
3218
3237
|
else {
|
|
@@ -3223,6 +3242,38 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3223
3242
|
appendSessionEndCloseLog(closeLogFile, `snapshot close failed: ${err.message}`);
|
|
3224
3243
|
}
|
|
3225
3244
|
}
|
|
3245
|
+
/** Books the ending session's re-reads in each store its ledger rows can land in (project and global); returns the log lines. */
|
|
3246
|
+
async function bookSessionRereads(hippoRoot, transcriptPath, sessionId) {
|
|
3247
|
+
if (!transcriptPath || !sessionId)
|
|
3248
|
+
return [];
|
|
3249
|
+
let read;
|
|
3250
|
+
try {
|
|
3251
|
+
read = await readApiCalls(transcriptPath);
|
|
3252
|
+
}
|
|
3253
|
+
catch (err) {
|
|
3254
|
+
return [`skip re-read count: cannot read the transcript: ${err instanceof Error ? err.message : String(err)}`];
|
|
3255
|
+
}
|
|
3256
|
+
const roots = new Set([hippoRoot, getGlobalRoot()].filter((root) => isInitialized(root)).map((root) => path.resolve(root)));
|
|
3257
|
+
const lines = [];
|
|
3258
|
+
let tokens = 0;
|
|
3259
|
+
for (const root of roots) {
|
|
3260
|
+
try {
|
|
3261
|
+
const db = openHippoDb(root);
|
|
3262
|
+
try {
|
|
3263
|
+
tokens += recordRereads(db, resolveTenantId({}), sessionId, read.calls);
|
|
3264
|
+
}
|
|
3265
|
+
finally {
|
|
3266
|
+
closeHippoDb(db);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
catch (err) {
|
|
3270
|
+
lines.push(`re-read count failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
const skipped = read.malformed > 0 ? `, ${read.malformed} unparsable transcript lines skipped` : '';
|
|
3274
|
+
lines.push(`re-read ${tokens} tokens over ${read.calls.length} model calls for session ${sessionId}${skipped}`);
|
|
3275
|
+
return lines;
|
|
3276
|
+
}
|
|
3226
3277
|
/**
|
|
3227
3278
|
* Best-effort log line for the DF1 T3 snapshot-close step in
|
|
3228
3279
|
* `cmdSessionEndWorker`. `cmdSleep`/`cmdCapture` each tee console output to
|
|
@@ -3231,14 +3282,15 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3231
3282
|
* the detached worker's `stdio: 'ignore'` — write straight to the file
|
|
3232
3283
|
* instead, matching capture.ts's `appendPreCompactLog` convention.
|
|
3233
3284
|
*/
|
|
3234
|
-
function appendSessionEndCloseLog(logFile, message) {
|
|
3285
|
+
function appendSessionEndCloseLog(logFile, message, opts = {}) {
|
|
3235
3286
|
if (!logFile)
|
|
3236
3287
|
return;
|
|
3237
3288
|
try {
|
|
3238
3289
|
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
3239
3290
|
// sanitizeLogMessage: `message` interpolates the payload-controlled
|
|
3240
3291
|
// session_id — same log-forgery guard appendPreCompactLog applies.
|
|
3241
|
-
|
|
3292
|
+
const write = opts.startFresh ? fs.writeFileSync : fs.appendFileSync;
|
|
3293
|
+
write(logFile, `[hippo] ${new Date().toISOString()} ${sanitizeLogMessage(message)}\n`, 'utf8');
|
|
3242
3294
|
}
|
|
3243
3295
|
catch {
|
|
3244
3296
|
// Best-effort only — never let a log-write failure surface as an error.
|
|
@@ -3338,11 +3390,23 @@ function cmdCodexRun(hippoRoot, args) {
|
|
|
3338
3390
|
}
|
|
3339
3391
|
async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
3340
3392
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3341
|
-
|
|
3342
|
-
|
|
3393
|
+
// Like the other hooks: project store, else global; a folder with neither must not get one made.
|
|
3394
|
+
const store = hookStoreRoot(hippoRoot);
|
|
3395
|
+
if (!isInitialized(store)) {
|
|
3396
|
+
appendSessionEndCloseLog(logFile ?? null, 'skip: no hippo store for this folder or globally', { startFresh: true });
|
|
3397
|
+
return;
|
|
3343
3398
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3399
|
+
// Sleeping the global store from here would learn this folder's git commits into it; it has its own daily sleep.
|
|
3400
|
+
if (isInitialized(hippoRoot)) {
|
|
3401
|
+
try {
|
|
3402
|
+
await cmdSleep(hippoRoot, logFile ? { 'log-file': logFile } : {});
|
|
3403
|
+
}
|
|
3404
|
+
catch {
|
|
3405
|
+
// sleep errors are already written via cmdSleep
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
else {
|
|
3409
|
+
appendSessionEndCloseLog(logFile ?? null, 'skip sleep: this folder has no store of its own', { startFresh: true });
|
|
3346
3410
|
}
|
|
3347
3411
|
try {
|
|
3348
3412
|
const codexHome = typeof flags['codex-home'] === 'string'
|
|
@@ -3364,6 +3428,8 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3364
3428
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no Codex transcript for this session');
|
|
3365
3429
|
return;
|
|
3366
3430
|
}
|
|
3431
|
+
const digestLog = (message) => appendSessionEndCloseLog(logFile ?? null, message);
|
|
3432
|
+
const scan = readSessionScan(transcriptPath, digestLog);
|
|
3367
3433
|
const captureOpts = {
|
|
3368
3434
|
source: 'last-session',
|
|
3369
3435
|
transcriptPath,
|
|
@@ -3371,8 +3437,17 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3371
3437
|
dryRun: false,
|
|
3372
3438
|
global: false,
|
|
3373
3439
|
tenantId: resolveTenantId({}),
|
|
3440
|
+
originProject: store === hippoRoot ? undefined : deriveOriginProject(process.cwd()),
|
|
3441
|
+
sessionTurns: scan?.turns,
|
|
3374
3442
|
};
|
|
3375
|
-
|
|
3443
|
+
try {
|
|
3444
|
+
cmdCapture(store, captureOpts);
|
|
3445
|
+
}
|
|
3446
|
+
catch {
|
|
3447
|
+
// capture path logs its own failures
|
|
3448
|
+
}
|
|
3449
|
+
// The Codex wrapper passes no session id, so the rollout file names the session.
|
|
3450
|
+
recordSessionDigest(hippoRoot, scan, { key: path.basename(transcriptPath, '.jsonl'), tenantId: resolveTenantId({}), log: digestLog });
|
|
3376
3451
|
}
|
|
3377
3452
|
catch {
|
|
3378
3453
|
// capture path logs its own failures
|
|
@@ -3695,37 +3770,10 @@ function cmdInspect(hippoRoot, id) {
|
|
|
3695
3770
|
console.log(entry.content);
|
|
3696
3771
|
}
|
|
3697
3772
|
function printActiveTaskSnapshot(snapshot) {
|
|
3698
|
-
console.log(
|
|
3699
|
-
console.log(`- Task: ${snapshot.task}`);
|
|
3700
|
-
console.log(`- Status: ${snapshot.status}`);
|
|
3701
|
-
console.log(`- Updated: ${snapshot.updated_at}`);
|
|
3702
|
-
console.log(`- Source: ${snapshot.source}`);
|
|
3703
|
-
if (snapshot.session_id) {
|
|
3704
|
-
console.log(`- Session: ${snapshot.session_id}`);
|
|
3705
|
-
}
|
|
3706
|
-
console.log('');
|
|
3707
|
-
console.log('### Summary');
|
|
3708
|
-
console.log(snapshot.summary);
|
|
3709
|
-
console.log('');
|
|
3710
|
-
console.log('### Next step');
|
|
3711
|
-
console.log(snapshot.next_step);
|
|
3712
|
-
console.log('');
|
|
3773
|
+
console.log(snapshotText(snapshot));
|
|
3713
3774
|
}
|
|
3714
3775
|
function printSessionEvents(events) {
|
|
3715
|
-
|
|
3716
|
-
console.log('No session events found.');
|
|
3717
|
-
return;
|
|
3718
|
-
}
|
|
3719
|
-
const latest = events[events.length - 1];
|
|
3720
|
-
console.log('## Recent Session Trail\n');
|
|
3721
|
-
console.log(`- Session: ${latest.session_id}`);
|
|
3722
|
-
console.log(`- Task: ${latest.task ?? 'n/a'}`);
|
|
3723
|
-
console.log(`- Updated: ${latest.created_at}`);
|
|
3724
|
-
console.log('');
|
|
3725
|
-
for (const event of events) {
|
|
3726
|
-
console.log(`- [${event.created_at}] (${event.event_type}) ${event.content}`);
|
|
3727
|
-
}
|
|
3728
|
-
console.log('');
|
|
3776
|
+
console.log(events.length === 0 ? 'No session events found.' : sessionTrailText(events));
|
|
3729
3777
|
}
|
|
3730
3778
|
function cmdConflicts(hippoRoot, flags) {
|
|
3731
3779
|
requireInit(hippoRoot);
|
|
@@ -3850,6 +3898,12 @@ function cmdReject(hippoRoot, args, flags) {
|
|
|
3850
3898
|
console.log(` Reason: ${reason}`);
|
|
3851
3899
|
if (result.removedIds.length > 0) {
|
|
3852
3900
|
console.log(` Removed ${result.removedIds.length} matching row(s): ${result.removedIds.join(', ')}`);
|
|
3901
|
+
if (result.successorIds.length > 0) {
|
|
3902
|
+
console.log(` Merged rows that held it keep their other texts in: ${result.successorIds.join(', ')}`);
|
|
3903
|
+
}
|
|
3904
|
+
if (result.dormantSuccessorIds.length > 0) {
|
|
3905
|
+
console.log(` Dormant merged rows that held it keep their other texts in: ${result.dormantSuccessorIds.join(', ')}`);
|
|
3906
|
+
}
|
|
3853
3907
|
}
|
|
3854
3908
|
else {
|
|
3855
3909
|
console.log(' No live rows matched (pre-emptive tombstone).');
|
|
@@ -4028,7 +4082,8 @@ function cmdQuarantine(hippoRoot, args, flags) {
|
|
|
4028
4082
|
/**
|
|
4029
4083
|
* `hippo tokens [--days <n>] [--json] [--global]`: the token ledger
|
|
4030
4084
|
* (ROADMAP TE0). Tokens of memory text handed to agents per surface, blocks
|
|
4031
|
-
* the per-prompt hook skipped as unchanged (TE2) and the tokens that saved
|
|
4085
|
+
* the per-prompt hook skipped as unchanged (TE2) and the tokens that saved,
|
|
4086
|
+
* and the hook blocks' tokens later model calls re-read, counted when each session ends.
|
|
4032
4087
|
* Counts are estimates (characters / 4), the same estimate every budget uses.
|
|
4033
4088
|
*/
|
|
4034
4089
|
function cmdTokens(hippoRoot, flags) {
|
|
@@ -4049,17 +4104,22 @@ function cmdTokens(hippoRoot, flags) {
|
|
|
4049
4104
|
console.log(`No memory text recorded in the last ${windowDays} days.`);
|
|
4050
4105
|
return;
|
|
4051
4106
|
}
|
|
4052
|
-
console.log(`Memory text handed to agents, last ${windowDays} days (estimated tokens)\n`);
|
|
4053
|
-
console.log(` ${'surface'.padEnd(14)}${'sent'.padStart(8)}${'tokens'.padStart(12)}${'skipped'.padStart(10)}${'saved'.padStart(12)}`
|
|
4107
|
+
console.log(`Memory text handed to agents, last ${windowDays} days (estimated tokens, characters / 4)\n`);
|
|
4108
|
+
console.log(` ${'surface'.padEnd(14)}${'sent'.padStart(8)}${'tokens'.padStart(12)}${'skipped'.padStart(10)}${'saved'.padStart(12)}`
|
|
4109
|
+
+ `${'re-read'.padStart(12)}`);
|
|
4054
4110
|
for (const row of summary.surfaces) {
|
|
4055
4111
|
console.log(` ${row.surface.padEnd(14)}${String(row.injected).padStart(8)}${String(row.tokens).padStart(12)}`
|
|
4056
|
-
+ `${String(row.skipped).padStart(10)}${String(row.tokensAvoided).padStart(12)}`);
|
|
4112
|
+
+ `${String(row.skipped).padStart(10)}${String(row.tokensAvoided).padStart(12)}${String(row.tokensReread).padStart(12)}`);
|
|
4057
4113
|
}
|
|
4058
4114
|
console.log('');
|
|
4059
4115
|
console.log(` Total sent: ${summary.totalTokens} tokens. Saved by skipping unchanged blocks: ${summary.totalTokensAvoided}.`);
|
|
4116
|
+
console.log(` Re-read by later model calls until compaction: ${summary.totalTokensReread} tokens,`
|
|
4117
|
+
+ ` counted for ${summary.rereadSessions} of ${summary.hookSessions} sessions.`);
|
|
4060
4118
|
if (summary.meanTokensPerSession > 0) {
|
|
4061
4119
|
console.log(` Mean per session (rows with a session id): ${summary.meanTokensPerSession} tokens.`);
|
|
4062
4120
|
}
|
|
4121
|
+
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.');
|
|
4122
|
+
console.log(" Re-reads usually bill at the provider's cached-input rate, a fraction of the full input price.");
|
|
4063
4123
|
}
|
|
4064
4124
|
/** `hippo failures [--days <n>] [--json] [--global]`: failed tool calls by outcome, and repeats across sessions (CD13). */
|
|
4065
4125
|
function cmdFailures(hippoRoot, flags) {
|
|
@@ -4297,47 +4357,7 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
4297
4357
|
process.exit(1);
|
|
4298
4358
|
}
|
|
4299
4359
|
function printHandoff(handoff) {
|
|
4300
|
-
console.log(
|
|
4301
|
-
console.log(`- Session: ${handoff.sessionId}`);
|
|
4302
|
-
console.log(`- Updated: ${handoff.updatedAt}`);
|
|
4303
|
-
if (handoff.taskId)
|
|
4304
|
-
console.log(`- Task: ${handoff.taskId}`);
|
|
4305
|
-
if (handoff.repoRoot)
|
|
4306
|
-
console.log(`- Repo: ${handoff.repoRoot}`);
|
|
4307
|
-
if (handoff.outcome)
|
|
4308
|
-
console.log(`- Outcome: ${handoff.outcome}`);
|
|
4309
|
-
if (handoff.targetRuntime)
|
|
4310
|
-
console.log(`- Target runtime: ${handoff.targetRuntime}`);
|
|
4311
|
-
if (handoff.cardId)
|
|
4312
|
-
console.log(`- Card: ${handoff.cardId}`);
|
|
4313
|
-
console.log('');
|
|
4314
|
-
console.log('### Summary');
|
|
4315
|
-
console.log(handoff.summary);
|
|
4316
|
-
if (handoff.nextAction) {
|
|
4317
|
-
console.log('');
|
|
4318
|
-
console.log('### Next action');
|
|
4319
|
-
console.log(handoff.nextAction);
|
|
4320
|
-
}
|
|
4321
|
-
if (handoff.artifacts && handoff.artifacts.length > 0) {
|
|
4322
|
-
console.log('');
|
|
4323
|
-
console.log('### Artifacts');
|
|
4324
|
-
for (const artifact of handoff.artifacts) {
|
|
4325
|
-
console.log(`- ${artifact}`);
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
if (handoff.constraints && handoff.constraints.length > 0) {
|
|
4329
|
-
console.log('');
|
|
4330
|
-
console.log('### Constraints');
|
|
4331
|
-
for (const constraint of handoff.constraints) {
|
|
4332
|
-
console.log(`- ${constraint}`);
|
|
4333
|
-
}
|
|
4334
|
-
}
|
|
4335
|
-
if (handoff.evidence) {
|
|
4336
|
-
console.log('');
|
|
4337
|
-
console.log('### Evidence');
|
|
4338
|
-
console.log(formatHandoffEvidenceLine(handoff.evidence));
|
|
4339
|
-
}
|
|
4340
|
-
console.log('');
|
|
4360
|
+
console.log(handoffText(handoff));
|
|
4341
4361
|
}
|
|
4342
4362
|
function cmdHandoff(hippoRoot, args, flags) {
|
|
4343
4363
|
requireInit(hippoRoot);
|
|
@@ -6127,7 +6147,7 @@ function cmdGraph(hippoRoot, args, flags) {
|
|
|
6127
6147
|
: process.platform === 'darwin'
|
|
6128
6148
|
? ['open', [out]]
|
|
6129
6149
|
: ['xdg-open', [out]];
|
|
6130
|
-
const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore' });
|
|
6150
|
+
const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true });
|
|
6131
6151
|
// A missing launcher (e.g. xdg-open absent) emits 'error' asynchronously;
|
|
6132
6152
|
// an unhandled 'error' event would throw, so swallow it — the file is
|
|
6133
6153
|
// already written and its path printed above.
|
|
@@ -6390,6 +6410,13 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6390
6410
|
}
|
|
6391
6411
|
}
|
|
6392
6412
|
const currentSessionId = payloadSessionId ?? hostSessionId();
|
|
6413
|
+
// A sub-agent's payload and env both carry its parent's session id, so it books no session and never skips a block.
|
|
6414
|
+
const subagent = isSubagentPayload(stdinText);
|
|
6415
|
+
const ledgerSessionId = subagent ? undefined : currentSessionId;
|
|
6416
|
+
if (subagent)
|
|
6417
|
+
payloadSessionId = undefined;
|
|
6418
|
+
const format = String(flags['format'] ?? 'markdown');
|
|
6419
|
+
const framing = String(flags['framing'] ?? 'observe');
|
|
6393
6420
|
const opts = {
|
|
6394
6421
|
q: query,
|
|
6395
6422
|
budget,
|
|
@@ -6400,6 +6427,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6400
6427
|
crossProject,
|
|
6401
6428
|
currentSessionId,
|
|
6402
6429
|
prompt: payloadPrompt,
|
|
6430
|
+
// JSON is budgeted as the markdown it stands for, so one budget picks the same memories in every format.
|
|
6431
|
+
cost: contextCost(format === 'additional-context' ? 'additional-context' : 'markdown', framing),
|
|
6403
6432
|
};
|
|
6404
6433
|
const result = await api.getContext(ctx, opts);
|
|
6405
6434
|
// Early exit when there's nothing to render (matches pre-extraction behavior).
|
|
@@ -6409,9 +6438,6 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6409
6438
|
(result.recentEvents && result.recentEvents.length > 0);
|
|
6410
6439
|
if (!hasContextData)
|
|
6411
6440
|
return;
|
|
6412
|
-
// Format + framing are CLI rendering concerns; api.getContext doesn't see them.
|
|
6413
|
-
const format = String(flags['format'] ?? 'markdown');
|
|
6414
|
-
const framing = String(flags['framing'] ?? 'observe');
|
|
6415
6441
|
// Adapter: ContextResultEntry -> the print-helper input shape. v39:
|
|
6416
6442
|
// cross-project inclusions (only present under --cross-project or with
|
|
6417
6443
|
// isolation disabled via crossProject) render in their own demarcated
|
|
@@ -6446,7 +6472,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6446
6472
|
});
|
|
6447
6473
|
console.log(jsonText);
|
|
6448
6474
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6449
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6475
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6450
6476
|
event: 'inject', items: output.length, tokens: estimateTokens(jsonText),
|
|
6451
6477
|
}));
|
|
6452
6478
|
}
|
|
@@ -6458,11 +6484,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6458
6484
|
const recallEntries = result.entries.filter((r) => r.promptRecall);
|
|
6459
6485
|
const staticItems = staticEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6460
6486
|
const recallItems = recallEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6461
|
-
|
|
6462
|
-
const staticHeaderTokens = recallItems.length > 0
|
|
6463
|
-
? staticItems.reduce((sum, r) => sum + r.tokens, 0)
|
|
6464
|
-
: result.tokens;
|
|
6465
|
-
const staticBlock = captureConsole(() => {
|
|
6487
|
+
const staticBlock = settleTokens((t) => captureConsole(() => {
|
|
6466
6488
|
if (result.activeSnapshot)
|
|
6467
6489
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6468
6490
|
if (result.sessionHandoff)
|
|
@@ -6470,16 +6492,13 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6470
6492
|
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6471
6493
|
printSessionEvents(result.recentEvents);
|
|
6472
6494
|
}
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
printContextMarkdown(staticItems, staticHeaderTokens, framing, { showStrength: false });
|
|
6477
|
-
}
|
|
6495
|
+
// TE1: no live strength percentage, so an unchanged set of memories renders byte-identically turn after turn.
|
|
6496
|
+
if (staticItems.length > 0)
|
|
6497
|
+
printContextMarkdown(staticItems, t, framing, { showStrength: false });
|
|
6478
6498
|
printCrossProjectSection(staticCrossEntries);
|
|
6479
|
-
});
|
|
6480
|
-
const recallTokens = recallItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
6499
|
+
}));
|
|
6481
6500
|
const recallBlock = recallItems.length > 0
|
|
6482
|
-
? captureConsole(() => printContextMarkdown(recallItems,
|
|
6501
|
+
? settleTokens((t) => captureConsole(() => printContextMarkdown(recallItems, t, framing, { showStrength: false, heading: 'Prompt-Relevant Memory' })))
|
|
6483
6502
|
: '';
|
|
6484
6503
|
if (!staticBlock.trim() && !recallBlock.trim())
|
|
6485
6504
|
return;
|
|
@@ -6524,7 +6543,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6524
6543
|
if (finalStatic) {
|
|
6525
6544
|
try {
|
|
6526
6545
|
recordTokenUse(db, {
|
|
6527
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6546
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface, event: 'inject',
|
|
6528
6547
|
items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
|
|
6529
6548
|
});
|
|
6530
6549
|
}
|
|
@@ -6533,7 +6552,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6533
6552
|
if (recallBlock) {
|
|
6534
6553
|
try {
|
|
6535
6554
|
recordTokenUse(db, {
|
|
6536
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6555
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: 'hook_recall', event: 'inject',
|
|
6537
6556
|
items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
|
|
6538
6557
|
});
|
|
6539
6558
|
}
|
|
@@ -6543,8 +6562,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6543
6562
|
}
|
|
6544
6563
|
}
|
|
6545
6564
|
else {
|
|
6546
|
-
// markdown (default)
|
|
6547
|
-
const text = captureConsole(() => {
|
|
6565
|
+
// markdown (default); the header figure counts the whole block, sections included, as the ledger does.
|
|
6566
|
+
const text = settleTokens((t) => captureConsole(() => {
|
|
6548
6567
|
if (result.activeSnapshot) {
|
|
6549
6568
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6550
6569
|
}
|
|
@@ -6554,18 +6573,17 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6554
6573
|
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6555
6574
|
printSessionEvents(result.recentEvents);
|
|
6556
6575
|
}
|
|
6557
|
-
if (renderItems.length > 0)
|
|
6558
|
-
printContextMarkdown(renderItems,
|
|
6559
|
-
}
|
|
6576
|
+
if (renderItems.length > 0)
|
|
6577
|
+
printContextMarkdown(renderItems, t, framing);
|
|
6560
6578
|
printCrossProjectSection(crossEntries);
|
|
6561
6579
|
if (result.ambientState) {
|
|
6562
6580
|
console.log(`\n${renderAmbientSummary(result.ambientState)}`);
|
|
6563
6581
|
}
|
|
6564
|
-
});
|
|
6582
|
+
}));
|
|
6565
6583
|
if (text.length > 0)
|
|
6566
6584
|
console.log(text);
|
|
6567
6585
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6568
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6586
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6569
6587
|
event: 'inject', items: renderItems.length, tokens: estimateTokens(text),
|
|
6570
6588
|
}));
|
|
6571
6589
|
}
|
|
@@ -6579,7 +6597,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6579
6597
|
*/
|
|
6580
6598
|
function resetHookInjection(hippoRoot, stdinText, requiredSource) {
|
|
6581
6599
|
const sessionId = hookPayloadSessionId(stdinText, requiredSource);
|
|
6582
|
-
|
|
6600
|
+
// A sub-agent's compaction leaves its parent's context, and the blocks in it, as they were.
|
|
6601
|
+
if (sessionId === null || isSubagentPayload(stdinText))
|
|
6583
6602
|
return;
|
|
6584
6603
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6585
6604
|
tenantId: resolveTenantId({}), sessionId, surface: 'hook', event: 'reset', items: 0, tokens: 0,
|
|
@@ -6654,50 +6673,17 @@ function withLedgerDb(hippoRoot, fn) {
|
|
|
6654
6673
|
function printCrossProjectSection(items) {
|
|
6655
6674
|
if (items.length === 0)
|
|
6656
6675
|
return;
|
|
6657
|
-
console.log(
|
|
6658
|
-
for (const item of items)
|
|
6659
|
-
|
|
6660
|
-
const tagStr = item.entry.tags.length > 0 ? ` [${item.entry.tags.join(', ')}]` : '';
|
|
6661
|
-
console.log(`- **[${originLabel}]** ${item.entry.content}${tagStr}`);
|
|
6662
|
-
}
|
|
6676
|
+
console.log(crossProjectHeading(items.length));
|
|
6677
|
+
for (const item of items)
|
|
6678
|
+
console.log(crossProjectLine(item));
|
|
6663
6679
|
}
|
|
6664
6680
|
/** @internal — exported for snapshot tests (tests/cli-context-render-snapshot.test.ts). NOT a stable public API. */
|
|
6665
6681
|
export function printContextMarkdown(items, totalTokens, framing = 'observe', opts = {}) {
|
|
6666
6682
|
const now = evalNow();
|
|
6667
6683
|
const showStrength = opts.showStrength !== false;
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
const e = item.entry;
|
|
6672
|
-
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
6673
|
-
const strengthPct = Math.round(calculateStrength(e) * 100);
|
|
6674
|
-
const strengthStr = showStrength ? ` (${strengthPct}%)` : '';
|
|
6675
|
-
const globalPrefix = item.isGlobal ? '[global] ' : '';
|
|
6676
|
-
const effectiveConf = confidenceFacets(e, now).tier;
|
|
6677
|
-
const label = confidenceLabel(e, now);
|
|
6678
|
-
const confWarning = label.warn ? ' \u26A0\uFE0F' : '';
|
|
6679
|
-
const confTag = `[${label.text}]${confWarning}`;
|
|
6680
|
-
if (framing === 'observe') {
|
|
6681
|
-
const dateStr = new Date(e.created).toISOString().slice(0, 10);
|
|
6682
|
-
if (effectiveConf === 'verified') {
|
|
6683
|
-
// Verified: no date prefix, just the rule
|
|
6684
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6685
|
-
}
|
|
6686
|
-
else if (effectiveConf === 'stale') {
|
|
6687
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6688
|
-
}
|
|
6689
|
-
else {
|
|
6690
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6691
|
-
}
|
|
6692
|
-
}
|
|
6693
|
-
else if (framing === 'suggest') {
|
|
6694
|
-
console.log(`- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6695
|
-
}
|
|
6696
|
-
else {
|
|
6697
|
-
// framing === 'assert': no prefix (bare facts)
|
|
6698
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6699
|
-
}
|
|
6700
|
-
}
|
|
6684
|
+
console.log(contextHeading(opts.heading ?? 'Project Memory', items.length, totalTokens));
|
|
6685
|
+
for (const item of items)
|
|
6686
|
+
console.log(contextLine(item, framing, showStrength, now));
|
|
6701
6687
|
}
|
|
6702
6688
|
function autoDetectContext() {
|
|
6703
6689
|
// Try git diff --name-only for changed files
|
|
@@ -6705,6 +6691,7 @@ function autoDetectContext() {
|
|
|
6705
6691
|
const diff = execSync('git diff --name-only HEAD 2>&1', {
|
|
6706
6692
|
encoding: 'utf8',
|
|
6707
6693
|
timeout: 3000,
|
|
6694
|
+
windowsHide: true,
|
|
6708
6695
|
}).trim();
|
|
6709
6696
|
if (diff) {
|
|
6710
6697
|
// Extract meaningful terms from file paths
|
|
@@ -6720,6 +6707,7 @@ function autoDetectContext() {
|
|
|
6720
6707
|
const branch = execSync('git branch --show-current 2>&1', {
|
|
6721
6708
|
encoding: 'utf8',
|
|
6722
6709
|
timeout: 3000,
|
|
6710
|
+
windowsHide: true,
|
|
6723
6711
|
}).trim();
|
|
6724
6712
|
if (branch && branch !== 'main' && branch !== 'master') {
|
|
6725
6713
|
return branch.replace(/[-_\/]/g, ' ');
|
|
@@ -6848,9 +6836,7 @@ async function cmdWatch(command, hippoRoot) {
|
|
|
6848
6836
|
try {
|
|
6849
6837
|
writeEntry(hippoRoot, entry);
|
|
6850
6838
|
updateStats(hippoRoot, { remembered: 1 });
|
|
6851
|
-
|
|
6852
|
-
embedMemory(hippoRoot, entry).catch(() => { });
|
|
6853
|
-
}
|
|
6839
|
+
void embedMemory(hippoRoot, entry);
|
|
6854
6840
|
const preview = stderr.trim().slice(0, 80);
|
|
6855
6841
|
console.error(`\nHippo learned from failure: "${preview}"`);
|
|
6856
6842
|
}
|
|
@@ -6898,7 +6884,7 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6898
6884
|
// parsed lesson with the gate on the write alone.
|
|
6899
6885
|
//
|
|
6900
6886
|
// Round 2 then found the cure was worse. STORAGE is what makes invalidation
|
|
6901
|
-
// idempotent here: a stored lesson is recognised by
|
|
6887
|
+
// idempotent here: a stored lesson is recognised by its same-text key on
|
|
6902
6888
|
// the next scan and short-circuits before invalidating again. A lesson that
|
|
6903
6889
|
// invalidates but is never stored has no such record, so every rescan
|
|
6904
6890
|
// re-invalidates, and invalidateMatching halves half_life_days each time.
|
|
@@ -6926,9 +6912,9 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6926
6912
|
let rejected = 0;
|
|
6927
6913
|
const gitLearnTags = ['error', 'git-learned'];
|
|
6928
6914
|
const existingForSchema = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
6915
|
+
const keys = storedTextKeys(existingForSchema);
|
|
6929
6916
|
for (const lesson of lessons) {
|
|
6930
|
-
|
|
6931
|
-
if (deduplicateLesson(existingForSchema, lesson, 0.7, resolveTenantId({}))) {
|
|
6917
|
+
if (keys.has(duplicateKey(lesson))) {
|
|
6932
6918
|
skipped++;
|
|
6933
6919
|
continue;
|
|
6934
6920
|
}
|
|
@@ -6966,9 +6952,8 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6966
6952
|
throw err;
|
|
6967
6953
|
}
|
|
6968
6954
|
updateStats(hippoRoot, { remembered: 1 });
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
}
|
|
6955
|
+
keys.add(duplicateKey(lesson));
|
|
6956
|
+
void embedMemory(hippoRoot, entry);
|
|
6972
6957
|
added++;
|
|
6973
6958
|
}
|
|
6974
6959
|
console.log(`${prefix}${added} new lessons added, ${skipped} duplicates skipped` +
|
|
@@ -7251,6 +7236,13 @@ never as a closing step:
|
|
|
7251
7236
|
hippo remember "<description of what went wrong>" --error
|
|
7252
7237
|
\`\`\`
|
|
7253
7238
|
|
|
7239
|
+
When you learn something that should outlive this session (a decision and
|
|
7240
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7241
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7242
|
+
\`\`\`bash
|
|
7243
|
+
hippo remember "<what you learned and why>"
|
|
7244
|
+
\`\`\`
|
|
7245
|
+
|
|
7254
7246
|
When Hippo's Codex wrapper is installed, session-end capture runs automatically.
|
|
7255
7247
|
If the wrapper is not installed, capture a brief summary manually:
|
|
7256
7248
|
\`\`\`bash
|
|
@@ -7276,6 +7268,13 @@ never as a closing step:
|
|
|
7276
7268
|
hippo remember "<description of what went wrong>" --error
|
|
7277
7269
|
\`\`\`
|
|
7278
7270
|
|
|
7271
|
+
When you learn something that should outlive this session (a decision and
|
|
7272
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7273
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7274
|
+
\`\`\`bash
|
|
7275
|
+
hippo remember "<what you learned and why>"
|
|
7276
|
+
\`\`\`
|
|
7277
|
+
|
|
7279
7278
|
When ending a session, capture a brief summary:
|
|
7280
7279
|
\`\`\`bash
|
|
7281
7280
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7300,6 +7299,13 @@ never as a closing step:
|
|
|
7300
7299
|
hippo remember "<description of what went wrong>" --error
|
|
7301
7300
|
\`\`\`
|
|
7302
7301
|
|
|
7302
|
+
When you learn something that should outlive this session (a decision and
|
|
7303
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7304
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7305
|
+
\`\`\`bash
|
|
7306
|
+
hippo remember "<what you learned and why>"
|
|
7307
|
+
\`\`\`
|
|
7308
|
+
|
|
7303
7309
|
When ending a session, capture a brief summary:
|
|
7304
7310
|
\`\`\`bash
|
|
7305
7311
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7318,10 +7324,17 @@ hippo context --auto --budget 1500
|
|
|
7318
7324
|
\`\`\`
|
|
7319
7325
|
Read the output before writing any code.
|
|
7320
7326
|
|
|
7321
|
-
|
|
7322
|
-
|
|
7327
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
7328
|
+
never as a closing step:
|
|
7323
7329
|
\`\`\`bash
|
|
7324
|
-
hippo remember "<
|
|
7330
|
+
hippo remember "<description of what went wrong>" --error
|
|
7331
|
+
\`\`\`
|
|
7332
|
+
|
|
7333
|
+
When you learn something that should outlive this session (a decision and
|
|
7334
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7335
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7336
|
+
\`\`\`bash
|
|
7337
|
+
hippo remember "<what you learned and why>"
|
|
7325
7338
|
\`\`\`
|
|
7326
7339
|
|
|
7327
7340
|
When stuck or repeating yourself, check if this happened before:
|
|
@@ -7353,6 +7366,13 @@ never as a closing step:
|
|
|
7353
7366
|
hippo remember "<description of what went wrong>" --error
|
|
7354
7367
|
\`\`\`
|
|
7355
7368
|
|
|
7369
|
+
When you learn something that should outlive this session (a decision and
|
|
7370
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
7371
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
7372
|
+
\`\`\`bash
|
|
7373
|
+
hippo remember "<what you learned and why>"
|
|
7374
|
+
\`\`\`
|
|
7375
|
+
|
|
7356
7376
|
When ending a session, capture a brief summary:
|
|
7357
7377
|
\`\`\`bash
|
|
7358
7378
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7371,14 +7391,19 @@ const SHIPPED_HOOK_HASHES = new Map([
|
|
|
7371
7391
|
['15abcece9712279fb4721f7a8f0ba117457400278977beb5cf5b5d7ba49f7b1a', 'codex'],
|
|
7372
7392
|
['0c81a6b2c21473313001f624b80ea870e661aecbfda9bfe8503febc0d5f34533', 'codex'],
|
|
7373
7393
|
['88e45358aba4f17912f113221c991dc758275991335d1daa4aa1974a69c46769', 'codex'],
|
|
7394
|
+
['e61632fe177450a06541c148a9a4f9182530d8df667806927a99792825903298', 'codex'],
|
|
7374
7395
|
['a1415ecda9b2f8f317c233738e4a5ac16e6b2cc385a017c0c8ecfbfacbcab6a3', 'cursor'],
|
|
7375
7396
|
['a38c428bbdfc14ec50f6f7b9183785170a4eae1ce9cde60257cca6efc7206b3a', 'cursor'],
|
|
7397
|
+
['0ec9f556abfd55e94f9e6fb47ece0fc5acb841977d144b35a2371e03645d8636', 'cursor'],
|
|
7376
7398
|
['40524c3bd5a2eb04036567cc761451961d950995768bccd93a9900b0f75eafea', 'openclaw'],
|
|
7377
7399
|
['7b3518e8c0feaa7b8b454cde7743f7598ad14cd9979e1680d0954484e2464aae', 'openclaw'],
|
|
7400
|
+
['1137dcf04568caf011e41db77bc55324faee88bc29c3a5fcc98ab687cd952a16', 'openclaw'],
|
|
7378
7401
|
['4601c67c31f41cd5b1324cfccdb1afc66872b7fb0bc1e7c5789ecabb1f6bd942', 'opencode'],
|
|
7379
7402
|
['90d9e21d8d1ecbe99a0fc7b7f2d9f8af7b5315a6b4b0203df4f7a9bdc0699b98', 'opencode'],
|
|
7403
|
+
['ca4e00284f1397ed2f2fcc53210c27f63b90edf6b37fd66dad5ee58b94ea3eee', 'opencode'],
|
|
7380
7404
|
['8b8f5986d7f7ed15f06e68720d8913c3cab23d94366b411935ca2bbaa334553b', 'pi'],
|
|
7381
7405
|
['37767b355e18beac726b05b9e2b898dab8c6135fd7b98f3aa52edc734d5dd283', 'pi'],
|
|
7406
|
+
['6e85a5cccb3cfeaa9a080713754936db730f96376f94cc9a9888a746149c7268', 'pi'],
|
|
7382
7407
|
]);
|
|
7383
7408
|
function cmdHook(args, flags) {
|
|
7384
7409
|
const subcommand = args[0];
|
|
@@ -7482,9 +7507,16 @@ function cmdHook(args, flags) {
|
|
|
7482
7507
|
}
|
|
7483
7508
|
}
|
|
7484
7509
|
else if (target === 'codex') {
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7510
|
+
installCodexMemoryHooks('');
|
|
7511
|
+
// The wrapper stays the capture path; the hooks above work without it, so a missing launcher is not an error.
|
|
7512
|
+
if (detectRealCodexPath()) {
|
|
7513
|
+
const result = installCodexWrapper();
|
|
7514
|
+
console.log(`Installed Codex session-end integration -> ${result.metadataPath}`);
|
|
7515
|
+
console.log(` Wrapped detected Codex launcher at ${result.commandPath}`);
|
|
7516
|
+
}
|
|
7517
|
+
else {
|
|
7518
|
+
console.log('No codex launcher on PATH, so session-end capture was not set up; re-run once `codex` is on PATH.');
|
|
7519
|
+
}
|
|
7488
7520
|
}
|
|
7489
7521
|
return;
|
|
7490
7522
|
}
|
|
@@ -7528,6 +7560,9 @@ function cmdHook(args, flags) {
|
|
|
7528
7560
|
}
|
|
7529
7561
|
}
|
|
7530
7562
|
else if (target === 'codex') {
|
|
7563
|
+
if (uninstallJsonHooks('codex')) {
|
|
7564
|
+
console.log(`Removed hippo's Codex memory hooks from ${resolveJsonHookPaths('codex').settings}`);
|
|
7565
|
+
}
|
|
7531
7566
|
if (uninstallCodexWrapper()) {
|
|
7532
7567
|
console.log('Removed Codex wrapper integration');
|
|
7533
7568
|
}
|
|
@@ -7617,10 +7652,13 @@ function cmdSetup(flags) {
|
|
|
7617
7652
|
}
|
|
7618
7653
|
for (const tool of wrapperTools) {
|
|
7619
7654
|
if (dryRun) {
|
|
7655
|
+
if (tool.name === 'codex')
|
|
7656
|
+
console.log(`[dry-run] would install Codex memory hooks in ${resolveJsonHookPaths('codex').settings}`);
|
|
7620
7657
|
console.log(`[dry-run] would wrap the detected ${tool.name} launcher in place`);
|
|
7621
7658
|
continue;
|
|
7622
7659
|
}
|
|
7623
7660
|
if (tool.name === 'codex') {
|
|
7661
|
+
installCodexMemoryHooks(` ${tool.name.padEnd(14)} `);
|
|
7624
7662
|
const result = ensureCodexWrapperInstalled();
|
|
7625
7663
|
if (result.status === 'installed') {
|
|
7626
7664
|
console.log(` ${tool.name.padEnd(14)} wrapped launcher -> ${result.commandPath}`);
|
|
@@ -7883,17 +7921,20 @@ function cmdAssemble(hippoRoot, sessionId, flags) {
|
|
|
7883
7921
|
...(Number.isFinite(freshTailCount) && freshTailCount >= 0 ? { freshTailCount } : {}),
|
|
7884
7922
|
summarizeOlder,
|
|
7885
7923
|
...(scope !== undefined ? { scope } : {}),
|
|
7924
|
+
cost: assembleCost(sessionId),
|
|
7886
7925
|
});
|
|
7887
7926
|
if (flags['json']) {
|
|
7888
7927
|
console.log(JSON.stringify(r, null, 2));
|
|
7889
7928
|
return;
|
|
7890
7929
|
}
|
|
7891
|
-
console.log(
|
|
7892
|
-
|
|
7893
|
-
const
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7930
|
+
console.log(settleTokens((t) => captureConsole(() => {
|
|
7931
|
+
console.log(assembleHeading({ ...r, items: r.items.length, tokens: t }));
|
|
7932
|
+
for (const it of r.items) {
|
|
7933
|
+
const prefix = it.isSummary ? '[summary]' : it.isFreshTail ? '[tail]' : '[older]';
|
|
7934
|
+
const head = it.content.slice(0, 120);
|
|
7935
|
+
console.log(` ${prefix} ${it.createdAt} ${it.id} \u2014 ${head}${it.content.length > 120 ? '…' : ''}`);
|
|
7936
|
+
}
|
|
7937
|
+
})));
|
|
7897
7938
|
}
|
|
7898
7939
|
function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
7899
7940
|
requireInit(hippoRoot);
|
|
@@ -7920,6 +7961,7 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
|
7920
7961
|
...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
|
|
7921
7962
|
...(Number.isFinite(budget) && budget > 0 ? { budget } : {}),
|
|
7922
7963
|
...(depth !== undefined ? { depth } : {}),
|
|
7964
|
+
cost: drillCost,
|
|
7923
7965
|
});
|
|
7924
7966
|
if ('failure' in r) {
|
|
7925
7967
|
// v1.6.4: only `not_drillable` is caller-actionable. `not_found`
|
|
@@ -8124,65 +8166,7 @@ function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
|
|
|
8124
8166
|
// ---------------------------------------------------------------------------
|
|
8125
8167
|
// Audit log subcommands (A5 stub auth — `hippo audit list`)
|
|
8126
8168
|
// ---------------------------------------------------------------------------
|
|
8127
|
-
const VALID_AUDIT_OPS = new Set(
|
|
8128
|
-
'remember',
|
|
8129
|
-
'recall',
|
|
8130
|
-
'promote',
|
|
8131
|
-
'supersede',
|
|
8132
|
-
'forget',
|
|
8133
|
-
'archive_raw',
|
|
8134
|
-
'auth_revoke',
|
|
8135
|
-
'auth_create', // v1.12.4: emitted by api.authCreate
|
|
8136
|
-
'outcome', // v1.11.5: pre-existing drift — emitted today but rejected by old Set
|
|
8137
|
-
'consolidate', // v1.11.5: emitted by api.sleep / hippo sleep
|
|
8138
|
-
'audit_prune', // v1.12.9: emitted by pruneAuditLog
|
|
8139
|
-
'summary_marked_dirty', // v0.30 / E1 — lockstep with AuditOp union + server.ts VALID_AUDIT_OPS (v1.11.5 CRIT A institutional rule)
|
|
8140
|
-
'summary_marked_clean', // v0.30 / E3 — buildDag post-link clean op; lockstep
|
|
8141
|
-
'summary_rebuilt', // v0.30 / E3 — sleep-cycle rebuild op; lockstep
|
|
8142
|
-
'predict_create', // v0.31 / E2 prediction first-class object — emitted by savePrediction
|
|
8143
|
-
'predict_close', // v0.31 / E2 — emitted by closePrediction
|
|
8144
|
-
'predict_baserate', // v0.31 / J3 — emitted by computePredictionBaserate
|
|
8145
|
-
'recall_autodebias_hint', // v0.32 / J3.2 — emitted by computePlanningFallacyHint on success
|
|
8146
|
-
'recall_autodebias_hint_no_class_match', // v0.32 / J3.2 — telemetry: forward-claim, no class scored
|
|
8147
|
-
'recall_autodebias_hint_tiebreak', // v0.32 / J3.2 — telemetry: forward-claim, >=2 classes tied
|
|
8148
|
-
'recall_anchor_detected_query_repeat', // v0.33 / J1 — emitted by detector on R1 fire
|
|
8149
|
-
'recall_anchor_detected_memory_dominance', // v0.33 / J1 — emitted by detector on R2 fire
|
|
8150
|
-
'recall_anchor_skipped_no_session', // v0.33 / J1 — telemetry: no sessionId, ring skipped
|
|
8151
|
-
'recall_availability_detected', // v1.13.x / J2 - emitted when availability/recency-bias hint fires
|
|
8152
|
-
'decision_create', // E2 decision first-class object — emitted by saveDecision
|
|
8153
|
-
'decision_supersede', // E2 — emitted by saveDecision when --supersedes resolves to an active decision row
|
|
8154
|
-
'decision_close', // E2 — emitted by closeDecision
|
|
8155
|
-
'incident_open', // E2 incident first-class object — emitted by saveIncident
|
|
8156
|
-
'incident_resolve', // E2 — emitted by resolveIncident (open -> resolved)
|
|
8157
|
-
'incident_close', // E2 — emitted by closeIncident (open|resolved -> closed)
|
|
8158
|
-
'process_create', // E2 process first-class object — emitted by saveProcess
|
|
8159
|
-
'process_supersede', // E2 — emitted by saveProcess on a supersession
|
|
8160
|
-
'process_close', // E2 — emitted by closeProcess
|
|
8161
|
-
'policy_create', // E2 policy first-class object — emitted by savePolicy
|
|
8162
|
-
'policy_supersede', // E2 — emitted by savePolicy on a supersession
|
|
8163
|
-
'policy_close', // E2 — emitted by closePolicy
|
|
8164
|
-
'skill_create', // E2 skill first-class object — emitted by saveSkill
|
|
8165
|
-
'skill_supersede', // E2 — emitted by saveSkill on a supersession
|
|
8166
|
-
'skill_close', // E2 — emitted by closeSkill
|
|
8167
|
-
'project_brief_create', // E2 project_brief first-class object — emitted by saveProjectBrief
|
|
8168
|
-
'project_brief_supersede', // E2 — emitted by saveProjectBrief on a supersession (incl. refresh)
|
|
8169
|
-
'project_brief_close', // E2 — emitted by closeProjectBrief
|
|
8170
|
-
'customer_note_create', // E2 customer_note first-class object — emitted by saveCustomerNote
|
|
8171
|
-
'customer_note_supersede', // E2 — emitted by saveCustomerNote on a supersession
|
|
8172
|
-
'customer_note_close', // E2 — emitted by closeCustomerNote
|
|
8173
|
-
'mv_rescue', // LC2-E3 — emitted by consolidate() per rescue; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8174
|
-
'reject_value', // AT1 — emitted by `hippo reject`; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8175
|
-
'reject_refusal', // AT1 — emitted when the rejection guard refuses a write; lockstep
|
|
8176
|
-
'unreject_value', // AT1 — emitted by `hippo unreject`; lockstep
|
|
8177
|
-
'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
|
|
8178
|
-
'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
|
|
8179
|
-
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8180
|
-
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8181
|
-
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8182
|
-
'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8183
|
-
'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
|
|
8184
|
-
'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
|
|
8185
|
-
]);
|
|
8169
|
+
const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
|
|
8186
8170
|
function formatAuditRow(ev) {
|
|
8187
8171
|
const target = ev.targetId ?? '-';
|
|
8188
8172
|
const meta = JSON.stringify(ev.metadata ?? {});
|
|
@@ -8708,7 +8692,7 @@ function cmdSlack(hippoRoot, args, flags) {
|
|
|
8708
8692
|
}
|
|
8709
8693
|
export function usageText() {
|
|
8710
8694
|
return `
|
|
8711
|
-
Hippo - memory for AI agents that learns what is wrong and
|
|
8695
|
+
Hippo - memory for AI agents that learns what is wrong and ranks it down
|
|
8712
8696
|
|
|
8713
8697
|
Usage: hippo <command> [options]
|
|
8714
8698
|
|
|
@@ -8718,6 +8702,7 @@ Commands:
|
|
|
8718
8702
|
--days <n> Days of git history to seed (default: 365 for --scan, 30 for single)
|
|
8719
8703
|
--global Init the global store ($HIPPO_HOME or ~/.hippo/)
|
|
8720
8704
|
--no-hooks Skip auto-detecting and installing agent hooks
|
|
8705
|
+
(HIPPO_SKIP_AUTO_INTEGRATIONS=1 does the same)
|
|
8721
8706
|
--no-schedule Skip auto-creating the machine-level daily runner
|
|
8722
8707
|
--no-learn Skip seeding memories from git history
|
|
8723
8708
|
remember <text> Store a memory
|
|
@@ -8733,7 +8718,7 @@ Commands:
|
|
|
8733
8718
|
--tag <tag> Tag for the new memory (repeatable; default: the old memory's tags)
|
|
8734
8719
|
--pin Pin the new memory (default: pinned if the old one was)
|
|
8735
8720
|
recall <query> Search and retrieve memories (local + global)
|
|
8736
|
-
--budget <n> Token budget (default: 4000)
|
|
8721
|
+
--budget <n> Token budget for the whole printed block (default: 4000)
|
|
8737
8722
|
--min-results <n> Minimum results regardless of budget (default: 1)
|
|
8738
8723
|
--json Output as JSON
|
|
8739
8724
|
--why Show match reasons and source annotations
|
|
@@ -8814,7 +8799,7 @@ Commands:
|
|
|
8814
8799
|
handoff/events (use 'hippo session resume' for
|
|
8815
8800
|
the explicit handoff-without-snapshot path).
|
|
8816
8801
|
explain <query> Show full score breakdown for each retrieved memory
|
|
8817
|
-
--budget <n> Token budget (default: 4000)
|
|
8802
|
+
--budget <n> Token budget, counted as recall prints (default: 4000)
|
|
8818
8803
|
--limit <n> Cap the number of results displayed
|
|
8819
8804
|
--json Output as JSON
|
|
8820
8805
|
--physics | --classic Force search mode (default: from config)
|
|
@@ -8845,20 +8830,23 @@ Commands:
|
|
|
8845
8830
|
--json Output full summary as JSON
|
|
8846
8831
|
context Smart context injection for AI agents
|
|
8847
8832
|
--auto Auto-detect task from git state
|
|
8848
|
-
--budget <n> Token budget (default: 1500)
|
|
8833
|
+
--budget <n> Token budget for the whole printed block (default: 1500)
|
|
8849
8834
|
--pinned-only Only inject pinned memories (used by UserPromptSubmit hook)
|
|
8850
8835
|
--include-recent <n> With --pinned-only, also inject the last N writes regardless of pinning
|
|
8851
8836
|
(the hook payload's "prompt" drives prompt recall instead of --include-recent when pinnedInject.promptRecall is on)
|
|
8852
8837
|
--format <fmt> Output format: markdown (default), json, or additional-context (Claude Code hook JSON)
|
|
8853
8838
|
--framing <mode> Framing: observe (default), suggest, assert
|
|
8854
8839
|
sleep Run consolidation pass (auto-learns + dedup + auto-shares)
|
|
8840
|
+
Runs at Claude Code and OpenCode session end and in the daily job.
|
|
8841
|
+
With ANTHROPIC_API_KEY set it sends memory text to Anthropic for
|
|
8842
|
+
fact extraction; {"extraction":{"enabled":false}} turns that off
|
|
8855
8843
|
--dry-run Preview without writing
|
|
8856
8844
|
--no-learn Skip auto git-learn before consolidation
|
|
8857
8845
|
--no-share Skip auto-sharing to global store
|
|
8858
8846
|
daily-runner Sweep registered workspaces and run daily learn+sleep
|
|
8859
8847
|
dedup Remove duplicate memories (keeps stronger copy)
|
|
8860
8848
|
--dry-run Preview without removing
|
|
8861
|
-
--threshold <n>
|
|
8849
|
+
--threshold <n> Ignored, kept for old scripts: a duplicate is the same text apart from spacing
|
|
8862
8850
|
status Show memory health stats
|
|
8863
8851
|
audit [--fix] Check memory quality (--fix removes junk)
|
|
8864
8852
|
github GitHub connector subcommands (backfill, dlq)
|
|
@@ -8882,10 +8870,10 @@ Commands:
|
|
|
8882
8870
|
--stats Count memories per DAG level instead
|
|
8883
8871
|
drill <summary-id> Walk down a DAG level-2 summary to its children
|
|
8884
8872
|
--limit N Cap children list (default 50)
|
|
8885
|
-
--budget N
|
|
8873
|
+
--budget N Token budget for the printed children (≈ chars/4)
|
|
8886
8874
|
--json Output as JSON
|
|
8887
8875
|
assemble --session <id> Build a session's chronological context window
|
|
8888
|
-
--budget N Token budget (default 4000)
|
|
8876
|
+
--budget N Token budget for the printed window (default 4000)
|
|
8889
8877
|
--fresh-tail N Recent rows always kept verbatim (default 10)
|
|
8890
8878
|
--no-summarize-older Disable older-row summary substitution
|
|
8891
8879
|
--scope <s> Restrict to exact scope (default: deny *:private:*)
|
|
@@ -8936,8 +8924,10 @@ Commands:
|
|
|
8936
8924
|
--out <file> Where to write it (default: hippo-support-<time>.json here)
|
|
8937
8925
|
--include-logs Add the last ${TAIL_MAX_LINES} lines of each hippo log, known secret shapes removed
|
|
8938
8926
|
tokens Tokens of memory text hippo handed agents, per surface
|
|
8939
|
-
(hook, context, recall, MCP, HTTP),
|
|
8940
|
-
unchanged hook blocks saved
|
|
8927
|
+
(hook, compact-resume, context, recall, MCP, HTTP), what
|
|
8928
|
+
skipping unchanged hook blocks saved, and how much of the
|
|
8929
|
+
hook and compact-resume blocks later model calls re-read,
|
|
8930
|
+
counted when a session ends. Estimates (characters / 4)
|
|
8941
8931
|
--days <n> Window in days (default: 30)
|
|
8942
8932
|
--json Output as JSON
|
|
8943
8933
|
--global Operate on the global store
|
|
@@ -9057,34 +9047,40 @@ Commands:
|
|
|
9057
9047
|
capture Extract memories from conversation text
|
|
9058
9048
|
--stdin Read from piped input
|
|
9059
9049
|
--file <path> Read from a file
|
|
9060
|
-
--last-session Read
|
|
9050
|
+
--last-session Read the transcript a hook names on stdin, else the newest
|
|
9051
|
+
Claude Code one from any project
|
|
9061
9052
|
--transcript <path> Explicit transcript path (implies --last-session)
|
|
9062
9053
|
--log-file <path> Tee output to a log file (paired with 'hippo last-sleep')
|
|
9063
9054
|
--dry-run Preview without writing
|
|
9064
9055
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
9065
|
-
setup One-shot: detect installed AI tools and install
|
|
9066
|
-
|
|
9056
|
+
setup One-shot: detect installed AI tools and install their hooks:
|
|
9057
|
+
claude-code gets 7 hooks in ~/.claude/settings.json, opencode
|
|
9058
|
+
a plugin, codex 2 hooks in its hooks.json plus a launcher
|
|
9059
|
+
wrapper; other tools get a hint
|
|
9067
9060
|
--all Install for every JSON-hook tool, even if not detected
|
|
9068
9061
|
--dry-run Show what would be installed without writing
|
|
9069
9062
|
--no-schedule Skip installing or repairing the daily runner
|
|
9070
|
-
last-sleep Print the last 'hippo sleep --log-file' output and clear it
|
|
9063
|
+
last-sleep Print the last 'hippo sleep --log-file' output to stderr and clear it
|
|
9071
9064
|
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
9072
9065
|
--keep Print without clearing
|
|
9073
|
-
session-end SessionEnd hook:
|
|
9066
|
+
session-end SessionEnd hook: count this session's re-read tokens, run sleep, then
|
|
9067
|
+
capture from the session's last 20 user and 10 assistant messages,
|
|
9068
|
+
in a detached worker
|
|
9074
9069
|
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
|
|
9075
9070
|
pre-compact PreCompact hook: save a working-state snapshot before compaction
|
|
9076
9071
|
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
9077
|
-
compact-resume SessionStart(compact) hook: re-print the snapshot
|
|
9072
|
+
compact-resume SessionStart(compact) hook: re-print the snapshot, if under 15 minutes old
|
|
9078
9073
|
post-compact PostCompact hook: tell the user what pre-compact saved
|
|
9079
9074
|
--log-file <p> Same log path as pre-compact (default: ~/.hippo/logs/pre-compact.log)
|
|
9080
9075
|
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
9081
9076
|
hook <sub> [target] Manage framework integrations
|
|
9082
9077
|
hook list Show available hooks
|
|
9083
9078
|
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
9084
|
-
claude-code
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9079
|
+
claude-code adds 7 hooks to ~/.claude/settings.json;
|
|
9080
|
+
opencode installs a plugin; codex adds 2 hooks to
|
|
9081
|
+
$CODEX_HOME/hooks.json (trust them once in /hooks) and
|
|
9082
|
+
wraps the detected launcher in place; all but claude-code
|
|
9083
|
+
also patch an existing AGENTS.md
|
|
9088
9084
|
hook uninstall <target> Remove hook
|
|
9089
9085
|
predict "<claim>" Record a prediction to score against the actual outcome later
|
|
9090
9086
|
--class <c> Reference class (required)
|
|
@@ -9205,7 +9201,7 @@ Commands:
|
|
|
9205
9201
|
wm clear Clear working memory entries
|
|
9206
9202
|
--scope <scope> Filter by scope
|
|
9207
9203
|
--session <id> Filter by session
|
|
9208
|
-
wm flush
|
|
9204
|
+
wm flush Same as clear; nothing runs it at session end
|
|
9209
9205
|
--scope <scope> Filter by scope
|
|
9210
9206
|
--session <id> Filter by session
|
|
9211
9207
|
dashboard Open web dashboard for memory health
|
|
@@ -9447,10 +9443,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9447
9443
|
const rememberKindRaw = typeof flags['kind'] === 'string' ? flags['kind'].toLowerCase() : undefined;
|
|
9448
9444
|
const rememberKindAllowed = ['distilled', 'superseded'];
|
|
9449
9445
|
if (rememberKindRaw === undefined || rememberKindAllowed.includes(rememberKindRaw)) {
|
|
9450
|
-
const
|
|
9451
|
-
const tags = Array.isArray(tagsRaw)
|
|
9452
|
-
? tagsRaw.map(String)
|
|
9453
|
-
: typeof tagsRaw === 'string' ? [tagsRaw] : undefined;
|
|
9446
|
+
const tags = rememberTags(flags, process.cwd()).all;
|
|
9454
9447
|
// B2 v1.12.6 — validate --owner on the thin-client path too.
|
|
9455
9448
|
// Failure on this path exits early so the user gets the same
|
|
9456
9449
|
// validation experience whether or not a server is up.
|