@femtomc/mu-server 26.2.73 → 26.2.74

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 (51) hide show
  1. package/README.md +54 -66
  2. package/dist/api/control_plane.js +56 -0
  3. package/dist/api/cron.js +2 -23
  4. package/dist/api/heartbeats.js +1 -66
  5. package/dist/api/identities.js +3 -2
  6. package/dist/api/runs.js +0 -83
  7. package/dist/api/session_flash.d.ts +60 -0
  8. package/dist/api/session_flash.js +326 -0
  9. package/dist/api/session_turn.d.ts +38 -0
  10. package/dist/api/session_turn.js +423 -0
  11. package/dist/config.d.ts +9 -4
  12. package/dist/config.js +24 -24
  13. package/dist/control_plane.d.ts +2 -16
  14. package/dist/control_plane.js +57 -83
  15. package/dist/control_plane_adapter_registry.d.ts +19 -0
  16. package/dist/control_plane_adapter_registry.js +74 -0
  17. package/dist/control_plane_contract.d.ts +1 -7
  18. package/dist/control_plane_run_queue_coordinator.d.ts +1 -7
  19. package/dist/control_plane_run_queue_coordinator.js +1 -62
  20. package/dist/control_plane_telegram_generation.js +1 -0
  21. package/dist/control_plane_wake_delivery.js +1 -0
  22. package/dist/cron_programs.d.ts +21 -35
  23. package/dist/cron_programs.js +32 -113
  24. package/dist/cron_request.d.ts +0 -6
  25. package/dist/cron_request.js +0 -41
  26. package/dist/heartbeat_programs.d.ts +20 -35
  27. package/dist/heartbeat_programs.js +26 -122
  28. package/dist/index.d.ts +2 -2
  29. package/dist/outbound_delivery_router.d.ts +12 -0
  30. package/dist/outbound_delivery_router.js +29 -0
  31. package/dist/run_supervisor.d.ts +1 -16
  32. package/dist/run_supervisor.js +0 -70
  33. package/dist/server.d.ts +0 -5
  34. package/dist/server.js +95 -127
  35. package/dist/server_program_orchestration.d.ts +4 -19
  36. package/dist/server_program_orchestration.js +49 -200
  37. package/dist/server_routing.d.ts +0 -9
  38. package/dist/server_routing.js +19 -654
  39. package/dist/server_runtime.js +0 -1
  40. package/dist/server_types.d.ts +0 -2
  41. package/dist/server_types.js +0 -7
  42. package/package.json +6 -9
  43. package/dist/api/context.d.ts +0 -5
  44. package/dist/api/context.js +0 -1147
  45. package/dist/api/forum.d.ts +0 -2
  46. package/dist/api/forum.js +0 -75
  47. package/dist/api/issues.d.ts +0 -2
  48. package/dist/api/issues.js +0 -173
  49. package/public/assets/index-CxkevQNh.js +0 -100
  50. package/public/assets/index-D_8anM-D.css +0 -1
  51. package/public/index.html +0 -14
@@ -0,0 +1,326 @@
1
+ import { appendJsonl, readJsonl } from "@femtomc/mu-core/node";
2
+ import { join } from "node:path";
3
+ function asRecord(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5
+ return null;
6
+ }
7
+ return value;
8
+ }
9
+ function nonEmptyString(value) {
10
+ if (typeof value !== "string") {
11
+ return null;
12
+ }
13
+ const trimmed = value.trim();
14
+ return trimmed.length > 0 ? trimmed : null;
15
+ }
16
+ function finiteInt(value) {
17
+ if (typeof value !== "number" || !Number.isFinite(value)) {
18
+ return null;
19
+ }
20
+ return Math.trunc(value);
21
+ }
22
+ function parseStringList(value) {
23
+ if (Array.isArray(value)) {
24
+ const out = [];
25
+ for (const item of value) {
26
+ const parsed = nonEmptyString(item);
27
+ if (parsed) {
28
+ out.push(parsed);
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ if (typeof value === "string") {
34
+ return value
35
+ .split(",")
36
+ .map((part) => part.trim())
37
+ .filter((part) => part.length > 0);
38
+ }
39
+ return [];
40
+ }
41
+ function parseLimit(value, fallback, max) {
42
+ if (value == null) {
43
+ return fallback;
44
+ }
45
+ const parsed = Number.parseInt(value, 10);
46
+ if (!Number.isFinite(parsed)) {
47
+ return fallback;
48
+ }
49
+ return Math.max(1, Math.min(max, Math.trunc(parsed)));
50
+ }
51
+ function sessionFlashPath(repoRoot) {
52
+ return join(repoRoot, ".mu", "control-plane", "session_flash.jsonl");
53
+ }
54
+ export function getSessionFlashPath(repoRoot) {
55
+ return sessionFlashPath(repoRoot);
56
+ }
57
+ function normalizeCreateRow(row) {
58
+ return {
59
+ flash_id: row.flash_id,
60
+ created_at_ms: row.ts_ms,
61
+ session_id: row.session_id,
62
+ session_kind: row.session_kind,
63
+ body: row.body,
64
+ context_ids: row.context_ids,
65
+ source: row.source,
66
+ metadata: row.metadata,
67
+ from: row.from,
68
+ status: "pending",
69
+ delivered_at_ms: null,
70
+ delivered_by: null,
71
+ delivery_note: null,
72
+ };
73
+ }
74
+ async function loadSessionFlashState(repoRoot) {
75
+ const rows = await readJsonl(sessionFlashPath(repoRoot));
76
+ const state = new Map();
77
+ for (const row of rows) {
78
+ const rec = asRecord(row);
79
+ if (!rec) {
80
+ continue;
81
+ }
82
+ const kind = nonEmptyString(rec.kind);
83
+ if (kind === "session_flash.create") {
84
+ const flashId = nonEmptyString(rec.flash_id);
85
+ const tsMs = finiteInt(rec.ts_ms);
86
+ const sessionId = nonEmptyString(rec.session_id);
87
+ const body = nonEmptyString(rec.body);
88
+ if (!flashId || tsMs == null || !sessionId || !body) {
89
+ continue;
90
+ }
91
+ const from = asRecord(rec.from);
92
+ const createRow = {
93
+ kind: "session_flash.create",
94
+ ts_ms: tsMs,
95
+ flash_id: flashId,
96
+ session_id: sessionId,
97
+ session_kind: nonEmptyString(rec.session_kind),
98
+ body,
99
+ context_ids: parseStringList(rec.context_ids),
100
+ source: nonEmptyString(rec.source),
101
+ metadata: asRecord(rec.metadata) ?? {},
102
+ from: {
103
+ channel: nonEmptyString(from?.channel),
104
+ channel_tenant_id: nonEmptyString(from?.channel_tenant_id),
105
+ channel_conversation_id: nonEmptyString(from?.channel_conversation_id),
106
+ actor_binding_id: nonEmptyString(from?.actor_binding_id),
107
+ },
108
+ };
109
+ state.set(flashId, normalizeCreateRow(createRow));
110
+ continue;
111
+ }
112
+ if (kind === "session_flash.delivery") {
113
+ const flashId = nonEmptyString(rec.flash_id);
114
+ const sessionId = nonEmptyString(rec.session_id);
115
+ const tsMs = finiteInt(rec.ts_ms);
116
+ if (!flashId || !sessionId || tsMs == null) {
117
+ continue;
118
+ }
119
+ const current = state.get(flashId);
120
+ if (!current) {
121
+ continue;
122
+ }
123
+ if (current.session_id !== sessionId) {
124
+ continue;
125
+ }
126
+ state.set(flashId, {
127
+ ...current,
128
+ status: "delivered",
129
+ delivered_at_ms: tsMs,
130
+ delivered_by: nonEmptyString(rec.delivered_by),
131
+ delivery_note: nonEmptyString(rec.note),
132
+ });
133
+ }
134
+ }
135
+ return state;
136
+ }
137
+ export async function listSessionFlashRecords(opts) {
138
+ const state = await loadSessionFlashState(opts.repoRoot);
139
+ const status = opts.status ?? "all";
140
+ const contains = nonEmptyString(opts.contains)?.toLowerCase() ?? null;
141
+ const out = [...state.values()].filter((record) => {
142
+ if (opts.sessionId && record.session_id !== opts.sessionId) {
143
+ return false;
144
+ }
145
+ if (opts.sessionKind && record.session_kind !== opts.sessionKind) {
146
+ return false;
147
+ }
148
+ if (status !== "all" && record.status !== status) {
149
+ return false;
150
+ }
151
+ if (contains) {
152
+ const haystack = [record.body, record.flash_id, record.session_id, record.context_ids.join(" ")]
153
+ .join("\n")
154
+ .toLowerCase();
155
+ if (!haystack.includes(contains)) {
156
+ return false;
157
+ }
158
+ }
159
+ return true;
160
+ });
161
+ out.sort((a, b) => {
162
+ if (a.created_at_ms !== b.created_at_ms) {
163
+ return b.created_at_ms - a.created_at_ms;
164
+ }
165
+ return a.flash_id.localeCompare(b.flash_id);
166
+ });
167
+ const limit = Math.max(1, Math.trunc(opts.limit ?? 50));
168
+ return out.slice(0, limit);
169
+ }
170
+ export async function getSessionFlashRecord(opts) {
171
+ const state = await loadSessionFlashState(opts.repoRoot);
172
+ return state.get(opts.flashId) ?? null;
173
+ }
174
+ export async function createSessionFlashRecord(opts) {
175
+ const nowMs = Math.trunc(opts.nowMs ?? Date.now());
176
+ const flashId = `flash-${crypto.randomUUID()}`;
177
+ const row = {
178
+ kind: "session_flash.create",
179
+ ts_ms: nowMs,
180
+ flash_id: flashId,
181
+ session_id: opts.sessionId,
182
+ session_kind: opts.sessionKind ?? null,
183
+ body: opts.body,
184
+ context_ids: opts.contextIds ?? [],
185
+ source: opts.source ?? null,
186
+ metadata: opts.metadata ?? {},
187
+ from: {
188
+ channel: opts.from?.channel ?? null,
189
+ channel_tenant_id: opts.from?.channel_tenant_id ?? null,
190
+ channel_conversation_id: opts.from?.channel_conversation_id ?? null,
191
+ actor_binding_id: opts.from?.actor_binding_id ?? null,
192
+ },
193
+ };
194
+ await appendJsonl(sessionFlashPath(opts.repoRoot), row);
195
+ return normalizeCreateRow(row);
196
+ }
197
+ export async function ackSessionFlashRecord(opts) {
198
+ const current = await getSessionFlashRecord({ repoRoot: opts.repoRoot, flashId: opts.flashId });
199
+ if (!current) {
200
+ return null;
201
+ }
202
+ const sessionId = opts.sessionId ?? current.session_id;
203
+ const row = {
204
+ kind: "session_flash.delivery",
205
+ ts_ms: Math.trunc(opts.nowMs ?? Date.now()),
206
+ flash_id: current.flash_id,
207
+ session_id: sessionId,
208
+ delivered_by: nonEmptyString(opts.deliveredBy) ?? "api_ack",
209
+ note: nonEmptyString(opts.note),
210
+ };
211
+ await appendJsonl(sessionFlashPath(opts.repoRoot), row);
212
+ return {
213
+ ...current,
214
+ status: "delivered",
215
+ delivered_at_ms: row.ts_ms,
216
+ delivered_by: row.delivered_by,
217
+ delivery_note: row.note,
218
+ };
219
+ }
220
+ export async function sessionFlashRoutes(request, url, deps, headers) {
221
+ const path = url.pathname;
222
+ if (path === "/api/session-flash") {
223
+ if (request.method === "GET") {
224
+ const statusRaw = nonEmptyString(url.searchParams.get("status"))?.toLowerCase();
225
+ const status = statusRaw === "pending" || statusRaw === "delivered" ? statusRaw : "all";
226
+ const flashes = await listSessionFlashRecords({
227
+ repoRoot: deps.context.repoRoot,
228
+ sessionId: nonEmptyString(url.searchParams.get("session_id")),
229
+ sessionKind: nonEmptyString(url.searchParams.get("session_kind")),
230
+ status,
231
+ contains: nonEmptyString(url.searchParams.get("contains")),
232
+ limit: parseLimit(url.searchParams.get("limit"), 50, 500),
233
+ });
234
+ return Response.json({
235
+ ok: true,
236
+ count: flashes.length,
237
+ status,
238
+ flashes,
239
+ }, { headers });
240
+ }
241
+ if (request.method !== "POST") {
242
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
243
+ }
244
+ let body;
245
+ try {
246
+ body = (await request.json());
247
+ }
248
+ catch {
249
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
250
+ }
251
+ const sessionId = nonEmptyString(body.session_id);
252
+ const messageBody = nonEmptyString(body.body);
253
+ if (!sessionId) {
254
+ return Response.json({ error: "session_id is required" }, { status: 400, headers });
255
+ }
256
+ if (!messageBody) {
257
+ return Response.json({ error: "body is required" }, { status: 400, headers });
258
+ }
259
+ if (body.metadata != null && !asRecord(body.metadata)) {
260
+ return Response.json({ error: "metadata must be an object" }, { status: 400, headers });
261
+ }
262
+ const from = asRecord(body.from);
263
+ const flash = await createSessionFlashRecord({
264
+ repoRoot: deps.context.repoRoot,
265
+ sessionId,
266
+ body: messageBody,
267
+ sessionKind: nonEmptyString(body.session_kind),
268
+ contextIds: parseStringList(body.context_ids),
269
+ source: nonEmptyString(body.source),
270
+ metadata: asRecord(body.metadata) ?? {},
271
+ from: {
272
+ channel: nonEmptyString(from?.channel),
273
+ channel_tenant_id: nonEmptyString(from?.channel_tenant_id),
274
+ channel_conversation_id: nonEmptyString(from?.channel_conversation_id),
275
+ actor_binding_id: nonEmptyString(from?.actor_binding_id),
276
+ },
277
+ });
278
+ return Response.json({ ok: true, flash }, { status: 201, headers });
279
+ }
280
+ if (path === "/api/session-flash/ack") {
281
+ if (request.method !== "POST") {
282
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
283
+ }
284
+ let body;
285
+ try {
286
+ body = (await request.json());
287
+ }
288
+ catch {
289
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
290
+ }
291
+ const flashId = nonEmptyString(body.flash_id);
292
+ if (!flashId) {
293
+ return Response.json({ error: "flash_id is required" }, { status: 400, headers });
294
+ }
295
+ const flash = await ackSessionFlashRecord({
296
+ repoRoot: deps.context.repoRoot,
297
+ flashId,
298
+ sessionId: nonEmptyString(body.session_id),
299
+ deliveredBy: nonEmptyString(body.delivered_by),
300
+ note: nonEmptyString(body.note),
301
+ });
302
+ if (!flash) {
303
+ return Response.json({ error: "flash not found" }, { status: 404, headers });
304
+ }
305
+ return Response.json({ ok: true, flash }, { headers });
306
+ }
307
+ if (path.startsWith("/api/session-flash/")) {
308
+ if (request.method !== "GET") {
309
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
310
+ }
311
+ const rawId = path.slice("/api/session-flash/".length);
312
+ const flashId = nonEmptyString(decodeURIComponent(rawId));
313
+ if (!flashId) {
314
+ return Response.json({ error: "flash id is required" }, { status: 400, headers });
315
+ }
316
+ const flash = await getSessionFlashRecord({
317
+ repoRoot: deps.context.repoRoot,
318
+ flashId,
319
+ });
320
+ if (!flash) {
321
+ return Response.json({ error: "flash not found" }, { status: 404, headers });
322
+ }
323
+ return Response.json({ ok: true, flash }, { headers });
324
+ }
325
+ return Response.json({ error: "Not Found" }, { status: 404, headers });
326
+ }
@@ -0,0 +1,38 @@
1
+ import { type CreateMuSessionOpts, type MuSession } from "@femtomc/mu-agent";
2
+ import type { ServerRoutingDependencies } from "../server_routing.js";
3
+ export type SessionTurnRequest = {
4
+ session_id: string;
5
+ session_kind: string | null;
6
+ body: string;
7
+ source: string | null;
8
+ provider: string | null;
9
+ model: string | null;
10
+ thinking: string | null;
11
+ session_file: string | null;
12
+ session_dir: string | null;
13
+ extension_profile: string | null;
14
+ };
15
+ export type SessionTurnResult = {
16
+ session_id: string;
17
+ session_kind: string | null;
18
+ session_file: string;
19
+ context_entry_id: string | null;
20
+ reply: string;
21
+ source: string | null;
22
+ completed_at_ms: number;
23
+ };
24
+ export declare class SessionTurnError extends Error {
25
+ readonly status: number;
26
+ constructor(status: number, message: string);
27
+ }
28
+ export declare function parseSessionTurnRequest(body: Record<string, unknown>): {
29
+ request: SessionTurnRequest | null;
30
+ error: string | null;
31
+ };
32
+ export declare function executeSessionTurn(opts: {
33
+ repoRoot: string;
34
+ request: SessionTurnRequest;
35
+ sessionFactory?: (opts: CreateMuSessionOpts) => Promise<MuSession>;
36
+ nowMs?: () => number;
37
+ }): Promise<SessionTurnResult>;
38
+ export declare function sessionTurnRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;