@femtomc/mu-server 26.2.69 → 26.2.71
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 +7 -3
- package/dist/api/activities.d.ts +2 -0
- package/dist/api/activities.js +160 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +45 -0
- package/dist/api/control_plane.d.ts +2 -0
- package/dist/api/control_plane.js +28 -0
- package/dist/api/cron.d.ts +2 -0
- package/dist/api/cron.js +182 -0
- package/dist/api/events.js +77 -19
- package/dist/api/forum.js +52 -18
- package/dist/api/heartbeats.d.ts +2 -0
- package/dist/api/heartbeats.js +211 -0
- package/dist/api/identities.d.ts +2 -0
- package/dist/api/identities.js +103 -0
- package/dist/api/issues.js +120 -33
- package/dist/api/runs.d.ts +2 -0
- package/dist/api/runs.js +207 -0
- package/dist/cli.js +58 -3
- package/dist/config.d.ts +4 -21
- package/dist/config.js +24 -75
- package/dist/control_plane.d.ts +7 -114
- package/dist/control_plane.js +238 -654
- package/dist/control_plane_bootstrap_helpers.d.ts +16 -0
- package/dist/control_plane_bootstrap_helpers.js +85 -0
- package/dist/control_plane_contract.d.ts +176 -0
- package/dist/control_plane_contract.js +1 -0
- package/dist/control_plane_reload.d.ts +63 -0
- package/dist/control_plane_reload.js +525 -0
- package/dist/control_plane_run_outbox.d.ts +7 -0
- package/dist/control_plane_run_outbox.js +52 -0
- package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
- package/dist/control_plane_run_queue_coordinator.js +327 -0
- package/dist/control_plane_telegram_generation.d.ts +27 -0
- package/dist/control_plane_telegram_generation.js +520 -0
- package/dist/control_plane_wake_delivery.d.ts +50 -0
- package/dist/control_plane_wake_delivery.js +123 -0
- package/dist/cron_request.d.ts +8 -0
- package/dist/cron_request.js +65 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -1
- package/dist/run_queue.d.ts +95 -0
- package/dist/run_queue.js +817 -0
- package/dist/run_supervisor.d.ts +20 -0
- package/dist/run_supervisor.js +25 -1
- package/dist/server.d.ts +12 -49
- package/dist/server.js +365 -2128
- package/dist/server_program_orchestration.d.ts +38 -0
- package/dist/server_program_orchestration.js +254 -0
- package/dist/server_routing.d.ts +31 -0
- package/dist/server_routing.js +230 -0
- package/dist/server_runtime.d.ts +30 -0
- package/dist/server_runtime.js +43 -0
- package/dist/server_types.d.ts +3 -0
- package/dist/server_types.js +16 -0
- package/dist/session_lifecycle.d.ts +11 -0
- package/dist/session_lifecycle.js +149 -0
- package/package.json +7 -6
package/dist/server.js
CHANGED
|
@@ -1,273 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { GenerationTelemetryRecorder, getControlPlanePaths, IdentityStore, ROLE_SCOPES, } from "@femtomc/mu-control-plane";
|
|
1
|
+
import { GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
|
|
3
2
|
import { currentRunId, EventLog, FsJsonlStore, getStorePaths, JsonlEventSink } from "@femtomc/mu-core/node";
|
|
4
3
|
import { ForumStore } from "@femtomc/mu-forum";
|
|
5
4
|
import { IssueStore } from "@femtomc/mu-issue";
|
|
6
5
|
import { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
|
|
11
|
-
import { bootstrapControlPlane, } from "./control_plane.js";
|
|
12
|
-
import { CronProgramRegistry } from "./cron_programs.js";
|
|
13
|
-
import { ControlPlaneGenerationSupervisor } from "./generation_supervisor.js";
|
|
14
|
-
import { HeartbeatProgramRegistry, } from "./heartbeat_programs.js";
|
|
6
|
+
import { DEFAULT_MU_CONFIG, readMuConfigFile, writeMuConfigFile, } from "./config.js";
|
|
7
|
+
import { bootstrapControlPlane } from "./control_plane.js";
|
|
8
|
+
import { createReloadManager, } from "./control_plane_reload.js";
|
|
15
9
|
import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
".json": "application/json",
|
|
21
|
-
".png": "image/png",
|
|
22
|
-
".jpg": "image/jpeg",
|
|
23
|
-
".svg": "image/svg+xml",
|
|
24
|
-
".ico": "image/x-icon",
|
|
25
|
-
".woff": "font/woff",
|
|
26
|
-
".woff2": "font/woff2",
|
|
27
|
-
};
|
|
28
|
-
// Resolve public/ dir relative to this file (works in npm global installs)
|
|
29
|
-
const PUBLIC_DIR = join(new URL(".", import.meta.url).pathname, "..", "public");
|
|
10
|
+
import { createProcessSessionLifecycle } from "./session_lifecycle.js";
|
|
11
|
+
import { createServerProgramOrchestration } from "./server_program_orchestration.js";
|
|
12
|
+
import { createServerRequestHandler } from "./server_routing.js";
|
|
13
|
+
import { toNonNegativeInt } from "./server_types.js";
|
|
30
14
|
const DEFAULT_OPERATOR_WAKE_COALESCE_MS = 2_000;
|
|
31
15
|
const DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS = 15_000;
|
|
32
|
-
|
|
33
|
-
function normalizeWakeMode(value) {
|
|
34
|
-
if (typeof value !== "string") {
|
|
35
|
-
return "immediate";
|
|
36
|
-
}
|
|
37
|
-
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
38
|
-
return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
|
|
39
|
-
}
|
|
40
|
-
function toNonNegativeInt(value, fallback) {
|
|
41
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
42
|
-
return Math.max(0, Math.trunc(value));
|
|
43
|
-
}
|
|
44
|
-
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
45
|
-
return Math.max(0, Number.parseInt(value, 10));
|
|
46
|
-
}
|
|
47
|
-
return Math.max(0, Math.trunc(fallback));
|
|
48
|
-
}
|
|
49
|
-
function shellQuoteArg(value) {
|
|
50
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
51
|
-
}
|
|
52
|
-
function shellJoin(args) {
|
|
53
|
-
return args.map(shellQuoteArg).join(" ");
|
|
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
|
-
}
|
|
16
|
+
export { createProcessSessionLifecycle };
|
|
192
17
|
function describeError(err) {
|
|
193
18
|
if (err instanceof Error)
|
|
194
19
|
return err.message;
|
|
195
20
|
return String(err);
|
|
196
21
|
}
|
|
197
|
-
function
|
|
198
|
-
if (!handle) {
|
|
199
|
-
return { active: false, adapters: [], routes: [] };
|
|
200
|
-
}
|
|
22
|
+
function emptyNotifyOperatorsResult() {
|
|
201
23
|
return {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
24
|
+
queued: 0,
|
|
25
|
+
duplicate: 0,
|
|
26
|
+
skipped: 0,
|
|
27
|
+
decisions: [],
|
|
205
28
|
};
|
|
206
29
|
}
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
|
|
211
|
-
const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
|
|
212
|
-
if (!jobId && !rootIssueId) {
|
|
213
|
-
return {
|
|
214
|
-
target: null,
|
|
215
|
-
error: "run target requires run_job_id or run_root_issue_id",
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
target: {
|
|
220
|
-
kind: "run",
|
|
221
|
-
job_id: jobId || null,
|
|
222
|
-
root_issue_id: rootIssueId || null,
|
|
223
|
-
},
|
|
224
|
-
error: null,
|
|
225
|
-
};
|
|
30
|
+
function normalizeWakeTurnMode(value) {
|
|
31
|
+
if (typeof value !== "string") {
|
|
32
|
+
return "off";
|
|
226
33
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
target: null,
|
|
232
|
-
error: "activity target requires activity_id",
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
target: {
|
|
237
|
-
kind: "activity",
|
|
238
|
-
activity_id: activityId,
|
|
239
|
-
},
|
|
240
|
-
error: null,
|
|
241
|
-
};
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
if (normalized === "shadow") {
|
|
36
|
+
return "shadow";
|
|
242
37
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
38
|
+
if (normalized === "active") {
|
|
39
|
+
return "active";
|
|
40
|
+
}
|
|
41
|
+
return "off";
|
|
247
42
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
43
|
+
function stringField(payload, key) {
|
|
44
|
+
const value = payload[key];
|
|
45
|
+
if (typeof value !== "string") {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
50
|
+
}
|
|
51
|
+
function numberField(payload, key) {
|
|
52
|
+
const value = payload[key];
|
|
53
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return Math.trunc(value);
|
|
257
57
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
58
|
+
function computeWakeId(opts) {
|
|
59
|
+
const source = stringField(opts.payload, "wake_source") ?? "unknown";
|
|
60
|
+
const programId = stringField(opts.payload, "program_id") ?? "unknown";
|
|
61
|
+
const sourceTsMs = numberField(opts.payload, "source_ts_ms");
|
|
62
|
+
const target = Object.hasOwn(opts.payload, "target") ? opts.payload.target : null;
|
|
63
|
+
let targetFingerprint = "null";
|
|
64
|
+
try {
|
|
65
|
+
targetFingerprint = JSON.stringify(target) ?? "null";
|
|
261
66
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
67
|
+
catch {
|
|
68
|
+
targetFingerprint = "[unserializable]";
|
|
69
|
+
}
|
|
70
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
71
|
+
hasher.update(`${source}|${programId}|${sourceTsMs ?? "na"}|${opts.dedupeKey}|${targetFingerprint}`);
|
|
72
|
+
return hasher.digest("hex").slice(0, 16);
|
|
73
|
+
}
|
|
74
|
+
function buildWakeTurnCommandText(opts) {
|
|
75
|
+
const wakeSource = stringField(opts.payload, "wake_source") ?? "unknown";
|
|
76
|
+
const programId = stringField(opts.payload, "program_id") ?? "unknown";
|
|
77
|
+
const wakeMode = stringField(opts.payload, "wake_mode") ?? "immediate";
|
|
78
|
+
const targetKind = stringField(opts.payload, "target_kind") ?? "unknown";
|
|
79
|
+
const reason = stringField(opts.payload, "reason") ?? "scheduled";
|
|
80
|
+
let target = "null";
|
|
81
|
+
try {
|
|
82
|
+
target = JSON.stringify(Object.hasOwn(opts.payload, "target") ? opts.payload.target : null) ?? "null";
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
target = "[unserializable]";
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
"Autonomous wake turn triggered by heartbeat/cron scheduler.",
|
|
89
|
+
`wake_id=${opts.wakeId}`,
|
|
90
|
+
`wake_source=${wakeSource}`,
|
|
91
|
+
`program_id=${programId}`,
|
|
92
|
+
`wake_mode=${wakeMode}`,
|
|
93
|
+
`target_kind=${targetKind}`,
|
|
94
|
+
`reason=${reason}`,
|
|
95
|
+
`message=${opts.message}`,
|
|
96
|
+
`target=${target}`,
|
|
97
|
+
"",
|
|
98
|
+
"If an action is needed, produce exactly one `/mu ...` command. If no action is needed, provide a short operator response.",
|
|
99
|
+
].join("\n");
|
|
271
100
|
}
|
|
272
101
|
export function createContext(repoRoot) {
|
|
273
102
|
const paths = getStorePaths(repoRoot);
|
|
@@ -307,8 +136,13 @@ function createServer(options = {}) {
|
|
|
307
136
|
const operatorWakeCoalesceMs = toNonNegativeInt(options.operatorWakeCoalesceMs, DEFAULT_OPERATOR_WAKE_COALESCE_MS);
|
|
308
137
|
const autoRunHeartbeatEveryMs = Math.max(1_000, toNonNegativeInt(options.autoRunHeartbeatEveryMs, DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS));
|
|
309
138
|
const operatorWakeLastByKey = new Map();
|
|
310
|
-
const autoRunHeartbeatProgramByJobId = new Map();
|
|
311
139
|
const sessionLifecycle = options.sessionLifecycle ?? createProcessSessionLifecycle({ repoRoot });
|
|
140
|
+
const emitWakeDeliveryEvent = async (payload) => {
|
|
141
|
+
await context.eventLog.emit("operator.wake.delivery", {
|
|
142
|
+
source: "mu-server.operator-wake",
|
|
143
|
+
payload,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
312
146
|
const emitOperatorWake = async (opts) => {
|
|
313
147
|
const dedupeKey = opts.dedupeKey.trim();
|
|
314
148
|
if (!dedupeKey) {
|
|
@@ -321,6 +155,160 @@ function createServer(options = {}) {
|
|
|
321
155
|
return false;
|
|
322
156
|
}
|
|
323
157
|
operatorWakeLastByKey.set(dedupeKey, nowMs);
|
|
158
|
+
const wakeId = computeWakeId({ dedupeKey, payload: opts.payload });
|
|
159
|
+
const selectedWakeMode = stringField(opts.payload, "wake_mode");
|
|
160
|
+
const wakeSource = stringField(opts.payload, "wake_source");
|
|
161
|
+
const programId = stringField(opts.payload, "program_id");
|
|
162
|
+
const sourceTsMs = numberField(opts.payload, "source_ts_ms");
|
|
163
|
+
let wakeTurnMode = normalizeWakeTurnMode(fallbackConfig.control_plane.operator.wake_turn_mode);
|
|
164
|
+
let configReadError = null;
|
|
165
|
+
try {
|
|
166
|
+
const config = await loadConfigFromDisk();
|
|
167
|
+
wakeTurnMode = normalizeWakeTurnMode(config.control_plane.operator.wake_turn_mode);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
configReadError = describeError(err);
|
|
171
|
+
}
|
|
172
|
+
let decision;
|
|
173
|
+
if (wakeTurnMode === "off") {
|
|
174
|
+
decision = {
|
|
175
|
+
outcome: "skipped",
|
|
176
|
+
reason: "feature_disabled",
|
|
177
|
+
wakeTurnMode,
|
|
178
|
+
selectedWakeMode,
|
|
179
|
+
turnRequestId: null,
|
|
180
|
+
turnResultKind: null,
|
|
181
|
+
error: configReadError,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
else if (wakeTurnMode === "shadow") {
|
|
185
|
+
decision = {
|
|
186
|
+
outcome: "skipped",
|
|
187
|
+
reason: "shadow_mode",
|
|
188
|
+
wakeTurnMode,
|
|
189
|
+
selectedWakeMode,
|
|
190
|
+
turnRequestId: null,
|
|
191
|
+
turnResultKind: null,
|
|
192
|
+
error: configReadError,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
else if (typeof controlPlaneProxy.submitTerminalCommand !== "function") {
|
|
196
|
+
decision = {
|
|
197
|
+
outcome: "fallback",
|
|
198
|
+
reason: "control_plane_unavailable",
|
|
199
|
+
wakeTurnMode,
|
|
200
|
+
selectedWakeMode,
|
|
201
|
+
turnRequestId: null,
|
|
202
|
+
turnResultKind: null,
|
|
203
|
+
error: configReadError,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const turnRequestId = `wake-turn-${wakeId}`;
|
|
208
|
+
try {
|
|
209
|
+
const turnResult = await controlPlaneProxy.submitTerminalCommand({
|
|
210
|
+
commandText: buildWakeTurnCommandText({
|
|
211
|
+
wakeId,
|
|
212
|
+
message: opts.message,
|
|
213
|
+
payload: opts.payload,
|
|
214
|
+
}),
|
|
215
|
+
repoRoot: context.repoRoot,
|
|
216
|
+
requestId: turnRequestId,
|
|
217
|
+
});
|
|
218
|
+
if (turnResult.kind === "noop" || turnResult.kind === "invalid") {
|
|
219
|
+
decision = {
|
|
220
|
+
outcome: "fallback",
|
|
221
|
+
reason: `turn_result_${turnResult.kind}`,
|
|
222
|
+
wakeTurnMode,
|
|
223
|
+
selectedWakeMode,
|
|
224
|
+
turnRequestId,
|
|
225
|
+
turnResultKind: turnResult.kind,
|
|
226
|
+
error: configReadError,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
decision = {
|
|
231
|
+
outcome: "triggered",
|
|
232
|
+
reason: "turn_invoked",
|
|
233
|
+
wakeTurnMode,
|
|
234
|
+
selectedWakeMode,
|
|
235
|
+
turnRequestId,
|
|
236
|
+
turnResultKind: turnResult.kind,
|
|
237
|
+
error: configReadError,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const error = describeError(err);
|
|
243
|
+
decision = {
|
|
244
|
+
outcome: "fallback",
|
|
245
|
+
reason: error === "control_plane_unavailable" ? "control_plane_unavailable" : "turn_execution_failed",
|
|
246
|
+
wakeTurnMode,
|
|
247
|
+
selectedWakeMode,
|
|
248
|
+
turnRequestId,
|
|
249
|
+
turnResultKind: null,
|
|
250
|
+
error,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
await context.eventLog.emit("operator.wake.decision", {
|
|
255
|
+
source: "mu-server.operator-wake",
|
|
256
|
+
payload: {
|
|
257
|
+
wake_id: wakeId,
|
|
258
|
+
dedupe_key: dedupeKey,
|
|
259
|
+
wake_source: wakeSource,
|
|
260
|
+
program_id: programId,
|
|
261
|
+
source_ts_ms: sourceTsMs,
|
|
262
|
+
selected_wake_mode: selectedWakeMode,
|
|
263
|
+
wake_turn_mode: decision.wakeTurnMode,
|
|
264
|
+
wake_turn_feature_enabled: decision.wakeTurnMode === "active",
|
|
265
|
+
outcome: decision.outcome,
|
|
266
|
+
reason: decision.reason,
|
|
267
|
+
turn_request_id: decision.turnRequestId,
|
|
268
|
+
turn_result_kind: decision.turnResultKind,
|
|
269
|
+
error: decision.error,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
let notifyResult = emptyNotifyOperatorsResult();
|
|
273
|
+
let notifyError = null;
|
|
274
|
+
if (typeof controlPlaneProxy.notifyOperators === "function") {
|
|
275
|
+
try {
|
|
276
|
+
notifyResult = await controlPlaneProxy.notifyOperators({
|
|
277
|
+
message: opts.message,
|
|
278
|
+
dedupeKey,
|
|
279
|
+
wake: {
|
|
280
|
+
wakeId,
|
|
281
|
+
wakeSource,
|
|
282
|
+
programId,
|
|
283
|
+
sourceTsMs,
|
|
284
|
+
},
|
|
285
|
+
metadata: {
|
|
286
|
+
wake_delivery_reason: "heartbeat_cron_wake",
|
|
287
|
+
wake_turn_outcome: decision.outcome,
|
|
288
|
+
wake_turn_reason: decision.reason,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
notifyError = describeError(err);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const deliveryDecision of notifyResult.decisions) {
|
|
297
|
+
await emitWakeDeliveryEvent({
|
|
298
|
+
state: deliveryDecision.state,
|
|
299
|
+
reason_code: deliveryDecision.reason_code,
|
|
300
|
+
wake_id: wakeId,
|
|
301
|
+
dedupe_key: dedupeKey,
|
|
302
|
+
binding_id: deliveryDecision.binding_id,
|
|
303
|
+
channel: deliveryDecision.channel,
|
|
304
|
+
outbox_id: deliveryDecision.outbox_id,
|
|
305
|
+
outbox_dedupe_key: deliveryDecision.dedupe_key,
|
|
306
|
+
attempt_count: null,
|
|
307
|
+
wake_source: wakeSource,
|
|
308
|
+
program_id: programId,
|
|
309
|
+
source_ts_ms: sourceTsMs,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
324
312
|
await context.eventLog.emit("operator.wake", {
|
|
325
313
|
source: "mu-server.operator-wake",
|
|
326
314
|
payload: {
|
|
@@ -328,28 +316,43 @@ function createServer(options = {}) {
|
|
|
328
316
|
dedupe_key: dedupeKey,
|
|
329
317
|
coalesce_ms: coalesceMs,
|
|
330
318
|
...opts.payload,
|
|
319
|
+
wake_id: wakeId,
|
|
320
|
+
decision_outcome: decision.outcome,
|
|
321
|
+
decision_reason: decision.reason,
|
|
322
|
+
wake_turn_mode: decision.wakeTurnMode,
|
|
323
|
+
selected_wake_mode: decision.selectedWakeMode,
|
|
324
|
+
wake_turn_feature_enabled: decision.wakeTurnMode === "active",
|
|
325
|
+
turn_request_id: decision.turnRequestId,
|
|
326
|
+
turn_result_kind: decision.turnResultKind,
|
|
327
|
+
decision_error: decision.error,
|
|
328
|
+
delivery: {
|
|
329
|
+
queued: notifyResult.queued,
|
|
330
|
+
duplicate: notifyResult.duplicate,
|
|
331
|
+
skipped: notifyResult.skipped,
|
|
332
|
+
},
|
|
333
|
+
delivery_summary_v2: {
|
|
334
|
+
queued: notifyResult.queued,
|
|
335
|
+
duplicate: notifyResult.duplicate,
|
|
336
|
+
skipped: notifyResult.skipped,
|
|
337
|
+
total: notifyResult.decisions.length,
|
|
338
|
+
},
|
|
339
|
+
delivery_error: notifyError,
|
|
331
340
|
},
|
|
332
341
|
});
|
|
333
342
|
return true;
|
|
334
343
|
};
|
|
335
|
-
let controlPlaneCurrent = options.controlPlane ?? null;
|
|
336
|
-
let reloadInFlight = null;
|
|
337
344
|
const generationTelemetry = options.generationTelemetry ?? new GenerationTelemetryRecorder();
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
345
|
+
const loadConfigFromDisk = async () => {
|
|
346
|
+
try {
|
|
347
|
+
return await readConfig(context.repoRoot);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
if (err?.code === "ENOENT") {
|
|
351
|
+
return fallbackConfig;
|
|
344
352
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
generation_id: generation.generation_id,
|
|
349
|
-
generation_seq: generation.generation_seq,
|
|
350
|
-
supervisor: "control_plane",
|
|
351
|
-
component,
|
|
352
|
-
});
|
|
353
|
+
throw err;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
353
356
|
const controlPlaneReloader = options.controlPlaneReloader ??
|
|
354
357
|
(async ({ repoRoot, config, generation }) => {
|
|
355
358
|
return await bootstrapControlPlane({
|
|
@@ -359,1934 +362,168 @@ function createServer(options = {}) {
|
|
|
359
362
|
generation,
|
|
360
363
|
telemetry: generationTelemetry,
|
|
361
364
|
sessionLifecycle,
|
|
365
|
+
wakeDeliveryObserver: (event) => {
|
|
366
|
+
void emitWakeDeliveryEvent({
|
|
367
|
+
state: event.state,
|
|
368
|
+
reason_code: event.reason_code,
|
|
369
|
+
wake_id: event.wake_id,
|
|
370
|
+
dedupe_key: event.dedupe_key,
|
|
371
|
+
binding_id: event.binding_id,
|
|
372
|
+
channel: event.channel,
|
|
373
|
+
outbox_id: event.outbox_id,
|
|
374
|
+
outbox_dedupe_key: event.outbox_dedupe_key,
|
|
375
|
+
attempt_count: event.attempt_count,
|
|
376
|
+
});
|
|
377
|
+
},
|
|
362
378
|
terminalEnabled: true,
|
|
363
379
|
});
|
|
364
380
|
});
|
|
381
|
+
const reloadManager = createReloadManager({
|
|
382
|
+
repoRoot: context.repoRoot,
|
|
383
|
+
initialControlPlane: options.controlPlane ?? null,
|
|
384
|
+
controlPlaneReloader,
|
|
385
|
+
generationTelemetry,
|
|
386
|
+
loadConfigFromDisk,
|
|
387
|
+
});
|
|
388
|
+
const applyWakeDeliveryObserver = () => {
|
|
389
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
390
|
+
handle?.setWakeDeliveryObserver?.((event) => {
|
|
391
|
+
void emitWakeDeliveryEvent({
|
|
392
|
+
state: event.state,
|
|
393
|
+
reason_code: event.reason_code,
|
|
394
|
+
wake_id: event.wake_id,
|
|
395
|
+
dedupe_key: event.dedupe_key,
|
|
396
|
+
binding_id: event.binding_id,
|
|
397
|
+
channel: event.channel,
|
|
398
|
+
outbox_id: event.outbox_id,
|
|
399
|
+
outbox_dedupe_key: event.outbox_dedupe_key,
|
|
400
|
+
attempt_count: event.attempt_count,
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
applyWakeDeliveryObserver();
|
|
405
|
+
const reloadControlPlane = async (reason) => {
|
|
406
|
+
const result = await reloadManager.reloadControlPlane(reason);
|
|
407
|
+
applyWakeDeliveryObserver();
|
|
408
|
+
return result;
|
|
409
|
+
};
|
|
365
410
|
const controlPlaneProxy = {
|
|
366
411
|
get activeAdapters() {
|
|
367
|
-
return
|
|
412
|
+
return reloadManager.getControlPlaneCurrent()?.activeAdapters ?? [];
|
|
368
413
|
},
|
|
369
414
|
async handleWebhook(path, req) {
|
|
370
|
-
const handle =
|
|
415
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
371
416
|
if (!handle)
|
|
372
417
|
return null;
|
|
373
418
|
return await handle.handleWebhook(path, req);
|
|
374
419
|
},
|
|
420
|
+
async notifyOperators(opts) {
|
|
421
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
422
|
+
if (!handle?.notifyOperators) {
|
|
423
|
+
return emptyNotifyOperatorsResult();
|
|
424
|
+
}
|
|
425
|
+
return await handle.notifyOperators(opts);
|
|
426
|
+
},
|
|
427
|
+
setWakeDeliveryObserver(observer) {
|
|
428
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
429
|
+
handle?.setWakeDeliveryObserver?.(observer ?? null);
|
|
430
|
+
},
|
|
375
431
|
async listRuns(opts) {
|
|
376
|
-
const handle =
|
|
432
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
377
433
|
if (!handle?.listRuns)
|
|
378
434
|
return [];
|
|
379
435
|
return await handle.listRuns(opts);
|
|
380
436
|
},
|
|
381
437
|
async getRun(idOrRoot) {
|
|
382
|
-
const handle =
|
|
438
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
383
439
|
if (!handle?.getRun)
|
|
384
440
|
return null;
|
|
385
441
|
return await handle.getRun(idOrRoot);
|
|
386
442
|
},
|
|
387
443
|
async startRun(opts) {
|
|
388
|
-
const handle =
|
|
444
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
389
445
|
if (!handle?.startRun) {
|
|
390
446
|
throw new Error("run_supervisor_unavailable");
|
|
391
447
|
}
|
|
392
448
|
return await handle.startRun(opts);
|
|
393
449
|
},
|
|
394
450
|
async resumeRun(opts) {
|
|
395
|
-
const handle =
|
|
451
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
396
452
|
if (!handle?.resumeRun) {
|
|
397
453
|
throw new Error("run_supervisor_unavailable");
|
|
398
454
|
}
|
|
399
455
|
return await handle.resumeRun(opts);
|
|
400
456
|
},
|
|
401
457
|
async interruptRun(opts) {
|
|
402
|
-
const handle =
|
|
458
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
403
459
|
if (!handle?.interruptRun) {
|
|
404
460
|
return { ok: false, reason: "not_found", run: null };
|
|
405
461
|
}
|
|
406
462
|
return await handle.interruptRun(opts);
|
|
407
463
|
},
|
|
408
464
|
async heartbeatRun(opts) {
|
|
409
|
-
const handle =
|
|
465
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
410
466
|
if (!handle?.heartbeatRun) {
|
|
411
467
|
return { ok: false, reason: "not_found", run: null };
|
|
412
468
|
}
|
|
413
469
|
return await handle.heartbeatRun(opts);
|
|
414
470
|
},
|
|
415
471
|
async traceRun(opts) {
|
|
416
|
-
const handle =
|
|
472
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
417
473
|
if (!handle?.traceRun)
|
|
418
474
|
return null;
|
|
419
475
|
return await handle.traceRun(opts);
|
|
420
476
|
},
|
|
421
477
|
async submitTerminalCommand(opts) {
|
|
422
|
-
const handle =
|
|
478
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
423
479
|
if (!handle?.submitTerminalCommand) {
|
|
424
480
|
throw new Error("control_plane_unavailable");
|
|
425
481
|
}
|
|
426
482
|
return await handle.submitTerminalCommand(opts);
|
|
427
483
|
},
|
|
428
484
|
async stop() {
|
|
429
|
-
const handle =
|
|
430
|
-
|
|
485
|
+
const handle = reloadManager.getControlPlaneCurrent();
|
|
486
|
+
handle?.setWakeDeliveryObserver?.(null);
|
|
487
|
+
reloadManager.setControlPlaneCurrent(null);
|
|
431
488
|
await handle?.stop();
|
|
432
489
|
},
|
|
433
490
|
};
|
|
434
|
-
const heartbeatPrograms =
|
|
491
|
+
const { heartbeatPrograms, cronPrograms, registerAutoRunHeartbeatProgram, disableAutoRunHeartbeatProgram, } = createServerProgramOrchestration({
|
|
435
492
|
repoRoot,
|
|
436
493
|
heartbeatScheduler,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
wakeMode: opts.wakeMode,
|
|
443
|
-
});
|
|
444
|
-
return result ?? { ok: false, reason: "not_found" };
|
|
445
|
-
},
|
|
446
|
-
activityHeartbeat: async (opts) => {
|
|
447
|
-
return activitySupervisor.heartbeat({
|
|
448
|
-
activityId: opts.activityId ?? null,
|
|
449
|
-
reason: opts.reason ?? null,
|
|
450
|
-
});
|
|
451
|
-
},
|
|
452
|
-
onTickEvent: async (event) => {
|
|
453
|
-
await context.eventLog.emit("heartbeat_program.tick", {
|
|
454
|
-
source: "mu-server.heartbeat-programs",
|
|
455
|
-
payload: {
|
|
456
|
-
program_id: event.program_id,
|
|
457
|
-
status: event.status,
|
|
458
|
-
reason: event.reason,
|
|
459
|
-
message: event.message,
|
|
460
|
-
program: event.program,
|
|
461
|
-
},
|
|
462
|
-
});
|
|
463
|
-
await emitOperatorWake({
|
|
464
|
-
dedupeKey: `heartbeat-program:${event.program_id}`,
|
|
465
|
-
message: event.message,
|
|
466
|
-
payload: {
|
|
467
|
-
wake_source: "heartbeat_program",
|
|
468
|
-
program_id: event.program_id,
|
|
469
|
-
status: event.status,
|
|
470
|
-
reason: event.reason,
|
|
471
|
-
wake_mode: event.program.wake_mode,
|
|
472
|
-
target_kind: event.program.target.kind,
|
|
473
|
-
target: event.program.target.kind === "run"
|
|
474
|
-
? {
|
|
475
|
-
job_id: event.program.target.job_id,
|
|
476
|
-
root_issue_id: event.program.target.root_issue_id,
|
|
477
|
-
}
|
|
478
|
-
: { activity_id: event.program.target.activity_id },
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
},
|
|
494
|
+
controlPlaneProxy,
|
|
495
|
+
activitySupervisor,
|
|
496
|
+
eventLog: context.eventLog,
|
|
497
|
+
autoRunHeartbeatEveryMs,
|
|
498
|
+
emitOperatorWake,
|
|
482
499
|
});
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
activityId: opts.activityId ?? null,
|
|
498
|
-
reason: opts.reason ?? null,
|
|
499
|
-
});
|
|
500
|
-
},
|
|
501
|
-
onLifecycleEvent: async (event) => {
|
|
502
|
-
await context.eventLog.emit("cron_program.lifecycle", {
|
|
503
|
-
source: "mu-server.cron-programs",
|
|
504
|
-
payload: {
|
|
505
|
-
action: event.action,
|
|
506
|
-
program_id: event.program_id,
|
|
507
|
-
message: event.message,
|
|
508
|
-
program: event.program,
|
|
509
|
-
},
|
|
510
|
-
});
|
|
511
|
-
},
|
|
512
|
-
onTickEvent: async (event) => {
|
|
513
|
-
await context.eventLog.emit("cron_program.tick", {
|
|
514
|
-
source: "mu-server.cron-programs",
|
|
515
|
-
payload: {
|
|
516
|
-
program_id: event.program_id,
|
|
517
|
-
status: event.status,
|
|
518
|
-
reason: event.reason,
|
|
519
|
-
message: event.message,
|
|
520
|
-
program: event.program,
|
|
521
|
-
},
|
|
522
|
-
});
|
|
523
|
-
await emitOperatorWake({
|
|
524
|
-
dedupeKey: `cron-program:${event.program_id}`,
|
|
525
|
-
message: event.message,
|
|
526
|
-
payload: {
|
|
527
|
-
wake_source: "cron_program",
|
|
528
|
-
program_id: event.program_id,
|
|
529
|
-
status: event.status,
|
|
530
|
-
reason: event.reason,
|
|
531
|
-
wake_mode: event.program.wake_mode,
|
|
532
|
-
target_kind: event.program.target.kind,
|
|
533
|
-
target: event.program.target.kind === "run"
|
|
534
|
-
? {
|
|
535
|
-
job_id: event.program.target.job_id,
|
|
536
|
-
root_issue_id: event.program.target.root_issue_id,
|
|
537
|
-
}
|
|
538
|
-
: { activity_id: event.program.target.activity_id },
|
|
539
|
-
},
|
|
540
|
-
});
|
|
541
|
-
},
|
|
500
|
+
const handleRequest = createServerRequestHandler({
|
|
501
|
+
context,
|
|
502
|
+
controlPlaneProxy,
|
|
503
|
+
activitySupervisor,
|
|
504
|
+
heartbeatPrograms,
|
|
505
|
+
cronPrograms,
|
|
506
|
+
loadConfigFromDisk,
|
|
507
|
+
writeConfig,
|
|
508
|
+
reloadControlPlane,
|
|
509
|
+
getControlPlaneStatus: reloadManager.getControlPlaneStatus,
|
|
510
|
+
registerAutoRunHeartbeatProgram,
|
|
511
|
+
disableAutoRunHeartbeatProgram,
|
|
512
|
+
describeError,
|
|
513
|
+
initiateShutdown: options.initiateShutdown,
|
|
542
514
|
});
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (knownProgram) {
|
|
552
|
-
return knownProgram;
|
|
553
|
-
}
|
|
554
|
-
autoRunHeartbeatProgramByJobId.delete(normalizedJobId);
|
|
555
|
-
}
|
|
556
|
-
const programs = await heartbeatPrograms.list({ targetKind: "run", limit: 500 });
|
|
557
|
-
for (const program of programs) {
|
|
558
|
-
if (program.metadata.auto_run_job_id !== normalizedJobId) {
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
autoRunHeartbeatProgramByJobId.set(normalizedJobId, program.program_id);
|
|
562
|
-
return program;
|
|
563
|
-
}
|
|
564
|
-
return null;
|
|
565
|
-
};
|
|
566
|
-
const registerAutoRunHeartbeatProgram = async (run) => {
|
|
567
|
-
if (run.source === "command") {
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
const jobId = run.job_id.trim();
|
|
571
|
-
if (!jobId || run.status !== "running") {
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
const rootIssueId = typeof run.root_issue_id === "string" ? run.root_issue_id.trim() : "";
|
|
575
|
-
const metadata = {
|
|
576
|
-
auto_run_heartbeat: true,
|
|
577
|
-
auto_run_job_id: jobId,
|
|
578
|
-
auto_run_root_issue_id: rootIssueId || null,
|
|
579
|
-
auto_disable_on_terminal: true,
|
|
580
|
-
run_mode: run.mode,
|
|
581
|
-
run_source: run.source,
|
|
582
|
-
};
|
|
583
|
-
const existing = await findAutoRunHeartbeatProgram(jobId);
|
|
584
|
-
if (existing) {
|
|
585
|
-
const result = await heartbeatPrograms.update({
|
|
586
|
-
programId: existing.program_id,
|
|
587
|
-
title: `Run heartbeat: ${rootIssueId || jobId}`,
|
|
588
|
-
target: {
|
|
589
|
-
kind: "run",
|
|
590
|
-
job_id: jobId,
|
|
591
|
-
root_issue_id: rootIssueId || null,
|
|
592
|
-
},
|
|
593
|
-
enabled: true,
|
|
594
|
-
everyMs: autoRunHeartbeatEveryMs,
|
|
595
|
-
reason: AUTO_RUN_HEARTBEAT_REASON,
|
|
596
|
-
wakeMode: "next_heartbeat",
|
|
597
|
-
metadata,
|
|
598
|
-
});
|
|
599
|
-
if (result.ok && result.program) {
|
|
600
|
-
autoRunHeartbeatProgramByJobId.set(jobId, result.program.program_id);
|
|
601
|
-
await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
602
|
-
source: "mu-server.runs",
|
|
603
|
-
payload: {
|
|
604
|
-
action: "updated",
|
|
605
|
-
run_job_id: jobId,
|
|
606
|
-
run_root_issue_id: rootIssueId || null,
|
|
607
|
-
program_id: result.program.program_id,
|
|
608
|
-
program: result.program,
|
|
609
|
-
},
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
const created = await heartbeatPrograms.create({
|
|
615
|
-
title: `Run heartbeat: ${rootIssueId || jobId}`,
|
|
616
|
-
target: {
|
|
617
|
-
kind: "run",
|
|
618
|
-
job_id: jobId,
|
|
619
|
-
root_issue_id: rootIssueId || null,
|
|
620
|
-
},
|
|
621
|
-
everyMs: autoRunHeartbeatEveryMs,
|
|
622
|
-
reason: AUTO_RUN_HEARTBEAT_REASON,
|
|
623
|
-
wakeMode: "next_heartbeat",
|
|
624
|
-
metadata,
|
|
625
|
-
enabled: true,
|
|
626
|
-
});
|
|
627
|
-
autoRunHeartbeatProgramByJobId.set(jobId, created.program_id);
|
|
628
|
-
await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
629
|
-
source: "mu-server.runs",
|
|
630
|
-
payload: {
|
|
631
|
-
action: "registered",
|
|
632
|
-
run_job_id: jobId,
|
|
633
|
-
run_root_issue_id: rootIssueId || null,
|
|
634
|
-
program_id: created.program_id,
|
|
635
|
-
program: created,
|
|
636
|
-
},
|
|
637
|
-
});
|
|
638
|
-
};
|
|
639
|
-
const disableAutoRunHeartbeatProgram = async (opts) => {
|
|
640
|
-
const program = await findAutoRunHeartbeatProgram(opts.jobId);
|
|
641
|
-
if (!program) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
const metadata = {
|
|
645
|
-
...program.metadata,
|
|
646
|
-
auto_disabled_from_status: opts.status,
|
|
647
|
-
auto_disabled_reason: opts.reason,
|
|
648
|
-
auto_disabled_at_ms: Date.now(),
|
|
649
|
-
};
|
|
650
|
-
const result = await heartbeatPrograms.update({
|
|
651
|
-
programId: program.program_id,
|
|
652
|
-
enabled: false,
|
|
653
|
-
everyMs: 0,
|
|
654
|
-
reason: AUTO_RUN_HEARTBEAT_REASON,
|
|
655
|
-
wakeMode: program.wake_mode,
|
|
656
|
-
metadata,
|
|
657
|
-
});
|
|
658
|
-
autoRunHeartbeatProgramByJobId.delete(opts.jobId.trim());
|
|
659
|
-
if (!result.ok || !result.program) {
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
663
|
-
source: "mu-server.runs",
|
|
664
|
-
payload: {
|
|
665
|
-
action: "disabled",
|
|
666
|
-
run_job_id: opts.jobId,
|
|
667
|
-
status: opts.status,
|
|
668
|
-
reason: opts.reason,
|
|
669
|
-
program_id: result.program.program_id,
|
|
670
|
-
program: result.program,
|
|
671
|
-
},
|
|
672
|
-
});
|
|
673
|
-
};
|
|
674
|
-
const loadConfigFromDisk = async () => {
|
|
675
|
-
try {
|
|
676
|
-
return await readConfig(context.repoRoot);
|
|
677
|
-
}
|
|
678
|
-
catch (err) {
|
|
679
|
-
if (err?.code === "ENOENT") {
|
|
680
|
-
return fallbackConfig;
|
|
681
|
-
}
|
|
682
|
-
throw err;
|
|
683
|
-
}
|
|
684
|
-
};
|
|
685
|
-
const performControlPlaneReload = async (reason) => {
|
|
686
|
-
const startedAtMs = Date.now();
|
|
687
|
-
const planned = generationSupervisor.beginReload(reason);
|
|
688
|
-
const attempt = planned.attempt;
|
|
689
|
-
const previous = controlPlaneCurrent;
|
|
690
|
-
const previousSummary = summarizeControlPlane(previous);
|
|
691
|
-
const tags = generationTagsFor(attempt.to_generation, "server.reload");
|
|
692
|
-
const baseFields = {
|
|
693
|
-
reason,
|
|
694
|
-
attempt_id: attempt.attempt_id,
|
|
695
|
-
coalesced: planned.coalesced,
|
|
696
|
-
from_generation_id: attempt.from_generation?.generation_id ?? null,
|
|
697
|
-
};
|
|
698
|
-
const logLifecycle = (opts) => {
|
|
699
|
-
generationTelemetry.log({
|
|
700
|
-
level: opts.level,
|
|
701
|
-
message: `reload transition ${opts.stage}:${opts.state}`,
|
|
702
|
-
fields: {
|
|
703
|
-
...tags,
|
|
704
|
-
...baseFields,
|
|
705
|
-
...(opts.extra ?? {}),
|
|
706
|
-
},
|
|
707
|
-
});
|
|
708
|
-
};
|
|
709
|
-
let swapped = false;
|
|
710
|
-
let failedStage = "warmup";
|
|
711
|
-
let drainDurationMs = 0;
|
|
712
|
-
let drainStartedAtMs = null;
|
|
713
|
-
let nextHandle = null;
|
|
714
|
-
try {
|
|
715
|
-
logLifecycle({ level: "info", stage: "warmup", state: "start" });
|
|
716
|
-
const latestConfig = await loadConfigFromDisk();
|
|
717
|
-
const telegramGeneration = (await previous?.reloadTelegramGeneration?.({
|
|
718
|
-
config: latestConfig.control_plane,
|
|
719
|
-
reason,
|
|
720
|
-
})) ?? null;
|
|
721
|
-
if (telegramGeneration?.handled) {
|
|
722
|
-
if (telegramGeneration.warmup) {
|
|
723
|
-
logLifecycle({
|
|
724
|
-
level: telegramGeneration.warmup.ok ? "info" : "error",
|
|
725
|
-
stage: "warmup",
|
|
726
|
-
state: telegramGeneration.warmup.ok ? "complete" : "failed",
|
|
727
|
-
extra: {
|
|
728
|
-
warmup_elapsed_ms: telegramGeneration.warmup.elapsed_ms,
|
|
729
|
-
error: telegramGeneration.warmup.error,
|
|
730
|
-
telegram_generation_id: telegramGeneration.to_generation?.generation_id ?? null,
|
|
731
|
-
},
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
logLifecycle({
|
|
736
|
-
level: "info",
|
|
737
|
-
stage: "warmup",
|
|
738
|
-
state: "skipped",
|
|
739
|
-
extra: {
|
|
740
|
-
warmup_reason: "telegram_generation_no_warmup",
|
|
741
|
-
telegram_generation_id: telegramGeneration.to_generation?.generation_id ?? null,
|
|
742
|
-
},
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
if (telegramGeneration.cutover) {
|
|
746
|
-
logLifecycle({ level: "info", stage: "cutover", state: "start" });
|
|
747
|
-
logLifecycle({
|
|
748
|
-
level: telegramGeneration.cutover.ok ? "info" : "error",
|
|
749
|
-
stage: "cutover",
|
|
750
|
-
state: telegramGeneration.cutover.ok ? "complete" : "failed",
|
|
751
|
-
extra: {
|
|
752
|
-
cutover_elapsed_ms: telegramGeneration.cutover.elapsed_ms,
|
|
753
|
-
error: telegramGeneration.cutover.error,
|
|
754
|
-
active_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
755
|
-
},
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
else {
|
|
759
|
-
logLifecycle({
|
|
760
|
-
level: "info",
|
|
761
|
-
stage: "cutover",
|
|
762
|
-
state: "skipped",
|
|
763
|
-
extra: {
|
|
764
|
-
cutover_reason: "telegram_generation_no_cutover",
|
|
765
|
-
active_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
766
|
-
},
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
if (telegramGeneration.drain) {
|
|
770
|
-
logLifecycle({ level: "info", stage: "drain", state: "start" });
|
|
771
|
-
drainDurationMs = Math.max(0, Math.trunc(telegramGeneration.drain.elapsed_ms));
|
|
772
|
-
generationTelemetry.recordDrainDuration(tags, {
|
|
773
|
-
durationMs: drainDurationMs,
|
|
774
|
-
timedOut: telegramGeneration.drain.timed_out,
|
|
775
|
-
metadata: {
|
|
776
|
-
...baseFields,
|
|
777
|
-
telegram_forced_stop: telegramGeneration.drain.forced_stop,
|
|
778
|
-
telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
logLifecycle({
|
|
782
|
-
level: telegramGeneration.drain.ok ? "info" : "warn",
|
|
783
|
-
stage: "drain",
|
|
784
|
-
state: telegramGeneration.drain.ok ? "complete" : "failed",
|
|
785
|
-
extra: {
|
|
786
|
-
drain_duration_ms: telegramGeneration.drain.elapsed_ms,
|
|
787
|
-
drain_timed_out: telegramGeneration.drain.timed_out,
|
|
788
|
-
forced_stop: telegramGeneration.drain.forced_stop,
|
|
789
|
-
error: telegramGeneration.drain.error,
|
|
790
|
-
},
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
else {
|
|
794
|
-
logLifecycle({
|
|
795
|
-
level: "info",
|
|
796
|
-
stage: "drain",
|
|
797
|
-
state: "skipped",
|
|
798
|
-
extra: {
|
|
799
|
-
drain_reason: "telegram_generation_no_drain",
|
|
800
|
-
telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
801
|
-
},
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
const shouldLogRollbackStart = telegramGeneration.rollback.requested ||
|
|
805
|
-
telegramGeneration.rollback.attempted ||
|
|
806
|
-
telegramGeneration.rollback.trigger != null ||
|
|
807
|
-
!telegramGeneration.ok;
|
|
808
|
-
if (shouldLogRollbackStart) {
|
|
809
|
-
logLifecycle({
|
|
810
|
-
level: telegramGeneration.rollback.ok ? "warn" : "error",
|
|
811
|
-
stage: "rollback",
|
|
812
|
-
state: "start",
|
|
813
|
-
extra: {
|
|
814
|
-
rollback_requested: telegramGeneration.rollback.requested,
|
|
815
|
-
rollback_trigger: telegramGeneration.rollback.trigger,
|
|
816
|
-
rollback_attempted: telegramGeneration.rollback.attempted,
|
|
817
|
-
},
|
|
818
|
-
});
|
|
819
|
-
logLifecycle({
|
|
820
|
-
level: telegramGeneration.rollback.ok ? "info" : "error",
|
|
821
|
-
stage: "rollback",
|
|
822
|
-
state: telegramGeneration.rollback.ok ? "complete" : "failed",
|
|
823
|
-
extra: {
|
|
824
|
-
rollback_requested: telegramGeneration.rollback.requested,
|
|
825
|
-
rollback_trigger: telegramGeneration.rollback.trigger,
|
|
826
|
-
rollback_attempted: telegramGeneration.rollback.attempted,
|
|
827
|
-
error: telegramGeneration.rollback.error,
|
|
828
|
-
},
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
else {
|
|
832
|
-
logLifecycle({
|
|
833
|
-
level: "debug",
|
|
834
|
-
stage: "rollback",
|
|
835
|
-
state: "skipped",
|
|
836
|
-
extra: {
|
|
837
|
-
rollback_reason: "not_requested",
|
|
838
|
-
},
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
if (telegramGeneration.ok) {
|
|
842
|
-
swapped = generationSupervisor.markSwapInstalled(attempt.attempt_id);
|
|
843
|
-
generationSupervisor.finishReload(attempt.attempt_id, "success");
|
|
844
|
-
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
845
|
-
generationTelemetry.recordReloadSuccess(tags, {
|
|
846
|
-
...baseFields,
|
|
847
|
-
elapsed_ms: elapsedMs,
|
|
848
|
-
drain_duration_ms: drainDurationMs,
|
|
849
|
-
telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
850
|
-
telegram_rollback_attempted: telegramGeneration.rollback.attempted,
|
|
851
|
-
telegram_rollback_trigger: telegramGeneration.rollback.trigger,
|
|
852
|
-
});
|
|
853
|
-
generationTelemetry.trace({
|
|
854
|
-
name: "control_plane.reload",
|
|
855
|
-
status: "ok",
|
|
856
|
-
durationMs: elapsedMs,
|
|
857
|
-
fields: {
|
|
858
|
-
...tags,
|
|
859
|
-
...baseFields,
|
|
860
|
-
telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
861
|
-
},
|
|
862
|
-
});
|
|
863
|
-
return {
|
|
864
|
-
ok: true,
|
|
865
|
-
reason,
|
|
866
|
-
previous_control_plane: previousSummary,
|
|
867
|
-
control_plane: summarizeControlPlane(controlPlaneCurrent),
|
|
868
|
-
generation: {
|
|
869
|
-
attempt_id: attempt.attempt_id,
|
|
870
|
-
coalesced: planned.coalesced,
|
|
871
|
-
from_generation: attempt.from_generation,
|
|
872
|
-
to_generation: attempt.to_generation,
|
|
873
|
-
active_generation: generationSupervisor.activeGeneration(),
|
|
874
|
-
outcome: "success",
|
|
875
|
-
},
|
|
876
|
-
telegram_generation: telegramGeneration,
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
generationSupervisor.finishReload(attempt.attempt_id, "failure");
|
|
880
|
-
const error = telegramGeneration.error ?? "telegram_generation_reload_failed";
|
|
881
|
-
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
882
|
-
generationTelemetry.recordReloadFailure(tags, {
|
|
883
|
-
...baseFields,
|
|
884
|
-
elapsed_ms: elapsedMs,
|
|
885
|
-
drain_duration_ms: drainDurationMs,
|
|
886
|
-
error,
|
|
887
|
-
telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
888
|
-
telegram_rollback_trigger: telegramGeneration.rollback.trigger,
|
|
889
|
-
});
|
|
890
|
-
generationTelemetry.trace({
|
|
891
|
-
name: "control_plane.reload",
|
|
892
|
-
status: "error",
|
|
893
|
-
durationMs: elapsedMs,
|
|
894
|
-
fields: {
|
|
895
|
-
...tags,
|
|
896
|
-
...baseFields,
|
|
897
|
-
error,
|
|
898
|
-
telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
|
|
899
|
-
telegram_rollback_trigger: telegramGeneration.rollback.trigger,
|
|
900
|
-
},
|
|
901
|
-
});
|
|
902
|
-
return {
|
|
903
|
-
ok: false,
|
|
904
|
-
reason,
|
|
905
|
-
previous_control_plane: previousSummary,
|
|
906
|
-
control_plane: summarizeControlPlane(controlPlaneCurrent),
|
|
907
|
-
generation: {
|
|
908
|
-
attempt_id: attempt.attempt_id,
|
|
909
|
-
coalesced: planned.coalesced,
|
|
910
|
-
from_generation: attempt.from_generation,
|
|
911
|
-
to_generation: attempt.to_generation,
|
|
912
|
-
active_generation: generationSupervisor.activeGeneration(),
|
|
913
|
-
outcome: "failure",
|
|
914
|
-
},
|
|
915
|
-
telegram_generation: telegramGeneration,
|
|
916
|
-
error,
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
const next = await controlPlaneReloader({
|
|
920
|
-
repoRoot: context.repoRoot,
|
|
921
|
-
previous,
|
|
922
|
-
config: latestConfig.control_plane,
|
|
923
|
-
generation: attempt.to_generation,
|
|
924
|
-
});
|
|
925
|
-
nextHandle = next;
|
|
926
|
-
logLifecycle({ level: "info", stage: "warmup", state: "complete" });
|
|
927
|
-
failedStage = "cutover";
|
|
928
|
-
logLifecycle({ level: "info", stage: "cutover", state: "start" });
|
|
929
|
-
controlPlaneCurrent = next;
|
|
930
|
-
swapped = generationSupervisor.markSwapInstalled(attempt.attempt_id);
|
|
931
|
-
logLifecycle({
|
|
932
|
-
level: "info",
|
|
933
|
-
stage: "cutover",
|
|
934
|
-
state: "complete",
|
|
935
|
-
extra: {
|
|
936
|
-
active_generation_id: generationSupervisor.activeGeneration()?.generation_id ?? null,
|
|
937
|
-
},
|
|
938
|
-
});
|
|
939
|
-
failedStage = "drain";
|
|
940
|
-
if (previous && previous !== next) {
|
|
941
|
-
logLifecycle({ level: "info", stage: "drain", state: "start" });
|
|
942
|
-
drainStartedAtMs = Date.now();
|
|
943
|
-
await previous.stop();
|
|
944
|
-
drainDurationMs = Math.max(0, Date.now() - drainStartedAtMs);
|
|
945
|
-
generationTelemetry.recordDrainDuration(tags, {
|
|
946
|
-
durationMs: drainDurationMs,
|
|
947
|
-
metadata: {
|
|
948
|
-
...baseFields,
|
|
949
|
-
},
|
|
950
|
-
});
|
|
951
|
-
logLifecycle({
|
|
952
|
-
level: "info",
|
|
953
|
-
stage: "drain",
|
|
954
|
-
state: "complete",
|
|
955
|
-
extra: {
|
|
956
|
-
drain_duration_ms: drainDurationMs,
|
|
957
|
-
},
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
logLifecycle({
|
|
962
|
-
level: "info",
|
|
963
|
-
stage: "drain",
|
|
964
|
-
state: "skipped",
|
|
965
|
-
extra: {
|
|
966
|
-
drain_reason: "no_previous_generation",
|
|
967
|
-
},
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
logLifecycle({
|
|
971
|
-
level: "debug",
|
|
972
|
-
stage: "rollback",
|
|
973
|
-
state: "skipped",
|
|
974
|
-
extra: {
|
|
975
|
-
rollback_reason: "not_requested",
|
|
976
|
-
},
|
|
977
|
-
});
|
|
978
|
-
generationSupervisor.finishReload(attempt.attempt_id, "success");
|
|
979
|
-
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
980
|
-
generationTelemetry.recordReloadSuccess(tags, {
|
|
981
|
-
...baseFields,
|
|
982
|
-
elapsed_ms: elapsedMs,
|
|
983
|
-
drain_duration_ms: drainDurationMs,
|
|
984
|
-
});
|
|
985
|
-
generationTelemetry.trace({
|
|
986
|
-
name: "control_plane.reload",
|
|
987
|
-
status: "ok",
|
|
988
|
-
durationMs: elapsedMs,
|
|
989
|
-
fields: {
|
|
990
|
-
...tags,
|
|
991
|
-
...baseFields,
|
|
992
|
-
},
|
|
993
|
-
});
|
|
994
|
-
return {
|
|
995
|
-
ok: true,
|
|
996
|
-
reason,
|
|
997
|
-
previous_control_plane: previousSummary,
|
|
998
|
-
control_plane: summarizeControlPlane(next),
|
|
999
|
-
generation: {
|
|
1000
|
-
attempt_id: attempt.attempt_id,
|
|
1001
|
-
coalesced: planned.coalesced,
|
|
1002
|
-
from_generation: attempt.from_generation,
|
|
1003
|
-
to_generation: attempt.to_generation,
|
|
1004
|
-
active_generation: generationSupervisor.activeGeneration(),
|
|
1005
|
-
outcome: "success",
|
|
1006
|
-
},
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
catch (err) {
|
|
1010
|
-
const error = describeError(err);
|
|
1011
|
-
if (failedStage === "drain" && drainStartedAtMs != null) {
|
|
1012
|
-
drainDurationMs = Math.max(0, Date.now() - drainStartedAtMs);
|
|
1013
|
-
generationTelemetry.recordDrainDuration(tags, {
|
|
1014
|
-
durationMs: drainDurationMs,
|
|
1015
|
-
metadata: {
|
|
1016
|
-
...baseFields,
|
|
1017
|
-
error,
|
|
1018
|
-
},
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
logLifecycle({
|
|
1022
|
-
level: "error",
|
|
1023
|
-
stage: failedStage,
|
|
1024
|
-
state: "failed",
|
|
1025
|
-
extra: {
|
|
1026
|
-
error,
|
|
1027
|
-
drain_duration_ms: failedStage === "drain" ? drainDurationMs : undefined,
|
|
1028
|
-
},
|
|
1029
|
-
});
|
|
1030
|
-
if (swapped) {
|
|
1031
|
-
logLifecycle({
|
|
1032
|
-
level: "warn",
|
|
1033
|
-
stage: "rollback",
|
|
1034
|
-
state: "start",
|
|
1035
|
-
extra: {
|
|
1036
|
-
rollback_reason: "reload_failed_after_cutover",
|
|
1037
|
-
rollback_target_generation_id: attempt.from_generation?.generation_id ?? null,
|
|
1038
|
-
rollback_source_generation_id: attempt.to_generation.generation_id,
|
|
1039
|
-
},
|
|
1040
|
-
});
|
|
1041
|
-
if (!previous) {
|
|
1042
|
-
logLifecycle({
|
|
1043
|
-
level: "error",
|
|
1044
|
-
stage: "rollback",
|
|
1045
|
-
state: "failed",
|
|
1046
|
-
extra: {
|
|
1047
|
-
rollback_reason: "no_previous_generation",
|
|
1048
|
-
rollback_source_generation_id: attempt.to_generation.generation_id,
|
|
1049
|
-
},
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
else {
|
|
1053
|
-
try {
|
|
1054
|
-
const restored = generationSupervisor.rollbackSwapInstalled(attempt.attempt_id);
|
|
1055
|
-
if (!restored) {
|
|
1056
|
-
throw new Error("generation_rollback_state_mismatch");
|
|
1057
|
-
}
|
|
1058
|
-
controlPlaneCurrent = previous;
|
|
1059
|
-
if (nextHandle && nextHandle !== previous) {
|
|
1060
|
-
await nextHandle.stop();
|
|
1061
|
-
}
|
|
1062
|
-
logLifecycle({
|
|
1063
|
-
level: "info",
|
|
1064
|
-
stage: "rollback",
|
|
1065
|
-
state: "complete",
|
|
1066
|
-
extra: {
|
|
1067
|
-
active_generation_id: generationSupervisor.activeGeneration()?.generation_id ?? null,
|
|
1068
|
-
rollback_target_generation_id: attempt.from_generation?.generation_id ?? null,
|
|
1069
|
-
},
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
catch (rollbackErr) {
|
|
1073
|
-
logLifecycle({
|
|
1074
|
-
level: "error",
|
|
1075
|
-
stage: "rollback",
|
|
1076
|
-
state: "failed",
|
|
1077
|
-
extra: {
|
|
1078
|
-
error: describeError(rollbackErr),
|
|
1079
|
-
active_generation_id: generationSupervisor.activeGeneration()?.generation_id ?? null,
|
|
1080
|
-
rollback_target_generation_id: attempt.from_generation?.generation_id ?? null,
|
|
1081
|
-
rollback_source_generation_id: attempt.to_generation.generation_id,
|
|
1082
|
-
},
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
else {
|
|
1088
|
-
logLifecycle({
|
|
1089
|
-
level: "debug",
|
|
1090
|
-
stage: "rollback",
|
|
1091
|
-
state: "skipped",
|
|
1092
|
-
extra: {
|
|
1093
|
-
rollback_reason: "cutover_not_installed",
|
|
1094
|
-
},
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
generationSupervisor.finishReload(attempt.attempt_id, "failure");
|
|
1098
|
-
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
1099
|
-
generationTelemetry.recordReloadFailure(tags, {
|
|
1100
|
-
...baseFields,
|
|
1101
|
-
elapsed_ms: elapsedMs,
|
|
1102
|
-
drain_duration_ms: drainDurationMs,
|
|
1103
|
-
error,
|
|
1104
|
-
});
|
|
1105
|
-
generationTelemetry.trace({
|
|
1106
|
-
name: "control_plane.reload",
|
|
1107
|
-
status: "error",
|
|
1108
|
-
durationMs: elapsedMs,
|
|
1109
|
-
fields: {
|
|
1110
|
-
...tags,
|
|
1111
|
-
...baseFields,
|
|
1112
|
-
error,
|
|
1113
|
-
},
|
|
1114
|
-
});
|
|
1115
|
-
return {
|
|
1116
|
-
ok: false,
|
|
1117
|
-
reason,
|
|
1118
|
-
previous_control_plane: previousSummary,
|
|
1119
|
-
control_plane: summarizeControlPlane(controlPlaneCurrent),
|
|
1120
|
-
generation: {
|
|
1121
|
-
attempt_id: attempt.attempt_id,
|
|
1122
|
-
coalesced: planned.coalesced,
|
|
1123
|
-
from_generation: attempt.from_generation,
|
|
1124
|
-
to_generation: attempt.to_generation,
|
|
1125
|
-
active_generation: generationSupervisor.activeGeneration(),
|
|
1126
|
-
outcome: "failure",
|
|
1127
|
-
},
|
|
1128
|
-
error,
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
};
|
|
1132
|
-
const reloadControlPlane = async (reason) => {
|
|
1133
|
-
if (reloadInFlight) {
|
|
1134
|
-
const pending = generationSupervisor.pendingReload();
|
|
1135
|
-
const fallbackGeneration = generationSupervisor.activeGeneration() ??
|
|
1136
|
-
generationSupervisor.snapshot().last_reload?.to_generation ??
|
|
1137
|
-
null;
|
|
1138
|
-
const generation = pending?.to_generation ?? fallbackGeneration;
|
|
1139
|
-
if (generation) {
|
|
1140
|
-
generationTelemetry.recordDuplicateSignal(generationTagsFor(generation, "server.reload"), {
|
|
1141
|
-
source: "server_reload",
|
|
1142
|
-
signal: "coalesced_reload_request",
|
|
1143
|
-
dedupe_key: pending?.attempt_id ?? "reload_in_flight",
|
|
1144
|
-
record_id: pending?.attempt_id ?? "reload_in_flight",
|
|
1145
|
-
metadata: {
|
|
1146
|
-
reason,
|
|
1147
|
-
pending_reason: pending?.reason ?? null,
|
|
1148
|
-
},
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
return await reloadInFlight;
|
|
1152
|
-
}
|
|
1153
|
-
reloadInFlight = performControlPlaneReload(reason).finally(() => {
|
|
1154
|
-
reloadInFlight = null;
|
|
1155
|
-
});
|
|
1156
|
-
return await reloadInFlight;
|
|
1157
|
-
};
|
|
1158
|
-
const handleRequest = async (request) => {
|
|
1159
|
-
const url = new URL(request.url);
|
|
1160
|
-
const path = url.pathname;
|
|
1161
|
-
const headers = new Headers({
|
|
1162
|
-
"Access-Control-Allow-Origin": "*",
|
|
1163
|
-
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
1164
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
1165
|
-
});
|
|
1166
|
-
if (request.method === "OPTIONS") {
|
|
1167
|
-
return new Response(null, { status: 204, headers });
|
|
1168
|
-
}
|
|
1169
|
-
if (path === "/healthz" || path === "/health") {
|
|
1170
|
-
return new Response("ok", { status: 200, headers });
|
|
1171
|
-
}
|
|
1172
|
-
if (path === "/api/config") {
|
|
1173
|
-
if (request.method === "GET") {
|
|
1174
|
-
try {
|
|
1175
|
-
const config = await loadConfigFromDisk();
|
|
1176
|
-
return Response.json({
|
|
1177
|
-
repo_root: context.repoRoot,
|
|
1178
|
-
config_path: getMuConfigPath(context.repoRoot),
|
|
1179
|
-
config: redactMuConfigSecrets(config),
|
|
1180
|
-
presence: muConfigPresence(config),
|
|
1181
|
-
}, { headers });
|
|
1182
|
-
}
|
|
1183
|
-
catch (err) {
|
|
1184
|
-
return Response.json({ error: `failed to read config: ${describeError(err)}` }, { status: 500, headers });
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
if (request.method === "POST") {
|
|
1188
|
-
let body;
|
|
1189
|
-
try {
|
|
1190
|
-
body = (await request.json());
|
|
1191
|
-
}
|
|
1192
|
-
catch {
|
|
1193
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1194
|
-
}
|
|
1195
|
-
if (!body || !("patch" in body)) {
|
|
1196
|
-
return Response.json({ error: "missing patch payload" }, { status: 400, headers });
|
|
1197
|
-
}
|
|
1198
|
-
try {
|
|
1199
|
-
const base = await loadConfigFromDisk();
|
|
1200
|
-
const next = applyMuConfigPatch(base, body.patch);
|
|
1201
|
-
const configPath = await writeConfig(context.repoRoot, next);
|
|
1202
|
-
return Response.json({
|
|
1203
|
-
ok: true,
|
|
1204
|
-
repo_root: context.repoRoot,
|
|
1205
|
-
config_path: configPath,
|
|
1206
|
-
config: redactMuConfigSecrets(next),
|
|
1207
|
-
presence: muConfigPresence(next),
|
|
1208
|
-
}, { headers });
|
|
1209
|
-
}
|
|
1210
|
-
catch (err) {
|
|
1211
|
-
return Response.json({ error: `failed to write config: ${describeError(err)}` }, { status: 500, headers });
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1215
|
-
}
|
|
1216
|
-
if (path === "/api/control-plane/reload") {
|
|
1217
|
-
if (request.method !== "POST") {
|
|
1218
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1219
|
-
}
|
|
1220
|
-
let reason = "api_control_plane_reload";
|
|
1221
|
-
try {
|
|
1222
|
-
const body = (await request.json());
|
|
1223
|
-
if (typeof body.reason === "string" && body.reason.trim().length > 0) {
|
|
1224
|
-
reason = body.reason.trim();
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
catch {
|
|
1228
|
-
// ignore invalid body for reason
|
|
1229
|
-
}
|
|
1230
|
-
const result = await reloadControlPlane(reason);
|
|
1231
|
-
return Response.json(result, { status: result.ok ? 200 : 500, headers });
|
|
1232
|
-
}
|
|
1233
|
-
if (path === "/api/control-plane/rollback") {
|
|
1234
|
-
if (request.method !== "POST") {
|
|
1235
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1236
|
-
}
|
|
1237
|
-
const result = await reloadControlPlane("rollback");
|
|
1238
|
-
return Response.json(result, { status: result.ok ? 200 : 500, headers });
|
|
1239
|
-
}
|
|
1240
|
-
if (path === "/api/status") {
|
|
1241
|
-
const issues = await context.issueStore.list();
|
|
1242
|
-
const openIssues = issues.filter((i) => i.status === "open");
|
|
1243
|
-
const readyIssues = await context.issueStore.ready();
|
|
1244
|
-
const controlPlane = {
|
|
1245
|
-
...summarizeControlPlane(controlPlaneCurrent),
|
|
1246
|
-
generation: generationSupervisor.snapshot(),
|
|
1247
|
-
observability: {
|
|
1248
|
-
counters: generationTelemetry.counters(),
|
|
1249
|
-
},
|
|
1250
|
-
};
|
|
1251
|
-
return Response.json({
|
|
1252
|
-
repo_root: context.repoRoot,
|
|
1253
|
-
open_count: openIssues.length,
|
|
1254
|
-
ready_count: readyIssues.length,
|
|
1255
|
-
control_plane: controlPlane,
|
|
1256
|
-
}, { headers });
|
|
1257
|
-
}
|
|
1258
|
-
if (path === "/api/commands/submit") {
|
|
1259
|
-
if (request.method !== "POST") {
|
|
1260
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1261
|
-
}
|
|
1262
|
-
let body;
|
|
1263
|
-
try {
|
|
1264
|
-
body = (await request.json());
|
|
1265
|
-
}
|
|
1266
|
-
catch {
|
|
1267
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1268
|
-
}
|
|
1269
|
-
const kind = typeof body.kind === "string" ? body.kind.trim() : "";
|
|
1270
|
-
if (!kind) {
|
|
1271
|
-
return Response.json({ error: "kind is required" }, { status: 400, headers });
|
|
1272
|
-
}
|
|
1273
|
-
let commandText;
|
|
1274
|
-
switch (kind) {
|
|
1275
|
-
case "run_start": {
|
|
1276
|
-
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1277
|
-
if (!prompt) {
|
|
1278
|
-
return Response.json({ error: "prompt is required for run_start" }, { status: 400, headers });
|
|
1279
|
-
}
|
|
1280
|
-
const maxStepsSuffix = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
|
|
1281
|
-
? ` --max-steps ${Math.max(1, Math.trunc(body.max_steps))}`
|
|
1282
|
-
: "";
|
|
1283
|
-
commandText = `mu! run start ${prompt}${maxStepsSuffix}`;
|
|
1284
|
-
break;
|
|
1285
|
-
}
|
|
1286
|
-
case "run_resume": {
|
|
1287
|
-
const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
|
|
1288
|
-
const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
|
|
1289
|
-
? ` ${Math.max(1, Math.trunc(body.max_steps))}`
|
|
1290
|
-
: "";
|
|
1291
|
-
commandText = `mu! run resume${rootId ? ` ${rootId}` : ""}${maxSteps}`;
|
|
1292
|
-
break;
|
|
1293
|
-
}
|
|
1294
|
-
case "run_interrupt": {
|
|
1295
|
-
const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
|
|
1296
|
-
commandText = `mu! run interrupt${rootId ? ` ${rootId}` : ""}`;
|
|
1297
|
-
break;
|
|
1298
|
-
}
|
|
1299
|
-
case "reload":
|
|
1300
|
-
commandText = "/mu reload";
|
|
1301
|
-
break;
|
|
1302
|
-
case "update":
|
|
1303
|
-
commandText = "/mu update";
|
|
1304
|
-
break;
|
|
1305
|
-
case "status":
|
|
1306
|
-
commandText = "/mu status";
|
|
1307
|
-
break;
|
|
1308
|
-
case "issue_list":
|
|
1309
|
-
commandText = "/mu issue list";
|
|
1310
|
-
break;
|
|
1311
|
-
case "issue_get": {
|
|
1312
|
-
const issueId = typeof body.issue_id === "string" ? body.issue_id.trim() : "";
|
|
1313
|
-
commandText = `/mu issue get${issueId ? ` ${issueId}` : ""}`;
|
|
1314
|
-
break;
|
|
1315
|
-
}
|
|
1316
|
-
case "forum_read": {
|
|
1317
|
-
const topic = typeof body.topic === "string" ? body.topic.trim() : "";
|
|
1318
|
-
const limit = typeof body.limit === "number" && Number.isFinite(body.limit)
|
|
1319
|
-
? ` ${Math.max(1, Math.trunc(body.limit))}`
|
|
1320
|
-
: "";
|
|
1321
|
-
commandText = `/mu forum read${topic ? ` ${topic}` : ""}${limit}`;
|
|
1322
|
-
break;
|
|
1323
|
-
}
|
|
1324
|
-
case "run_list":
|
|
1325
|
-
commandText = "/mu run list";
|
|
1326
|
-
break;
|
|
1327
|
-
case "run_status": {
|
|
1328
|
-
const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
|
|
1329
|
-
commandText = `/mu run status${rootId ? ` ${rootId}` : ""}`;
|
|
1330
|
-
break;
|
|
1331
|
-
}
|
|
1332
|
-
case "ready":
|
|
1333
|
-
commandText = "/mu ready";
|
|
1334
|
-
break;
|
|
1335
|
-
default:
|
|
1336
|
-
return Response.json({ error: `unknown command kind: ${kind}` }, { status: 400, headers });
|
|
1337
|
-
}
|
|
1338
|
-
try {
|
|
1339
|
-
if (!controlPlaneProxy.submitTerminalCommand) {
|
|
1340
|
-
return Response.json({ error: "control plane not available" }, { status: 503, headers });
|
|
1341
|
-
}
|
|
1342
|
-
const result = await controlPlaneProxy.submitTerminalCommand({
|
|
1343
|
-
commandText,
|
|
1344
|
-
repoRoot: context.repoRoot,
|
|
1345
|
-
});
|
|
1346
|
-
return Response.json({ ok: true, result }, { headers });
|
|
1347
|
-
}
|
|
1348
|
-
catch (err) {
|
|
1349
|
-
return Response.json({ error: `command failed: ${describeError(err)}` }, { status: 500, headers });
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
if (path === "/api/runs") {
|
|
1353
|
-
if (request.method !== "GET") {
|
|
1354
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1355
|
-
}
|
|
1356
|
-
const status = url.searchParams.get("status")?.trim() || undefined;
|
|
1357
|
-
const limitRaw = url.searchParams.get("limit");
|
|
1358
|
-
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
1359
|
-
const runs = await controlPlaneProxy.listRuns?.({ status, limit });
|
|
1360
|
-
return Response.json({ count: runs?.length ?? 0, runs: runs ?? [] }, { headers });
|
|
1361
|
-
}
|
|
1362
|
-
if (path === "/api/runs/start") {
|
|
1363
|
-
if (request.method !== "POST") {
|
|
1364
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1365
|
-
}
|
|
1366
|
-
let body;
|
|
1367
|
-
try {
|
|
1368
|
-
body = (await request.json());
|
|
1369
|
-
}
|
|
1370
|
-
catch {
|
|
1371
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1372
|
-
}
|
|
1373
|
-
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1374
|
-
if (prompt.length === 0) {
|
|
1375
|
-
return Response.json({ error: "prompt is required" }, { status: 400, headers });
|
|
1376
|
-
}
|
|
1377
|
-
const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
|
|
1378
|
-
? Math.max(1, Math.trunc(body.max_steps))
|
|
1379
|
-
: undefined;
|
|
1380
|
-
try {
|
|
1381
|
-
const run = await controlPlaneProxy.startRun?.({ prompt, maxSteps });
|
|
1382
|
-
if (!run) {
|
|
1383
|
-
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
1384
|
-
}
|
|
1385
|
-
await registerAutoRunHeartbeatProgram(run).catch(async (error) => {
|
|
1386
|
-
await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
1387
|
-
source: "mu-server.runs",
|
|
1388
|
-
payload: {
|
|
1389
|
-
action: "register_failed",
|
|
1390
|
-
run_job_id: run.job_id,
|
|
1391
|
-
error: describeError(error),
|
|
1392
|
-
},
|
|
1393
|
-
});
|
|
1394
|
-
});
|
|
1395
|
-
return Response.json({ ok: true, run }, { status: 201, headers });
|
|
1396
|
-
}
|
|
1397
|
-
catch (err) {
|
|
1398
|
-
return Response.json({ error: describeError(err) }, { status: 500, headers });
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
if (path === "/api/runs/resume") {
|
|
1402
|
-
if (request.method !== "POST") {
|
|
1403
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1404
|
-
}
|
|
1405
|
-
let body;
|
|
1406
|
-
try {
|
|
1407
|
-
body = (await request.json());
|
|
1408
|
-
}
|
|
1409
|
-
catch {
|
|
1410
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1411
|
-
}
|
|
1412
|
-
const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
|
|
1413
|
-
if (rootIssueId.length === 0) {
|
|
1414
|
-
return Response.json({ error: "root_issue_id is required" }, { status: 400, headers });
|
|
1415
|
-
}
|
|
1416
|
-
const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
|
|
1417
|
-
? Math.max(1, Math.trunc(body.max_steps))
|
|
1418
|
-
: undefined;
|
|
1419
|
-
try {
|
|
1420
|
-
const run = await controlPlaneProxy.resumeRun?.({ rootIssueId, maxSteps });
|
|
1421
|
-
if (!run) {
|
|
1422
|
-
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
1423
|
-
}
|
|
1424
|
-
await registerAutoRunHeartbeatProgram(run).catch(async (error) => {
|
|
1425
|
-
await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
1426
|
-
source: "mu-server.runs",
|
|
1427
|
-
payload: {
|
|
1428
|
-
action: "register_failed",
|
|
1429
|
-
run_job_id: run.job_id,
|
|
1430
|
-
error: describeError(error),
|
|
1431
|
-
},
|
|
1432
|
-
});
|
|
1433
|
-
});
|
|
1434
|
-
return Response.json({ ok: true, run }, { status: 201, headers });
|
|
1435
|
-
}
|
|
1436
|
-
catch (err) {
|
|
1437
|
-
return Response.json({ error: describeError(err) }, { status: 500, headers });
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
if (path === "/api/runs/interrupt") {
|
|
1441
|
-
if (request.method !== "POST") {
|
|
1442
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1443
|
-
}
|
|
1444
|
-
let body;
|
|
1445
|
-
try {
|
|
1446
|
-
body = (await request.json());
|
|
1447
|
-
}
|
|
1448
|
-
catch {
|
|
1449
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1450
|
-
}
|
|
1451
|
-
const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
|
|
1452
|
-
const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
|
|
1453
|
-
const result = await controlPlaneProxy.interruptRun?.({
|
|
1454
|
-
rootIssueId,
|
|
1455
|
-
jobId,
|
|
1456
|
-
});
|
|
1457
|
-
if (!result) {
|
|
1458
|
-
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
1459
|
-
}
|
|
1460
|
-
if (!result.ok && result.reason === "not_running" && result.run) {
|
|
1461
|
-
await disableAutoRunHeartbeatProgram({
|
|
1462
|
-
jobId: result.run.job_id,
|
|
1463
|
-
status: result.run.status,
|
|
1464
|
-
reason: "interrupt_not_running",
|
|
1465
|
-
}).catch(() => {
|
|
1466
|
-
// best effort cleanup only
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
return Response.json(result, { status: result.ok ? 200 : 404, headers });
|
|
1470
|
-
}
|
|
1471
|
-
if (path === "/api/runs/heartbeat") {
|
|
1472
|
-
if (request.method !== "POST") {
|
|
1473
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1474
|
-
}
|
|
1475
|
-
let body;
|
|
1476
|
-
try {
|
|
1477
|
-
body = (await request.json());
|
|
1478
|
-
}
|
|
1479
|
-
catch {
|
|
1480
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1481
|
-
}
|
|
1482
|
-
const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
|
|
1483
|
-
const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
|
|
1484
|
-
const reason = typeof body.reason === "string" ? body.reason.trim() : null;
|
|
1485
|
-
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
1486
|
-
const result = await controlPlaneProxy.heartbeatRun?.({
|
|
1487
|
-
rootIssueId,
|
|
1488
|
-
jobId,
|
|
1489
|
-
reason,
|
|
1490
|
-
wakeMode,
|
|
1491
|
-
});
|
|
1492
|
-
if (!result) {
|
|
1493
|
-
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
1494
|
-
}
|
|
1495
|
-
if (!result.ok && result.reason === "not_running" && result.run) {
|
|
1496
|
-
await disableAutoRunHeartbeatProgram({
|
|
1497
|
-
jobId: result.run.job_id,
|
|
1498
|
-
status: result.run.status,
|
|
1499
|
-
reason: "run_not_running",
|
|
1500
|
-
}).catch(() => {
|
|
1501
|
-
// best effort cleanup only
|
|
1502
|
-
});
|
|
1503
|
-
}
|
|
1504
|
-
if (result.ok) {
|
|
1505
|
-
return Response.json(result, { status: 200, headers });
|
|
1506
|
-
}
|
|
1507
|
-
if (result.reason === "missing_target") {
|
|
1508
|
-
return Response.json(result, { status: 400, headers });
|
|
1509
|
-
}
|
|
1510
|
-
if (result.reason === "not_running") {
|
|
1511
|
-
return Response.json(result, { status: 409, headers });
|
|
1512
|
-
}
|
|
1513
|
-
return Response.json(result, { status: 404, headers });
|
|
1514
|
-
}
|
|
1515
|
-
if (path.startsWith("/api/runs/")) {
|
|
1516
|
-
const rest = path.slice("/api/runs/".length);
|
|
1517
|
-
const [rawId, maybeSub] = rest.split("/");
|
|
1518
|
-
const idOrRoot = decodeURIComponent(rawId ?? "").trim();
|
|
1519
|
-
if (idOrRoot.length === 0) {
|
|
1520
|
-
return Response.json({ error: "missing run id" }, { status: 400, headers });
|
|
1521
|
-
}
|
|
1522
|
-
if (maybeSub === "trace") {
|
|
1523
|
-
if (request.method !== "GET") {
|
|
1524
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1525
|
-
}
|
|
1526
|
-
const limitRaw = url.searchParams.get("limit");
|
|
1527
|
-
const limit = limitRaw && /^\d+$/.test(limitRaw)
|
|
1528
|
-
? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
|
|
1529
|
-
: undefined;
|
|
1530
|
-
const trace = await controlPlaneProxy.traceRun?.({ idOrRoot, limit });
|
|
1531
|
-
if (!trace) {
|
|
1532
|
-
return Response.json({ error: "run trace not found" }, { status: 404, headers });
|
|
1533
|
-
}
|
|
1534
|
-
return Response.json(trace, { headers });
|
|
1535
|
-
}
|
|
1536
|
-
if (request.method !== "GET") {
|
|
1537
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1538
|
-
}
|
|
1539
|
-
const run = await controlPlaneProxy.getRun?.(idOrRoot);
|
|
1540
|
-
if (!run) {
|
|
1541
|
-
return Response.json({ error: "run not found" }, { status: 404, headers });
|
|
1542
|
-
}
|
|
1543
|
-
if (run.status !== "running") {
|
|
1544
|
-
await disableAutoRunHeartbeatProgram({
|
|
1545
|
-
jobId: run.job_id,
|
|
1546
|
-
status: run.status,
|
|
1547
|
-
reason: "run_terminal_snapshot",
|
|
1548
|
-
}).catch(() => {
|
|
1549
|
-
// best effort cleanup only
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
return Response.json(run, { headers });
|
|
1553
|
-
}
|
|
1554
|
-
if (path === "/api/cron/status") {
|
|
1555
|
-
if (request.method !== "GET") {
|
|
1556
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1557
|
-
}
|
|
1558
|
-
const status = await cronPrograms.status();
|
|
1559
|
-
return Response.json(status, { headers });
|
|
1560
|
-
}
|
|
1561
|
-
if (path === "/api/cron") {
|
|
1562
|
-
if (request.method !== "GET") {
|
|
1563
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1564
|
-
}
|
|
1565
|
-
const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
|
|
1566
|
-
const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
|
|
1567
|
-
const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
|
|
1568
|
-
const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
|
|
1569
|
-
const scheduleKindRaw = url.searchParams.get("schedule_kind")?.trim().toLowerCase();
|
|
1570
|
-
const scheduleKind = scheduleKindRaw === "at" || scheduleKindRaw === "every" || scheduleKindRaw === "cron"
|
|
1571
|
-
? scheduleKindRaw
|
|
1572
|
-
: undefined;
|
|
1573
|
-
const limitRaw = url.searchParams.get("limit");
|
|
1574
|
-
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
1575
|
-
const programs = await cronPrograms.list({ enabled, targetKind, scheduleKind, limit });
|
|
1576
|
-
return Response.json({ count: programs.length, programs }, { headers });
|
|
1577
|
-
}
|
|
1578
|
-
if (path === "/api/cron/create") {
|
|
1579
|
-
if (request.method !== "POST") {
|
|
1580
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1581
|
-
}
|
|
1582
|
-
let body;
|
|
1583
|
-
try {
|
|
1584
|
-
body = (await request.json());
|
|
1585
|
-
}
|
|
1586
|
-
catch {
|
|
1587
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1588
|
-
}
|
|
1589
|
-
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
1590
|
-
if (!title) {
|
|
1591
|
-
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
1592
|
-
}
|
|
1593
|
-
const parsedTarget = parseCronTarget(body);
|
|
1594
|
-
if (!parsedTarget.target) {
|
|
1595
|
-
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
1596
|
-
}
|
|
1597
|
-
if (!hasCronScheduleInput(body)) {
|
|
1598
|
-
return Response.json({ error: "schedule is required" }, { status: 400, headers });
|
|
1599
|
-
}
|
|
1600
|
-
const schedule = cronScheduleInputFromBody(body);
|
|
1601
|
-
const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
|
|
1602
|
-
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
1603
|
-
const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
|
|
1604
|
-
try {
|
|
1605
|
-
const program = await cronPrograms.create({
|
|
1606
|
-
title,
|
|
1607
|
-
target: parsedTarget.target,
|
|
1608
|
-
schedule,
|
|
1609
|
-
reason,
|
|
1610
|
-
wakeMode,
|
|
1611
|
-
enabled,
|
|
1612
|
-
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
1613
|
-
? body.metadata
|
|
1614
|
-
: undefined,
|
|
1615
|
-
});
|
|
1616
|
-
return Response.json({ ok: true, program }, { status: 201, headers });
|
|
1617
|
-
}
|
|
1618
|
-
catch (err) {
|
|
1619
|
-
return Response.json({ error: describeError(err) }, { status: 400, headers });
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
if (path === "/api/cron/update") {
|
|
1623
|
-
if (request.method !== "POST") {
|
|
1624
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1625
|
-
}
|
|
1626
|
-
let body;
|
|
1627
|
-
try {
|
|
1628
|
-
body = (await request.json());
|
|
1629
|
-
}
|
|
1630
|
-
catch {
|
|
1631
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1632
|
-
}
|
|
1633
|
-
const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
|
|
1634
|
-
if (!programId) {
|
|
1635
|
-
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
1636
|
-
}
|
|
1637
|
-
let target;
|
|
1638
|
-
if (typeof body.target_kind === "string") {
|
|
1639
|
-
const parsedTarget = parseCronTarget(body);
|
|
1640
|
-
if (!parsedTarget.target) {
|
|
1641
|
-
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
1642
|
-
}
|
|
1643
|
-
target = parsedTarget.target;
|
|
1644
|
-
}
|
|
1645
|
-
const schedule = hasCronScheduleInput(body) ? cronScheduleInputFromBody(body) : undefined;
|
|
1646
|
-
const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
|
|
1647
|
-
try {
|
|
1648
|
-
const result = await cronPrograms.update({
|
|
1649
|
-
programId,
|
|
1650
|
-
title: typeof body.title === "string" ? body.title : undefined,
|
|
1651
|
-
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
1652
|
-
wakeMode,
|
|
1653
|
-
enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
|
|
1654
|
-
target,
|
|
1655
|
-
schedule,
|
|
1656
|
-
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
1657
|
-
? body.metadata
|
|
1658
|
-
: undefined,
|
|
1659
|
-
});
|
|
1660
|
-
if (result.ok) {
|
|
1661
|
-
return Response.json(result, { headers });
|
|
1662
|
-
}
|
|
1663
|
-
if (result.reason === "not_found") {
|
|
1664
|
-
return Response.json(result, { status: 404, headers });
|
|
1665
|
-
}
|
|
1666
|
-
return Response.json(result, { status: 400, headers });
|
|
1667
|
-
}
|
|
1668
|
-
catch (err) {
|
|
1669
|
-
return Response.json({ error: describeError(err) }, { status: 400, headers });
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
if (path === "/api/cron/delete") {
|
|
1673
|
-
if (request.method !== "POST") {
|
|
1674
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1675
|
-
}
|
|
1676
|
-
let body;
|
|
1677
|
-
try {
|
|
1678
|
-
body = (await request.json());
|
|
1679
|
-
}
|
|
1680
|
-
catch {
|
|
1681
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1682
|
-
}
|
|
1683
|
-
const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
|
|
1684
|
-
if (!programId) {
|
|
1685
|
-
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
1686
|
-
}
|
|
1687
|
-
const result = await cronPrograms.remove(programId);
|
|
1688
|
-
return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
|
|
1689
|
-
}
|
|
1690
|
-
if (path === "/api/cron/trigger") {
|
|
1691
|
-
if (request.method !== "POST") {
|
|
1692
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1693
|
-
}
|
|
1694
|
-
let body;
|
|
1695
|
-
try {
|
|
1696
|
-
body = (await request.json());
|
|
1697
|
-
}
|
|
1698
|
-
catch {
|
|
1699
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1700
|
-
}
|
|
1701
|
-
const result = await cronPrograms.trigger({
|
|
1702
|
-
programId: typeof body.program_id === "string" ? body.program_id : null,
|
|
1703
|
-
reason: typeof body.reason === "string" ? body.reason : null,
|
|
1704
|
-
});
|
|
1705
|
-
if (result.ok) {
|
|
1706
|
-
return Response.json(result, { headers });
|
|
1707
|
-
}
|
|
1708
|
-
if (result.reason === "missing_target") {
|
|
1709
|
-
return Response.json(result, { status: 400, headers });
|
|
1710
|
-
}
|
|
1711
|
-
if (result.reason === "not_found") {
|
|
1712
|
-
return Response.json(result, { status: 404, headers });
|
|
1713
|
-
}
|
|
1714
|
-
return Response.json(result, { status: 409, headers });
|
|
1715
|
-
}
|
|
1716
|
-
if (path.startsWith("/api/cron/")) {
|
|
1717
|
-
if (request.method !== "GET") {
|
|
1718
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1719
|
-
}
|
|
1720
|
-
const id = decodeURIComponent(path.slice("/api/cron/".length)).trim();
|
|
1721
|
-
if (!id) {
|
|
1722
|
-
return Response.json({ error: "missing program id" }, { status: 400, headers });
|
|
1723
|
-
}
|
|
1724
|
-
const program = await cronPrograms.get(id);
|
|
1725
|
-
if (!program) {
|
|
1726
|
-
return Response.json({ error: "program not found" }, { status: 404, headers });
|
|
1727
|
-
}
|
|
1728
|
-
return Response.json(program, { headers });
|
|
1729
|
-
}
|
|
1730
|
-
if (path === "/api/heartbeats") {
|
|
1731
|
-
if (request.method !== "GET") {
|
|
1732
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1733
|
-
}
|
|
1734
|
-
const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
|
|
1735
|
-
const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
|
|
1736
|
-
const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
|
|
1737
|
-
const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
|
|
1738
|
-
const limitRaw = url.searchParams.get("limit");
|
|
1739
|
-
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
1740
|
-
const programs = await heartbeatPrograms.list({ enabled, targetKind, limit });
|
|
1741
|
-
return Response.json({ count: programs.length, programs }, { headers });
|
|
1742
|
-
}
|
|
1743
|
-
if (path === "/api/heartbeats/create") {
|
|
1744
|
-
if (request.method !== "POST") {
|
|
1745
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1746
|
-
}
|
|
1747
|
-
let body;
|
|
1748
|
-
try {
|
|
1749
|
-
body = (await request.json());
|
|
1750
|
-
}
|
|
1751
|
-
catch {
|
|
1752
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1753
|
-
}
|
|
1754
|
-
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
1755
|
-
if (!title) {
|
|
1756
|
-
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
1757
|
-
}
|
|
1758
|
-
const targetKind = typeof body.target_kind === "string" ? body.target_kind.trim().toLowerCase() : "";
|
|
1759
|
-
let target = null;
|
|
1760
|
-
if (targetKind === "run") {
|
|
1761
|
-
const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
|
|
1762
|
-
const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
|
|
1763
|
-
if (!jobId && !rootIssueId) {
|
|
1764
|
-
return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
|
|
1765
|
-
}
|
|
1766
|
-
target = {
|
|
1767
|
-
kind: "run",
|
|
1768
|
-
job_id: jobId || null,
|
|
1769
|
-
root_issue_id: rootIssueId || null,
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
else if (targetKind === "activity") {
|
|
1773
|
-
const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
|
|
1774
|
-
if (!activityId) {
|
|
1775
|
-
return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
|
|
1776
|
-
}
|
|
1777
|
-
target = {
|
|
1778
|
-
kind: "activity",
|
|
1779
|
-
activity_id: activityId,
|
|
1780
|
-
};
|
|
1781
|
-
}
|
|
1782
|
-
else {
|
|
1783
|
-
return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
|
|
1784
|
-
}
|
|
1785
|
-
const everyMs = typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
|
|
1786
|
-
? Math.max(0, Math.trunc(body.every_ms))
|
|
1787
|
-
: undefined;
|
|
1788
|
-
const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
|
|
1789
|
-
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
1790
|
-
const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
|
|
1791
|
-
try {
|
|
1792
|
-
const program = await heartbeatPrograms.create({
|
|
1793
|
-
title,
|
|
1794
|
-
target,
|
|
1795
|
-
everyMs,
|
|
1796
|
-
reason,
|
|
1797
|
-
wakeMode,
|
|
1798
|
-
enabled,
|
|
1799
|
-
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
1800
|
-
? body.metadata
|
|
1801
|
-
: undefined,
|
|
1802
|
-
});
|
|
1803
|
-
return Response.json({ ok: true, program }, { status: 201, headers });
|
|
1804
|
-
}
|
|
1805
|
-
catch (err) {
|
|
1806
|
-
return Response.json({ error: describeError(err) }, { status: 400, headers });
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
if (path === "/api/heartbeats/update") {
|
|
1810
|
-
if (request.method !== "POST") {
|
|
1811
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1812
|
-
}
|
|
1813
|
-
let body;
|
|
1814
|
-
try {
|
|
1815
|
-
body = (await request.json());
|
|
1816
|
-
}
|
|
1817
|
-
catch {
|
|
1818
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1819
|
-
}
|
|
1820
|
-
const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
|
|
1821
|
-
if (!programId) {
|
|
1822
|
-
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
1823
|
-
}
|
|
1824
|
-
let target;
|
|
1825
|
-
if (typeof body.target_kind === "string") {
|
|
1826
|
-
const targetKind = body.target_kind.trim().toLowerCase();
|
|
1827
|
-
if (targetKind === "run") {
|
|
1828
|
-
const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
|
|
1829
|
-
const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
|
|
1830
|
-
if (!jobId && !rootIssueId) {
|
|
1831
|
-
return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
|
|
1832
|
-
}
|
|
1833
|
-
target = {
|
|
1834
|
-
kind: "run",
|
|
1835
|
-
job_id: jobId || null,
|
|
1836
|
-
root_issue_id: rootIssueId || null,
|
|
1837
|
-
};
|
|
1838
|
-
}
|
|
1839
|
-
else if (targetKind === "activity") {
|
|
1840
|
-
const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
|
|
1841
|
-
if (!activityId) {
|
|
1842
|
-
return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
|
|
1843
|
-
}
|
|
1844
|
-
target = {
|
|
1845
|
-
kind: "activity",
|
|
1846
|
-
activity_id: activityId,
|
|
1847
|
-
};
|
|
1848
|
-
}
|
|
1849
|
-
else {
|
|
1850
|
-
return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
|
|
1854
|
-
try {
|
|
1855
|
-
const result = await heartbeatPrograms.update({
|
|
1856
|
-
programId,
|
|
1857
|
-
title: typeof body.title === "string" ? body.title : undefined,
|
|
1858
|
-
target,
|
|
1859
|
-
everyMs: typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
|
|
1860
|
-
? Math.max(0, Math.trunc(body.every_ms))
|
|
1861
|
-
: undefined,
|
|
1862
|
-
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
1863
|
-
wakeMode,
|
|
1864
|
-
enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
|
|
1865
|
-
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
1866
|
-
? body.metadata
|
|
1867
|
-
: undefined,
|
|
1868
|
-
});
|
|
1869
|
-
if (result.ok) {
|
|
1870
|
-
return Response.json(result, { headers });
|
|
1871
|
-
}
|
|
1872
|
-
return Response.json(result, { status: result.reason === "not_found" ? 404 : 400, headers });
|
|
1873
|
-
}
|
|
1874
|
-
catch (err) {
|
|
1875
|
-
return Response.json({ error: describeError(err) }, { status: 400, headers });
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
if (path === "/api/heartbeats/delete") {
|
|
1879
|
-
if (request.method !== "POST") {
|
|
1880
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1881
|
-
}
|
|
1882
|
-
let body;
|
|
1883
|
-
try {
|
|
1884
|
-
body = (await request.json());
|
|
1885
|
-
}
|
|
1886
|
-
catch {
|
|
1887
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1888
|
-
}
|
|
1889
|
-
const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
|
|
1890
|
-
if (!programId) {
|
|
1891
|
-
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
1892
|
-
}
|
|
1893
|
-
const result = await heartbeatPrograms.remove(programId);
|
|
1894
|
-
return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
|
|
1895
|
-
}
|
|
1896
|
-
if (path === "/api/heartbeats/trigger") {
|
|
1897
|
-
if (request.method !== "POST") {
|
|
1898
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1899
|
-
}
|
|
1900
|
-
let body;
|
|
1901
|
-
try {
|
|
1902
|
-
body = (await request.json());
|
|
1903
|
-
}
|
|
1904
|
-
catch {
|
|
1905
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1906
|
-
}
|
|
1907
|
-
const result = await heartbeatPrograms.trigger({
|
|
1908
|
-
programId: typeof body.program_id === "string" ? body.program_id : null,
|
|
1909
|
-
reason: typeof body.reason === "string" ? body.reason : null,
|
|
1910
|
-
});
|
|
1911
|
-
if (result.ok) {
|
|
1912
|
-
return Response.json(result, { headers });
|
|
1913
|
-
}
|
|
1914
|
-
if (result.reason === "missing_target") {
|
|
1915
|
-
return Response.json(result, { status: 400, headers });
|
|
1916
|
-
}
|
|
1917
|
-
if (result.reason === "not_found") {
|
|
1918
|
-
return Response.json(result, { status: 404, headers });
|
|
1919
|
-
}
|
|
1920
|
-
return Response.json(result, { status: 409, headers });
|
|
1921
|
-
}
|
|
1922
|
-
if (path.startsWith("/api/heartbeats/")) {
|
|
1923
|
-
if (request.method !== "GET") {
|
|
1924
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1925
|
-
}
|
|
1926
|
-
const id = decodeURIComponent(path.slice("/api/heartbeats/".length)).trim();
|
|
1927
|
-
if (!id) {
|
|
1928
|
-
return Response.json({ error: "missing program id" }, { status: 400, headers });
|
|
1929
|
-
}
|
|
1930
|
-
const program = await heartbeatPrograms.get(id);
|
|
1931
|
-
if (!program) {
|
|
1932
|
-
return Response.json({ error: "program not found" }, { status: 404, headers });
|
|
1933
|
-
}
|
|
1934
|
-
return Response.json(program, { headers });
|
|
1935
|
-
}
|
|
1936
|
-
if (path === "/api/activities") {
|
|
1937
|
-
if (request.method !== "GET") {
|
|
1938
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1939
|
-
}
|
|
1940
|
-
const statusRaw = url.searchParams.get("status")?.trim().toLowerCase();
|
|
1941
|
-
const status = statusRaw === "running" || statusRaw === "completed" || statusRaw === "failed" || statusRaw === "cancelled"
|
|
1942
|
-
? statusRaw
|
|
1943
|
-
: undefined;
|
|
1944
|
-
const kind = url.searchParams.get("kind")?.trim() || undefined;
|
|
1945
|
-
const limitRaw = url.searchParams.get("limit");
|
|
1946
|
-
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
1947
|
-
const activities = activitySupervisor.list({ status, kind, limit });
|
|
1948
|
-
return Response.json({ count: activities.length, activities }, { headers });
|
|
1949
|
-
}
|
|
1950
|
-
if (path === "/api/activities/start") {
|
|
1951
|
-
if (request.method !== "POST") {
|
|
1952
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1953
|
-
}
|
|
1954
|
-
let body;
|
|
1955
|
-
try {
|
|
1956
|
-
body = (await request.json());
|
|
1957
|
-
}
|
|
1958
|
-
catch {
|
|
1959
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1960
|
-
}
|
|
1961
|
-
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
1962
|
-
if (!title) {
|
|
1963
|
-
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
1964
|
-
}
|
|
1965
|
-
const kind = typeof body.kind === "string" ? body.kind.trim() : undefined;
|
|
1966
|
-
const heartbeatEveryMs = typeof body.heartbeat_every_ms === "number" && Number.isFinite(body.heartbeat_every_ms)
|
|
1967
|
-
? Math.max(0, Math.trunc(body.heartbeat_every_ms))
|
|
1968
|
-
: undefined;
|
|
1969
|
-
const source = body.source === "api" || body.source === "command" || body.source === "system" ? body.source : "api";
|
|
1970
|
-
try {
|
|
1971
|
-
const activity = activitySupervisor.start({
|
|
1972
|
-
title,
|
|
1973
|
-
kind,
|
|
1974
|
-
heartbeatEveryMs,
|
|
1975
|
-
metadata: body.metadata ?? undefined,
|
|
1976
|
-
source,
|
|
1977
|
-
});
|
|
1978
|
-
return Response.json({ ok: true, activity }, { status: 201, headers });
|
|
1979
|
-
}
|
|
1980
|
-
catch (err) {
|
|
1981
|
-
return Response.json({ error: describeError(err) }, { status: 400, headers });
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
if (path === "/api/activities/progress") {
|
|
1985
|
-
if (request.method !== "POST") {
|
|
1986
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
1987
|
-
}
|
|
1988
|
-
let body;
|
|
1989
|
-
try {
|
|
1990
|
-
body = (await request.json());
|
|
1991
|
-
}
|
|
1992
|
-
catch {
|
|
1993
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
1994
|
-
}
|
|
1995
|
-
const result = activitySupervisor.progress({
|
|
1996
|
-
activityId: typeof body.activity_id === "string" ? body.activity_id : null,
|
|
1997
|
-
message: typeof body.message === "string" ? body.message : null,
|
|
1998
|
-
});
|
|
1999
|
-
if (result.ok) {
|
|
2000
|
-
return Response.json(result, { headers });
|
|
2001
|
-
}
|
|
2002
|
-
if (result.reason === "missing_target") {
|
|
2003
|
-
return Response.json(result, { status: 400, headers });
|
|
2004
|
-
}
|
|
2005
|
-
if (result.reason === "not_running") {
|
|
2006
|
-
return Response.json(result, { status: 409, headers });
|
|
2007
|
-
}
|
|
2008
|
-
return Response.json(result, { status: 404, headers });
|
|
2009
|
-
}
|
|
2010
|
-
if (path === "/api/activities/heartbeat") {
|
|
2011
|
-
if (request.method !== "POST") {
|
|
2012
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
2013
|
-
}
|
|
2014
|
-
let body;
|
|
2015
|
-
try {
|
|
2016
|
-
body = (await request.json());
|
|
2017
|
-
}
|
|
2018
|
-
catch {
|
|
2019
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
2020
|
-
}
|
|
2021
|
-
const result = activitySupervisor.heartbeat({
|
|
2022
|
-
activityId: typeof body.activity_id === "string" ? body.activity_id : null,
|
|
2023
|
-
reason: typeof body.reason === "string" ? body.reason : null,
|
|
2024
|
-
});
|
|
2025
|
-
if (result.ok) {
|
|
2026
|
-
return Response.json(result, { headers });
|
|
2027
|
-
}
|
|
2028
|
-
if (result.reason === "missing_target") {
|
|
2029
|
-
return Response.json(result, { status: 400, headers });
|
|
2030
|
-
}
|
|
2031
|
-
if (result.reason === "not_running") {
|
|
2032
|
-
return Response.json(result, { status: 409, headers });
|
|
2033
|
-
}
|
|
2034
|
-
return Response.json(result, { status: 404, headers });
|
|
2035
|
-
}
|
|
2036
|
-
if (path === "/api/activities/complete" || path === "/api/activities/fail" || path === "/api/activities/cancel") {
|
|
2037
|
-
if (request.method !== "POST") {
|
|
2038
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
2039
|
-
}
|
|
2040
|
-
let body;
|
|
2041
|
-
try {
|
|
2042
|
-
body = (await request.json());
|
|
2043
|
-
}
|
|
2044
|
-
catch {
|
|
2045
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
2046
|
-
}
|
|
2047
|
-
const activityId = typeof body.activity_id === "string" ? body.activity_id : null;
|
|
2048
|
-
const message = typeof body.message === "string" ? body.message : null;
|
|
2049
|
-
const result = path === "/api/activities/complete"
|
|
2050
|
-
? activitySupervisor.complete({ activityId, message })
|
|
2051
|
-
: path === "/api/activities/fail"
|
|
2052
|
-
? activitySupervisor.fail({ activityId, message })
|
|
2053
|
-
: activitySupervisor.cancel({ activityId, message });
|
|
2054
|
-
if (result.ok) {
|
|
2055
|
-
return Response.json(result, { headers });
|
|
2056
|
-
}
|
|
2057
|
-
if (result.reason === "missing_target") {
|
|
2058
|
-
return Response.json(result, { status: 400, headers });
|
|
2059
|
-
}
|
|
2060
|
-
if (result.reason === "not_running") {
|
|
2061
|
-
return Response.json(result, { status: 409, headers });
|
|
2062
|
-
}
|
|
2063
|
-
return Response.json(result, { status: 404, headers });
|
|
2064
|
-
}
|
|
2065
|
-
if (path.startsWith("/api/activities/")) {
|
|
2066
|
-
if (request.method !== "GET") {
|
|
2067
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
2068
|
-
}
|
|
2069
|
-
const rest = path.slice("/api/activities/".length);
|
|
2070
|
-
const [rawId, maybeSub] = rest.split("/");
|
|
2071
|
-
const activityId = decodeURIComponent(rawId ?? "").trim();
|
|
2072
|
-
if (activityId.length === 0) {
|
|
2073
|
-
return Response.json({ error: "missing activity id" }, { status: 400, headers });
|
|
2074
|
-
}
|
|
2075
|
-
if (maybeSub === "events") {
|
|
2076
|
-
const limitRaw = url.searchParams.get("limit");
|
|
2077
|
-
const limit = limitRaw && /^\d+$/.test(limitRaw)
|
|
2078
|
-
? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
|
|
2079
|
-
: undefined;
|
|
2080
|
-
const events = activitySupervisor.events(activityId, { limit });
|
|
2081
|
-
if (!events) {
|
|
2082
|
-
return Response.json({ error: "activity not found" }, { status: 404, headers });
|
|
2083
|
-
}
|
|
2084
|
-
return Response.json({ count: events.length, events }, { headers });
|
|
2085
|
-
}
|
|
2086
|
-
const activity = activitySupervisor.get(activityId);
|
|
2087
|
-
if (!activity) {
|
|
2088
|
-
return Response.json({ error: "activity not found" }, { status: 404, headers });
|
|
2089
|
-
}
|
|
2090
|
-
return Response.json(activity, { headers });
|
|
2091
|
-
}
|
|
2092
|
-
if (path === "/api/identities" || path === "/api/identities/link" || path === "/api/identities/unlink") {
|
|
2093
|
-
const cpPaths = getControlPlanePaths(context.repoRoot);
|
|
2094
|
-
const identityStore = new IdentityStore(cpPaths.identitiesPath);
|
|
2095
|
-
await identityStore.load();
|
|
2096
|
-
if (path === "/api/identities") {
|
|
2097
|
-
if (request.method !== "GET") {
|
|
2098
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
2099
|
-
}
|
|
2100
|
-
const includeInactive = url.searchParams.get("include_inactive")?.trim().toLowerCase() === "true";
|
|
2101
|
-
const bindings = identityStore.listBindings({ includeInactive });
|
|
2102
|
-
return Response.json({ count: bindings.length, bindings }, { headers });
|
|
2103
|
-
}
|
|
2104
|
-
if (path === "/api/identities/link") {
|
|
2105
|
-
if (request.method !== "POST") {
|
|
2106
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
2107
|
-
}
|
|
2108
|
-
let body;
|
|
2109
|
-
try {
|
|
2110
|
-
body = (await request.json());
|
|
2111
|
-
}
|
|
2112
|
-
catch {
|
|
2113
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
2114
|
-
}
|
|
2115
|
-
const channel = typeof body.channel === "string" ? body.channel.trim() : "";
|
|
2116
|
-
if (!channel || (channel !== "slack" && channel !== "discord" && channel !== "telegram")) {
|
|
2117
|
-
return Response.json({ error: "channel is required (slack, discord, telegram)" }, { status: 400, headers });
|
|
2118
|
-
}
|
|
2119
|
-
const actorId = typeof body.actor_id === "string" ? body.actor_id.trim() : "";
|
|
2120
|
-
if (!actorId) {
|
|
2121
|
-
return Response.json({ error: "actor_id is required" }, { status: 400, headers });
|
|
2122
|
-
}
|
|
2123
|
-
const tenantId = typeof body.tenant_id === "string" ? body.tenant_id.trim() : "";
|
|
2124
|
-
if (!tenantId) {
|
|
2125
|
-
return Response.json({ error: "tenant_id is required" }, { status: 400, headers });
|
|
2126
|
-
}
|
|
2127
|
-
const roleKey = typeof body.role === "string" ? body.role.trim() : "operator";
|
|
2128
|
-
const roleScopes = ROLE_SCOPES[roleKey];
|
|
2129
|
-
if (!roleScopes) {
|
|
2130
|
-
return Response.json({ error: `invalid role: ${roleKey} (operator, contributor, viewer)` }, { status: 400, headers });
|
|
2131
|
-
}
|
|
2132
|
-
const bindingId = typeof body.binding_id === "string" && body.binding_id.trim().length > 0
|
|
2133
|
-
? body.binding_id.trim()
|
|
2134
|
-
: `bind-${crypto.randomUUID()}`;
|
|
2135
|
-
const operatorId = typeof body.operator_id === "string" && body.operator_id.trim().length > 0
|
|
2136
|
-
? body.operator_id.trim()
|
|
2137
|
-
: "default";
|
|
2138
|
-
const decision = await identityStore.link({
|
|
2139
|
-
bindingId,
|
|
2140
|
-
operatorId,
|
|
2141
|
-
channel: channel,
|
|
2142
|
-
channelTenantId: tenantId,
|
|
2143
|
-
channelActorId: actorId,
|
|
2144
|
-
scopes: [...roleScopes],
|
|
2145
|
-
});
|
|
2146
|
-
switch (decision.kind) {
|
|
2147
|
-
case "linked":
|
|
2148
|
-
return Response.json({ ok: true, kind: "linked", binding: decision.binding }, { status: 201, headers });
|
|
2149
|
-
case "binding_exists":
|
|
2150
|
-
return Response.json({ ok: false, kind: "binding_exists", binding: decision.binding }, { status: 409, headers });
|
|
2151
|
-
case "principal_already_linked":
|
|
2152
|
-
return Response.json({ ok: false, kind: "principal_already_linked", binding: decision.binding }, { status: 409, headers });
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
if (path === "/api/identities/unlink") {
|
|
2156
|
-
if (request.method !== "POST") {
|
|
2157
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
2158
|
-
}
|
|
2159
|
-
let body;
|
|
2160
|
-
try {
|
|
2161
|
-
body = (await request.json());
|
|
2162
|
-
}
|
|
2163
|
-
catch {
|
|
2164
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
2165
|
-
}
|
|
2166
|
-
const bindingId = typeof body.binding_id === "string" ? body.binding_id.trim() : "";
|
|
2167
|
-
if (!bindingId) {
|
|
2168
|
-
return Response.json({ error: "binding_id is required" }, { status: 400, headers });
|
|
2169
|
-
}
|
|
2170
|
-
const actorBindingId = typeof body.actor_binding_id === "string" ? body.actor_binding_id.trim() : "";
|
|
2171
|
-
if (!actorBindingId) {
|
|
2172
|
-
return Response.json({ error: "actor_binding_id is required" }, { status: 400, headers });
|
|
2173
|
-
}
|
|
2174
|
-
const reason = typeof body.reason === "string" ? body.reason.trim() : null;
|
|
2175
|
-
const decision = await identityStore.unlinkSelf({
|
|
2176
|
-
bindingId,
|
|
2177
|
-
actorBindingId,
|
|
2178
|
-
reason: reason || null,
|
|
2179
|
-
});
|
|
2180
|
-
switch (decision.kind) {
|
|
2181
|
-
case "unlinked":
|
|
2182
|
-
return Response.json({ ok: true, kind: "unlinked", binding: decision.binding }, { headers });
|
|
2183
|
-
case "not_found":
|
|
2184
|
-
return Response.json({ ok: false, kind: "not_found" }, { status: 404, headers });
|
|
2185
|
-
case "invalid_actor":
|
|
2186
|
-
return Response.json({ ok: false, kind: "invalid_actor" }, { status: 403, headers });
|
|
2187
|
-
case "already_inactive":
|
|
2188
|
-
return Response.json({ ok: false, kind: "already_inactive", binding: decision.binding }, { status: 409, headers });
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
if (path.startsWith("/api/issues")) {
|
|
2193
|
-
const response = await issueRoutes(request, context);
|
|
2194
|
-
headers.forEach((value, key) => {
|
|
2195
|
-
response.headers.set(key, value);
|
|
2196
|
-
});
|
|
2197
|
-
return response;
|
|
2198
|
-
}
|
|
2199
|
-
if (path.startsWith("/api/forum")) {
|
|
2200
|
-
const response = await forumRoutes(request, context);
|
|
2201
|
-
headers.forEach((value, key) => {
|
|
2202
|
-
response.headers.set(key, value);
|
|
2203
|
-
});
|
|
2204
|
-
return response;
|
|
2205
|
-
}
|
|
2206
|
-
if (path.startsWith("/api/events")) {
|
|
2207
|
-
const response = await eventRoutes(request, context);
|
|
2208
|
-
headers.forEach((value, key) => {
|
|
2209
|
-
response.headers.set(key, value);
|
|
2210
|
-
});
|
|
2211
|
-
return response;
|
|
2212
|
-
}
|
|
2213
|
-
if (path.startsWith("/webhooks/")) {
|
|
2214
|
-
const response = await controlPlaneProxy.handleWebhook(path, request);
|
|
2215
|
-
if (response) {
|
|
2216
|
-
headers.forEach((value, key) => {
|
|
2217
|
-
response.headers.set(key, value);
|
|
2218
|
-
});
|
|
2219
|
-
return response;
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
const filePath = resolve(PUBLIC_DIR, `.${path === "/" ? "/index.html" : path}`);
|
|
2223
|
-
if (!filePath.startsWith(PUBLIC_DIR)) {
|
|
2224
|
-
return new Response("Forbidden", { status: 403, headers });
|
|
2225
|
-
}
|
|
2226
|
-
const file = Bun.file(filePath);
|
|
2227
|
-
if (await file.exists()) {
|
|
2228
|
-
const ext = extname(filePath);
|
|
2229
|
-
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2230
|
-
headers.set("Content-Type", mime);
|
|
2231
|
-
return new Response(await file.arrayBuffer(), { status: 200, headers });
|
|
2232
|
-
}
|
|
2233
|
-
const indexPath = join(PUBLIC_DIR, "index.html");
|
|
2234
|
-
const indexFile = Bun.file(indexPath);
|
|
2235
|
-
if (await indexFile.exists()) {
|
|
2236
|
-
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
2237
|
-
return new Response(await indexFile.arrayBuffer(), { status: 200, headers });
|
|
2238
|
-
}
|
|
2239
|
-
return new Response("Not Found", { status: 404, headers });
|
|
2240
|
-
};
|
|
2241
|
-
const server = {
|
|
2242
|
-
port: options.port || 3000,
|
|
2243
|
-
fetch: handleRequest,
|
|
2244
|
-
hostname: "0.0.0.0",
|
|
2245
|
-
controlPlane: controlPlaneProxy,
|
|
2246
|
-
activitySupervisor,
|
|
2247
|
-
heartbeatPrograms,
|
|
2248
|
-
cronPrograms,
|
|
515
|
+
const server = {
|
|
516
|
+
port: options.port || 3000,
|
|
517
|
+
fetch: handleRequest,
|
|
518
|
+
hostname: "0.0.0.0",
|
|
519
|
+
controlPlane: controlPlaneProxy,
|
|
520
|
+
activitySupervisor,
|
|
521
|
+
heartbeatPrograms,
|
|
522
|
+
cronPrograms,
|
|
2249
523
|
};
|
|
2250
524
|
return server;
|
|
2251
525
|
}
|
|
2252
|
-
|
|
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 = {}) {
|
|
2260
|
-
const repoRoot = options.repoRoot || process.cwd();
|
|
2261
|
-
const readConfig = options.configReader ?? readMuConfigFile;
|
|
2262
|
-
const config = options.config ?? (await readConfig(repoRoot));
|
|
2263
|
-
const heartbeatScheduler = options.heartbeatScheduler ?? new ActivityHeartbeatScheduler();
|
|
2264
|
-
const generationTelemetry = options.generationTelemetry ?? new GenerationTelemetryRecorder();
|
|
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 {
|
|
2281
|
-
repoRoot,
|
|
2282
|
-
config,
|
|
2283
|
-
heartbeatScheduler,
|
|
2284
|
-
generationTelemetry,
|
|
2285
|
-
sessionLifecycle,
|
|
2286
|
-
controlPlane,
|
|
2287
|
-
capabilities: computeServerRuntimeCapabilities(controlPlane),
|
|
2288
|
-
};
|
|
2289
|
-
}
|
|
526
|
+
export { composeServerRuntime } from "./server_runtime.js";
|
|
2290
527
|
export function createServerFromRuntime(runtime, options = {}) {
|
|
2291
528
|
return createServer({
|
|
2292
529
|
...options,
|