nlm-memory 0.9.1 → 0.10.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 (69) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/nlm.js +54 -0
  3. package/dist/cli/nlm.js.map +1 -1
  4. package/dist/core/dataset/build-dataset.d.ts +13 -0
  5. package/dist/core/dataset/build-dataset.js +31 -2
  6. package/dist/core/dataset/build-dataset.js.map +1 -1
  7. package/dist/core/integrity/check-invariants.d.ts +41 -0
  8. package/dist/core/integrity/check-invariants.js +349 -0
  9. package/dist/core/integrity/check-invariants.js.map +1 -0
  10. package/dist/core/scheduler/scan-once.d.ts +4 -1
  11. package/dist/core/scheduler/scan-once.js +53 -14
  12. package/dist/core/scheduler/scan-once.js.map +1 -1
  13. package/dist/core/scheduler/scheduler.js +64 -11
  14. package/dist/core/scheduler/scheduler.js.map +1 -1
  15. package/dist/core/storage/live-status.d.ts +5 -3
  16. package/dist/core/storage/live-status.js +6 -2
  17. package/dist/core/storage/live-status.js.map +1 -1
  18. package/dist/core/storage/migrate.d.ts +5 -0
  19. package/dist/core/storage/migrate.js +24 -8
  20. package/dist/core/storage/migrate.js.map +1 -1
  21. package/dist/core/storage/pg-session-store.d.ts +2 -2
  22. package/dist/core/storage/pg-session-store.js +31 -6
  23. package/dist/core/storage/pg-session-store.js.map +1 -1
  24. package/dist/core/storage/sqlite-fact-store.js +7 -15
  25. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  26. package/dist/core/storage/sqlite-session-store.d.ts +14 -2
  27. package/dist/core/storage/sqlite-session-store.js +61 -24
  28. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  29. package/dist/hook/prompt-recall-hook.js +1 -1
  30. package/dist/hook/prompt-recall-hook.js.map +1 -1
  31. package/dist/hook/stop-hook.js +1 -1
  32. package/dist/hook/stop-hook.js.map +1 -1
  33. package/dist/http/app.js +3 -1
  34. package/dist/http/app.js.map +1 -1
  35. package/dist/shared/types.d.ts +24 -5
  36. package/dist/ui/assets/index-CJgVgxlM.js +11 -0
  37. package/dist/ui/assets/index-D5lKaksH.css +1 -0
  38. package/dist/ui/index.html +2 -2
  39. package/migrations/000_initial_schema.sql +174 -0
  40. package/migrations/001_entity_type_rename.sql +17 -0
  41. package/migrations/002_adapter_state_extend.sql +12 -0
  42. package/migrations/003_session_embeddings.sql +11 -0
  43. package/migrations/004_facts.sql +46 -0
  44. package/migrations/005_sources.sql +31 -0
  45. package/migrations/006_providers.sql +33 -0
  46. package/migrations/007_source_tokens.sql +17 -0
  47. package/migrations/008_fts_rebuild.sql +9 -0
  48. package/migrations/009_session_embedding_chunks.sql +46 -0
  49. package/migrations/010_sources_opencode.sql +30 -0
  50. package/migrations/011_sources_hermes_agent.sql +30 -0
  51. package/migrations/012_sources_aider.sql +30 -0
  52. package/migrations/013_adapter_state_failure_count.sql +12 -0
  53. package/migrations/014_sources_cursor.sql +30 -0
  54. package/migrations/015_sources_windsurf.sql +30 -0
  55. package/migrations/016_sources_codex.sql +30 -0
  56. package/migrations/017_signals.sql +31 -0
  57. package/migrations/018_repair_self_supersede.sql +21 -0
  58. package/migrations/019_split_replaces.sql +117 -0
  59. package/migrations/020_collapse_duplicate_active_facts.sql +45 -0
  60. package/migrations/pg/001_initial.sql +214 -0
  61. package/migrations/pg/018_repair_self_supersede.sql +16 -0
  62. package/migrations/pg/019_split_replaces.sql +32 -0
  63. package/migrations/pg/020_collapse_duplicate_active_facts.sql +46 -0
  64. package/nlm/index.js +1 -1
  65. package/package.json +2 -1
  66. package/plugin/scripts/prompt-recall-hook.mjs +1 -1
  67. package/plugin/scripts/stop-hook.mjs +1 -1
  68. package/dist/ui/assets/index-BTj60bAH.js +0 -11
  69. package/dist/ui/assets/index-Baw2qIKc.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-replaced{color:var(--text-3)}.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)}.session-row-end{align-items:center;gap:8px;display:flex}.session-row.is-replaced{opacity:.55}.earlier-versions-list{border-left:2px solid var(--border-2);margin-left:28px;list-style:none}.earlier-versions-list .session-row{padding-left:10px}.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-BTj60bAH.js"></script>
8
- <link rel="stylesheet" crossorigin href="/ui/assets/index-Baw2qIKc.css">
7
+ <script type="module" crossorigin src="/ui/assets/index-CJgVgxlM.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/ui/assets/index-D5lKaksH.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -0,0 +1,174 @@
1
+ -- NLE Memory canonical store — SQLite schema
2
+ -- Default zero-config backend. Postgres mirror lives in schema/postgres.sql (TBD).
3
+ --
4
+ -- Design principles:
5
+ -- • Sessions are immutable once written — supersedence is via edges, never via UPDATE
6
+ -- • Entity registry is mutable (canonical merges, retitles, retirements)
7
+ -- • All timestamps are ISO 8601 strings in TEXT columns (SQLite lacks a native datetime type;
8
+ -- ISO strings sort correctly and are dialect-portable to Postgres)
9
+ -- • Foreign keys enforced; ON DELETE CASCADE only on edges, never on sessions
10
+
11
+ PRAGMA foreign_keys = ON;
12
+ PRAGMA journal_mode = WAL;
13
+
14
+ -- ── Sessions ───────────────────────────────────────────────────────────────
15
+ CREATE TABLE IF NOT EXISTS sessions (
16
+ id TEXT PRIMARY KEY, -- e.g. sess_2026-05-07T14-32-CAMEL
17
+ runtime TEXT NOT NULL, -- e.g. claude-code/1.0, hermes/0.5
18
+ runtime_session_id TEXT, -- the runtime's own session identifier
19
+ started_at TEXT NOT NULL, -- ISO 8601
20
+ ended_at TEXT, -- ISO 8601; NULL while session is active
21
+ duration_min INTEGER, -- computed; null while active
22
+ label TEXT NOT NULL, -- human-readable session title
23
+ summary TEXT NOT NULL, -- ~80-token classifier output
24
+ body TEXT, -- full markdown body with inline markers
25
+ status TEXT NOT NULL CHECK(status IN ('active','closed','superseded')),
26
+ transcript_kind TEXT, -- e.g. claude-code-jsonl
27
+ transcript_path TEXT, -- runtime-resolvable opaque pointer
28
+ transcript_offset INTEGER, -- byte offset start
29
+ transcript_length INTEGER, -- byte length
30
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
31
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at DESC);
35
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
36
+ CREATE INDEX IF NOT EXISTS idx_sessions_runtime ON sessions(runtime);
37
+
38
+ -- ── Entities ───────────────────────────────────────────────────────────────
39
+ CREATE TABLE IF NOT EXISTS entities (
40
+ canonical TEXT PRIMARY KEY, -- preferred spelling — also the primary key
41
+ type TEXT NOT NULL, -- 'candidate' until labeled. Built-in labels: project | tool | contact | service | concept. Custom labels are user-defined via the UI / `nle-daemon action label` and have no CHECK constraint.
42
+ status TEXT NOT NULL CHECK(status IN ('active','dormant','retired','rejected','candidate')),
43
+ source TEXT, -- e.g. 'property:.claude/properties/polysignal.yaml', 'auto-detected', 'user-registered'
44
+ notes TEXT, -- freeform user notes
45
+ first_seen_session TEXT REFERENCES sessions(id),
46
+ last_seen_session TEXT REFERENCES sessions(id),
47
+ session_count INTEGER NOT NULL DEFAULT 0,
48
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
49
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
50
+ );
51
+
52
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
53
+ CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status);
54
+
55
+ -- ── Entity variants (case-insensitive normalization) ───────────────────────
56
+ CREATE TABLE IF NOT EXISTS entity_variants (
57
+ variant TEXT PRIMARY KEY, -- raw form as it appeared
58
+ canonical TEXT NOT NULL REFERENCES entities(canonical) ON DELETE CASCADE,
59
+ source_session_id TEXT REFERENCES sessions(id)
60
+ );
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_entity_variants_canonical ON entity_variants(canonical);
63
+
64
+ -- ── Session ↔ Entity (many-to-many) ───────────────────────────────────────
65
+ CREATE TABLE IF NOT EXISTS session_entities (
66
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
67
+ entity_canonical TEXT NOT NULL REFERENCES entities(canonical),
68
+ PRIMARY KEY (session_id, entity_canonical)
69
+ );
70
+
71
+ CREATE INDEX IF NOT EXISTS idx_session_entities_entity ON session_entities(entity_canonical);
72
+
73
+ -- ── Markers (decisions / open questions) ──────────────────────────────────
74
+ -- Extracted from inline body markers. Body is canonical — these rows are a queryable cache.
75
+ CREATE TABLE IF NOT EXISTS markers (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
78
+ kind TEXT NOT NULL CHECK(kind IN ('decision','open')),
79
+ text TEXT NOT NULL,
80
+ position INTEGER NOT NULL DEFAULT 0 -- ordering within session
81
+ );
82
+
83
+ CREATE INDEX IF NOT EXISTS idx_markers_session ON markers(session_id);
84
+ CREATE INDEX IF NOT EXISTS idx_markers_kind ON markers(kind);
85
+
86
+ -- ── Supersedence + continues edges ────────────────────────────────────────
87
+ -- Edge table for non-destructive editing. A session can supersede or continue another.
88
+ -- Bidirectional lookups handled by indexes on both columns.
89
+ CREATE TABLE IF NOT EXISTS session_edges (
90
+ from_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
91
+ to_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
92
+ kind TEXT NOT NULL CHECK(kind IN ('supersedes','continues','branched_from','merged_from')),
93
+ PRIMARY KEY (from_session, to_session, kind)
94
+ );
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_session_edges_from ON session_edges(from_session);
97
+ CREATE INDEX IF NOT EXISTS idx_session_edges_to ON session_edges(to_session);
98
+ CREATE INDEX IF NOT EXISTS idx_session_edges_kind ON session_edges(kind);
99
+
100
+ -- ── Full-text search over sessions ────────────────────────────────────────
101
+ -- FTS5 virtual table for label/summary/body search. Maintained via triggers.
102
+ CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
103
+ label, summary, body,
104
+ content='sessions',
105
+ content_rowid='rowid'
106
+ );
107
+
108
+ CREATE TRIGGER IF NOT EXISTS sessions_ai AFTER INSERT ON sessions BEGIN
109
+ INSERT INTO sessions_fts(rowid, label, summary, body)
110
+ VALUES (new.rowid, new.label, new.summary, new.body);
111
+ END;
112
+
113
+ CREATE TRIGGER IF NOT EXISTS sessions_au AFTER UPDATE ON sessions BEGIN
114
+ INSERT INTO sessions_fts(sessions_fts, rowid, label, summary, body)
115
+ VALUES('delete', old.rowid, old.label, old.summary, old.body);
116
+ INSERT INTO sessions_fts(rowid, label, summary, body)
117
+ VALUES (new.rowid, new.label, new.summary, new.body);
118
+ END;
119
+
120
+ CREATE TRIGGER IF NOT EXISTS sessions_ad AFTER DELETE ON sessions BEGIN
121
+ INSERT INTO sessions_fts(sessions_fts, rowid, label, summary, body)
122
+ VALUES('delete', old.rowid, old.label, old.summary, old.body);
123
+ END;
124
+
125
+ -- ── Vector embeddings (sqlite-vec) ────────────────────────────────────────
126
+ -- Loaded as an extension at runtime: SELECT load_extension('vec0');
127
+ -- Schema declared here for reference; real CREATE happens at daemon startup
128
+ -- after the extension is loaded.
129
+ --
130
+ -- CREATE VIRTUAL TABLE session_embeddings USING vec0(
131
+ -- session_id TEXT PRIMARY KEY,
132
+ -- embedding float[768] -- nomic-embed-text dim; configurable
133
+ -- );
134
+
135
+ -- ── Schema migrations tracker ─────────────────────────────────────────────
136
+ -- Applied by SQLiteStore.migrate() on daemon start. Tracks which versioned
137
+ -- migration files in daemon/migrations/ have been run on this database.
138
+ CREATE TABLE IF NOT EXISTS schema_migrations (
139
+ version INTEGER PRIMARY KEY,
140
+ name TEXT NOT NULL,
141
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
142
+ );
143
+
144
+ -- ── Adapter state (per-runtime offsets for resumability) ──────────────────
145
+ CREATE TABLE IF NOT EXISTS adapter_state (
146
+ adapter_name TEXT NOT NULL, -- e.g. 'claude-code'
147
+ source_path TEXT NOT NULL, -- e.g. ~/.claude/projects/foo/abc123.jsonl
148
+ last_offset INTEGER NOT NULL DEFAULT 0,
149
+ last_processed_at TEXT NOT NULL DEFAULT (datetime('now')),
150
+ PRIMARY KEY (adapter_name, source_path)
151
+ );
152
+
153
+ -- ── Actions (event-sourced action log) ────────────────────────────────────
154
+ -- Universal primitive for every interactive change: alert dismiss/snooze, entity
155
+ -- retire, link supersedes/continues, mark intentional, undo, etc. Append-only.
156
+ -- Computed tables (session_edges, entities.status) become projections of this log.
157
+ -- Same schema across web UI, MCP tool calls, CLI, future mobile/api.
158
+ CREATE TABLE IF NOT EXISTS actions (
159
+ id TEXT PRIMARY KEY, -- act_<iso-ts>_<short-uuid>
160
+ timestamp TEXT NOT NULL, -- ISO 8601 — when the action was taken
161
+ kind TEXT NOT NULL, -- dismiss | snooze | retire_entity | label_entity | merge_entity | link_supersedes | link_continues | resolve_open | mark_intentional | undo | sync_localstorage
162
+ subject_type TEXT NOT NULL, -- alert | entity | session | decision | open_question | action
163
+ subject_id TEXT NOT NULL, -- e.g. 'stale_Squarespace', 'sess_002', 'NocoDB'
164
+ payload TEXT, -- JSON: action-specific data (snoozed_until, target_session_id, new_type, ...)
165
+ actor TEXT NOT NULL DEFAULT 'user', -- user | agent:claude-code | agent:hermes | system
166
+ runtime TEXT, -- web-ui | mcp:claude-code | cli | mobile-ios | api
167
+ reverted_by TEXT REFERENCES actions(id), -- the action that undid this one (null = active)
168
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
169
+ );
170
+
171
+ CREATE INDEX IF NOT EXISTS idx_actions_subject ON actions(subject_type, subject_id);
172
+ CREATE INDEX IF NOT EXISTS idx_actions_kind ON actions(kind);
173
+ CREATE INDEX IF NOT EXISTS idx_actions_timestamp ON actions(timestamp DESC);
174
+ CREATE INDEX IF NOT EXISTS idx_actions_active ON actions(subject_type, subject_id, reverted_by) WHERE reverted_by IS NULL;
@@ -0,0 +1,17 @@
1
+ -- Migration 001: rename entity types and action kind
2
+ --
3
+ -- Renames entity.type values: property→project, person→contact, external→service
4
+ -- (The CHECK constraint on entities.type was already dropped in the initial schema.)
5
+ -- Safe to re-run: CASE expression only matches old values; INSERT OR IGNORE is a no-op
6
+ -- if this migration has already been applied.
7
+
8
+ UPDATE entities
9
+ SET type = CASE
10
+ WHEN type = 'property' THEN 'project'
11
+ WHEN type = 'person' THEN 'contact'
12
+ WHEN type = 'external' THEN 'service'
13
+ ELSE type
14
+ END
15
+ WHERE type IN ('property', 'person', 'external');
16
+
17
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (1, 'entity_type_rename');
@@ -0,0 +1,12 @@
1
+ -- Migration 002: extend adapter_state with columns needed for supersede-on-resume.
2
+ --
3
+ -- file_size — bytes-on-disk at last classification (detects file growth)
4
+ -- session_id — NLE session id produced by the last classification (target for supersede)
5
+ --
6
+ -- last_offset stays for future chunking use (Phase 3+). For supersede-on-resume in
7
+ -- Phase 2, file_size carries the equivalent signal at whole-file granularity.
8
+
9
+ ALTER TABLE adapter_state ADD COLUMN file_size INTEGER;
10
+ ALTER TABLE adapter_state ADD COLUMN session_id TEXT;
11
+
12
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (2, '002_adapter_state_extend');
@@ -0,0 +1,11 @@
1
+ -- Migration 003: session_embeddings virtual table via sqlite-vec.
2
+ --
3
+ -- Requires sqlite-vec loaded at connection time (handled by SQLiteStore.connect()).
4
+ -- 768 dims matches nomic-embed-text, the default embedding model.
5
+
6
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_embeddings USING vec0(
7
+ session_id TEXT PRIMARY KEY,
8
+ embedding float[768]
9
+ );
10
+
11
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (3, '003_session_embeddings');
@@ -0,0 +1,46 @@
1
+ -- Migration 004: facts + fact_embeddings.
2
+ --
3
+ -- Facts are the agent-recall projection of session content: normalized
4
+ -- (subject, predicate, value) triples derived from session classifier output,
5
+ -- supersedence-aware via the tombstone pointer `superseded_by`. See
6
+ -- docs/plans/factstore-design.md.
7
+ --
8
+ -- Phase B.1 creates the tables. Writes start in Phase B.2 (classifier prompt
9
+ -- extension). The fact_embeddings vec0 table is created now so Phase B.3 can
10
+ -- light up semantic fact recall without a second migration.
11
+
12
+ CREATE TABLE IF NOT EXISTS facts (
13
+ id TEXT PRIMARY KEY,
14
+ kind TEXT NOT NULL CHECK (kind IN ('decision', 'open', 'attribute')),
15
+ subject TEXT NOT NULL,
16
+ predicate TEXT NOT NULL,
17
+ value TEXT NOT NULL,
18
+ source_session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
19
+ source_quote TEXT,
20
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
21
+ superseded_by TEXT REFERENCES facts(id) ON DELETE SET NULL,
22
+ confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0)
23
+ );
24
+
25
+ -- Hot path: deterministic supersedence collision check on ingest
26
+ -- (subject, predicate) lookups against current rows only.
27
+ CREATE INDEX IF NOT EXISTS idx_facts_subject_predicate_current
28
+ ON facts(subject, predicate)
29
+ WHERE superseded_by IS NULL;
30
+
31
+ -- "What do we know about X?" — subject-only browsing.
32
+ CREATE INDEX IF NOT EXISTS idx_facts_subject_current
33
+ ON facts(subject)
34
+ WHERE superseded_by IS NULL;
35
+
36
+ -- Reverse lookup: which facts came from this session?
37
+ CREATE INDEX IF NOT EXISTS idx_facts_session
38
+ ON facts(source_session_id);
39
+
40
+ -- Semantic recall index (Phase B.3). 768 dims matches nomic-embed-text.
41
+ CREATE VIRTUAL TABLE IF NOT EXISTS fact_embeddings USING vec0(
42
+ fact_id TEXT PRIMARY KEY,
43
+ embedding float[768]
44
+ );
45
+
46
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (4, '004_facts');
@@ -0,0 +1,31 @@
1
+ -- Migration 005: sources registry.
2
+ --
3
+ -- Each row represents one transcript source the daemon should scan. The
4
+ -- three hardcoded adapters (claude-code / hermes / pi) become seeded rows
5
+ -- pointing at the same parse logic; future custom JSONL or webhook sources
6
+ -- live alongside them.
7
+ --
8
+ -- Parse config is a JSON blob whose shape depends on `kind`:
9
+ -- - "claude-code" / "hermes" / "pi": preset adapters. parse_config is
10
+ -- reserved but unused — paths come from path_or_url.
11
+ -- - "jsonl-generic": { sessionIdField, textField, startedAtField,
12
+ -- roleField, runtimeLabel, ... }
13
+ -- - "webhook": parse_config is empty; ingest is push-based.
14
+ --
15
+ -- See docs/plans/desktop-product.md (Phase 0).
16
+
17
+ CREATE TABLE IF NOT EXISTS sources (
18
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
19
+ kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'pi', 'jsonl-generic', 'webhook')),
20
+ name TEXT NOT NULL UNIQUE,
21
+ path_or_url TEXT,
22
+ runtime_label TEXT NOT NULL,
23
+ parse_config TEXT NOT NULL DEFAULT '{}',
24
+ enabled INTEGER NOT NULL DEFAULT 1,
25
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
30
+
31
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (5, '005_sources');
@@ -0,0 +1,33 @@
1
+ -- Migration 006: providers registry.
2
+ --
3
+ -- Each row represents one LLM endpoint configured for classification or
4
+ -- (future) embedding. The classifier reads this table to choose a
5
+ -- provider/model at boot; the UI mutates it through /api/providers.
6
+ --
7
+ -- API key storage. v0 stores keys in the api_key column. This is fine
8
+ -- because the SQLite file already contains the user's transcripts —
9
+ -- anyone with disk access already has everything sensitive. Phase 2
10
+ -- (Tauri shell) migrates keys to the OS keychain and replaces the column
11
+ -- with a keychain reference. The API shape stays the same.
12
+ --
13
+ -- `kind` is the structural family (openai-compatible, ollama,
14
+ -- anthropic-native, deepseek). The classifier+model UI uses it to decide
15
+ -- which client class to instantiate.
16
+ --
17
+ -- See docs/plans/desktop-product.md (Phase 0 task 3).
18
+
19
+ CREATE TABLE IF NOT EXISTS providers (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ kind TEXT NOT NULL CHECK (kind IN ('deepseek', 'ollama', 'openai', 'anthropic', 'openrouter', 'openai-compatible')),
22
+ name TEXT NOT NULL UNIQUE,
23
+ base_url TEXT,
24
+ api_key TEXT,
25
+ default_model TEXT,
26
+ enabled INTEGER NOT NULL DEFAULT 1,
27
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
28
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
29
+ );
30
+
31
+ CREATE INDEX IF NOT EXISTS idx_providers_enabled ON providers(enabled) WHERE enabled = 1;
32
+
33
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (6, '006_providers');
@@ -0,0 +1,17 @@
1
+ -- Migration 007: webhook token on sources.
2
+ --
3
+ -- Webhook sources need a bearer token so external tools (anything that
4
+ -- pushes sessions via POST /api/ingest) can authenticate. The token is
5
+ -- stored alongside the source row so one webhook = one token = one
6
+ -- provenance label.
7
+ --
8
+ -- Storage policy mirrors providers.api_key: column in the canonical
9
+ -- SQLite for v0 (the DB file already holds the user's transcripts,
10
+ -- adding a token doesn't change the threat model). Phase 2 migrates to
11
+ -- OS keychain without changing the API shape.
12
+ --
13
+ -- See docs/plans/desktop-product.md (Phase 0 task 5).
14
+
15
+ ALTER TABLE sources ADD COLUMN token TEXT;
16
+
17
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (7, '007_source_tokens');
@@ -0,0 +1,9 @@
1
+ -- One-time safety rebuild of the sessions_fts external-content FTS5 index.
2
+ -- The virtual table and its sync triggers (sessions_ai / sessions_au /
3
+ -- sessions_ad) were declared in migration 000 and have fired on every write
4
+ -- since, so the index is normally already in sync. This rebuild guarantees
5
+ -- the index matches every existing sessions row before the recall path
6
+ -- starts depending on FTS5 for keyword search. Safe and idempotent.
7
+ INSERT INTO sessions_fts(sessions_fts) VALUES('rebuild');
8
+
9
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (8, 'fts_rebuild');
@@ -0,0 +1,46 @@
1
+ -- Migration 009: chunk + max-pool semantic index.
2
+ --
3
+ -- Replaces session_embeddings (one vector per session, truncated at
4
+ -- MAX_EMBED_CHARS=8000) with per-chunk vectors. Recall-time score is
5
+ -- max cosine across chunks per session.
6
+ --
7
+ -- The 2026-05-25 LongMemEval-S baseline showed 98% of gold sessions
8
+ -- exceed 8000 chars and were silently truncated. Raising the per-call
9
+ -- cap (#172) hit Ollama 500s on >50% of long inputs. Chunking sidesteps
10
+ -- both: each chunk is well under the Ollama failure cliff, and the full
11
+ -- body becomes searchable. Expected lift: semantic R@5 87.2 → >92,
12
+ -- hybrid R@5 94.6 → >96.
13
+ --
14
+ -- Schema choices:
15
+ -- * Auxiliary columns (+session_id, +chunk_idx) so KNN queries return
16
+ -- session attribution without a join.
17
+ -- * Separate session_chunk_map keyed on session_id supports
18
+ -- `DELETE FROM session_embedding_chunks WHERE chunk_id IN
19
+ -- (SELECT chunk_id FROM session_chunk_map WHERE session_id = ?)`
20
+ -- since vec0 has no documented filtering on aux columns.
21
+ --
22
+ -- session_embeddings (single-vector) is intentionally left in place:
23
+ -- * keeps rollback trivial (revert recall code, old vectors still there)
24
+ -- * avoids forcing a multi-hour re-embed at deploy time; backfill
25
+ -- populates chunks asynchronously
26
+ -- * a future cleanup migration drops it once chunks are validated
27
+
28
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_embedding_chunks USING vec0(
29
+ chunk_id INTEGER PRIMARY KEY,
30
+ embedding float[768],
31
+ +session_id TEXT,
32
+ +chunk_idx INTEGER
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS session_chunk_map (
36
+ chunk_id INTEGER PRIMARY KEY,
37
+ session_id TEXT NOT NULL,
38
+ chunk_idx INTEGER NOT NULL,
39
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
40
+ );
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_session_chunk_map_session
43
+ ON session_chunk_map(session_id);
44
+
45
+ INSERT OR IGNORE INTO schema_migrations (version, name)
46
+ VALUES (9, '009_session_embedding_chunks');
@@ -0,0 +1,30 @@
1
+ -- Migration 010: add 'opencode' to the sources.kind CHECK constraint.
2
+ --
3
+ -- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
4
+ -- Standard approach: rename → recreate → copy → drop old.
5
+
6
+ PRAGMA foreign_keys = OFF;
7
+
8
+ CREATE TABLE sources_new (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
11
+ name TEXT NOT NULL UNIQUE,
12
+ path_or_url TEXT,
13
+ runtime_label TEXT NOT NULL,
14
+ parse_config TEXT NOT NULL DEFAULT '{}',
15
+ enabled INTEGER NOT NULL DEFAULT 1,
16
+ token TEXT,
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
19
+ );
20
+
21
+ INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
22
+
23
+ DROP TABLE sources;
24
+ ALTER TABLE sources_new RENAME TO sources;
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
27
+
28
+ PRAGMA foreign_keys = ON;
29
+
30
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (10, '010_sources_opencode');
@@ -0,0 +1,30 @@
1
+ -- Migration 011: add 'hermes-agent' to the sources.kind CHECK constraint.
2
+ --
3
+ -- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
4
+ -- Standard approach: rename → recreate → copy → drop old.
5
+
6
+ PRAGMA foreign_keys = OFF;
7
+
8
+ CREATE TABLE sources_new (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'hermes-agent', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
11
+ name TEXT NOT NULL UNIQUE,
12
+ path_or_url TEXT,
13
+ runtime_label TEXT NOT NULL,
14
+ parse_config TEXT NOT NULL DEFAULT '{}',
15
+ enabled INTEGER NOT NULL DEFAULT 1,
16
+ token TEXT,
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
19
+ );
20
+
21
+ INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
22
+
23
+ DROP TABLE sources;
24
+ ALTER TABLE sources_new RENAME TO sources;
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
27
+
28
+ PRAGMA foreign_keys = ON;
29
+
30
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (11, '011_sources_hermes_agent');
@@ -0,0 +1,30 @@
1
+ -- Migration 012: add 'aider' to the sources.kind CHECK constraint.
2
+ --
3
+ -- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
4
+ -- Standard approach: rename → recreate → copy → drop old.
5
+
6
+ PRAGMA foreign_keys = OFF;
7
+
8
+ CREATE TABLE sources_new (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'hermes-agent', 'aider', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
11
+ name TEXT NOT NULL UNIQUE,
12
+ path_or_url TEXT,
13
+ runtime_label TEXT NOT NULL,
14
+ parse_config TEXT NOT NULL DEFAULT '{}',
15
+ enabled INTEGER NOT NULL DEFAULT 1,
16
+ token TEXT,
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
19
+ );
20
+
21
+ INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
22
+
23
+ DROP TABLE sources;
24
+ ALTER TABLE sources_new RENAME TO sources;
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
27
+
28
+ PRAGMA foreign_keys = ON;
29
+
30
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (12, '012_sources_aider');
@@ -0,0 +1,12 @@
1
+ -- Migration 013: add failure_count to adapter_state.
2
+ --
3
+ -- Tracks consecutive classify/storage failures per source file. The scheduler
4
+ -- skips files whose failure_count has reached the backoff ceiling (default 3)
5
+ -- and whose file_size has not changed since the last attempt. When the file
6
+ -- grows (new content appended), failure_count resets to 0 and the file is
7
+ -- retried. This prevents a single permanently-broken or over-large transcript
8
+ -- from flooding daemon-err.log on every 30-minute tick.
9
+
10
+ ALTER TABLE adapter_state ADD COLUMN failure_count INTEGER NOT NULL DEFAULT 0;
11
+
12
+ INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (13, '013_adapter_state_failure_count');