@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
@@ -1,4 +1,4 @@
1
- import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, DiscordControlPlaneAdapter, getControlPlanePaths, SlackControlPlaneAdapter, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
1
+ import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, getControlPlanePaths, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
2
2
  import { DEFAULT_MU_CONFIG } from "./config.js";
3
3
  import { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, } from "./control_plane_contract.js";
4
4
  import { ControlPlaneRunSupervisor, } from "./run_supervisor.js";
@@ -7,6 +7,8 @@ import { buildMessagingOperatorRuntime, createOutboxDrainLoop } from "./control_
7
7
  import { ControlPlaneRunQueueCoordinator } from "./control_plane_run_queue_coordinator.js";
8
8
  import { enqueueRunEventOutbox } from "./control_plane_run_outbox.js";
9
9
  import { buildWakeOutboundEnvelope, resolveWakeFanoutCapability, wakeDeliveryMetadataFromOutboxRecord, wakeDispatchReasonCode, wakeFanoutDedupeKey, } from "./control_plane_wake_delivery.js";
10
+ import { createStaticAdaptersFromDetected, detectAdapters, } from "./control_plane_adapter_registry.js";
11
+ import { OutboundDeliveryRouter } from "./outbound_delivery_router.js";
10
12
  import { TelegramAdapterGenerationManager } from "./control_plane_telegram_generation.js";
11
13
  function generationTags(generation, component) {
12
14
  return {
@@ -35,27 +37,7 @@ function normalizeIssueId(value) {
35
37
  }
36
38
  return trimmed.toLowerCase();
37
39
  }
38
- export function detectAdapters(config) {
39
- const adapters = [];
40
- const slackSecret = config.adapters.slack.signing_secret;
41
- if (slackSecret) {
42
- adapters.push({ name: "slack", signingSecret: slackSecret });
43
- }
44
- const discordSecret = config.adapters.discord.signing_secret;
45
- if (discordSecret) {
46
- adapters.push({ name: "discord", signingSecret: discordSecret });
47
- }
48
- const telegramSecret = config.adapters.telegram.webhook_secret;
49
- if (telegramSecret) {
50
- adapters.push({
51
- name: "telegram",
52
- webhookSecret: telegramSecret,
53
- botToken: config.adapters.telegram.bot_token,
54
- botUsername: config.adapters.telegram.bot_username,
55
- });
56
- }
57
- return adapters;
58
- }
40
+ export { detectAdapters };
59
41
  /**
60
42
  * Telegram supports a markdown dialect that uses single markers for emphasis.
61
43
  * Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
@@ -145,7 +127,7 @@ export async function bootstrapControlPlane(opts) {
145
127
  let runSupervisor = null;
146
128
  let outboxDrainLoop = null;
147
129
  let wakeDeliveryObserver = opts.wakeDeliveryObserver ?? null;
148
- const outboundDeliveryChannels = new Set(["telegram"]);
130
+ const outboundDeliveryChannels = new Set();
149
131
  const adapterMap = new Map();
150
132
  try {
151
133
  await runtime.start();
@@ -170,8 +152,6 @@ export async function bootstrapControlPlane(opts) {
170
152
  });
171
153
  runSupervisor = new ControlPlaneRunSupervisor({
172
154
  repoRoot: opts.repoRoot,
173
- heartbeatScheduler: opts.heartbeatScheduler,
174
- heartbeatIntervalMs: opts.runSupervisorHeartbeatIntervalMs,
175
155
  spawnProcess: opts.runSupervisorSpawnProcess,
176
156
  onEvent: async (event) => {
177
157
  await runQueueCoordinator.onRunEvent(event);
@@ -384,21 +364,11 @@ export async function bootstrapControlPlane(opts) {
384
364
  hooks: opts.telegramGenerationHooks,
385
365
  });
386
366
  await telegramManager.initialize();
387
- for (const d of detected) {
388
- if (d.name === "telegram") {
389
- continue;
390
- }
391
- const adapter = d.name === "slack"
392
- ? new SlackControlPlaneAdapter({
393
- pipeline,
394
- outbox,
395
- signingSecret: d.signingSecret,
396
- })
397
- : new DiscordControlPlaneAdapter({
398
- pipeline,
399
- outbox,
400
- signingSecret: d.signingSecret,
401
- });
367
+ for (const adapter of createStaticAdaptersFromDetected({
368
+ detected,
369
+ pipeline,
370
+ outbox,
371
+ })) {
402
372
  const route = adapter.spec.route;
403
373
  if (adapterMap.has(route)) {
404
374
  throw new Error(`duplicate control-plane webhook route: ${route}`);
@@ -445,6 +415,52 @@ export async function bootstrapControlPlane(opts) {
445
415
  },
446
416
  isActive: () => telegramManager.hasActiveGeneration(),
447
417
  });
418
+ const deliveryRouter = new OutboundDeliveryRouter([
419
+ {
420
+ channel: "telegram",
421
+ deliver: async (record) => {
422
+ const telegramBotToken = telegramManager.activeBotToken();
423
+ if (!telegramBotToken) {
424
+ return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
425
+ }
426
+ const richPayload = buildTelegramSendMessagePayload({
427
+ chatId: record.envelope.channel_conversation_id,
428
+ text: record.envelope.body,
429
+ richFormatting: true,
430
+ });
431
+ let res = await postTelegramMessage(telegramBotToken, richPayload);
432
+ // Fallback: if Telegram rejects markdown entities, retry as plain text.
433
+ if (!res.ok && res.status === 400 && richPayload.parse_mode) {
434
+ const plainPayload = buildTelegramSendMessagePayload({
435
+ chatId: record.envelope.channel_conversation_id,
436
+ text: record.envelope.body,
437
+ richFormatting: false,
438
+ });
439
+ res = await postTelegramMessage(telegramBotToken, plainPayload);
440
+ }
441
+ if (res.ok) {
442
+ return { kind: "delivered" };
443
+ }
444
+ const responseBody = await res.text().catch(() => "");
445
+ if (res.status === 429 || res.status >= 500) {
446
+ const retryAfter = res.headers.get("retry-after");
447
+ const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
448
+ return {
449
+ kind: "retry",
450
+ error: `telegram sendMessage ${res.status}: ${responseBody}`,
451
+ retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
452
+ };
453
+ }
454
+ return {
455
+ kind: "retry",
456
+ error: `telegram sendMessage ${res.status}: ${responseBody}`,
457
+ };
458
+ },
459
+ },
460
+ ]);
461
+ for (const channel of deliveryRouter.supportedChannels()) {
462
+ outboundDeliveryChannels.add(channel);
463
+ }
448
464
  const notifyOperators = async (notifyOpts) => {
449
465
  if (!pipeline) {
450
466
  return emptyNotifyOperatorsResult();
@@ -544,46 +560,7 @@ export async function bootstrapControlPlane(opts) {
544
560
  return result;
545
561
  };
546
562
  const deliver = async (record) => {
547
- const { envelope } = record;
548
- if (envelope.channel === "telegram") {
549
- const telegramBotToken = telegramManager.activeBotToken();
550
- if (!telegramBotToken) {
551
- return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
552
- }
553
- const richPayload = buildTelegramSendMessagePayload({
554
- chatId: envelope.channel_conversation_id,
555
- text: envelope.body,
556
- richFormatting: true,
557
- });
558
- let res = await postTelegramMessage(telegramBotToken, richPayload);
559
- // Fallback: if Telegram rejects markdown entities, retry as plain text.
560
- if (!res.ok && res.status === 400 && richPayload.parse_mode) {
561
- const plainPayload = buildTelegramSendMessagePayload({
562
- chatId: envelope.channel_conversation_id,
563
- text: envelope.body,
564
- richFormatting: false,
565
- });
566
- res = await postTelegramMessage(telegramBotToken, plainPayload);
567
- }
568
- if (res.ok) {
569
- return { kind: "delivered" };
570
- }
571
- const responseBody = await res.text().catch(() => "");
572
- if (res.status === 429 || res.status >= 500) {
573
- const retryAfter = res.headers.get("retry-after");
574
- const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
575
- return {
576
- kind: "retry",
577
- error: `telegram sendMessage ${res.status}: ${responseBody}`,
578
- retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
579
- };
580
- }
581
- return {
582
- kind: "retry",
583
- error: `telegram sendMessage ${res.status}: ${responseBody}`,
584
- };
585
- }
586
- return undefined;
563
+ return await deliveryRouter.deliver(record);
587
564
  };
588
565
  const outboxDrain = createOutboxDrainLoop({
589
566
  outbox,
@@ -714,9 +691,6 @@ export async function bootstrapControlPlane(opts) {
714
691
  async interruptRun(interruptOpts) {
715
692
  return await runQueueCoordinator.interruptQueuedRun(interruptOpts);
716
693
  },
717
- async heartbeatRun(heartbeatOpts) {
718
- return await runQueueCoordinator.heartbeatQueuedRun(heartbeatOpts);
719
- },
720
694
  async traceRun(traceOpts) {
721
695
  return (await runSupervisor?.trace(traceOpts.idOrRoot, { limit: traceOpts.limit })) ?? null;
722
696
  },
@@ -0,0 +1,19 @@
1
+ import { type ControlPlaneAdapter, type ControlPlaneCommandPipeline, type ControlPlaneOutbox } from "@femtomc/mu-control-plane";
2
+ import type { ControlPlaneConfig } from "./control_plane_contract.js";
3
+ export type DetectedStaticAdapter = {
4
+ name: "slack" | "discord" | "neovim";
5
+ secret: string;
6
+ };
7
+ export type DetectedTelegramAdapter = {
8
+ name: "telegram";
9
+ webhookSecret: string;
10
+ botToken: string | null;
11
+ botUsername: string | null;
12
+ };
13
+ export type DetectedAdapter = DetectedStaticAdapter | DetectedTelegramAdapter;
14
+ export declare function detectAdapters(config: ControlPlaneConfig): DetectedAdapter[];
15
+ export declare function createStaticAdaptersFromDetected(opts: {
16
+ detected: readonly DetectedAdapter[];
17
+ pipeline: ControlPlaneCommandPipeline;
18
+ outbox: ControlPlaneOutbox;
19
+ }): ControlPlaneAdapter[];
@@ -0,0 +1,74 @@
1
+ import { DiscordControlPlaneAdapter, NeovimControlPlaneAdapter, SlackControlPlaneAdapter, } from "@femtomc/mu-control-plane";
2
+ const STATIC_ADAPTER_MODULES = [
3
+ {
4
+ name: "slack",
5
+ detectSecret: (config) => config.adapters.slack.signing_secret,
6
+ create: (opts) => new SlackControlPlaneAdapter({
7
+ pipeline: opts.pipeline,
8
+ outbox: opts.outbox,
9
+ signingSecret: opts.secret,
10
+ }),
11
+ },
12
+ {
13
+ name: "discord",
14
+ detectSecret: (config) => config.adapters.discord.signing_secret,
15
+ create: (opts) => new DiscordControlPlaneAdapter({
16
+ pipeline: opts.pipeline,
17
+ outbox: opts.outbox,
18
+ signingSecret: opts.secret,
19
+ }),
20
+ },
21
+ {
22
+ name: "neovim",
23
+ detectSecret: (config) => config.adapters.neovim.shared_secret,
24
+ create: (opts) => new NeovimControlPlaneAdapter({
25
+ pipeline: opts.pipeline,
26
+ sharedSecret: opts.secret,
27
+ }),
28
+ },
29
+ ];
30
+ const STATIC_ADAPTER_BY_NAME = new Map(STATIC_ADAPTER_MODULES.map((module) => [module.name, module]));
31
+ function isStaticAdapter(adapter) {
32
+ return adapter.name !== "telegram";
33
+ }
34
+ export function detectAdapters(config) {
35
+ const detected = [];
36
+ for (const module of STATIC_ADAPTER_MODULES) {
37
+ const secret = module.detectSecret(config);
38
+ if (!secret) {
39
+ continue;
40
+ }
41
+ detected.push({
42
+ name: module.name,
43
+ secret,
44
+ });
45
+ }
46
+ const telegramSecret = config.adapters.telegram.webhook_secret;
47
+ if (telegramSecret) {
48
+ detected.push({
49
+ name: "telegram",
50
+ webhookSecret: telegramSecret,
51
+ botToken: config.adapters.telegram.bot_token,
52
+ botUsername: config.adapters.telegram.bot_username,
53
+ });
54
+ }
55
+ return detected;
56
+ }
57
+ export function createStaticAdaptersFromDetected(opts) {
58
+ const adapters = [];
59
+ for (const detected of opts.detected) {
60
+ if (!isStaticAdapter(detected)) {
61
+ continue;
62
+ }
63
+ const module = STATIC_ADAPTER_BY_NAME.get(detected.name);
64
+ if (!module) {
65
+ throw new Error(`missing static adapter module: ${detected.name}`);
66
+ }
67
+ adapters.push(module.create({
68
+ pipeline: opts.pipeline,
69
+ outbox: opts.outbox,
70
+ secret: detected.secret,
71
+ }));
72
+ }
73
+ return adapters;
74
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Channel, CommandPipelineResult, ReloadableGenerationIdentity } from "@femtomc/mu-control-plane";
2
- import type { ControlPlaneRunHeartbeatResult, ControlPlaneRunInterruptResult, ControlPlaneRunSnapshot, ControlPlaneRunTrace } from "./run_supervisor.js";
2
+ import type { ControlPlaneRunInterruptResult, ControlPlaneRunSnapshot, ControlPlaneRunTrace } from "./run_supervisor.js";
3
3
  import type { MuConfig } from "./config.js";
4
4
  /**
5
5
  * Boundary contracts for server/control-plane composition.
@@ -154,12 +154,6 @@ export type ControlPlaneHandle = {
154
154
  jobId?: string | null;
155
155
  rootIssueId?: string | null;
156
156
  }): Promise<ControlPlaneRunInterruptResult>;
157
- heartbeatRun?(opts: {
158
- jobId?: string | null;
159
- rootIssueId?: string | null;
160
- reason?: string | null;
161
- wakeMode?: string | null;
162
- }): Promise<ControlPlaneRunHeartbeatResult>;
163
157
  traceRun?(opts: {
164
158
  idOrRoot: string;
165
159
  limit?: number;
@@ -1,7 +1,7 @@
1
1
  import type { CommandRecord } from "@femtomc/mu-control-plane";
2
2
  import type { InterRootQueuePolicy } from "./control_plane_contract.js";
3
3
  import { DurableRunQueue } from "./run_queue.js";
4
- import type { ControlPlaneRunEvent, ControlPlaneRunHeartbeatResult, ControlPlaneRunInterruptResult, ControlPlaneRunSnapshot, ControlPlaneRunSupervisor } from "./run_supervisor.js";
4
+ import type { ControlPlaneRunEvent, ControlPlaneRunInterruptResult, ControlPlaneRunSnapshot, ControlPlaneRunSupervisor } from "./run_supervisor.js";
5
5
  export type QueueLaunchOpts = {
6
6
  mode: "run_start" | "run_resume";
7
7
  prompt?: string;
@@ -37,12 +37,6 @@ export declare class ControlPlaneRunQueueCoordinator {
37
37
  jobId?: string | null;
38
38
  rootIssueId?: string | null;
39
39
  }): Promise<ControlPlaneRunInterruptResult>;
40
- heartbeatQueuedRun(opts: {
41
- jobId?: string | null;
42
- rootIssueId?: string | null;
43
- reason?: string | null;
44
- wakeMode?: string | null;
45
- }): Promise<ControlPlaneRunHeartbeatResult>;
46
40
  onRunEvent(event: ControlPlaneRunEvent): Promise<void>;
47
41
  stop(): void;
48
42
  }
@@ -20,12 +20,6 @@ function normalizeIssueId(value) {
20
20
  }
21
21
  return trimmed.toLowerCase();
22
22
  }
23
- function isTerminalQueueState(state) {
24
- return state === "done" || state === "failed" || state === "cancelled";
25
- }
26
- function isInFlightQueueState(state) {
27
- return state === "active" || state === "waiting_review" || state === "refining";
28
- }
29
23
  /**
30
24
  * Queue/reconcile adapter for control-plane run lifecycle operations.
31
25
  *
@@ -251,60 +245,8 @@ export class ControlPlaneRunQueueCoordinator {
251
245
  run: runSnapshotFromQueueSnapshot(queued),
252
246
  };
253
247
  }
254
- async heartbeatQueuedRun(opts) {
255
- const runSupervisor = this.#getRunSupervisor();
256
- const result = runSupervisor?.heartbeat(opts) ?? { ok: false, reason: "not_found", run: null };
257
- let syncedQueue = null;
258
- if (result.run) {
259
- syncedQueue = await this.#runQueue
260
- .applyRunSnapshot({
261
- run: result.run,
262
- operationId: `heartbeat-sync:${result.run.job_id}:${result.run.updated_at_ms}`,
263
- createIfMissing: true,
264
- })
265
- .catch(() => {
266
- // Best effort only.
267
- return null;
268
- });
269
- }
270
- if (result.ok || result.reason === "missing_target") {
271
- if (syncedQueue && isTerminalQueueState(syncedQueue.state)) {
272
- await this.scheduleReconcile(`heartbeat-terminal:${syncedQueue.queue_id}`);
273
- }
274
- return result;
275
- }
276
- const target = opts.jobId?.trim() || opts.rootIssueId?.trim() || "";
277
- if (!target) {
278
- return result;
279
- }
280
- const queued = await this.#runQueue.get(target);
281
- if (!queued) {
282
- return result;
283
- }
284
- if (result.reason === "not_found" && queued.job_id && isInFlightQueueState(queued.state)) {
285
- const failed = await this.#runQueue.transition({
286
- queueId: queued.queue_id,
287
- toState: "failed",
288
- operationId: `heartbeat-fallback:not-found:${queued.queue_id}`,
289
- });
290
- await this.scheduleReconcile(`heartbeat-fallback:not-found:${queued.queue_id}`);
291
- return {
292
- ok: false,
293
- reason: "not_running",
294
- run: runSnapshotFromQueueSnapshot(failed),
295
- };
296
- }
297
- if ((syncedQueue && isTerminalQueueState(syncedQueue.state)) || (result.reason === "not_running" && isInFlightQueueState(queued.state))) {
298
- await this.scheduleReconcile(`heartbeat-wake:${queued.queue_id}:${result.reason ?? "unknown"}`);
299
- }
300
- return {
301
- ok: false,
302
- reason: "not_running",
303
- run: runSnapshotFromQueueSnapshot(queued),
304
- };
305
- }
306
248
  async onRunEvent(event) {
307
- const queueEventSnapshot = await this.#runQueue
249
+ await this.#runQueue
308
250
  .applyRunSnapshot({
309
251
  run: event.run,
310
252
  operationId: `run-event:${event.seq}:${event.kind}`,
@@ -314,9 +256,6 @@ export class ControlPlaneRunQueueCoordinator {
314
256
  // Best effort queue reconciliation from runtime events.
315
257
  return null;
316
258
  });
317
- if (event.kind === "run_heartbeat" && queueEventSnapshot && isInFlightQueueState(queueEventSnapshot.state)) {
318
- await this.scheduleReconcile(`event-wake:run_heartbeat:${queueEventSnapshot.queue_id}:${event.seq}`);
319
- }
320
259
  if (event.kind === "run_completed" || event.kind === "run_failed" || event.kind === "run_cancelled") {
321
260
  await this.scheduleReconcile(`terminal:${event.kind}:${event.run.job_id}`);
322
261
  }
@@ -10,6 +10,7 @@ function controlPlaneNonTelegramFingerprint(config) {
10
10
  adapters: {
11
11
  slack: config.adapters.slack,
12
12
  discord: config.adapters.discord,
13
+ neovim: config.adapters.neovim,
13
14
  },
14
15
  operator: config.operator,
15
16
  });
@@ -15,6 +15,7 @@ function normalizeChannel(value) {
15
15
  case "slack":
16
16
  case "discord":
17
17
  case "telegram":
18
+ case "neovim":
18
19
  case "terminal":
19
20
  return value;
20
21
  default:
@@ -2,15 +2,6 @@ import type { JsonlStore } from "@femtomc/mu-core";
2
2
  import { type CronProgramSchedule } from "./cron_schedule.js";
3
3
  import { CronTimerRegistry } from "./cron_timer.js";
4
4
  import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
5
- export type CronProgramTarget = {
6
- kind: "run";
7
- job_id: string | null;
8
- root_issue_id: string | null;
9
- } | {
10
- kind: "activity";
11
- activity_id: string;
12
- };
13
- export type CronProgramWakeMode = "immediate" | "next_heartbeat";
14
5
  export type CronProgramSnapshot = {
15
6
  v: 1;
16
7
  program_id: string;
@@ -18,14 +9,12 @@ export type CronProgramSnapshot = {
18
9
  enabled: boolean;
19
10
  schedule: CronProgramSchedule;
20
11
  reason: string;
21
- wake_mode: CronProgramWakeMode;
22
- target: CronProgramTarget;
23
12
  metadata: Record<string, unknown>;
24
13
  created_at_ms: number;
25
14
  updated_at_ms: number;
26
15
  next_run_at_ms: number | null;
27
16
  last_triggered_at_ms: number | null;
28
- last_result: "ok" | "not_found" | "not_running" | "failed" | null;
17
+ last_result: "ok" | "coalesced" | "failed" | null;
29
18
  last_error: string | null;
30
19
  };
31
20
  export type CronProgramLifecycleAction = "created" | "updated" | "deleted" | "scheduled" | "disabled" | "oneshot_completed";
@@ -40,13 +29,13 @@ export type CronProgramTickEvent = {
40
29
  ts_ms: number;
41
30
  program_id: string;
42
31
  message: string;
43
- status: "ok" | "not_found" | "not_running" | "failed";
32
+ status: "ok" | "coalesced" | "failed";
44
33
  reason: string | null;
45
34
  program: CronProgramSnapshot;
46
35
  };
47
36
  export type CronProgramOperationResult = {
48
37
  ok: boolean;
49
- reason: "not_found" | "missing_target" | "invalid_target" | "invalid_schedule" | "not_running" | "failed" | null;
38
+ reason: "not_found" | "missing_target" | "invalid_schedule" | "not_running" | "failed" | null;
50
39
  program: CronProgramSnapshot | null;
51
40
  };
52
41
  export type CronProgramStatusSnapshot = {
@@ -58,28 +47,30 @@ export type CronProgramStatusSnapshot = {
58
47
  due_at_ms: number;
59
48
  }>;
60
49
  };
50
+ export type CronProgramDispatchResult = {
51
+ status: "ok";
52
+ reason?: string | null;
53
+ } | {
54
+ status: "coalesced";
55
+ reason?: string | null;
56
+ } | {
57
+ status: "failed";
58
+ reason: string;
59
+ };
61
60
  export type CronProgramRegistryOpts = {
62
61
  repoRoot: string;
63
62
  heartbeatScheduler: ActivityHeartbeatScheduler;
64
63
  nowMs?: () => number;
65
64
  timer?: CronTimerRegistry;
66
65
  store?: JsonlStore<CronProgramSnapshot>;
67
- runHeartbeat: (opts: {
68
- jobId?: string | null;
69
- rootIssueId?: string | null;
70
- reason?: string | null;
71
- wakeMode?: CronProgramWakeMode;
72
- }) => Promise<{
73
- ok: boolean;
74
- reason: "not_found" | "not_running" | "missing_target" | null;
75
- }>;
76
- activityHeartbeat: (opts: {
77
- activityId?: string | null;
78
- reason?: string | null;
79
- }) => Promise<{
80
- ok: boolean;
81
- reason: "not_found" | "not_running" | "missing_target" | null;
82
- }>;
66
+ dispatchWake: (opts: {
67
+ programId: string;
68
+ title: string;
69
+ reason: string;
70
+ metadata: Record<string, unknown>;
71
+ triggeredAtMs: number;
72
+ schedule: CronProgramSchedule;
73
+ }) => Promise<CronProgramDispatchResult>;
83
74
  onTickEvent?: (event: CronProgramTickEvent) => void | Promise<void>;
84
75
  onLifecycleEvent?: (event: CronProgramLifecycleEvent) => void | Promise<void>;
85
76
  };
@@ -88,7 +79,6 @@ export declare class CronProgramRegistry {
88
79
  constructor(opts: CronProgramRegistryOpts);
89
80
  list(opts?: {
90
81
  enabled?: boolean;
91
- targetKind?: "run" | "activity";
92
82
  scheduleKind?: "at" | "every" | "cron";
93
83
  limit?: number;
94
84
  }): Promise<CronProgramSnapshot[]>;
@@ -96,10 +86,8 @@ export declare class CronProgramRegistry {
96
86
  get(programId: string): Promise<CronProgramSnapshot | null>;
97
87
  create(opts: {
98
88
  title: string;
99
- target: CronProgramTarget;
100
89
  schedule: unknown;
101
90
  reason?: string;
102
- wakeMode?: CronProgramWakeMode;
103
91
  enabled?: boolean;
104
92
  metadata?: Record<string, unknown>;
105
93
  }): Promise<CronProgramSnapshot>;
@@ -107,9 +95,7 @@ export declare class CronProgramRegistry {
107
95
  programId: string;
108
96
  title?: string;
109
97
  reason?: string;
110
- wakeMode?: CronProgramWakeMode;
111
98
  enabled?: boolean;
112
- target?: CronProgramTarget;
113
99
  schedule?: unknown;
114
100
  metadata?: Record<string, unknown>;
115
101
  }): Promise<CronProgramOperationResult>;