@femtomc/mu-server 26.2.69 → 26.2.70

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,30 @@
1
+ import { GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
2
+ import type { MuConfig } from "./config.js";
3
+ import type { ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction } from "./control_plane_contract.js";
4
+ import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
5
+ type ConfigReader = (repoRoot: string) => Promise<MuConfig>;
6
+ export type ServerRuntimeOptions = {
7
+ repoRoot?: string;
8
+ controlPlane?: ControlPlaneHandle | null;
9
+ heartbeatScheduler?: ActivityHeartbeatScheduler;
10
+ generationTelemetry?: GenerationTelemetryRecorder;
11
+ config?: MuConfig;
12
+ configReader?: ConfigReader;
13
+ sessionLifecycle?: ControlPlaneSessionLifecycle;
14
+ };
15
+ export type ServerRuntimeCapabilities = {
16
+ session_lifecycle_actions: readonly ControlPlaneSessionMutationAction[];
17
+ control_plane_bootstrapped: boolean;
18
+ control_plane_adapters: string[];
19
+ };
20
+ export type ServerRuntime = {
21
+ repoRoot: string;
22
+ config: MuConfig;
23
+ heartbeatScheduler: ActivityHeartbeatScheduler;
24
+ generationTelemetry: GenerationTelemetryRecorder;
25
+ sessionLifecycle: ControlPlaneSessionLifecycle;
26
+ controlPlane: ControlPlaneHandle | null;
27
+ capabilities: ServerRuntimeCapabilities;
28
+ };
29
+ export declare function composeServerRuntime(options?: ServerRuntimeOptions): Promise<ServerRuntime>;
30
+ export {};
@@ -0,0 +1,43 @@
1
+ import { GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
2
+ import { readMuConfigFile } from "./config.js";
3
+ import { bootstrapControlPlane } from "./control_plane.js";
4
+ import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
5
+ import { createProcessSessionLifecycle } from "./session_lifecycle.js";
6
+ function computeServerRuntimeCapabilities(controlPlane) {
7
+ return {
8
+ session_lifecycle_actions: ["reload", "update"],
9
+ control_plane_bootstrapped: controlPlane !== null,
10
+ control_plane_adapters: controlPlane?.activeAdapters.map((adapter) => adapter.name) ?? [],
11
+ };
12
+ }
13
+ export async function composeServerRuntime(options = {}) {
14
+ const repoRoot = options.repoRoot || process.cwd();
15
+ const readConfig = options.configReader ?? readMuConfigFile;
16
+ const config = options.config ?? (await readConfig(repoRoot));
17
+ const heartbeatScheduler = options.heartbeatScheduler ?? new ActivityHeartbeatScheduler();
18
+ const generationTelemetry = options.generationTelemetry ?? new GenerationTelemetryRecorder();
19
+ const sessionLifecycle = options.sessionLifecycle ?? createProcessSessionLifecycle({ repoRoot });
20
+ const controlPlane = options.controlPlane !== undefined
21
+ ? options.controlPlane
22
+ : await bootstrapControlPlane({
23
+ repoRoot,
24
+ config: config.control_plane,
25
+ heartbeatScheduler,
26
+ generation: {
27
+ generation_id: "control-plane-gen-0",
28
+ generation_seq: 0,
29
+ },
30
+ telemetry: generationTelemetry,
31
+ sessionLifecycle,
32
+ terminalEnabled: true,
33
+ });
34
+ return {
35
+ repoRoot,
36
+ config,
37
+ heartbeatScheduler,
38
+ generationTelemetry,
39
+ sessionLifecycle,
40
+ controlPlane,
41
+ capabilities: computeServerRuntimeCapabilities(controlPlane),
42
+ };
43
+ }
@@ -0,0 +1,3 @@
1
+ export type ProgramWakeMode = "immediate" | "next_heartbeat";
2
+ export declare function normalizeWakeMode(value: unknown): ProgramWakeMode;
3
+ export declare function toNonNegativeInt(value: unknown, fallback: number): number;
@@ -0,0 +1,16 @@
1
+ export function normalizeWakeMode(value) {
2
+ if (typeof value !== "string") {
3
+ return "immediate";
4
+ }
5
+ const normalized = value.trim().toLowerCase().replaceAll("-", "_");
6
+ return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
7
+ }
8
+ export function toNonNegativeInt(value, fallback) {
9
+ if (typeof value === "number" && Number.isFinite(value)) {
10
+ return Math.max(0, Math.trunc(value));
11
+ }
12
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) {
13
+ return Math.max(0, Number.parseInt(value, 10));
14
+ }
15
+ return Math.max(0, Math.trunc(fallback));
16
+ }
@@ -0,0 +1,11 @@
1
+ import type { ControlPlaneSessionLifecycle } from "./control_plane_contract.js";
2
+ export type ShellCommandResult = {
3
+ exitCode: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ };
7
+ export type ShellCommandRunner = (command: string) => Promise<ShellCommandResult>;
8
+ export declare function createProcessSessionLifecycle(opts: {
9
+ repoRoot: string;
10
+ runShellCommand?: ShellCommandRunner;
11
+ }): ControlPlaneSessionLifecycle;
@@ -0,0 +1,149 @@
1
+ function shellQuoteArg(value) {
2
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
3
+ }
4
+ function shellJoin(args) {
5
+ return args.map(shellQuoteArg).join(" ");
6
+ }
7
+ function createShellCommandRunner(repoRoot) {
8
+ return async (command) => {
9
+ const proc = Bun.spawn({
10
+ cmd: ["bash", "-lc", command],
11
+ cwd: repoRoot,
12
+ env: Bun.env,
13
+ stdin: "ignore",
14
+ stdout: "pipe",
15
+ stderr: "pipe",
16
+ });
17
+ const [exitCode, stdout, stderr] = await Promise.all([
18
+ proc.exited,
19
+ proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
20
+ proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
21
+ ]);
22
+ return {
23
+ exitCode: Number.isFinite(exitCode) ? Number(exitCode) : 1,
24
+ stdout,
25
+ stderr,
26
+ };
27
+ };
28
+ }
29
+ function describeLifecycleError(err) {
30
+ if (err instanceof Error) {
31
+ return err.message;
32
+ }
33
+ return String(err);
34
+ }
35
+ export function createProcessSessionLifecycle(opts) {
36
+ const runShellCommand = opts.runShellCommand ?? createShellCommandRunner(opts.repoRoot);
37
+ let sessionMutationScheduled = null;
38
+ const scheduleReload = async () => {
39
+ if (sessionMutationScheduled) {
40
+ return {
41
+ ok: true,
42
+ action: sessionMutationScheduled.action,
43
+ message: `session ${sessionMutationScheduled.action} already scheduled`,
44
+ details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
45
+ };
46
+ }
47
+ const nowMs = Date.now();
48
+ const restartCommand = Bun.env.MU_RESTART_COMMAND?.trim();
49
+ const inferredArgs = process.argv[0] === process.execPath
50
+ ? [process.execPath, ...process.argv.slice(1)]
51
+ : [process.execPath, ...process.argv];
52
+ const restartShellCommand = restartCommand && restartCommand.length > 0 ? restartCommand : shellJoin(inferredArgs);
53
+ if (!restartShellCommand.trim()) {
54
+ return {
55
+ ok: false,
56
+ action: "reload",
57
+ message: "unable to determine restart command",
58
+ };
59
+ }
60
+ const exitDelayMs = 1_000;
61
+ const launchDelayMs = exitDelayMs + 300;
62
+ const delayedShellCommand = `sleep ${(launchDelayMs / 1_000).toFixed(2)}; ${restartShellCommand}`;
63
+ let spawnedPid = null;
64
+ try {
65
+ const proc = Bun.spawn({
66
+ cmd: ["bash", "-lc", delayedShellCommand],
67
+ cwd: opts.repoRoot,
68
+ env: Bun.env,
69
+ stdin: "ignore",
70
+ stdout: "inherit",
71
+ stderr: "inherit",
72
+ });
73
+ spawnedPid = proc.pid ?? null;
74
+ }
75
+ catch (err) {
76
+ return {
77
+ ok: false,
78
+ action: "reload",
79
+ message: `failed to spawn replacement process: ${describeLifecycleError(err)}`,
80
+ };
81
+ }
82
+ sessionMutationScheduled = { action: "reload", at_ms: nowMs };
83
+ setTimeout(() => {
84
+ process.exit(0);
85
+ }, exitDelayMs);
86
+ return {
87
+ ok: true,
88
+ action: "reload",
89
+ message: "reload scheduled; restarting process",
90
+ details: {
91
+ restart_command: restartShellCommand,
92
+ restart_launch_command: delayedShellCommand,
93
+ spawned_pid: spawnedPid,
94
+ exit_delay_ms: exitDelayMs,
95
+ launch_delay_ms: launchDelayMs,
96
+ },
97
+ };
98
+ };
99
+ const scheduleUpdate = async () => {
100
+ if (sessionMutationScheduled) {
101
+ return {
102
+ ok: true,
103
+ action: sessionMutationScheduled.action,
104
+ message: `session ${sessionMutationScheduled.action} already scheduled`,
105
+ details: { scheduled_at_ms: sessionMutationScheduled.at_ms },
106
+ };
107
+ }
108
+ const updateCommand = Bun.env.MU_UPDATE_COMMAND?.trim() || "npm install -g @femtomc/mu@latest";
109
+ const result = await runShellCommand(updateCommand);
110
+ if (result.exitCode !== 0) {
111
+ return {
112
+ ok: false,
113
+ action: "update",
114
+ message: `update command failed (exit ${result.exitCode})`,
115
+ details: {
116
+ update_command: updateCommand,
117
+ stdout: result.stdout.slice(-4_000),
118
+ stderr: result.stderr.slice(-4_000),
119
+ },
120
+ };
121
+ }
122
+ const reloadResult = await scheduleReload();
123
+ if (!reloadResult.ok) {
124
+ return {
125
+ ok: false,
126
+ action: "update",
127
+ message: reloadResult.message,
128
+ details: {
129
+ update_command: updateCommand,
130
+ reload: reloadResult.details ?? null,
131
+ },
132
+ };
133
+ }
134
+ return {
135
+ ok: true,
136
+ action: "update",
137
+ message: "update applied; reload scheduled",
138
+ details: {
139
+ update_command: updateCommand,
140
+ reload: reloadResult.details ?? null,
141
+ update_stdout_tail: result.stdout.slice(-1_000),
142
+ },
143
+ };
144
+ };
145
+ return {
146
+ reload: scheduleReload,
147
+ update: scheduleUpdate,
148
+ };
149
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.69",
3
+ "version": "26.2.70",
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.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"
34
+ "@femtomc/mu-agent": "26.2.70",
35
+ "@femtomc/mu-control-plane": "26.2.70",
36
+ "@femtomc/mu-core": "26.2.70",
37
+ "@femtomc/mu-forum": "26.2.70",
38
+ "@femtomc/mu-issue": "26.2.70"
39
39
  }
40
40
  }