@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 +8 -7
- package/dist/cli.js +8 -6
- package/dist/control_plane.d.ts +4 -4
- package/dist/control_plane.js +2 -23
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +38 -8
- package/dist/server.js +181 -161
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -11,18 +11,19 @@ bun add @femtomc/mu-server
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import {
|
|
14
|
+
import { composeServerRuntime, createServerFromRuntime } from "@femtomc/mu-server";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
};
|
package/dist/control_plane.d.ts
CHANGED
|
@@ -108,9 +108,9 @@ export type ControlPlaneSessionMutationResult = {
|
|
|
108
108
|
message: string;
|
|
109
109
|
details?: Record<string, unknown>;
|
|
110
110
|
};
|
|
111
|
-
export type
|
|
112
|
-
reload
|
|
113
|
-
update
|
|
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
|
-
|
|
154
|
+
sessionLifecycle: ControlPlaneSessionLifecycle;
|
|
155
155
|
generation?: ControlPlaneGenerationContext;
|
|
156
156
|
telemetry?: GenerationTelemetryRecorder | null;
|
|
157
157
|
telegramGenerationHooks?: TelegramGenerationSwapHooks;
|
package/dist/control_plane.js
CHANGED
|
@@ -767,30 +767,9 @@ export async function bootstrapControlPlane(opts) {
|
|
|
767
767
|
};
|
|
768
768
|
}
|
|
769
769
|
const action = record.target_type;
|
|
770
|
-
const
|
|
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
|
|
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,
|
|
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,
|
|
18
|
-
export { createContext,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2277
|
-
|
|
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.
|
|
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.
|
|
35
|
-
"@femtomc/mu-control-plane": "26.2.
|
|
36
|
-
"@femtomc/mu-core": "26.2.
|
|
37
|
-
"@femtomc/mu-forum": "26.2.
|
|
38
|
-
"@femtomc/mu-issue": "26.2.
|
|
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
|
}
|