@femtomc/mu-server 26.2.67 → 26.2.69

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.
package/README.md CHANGED
@@ -11,18 +11,19 @@ bun add @femtomc/mu-server
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
- import { createServer } from "@femtomc/mu-server";
14
+ import { composeServerRuntime, createServerFromRuntime } from "@femtomc/mu-server";
15
15
 
16
- // Create server with default options (uses current directory as repo root)
17
- const server = createServer();
16
+ const runtime = await composeServerRuntime({
17
+ repoRoot: "/path/to/repo"
18
+ });
19
+
20
+ // Optional: inspect startup capabilities
21
+ console.log(runtime.capabilities);
18
22
 
19
- // Or specify custom repo root and port
20
- const server = createServer({
21
- repoRoot: "/path/to/repo",
23
+ const server = createServerFromRuntime(runtime, {
22
24
  port: 8080
23
25
  });
24
26
 
25
- // Start the server
26
27
  Bun.serve(server);
27
28
  ```
28
29
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { findRepoRoot } from "@femtomc/mu-core/node";
3
- import { createServerAsync } from "./server.js";
3
+ import { composeServerRuntime, createServerFromRuntime } from "./server.js";
4
4
  const port = parseInt(Bun.env.PORT || "3000", 10);
5
5
  let repoRoot;
6
6
  try {
@@ -12,14 +12,15 @@ catch {
12
12
  }
13
13
  console.log(`Starting mu-server on port ${port}...`);
14
14
  console.log(`Repository root: ${repoRoot}`);
15
- const { serverConfig, controlPlane } = await createServerAsync({ repoRoot, port });
15
+ const runtime = await composeServerRuntime({ repoRoot });
16
+ const serverConfig = createServerFromRuntime(runtime, { port });
16
17
  let server;
17
18
  try {
18
19
  server = Bun.serve(serverConfig);
19
20
  }
20
21
  catch (err) {
21
22
  try {
22
- await controlPlane?.stop();
23
+ await runtime.controlPlane?.stop();
23
24
  }
24
25
  catch {
25
26
  // Best effort cleanup. Preserve the startup error.
@@ -27,9 +28,10 @@ catch (err) {
27
28
  throw err;
28
29
  }
29
30
  console.log(`Server running at http://localhost:${port}`);
30
- if (controlPlane && controlPlane.activeAdapters.length > 0) {
31
+ console.log(`Capabilities: lifecycle=[${runtime.capabilities.session_lifecycle_actions.join(",")}]`);
32
+ if (runtime.controlPlane && runtime.controlPlane.activeAdapters.length > 0) {
31
33
  console.log("Control plane: active");
32
- for (const a of controlPlane.activeAdapters) {
34
+ for (const a of runtime.controlPlane.activeAdapters) {
33
35
  console.log(` ${a.name.padEnd(12)} ${a.route}`);
34
36
  }
35
37
  }
@@ -38,7 +40,7 @@ else {
38
40
  console.log(`API Status: http://localhost:${port}/api/status`);
39
41
  }
40
42
  const cleanup = async () => {
41
- await controlPlane?.stop();
43
+ await runtime.controlPlane?.stop();
42
44
  server.stop();
43
45
  process.exit(0);
44
46
  };
@@ -108,9 +108,9 @@ export type ControlPlaneSessionMutationResult = {
108
108
  message: string;
109
109
  details?: Record<string, unknown>;
110
110
  };
111
- export type ControlPlaneSessionMutationHooks = {
112
- reload?: () => Promise<ControlPlaneSessionMutationResult>;
113
- update?: () => Promise<ControlPlaneSessionMutationResult>;
111
+ export type ControlPlaneSessionLifecycle = {
112
+ reload: () => Promise<ControlPlaneSessionMutationResult>;
113
+ update: () => Promise<ControlPlaneSessionMutationResult>;
114
114
  };
115
115
  type DetectedAdapter = {
116
116
  name: "slack";
@@ -151,7 +151,7 @@ export type BootstrapControlPlaneOpts = {
151
151
  heartbeatScheduler?: ActivityHeartbeatScheduler;
152
152
  runSupervisorSpawnProcess?: ControlPlaneRunSupervisorOpts["spawnProcess"];
153
153
  runSupervisorHeartbeatIntervalMs?: number;
154
- sessionMutationHooks?: ControlPlaneSessionMutationHooks;
154
+ sessionLifecycle: ControlPlaneSessionLifecycle;
155
155
  generation?: ControlPlaneGenerationContext;
156
156
  telemetry?: GenerationTelemetryRecorder | null;
157
157
  telegramGenerationHooks?: TelegramGenerationSwapHooks;
@@ -767,30 +767,9 @@ export async function bootstrapControlPlane(opts) {
767
767
  };
768
768
  }
769
769
  const action = record.target_type;
770
- const hook = action === "reload"
771
- ? opts.sessionMutationHooks?.reload
772
- : opts.sessionMutationHooks?.update;
773
- if (!hook) {
774
- return {
775
- terminalState: "failed",
776
- errorCode: "session_lifecycle_unavailable",
777
- trace: {
778
- cliCommandKind: action,
779
- runRootId: null,
780
- },
781
- mutatingEvents: [
782
- {
783
- eventType: "session.lifecycle.command.failed",
784
- payload: {
785
- action,
786
- reason: "hook_missing",
787
- },
788
- },
789
- ],
790
- };
791
- }
770
+ const executeLifecycleAction = action === "reload" ? opts.sessionLifecycle.reload : opts.sessionLifecycle.update;
792
771
  try {
793
- const lifecycle = await hook();
772
+ const lifecycle = await executeLifecycleAction();
794
773
  if (!lifecycle.ok) {
795
774
  return {
796
775
  terminalState: "failed",
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export type { ControlPlaneActivityEvent, ControlPlaneActivityEventKind, ControlP
2
2
  export { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
3
3
  export type { MuConfig, MuConfigPatch, MuConfigPresence } from "./config.js";
4
4
  export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
5
- export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationHooks, ControlPlaneSessionMutationResult, } from "./control_plane.js";
5
+ export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, } from "./control_plane.js";
6
6
  export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
7
7
  export type { CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot, CronProgramTarget, CronProgramTickEvent, CronProgramWakeMode, } from "./cron_programs.js";
8
8
  export { CronProgramRegistry } from "./cron_programs.js";
@@ -14,5 +14,5 @@ export type { HeartbeatProgramOperationResult, HeartbeatProgramRegistryOpts, Hea
14
14
  export { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
15
15
  export type { ActivityHeartbeatSchedulerOpts, HeartbeatRunResult, HeartbeatTickHandler, } from "./heartbeat_scheduler.js";
16
16
  export { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
17
- export type { ServerContext, ServerOptions, ServerWithControlPlane } from "./server.js";
18
- export { createContext, createServer, createServerAsync } from "./server.js";
17
+ export type { ServerContext, ServerInstanceOptions, ServerOptions, ServerRuntime, ServerRuntimeCapabilities, ServerRuntimeOptions, } from "./server.js";
18
+ export { composeServerRuntime, createContext, createProcessSessionLifecycle, createServerFromRuntime, } from "./server.js";
package/dist/index.js CHANGED
@@ -6,4 +6,4 @@ export { computeNextScheduleRunAtMs, normalizeCronSchedule } from "./cron_schedu
6
6
  export { CronTimerRegistry } from "./cron_timer.js";
7
7
  export { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
8
8
  export { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
9
- export { createContext, createServer, createServerAsync } from "./server.js";
9
+ export { composeServerRuntime, createContext, createProcessSessionLifecycle, createServerFromRuntime, } from "./server.js";
package/dist/server.d.ts CHANGED
@@ -5,10 +5,20 @@ import { ForumStore } from "@femtomc/mu-forum";
5
5
  import { IssueStore } from "@femtomc/mu-issue";
6
6
  import { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
7
7
  import { type MuConfig } from "./config.js";
8
- import { type ControlPlaneConfig, type ControlPlaneHandle, type ControlPlaneSessionMutationHooks } from "./control_plane.js";
8
+ import { type ControlPlaneConfig, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type ControlPlaneSessionMutationAction } from "./control_plane.js";
9
9
  import { CronProgramRegistry } from "./cron_programs.js";
10
10
  import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
11
11
  import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
12
+ type ShellCommandResult = {
13
+ exitCode: number;
14
+ stdout: string;
15
+ stderr: string;
16
+ };
17
+ type ShellCommandRunner = (command: string) => Promise<ShellCommandResult>;
18
+ export declare function createProcessSessionLifecycle(opts: {
19
+ repoRoot: string;
20
+ runShellCommand?: ShellCommandRunner;
21
+ }): ControlPlaneSessionLifecycle;
12
22
  type ControlPlaneReloader = (opts: {
13
23
  repoRoot: string;
14
24
  previous: ControlPlaneHandle | null;
@@ -30,8 +40,18 @@ export type ServerOptions = {
30
40
  config?: MuConfig;
31
41
  configReader?: ConfigReader;
32
42
  configWriter?: ConfigWriter;
33
- sessionMutationHooks?: ControlPlaneSessionMutationHooks;
43
+ sessionLifecycle?: ControlPlaneSessionLifecycle;
34
44
  };
45
+ export type ServerRuntimeOptions = {
46
+ repoRoot?: string;
47
+ controlPlane?: ControlPlaneHandle | null;
48
+ heartbeatScheduler?: ActivityHeartbeatScheduler;
49
+ generationTelemetry?: GenerationTelemetryRecorder;
50
+ config?: MuConfig;
51
+ configReader?: ConfigReader;
52
+ sessionLifecycle?: ControlPlaneSessionLifecycle;
53
+ };
54
+ export type ServerInstanceOptions = Omit<ServerOptions, "repoRoot" | "controlPlane" | "heartbeatScheduler" | "generationTelemetry" | "config" | "sessionLifecycle">;
35
55
  export type ServerContext = {
36
56
  repoRoot: string;
37
57
  issueStore: IssueStore;
@@ -40,7 +60,22 @@ export type ServerContext = {
40
60
  eventsStore: JsonlStore<EventEnvelope>;
41
61
  };
42
62
  export declare function createContext(repoRoot: string): ServerContext;
43
- export declare function createServer(options?: ServerOptions): {
63
+ export type ServerRuntimeCapabilities = {
64
+ session_lifecycle_actions: readonly ControlPlaneSessionMutationAction[];
65
+ control_plane_bootstrapped: boolean;
66
+ control_plane_adapters: string[];
67
+ };
68
+ export type ServerRuntime = {
69
+ repoRoot: string;
70
+ config: MuConfig;
71
+ heartbeatScheduler: ActivityHeartbeatScheduler;
72
+ generationTelemetry: GenerationTelemetryRecorder;
73
+ sessionLifecycle: ControlPlaneSessionLifecycle;
74
+ controlPlane: ControlPlaneHandle | null;
75
+ capabilities: ServerRuntimeCapabilities;
76
+ };
77
+ export declare function composeServerRuntime(options?: ServerRuntimeOptions): Promise<ServerRuntime>;
78
+ export declare function createServerFromRuntime(runtime: ServerRuntime, options?: ServerInstanceOptions): {
44
79
  port: number;
45
80
  fetch: (request: Request) => Promise<Response>;
46
81
  hostname: string;
@@ -49,9 +84,4 @@ export declare function createServer(options?: ServerOptions): {
49
84
  heartbeatPrograms: HeartbeatProgramRegistry;
50
85
  cronPrograms: CronProgramRegistry;
51
86
  };
52
- export type ServerWithControlPlane = {
53
- serverConfig: ReturnType<typeof createServer>;
54
- controlPlane: ControlPlaneHandle | null;
55
- };
56
- export declare function createServerAsync(options?: Omit<ServerOptions, "controlPlane">): Promise<ServerWithControlPlane>;
57
87
  export {};
package/dist/server.js CHANGED
@@ -52,6 +52,143 @@ function shellQuoteArg(value) {
52
52
  function shellJoin(args) {
53
53
  return args.map(shellQuoteArg).join(" ");
54
54
  }
55
+ function createShellCommandRunner(repoRoot) {
56
+ return async (command) => {
57
+ const proc = Bun.spawn({
58
+ cmd: ["bash", "-lc", command],
59
+ cwd: repoRoot,
60
+ env: Bun.env,
61
+ stdin: "ignore",
62
+ stdout: "pipe",
63
+ stderr: "pipe",
64
+ });
65
+ const [exitCode, stdout, stderr] = await Promise.all([
66
+ proc.exited,
67
+ proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
68
+ proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
69
+ ]);
70
+ return {
71
+ exitCode: Number.isFinite(exitCode) ? Number(exitCode) : 1,
72
+ stdout,
73
+ stderr,
74
+ };
75
+ };
76
+ }
77
+ export function createProcessSessionLifecycle(opts) {
78
+ const runShellCommand = opts.runShellCommand ?? createShellCommandRunner(opts.repoRoot);
79
+ let sessionMutationScheduled = null;
80
+ const scheduleReload = async () => {
81
+ if (sessionMutationScheduled) {
82
+ return {
83
+ ok: true,
84
+ action: sessionMutationScheduled.action,
85
+ message: `session ${sessionMutationScheduled.action} already scheduled`,
86
+ details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
87
+ };
88
+ }
89
+ const nowMs = Date.now();
90
+ const restartCommand = Bun.env.MU_RESTART_COMMAND?.trim();
91
+ const inferredArgs = process.argv[0] === process.execPath
92
+ ? [process.execPath, ...process.argv.slice(1)]
93
+ : [process.execPath, ...process.argv];
94
+ const restartShellCommand = restartCommand && restartCommand.length > 0 ? restartCommand : shellJoin(inferredArgs);
95
+ if (!restartShellCommand.trim()) {
96
+ return {
97
+ ok: false,
98
+ action: "reload",
99
+ message: "unable to determine restart command",
100
+ };
101
+ }
102
+ const exitDelayMs = 1_000;
103
+ const launchDelayMs = exitDelayMs + 300;
104
+ const delayedShellCommand = `sleep ${(launchDelayMs / 1_000).toFixed(2)}; ${restartShellCommand}`;
105
+ let spawnedPid = null;
106
+ try {
107
+ const proc = Bun.spawn({
108
+ cmd: ["bash", "-lc", delayedShellCommand],
109
+ cwd: opts.repoRoot,
110
+ env: Bun.env,
111
+ stdin: "ignore",
112
+ stdout: "inherit",
113
+ stderr: "inherit",
114
+ });
115
+ spawnedPid = proc.pid ?? null;
116
+ }
117
+ catch (err) {
118
+ return {
119
+ ok: false,
120
+ action: "reload",
121
+ message: `failed to spawn replacement process: ${describeError(err)}`,
122
+ };
123
+ }
124
+ sessionMutationScheduled = { action: "reload", at_ms: nowMs };
125
+ setTimeout(() => {
126
+ process.exit(0);
127
+ }, exitDelayMs);
128
+ return {
129
+ ok: true,
130
+ action: "reload",
131
+ message: "reload scheduled; restarting process",
132
+ details: {
133
+ restart_command: restartShellCommand,
134
+ restart_launch_command: delayedShellCommand,
135
+ spawned_pid: spawnedPid,
136
+ exit_delay_ms: exitDelayMs,
137
+ launch_delay_ms: launchDelayMs,
138
+ },
139
+ };
140
+ };
141
+ const scheduleUpdate = async () => {
142
+ if (sessionMutationScheduled) {
143
+ return {
144
+ ok: true,
145
+ action: sessionMutationScheduled.action,
146
+ message: `session ${sessionMutationScheduled.action} already scheduled`,
147
+ details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
148
+ };
149
+ }
150
+ const updateCommand = Bun.env.MU_UPDATE_COMMAND?.trim() || "npm install -g @femtomc/mu@latest";
151
+ const result = await runShellCommand(updateCommand);
152
+ if (result.exitCode !== 0) {
153
+ return {
154
+ ok: false,
155
+ action: "update",
156
+ message: `update command failed (exit ${result.exitCode})`,
157
+ details: {
158
+ update_command: updateCommand,
159
+ stdout: result.stdout.slice(-4_000),
160
+ stderr: result.stderr.slice(-4_000),
161
+ },
162
+ };
163
+ }
164
+ const reloadResult = await scheduleReload();
165
+ if (!reloadResult.ok) {
166
+ return {
167
+ ok: false,
168
+ action: "update",
169
+ message: reloadResult.message,
170
+ details: {
171
+ update_command: updateCommand,
172
+ reload: reloadResult.details ?? null,
173
+ },
174
+ };
175
+ }
176
+ return {
177
+ ok: true,
178
+ action: "update",
179
+ message: "update applied; reload scheduled",
180
+ details: {
181
+ update_command: updateCommand,
182
+ reload: reloadResult.details ?? null,
183
+ update_stdout_tail: result.stdout.slice(-1_000),
184
+ },
185
+ };
186
+ };
187
+ return {
188
+ reload: scheduleReload,
189
+ update: scheduleUpdate,
190
+ };
191
+ }
55
192
  function describeError(err) {
56
193
  if (err instanceof Error)
57
194
  return err.message;
@@ -142,7 +279,7 @@ export function createContext(repoRoot) {
142
279
  const forumStore = new ForumStore(new FsJsonlStore(paths.forumPath), { events: eventLog });
143
280
  return { repoRoot, issueStore, forumStore, eventLog, eventsStore };
144
281
  }
145
- export function createServer(options = {}) {
282
+ function createServer(options = {}) {
146
283
  const repoRoot = options.repoRoot || process.cwd();
147
284
  const context = createContext(repoRoot);
148
285
  const readConfig = options.configReader ?? readMuConfigFile;
@@ -171,144 +308,7 @@ export function createServer(options = {}) {
171
308
  const autoRunHeartbeatEveryMs = Math.max(1_000, toNonNegativeInt(options.autoRunHeartbeatEveryMs, DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS));
172
309
  const operatorWakeLastByKey = new Map();
173
310
  const autoRunHeartbeatProgramByJobId = new Map();
174
- let sessionMutationScheduled = null;
175
- const runShellCommand = async (command) => {
176
- const proc = Bun.spawn({
177
- cmd: ["bash", "-lc", command],
178
- cwd: repoRoot,
179
- env: Bun.env,
180
- stdin: "ignore",
181
- stdout: "pipe",
182
- stderr: "pipe",
183
- });
184
- const [exitCode, stdout, stderr] = await Promise.all([
185
- proc.exited,
186
- proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
187
- proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
188
- ]);
189
- return {
190
- exitCode: Number.isFinite(exitCode) ? Number(exitCode) : 1,
191
- stdout,
192
- stderr,
193
- };
194
- };
195
- const defaultSessionMutationHooks = {
196
- reload: async () => {
197
- if (sessionMutationScheduled) {
198
- return {
199
- ok: true,
200
- action: sessionMutationScheduled.action,
201
- message: `session ${sessionMutationScheduled.action} already scheduled`,
202
- details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
203
- };
204
- }
205
- const nowMs = Date.now();
206
- const restartCommand = Bun.env.MU_RESTART_COMMAND?.trim();
207
- const inferredArgs = process.argv[0] === process.execPath
208
- ? [process.execPath, ...process.argv.slice(1)]
209
- : [process.execPath, ...process.argv];
210
- const restartShellCommand = restartCommand && restartCommand.length > 0 ? restartCommand : shellJoin(inferredArgs);
211
- if (!restartShellCommand.trim()) {
212
- return {
213
- ok: false,
214
- action: "reload",
215
- message: "unable to determine restart command",
216
- };
217
- }
218
- const exitDelayMs = 1_000;
219
- const launchDelayMs = exitDelayMs + 300;
220
- const delayedShellCommand = `sleep ${(launchDelayMs / 1_000).toFixed(2)}; ${restartShellCommand}`;
221
- let spawnedPid = null;
222
- try {
223
- const proc = Bun.spawn({
224
- cmd: ["bash", "-lc", delayedShellCommand],
225
- cwd: repoRoot,
226
- env: Bun.env,
227
- stdin: "ignore",
228
- stdout: "inherit",
229
- stderr: "inherit",
230
- });
231
- spawnedPid = proc.pid ?? null;
232
- }
233
- catch (err) {
234
- return {
235
- ok: false,
236
- action: "reload",
237
- message: `failed to spawn replacement process: ${describeError(err)}`,
238
- };
239
- }
240
- sessionMutationScheduled = { action: "reload", at_ms: nowMs };
241
- setTimeout(() => {
242
- process.exit(0);
243
- }, exitDelayMs);
244
- return {
245
- ok: true,
246
- action: "reload",
247
- message: "reload scheduled; restarting process",
248
- details: {
249
- restart_command: restartShellCommand,
250
- restart_launch_command: delayedShellCommand,
251
- spawned_pid: spawnedPid,
252
- exit_delay_ms: exitDelayMs,
253
- launch_delay_ms: launchDelayMs,
254
- },
255
- };
256
- },
257
- update: async () => {
258
- if (sessionMutationScheduled) {
259
- return {
260
- ok: true,
261
- action: sessionMutationScheduled.action,
262
- message: `session ${sessionMutationScheduled.action} already scheduled`,
263
- details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
264
- };
265
- }
266
- const updateCommand = Bun.env.MU_UPDATE_COMMAND?.trim() || "npm install -g @femtomc/mu@latest";
267
- const result = await runShellCommand(updateCommand);
268
- if (result.exitCode !== 0) {
269
- return {
270
- ok: false,
271
- action: "update",
272
- message: `update command failed (exit ${result.exitCode})`,
273
- details: {
274
- update_command: updateCommand,
275
- stdout: result.stdout.slice(-4_000),
276
- stderr: result.stderr.slice(-4_000),
277
- },
278
- };
279
- }
280
- const reloadResult = await defaultSessionMutationHooks.reload?.();
281
- if (!reloadResult) {
282
- return {
283
- ok: false,
284
- action: "update",
285
- message: "reload hook unavailable after update",
286
- };
287
- }
288
- if (!reloadResult.ok) {
289
- return {
290
- ok: false,
291
- action: "update",
292
- message: reloadResult.message,
293
- details: {
294
- update_command: updateCommand,
295
- reload: reloadResult.details ?? null,
296
- },
297
- };
298
- }
299
- return {
300
- ok: true,
301
- action: "update",
302
- message: "update applied; reload scheduled",
303
- details: {
304
- update_command: updateCommand,
305
- reload: reloadResult.details ?? null,
306
- update_stdout_tail: result.stdout.slice(-1_000),
307
- },
308
- };
309
- },
310
- };
311
- const sessionMutationHooks = options.sessionMutationHooks ?? defaultSessionMutationHooks;
311
+ const sessionLifecycle = options.sessionLifecycle ?? createProcessSessionLifecycle({ repoRoot });
312
312
  const emitOperatorWake = async (opts) => {
313
313
  const dedupeKey = opts.dedupeKey.trim();
314
314
  if (!dedupeKey) {
@@ -358,7 +358,7 @@ export function createServer(options = {}) {
358
358
  heartbeatScheduler,
359
359
  generation,
360
360
  telemetry: generationTelemetry,
361
- sessionMutationHooks,
361
+ sessionLifecycle,
362
362
  terminalEnabled: true,
363
363
  });
364
364
  });
@@ -2249,32 +2249,52 @@ export function createServer(options = {}) {
2249
2249
  };
2250
2250
  return server;
2251
2251
  }
2252
- export async function createServerAsync(options = {}) {
2252
+ function computeServerRuntimeCapabilities(controlPlane) {
2253
+ return {
2254
+ session_lifecycle_actions: ["reload", "update"],
2255
+ control_plane_bootstrapped: controlPlane !== null,
2256
+ control_plane_adapters: controlPlane?.activeAdapters.map((adapter) => adapter.name) ?? [],
2257
+ };
2258
+ }
2259
+ export async function composeServerRuntime(options = {}) {
2253
2260
  const repoRoot = options.repoRoot || process.cwd();
2254
- const config = options.config ?? (await readMuConfigFile(repoRoot));
2261
+ const readConfig = options.configReader ?? readMuConfigFile;
2262
+ const config = options.config ?? (await readConfig(repoRoot));
2255
2263
  const heartbeatScheduler = options.heartbeatScheduler ?? new ActivityHeartbeatScheduler();
2256
2264
  const generationTelemetry = options.generationTelemetry ?? new GenerationTelemetryRecorder();
2257
- const controlPlane = await bootstrapControlPlane({
2265
+ const sessionLifecycle = options.sessionLifecycle ?? createProcessSessionLifecycle({ repoRoot });
2266
+ const controlPlane = options.controlPlane !== undefined
2267
+ ? options.controlPlane
2268
+ : await bootstrapControlPlane({
2269
+ repoRoot,
2270
+ config: config.control_plane,
2271
+ heartbeatScheduler,
2272
+ generation: {
2273
+ generation_id: "control-plane-gen-0",
2274
+ generation_seq: 0,
2275
+ },
2276
+ telemetry: generationTelemetry,
2277
+ sessionLifecycle,
2278
+ terminalEnabled: true,
2279
+ });
2280
+ return {
2258
2281
  repoRoot,
2259
- config: config.control_plane,
2260
- heartbeatScheduler,
2261
- generation: {
2262
- generation_id: "control-plane-gen-0",
2263
- generation_seq: 0,
2264
- },
2265
- telemetry: generationTelemetry,
2266
- sessionMutationHooks: options.sessionMutationHooks,
2267
- terminalEnabled: true,
2268
- });
2269
- const serverConfig = createServer({
2270
- ...options,
2271
- heartbeatScheduler,
2272
- controlPlane,
2273
2282
  config,
2283
+ heartbeatScheduler,
2274
2284
  generationTelemetry,
2275
- });
2276
- return {
2277
- serverConfig,
2278
- controlPlane: serverConfig.controlPlane,
2285
+ sessionLifecycle,
2286
+ controlPlane,
2287
+ capabilities: computeServerRuntimeCapabilities(controlPlane),
2279
2288
  };
2280
2289
  }
2290
+ export function createServerFromRuntime(runtime, options = {}) {
2291
+ return createServer({
2292
+ ...options,
2293
+ repoRoot: runtime.repoRoot,
2294
+ config: runtime.config,
2295
+ heartbeatScheduler: runtime.heartbeatScheduler,
2296
+ generationTelemetry: runtime.generationTelemetry,
2297
+ sessionLifecycle: runtime.sessionLifecycle,
2298
+ controlPlane: runtime.controlPlane,
2299
+ });
2300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.67",
3
+ "version": "26.2.69",
4
4
  "description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
5
5
  "keywords": [
6
6
  "mu",
@@ -31,10 +31,10 @@
31
31
  "start": "bun run dist/cli.js"
32
32
  },
33
33
  "dependencies": {
34
- "@femtomc/mu-agent": "26.2.67",
35
- "@femtomc/mu-control-plane": "26.2.67",
36
- "@femtomc/mu-core": "26.2.67",
37
- "@femtomc/mu-forum": "26.2.67",
38
- "@femtomc/mu-issue": "26.2.67"
34
+ "@femtomc/mu-agent": "26.2.69",
35
+ "@femtomc/mu-control-plane": "26.2.69",
36
+ "@femtomc/mu-core": "26.2.69",
37
+ "@femtomc/mu-forum": "26.2.69",
38
+ "@femtomc/mu-issue": "26.2.69"
39
39
  }
40
40
  }