@femtomc/mu-server 26.2.69 → 26.2.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/api/events.js +77 -19
- package/dist/api/forum.js +52 -18
- package/dist/api/issues.js +120 -33
- package/dist/control_plane.d.ts +5 -114
- package/dist/control_plane.js +14 -631
- package/dist/control_plane_bootstrap_helpers.d.ts +15 -0
- package/dist/control_plane_bootstrap_helpers.js +75 -0
- package/dist/control_plane_contract.d.ts +119 -0
- package/dist/control_plane_contract.js +1 -0
- package/dist/control_plane_run_outbox.d.ts +7 -0
- package/dist/control_plane_run_outbox.js +52 -0
- package/dist/control_plane_telegram_generation.d.ts +27 -0
- package/dist/control_plane_telegram_generation.js +521 -0
- package/dist/cron_request.d.ts +8 -0
- package/dist/cron_request.js +65 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/server.d.ts +8 -40
- package/dist/server.js +35 -1607
- package/dist/server_program_orchestration.d.ts +38 -0
- package/dist/server_program_orchestration.js +252 -0
- package/dist/server_routing.d.ts +30 -0
- package/dist/server_routing.js +1102 -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 +6 -6
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EventLog } from "@femtomc/mu-core/node";
|
|
2
|
+
import type { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
|
|
3
|
+
import type { ControlPlaneHandle } from "./control_plane_contract.js";
|
|
4
|
+
import { CronProgramRegistry } from "./cron_programs.js";
|
|
5
|
+
import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
|
|
6
|
+
import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
7
|
+
export type AutoHeartbeatRunSnapshot = {
|
|
8
|
+
job_id: string;
|
|
9
|
+
root_issue_id: string | null;
|
|
10
|
+
status: string;
|
|
11
|
+
source: "command" | "api";
|
|
12
|
+
mode: string;
|
|
13
|
+
};
|
|
14
|
+
type OperatorWakeEmitter = (opts: {
|
|
15
|
+
dedupeKey: string;
|
|
16
|
+
message: string;
|
|
17
|
+
payload: Record<string, unknown>;
|
|
18
|
+
coalesceMs?: number;
|
|
19
|
+
}) => Promise<boolean>;
|
|
20
|
+
export declare function createServerProgramOrchestration(opts: {
|
|
21
|
+
repoRoot: string;
|
|
22
|
+
heartbeatScheduler: ActivityHeartbeatScheduler;
|
|
23
|
+
controlPlaneProxy: ControlPlaneHandle;
|
|
24
|
+
activitySupervisor: ControlPlaneActivitySupervisor;
|
|
25
|
+
eventLog: EventLog;
|
|
26
|
+
autoRunHeartbeatEveryMs: number;
|
|
27
|
+
emitOperatorWake: OperatorWakeEmitter;
|
|
28
|
+
}): {
|
|
29
|
+
heartbeatPrograms: HeartbeatProgramRegistry;
|
|
30
|
+
cronPrograms: CronProgramRegistry;
|
|
31
|
+
registerAutoRunHeartbeatProgram: (run: AutoHeartbeatRunSnapshot) => Promise<void>;
|
|
32
|
+
disableAutoRunHeartbeatProgram: (opts: {
|
|
33
|
+
jobId: string;
|
|
34
|
+
status: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { CronProgramRegistry } from "./cron_programs.js";
|
|
2
|
+
import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
|
|
3
|
+
const AUTO_RUN_HEARTBEAT_REASON = "auto-run-heartbeat";
|
|
4
|
+
export function createServerProgramOrchestration(opts) {
|
|
5
|
+
const autoRunHeartbeatProgramByJobId = new Map();
|
|
6
|
+
const heartbeatPrograms = new HeartbeatProgramRegistry({
|
|
7
|
+
repoRoot: opts.repoRoot,
|
|
8
|
+
heartbeatScheduler: opts.heartbeatScheduler,
|
|
9
|
+
runHeartbeat: async (runOpts) => {
|
|
10
|
+
const result = await opts.controlPlaneProxy.heartbeatRun?.({
|
|
11
|
+
jobId: runOpts.jobId ?? null,
|
|
12
|
+
rootIssueId: runOpts.rootIssueId ?? null,
|
|
13
|
+
reason: runOpts.reason ?? null,
|
|
14
|
+
wakeMode: runOpts.wakeMode,
|
|
15
|
+
});
|
|
16
|
+
return result ?? { ok: false, reason: "not_found" };
|
|
17
|
+
},
|
|
18
|
+
activityHeartbeat: async (activityOpts) => {
|
|
19
|
+
return opts.activitySupervisor.heartbeat({
|
|
20
|
+
activityId: activityOpts.activityId ?? null,
|
|
21
|
+
reason: activityOpts.reason ?? null,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
onTickEvent: async (event) => {
|
|
25
|
+
await opts.eventLog.emit("heartbeat_program.tick", {
|
|
26
|
+
source: "mu-server.heartbeat-programs",
|
|
27
|
+
payload: {
|
|
28
|
+
program_id: event.program_id,
|
|
29
|
+
status: event.status,
|
|
30
|
+
reason: event.reason,
|
|
31
|
+
message: event.message,
|
|
32
|
+
program: event.program,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
await opts.emitOperatorWake({
|
|
36
|
+
dedupeKey: `heartbeat-program:${event.program_id}`,
|
|
37
|
+
message: event.message,
|
|
38
|
+
payload: {
|
|
39
|
+
wake_source: "heartbeat_program",
|
|
40
|
+
program_id: event.program_id,
|
|
41
|
+
status: event.status,
|
|
42
|
+
reason: event.reason,
|
|
43
|
+
wake_mode: event.program.wake_mode,
|
|
44
|
+
target_kind: event.program.target.kind,
|
|
45
|
+
target: event.program.target.kind === "run"
|
|
46
|
+
? {
|
|
47
|
+
job_id: event.program.target.job_id,
|
|
48
|
+
root_issue_id: event.program.target.root_issue_id,
|
|
49
|
+
}
|
|
50
|
+
: { activity_id: event.program.target.activity_id },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const cronPrograms = new CronProgramRegistry({
|
|
56
|
+
repoRoot: opts.repoRoot,
|
|
57
|
+
heartbeatScheduler: opts.heartbeatScheduler,
|
|
58
|
+
runHeartbeat: async (runOpts) => {
|
|
59
|
+
const result = await opts.controlPlaneProxy.heartbeatRun?.({
|
|
60
|
+
jobId: runOpts.jobId ?? null,
|
|
61
|
+
rootIssueId: runOpts.rootIssueId ?? null,
|
|
62
|
+
reason: runOpts.reason ?? null,
|
|
63
|
+
wakeMode: runOpts.wakeMode,
|
|
64
|
+
});
|
|
65
|
+
return result ?? { ok: false, reason: "not_found" };
|
|
66
|
+
},
|
|
67
|
+
activityHeartbeat: async (activityOpts) => {
|
|
68
|
+
return opts.activitySupervisor.heartbeat({
|
|
69
|
+
activityId: activityOpts.activityId ?? null,
|
|
70
|
+
reason: activityOpts.reason ?? null,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
onLifecycleEvent: async (event) => {
|
|
74
|
+
await opts.eventLog.emit("cron_program.lifecycle", {
|
|
75
|
+
source: "mu-server.cron-programs",
|
|
76
|
+
payload: {
|
|
77
|
+
action: event.action,
|
|
78
|
+
program_id: event.program_id,
|
|
79
|
+
message: event.message,
|
|
80
|
+
program: event.program,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
onTickEvent: async (event) => {
|
|
85
|
+
await opts.eventLog.emit("cron_program.tick", {
|
|
86
|
+
source: "mu-server.cron-programs",
|
|
87
|
+
payload: {
|
|
88
|
+
program_id: event.program_id,
|
|
89
|
+
status: event.status,
|
|
90
|
+
reason: event.reason,
|
|
91
|
+
message: event.message,
|
|
92
|
+
program: event.program,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
await opts.emitOperatorWake({
|
|
96
|
+
dedupeKey: `cron-program:${event.program_id}`,
|
|
97
|
+
message: event.message,
|
|
98
|
+
payload: {
|
|
99
|
+
wake_source: "cron_program",
|
|
100
|
+
program_id: event.program_id,
|
|
101
|
+
status: event.status,
|
|
102
|
+
reason: event.reason,
|
|
103
|
+
wake_mode: event.program.wake_mode,
|
|
104
|
+
target_kind: event.program.target.kind,
|
|
105
|
+
target: event.program.target.kind === "run"
|
|
106
|
+
? {
|
|
107
|
+
job_id: event.program.target.job_id,
|
|
108
|
+
root_issue_id: event.program.target.root_issue_id,
|
|
109
|
+
}
|
|
110
|
+
: { activity_id: event.program.target.activity_id },
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const findAutoRunHeartbeatProgram = async (jobId) => {
|
|
116
|
+
const normalizedJobId = jobId.trim();
|
|
117
|
+
if (!normalizedJobId) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const knownProgramId = autoRunHeartbeatProgramByJobId.get(normalizedJobId);
|
|
121
|
+
if (knownProgramId) {
|
|
122
|
+
const knownProgram = await heartbeatPrograms.get(knownProgramId);
|
|
123
|
+
if (knownProgram) {
|
|
124
|
+
return knownProgram;
|
|
125
|
+
}
|
|
126
|
+
autoRunHeartbeatProgramByJobId.delete(normalizedJobId);
|
|
127
|
+
}
|
|
128
|
+
const programs = await heartbeatPrograms.list({ targetKind: "run", limit: 500 });
|
|
129
|
+
for (const program of programs) {
|
|
130
|
+
if (program.metadata.auto_run_job_id !== normalizedJobId) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
autoRunHeartbeatProgramByJobId.set(normalizedJobId, program.program_id);
|
|
134
|
+
return program;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
const registerAutoRunHeartbeatProgram = async (run) => {
|
|
139
|
+
if (run.source === "command") {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const jobId = run.job_id.trim();
|
|
143
|
+
if (!jobId || run.status !== "running") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const rootIssueId = typeof run.root_issue_id === "string" ? run.root_issue_id.trim() : "";
|
|
147
|
+
const metadata = {
|
|
148
|
+
auto_run_heartbeat: true,
|
|
149
|
+
auto_run_job_id: jobId,
|
|
150
|
+
auto_run_root_issue_id: rootIssueId || null,
|
|
151
|
+
auto_disable_on_terminal: true,
|
|
152
|
+
run_mode: run.mode,
|
|
153
|
+
run_source: run.source,
|
|
154
|
+
};
|
|
155
|
+
const existing = await findAutoRunHeartbeatProgram(jobId);
|
|
156
|
+
if (existing) {
|
|
157
|
+
const result = await heartbeatPrograms.update({
|
|
158
|
+
programId: existing.program_id,
|
|
159
|
+
title: `Run heartbeat: ${rootIssueId || jobId}`,
|
|
160
|
+
target: {
|
|
161
|
+
kind: "run",
|
|
162
|
+
job_id: jobId,
|
|
163
|
+
root_issue_id: rootIssueId || null,
|
|
164
|
+
},
|
|
165
|
+
enabled: true,
|
|
166
|
+
everyMs: opts.autoRunHeartbeatEveryMs,
|
|
167
|
+
reason: AUTO_RUN_HEARTBEAT_REASON,
|
|
168
|
+
wakeMode: "next_heartbeat",
|
|
169
|
+
metadata,
|
|
170
|
+
});
|
|
171
|
+
if (result.ok && result.program) {
|
|
172
|
+
autoRunHeartbeatProgramByJobId.set(jobId, result.program.program_id);
|
|
173
|
+
await opts.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
174
|
+
source: "mu-server.runs",
|
|
175
|
+
payload: {
|
|
176
|
+
action: "updated",
|
|
177
|
+
run_job_id: jobId,
|
|
178
|
+
run_root_issue_id: rootIssueId || null,
|
|
179
|
+
program_id: result.program.program_id,
|
|
180
|
+
program: result.program,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const created = await heartbeatPrograms.create({
|
|
187
|
+
title: `Run heartbeat: ${rootIssueId || jobId}`,
|
|
188
|
+
target: {
|
|
189
|
+
kind: "run",
|
|
190
|
+
job_id: jobId,
|
|
191
|
+
root_issue_id: rootIssueId || null,
|
|
192
|
+
},
|
|
193
|
+
everyMs: opts.autoRunHeartbeatEveryMs,
|
|
194
|
+
reason: AUTO_RUN_HEARTBEAT_REASON,
|
|
195
|
+
wakeMode: "next_heartbeat",
|
|
196
|
+
metadata,
|
|
197
|
+
enabled: true,
|
|
198
|
+
});
|
|
199
|
+
autoRunHeartbeatProgramByJobId.set(jobId, created.program_id);
|
|
200
|
+
await opts.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
201
|
+
source: "mu-server.runs",
|
|
202
|
+
payload: {
|
|
203
|
+
action: "registered",
|
|
204
|
+
run_job_id: jobId,
|
|
205
|
+
run_root_issue_id: rootIssueId || null,
|
|
206
|
+
program_id: created.program_id,
|
|
207
|
+
program: created,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
const disableAutoRunHeartbeatProgram = async (disableOpts) => {
|
|
212
|
+
const program = await findAutoRunHeartbeatProgram(disableOpts.jobId);
|
|
213
|
+
if (!program) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const metadata = {
|
|
217
|
+
...program.metadata,
|
|
218
|
+
auto_disabled_from_status: disableOpts.status,
|
|
219
|
+
auto_disabled_reason: disableOpts.reason,
|
|
220
|
+
auto_disabled_at_ms: Date.now(),
|
|
221
|
+
};
|
|
222
|
+
const result = await heartbeatPrograms.update({
|
|
223
|
+
programId: program.program_id,
|
|
224
|
+
enabled: false,
|
|
225
|
+
everyMs: 0,
|
|
226
|
+
reason: AUTO_RUN_HEARTBEAT_REASON,
|
|
227
|
+
wakeMode: program.wake_mode,
|
|
228
|
+
metadata,
|
|
229
|
+
});
|
|
230
|
+
autoRunHeartbeatProgramByJobId.delete(disableOpts.jobId.trim());
|
|
231
|
+
if (!result.ok || !result.program) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await opts.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
235
|
+
source: "mu-server.runs",
|
|
236
|
+
payload: {
|
|
237
|
+
action: "disabled",
|
|
238
|
+
run_job_id: disableOpts.jobId,
|
|
239
|
+
status: disableOpts.status,
|
|
240
|
+
reason: disableOpts.reason,
|
|
241
|
+
program_id: result.program.program_id,
|
|
242
|
+
program: result.program,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
heartbeatPrograms,
|
|
248
|
+
cronPrograms,
|
|
249
|
+
registerAutoRunHeartbeatProgram,
|
|
250
|
+
disableAutoRunHeartbeatProgram,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
|
|
2
|
+
import { type MuConfig } from "./config.js";
|
|
3
|
+
import type { ControlPlaneHandle } from "./control_plane_contract.js";
|
|
4
|
+
import type { CronProgramRegistry } from "./cron_programs.js";
|
|
5
|
+
import type { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
|
|
6
|
+
import type { AutoHeartbeatRunSnapshot } from "./server_program_orchestration.js";
|
|
7
|
+
import type { ServerContext } from "./server.js";
|
|
8
|
+
export type ServerRoutingDependencies = {
|
|
9
|
+
context: ServerContext;
|
|
10
|
+
controlPlaneProxy: ControlPlaneHandle;
|
|
11
|
+
activitySupervisor: ControlPlaneActivitySupervisor;
|
|
12
|
+
heartbeatPrograms: HeartbeatProgramRegistry;
|
|
13
|
+
cronPrograms: CronProgramRegistry;
|
|
14
|
+
loadConfigFromDisk: () => Promise<MuConfig>;
|
|
15
|
+
writeConfig: (repoRoot: string, config: MuConfig) => Promise<string>;
|
|
16
|
+
reloadControlPlane: (reason: string) => Promise<{
|
|
17
|
+
ok: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
getControlPlaneStatus: () => unknown;
|
|
20
|
+
registerAutoRunHeartbeatProgram: (run: AutoHeartbeatRunSnapshot) => Promise<void>;
|
|
21
|
+
disableAutoRunHeartbeatProgram: (opts: {
|
|
22
|
+
jobId: string;
|
|
23
|
+
status: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}) => Promise<void>;
|
|
26
|
+
describeError: (error: unknown) => string;
|
|
27
|
+
publicDir?: string;
|
|
28
|
+
mimeTypes?: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
export declare function createServerRequestHandler(deps: ServerRoutingDependencies): (request: Request) => Promise<Response>;
|