@femtomc/mu-server 26.2.73 → 26.2.74

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.
Files changed (51) hide show
  1. package/README.md +54 -66
  2. package/dist/api/control_plane.js +56 -0
  3. package/dist/api/cron.js +2 -23
  4. package/dist/api/heartbeats.js +1 -66
  5. package/dist/api/identities.js +3 -2
  6. package/dist/api/runs.js +0 -83
  7. package/dist/api/session_flash.d.ts +60 -0
  8. package/dist/api/session_flash.js +326 -0
  9. package/dist/api/session_turn.d.ts +38 -0
  10. package/dist/api/session_turn.js +423 -0
  11. package/dist/config.d.ts +9 -4
  12. package/dist/config.js +24 -24
  13. package/dist/control_plane.d.ts +2 -16
  14. package/dist/control_plane.js +57 -83
  15. package/dist/control_plane_adapter_registry.d.ts +19 -0
  16. package/dist/control_plane_adapter_registry.js +74 -0
  17. package/dist/control_plane_contract.d.ts +1 -7
  18. package/dist/control_plane_run_queue_coordinator.d.ts +1 -7
  19. package/dist/control_plane_run_queue_coordinator.js +1 -62
  20. package/dist/control_plane_telegram_generation.js +1 -0
  21. package/dist/control_plane_wake_delivery.js +1 -0
  22. package/dist/cron_programs.d.ts +21 -35
  23. package/dist/cron_programs.js +32 -113
  24. package/dist/cron_request.d.ts +0 -6
  25. package/dist/cron_request.js +0 -41
  26. package/dist/heartbeat_programs.d.ts +20 -35
  27. package/dist/heartbeat_programs.js +26 -122
  28. package/dist/index.d.ts +2 -2
  29. package/dist/outbound_delivery_router.d.ts +12 -0
  30. package/dist/outbound_delivery_router.js +29 -0
  31. package/dist/run_supervisor.d.ts +1 -16
  32. package/dist/run_supervisor.js +0 -70
  33. package/dist/server.d.ts +0 -5
  34. package/dist/server.js +95 -127
  35. package/dist/server_program_orchestration.d.ts +4 -19
  36. package/dist/server_program_orchestration.js +49 -200
  37. package/dist/server_routing.d.ts +0 -9
  38. package/dist/server_routing.js +19 -654
  39. package/dist/server_runtime.js +0 -1
  40. package/dist/server_types.d.ts +0 -2
  41. package/dist/server_types.js +0 -7
  42. package/package.json +6 -9
  43. package/dist/api/context.d.ts +0 -5
  44. package/dist/api/context.js +0 -1147
  45. package/dist/api/forum.d.ts +0 -2
  46. package/dist/api/forum.js +0 -75
  47. package/dist/api/issues.d.ts +0 -2
  48. package/dist/api/issues.js +0 -173
  49. package/public/assets/index-CxkevQNh.js +0 -100
  50. package/public/assets/index-D_8anM-D.css +0 -1
  51. package/public/index.html +0 -14
@@ -4,43 +4,6 @@ const HEARTBEAT_PROGRAMS_FILENAME = "heartbeats.jsonl";
4
4
  function defaultNowMs() {
5
5
  return Date.now();
6
6
  }
7
- function normalizeTarget(input) {
8
- if (!input || typeof input !== "object" || Array.isArray(input)) {
9
- return null;
10
- }
11
- const record = input;
12
- const kind = typeof record.kind === "string" ? record.kind.trim().toLowerCase() : "";
13
- if (kind === "run") {
14
- const jobId = typeof record.job_id === "string" ? record.job_id.trim() : "";
15
- const rootIssueId = typeof record.root_issue_id === "string" ? record.root_issue_id.trim() : "";
16
- if (!jobId && !rootIssueId) {
17
- return null;
18
- }
19
- return {
20
- kind: "run",
21
- job_id: jobId || null,
22
- root_issue_id: rootIssueId || null,
23
- };
24
- }
25
- if (kind === "activity") {
26
- const activityId = typeof record.activity_id === "string" ? record.activity_id.trim() : "";
27
- if (!activityId) {
28
- return null;
29
- }
30
- return {
31
- kind: "activity",
32
- activity_id: activityId,
33
- };
34
- }
35
- return null;
36
- }
37
- function normalizeWakeMode(value) {
38
- if (typeof value !== "string") {
39
- return "immediate";
40
- }
41
- const normalized = value.trim().toLowerCase().replaceAll("-", "_");
42
- return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
43
- }
44
7
  function sanitizeMetadata(value) {
45
8
  if (!value || typeof value !== "object" || Array.isArray(value)) {
46
9
  return {};
@@ -54,8 +17,7 @@ function normalizeProgram(row) {
54
17
  const record = row;
55
18
  const programId = typeof record.program_id === "string" ? record.program_id.trim() : "";
56
19
  const title = typeof record.title === "string" ? record.title.trim() : "";
57
- const target = normalizeTarget(record.target);
58
- if (!programId || !title || !target) {
20
+ if (!programId || !title) {
59
21
  return null;
60
22
  }
61
23
  const everyMsRaw = record.every_ms;
@@ -70,14 +32,8 @@ function normalizeProgram(row) {
70
32
  ? Math.trunc(record.last_triggered_at_ms)
71
33
  : null;
72
34
  const lastResultRaw = typeof record.last_result === "string" ? record.last_result.trim().toLowerCase() : null;
73
- const lastResult = lastResultRaw === "ok" ||
74
- lastResultRaw === "not_found" ||
75
- lastResultRaw === "not_running" ||
76
- lastResultRaw === "failed"
77
- ? lastResultRaw
78
- : null;
35
+ const lastResult = lastResultRaw === "ok" || lastResultRaw === "coalesced" || lastResultRaw === "failed" ? lastResultRaw : null;
79
36
  const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : "scheduled";
80
- const wakeMode = normalizeWakeMode(record.wake_mode);
81
37
  return {
82
38
  v: 1,
83
39
  program_id: programId,
@@ -85,8 +41,6 @@ function normalizeProgram(row) {
85
41
  enabled: record.enabled !== false,
86
42
  every_ms: everyMs,
87
43
  reason,
88
- wake_mode: wakeMode,
89
- target,
90
44
  metadata: sanitizeMetadata(record.metadata),
91
45
  created_at_ms: createdAt,
92
46
  updated_at_ms: updatedAt,
@@ -106,16 +60,14 @@ function sortPrograms(programs) {
106
60
  export class HeartbeatProgramRegistry {
107
61
  #store;
108
62
  #heartbeatScheduler;
109
- #runHeartbeat;
110
- #activityHeartbeat;
63
+ #dispatchWake;
111
64
  #onTickEvent;
112
65
  #nowMs;
113
66
  #programs = new Map();
114
67
  #loaded = null;
115
68
  constructor(opts) {
116
69
  this.#heartbeatScheduler = opts.heartbeatScheduler;
117
- this.#runHeartbeat = opts.runHeartbeat;
118
- this.#activityHeartbeat = opts.activityHeartbeat;
70
+ this.#dispatchWake = opts.dispatchWake;
119
71
  this.#onTickEvent = opts.onTickEvent;
120
72
  this.#nowMs = opts.nowMs ?? defaultNowMs;
121
73
  this.#store =
@@ -128,7 +80,6 @@ export class HeartbeatProgramRegistry {
128
80
  #snapshot(program) {
129
81
  return {
130
82
  ...program,
131
- target: program.target.kind === "run" ? { ...program.target } : { ...program.target },
132
83
  metadata: { ...program.metadata },
133
84
  };
134
85
  }
@@ -187,50 +138,38 @@ export class HeartbeatProgramRegistry {
187
138
  const nowMs = Math.trunc(this.#nowMs());
188
139
  program.last_triggered_at_ms = nowMs;
189
140
  program.updated_at_ms = nowMs;
190
- let heartbeatResult;
141
+ let tickResult;
191
142
  let eventStatus = "ok";
192
143
  let eventReason = heartbeatReason;
193
- let eventMessage = `heartbeat program tick: ${program.title}`;
144
+ let eventMessage = `heartbeat program dispatched wake: ${program.title}`;
194
145
  try {
195
- const result = program.target.kind === "run"
196
- ? await this.#runHeartbeat({
197
- jobId: program.target.job_id,
198
- rootIssueId: program.target.root_issue_id,
199
- reason: heartbeatReason,
200
- wakeMode: program.wake_mode,
201
- })
202
- : await this.#activityHeartbeat({
203
- activityId: program.target.activity_id,
204
- reason: heartbeatReason,
205
- });
206
- if (result.ok) {
146
+ const result = await this.#dispatchWake({
147
+ programId: program.program_id,
148
+ title: program.title,
149
+ reason: heartbeatReason,
150
+ metadata: { ...program.metadata },
151
+ triggeredAtMs: nowMs,
152
+ });
153
+ if (result.status === "ok") {
207
154
  program.last_result = "ok";
208
155
  program.last_error = null;
209
- heartbeatResult = { status: "ran" };
156
+ tickResult = { status: "ran" };
210
157
  }
211
- else if (result.reason === "not_running") {
212
- program.last_result = "not_running";
158
+ else if (result.status === "coalesced") {
159
+ program.last_result = "coalesced";
213
160
  program.last_error = null;
214
- eventStatus = "not_running";
215
- eventReason = result.reason;
216
- eventMessage = `heartbeat program skipped (not running): ${program.title}`;
217
- heartbeatResult = { status: "skipped", reason: "not_running" };
218
- }
219
- else if (result.reason === "not_found") {
220
- program.last_result = "not_found";
221
- program.last_error = null;
222
- eventStatus = "not_found";
223
- eventReason = result.reason;
224
- eventMessage = `heartbeat program skipped (not found): ${program.title}`;
225
- heartbeatResult = { status: "skipped", reason: "not_found" };
161
+ eventStatus = "coalesced";
162
+ eventReason = result.reason ?? "coalesced";
163
+ eventMessage = `heartbeat program coalesced wake: ${program.title}`;
164
+ tickResult = { status: "skipped", reason: "coalesced" };
226
165
  }
227
166
  else {
228
167
  program.last_result = "failed";
229
- program.last_error = result.reason ?? "heartbeat_program_tick_failed";
168
+ program.last_error = result.reason;
230
169
  eventStatus = "failed";
231
- eventReason = program.last_error;
170
+ eventReason = result.reason;
232
171
  eventMessage = `heartbeat program failed: ${program.title}`;
233
- heartbeatResult = { status: "failed", reason: program.last_error };
172
+ tickResult = { status: "failed", reason: result.reason };
234
173
  }
235
174
  }
236
175
  catch (err) {
@@ -239,23 +178,7 @@ export class HeartbeatProgramRegistry {
239
178
  eventStatus = "failed";
240
179
  eventReason = program.last_error;
241
180
  eventMessage = `heartbeat program failed: ${program.title}`;
242
- heartbeatResult = { status: "failed", reason: program.last_error };
243
- }
244
- const shouldAutoDisableOnTerminal = program.target.kind === "run" &&
245
- program.metadata.auto_disable_on_terminal === true &&
246
- heartbeatResult.status === "skipped" &&
247
- (heartbeatResult.reason === "not_running" || heartbeatResult.reason === "not_found");
248
- if (shouldAutoDisableOnTerminal) {
249
- program.enabled = false;
250
- program.every_ms = 0;
251
- program.updated_at_ms = Math.trunc(this.#nowMs());
252
- program.metadata = {
253
- ...program.metadata,
254
- auto_disabled_at_ms: Math.trunc(this.#nowMs()),
255
- auto_disabled_reason: heartbeatResult.status === "skipped" ? (heartbeatResult.reason ?? null) : null,
256
- };
257
- this.#heartbeatScheduler.unregister(this.#scheduleId(program.program_id));
258
- eventMessage = `${eventMessage} (auto-disabled)`;
181
+ tickResult = { status: "failed", reason: program.last_error };
259
182
  }
260
183
  await this.#persist();
261
184
  await this.#emitTickEvent({
@@ -268,7 +191,7 @@ export class HeartbeatProgramRegistry {
268
191
  }).catch(() => {
269
192
  // best effort only
270
193
  });
271
- return heartbeatResult;
194
+ return tickResult;
272
195
  }
273
196
  async list(opts = {}) {
274
197
  await this.#ensureLoaded();
@@ -278,9 +201,6 @@ export class HeartbeatProgramRegistry {
278
201
  if (typeof opts.enabled === "boolean" && program.enabled !== opts.enabled) {
279
202
  return false;
280
203
  }
281
- if (opts.targetKind && program.target.kind !== opts.targetKind) {
282
- return false;
283
- }
284
204
  return true;
285
205
  })
286
206
  .slice(0, limit)
@@ -297,10 +217,6 @@ export class HeartbeatProgramRegistry {
297
217
  if (!title) {
298
218
  throw new Error("heartbeat_program_title_required");
299
219
  }
300
- const target = normalizeTarget(opts.target);
301
- if (!target) {
302
- throw new Error("heartbeat_program_invalid_target");
303
- }
304
220
  const nowMs = Math.trunc(this.#nowMs());
305
221
  const program = {
306
222
  v: 1,
@@ -311,8 +227,6 @@ export class HeartbeatProgramRegistry {
311
227
  ? Math.max(0, Math.trunc(opts.everyMs))
312
228
  : 15_000,
313
229
  reason: opts.reason?.trim() || "scheduled",
314
- wake_mode: normalizeWakeMode(opts.wakeMode),
315
- target,
316
230
  metadata: sanitizeMetadata(opts.metadata),
317
231
  created_at_ms: nowMs,
318
232
  updated_at_ms: nowMs,
@@ -344,19 +258,9 @@ export class HeartbeatProgramRegistry {
344
258
  if (typeof opts.reason === "string") {
345
259
  program.reason = opts.reason.trim() || "scheduled";
346
260
  }
347
- if (typeof opts.wakeMode === "string") {
348
- program.wake_mode = normalizeWakeMode(opts.wakeMode);
349
- }
350
261
  if (typeof opts.enabled === "boolean") {
351
262
  program.enabled = opts.enabled;
352
263
  }
353
- if (opts.target) {
354
- const target = normalizeTarget(opts.target);
355
- if (!target) {
356
- return { ok: false, reason: "invalid_target", program: this.#snapshot(program) };
357
- }
358
- program.target = target;
359
- }
360
264
  if (opts.metadata) {
361
265
  program.metadata = sanitizeMetadata(opts.metadata);
362
266
  }
package/dist/index.d.ts CHANGED
@@ -7,13 +7,13 @@ export { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, ORCHEST
7
7
  export type { DurableRunQueueClaimOpts, DurableRunQueueEnqueueOpts, DurableRunQueueOpts, DurableRunQueueSnapshot, DurableRunQueueState, DurableRunQueueTransitionOpts, RunQueueReconcilePlan, } from "./run_queue.js";
8
8
  export { DurableRunQueue, queueStatesForRunStatusFilter, reconcileRunQueue, RUN_QUEUE_RECONCILE_INVARIANTS, runQueuePath, runSnapshotFromQueueSnapshot, runStatusFromQueueState, } from "./run_queue.js";
9
9
  export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
10
- export type { CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot, CronProgramTarget, CronProgramTickEvent, CronProgramWakeMode, } from "./cron_programs.js";
10
+ export type { CronProgramDispatchResult, CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot, CronProgramTickEvent, } from "./cron_programs.js";
11
11
  export { CronProgramRegistry } from "./cron_programs.js";
12
12
  export type { CronProgramSchedule as CronSchedule, CronProgramSchedule } from "./cron_schedule.js";
13
13
  export { computeNextScheduleRunAtMs, normalizeCronSchedule } from "./cron_schedule.js";
14
14
  export type { CronTimerRegistryOpts, CronTimerSnapshot } from "./cron_timer.js";
15
15
  export { CronTimerRegistry } from "./cron_timer.js";
16
- export type { HeartbeatProgramOperationResult, HeartbeatProgramRegistryOpts, HeartbeatProgramSnapshot, HeartbeatProgramTarget, HeartbeatProgramTickEvent, HeartbeatProgramWakeMode, } from "./heartbeat_programs.js";
16
+ export type { HeartbeatProgramDispatchResult, HeartbeatProgramOperationResult, HeartbeatProgramRegistryOpts, HeartbeatProgramSnapshot, HeartbeatProgramTickEvent, } from "./heartbeat_programs.js";
17
17
  export { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
18
18
  export type { ActivityHeartbeatSchedulerOpts, HeartbeatRunResult, HeartbeatTickHandler, } from "./heartbeat_scheduler.js";
19
19
  export { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
@@ -0,0 +1,12 @@
1
+ import { type Channel, type OutboxDeliveryHandlerResult, type OutboxRecord } from "@femtomc/mu-control-plane";
2
+ export type OutboundDeliveryDriver = {
3
+ channel: Channel;
4
+ deliver: (record: OutboxRecord) => Promise<OutboxDeliveryHandlerResult>;
5
+ };
6
+ export declare class OutboundDeliveryRouter {
7
+ #private;
8
+ constructor(drivers: readonly OutboundDeliveryDriver[]);
9
+ supportsChannel(channel: Channel): boolean;
10
+ supportedChannels(): Channel[];
11
+ deliver(record: OutboxRecord): Promise<undefined | OutboxDeliveryHandlerResult>;
12
+ }
@@ -0,0 +1,29 @@
1
+ import { ChannelSchema, } from "@femtomc/mu-control-plane";
2
+ export class OutboundDeliveryRouter {
3
+ #driversByChannel = new Map();
4
+ constructor(drivers) {
5
+ for (const driver of drivers) {
6
+ if (this.#driversByChannel.has(driver.channel)) {
7
+ throw new Error(`duplicate outbound delivery driver: ${driver.channel}`);
8
+ }
9
+ this.#driversByChannel.set(driver.channel, driver);
10
+ }
11
+ }
12
+ supportsChannel(channel) {
13
+ return this.#driversByChannel.has(channel);
14
+ }
15
+ supportedChannels() {
16
+ return [...this.#driversByChannel.keys()].sort((a, b) => a.localeCompare(b));
17
+ }
18
+ async deliver(record) {
19
+ const parsedChannel = ChannelSchema.safeParse(record.envelope.channel);
20
+ if (!parsedChannel.success) {
21
+ return undefined;
22
+ }
23
+ const driver = this.#driversByChannel.get(parsedChannel.data);
24
+ if (!driver) {
25
+ return undefined;
26
+ }
27
+ return await driver.deliver(record);
28
+ }
29
+ }
@@ -1,8 +1,6 @@
1
1
  import type { CommandRecord } from "@femtomc/mu-control-plane";
2
- import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
3
2
  export type ControlPlaneRunMode = "run_start" | "run_resume";
4
3
  export type ControlPlaneRunStatus = "running" | "completed" | "failed" | "cancelled";
5
- export type ControlPlaneRunWakeMode = "immediate" | "next_heartbeat";
6
4
  export type ControlPlaneRunSnapshot = {
7
5
  job_id: string;
8
6
  mode: ControlPlaneRunMode;
@@ -33,12 +31,7 @@ export type ControlPlaneRunInterruptResult = {
33
31
  reason: "not_found" | "not_running" | "missing_target" | null;
34
32
  run: ControlPlaneRunSnapshot | null;
35
33
  };
36
- export type ControlPlaneRunHeartbeatResult = {
37
- ok: boolean;
38
- reason: "not_found" | "not_running" | "missing_target" | null;
39
- run: ControlPlaneRunSnapshot | null;
40
- };
41
- export type ControlPlaneRunEventKind = "run_started" | "run_root_discovered" | "run_progress" | "run_heartbeat" | "run_interrupt_requested" | "run_completed" | "run_failed" | "run_cancelled";
34
+ export type ControlPlaneRunEventKind = "run_started" | "run_root_discovered" | "run_progress" | "run_interrupt_requested" | "run_completed" | "run_failed" | "run_cancelled";
42
35
  export type ControlPlaneRunEvent = {
43
36
  seq: number;
44
37
  ts_ms: number;
@@ -61,8 +54,6 @@ export type ControlPlaneRunSupervisorOpts = {
61
54
  argv: string[];
62
55
  cwd: string;
63
56
  }) => ControlPlaneRunProcess;
64
- heartbeatIntervalMs?: number;
65
- heartbeatScheduler?: ActivityHeartbeatScheduler;
66
57
  maxStoredLines?: number;
67
58
  maxHistory?: number;
68
59
  onEvent?: (event: ControlPlaneRunEvent) => void | Promise<void>;
@@ -112,12 +103,6 @@ export declare class ControlPlaneRunSupervisor {
112
103
  jobId?: string | null;
113
104
  rootIssueId?: string | null;
114
105
  }): ControlPlaneRunInterruptResult;
115
- heartbeat(opts: {
116
- jobId?: string | null;
117
- rootIssueId?: string | null;
118
- reason?: string | null;
119
- wakeMode?: string | null;
120
- }): ControlPlaneRunHeartbeatResult;
121
106
  startFromCommand(command: CommandRecord): Promise<ControlPlaneRunSnapshot | null>;
122
107
  stop(): Promise<void>;
123
108
  }
@@ -1,6 +1,5 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import { join, relative } from "node:path";
3
- import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
4
3
  const DEFAULT_MAX_STEPS = 20;
5
4
  const ROOT_RE = /\bRoot:\s*(mu-[a-z0-9][a-z0-9-]*)\b/i;
6
5
  const STEP_RE = /^(Step|Done)\s+\d+\/\d+\s+/;
@@ -45,13 +44,6 @@ function normalizeIssueId(value) {
45
44
  }
46
45
  return trimmed.toLowerCase();
47
46
  }
48
- function normalizeWakeMode(value) {
49
- if (typeof value !== "string") {
50
- return "immediate";
51
- }
52
- const normalized = value.trim().toLowerCase().replaceAll("-", "_");
53
- return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
54
- }
55
47
  function pushBounded(lines, line, maxLines) {
56
48
  lines.push(line);
57
49
  if (lines.length <= maxLines) {
@@ -109,9 +101,6 @@ export class ControlPlaneRunSupervisor {
109
101
  #repoRoot;
110
102
  #nowMs;
111
103
  #spawnProcess;
112
- #heartbeatIntervalMs;
113
- #heartbeatScheduler;
114
- #ownsHeartbeatScheduler;
115
104
  #maxStoredLines;
116
105
  #maxHistory;
117
106
  #onEvent;
@@ -123,9 +112,6 @@ export class ControlPlaneRunSupervisor {
123
112
  this.#repoRoot = opts.repoRoot;
124
113
  this.#nowMs = opts.nowMs ?? defaultNowMs;
125
114
  this.#spawnProcess = opts.spawnProcess ?? defaultSpawnProcess;
126
- this.#heartbeatIntervalMs = Math.max(2_000, Math.trunc(opts.heartbeatIntervalMs ?? 15_000));
127
- this.#heartbeatScheduler = opts.heartbeatScheduler ?? new ActivityHeartbeatScheduler();
128
- this.#ownsHeartbeatScheduler = !opts.heartbeatScheduler;
129
115
  this.#maxStoredLines = Math.max(50, Math.trunc(opts.maxStoredLines ?? 1_000));
130
116
  this.#maxHistory = Math.max(20, Math.trunc(opts.maxHistory ?? 200));
131
117
  this.#onEvent = opts.onEvent ?? null;
@@ -253,7 +239,6 @@ export class ControlPlaneRunSupervisor {
253
239
  stderr_lines: [],
254
240
  log_hints: new Set(),
255
241
  interrupt_requested: false,
256
- next_heartbeat_reason: null,
257
242
  hard_kill_timer: null,
258
243
  };
259
244
  this.#jobsById.set(snapshot.job_id, job);
@@ -261,34 +246,11 @@ export class ControlPlaneRunSupervisor {
261
246
  this.#jobIdByRootIssueId.set(snapshot.root_issue_id, snapshot.job_id);
262
247
  }
263
248
  this.#emit("run_started", job, `🚀 Started ${describeRun(snapshot)} (job ${snapshot.job_id}, pid ${snapshot.pid ?? "?"})`);
264
- this.#heartbeatScheduler.register({
265
- activityId: snapshot.job_id,
266
- everyMs: this.#heartbeatIntervalMs,
267
- handler: async ({ reason }) => {
268
- if (job.snapshot.status !== "running") {
269
- return { status: "skipped", reason: "not_running" };
270
- }
271
- const normalizedReason = reason?.trim();
272
- const heartbeatReason = normalizedReason && normalizedReason.length > 0 && normalizedReason !== "interval"
273
- ? normalizedReason
274
- : job.next_heartbeat_reason;
275
- if (heartbeatReason) {
276
- job.next_heartbeat_reason = null;
277
- }
278
- const elapsedSec = Math.max(0, Math.trunc((this.#nowMs() - job.snapshot.started_at_ms) / 1_000));
279
- const root = job.snapshot.root_issue_id ?? job.snapshot.job_id;
280
- const progress = job.snapshot.last_progress ? ` · ${job.snapshot.last_progress}` : "";
281
- const reasonSuffix = heartbeatReason ? ` · wake=${heartbeatReason}` : "";
282
- this.#emit("run_heartbeat", job, `⏱ ${root} running for ${elapsedSec}s${progress}${reasonSuffix}`);
283
- return { status: "ran" };
284
- },
285
- });
286
249
  void (async () => {
287
250
  const stdoutTask = consumeStreamLines(process.stdout, (line) => this.#handleLine(job, "stdout", line));
288
251
  const stderrTask = consumeStreamLines(process.stderr, (line) => this.#handleLine(job, "stderr", line));
289
252
  const exitCode = await process.exited.catch(() => -1);
290
253
  await Promise.allSettled([stdoutTask, stderrTask]);
291
- this.#heartbeatScheduler.unregister(job.snapshot.job_id);
292
254
  if (job.hard_kill_timer) {
293
255
  clearTimeout(job.hard_kill_timer);
294
256
  job.hard_kill_timer = null;
@@ -451,34 +413,6 @@ export class ControlPlaneRunSupervisor {
451
413
  this.#emit("run_interrupt_requested", job, `⚠️ Interrupt requested for ${root}.`);
452
414
  return { ok: true, reason: null, run: this.#snapshot(job) };
453
415
  }
454
- heartbeat(opts) {
455
- const target = opts.jobId?.trim() || opts.rootIssueId?.trim() || "";
456
- if (target.length === 0) {
457
- return { ok: false, reason: "missing_target", run: null };
458
- }
459
- const job = this.#resolveJob(target);
460
- if (!job) {
461
- return { ok: false, reason: "not_found", run: null };
462
- }
463
- if (job.snapshot.status !== "running") {
464
- return { ok: false, reason: "not_running", run: this.#snapshot(job) };
465
- }
466
- const reason = opts.reason?.trim() || "manual";
467
- const wakeMode = normalizeWakeMode(opts.wakeMode);
468
- if (wakeMode === "next_heartbeat") {
469
- job.next_heartbeat_reason = reason;
470
- this.#touch(job);
471
- return { ok: true, reason: null, run: this.#snapshot(job) };
472
- }
473
- if (reason !== "interval") {
474
- job.next_heartbeat_reason = null;
475
- }
476
- this.#heartbeatScheduler.requestNow(job.snapshot.job_id, {
477
- reason,
478
- coalesceMs: 0,
479
- });
480
- return { ok: true, reason: null, run: this.#snapshot(job) };
481
- }
482
416
  async startFromCommand(command) {
483
417
  switch (command.target_type) {
484
418
  case "run start": {
@@ -508,7 +442,6 @@ export class ControlPlaneRunSupervisor {
508
442
  }
509
443
  async stop() {
510
444
  for (const job of this.#jobsById.values()) {
511
- this.#heartbeatScheduler.unregister(job.snapshot.job_id);
512
445
  if (job.hard_kill_timer) {
513
446
  clearTimeout(job.hard_kill_timer);
514
447
  job.hard_kill_timer = null;
@@ -522,8 +455,5 @@ export class ControlPlaneRunSupervisor {
522
455
  }
523
456
  }
524
457
  }
525
- if (this.#ownsHeartbeatScheduler) {
526
- this.#heartbeatScheduler.stop();
527
- }
528
458
  }
529
459
  }
package/dist/server.d.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
2
2
  import type { EventEnvelope, JsonlStore } from "@femtomc/mu-core";
3
3
  import { EventLog } from "@femtomc/mu-core/node";
4
- import { ForumStore } from "@femtomc/mu-forum";
5
- import { IssueStore } from "@femtomc/mu-issue";
6
4
  import { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
7
5
  import { type MuConfig } from "./config.js";
8
6
  import type { ControlPlaneHandle, ControlPlaneSessionLifecycle } from "./control_plane_contract.js";
@@ -21,7 +19,6 @@ export type ServerOptions = {
21
19
  controlPlaneReloader?: ControlPlaneReloader;
22
20
  generationTelemetry?: GenerationTelemetryRecorder;
23
21
  operatorWakeCoalesceMs?: number;
24
- autoRunHeartbeatEveryMs?: number;
25
22
  config?: MuConfig;
26
23
  configReader?: ConfigReader;
27
24
  configWriter?: ConfigWriter;
@@ -31,8 +28,6 @@ export type ServerOptions = {
31
28
  export type ServerInstanceOptions = Omit<ServerOptions, "repoRoot" | "controlPlane" | "heartbeatScheduler" | "generationTelemetry" | "config" | "sessionLifecycle">;
32
29
  export type ServerContext = {
33
30
  repoRoot: string;
34
- issueStore: IssueStore;
35
- forumStore: ForumStore;
36
31
  eventLog: EventLog;
37
32
  eventsStore: JsonlStore<EventEnvelope>;
38
33
  };