@femtomc/mu-server 26.2.72 → 26.2.73

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.
@@ -0,0 +1,1147 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import { join, relative } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { getStorePaths } from "@femtomc/mu-core/node";
6
+ import { getControlPlanePaths } from "@femtomc/mu-control-plane";
7
+ const DEFAULT_LIMIT = 40;
8
+ const MAX_LIMIT = 500;
9
+ const MAX_TEXT_LENGTH = 8_000;
10
+ const PREVIEW_LENGTH = 240;
11
+ const CONTEXT_SOURCE_KINDS = [
12
+ "issues",
13
+ "forum",
14
+ "events",
15
+ "cp_commands",
16
+ "cp_outbox",
17
+ "cp_adapter_audit",
18
+ "cp_operator_turns",
19
+ "cp_telegram_ingress",
20
+ "operator_sessions",
21
+ "cp_operator_sessions",
22
+ ];
23
+ class QueryValidationError extends Error {
24
+ status = 400;
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "QueryValidationError";
28
+ }
29
+ }
30
+ function asRecord(value) {
31
+ if (typeof value !== "object" || value == null || Array.isArray(value)) {
32
+ return null;
33
+ }
34
+ return value;
35
+ }
36
+ function nonEmptyString(value) {
37
+ if (typeof value !== "string") {
38
+ return null;
39
+ }
40
+ const trimmed = value.trim();
41
+ return trimmed.length > 0 ? trimmed : null;
42
+ }
43
+ function asInt(value) {
44
+ if (typeof value !== "number" || !Number.isFinite(value)) {
45
+ return null;
46
+ }
47
+ return Math.trunc(value);
48
+ }
49
+ function parseTimestamp(value) {
50
+ if (typeof value === "number" && Number.isFinite(value)) {
51
+ return Math.trunc(value);
52
+ }
53
+ if (typeof value === "string") {
54
+ const parsed = new Date(value);
55
+ if (!Number.isNaN(parsed.getTime())) {
56
+ return Math.trunc(parsed.getTime());
57
+ }
58
+ }
59
+ if (value instanceof Date) {
60
+ const ms = value.getTime();
61
+ if (!Number.isNaN(ms)) {
62
+ return Math.trunc(ms);
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function toSingleLine(value) {
68
+ return value.replace(/\s+/g, " ").trim();
69
+ }
70
+ function clampText(value, maxLength) {
71
+ if (value.length <= maxLength) {
72
+ return value;
73
+ }
74
+ if (maxLength <= 1) {
75
+ return value.slice(0, Math.max(0, maxLength));
76
+ }
77
+ return `${value.slice(0, maxLength - 1)}…`;
78
+ }
79
+ function buildPreview(text) {
80
+ const normalized = toSingleLine(text);
81
+ if (normalized.length === 0) {
82
+ return "";
83
+ }
84
+ return clampText(normalized, PREVIEW_LENGTH);
85
+ }
86
+ function textFromUnknown(value) {
87
+ if (typeof value === "string") {
88
+ return value;
89
+ }
90
+ if (value == null) {
91
+ return "";
92
+ }
93
+ try {
94
+ return JSON.stringify(value);
95
+ }
96
+ catch {
97
+ return String(value);
98
+ }
99
+ }
100
+ function buildConversationKey(parts) {
101
+ if (!parts.channel || !parts.tenantId || !parts.conversationId || !parts.bindingId) {
102
+ return null;
103
+ }
104
+ return `${parts.channel}:${parts.tenantId}:${parts.conversationId}:${parts.bindingId}`;
105
+ }
106
+ function conversationScopeKey(key) {
107
+ const parts = key.split(":");
108
+ if (parts.length < 3) {
109
+ return key;
110
+ }
111
+ return `${parts[0]}:${parts[1]}:${parts[2]}`;
112
+ }
113
+ function parseCsv(value) {
114
+ if (!value) {
115
+ return [];
116
+ }
117
+ return value
118
+ .split(",")
119
+ .map((part) => part.trim())
120
+ .filter((part) => part.length > 0);
121
+ }
122
+ function parseLimit(value) {
123
+ if (value == null || value.trim().length === 0) {
124
+ return DEFAULT_LIMIT;
125
+ }
126
+ const parsed = Number.parseInt(value, 10);
127
+ if (!Number.isFinite(parsed)) {
128
+ throw new QueryValidationError("invalid limit: expected integer");
129
+ }
130
+ if (parsed < 1) {
131
+ throw new QueryValidationError("invalid limit: must be >= 1");
132
+ }
133
+ return Math.min(parsed, MAX_LIMIT);
134
+ }
135
+ function parseOptionalTs(value, name) {
136
+ if (value == null || value.trim().length === 0) {
137
+ return null;
138
+ }
139
+ const parsed = Number.parseInt(value, 10);
140
+ if (!Number.isFinite(parsed)) {
141
+ throw new QueryValidationError(`invalid ${name}: expected integer epoch ms`);
142
+ }
143
+ return parsed;
144
+ }
145
+ function parseSourceFilter(value) {
146
+ const parts = parseCsv(value);
147
+ if (parts.length === 0) {
148
+ return null;
149
+ }
150
+ const out = new Set();
151
+ for (const part of parts) {
152
+ const normalized = part.trim().toLowerCase();
153
+ if (normalized === "all") {
154
+ for (const source of CONTEXT_SOURCE_KINDS) {
155
+ out.add(source);
156
+ }
157
+ continue;
158
+ }
159
+ if (CONTEXT_SOURCE_KINDS.includes(normalized)) {
160
+ out.add(normalized);
161
+ continue;
162
+ }
163
+ throw new QueryValidationError(`unknown context source: ${part}. valid sources: ${CONTEXT_SOURCE_KINDS.join(", ")}`);
164
+ }
165
+ return out;
166
+ }
167
+ function parseSearchFilters(url) {
168
+ const query = nonEmptyString(url.searchParams.get("query")) ??
169
+ nonEmptyString(url.searchParams.get("q")) ??
170
+ nonEmptyString(url.searchParams.get("contains"));
171
+ return {
172
+ query,
173
+ sources: parseSourceFilter(url.searchParams.get("sources") ?? url.searchParams.get("source")),
174
+ limit: parseLimit(url.searchParams.get("limit")),
175
+ sinceMs: parseOptionalTs(url.searchParams.get("since"), "since"),
176
+ untilMs: parseOptionalTs(url.searchParams.get("until"), "until"),
177
+ issueId: nonEmptyString(url.searchParams.get("issue_id")),
178
+ runId: nonEmptyString(url.searchParams.get("run_id")),
179
+ sessionId: nonEmptyString(url.searchParams.get("session_id")),
180
+ conversationKey: nonEmptyString(url.searchParams.get("conversation_key")),
181
+ channel: nonEmptyString(url.searchParams.get("channel")),
182
+ channelTenantId: nonEmptyString(url.searchParams.get("channel_tenant_id")),
183
+ channelConversationId: nonEmptyString(url.searchParams.get("channel_conversation_id")),
184
+ actorBindingId: nonEmptyString(url.searchParams.get("actor_binding_id")),
185
+ topic: nonEmptyString(url.searchParams.get("topic")),
186
+ author: nonEmptyString(url.searchParams.get("author")),
187
+ role: nonEmptyString(url.searchParams.get("role")),
188
+ };
189
+ }
190
+ function parseTimelineFilters(url) {
191
+ const base = parseSearchFilters(url);
192
+ const orderRaw = nonEmptyString(url.searchParams.get("order"))?.toLowerCase();
193
+ const order = orderRaw === "desc" ? "desc" : "asc";
194
+ if (!base.conversationKey &&
195
+ !base.issueId &&
196
+ !base.runId &&
197
+ !base.sessionId &&
198
+ !base.topic &&
199
+ !base.channel) {
200
+ throw new QueryValidationError("timeline requires one anchor filter: conversation_key, issue_id, run_id, session_id, topic, or channel");
201
+ }
202
+ return { ...base, order };
203
+ }
204
+ function matchesConversation(item, requested) {
205
+ const requestedTrimmed = requested.trim();
206
+ if (requestedTrimmed.length === 0) {
207
+ return true;
208
+ }
209
+ const direct = item.conversation_key ? [item.conversation_key] : [];
210
+ const metadataKeys = Array.isArray(item.metadata.conversation_keys)
211
+ ? item.metadata.conversation_keys.filter((v) => typeof v === "string" && v.trim().length > 0)
212
+ : [];
213
+ const allKeys = [...direct, ...metadataKeys];
214
+ if (allKeys.length === 0) {
215
+ return false;
216
+ }
217
+ if (requestedTrimmed.includes("*")) {
218
+ const prefix = requestedTrimmed.replace(/\*/g, "");
219
+ return allKeys.some((key) => key.startsWith(prefix));
220
+ }
221
+ if (allKeys.includes(requestedTrimmed)) {
222
+ return true;
223
+ }
224
+ const requestedScope = conversationScopeKey(requestedTrimmed);
225
+ return allKeys.some((key) => conversationScopeKey(key) === requestedScope);
226
+ }
227
+ function matchSource(item, sources) {
228
+ if (!sources) {
229
+ return true;
230
+ }
231
+ return sources.has(item.source_kind);
232
+ }
233
+ function matchSearchFilters(item, filters) {
234
+ if (!matchSource(item, filters.sources)) {
235
+ return false;
236
+ }
237
+ if (filters.sinceMs != null && item.ts_ms < filters.sinceMs) {
238
+ return false;
239
+ }
240
+ if (filters.untilMs != null && item.ts_ms > filters.untilMs) {
241
+ return false;
242
+ }
243
+ if (filters.issueId && item.issue_id !== filters.issueId) {
244
+ return false;
245
+ }
246
+ if (filters.runId && item.run_id !== filters.runId) {
247
+ return false;
248
+ }
249
+ if (filters.sessionId && item.session_id !== filters.sessionId) {
250
+ return false;
251
+ }
252
+ if (filters.channel && item.channel !== filters.channel) {
253
+ return false;
254
+ }
255
+ if (filters.channelTenantId && item.channel_tenant_id !== filters.channelTenantId) {
256
+ return false;
257
+ }
258
+ if (filters.channelConversationId && item.channel_conversation_id !== filters.channelConversationId) {
259
+ return false;
260
+ }
261
+ if (filters.actorBindingId && item.actor_binding_id !== filters.actorBindingId) {
262
+ return false;
263
+ }
264
+ if (filters.topic && item.topic !== filters.topic) {
265
+ return false;
266
+ }
267
+ if (filters.author && item.author !== filters.author) {
268
+ return false;
269
+ }
270
+ if (filters.role && item.role !== filters.role) {
271
+ return false;
272
+ }
273
+ if (filters.conversationKey && !matchesConversation(item, filters.conversationKey)) {
274
+ return false;
275
+ }
276
+ return true;
277
+ }
278
+ function tokenizeQuery(query) {
279
+ return query
280
+ .toLowerCase()
281
+ .split(/\s+/)
282
+ .map((part) => part.trim())
283
+ .filter((part) => part.length > 0);
284
+ }
285
+ function searchableText(item) {
286
+ const tags = item.tags.join(" ");
287
+ const fields = [
288
+ item.text,
289
+ item.preview,
290
+ item.source_kind,
291
+ item.issue_id ?? "",
292
+ item.run_id ?? "",
293
+ item.session_id ?? "",
294
+ item.channel ?? "",
295
+ item.topic ?? "",
296
+ item.author ?? "",
297
+ tags,
298
+ ];
299
+ return fields.join("\n").toLowerCase();
300
+ }
301
+ function scoreItem(item, query) {
302
+ if (!query) {
303
+ return item.ts_ms;
304
+ }
305
+ const haystack = searchableText(item);
306
+ const needle = query.toLowerCase();
307
+ const tokens = tokenizeQuery(needle);
308
+ let score = 0;
309
+ if (haystack.includes(needle)) {
310
+ score += 100;
311
+ }
312
+ let tokenHits = 0;
313
+ for (const token of tokens) {
314
+ if (haystack.includes(token)) {
315
+ tokenHits += 1;
316
+ score += 20;
317
+ }
318
+ }
319
+ if (tokens.length > 0 && tokenHits === 0) {
320
+ return -1;
321
+ }
322
+ if (item.role === "user") {
323
+ score += 8;
324
+ }
325
+ if (item.role === "assistant") {
326
+ score += 4;
327
+ }
328
+ if (item.source_kind === "cp_operator_sessions" || item.source_kind === "operator_sessions") {
329
+ score += 5;
330
+ }
331
+ const ageMs = Math.max(0, Date.now() - item.ts_ms);
332
+ const recencyBonus = Math.max(0, 24 - Math.trunc(ageMs / (1000 * 60 * 60 * 24)));
333
+ score += recencyBonus;
334
+ return score;
335
+ }
336
+ function isErrnoCode(err, code) {
337
+ return Boolean(err && typeof err === "object" && "code" in err && err.code === code);
338
+ }
339
+ async function readJsonlRows(path) {
340
+ const rows = [];
341
+ let stream = null;
342
+ let rl = null;
343
+ try {
344
+ stream = createReadStream(path, { encoding: "utf8" });
345
+ rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
346
+ let line = 0;
347
+ for await (const raw of rl) {
348
+ line += 1;
349
+ const trimmed = raw.trim();
350
+ if (trimmed.length === 0) {
351
+ continue;
352
+ }
353
+ try {
354
+ rows.push({ line, value: JSON.parse(trimmed) });
355
+ }
356
+ catch {
357
+ // Keep malformed JSON rows non-fatal for retrieval.
358
+ }
359
+ }
360
+ return rows;
361
+ }
362
+ catch (err) {
363
+ if (isErrnoCode(err, "ENOENT")) {
364
+ return [];
365
+ }
366
+ throw err;
367
+ }
368
+ finally {
369
+ rl?.close();
370
+ stream?.close();
371
+ }
372
+ }
373
+ async function listJsonlFiles(dir) {
374
+ try {
375
+ const entries = await readdir(dir, { withFileTypes: true });
376
+ return entries
377
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
378
+ .map((entry) => join(dir, entry.name))
379
+ .sort((a, b) => a.localeCompare(b));
380
+ }
381
+ catch (err) {
382
+ if (isErrnoCode(err, "ENOENT")) {
383
+ return [];
384
+ }
385
+ throw err;
386
+ }
387
+ }
388
+ function extractMessageText(messageRaw) {
389
+ if (typeof messageRaw === "string") {
390
+ return messageRaw;
391
+ }
392
+ const message = asRecord(messageRaw);
393
+ if (!message) {
394
+ return "";
395
+ }
396
+ if (typeof message.content === "string") {
397
+ return message.content;
398
+ }
399
+ if (Array.isArray(message.content)) {
400
+ const chunks = [];
401
+ for (const itemRaw of message.content) {
402
+ const item = asRecord(itemRaw);
403
+ if (!item) {
404
+ continue;
405
+ }
406
+ const text = nonEmptyString(item.text);
407
+ if (text) {
408
+ chunks.push(text);
409
+ }
410
+ }
411
+ if (chunks.length > 0) {
412
+ return chunks.join("\n");
413
+ }
414
+ }
415
+ const text = nonEmptyString(message.text);
416
+ if (text) {
417
+ return text;
418
+ }
419
+ return "";
420
+ }
421
+ async function loadConversationBindings(path) {
422
+ const out = new Map();
423
+ let raw = "";
424
+ try {
425
+ raw = await readFile(path, "utf8");
426
+ }
427
+ catch (err) {
428
+ if (isErrnoCode(err, "ENOENT")) {
429
+ return out;
430
+ }
431
+ throw err;
432
+ }
433
+ const parsed = asRecord(JSON.parse(raw));
434
+ const bindings = parsed ? asRecord(parsed.bindings) : null;
435
+ if (!bindings) {
436
+ return out;
437
+ }
438
+ for (const [conversationKey, sessionIdRaw] of Object.entries(bindings)) {
439
+ const sessionId = nonEmptyString(sessionIdRaw);
440
+ if (conversationKey.trim().length === 0 || !sessionId) {
441
+ continue;
442
+ }
443
+ out.set(conversationKey, sessionId);
444
+ }
445
+ return out;
446
+ }
447
+ function reverseConversationBindings(bindings) {
448
+ const out = new Map();
449
+ for (const [conversationKey, sessionId] of bindings.entries()) {
450
+ const rows = out.get(sessionId) ?? [];
451
+ rows.push(conversationKey);
452
+ out.set(sessionId, rows);
453
+ }
454
+ for (const values of out.values()) {
455
+ values.sort((a, b) => a.localeCompare(b));
456
+ }
457
+ return out;
458
+ }
459
+ function normalizeRelative(repoRoot, path) {
460
+ return relative(repoRoot, path).replaceAll("\\", "/");
461
+ }
462
+ function pushItem(out, item) {
463
+ const trimmed = item.text.trim();
464
+ if (trimmed.length === 0) {
465
+ return;
466
+ }
467
+ const text = clampText(trimmed, MAX_TEXT_LENGTH);
468
+ out.push({
469
+ ...item,
470
+ text,
471
+ preview: buildPreview(text),
472
+ });
473
+ }
474
+ async function collectIssues(repoRoot, path) {
475
+ const out = [];
476
+ for (const row of await readJsonlRows(path)) {
477
+ const rec = asRecord(row.value);
478
+ if (!rec) {
479
+ continue;
480
+ }
481
+ const issueId = nonEmptyString(rec.id);
482
+ const title = nonEmptyString(rec.title) ?? "";
483
+ const body = nonEmptyString(rec.body) ?? "";
484
+ const status = nonEmptyString(rec.status) ?? "unknown";
485
+ const tags = Array.isArray(rec.tags)
486
+ ? rec.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0)
487
+ : [];
488
+ if (!issueId) {
489
+ continue;
490
+ }
491
+ pushItem(out, {
492
+ id: `issues:${issueId}:${row.line}`,
493
+ ts_ms: asInt(rec.updated_at) ?? asInt(rec.created_at) ?? 0,
494
+ source_kind: "issues",
495
+ source_path: normalizeRelative(repoRoot, path),
496
+ source_line: row.line,
497
+ repo_root: repoRoot,
498
+ issue_id: issueId,
499
+ run_id: null,
500
+ session_id: null,
501
+ channel: null,
502
+ channel_tenant_id: null,
503
+ channel_conversation_id: null,
504
+ actor_binding_id: null,
505
+ conversation_key: null,
506
+ topic: null,
507
+ author: null,
508
+ role: null,
509
+ tags: ["issue", status, ...tags],
510
+ metadata: {
511
+ status,
512
+ priority: asInt(rec.priority),
513
+ outcome: rec.outcome ?? null,
514
+ },
515
+ text: [title, body, tags.join(" ")].filter((part) => part.length > 0).join("\n"),
516
+ });
517
+ }
518
+ return out;
519
+ }
520
+ async function collectForum(repoRoot, path) {
521
+ const out = [];
522
+ for (const row of await readJsonlRows(path)) {
523
+ const rec = asRecord(row.value);
524
+ if (!rec) {
525
+ continue;
526
+ }
527
+ const topic = nonEmptyString(rec.topic);
528
+ const body = nonEmptyString(rec.body) ?? "";
529
+ const author = nonEmptyString(rec.author);
530
+ if (!topic) {
531
+ continue;
532
+ }
533
+ pushItem(out, {
534
+ id: `forum:${topic}:${row.line}`,
535
+ ts_ms: asInt(rec.created_at) ?? 0,
536
+ source_kind: "forum",
537
+ source_path: normalizeRelative(repoRoot, path),
538
+ source_line: row.line,
539
+ repo_root: repoRoot,
540
+ issue_id: topic.startsWith("issue:") ? topic.slice("issue:".length) : null,
541
+ run_id: null,
542
+ session_id: null,
543
+ channel: null,
544
+ channel_tenant_id: null,
545
+ channel_conversation_id: null,
546
+ actor_binding_id: null,
547
+ conversation_key: null,
548
+ topic,
549
+ author,
550
+ role: null,
551
+ tags: ["forum", topic],
552
+ metadata: {},
553
+ text: [topic, author ?? "", body].filter((part) => part.length > 0).join("\n"),
554
+ });
555
+ }
556
+ return out;
557
+ }
558
+ async function collectEvents(repoRoot, path) {
559
+ const out = [];
560
+ for (const row of await readJsonlRows(path)) {
561
+ const rec = asRecord(row.value);
562
+ if (!rec) {
563
+ continue;
564
+ }
565
+ const type = nonEmptyString(rec.type) ?? "event";
566
+ const source = nonEmptyString(rec.source) ?? "unknown";
567
+ const payload = rec.payload ?? null;
568
+ const payloadText = textFromUnknown(payload);
569
+ const issueId = nonEmptyString(rec.issue_id);
570
+ const runId = nonEmptyString(rec.run_id);
571
+ pushItem(out, {
572
+ id: `events:${type}:${row.line}`,
573
+ ts_ms: asInt(rec.ts_ms) ?? 0,
574
+ source_kind: "events",
575
+ source_path: normalizeRelative(repoRoot, path),
576
+ source_line: row.line,
577
+ repo_root: repoRoot,
578
+ issue_id: issueId,
579
+ run_id: runId,
580
+ session_id: null,
581
+ channel: null,
582
+ channel_tenant_id: null,
583
+ channel_conversation_id: null,
584
+ actor_binding_id: null,
585
+ conversation_key: null,
586
+ topic: null,
587
+ author: null,
588
+ role: null,
589
+ tags: ["event", type, source],
590
+ metadata: {
591
+ type,
592
+ source,
593
+ },
594
+ text: [type, source, issueId ?? "", runId ?? "", payloadText]
595
+ .filter((part) => part.length > 0)
596
+ .join("\n"),
597
+ });
598
+ }
599
+ return out;
600
+ }
601
+ async function collectCommandJournal(repoRoot, path) {
602
+ const out = [];
603
+ for (const row of await readJsonlRows(path)) {
604
+ const rec = asRecord(row.value);
605
+ if (!rec) {
606
+ continue;
607
+ }
608
+ const kind = nonEmptyString(rec.kind) ?? "unknown";
609
+ if (kind === "command.lifecycle") {
610
+ const command = asRecord(rec.command);
611
+ const commandId = command ? nonEmptyString(command.command_id) : null;
612
+ const commandText = command ? nonEmptyString(command.command_text) : null;
613
+ const state = command ? nonEmptyString(command.state) : null;
614
+ const targetType = command ? nonEmptyString(command.target_type) : null;
615
+ const targetId = command ? nonEmptyString(command.target_id) : null;
616
+ const channel = command ? nonEmptyString(command.channel) : null;
617
+ const channelTenantId = command ? nonEmptyString(command.channel_tenant_id) : null;
618
+ const channelConversationId = command ? nonEmptyString(command.channel_conversation_id) : null;
619
+ const actorBindingId = command ? nonEmptyString(command.actor_binding_id) : null;
620
+ const runId = command ? nonEmptyString(command.run_root_id) : null;
621
+ const sessionId = command ? nonEmptyString(command.operator_session_id) : null;
622
+ const conversationKey = buildConversationKey({
623
+ channel,
624
+ tenantId: channelTenantId,
625
+ conversationId: channelConversationId,
626
+ bindingId: actorBindingId,
627
+ });
628
+ const eventType = nonEmptyString(rec.event_type) ?? "command.lifecycle";
629
+ pushItem(out, {
630
+ id: `cp_commands:lifecycle:${commandId ?? row.line}`,
631
+ ts_ms: asInt(rec.ts_ms) ?? (command ? asInt(command.updated_at_ms) : null) ?? 0,
632
+ source_kind: "cp_commands",
633
+ source_path: normalizeRelative(repoRoot, path),
634
+ source_line: row.line,
635
+ repo_root: repoRoot,
636
+ issue_id: null,
637
+ run_id: runId,
638
+ session_id: sessionId,
639
+ channel,
640
+ channel_tenant_id: channelTenantId,
641
+ channel_conversation_id: channelConversationId,
642
+ actor_binding_id: actorBindingId,
643
+ conversation_key: conversationKey,
644
+ topic: null,
645
+ author: null,
646
+ role: null,
647
+ tags: ["cp", "command.lifecycle", state ?? "unknown"],
648
+ metadata: {
649
+ kind,
650
+ event_type: eventType,
651
+ command_id: commandId,
652
+ state,
653
+ target_type: targetType,
654
+ target_id: targetId,
655
+ },
656
+ text: [eventType, commandText ?? "", targetType ?? "", targetId ?? "", state ?? ""]
657
+ .filter((part) => part.length > 0)
658
+ .join("\n"),
659
+ });
660
+ continue;
661
+ }
662
+ if (kind === "domain.mutating") {
663
+ const correlation = asRecord(rec.correlation);
664
+ const eventType = nonEmptyString(rec.event_type) ?? "domain.mutating";
665
+ const channel = correlation ? nonEmptyString(correlation.channel) : null;
666
+ const channelTenantId = correlation ? nonEmptyString(correlation.channel_tenant_id) : null;
667
+ const channelConversationId = correlation ? nonEmptyString(correlation.channel_conversation_id) : null;
668
+ const actorBindingId = correlation ? nonEmptyString(correlation.actor_binding_id) : null;
669
+ const runId = correlation ? nonEmptyString(correlation.run_root_id) : null;
670
+ const sessionId = correlation ? nonEmptyString(correlation.operator_session_id) : null;
671
+ const conversationKey = buildConversationKey({
672
+ channel,
673
+ tenantId: channelTenantId,
674
+ conversationId: channelConversationId,
675
+ bindingId: actorBindingId,
676
+ });
677
+ const payload = rec.payload ?? null;
678
+ pushItem(out, {
679
+ id: `cp_commands:mutating:${row.line}`,
680
+ ts_ms: asInt(rec.ts_ms) ?? 0,
681
+ source_kind: "cp_commands",
682
+ source_path: normalizeRelative(repoRoot, path),
683
+ source_line: row.line,
684
+ repo_root: repoRoot,
685
+ issue_id: null,
686
+ run_id: runId,
687
+ session_id: sessionId,
688
+ channel,
689
+ channel_tenant_id: channelTenantId,
690
+ channel_conversation_id: channelConversationId,
691
+ actor_binding_id: actorBindingId,
692
+ conversation_key: conversationKey,
693
+ topic: null,
694
+ author: null,
695
+ role: null,
696
+ tags: ["cp", "domain.mutating", eventType],
697
+ metadata: {
698
+ kind,
699
+ event_type: eventType,
700
+ },
701
+ text: [eventType, textFromUnknown(payload)].filter((part) => part.length > 0).join("\n"),
702
+ });
703
+ }
704
+ }
705
+ return out;
706
+ }
707
+ async function collectOutbox(repoRoot, path) {
708
+ const out = [];
709
+ for (const row of await readJsonlRows(path)) {
710
+ const rec = asRecord(row.value);
711
+ if (!rec || nonEmptyString(rec.kind) !== "outbox.state") {
712
+ continue;
713
+ }
714
+ const record = asRecord(rec.record);
715
+ const envelope = record ? asRecord(record.envelope) : null;
716
+ const correlation = envelope ? asRecord(envelope.correlation) : null;
717
+ const outboxId = record ? nonEmptyString(record.outbox_id) : null;
718
+ const channel = envelope ? nonEmptyString(envelope.channel) : null;
719
+ const channelTenantId = envelope ? nonEmptyString(envelope.channel_tenant_id) : null;
720
+ const channelConversationId = envelope ? nonEmptyString(envelope.channel_conversation_id) : null;
721
+ const actorBindingId = correlation ? nonEmptyString(correlation.actor_binding_id) : null;
722
+ const conversationKey = buildConversationKey({
723
+ channel,
724
+ tenantId: channelTenantId,
725
+ conversationId: channelConversationId,
726
+ bindingId: actorBindingId,
727
+ });
728
+ const runId = correlation ? nonEmptyString(correlation.run_root_id) : null;
729
+ const sessionId = correlation ? nonEmptyString(correlation.operator_session_id) : null;
730
+ const body = envelope ? nonEmptyString(envelope.body) : null;
731
+ const kind = envelope ? nonEmptyString(envelope.kind) : null;
732
+ const state = record ? nonEmptyString(record.state) : null;
733
+ pushItem(out, {
734
+ id: `cp_outbox:${outboxId ?? row.line}`,
735
+ ts_ms: asInt(rec.ts_ms) ?? (record ? asInt(record.updated_at_ms) : null) ?? 0,
736
+ source_kind: "cp_outbox",
737
+ source_path: normalizeRelative(repoRoot, path),
738
+ source_line: row.line,
739
+ repo_root: repoRoot,
740
+ issue_id: null,
741
+ run_id: runId,
742
+ session_id: sessionId,
743
+ channel,
744
+ channel_tenant_id: channelTenantId,
745
+ channel_conversation_id: channelConversationId,
746
+ actor_binding_id: actorBindingId,
747
+ conversation_key: conversationKey,
748
+ topic: null,
749
+ author: null,
750
+ role: null,
751
+ tags: ["cp", "outbox", state ?? "unknown", kind ?? "unknown"],
752
+ metadata: {
753
+ outbox_id: outboxId,
754
+ state,
755
+ kind,
756
+ attempt_count: record ? asInt(record.attempt_count) : null,
757
+ max_attempts: record ? asInt(record.max_attempts) : null,
758
+ },
759
+ text: [kind ?? "", body ?? "", state ?? ""].filter((part) => part.length > 0).join("\n"),
760
+ });
761
+ }
762
+ return out;
763
+ }
764
+ async function collectAdapterAudit(repoRoot, path) {
765
+ const out = [];
766
+ for (const row of await readJsonlRows(path)) {
767
+ const rec = asRecord(row.value);
768
+ if (!rec || nonEmptyString(rec.kind) !== "adapter.audit") {
769
+ continue;
770
+ }
771
+ const channel = nonEmptyString(rec.channel);
772
+ const channelTenantId = nonEmptyString(rec.channel_tenant_id);
773
+ const channelConversationId = nonEmptyString(rec.channel_conversation_id);
774
+ const event = nonEmptyString(rec.event) ?? "adapter.audit";
775
+ const commandText = nonEmptyString(rec.command_text) ?? "";
776
+ const reason = nonEmptyString(rec.reason);
777
+ pushItem(out, {
778
+ id: `cp_adapter_audit:${event}:${row.line}`,
779
+ ts_ms: asInt(rec.ts_ms) ?? 0,
780
+ source_kind: "cp_adapter_audit",
781
+ source_path: normalizeRelative(repoRoot, path),
782
+ source_line: row.line,
783
+ repo_root: repoRoot,
784
+ issue_id: null,
785
+ run_id: null,
786
+ session_id: null,
787
+ channel,
788
+ channel_tenant_id: channelTenantId,
789
+ channel_conversation_id: channelConversationId,
790
+ actor_binding_id: null,
791
+ conversation_key: null,
792
+ topic: null,
793
+ author: nonEmptyString(rec.actor_id),
794
+ role: null,
795
+ tags: ["cp", "adapter.audit", event],
796
+ metadata: {
797
+ request_id: nonEmptyString(rec.request_id),
798
+ delivery_id: nonEmptyString(rec.delivery_id),
799
+ reason,
800
+ },
801
+ text: [event, commandText, reason ?? ""].filter((part) => part.length > 0).join("\n"),
802
+ });
803
+ }
804
+ return out;
805
+ }
806
+ async function collectOperatorTurns(repoRoot, path) {
807
+ const out = [];
808
+ for (const row of await readJsonlRows(path)) {
809
+ const rec = asRecord(row.value);
810
+ if (!rec || nonEmptyString(rec.kind) !== "operator.turn") {
811
+ continue;
812
+ }
813
+ const outcome = nonEmptyString(rec.outcome) ?? "unknown";
814
+ const reason = nonEmptyString(rec.reason);
815
+ const sessionId = nonEmptyString(rec.session_id);
816
+ const commandText = textFromUnknown(rec.command);
817
+ const messagePreview = nonEmptyString(rec.message_preview) ?? "";
818
+ pushItem(out, {
819
+ id: `cp_operator_turns:${sessionId ?? "unknown"}:${row.line}`,
820
+ ts_ms: asInt(rec.ts_ms) ?? 0,
821
+ source_kind: "cp_operator_turns",
822
+ source_path: normalizeRelative(repoRoot, path),
823
+ source_line: row.line,
824
+ repo_root: repoRoot,
825
+ issue_id: null,
826
+ run_id: null,
827
+ session_id: sessionId,
828
+ channel: nonEmptyString(rec.channel),
829
+ channel_tenant_id: null,
830
+ channel_conversation_id: null,
831
+ actor_binding_id: null,
832
+ conversation_key: null,
833
+ topic: null,
834
+ author: null,
835
+ role: null,
836
+ tags: ["cp", "operator.turn", outcome],
837
+ metadata: {
838
+ request_id: nonEmptyString(rec.request_id),
839
+ turn_id: nonEmptyString(rec.turn_id),
840
+ reason,
841
+ },
842
+ text: [outcome, reason ?? "", messagePreview, commandText].filter((part) => part.length > 0).join("\n"),
843
+ });
844
+ }
845
+ return out;
846
+ }
847
+ async function collectTelegramIngress(repoRoot, path) {
848
+ const out = [];
849
+ for (const row of await readJsonlRows(path)) {
850
+ const rec = asRecord(row.value);
851
+ if (!rec || nonEmptyString(rec.kind) !== "telegram.ingress.state") {
852
+ continue;
853
+ }
854
+ const record = asRecord(rec.record);
855
+ const inbound = record ? asRecord(record.inbound) : null;
856
+ const channel = inbound ? nonEmptyString(inbound.channel) : null;
857
+ const channelTenantId = inbound ? nonEmptyString(inbound.channel_tenant_id) : null;
858
+ const channelConversationId = inbound ? nonEmptyString(inbound.channel_conversation_id) : null;
859
+ const actorBindingId = inbound ? nonEmptyString(inbound.actor_binding_id) : null;
860
+ const conversationKey = buildConversationKey({
861
+ channel,
862
+ tenantId: channelTenantId,
863
+ conversationId: channelConversationId,
864
+ bindingId: actorBindingId,
865
+ });
866
+ const ingressId = record ? nonEmptyString(record.ingress_id) : null;
867
+ const state = record ? nonEmptyString(record.state) : null;
868
+ const commandText = inbound ? nonEmptyString(inbound.command_text) : null;
869
+ pushItem(out, {
870
+ id: `cp_telegram_ingress:${ingressId ?? row.line}`,
871
+ ts_ms: asInt(rec.ts_ms) ?? (record ? asInt(record.updated_at_ms) : null) ?? 0,
872
+ source_kind: "cp_telegram_ingress",
873
+ source_path: normalizeRelative(repoRoot, path),
874
+ source_line: row.line,
875
+ repo_root: repoRoot,
876
+ issue_id: null,
877
+ run_id: null,
878
+ session_id: null,
879
+ channel,
880
+ channel_tenant_id: channelTenantId,
881
+ channel_conversation_id: channelConversationId,
882
+ actor_binding_id: actorBindingId,
883
+ conversation_key: conversationKey,
884
+ topic: null,
885
+ author: inbound ? nonEmptyString(inbound.actor_id) : null,
886
+ role: null,
887
+ tags: ["cp", "telegram.ingress", state ?? "unknown"],
888
+ metadata: {
889
+ ingress_id: ingressId,
890
+ state,
891
+ request_id: inbound ? nonEmptyString(inbound.request_id) : null,
892
+ },
893
+ text: [commandText ?? "", state ?? ""].filter((part) => part.length > 0).join("\n"),
894
+ });
895
+ }
896
+ return out;
897
+ }
898
+ function sessionIdFromPath(path) {
899
+ const fileName = path.split(/[\\/]/).pop() ?? path;
900
+ return fileName.replace(/\.jsonl$/i, "") || `session-${crypto.randomUUID()}`;
901
+ }
902
+ async function collectSessionMessages(opts) {
903
+ const out = [];
904
+ const files = await listJsonlFiles(opts.dir);
905
+ for (const filePath of files) {
906
+ const rows = await readJsonlRows(filePath);
907
+ const fileStat = await stat(filePath).catch(() => null);
908
+ let sessionId = null;
909
+ for (const row of rows) {
910
+ const rec = asRecord(row.value);
911
+ if (!rec) {
912
+ continue;
913
+ }
914
+ const entryType = nonEmptyString(rec.type);
915
+ if (entryType === "session") {
916
+ sessionId = nonEmptyString(rec.id) ?? sessionId;
917
+ continue;
918
+ }
919
+ if (entryType !== "message") {
920
+ continue;
921
+ }
922
+ const msg = asRecord(rec.message);
923
+ if (!msg) {
924
+ continue;
925
+ }
926
+ const role = nonEmptyString(msg.role);
927
+ const text = extractMessageText(msg);
928
+ if (text.trim().length === 0) {
929
+ continue;
930
+ }
931
+ const resolvedSessionId = sessionId ?? sessionIdFromPath(filePath);
932
+ const tsMs = parseTimestamp(rec.timestamp) ??
933
+ Math.trunc(fileStat?.mtimeMs ?? fileStat?.ctimeMs ?? fileStat?.birthtimeMs ?? 0);
934
+ const conversationKeys = opts.conversationKeysBySessionId?.get(resolvedSessionId)?.slice().sort((a, b) => a.localeCompare(b)) ?? [];
935
+ pushItem(out, {
936
+ id: `${opts.sourceKind}:${resolvedSessionId}:${row.line}:${normalizeRelative(opts.repoRoot, filePath)}`,
937
+ ts_ms: tsMs,
938
+ source_kind: opts.sourceKind,
939
+ source_path: normalizeRelative(opts.repoRoot, filePath),
940
+ source_line: row.line,
941
+ repo_root: opts.repoRoot,
942
+ issue_id: null,
943
+ run_id: null,
944
+ session_id: resolvedSessionId,
945
+ channel: null,
946
+ channel_tenant_id: null,
947
+ channel_conversation_id: null,
948
+ actor_binding_id: null,
949
+ conversation_key: conversationKeys[0] ?? null,
950
+ topic: null,
951
+ author: null,
952
+ role,
953
+ tags: ["session", opts.sourceKind, role ?? "unknown"],
954
+ metadata: {
955
+ entry_id: nonEmptyString(rec.id),
956
+ parent_id: nonEmptyString(rec.parentId),
957
+ conversation_keys: conversationKeys,
958
+ },
959
+ text,
960
+ });
961
+ }
962
+ }
963
+ return out;
964
+ }
965
+ async function collectContextItems(repoRoot, requestedSources) {
966
+ const include = (kind) => (requestedSources ? requestedSources.has(kind) : true);
967
+ const paths = getStorePaths(repoRoot);
968
+ const cp = getControlPlanePaths(repoRoot);
969
+ const cpDir = cp.controlPlaneDir;
970
+ const conversationMap = await loadConversationBindings(join(cpDir, "operator_conversations.json")).catch(() => new Map());
971
+ const conversationKeysBySessionId = reverseConversationBindings(conversationMap);
972
+ const tasks = [];
973
+ if (include("issues")) {
974
+ tasks.push(collectIssues(repoRoot, paths.issuesPath));
975
+ }
976
+ if (include("forum")) {
977
+ tasks.push(collectForum(repoRoot, paths.forumPath));
978
+ }
979
+ if (include("events")) {
980
+ tasks.push(collectEvents(repoRoot, paths.eventsPath));
981
+ }
982
+ if (include("cp_commands")) {
983
+ tasks.push(collectCommandJournal(repoRoot, cp.commandsPath));
984
+ }
985
+ if (include("cp_outbox")) {
986
+ tasks.push(collectOutbox(repoRoot, cp.outboxPath));
987
+ }
988
+ if (include("cp_adapter_audit")) {
989
+ tasks.push(collectAdapterAudit(repoRoot, cp.adapterAuditPath));
990
+ }
991
+ if (include("cp_operator_turns")) {
992
+ tasks.push(collectOperatorTurns(repoRoot, join(cpDir, "operator_turns.jsonl")));
993
+ }
994
+ if (include("cp_telegram_ingress")) {
995
+ tasks.push(collectTelegramIngress(repoRoot, join(cpDir, "telegram_ingress.jsonl")));
996
+ }
997
+ if (include("operator_sessions")) {
998
+ tasks.push(collectSessionMessages({
999
+ repoRoot,
1000
+ dir: join(repoRoot, ".mu", "operator", "sessions"),
1001
+ sourceKind: "operator_sessions",
1002
+ }));
1003
+ }
1004
+ if (include("cp_operator_sessions")) {
1005
+ tasks.push(collectSessionMessages({
1006
+ repoRoot,
1007
+ dir: join(cpDir, "operator-sessions"),
1008
+ sourceKind: "cp_operator_sessions",
1009
+ conversationKeysBySessionId,
1010
+ }));
1011
+ }
1012
+ const chunks = await Promise.all(tasks);
1013
+ const items = chunks.flat();
1014
+ items.sort((a, b) => {
1015
+ if (a.ts_ms !== b.ts_ms) {
1016
+ return b.ts_ms - a.ts_ms;
1017
+ }
1018
+ return a.id.localeCompare(b.id);
1019
+ });
1020
+ return items;
1021
+ }
1022
+ function searchContext(items, filters) {
1023
+ const scored = [];
1024
+ for (const item of items) {
1025
+ if (!matchSearchFilters(item, filters)) {
1026
+ continue;
1027
+ }
1028
+ const score = scoreItem(item, filters.query);
1029
+ if (score < 0) {
1030
+ continue;
1031
+ }
1032
+ scored.push({ ...item, score });
1033
+ }
1034
+ scored.sort((a, b) => {
1035
+ if (a.score !== b.score) {
1036
+ return b.score - a.score;
1037
+ }
1038
+ if (a.ts_ms !== b.ts_ms) {
1039
+ return b.ts_ms - a.ts_ms;
1040
+ }
1041
+ return a.id.localeCompare(b.id);
1042
+ });
1043
+ return scored;
1044
+ }
1045
+ function timelineContext(items, filters) {
1046
+ const out = items.filter((item) => matchSearchFilters(item, filters));
1047
+ out.sort((a, b) => {
1048
+ if (a.ts_ms !== b.ts_ms) {
1049
+ return filters.order === "asc" ? a.ts_ms - b.ts_ms : b.ts_ms - a.ts_ms;
1050
+ }
1051
+ return a.id.localeCompare(b.id);
1052
+ });
1053
+ if (filters.query) {
1054
+ const query = filters.query.toLowerCase();
1055
+ const tokens = tokenizeQuery(query);
1056
+ return out.filter((item) => {
1057
+ const haystack = searchableText(item);
1058
+ if (haystack.includes(query)) {
1059
+ return true;
1060
+ }
1061
+ if (tokens.length === 0) {
1062
+ return true;
1063
+ }
1064
+ return tokens.every((token) => haystack.includes(token));
1065
+ });
1066
+ }
1067
+ return out;
1068
+ }
1069
+ function buildSourceStats(items) {
1070
+ const map = new Map();
1071
+ for (const item of items) {
1072
+ const row = map.get(item.source_kind) ?? { count: 0, textBytes: 0, lastTsMs: 0 };
1073
+ row.count += 1;
1074
+ row.textBytes += item.text.length;
1075
+ row.lastTsMs = Math.max(row.lastTsMs, item.ts_ms);
1076
+ map.set(item.source_kind, row);
1077
+ }
1078
+ const out = [...map.entries()].map(([source, row]) => ({
1079
+ source_kind: source,
1080
+ count: row.count,
1081
+ text_bytes: row.textBytes,
1082
+ last_ts_ms: row.lastTsMs,
1083
+ }));
1084
+ out.sort((a, b) => {
1085
+ if (a.count !== b.count) {
1086
+ return b.count - a.count;
1087
+ }
1088
+ return a.source_kind.localeCompare(b.source_kind);
1089
+ });
1090
+ return out;
1091
+ }
1092
+ export async function contextRoutes(request, url, deps, headers) {
1093
+ if (request.method !== "GET") {
1094
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1095
+ }
1096
+ const path = url.pathname.replace("/api/context", "") || "/";
1097
+ try {
1098
+ if (path === "/" || path === "/search") {
1099
+ const filters = parseSearchFilters(url);
1100
+ const items = await collectContextItems(deps.context.repoRoot, filters.sources);
1101
+ const ranked = searchContext(items, filters);
1102
+ const sliced = ranked.slice(0, filters.limit);
1103
+ return Response.json({
1104
+ mode: "search",
1105
+ repo_root: deps.context.repoRoot,
1106
+ query: filters.query,
1107
+ count: sliced.length,
1108
+ total: ranked.length,
1109
+ items: sliced,
1110
+ }, { headers });
1111
+ }
1112
+ if (path === "/timeline") {
1113
+ const filters = parseTimelineFilters(url);
1114
+ const items = await collectContextItems(deps.context.repoRoot, filters.sources);
1115
+ const timeline = timelineContext(items, filters);
1116
+ const sliced = timeline.slice(0, filters.limit);
1117
+ return Response.json({
1118
+ mode: "timeline",
1119
+ repo_root: deps.context.repoRoot,
1120
+ order: filters.order,
1121
+ count: sliced.length,
1122
+ total: timeline.length,
1123
+ items: sliced,
1124
+ }, { headers });
1125
+ }
1126
+ if (path === "/stats") {
1127
+ const filters = parseSearchFilters(url);
1128
+ const items = await collectContextItems(deps.context.repoRoot, filters.sources);
1129
+ const filtered = items.filter((item) => matchSearchFilters(item, { ...filters, query: null }));
1130
+ const sources = buildSourceStats(filtered);
1131
+ return Response.json({
1132
+ mode: "stats",
1133
+ repo_root: deps.context.repoRoot,
1134
+ total_count: filtered.length,
1135
+ total_text_bytes: filtered.reduce((sum, item) => sum + item.text.length, 0),
1136
+ sources,
1137
+ }, { headers });
1138
+ }
1139
+ return Response.json({ error: "Not Found" }, { status: 404, headers });
1140
+ }
1141
+ catch (err) {
1142
+ if (err instanceof QueryValidationError) {
1143
+ return Response.json({ error: err.message }, { status: err.status, headers });
1144
+ }
1145
+ return Response.json({ error: `context query failed: ${deps.describeError(err)}` }, { status: 500, headers });
1146
+ }
1147
+ }