hippo-memory 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -27,8 +27,8 @@
27
27
  import * as path from 'path';
28
28
  import * as fs from 'fs';
29
29
  import * as os from 'os';
30
- import { execSync } from 'child_process';
31
- import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, } from './hooks.js';
30
+ import { execSync, spawn } from 'child_process';
31
+ import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, } from './hooks.js';
32
32
  import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, applyOutcome, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
33
33
  import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, } from './store.js';
34
34
  import { markRetrieved, estimateTokens, hybridSearch, physicsSearch, explainMatch, textOverlap } from './search.js';
@@ -67,6 +67,10 @@ function parseArgs(argv) {
67
67
  let i = 0;
68
68
  while (i < rest.length) {
69
69
  const part = rest[i];
70
+ if (part === '--') {
71
+ args.push(...rest.slice(i + 1));
72
+ break;
73
+ }
70
74
  if (part.startsWith('--')) {
71
75
  const key = part.slice(2);
72
76
  const next = rest[i + 1];
@@ -279,19 +283,19 @@ function autoInstallHooks(quiet) {
279
283
  if (hook === 'claude-code' || hook === 'opencode') {
280
284
  const result = installJsonHooks(hook);
281
285
  if (result.installedSessionEnd) {
282
- console.log(` Auto-installed hippo sleep SessionEnd hook in ${hook} settings`);
286
+ console.log(` Auto-installed hippo session-end SessionEnd hook in ${hook} settings`);
283
287
  }
284
288
  if (result.installedSessionStart) {
285
289
  console.log(` Auto-installed hippo last-sleep SessionStart hook in ${hook} settings`);
286
290
  }
287
- if (result.installedSessionCapture) {
288
- console.log(` Auto-installed hippo capture SessionEnd hook in ${hook} settings`);
289
- }
290
291
  if (result.migratedFromStop) {
291
292
  console.log(` Migrated legacy Stop hook → SessionEnd (no longer runs every turn)`);
292
293
  }
293
- if (result.migratedLegacySessionEnd) {
294
- console.log(` Migrated legacy SessionEnd entry to the new --log-file form`);
294
+ if (result.migratedSplitSessionEnd) {
295
+ console.log(` Migrated split sleep+capture SessionEnd entries single detached hippo session-end`);
296
+ }
297
+ else if (result.migratedLegacySessionEnd) {
298
+ console.log(` Migrated legacy SessionEnd entry to the new detached form`);
295
299
  }
296
300
  }
297
301
  }
@@ -798,6 +802,229 @@ function cmdLastSleep(flags) {
798
802
  catch { /* non-fatal */ }
799
803
  }
800
804
  }
805
+ /**
806
+ * SessionEnd entry point. Claude Code / OpenCode fire this on /exit while
807
+ * tearing down the TUI, which kills any child that is still running when
808
+ * the parent returns. Running sleep + capture synchronously here means both
809
+ * get SIGTERM'd mid-consolidation.
810
+ *
811
+ * So we do the minimum inline (read stdin for transcript_path), then spawn
812
+ * a fully detached Node child that runs sleep → capture and exit the parent
813
+ * immediately. The child writes to the log file and survives TUI teardown;
814
+ * the next SessionStart reads the log via `hippo last-sleep`.
815
+ */
816
+ function cmdSessionEnd(hippoRoot, flags) {
817
+ const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
818
+ // Read stdin synchronously. The SessionEnd hook payload carries
819
+ // `transcript_path` as JSON; we extract it here and pass it to the worker
820
+ // via argv so the detached child doesn't need to inherit stdin.
821
+ let transcriptPath = null;
822
+ try {
823
+ const stdinText = fs.readFileSync(0, 'utf8');
824
+ if (stdinText && stdinText.trim().startsWith('{')) {
825
+ const payload = JSON.parse(stdinText);
826
+ if (typeof payload.transcript_path === 'string') {
827
+ transcriptPath = payload.transcript_path;
828
+ }
829
+ }
830
+ }
831
+ catch {
832
+ // No stdin, not JSON, or read failure — capture will fall back to
833
+ // transcript auto-discovery.
834
+ }
835
+ const workerArgs = [process.argv[1], '__session-end-worker'];
836
+ if (logFile)
837
+ workerArgs.push('--log-file', logFile);
838
+ if (transcriptPath)
839
+ workerArgs.push('--transcript', transcriptPath);
840
+ try {
841
+ const child = spawn(process.execPath, workerArgs, {
842
+ detached: true,
843
+ stdio: 'ignore',
844
+ windowsHide: true,
845
+ });
846
+ child.unref();
847
+ }
848
+ catch (err) {
849
+ // If spawn fails, run inline as a last resort — better late output than
850
+ // no consolidation at all.
851
+ cmdSessionEndWorker(hippoRoot, flags);
852
+ return;
853
+ }
854
+ }
855
+ /**
856
+ * Detached worker that runs sleep, then capture. Invoked via the internal
857
+ * `__session-end-worker` subcommand (not user-facing). Failures in one stage
858
+ * do not block the other.
859
+ */
860
+ function cmdSessionEndWorker(hippoRoot, flags) {
861
+ try {
862
+ cmdSleep(hippoRoot, flags);
863
+ }
864
+ catch {
865
+ // sleep errors are already tee'd to the log file via cmdSleep's
866
+ // `[hippo] sleep failed: ...` line. Continue to capture regardless.
867
+ }
868
+ try {
869
+ const captureOpts = {
870
+ source: 'last-session',
871
+ transcriptPath: typeof flags['transcript'] === 'string'
872
+ ? flags['transcript']
873
+ : undefined,
874
+ logFile: typeof flags['log-file'] === 'string'
875
+ ? flags['log-file']
876
+ : undefined,
877
+ dryRun: false,
878
+ global: false,
879
+ };
880
+ cmdCapture(hippoRoot, captureOpts);
881
+ }
882
+ catch {
883
+ // Same treatment — the failure line is already in the log.
884
+ }
885
+ }
886
+ function loadCodexWrapperMetadata() {
887
+ const { metadataPath } = resolveCodexWrapperPaths();
888
+ if (!fs.existsSync(metadataPath)) {
889
+ throw new Error('Codex wrapper is not installed. Run `hippo hook install codex` first.');
890
+ }
891
+ return JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
892
+ }
893
+ function quoteCmdArg(arg) {
894
+ if (arg.length === 0)
895
+ return '""';
896
+ if (!/[ \t"&()^<>|]/.test(arg))
897
+ return arg;
898
+ return `"${arg.replace(/"/g, '""')}"`;
899
+ }
900
+ function spawnRealCodex(realCodexPath, forwardArgs, cwd) {
901
+ const ext = path.extname(realCodexPath).toLowerCase();
902
+ if (process.platform === 'win32' && ext === '.ps1') {
903
+ return spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', realCodexPath, ...forwardArgs], { cwd, stdio: 'inherit', windowsHide: false });
904
+ }
905
+ if (process.platform === 'win32' && (ext === '.cmd' || ext === '.bat')) {
906
+ const command = `"${realCodexPath}"${forwardArgs.length > 0 ? ` ${forwardArgs.map(quoteCmdArg).join(' ')}` : ''}`;
907
+ return spawn('cmd.exe', ['/d', '/s', '/c', command], { cwd, stdio: 'inherit', windowsHide: false });
908
+ }
909
+ return spawn(realCodexPath, forwardArgs, { cwd, stdio: 'inherit', windowsHide: false });
910
+ }
911
+ function cmdCodexRun(hippoRoot, args) {
912
+ const metadata = loadCodexWrapperMetadata();
913
+ const startedAtMs = Date.now();
914
+ const historyPath = metadata.historyPath;
915
+ const startOffsetBytes = fs.existsSync(historyPath) ? fs.statSync(historyPath).size : 0;
916
+ try {
917
+ cmdLastSleep({ path: metadata.logFile });
918
+ }
919
+ catch {
920
+ // best-effort only
921
+ }
922
+ const child = spawnRealCodex(metadata.realCodexPath, args, process.cwd());
923
+ child.on('error', (err) => {
924
+ console.error(`Failed to launch Codex: ${err.message}`);
925
+ process.exit(1);
926
+ });
927
+ child.on('exit', (code, signal) => {
928
+ const workerArgs = [
929
+ process.argv[1],
930
+ '__codex-session-end-worker',
931
+ '--codex-home',
932
+ path.dirname(historyPath),
933
+ '--history-path',
934
+ historyPath,
935
+ '--start-offset',
936
+ String(startOffsetBytes),
937
+ '--started-at',
938
+ String(startedAtMs),
939
+ '--log-file',
940
+ metadata.logFile,
941
+ ];
942
+ try {
943
+ const worker = spawn(process.execPath, workerArgs, {
944
+ detached: true,
945
+ stdio: 'ignore',
946
+ windowsHide: true,
947
+ });
948
+ worker.unref();
949
+ }
950
+ catch {
951
+ // Fall back to the inline path if the detached worker cannot be created.
952
+ cmdCodexSessionEndWorker(hippoRoot, {
953
+ 'codex-home': path.dirname(historyPath),
954
+ 'history-path': historyPath,
955
+ 'start-offset': String(startOffsetBytes),
956
+ 'started-at': String(startedAtMs),
957
+ 'log-file': metadata.logFile,
958
+ });
959
+ }
960
+ if (signal) {
961
+ try {
962
+ process.kill(process.pid, signal);
963
+ }
964
+ catch {
965
+ process.exit(1);
966
+ }
967
+ return;
968
+ }
969
+ process.exit(code ?? 0);
970
+ });
971
+ }
972
+ function cmdCodexSessionEndWorker(hippoRoot, flags) {
973
+ const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
974
+ try {
975
+ cmdSleep(hippoRoot, logFile ? { 'log-file': logFile } : {});
976
+ }
977
+ catch {
978
+ // sleep errors are already written via cmdSleep
979
+ }
980
+ try {
981
+ const codexHome = typeof flags['codex-home'] === 'string'
982
+ ? flags['codex-home']
983
+ : path.join(os.homedir(), '.codex');
984
+ const historyPath = typeof flags['history-path'] === 'string'
985
+ ? flags['history-path']
986
+ : path.join(codexHome, 'history.jsonl');
987
+ const startOffsetBytes = parseInt(String(flags['start-offset'] ?? '0'), 10) || 0;
988
+ const startedAtMs = parseInt(String(flags['started-at'] ?? Date.now()), 10) || Date.now();
989
+ const transcriptPath = resolveCodexSessionTranscript({
990
+ codexHome,
991
+ historyPath,
992
+ startOffsetBytes,
993
+ startedAtMs,
994
+ }) ?? undefined;
995
+ const captureOpts = {
996
+ source: 'last-session',
997
+ transcriptPath,
998
+ logFile,
999
+ dryRun: false,
1000
+ global: false,
1001
+ };
1002
+ cmdCapture(hippoRoot, captureOpts);
1003
+ }
1004
+ catch {
1005
+ // capture path logs its own failures
1006
+ }
1007
+ }
1008
+ function shouldAutoInstallCodexWrapper(currentCommand, currentArgs) {
1009
+ if (process.env.HIPPO_SKIP_AUTO_INTEGRATIONS === '1')
1010
+ return false;
1011
+ if (!['context', 'remember', 'recall', 'sleep', 'capture', 'outcome', 'status', 'init'].includes(currentCommand)) {
1012
+ return false;
1013
+ }
1014
+ if (currentCommand === 'init' && currentArgs.includes('--no-hooks'))
1015
+ return false;
1016
+ return true;
1017
+ }
1018
+ function maybeAutoInstallCodexWrapper(currentCommand, currentArgs) {
1019
+ if (!shouldAutoInstallCodexWrapper(currentCommand, currentArgs))
1020
+ return;
1021
+ try {
1022
+ ensureCodexWrapperInstalled();
1023
+ }
1024
+ catch {
1025
+ // best-effort only
1026
+ }
1027
+ }
801
1028
  function cmdStatus(hippoRoot) {
802
1029
  requireInit(hippoRoot);
803
1030
  const entries = loadAllEntries(hippoRoot);
@@ -1914,7 +2141,7 @@ hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
1914
2141
  'codex': {
1915
2142
  file: 'AGENTS.md',
1916
2143
  description: 'OpenAI Codex',
1917
- content: `
2144
+ content: `
1918
2145
  ## Project Memory (Hippo)
1919
2146
 
1920
2147
  At the start of every task, run:
@@ -1933,7 +2160,8 @@ On task completion:
1933
2160
  hippo outcome --good
1934
2161
  \`\`\`
1935
2162
 
1936
- When ending a session, capture a brief summary:
2163
+ When Hippo's Codex wrapper is installed, session-end capture runs automatically.
2164
+ If the wrapper is not installed, capture a brief summary manually:
1937
2165
  \`\`\`bash
1938
2166
  hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
1939
2167
  \`\`\`
@@ -2094,21 +2322,26 @@ function cmdHook(args, flags) {
2094
2322
  if (target === 'claude-code' || target === 'opencode') {
2095
2323
  const result = installJsonHooks(target);
2096
2324
  if (result.installedSessionEnd) {
2097
- console.log(`Installed hippo sleep SessionEnd hook in ${result.target} settings`);
2325
+ console.log(`Installed hippo session-end SessionEnd hook in ${result.target} settings`);
2098
2326
  }
2099
2327
  if (result.installedSessionStart) {
2100
2328
  console.log(`Installed hippo last-sleep SessionStart hook in ${result.target} settings`);
2101
2329
  }
2102
- if (result.installedSessionCapture) {
2103
- console.log(`Installed hippo capture SessionEnd hook in ${result.target} settings`);
2104
- }
2105
2330
  if (result.migratedFromStop) {
2106
2331
  console.log(`Migrated legacy Stop hook → SessionEnd (was running every turn; now fires once on session exit)`);
2107
2332
  }
2108
- if (result.migratedLegacySessionEnd) {
2109
- console.log(`Migrated legacy SessionEnd entry to the new --log-file form`);
2333
+ if (result.migratedSplitSessionEnd) {
2334
+ console.log(`Migrated split sleep+capture SessionEnd entries single detached hippo session-end`);
2335
+ }
2336
+ else if (result.migratedLegacySessionEnd) {
2337
+ console.log(`Migrated legacy SessionEnd entry to the new detached form`);
2110
2338
  }
2111
2339
  }
2340
+ else if (target === 'codex') {
2341
+ const result = installCodexWrapper();
2342
+ console.log(`Installed Codex session-end integration -> ${result.metadataPath}`);
2343
+ console.log(` Wrapped detected Codex launcher at ${result.commandPath}`);
2344
+ }
2112
2345
  return;
2113
2346
  }
2114
2347
  if (subcommand === 'uninstall') {
@@ -2118,25 +2351,32 @@ function cmdHook(args, flags) {
2118
2351
  }
2119
2352
  const hook = HOOKS[target];
2120
2353
  const filepath = path.resolve(process.cwd(), hook.file);
2121
- if (!fs.existsSync(filepath)) {
2122
- console.log(`${hook.file} not found, nothing to uninstall.`);
2123
- return;
2354
+ if (fs.existsSync(filepath)) {
2355
+ const existing = fs.readFileSync(filepath, 'utf8');
2356
+ if (existing.includes(HOOK_MARKERS.start)) {
2357
+ const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
2358
+ const cleaned = existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
2359
+ fs.writeFileSync(filepath, cleaned + '\n', 'utf8');
2360
+ console.log(`Removed Hippo hook from ${hook.file}`);
2361
+ }
2362
+ else {
2363
+ console.log(`No Hippo hook found in ${hook.file}.`);
2364
+ }
2124
2365
  }
2125
- const existing = fs.readFileSync(filepath, 'utf8');
2126
- if (!existing.includes(HOOK_MARKERS.start)) {
2127
- console.log(`No Hippo hook found in ${hook.file}.`);
2128
- return;
2366
+ else {
2367
+ console.log(`${hook.file} not found, skipping agent-instructions uninstall.`);
2129
2368
  }
2130
- const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
2131
- const cleaned = existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
2132
- fs.writeFileSync(filepath, cleaned + '\n', 'utf8');
2133
- console.log(`Removed Hippo hook from ${hook.file}`);
2134
2369
  // For JSON-hook tools, also strip their SessionEnd/SessionStart entries.
2135
2370
  if (target === 'claude-code' || target === 'opencode') {
2136
2371
  if (uninstallJsonHooks(target)) {
2137
2372
  console.log(`Removed hippo hooks from ${target} settings`);
2138
2373
  }
2139
2374
  }
2375
+ else if (target === 'codex') {
2376
+ if (uninstallCodexWrapper()) {
2377
+ console.log('Removed Codex wrapper integration');
2378
+ }
2379
+ }
2140
2380
  return;
2141
2381
  }
2142
2382
  console.error('Usage: hippo hook <install|uninstall|list> [target]');
@@ -2154,6 +2394,7 @@ function cmdSetup(flags) {
2154
2394
  console.log('Hippo setup -- configuring SessionEnd + SessionStart hooks');
2155
2395
  console.log('');
2156
2396
  const jsonTools = tools.filter((t) => t.kind === 'json-hook' && (t.detected || forceAll));
2397
+ const wrapperTools = tools.filter((t) => t.kind === 'wrapper' && (t.detected || forceAll));
2157
2398
  const skipped = tools.filter((t) => t.kind === 'json-hook' && !t.detected && !forceAll);
2158
2399
  const markdownTools = tools.filter((t) => t.kind === 'markdown-instruction' && t.detected);
2159
2400
  const pluginTools = tools.filter((t) => t.kind === 'plugin' && t.detected);
@@ -2172,14 +2413,14 @@ function cmdSetup(flags) {
2172
2413
  const result = installJsonHooks(tool.name);
2173
2414
  const bits = [];
2174
2415
  if (result.installedSessionEnd)
2175
- bits.push('SessionEnd (sleep)');
2176
- if (result.installedSessionCapture)
2177
- bits.push('SessionEnd (capture)');
2416
+ bits.push('SessionEnd (session-end)');
2178
2417
  if (result.installedSessionStart)
2179
2418
  bits.push('SessionStart');
2180
2419
  if (result.migratedFromStop)
2181
2420
  bits.push('migrated legacy Stop');
2182
- if (result.migratedLegacySessionEnd)
2421
+ if (result.migratedSplitSessionEnd)
2422
+ bits.push('migrated split SessionEnd → session-end');
2423
+ else if (result.migratedLegacySessionEnd)
2183
2424
  bits.push('migrated legacy SessionEnd');
2184
2425
  if (bits.length === 0) {
2185
2426
  console.log(` ${tool.name.padEnd(14)} already configured (${result.settingsPath})`);
@@ -2191,6 +2432,24 @@ function cmdSetup(flags) {
2191
2432
  for (const tool of skipped) {
2192
2433
  console.log(` ${tool.name.padEnd(14)} not detected at ${tool.configDir} -- skipping`);
2193
2434
  }
2435
+ for (const tool of wrapperTools) {
2436
+ if (dryRun) {
2437
+ console.log(`[dry-run] would wrap the detected ${tool.name} launcher in place`);
2438
+ continue;
2439
+ }
2440
+ if (tool.name === 'codex') {
2441
+ const result = ensureCodexWrapperInstalled();
2442
+ if (result.status === 'installed') {
2443
+ console.log(` ${tool.name.padEnd(14)} wrapped launcher -> ${result.commandPath}`);
2444
+ }
2445
+ else if (result.status === 'already-installed') {
2446
+ console.log(` ${tool.name.padEnd(14)} already wrapped -> ${result.commandPath}`);
2447
+ }
2448
+ else {
2449
+ console.log(` ${tool.name.padEnd(14)} not found on PATH -- skipping`);
2450
+ }
2451
+ }
2452
+ }
2194
2453
  if (pluginTools.length > 0) {
2195
2454
  console.log('');
2196
2455
  console.log('Plugin-based tools (hook API via plugin, not JSON):');
@@ -2215,8 +2474,7 @@ function installClaudeCodeSessionEndHook() {
2215
2474
  const result = installJsonHooks('claude-code');
2216
2475
  return {
2217
2476
  installed: result.installedSessionEnd ||
2218
- result.installedSessionStart ||
2219
- result.installedSessionCapture,
2477
+ result.installedSessionStart,
2220
2478
  migratedFromStop: result.migratedFromStop,
2221
2479
  };
2222
2480
  }
@@ -2421,13 +2679,15 @@ Commands:
2421
2679
  available SessionEnd+SessionStart hooks
2422
2680
  --all Install for every JSON-hook tool, even if not detected
2423
2681
  --dry-run Show what would be installed without writing
2424
- last-sleep Print the last 'hippo sleep --log-file' output and clear it
2425
- --path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
2426
- --keep Print without clearing
2427
- hook <sub> [target] Manage framework integrations
2428
- hook list Show available hooks
2429
- hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
2430
- claude-code also installs SessionEnd+SessionStart in settings.json
2682
+ last-sleep Print the last 'hippo sleep --log-file' output and clear it
2683
+ --path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
2684
+ --keep Print without clearing
2685
+ codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
2686
+ hook <sub> [target] Manage framework integrations
2687
+ hook list Show available hooks
2688
+ hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
2689
+ claude-code/opencode install SessionEnd+SessionStart;
2690
+ codex wraps the detected launcher in place
2431
2691
  hook uninstall <target> Remove hook
2432
2692
  decide "<decision>" Record an architectural decision (90-day half-life)
2433
2693
  --context "<why>" Why this decision was made
@@ -2489,6 +2749,7 @@ Examples:
2489
2749
  const { command, args, flags } = parseArgs(process.argv);
2490
2750
  const hippoRoot = getHippoRoot(process.cwd());
2491
2751
  async function main() {
2752
+ maybeAutoInstallCodexWrapper(command, args);
2492
2753
  switch (command) {
2493
2754
  case 'init':
2494
2755
  cmdInit(hippoRoot, flags);
@@ -2517,6 +2778,18 @@ async function main() {
2517
2778
  case 'last-sleep':
2518
2779
  cmdLastSleep(flags);
2519
2780
  break;
2781
+ case 'session-end':
2782
+ cmdSessionEnd(hippoRoot, flags);
2783
+ break;
2784
+ case '__session-end-worker':
2785
+ cmdSessionEndWorker(hippoRoot, flags);
2786
+ break;
2787
+ case 'codex-run':
2788
+ cmdCodexRun(hippoRoot, args);
2789
+ break;
2790
+ case '__codex-session-end-worker':
2791
+ cmdCodexSessionEndWorker(hippoRoot, flags);
2792
+ break;
2520
2793
  case 'dedup':
2521
2794
  cmdDedup(hippoRoot, flags);
2522
2795
  break;