hippo-memory 1.52.8 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +185 -101
  2. package/dist/agent-memories/apply.d.ts +47 -0
  3. package/dist/agent-memories/apply.js +253 -0
  4. package/dist/agent-memories/claude-code.d.ts +11 -0
  5. package/dist/agent-memories/claude-code.js +113 -0
  6. package/dist/agent-memories/codex.d.ts +3 -0
  7. package/dist/agent-memories/codex.js +47 -0
  8. package/dist/agent-memories/copilot.d.ts +3 -0
  9. package/dist/agent-memories/copilot.js +125 -0
  10. package/dist/agent-memories/files.d.ts +37 -0
  11. package/dist/agent-memories/files.js +77 -0
  12. package/dist/agent-memories/folder-store.d.ts +17 -0
  13. package/dist/agent-memories/folder-store.js +44 -0
  14. package/dist/agent-memories/gemini.d.ts +3 -0
  15. package/dist/agent-memories/gemini.js +103 -0
  16. package/dist/agent-memories/git.d.ts +8 -0
  17. package/dist/agent-memories/git.js +11 -0
  18. package/dist/agent-memories/keys.d.ts +9 -0
  19. package/dist/agent-memories/keys.js +20 -0
  20. package/dist/agent-memories/legacy.d.ts +17 -0
  21. package/dist/agent-memories/legacy.js +45 -0
  22. package/dist/agent-memories/markdown.d.ts +13 -0
  23. package/dist/agent-memories/markdown.js +123 -0
  24. package/dist/agent-memories/openclaw.d.ts +3 -0
  25. package/dist/agent-memories/openclaw.js +42 -0
  26. package/dist/agent-memories/plan.d.ts +78 -0
  27. package/dist/agent-memories/plan.js +123 -0
  28. package/dist/agent-memories/qwen-code.d.ts +5 -0
  29. package/dist/agent-memories/qwen-code.js +50 -0
  30. package/dist/agent-memories/report.d.ts +52 -0
  31. package/dist/agent-memories/report.js +88 -0
  32. package/dist/agent-memories/source.d.ts +16 -0
  33. package/dist/agent-memories/source.js +32 -0
  34. package/dist/agent-memories/sync.d.ts +33 -0
  35. package/dist/agent-memories/sync.js +336 -0
  36. package/dist/agent-memories/tools.d.ts +33 -0
  37. package/dist/agent-memories/tools.js +19 -0
  38. package/dist/agent-memories/types.d.ts +42 -0
  39. package/dist/agent-memories/types.js +2 -0
  40. package/dist/api.d.ts +53 -20
  41. package/dist/api.js +141 -97
  42. package/dist/audit.d.ts +2 -1
  43. package/dist/audit.js +68 -2
  44. package/dist/capture.d.ts +48 -22
  45. package/dist/capture.js +186 -161
  46. package/dist/cli.d.ts +0 -2
  47. package/dist/cli.js +750 -797
  48. package/dist/codex-patch.d.ts +12 -0
  49. package/dist/codex-patch.js +71 -0
  50. package/dist/compaction-items.d.ts +18 -0
  51. package/dist/compaction-items.js +60 -0
  52. package/dist/compaction-record.d.ts +94 -0
  53. package/dist/compaction-record.js +546 -0
  54. package/dist/config.d.ts +4 -1
  55. package/dist/config.js +13 -4
  56. package/dist/connectors/slack/types.d.ts +0 -1
  57. package/dist/consolidate.js +87 -34
  58. package/dist/context-render.d.ts +36 -0
  59. package/dist/context-render.js +154 -0
  60. package/dist/dag.js +3 -2
  61. package/dist/db.d.ts +5 -1
  62. package/dist/db.js +46 -14
  63. package/dist/dedupe.d.ts +6 -6
  64. package/dist/dedupe.js +10 -9
  65. package/dist/doctor.d.ts +1 -1
  66. package/dist/doctor.js +69 -4
  67. package/dist/dormant.d.ts +9 -3
  68. package/dist/dormant.js +26 -2
  69. package/dist/embedding-provider.d.ts +2 -1
  70. package/dist/embedding-provider.js +2 -1
  71. package/dist/embeddings.js +23 -3
  72. package/dist/extract.js +5 -1
  73. package/dist/forward-claim-detector.d.ts +1 -1
  74. package/dist/forward-claim-detector.js +1 -1
  75. package/dist/gated-write.d.ts +9 -0
  76. package/dist/gated-write.js +24 -0
  77. package/dist/graph-recall.d.ts +3 -1
  78. package/dist/graph-recall.js +5 -3
  79. package/dist/hooks.d.ts +18 -2
  80. package/dist/hooks.js +128 -32
  81. package/dist/importers.js +5 -12
  82. package/dist/judgment.d.ts +30 -0
  83. package/dist/judgment.js +122 -0
  84. package/dist/mcp/server.js +171 -210
  85. package/dist/memory.d.ts +19 -2
  86. package/dist/memory.js +35 -3
  87. package/dist/merged-row.d.ts +6 -0
  88. package/dist/merged-row.js +35 -0
  89. package/dist/multihop.d.ts +2 -1
  90. package/dist/multihop.js +7 -4
  91. package/dist/physics-state.d.ts +0 -4
  92. package/dist/physics-state.js +0 -6
  93. package/dist/predictions.d.ts +2 -17
  94. package/dist/predictions.js +2 -15
  95. package/dist/reject-flow.d.ts +7 -5
  96. package/dist/reject-flow.js +41 -12
  97. package/dist/salience.js +12 -5
  98. package/dist/same-text.d.ts +17 -0
  99. package/dist/same-text.js +38 -0
  100. package/dist/scheduler.d.ts +4 -0
  101. package/dist/scheduler.js +8 -0
  102. package/dist/search.d.ts +7 -0
  103. package/dist/search.js +16 -32
  104. package/dist/secret-detect.d.ts +2 -0
  105. package/dist/secret-detect.js +6 -0
  106. package/dist/server-detect.js +9 -33
  107. package/dist/server.js +6 -62
  108. package/dist/session-digest.d.ts +79 -0
  109. package/dist/session-digest.js +528 -0
  110. package/dist/shared.d.ts +10 -2
  111. package/dist/shared.js +44 -36
  112. package/dist/store.d.ts +9 -2
  113. package/dist/store.js +25 -2
  114. package/dist/token-ledger.d.ts +46 -8
  115. package/dist/token-ledger.js +140 -21
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.js +1 -1
  118. package/extensions/openclaw-plugin/README.md +4 -4
  119. package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
  120. package/extensions/openclaw-plugin/package.json +1 -1
  121. package/openclaw.plugin.json +2 -2
  122. package/package.json +2 -2
package/dist/hooks.js CHANGED
@@ -16,6 +16,10 @@
16
16
  * - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
17
17
  * - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
18
18
  * - 0.22.x: separate sleep + capture SessionEnd entries.
19
+ * PreCompact and PostCompact entries go in too: the first records the compaction and
20
+ * asks the summariser for a "Memories for hippo" list, the second saves that list.
21
+ * Codex's hooks.json gets only two groups (per-prompt memory and
22
+ * compact-resume); see installCodexHooks.
19
23
  *
20
24
  * 2. Plugin install (OpenCode only). OpenCode does NOT share Claude Code's
21
25
  * JSON-hook schema — its config has `additionalProperties: false` and no
@@ -40,7 +44,7 @@ function isJsonString(value) {
40
44
  }
41
45
  /** JSON-value plain-object check (excludes arrays and null), typeof-free for the same
42
46
  * reason as isJsonString above. */
43
- function isJsonObject(value) {
47
+ export function isJsonObject(value) {
44
48
  return value !== undefined && value !== null && !Array.isArray(value) && value.constructor === Object;
45
49
  }
46
50
  const HIPPO_SLEEP_MARKER = 'hippo sleep';
@@ -121,6 +125,16 @@ export { HIPPO_OPENCODE_PLUGIN_MARKER };
121
125
  function homeDir() {
122
126
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
123
127
  }
128
+ /** Codex's config folder: $CODEX_HOME, else ~/.codex, as the Codex hooks docs describe. */
129
+ export function codexHomeDir(home = homeDir(), env = process.env) {
130
+ return env.CODEX_HOME || path.join(home, '.codex');
131
+ }
132
+ /** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
133
+ export function isCodexPresent(home = homeDir()) {
134
+ return fs.statSync(codexHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true;
135
+ }
136
+ /** Codex hashes each hook and skips new or changed ones until the user reviews them in `/hooks`, so the reminder says what they would trust. */
137
+ export const CODEX_TRUST_LINE = "The per-prompt hook sends your pinned memories plus the five most recent ones. Codex runs hippo's hooks only after you trust them once in `/hooks`.";
124
138
  /**
125
139
  * Default log path consumed by `hippo last-sleep`. Shared fallback when
126
140
  * a caller doesn't pass --path explicitly.
@@ -506,6 +520,12 @@ export function resolveJsonHookPaths(target) {
506
520
  logFile: path.join(logsDir, 'claude-code-sleep.log'),
507
521
  display: 'Claude Code',
508
522
  };
523
+ case 'codex':
524
+ return {
525
+ settings: path.join(codexHomeDir(home), 'hooks.json'),
526
+ logFile: path.join(logsDir, 'codex-sleep.log'),
527
+ display: 'Codex',
528
+ };
509
529
  }
510
530
  }
511
531
  function hookArrayContains(hookArray, marker) {
@@ -560,6 +580,59 @@ function hasLegacySplitSessionEnd(hookArray) {
560
580
  const hasCapture = serialized.includes(HIPPO_CAPTURE_MARKER);
561
581
  return (hasSleep || hasCapture) && !serialized.includes(HIPPO_SESSION_END_MARKER);
562
582
  }
583
+ function nothingInstalled(target, settingsPath) {
584
+ return {
585
+ target,
586
+ settingsPath,
587
+ installedSessionEnd: false,
588
+ installedSessionStart: false,
589
+ installedUserPromptSubmit: false,
590
+ installedPreCompact: false,
591
+ installedCompactResume: false,
592
+ installedPostCompact: false,
593
+ installedCaptureError: false,
594
+ migratedPinnedInjectRecent: false,
595
+ migratedFromStop: false,
596
+ migratedLegacySessionEnd: false,
597
+ migratedSplitSessionEnd: false,
598
+ invalidJson: false,
599
+ };
600
+ }
601
+ /** A command hook with a Windows form: Codex runs hooks in PowerShell there, whose execution policy can block npm's hippo.ps1. */
602
+ function codexCommandHook(command, timeout) {
603
+ return { type: 'command', command, commandWindows: command.replace(/^hippo /, 'hippo.cmd '), timeout };
604
+ }
605
+ /** Codex keys trust to each hook's position and hash and re-asks for a changed one, so hippo only appends and never edits an entry. */
606
+ function installCodexHooks(settingsPath, settings) {
607
+ const result = nothingInstalled('codex', settingsPath);
608
+ if (!isJsonObject(settings))
609
+ return { ...result, invalidJson: true };
610
+ if (settings.hooks === undefined)
611
+ settings.hooks = {};
612
+ const hooks = settings.hooks;
613
+ const events = ['UserPromptSubmit', 'SessionStart'];
614
+ if (!isJsonObject(hooks) || events.some((e) => hooks[e] !== undefined && !Array.isArray(hooks[e]))) {
615
+ return { ...result, invalidJson: true };
616
+ }
617
+ const append = (event, marker, group) => {
618
+ const groups = hooks[event];
619
+ if (hookArrayContains(groups, marker))
620
+ return false;
621
+ hooks[event] = [...(Array.isArray(groups) ? groups : []), group];
622
+ return true;
623
+ };
624
+ const installedUserPromptSubmit = append('UserPromptSubmit', HIPPO_PINNED_INJECT_MARKER, {
625
+ hooks: [codexCommandHook(HIPPO_PINNED_INJECT_COMMAND, 5)],
626
+ });
627
+ const installedCompactResume = append('SessionStart', HIPPO_COMPACT_RESUME_MARKER, {
628
+ matcher: 'compact',
629
+ hooks: [codexCommandHook(HIPPO_COMPACT_RESUME_MARKER, 10)],
630
+ });
631
+ if (installedUserPromptSubmit || installedCompactResume) {
632
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
633
+ }
634
+ return { ...result, installedUserPromptSubmit, installedCompactResume };
635
+ }
563
636
  export function installJsonHooks(target) {
564
637
  const { settings: settingsPath, logFile } = resolveJsonHookPaths(target);
565
638
  const dir = path.dirname(settingsPath);
@@ -571,23 +644,11 @@ export function installJsonHooks(target) {
571
644
  settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
572
645
  }
573
646
  catch {
574
- return {
575
- target,
576
- settingsPath,
577
- installedSessionEnd: false,
578
- installedSessionStart: false,
579
- installedUserPromptSubmit: false,
580
- installedPreCompact: false,
581
- installedCompactResume: false,
582
- installedPostCompact: false,
583
- installedCaptureError: false,
584
- migratedPinnedInjectRecent: false,
585
- migratedFromStop: false,
586
- migratedLegacySessionEnd: false,
587
- migratedSplitSessionEnd: false,
588
- };
647
+ return { ...nothingInstalled(target, settingsPath), invalidJson: true };
589
648
  }
590
649
  }
650
+ if (target === 'codex')
651
+ return installCodexHooks(settingsPath, settings);
591
652
  if (!settings.hooks)
592
653
  settings.hooks = {};
593
654
  // SAFETY: settings.hooks is either freshly initialised to {} on the line above, or an
@@ -672,8 +733,8 @@ export function installJsonHooks(target) {
672
733
  });
673
734
  installedUserPromptSubmit = true;
674
735
  }
675
- // PreCompact: fires on manual AND auto compaction (no matcher). Writes a
676
- // working-state snapshot before the transcript summary drops detail.
736
+ // PreCompact: fires on manual AND auto compaction (no matcher). Records the compaction, asks the
737
+ // summariser for a "Memories for hippo" list and saves a working-state snapshot before the summary drops detail.
677
738
  // Exit-0 contract lives in the verb itself (src/capture.ts cmdPreCompact),
678
739
  // not here — this is install-time wiring only.
679
740
  let installedPreCompact = false;
@@ -713,9 +774,8 @@ export function installJsonHooks(target) {
713
774
  });
714
775
  installedCompactResume = true;
715
776
  }
716
- // PostCompact: tells the user what pre-compact saved. PreCompact itself
717
- // must stay silent, because Claude Code hands PreCompact stdout to the
718
- // summarising model as instructions; PostCompact stdout is only shown.
777
+ // PostCompact: saves the memories the summariser listed and prints one line, which Claude Code only shows.
778
+ // PreCompact stdout, by contrast, is handed to the summariser as instructions, so pre-compact prints just the request.
719
779
  let installedPostCompact = false;
720
780
  if (!hookArrayContains(hooks.PostCompact, HIPPO_POST_COMPACT_MARKER)) {
721
781
  if (!Array.isArray(hooks.PostCompact))
@@ -777,8 +837,41 @@ export function installJsonHooks(target) {
777
837
  migratedFromStop,
778
838
  migratedLegacySessionEnd,
779
839
  migratedSplitSessionEnd,
840
+ invalidJson: false,
780
841
  };
781
842
  }
843
+ /** The exact command hippo writes for each Codex event; uninstall removes only these handlers. */
844
+ const CODEX_HOOK_COMMANDS = [
845
+ ['UserPromptSubmit', HIPPO_PINNED_INJECT_COMMAND],
846
+ ['SessionStart', HIPPO_COMPACT_RESUME_MARKER],
847
+ ];
848
+ /** A group loses only hippo's handlers and goes only once empty, so a user's hook beside or like hippo's stays. */
849
+ function uninstallCodexHooks(hooks) {
850
+ let changed = false;
851
+ for (const [event, command] of CODEX_HOOK_COMMANDS) {
852
+ const groups = hooks[event];
853
+ if (!Array.isArray(groups))
854
+ continue;
855
+ let removed = false;
856
+ const kept = groups.flatMap((group) => {
857
+ if (!isJsonObject(group) || !Array.isArray(group.hooks))
858
+ return [group];
859
+ const handlers = group.hooks.filter((h) => !(isJsonObject(h) && h.command === command));
860
+ if (handlers.length === group.hooks.length)
861
+ return [group];
862
+ removed = true;
863
+ return handlers.length > 0 ? [{ ...group, hooks: handlers }] : [];
864
+ });
865
+ if (!removed)
866
+ continue;
867
+ changed = true;
868
+ if (kept.length > 0)
869
+ hooks[event] = kept;
870
+ else
871
+ delete hooks[event];
872
+ }
873
+ return changed;
874
+ }
782
875
  export function uninstallJsonHooks(target) {
783
876
  const { settings: settingsPath } = resolveJsonHookPaths(target);
784
877
  if (!fs.existsSync(settingsPath))
@@ -790,11 +883,19 @@ export function uninstallJsonHooks(target) {
790
883
  catch {
791
884
  return false;
792
885
  }
793
- // SAFETY: Claude Code's settings.json always stores `hooks` as an object when
794
- // present; each event key below is still re-validated with Array.isArray before use.
795
- const hooks = settings.hooks;
796
- if (!hooks)
886
+ if (!isJsonObject(settings) || !isJsonObject(settings.hooks))
887
+ return false;
888
+ const changed = target === 'codex' ? uninstallCodexHooks(settings.hooks) : uninstallClaudeCodeHooks(settings.hooks);
889
+ if (!changed)
797
890
  return false;
891
+ if (Object.keys(settings.hooks).length === 0)
892
+ delete settings.hooks;
893
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
894
+ return true;
895
+ }
896
+ function uninstallClaudeCodeHooks(settingsHooks) {
897
+ // SAFETY: each event key below is re-validated with Array.isArray before use.
898
+ const hooks = settingsHooks;
798
899
  let changed = false;
799
900
  const markersByKey = {
800
901
  SessionEnd: [HIPPO_SESSION_END_MARKER, HIPPO_SLEEP_MARKER, HIPPO_CAPTURE_MARKER],
@@ -819,12 +920,7 @@ export function uninstallJsonHooks(target) {
819
920
  delete hooks[key];
820
921
  }
821
922
  }
822
- if (!changed)
823
- return false;
824
- if (Object.keys(hooks).length === 0)
825
- delete settings.hooks;
826
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
827
- return true;
923
+ return changed;
828
924
  }
829
925
  export function resolveOpencodePluginPath() {
830
926
  return path.join(homeDir(), '.config', 'opencode', 'plugins', 'hippo.ts');
@@ -971,7 +1067,7 @@ export function detectInstalledTools() {
971
1067
  { name: 'claude-code', configDir: '~/.claude', detected: exists('.claude'), kind: 'json-hook' },
972
1068
  { name: 'opencode', configDir: '~/.config/opencode', detected: exists('.config', 'opencode'), kind: 'plugin', notes: 'installs a TS plugin at ~/.config/opencode/plugins/hippo.ts' },
973
1069
  { name: 'openclaw', configDir: '~/.openclaw', detected: exists('.openclaw'), kind: 'plugin', notes: 'install via `openclaw plugins install hippo-memory`' },
974
- { name: 'codex', configDir: '~/.codex', detected: exists('.codex'), kind: 'wrapper', notes: 'wraps the detected codex launcher for session-end consolidation' },
1070
+ { name: 'codex', configDir: '~/.codex', detected: isCodexPresent(home), kind: 'wrapper', notes: 'memory hooks in hooks.json, and wraps the detected codex launcher for session-end consolidation' },
975
1071
  { name: 'cursor', configDir: '~/.cursor', detected: exists('.cursor'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
976
1072
  { name: 'pi', configDir: '~/.pi', detected: exists('.pi'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
977
1073
  ];
package/dist/importers.js CHANGED
@@ -7,7 +7,7 @@ import * as path from 'path';
7
7
  import { createHash } from 'node:crypto';
8
8
  import { createMemory, Layer } from './memory.js';
9
9
  import { initStore, loadAllEntries, writeEntry } from './store.js';
10
- import { textOverlap } from './search.js';
10
+ import { duplicateKey, storedTextKeys } from './same-text.js';
11
11
  import { getGlobalRoot, initGlobal } from './shared.js';
12
12
  import { remember, archiveRaw, isPrivateScope } from './api.js';
13
13
  import { openHippoDb, closeHippoDb } from './db.js';
@@ -26,7 +26,7 @@ export function importEntries(chunks, source, tags, options) {
26
26
  if (options.global) {
27
27
  initGlobal();
28
28
  }
29
- const existing = loadAllEntries(targetRoot, options.global ? undefined : options.tenantId);
29
+ const keys = storedTextKeys(loadAllEntries(targetRoot, options.global ? undefined : options.tenantId));
30
30
  const allTags = [...new Set([...tags, ...(options.extraTags ?? [])])];
31
31
  const baseHalfLifeDays = loadConfig(targetRoot).defaultHalfLifeDays;
32
32
  let total = 0;
@@ -53,15 +53,8 @@ export function importEntries(chunks, source, tags, options) {
53
53
  continue;
54
54
  }
55
55
  total++;
56
- // Dedup check: textOverlap > 0.7 with any existing memory = skip
57
- let isDuplicate = false;
58
- for (const existing_entry of existing) {
59
- if (textOverlap(chunk, existing_entry.content) > 0.7) {
60
- isDuplicate = true;
61
- break;
62
- }
63
- }
64
- if (isDuplicate) {
56
+ // Dedup check: skip only when the same text is already stored
57
+ if (keys.has(duplicateKey(chunk))) {
65
58
  skipped++;
66
59
  continue;
67
60
  }
@@ -112,7 +105,7 @@ export function importEntries(chunks, source, tags, options) {
112
105
  throw err;
113
106
  }
114
107
  // Add to existing so subsequent chunks dedup against freshly imported ones
115
- existing.push(entry);
108
+ keys.add(duplicateKey(chunk));
116
109
  }
117
110
  entries.push(entry);
118
111
  imported++;
@@ -0,0 +1,30 @@
1
+ /** Typed judgment over capture candidates via TypeSafe's Jev (System One).
2
+ * Regex picks WHAT is a candidate; it cannot say what is worth keeping, so
3
+ * every captured memory currently lands on a flat schema_fit of 0.5. */
4
+ import { ConfidenceLevel, EmotionalValence } from './memory.js';
5
+ export type JudgedKind = 'error' | 'decision' | 'convention' | 'preference' | 'trivia';
6
+ export interface Judgment {
7
+ /** Jev noul, 0..1. Maps to `schema_fit` on the written entry. */
8
+ durable: number;
9
+ kind: JudgedKind;
10
+ valence: EmotionalValence;
11
+ confidence: ConfidenceLevel;
12
+ /** Jev's calibrated confidence on the kind choice, 0..1. */
13
+ kindConfidence: number;
14
+ }
15
+ export interface JudgeOptions {
16
+ apiKey: string;
17
+ model?: string;
18
+ /** Injected for testing — defaults to the real fetch. */
19
+ fetcher?: typeof fetch;
20
+ /** Bounded parallelism for `judgeAll`. */
21
+ concurrency?: number;
22
+ }
23
+ /** Absent key means hippo keeps its pre-Jev behaviour and makes no HTTP call. */
24
+ export declare function judgmentApiKey(): string | undefined;
25
+ /** `null` on any failure, so a Jev outage degrades capture to today's
26
+ * behaviour instead of blocking the write. */
27
+ export declare function judge(content: string, opts: JudgeOptions): Promise<Judgment | null>;
28
+ /** Judge many candidates under a bounded concurrency pool, order preserved. */
29
+ export declare function judgeAll(contents: readonly string[], opts: JudgeOptions): Promise<(Judgment | null)[]>;
30
+ //# sourceMappingURL=judgment.d.ts.map
@@ -0,0 +1,122 @@
1
+ /** Typed judgment over capture candidates via TypeSafe's Jev (System One).
2
+ * Regex picks WHAT is a candidate; it cannot say what is worth keeping, so
3
+ * every captured memory currently lands on a flat schema_fit of 0.5. */
4
+ const ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
5
+ const DEFAULT_MODEL = 'jev-1.13.0';
6
+ const MAX_CONCURRENCY = 8;
7
+ const RETRY_STATUS = new Set([429, 529]);
8
+ const QUESTIONS = {
9
+ durable: {
10
+ type: 'noul',
11
+ instructions: 'This text was extracted from an AI coding agent transcript as a candidate memory. It is worth storing long-term only if it would still be useful to a future agent working on this codebase weeks from now: a durable preference, a convention, a decision with a reason, or a gotcha that will recur. Transient chatter, one-off status, restatements of code already in the repo, and anything only true inside this one session are not worth storing.',
12
+ },
13
+ kind: {
14
+ type: 'choice',
15
+ instructions: 'Classify what kind of durable knowledge this is.',
16
+ criteria: {
17
+ error: 'A failure, gotcha, or thing that went wrong, and why.',
18
+ decision: 'A choice that was made, ideally with its reason.',
19
+ convention: 'A rule, standard, or way this project does things.',
20
+ preference: 'A stated preference of the user or team.',
21
+ trivia: 'None of the above; incidental detail with no reuse value.',
22
+ },
23
+ },
24
+ valence: {
25
+ type: 'choice',
26
+ instructions: 'Classify the emotional charge of this memory for replay priority.',
27
+ criteria: {
28
+ critical: 'A costly failure or a rule whose violation causes real damage.',
29
+ negative: 'Something that went wrong, or a warning.',
30
+ positive: 'Something that worked, or a confirmed good approach.',
31
+ neutral: 'Plain fact with no success or failure charge.',
32
+ },
33
+ },
34
+ };
35
+ const KINDS = ['error', 'decision', 'convention', 'preference', 'trivia'];
36
+ const VALENCES = ['critical', 'negative', 'positive', 'neutral'];
37
+ /** Absent key means hippo keeps its pre-Jev behaviour and makes no HTTP call. */
38
+ export function judgmentApiKey() {
39
+ const key = process.env.TYPESAFE_API_KEY?.trim();
40
+ return key ? key : undefined;
41
+ }
42
+ function oneOf(value, allowed) {
43
+ return allowed.find((option) => option === value) ?? null;
44
+ }
45
+ /** `verified` is unreachable: that tier means a human or a test confirmed it. */
46
+ function toConfidenceTier(kindConfidence) {
47
+ if (kindConfidence >= 0.8)
48
+ return 'observed';
49
+ return 'inferred';
50
+ }
51
+ async function postOnce(content, opts) {
52
+ const fetchFn = opts.fetcher ?? fetch;
53
+ let res;
54
+ try {
55
+ res = await fetchFn(ENDPOINT, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'content-type': 'application/json',
59
+ authorization: `Bearer ${opts.apiKey}`,
60
+ },
61
+ body: JSON.stringify({
62
+ state: content,
63
+ model: opts.model ?? DEFAULT_MODEL,
64
+ questions: QUESTIONS,
65
+ }),
66
+ });
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ if (RETRY_STATUS.has(res.status))
72
+ return { retryable: true };
73
+ if (!res.ok)
74
+ return null;
75
+ return { res };
76
+ }
77
+ /** `null` on any failure, so a Jev outage degrades capture to today's
78
+ * behaviour instead of blocking the write. */
79
+ export async function judge(content, opts) {
80
+ const trimmed = content.trim();
81
+ if (trimmed.length < 3)
82
+ return null;
83
+ let attempt = await postOnce(trimmed, opts);
84
+ if (attempt && 'retryable' in attempt) {
85
+ await new Promise((resolve) => setTimeout(resolve, 500));
86
+ attempt = await postOnce(trimmed, opts);
87
+ }
88
+ if (!attempt || 'retryable' in attempt)
89
+ return null;
90
+ let data;
91
+ try {
92
+ // SAFETY: the documented Jev response is `{ answers: { <name>: Answer } }`
93
+ // keyed by the question names posted above; every field read below is
94
+ // optional-chained and range-checked before use, so a lie here returns null.
95
+ data = await attempt.res.json();
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ const durable = data.answers?.durable?.noul;
101
+ const kind = oneOf(data.answers?.kind?.choice, KINDS);
102
+ const valence = oneOf(data.answers?.valence?.choice, VALENCES);
103
+ if (durable === undefined || durable < 0 || durable > 1 || !kind || !valence)
104
+ return null;
105
+ const kindConfidence = data.answers?.kind?.confidence ?? 0;
106
+ return { durable, kind, valence, confidence: toConfidenceTier(kindConfidence), kindConfidence };
107
+ }
108
+ /** Judge many candidates under a bounded concurrency pool, order preserved. */
109
+ export async function judgeAll(contents, opts) {
110
+ const out = Array.from({ length: contents.length }, () => null);
111
+ const limit = Math.max(1, opts.concurrency ?? MAX_CONCURRENCY);
112
+ let cursor = 0;
113
+ const worker = async () => {
114
+ while (cursor < contents.length) {
115
+ const i = cursor++;
116
+ out[i] = await judge(contents[i], opts);
117
+ }
118
+ };
119
+ await Promise.all(Array.from({ length: Math.min(limit, contents.length) }, worker));
120
+ return out;
121
+ }
122
+ //# sourceMappingURL=judgment.js.map