@animalabs/connectome-host 0.7.3 → 0.7.4

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 (50) hide show
  1. package/CHANGELOG.md +156 -10
  2. package/HEADLESS-FLEET-PLAN.md +22 -0
  3. package/README.md +12 -1
  4. package/docs/AGENT-ONBOARDING.md +1 -1
  5. package/docs/debug-context-api.md +2 -2
  6. package/docs/retrieval-traces.md +173 -0
  7. package/docs/webui-deployment.md +2 -1
  8. package/package.json +2 -2
  9. package/scripts/audit-module-optins.ts +288 -0
  10. package/src/framework-strategy.ts +13 -4
  11. package/src/headless.ts +14 -0
  12. package/src/index.ts +12 -9
  13. package/src/modules/fleet-module.ts +60 -1
  14. package/src/modules/fleet-types.ts +30 -1
  15. package/src/modules/mcpl-admin-module.ts +33 -4
  16. package/src/modules/retrieval-module.ts +249 -51
  17. package/src/modules/retrieval-trace-page.ts +254 -0
  18. package/src/modules/retrieval-trace.ts +904 -0
  19. package/src/modules/tts-relay-module.ts +33 -18
  20. package/src/modules/web-ui-module.ts +445 -894
  21. package/src/recipe.ts +55 -4
  22. package/src/retrieval-config.ts +39 -0
  23. package/src/strategies/frontdesk-strategy.ts +34 -125
  24. package/src/tui.ts +325 -54
  25. package/src/web/panel-data.ts +1187 -0
  26. package/src/web/protocol.ts +75 -10
  27. package/test/audit-module-optins.test.ts +167 -0
  28. package/test/fleet-panel-request.test.ts +90 -0
  29. package/test/framework-strategy-defaults.test.ts +22 -0
  30. package/test/frontdesk-strategy.test.ts +25 -37
  31. package/test/headless-panel-request.test.ts +201 -0
  32. package/test/mcpl-admin-module.test.ts +23 -0
  33. package/test/mock-headless-child.ts +14 -0
  34. package/test/retrieval-auth-loopback.test.ts +49 -0
  35. package/test/retrieval-config.test.ts +74 -0
  36. package/test/retrieval-module.test.ts +821 -0
  37. package/test/tui-format.test.ts +106 -0
  38. package/test/web-ui-context-coverage.test.ts +1 -1
  39. package/test/web-ui-module.test.ts +189 -3
  40. package/test/web-ui-observers.test.ts +8 -5
  41. package/test/web-ui-protocol.test.ts +0 -0
  42. package/web/src/App.tsx +159 -44
  43. package/web/src/Context.tsx +35 -8
  44. package/web/src/ContextDocument.tsx +20 -5
  45. package/web/src/Files.tsx +2 -8
  46. package/web/src/Lessons.tsx +2 -38
  47. package/web/src/Mcpl.tsx +80 -14
  48. package/web/src/Pins.tsx +5 -0
  49. package/web/src/Settings.tsx +5 -0
  50. package/web/vite.config.ts +8 -2
@@ -0,0 +1,254 @@
1
+ /** Dependency-free operator viewer for the authenticated retrieval trace endpoint. */
2
+ export const RETRIEVAL_TRACE_PAGE_HTML = `<!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>Retrieval traces</title>
8
+ <style>
9
+ :root { color-scheme: dark; font: 15px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
10
+ * { box-sizing: border-box; }
11
+ body { max-width: 1240px; margin: 2rem auto; padding: 0 1rem 4rem; background: #0c0e14; color: #d8deea; }
12
+ header { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; }
13
+ h1 { font-size: 1.45rem; line-height: 1.2; margin: 0 auto 0 0; }
14
+ h2 { font-size: 1rem; margin: 0; }
15
+ button, label { font: inherit; }
16
+ button { cursor: pointer; padding: .42rem .78rem; background: #1c2434; color: inherit; border: 1px solid #3a4863; border-radius: .45rem; }
17
+ button:hover { background: #263249; }
18
+ code, pre, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
19
+ .note { color: #9ca9bd; margin: .5rem 0 1.25rem; }
20
+ .error { color: #ff9b9b; }
21
+ .trace { border: 1px solid #283247; border-radius: .65rem; margin: .8rem 0; background: #111722; overflow: hidden; }
22
+ .trace > summary { cursor: pointer; padding: .8rem .95rem; color: #e7bf76; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
23
+ .trace[open] > summary { border-bottom: 1px solid #283247; }
24
+ .trace-body { padding: 1rem; display: grid; gap: 1.15rem; }
25
+ .trace-meta { display: flex; flex-wrap: wrap; gap: .45rem .75rem; color: #aebbd0; }
26
+ .section { display: grid; gap: .65rem; }
27
+ .section-heading { display: flex; align-items: baseline; gap: .55rem; }
28
+ .count { color: #8fa0b9; font-size: .88rem; }
29
+ .lesson-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr)); gap: .65rem; }
30
+ .lesson-card { border: 1px solid #33415b; border-radius: .55rem; background: #151d2a; padding: .8rem; min-width: 0; }
31
+ .lesson-card.selected { border-color: #4f8e78; background: #14231f; box-shadow: inset 3px 0 #63b392; }
32
+ .lesson-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .55rem; }
33
+ .lesson-id { color: #e7bf76; font-weight: 700; }
34
+ .confidence { color: #aebbd0; margin-left: auto; }
35
+ .lesson-content { margin: .45rem 0 .7rem; white-space: pre-wrap; overflow-wrap: anywhere; }
36
+ .chips { display: flex; flex-wrap: wrap; gap: .3rem; }
37
+ .chip, .badge { display: inline-block; border: 1px solid #3a4863; border-radius: 999px; padding: .12rem .45rem; color: #b9c7dc; background: #111722; font-size: .78rem; }
38
+ .badge.selected { color: #a9e6cd; border-color: #4f8e78; background: #183027; font-weight: 700; }
39
+ .candidate-list { display: grid; gap: .5rem; }
40
+ .candidate { border: 1px solid #2f3b52; border-radius: .5rem; background: #131a26; overflow: hidden; }
41
+ .candidate.selected { border-color: #4f8e78; }
42
+ .candidate > summary { cursor: pointer; padding: .7rem .8rem; display: grid; grid-template-columns: auto auto 1fr auto; gap: .5rem; align-items: baseline; }
43
+ .candidate-preview { color: #b7c2d3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
44
+ .candidate-body { border-top: 1px solid #2f3b52; padding: .8rem; }
45
+ .matches { display: grid; gap: .35rem; margin-top: .7rem; }
46
+ .match { color: #9facc0; font-size: .85rem; padding-left: .65rem; border-left: 2px solid #34445f; overflow-wrap: anywhere; }
47
+ .decision { display: grid; gap: .5rem; border: 1px solid #283247; border-radius: .5rem; padding: .75rem; background: #0f141e; }
48
+ .empty { color: #8998ae; border: 1px dashed #344158; border-radius: .5rem; padding: .75rem; }
49
+ .raw { border: 1px solid #283247; border-radius: .5rem; background: #0f141e; }
50
+ .raw > summary { cursor: pointer; padding: .65rem .8rem; color: #9facc0; }
51
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; padding: .85rem; border-top: 1px solid #283247; color: #c8d4e8; max-height: 65vh; overflow: auto; }
52
+ @media (max-width: 700px) {
53
+ body { margin-top: 1rem; }
54
+ .candidate > summary { grid-template-columns: auto 1fr auto; }
55
+ .candidate-preview { grid-column: 1 / -1; }
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <header>
61
+ <h1>Retrieval traces</h1>
62
+ <label><input id="inputs" type="checkbox"> include exact conversation inputs</label>
63
+ <button id="refresh" type="button">refresh</button>
64
+ </header>
65
+ <p class="note">Newest first. In-memory only; reset on Host restart. "Raw output" is selector text returned to the application, not hidden chain-of-thought.</p>
66
+ <div id="status" class="note"></div>
67
+ <main id="traces"></main>
68
+ <script>
69
+ const root = document.getElementById('traces');
70
+ const status = document.getElementById('status');
71
+ const inputs = document.getElementById('inputs');
72
+
73
+ function element(tag, className, text) {
74
+ const node = document.createElement(tag);
75
+ if (className) node.className = className;
76
+ if (text !== undefined) node.textContent = String(text);
77
+ return node;
78
+ }
79
+
80
+ function percentage(value) {
81
+ return Number.isFinite(value) ? Math.round(value * 100) + '%' : 'unknown';
82
+ }
83
+
84
+ function localTime(value) {
85
+ const date = new Date(value);
86
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
87
+ }
88
+
89
+ function appendChips(parent, values) {
90
+ const chips = element('div', 'chips');
91
+ for (const value of values || []) chips.append(element('span', 'chip', value));
92
+ parent.append(chips);
93
+ }
94
+
95
+ function lessonCard(lesson, selected) {
96
+ const card = element('article', 'lesson-card' + (selected ? ' selected' : ''));
97
+ const head = element('div', 'lesson-head');
98
+ head.append(element('code', 'lesson-id', lesson.id || 'unknown lesson'));
99
+ if (selected) head.append(element('span', 'badge selected', 'SELECTED'));
100
+ head.append(element('span', 'confidence', percentage(lesson.confidence) + ' confidence'));
101
+ card.append(head, element('p', 'lesson-content', lesson.content || '(no lesson content recorded)'));
102
+ appendChips(card, lesson.tags);
103
+ return card;
104
+ }
105
+
106
+ function matchText(match) {
107
+ const location = match.field === 'tag' ? 'tag ' + (match.tag || '') : 'content';
108
+ return match.concept + ' -> ' + location + ' matched "' + match.keyword + '"';
109
+ }
110
+
111
+ function candidateCard(candidate, selectedIds) {
112
+ const selected = selectedIds.has(candidate.id);
113
+ const details = element('details', 'candidate' + (selected ? ' selected' : ''));
114
+ if (selected) details.open = true;
115
+ const summary = element('summary');
116
+ summary.append(element('code', 'lesson-id', candidate.id));
117
+ if (selected) summary.append(element('span', 'badge selected', 'SELECTED'));
118
+ summary.append(element('span', 'candidate-preview', candidate.content || '(no content)'));
119
+ summary.append(element('span', 'confidence', percentage(candidate.confidence)));
120
+
121
+ const body = element('div', 'candidate-body');
122
+ body.append(element('p', 'lesson-content', candidate.content || '(no lesson content recorded)'));
123
+ appendChips(body, candidate.tags);
124
+ const matches = element('div', 'matches');
125
+ const seen = new Set();
126
+ for (const match of candidate.matches || []) {
127
+ const text = matchText(match);
128
+ if (!seen.has(text)) {
129
+ seen.add(text);
130
+ matches.append(element('div', 'match', text));
131
+ }
132
+ }
133
+ if (matches.childElementCount) body.append(matches);
134
+ details.append(summary, body);
135
+ return details;
136
+ }
137
+
138
+ function renderSelected(trace, body) {
139
+ const section = element('section', 'section');
140
+ const lessons = trace.injected && Array.isArray(trace.injected.lessons) ? trace.injected.lessons : [];
141
+ const ids = trace.injected && Array.isArray(trace.injected.lessonIds) ? trace.injected.lessonIds : [];
142
+ const heading = element('div', 'section-heading');
143
+ heading.append(element('h2', '', 'Selected lessons'), element('span', 'count', lessons.length + ' injected'));
144
+ section.append(heading);
145
+ if (lessons.length) {
146
+ const grid = element('div', 'lesson-grid');
147
+ for (const lesson of lessons) grid.append(lessonCard(lesson, true));
148
+ section.append(grid);
149
+ } else if (ids.length) {
150
+ const fallback = element('div', 'empty', 'Selected IDs: ' + ids.join(', ') + ' (structured snapshots unavailable)');
151
+ section.append(fallback);
152
+ } else {
153
+ section.append(element('div', 'empty', 'No lessons were injected for this run.'));
154
+ }
155
+ body.append(section);
156
+ }
157
+
158
+ function renderCandidates(trace, body) {
159
+ const section = element('section', 'section');
160
+ const candidates = Array.isArray(trace.candidates) ? trace.candidates : [];
161
+ const selectedIds = new Set(trace.injected && Array.isArray(trace.injected.lessonIds) ? trace.injected.lessonIds : []);
162
+ const heading = element('div', 'section-heading');
163
+ heading.append(element('h2', '', 'Candidate lessons'), element('span', 'count', candidates.length + ' mechanically matched'));
164
+ section.append(heading);
165
+ if (candidates.length) {
166
+ const list = element('div', 'candidate-list');
167
+ for (const candidate of candidates) list.append(candidateCard(candidate, selectedIds));
168
+ section.append(list);
169
+ } else {
170
+ section.append(element('div', 'empty', 'No candidate lessons matched this run.'));
171
+ }
172
+ body.append(section);
173
+ }
174
+
175
+ function renderDecision(trace, body) {
176
+ const decision = element('section', 'decision');
177
+ decision.append(element('h2', '', 'Decision details'));
178
+ const concepts = trace.conceptExtraction && Array.isArray(trace.conceptExtraction.parsedValues)
179
+ ? trace.conceptExtraction.parsedValues : [];
180
+ const relevant = Array.isArray(trace.relevantLessonIds) ? trace.relevantLessonIds : [];
181
+ const conceptLine = element('div');
182
+ conceptLine.append(element('strong', '', 'Concepts: '));
183
+ conceptLine.append(document.createTextNode(concepts.length ? concepts.join(', ') : 'none'));
184
+ const relevanceLine = element('div');
185
+ relevanceLine.append(element('strong', '', 'Relevant IDs: '));
186
+ relevanceLine.append(document.createTextNode(relevant.length ? relevant.join(', ') : 'none'));
187
+ const injectionLine = element('div');
188
+ injectionLine.append(element('strong', '', 'Injection: '));
189
+ const namespace = trace.injected && trace.injected.namespace ? trace.injected.namespace : 'none';
190
+ const position = trace.injected && trace.injected.position ? trace.injected.position : 'none';
191
+ injectionLine.append(document.createTextNode(namespace + ' / ' + position));
192
+ decision.append(conceptLine, relevanceLine, injectionLine);
193
+ body.append(decision);
194
+ }
195
+
196
+ function traceSummary(trace) {
197
+ const selected = trace.injected && Array.isArray(trace.injected.lessonIds) ? trace.injected.lessonIds : [];
198
+ return '#' + trace.id + ' ' + (trace.agentName || 'unknown agent') + ' ' +
199
+ (trace.outcome || 'running') + ' ' + localTime(trace.startedAt) +
200
+ ' selected: ' + selected.length;
201
+ }
202
+
203
+ function renderTrace(trace, index) {
204
+ const details = element('details', 'trace');
205
+ if (index === 0) details.open = true;
206
+ details.append(element('summary', '', traceSummary(trace)));
207
+ const body = element('div', 'trace-body');
208
+ const meta = element('div', 'trace-meta');
209
+ const reasoning = trace.config && trace.config.requestedReasoning;
210
+ meta.append(
211
+ element('span', '', 'agent: ' + (trace.agentName || 'unknown')),
212
+ element('span', '', 'model: ' + ((trace.config && trace.config.model) || 'unknown')),
213
+ element('span', '', 'reasoning effort: ' + (reasoning ? reasoning.effort : 'default')),
214
+ element('span', '', 'cache: ' + (trace.cache && trace.cache.hit ? 'hit' : 'miss')),
215
+ element('span', '', 'duration: ' + (Number.isFinite(trace.durationMs) ? trace.durationMs + ' ms' : 'running'))
216
+ );
217
+ body.append(meta);
218
+ renderSelected(trace, body);
219
+ renderCandidates(trace, body);
220
+ renderDecision(trace, body);
221
+ const raw = element('details', 'raw');
222
+ raw.append(element('summary', '', 'Raw JSON (diagnostic)'), element('pre', '', JSON.stringify(trace, null, 2)));
223
+ body.append(raw);
224
+ details.append(body);
225
+ return details;
226
+ }
227
+
228
+ async function load() {
229
+ status.className = 'note';
230
+ status.textContent = 'loading...';
231
+ root.replaceChildren();
232
+ try {
233
+ const url = '/debug/retrieval?limit=100&includeInputs=' + (inputs.checked ? '1' : '0');
234
+ const response = await fetch(url, { credentials: 'same-origin' });
235
+ const payload = await response.json();
236
+ if (!response.ok) throw new Error(payload.error || ('HTTP ' + response.status));
237
+ status.textContent = payload.enabled
238
+ ? payload.traces.length + ' retained run(s)'
239
+ : 'retrieval module is not enabled';
240
+ payload.traces.forEach(function(trace, index) {
241
+ root.append(renderTrace(trace, index));
242
+ });
243
+ } catch (error) {
244
+ status.textContent = String(error);
245
+ status.className = 'error';
246
+ }
247
+ }
248
+
249
+ document.getElementById('refresh').addEventListener('click', load);
250
+ inputs.addEventListener('change', load);
251
+ load();
252
+ </script>
253
+ </body>
254
+ </html>`;