hippo-memory 1.52.8 → 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 +158 -98
- package/dist/api.d.ts +51 -18
- package/dist/api.js +121 -76
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/capture.d.ts +37 -0
- package/dist/capture.js +111 -81
- package/dist/cli.js +627 -654
- 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/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/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 +5 -3
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +122 -27
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +171 -210
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +6 -0
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +10 -2
- package/dist/shared.js +35 -30
- package/dist/store.d.ts +1 -0
- package/dist/store.js +4 -0
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -38,23 +38,24 @@ import * as fs from 'fs';
|
|
|
38
38
|
import * as os from 'os';
|
|
39
39
|
import { fileURLToPath } from 'node:url';
|
|
40
40
|
import { execFileSync, execSync, spawn, spawnSync } from 'child_process';
|
|
41
|
-
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
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 { realpathOrResolve, 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
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';
|
|
@@ -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)
|
|
@@ -553,6 +620,15 @@ function cmdInit(hippoRoot, flags) {
|
|
|
553
620
|
}
|
|
554
621
|
}
|
|
555
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
|
+
}
|
|
556
632
|
/** Plain init: patch the detected agents' instruction files in cwd, then install their user-level hooks. */
|
|
557
633
|
function autoInstallHooks() {
|
|
558
634
|
const cwd = process.cwd();
|
|
@@ -628,13 +704,32 @@ function refreshShippedBlock(filePath, text, hook) {
|
|
|
628
704
|
fs.writeFileSync(filePath, `${text.slice(0, start)}${eol}${HOOKS[owner].content.replace(/\n/g, eol)}${eol}${text.slice(end)}`, 'utf8');
|
|
629
705
|
console.log(` Refreshed the ${owner} hippo block in ${name}`);
|
|
630
706
|
}
|
|
631
|
-
/**
|
|
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. */
|
|
632
724
|
function installUserLevelHooks(agents, codexHint) {
|
|
633
725
|
for (const hook of agents) {
|
|
634
726
|
// The Codex capture wrapper swaps the codex launcher binary, so init only points at the opt-in (issue #133).
|
|
635
727
|
if (hook === 'codex' && codexHint && !isCodexWrapperInstalled()) {
|
|
636
728
|
console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
|
|
637
729
|
}
|
|
730
|
+
// Checked first so init never creates ~/.codex on a machine without Codex.
|
|
731
|
+
if (hook === 'codex' && isCodexPresent())
|
|
732
|
+
installCodexMemoryHooks(' ');
|
|
638
733
|
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
639
734
|
// settings.json. Keeps `hippo init` in lockstep with `hippo hook install
|
|
640
735
|
// claude-code` and `hippo setup`.
|
|
@@ -719,13 +814,13 @@ function setupDailySchedule(globalRoot) {
|
|
|
719
814
|
// Task doesn't exist, create it
|
|
720
815
|
}
|
|
721
816
|
try {
|
|
722
|
-
|
|
817
|
+
execFileSync('schtasks', buildSchtasksCreateArgs(taskName, cmd), { stdio: 'pipe', windowsHide: true });
|
|
723
818
|
console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
|
|
724
819
|
}
|
|
725
820
|
catch {
|
|
726
821
|
// No admin rights or schtasks unavailable, fall back to printing instructions
|
|
727
822
|
console.log(` To schedule the machine-level daily runner, run:`);
|
|
728
|
-
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`);
|
|
729
824
|
}
|
|
730
825
|
}
|
|
731
826
|
else {
|
|
@@ -748,6 +843,25 @@ function setupDailySchedule(globalRoot) {
|
|
|
748
843
|
}
|
|
749
844
|
}
|
|
750
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
|
+
}
|
|
751
865
|
async function cmdRemember(hippoRoot, text, flags) {
|
|
752
866
|
const useGlobal = Boolean(flags['global']);
|
|
753
867
|
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
@@ -757,9 +871,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
757
871
|
else {
|
|
758
872
|
requireInit(hippoRoot);
|
|
759
873
|
}
|
|
760
|
-
const
|
|
761
|
-
if (flags['error'])
|
|
762
|
-
rawTags.push('error');
|
|
874
|
+
const { requested: requestedTags, all: allTags } = rememberTags(flags, process.cwd());
|
|
763
875
|
// Resolve explicit confidence flag (default: 'verified' for manual remember)
|
|
764
876
|
let confidence = 'verified';
|
|
765
877
|
if (flags['observed'])
|
|
@@ -768,9 +880,9 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
768
880
|
confidence = 'inferred';
|
|
769
881
|
if (flags['verified'])
|
|
770
882
|
confidence = 'verified';
|
|
771
|
-
//
|
|
883
|
+
// Schema fit needs the store, which the routed request has no access to, so it stays here.
|
|
772
884
|
const existing = loadAllEntries(targetRoot, resolveTenantId({}));
|
|
773
|
-
const schemaFit = computeSchemaFit(text,
|
|
885
|
+
const schemaFit = computeSchemaFit(text, requestedTags, existing);
|
|
774
886
|
// A3 envelope flags
|
|
775
887
|
const kindFlagRaw = typeof flags['kind'] === 'string' ? flags['kind'] : undefined;
|
|
776
888
|
const kindFlag = kindFlagRaw === undefined ? undefined : kindFlagRaw.toLowerCase();
|
|
@@ -802,7 +914,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
802
914
|
const rememberConfig = loadConfig(targetRoot);
|
|
803
915
|
const entry = createMemory(text, {
|
|
804
916
|
layer: Layer.Episodic,
|
|
805
|
-
tags:
|
|
917
|
+
tags: allTags,
|
|
806
918
|
pinned: Boolean(flags['pin']),
|
|
807
919
|
source: useGlobal ? 'cli-global' : 'cli',
|
|
808
920
|
confidence,
|
|
@@ -814,20 +926,6 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
814
926
|
tenantId,
|
|
815
927
|
baseHalfLifeDays: rememberConfig.defaultHalfLifeDays,
|
|
816
928
|
});
|
|
817
|
-
// Auto-tag with path context
|
|
818
|
-
const pathTags = extractPathTags(process.cwd());
|
|
819
|
-
for (const pt of pathTags) {
|
|
820
|
-
if (!entry.tags.includes(pt))
|
|
821
|
-
entry.tags.push(pt);
|
|
822
|
-
}
|
|
823
|
-
// Scope tagging: explicit --scope or auto-detected
|
|
824
|
-
const explicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
825
|
-
const activeScope = explicitScope || detectScope();
|
|
826
|
-
if (activeScope) {
|
|
827
|
-
const scopeTag = `scope:${activeScope}`;
|
|
828
|
-
if (!entry.tags.includes(scopeTag))
|
|
829
|
-
entry.tags.push(scopeTag);
|
|
830
|
-
}
|
|
831
929
|
// Salience gate: decide if this memory is worth storing
|
|
832
930
|
if (rememberConfig.salience.enabled && !Boolean(flags['pin']) && !Boolean(flags['force'])) {
|
|
833
931
|
const salienceResult = computeSalience(text, entry.tags, existing, {
|
|
@@ -855,12 +953,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
855
953
|
console.log(` Tags: ${entry.tags.join(', ')}`);
|
|
856
954
|
if (entry.pinned)
|
|
857
955
|
console.log(' Pinned (no decay)');
|
|
858
|
-
|
|
859
|
-
if (isEmbeddingConfigured(targetRoot)) {
|
|
860
|
-
embedMemory(targetRoot, entry).catch(() => {
|
|
861
|
-
// Silently ignore embedding errors
|
|
862
|
-
});
|
|
863
|
-
}
|
|
956
|
+
void embedMemory(targetRoot, entry);
|
|
864
957
|
const config = loadConfig(targetRoot);
|
|
865
958
|
const shouldExtract = flags['extract'] || config.extraction.enabled === true;
|
|
866
959
|
const apiKey = process.env.ANTHROPIC_API_KEY ?? '';
|
|
@@ -1096,6 +1189,12 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1096
1189
|
}
|
|
1097
1190
|
graphStreamSeeds = s;
|
|
1098
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)));
|
|
1099
1198
|
let results;
|
|
1100
1199
|
if (useGraphStream) {
|
|
1101
1200
|
if (!isEmbeddingAvailable()) {
|
|
@@ -1111,7 +1210,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1111
1210
|
// tail; on a pool with <= seedCount candidates EVERY candidate is a seed and the stream
|
|
1112
1211
|
// is inert (it degrades to the 2-list fusion). Tune the anchor count with --graph-seeds.
|
|
1113
1212
|
results = await hybridSearch(query, localEntries, {
|
|
1114
|
-
budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1213
|
+
budget: entryBudget, cost: printCost, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
|
|
1115
1214
|
includeSuperseded, asOf,
|
|
1116
1215
|
scoring: 'rrf',
|
|
1117
1216
|
graphStream: { weight: DEFAULT_GRAPH_STREAM_WEIGHT, tenantId, hops: graphStreamHops, seedCount: graphStreamSeeds },
|
|
@@ -1121,7 +1220,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1121
1220
|
// Unlike searchBothHybrid below, multihop ranks one pooled list, so a shared memory's two copies both compete.
|
|
1122
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) {
|
|
@@ -2684,7 +2644,7 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2684
2644
|
.filter((memDir) => fs.existsSync(memDir));
|
|
2685
2645
|
if (memoryDirs.length === 0)
|
|
2686
2646
|
return 0;
|
|
2687
|
-
const
|
|
2647
|
+
const keys = storedTextKeys(loadAllEntries(hippoRoot, resolveTenantId({})));
|
|
2688
2648
|
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
2689
2649
|
let imported = 0;
|
|
2690
2650
|
let skippedSecret = 0;
|
|
@@ -2716,12 +2676,8 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2716
2676
|
skippedSecret++;
|
|
2717
2677
|
continue;
|
|
2718
2678
|
}
|
|
2719
|
-
// Dedup:
|
|
2720
|
-
|
|
2721
|
-
const overlap = textOverlap(content.slice(0, 200), e.content.slice(0, 200));
|
|
2722
|
-
return overlap > 0.6;
|
|
2723
|
-
});
|
|
2724
|
-
if (isDup)
|
|
2679
|
+
// Dedup: skip only when the same text is already stored
|
|
2680
|
+
if (keys.has(duplicateKey(content)))
|
|
2725
2681
|
continue;
|
|
2726
2682
|
const entry = createMemory(content, {
|
|
2727
2683
|
layer: Layer.Episodic,
|
|
@@ -2741,7 +2697,7 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2741
2697
|
}
|
|
2742
2698
|
throw err;
|
|
2743
2699
|
}
|
|
2744
|
-
|
|
2700
|
+
keys.add(duplicateKey(content)); // prevent self-dedup within batch
|
|
2745
2701
|
imported++;
|
|
2746
2702
|
}
|
|
2747
2703
|
}
|
|
@@ -2758,10 +2714,12 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2758
2714
|
function cmdDedup(hippoRoot, flags) {
|
|
2759
2715
|
requireInit(hippoRoot);
|
|
2760
2716
|
const dryRun = Boolean(flags['dry-run']);
|
|
2761
|
-
|
|
2717
|
+
if (flags['threshold'] !== undefined) {
|
|
2718
|
+
console.error('hippo dedup: --threshold is ignored; a duplicate is the same text apart from spacing.');
|
|
2719
|
+
}
|
|
2762
2720
|
const entries = loadAllEntries(hippoRoot);
|
|
2763
|
-
console.log(`Scanning ${entries.length} memories for duplicates (
|
|
2764
|
-
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 });
|
|
2765
2723
|
if (result.removed === 0) {
|
|
2766
2724
|
console.log('No duplicates found.');
|
|
2767
2725
|
return;
|
|
@@ -3036,7 +2994,8 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
3036
2994
|
// source === 'compact' to print. Real SessionStart payloads always
|
|
3037
2995
|
// carry source; only the TTY/no-stdin manual path prints without
|
|
3038
2996
|
// one (codex round 3).
|
|
3039
|
-
|
|
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)) {
|
|
3040
2999
|
suppressOutput = true;
|
|
3041
3000
|
}
|
|
3042
3001
|
if (typeof payload.session_id === 'string') {
|
|
@@ -3055,29 +3014,41 @@ function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
|
3055
3014
|
snapshot.session_id !== null &&
|
|
3056
3015
|
payloadSessionId !== snapshot.session_id;
|
|
3057
3016
|
if (snapshot && !sessionMismatch) {
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
if (snapshot.session_id) {
|
|
3064
|
-
const events = listSessionEvents(hippoRoot, tenantId, { session_id: snapshot.session_id });
|
|
3065
|
-
// Nothing auto-populates session_events, so an empty table is the
|
|
3066
|
-
// common real case — printSessionEvents([]) would otherwise inject
|
|
3067
|
-
// a bare "No session events found." line into every compaction.
|
|
3068
|
-
if (events.length > 0) {
|
|
3069
|
-
const cappedEvents = events.map((e) => ({
|
|
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) => ({
|
|
3070
3022
|
...e,
|
|
3071
3023
|
content: truncateCodePointSafe(e.content, COMPACT_RESUME_EVENT_CONTENT_CAP),
|
|
3072
3024
|
}));
|
|
3073
|
-
printSessionEvents(cappedEvents);
|
|
3074
3025
|
}
|
|
3075
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
|
+
}));
|
|
3076
3046
|
}
|
|
3077
3047
|
}
|
|
3078
3048
|
}
|
|
3079
|
-
catch {
|
|
3080
|
-
//
|
|
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)}`);
|
|
3081
3052
|
}
|
|
3082
3053
|
process.exit(0);
|
|
3083
3054
|
}
|
|
@@ -3137,7 +3108,7 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
3137
3108
|
}
|
|
3138
3109
|
}
|
|
3139
3110
|
/**
|
|
3140
|
-
* 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
|
|
3141
3112
|
* `__session-end-worker` subcommand (not user-facing). Failures in one stage
|
|
3142
3113
|
* do not block the other.
|
|
3143
3114
|
*/
|
|
@@ -3166,14 +3137,38 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3166
3137
|
return { gitRef, dirtyTree, testStatus };
|
|
3167
3138
|
}
|
|
3168
3139
|
async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
3169
|
-
|
|
3170
|
-
|
|
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
|
+
}
|
|
3171
3165
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
// `[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 });
|
|
3175
3168
|
}
|
|
3176
|
-
|
|
3169
|
+
flushRereadLog();
|
|
3170
|
+
const digestLog = (message) => appendSessionEndCloseLog(closeLogFile, message);
|
|
3171
|
+
const scan = transcriptPath ? readSessionScan(transcriptPath, digestLog) : null;
|
|
3177
3172
|
try {
|
|
3178
3173
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3179
3174
|
// With no stdin of its own, capture would read this as a manual run and scan every project.
|
|
@@ -3181,19 +3176,27 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3181
3176
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
|
|
3182
3177
|
}
|
|
3183
3178
|
else {
|
|
3184
|
-
cmdCapture(
|
|
3179
|
+
cmdCapture(store, {
|
|
3185
3180
|
source: 'last-session',
|
|
3186
3181
|
transcriptPath,
|
|
3187
3182
|
logFile,
|
|
3188
3183
|
dryRun: false,
|
|
3189
3184
|
global: false,
|
|
3190
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,
|
|
3191
3189
|
});
|
|
3192
3190
|
}
|
|
3193
3191
|
}
|
|
3194
3192
|
catch {
|
|
3195
3193
|
// Same treatment — the failure line is already in the log.
|
|
3196
3194
|
}
|
|
3195
|
+
recordSessionDigest(hippoRoot, scan, {
|
|
3196
|
+
key: closeSessionId || path.basename(transcriptPath ?? '', '.jsonl'),
|
|
3197
|
+
tenantId: resolveTenantId({}),
|
|
3198
|
+
log: digestLog,
|
|
3199
|
+
});
|
|
3197
3200
|
// DF1 T3: close the ending session's own active task snapshot AFTER
|
|
3198
3201
|
// sleep+capture complete — neither producer (runPreCompact,
|
|
3199
3202
|
// `hippo snapshot save`) runs inside session-end, so this can never
|
|
@@ -3202,14 +3205,12 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3202
3205
|
// clause). Absent session id -> no-op plus one log line; session-end is
|
|
3203
3206
|
// not guaranteed to fire at all (crash, kill -9), so the freshness bound
|
|
3204
3207
|
// in loadFreshActiveTaskSnapshot is the backstop layer, not this close.
|
|
3205
|
-
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
3206
|
-
const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
|
|
3207
3208
|
// Handoff write happens BEFORE the snapshot close below, while the
|
|
3208
3209
|
// snapshot writeSessionEndHandoff reads is still active.
|
|
3209
3210
|
if (closeSessionId) {
|
|
3210
3211
|
try {
|
|
3211
3212
|
const tenantId = resolveTenantId({});
|
|
3212
|
-
const ownSnapshot = loadActiveTaskSnapshot(
|
|
3213
|
+
const ownSnapshot = loadActiveTaskSnapshot(store, tenantId)?.session_id === closeSessionId;
|
|
3213
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.
|
|
3214
3215
|
const derived = transcriptPath
|
|
3215
3216
|
? transcriptWorkingState(transcriptPath, (message) => appendSessionEndCloseLog(closeLogFile, message))
|
|
@@ -3219,7 +3220,7 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3219
3220
|
}
|
|
3220
3221
|
else {
|
|
3221
3222
|
const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
|
|
3222
|
-
const handoff = writeSessionEndHandoff(
|
|
3223
|
+
const handoff = writeSessionEndHandoff(store, tenantId, closeSessionId, evidence, derived);
|
|
3223
3224
|
appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : `skip: kept the existing handoff for session ${closeSessionId}`);
|
|
3224
3225
|
}
|
|
3225
3226
|
}
|
|
@@ -3230,7 +3231,7 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3230
3231
|
}
|
|
3231
3232
|
try {
|
|
3232
3233
|
if (closeSessionId) {
|
|
3233
|
-
const closed = closeTaskSnapshotsForSession(
|
|
3234
|
+
const closed = closeTaskSnapshotsForSession(store, resolveTenantId({}), closeSessionId);
|
|
3234
3235
|
appendSessionEndCloseLog(closeLogFile, `closed ${closed} active snapshot(s) for session ${closeSessionId}`);
|
|
3235
3236
|
}
|
|
3236
3237
|
else {
|
|
@@ -3241,6 +3242,38 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3241
3242
|
appendSessionEndCloseLog(closeLogFile, `snapshot close failed: ${err.message}`);
|
|
3242
3243
|
}
|
|
3243
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
|
+
}
|
|
3244
3277
|
/**
|
|
3245
3278
|
* Best-effort log line for the DF1 T3 snapshot-close step in
|
|
3246
3279
|
* `cmdSessionEndWorker`. `cmdSleep`/`cmdCapture` each tee console output to
|
|
@@ -3249,14 +3282,15 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3249
3282
|
* the detached worker's `stdio: 'ignore'` — write straight to the file
|
|
3250
3283
|
* instead, matching capture.ts's `appendPreCompactLog` convention.
|
|
3251
3284
|
*/
|
|
3252
|
-
function appendSessionEndCloseLog(logFile, message) {
|
|
3285
|
+
function appendSessionEndCloseLog(logFile, message, opts = {}) {
|
|
3253
3286
|
if (!logFile)
|
|
3254
3287
|
return;
|
|
3255
3288
|
try {
|
|
3256
3289
|
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
3257
3290
|
// sanitizeLogMessage: `message` interpolates the payload-controlled
|
|
3258
3291
|
// session_id — same log-forgery guard appendPreCompactLog applies.
|
|
3259
|
-
|
|
3292
|
+
const write = opts.startFresh ? fs.writeFileSync : fs.appendFileSync;
|
|
3293
|
+
write(logFile, `[hippo] ${new Date().toISOString()} ${sanitizeLogMessage(message)}\n`, 'utf8');
|
|
3260
3294
|
}
|
|
3261
3295
|
catch {
|
|
3262
3296
|
// Best-effort only — never let a log-write failure surface as an error.
|
|
@@ -3356,11 +3390,23 @@ function cmdCodexRun(hippoRoot, args) {
|
|
|
3356
3390
|
}
|
|
3357
3391
|
async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
3358
3392
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3359
|
-
|
|
3360
|
-
|
|
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;
|
|
3361
3398
|
}
|
|
3362
|
-
|
|
3363
|
-
|
|
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 });
|
|
3364
3410
|
}
|
|
3365
3411
|
try {
|
|
3366
3412
|
const codexHome = typeof flags['codex-home'] === 'string'
|
|
@@ -3382,6 +3428,8 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3382
3428
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no Codex transcript for this session');
|
|
3383
3429
|
return;
|
|
3384
3430
|
}
|
|
3431
|
+
const digestLog = (message) => appendSessionEndCloseLog(logFile ?? null, message);
|
|
3432
|
+
const scan = readSessionScan(transcriptPath, digestLog);
|
|
3385
3433
|
const captureOpts = {
|
|
3386
3434
|
source: 'last-session',
|
|
3387
3435
|
transcriptPath,
|
|
@@ -3389,8 +3437,17 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3389
3437
|
dryRun: false,
|
|
3390
3438
|
global: false,
|
|
3391
3439
|
tenantId: resolveTenantId({}),
|
|
3440
|
+
originProject: store === hippoRoot ? undefined : deriveOriginProject(process.cwd()),
|
|
3441
|
+
sessionTurns: scan?.turns,
|
|
3392
3442
|
};
|
|
3393
|
-
|
|
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 });
|
|
3394
3451
|
}
|
|
3395
3452
|
catch {
|
|
3396
3453
|
// capture path logs its own failures
|
|
@@ -3713,37 +3770,10 @@ function cmdInspect(hippoRoot, id) {
|
|
|
3713
3770
|
console.log(entry.content);
|
|
3714
3771
|
}
|
|
3715
3772
|
function printActiveTaskSnapshot(snapshot) {
|
|
3716
|
-
console.log(
|
|
3717
|
-
console.log(`- Task: ${snapshot.task}`);
|
|
3718
|
-
console.log(`- Status: ${snapshot.status}`);
|
|
3719
|
-
console.log(`- Updated: ${snapshot.updated_at}`);
|
|
3720
|
-
console.log(`- Source: ${snapshot.source}`);
|
|
3721
|
-
if (snapshot.session_id) {
|
|
3722
|
-
console.log(`- Session: ${snapshot.session_id}`);
|
|
3723
|
-
}
|
|
3724
|
-
console.log('');
|
|
3725
|
-
console.log('### Summary');
|
|
3726
|
-
console.log(snapshot.summary);
|
|
3727
|
-
console.log('');
|
|
3728
|
-
console.log('### Next step');
|
|
3729
|
-
console.log(snapshot.next_step);
|
|
3730
|
-
console.log('');
|
|
3773
|
+
console.log(snapshotText(snapshot));
|
|
3731
3774
|
}
|
|
3732
3775
|
function printSessionEvents(events) {
|
|
3733
|
-
|
|
3734
|
-
console.log('No session events found.');
|
|
3735
|
-
return;
|
|
3736
|
-
}
|
|
3737
|
-
const latest = events[events.length - 1];
|
|
3738
|
-
console.log('## Recent Session Trail\n');
|
|
3739
|
-
console.log(`- Session: ${latest.session_id}`);
|
|
3740
|
-
console.log(`- Task: ${latest.task ?? 'n/a'}`);
|
|
3741
|
-
console.log(`- Updated: ${latest.created_at}`);
|
|
3742
|
-
console.log('');
|
|
3743
|
-
for (const event of events) {
|
|
3744
|
-
console.log(`- [${event.created_at}] (${event.event_type}) ${event.content}`);
|
|
3745
|
-
}
|
|
3746
|
-
console.log('');
|
|
3776
|
+
console.log(events.length === 0 ? 'No session events found.' : sessionTrailText(events));
|
|
3747
3777
|
}
|
|
3748
3778
|
function cmdConflicts(hippoRoot, flags) {
|
|
3749
3779
|
requireInit(hippoRoot);
|
|
@@ -3868,6 +3898,12 @@ function cmdReject(hippoRoot, args, flags) {
|
|
|
3868
3898
|
console.log(` Reason: ${reason}`);
|
|
3869
3899
|
if (result.removedIds.length > 0) {
|
|
3870
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
|
+
}
|
|
3871
3907
|
}
|
|
3872
3908
|
else {
|
|
3873
3909
|
console.log(' No live rows matched (pre-emptive tombstone).');
|
|
@@ -4046,7 +4082,8 @@ function cmdQuarantine(hippoRoot, args, flags) {
|
|
|
4046
4082
|
/**
|
|
4047
4083
|
* `hippo tokens [--days <n>] [--json] [--global]`: the token ledger
|
|
4048
4084
|
* (ROADMAP TE0). Tokens of memory text handed to agents per surface, blocks
|
|
4049
|
-
* the per-prompt hook skipped as unchanged (TE2) and the tokens that saved
|
|
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.
|
|
4050
4087
|
* Counts are estimates (characters / 4), the same estimate every budget uses.
|
|
4051
4088
|
*/
|
|
4052
4089
|
function cmdTokens(hippoRoot, flags) {
|
|
@@ -4067,17 +4104,22 @@ function cmdTokens(hippoRoot, flags) {
|
|
|
4067
4104
|
console.log(`No memory text recorded in the last ${windowDays} days.`);
|
|
4068
4105
|
return;
|
|
4069
4106
|
}
|
|
4070
|
-
console.log(`Memory text handed to agents, last ${windowDays} days (estimated tokens)\n`);
|
|
4071
|
-
console.log(` ${'surface'.padEnd(14)}${'sent'.padStart(8)}${'tokens'.padStart(12)}${'skipped'.padStart(10)}${'saved'.padStart(12)}`
|
|
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)}`);
|
|
4072
4110
|
for (const row of summary.surfaces) {
|
|
4073
4111
|
console.log(` ${row.surface.padEnd(14)}${String(row.injected).padStart(8)}${String(row.tokens).padStart(12)}`
|
|
4074
|
-
+ `${String(row.skipped).padStart(10)}${String(row.tokensAvoided).padStart(12)}`);
|
|
4112
|
+
+ `${String(row.skipped).padStart(10)}${String(row.tokensAvoided).padStart(12)}${String(row.tokensReread).padStart(12)}`);
|
|
4075
4113
|
}
|
|
4076
4114
|
console.log('');
|
|
4077
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.`);
|
|
4078
4118
|
if (summary.meanTokensPerSession > 0) {
|
|
4079
4119
|
console.log(` Mean per session (rows with a session id): ${summary.meanTokensPerSession} tokens.`);
|
|
4080
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.");
|
|
4081
4123
|
}
|
|
4082
4124
|
/** `hippo failures [--days <n>] [--json] [--global]`: failed tool calls by outcome, and repeats across sessions (CD13). */
|
|
4083
4125
|
function cmdFailures(hippoRoot, flags) {
|
|
@@ -4315,47 +4357,7 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
4315
4357
|
process.exit(1);
|
|
4316
4358
|
}
|
|
4317
4359
|
function printHandoff(handoff) {
|
|
4318
|
-
console.log(
|
|
4319
|
-
console.log(`- Session: ${handoff.sessionId}`);
|
|
4320
|
-
console.log(`- Updated: ${handoff.updatedAt}`);
|
|
4321
|
-
if (handoff.taskId)
|
|
4322
|
-
console.log(`- Task: ${handoff.taskId}`);
|
|
4323
|
-
if (handoff.repoRoot)
|
|
4324
|
-
console.log(`- Repo: ${handoff.repoRoot}`);
|
|
4325
|
-
if (handoff.outcome)
|
|
4326
|
-
console.log(`- Outcome: ${handoff.outcome}`);
|
|
4327
|
-
if (handoff.targetRuntime)
|
|
4328
|
-
console.log(`- Target runtime: ${handoff.targetRuntime}`);
|
|
4329
|
-
if (handoff.cardId)
|
|
4330
|
-
console.log(`- Card: ${handoff.cardId}`);
|
|
4331
|
-
console.log('');
|
|
4332
|
-
console.log('### Summary');
|
|
4333
|
-
console.log(handoff.summary);
|
|
4334
|
-
if (handoff.nextAction) {
|
|
4335
|
-
console.log('');
|
|
4336
|
-
console.log('### Next action');
|
|
4337
|
-
console.log(handoff.nextAction);
|
|
4338
|
-
}
|
|
4339
|
-
if (handoff.artifacts && handoff.artifacts.length > 0) {
|
|
4340
|
-
console.log('');
|
|
4341
|
-
console.log('### Artifacts');
|
|
4342
|
-
for (const artifact of handoff.artifacts) {
|
|
4343
|
-
console.log(`- ${artifact}`);
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
4346
|
-
if (handoff.constraints && handoff.constraints.length > 0) {
|
|
4347
|
-
console.log('');
|
|
4348
|
-
console.log('### Constraints');
|
|
4349
|
-
for (const constraint of handoff.constraints) {
|
|
4350
|
-
console.log(`- ${constraint}`);
|
|
4351
|
-
}
|
|
4352
|
-
}
|
|
4353
|
-
if (handoff.evidence) {
|
|
4354
|
-
console.log('');
|
|
4355
|
-
console.log('### Evidence');
|
|
4356
|
-
console.log(formatHandoffEvidenceLine(handoff.evidence));
|
|
4357
|
-
}
|
|
4358
|
-
console.log('');
|
|
4360
|
+
console.log(handoffText(handoff));
|
|
4359
4361
|
}
|
|
4360
4362
|
function cmdHandoff(hippoRoot, args, flags) {
|
|
4361
4363
|
requireInit(hippoRoot);
|
|
@@ -6408,6 +6410,13 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6408
6410
|
}
|
|
6409
6411
|
}
|
|
6410
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');
|
|
6411
6420
|
const opts = {
|
|
6412
6421
|
q: query,
|
|
6413
6422
|
budget,
|
|
@@ -6418,6 +6427,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6418
6427
|
crossProject,
|
|
6419
6428
|
currentSessionId,
|
|
6420
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),
|
|
6421
6432
|
};
|
|
6422
6433
|
const result = await api.getContext(ctx, opts);
|
|
6423
6434
|
// Early exit when there's nothing to render (matches pre-extraction behavior).
|
|
@@ -6427,9 +6438,6 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6427
6438
|
(result.recentEvents && result.recentEvents.length > 0);
|
|
6428
6439
|
if (!hasContextData)
|
|
6429
6440
|
return;
|
|
6430
|
-
// Format + framing are CLI rendering concerns; api.getContext doesn't see them.
|
|
6431
|
-
const format = String(flags['format'] ?? 'markdown');
|
|
6432
|
-
const framing = String(flags['framing'] ?? 'observe');
|
|
6433
6441
|
// Adapter: ContextResultEntry -> the print-helper input shape. v39:
|
|
6434
6442
|
// cross-project inclusions (only present under --cross-project or with
|
|
6435
6443
|
// isolation disabled via crossProject) render in their own demarcated
|
|
@@ -6464,7 +6472,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6464
6472
|
});
|
|
6465
6473
|
console.log(jsonText);
|
|
6466
6474
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6467
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6475
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6468
6476
|
event: 'inject', items: output.length, tokens: estimateTokens(jsonText),
|
|
6469
6477
|
}));
|
|
6470
6478
|
}
|
|
@@ -6476,11 +6484,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6476
6484
|
const recallEntries = result.entries.filter((r) => r.promptRecall);
|
|
6477
6485
|
const staticItems = staticEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6478
6486
|
const recallItems = recallEntries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
6479
|
-
|
|
6480
|
-
const staticHeaderTokens = recallItems.length > 0
|
|
6481
|
-
? staticItems.reduce((sum, r) => sum + r.tokens, 0)
|
|
6482
|
-
: result.tokens;
|
|
6483
|
-
const staticBlock = captureConsole(() => {
|
|
6487
|
+
const staticBlock = settleTokens((t) => captureConsole(() => {
|
|
6484
6488
|
if (result.activeSnapshot)
|
|
6485
6489
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6486
6490
|
if (result.sessionHandoff)
|
|
@@ -6488,16 +6492,13 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6488
6492
|
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6489
6493
|
printSessionEvents(result.recentEvents);
|
|
6490
6494
|
}
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
printContextMarkdown(staticItems, staticHeaderTokens, framing, { showStrength: false });
|
|
6495
|
-
}
|
|
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 });
|
|
6496
6498
|
printCrossProjectSection(staticCrossEntries);
|
|
6497
|
-
});
|
|
6498
|
-
const recallTokens = recallItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
6499
|
+
}));
|
|
6499
6500
|
const recallBlock = recallItems.length > 0
|
|
6500
|
-
? captureConsole(() => printContextMarkdown(recallItems,
|
|
6501
|
+
? settleTokens((t) => captureConsole(() => printContextMarkdown(recallItems, t, framing, { showStrength: false, heading: 'Prompt-Relevant Memory' })))
|
|
6501
6502
|
: '';
|
|
6502
6503
|
if (!staticBlock.trim() && !recallBlock.trim())
|
|
6503
6504
|
return;
|
|
@@ -6542,7 +6543,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6542
6543
|
if (finalStatic) {
|
|
6543
6544
|
try {
|
|
6544
6545
|
recordTokenUse(db, {
|
|
6545
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6546
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface, event: 'inject',
|
|
6546
6547
|
items: staticItems.length, tokens: estimateTokens(finalStatic), hash: blockHash(finalStatic),
|
|
6547
6548
|
});
|
|
6548
6549
|
}
|
|
@@ -6551,7 +6552,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6551
6552
|
if (recallBlock) {
|
|
6552
6553
|
try {
|
|
6553
6554
|
recordTokenUse(db, {
|
|
6554
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6555
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: 'hook_recall', event: 'inject',
|
|
6555
6556
|
items: recallItems.length, tokens: estimateTokens(recallBlock), hash: blockHash(recallBlock),
|
|
6556
6557
|
});
|
|
6557
6558
|
}
|
|
@@ -6561,8 +6562,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6561
6562
|
}
|
|
6562
6563
|
}
|
|
6563
6564
|
else {
|
|
6564
|
-
// markdown (default)
|
|
6565
|
-
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(() => {
|
|
6566
6567
|
if (result.activeSnapshot) {
|
|
6567
6568
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6568
6569
|
}
|
|
@@ -6572,18 +6573,17 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6572
6573
|
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6573
6574
|
printSessionEvents(result.recentEvents);
|
|
6574
6575
|
}
|
|
6575
|
-
if (renderItems.length > 0)
|
|
6576
|
-
printContextMarkdown(renderItems,
|
|
6577
|
-
}
|
|
6576
|
+
if (renderItems.length > 0)
|
|
6577
|
+
printContextMarkdown(renderItems, t, framing);
|
|
6578
6578
|
printCrossProjectSection(crossEntries);
|
|
6579
6579
|
if (result.ambientState) {
|
|
6580
6580
|
console.log(`\n${renderAmbientSummary(result.ambientState)}`);
|
|
6581
6581
|
}
|
|
6582
|
-
});
|
|
6582
|
+
}));
|
|
6583
6583
|
if (text.length > 0)
|
|
6584
6584
|
console.log(text);
|
|
6585
6585
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6586
|
-
tenantId: ctx.tenantId, sessionId:
|
|
6586
|
+
tenantId: ctx.tenantId, sessionId: ledgerSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6587
6587
|
event: 'inject', items: renderItems.length, tokens: estimateTokens(text),
|
|
6588
6588
|
}));
|
|
6589
6589
|
}
|
|
@@ -6597,7 +6597,8 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6597
6597
|
*/
|
|
6598
6598
|
function resetHookInjection(hippoRoot, stdinText, requiredSource) {
|
|
6599
6599
|
const sessionId = hookPayloadSessionId(stdinText, requiredSource);
|
|
6600
|
-
|
|
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))
|
|
6601
6602
|
return;
|
|
6602
6603
|
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6603
6604
|
tenantId: resolveTenantId({}), sessionId, surface: 'hook', event: 'reset', items: 0, tokens: 0,
|
|
@@ -6672,50 +6673,17 @@ function withLedgerDb(hippoRoot, fn) {
|
|
|
6672
6673
|
function printCrossProjectSection(items) {
|
|
6673
6674
|
if (items.length === 0)
|
|
6674
6675
|
return;
|
|
6675
|
-
console.log(
|
|
6676
|
-
for (const item of items)
|
|
6677
|
-
|
|
6678
|
-
const tagStr = item.entry.tags.length > 0 ? ` [${item.entry.tags.join(', ')}]` : '';
|
|
6679
|
-
console.log(`- **[${originLabel}]** ${item.entry.content}${tagStr}`);
|
|
6680
|
-
}
|
|
6676
|
+
console.log(crossProjectHeading(items.length));
|
|
6677
|
+
for (const item of items)
|
|
6678
|
+
console.log(crossProjectLine(item));
|
|
6681
6679
|
}
|
|
6682
6680
|
/** @internal — exported for snapshot tests (tests/cli-context-render-snapshot.test.ts). NOT a stable public API. */
|
|
6683
6681
|
export function printContextMarkdown(items, totalTokens, framing = 'observe', opts = {}) {
|
|
6684
6682
|
const now = evalNow();
|
|
6685
6683
|
const showStrength = opts.showStrength !== false;
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
const e = item.entry;
|
|
6690
|
-
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
6691
|
-
const strengthPct = Math.round(calculateStrength(e) * 100);
|
|
6692
|
-
const strengthStr = showStrength ? ` (${strengthPct}%)` : '';
|
|
6693
|
-
const globalPrefix = item.isGlobal ? '[global] ' : '';
|
|
6694
|
-
const effectiveConf = confidenceFacets(e, now).tier;
|
|
6695
|
-
const label = confidenceLabel(e, now);
|
|
6696
|
-
const confWarning = label.warn ? ' \u26A0\uFE0F' : '';
|
|
6697
|
-
const confTag = `[${label.text}]${confWarning}`;
|
|
6698
|
-
if (framing === 'observe') {
|
|
6699
|
-
const dateStr = new Date(e.created).toISOString().slice(0, 10);
|
|
6700
|
-
if (effectiveConf === 'verified') {
|
|
6701
|
-
// Verified: no date prefix, just the rule
|
|
6702
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6703
|
-
}
|
|
6704
|
-
else if (effectiveConf === 'stale') {
|
|
6705
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6706
|
-
}
|
|
6707
|
-
else {
|
|
6708
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6709
|
-
}
|
|
6710
|
-
}
|
|
6711
|
-
else if (framing === 'suggest') {
|
|
6712
|
-
console.log(`- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6713
|
-
}
|
|
6714
|
-
else {
|
|
6715
|
-
// framing === 'assert': no prefix (bare facts)
|
|
6716
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6717
|
-
}
|
|
6718
|
-
}
|
|
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));
|
|
6719
6687
|
}
|
|
6720
6688
|
function autoDetectContext() {
|
|
6721
6689
|
// Try git diff --name-only for changed files
|
|
@@ -6868,9 +6836,7 @@ async function cmdWatch(command, hippoRoot) {
|
|
|
6868
6836
|
try {
|
|
6869
6837
|
writeEntry(hippoRoot, entry);
|
|
6870
6838
|
updateStats(hippoRoot, { remembered: 1 });
|
|
6871
|
-
|
|
6872
|
-
embedMemory(hippoRoot, entry).catch(() => { });
|
|
6873
|
-
}
|
|
6839
|
+
void embedMemory(hippoRoot, entry);
|
|
6874
6840
|
const preview = stderr.trim().slice(0, 80);
|
|
6875
6841
|
console.error(`\nHippo learned from failure: "${preview}"`);
|
|
6876
6842
|
}
|
|
@@ -6918,7 +6884,7 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6918
6884
|
// parsed lesson with the gate on the write alone.
|
|
6919
6885
|
//
|
|
6920
6886
|
// Round 2 then found the cure was worse. STORAGE is what makes invalidation
|
|
6921
|
-
// idempotent here: a stored lesson is recognised by
|
|
6887
|
+
// idempotent here: a stored lesson is recognised by its same-text key on
|
|
6922
6888
|
// the next scan and short-circuits before invalidating again. A lesson that
|
|
6923
6889
|
// invalidates but is never stored has no such record, so every rescan
|
|
6924
6890
|
// re-invalidates, and invalidateMatching halves half_life_days each time.
|
|
@@ -6946,9 +6912,9 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6946
6912
|
let rejected = 0;
|
|
6947
6913
|
const gitLearnTags = ['error', 'git-learned'];
|
|
6948
6914
|
const existingForSchema = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
6915
|
+
const keys = storedTextKeys(existingForSchema);
|
|
6949
6916
|
for (const lesson of lessons) {
|
|
6950
|
-
|
|
6951
|
-
if (deduplicateLesson(existingForSchema, lesson, 0.7, resolveTenantId({}))) {
|
|
6917
|
+
if (keys.has(duplicateKey(lesson))) {
|
|
6952
6918
|
skipped++;
|
|
6953
6919
|
continue;
|
|
6954
6920
|
}
|
|
@@ -6986,9 +6952,8 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6986
6952
|
throw err;
|
|
6987
6953
|
}
|
|
6988
6954
|
updateStats(hippoRoot, { remembered: 1 });
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
}
|
|
6955
|
+
keys.add(duplicateKey(lesson));
|
|
6956
|
+
void embedMemory(hippoRoot, entry);
|
|
6992
6957
|
added++;
|
|
6993
6958
|
}
|
|
6994
6959
|
console.log(`${prefix}${added} new lessons added, ${skipped} duplicates skipped` +
|
|
@@ -7271,6 +7236,13 @@ never as a closing step:
|
|
|
7271
7236
|
hippo remember "<description of what went wrong>" --error
|
|
7272
7237
|
\`\`\`
|
|
7273
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
|
+
|
|
7274
7246
|
When Hippo's Codex wrapper is installed, session-end capture runs automatically.
|
|
7275
7247
|
If the wrapper is not installed, capture a brief summary manually:
|
|
7276
7248
|
\`\`\`bash
|
|
@@ -7296,6 +7268,13 @@ never as a closing step:
|
|
|
7296
7268
|
hippo remember "<description of what went wrong>" --error
|
|
7297
7269
|
\`\`\`
|
|
7298
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
|
+
|
|
7299
7278
|
When ending a session, capture a brief summary:
|
|
7300
7279
|
\`\`\`bash
|
|
7301
7280
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7320,6 +7299,13 @@ never as a closing step:
|
|
|
7320
7299
|
hippo remember "<description of what went wrong>" --error
|
|
7321
7300
|
\`\`\`
|
|
7322
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
|
+
|
|
7323
7309
|
When ending a session, capture a brief summary:
|
|
7324
7310
|
\`\`\`bash
|
|
7325
7311
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7338,10 +7324,17 @@ hippo context --auto --budget 1500
|
|
|
7338
7324
|
\`\`\`
|
|
7339
7325
|
Read the output before writing any code.
|
|
7340
7326
|
|
|
7341
|
-
|
|
7342
|
-
|
|
7327
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
7328
|
+
never as a closing step:
|
|
7343
7329
|
\`\`\`bash
|
|
7344
|
-
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>"
|
|
7345
7338
|
\`\`\`
|
|
7346
7339
|
|
|
7347
7340
|
When stuck or repeating yourself, check if this happened before:
|
|
@@ -7373,6 +7366,13 @@ never as a closing step:
|
|
|
7373
7366
|
hippo remember "<description of what went wrong>" --error
|
|
7374
7367
|
\`\`\`
|
|
7375
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
|
+
|
|
7376
7376
|
When ending a session, capture a brief summary:
|
|
7377
7377
|
\`\`\`bash
|
|
7378
7378
|
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
@@ -7391,14 +7391,19 @@ const SHIPPED_HOOK_HASHES = new Map([
|
|
|
7391
7391
|
['15abcece9712279fb4721f7a8f0ba117457400278977beb5cf5b5d7ba49f7b1a', 'codex'],
|
|
7392
7392
|
['0c81a6b2c21473313001f624b80ea870e661aecbfda9bfe8503febc0d5f34533', 'codex'],
|
|
7393
7393
|
['88e45358aba4f17912f113221c991dc758275991335d1daa4aa1974a69c46769', 'codex'],
|
|
7394
|
+
['e61632fe177450a06541c148a9a4f9182530d8df667806927a99792825903298', 'codex'],
|
|
7394
7395
|
['a1415ecda9b2f8f317c233738e4a5ac16e6b2cc385a017c0c8ecfbfacbcab6a3', 'cursor'],
|
|
7395
7396
|
['a38c428bbdfc14ec50f6f7b9183785170a4eae1ce9cde60257cca6efc7206b3a', 'cursor'],
|
|
7397
|
+
['0ec9f556abfd55e94f9e6fb47ece0fc5acb841977d144b35a2371e03645d8636', 'cursor'],
|
|
7396
7398
|
['40524c3bd5a2eb04036567cc761451961d950995768bccd93a9900b0f75eafea', 'openclaw'],
|
|
7397
7399
|
['7b3518e8c0feaa7b8b454cde7743f7598ad14cd9979e1680d0954484e2464aae', 'openclaw'],
|
|
7400
|
+
['1137dcf04568caf011e41db77bc55324faee88bc29c3a5fcc98ab687cd952a16', 'openclaw'],
|
|
7398
7401
|
['4601c67c31f41cd5b1324cfccdb1afc66872b7fb0bc1e7c5789ecabb1f6bd942', 'opencode'],
|
|
7399
7402
|
['90d9e21d8d1ecbe99a0fc7b7f2d9f8af7b5315a6b4b0203df4f7a9bdc0699b98', 'opencode'],
|
|
7403
|
+
['ca4e00284f1397ed2f2fcc53210c27f63b90edf6b37fd66dad5ee58b94ea3eee', 'opencode'],
|
|
7400
7404
|
['8b8f5986d7f7ed15f06e68720d8913c3cab23d94366b411935ca2bbaa334553b', 'pi'],
|
|
7401
7405
|
['37767b355e18beac726b05b9e2b898dab8c6135fd7b98f3aa52edc734d5dd283', 'pi'],
|
|
7406
|
+
['6e85a5cccb3cfeaa9a080713754936db730f96376f94cc9a9888a746149c7268', 'pi'],
|
|
7402
7407
|
]);
|
|
7403
7408
|
function cmdHook(args, flags) {
|
|
7404
7409
|
const subcommand = args[0];
|
|
@@ -7502,9 +7507,16 @@ function cmdHook(args, flags) {
|
|
|
7502
7507
|
}
|
|
7503
7508
|
}
|
|
7504
7509
|
else if (target === 'codex') {
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
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
|
+
}
|
|
7508
7520
|
}
|
|
7509
7521
|
return;
|
|
7510
7522
|
}
|
|
@@ -7548,6 +7560,9 @@ function cmdHook(args, flags) {
|
|
|
7548
7560
|
}
|
|
7549
7561
|
}
|
|
7550
7562
|
else if (target === 'codex') {
|
|
7563
|
+
if (uninstallJsonHooks('codex')) {
|
|
7564
|
+
console.log(`Removed hippo's Codex memory hooks from ${resolveJsonHookPaths('codex').settings}`);
|
|
7565
|
+
}
|
|
7551
7566
|
if (uninstallCodexWrapper()) {
|
|
7552
7567
|
console.log('Removed Codex wrapper integration');
|
|
7553
7568
|
}
|
|
@@ -7637,10 +7652,13 @@ function cmdSetup(flags) {
|
|
|
7637
7652
|
}
|
|
7638
7653
|
for (const tool of wrapperTools) {
|
|
7639
7654
|
if (dryRun) {
|
|
7655
|
+
if (tool.name === 'codex')
|
|
7656
|
+
console.log(`[dry-run] would install Codex memory hooks in ${resolveJsonHookPaths('codex').settings}`);
|
|
7640
7657
|
console.log(`[dry-run] would wrap the detected ${tool.name} launcher in place`);
|
|
7641
7658
|
continue;
|
|
7642
7659
|
}
|
|
7643
7660
|
if (tool.name === 'codex') {
|
|
7661
|
+
installCodexMemoryHooks(` ${tool.name.padEnd(14)} `);
|
|
7644
7662
|
const result = ensureCodexWrapperInstalled();
|
|
7645
7663
|
if (result.status === 'installed') {
|
|
7646
7664
|
console.log(` ${tool.name.padEnd(14)} wrapped launcher -> ${result.commandPath}`);
|
|
@@ -7903,17 +7921,20 @@ function cmdAssemble(hippoRoot, sessionId, flags) {
|
|
|
7903
7921
|
...(Number.isFinite(freshTailCount) && freshTailCount >= 0 ? { freshTailCount } : {}),
|
|
7904
7922
|
summarizeOlder,
|
|
7905
7923
|
...(scope !== undefined ? { scope } : {}),
|
|
7924
|
+
cost: assembleCost(sessionId),
|
|
7906
7925
|
});
|
|
7907
7926
|
if (flags['json']) {
|
|
7908
7927
|
console.log(JSON.stringify(r, null, 2));
|
|
7909
7928
|
return;
|
|
7910
7929
|
}
|
|
7911
|
-
console.log(
|
|
7912
|
-
|
|
7913
|
-
const
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
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
|
+
})));
|
|
7917
7938
|
}
|
|
7918
7939
|
function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
7919
7940
|
requireInit(hippoRoot);
|
|
@@ -7940,6 +7961,7 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
|
7940
7961
|
...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
|
|
7941
7962
|
...(Number.isFinite(budget) && budget > 0 ? { budget } : {}),
|
|
7942
7963
|
...(depth !== undefined ? { depth } : {}),
|
|
7964
|
+
cost: drillCost,
|
|
7943
7965
|
});
|
|
7944
7966
|
if ('failure' in r) {
|
|
7945
7967
|
// v1.6.4: only `not_drillable` is caller-actionable. `not_found`
|
|
@@ -8144,65 +8166,7 @@ function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
|
|
|
8144
8166
|
// ---------------------------------------------------------------------------
|
|
8145
8167
|
// Audit log subcommands (A5 stub auth — `hippo audit list`)
|
|
8146
8168
|
// ---------------------------------------------------------------------------
|
|
8147
|
-
const VALID_AUDIT_OPS = new Set(
|
|
8148
|
-
'remember',
|
|
8149
|
-
'recall',
|
|
8150
|
-
'promote',
|
|
8151
|
-
'supersede',
|
|
8152
|
-
'forget',
|
|
8153
|
-
'archive_raw',
|
|
8154
|
-
'auth_revoke',
|
|
8155
|
-
'auth_create', // v1.12.4: emitted by api.authCreate
|
|
8156
|
-
'outcome', // v1.11.5: pre-existing drift — emitted today but rejected by old Set
|
|
8157
|
-
'consolidate', // v1.11.5: emitted by api.sleep / hippo sleep
|
|
8158
|
-
'audit_prune', // v1.12.9: emitted by pruneAuditLog
|
|
8159
|
-
'summary_marked_dirty', // v0.30 / E1 — lockstep with AuditOp union + server.ts VALID_AUDIT_OPS (v1.11.5 CRIT A institutional rule)
|
|
8160
|
-
'summary_marked_clean', // v0.30 / E3 — buildDag post-link clean op; lockstep
|
|
8161
|
-
'summary_rebuilt', // v0.30 / E3 — sleep-cycle rebuild op; lockstep
|
|
8162
|
-
'predict_create', // v0.31 / E2 prediction first-class object — emitted by savePrediction
|
|
8163
|
-
'predict_close', // v0.31 / E2 — emitted by closePrediction
|
|
8164
|
-
'predict_baserate', // v0.31 / J3 — emitted by computePredictionBaserate
|
|
8165
|
-
'recall_autodebias_hint', // v0.32 / J3.2 — emitted by computePlanningFallacyHint on success
|
|
8166
|
-
'recall_autodebias_hint_no_class_match', // v0.32 / J3.2 — telemetry: forward-claim, no class scored
|
|
8167
|
-
'recall_autodebias_hint_tiebreak', // v0.32 / J3.2 — telemetry: forward-claim, >=2 classes tied
|
|
8168
|
-
'recall_anchor_detected_query_repeat', // v0.33 / J1 — emitted by detector on R1 fire
|
|
8169
|
-
'recall_anchor_detected_memory_dominance', // v0.33 / J1 — emitted by detector on R2 fire
|
|
8170
|
-
'recall_anchor_skipped_no_session', // v0.33 / J1 — telemetry: no sessionId, ring skipped
|
|
8171
|
-
'recall_availability_detected', // v1.13.x / J2 - emitted when availability/recency-bias hint fires
|
|
8172
|
-
'decision_create', // E2 decision first-class object — emitted by saveDecision
|
|
8173
|
-
'decision_supersede', // E2 — emitted by saveDecision when --supersedes resolves to an active decision row
|
|
8174
|
-
'decision_close', // E2 — emitted by closeDecision
|
|
8175
|
-
'incident_open', // E2 incident first-class object — emitted by saveIncident
|
|
8176
|
-
'incident_resolve', // E2 — emitted by resolveIncident (open -> resolved)
|
|
8177
|
-
'incident_close', // E2 — emitted by closeIncident (open|resolved -> closed)
|
|
8178
|
-
'process_create', // E2 process first-class object — emitted by saveProcess
|
|
8179
|
-
'process_supersede', // E2 — emitted by saveProcess on a supersession
|
|
8180
|
-
'process_close', // E2 — emitted by closeProcess
|
|
8181
|
-
'policy_create', // E2 policy first-class object — emitted by savePolicy
|
|
8182
|
-
'policy_supersede', // E2 — emitted by savePolicy on a supersession
|
|
8183
|
-
'policy_close', // E2 — emitted by closePolicy
|
|
8184
|
-
'skill_create', // E2 skill first-class object — emitted by saveSkill
|
|
8185
|
-
'skill_supersede', // E2 — emitted by saveSkill on a supersession
|
|
8186
|
-
'skill_close', // E2 — emitted by closeSkill
|
|
8187
|
-
'project_brief_create', // E2 project_brief first-class object — emitted by saveProjectBrief
|
|
8188
|
-
'project_brief_supersede', // E2 — emitted by saveProjectBrief on a supersession (incl. refresh)
|
|
8189
|
-
'project_brief_close', // E2 — emitted by closeProjectBrief
|
|
8190
|
-
'customer_note_create', // E2 customer_note first-class object — emitted by saveCustomerNote
|
|
8191
|
-
'customer_note_supersede', // E2 — emitted by saveCustomerNote on a supersession
|
|
8192
|
-
'customer_note_close', // E2 — emitted by closeCustomerNote
|
|
8193
|
-
'mv_rescue', // LC2-E3 — emitted by consolidate() per rescue; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8194
|
-
'reject_value', // AT1 — emitted by `hippo reject`; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8195
|
-
'reject_refusal', // AT1 — emitted when the rejection guard refuses a write; lockstep
|
|
8196
|
-
'unreject_value', // AT1 — emitted by `hippo unreject`; lockstep
|
|
8197
|
-
'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
|
|
8198
|
-
'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
|
|
8199
|
-
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8200
|
-
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8201
|
-
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8202
|
-
'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8203
|
-
'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
|
|
8204
|
-
'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
|
|
8205
|
-
]);
|
|
8169
|
+
const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
|
|
8206
8170
|
function formatAuditRow(ev) {
|
|
8207
8171
|
const target = ev.targetId ?? '-';
|
|
8208
8172
|
const meta = JSON.stringify(ev.metadata ?? {});
|
|
@@ -8728,7 +8692,7 @@ function cmdSlack(hippoRoot, args, flags) {
|
|
|
8728
8692
|
}
|
|
8729
8693
|
export function usageText() {
|
|
8730
8694
|
return `
|
|
8731
|
-
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
|
|
8732
8696
|
|
|
8733
8697
|
Usage: hippo <command> [options]
|
|
8734
8698
|
|
|
@@ -8738,6 +8702,7 @@ Commands:
|
|
|
8738
8702
|
--days <n> Days of git history to seed (default: 365 for --scan, 30 for single)
|
|
8739
8703
|
--global Init the global store ($HIPPO_HOME or ~/.hippo/)
|
|
8740
8704
|
--no-hooks Skip auto-detecting and installing agent hooks
|
|
8705
|
+
(HIPPO_SKIP_AUTO_INTEGRATIONS=1 does the same)
|
|
8741
8706
|
--no-schedule Skip auto-creating the machine-level daily runner
|
|
8742
8707
|
--no-learn Skip seeding memories from git history
|
|
8743
8708
|
remember <text> Store a memory
|
|
@@ -8753,7 +8718,7 @@ Commands:
|
|
|
8753
8718
|
--tag <tag> Tag for the new memory (repeatable; default: the old memory's tags)
|
|
8754
8719
|
--pin Pin the new memory (default: pinned if the old one was)
|
|
8755
8720
|
recall <query> Search and retrieve memories (local + global)
|
|
8756
|
-
--budget <n> Token budget (default: 4000)
|
|
8721
|
+
--budget <n> Token budget for the whole printed block (default: 4000)
|
|
8757
8722
|
--min-results <n> Minimum results regardless of budget (default: 1)
|
|
8758
8723
|
--json Output as JSON
|
|
8759
8724
|
--why Show match reasons and source annotations
|
|
@@ -8834,7 +8799,7 @@ Commands:
|
|
|
8834
8799
|
handoff/events (use 'hippo session resume' for
|
|
8835
8800
|
the explicit handoff-without-snapshot path).
|
|
8836
8801
|
explain <query> Show full score breakdown for each retrieved memory
|
|
8837
|
-
--budget <n> Token budget (default: 4000)
|
|
8802
|
+
--budget <n> Token budget, counted as recall prints (default: 4000)
|
|
8838
8803
|
--limit <n> Cap the number of results displayed
|
|
8839
8804
|
--json Output as JSON
|
|
8840
8805
|
--physics | --classic Force search mode (default: from config)
|
|
@@ -8865,20 +8830,23 @@ Commands:
|
|
|
8865
8830
|
--json Output full summary as JSON
|
|
8866
8831
|
context Smart context injection for AI agents
|
|
8867
8832
|
--auto Auto-detect task from git state
|
|
8868
|
-
--budget <n> Token budget (default: 1500)
|
|
8833
|
+
--budget <n> Token budget for the whole printed block (default: 1500)
|
|
8869
8834
|
--pinned-only Only inject pinned memories (used by UserPromptSubmit hook)
|
|
8870
8835
|
--include-recent <n> With --pinned-only, also inject the last N writes regardless of pinning
|
|
8871
8836
|
(the hook payload's "prompt" drives prompt recall instead of --include-recent when pinnedInject.promptRecall is on)
|
|
8872
8837
|
--format <fmt> Output format: markdown (default), json, or additional-context (Claude Code hook JSON)
|
|
8873
8838
|
--framing <mode> Framing: observe (default), suggest, assert
|
|
8874
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
|
|
8875
8843
|
--dry-run Preview without writing
|
|
8876
8844
|
--no-learn Skip auto git-learn before consolidation
|
|
8877
8845
|
--no-share Skip auto-sharing to global store
|
|
8878
8846
|
daily-runner Sweep registered workspaces and run daily learn+sleep
|
|
8879
8847
|
dedup Remove duplicate memories (keeps stronger copy)
|
|
8880
8848
|
--dry-run Preview without removing
|
|
8881
|
-
--threshold <n>
|
|
8849
|
+
--threshold <n> Ignored, kept for old scripts: a duplicate is the same text apart from spacing
|
|
8882
8850
|
status Show memory health stats
|
|
8883
8851
|
audit [--fix] Check memory quality (--fix removes junk)
|
|
8884
8852
|
github GitHub connector subcommands (backfill, dlq)
|
|
@@ -8902,10 +8870,10 @@ Commands:
|
|
|
8902
8870
|
--stats Count memories per DAG level instead
|
|
8903
8871
|
drill <summary-id> Walk down a DAG level-2 summary to its children
|
|
8904
8872
|
--limit N Cap children list (default 50)
|
|
8905
|
-
--budget N
|
|
8873
|
+
--budget N Token budget for the printed children (≈ chars/4)
|
|
8906
8874
|
--json Output as JSON
|
|
8907
8875
|
assemble --session <id> Build a session's chronological context window
|
|
8908
|
-
--budget N Token budget (default 4000)
|
|
8876
|
+
--budget N Token budget for the printed window (default 4000)
|
|
8909
8877
|
--fresh-tail N Recent rows always kept verbatim (default 10)
|
|
8910
8878
|
--no-summarize-older Disable older-row summary substitution
|
|
8911
8879
|
--scope <s> Restrict to exact scope (default: deny *:private:*)
|
|
@@ -8956,8 +8924,10 @@ Commands:
|
|
|
8956
8924
|
--out <file> Where to write it (default: hippo-support-<time>.json here)
|
|
8957
8925
|
--include-logs Add the last ${TAIL_MAX_LINES} lines of each hippo log, known secret shapes removed
|
|
8958
8926
|
tokens Tokens of memory text hippo handed agents, per surface
|
|
8959
|
-
(hook, context, recall, MCP, HTTP),
|
|
8960
|
-
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)
|
|
8961
8931
|
--days <n> Window in days (default: 30)
|
|
8962
8932
|
--json Output as JSON
|
|
8963
8933
|
--global Operate on the global store
|
|
@@ -9077,34 +9047,40 @@ Commands:
|
|
|
9077
9047
|
capture Extract memories from conversation text
|
|
9078
9048
|
--stdin Read from piped input
|
|
9079
9049
|
--file <path> Read from a file
|
|
9080
|
-
--last-session Read
|
|
9050
|
+
--last-session Read the transcript a hook names on stdin, else the newest
|
|
9051
|
+
Claude Code one from any project
|
|
9081
9052
|
--transcript <path> Explicit transcript path (implies --last-session)
|
|
9082
9053
|
--log-file <path> Tee output to a log file (paired with 'hippo last-sleep')
|
|
9083
9054
|
--dry-run Preview without writing
|
|
9084
9055
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
9085
|
-
setup One-shot: detect installed AI tools and install
|
|
9086
|
-
|
|
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
|
|
9087
9060
|
--all Install for every JSON-hook tool, even if not detected
|
|
9088
9061
|
--dry-run Show what would be installed without writing
|
|
9089
9062
|
--no-schedule Skip installing or repairing the daily runner
|
|
9090
9063
|
last-sleep Print the last 'hippo sleep --log-file' output to stderr and clear it
|
|
9091
9064
|
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
9092
9065
|
--keep Print without clearing
|
|
9093
|
-
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
|
|
9094
9069
|
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
|
|
9095
9070
|
pre-compact PreCompact hook: save a working-state snapshot before compaction
|
|
9096
9071
|
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
9097
|
-
compact-resume SessionStart(compact) hook: re-print the snapshot
|
|
9072
|
+
compact-resume SessionStart(compact) hook: re-print the snapshot, if under 15 minutes old
|
|
9098
9073
|
post-compact PostCompact hook: tell the user what pre-compact saved
|
|
9099
9074
|
--log-file <p> Same log path as pre-compact (default: ~/.hippo/logs/pre-compact.log)
|
|
9100
9075
|
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
9101
9076
|
hook <sub> [target] Manage framework integrations
|
|
9102
9077
|
hook list Show available hooks
|
|
9103
9078
|
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
9104
|
-
claude-code
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
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
|
|
9108
9084
|
hook uninstall <target> Remove hook
|
|
9109
9085
|
predict "<claim>" Record a prediction to score against the actual outcome later
|
|
9110
9086
|
--class <c> Reference class (required)
|
|
@@ -9225,7 +9201,7 @@ Commands:
|
|
|
9225
9201
|
wm clear Clear working memory entries
|
|
9226
9202
|
--scope <scope> Filter by scope
|
|
9227
9203
|
--session <id> Filter by session
|
|
9228
|
-
wm flush
|
|
9204
|
+
wm flush Same as clear; nothing runs it at session end
|
|
9229
9205
|
--scope <scope> Filter by scope
|
|
9230
9206
|
--session <id> Filter by session
|
|
9231
9207
|
dashboard Open web dashboard for memory health
|
|
@@ -9467,10 +9443,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9467
9443
|
const rememberKindRaw = typeof flags['kind'] === 'string' ? flags['kind'].toLowerCase() : undefined;
|
|
9468
9444
|
const rememberKindAllowed = ['distilled', 'superseded'];
|
|
9469
9445
|
if (rememberKindRaw === undefined || rememberKindAllowed.includes(rememberKindRaw)) {
|
|
9470
|
-
const
|
|
9471
|
-
const tags = Array.isArray(tagsRaw)
|
|
9472
|
-
? tagsRaw.map(String)
|
|
9473
|
-
: typeof tagsRaw === 'string' ? [tagsRaw] : undefined;
|
|
9446
|
+
const tags = rememberTags(flags, process.cwd()).all;
|
|
9474
9447
|
// B2 v1.12.6 — validate --owner on the thin-client path too.
|
|
9475
9448
|
// Failure on this path exits early so the user gets the same
|
|
9476
9449
|
// validation experience whether or not a server is up.
|