hippo-memory 1.52.7 → 1.52.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +159 -99
  2. package/dist/api.d.ts +52 -18
  3. package/dist/api.js +155 -86
  4. package/dist/audit.d.ts +2 -1
  5. package/dist/audit.js +63 -0
  6. package/dist/autolearn.js +3 -2
  7. package/dist/capture.d.ts +40 -0
  8. package/dist/capture.js +141 -119
  9. package/dist/churn-git.js +2 -2
  10. package/dist/cli.d.ts +1 -4
  11. package/dist/cli.js +695 -702
  12. package/dist/codex-patch.d.ts +12 -0
  13. package/dist/codex-patch.js +71 -0
  14. package/dist/config.d.ts +0 -1
  15. package/dist/config.js +0 -4
  16. package/dist/connectors/slack/types.d.ts +0 -1
  17. package/dist/consolidate.js +85 -32
  18. package/dist/context-render.d.ts +36 -0
  19. package/dist/context-render.js +154 -0
  20. package/dist/dag.js +3 -2
  21. package/dist/dashboard.js +4 -0
  22. package/dist/db.js +6 -6
  23. package/dist/dedupe.d.ts +6 -6
  24. package/dist/dedupe.js +10 -9
  25. package/dist/doctor.d.ts +1 -1
  26. package/dist/doctor.js +35 -2
  27. package/dist/dormant.d.ts +4 -0
  28. package/dist/dormant.js +17 -2
  29. package/dist/embedding-provider.d.ts +2 -1
  30. package/dist/embedding-provider.js +2 -1
  31. package/dist/embeddings.js +23 -3
  32. package/dist/extensions/openclaw-plugin/index.js +1 -0
  33. package/dist/extract.js +5 -1
  34. package/dist/forward-claim-detector.d.ts +1 -1
  35. package/dist/forward-claim-detector.js +1 -1
  36. package/dist/graph-recall.d.ts +3 -1
  37. package/dist/graph-recall.js +13 -10
  38. package/dist/handoff.d.ts +2 -0
  39. package/dist/hooks.d.ts +19 -5
  40. package/dist/hooks.js +131 -30
  41. package/dist/importers.js +5 -12
  42. package/dist/judgment.d.ts +30 -0
  43. package/dist/judgment.js +122 -0
  44. package/dist/mcp/server.js +174 -213
  45. package/dist/merged-row.d.ts +6 -0
  46. package/dist/merged-row.js +35 -0
  47. package/dist/multihop.d.ts +2 -1
  48. package/dist/multihop.js +7 -4
  49. package/dist/physics-state.d.ts +0 -4
  50. package/dist/physics-state.js +0 -6
  51. package/dist/predictions.d.ts +2 -17
  52. package/dist/predictions.js +2 -15
  53. package/dist/reject-flow.d.ts +7 -5
  54. package/dist/reject-flow.js +41 -12
  55. package/dist/salience.js +12 -5
  56. package/dist/same-text.d.ts +17 -0
  57. package/dist/same-text.js +38 -0
  58. package/dist/scheduler.d.ts +4 -0
  59. package/dist/scheduler.js +8 -0
  60. package/dist/search.d.ts +7 -0
  61. package/dist/search.js +16 -32
  62. package/dist/secret-detect.d.ts +2 -0
  63. package/dist/secret-detect.js +9 -3
  64. package/dist/server-detect.js +9 -33
  65. package/dist/server.js +6 -62
  66. package/dist/session-digest.d.ts +79 -0
  67. package/dist/session-digest.js +528 -0
  68. package/dist/shared.d.ts +11 -3
  69. package/dist/shared.js +41 -31
  70. package/dist/store.d.ts +4 -5
  71. package/dist/store.js +26 -13
  72. package/dist/token-ledger.d.ts +46 -8
  73. package/dist/token-ledger.js +140 -21
  74. package/dist/version.d.ts +1 -1
  75. package/dist/version.js +1 -1
  76. package/dist-ui/assets/index-BhT8RvO6.js +61 -0
  77. package/dist-ui/index.html +1 -1
  78. package/extensions/openclaw-plugin/README.md +4 -4
  79. package/extensions/openclaw-plugin/index.ts +1 -0
  80. package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
  81. package/extensions/openclaw-plugin/package.json +1 -1
  82. package/openclaw.plugin.json +2 -2
  83. package/package.json +2 -2
  84. package/dist-ui/assets/index-BgmA7Hwe.js +0 -61
package/dist/hooks.js CHANGED
@@ -10,12 +10,14 @@
10
10
  * sequence, writing both outputs to the log file. The parent returns in
11
11
  * <100ms so the TUI teardown can't kill the child before it finishes.
12
12
  * - SessionStart: `hippo last-sleep --path <path>` - prints the log
13
- * written by the previous session's detached worker and then clears it,
14
- * so the user actually sees what was consolidated.
13
+ * written by the previous session's detached worker to stderr, which
14
+ * keeps it out of the model's context, and then clears it.
15
15
  * Earlier Claude Code forms are detected and migrated automatically:
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
+ * Codex's hooks.json gets only two groups (per-prompt memory and
20
+ * compact-resume); see installCodexHooks.
19
21
  *
20
22
  * 2. Plugin install (OpenCode only). OpenCode does NOT share Claude Code's
21
23
  * JSON-hook schema — its config has `additionalProperties: false` and no
@@ -40,7 +42,7 @@ function isJsonString(value) {
40
42
  }
41
43
  /** JSON-value plain-object check (excludes arrays and null), typeof-free for the same
42
44
  * reason as isJsonString above. */
43
- function isJsonObject(value) {
45
+ export function isJsonObject(value) {
44
46
  return value !== undefined && value !== null && !Array.isArray(value) && value.constructor === Object;
45
47
  }
46
48
  const HIPPO_SLEEP_MARKER = 'hippo sleep';
@@ -121,6 +123,16 @@ export { HIPPO_OPENCODE_PLUGIN_MARKER };
121
123
  function homeDir() {
122
124
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
123
125
  }
126
+ /** Codex's config folder: $CODEX_HOME, else ~/.codex, as the Codex hooks docs describe. */
127
+ export function codexHomeDir(home = homeDir()) {
128
+ return process.env.CODEX_HOME || path.join(home, '.codex');
129
+ }
130
+ /** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
131
+ export function isCodexPresent(home = homeDir()) {
132
+ return fs.statSync(codexHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true;
133
+ }
134
+ /** 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. */
135
+ 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
136
  /**
125
137
  * Default log path consumed by `hippo last-sleep`. Shared fallback when
126
138
  * a caller doesn't pass --path explicitly.
@@ -412,10 +424,16 @@ export function isCodexWrapperInstalled() {
412
424
  * doing it from postinstall or routine commands is a consent violation and
413
425
  * reads as binary hijacking to security scanners (issue #133).
414
426
  */
415
- export function repairCodexWrapperIfInstalled() {
427
+ export function repairCodexWrapperIfInstalled(hippoCliPath = resolveHippoCliPath()) {
416
428
  if (readCodexWrapperMetadata() === null) {
417
429
  return { status: 'not-found' };
418
430
  }
431
+ // A checkout's own `npm install` or CLI would point the user's launcher at a folder that may be deleted.
432
+ // SHORTCUT: any node_modules copy (npx cache, a project dependency) still repairs; record the opted-in CLI path in the metadata to close that.
433
+ const packageDir = path.dirname(path.dirname(hippoCliPath));
434
+ if (path.basename(path.dirname(packageDir)).toLowerCase() !== 'node_modules') {
435
+ return { status: 'source-checkout' };
436
+ }
419
437
  return ensureCodexWrapperInstalled();
420
438
  }
421
439
  function collectFiles(dir) {
@@ -500,6 +518,12 @@ export function resolveJsonHookPaths(target) {
500
518
  logFile: path.join(logsDir, 'claude-code-sleep.log'),
501
519
  display: 'Claude Code',
502
520
  };
521
+ case 'codex':
522
+ return {
523
+ settings: path.join(codexHomeDir(home), 'hooks.json'),
524
+ logFile: path.join(logsDir, 'codex-sleep.log'),
525
+ display: 'Codex',
526
+ };
503
527
  }
504
528
  }
505
529
  function hookArrayContains(hookArray, marker) {
@@ -554,6 +578,59 @@ function hasLegacySplitSessionEnd(hookArray) {
554
578
  const hasCapture = serialized.includes(HIPPO_CAPTURE_MARKER);
555
579
  return (hasSleep || hasCapture) && !serialized.includes(HIPPO_SESSION_END_MARKER);
556
580
  }
581
+ function nothingInstalled(target, settingsPath) {
582
+ return {
583
+ target,
584
+ settingsPath,
585
+ installedSessionEnd: false,
586
+ installedSessionStart: false,
587
+ installedUserPromptSubmit: false,
588
+ installedPreCompact: false,
589
+ installedCompactResume: false,
590
+ installedPostCompact: false,
591
+ installedCaptureError: false,
592
+ migratedPinnedInjectRecent: false,
593
+ migratedFromStop: false,
594
+ migratedLegacySessionEnd: false,
595
+ migratedSplitSessionEnd: false,
596
+ invalidJson: false,
597
+ };
598
+ }
599
+ /** A command hook with a Windows form: Codex runs hooks in PowerShell there, whose execution policy can block npm's hippo.ps1. */
600
+ function codexCommandHook(command, timeout) {
601
+ return { type: 'command', command, commandWindows: command.replace(/^hippo /, 'hippo.cmd '), timeout };
602
+ }
603
+ /** 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. */
604
+ function installCodexHooks(settingsPath, settings) {
605
+ const result = nothingInstalled('codex', settingsPath);
606
+ if (!isJsonObject(settings))
607
+ return { ...result, invalidJson: true };
608
+ if (settings.hooks === undefined)
609
+ settings.hooks = {};
610
+ const hooks = settings.hooks;
611
+ const events = ['UserPromptSubmit', 'SessionStart'];
612
+ if (!isJsonObject(hooks) || events.some((e) => hooks[e] !== undefined && !Array.isArray(hooks[e]))) {
613
+ return { ...result, invalidJson: true };
614
+ }
615
+ const append = (event, marker, group) => {
616
+ const groups = hooks[event];
617
+ if (hookArrayContains(groups, marker))
618
+ return false;
619
+ hooks[event] = [...(Array.isArray(groups) ? groups : []), group];
620
+ return true;
621
+ };
622
+ const installedUserPromptSubmit = append('UserPromptSubmit', HIPPO_PINNED_INJECT_MARKER, {
623
+ hooks: [codexCommandHook(HIPPO_PINNED_INJECT_COMMAND, 5)],
624
+ });
625
+ const installedCompactResume = append('SessionStart', HIPPO_COMPACT_RESUME_MARKER, {
626
+ matcher: 'compact',
627
+ hooks: [codexCommandHook(HIPPO_COMPACT_RESUME_MARKER, 10)],
628
+ });
629
+ if (installedUserPromptSubmit || installedCompactResume) {
630
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
631
+ }
632
+ return { ...result, installedUserPromptSubmit, installedCompactResume };
633
+ }
557
634
  export function installJsonHooks(target) {
558
635
  const { settings: settingsPath, logFile } = resolveJsonHookPaths(target);
559
636
  const dir = path.dirname(settingsPath);
@@ -565,23 +642,11 @@ export function installJsonHooks(target) {
565
642
  settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
566
643
  }
567
644
  catch {
568
- return {
569
- target,
570
- settingsPath,
571
- installedSessionEnd: false,
572
- installedSessionStart: false,
573
- installedUserPromptSubmit: false,
574
- installedPreCompact: false,
575
- installedCompactResume: false,
576
- installedPostCompact: false,
577
- installedCaptureError: false,
578
- migratedPinnedInjectRecent: false,
579
- migratedFromStop: false,
580
- migratedLegacySessionEnd: false,
581
- migratedSplitSessionEnd: false,
582
- };
645
+ return { ...nothingInstalled(target, settingsPath), invalidJson: true };
583
646
  }
584
647
  }
648
+ if (target === 'codex')
649
+ return installCodexHooks(settingsPath, settings);
585
650
  if (!settings.hooks)
586
651
  settings.hooks = {};
587
652
  // SAFETY: settings.hooks is either freshly initialised to {} on the line above, or an
@@ -771,8 +836,41 @@ export function installJsonHooks(target) {
771
836
  migratedFromStop,
772
837
  migratedLegacySessionEnd,
773
838
  migratedSplitSessionEnd,
839
+ invalidJson: false,
774
840
  };
775
841
  }
842
+ /** The exact command hippo writes for each Codex event; uninstall removes only these handlers. */
843
+ const CODEX_HOOK_COMMANDS = [
844
+ ['UserPromptSubmit', HIPPO_PINNED_INJECT_COMMAND],
845
+ ['SessionStart', HIPPO_COMPACT_RESUME_MARKER],
846
+ ];
847
+ /** A group loses only hippo's handlers and goes only once empty, so a user's hook beside or like hippo's stays. */
848
+ function uninstallCodexHooks(hooks) {
849
+ let changed = false;
850
+ for (const [event, command] of CODEX_HOOK_COMMANDS) {
851
+ const groups = hooks[event];
852
+ if (!Array.isArray(groups))
853
+ continue;
854
+ let removed = false;
855
+ const kept = groups.flatMap((group) => {
856
+ if (!isJsonObject(group) || !Array.isArray(group.hooks))
857
+ return [group];
858
+ const handlers = group.hooks.filter((h) => !(isJsonObject(h) && h.command === command));
859
+ if (handlers.length === group.hooks.length)
860
+ return [group];
861
+ removed = true;
862
+ return handlers.length > 0 ? [{ ...group, hooks: handlers }] : [];
863
+ });
864
+ if (!removed)
865
+ continue;
866
+ changed = true;
867
+ if (kept.length > 0)
868
+ hooks[event] = kept;
869
+ else
870
+ delete hooks[event];
871
+ }
872
+ return changed;
873
+ }
776
874
  export function uninstallJsonHooks(target) {
777
875
  const { settings: settingsPath } = resolveJsonHookPaths(target);
778
876
  if (!fs.existsSync(settingsPath))
@@ -784,11 +882,19 @@ export function uninstallJsonHooks(target) {
784
882
  catch {
785
883
  return false;
786
884
  }
787
- // SAFETY: Claude Code's settings.json always stores `hooks` as an object when
788
- // present; each event key below is still re-validated with Array.isArray before use.
789
- const hooks = settings.hooks;
790
- if (!hooks)
885
+ if (!isJsonObject(settings) || !isJsonObject(settings.hooks))
886
+ return false;
887
+ const changed = target === 'codex' ? uninstallCodexHooks(settings.hooks) : uninstallClaudeCodeHooks(settings.hooks);
888
+ if (!changed)
791
889
  return false;
890
+ if (Object.keys(settings.hooks).length === 0)
891
+ delete settings.hooks;
892
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
893
+ return true;
894
+ }
895
+ function uninstallClaudeCodeHooks(settingsHooks) {
896
+ // SAFETY: each event key below is re-validated with Array.isArray before use.
897
+ const hooks = settingsHooks;
792
898
  let changed = false;
793
899
  const markersByKey = {
794
900
  SessionEnd: [HIPPO_SESSION_END_MARKER, HIPPO_SLEEP_MARKER, HIPPO_CAPTURE_MARKER],
@@ -813,12 +919,7 @@ export function uninstallJsonHooks(target) {
813
919
  delete hooks[key];
814
920
  }
815
921
  }
816
- if (!changed)
817
- return false;
818
- if (Object.keys(hooks).length === 0)
819
- delete settings.hooks;
820
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
821
- return true;
922
+ return changed;
822
923
  }
823
924
  export function resolveOpencodePluginPath() {
824
925
  return path.join(homeDir(), '.config', 'opencode', 'plugins', 'hippo.ts');
@@ -965,7 +1066,7 @@ export function detectInstalledTools() {
965
1066
  { name: 'claude-code', configDir: '~/.claude', detected: exists('.claude'), kind: 'json-hook' },
966
1067
  { name: 'opencode', configDir: '~/.config/opencode', detected: exists('.config', 'opencode'), kind: 'plugin', notes: 'installs a TS plugin at ~/.config/opencode/plugins/hippo.ts' },
967
1068
  { name: 'openclaw', configDir: '~/.openclaw', detected: exists('.openclaw'), kind: 'plugin', notes: 'install via `openclaw plugins install hippo-memory`' },
968
- { name: 'codex', configDir: '~/.codex', detected: exists('.codex'), kind: 'wrapper', notes: 'wraps the detected codex launcher for session-end consolidation' },
1069
+ { 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' },
969
1070
  { name: 'cursor', configDir: '~/.cursor', detected: exists('.cursor'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
970
1071
  { name: 'pi', configDir: '~/.pi', detected: exists('.pi'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
971
1072
  ];
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