@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 +7 -3
- package/dist/api/events.js +77 -19
- package/dist/api/forum.js +52 -18
- package/dist/api/issues.js +120 -33
- package/dist/control_plane.d.ts +5 -114
- package/dist/control_plane.js +14 -631
- package/dist/control_plane_bootstrap_helpers.d.ts +15 -0
- package/dist/control_plane_bootstrap_helpers.js +75 -0
- package/dist/control_plane_contract.d.ts +119 -0
- package/dist/control_plane_contract.js +1 -0
- package/dist/control_plane_run_outbox.d.ts +7 -0
- package/dist/control_plane_run_outbox.js +52 -0
- package/dist/control_plane_telegram_generation.d.ts +27 -0
- package/dist/control_plane_telegram_generation.js +521 -0
- package/dist/cron_request.d.ts +8 -0
- package/dist/cron_request.js +65 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/server.d.ts +8 -40
- package/dist/server.js +35 -1607
- package/dist/server_program_orchestration.d.ts +38 -0
- package/dist/server_program_orchestration.js +252 -0
- package/dist/server_routing.d.ts +30 -0
- package/dist/server_routing.js +1102 -0
- package/dist/server_runtime.d.ts +30 -0
- package/dist/server_runtime.js +43 -0
- package/dist/server_types.d.ts +3 -0
- package/dist/server_types.js +16 -0
- package/dist/session_lifecycle.d.ts +11 -0
- package/dist/session_lifecycle.js +149 -0
- package/package.json +6 -6
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
|
|
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
|
|
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
|
{
|
package/dist/api/events.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/api/issues.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
36
|
-
const
|
|
96
|
+
const body = await readJsonBody(request);
|
|
97
|
+
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
37
98
|
if (!title) {
|
|
38
|
-
return
|
|
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
|
-
|
|
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, -
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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, -
|
|
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
|
|
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
|
-
|
|
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/control_plane.d.ts
CHANGED
|
@@ -1,117 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type
|
|
3
|
-
import {
|
|
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
|
|
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 {};
|