@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 CHANGED
@@ -103,7 +103,8 @@ Bun.serve(server);
103
103
  ### Issues
104
104
 
105
105
  - `GET /api/issues` - List issues
106
- - 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`.
107
108
  - `GET /api/issues/:id` - Get issue by ID
108
109
  - `POST /api/issues` - Create new issue
109
110
  ```json
@@ -123,14 +124,17 @@ Bun.serve(server);
123
124
  ```
124
125
  - `POST /api/issues/:id/claim` - Claim issue (changes status to in_progress)
125
126
  - `GET /api/issues/ready` - Get ready issues
126
- - 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`.
127
129
 
128
130
  ### Forum
129
131
 
130
132
  - `GET /api/forum/topics` - List forum topics
131
- - Query param: `?prefix=issue:`
133
+ - Query params: `?prefix=issue:&limit=20`
134
+ - `limit` defaults to `100` and is clamped to `<= 200`.
132
135
  - `GET /api/forum/read` - Read messages from topic
133
136
  - Query params: `?topic=issue:123&limit=50`
137
+ - `limit` defaults to `50` and is clamped to `<= 200`.
134
138
  - `POST /api/forum/post` - Post message to topic
135
139
  ```json
136
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
  }
@@ -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 ControlPlaneSessionLifecycle = {
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;
@@ -158,4 +50,3 @@ export type BootstrapControlPlaneOpts = {
158
50
  terminalEnabled?: boolean;
159
51
  };
160
52
  export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;
161
- export {};