@femtomc/mu-server 26.2.54 → 26.2.56

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 CHANGED
@@ -34,7 +34,7 @@ Bun.serve(server);
34
34
 
35
35
  ### Status
36
36
 
37
- - `GET /api/status` - Returns repository status
37
+ - `GET /api/status` - Returns repository + control-plane runtime status
38
38
  ```json
39
39
  {
40
40
  "repo_root": "/path/to/repo",
@@ -43,7 +43,32 @@ Bun.serve(server);
43
43
  "control_plane": {
44
44
  "active": true,
45
45
  "adapters": ["slack"],
46
- "routes": [{ "name": "slack", "route": "/webhooks/slack" }]
46
+ "routes": [{ "name": "slack", "route": "/webhooks/slack" }],
47
+ "generation": {
48
+ "supervisor_id": "control-plane",
49
+ "active_generation": { "generation_id": "control-plane-gen-3", "generation_seq": 3 },
50
+ "pending_reload": null,
51
+ "last_reload": {
52
+ "attempt_id": "control-plane-reload-4",
53
+ "reason": "mu_setup_apply_slack",
54
+ "state": "completed",
55
+ "requested_at_ms": 0,
56
+ "swapped_at_ms": 0,
57
+ "finished_at_ms": 0,
58
+ "from_generation": { "generation_id": "control-plane-gen-2", "generation_seq": 2 },
59
+ "to_generation": { "generation_id": "control-plane-gen-3", "generation_seq": 3 }
60
+ }
61
+ },
62
+ "observability": {
63
+ "counters": {
64
+ "reload_success_total": 4,
65
+ "reload_failure_total": 0,
66
+ "reload_drain_duration_ms_total": 73,
67
+ "reload_drain_duration_samples_total": 4,
68
+ "duplicate_signal_total": 0,
69
+ "drop_signal_total": 0
70
+ }
71
+ }
47
72
  }
48
73
  }
49
74
  ```
@@ -64,12 +89,15 @@ Bun.serve(server);
64
89
  }
65
90
  }
66
91
  ```
67
- - `POST /api/control-plane/reload` - Re-bootstrap control-plane adapters in-process
68
- - Re-reads current config from `.mu/config.json` and re-mounts adapters without restarting
92
+ - `POST /api/control-plane/reload` - Trigger generation-scoped control-plane hot reload
93
+ - Re-reads current config from `.mu/config.json` and executes warmup/cutover/drain/rollback flow
94
+ - Coalesces concurrent requests onto a single in-flight attempt
69
95
  - Body (optional):
70
96
  ```json
71
97
  { "reason": "mu_setup_apply" }
72
98
  ```
99
+ - Response includes generation metadata and, when telegram generation handling runs, `telegram_generation` lifecycle detail.
100
+ - `POST /api/control-plane/rollback` - Explicit rollback trigger (same pipeline, reason=`rollback`)
73
101
 
74
102
  ### Issues
75
103
 
@@ -169,6 +197,7 @@ The server uses:
169
197
  - IssueStore and ForumStore from mu packages
170
198
  - Bun's built-in HTTP server
171
199
  - Simple REST-style JSON API
200
+ - Generation-supervised control-plane hot reload lifecycle (see `docs/adr-0001-control-plane-hot-reload.md`)
172
201
 
173
202
  All data is persisted to `.mu/` directory:
174
203
  - `.mu/issues.jsonl` - Issue data
@@ -1,15 +1,54 @@
1
1
  import { type MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
- import { type Channel } from "@femtomc/mu-control-plane";
2
+ import { type Channel, type GenerationTelemetryRecorder, type ReloadableGenerationIdentity } from "@femtomc/mu-control-plane";
3
3
  import { type MuConfig } from "./config.js";
4
- import { type ControlPlaneRunHeartbeatResult, type ControlPlaneRunInterruptResult, type ControlPlaneRunSnapshot, type ControlPlaneRunTrace } from "./run_supervisor.js";
5
4
  import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
5
+ import { type ControlPlaneRunHeartbeatResult, type ControlPlaneRunInterruptResult, type ControlPlaneRunSnapshot, type ControlPlaneRunTrace } from "./run_supervisor.js";
6
6
  export type ActiveAdapter = {
7
7
  name: Channel;
8
8
  route: string;
9
9
  };
10
+ export type TelegramGenerationRollbackTrigger = "manual" | "warmup_failed" | "health_gate_failed" | "cutover_failed" | "post_cutover_health_failed" | "rollback_unavailable" | "rollback_failed";
11
+ export type TelegramGenerationReloadResult = {
12
+ handled: boolean;
13
+ ok: boolean;
14
+ reason: string;
15
+ route: string;
16
+ from_generation: ReloadableGenerationIdentity | null;
17
+ to_generation: ReloadableGenerationIdentity | null;
18
+ active_generation: ReloadableGenerationIdentity | null;
19
+ warmup: {
20
+ ok: boolean;
21
+ elapsed_ms: number;
22
+ error?: string;
23
+ } | null;
24
+ cutover: {
25
+ ok: boolean;
26
+ elapsed_ms: number;
27
+ error?: string;
28
+ } | null;
29
+ drain: {
30
+ ok: boolean;
31
+ elapsed_ms: number;
32
+ timed_out: boolean;
33
+ forced_stop: boolean;
34
+ error?: string;
35
+ } | null;
36
+ rollback: {
37
+ requested: boolean;
38
+ trigger: TelegramGenerationRollbackTrigger | null;
39
+ attempted: boolean;
40
+ ok: boolean;
41
+ error?: string;
42
+ };
43
+ error?: string;
44
+ };
10
45
  export type ControlPlaneHandle = {
11
46
  activeAdapters: ActiveAdapter[];
12
47
  handleWebhook(path: string, req: Request): Promise<Response | null>;
48
+ reloadTelegramGeneration?(opts: {
49
+ config: ControlPlaneConfig;
50
+ reason: string;
51
+ }): Promise<TelegramGenerationReloadResult>;
13
52
  listRuns?(opts?: {
14
53
  status?: string;
15
54
  limit?: number;
@@ -39,6 +78,23 @@ export type ControlPlaneHandle = {
39
78
  stop(): Promise<void>;
40
79
  };
41
80
  export type ControlPlaneConfig = MuConfig["control_plane"];
81
+ export type ControlPlaneGenerationContext = ReloadableGenerationIdentity;
82
+ export type TelegramGenerationSwapHooks = {
83
+ onWarmup?: (ctx: {
84
+ generation: ReloadableGenerationIdentity;
85
+ reason: string;
86
+ }) => void | Promise<void>;
87
+ onCutover?: (ctx: {
88
+ from_generation: ReloadableGenerationIdentity | null;
89
+ to_generation: ReloadableGenerationIdentity;
90
+ reason: string;
91
+ }) => void | Promise<void>;
92
+ onDrain?: (ctx: {
93
+ generation: ReloadableGenerationIdentity;
94
+ reason: string;
95
+ timeout_ms: number;
96
+ }) => void | Promise<void>;
97
+ };
42
98
  type DetectedAdapter = {
43
99
  name: "slack";
44
100
  signingSecret: string;
@@ -76,6 +132,9 @@ export type BootstrapControlPlaneOpts = {
76
132
  operatorRuntime?: MessagingOperatorRuntime | null;
77
133
  operatorBackend?: MessagingOperatorBackend;
78
134
  heartbeatScheduler?: ActivityHeartbeatScheduler;
135
+ generation?: ControlPlaneGenerationContext;
136
+ telemetry?: GenerationTelemetryRecorder | null;
137
+ telegramGenerationHooks?: TelegramGenerationSwapHooks;
79
138
  };
80
139
  export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;
81
140
  export {};