nlm-memory 0.5.0 → 0.5.1

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 (247) hide show
  1. package/README.md +72 -34
  2. package/dist/cli/nlm.js +2 -1
  3. package/dist/cli/nlm.js.map +1 -1
  4. package/dist/http/app.js +2 -1
  5. package/dist/http/app.js.map +1 -1
  6. package/dist/mcp/server.js +20 -1
  7. package/dist/mcp/server.js.map +1 -1
  8. package/dist/ui/assets/{index-C8cpwbYJ.css → index-Beo8psd-.css} +1 -1
  9. package/dist/ui/assets/{index-CB50QnL-.js → index-CSPTTeeM.js} +8 -8
  10. package/dist/ui/index.html +2 -2
  11. package/package.json +26 -1
  12. package/.agents/plugins/marketplace.json +0 -20
  13. package/.github/workflows/ci.yml +0 -30
  14. package/docs/methodology/re-derivation-rate.md +0 -112
  15. package/docs/methodology/useful-hit-rate.md +0 -79
  16. package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
  17. package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
  18. package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
  19. package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
  20. package/docs/plans/desktop-product.md +0 -69
  21. package/docs/plans/factstore-design.md +0 -236
  22. package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1575
  23. package/logs/CHANGELOG/CHANGELOG.md +0 -209
  24. package/migrations/000_initial_schema.sql +0 -174
  25. package/migrations/001_entity_type_rename.sql +0 -17
  26. package/migrations/002_adapter_state_extend.sql +0 -12
  27. package/migrations/003_session_embeddings.sql +0 -11
  28. package/migrations/004_facts.sql +0 -46
  29. package/migrations/005_sources.sql +0 -31
  30. package/migrations/006_providers.sql +0 -33
  31. package/migrations/007_source_tokens.sql +0 -17
  32. package/migrations/008_fts_rebuild.sql +0 -9
  33. package/migrations/009_session_embedding_chunks.sql +0 -46
  34. package/migrations/010_sources_opencode.sql +0 -30
  35. package/migrations/011_sources_hermes_agent.sql +0 -30
  36. package/migrations/012_sources_aider.sql +0 -30
  37. package/migrations/013_adapter_state_failure_count.sql +0 -12
  38. package/migrations/014_sources_cursor.sql +0 -30
  39. package/migrations/015_sources_windsurf.sql +0 -30
  40. package/plugin-hermes-agent/README.md +0 -49
  41. package/plugin-hermes-agent/__init__.py +0 -75
  42. package/plugin-hermes-agent/plugin.yaml +0 -15
  43. package/scripts/backfill-citations.mjs +0 -0
  44. package/scripts/build-codex-plugin.mjs +0 -61
  45. package/scripts/deepseek-probe.mjs +0 -67
  46. package/scripts/extract-triples.mjs +0 -207
  47. package/scripts/longmemeval/embedding-cache.ts +0 -77
  48. package/scripts/longmemeval/fetch-dataset.sh +0 -25
  49. package/scripts/longmemeval/run-harness.ts +0 -315
  50. package/scripts/longmemeval/scorer.ts +0 -99
  51. package/scripts/longmemeval/tsconfig.json +0 -9
  52. package/scripts/longmemeval/types.ts +0 -35
  53. package/scripts/nlm-daily-digest.py +0 -239
  54. package/scripts/nlm-daily-digest.sh +0 -28
  55. package/src/cli/classify-parity.ts +0 -257
  56. package/src/cli/launchctl-helpers.ts +0 -49
  57. package/src/cli/nlm.ts +0 -1078
  58. package/src/core/actions/actions-log.ts +0 -118
  59. package/src/core/actions/overlay.ts +0 -117
  60. package/src/core/adapters/aider.ts +0 -205
  61. package/src/core/adapters/claude-code.ts +0 -293
  62. package/src/core/adapters/common.ts +0 -54
  63. package/src/core/adapters/cursor.ts +0 -486
  64. package/src/core/adapters/from-source.ts +0 -67
  65. package/src/core/adapters/hermes-agent.ts +0 -240
  66. package/src/core/adapters/hermes.ts +0 -277
  67. package/src/core/adapters/jsonl-generic.ts +0 -208
  68. package/src/core/adapters/opencode.ts +0 -281
  69. package/src/core/adapters/pi.ts +0 -264
  70. package/src/core/adapters/windsurf.ts +0 -386
  71. package/src/core/classifier/prompt.ts +0 -200
  72. package/src/core/dataset/build-dataset.ts +0 -463
  73. package/src/core/embedding/chunk-body.ts +0 -76
  74. package/src/core/embedding/embed-backfill.ts +0 -210
  75. package/src/core/embedding/embed-normalize.ts +0 -135
  76. package/src/core/facts/backfill-facts.ts +0 -254
  77. package/src/core/facts/extract-facts.ts +0 -50
  78. package/src/core/hook/citation-detect.ts +0 -124
  79. package/src/core/hook/cite-memo.ts +0 -68
  80. package/src/core/hook/claude-settings.ts +0 -187
  81. package/src/core/hook/gate.ts +0 -25
  82. package/src/core/hook/hook-log.ts +0 -41
  83. package/src/core/hook/memo-sweep.ts +0 -164
  84. package/src/core/hook/memo.ts +0 -67
  85. package/src/core/hook/pointer-block.ts +0 -26
  86. package/src/core/hook/select.ts +0 -32
  87. package/src/core/hook/transcript.ts +0 -121
  88. package/src/core/ingest/ingest-session.ts +0 -111
  89. package/src/core/providers/provider-models.ts +0 -100
  90. package/src/core/providers/provider-registry.ts +0 -196
  91. package/src/core/recall/citation-log.ts +0 -108
  92. package/src/core/recall/filter.ts +0 -27
  93. package/src/core/recall/index.ts +0 -6
  94. package/src/core/recall/match-fields.ts +0 -40
  95. package/src/core/recall/query-log.ts +0 -149
  96. package/src/core/recall/query-shape.ts +0 -66
  97. package/src/core/recall/recall-service.ts +0 -320
  98. package/src/core/recall/recent-log.ts +0 -59
  99. package/src/core/recall/tokenize.ts +0 -18
  100. package/src/core/recall/useful-scan.ts +0 -336
  101. package/src/core/recall-facts/fact-query-log.ts +0 -150
  102. package/src/core/recall-facts/fact-recall-service.ts +0 -327
  103. package/src/core/scheduler/scan-once.ts +0 -142
  104. package/src/core/scheduler/scheduler.ts +0 -225
  105. package/src/core/sources/source-registry.ts +0 -278
  106. package/src/core/storage/db-restore.ts +0 -133
  107. package/src/core/storage/live-status.ts +0 -45
  108. package/src/core/storage/migrate.ts +0 -72
  109. package/src/core/storage/sqlite-fact-store.ts +0 -304
  110. package/src/core/storage/sqlite-session-store.ts +0 -810
  111. package/src/hook/hook-auth.ts +0 -18
  112. package/src/hook/prompt-recall-hook.ts +0 -180
  113. package/src/hook/session-end-hook.ts +0 -81
  114. package/src/hook/session-start-hook.ts +0 -168
  115. package/src/hook/stop-hook.ts +0 -239
  116. package/src/http/app.ts +0 -1215
  117. package/src/install/claude-code.ts +0 -128
  118. package/src/install/codex.ts +0 -367
  119. package/src/install/cursor.ts +0 -68
  120. package/src/install/hermes-agent.ts +0 -76
  121. package/src/install/hermes.ts +0 -78
  122. package/src/install/nlm-dir-perms.ts +0 -55
  123. package/src/install/ollama.ts +0 -284
  124. package/src/install/setup.ts +0 -489
  125. package/src/install/windsurf.ts +0 -68
  126. package/src/llm/classifier-box.ts +0 -64
  127. package/src/llm/deepseek-client.ts +0 -150
  128. package/src/llm/env-autoload.ts +0 -55
  129. package/src/llm/ollama-client.ts +0 -189
  130. package/src/mcp/server.ts +0 -534
  131. package/src/ports/fact-store.ts +0 -102
  132. package/src/ports/llm-client.ts +0 -52
  133. package/src/ports/logger.ts +0 -16
  134. package/src/ports/session-store.ts +0 -45
  135. package/src/ports/transcript-adapter.ts +0 -55
  136. package/src/shared/types.ts +0 -149
  137. package/src/ui/App.tsx +0 -58
  138. package/src/ui/components/PromoteOpenButton.tsx +0 -65
  139. package/src/ui/components/SessionDrawer.tsx +0 -199
  140. package/src/ui/components/SideNav.tsx +0 -162
  141. package/src/ui/components/Skeleton.tsx +0 -107
  142. package/src/ui/index.html +0 -13
  143. package/src/ui/lib/actions.ts +0 -30
  144. package/src/ui/lib/api.ts +0 -92
  145. package/src/ui/lib/dataset.ts +0 -141
  146. package/src/ui/lib/registries.ts +0 -155
  147. package/src/ui/lib/view-settings.ts +0 -41
  148. package/src/ui/main.tsx +0 -15
  149. package/src/ui/pages/Live.tsx +0 -229
  150. package/src/ui/pages/Pulse.tsx +0 -415
  151. package/src/ui/pages/Recall.tsx +0 -190
  152. package/src/ui/pages/River.tsx +0 -354
  153. package/src/ui/pages/Search.tsx +0 -386
  154. package/src/ui/pages/Stub.tsx +0 -9
  155. package/src/ui/pages/Thread.tsx +0 -473
  156. package/src/ui/pages/settings/Classifier.tsx +0 -227
  157. package/src/ui/pages/settings/Data.tsx +0 -190
  158. package/src/ui/pages/settings/Index.tsx +0 -65
  159. package/src/ui/pages/settings/Labels.tsx +0 -224
  160. package/src/ui/pages/settings/Providers.tsx +0 -305
  161. package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
  162. package/src/ui/pages/settings/Sources.tsx +0 -326
  163. package/src/ui/pages/settings/Views.tsx +0 -96
  164. package/src/ui/styles.css +0 -1890
  165. package/src/ui/tsconfig.json +0 -21
  166. package/src/ui/vite.config.ts +0 -19
  167. package/tests/fixtures/claude_code/short_session.jsonl +0 -2
  168. package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
  169. package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
  170. package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
  171. package/tests/fixtures/facts.ts +0 -17
  172. package/tests/fixtures/golden-corpus.ts +0 -85
  173. package/tests/fixtures/hermes/paired_request_dump.json +0 -24
  174. package/tests/fixtures/hermes/paired_session.json +0 -23
  175. package/tests/fixtures/hermes/request_dump.json +0 -28
  176. package/tests/fixtures/hermes/session_iso.json +0 -38
  177. package/tests/fixtures/hermes/session_unix.json +0 -38
  178. package/tests/fixtures/hermes/system_only.json +0 -18
  179. package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
  180. package/tests/fixtures/pi/short-successful.jsonl +0 -5
  181. package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
  182. package/tests/fixtures/sessions.ts +0 -22
  183. package/tests/integration/backfill-facts.test.ts +0 -362
  184. package/tests/integration/citation-explicit.test.ts +0 -111
  185. package/tests/integration/cite-event.test.ts +0 -169
  186. package/tests/integration/cite-memo.test.ts +0 -87
  187. package/tests/integration/db-restore.test.ts +0 -153
  188. package/tests/integration/embed-backfill.test.ts +0 -176
  189. package/tests/integration/fact-supersedence.test.ts +0 -313
  190. package/tests/integration/fts-index.test.ts +0 -60
  191. package/tests/integration/getbyids-sqlite.test.ts +0 -100
  192. package/tests/integration/hermes-agent-hooks.test.ts +0 -248
  193. package/tests/integration/hook-claude-settings.test.ts +0 -218
  194. package/tests/integration/hook-log.test.ts +0 -54
  195. package/tests/integration/hook-memo.test.ts +0 -68
  196. package/tests/integration/hook-pre-compact.test.ts +0 -105
  197. package/tests/integration/hook-subagent-start.test.ts +0 -102
  198. package/tests/integration/http.test.ts +0 -401
  199. package/tests/integration/keyword-search-fts.test.ts +0 -66
  200. package/tests/integration/mcp-recall-logging.test.ts +0 -88
  201. package/tests/integration/mcp.test.ts +0 -260
  202. package/tests/integration/memo-sweep.test.ts +0 -91
  203. package/tests/integration/prompt-recall-hook.test.ts +0 -88
  204. package/tests/integration/provider-registry.test.ts +0 -107
  205. package/tests/integration/recall-golden.test.ts +0 -59
  206. package/tests/integration/recall-sqlite.test.ts +0 -169
  207. package/tests/integration/scheduler.test.ts +0 -391
  208. package/tests/integration/session-end-hook.test.ts +0 -48
  209. package/tests/integration/session-start-hook.test.ts +0 -126
  210. package/tests/integration/source-registry.test.ts +0 -122
  211. package/tests/integration/sqlite-fact-store.test.ts +0 -346
  212. package/tests/integration/stop-hook.test.ts +0 -560
  213. package/tests/integration/wal-checkpoint.test.ts +0 -49
  214. package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
  215. package/tests/unit/core/adapters/aider.test.ts +0 -230
  216. package/tests/unit/core/adapters/claude-code.test.ts +0 -118
  217. package/tests/unit/core/adapters/cursor.test.ts +0 -485
  218. package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
  219. package/tests/unit/core/adapters/hermes.test.ts +0 -81
  220. package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
  221. package/tests/unit/core/adapters/opencode.test.ts +0 -354
  222. package/tests/unit/core/adapters/pi.test.ts +0 -110
  223. package/tests/unit/core/adapters/windsurf.test.ts +0 -416
  224. package/tests/unit/core/classifier/prompt.test.ts +0 -126
  225. package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
  226. package/tests/unit/core/facts/extract-facts.test.ts +0 -117
  227. package/tests/unit/core/filter.test.ts +0 -40
  228. package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
  229. package/tests/unit/core/hook/citation-detect.test.ts +0 -124
  230. package/tests/unit/core/hook/gate.test.ts +0 -29
  231. package/tests/unit/core/hook/pointer-block.test.ts +0 -22
  232. package/tests/unit/core/hook/select.test.ts +0 -66
  233. package/tests/unit/core/match-fields.test.ts +0 -39
  234. package/tests/unit/core/mcp-cite-session.test.ts +0 -51
  235. package/tests/unit/core/providers/provider-models.test.ts +0 -101
  236. package/tests/unit/core/query-shape.test.ts +0 -92
  237. package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
  238. package/tests/unit/core/recall-service.test.ts +0 -200
  239. package/tests/unit/core/storage/live-status.test.ts +0 -54
  240. package/tests/unit/core/tokenize.test.ts +0 -32
  241. package/tests/unit/core/useful-scan.test.ts +0 -537
  242. package/tests/unit/llm/embed.test.ts +0 -93
  243. package/tests/unit/llm/ollama-client.test.ts +0 -124
  244. package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
  245. package/tsconfig.json +0 -31
  246. package/tsconfig.test.json +0 -11
  247. package/vitest.config.ts +0 -22
@@ -1,415 +0,0 @@
1
- import { useEffect, useMemo, useState } from "react";
2
- import { Link, useNavigate } from "react-router-dom";
3
- import { useDataset, relativeAge } from "../lib/dataset.js";
4
- import type { DatasetAlert, DatasetEntity, DatasetRuntime, DatasetSession } from "../lib/dataset.js";
5
- import { postAction } from "../lib/actions.js";
6
- import { SessionDrawer } from "../components/SessionDrawer.js";
7
- import { PromoteOpenButton } from "../components/PromoteOpenButton.js";
8
- import { PulseSkeleton } from "../components/Skeleton.js";
9
-
10
- type SeverityFilter = "all" | "high" | "medium";
11
- type AlertSort = "oldest" | "recent";
12
-
13
- export function PulsePage() {
14
- const { data, loading, error, refetch } = useDataset();
15
- const [severity, setSeverity] = useState<SeverityFilter>("all");
16
- const [sort, setSort] = useState<AlertSort>("oldest");
17
- const [detailId, setDetailId] = useState<string | null>(null);
18
- const [sessionId, setSessionId] = useState<string | null>(null);
19
-
20
- const filteredAlerts = useMemo(() => {
21
- if (!data) return [];
22
- const filtered = severity === "all" ? data.alerts : data.alerts.filter((a) => a.severity === severity);
23
- return [...filtered].sort((a, b) => (sort === "oldest" ? b.age_days - a.age_days : a.age_days - b.age_days));
24
- }, [data, severity, sort]);
25
-
26
- const dismissAlert = async (alertId: string) => {
27
- await postAction({ kind: "dismiss", subject_type: "alert", subject_id: alertId });
28
- await refetch();
29
- };
30
- const snoozeAlert = async (alertId: string, days: number) => {
31
- const until = new Date(Date.now() + days * 86_400_000).toISOString();
32
- await postAction({
33
- kind: "snooze",
34
- subject_type: "alert",
35
- subject_id: alertId,
36
- payload: { snoozed_until: until },
37
- });
38
- await refetch();
39
- };
40
-
41
- const recent = useMemo(() => {
42
- if (!data) return [];
43
- return [...data.sessions]
44
- .sort((a, b) => (b.started_at ?? "").localeCompare(a.started_at ?? ""))
45
- .slice(0, 20);
46
- }, [data]);
47
-
48
- if (loading && !data) return <PulseSkeleton />;
49
- if (error && !data) return <div className="page-pad"><div className="muted error">{error}</div></div>;
50
- if (!data) return null;
51
-
52
- const detailAlert = detailId ? data.alerts.find((a) => a.id === detailId) ?? null : null;
53
-
54
- return (
55
- <div className="page-pad">
56
- <div className="kpi-row">
57
- <Kpi label="This week" value={data.metrics.this_week} hint={`${data.metrics.last_week} last week`} />
58
- <Kpi label="Sessions" value={data.meta.sessions_total} hint="total" />
59
- <Kpi label="Entities" value={data.meta.entities_total} hint="catalogued" />
60
- <Kpi label="Decisions" value={data.metrics.closed_decisions} hint="non-superseded" />
61
- <KpiSparkline values={data.metrics.sparkline} />
62
- </div>
63
-
64
- <div className="pulse-grid">
65
- <section className="card pulse-area-coherence">
66
- <header className="card-head"><h3>Coherence</h3></header>
67
- <CoherenceBars metrics={data.metrics} />
68
- </section>
69
-
70
- <section className="card pulse-area-runtimes">
71
- <header className="card-head"><h3>Runtimes</h3></header>
72
- <RuntimesPanel runtimes={data.runtimes} />
73
- </section>
74
-
75
- <section className="card pulse-scroll-card pulse-area-recent">
76
- <header className="card-head"><h3>Recent sessions</h3></header>
77
- <div className="pulse-scroll-body">
78
- <ul className="session-list">
79
- {recent.map((s) => (
80
- <li
81
- key={s.id}
82
- className="session-row clickable"
83
- onClick={() => setSessionId(s.id)}
84
- role="button"
85
- tabIndex={0}
86
- onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setSessionId(s.id); } }}
87
- >
88
- <span className={`chip-inline status-${s.status}`}>{s.status}</span>
89
- <span className="session-label">{s.label}</span>
90
- <span className="session-meta">{relativeAge(s.started_at)} · {s.entities.slice(0, 3).join(", ")}{s.entities.length > 3 ? ` +${s.entities.length - 3}` : ""}</span>
91
- </li>
92
- ))}
93
- </ul>
94
- </div>
95
- </section>
96
-
97
- <section className="card pulse-scroll-card pulse-area-stale">
98
- <header className="card-head card-head-stack">
99
- <div className="card-head-row">
100
- <h3>Stale alerts</h3>
101
- <span className="muted small">
102
- {filteredAlerts.length}{filteredAlerts.length !== data.alerts.length ? ` / ${data.alerts.length}` : ""}
103
- </span>
104
- </div>
105
- <div className="card-filters">
106
- <div className="filter-group" role="group" aria-label="Severity">
107
- {(["all", "high", "medium"] as const).map((s) => (
108
- <button
109
- key={s}
110
- type="button"
111
- className={`chip${severity === s ? " active" : ""}`}
112
- data-severity={s === "all" ? undefined : s}
113
- onClick={() => setSeverity(s)}
114
- >{s}</button>
115
- ))}
116
- </div>
117
- <div className="filter-group" role="group" aria-label="Sort">
118
- <button type="button" className={`chip${sort === "oldest" ? " active" : ""}`} onClick={() => setSort("oldest")}>oldest</button>
119
- <button type="button" className={`chip${sort === "recent" ? " active" : ""}`} onClick={() => setSort("recent")}>recent</button>
120
- </div>
121
- </div>
122
- </header>
123
- <div className="pulse-scroll-body">
124
- <ul className="alert-list">
125
- {filteredAlerts.map((a) => (
126
- <li
127
- key={a.id}
128
- className="alert-row clickable"
129
- onClick={() => setDetailId(a.id)}
130
- role="button"
131
- tabIndex={0}
132
- onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setDetailId(a.id); } }}
133
- >
134
- <span className={`chip-inline severity-${a.severity}`}>{a.severity}</span>
135
- <span className="alert-entity">{a.entity}</span>
136
- <span className="alert-summary">{a.summary}</span>
137
- <div className="alert-actions" onClick={(e) => e.stopPropagation()}>
138
- <button type="button" className="chip" onClick={() => void snoozeAlert(a.id, 7)}>snooze 7d</button>
139
- <button type="button" className="chip" onClick={() => void dismissAlert(a.id)}>dismiss</button>
140
- </div>
141
- </li>
142
- ))}
143
- {filteredAlerts.length === 0 && (
144
- <li className="muted alert-row-empty">
145
- {data.alerts.length === 0 ? "No stale alerts." : "No alerts match the current filters."}
146
- </li>
147
- )}
148
- </ul>
149
- </div>
150
- </section>
151
- </div>
152
-
153
- {detailAlert && (
154
- <AlertDrawer
155
- alert={detailAlert}
156
- entity={data.entities.find((e) => e.canonical === detailAlert.entity) ?? null}
157
- entityColor={data.entity_colors[detailAlert.entity] ?? "#666"}
158
- sessions={data.sessions}
159
- onClose={() => setDetailId(null)}
160
- onDismiss={async () => { await dismissAlert(detailAlert.id); setDetailId(null); }}
161
- onSnooze={async (days) => { await snoozeAlert(detailAlert.id, days); setDetailId(null); }}
162
- onPromoted={refetch}
163
- />
164
- )}
165
-
166
- {sessionId && (
167
- <SessionDrawer
168
- sessionId={sessionId}
169
- onClose={() => setSessionId(null)}
170
- entityColor={(() => {
171
- const s = data.sessions.find((x) => x.id === sessionId);
172
- const e = s?.entities[0];
173
- return e ? data.entity_colors[e] : undefined;
174
- })()}
175
- />
176
- )}
177
- </div>
178
- );
179
- }
180
-
181
- interface AlertDrawerProps {
182
- alert: DatasetAlert;
183
- entity: DatasetEntity | null;
184
- entityColor: string;
185
- sessions: DatasetSession[];
186
- onClose: () => void;
187
- onDismiss: () => Promise<void> | void;
188
- onSnooze: (days: number) => Promise<void> | void;
189
- onPromoted: () => Promise<void> | void;
190
- }
191
-
192
- const DRAWER_PAGE_SIZES = [10, 25, 50] as const;
193
-
194
- function AlertDrawer({ alert, entity, entityColor, sessions, onClose, onDismiss, onSnooze, onPromoted }: AlertDrawerProps) {
195
- const navigate = useNavigate();
196
- const [pageSize, setPageSize] = useState<number>(10);
197
- const [page, setPage] = useState(0);
198
-
199
- const related = useMemo(() => {
200
- return sessions
201
- .filter((s) => s.entities.includes(alert.entity))
202
- .sort((a, b) => (b.started_at ?? "").localeCompare(a.started_at ?? ""));
203
- }, [sessions, alert.entity]);
204
-
205
- // Reset page when alert changes (drawer opened for a different entity) or page size changes.
206
- useEffect(() => { setPage(0); }, [alert.id, pageSize]);
207
-
208
- const pageCount = Math.max(1, Math.ceil(related.length / pageSize));
209
- const currentPage = Math.min(page, pageCount - 1);
210
- const start = currentPage * pageSize;
211
- const sessionSlice = related.slice(start, start + pageSize);
212
-
213
- const openQuestions = useMemo(
214
- () => related.flatMap((s) => s.open_questions.map((q) => ({ id: q.id, text: q.text, sid: s.id, when: s.started_at }))),
215
- [related],
216
- );
217
-
218
- const decisions = useMemo(
219
- () => related.flatMap((s) => s.decisions.map((d) => ({ text: d, sid: s.id, when: s.started_at }))),
220
- [related],
221
- );
222
-
223
- useEffect(() => {
224
- const onEsc = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
225
- window.addEventListener("keydown", onEsc);
226
- return () => window.removeEventListener("keydown", onEsc);
227
- }, [onClose]);
228
-
229
- return (
230
- <>
231
- <div className="drawer-backdrop" onClick={onClose} />
232
- <aside className="session-drawer" role="dialog" aria-modal="true" aria-label={`Alert detail: ${alert.entity}`}>
233
- <header className="drawer-head">
234
- <span className="dot lg" style={{ background: entityColor }} />
235
- <h3 className="drawer-title">{alert.entity}</h3>
236
- <span className={`chip-inline severity-${alert.severity}`}>{alert.severity}</span>
237
- <button type="button" className="drawer-close" onClick={onClose} aria-label="Close">×</button>
238
- </header>
239
- <div className="drawer-body">
240
- <p className="drawer-paragraph">{alert.summary}</p>
241
-
242
- <dl className="kv-list">
243
- <dt className="kv-label">Last touch</dt>
244
- <dd className="kv-value">{alert.age_days} day{alert.age_days === 1 ? "" : "s"} ago</dd>
245
- <dt className="kv-label">Last session</dt>
246
- <dd className="kv-value mono small">{alert.last_touch_at ?? "—"}</dd>
247
- {entity && (
248
- <>
249
- <dt className="kv-label">Entity type</dt>
250
- <dd className="kv-value">{entity.type}</dd>
251
- <dt className="kv-label">Total sessions</dt>
252
- <dd className="kv-value mono">{entity.session_count}</dd>
253
- </>
254
- )}
255
- </dl>
256
-
257
- <div className="drawer-actions">
258
- <button type="button" className="btn btn-accent" onClick={() => navigate(`/thread?entity=${encodeURIComponent(alert.entity)}`)}>
259
- Open thread
260
- </button>
261
- <button type="button" className="btn" onClick={() => void onSnooze(7)}>Snooze 7d</button>
262
- <button type="button" className="btn" onClick={() => void onSnooze(30)}>Snooze 30d</button>
263
- <button type="button" className="btn btn-danger" onClick={() => void onDismiss()}>Dismiss</button>
264
- </div>
265
-
266
- {openQuestions.length > 0 && (
267
- <>
268
- <h4 className="drawer-section">Open questions ({openQuestions.length})</h4>
269
- <ul className="drawer-list">
270
- {openQuestions.slice(0, 12).map((q, i) => (
271
- <li key={`${q.id}-${i}`} className="marker-row-promotable">
272
- <span className="live-tag" data-kind="open">open</span>
273
- <span className="marker-text">{q.text}</span>
274
- <PromoteOpenButton openId={q.id} defaultText={q.text} onPromoted={onPromoted} />
275
- <span className="muted small">{relativeAge(q.when)}</span>
276
- </li>
277
- ))}
278
- </ul>
279
- {openQuestions.length > 12 && <p className="muted small">Showing first 12 of {openQuestions.length}.</p>}
280
- </>
281
- )}
282
-
283
- {decisions.length > 0 && (
284
- <>
285
- <h4 className="drawer-section">Decisions ({decisions.length})</h4>
286
- <ul className="drawer-list">
287
- {decisions.slice(0, 8).map((d, i) => (
288
- <li key={`${d.sid}-${i}`}>
289
- <span className="live-tag" data-kind="decision">decision</span>
290
- <span className="marker-text">{d.text}</span>
291
- <span className="muted small">{relativeAge(d.when)}</span>
292
- </li>
293
- ))}
294
- </ul>
295
- {decisions.length > 8 && <p className="muted small">Showing first 8 of {decisions.length}.</p>}
296
- </>
297
- )}
298
-
299
- <h4 className="drawer-section">Recent sessions ({related.length})</h4>
300
- <ul className="session-list">
301
- {sessionSlice.map((s) => (
302
- <li key={s.id} className="session-row">
303
- <span className={`chip-inline status-${s.status}`}>{s.status}</span>
304
- <div className="session-row-main">
305
- <Link to={`/thread?entity=${encodeURIComponent(alert.entity)}&session=${encodeURIComponent(s.id)}`} className="session-label">{s.label}</Link>
306
- <span className="session-meta">{s.summary}</span>
307
- </div>
308
- <span className="muted small mono">{relativeAge(s.started_at)}</span>
309
- </li>
310
- ))}
311
- </ul>
312
- {related.length > 0 && (
313
- <div className="pagination pagination-compact">
314
- <div className="page-size">
315
- <label className="form-label">Per page</label>
316
- <select
317
- className="form-input form-input-inline"
318
- value={pageSize}
319
- onChange={(e) => setPageSize(Number.parseInt(e.target.value, 10))}
320
- >
321
- {DRAWER_PAGE_SIZES.map((n) => <option key={n} value={n}>{n}</option>)}
322
- </select>
323
- </div>
324
- <span className="header-spacer" />
325
- <span className="muted small">
326
- {start + 1}–{Math.min(start + pageSize, related.length)} of {related.length}
327
- </span>
328
- <div className="page-nav">
329
- <button type="button" className="chip" disabled={currentPage === 0} onClick={() => setPage(0)}>«</button>
330
- <button type="button" className="chip" disabled={currentPage === 0} onClick={() => setPage((p) => Math.max(0, p - 1))}>‹</button>
331
- <span className="page-indicator mono">{currentPage + 1} / {pageCount}</span>
332
- <button type="button" className="chip" disabled={currentPage >= pageCount - 1} onClick={() => setPage((p) => Math.min(pageCount - 1, p + 1))}>›</button>
333
- <button type="button" className="chip" disabled={currentPage >= pageCount - 1} onClick={() => setPage(pageCount - 1)}>»</button>
334
- </div>
335
- </div>
336
- )}
337
- </div>
338
- </aside>
339
- </>
340
- );
341
- }
342
-
343
- function Kpi({ label, value, hint }: { label: string; value: number; hint?: string }) {
344
- return (
345
- <div className="kpi">
346
- <span className="kpi-label">{label}</span>
347
- <span className="kpi-value">{value.toLocaleString()}</span>
348
- {hint && <span className="kpi-hint">{hint}</span>}
349
- </div>
350
- );
351
- }
352
-
353
- function KpiSparkline({ values }: { values: number[] }) {
354
- const max = Math.max(1, ...values);
355
- return (
356
- <div className="kpi kpi-sparkline">
357
- <span className="kpi-label">Last 7 days</span>
358
- <div className="sparkline">
359
- {values.map((v, i) => (
360
- <span key={i} className="spark-bar" style={{ height: `${(v / max) * 100}%` }} title={`${v} sessions`} />
361
- ))}
362
- </div>
363
- </div>
364
- );
365
- }
366
-
367
- function CoherenceBars({ metrics }: { metrics: { healthy: number; sparse: number; stale: number } }) {
368
- const total = metrics.healthy + metrics.sparse + metrics.stale;
369
- const pct = (v: number) => (total > 0 ? (v / total) * 100 : 0);
370
- return (
371
- <div className="bar-stack">
372
- <Bar tone="active" label="Healthy" value={metrics.healthy} pct={pct(metrics.healthy)} />
373
- <Bar tone="warn" label="Sparse" value={metrics.sparse} pct={pct(metrics.sparse)} />
374
- <Bar tone="danger" label="Stale" value={metrics.stale} pct={pct(metrics.stale)} />
375
- </div>
376
- );
377
- }
378
-
379
- function RuntimesPanel({ runtimes }: { runtimes: DatasetRuntime[] }) {
380
- if (runtimes.length === 0) {
381
- return <div className="muted small" style={{ padding: "8px 12px" }}>No runtime activity yet.</div>;
382
- }
383
- return (
384
- <ul className="runtime-list">
385
- {runtimes.map((r) => (
386
- <li key={r.name} className="runtime-row">
387
- <span className={`runtime-dot runtime-${r.status}`} title={r.status} />
388
- <span className="runtime-name mono">{r.name}</span>
389
- <span className="runtime-counts muted small">
390
- {r.this_week}<span className="runtime-counts-sep">·</span><span className="runtime-counts-prev">{r.last_week} prev</span>
391
- </span>
392
- <span className="muted small mono runtime-age">{relativeAge(r.last_session_at)}</span>
393
- </li>
394
- ))}
395
- </ul>
396
- );
397
- }
398
-
399
- function Bar({ tone, label, value, pct }: {
400
- tone: "active" | "warn" | "danger";
401
- label: string;
402
- value: number;
403
- pct: number;
404
- }) {
405
- const rounded = Math.round(pct);
406
- return (
407
- <div className="bar-item">
408
- <span className="bar-label">{label}</span>
409
- <div className="bar-track" title={`${value.toLocaleString()} entit${value === 1 ? "y" : "ies"} · ${rounded}% of total`}>
410
- <div className={`bar-fill tone-${tone}`} style={{ width: `${pct}%` }} />
411
- </div>
412
- <span className="bar-value mono">{value.toLocaleString()}<span className="bar-pct muted small"> · {rounded}%</span></span>
413
- </div>
414
- );
415
- }
@@ -1,190 +0,0 @@
1
- /**
2
- * Recall — adoption + coverage telemetry for the memory system.
3
- *
4
- * Two surfaces, two audiences:
5
- * - Session recall → what the orchestrator pulls answering questions
6
- * about past work (the human-operator surface).
7
- * - Fact recall → structured facts agents pull mid-task.
8
- *
9
- * Hit rate answers "did recall return something," not "did the agent use
10
- * it." By-source answers "is anything calling this at all." Read together
11
- * they distinguish an adoption problem from a corpus-coverage problem.
12
- */
13
-
14
- import { useState } from "react";
15
- import { usePolledEndpoint } from "../lib/api.js";
16
-
17
- interface BaseStats {
18
- days: number;
19
- total: number;
20
- with_results: number;
21
- hit_rate: number;
22
- by_source: Record<string, number>;
23
- log_present: boolean;
24
- }
25
-
26
- interface SessionStats extends BaseStats {
27
- top_queries: { query: string; count: number }[];
28
- }
29
-
30
- interface FactStats extends BaseStats {
31
- top_subjects: { subject: string; count: number }[];
32
- top_predicates: { predicate: string; count: number }[];
33
- }
34
-
35
- const EMPTY_SESSION: SessionStats = {
36
- days: 7, total: 0, with_results: 0, hit_rate: 0, by_source: {}, top_queries: [], log_present: false,
37
- };
38
- const EMPTY_FACT: FactStats = {
39
- days: 7, total: 0, with_results: 0, hit_rate: 0, by_source: {}, top_subjects: [], top_predicates: [], log_present: false,
40
- };
41
-
42
- const WINDOWS = [7, 30, 90] as const;
43
-
44
- interface BarRow {
45
- label: string;
46
- count: number;
47
- }
48
-
49
- export function RecallPage() {
50
- const [days, setDays] = useState<number>(7);
51
- const session = usePolledEndpoint<SessionStats>(`/api/recall/stats?days=${days}`, 30_000, EMPTY_SESSION);
52
- const facts = usePolledEndpoint<FactStats>(`/api/recall/facts/stats?days=${days}`, 30_000, EMPTY_FACT);
53
-
54
- return (
55
- <div className="page-pad">
56
- <div className="recall-head">
57
- <p className="muted small recall-note">
58
- Hit rate measures whether recall returned something — not whether the agent used it.
59
- Watch <strong>by source</strong> for adoption and <strong>hit rate</strong> for
60
- whether the corpus covers what's being asked.
61
- </p>
62
- <div className="filter-group" role="group" aria-label="Time window">
63
- {WINDOWS.map((w) => (
64
- <button
65
- key={w}
66
- type="button"
67
- className={`chip${days === w ? " active" : ""}`}
68
- onClick={() => setDays(w)}
69
- >{w}d</button>
70
- ))}
71
- </div>
72
- </div>
73
-
74
- <StatsBlock
75
- title="Session recall"
76
- subtitle="What the orchestrator pulls when answering questions about past work — the human-operator surface."
77
- stats={session.data}
78
- error={session.error}
79
- topLabel="Top queries"
80
- topRows={session.data.top_queries.map((q) => ({ label: q.query, count: q.count }))}
81
- />
82
-
83
- <StatsBlock
84
- title="Fact recall"
85
- subtitle="Structured facts agents pull mid-task — the orchestrator surface."
86
- stats={facts.data}
87
- error={facts.error}
88
- topLabel="Top subjects"
89
- topRows={facts.data.top_subjects.map((s) => ({ label: s.subject, count: s.count }))}
90
- extraLabel="Top predicates"
91
- extraRows={facts.data.top_predicates.map((p) => ({ label: p.predicate, count: p.count }))}
92
- />
93
- </div>
94
- );
95
- }
96
-
97
- interface StatsBlockProps {
98
- title: string;
99
- subtitle: string;
100
- stats: BaseStats;
101
- error: string | null;
102
- topLabel: string;
103
- topRows: BarRow[];
104
- extraLabel?: string;
105
- extraRows?: BarRow[];
106
- }
107
-
108
- function StatsBlock({ title, subtitle, stats, error, topLabel, topRows, extraLabel, extraRows }: StatsBlockProps) {
109
- const zeroResult = stats.total - stats.with_results;
110
- const sourceRows: BarRow[] = Object.entries(stats.by_source)
111
- .map(([label, count]) => ({ label, count }))
112
- .sort((a, b) => b.count - a.count);
113
-
114
- return (
115
- <section className="recall-block">
116
- <header className="recall-block-head">
117
- <h2 className="page-title">{title}</h2>
118
- <p className="muted small">{subtitle}</p>
119
- </header>
120
-
121
- {error && <div className="muted error small">{error}</div>}
122
-
123
- {!stats.log_present ? (
124
- <div className="card recall-empty muted">
125
- No query log on disk yet — nothing has called this recall surface.
126
- </div>
127
- ) : stats.total === 0 ? (
128
- <div className="card recall-empty muted">
129
- No queries in the last {stats.days} days. The log exists but this window is empty.
130
- </div>
131
- ) : (
132
- <>
133
- <div className="kpi-row">
134
- <Kpi label="Queries" value={stats.total} hint={`last ${stats.days}d`} />
135
- <Kpi
136
- label="Hit rate"
137
- value={`${Math.round(stats.hit_rate * 100)}%`}
138
- hint={`${stats.with_results.toLocaleString()} returned ≥1`}
139
- />
140
- <Kpi
141
- label="Zero-result"
142
- value={zeroResult}
143
- hint={zeroResult > 0 ? "corpus gaps" : "full coverage"}
144
- />
145
- <Kpi label="Sources" value={sourceRows.length} hint="distinct callers" />
146
- </div>
147
-
148
- <div className="recall-cards">
149
- <BarCard title="By source" rows={sourceRows} emptyText="No sources recorded." />
150
- <BarCard title={topLabel} rows={topRows} emptyText="None recorded." />
151
- {extraLabel && extraRows && (
152
- <BarCard title={extraLabel} rows={extraRows} emptyText="None recorded." />
153
- )}
154
- </div>
155
- </>
156
- )}
157
- </section>
158
- );
159
- }
160
-
161
- function Kpi({ label, value, hint }: { label: string; value: number | string; hint?: string }) {
162
- return (
163
- <div className="kpi">
164
- <span className="kpi-label">{label}</span>
165
- <span className="kpi-value">{typeof value === "number" ? value.toLocaleString() : value}</span>
166
- {hint && <span className="kpi-hint">{hint}</span>}
167
- </div>
168
- );
169
- }
170
-
171
- function BarCard({ title, rows, emptyText }: { title: string; rows: BarRow[]; emptyText: string }) {
172
- const max = Math.max(1, ...rows.map((r) => r.count));
173
- return (
174
- <section className="card">
175
- <header className="card-head"><h3>{title}</h3></header>
176
- <div className="bar-stack recall-bars">
177
- {rows.length === 0 && <div className="muted small recall-bars-empty">{emptyText}</div>}
178
- {rows.map((r) => (
179
- <div key={r.label} className="bar-item">
180
- <span className="bar-label recall-bar-label" title={r.label}>{r.label}</span>
181
- <div className="bar-track">
182
- <div className="bar-fill tone-active" style={{ width: `${(r.count / max) * 100}%` }} />
183
- </div>
184
- <span className="bar-value mono">{r.count.toLocaleString()}</span>
185
- </div>
186
- ))}
187
- </div>
188
- </section>
189
- );
190
- }