@femtomc/mu-server 26.2.73 → 26.2.75

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,423 @@
1
+ import { createMuSession, DEFAULT_OPERATOR_SYSTEM_PROMPT, DEFAULT_ORCHESTRATOR_PROMPT, DEFAULT_REVIEWER_PROMPT, DEFAULT_WORKER_PROMPT, operatorExtensionPaths, orchestratorToolExtensionPaths, workerToolExtensionPaths, } from "@femtomc/mu-agent";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ export class SessionTurnError extends Error {
5
+ status;
6
+ constructor(status, message) {
7
+ super(message);
8
+ this.status = status;
9
+ }
10
+ }
11
+ function nonEmptyString(value) {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const trimmed = value.trim();
16
+ return trimmed.length > 0 ? trimmed : null;
17
+ }
18
+ function asRecord(value) {
19
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20
+ return null;
21
+ }
22
+ return value;
23
+ }
24
+ function normalizeSessionKind(value) {
25
+ if (!value) {
26
+ return null;
27
+ }
28
+ const normalized = value.trim().toLowerCase().replaceAll("-", "_");
29
+ if (normalized === "cpoperator" || normalized === "control_plane_operator") {
30
+ return "cp_operator";
31
+ }
32
+ return normalized;
33
+ }
34
+ function normalizeExtensionProfile(value) {
35
+ if (!value) {
36
+ return null;
37
+ }
38
+ const normalized = value.trim().toLowerCase();
39
+ if (normalized === "operator" ||
40
+ normalized === "worker" ||
41
+ normalized === "orchestrator" ||
42
+ normalized === "reviewer" ||
43
+ normalized === "none") {
44
+ return normalized;
45
+ }
46
+ return null;
47
+ }
48
+ function sessionFileStem(sessionId) {
49
+ const normalized = sessionId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
50
+ const compact = normalized.replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
51
+ return compact.length > 0 ? compact : "session";
52
+ }
53
+ function resolveRepoPath(repoRoot, candidate) {
54
+ return isAbsolute(candidate) ? resolve(candidate) : resolve(repoRoot, candidate);
55
+ }
56
+ function defaultSessionDirForKind(repoRoot, sessionKind) {
57
+ switch (sessionKind) {
58
+ case "operator":
59
+ return join(repoRoot, ".mu", "operator", "sessions");
60
+ case "cp_operator":
61
+ return join(repoRoot, ".mu", "control-plane", "operator-sessions");
62
+ case "orchestrator":
63
+ return join(repoRoot, ".mu", "orchestrator", "sessions");
64
+ case "worker":
65
+ return join(repoRoot, ".mu", "worker", "sessions");
66
+ case "reviewer":
67
+ return join(repoRoot, ".mu", "reviewer", "sessions");
68
+ default:
69
+ return join(repoRoot, ".mu", "control-plane", "operator-sessions");
70
+ }
71
+ }
72
+ async function pathExists(path) {
73
+ try {
74
+ await stat(path);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ async function directoryExists(path) {
82
+ try {
83
+ const info = await stat(path);
84
+ return info.isDirectory();
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ async function readSessionHeaderId(sessionFile) {
91
+ let raw;
92
+ try {
93
+ raw = await readFile(sessionFile, "utf8");
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ const firstLine = raw
99
+ .split("\n")
100
+ .map((line) => line.trim())
101
+ .find((line) => line.length > 0);
102
+ if (!firstLine) {
103
+ return null;
104
+ }
105
+ try {
106
+ const parsed = JSON.parse(firstLine);
107
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
108
+ return null;
109
+ }
110
+ const header = parsed;
111
+ if (header.type !== "session") {
112
+ return null;
113
+ }
114
+ return nonEmptyString(header.id);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ async function resolveSessionFileById(opts) {
121
+ const direct = join(opts.sessionDir, `${sessionFileStem(opts.sessionId)}.jsonl`);
122
+ if (await pathExists(direct)) {
123
+ const headerId = await readSessionHeaderId(direct);
124
+ if (headerId === opts.sessionId) {
125
+ return direct;
126
+ }
127
+ }
128
+ const entries = await readdir(opts.sessionDir, { withFileTypes: true });
129
+ for (const entry of entries) {
130
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
131
+ continue;
132
+ }
133
+ const filePath = join(opts.sessionDir, entry.name);
134
+ if (filePath === direct) {
135
+ continue;
136
+ }
137
+ const headerId = await readSessionHeaderId(filePath);
138
+ if (headerId === opts.sessionId) {
139
+ return filePath;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ function extensionPathsForTurn(opts) {
145
+ if (opts.extensionProfile === "none") {
146
+ return [];
147
+ }
148
+ if (opts.extensionProfile === "operator") {
149
+ return [...operatorExtensionPaths];
150
+ }
151
+ if (opts.extensionProfile === "orchestrator") {
152
+ return [...orchestratorToolExtensionPaths];
153
+ }
154
+ if (opts.extensionProfile === "worker" || opts.extensionProfile === "reviewer") {
155
+ return [...workerToolExtensionPaths];
156
+ }
157
+ if (opts.sessionKind === "operator" || opts.sessionKind === "cp_operator") {
158
+ return [...operatorExtensionPaths];
159
+ }
160
+ if (opts.sessionKind === "orchestrator") {
161
+ return [...orchestratorToolExtensionPaths];
162
+ }
163
+ if (opts.sessionKind === "worker" || opts.sessionKind === "reviewer") {
164
+ return [...workerToolExtensionPaths];
165
+ }
166
+ return [...operatorExtensionPaths];
167
+ }
168
+ function systemPromptForTurn(opts) {
169
+ const role = opts.extensionProfile ?? opts.sessionKind;
170
+ if (role === "operator" || role === "cp_operator") {
171
+ return DEFAULT_OPERATOR_SYSTEM_PROMPT;
172
+ }
173
+ if (role === "orchestrator") {
174
+ return DEFAULT_ORCHESTRATOR_PROMPT;
175
+ }
176
+ if (role === "reviewer") {
177
+ return DEFAULT_REVIEWER_PROMPT;
178
+ }
179
+ if (role === "worker") {
180
+ return DEFAULT_WORKER_PROMPT;
181
+ }
182
+ return undefined;
183
+ }
184
+ function extractAssistantText(message) {
185
+ if (!message || typeof message !== "object") {
186
+ return "";
187
+ }
188
+ const record = message;
189
+ if (typeof record.text === "string") {
190
+ return record.text;
191
+ }
192
+ if (typeof record.content === "string") {
193
+ return record.content;
194
+ }
195
+ if (Array.isArray(record.content)) {
196
+ const parts = [];
197
+ for (const item of record.content) {
198
+ if (typeof item === "string") {
199
+ if (item.trim().length > 0) {
200
+ parts.push(item);
201
+ }
202
+ continue;
203
+ }
204
+ if (!item || typeof item !== "object") {
205
+ continue;
206
+ }
207
+ const text = nonEmptyString(item.text);
208
+ if (text) {
209
+ parts.push(text);
210
+ }
211
+ }
212
+ return parts.join("\n");
213
+ }
214
+ return "";
215
+ }
216
+ function safeLeafId(session) {
217
+ const manager = session.sessionManager;
218
+ if (!manager || typeof manager.getLeafId !== "function") {
219
+ return null;
220
+ }
221
+ const value = manager.getLeafId();
222
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
223
+ }
224
+ function safeSessionId(session) {
225
+ const value = session.sessionId;
226
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
227
+ }
228
+ function safeSessionFile(session) {
229
+ const value = session.sessionFile;
230
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
231
+ }
232
+ async function resolveSessionTarget(opts) {
233
+ const sessionDir = opts.request.session_dir
234
+ ? resolveRepoPath(opts.repoRoot, opts.request.session_dir)
235
+ : defaultSessionDirForKind(opts.repoRoot, opts.normalizedSessionKind);
236
+ if (opts.request.session_file) {
237
+ const sessionFile = resolveRepoPath(opts.repoRoot, opts.request.session_file);
238
+ if (!(await pathExists(sessionFile))) {
239
+ throw new SessionTurnError(404, `session_file not found: ${sessionFile}`);
240
+ }
241
+ const headerId = await readSessionHeaderId(sessionFile);
242
+ if (!headerId) {
243
+ throw new SessionTurnError(400, `session_file is missing a valid session header: ${sessionFile}`);
244
+ }
245
+ if (headerId !== opts.request.session_id) {
246
+ throw new SessionTurnError(409, `session_file header id mismatch (expected ${opts.request.session_id}, found ${headerId})`);
247
+ }
248
+ return {
249
+ sessionFile,
250
+ sessionDir: opts.request.session_dir ? sessionDir : dirname(sessionFile),
251
+ };
252
+ }
253
+ if (!(await directoryExists(sessionDir))) {
254
+ throw new SessionTurnError(404, `session directory not found: ${sessionDir}`);
255
+ }
256
+ const sessionFile = await resolveSessionFileById({
257
+ sessionDir,
258
+ sessionId: opts.request.session_id,
259
+ });
260
+ if (!sessionFile) {
261
+ throw new SessionTurnError(404, `session_id not found in ${sessionDir}: ${opts.request.session_id}`);
262
+ }
263
+ return { sessionFile, sessionDir };
264
+ }
265
+ export function parseSessionTurnRequest(body) {
266
+ const sessionId = nonEmptyString(body.session_id);
267
+ if (!sessionId) {
268
+ return { request: null, error: "session_id is required" };
269
+ }
270
+ const messageBody = nonEmptyString(body.body) ?? nonEmptyString(body.message) ?? nonEmptyString(body.prompt);
271
+ if (!messageBody) {
272
+ return { request: null, error: "body (or message/prompt) is required" };
273
+ }
274
+ const extensionProfileRaw = nonEmptyString(body.extension_profile);
275
+ if (extensionProfileRaw && !normalizeExtensionProfile(extensionProfileRaw)) {
276
+ return {
277
+ request: null,
278
+ error: "extension_profile must be one of operator|worker|orchestrator|reviewer|none",
279
+ };
280
+ }
281
+ return {
282
+ request: {
283
+ session_id: sessionId,
284
+ session_kind: nonEmptyString(body.session_kind),
285
+ body: messageBody,
286
+ source: nonEmptyString(body.source),
287
+ provider: nonEmptyString(body.provider),
288
+ model: nonEmptyString(body.model),
289
+ thinking: nonEmptyString(body.thinking),
290
+ session_file: nonEmptyString(body.session_file),
291
+ session_dir: nonEmptyString(body.session_dir),
292
+ extension_profile: extensionProfileRaw,
293
+ },
294
+ error: null,
295
+ };
296
+ }
297
+ export async function executeSessionTurn(opts) {
298
+ const normalizedSessionKind = normalizeSessionKind(opts.request.session_kind);
299
+ const extensionProfile = normalizeExtensionProfile(opts.request.extension_profile);
300
+ const target = await resolveSessionTarget({
301
+ repoRoot: opts.repoRoot,
302
+ request: opts.request,
303
+ normalizedSessionKind,
304
+ });
305
+ const sessionFactory = opts.sessionFactory ?? createMuSession;
306
+ const session = await sessionFactory({
307
+ cwd: opts.repoRoot,
308
+ systemPrompt: systemPromptForTurn({
309
+ sessionKind: normalizedSessionKind,
310
+ extensionProfile,
311
+ }),
312
+ provider: opts.request.provider ?? undefined,
313
+ model: opts.request.model ?? undefined,
314
+ thinking: opts.request.thinking ?? undefined,
315
+ extensionPaths: extensionPathsForTurn({
316
+ sessionKind: normalizedSessionKind,
317
+ extensionProfile,
318
+ }),
319
+ session: {
320
+ mode: "open",
321
+ sessionDir: target.sessionDir,
322
+ sessionFile: target.sessionFile,
323
+ },
324
+ });
325
+ let assistantText = "";
326
+ let contextEntryId = null;
327
+ let resolvedSessionId = null;
328
+ let resolvedSessionFile = null;
329
+ const nowMs = opts.nowMs ?? Date.now;
330
+ try {
331
+ await session.bindExtensions({
332
+ commandContextActions: {
333
+ waitForIdle: () => session.agent.waitForIdle(),
334
+ newSession: async () => ({ cancelled: true }),
335
+ fork: async () => ({ cancelled: true }),
336
+ navigateTree: async () => ({ cancelled: true }),
337
+ switchSession: async () => ({ cancelled: true }),
338
+ reload: async () => { },
339
+ },
340
+ onError: () => { },
341
+ });
342
+ const unsubscribe = session.subscribe((event) => {
343
+ const rec = asRecord(event);
344
+ if (!rec || rec.type !== "message_end") {
345
+ return;
346
+ }
347
+ const message = asRecord(rec.message);
348
+ if (!message || message.role !== "assistant") {
349
+ return;
350
+ }
351
+ const text = extractAssistantText(message);
352
+ if (text.trim().length > 0) {
353
+ assistantText = text;
354
+ }
355
+ });
356
+ try {
357
+ await session.prompt(opts.request.body, { expandPromptTemplates: false });
358
+ await session.agent.waitForIdle();
359
+ }
360
+ finally {
361
+ unsubscribe();
362
+ }
363
+ contextEntryId = safeLeafId(session);
364
+ resolvedSessionId = safeSessionId(session) ?? opts.request.session_id;
365
+ resolvedSessionFile = safeSessionFile(session) ?? target.sessionFile;
366
+ }
367
+ finally {
368
+ try {
369
+ session.dispose();
370
+ }
371
+ catch {
372
+ // Best effort cleanup.
373
+ }
374
+ }
375
+ const reply = assistantText.trim();
376
+ if (reply.length === 0) {
377
+ throw new SessionTurnError(502, "session turn completed without an assistant reply");
378
+ }
379
+ return {
380
+ session_id: resolvedSessionId ?? opts.request.session_id,
381
+ session_kind: normalizedSessionKind,
382
+ session_file: resolvedSessionFile ?? target.sessionFile,
383
+ context_entry_id: contextEntryId,
384
+ reply,
385
+ source: opts.request.source ?? null,
386
+ completed_at_ms: Math.trunc(nowMs()),
387
+ };
388
+ }
389
+ export async function sessionTurnRoutes(request, url, deps, headers) {
390
+ if (url.pathname !== "/api/session-turn") {
391
+ return Response.json({ error: "Not Found" }, { status: 404, headers });
392
+ }
393
+ if (request.method !== "POST") {
394
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
395
+ }
396
+ let body;
397
+ try {
398
+ const parsed = (await request.json());
399
+ const rec = asRecord(parsed);
400
+ if (!rec) {
401
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
402
+ }
403
+ body = rec;
404
+ }
405
+ catch {
406
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
407
+ }
408
+ const parsedRequest = parseSessionTurnRequest(body);
409
+ if (!parsedRequest.request) {
410
+ return Response.json({ error: parsedRequest.error ?? "invalid session turn request" }, { status: 400, headers });
411
+ }
412
+ try {
413
+ const turn = await executeSessionTurn({
414
+ repoRoot: deps.context.repoRoot,
415
+ request: parsedRequest.request,
416
+ });
417
+ return Response.json({ ok: true, turn }, { headers });
418
+ }
419
+ catch (error) {
420
+ const status = error instanceof SessionTurnError ? error.status : 500;
421
+ return Response.json({ error: deps.describeError(error) }, { status, headers });
422
+ }
423
+ }
package/dist/config.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export type WakeTurnMode = "off" | "shadow" | "active";
2
1
  export type MuConfig = {
3
2
  version: 1;
4
3
  control_plane: {
@@ -14,11 +13,13 @@ export type MuConfig = {
14
13
  bot_token: string | null;
15
14
  bot_username: string | null;
16
15
  };
16
+ neovim: {
17
+ shared_secret: string | null;
18
+ };
17
19
  };
18
20
  operator: {
19
21
  enabled: boolean;
20
22
  run_triggers_enabled: boolean;
21
- wake_turn_mode: WakeTurnMode;
22
23
  provider: string | null;
23
24
  model: string | null;
24
25
  };
@@ -38,11 +39,13 @@ export type MuConfigPatch = {
38
39
  bot_token?: string | null;
39
40
  bot_username?: string | null;
40
41
  };
42
+ neovim?: {
43
+ shared_secret?: string | null;
44
+ };
41
45
  };
42
46
  operator?: {
43
47
  enabled?: boolean;
44
48
  run_triggers_enabled?: boolean;
45
- wake_turn_mode?: WakeTurnMode;
46
49
  provider?: string | null;
47
50
  model?: string | null;
48
51
  };
@@ -62,11 +65,13 @@ export type MuConfigPresence = {
62
65
  bot_token: boolean;
63
66
  bot_username: boolean;
64
67
  };
68
+ neovim: {
69
+ shared_secret: boolean;
70
+ };
65
71
  };
66
72
  operator: {
67
73
  enabled: boolean;
68
74
  run_triggers_enabled: boolean;
69
- wake_turn_mode: WakeTurnMode;
70
75
  provider: boolean;
71
76
  model: boolean;
72
77
  };
package/dist/config.js CHANGED
@@ -15,11 +15,13 @@ export const DEFAULT_MU_CONFIG = {
15
15
  bot_token: null,
16
16
  bot_username: null,
17
17
  },
18
+ neovim: {
19
+ shared_secret: null,
20
+ },
18
21
  },
19
22
  operator: {
20
23
  enabled: true,
21
24
  run_triggers_enabled: true,
22
- wake_turn_mode: "off",
23
25
  provider: null,
24
26
  model: null,
25
27
  },
@@ -54,19 +56,6 @@ function normalizeBoolean(value, fallback) {
54
56
  }
55
57
  return fallback;
56
58
  }
57
- function normalizeWakeTurnMode(value, fallback) {
58
- if (typeof value !== "string") {
59
- return fallback;
60
- }
61
- const normalized = value.trim().toLowerCase();
62
- if (normalized === "shadow") {
63
- return "shadow";
64
- }
65
- if (normalized === "active") {
66
- return "active";
67
- }
68
- return "off";
69
- }
70
59
  export function normalizeMuConfig(input) {
71
60
  const next = cloneDefault();
72
61
  const root = asRecord(input);
@@ -97,6 +86,10 @@ export function normalizeMuConfig(input) {
97
86
  next.control_plane.adapters.telegram.bot_username = normalizeNullableString(telegram.bot_username);
98
87
  }
99
88
  }
89
+ const neovim = asRecord(adapters.neovim);
90
+ if (neovim && "shared_secret" in neovim) {
91
+ next.control_plane.adapters.neovim.shared_secret = normalizeNullableString(neovim.shared_secret);
92
+ }
100
93
  }
101
94
  const operator = asRecord(controlPlane.operator);
102
95
  if (operator) {
@@ -106,9 +99,6 @@ export function normalizeMuConfig(input) {
106
99
  if ("run_triggers_enabled" in operator) {
107
100
  next.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, next.control_plane.operator.run_triggers_enabled);
108
101
  }
109
- if ("wake_turn_mode" in operator) {
110
- next.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, next.control_plane.operator.wake_turn_mode);
111
- }
112
102
  if ("provider" in operator) {
113
103
  next.control_plane.operator.provider = normalizeNullableString(operator.provider);
114
104
  }
@@ -158,6 +148,16 @@ function normalizeMuConfigPatch(input) {
158
148
  patch.control_plane.adapters.telegram = telegramPatch;
159
149
  }
160
150
  }
151
+ const neovim = asRecord(adapters.neovim);
152
+ if (neovim) {
153
+ const neovimPatch = {};
154
+ if ("shared_secret" in neovim) {
155
+ neovimPatch.shared_secret = normalizeNullableString(neovim.shared_secret);
156
+ }
157
+ if (Object.keys(neovimPatch).length > 0) {
158
+ patch.control_plane.adapters.neovim = neovimPatch;
159
+ }
160
+ }
161
161
  }
162
162
  const operator = asRecord(controlPlane.operator);
163
163
  if (operator) {
@@ -168,9 +168,6 @@ function normalizeMuConfigPatch(input) {
168
168
  if ("run_triggers_enabled" in operator) {
169
169
  patch.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, DEFAULT_MU_CONFIG.control_plane.operator.run_triggers_enabled);
170
170
  }
171
- if ("wake_turn_mode" in operator) {
172
- patch.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, DEFAULT_MU_CONFIG.control_plane.operator.wake_turn_mode);
173
- }
174
171
  if ("provider" in operator) {
175
172
  patch.control_plane.operator.provider = normalizeNullableString(operator.provider);
176
173
  }
@@ -214,6 +211,9 @@ export function applyMuConfigPatch(base, patchInput) {
214
211
  next.control_plane.adapters.telegram.bot_username = adapters.telegram.bot_username ?? null;
215
212
  }
216
213
  }
214
+ if (adapters.neovim && "shared_secret" in adapters.neovim) {
215
+ next.control_plane.adapters.neovim.shared_secret = adapters.neovim.shared_secret ?? null;
216
+ }
217
217
  }
218
218
  const operator = patch.control_plane.operator;
219
219
  if (operator) {
@@ -223,9 +223,6 @@ export function applyMuConfigPatch(base, patchInput) {
223
223
  if ("run_triggers_enabled" in operator && typeof operator.run_triggers_enabled === "boolean") {
224
224
  next.control_plane.operator.run_triggers_enabled = operator.run_triggers_enabled;
225
225
  }
226
- if ("wake_turn_mode" in operator) {
227
- next.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, next.control_plane.operator.wake_turn_mode);
228
- }
229
226
  if ("provider" in operator) {
230
227
  next.control_plane.operator.provider = operator.provider ?? null;
231
228
  }
@@ -276,6 +273,7 @@ export function redactMuConfigSecrets(config) {
276
273
  next.control_plane.adapters.discord.signing_secret = redacted(next.control_plane.adapters.discord.signing_secret);
277
274
  next.control_plane.adapters.telegram.webhook_secret = redacted(next.control_plane.adapters.telegram.webhook_secret);
278
275
  next.control_plane.adapters.telegram.bot_token = redacted(next.control_plane.adapters.telegram.bot_token);
276
+ next.control_plane.adapters.neovim.shared_secret = redacted(next.control_plane.adapters.neovim.shared_secret);
279
277
  return next;
280
278
  }
281
279
  function isPresent(value) {
@@ -296,11 +294,13 @@ export function muConfigPresence(config) {
296
294
  bot_token: isPresent(config.control_plane.adapters.telegram.bot_token),
297
295
  bot_username: isPresent(config.control_plane.adapters.telegram.bot_username),
298
296
  },
297
+ neovim: {
298
+ shared_secret: isPresent(config.control_plane.adapters.neovim.shared_secret),
299
+ },
299
300
  },
300
301
  operator: {
301
302
  enabled: config.control_plane.operator.enabled,
302
303
  run_triggers_enabled: config.control_plane.operator.run_triggers_enabled,
303
- wake_turn_mode: config.control_plane.operator.wake_turn_mode,
304
304
  provider: isPresent(config.control_plane.operator.provider),
305
305
  model: isPresent(config.control_plane.operator.model),
306
306
  },
@@ -1,22 +1,10 @@
1
1
  import type { MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
2
  import { type GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
3
3
  import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type InterRootQueuePolicy, type TelegramGenerationSwapHooks, type WakeDeliveryObserver } from "./control_plane_contract.js";
4
- import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
5
4
  import { type ControlPlaneRunSupervisorOpts } from "./run_supervisor.js";
5
+ import { detectAdapters } from "./control_plane_adapter_registry.js";
6
6
  export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
7
- type DetectedAdapter = {
8
- name: "slack";
9
- signingSecret: string;
10
- } | {
11
- name: "discord";
12
- signingSecret: string;
13
- } | {
14
- name: "telegram";
15
- webhookSecret: string;
16
- botToken: string | null;
17
- botUsername: string | null;
18
- };
19
- export declare function detectAdapters(config: ControlPlaneConfig): DetectedAdapter[];
7
+ export { detectAdapters };
20
8
  export type TelegramSendMessagePayload = {
21
9
  chat_id: string;
22
10
  text: string;
@@ -40,9 +28,7 @@ export type BootstrapControlPlaneOpts = {
40
28
  config?: ControlPlaneConfig;
41
29
  operatorRuntime?: MessagingOperatorRuntime | null;
42
30
  operatorBackend?: MessagingOperatorBackend;
43
- heartbeatScheduler?: ActivityHeartbeatScheduler;
44
31
  runSupervisorSpawnProcess?: ControlPlaneRunSupervisorOpts["spawnProcess"];
45
- runSupervisorHeartbeatIntervalMs?: number;
46
32
  sessionLifecycle: ControlPlaneSessionLifecycle;
47
33
  generation?: ControlPlaneGenerationContext;
48
34
  telemetry?: GenerationTelemetryRecorder | null;