nlm-memory 0.5.22 → 0.7.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 (117) hide show
  1. package/README.md +106 -14
  2. package/dist/cli/classify-parity.js +1 -1
  3. package/dist/cli/classify-parity.js.map +1 -1
  4. package/dist/cli/nlm.js +152 -12
  5. package/dist/cli/nlm.js.map +1 -1
  6. package/dist/core/adapters/pi.js +13 -3
  7. package/dist/core/adapters/pi.js.map +1 -1
  8. package/dist/core/classifier/prompt.d.ts +1 -1
  9. package/dist/core/classifier/prompt.js +1 -1
  10. package/dist/core/hook/pointer-block.d.ts +12 -1
  11. package/dist/core/hook/pointer-block.js +25 -8
  12. package/dist/core/hook/pointer-block.js.map +1 -1
  13. package/dist/core/providers/provider-registry.js +1 -1
  14. package/dist/core/providers/provider-registry.js.map +1 -1
  15. package/dist/core/recall/recall-service.d.ts +9 -0
  16. package/dist/core/recall/recall-service.js +81 -18
  17. package/dist/core/recall/recall-service.js.map +1 -1
  18. package/dist/core/recall-facts/fact-recall-service.d.ts +11 -0
  19. package/dist/core/recall-facts/fact-recall-service.js +69 -4
  20. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  21. package/dist/core/scheduler/scheduler.d.ts +7 -0
  22. package/dist/core/scheduler/scheduler.js +26 -0
  23. package/dist/core/scheduler/scheduler.js.map +1 -1
  24. package/dist/core/signals/aggregate.d.ts +21 -0
  25. package/dist/core/signals/aggregate.js +34 -0
  26. package/dist/core/signals/aggregate.js.map +1 -0
  27. package/dist/core/signals/failure-mode-recall.d.ts +18 -0
  28. package/dist/core/signals/failure-mode-recall.js +28 -0
  29. package/dist/core/signals/failure-mode-recall.js.map +1 -0
  30. package/dist/core/signals/ingest-signal.d.ts +19 -0
  31. package/dist/core/signals/ingest-signal.js +63 -0
  32. package/dist/core/signals/ingest-signal.js.map +1 -0
  33. package/dist/core/signals/install-scope.d.ts +8 -0
  34. package/dist/core/signals/install-scope.js +41 -0
  35. package/dist/core/signals/install-scope.js.map +1 -0
  36. package/dist/core/signals/recommend.d.ts +13 -0
  37. package/dist/core/signals/recommend.js +24 -0
  38. package/dist/core/signals/recommend.js.map +1 -0
  39. package/dist/core/storage/pg-fact-store.d.ts +5 -0
  40. package/dist/core/storage/pg-fact-store.js +29 -0
  41. package/dist/core/storage/pg-fact-store.js.map +1 -1
  42. package/dist/core/storage/pg-signal-store.d.ts +16 -0
  43. package/dist/core/storage/pg-signal-store.js +100 -0
  44. package/dist/core/storage/pg-signal-store.js.map +1 -0
  45. package/dist/core/storage/pg-storage.d.ts +2 -0
  46. package/dist/core/storage/pg-storage.js +3 -0
  47. package/dist/core/storage/pg-storage.js.map +1 -1
  48. package/dist/core/storage/pg-tx-context.d.ts +5 -0
  49. package/dist/core/storage/pg-tx-context.js +3 -0
  50. package/dist/core/storage/pg-tx-context.js.map +1 -1
  51. package/dist/core/storage/sqlite-fact-store.d.ts +5 -0
  52. package/dist/core/storage/sqlite-fact-store.js +30 -0
  53. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  54. package/dist/core/storage/sqlite-signal-store.d.ts +20 -0
  55. package/dist/core/storage/sqlite-signal-store.js +112 -0
  56. package/dist/core/storage/sqlite-signal-store.js.map +1 -0
  57. package/dist/core/storage/sqlite-storage.d.ts +2 -0
  58. package/dist/core/storage/sqlite-storage.js +6 -2
  59. package/dist/core/storage/sqlite-storage.js.map +1 -1
  60. package/dist/hook/prompt-recall-hook.d.ts +10 -1
  61. package/dist/hook/prompt-recall-hook.js +10 -4
  62. package/dist/hook/prompt-recall-hook.js.map +1 -1
  63. package/dist/hook/recall-over-http.d.ts +11 -1
  64. package/dist/hook/recall-over-http.js +16 -4
  65. package/dist/hook/recall-over-http.js.map +1 -1
  66. package/dist/hook/session-start-hook.d.ts +2 -0
  67. package/dist/hook/session-start-hook.js +32 -2
  68. package/dist/hook/session-start-hook.js.map +1 -1
  69. package/dist/hook/stop-hook.js +16 -0
  70. package/dist/hook/stop-hook.js.map +1 -1
  71. package/dist/http/app.d.ts +9 -0
  72. package/dist/http/app.js +124 -11
  73. package/dist/http/app.js.map +1 -1
  74. package/dist/install/codex.d.ts +1 -1
  75. package/dist/install/codex.js +4 -4
  76. package/dist/install/codex.js.map +1 -1
  77. package/dist/install/ollama.d.ts +10 -0
  78. package/dist/install/ollama.js +19 -3
  79. package/dist/install/ollama.js.map +1 -1
  80. package/dist/install/pi.d.ts +1 -1
  81. package/dist/install/pi.js +1 -1
  82. package/dist/install/setup.js +63 -10
  83. package/dist/install/setup.js.map +1 -1
  84. package/dist/llm/classifier-box.d.ts +2 -1
  85. package/dist/llm/classifier-box.js +3 -0
  86. package/dist/llm/classifier-box.js.map +1 -1
  87. package/dist/llm/deepseek-client.d.ts +5 -2
  88. package/dist/llm/deepseek-client.js +51 -1
  89. package/dist/llm/deepseek-client.js.map +1 -1
  90. package/dist/llm/ollama-client.d.ts +3 -2
  91. package/dist/llm/ollama-client.js +45 -2
  92. package/dist/llm/ollama-client.js.map +1 -1
  93. package/dist/mcp/server.d.ts +1 -0
  94. package/dist/mcp/server.js +22 -3
  95. package/dist/mcp/server.js.map +1 -1
  96. package/dist/ports/fact-store.d.ts +0 -0
  97. package/dist/ports/fact-store.js +0 -0
  98. package/dist/ports/fact-store.js.map +1 -1
  99. package/dist/ports/llm-client.d.ts +24 -0
  100. package/dist/ports/signal-store.d.ts +30 -0
  101. package/dist/ports/signal-store.js +9 -0
  102. package/dist/ports/signal-store.js.map +1 -0
  103. package/dist/ports/storage.d.ts +2 -0
  104. package/dist/ports/transcript-adapter.d.ts +6 -0
  105. package/dist/shared/types.d.ts +76 -0
  106. package/dist/ui/assets/index-BTj60bAH.js +11 -0
  107. package/dist/ui/assets/index-Baw2qIKc.css +1 -0
  108. package/dist/ui/index.html +2 -2
  109. package/nlm/README.md +1 -1
  110. package/nlm/index.js +40 -17
  111. package/package.json +19 -18
  112. package/plugin/.codex-plugin/plugin.json +2 -2
  113. package/plugin/README.md +3 -3
  114. package/plugin/scripts/prompt-recall-hook.mjs +40 -17
  115. package/plugin/scripts/stop-hook.mjs +80 -6
  116. package/dist/ui/assets/index-BR1BHwew.js +0 -69
  117. package/dist/ui/assets/index-CgHq6xeZ.css +0 -1
@@ -0,0 +1 @@
1
+ :root{--surface-0:#161616;--surface-1:#1d1d1d;--surface-2:#242424;--surface-float:#2c2c2c;--bg-1:var(--surface-float);--bg-2:var(--surface-2);--border-1:#262626;--border-2:#303030;--border-3:#3d3d3d;--border-4:#525252;--text-1:#e8e8e8;--text-2:#b0b0b0;--text-3:#686868;--text-4:#383838;--accent:#e8ff6e;--accent-dim:#e8ff6e1f;--accent-glow:#e8ff6e0f;--warn:#f93;--warn-dim:#ff99331f;--danger:#ff6b35;--danger-dim:#ff6b3512;--danger-glow:#ff6b351f;--font-mono:"IBM Plex Mono", "Fira Code", ui-monospace, monospace;--text-xs:10px;--text-sm:11px;--text-base:13px;--text-md:15px;--text-lg:18px;--text-xl:22px;--text-stat:36px;--lh-tight:1.3;--lh-base:1.5;--lh-loose:1.7;--page-x:28px;--section-gap:24px;--r-sm:3px;--r-md:5px;--r-lg:7px;--ease-fast:.1s ease;--ease:.15s ease;--ease-slow:.3s ease;--z-base:0;--z-raised:1;--z-sticky:10;--z-sidenav:20;--z-dropdown:30;--z-overlay:40;--z-drawer-bg:50;--z-drawer:60;--z-palette-bg:70;--z-palette:80;--z-tooltip:90;--z-toast:100}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html{background-color:var(--surface-0)}body{color:var(--text-1);font-family:var(--font-mono);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-image:radial-gradient(100% 40% at 50% -8%,#e8ff6e08 0%,#0000 100%);background-attachment:fixed;min-height:100vh}::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:var(--surface-1)}::-webkit-scrollbar-thumb{background:var(--border-3);border-radius:2px}@keyframes fadeSlideIn{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-delay:0s!important}}.skeleton{background:linear-gradient(90deg, var(--surface-1) 0%, var(--surface-2) 50%, var(--surface-1) 100%);vertical-align:middle;background-size:200% 100%;animation:1.2s ease-in-out infinite skeleton-shimmer;display:inline-block}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (prefers-reduced-motion:reduce){.skeleton{background:var(--surface-2);animation:none}}.card{background:var(--surface-1);border:1px solid var(--border-2);border-radius:var(--r-md)}.card-lift{transition:border-color var(--ease), background var(--ease)}.card-lift:hover{background:var(--surface-2);border-color:var(--border-3)}.btn{font-family:var(--font-mono);font-size:var(--text-sm);letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r-sm);border:1px solid var(--border-3);color:var(--text-2);cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;transition:border-color var(--ease), color var(--ease), background var(--ease);background:0 0;align-items:center;gap:5px;padding:5px 12px;text-decoration:none;display:inline-flex}.btn:hover{border-color:var(--border-4);color:var(--text-1)}.btn:active{background:var(--surface-2)}.btn.active{background:var(--surface-2);border-color:var(--border-3);color:var(--text-1)}.btn-primary{background:var(--accent);border-color:var(--accent);color:#080808;font-weight:600}.btn-primary:hover{color:#080808;background:#d4eb5e;border-color:#d4eb5e}.btn-accent{color:var(--accent);border-color:#e8ff6e40}.btn-accent:hover{background:var(--accent-glow);border-color:#e8ff6e66}.btn-danger:hover{border-color:var(--danger);color:var(--danger)}.btn.small{font-size:var(--text-xs);padding:3px 9px}.form-row{flex-wrap:wrap;align-items:center;gap:12px;margin:12px 0;display:flex}.form-row.between{justify-content:space-between}.form-row.tight{gap:8px}.form-row .form-label{color:var(--text-3);font-size:var(--text-xs)}input[type=file].file-input{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-3);max-width:24rem}input[type=file].file-input::file-selector-button{font-family:var(--font-mono);font-size:var(--text-sm);letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r-sm);border:1px solid var(--border-3);color:var(--text-2);cursor:pointer;transition:border-color var(--ease), color var(--ease), background var(--ease);background:0 0;align-items:center;margin-right:10px;padding:5px 12px;display:inline-flex}input[type=file].file-input::file-selector-button:hover{border-color:var(--border-4);color:var(--text-1)}input[type=file].file-input:disabled{cursor:not-allowed}input[type=file].file-input:disabled::file-selector-button{opacity:.5;cursor:not-allowed}.chip{font-family:var(--font-mono);font-size:var(--text-sm);letter-spacing:.06em;text-transform:lowercase;border-radius:var(--r-sm);border:1px solid var(--border-1);color:var(--text-3);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;transition:border-color var(--ease), color var(--ease), background var(--ease);background:0 0;align-items:center;gap:4px;padding:3px 9px;display:inline-flex}.chip:hover{border-color:var(--border-3);color:var(--text-2)}.chip.active{border-color:var(--border-3);color:var(--text-1);background:var(--surface-2);box-shadow:inset 0 0 0 1px var(--border-3);font-weight:600}.chip[data-severity=critical].active{color:var(--danger);background:var(--danger-dim);border-color:#ff6b3566}.chip[data-severity=high].active{color:#c42;background:#cc44220f;border-color:#cc442259}.chip[data-severity=medium].active{color:var(--warn);background:var(--warn-dim);border-color:#ff993359}.chip[data-severity=low].active{border-color:var(--border-3);color:var(--text-2);background:var(--surface-2)}.chip[data-status=active].active{color:var(--accent);background:var(--accent-glow);border-color:#e8ff6e4d}.chip[data-status=idle].active{color:#ffcc80}.chip[data-length].active{color:var(--text-1)}.chip[data-marker].active{color:var(--accent);background:var(--accent-glow);border-color:#e8ff6e4d}.panel-float{-webkit-backdrop-filter:blur(18px);border:1px solid var(--border-3);border-radius:var(--r-md);background:#101010f5;box-shadow:0 12px 40px #000000a6,0 0 0 1px #ffffff08}.nav-link{color:var(--text-3);border-radius:var(--r-sm);font-size:var(--text-sm);font-family:var(--font-mono);letter-spacing:.06em;text-transform:uppercase;transition:color var(--ease), border-color var(--ease), background var(--ease);background:0 0;border:1px solid #0000;align-items:center;padding:4px 10px;text-decoration:none;display:inline-flex}.nav-link:hover{color:var(--text-2);border-color:var(--border-3)}.nav-link.active{background:var(--surface-2);border-color:var(--border-3);color:var(--text-1)}.search-input{background:var(--surface-1);border:1px solid var(--border-2);color:var(--text-1);font-family:var(--font-mono);font-size:var(--text-xs);border-radius:var(--r-sm);letter-spacing:.04em;transition:border-color var(--ease);outline:none;padding:5px 10px}.search-input:focus{border-color:var(--border-3)}.search-input::placeholder{color:var(--text-4);letter-spacing:.06em}.ctrl-btn{background:var(--surface-1);border:1px solid var(--border-2);color:var(--text-2);font-family:var(--font-mono);font-size:var(--text-sm);letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r-sm);cursor:pointer;white-space:nowrap;transition:border-color var(--ease), color var(--ease), background var(--ease);padding:4px 10px}.ctrl-btn:hover{border-color:var(--border-3);color:var(--text-1)}.ctrl-btn.active{background:var(--surface-2);border-color:var(--border-3);color:var(--accent)}.ctrl-btn[data-active="1"]{color:var(--accent);border-color:#e8ff6e40}.ctrl-label{font-size:var(--text-sm);color:var(--text-3);letter-spacing:.06em;text-transform:uppercase;flex-shrink:0}.subnav{border-bottom:1px solid var(--border-1);gap:0;padding:0 28px;display:flex}.subnav-link{font-size:var(--text-sm);font-family:var(--font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--text-3);transition:color var(--ease), border-color var(--ease);border-bottom:2px solid #0000;margin-bottom:-1px;padding:10px 14px;text-decoration:none}.subnav-link:hover{color:var(--text-2)}.subnav-link.active{color:var(--accent);border-bottom-color:var(--accent)}.page-shell{background:var(--surface-0);height:100vh;display:flex;overflow:hidden}.page-main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.app-header{z-index:var(--z-sticky);height:52px;padding:0 var(--page-x);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--border-1);background:#161616e6;align-items:center;gap:20px;display:flex;position:sticky;top:0}.app-header .wordmark-page{font-size:var(--text-sm);color:var(--text-3);letter-spacing:.04em;text-transform:lowercase}.app-header .header-spacer{flex:1}.app-header .status-dot{background:var(--accent);width:7px;height:7px;box-shadow:0 0 6px var(--accent-dim);border-radius:50%;animation:2s ease-in-out infinite pulse-dot}@keyframes pulse-dot{0%,to{opacity:1}50%{opacity:.4}}.app-header .header-info{font-size:var(--text-xs);color:var(--text-3);letter-spacing:.04em}.sidenav{background:var(--surface-1);border-right:1px solid var(--border-1);width:180px;height:100vh;transition:width var(--ease);z-index:var(--z-sidenav);flex-direction:column;flex-shrink:0;display:flex;position:sticky;top:0;overflow:hidden}.sidenav.collapsed{width:52px}.sidenav-header{border-bottom:1px solid var(--border-1);flex-shrink:0;justify-content:space-between;align-items:center;height:52px;padding:0 8px 0 16px;display:flex}.sidenav-wordmark{font-size:var(--text-sm);font-family:var(--font-mono);letter-spacing:.2em;text-transform:uppercase;color:var(--accent);-webkit-user-select:none;user-select:none;white-space:nowrap;transition:opacity var(--ease-fast), width var(--ease-fast);overflow:hidden}.sidenav.collapsed .sidenav-wordmark{opacity:0;width:0;padding:0}.sidenav-toggle{color:var(--text-3);cursor:pointer;border-radius:var(--r-sm);transition:color var(--ease), background var(--ease);background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:5px;display:flex}.sidenav-toggle:hover{color:var(--text-1);background:var(--surface-2)}.sidenav.collapsed .sidenav-header{justify-content:center;padding:0 8px}.sidenav-items{flex-direction:column;flex:1;gap:2px;padding:10px 8px;display:flex;overflow:hidden}.sidenav-item{border-radius:var(--r-sm);font-size:var(--text-sm);font-family:var(--font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--text-3);transition:color var(--ease), background var(--ease), border-color var(--ease);-webkit-user-select:none;user-select:none;white-space:nowrap;border-left:3px solid #0000;align-items:center;gap:9px;padding:8px 10px;text-decoration:none;display:flex;position:relative;overflow:hidden}.sidenav-item:hover{color:var(--text-1);background:var(--surface-2)}.sidenav-item.active{color:var(--text-1);border-left-color:var(--accent);background:var(--accent-glow)}.item-icon{opacity:.7;flex-shrink:0;align-items:center;display:flex}.sidenav-item.active .item-icon{opacity:1}.item-label{transition:opacity var(--ease-fast)}.sidenav.collapsed .item-label{display:none}.sidenav.collapsed .sidenav-item{border-left:3px solid #0000;justify-content:center;gap:0;padding:8px}.sidenav.collapsed .sidenav-item.active{border-left-color:var(--accent)}.sidenav.collapsed .sidenav-item:after{content:attr(data-label);background:var(--surface-float);color:var(--text-1);font-size:var(--text-xs);font-family:var(--font-mono);letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r-sm);border:1px solid var(--border-2);white-space:nowrap;pointer-events:none;opacity:0;transition:opacity var(--ease-fast);z-index:var(--z-tooltip);padding:4px 8px;position:absolute;top:50%;left:calc(100% + 8px);transform:translateY(-50%)}.sidenav.collapsed .sidenav-item:hover:after{opacity:1}.sidenav-footer{border-top:1px solid var(--border-1);flex-shrink:0;padding:8px}.sidenav-data{font-size:var(--text-xs)}.update-banner{background:var(--bg-2);border:1px solid var(--border-1);font-family:var(--font-sans);border-radius:6px;margin-bottom:8px;padding:8px 10px}.update-banner-head{justify-content:space-between;align-items:center;gap:8px;display:flex}.update-banner-version{color:var(--text-1)}.update-banner-dismiss{color:var(--text-3);cursor:pointer;background:0 0;border:none;padding:0 4px;font-size:16px;line-height:1}.update-banner-dismiss:hover{color:var(--text-1)}.update-banner-body{color:var(--text-2,var(--text-1));margin:4px 0 6px}.update-banner-cmd{background:var(--bg-1);border:1px solid var(--border-1);color:var(--text-1);white-space:nowrap;border-radius:4px;margin-bottom:6px;padding:4px 6px;overflow-x:auto}.update-banner-actions{justify-content:space-between;align-items:center;gap:8px;display:flex}.update-banner-copy{background:var(--accent);color:var(--bg-1);cursor:pointer;font-family:var(--font-mono);font-size:var(--text-xs);border:none;border-radius:4px;padding:4px 8px}.update-banner-link{color:var(--text-3);text-underline-offset:2px;text-decoration:underline}.update-banner-link:hover{color:var(--text-1)}.update-banner-nudge{background:var(--bg-1);border-left:2px solid var(--accent);color:var(--text-2,var(--text-1));border-radius:3px;margin:8px 0 0;padding:6px 8px}.update-banner-nudge code{background:var(--bg-0,var(--bg-1));border-radius:3px;padding:1px 4px}.update-banner--collapsed{cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;width:100%;margin-bottom:8px;padding:6px 0;display:flex}.update-dot{color:var(--accent);font-size:14px;line-height:1;animation:2.6s ease-in-out infinite nlm-update-pulse}@keyframes nlm-update-pulse{0%,to{opacity:.45}50%{opacity:1}}.live-page{flex-direction:column;flex:1;min-height:0;display:flex}.live-status{padding:12px var(--page-x) 4px;align-items:center;gap:8px;display:flex}.live-status-dot{border-radius:50%;flex-shrink:0;width:7px;height:7px}.live-status-live{background:var(--accent);box-shadow:0 0 6px var(--accent)}.live-status-connecting{background:var(--text-3)}.live-status-reconnecting{background:var(--warn);animation:1.2s ease-in-out infinite pulse-dot}.live-status-label{font-family:var(--font-mono);font-size:var(--text-sm);letter-spacing:.06em;text-transform:uppercase;color:var(--text-2)}.live-board{padding:4px var(--page-x) 20px;flex:1;grid-template-columns:repeat(3,1fr);gap:16px;min-height:0;display:grid}.live-col{background:var(--surface-1);border:1px solid var(--border-2);border-radius:var(--r-md);flex-direction:column;min-height:0;display:flex;overflow:hidden}.live-col-head{border-bottom:1px solid var(--border-1);justify-content:space-between;align-items:baseline;padding:10px 14px;display:flex}.live-col-title{font-size:var(--text-sm);font-family:var(--font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--text-2)}.live-col-count{font-size:var(--text-xs);color:var(--text-3);font-family:var(--font-mono)}.live-col-body{flex:1;overflow:hidden auto}.live-row{border-bottom:1px solid var(--border-1);overflow-wrap:anywhere;min-width:0;padding:8px 14px;animation:.2s ease-out fadeSlideIn}.live-row .label,.live-row .body,.live-row .meta{overflow-wrap:anywhere;word-break:break-word;min-width:0}.live-row:last-child{border-bottom:none}.clickable:focus-visible,.bar-item-clickable:focus-visible,.kpi-clickable:focus-visible{outline:1px solid var(--accent);outline-offset:-1px}.clickable[aria-busy=true],.clickable.is-disabled,.bar-item-clickable[aria-busy=true],.bar-item-clickable.is-disabled{opacity:.5;pointer-events:none}.live-row.clickable{cursor:pointer;transition:background var(--ease)}.live-row.clickable:hover{background:var(--surface-2)}.live-row.is-new{animation:.2s ease-out fadeSlideIn,1.2s ease-out liveFlash}.live-row.is-related{background:var(--accent-glow);border-left:2px solid var(--accent);padding-left:12px}@keyframes liveFlash{0%{background:var(--accent-glow)}to{background:0 0}}.live-row .label{color:var(--text-1);font-size:var(--text-base);line-height:var(--lh-tight)}.live-row .body{color:var(--text-3);font-size:var(--text-sm);line-height:var(--lh-tight);margin-top:3px}.live-row .meta{color:var(--text-3);font-size:var(--text-xs);letter-spacing:.04em;margin-top:4px}.live-tag{font-size:var(--text-xs);font-family:var(--font-mono);letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r-sm);background:var(--surface-2);color:var(--text-2);border:1px solid var(--border-2);margin-right:6px;padding:1px 6px;display:inline-block}.live-tag[data-kind=decision]{background:var(--accent-glow);color:var(--accent);border-color:#e8ff6e40}.live-tag[data-kind=open]{background:var(--warn-dim);color:var(--warn);border-color:#ff993340}.live-empty{text-align:center;color:var(--text-3);font-size:var(--text-xs);letter-spacing:.06em;text-transform:uppercase;padding:32px 16px}.stub-shell{padding:80px var(--page-x);text-align:center;color:var(--text-3);font-size:var(--text-base);line-height:var(--lh-loose)}.stub-shell .stub-name{color:var(--accent);font-size:var(--text-md);text-transform:lowercase;letter-spacing:.04em;margin-bottom:12px;display:block}.stub-shell .stub-hint{color:var(--text-4);font-size:var(--text-sm)}.page-pad{padding:20px var(--page-x) 60px;flex:1;min-width:0;min-height:0;overflow-y:auto}.page-title{font-size:var(--text-lg);font-family:var(--font-mono);color:var(--text-1);margin-bottom:16px;font-weight:500}.page-header{align-items:center;gap:24px;margin-bottom:24px;padding-top:8px;display:flex}.subnav+.page-header,.subnav+.page-title{gap:28px;margin-top:28px;margin-bottom:28px;padding-top:0}.page-header .page-title,.form-row.between .page-title{margin-bottom:0}.page-header .header-spacer,.thread-header .header-spacer,.river-toolbar .header-spacer{flex:1}.section-title{font-size:var(--text-sm);text-transform:uppercase;letter-spacing:.06em;color:var(--text-3);margin:24px 0 8px;font-weight:500}.muted{color:var(--text-3)}.muted.small{font-size:var(--text-xs);letter-spacing:.04em}.muted.error{color:var(--danger)}.right{text-align:right}.mono{font-family:var(--font-mono)}.small{font-size:var(--text-xs)}code,.code-block{font-family:var(--font-mono);font-size:var(--text-sm);background:var(--surface-2);border:1px solid var(--border-2);border-radius:var(--r-sm);color:var(--text-1);padding:2px 6px}.code-block{margin:8px 0;padding:10px 14px;display:block}.settings-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:16px;display:grid}.settings-card{color:var(--text-1);flex-direction:column;gap:6px;padding:14px;text-decoration:none;display:flex}.settings-card-title{font-size:var(--text-sm);letter-spacing:.06em;text-transform:uppercase;color:var(--accent);font-weight:500}.settings-card-body{color:var(--text-2);font-size:var(--text-sm)}.kv-list{grid-template-columns:max-content 1fr;gap:6px 16px;margin-top:12px;display:grid}.kv-label{color:var(--text-3);font-size:var(--text-sm);letter-spacing:.04em}.kv-value{color:var(--text-1);font-size:var(--text-sm)}.kv-value.mono{font-family:var(--font-mono);word-break:break-all}.form-grid{gap:12px;max-width:420px;margin-top:12px;display:grid}.form-field{flex-direction:column;gap:4px;display:flex}.form-label{font-size:var(--text-xs);color:var(--text-3);letter-spacing:.06em;text-transform:uppercase}.form-input{background:var(--surface-1);border:1px solid var(--border-2);color:var(--text-1);font-family:var(--font-mono);font-size:var(--text-sm);border-radius:var(--r-sm);transition:border-color var(--ease);padding:6px 10px}.form-input:hover:not(:disabled){border-color:var(--border-3)}.form-input:focus{border-color:var(--border-4);outline:none}.form-input:focus-visible{outline:1px solid var(--accent);outline-offset:-1px}.form-input:disabled,.form-input.is-disabled{opacity:.5;cursor:not-allowed}.form-input[aria-invalid=true],.form-input.is-invalid{border-color:var(--danger)}.form-input[aria-invalid=true]:focus-visible,.form-input.is-invalid:focus-visible{outline-color:var(--danger);border-color:var(--danger)}.form-error{color:var(--danger);font-size:var(--text-xs);margin:4px 0 0}.data-table{border-collapse:collapse;width:100%;font-size:var(--text-sm);margin-top:12px}.data-table th{text-align:left;font-weight:500;font-size:var(--text-xs);letter-spacing:.06em;text-transform:uppercase;color:var(--text-3);border-bottom:1px solid var(--border-2);padding:8px 10px}.data-table th.right{text-align:right}.data-table td{border-bottom:1px solid var(--border-1);color:var(--text-1);vertical-align:middle;padding:7px 10px}.data-table tr:hover td{background:var(--surface-2)}.canonical .dot{vertical-align:middle;margin-right:8px}.topic-name{align-items:baseline;gap:8px;display:inline-flex}.topic-name .muted.small.mono{font-size:var(--text-xs);opacity:.7}.dot{border-radius:50%;flex-shrink:0;width:8px;height:8px;display:inline-block}.dot.lg{width:14px;height:14px}.chip-inline{border-radius:var(--r-sm);border:1px solid var(--border-2);background:var(--surface-2);color:var(--text-2);font-family:var(--font-mono);font-size:var(--text-xs);letter-spacing:.04em;text-transform:lowercase;align-items:center;gap:4px;padding:2px 8px;display:inline-flex}.chip-inline.status-active{color:var(--accent);background:var(--accent-glow);border-color:#e8ff6e40}.chip-inline.status-idle{color:#ffcc80;border-color:#ffcc8040}.chip-inline.status-closed{color:var(--text-2)}.chip-inline.status-superseded{color:var(--text-3);text-decoration:line-through}.chip-inline.status-retired{color:var(--danger);background:var(--danger-dim);border-color:#ff6b3540}.chip-inline.status-stale{color:var(--warn);background:var(--warn-dim);border-color:#ff993340}.chip-inline.severity-high{color:var(--danger);background:var(--danger-dim);border-color:#ff6b3559}.chip-inline.severity-medium{color:var(--warn);background:var(--warn-dim);border-color:#ff993359}.chip-inline.severity-low{color:var(--text-2);border-color:var(--border-3);background:var(--surface-2)}.chip-x{color:inherit;cursor:pointer;background:0 0;border:none;padding:0 0 0 4px;font-size:12px;line-height:1}.kpi-row{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:18px;display:grid}.kpi{background:var(--surface-1);border:1px solid var(--border-2);border-radius:var(--r-md);flex-direction:column;gap:4px;padding:14px 16px;display:flex}.kpi-label{font-size:var(--text-xs);letter-spacing:.06em;text-transform:uppercase;color:var(--text-3)}.kpi-value{font-size:var(--text-stat);font-family:var(--font-mono);color:var(--text-1);line-height:1}.kpi-hint{font-size:var(--text-xs);color:var(--text-3)}.kpi-sparkline{gap:8px}.sparkline{align-items:flex-end;gap:4px;height:40px;display:flex}.spark-bar{background:var(--accent);opacity:.7;border-radius:1px;flex:1;min-height:2px}.pulse-grid{grid-template:"coherence recent stale"1fr"runtimes recent stale"1fr/1fr 1fr 1fr;gap:12px;height:calc(100vh - 280px);min-height:360px;margin-top:6px;display:grid}.pulse-area-coherence{grid-area:coherence}.pulse-area-runtimes{grid-area:runtimes}.pulse-area-recent{grid-area:recent}.pulse-area-stale{grid-area:stale}.pulse-scroll-card{flex-direction:column;min-height:0;display:flex;overflow:hidden}.pulse-scroll-card .card-head{flex-shrink:0}.kpi,.pulse-grid>.card{transition:outline-color var(--ease), border-color var(--ease);outline-offset:-1px;outline:1px solid #0000}.kpi:hover{outline-color:#ffffff8c}.kpi-clickable{cursor:pointer;color:inherit;text-decoration:none}.kpi-clickable:hover .kpi-value{color:var(--accent)}.kpi-clickable:hover{border-color:var(--border-3)}.pulse-grid>.pulse-scroll-card:hover{outline-color:#ffffff8c}.pulse-scroll-body{flex:1;min-height:0;overflow:hidden auto}.card-head{border-bottom:1px solid var(--border-1);justify-content:space-between;align-items:baseline;padding:10px 14px;display:flex}.card-head h3{font-size:var(--text-sm);letter-spacing:.06em;text-transform:uppercase;color:var(--text-2);font-weight:500}.bar-stack{flex-direction:column;padding:4px 0;display:flex}.bar-item{border:0;border-bottom:1px solid var(--border-1);text-align:left;width:100%;color:inherit;font:inherit;background:0 0;grid-template-columns:80px 1fr auto;align-items:center;gap:10px;padding:8px 14px;display:grid}.bar-stack .bar-item:last-child{border-bottom:none}.bar-item-clickable{cursor:pointer;transition:background var(--ease)}.bar-item-clickable:hover{background:var(--surface-2)}.bar-item-clickable:hover .bar-label{color:var(--accent)}.bar-item[disabled]{cursor:default;opacity:.5}.bar-label{font-size:var(--text-sm);color:var(--text-2)}.bar-track{background:var(--surface-2);border-radius:4px;height:8px;overflow:hidden}.bar-fill{height:100%;transition:width var(--ease-slow)}.bar-fill.tone-active{background:var(--accent)}.bar-fill.tone-warn{background:var(--warn)}.bar-fill.tone-danger{background:var(--danger)}.bar-value{font-size:var(--text-sm);color:var(--text-1);text-align:right;white-space:nowrap;min-width:96px}.bar-pct{margin-left:2px}.recall-head{justify-content:space-between;align-items:flex-start;gap:24px;margin-bottom:20px;display:flex}.recall-note{max-width:60ch;margin:0;line-height:1.5}.recall-block{margin-bottom:28px}.recall-block-head{margin-bottom:14px}.recall-block-head .page-title{margin-bottom:2px}.recall-block-head p{max-width:70ch;margin:0}.recall-empty{font-size:var(--text-sm);padding:18px}.recall-cards{grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;margin-top:14px;display:grid}.recall-bars .bar-item{grid-template-columns:140px 1fr auto}.recall-bar-label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.recall-bars .bar-value{min-width:0}@media (width<=720px){.recall-head{flex-direction:column;gap:12px}}.runtime-list{margin:0;padding:4px 0;list-style:none}.runtime-row{border-bottom:1px solid var(--border-1);grid-template-columns:12px 1fr auto auto;align-items:center;gap:10px;padding:8px 14px;display:grid}.runtime-row:last-child{border-bottom:none}.runtime-dot{background:var(--text-3);border-radius:50%;width:8px;height:8px;box-shadow:0 0 0 2px #ffffff0a}.runtime-dot.runtime-active{background:var(--accent);box-shadow:0 0 6px #e8ff6e99}.runtime-dot.runtime-idle{background:var(--warn)}.runtime-dot.runtime-dormant{background:var(--text-3);opacity:.5}.runtime-name{color:var(--text-1);font-size:var(--text-sm)}.runtime-counts{white-space:nowrap}.runtime-counts-sep{opacity:.5;margin:0 6px}.runtime-counts-prev{opacity:.7}.runtime-age{text-align:right;min-width:60px}.runtime-row.clickable{cursor:pointer;transition:background var(--ease)}.runtime-row.clickable:hover{background:var(--surface-2)}.runtime-row.clickable:hover .runtime-name{color:var(--accent)}.runtime-topics{flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:4px;display:flex}.alert-list{padding:4px 0;list-style:none}.alert-row{border-bottom:1px solid var(--border-1);grid-template-columns:auto auto 1fr;align-items:center;gap:10px;padding:7px 14px;display:grid}.alert-entity{color:var(--text-1);font-size:var(--text-sm);text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.alert-summary{-webkit-line-clamp:1;text-overflow:ellipsis;color:var(--text-3);font-size:var(--text-xs);letter-spacing:.04em;min-width:0;line-height:var(--lh-base);-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.alert-row.clickable{cursor:pointer;transition:background var(--ease)}.alert-row.clickable:hover{background:var(--surface-2)}.alert-row.clickable:hover .alert-entity{color:var(--accent)}.drawer-actions{flex-wrap:wrap;gap:6px;margin:14px 0 6px;display:flex}.session-list{list-style:none}.session-list.compact .session-row{border-bottom:1px solid var(--border-1);padding:6px 0}.session-row{border-bottom:1px solid var(--border-1);grid-template-columns:auto 1fr auto;align-items:center;gap:10px;padding:8px 14px;display:grid}.session-row-detail{grid-template-columns:auto 1fr auto}.session-row-main{flex-direction:column;gap:2px;min-width:0;display:flex}.session-label{color:var(--text-1);font-size:var(--text-sm);white-space:nowrap;text-overflow:ellipsis;text-decoration:none;overflow:hidden}a.session-label:hover{color:var(--accent)}.session-meta{color:var(--text-3);font-size:var(--text-xs);letter-spacing:.04em}@media (width<=1100px){.pulse-grid{grid-template:"coherence runtimes""recent recent""stale stale"/1fr 1fr;height:auto;min-height:0}.pulse-scroll-body{max-height:360px}}@media (width<=700px){.pulse-grid{grid-template-columns:1fr;grid-template-areas:"coherence""runtimes""recent""stale"}}.search-bar{align-items:center;gap:10px;margin-bottom:12px;display:flex}.search-input.search-big{font-size:var(--text-md);flex:1;padding:10px 14px}.search-meta{margin:8px 0}.thread-header{align-items:center;gap:14px;margin-bottom:16px;display:flex}.thread-header .page-title{margin-bottom:0}.thread-grid{grid-template-columns:1fr 1fr;gap:12px;margin-bottom:18px;display:grid}.marker-list{max-height:360px;list-style:none;overflow-y:auto}.marker-row{border-bottom:1px solid var(--border-1);grid-template-columns:auto auto 1fr auto;align-items:baseline;gap:10px;padding:7px 14px;display:grid}.marker-text{color:var(--text-1);font-size:var(--text-sm);min-width:0}.marker-timestamp{font-variant-numeric:tabular-nums;white-space:nowrap}.marker-action-select{min-width:130px;font-size:var(--text-xs)}.marker-editor{align-items:center;gap:6px;display:inline-flex}.marker-editor-input{min-width:260px}.entity-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;margin-top:16px;list-style:none;display:grid}.entity-card{color:var(--text-1);align-items:center;gap:8px;padding:10px 12px;text-decoration:none;display:flex}.entity-name{font-size:var(--text-sm);text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}@media (width<=800px){.thread-grid{grid-template-columns:1fr}}.river-toolbar{align-items:center;gap:12px;margin-bottom:16px;display:flex}.river-toolbar .page-title{font-size:var(--text-lg);margin-bottom:0}.river-grid{--river-label-w:200px;--river-row-gap:3px;--river-cell-gap:2px;--river-cell-h:20px;gap:var(--river-row-gap);flex-direction:column;width:100%;padding:12px;display:flex}.river-grid.river-density-compact{--river-row-gap:1px;--river-cell-gap:1px;--river-cell-h:10px}.river-grid.river-density-spacious{--river-row-gap:6px;--river-cell-gap:4px;--river-cell-h:24px}.river-row{grid-template-columns:var(--river-label-w) 1fr;align-items:center;gap:6px;width:100%;min-width:0;display:grid}.river-row-dates{margin-bottom:2px}.river-lane-label{border-radius:var(--r-sm);color:var(--text-2);font-size:var(--text-sm);white-space:nowrap;align-items:center;gap:6px;width:100%;min-width:0;padding:2px 6px;text-decoration:none;display:flex;overflow:hidden}.river-lane-label:hover{background:var(--surface-2);color:var(--text-1)}.river-lane-label--header{background:0 0;padding:0}.river-lane-name{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.river-cells{grid-template-columns:repeat(var(--cells,1), minmax(0, 1fr));gap:var(--river-cell-gap);width:100%;min-width:0;display:grid}.river-date-cell{font-size:var(--text-xs);color:var(--text-2);letter-spacing:.04em;text-align:center;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.river-cell{height:var(--river-cell-h);background:var(--surface-2);border-radius:2px;min-width:0}.river-cell.tier-1{background:#e8ff6e33}.river-cell.tier-2{background:#e8ff6e66}.river-cell.tier-3{background:#e8ff6ea6}.river-cell.tier-4{background:var(--accent)}.river-cell.has-corrections{box-shadow:inset 0 -2px #ffb400b3}.alert-row{grid-template-columns:auto minmax(0,max-content) minmax(0,1fr) auto}.alert-actions{align-items:center;gap:6px;display:flex}.alert-actions .chip{font-size:var(--text-xs);padding:2px 8px}.row-actions{gap:6px;display:flex}.row-actions .chip{font-size:var(--text-xs);padding:2px 8px}.row-busy{opacity:.5;pointer-events:none;transition:opacity var(--ease)}.form-input-inline{font-size:var(--text-xs);letter-spacing:.04em;padding:3px 8px}.river-grid{-webkit-user-select:none;user-select:none;position:relative}.river-drag-rect{background:var(--accent-glow);border-left:1px dashed var(--accent);border-right:1px dashed var(--accent);pointer-events:none;z-index:var(--z-raised);position:absolute;top:0;bottom:0}.river-lane-label{text-align:left;cursor:pointer;font-family:var(--font-mono);background:0 0;border:none}.river-cell{transition:outline-color var(--ease);outline-offset:1px;z-index:var(--z-base);outline:1px solid #0000;position:relative}.river-cell:hover{z-index:var(--z-raised);outline-color:#ffffffb3}.river-hover{background:var(--surface-float);border:1px solid var(--border-3);border-radius:var(--r-sm);font-size:var(--text-xs);pointer-events:none;z-index:var(--z-tooltip);align-items:center;gap:8px;padding:6px 10px;display:flex;position:fixed;box-shadow:0 6px 20px #00000080}.river-hover-name{color:var(--text-1);font-size:var(--text-sm)}.session-row.clickable{cursor:pointer;transition:background var(--ease)}.session-row.clickable:hover{background:var(--surface-2)}.session-row.clickable:hover .session-label{color:var(--accent)}.link-button{color:var(--text-3);font-family:var(--font-mono);font-size:var(--text-xs);cursor:pointer;letter-spacing:.04em;background:0 0;border:none;padding:0}.link-button:hover{color:var(--accent)}.drawer-backdrop{z-index:var(--z-drawer-bg);background:#0000008c;animation:.15s fadeIn;position:fixed;inset:0}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.session-drawer{background:var(--surface-1);border-left:1px solid var(--border-3);width:min(560px,100vw);z-index:var(--z-drawer);flex-direction:column;animation:.18s slideInRight;display:flex;position:fixed;top:0;bottom:0;right:0;box-shadow:-16px 0 40px #0000008c}.coherence-drawer{width:min(480px,100vw)}@keyframes slideInRight{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.drawer-head{border-bottom:1px solid var(--border-1);align-items:center;gap:10px;padding:14px 18px;display:flex}.drawer-title{font-size:var(--text-md);font-family:var(--font-mono);color:var(--text-1);text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-weight:500;overflow:hidden}.drawer-close{color:var(--text-3);cursor:pointer;background:0 0;border:none;padding:0 6px;font-size:24px;line-height:1}.drawer-close:hover{color:var(--text-1)}.drawer-body{flex:1;min-height:0;padding:16px 18px 80px;overflow-y:auto}.drawer-paragraph.drawer-hint{color:var(--text-2);margin:0 0 16px}.coherence-session-list .session-row{transition:background var(--ease);grid-template-columns:auto 1fr auto auto}.coherence-session-list .coherence-meta{white-space:nowrap}.coherence-session-list .session-row{cursor:pointer}.coherence-session-list .session-row:hover{background:var(--surface-2)}.coherence-session-list .session-row:hover .session-label{color:var(--accent)}.coherence-session-list .session-row[aria-busy=true]{opacity:.5;pointer-events:none}.coherence-session-list .chip[data-bucket=active].active{color:var(--accent);background:var(--accent-glow);border-color:#e8ff6e4d}.coherence-session-list .chip[data-bucket=sparse].active{color:var(--warn);background:var(--warn-dim);border-color:#ff993359}.coherence-session-list .chip[data-bucket=stale].active{color:var(--danger);background:var(--danger-dim);border-color:#ff6b3566}.coherence-drawer .drawer-body{flex-direction:column;padding-bottom:0;display:flex;overflow:hidden}.coherence-session-list{flex:1;min-height:0;overflow-y:auto}.coherence-drawer .pagination{border-top:1px solid var(--border-1);flex-shrink:0;margin-top:0}.coherence-drawer .pagination.pagination-compact{padding:10px 18px}.merge-picker{background:var(--surface-2);border:1px solid var(--border-2);border-radius:6px;margin-bottom:12px;padding:12px}.merge-result-list{max-height:240px;margin:8px 0 0;padding:0;list-style:none;overflow-y:auto}.merge-result-row{cursor:pointer;border-radius:4px;align-items:center;gap:8px;padding:6px 8px;display:flex}.merge-result-row:hover{background:var(--surface-float)}.merge-result-row.is-selected{outline:2px solid var(--accent);outline-offset:-2px;background:var(--surface-float)}.coherence-rows-dimmed{opacity:.4;pointer-events:none}.coherence-filters{flex-direction:column;gap:8px;margin-bottom:12px;display:flex}.coherence-search{width:100%}.coherence-filter-row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.drawer-section{font-size:var(--text-xs);letter-spacing:.06em;text-transform:uppercase;color:var(--text-3);margin:18px 0 8px;font-weight:500}.drawer-list{font-size:var(--text-sm);color:var(--text-1);flex-direction:column;gap:8px;list-style:none;display:flex}.drawer-list li{align-items:baseline;gap:8px;display:flex}.drawer-paragraph{font-size:var(--text-sm);color:var(--text-1);line-height:var(--lh-loose)}.drawer-body-text{background:var(--surface-0);border:1px solid var(--border-1);border-radius:var(--r-sm);font-size:var(--text-xs);color:var(--text-2);white-space:pre-wrap;word-break:break-word;max-height:360px;line-height:var(--lh-base);padding:10px 12px;overflow-y:auto}.entity-chips{flex-wrap:wrap;gap:6px;display:flex}.entity-chips-row{align-items:center;margin-top:4px}.supersedence-banner{border-radius:var(--r-sm);font-size:var(--text-sm);flex-wrap:wrap;align-items:baseline;gap:6px;margin-bottom:10px;padding:8px 12px;display:flex}.supersedence-banner--superseded{color:var(--danger);background:#ff6b3514;border:1px solid #ff6b3540}.supersedence-banner--supersedes{border:1px solid var(--border-2);color:var(--text-2);background:#a8a8a80f}.supersedence-label{white-space:nowrap;font-weight:500}.supersedence-ids{flex-wrap:wrap;gap:4px;display:flex}.supersedence-id{color:inherit;opacity:.8}.supersedence-link{cursor:pointer;font-family:var(--font-mono);font-size:var(--text-xs);color:inherit;text-underline-offset:2px;background:0 0;border:none;padding:0;text-decoration:underline}.supersedence-link:hover{opacity:.75}.drawer-menu{position:relative}.drawer-menu-trigger{color:var(--text-3);font-size:var(--text-lg);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:4px 8px;line-height:1}.drawer-menu-trigger:hover{color:var(--text-1);background:var(--bg-2)}.drawer-menu-trigger:disabled{opacity:.4;cursor:not-allowed}.drawer-menu-list{z-index:var(--z-dropdown);background:var(--bg-1);border:1px solid var(--border-1);border-radius:6px;min-width:200px;margin:0;padding:4px;list-style:none;position:absolute;top:calc(100% + 4px);right:0;box-shadow:0 6px 20px #00000059}.drawer-menu-item{text-align:left;width:100%;color:var(--text-1);font-family:var(--font-sans);font-size:var(--text-sm);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:8px 10px;display:block}.drawer-menu-item:hover:not(:disabled){background:var(--bg-2)}.drawer-menu-item:disabled{opacity:.4;cursor:not-allowed}.palette-backdrop{z-index:var(--z-palette-bg);background:#0000008c;position:fixed;inset:0}.supersede-palette{background:var(--bg-1);border:1px solid var(--border-1);width:min(640px,92vw);max-height:76vh;z-index:var(--z-palette);border-radius:8px;flex-direction:column;display:flex;position:fixed;top:12vh;left:50%;transform:translate(-50%);box-shadow:0 24px 60px #00000073}.palette-head{border-bottom:1px solid var(--border-1);align-items:center;gap:10px;padding:12px 16px;display:flex}.palette-title{font-family:var(--font-mono);font-size:var(--text-md);color:var(--text-1);flex:1}.palette-body{flex-direction:column;flex:1;gap:12px;padding:14px 16px;display:flex;overflow-y:auto}.palette-context{background:var(--bg-2);border-left:2px solid var(--border-2);border-radius:4px;grid-template-columns:auto 1fr;gap:4px 10px;padding:10px 12px;display:grid}.palette-context-label{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-3);text-transform:uppercase;letter-spacing:.04em}.palette-context-value{color:var(--text-1);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.palette-context-id{color:var(--text-3);grid-column:2}.palette-field-label{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-3);text-transform:uppercase;letter-spacing:.04em}.palette-input{box-sizing:border-box;background:var(--bg-2);border:1px solid var(--border-1);width:100%;color:var(--text-1);font-size:var(--text-sm);border-radius:4px;padding:8px 10px}.palette-input:focus{border-color:var(--accent);outline:none}.palette-list{border:1px solid var(--border-1);border-radius:4px;max-height:280px;margin:0;padding:0;list-style:none;overflow-y:auto}.palette-row{cursor:pointer;border-bottom:1px solid var(--border-1);grid-template-columns:80px 110px minmax(0,1fr) minmax(80px,160px);align-items:center;gap:10px;padding:6px 10px;display:grid}.palette-row-id{text-overflow:ellipsis;white-space:nowrap;text-align:right;overflow:hidden}.palette-row:last-child{border-bottom:none}.palette-row--hot{background:var(--bg-2)}.palette-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.palette-row[aria-selected=true] .palette-row-date,.palette-row[aria-selected=true] .palette-row-runtime,.palette-row[aria-selected=true] .palette-row-id{color:var(--text-2,var(--text-1))}.palette-row-label{color:var(--text-1);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.palette-empty{text-align:center;padding:10px}.palette-pick{background:var(--bg-2);border-left:2px solid var(--accent);border-radius:4px;grid-template-columns:auto 1fr auto;gap:4px 10px;padding:10px 12px;display:grid}.palette-pick .palette-context-id{grid-column:2/3}.palette-undo{border:1px solid var(--border-1);color:var(--text-3);cursor:pointer;font-size:var(--text-xs);background:0 0;border-radius:4px;grid-area:1/3/span 2;align-self:center;padding:4px 8px}.palette-undo:hover:not(:disabled){color:var(--text-1);border-color:var(--border-2)}.palette-error{color:#f0a0a0;font-size:var(--text-sm);background:#dc50501f;border:1px solid #dc505059;border-radius:4px;padding:8px 10px}.palette-foot{border-top:1px solid var(--border-1);justify-content:space-between;align-items:center;gap:12px;padding:10px 16px;display:flex}.palette-hint{font-family:var(--font-mono)}.palette-confirm{background:var(--accent);color:var(--bg-1);cursor:pointer;font-family:var(--font-mono);font-size:var(--text-sm);border:none;border-radius:4px;padding:8px 14px}.palette-confirm:disabled{opacity:.4;cursor:not-allowed}.card-head.card-head-stack{flex-direction:column;align-items:stretch;gap:8px;padding:10px 14px 12px}.card-head-row{justify-content:space-between;align-items:baseline;display:flex}.card-filters{flex-wrap:wrap;gap:12px;display:flex}.filter-group{gap:4px;display:flex}.filter-group .chip{font-size:var(--text-xs);padding:2px 8px}.thread-sessions-head{align-items:center;gap:12px;margin:24px 0 8px;display:flex}.thread-sessions-title{flex-shrink:0;margin:0}.thread-sessions-head .search-input{flex:1}.thread-filters{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:8px;display:flex}.thread-filters .filter-group{flex-wrap:wrap}.pagination{border-top:1px solid var(--border-1);flex-wrap:wrap;align-items:center;gap:16px;margin-top:4px;padding:14px 0 4px;display:flex}.page-size{align-items:center;gap:8px;display:flex}.page-nav{align-items:center;gap:4px;display:flex}.page-nav .chip:disabled{opacity:.4;cursor:not-allowed}.page-indicator{font-size:var(--text-sm);color:var(--text-2);text-align:center;min-width:60px;padding:0 8px}.marker-row-promotable{grid-template-columns:auto 1fr auto auto;align-items:baseline;gap:10px;display:grid}.marker-row-promotable .marker-actions{align-items:baseline;gap:6px;display:flex}.promote-chip{font-size:var(--text-xs);color:var(--accent);background:var(--accent-glow);border-color:#e8ff6e4d;padding:2px 8px}.promote-chip:hover{color:var(--accent);background:var(--accent-dim);border-color:#e8ff6e8c}.promote-editor{align-items:center;gap:6px;min-width:280px;display:flex}.promote-input{flex:1;min-width:200px}.pagination.pagination-compact{font-size:var(--text-xs);flex-wrap:wrap;gap:10px;padding:10px 0 4px}.pagination.pagination-compact .page-indicator{min-width:40px;font-size:var(--text-xs);padding:0 4px}.pagination.pagination-compact .chip{font-size:var(--text-xs);padding:1px 6px}.pagination.pagination-compact .form-label{font-size:9px}.river-date-cell--month-start{border-left:1px solid var(--border-2);padding-left:3px}.river-legend{align-items:center;gap:3px;display:flex}.river-legend-cell{background:var(--surface-2);border-radius:2px;width:12px;height:12px}.river-legend-cell.tier-1{background:#e8ff6e33}.river-legend-cell.tier-2{background:#e8ff6e66}.river-legend-cell.tier-3{background:#e8ff6ea6}.river-legend-cell.tier-4{background:var(--accent)}.empty-row{color:var(--text-2);font-size:var(--text-sm);padding:16px 14px}.list-action-row{padding:8px 14px}.toast-host{z-index:var(--z-toast);pointer-events:none;flex-direction:column;gap:8px;max-width:min(420px,100vw - 48px);display:flex;position:fixed;bottom:24px;right:24px}.toast{background:var(--surface-float);border:1px solid var(--border-3);border-left:3px solid var(--text-2);border-radius:var(--r-md);font-size:var(--text-sm);color:var(--text-1);pointer-events:auto;align-items:flex-start;gap:8px;padding:10px 12px;animation:.2s ease-out fadeSlideIn;display:flex;box-shadow:0 12px 32px #00000073}.toast-success{border-left-color:var(--accent)}.toast-error{border-left-color:var(--danger)}.toast-info{border-left-color:var(--text-2)}.toast-message{line-height:var(--lh-base);flex:1}.toast-close{color:var(--text-3);cursor:pointer;font-size:var(--text-lg);background:0 0;border:0;margin-top:-2px;padding:0 2px;line-height:1}.toast-close:hover{color:var(--text-1)}.tooltip-anchor{display:inline-flex;position:relative}.tooltip{background:var(--surface-float);color:var(--text-1);border:1px solid var(--border-3);border-radius:var(--r-sm);font-size:var(--text-xs);font-family:var(--font-mono);letter-spacing:.04em;text-align:center;pointer-events:none;opacity:0;width:max-content;max-width:240px;transition:opacity var(--ease-fast), transform var(--ease-fast);z-index:var(--z-tooltip);padding:4px 8px;position:absolute;left:50%;transform:translate(-50%)translateY(-4px)}.tooltip-top .tooltip{margin-bottom:4px;bottom:100%}.tooltip-bottom .tooltip{margin-top:4px;top:100%}.tooltip-anchor:hover .tooltip,.tooltip-anchor:focus-within .tooltip{opacity:1;transform:translate(-50%)translateY(0)}.confirm-dialog{background:var(--surface-float);border:1px solid var(--border-3);border-radius:var(--r-md);width:min(440px,92vw);z-index:var(--z-palette);padding:20px 22px 16px;animation:.2s ease-out fadeSlideIn;position:fixed;top:22vh;left:50%;transform:translate(-50%);box-shadow:0 24px 60px #0000008c}.confirm-title{font-family:var(--font-mono);font-size:var(--text-base);color:var(--text-1);letter-spacing:.04em;margin:0 0 8px}.confirm-message{color:var(--text-2);font-size:var(--text-sm);line-height:var(--lh-base);margin:0 0 16px}.confirm-actions{justify-content:flex-end;gap:8px;display:flex}.session-meta{-webkit-line-clamp:2;line-clamp:2;white-space:normal;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.search-wrap{align-items:center;display:flex;position:relative}.search-wrap .search-input{width:100%;padding-right:28px}.search-clear{color:var(--text-3);cursor:pointer;background:0 0;border:none;align-items:center;padding:0;display:flex;position:absolute;right:6px}.search-clear:hover{color:var(--text-1)}.drawer-nav{flex-shrink:0;align-items:center;gap:4px;display:flex}.drawer-nav-btn{border:1px solid var(--border-2);color:var(--text-3);cursor:pointer;border-radius:var(--r-sm);transition:color var(--ease), border-color var(--ease), background var(--ease);background:0 0;justify-content:center;align-items:center;padding:3px;display:flex}.drawer-nav-btn:hover:not(:disabled){color:var(--text-1);border-color:var(--border-3);background:var(--surface-2)}.drawer-nav-btn:disabled{opacity:.35;cursor:not-allowed}.dot-pulse{animation:2s ease-in-out infinite dot-pulse}@keyframes dot-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(1.35)}}.search-hint{margin:8px 0 12px}.search-header{z-index:var(--z-sticky);background:var(--surface-0);border-bottom:1px solid var(--border-1);margin-bottom:16px;padding-bottom:12px;position:sticky;top:0}.session-row.is-selected{background:var(--surface-2);border-left:2px solid var(--accent);padding-left:12px}.live-tag[data-kind=summary]{background:var(--surface-2);color:var(--text-2)}.live-tag[data-kind=label]{background:var(--surface-2);color:var(--text-1)}.live-tag[data-kind=entity]{background:var(--accent-glow);color:var(--accent);border-color:#e8ff6e40}.match-snippet{color:var(--text-2);font-size:var(--text-xs);line-height:var(--lh-base);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin-top:4px;display:-webkit-box;overflow:hidden}.match-snippet mark{color:var(--accent);background:#e8ff6e26;border-radius:2px;padding:0 2px}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>nlm memory</title>
7
- <script type="module" crossorigin src="/ui/assets/index-BR1BHwew.js"></script>
8
- <link rel="stylesheet" crossorigin href="/ui/assets/index-CgHq6xeZ.css">
7
+ <script type="module" crossorigin src="/ui/assets/index-BTj60bAH.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/ui/assets/index-Baw2qIKc.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/nlm/README.md CHANGED
@@ -7,7 +7,7 @@ the prompt-recall hook.
7
7
 
8
8
  Pi's interactive UI strips `index.{ts,js}` from the load path, so this
9
9
  extension surfaces in pi's `[Extensions]` list as `nlm` — matching the
10
- naming convention used by `pi-mcp-adapter`, `whtnxt-tasks`, etc.
10
+ naming convention used by `pi-mcp-adapter`, `my-tasks`, etc.
11
11
 
12
12
  ## Install
13
13
 
package/nlm/index.js CHANGED
@@ -53,7 +53,7 @@ var RECALL_LIMIT = 5;
53
53
  var RECALL_TIMEOUT_MS = 2e3;
54
54
  async function recallOverHttp(prompt, runtime) {
55
55
  const portValue = process.env["NLM_PORT"] ?? "3940";
56
- const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}`;
56
+ const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}&withFacts=true`;
57
57
  const controller = new AbortController();
58
58
  const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
59
59
  try {
@@ -63,19 +63,26 @@ async function recallOverHttp(prompt, runtime) {
63
63
  headers: hookAuthHeaders(extra),
64
64
  signal: controller.signal
65
65
  });
66
- if (!res.ok) return [];
66
+ if (!res.ok) return { hits: [], facts: [] };
67
67
  let body;
68
68
  try {
69
69
  body = await res.json();
70
70
  } catch {
71
- return [];
71
+ return { hits: [], facts: [] };
72
72
  }
73
- return (body.results ?? []).map((r) => ({
73
+ const hits = (body.results ?? []).map((r) => ({
74
74
  id: r.id,
75
75
  label: r.label,
76
76
  startedAt: r.startedAt,
77
77
  matchScore: r.matchScore
78
78
  }));
79
+ const facts = (body.relatedFacts ?? []).map((f) => ({
80
+ subject: f.subject,
81
+ predicate: f.predicate,
82
+ value: f.value,
83
+ corroborationCount: f.corroborationCount
84
+ }));
85
+ return { hits, facts };
79
86
  } finally {
80
87
  clearTimeout(timer);
81
88
  }
@@ -145,16 +152,27 @@ function recordSurfaced(conversationId, ids) {
145
152
  }
146
153
 
147
154
  // src/core/hook/pointer-block.ts
148
- function formatPointerBlock(hits) {
149
- if (hits.length === 0) return "";
150
- const lines = hits.map(
151
- (h) => `- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`
152
- );
153
- return [
154
- "## Possibly-relevant prior sessions (nlm-memory)",
155
- ...lines,
155
+ function formatPointerBlock(hits, facts = []) {
156
+ if (hits.length === 0 && facts.length === 0) return "";
157
+ const out = [];
158
+ if (hits.length > 0) {
159
+ out.push("## Possibly-relevant prior sessions (nlm-memory)");
160
+ for (const h of hits) {
161
+ out.push(`- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`);
162
+ }
163
+ }
164
+ if (facts.length > 0) {
165
+ if (out.length > 0) out.push("");
166
+ out.push("## Known facts about top entities");
167
+ for (const f of facts) {
168
+ const tag = f.corroborationCount > 1 ? ` [${f.corroborationCount} sessions]` : "";
169
+ out.push(`- ${f.subject} ${f.predicate}: ${f.value}${tag}`);
170
+ }
171
+ }
172
+ out.push(
156
173
  "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved)."
157
- ].join("\n");
174
+ );
175
+ return out.join("\n");
158
176
  }
159
177
 
160
178
  // src/core/hook/select.ts
@@ -173,6 +191,10 @@ var SCORE_THRESHOLD = 0;
173
191
  var PER_FIRE_CAP = 3;
174
192
  var PER_CONVERSATION_CAP = 10;
175
193
  var PROMPT_PREVIEW_CHARS = 200;
194
+ function normalizeRecall(raw) {
195
+ if (Array.isArray(raw)) return { hits: raw, facts: [] };
196
+ return raw;
197
+ }
176
198
  async function runHook(input, deps) {
177
199
  const gate = classifyPrompt(input.prompt);
178
200
  const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
@@ -189,12 +211,13 @@ async function runHook(input, deps) {
189
211
  });
190
212
  return "";
191
213
  }
192
- let hits = [];
214
+ let fetched = { hits: [], facts: [] };
193
215
  try {
194
- hits = await deps.recall(input.prompt);
216
+ fetched = normalizeRecall(await deps.recall(input.prompt));
195
217
  } catch {
196
- hits = [];
218
+ fetched = { hits: [], facts: [] };
197
219
  }
220
+ const hits = fetched.hits;
198
221
  const surfaced = loadSurfaced(input.conversationId);
199
222
  const selected = selectHits({
200
223
  hits,
@@ -203,7 +226,7 @@ async function runHook(input, deps) {
203
226
  perFireCap: PER_FIRE_CAP,
204
227
  perConversationCap: PER_CONVERSATION_CAP
205
228
  });
206
- const block = formatPointerBlock(selected);
229
+ const block = formatPointerBlock(selected, fetched.facts);
207
230
  const estTokens = Math.ceil(block.length / 4);
208
231
  appendHookLog({
209
232
  ts: (/* @__PURE__ */ new Date()).toISOString(),
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "nlm-memory",
3
- "version": "0.5.22",
3
+ "version": "0.7.0",
4
4
  "description": "Local-first non-linear memory operating system for AI operators.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "engines": {
8
- "node": ">=20.0.0"
8
+ "node": ">=22.0.0"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/pbmagnet4/nlm-memory-ts.git"
12
+ "url": "git+https://github.com/pbmagnet4/nlm-memory.git"
13
13
  },
14
- "homepage": "https://github.com/pbmagnet4/nlm-memory-ts#readme",
14
+ "homepage": "https://github.com/pbmagnet4/nlm-memory#readme",
15
15
  "bugs": {
16
- "url": "https://github.com/pbmagnet4/nlm-memory-ts/issues"
16
+ "url": "https://github.com/pbmagnet4/nlm-memory/issues"
17
17
  },
18
18
  "keywords": [
19
19
  "ai",
@@ -54,37 +54,38 @@
54
54
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
55
55
  "bench:longmemeval": "tsx scripts/longmemeval/run-harness.ts",
56
56
  "bench:classifier": "tsx scripts/longmemeval/run-harness.ts --classifier",
57
- "bench:compare": "tsx scripts/longmemeval/compare-classifiers.ts"
57
+ "bench:compare": "tsx scripts/longmemeval/compare-classifiers.ts",
58
+ "eval:signals": "tsx scripts/eval/signals-eval.ts"
58
59
  },
59
60
  "dependencies": {
60
61
  "@clack/prompts": "^1.4.0",
61
62
  "@hono/node-server": "^2.0.3",
62
63
  "@modelcontextprotocol/sdk": "^1.29.0",
63
64
  "@toon-format/toon": "^2.3.0",
64
- "better-sqlite3": "^11.5.0",
65
- "commander": "^12.1.0",
65
+ "better-sqlite3": "^12.10.0",
66
+ "commander": "^15.0.0",
66
67
  "hono": "^4.6.0",
67
68
  "pg": "^8.21.0",
68
- "pgvector": "^0.2.1",
69
+ "pgvector": "^0.3.0",
69
70
  "sqlite-vec": "^0.1.6",
70
71
  "yaml": "^2.9.0",
71
- "zod": "^3.23.0"
72
+ "zod": "^4.4.3"
72
73
  },
73
74
  "devDependencies": {
74
75
  "@types/better-sqlite3": "^7.6.13",
75
76
  "@types/node": "^22.10.0",
76
77
  "@types/pg": "^8.20.0",
77
- "@types/react": "^18.3.28",
78
- "@types/react-dom": "^18.3.7",
79
- "@vitejs/plugin-react": "^4.7.0",
78
+ "@types/react": "^19.2.17",
79
+ "@types/react-dom": "^19.2.3",
80
+ "@vitejs/plugin-react": "^6.0.2",
80
81
  "esbuild": "^0.28.0",
81
- "react": "^18.3.1",
82
- "react-dom": "^18.3.1",
82
+ "react": "^19.2.7",
83
+ "react-dom": "^19.2.7",
83
84
  "react-router-dom": "^6.30.3",
84
85
  "tsc-alias": "^1.8.17",
85
86
  "tsx": "^4.19.0",
86
- "typescript": "^5.7.0",
87
- "vite": "^5.4.21",
88
- "vitest": "^2.1.0"
87
+ "typescript": "^6.0.3",
88
+ "vite": "^8.0.16",
89
+ "vitest": "^4.1.8"
89
90
  }
90
91
  }
@@ -7,8 +7,8 @@
7
7
  "url": "https://github.com/pbmagnet4"
8
8
  },
9
9
  "license": "MIT",
10
- "homepage": "https://github.com/pbmagnet4/nlm-memory-ts",
11
- "repository": "https://github.com/pbmagnet4/nlm-memory-ts",
10
+ "homepage": "https://github.com/pbmagnet4/nlm-memory",
11
+ "repository": "https://github.com/pbmagnet4/nlm-memory",
12
12
  "keywords": [
13
13
  "memory",
14
14
  "context",
package/plugin/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This directory is the Codex plugin distribution surface for nlm-memory. The
4
4
  repo root acts as a Codex marketplace (`codex plugin marketplace add
5
- pbmagnet4/nlm-memory-ts`), and this directory is the plugin Codex installs.
5
+ pbmagnet4/nlm-memory`), and this directory is the plugin Codex installs.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,8 +10,8 @@ Prerequisite: `npm install -g nlm-memory` (puts `nlm` on PATH; the MCP server
10
10
  spawns `nlm mcp`).
11
11
 
12
12
  ```bash
13
- codex plugin marketplace add pbmagnet4/nlm-memory-ts
14
- codex plugin add nlm-memory@nlm-memory-ts
13
+ codex plugin marketplace add pbmagnet4/nlm-memory
14
+ codex plugin add nlm-memory@nlm-memory
15
15
  ```
16
16
 
17
17
  Or use the wrapper: `nlm connect codex`.
@@ -64,16 +64,27 @@ function recordSurfaced(conversationId, ids) {
64
64
  }
65
65
 
66
66
  // src/core/hook/pointer-block.ts
67
- function formatPointerBlock(hits) {
68
- if (hits.length === 0) return "";
69
- const lines = hits.map(
70
- (h) => `- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`
71
- );
72
- return [
73
- "## Possibly-relevant prior sessions (nlm-memory)",
74
- ...lines,
67
+ function formatPointerBlock(hits, facts = []) {
68
+ if (hits.length === 0 && facts.length === 0) return "";
69
+ const out = [];
70
+ if (hits.length > 0) {
71
+ out.push("## Possibly-relevant prior sessions (nlm-memory)");
72
+ for (const h of hits) {
73
+ out.push(`- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`);
74
+ }
75
+ }
76
+ if (facts.length > 0) {
77
+ if (out.length > 0) out.push("");
78
+ out.push("## Known facts about top entities");
79
+ for (const f of facts) {
80
+ const tag = f.corroborationCount > 1 ? ` [${f.corroborationCount} sessions]` : "";
81
+ out.push(`- ${f.subject} ${f.predicate}: ${f.value}${tag}`);
82
+ }
83
+ }
84
+ out.push(
75
85
  "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved)."
76
- ].join("\n");
86
+ );
87
+ return out.join("\n");
77
88
  }
78
89
 
79
90
  // src/core/hook/select.ts
@@ -142,7 +153,7 @@ var RECALL_LIMIT = 5;
142
153
  var RECALL_TIMEOUT_MS = 2e3;
143
154
  async function recallOverHttp(prompt, runtime) {
144
155
  const portValue = process.env["NLM_PORT"] ?? "3940";
145
- const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}`;
156
+ const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}&withFacts=true`;
146
157
  const controller = new AbortController();
147
158
  const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
148
159
  try {
@@ -152,19 +163,26 @@ async function recallOverHttp(prompt, runtime) {
152
163
  headers: hookAuthHeaders(extra),
153
164
  signal: controller.signal
154
165
  });
155
- if (!res.ok) return [];
166
+ if (!res.ok) return { hits: [], facts: [] };
156
167
  let body;
157
168
  try {
158
169
  body = await res.json();
159
170
  } catch {
160
- return [];
171
+ return { hits: [], facts: [] };
161
172
  }
162
- return (body.results ?? []).map((r) => ({
173
+ const hits = (body.results ?? []).map((r) => ({
163
174
  id: r.id,
164
175
  label: r.label,
165
176
  startedAt: r.startedAt,
166
177
  matchScore: r.matchScore
167
178
  }));
179
+ const facts = (body.relatedFacts ?? []).map((f) => ({
180
+ subject: f.subject,
181
+ predicate: f.predicate,
182
+ value: f.value,
183
+ corroborationCount: f.corroborationCount
184
+ }));
185
+ return { hits, facts };
168
186
  } finally {
169
187
  clearTimeout(timer);
170
188
  }
@@ -175,6 +193,10 @@ var SCORE_THRESHOLD = 0;
175
193
  var PER_FIRE_CAP = 3;
176
194
  var PER_CONVERSATION_CAP = 10;
177
195
  var PROMPT_PREVIEW_CHARS = 200;
196
+ function normalizeRecall(raw) {
197
+ if (Array.isArray(raw)) return { hits: raw, facts: [] };
198
+ return raw;
199
+ }
178
200
  async function runHook(input, deps) {
179
201
  const gate = classifyPrompt(input.prompt);
180
202
  const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
@@ -191,12 +213,13 @@ async function runHook(input, deps) {
191
213
  });
192
214
  return "";
193
215
  }
194
- let hits = [];
216
+ let fetched = { hits: [], facts: [] };
195
217
  try {
196
- hits = await deps.recall(input.prompt);
218
+ fetched = normalizeRecall(await deps.recall(input.prompt));
197
219
  } catch {
198
- hits = [];
220
+ fetched = { hits: [], facts: [] };
199
221
  }
222
+ const hits = fetched.hits;
200
223
  const surfaced = loadSurfaced(input.conversationId);
201
224
  const selected = selectHits({
202
225
  hits,
@@ -205,7 +228,7 @@ async function runHook(input, deps) {
205
228
  perFireCap: PER_FIRE_CAP,
206
229
  perConversationCap: PER_CONVERSATION_CAP
207
230
  });
208
- const block = formatPointerBlock(selected);
231
+ const block = formatPointerBlock(selected, fetched.facts);
209
232
  const estTokens = Math.ceil(block.length / 4);
210
233
  appendHookLog({
211
234
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3,8 +3,8 @@
3
3
  // src/hook/stop-hook.ts
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
6
- import { homedir as homedir4 } from "node:os";
7
- import { dirname, join as join3 } from "node:path";
6
+ import { homedir as homedir5 } from "node:os";
7
+ import { dirname as dirname2, join as join4 } from "node:path";
8
8
 
9
9
  // src/core/hook/citation-detect.ts
10
10
  var MIN_ID_LEN = 6;
@@ -58,6 +58,43 @@ function safeStringify(value) {
58
58
  }
59
59
  }
60
60
 
61
+ // src/core/hook/miss-detect.ts
62
+ var MIN_ID_LEN2 = 6;
63
+ function detectMisses(input) {
64
+ const surfaced = /* @__PURE__ */ new Set();
65
+ for (const id of input.surfacedIds) {
66
+ if (id.length >= MIN_ID_LEN2) surfaced.add(id);
67
+ }
68
+ const seenMissed = /* @__PURE__ */ new Set();
69
+ const misses = [];
70
+ for (const tu of input.toolUses) {
71
+ if (!isNlmTool2(tu.name)) continue;
72
+ const kind = explicitIdKind(tu.name);
73
+ if (!kind) continue;
74
+ const id = extractId(tu.input);
75
+ if (!id || id.length < MIN_ID_LEN2) continue;
76
+ if (surfaced.has(id)) continue;
77
+ if (seenMissed.has(id)) continue;
78
+ seenMissed.add(id);
79
+ misses.push({ id, kind });
80
+ }
81
+ return misses;
82
+ }
83
+ function isNlmTool2(name) {
84
+ return /^mcp__[^_]*nlm[^_]*__/.test(name);
85
+ }
86
+ function explicitIdKind(name) {
87
+ if (name.endsWith("__get_session")) return "get_session";
88
+ if (name.endsWith("__cite_session")) return "cite_session";
89
+ return null;
90
+ }
91
+ function extractId(input) {
92
+ if (input === null || typeof input !== "object") return null;
93
+ const obj = input;
94
+ const candidate = obj["id"];
95
+ return typeof candidate === "string" ? candidate : null;
96
+ }
97
+
61
98
  // src/core/hook/memo.ts
62
99
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
63
100
  import { homedir } from "node:os";
@@ -114,6 +151,32 @@ function recordCited(conversationId, ids) {
114
151
  }
115
152
  }
116
153
 
154
+ // src/core/recall/miss-log.ts
155
+ import { appendFile, mkdir, readFile, stat } from "node:fs/promises";
156
+ import { dirname, join as join3 } from "node:path";
157
+ import { homedir as homedir3 } from "node:os";
158
+ function defaultLogPath() {
159
+ return process.env["NLM_MISS_LOG"] ?? join3(homedir3(), ".nlm", "miss-log.jsonl");
160
+ }
161
+ function isEnabled() {
162
+ const raw = process.env["NLM_MISS_LOG_ENABLED"];
163
+ if (raw === void 0) return true;
164
+ return raw !== "0" && raw.toLowerCase() !== "false";
165
+ }
166
+ async function appendMisses(entries, logPath2) {
167
+ if (!isEnabled()) return;
168
+ if (entries.length === 0) return;
169
+ const path = logPath2 ?? defaultLogPath();
170
+ try {
171
+ await mkdir(dirname(path), { recursive: true });
172
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
173
+ const block = entries.map((e) => `${JSON.stringify({ ts, ...e })}
174
+ `).join("");
175
+ await appendFile(path, block, "utf8");
176
+ } catch {
177
+ }
178
+ }
179
+
117
180
  // src/core/hook/transcript.ts
118
181
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
119
182
  function parseTurn(parsed) {
@@ -164,7 +227,7 @@ function readAllAssistantTurns(transcriptPath) {
164
227
 
165
228
  // src/llm/env-autoload.ts
166
229
  import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
167
- import { homedir as homedir3 } from "node:os";
230
+ import { homedir as homedir4 } from "node:os";
168
231
  import { resolve } from "node:path";
169
232
  var DEFAULT_SEARCH_PATHS = [
170
233
  "~/.nlm/.env",
@@ -173,7 +236,7 @@ var DEFAULT_SEARCH_PATHS = [
173
236
  "../../.env"
174
237
  ];
175
238
  function expandHome(p) {
176
- if (p.startsWith("~/")) return resolve(homedir3(), p.slice(2));
239
+ if (p.startsWith("~/")) return resolve(homedir4(), p.slice(2));
177
240
  return p;
178
241
  }
179
242
  function autoloadEnv(extraPaths = []) {
@@ -259,6 +322,17 @@ async function runStopHook(input, deps) {
259
322
  });
260
323
  const alreadyCited = loadCited(input.conversationId);
261
324
  const fresh = detected.filter((c) => !alreadyCited.has(c.id));
325
+ const misses = detectMisses({ toolUses: allToolUses, surfacedIds: surfaced });
326
+ if (misses.length > 0) {
327
+ void appendMisses(
328
+ misses.map((m) => ({
329
+ conversationId: input.conversationId,
330
+ missedId: m.id,
331
+ kind: m.kind,
332
+ surfacedCount: surfaced.size
333
+ }))
334
+ );
335
+ }
262
336
  const lastText = turns[turns.length - 1]?.text ?? "";
263
337
  const preview = lastText.slice(0, RESPONSE_PREVIEW_CHARS);
264
338
  for (const c of fresh) {
@@ -279,12 +353,12 @@ async function runStopHook(input, deps) {
279
353
  };
280
354
  }
281
355
  function logPath() {
282
- return process.env["NLM_HOOK_LOG"] ?? join3(homedir4(), ".nlm", "hook-log.jsonl");
356
+ return process.env["NLM_HOOK_LOG"] ?? join4(homedir5(), ".nlm", "hook-log.jsonl");
283
357
  }
284
358
  function logStopResult(result) {
285
359
  try {
286
360
  const path = logPath();
287
- mkdirSync3(dirname(path), { recursive: true });
361
+ mkdirSync3(dirname2(path), { recursive: true });
288
362
  appendFileSync(
289
363
  path,
290
364
  `${JSON.stringify({