@femtomc/mu-server 26.2.66 → 26.2.68
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/dist/control_plane.d.ts +12 -0
- package/dist/control_plane.js +110 -0
- package/dist/index.d.ts +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +152 -0
- package/package.json +6 -6
package/dist/control_plane.d.ts
CHANGED
|
@@ -101,6 +101,17 @@ export type TelegramGenerationSwapHooks = {
|
|
|
101
101
|
timeout_ms: number;
|
|
102
102
|
}) => void | Promise<void>;
|
|
103
103
|
};
|
|
104
|
+
export type ControlPlaneSessionMutationAction = "reload" | "update";
|
|
105
|
+
export type ControlPlaneSessionMutationResult = {
|
|
106
|
+
ok: boolean;
|
|
107
|
+
action: ControlPlaneSessionMutationAction;
|
|
108
|
+
message: string;
|
|
109
|
+
details?: Record<string, unknown>;
|
|
110
|
+
};
|
|
111
|
+
export type ControlPlaneSessionMutationHooks = {
|
|
112
|
+
reload?: () => Promise<ControlPlaneSessionMutationResult>;
|
|
113
|
+
update?: () => Promise<ControlPlaneSessionMutationResult>;
|
|
114
|
+
};
|
|
104
115
|
type DetectedAdapter = {
|
|
105
116
|
name: "slack";
|
|
106
117
|
signingSecret: string;
|
|
@@ -140,6 +151,7 @@ export type BootstrapControlPlaneOpts = {
|
|
|
140
151
|
heartbeatScheduler?: ActivityHeartbeatScheduler;
|
|
141
152
|
runSupervisorSpawnProcess?: ControlPlaneRunSupervisorOpts["spawnProcess"];
|
|
142
153
|
runSupervisorHeartbeatIntervalMs?: number;
|
|
154
|
+
sessionMutationHooks?: ControlPlaneSessionMutationHooks;
|
|
143
155
|
generation?: ControlPlaneGenerationContext;
|
|
144
156
|
telemetry?: GenerationTelemetryRecorder | null;
|
|
145
157
|
telegramGenerationHooks?: TelegramGenerationSwapHooks;
|
package/dist/control_plane.js
CHANGED
|
@@ -745,6 +745,116 @@ export async function bootstrapControlPlane(opts) {
|
|
|
745
745
|
runtime,
|
|
746
746
|
operator,
|
|
747
747
|
mutationExecutor: async (record) => {
|
|
748
|
+
if (record.target_type === "reload" || record.target_type === "update") {
|
|
749
|
+
if (record.command_args.length > 0) {
|
|
750
|
+
return {
|
|
751
|
+
terminalState: "failed",
|
|
752
|
+
errorCode: "cli_validation_failed",
|
|
753
|
+
trace: {
|
|
754
|
+
cliCommandKind: record.target_type,
|
|
755
|
+
runRootId: null,
|
|
756
|
+
},
|
|
757
|
+
mutatingEvents: [
|
|
758
|
+
{
|
|
759
|
+
eventType: "session.lifecycle.command.failed",
|
|
760
|
+
payload: {
|
|
761
|
+
action: record.target_type,
|
|
762
|
+
reason: "unexpected_args",
|
|
763
|
+
args: record.command_args,
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
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
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const lifecycle = await hook();
|
|
794
|
+
if (!lifecycle.ok) {
|
|
795
|
+
return {
|
|
796
|
+
terminalState: "failed",
|
|
797
|
+
errorCode: "session_lifecycle_failed",
|
|
798
|
+
trace: {
|
|
799
|
+
cliCommandKind: action,
|
|
800
|
+
runRootId: null,
|
|
801
|
+
},
|
|
802
|
+
mutatingEvents: [
|
|
803
|
+
{
|
|
804
|
+
eventType: "session.lifecycle.command.failed",
|
|
805
|
+
payload: {
|
|
806
|
+
action,
|
|
807
|
+
reason: lifecycle.message,
|
|
808
|
+
details: lifecycle.details ?? null,
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
terminalState: "completed",
|
|
816
|
+
result: {
|
|
817
|
+
ok: true,
|
|
818
|
+
action,
|
|
819
|
+
message: lifecycle.message,
|
|
820
|
+
details: lifecycle.details ?? null,
|
|
821
|
+
},
|
|
822
|
+
trace: {
|
|
823
|
+
cliCommandKind: action,
|
|
824
|
+
runRootId: null,
|
|
825
|
+
},
|
|
826
|
+
mutatingEvents: [
|
|
827
|
+
{
|
|
828
|
+
eventType: `session.lifecycle.command.${action}`,
|
|
829
|
+
payload: {
|
|
830
|
+
action,
|
|
831
|
+
message: lifecycle.message,
|
|
832
|
+
details: lifecycle.details ?? null,
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
],
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
return {
|
|
840
|
+
terminalState: "failed",
|
|
841
|
+
errorCode: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
|
|
842
|
+
trace: {
|
|
843
|
+
cliCommandKind: action,
|
|
844
|
+
runRootId: null,
|
|
845
|
+
},
|
|
846
|
+
mutatingEvents: [
|
|
847
|
+
{
|
|
848
|
+
eventType: "session.lifecycle.command.failed",
|
|
849
|
+
payload: {
|
|
850
|
+
action,
|
|
851
|
+
reason: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
748
858
|
if (record.target_type === "run start" || record.target_type === "run resume") {
|
|
749
859
|
try {
|
|
750
860
|
const launched = await runSupervisor?.startFromCommand(record);
|
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 } from "./control_plane.js";
|
|
5
|
+
export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationHooks, 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";
|
package/dist/server.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ 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 } from "./control_plane.js";
|
|
8
|
+
import { type ControlPlaneConfig, type ControlPlaneHandle, type ControlPlaneSessionMutationHooks } 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";
|
|
@@ -30,6 +30,7 @@ export type ServerOptions = {
|
|
|
30
30
|
config?: MuConfig;
|
|
31
31
|
configReader?: ConfigReader;
|
|
32
32
|
configWriter?: ConfigWriter;
|
|
33
|
+
sessionMutationHooks?: ControlPlaneSessionMutationHooks;
|
|
33
34
|
};
|
|
34
35
|
export type ServerContext = {
|
|
35
36
|
repoRoot: string;
|
package/dist/server.js
CHANGED
|
@@ -46,6 +46,12 @@ function toNonNegativeInt(value, fallback) {
|
|
|
46
46
|
}
|
|
47
47
|
return Math.max(0, Math.trunc(fallback));
|
|
48
48
|
}
|
|
49
|
+
function shellQuoteArg(value) {
|
|
50
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
51
|
+
}
|
|
52
|
+
function shellJoin(args) {
|
|
53
|
+
return args.map(shellQuoteArg).join(" ");
|
|
54
|
+
}
|
|
49
55
|
function describeError(err) {
|
|
50
56
|
if (err instanceof Error)
|
|
51
57
|
return err.message;
|
|
@@ -165,6 +171,144 @@ export function createServer(options = {}) {
|
|
|
165
171
|
const autoRunHeartbeatEveryMs = Math.max(1_000, toNonNegativeInt(options.autoRunHeartbeatEveryMs, DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS));
|
|
166
172
|
const operatorWakeLastByKey = new Map();
|
|
167
173
|
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;
|
|
168
312
|
const emitOperatorWake = async (opts) => {
|
|
169
313
|
const dedupeKey = opts.dedupeKey.trim();
|
|
170
314
|
if (!dedupeKey) {
|
|
@@ -214,6 +358,7 @@ export function createServer(options = {}) {
|
|
|
214
358
|
heartbeatScheduler,
|
|
215
359
|
generation,
|
|
216
360
|
telemetry: generationTelemetry,
|
|
361
|
+
sessionMutationHooks,
|
|
217
362
|
terminalEnabled: true,
|
|
218
363
|
});
|
|
219
364
|
});
|
|
@@ -1151,6 +1296,12 @@ export function createServer(options = {}) {
|
|
|
1151
1296
|
commandText = `mu! run interrupt${rootId ? ` ${rootId}` : ""}`;
|
|
1152
1297
|
break;
|
|
1153
1298
|
}
|
|
1299
|
+
case "reload":
|
|
1300
|
+
commandText = "/mu reload";
|
|
1301
|
+
break;
|
|
1302
|
+
case "update":
|
|
1303
|
+
commandText = "/mu update";
|
|
1304
|
+
break;
|
|
1154
1305
|
case "status":
|
|
1155
1306
|
commandText = "/mu status";
|
|
1156
1307
|
break;
|
|
@@ -2112,6 +2263,7 @@ export async function createServerAsync(options = {}) {
|
|
|
2112
2263
|
generation_seq: 0,
|
|
2113
2264
|
},
|
|
2114
2265
|
telemetry: generationTelemetry,
|
|
2266
|
+
sessionMutationHooks: options.sessionMutationHooks,
|
|
2115
2267
|
terminalEnabled: true,
|
|
2116
2268
|
});
|
|
2117
2269
|
const serverConfig = createServer({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-server",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.68",
|
|
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.68",
|
|
35
|
+
"@femtomc/mu-control-plane": "26.2.68",
|
|
36
|
+
"@femtomc/mu-core": "26.2.68",
|
|
37
|
+
"@femtomc/mu-forum": "26.2.68",
|
|
38
|
+
"@femtomc/mu-issue": "26.2.68"
|
|
39
39
|
}
|
|
40
40
|
}
|