@femtomc/mu-server 26.2.68 → 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 CHANGED
@@ -11,18 +11,19 @@ bun add @femtomc/mu-server
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
- import { createServer } from "@femtomc/mu-server";
14
+ import { composeServerRuntime, createServerFromRuntime } from "@femtomc/mu-server";
15
15
 
16
- // Create server with default options (uses current directory as repo root)
17
- const server = createServer();
16
+ const runtime = await composeServerRuntime({
17
+ repoRoot: "/path/to/repo"
18
+ });
19
+
20
+ // Optional: inspect startup capabilities
21
+ console.log(runtime.capabilities);
18
22
 
19
- // Or specify custom repo root and port
20
- const server = createServer({
21
- repoRoot: "/path/to/repo",
23
+ const server = createServerFromRuntime(runtime, {
22
24
  port: 8080
23
25
  });
24
26
 
25
- // Start the server
26
27
  Bun.serve(server);
27
28
  ```
28
29
 
@@ -102,7 +103,8 @@ Bun.serve(server);
102
103
  ### Issues
103
104
 
104
105
  - `GET /api/issues` - List issues
105
- - Query params: `?status=open&tag=bug`
106
+ - Query params: `?status=open&tag=bug&contains=crash&limit=50`
107
+ - `limit` defaults to `200` and is clamped to `<= 200`.
106
108
  - `GET /api/issues/:id` - Get issue by ID
107
109
  - `POST /api/issues` - Create new issue
108
110
  ```json
@@ -122,14 +124,17 @@ Bun.serve(server);
122
124
  ```
123
125
  - `POST /api/issues/:id/claim` - Claim issue (changes status to in_progress)
124
126
  - `GET /api/issues/ready` - Get ready issues
125
- - Query param: `?root=issue-id`
127
+ - Query params: `?root=issue-id&contains=worker&limit=20`
128
+ - `limit` defaults to `200` and is clamped to `<= 200`.
126
129
 
127
130
  ### Forum
128
131
 
129
132
  - `GET /api/forum/topics` - List forum topics
130
- - Query param: `?prefix=issue:`
133
+ - Query params: `?prefix=issue:&limit=20`
134
+ - `limit` defaults to `100` and is clamped to `<= 200`.
131
135
  - `GET /api/forum/read` - Read messages from topic
132
136
  - Query params: `?topic=issue:123&limit=50`
137
+ - `limit` defaults to `50` and is clamped to `<= 200`.
133
138
  - `POST /api/forum/post` - Post message to topic
134
139
  ```json
135
140
  {
@@ -1,3 +1,67 @@
1
+ function trimOrNull(value) {
2
+ if (value == null) {
3
+ return null;
4
+ }
5
+ const trimmed = value.trim();
6
+ return trimmed.length > 0 ? trimmed : null;
7
+ }
8
+ function previewText(value) {
9
+ if (typeof value === "string") {
10
+ return value;
11
+ }
12
+ if (value == null) {
13
+ return "";
14
+ }
15
+ try {
16
+ return JSON.stringify(value);
17
+ }
18
+ catch {
19
+ return String(value);
20
+ }
21
+ }
22
+ function includeByContains(event, contains) {
23
+ if (!contains) {
24
+ return true;
25
+ }
26
+ const needle = contains.toLowerCase();
27
+ const haystack = [
28
+ event.type ?? "",
29
+ event.source ?? "",
30
+ event.issue_id ?? "",
31
+ event.run_id ?? "",
32
+ previewText(event.payload),
33
+ ]
34
+ .join("\n")
35
+ .toLowerCase();
36
+ return haystack.includes(needle);
37
+ }
38
+ function applyEventFilters(events, filters) {
39
+ return events
40
+ .filter((event) => (filters.type ? event.type === filters.type : true))
41
+ .filter((event) => (filters.source ? event.source === filters.source : true))
42
+ .filter((event) => (filters.issueId ? event.issue_id === filters.issueId : true))
43
+ .filter((event) => (filters.runId ? event.run_id === filters.runId : true))
44
+ .filter((event) => {
45
+ if (filters.sinceMs == null) {
46
+ return true;
47
+ }
48
+ if (typeof event.ts_ms !== "number") {
49
+ return false;
50
+ }
51
+ return event.ts_ms >= filters.sinceMs;
52
+ })
53
+ .filter((event) => includeByContains(event, filters.contains));
54
+ }
55
+ function parseSinceMs(value) {
56
+ if (value == null || value.trim().length === 0) {
57
+ return null;
58
+ }
59
+ const parsed = Number.parseInt(value, 10);
60
+ if (!Number.isFinite(parsed)) {
61
+ return null;
62
+ }
63
+ return parsed;
64
+ }
1
65
  export async function eventRoutes(request, context) {
2
66
  const url = new URL(request.url);
3
67
  const path = url.pathname.replace("/api/events", "") || "/";
@@ -6,31 +70,25 @@ export async function eventRoutes(request, context) {
6
70
  return new Response("Method Not Allowed", { status: 405 });
7
71
  }
8
72
  try {
9
- const allEvents = await context.eventsStore.read();
73
+ const allEvents = (await context.eventsStore.read());
74
+ const filters = {
75
+ type: trimOrNull(url.searchParams.get("type")),
76
+ source: trimOrNull(url.searchParams.get("source")),
77
+ issueId: trimOrNull(url.searchParams.get("issue_id")),
78
+ runId: trimOrNull(url.searchParams.get("run_id")),
79
+ sinceMs: parseSinceMs(url.searchParams.get("since")),
80
+ contains: trimOrNull(url.searchParams.get("contains")),
81
+ };
10
82
  // Tail - GET /api/events/tail?n=50
11
83
  if (path === "/tail") {
12
84
  const n = Math.min(Math.max(1, parseInt(url.searchParams.get("n") ?? "50", 10) || 50), 500);
13
- return Response.json(allEvents.slice(-n));
85
+ const filtered = applyEventFilters(allEvents, filters);
86
+ return Response.json(filtered.slice(-n));
14
87
  }
15
- // Query - GET /api/events?type=...&source=...&since=...&limit=50
88
+ // Query - GET /api/events?type=...&source=...&issue_id=...&run_id=...&since=...&contains=...&limit=50
16
89
  if (path === "/") {
17
- const typeFilter = url.searchParams.get("type");
18
- const sourceFilter = url.searchParams.get("source");
19
- const sinceRaw = url.searchParams.get("since");
20
90
  const limit = Math.min(Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50), 500);
21
- let filtered = allEvents;
22
- if (typeFilter) {
23
- filtered = filtered.filter((e) => e.type === typeFilter);
24
- }
25
- if (sourceFilter) {
26
- filtered = filtered.filter((e) => e.source === sourceFilter);
27
- }
28
- if (sinceRaw) {
29
- const sinceMs = parseInt(sinceRaw, 10);
30
- if (!Number.isNaN(sinceMs)) {
31
- filtered = filtered.filter((e) => e.ts_ms >= sinceMs);
32
- }
33
- }
91
+ const filtered = applyEventFilters(allEvents, filters);
34
92
  return Response.json(filtered.slice(-limit));
35
93
  }
36
94
  return new Response("Not Found", { status: 404 });
package/dist/api/forum.js CHANGED
@@ -1,3 +1,33 @@
1
+ import { DEFAULT_FORUM_TOPICS_LIMIT, ForumStoreValidationError, normalizeForumPrefix, normalizeForumReadLimit, normalizeForumTopic, normalizeForumTopicsLimit, } from "@femtomc/mu-forum";
2
+ async function readJsonBody(request) {
3
+ let body;
4
+ try {
5
+ body = await request.json();
6
+ }
7
+ catch {
8
+ throw new ForumStoreValidationError("invalid json body");
9
+ }
10
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
11
+ throw new ForumStoreValidationError("json body must be an object");
12
+ }
13
+ return body;
14
+ }
15
+ function errorResponse(status, message) {
16
+ return new Response(JSON.stringify({ error: message }), {
17
+ status,
18
+ headers: { "Content-Type": "application/json" },
19
+ });
20
+ }
21
+ function mapForumRouteError(error) {
22
+ if (error instanceof ForumStoreValidationError) {
23
+ return errorResponse(400, error.message);
24
+ }
25
+ if (error instanceof Error && error.name === "ZodError") {
26
+ return errorResponse(400, error.message);
27
+ }
28
+ console.error("Forum API error:", error);
29
+ return errorResponse(500, error instanceof Error ? error.message : "Internal server error");
30
+ }
1
31
  export async function forumRoutes(request, context) {
2
32
  const url = new URL(request.url);
3
33
  const path = url.pathname.replace("/api/forum", "") || "/";
@@ -5,37 +35,41 @@ export async function forumRoutes(request, context) {
5
35
  try {
6
36
  // List topics - GET /api/forum/topics
7
37
  if (path === "/topics" && method === "GET") {
8
- const prefix = url.searchParams.get("prefix");
9
- const topics = await context.forumStore.topics(prefix);
38
+ const prefix = normalizeForumPrefix(url.searchParams.get("prefix"));
39
+ const limit = normalizeForumTopicsLimit(url.searchParams.get("limit"), {
40
+ defaultLimit: DEFAULT_FORUM_TOPICS_LIMIT,
41
+ });
42
+ const topics = await context.forumStore.topics(prefix, { limit });
10
43
  return Response.json(topics);
11
44
  }
12
45
  // Read messages - GET /api/forum/read
13
46
  if (path === "/read" && method === "GET") {
14
- const topic = url.searchParams.get("topic");
15
- const limit = url.searchParams.get("limit");
16
- if (!topic) {
17
- return new Response("Topic is required", { status: 400 });
18
- }
19
- const messages = await context.forumStore.read(topic, limit ? parseInt(limit, 10) : 50);
47
+ const topic = normalizeForumTopic(url.searchParams.get("topic"));
48
+ const limit = normalizeForumReadLimit(url.searchParams.get("limit"));
49
+ const messages = await context.forumStore.read(topic, limit);
20
50
  return Response.json(messages);
21
51
  }
22
52
  // Post message - POST /api/forum/post
23
53
  if (path === "/post" && method === "POST") {
24
- const body = (await request.json());
25
- const { topic, body: messageBody, author } = body;
26
- if (!topic || !messageBody) {
27
- return new Response("Topic and body are required", { status: 400 });
54
+ const body = await readJsonBody(request);
55
+ const topic = normalizeForumTopic(body.topic);
56
+ if (typeof body.body !== "string" || body.body.trim().length === 0) {
57
+ return errorResponse(400, "body is required");
58
+ }
59
+ const messageBody = body.body;
60
+ let author = "system";
61
+ if (body.author != null) {
62
+ if (typeof body.author !== "string" || body.author.trim().length === 0) {
63
+ return errorResponse(400, "author must be a non-empty string when provided");
64
+ }
65
+ author = body.author.trim();
28
66
  }
29
- const message = await context.forumStore.post(topic, messageBody, author || "system");
67
+ const message = await context.forumStore.post(topic, messageBody, author);
30
68
  return Response.json(message, { status: 201 });
31
69
  }
32
70
  return new Response("Not Found", { status: 404 });
33
71
  }
34
72
  catch (error) {
35
- console.error("Forum API error:", error);
36
- return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }), {
37
- status: 500,
38
- headers: { "Content-Type": "application/json" },
39
- });
73
+ return mapForumRouteError(error);
40
74
  }
41
75
  }
@@ -1,3 +1,58 @@
1
+ import { DEFAULT_ISSUE_QUERY_LIMIT, ISSUE_STATUS_VALUES, IssueStoreNotFoundError, IssueStoreValidationError, normalizeIssueContainsFilter, normalizeIssueQueryLimit, } from "@femtomc/mu-issue";
2
+ const ISSUE_STATUS_SET = new Set(ISSUE_STATUS_VALUES);
3
+ function normalizeIssueId(value) {
4
+ try {
5
+ return decodeURIComponent(value).trim();
6
+ }
7
+ catch (cause) {
8
+ throw new IssueStoreValidationError("invalid issue id encoding", { cause });
9
+ }
10
+ }
11
+ function normalizeIssueStatusFilter(value) {
12
+ if (value == null) {
13
+ return undefined;
14
+ }
15
+ const normalized = value.trim();
16
+ if (normalized.length === 0) {
17
+ return undefined;
18
+ }
19
+ if (!ISSUE_STATUS_SET.has(normalized)) {
20
+ throw new IssueStoreValidationError(`invalid issue status filter: ${normalized}`);
21
+ }
22
+ return normalized;
23
+ }
24
+ async function readJsonBody(request) {
25
+ let body;
26
+ try {
27
+ body = await request.json();
28
+ }
29
+ catch {
30
+ throw new IssueStoreValidationError("invalid json body");
31
+ }
32
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
33
+ throw new IssueStoreValidationError("json body must be an object");
34
+ }
35
+ return body;
36
+ }
37
+ function errorResponse(status, message) {
38
+ return new Response(JSON.stringify({ error: message }), {
39
+ status,
40
+ headers: { "Content-Type": "application/json" },
41
+ });
42
+ }
43
+ function mapIssueRouteError(error) {
44
+ if (error instanceof IssueStoreNotFoundError) {
45
+ return errorResponse(404, error.message);
46
+ }
47
+ if (error instanceof IssueStoreValidationError) {
48
+ return errorResponse(400, error.message);
49
+ }
50
+ if (error instanceof Error && error.name === "ZodError") {
51
+ return errorResponse(400, error.message);
52
+ }
53
+ console.error("Issue API error:", error);
54
+ return errorResponse(500, error instanceof Error ? error.message : "Internal server error");
55
+ }
1
56
  export async function issueRoutes(request, context) {
2
57
  const url = new URL(request.url);
3
58
  const path = url.pathname.replace("/api/issues", "") || "/";
@@ -5,37 +60,63 @@ export async function issueRoutes(request, context) {
5
60
  try {
6
61
  // List issues - GET /api/issues
7
62
  if (path === "/" && method === "GET") {
8
- const status = url.searchParams.get("status");
9
- const tag = url.searchParams.get("tag");
10
- const issues = await context.issueStore.list({
11
- status: status,
12
- tag: tag || undefined,
63
+ const status = normalizeIssueStatusFilter(url.searchParams.get("status"));
64
+ const tag = url.searchParams.get("tag")?.trim() || undefined;
65
+ const contains = normalizeIssueContainsFilter(url.searchParams.get("contains"));
66
+ const limit = normalizeIssueQueryLimit(url.searchParams.get("limit"), {
67
+ defaultLimit: DEFAULT_ISSUE_QUERY_LIMIT,
13
68
  });
69
+ const issues = await context.issueStore.list({ status, tag, contains, limit: limit ?? undefined });
14
70
  return Response.json(issues);
15
71
  }
16
72
  // Get ready issues - GET /api/issues/ready
17
73
  if (path === "/ready" && method === "GET") {
18
- const root = url.searchParams.get("root");
19
- const issues = await context.issueStore.ready(root || undefined);
74
+ const root = url.searchParams.get("root")?.trim() || undefined;
75
+ const contains = normalizeIssueContainsFilter(url.searchParams.get("contains"));
76
+ const limit = normalizeIssueQueryLimit(url.searchParams.get("limit"), {
77
+ defaultLimit: DEFAULT_ISSUE_QUERY_LIMIT,
78
+ });
79
+ const issues = await context.issueStore.ready(root, { contains, limit: limit ?? undefined });
20
80
  return Response.json(issues);
21
81
  }
22
82
  // Get single issue - GET /api/issues/:id
23
83
  if (path.startsWith("/") && method === "GET") {
24
- const id = path.slice(1);
25
- if (id) {
26
- const issue = await context.issueStore.get(id);
27
- if (!issue) {
28
- return new Response("Issue not found", { status: 404 });
29
- }
30
- return Response.json(issue);
84
+ const id = normalizeIssueId(path.slice(1));
85
+ if (id.length === 0) {
86
+ return errorResponse(400, "issue id is required");
31
87
  }
88
+ const issue = await context.issueStore.get(id);
89
+ if (!issue) {
90
+ return errorResponse(404, "issue not found");
91
+ }
92
+ return Response.json(issue);
32
93
  }
33
94
  // Create issue - POST /api/issues
34
95
  if (path === "/" && method === "POST") {
35
- const body = (await request.json());
36
- const { title, body: issueBody, tags, priority } = body;
96
+ const body = await readJsonBody(request);
97
+ const title = typeof body.title === "string" ? body.title.trim() : "";
37
98
  if (!title) {
38
- return new Response("Title is required", { status: 400 });
99
+ return errorResponse(400, "title is required");
100
+ }
101
+ const issueBody = body.body == null ? undefined : typeof body.body === "string" ? body.body : undefined;
102
+ if (body.body != null && issueBody == null) {
103
+ return errorResponse(400, "body must be a string when provided");
104
+ }
105
+ let tags;
106
+ if (body.tags != null) {
107
+ if (!Array.isArray(body.tags) || !body.tags.every((tag) => typeof tag === "string")) {
108
+ return errorResponse(400, "tags must be a string[] when provided");
109
+ }
110
+ tags = body.tags.map((tag) => tag.trim());
111
+ }
112
+ let priority;
113
+ if (body.priority != null) {
114
+ if (typeof body.priority !== "number" ||
115
+ !Number.isFinite(body.priority) ||
116
+ !Number.isInteger(body.priority)) {
117
+ return errorResponse(400, "priority must be an integer when provided");
118
+ }
119
+ priority = body.priority;
39
120
  }
40
121
  const issue = await context.issueStore.create(title, {
41
122
  body: issueBody,
@@ -46,41 +127,47 @@ export async function issueRoutes(request, context) {
46
127
  }
47
128
  // Update issue - PATCH /api/issues/:id
48
129
  if (path.startsWith("/") && method === "PATCH") {
49
- const id = path.slice(1);
50
- if (id) {
51
- const body = (await request.json());
52
- const issue = await context.issueStore.update(id, body);
53
- return Response.json(issue);
130
+ const id = normalizeIssueId(path.slice(1));
131
+ if (id.length === 0) {
132
+ return errorResponse(400, "issue id is required");
54
133
  }
134
+ const body = await readJsonBody(request);
135
+ const issue = await context.issueStore.update(id, body);
136
+ return Response.json(issue);
55
137
  }
56
138
  // Close issue - POST /api/issues/:id/close
57
139
  if (path.endsWith("/close") && method === "POST") {
58
- const id = path.slice(1, -6); // Remove leading / and trailing /close
59
- const body = (await request.json());
60
- const { outcome } = body;
140
+ const id = normalizeIssueId(path.slice(1, -"/close".length));
141
+ if (id.length === 0) {
142
+ return errorResponse(400, "issue id is required");
143
+ }
144
+ const body = await readJsonBody(request);
145
+ const outcome = typeof body.outcome === "string" ? body.outcome.trim() : "";
61
146
  if (!outcome) {
62
- return new Response("Outcome is required", { status: 400 });
147
+ return errorResponse(400, "outcome is required");
63
148
  }
64
149
  const issue = await context.issueStore.close(id, outcome);
65
150
  return Response.json(issue);
66
151
  }
67
152
  // Claim issue - POST /api/issues/:id/claim
68
153
  if (path.endsWith("/claim") && method === "POST") {
69
- const id = path.slice(1, -6); // Remove leading / and trailing /claim
154
+ const id = normalizeIssueId(path.slice(1, -"/claim".length));
155
+ if (id.length === 0) {
156
+ return errorResponse(400, "issue id is required");
157
+ }
70
158
  const success = await context.issueStore.claim(id);
71
159
  if (!success) {
72
- return new Response("Failed to claim issue", { status: 409 });
160
+ return errorResponse(409, "failed to claim issue");
73
161
  }
74
162
  const issue = await context.issueStore.get(id);
163
+ if (!issue) {
164
+ return errorResponse(404, "issue not found");
165
+ }
75
166
  return Response.json(issue);
76
167
  }
77
168
  return new Response("Not Found", { status: 404 });
78
169
  }
79
170
  catch (error) {
80
- console.error("Issue API error:", error);
81
- return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }), {
82
- status: 500,
83
- headers: { "Content-Type": "application/json" },
84
- });
171
+ return mapIssueRouteError(error);
85
172
  }
86
173
  }
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { findRepoRoot } from "@femtomc/mu-core/node";
3
- import { createServerAsync } from "./server.js";
3
+ import { composeServerRuntime, createServerFromRuntime } from "./server.js";
4
4
  const port = parseInt(Bun.env.PORT || "3000", 10);
5
5
  let repoRoot;
6
6
  try {
@@ -12,14 +12,15 @@ catch {
12
12
  }
13
13
  console.log(`Starting mu-server on port ${port}...`);
14
14
  console.log(`Repository root: ${repoRoot}`);
15
- const { serverConfig, controlPlane } = await createServerAsync({ repoRoot, port });
15
+ const runtime = await composeServerRuntime({ repoRoot });
16
+ const serverConfig = createServerFromRuntime(runtime, { port });
16
17
  let server;
17
18
  try {
18
19
  server = Bun.serve(serverConfig);
19
20
  }
20
21
  catch (err) {
21
22
  try {
22
- await controlPlane?.stop();
23
+ await runtime.controlPlane?.stop();
23
24
  }
24
25
  catch {
25
26
  // Best effort cleanup. Preserve the startup error.
@@ -27,9 +28,10 @@ catch (err) {
27
28
  throw err;
28
29
  }
29
30
  console.log(`Server running at http://localhost:${port}`);
30
- if (controlPlane && controlPlane.activeAdapters.length > 0) {
31
+ console.log(`Capabilities: lifecycle=[${runtime.capabilities.session_lifecycle_actions.join(",")}]`);
32
+ if (runtime.controlPlane && runtime.controlPlane.activeAdapters.length > 0) {
31
33
  console.log("Control plane: active");
32
- for (const a of controlPlane.activeAdapters) {
34
+ for (const a of runtime.controlPlane.activeAdapters) {
33
35
  console.log(` ${a.name.padEnd(12)} ${a.route}`);
34
36
  }
35
37
  }
@@ -38,7 +40,7 @@ else {
38
40
  console.log(`API Status: http://localhost:${port}/api/status`);
39
41
  }
40
42
  const cleanup = async () => {
41
- await controlPlane?.stop();
43
+ await runtime.controlPlane?.stop();
42
44
  server.stop();
43
45
  process.exit(0);
44
46
  };
@@ -1,117 +1,9 @@
1
- import { type MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
- import { type Channel, type CommandPipelineResult, type GenerationTelemetryRecorder, type ReloadableGenerationIdentity } from "@femtomc/mu-control-plane";
3
- import { type MuConfig } from "./config.js";
1
+ import type { MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
+ import { type GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
3
+ import type { ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, TelegramGenerationSwapHooks } from "./control_plane_contract.js";
4
4
  import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
5
- import { type ControlPlaneRunHeartbeatResult, type ControlPlaneRunInterruptResult, type ControlPlaneRunSnapshot, type ControlPlaneRunSupervisorOpts, type ControlPlaneRunTrace } from "./run_supervisor.js";
6
- export type ActiveAdapter = {
7
- name: Channel;
8
- route: string;
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
- };
45
- export type ControlPlaneHandle = {
46
- activeAdapters: ActiveAdapter[];
47
- handleWebhook(path: string, req: Request): Promise<Response | null>;
48
- reloadTelegramGeneration?(opts: {
49
- config: ControlPlaneConfig;
50
- reason: string;
51
- }): Promise<TelegramGenerationReloadResult>;
52
- listRuns?(opts?: {
53
- status?: string;
54
- limit?: number;
55
- }): Promise<ControlPlaneRunSnapshot[]>;
56
- getRun?(idOrRoot: string): Promise<ControlPlaneRunSnapshot | null>;
57
- startRun?(opts: {
58
- prompt: string;
59
- maxSteps?: number;
60
- }): Promise<ControlPlaneRunSnapshot>;
61
- resumeRun?(opts: {
62
- rootIssueId: string;
63
- maxSteps?: number;
64
- }): Promise<ControlPlaneRunSnapshot>;
65
- interruptRun?(opts: {
66
- jobId?: string | null;
67
- rootIssueId?: string | null;
68
- }): Promise<ControlPlaneRunInterruptResult>;
69
- heartbeatRun?(opts: {
70
- jobId?: string | null;
71
- rootIssueId?: string | null;
72
- reason?: string | null;
73
- wakeMode?: string | null;
74
- }): Promise<ControlPlaneRunHeartbeatResult>;
75
- traceRun?(opts: {
76
- idOrRoot: string;
77
- limit?: number;
78
- }): Promise<ControlPlaneRunTrace | null>;
79
- submitTerminalCommand?(opts: {
80
- commandText: string;
81
- repoRoot: string;
82
- requestId?: string;
83
- }): Promise<CommandPipelineResult>;
84
- stop(): Promise<void>;
85
- };
86
- export type ControlPlaneConfig = MuConfig["control_plane"];
87
- export type ControlPlaneGenerationContext = ReloadableGenerationIdentity;
88
- export type TelegramGenerationSwapHooks = {
89
- onWarmup?: (ctx: {
90
- generation: ReloadableGenerationIdentity;
91
- reason: string;
92
- }) => void | Promise<void>;
93
- onCutover?: (ctx: {
94
- from_generation: ReloadableGenerationIdentity | null;
95
- to_generation: ReloadableGenerationIdentity;
96
- reason: string;
97
- }) => void | Promise<void>;
98
- onDrain?: (ctx: {
99
- generation: ReloadableGenerationIdentity;
100
- reason: string;
101
- timeout_ms: number;
102
- }) => void | Promise<void>;
103
- };
104
- export type ControlPlaneSessionMutationAction = "reload" | "update";
105
- export type ControlPlaneSessionMutationResult = {
106
- ok: boolean;
107
- action: ControlPlaneSessionMutationAction;
108
- message: string;
109
- details?: Record<string, unknown>;
110
- };
111
- export type ControlPlaneSessionMutationHooks = {
112
- reload?: () => Promise<ControlPlaneSessionMutationResult>;
113
- update?: () => Promise<ControlPlaneSessionMutationResult>;
114
- };
5
+ import { type ControlPlaneRunSupervisorOpts } from "./run_supervisor.js";
6
+ export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, } from "./control_plane_contract.js";
115
7
  type DetectedAdapter = {
116
8
  name: "slack";
117
9
  signingSecret: string;
@@ -151,11 +43,10 @@ export type BootstrapControlPlaneOpts = {
151
43
  heartbeatScheduler?: ActivityHeartbeatScheduler;
152
44
  runSupervisorSpawnProcess?: ControlPlaneRunSupervisorOpts["spawnProcess"];
153
45
  runSupervisorHeartbeatIntervalMs?: number;
154
- sessionMutationHooks?: ControlPlaneSessionMutationHooks;
46
+ sessionLifecycle: ControlPlaneSessionLifecycle;
155
47
  generation?: ControlPlaneGenerationContext;
156
48
  telemetry?: GenerationTelemetryRecorder | null;
157
49
  telegramGenerationHooks?: TelegramGenerationSwapHooks;
158
50
  terminalEnabled?: boolean;
159
51
  };
160
52
  export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;
161
- export {};