@femtomc/mu-server 26.2.72 → 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.
- package/README.md +54 -49
- package/dist/api/control_plane.js +56 -0
- package/dist/api/cron.js +2 -23
- package/dist/api/heartbeats.js +1 -66
- package/dist/api/identities.js +3 -2
- package/dist/api/runs.js +0 -83
- package/dist/api/session_flash.d.ts +60 -0
- package/dist/api/session_flash.js +326 -0
- package/dist/api/session_turn.d.ts +38 -0
- package/dist/api/session_turn.js +423 -0
- package/dist/config.d.ts +9 -4
- package/dist/config.js +24 -24
- package/dist/control_plane.d.ts +2 -16
- package/dist/control_plane.js +57 -83
- package/dist/control_plane_adapter_registry.d.ts +19 -0
- package/dist/control_plane_adapter_registry.js +74 -0
- package/dist/control_plane_bootstrap_helpers.js +4 -1
- package/dist/control_plane_contract.d.ts +3 -12
- package/dist/control_plane_contract.js +1 -1
- package/dist/control_plane_run_queue_coordinator.d.ts +1 -7
- package/dist/control_plane_run_queue_coordinator.js +1 -62
- package/dist/control_plane_telegram_generation.js +1 -0
- package/dist/control_plane_wake_delivery.js +1 -0
- package/dist/cron_programs.d.ts +21 -35
- package/dist/cron_programs.js +32 -113
- package/dist/cron_request.d.ts +0 -6
- package/dist/cron_request.js +0 -41
- package/dist/heartbeat_programs.d.ts +20 -35
- package/dist/heartbeat_programs.js +26 -122
- package/dist/index.d.ts +2 -2
- package/dist/orchestration_queue.d.ts +44 -0
- package/dist/orchestration_queue.js +111 -0
- package/dist/outbound_delivery_router.d.ts +12 -0
- package/dist/outbound_delivery_router.js +29 -0
- package/dist/run_queue.d.ts +1 -1
- package/dist/run_queue.js +78 -79
- package/dist/run_supervisor.d.ts +2 -17
- package/dist/run_supervisor.js +1 -71
- package/dist/server.d.ts +0 -5
- package/dist/server.js +95 -127
- package/dist/server_program_orchestration.d.ts +4 -19
- package/dist/server_program_orchestration.js +49 -200
- package/dist/server_routing.d.ts +0 -9
- package/dist/server_routing.js +19 -151
- package/dist/server_runtime.js +0 -1
- package/dist/server_types.d.ts +0 -2
- package/dist/server_types.js +0 -7
- package/package.json +6 -10
- package/dist/api/forum.d.ts +0 -2
- package/dist/api/forum.js +0 -75
- package/dist/api/issues.d.ts +0 -2
- package/dist/api/issues.js +0 -173
- package/public/assets/index-CxkevQNh.js +0 -100
- package/public/assets/index-D_8anM-D.css +0 -1
- package/public/index.html +0 -14
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @femtomc/mu-server
|
|
2
2
|
|
|
3
|
-
HTTP API server for mu. Powers `mu serve`,
|
|
3
|
+
HTTP API server for mu control-plane infrastructure. Powers `mu serve`, messaging frontend transport routes, and control-plane/session coordination endpoints.
|
|
4
|
+
|
|
5
|
+
> Scope note: server-routed business query/mutation gateway endpoints have been removed. Business reads/writes are CLI-first, while long-lived runtime coordination (runs/activities/heartbeats/cron) stays server-owned.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -39,8 +41,6 @@ Bun.serve(server);
|
|
|
39
41
|
```json
|
|
40
42
|
{
|
|
41
43
|
"repo_root": "/path/to/repo",
|
|
42
|
-
"open_count": 10,
|
|
43
|
-
"ready_count": 3,
|
|
44
44
|
"control_plane": {
|
|
45
45
|
"active": true,
|
|
46
46
|
"adapters": ["slack"],
|
|
@@ -99,62 +99,71 @@ Bun.serve(server);
|
|
|
99
99
|
```
|
|
100
100
|
- Response includes generation metadata and, when telegram generation handling runs, `telegram_generation` lifecycle detail.
|
|
101
101
|
- `POST /api/control-plane/rollback` - Explicit rollback trigger (same pipeline, reason=`rollback`)
|
|
102
|
+
- `GET /api/control-plane/channels` - Capability/discovery snapshot for mounted adapter channels (route, verification contract, configured/active/frontend flags)
|
|
102
103
|
|
|
103
|
-
###
|
|
104
|
+
### Session Flash Inbox (cross-session context handoff)
|
|
104
105
|
|
|
105
|
-
- `
|
|
106
|
-
- Query params: `?status=open&tag=bug&contains=crash&limit=50`
|
|
107
|
-
- `limit` defaults to `200` and is clamped to `<= 200`.
|
|
108
|
-
- `GET /api/issues/:id` - Get issue by ID
|
|
109
|
-
- `POST /api/issues` - Create new issue
|
|
106
|
+
- `POST /api/session-flash` - Create a session-targeted flash message
|
|
110
107
|
```json
|
|
111
108
|
{
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
109
|
+
"session_id": "operator-abc123",
|
|
110
|
+
"session_kind": "cp_operator",
|
|
111
|
+
"body": "Use context id ctx-123 for this question",
|
|
112
|
+
"context_ids": ["ctx-123"],
|
|
113
|
+
"source": "neovim"
|
|
116
114
|
}
|
|
117
115
|
```
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
- `POST /api/
|
|
126
|
-
- `GET /api/issues/ready` - Get ready issues
|
|
127
|
-
- Query params: `?root=issue-id&contains=worker&limit=20`
|
|
128
|
-
- `limit` defaults to `200` and is clamped to `<= 200`.
|
|
129
|
-
|
|
130
|
-
### Forum
|
|
131
|
-
|
|
132
|
-
- `GET /api/forum/topics` - List forum topics
|
|
133
|
-
- Query params: `?prefix=issue:&limit=20`
|
|
134
|
-
- `limit` defaults to `100` and is clamped to `<= 200`.
|
|
135
|
-
- `GET /api/forum/read` - Read messages from topic
|
|
136
|
-
- Query params: `?topic=issue:123&limit=50`
|
|
137
|
-
- `limit` defaults to `50` and is clamped to `<= 200`.
|
|
138
|
-
- `POST /api/forum/post` - Post message to topic
|
|
116
|
+
- `GET /api/session-flash` - List flash messages
|
|
117
|
+
- Query params: `session_id`, `session_kind`, `status=pending|delivered|all`, `contains`, `limit`
|
|
118
|
+
- `GET /api/session-flash/:flash_id` - Get one flash message by id
|
|
119
|
+
- `POST /api/session-flash/ack` - Mark a flash message delivered/acknowledged
|
|
120
|
+
|
|
121
|
+
### Session Turn Injection (canonical transcript turn)
|
|
122
|
+
|
|
123
|
+
- `POST /api/session-turn` - Run a real turn in an existing target session and return reply + new context cursor
|
|
139
124
|
```json
|
|
140
125
|
{
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
126
|
+
"session_id": "operator-abc123",
|
|
127
|
+
"session_kind": "cp_operator",
|
|
128
|
+
"body": "Summarize the last plan and propose next steps.",
|
|
129
|
+
"source": "neovim"
|
|
144
130
|
}
|
|
145
131
|
```
|
|
132
|
+
- Optional overrides: `session_file`, `session_dir`, `provider`, `model`, `thinking`, `extension_profile`
|
|
133
|
+
- Response includes: `turn.reply`, `turn.context_entry_id`, `turn.session_file`
|
|
134
|
+
|
|
135
|
+
### Control-plane Coordination Endpoints
|
|
136
|
+
|
|
137
|
+
- Runs:
|
|
138
|
+
- `GET /api/runs`
|
|
139
|
+
- `POST /api/runs/start`
|
|
140
|
+
- `POST /api/runs/resume`
|
|
141
|
+
- `POST /api/runs/interrupt`
|
|
142
|
+
- `GET /api/runs/:id`
|
|
143
|
+
- `GET /api/runs/:id/trace`
|
|
144
|
+
- Scheduling + orchestration:
|
|
145
|
+
- `GET|POST|PATCH|DELETE /api/heartbeats...`
|
|
146
|
+
- `GET|POST|PATCH|DELETE /api/cron...`
|
|
147
|
+
- `GET|POST /api/activities...`
|
|
148
|
+
- Heartbeat/cron ticks dispatch operator wake turns and broadcast the resulting operator reply.
|
|
149
|
+
- Identity bindings:
|
|
150
|
+
- `GET /api/identities`
|
|
151
|
+
- `POST /api/identities/link`
|
|
152
|
+
- `POST /api/identities/unlink`
|
|
153
|
+
- Observability:
|
|
154
|
+
- `GET /api/events`
|
|
155
|
+
- `GET /api/events/tail`
|
|
146
156
|
|
|
147
157
|
## Running the Server
|
|
148
158
|
|
|
149
|
-
### With
|
|
159
|
+
### With terminal operator session (recommended)
|
|
150
160
|
|
|
151
|
-
The easiest way to run the server with the
|
|
161
|
+
The easiest way to run the server with the default terminal operator session:
|
|
152
162
|
|
|
153
163
|
```bash
|
|
154
164
|
# From any mu repository
|
|
155
|
-
mu serve # API +
|
|
156
|
-
mu serve --
|
|
157
|
-
mu serve --port 8080 # Custom shared API/web UI port
|
|
165
|
+
mu serve # API + terminal operator session
|
|
166
|
+
mu serve --port 8080 # Custom API/operator port
|
|
158
167
|
```
|
|
159
168
|
|
|
160
169
|
Type `/exit` (or press Ctrl+C) to stop both the operator session and server.
|
|
@@ -198,13 +207,9 @@ bun run start
|
|
|
198
207
|
## Architecture
|
|
199
208
|
|
|
200
209
|
The server uses:
|
|
201
|
-
- Filesystem-backed JSONL
|
|
202
|
-
- IssueStore and ForumStore from mu packages
|
|
210
|
+
- Filesystem-backed JSONL event storage (FsJsonlStore)
|
|
203
211
|
- Bun's built-in HTTP server
|
|
204
|
-
-
|
|
212
|
+
- Control-plane adapter/webhook transport + session coordination routes
|
|
205
213
|
- Generation-supervised control-plane hot reload lifecycle (see `docs/adr-0001-control-plane-hot-reload.md`)
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
- `.mu/issues.jsonl` - Issue data
|
|
209
|
-
- `.mu/forum.jsonl` - Forum messages
|
|
210
|
-
- `.mu/events.jsonl` - Event log
|
|
215
|
+
Control-plane/coordination data is persisted to `.mu/` (for example `.mu/events.jsonl` and `.mu/control-plane/*`).
|
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
import { CONTROL_PLANE_CHANNEL_ADAPTER_SPECS } from "@femtomc/mu-control-plane";
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function activeChannelsFromStatus(status) {
|
|
9
|
+
const record = asRecord(status);
|
|
10
|
+
const adapters = Array.isArray(record?.adapters) ? record.adapters : [];
|
|
11
|
+
const set = new Set();
|
|
12
|
+
for (const adapter of adapters) {
|
|
13
|
+
if (typeof adapter === "string" && adapter.trim().length > 0) {
|
|
14
|
+
set.add(adapter.trim());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return set;
|
|
18
|
+
}
|
|
19
|
+
function configuredForChannel(config, channel) {
|
|
20
|
+
switch (channel) {
|
|
21
|
+
case "slack":
|
|
22
|
+
return typeof config.control_plane.adapters.slack.signing_secret === "string";
|
|
23
|
+
case "discord":
|
|
24
|
+
return typeof config.control_plane.adapters.discord.signing_secret === "string";
|
|
25
|
+
case "telegram":
|
|
26
|
+
return typeof config.control_plane.adapters.telegram.webhook_secret === "string";
|
|
27
|
+
case "neovim":
|
|
28
|
+
return typeof config.control_plane.adapters.neovim.shared_secret === "string";
|
|
29
|
+
default:
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
1
33
|
export async function controlPlaneRoutes(request, url, deps, headers) {
|
|
2
34
|
const path = url.pathname;
|
|
3
35
|
if (path === "/api/control-plane/reload") {
|
|
@@ -24,5 +56,29 @@ export async function controlPlaneRoutes(request, url, deps, headers) {
|
|
|
24
56
|
const result = await deps.reloadControlPlane("rollback");
|
|
25
57
|
return Response.json(result, { status: result.ok ? 200 : 500, headers });
|
|
26
58
|
}
|
|
59
|
+
if (path === "/api/control-plane/channels") {
|
|
60
|
+
if (request.method !== "GET") {
|
|
61
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
62
|
+
}
|
|
63
|
+
const [config, status] = await Promise.all([deps.loadConfigFromDisk(), Promise.resolve(deps.getControlPlaneStatus())]);
|
|
64
|
+
const activeChannels = activeChannelsFromStatus(status);
|
|
65
|
+
const channels = CONTROL_PLANE_CHANNEL_ADAPTER_SPECS.map((spec) => ({
|
|
66
|
+
channel: spec.channel,
|
|
67
|
+
route: spec.route,
|
|
68
|
+
ingress_payload: spec.ingress_payload,
|
|
69
|
+
verification: spec.verification,
|
|
70
|
+
ack_format: spec.ack_format,
|
|
71
|
+
delivery_semantics: spec.delivery_semantics,
|
|
72
|
+
deferred_delivery: spec.deferred_delivery,
|
|
73
|
+
configured: configuredForChannel(config, spec.channel),
|
|
74
|
+
active: activeChannels.has(spec.channel),
|
|
75
|
+
frontend: spec.channel === "neovim",
|
|
76
|
+
}));
|
|
77
|
+
return Response.json({
|
|
78
|
+
ok: true,
|
|
79
|
+
generated_at_ms: Date.now(),
|
|
80
|
+
channels,
|
|
81
|
+
}, { headers });
|
|
82
|
+
}
|
|
27
83
|
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
28
84
|
}
|
package/dist/api/cron.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { cronScheduleInputFromBody, hasCronScheduleInput
|
|
2
|
-
import { normalizeWakeMode } from "../server_types.js";
|
|
1
|
+
import { cronScheduleInputFromBody, hasCronScheduleInput } from "../cron_request.js";
|
|
3
2
|
export async function cronRoutes(request, url, deps, headers) {
|
|
4
3
|
const path = url.pathname;
|
|
5
4
|
if (path === "/api/cron/status") {
|
|
@@ -15,15 +14,13 @@ export async function cronRoutes(request, url, deps, headers) {
|
|
|
15
14
|
}
|
|
16
15
|
const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
|
|
17
16
|
const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
|
|
18
|
-
const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
|
|
19
|
-
const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
|
|
20
17
|
const scheduleKindRaw = url.searchParams.get("schedule_kind")?.trim().toLowerCase();
|
|
21
18
|
const scheduleKind = scheduleKindRaw === "at" || scheduleKindRaw === "every" || scheduleKindRaw === "cron"
|
|
22
19
|
? scheduleKindRaw
|
|
23
20
|
: undefined;
|
|
24
21
|
const limitRaw = url.searchParams.get("limit");
|
|
25
22
|
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
26
|
-
const programs = await deps.cronPrograms.list({ enabled,
|
|
23
|
+
const programs = await deps.cronPrograms.list({ enabled, scheduleKind, limit });
|
|
27
24
|
return Response.json({ count: programs.length, programs }, { headers });
|
|
28
25
|
}
|
|
29
26
|
if (path === "/api/cron/create") {
|
|
@@ -41,24 +38,17 @@ export async function cronRoutes(request, url, deps, headers) {
|
|
|
41
38
|
if (!title) {
|
|
42
39
|
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
43
40
|
}
|
|
44
|
-
const parsedTarget = parseCronTarget(body);
|
|
45
|
-
if (!parsedTarget.target) {
|
|
46
|
-
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
47
|
-
}
|
|
48
41
|
if (!hasCronScheduleInput(body)) {
|
|
49
42
|
return Response.json({ error: "schedule is required" }, { status: 400, headers });
|
|
50
43
|
}
|
|
51
44
|
const schedule = cronScheduleInputFromBody(body);
|
|
52
45
|
const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
|
|
53
|
-
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
54
46
|
const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
|
|
55
47
|
try {
|
|
56
48
|
const program = await deps.cronPrograms.create({
|
|
57
49
|
title,
|
|
58
|
-
target: parsedTarget.target,
|
|
59
50
|
schedule,
|
|
60
51
|
reason,
|
|
61
|
-
wakeMode,
|
|
62
52
|
enabled,
|
|
63
53
|
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
64
54
|
? body.metadata
|
|
@@ -85,24 +75,13 @@ export async function cronRoutes(request, url, deps, headers) {
|
|
|
85
75
|
if (!programId) {
|
|
86
76
|
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
87
77
|
}
|
|
88
|
-
let target;
|
|
89
|
-
if (typeof body.target_kind === "string") {
|
|
90
|
-
const parsedTarget = parseCronTarget(body);
|
|
91
|
-
if (!parsedTarget.target) {
|
|
92
|
-
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
93
|
-
}
|
|
94
|
-
target = parsedTarget.target;
|
|
95
|
-
}
|
|
96
78
|
const schedule = hasCronScheduleInput(body) ? cronScheduleInputFromBody(body) : undefined;
|
|
97
|
-
const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
|
|
98
79
|
try {
|
|
99
80
|
const result = await deps.cronPrograms.update({
|
|
100
81
|
programId,
|
|
101
82
|
title: typeof body.title === "string" ? body.title : undefined,
|
|
102
83
|
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
103
|
-
wakeMode,
|
|
104
84
|
enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
|
|
105
|
-
target,
|
|
106
85
|
schedule,
|
|
107
86
|
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
108
87
|
? body.metadata
|
package/dist/api/heartbeats.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeWakeMode } from "../server_types.js";
|
|
2
1
|
export async function heartbeatRoutes(request, url, deps, headers) {
|
|
3
2
|
const path = url.pathname;
|
|
4
3
|
if (path === "/api/heartbeats") {
|
|
@@ -7,11 +6,9 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
7
6
|
}
|
|
8
7
|
const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
|
|
9
8
|
const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
|
|
10
|
-
const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
|
|
11
|
-
const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
|
|
12
9
|
const limitRaw = url.searchParams.get("limit");
|
|
13
10
|
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
14
|
-
const programs = await deps.heartbeatPrograms.list({ enabled,
|
|
11
|
+
const programs = await deps.heartbeatPrograms.list({ enabled, limit });
|
|
15
12
|
return Response.json({ count: programs.length, programs }, { headers });
|
|
16
13
|
}
|
|
17
14
|
if (path === "/api/heartbeats/create") {
|
|
@@ -29,46 +26,16 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
29
26
|
if (!title) {
|
|
30
27
|
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
31
28
|
}
|
|
32
|
-
const targetKind = typeof body.target_kind === "string" ? body.target_kind.trim().toLowerCase() : "";
|
|
33
|
-
let target = null;
|
|
34
|
-
if (targetKind === "run") {
|
|
35
|
-
const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
|
|
36
|
-
const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
|
|
37
|
-
if (!jobId && !rootIssueId) {
|
|
38
|
-
return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
|
|
39
|
-
}
|
|
40
|
-
target = {
|
|
41
|
-
kind: "run",
|
|
42
|
-
job_id: jobId || null,
|
|
43
|
-
root_issue_id: rootIssueId || null,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
else if (targetKind === "activity") {
|
|
47
|
-
const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
|
|
48
|
-
if (!activityId) {
|
|
49
|
-
return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
|
|
50
|
-
}
|
|
51
|
-
target = {
|
|
52
|
-
kind: "activity",
|
|
53
|
-
activity_id: activityId,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
|
|
58
|
-
}
|
|
59
29
|
const everyMs = typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
|
|
60
30
|
? Math.max(0, Math.trunc(body.every_ms))
|
|
61
31
|
: undefined;
|
|
62
32
|
const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
|
|
63
|
-
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
64
33
|
const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
|
|
65
34
|
try {
|
|
66
35
|
const program = await deps.heartbeatPrograms.create({
|
|
67
36
|
title,
|
|
68
|
-
target,
|
|
69
37
|
everyMs,
|
|
70
38
|
reason,
|
|
71
|
-
wakeMode,
|
|
72
39
|
enabled,
|
|
73
40
|
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
74
41
|
? body.metadata
|
|
@@ -95,46 +62,14 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
95
62
|
if (!programId) {
|
|
96
63
|
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
97
64
|
}
|
|
98
|
-
let target;
|
|
99
|
-
if (typeof body.target_kind === "string") {
|
|
100
|
-
const targetKind = body.target_kind.trim().toLowerCase();
|
|
101
|
-
if (targetKind === "run") {
|
|
102
|
-
const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
|
|
103
|
-
const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
|
|
104
|
-
if (!jobId && !rootIssueId) {
|
|
105
|
-
return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
|
|
106
|
-
}
|
|
107
|
-
target = {
|
|
108
|
-
kind: "run",
|
|
109
|
-
job_id: jobId || null,
|
|
110
|
-
root_issue_id: rootIssueId || null,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
else if (targetKind === "activity") {
|
|
114
|
-
const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
|
|
115
|
-
if (!activityId) {
|
|
116
|
-
return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
|
|
117
|
-
}
|
|
118
|
-
target = {
|
|
119
|
-
kind: "activity",
|
|
120
|
-
activity_id: activityId,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
|
|
128
65
|
try {
|
|
129
66
|
const result = await deps.heartbeatPrograms.update({
|
|
130
67
|
programId,
|
|
131
68
|
title: typeof body.title === "string" ? body.title : undefined,
|
|
132
|
-
target,
|
|
133
69
|
everyMs: typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
|
|
134
70
|
? Math.max(0, Math.trunc(body.every_ms))
|
|
135
71
|
: undefined,
|
|
136
72
|
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
137
|
-
wakeMode,
|
|
138
73
|
enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
|
|
139
74
|
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
140
75
|
? body.metadata
|
package/dist/api/identities.js
CHANGED
|
@@ -24,8 +24,9 @@ export async function identityRoutes(request, url, deps, headers) {
|
|
|
24
24
|
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
25
25
|
}
|
|
26
26
|
const channel = typeof body.channel === "string" ? body.channel.trim() : "";
|
|
27
|
-
if (!channel ||
|
|
28
|
-
|
|
27
|
+
if (!channel ||
|
|
28
|
+
(channel !== "slack" && channel !== "discord" && channel !== "telegram" && channel !== "neovim")) {
|
|
29
|
+
return Response.json({ error: "channel is required (slack, discord, telegram, neovim)" }, { status: 400, headers });
|
|
29
30
|
}
|
|
30
31
|
const actorId = typeof body.actor_id === "string" ? body.actor_id.trim() : "";
|
|
31
32
|
if (!actorId) {
|
package/dist/api/runs.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeWakeMode } from "../server_types.js";
|
|
2
1
|
export async function runRoutes(request, url, deps, headers) {
|
|
3
2
|
const path = url.pathname;
|
|
4
3
|
if (path === "/api/runs") {
|
|
@@ -34,16 +33,6 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
34
33
|
if (!run) {
|
|
35
34
|
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
36
35
|
}
|
|
37
|
-
await deps.registerAutoRunHeartbeatProgram(run).catch(async (error) => {
|
|
38
|
-
await deps.context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
39
|
-
source: "mu-server.runs",
|
|
40
|
-
payload: {
|
|
41
|
-
action: "register_failed",
|
|
42
|
-
run_job_id: run.job_id,
|
|
43
|
-
error: deps.describeError(error),
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
36
|
return Response.json({ ok: true, run }, { status: 201, headers });
|
|
48
37
|
}
|
|
49
38
|
catch (err) {
|
|
@@ -73,16 +62,6 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
73
62
|
if (!run) {
|
|
74
63
|
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
75
64
|
}
|
|
76
|
-
await deps.registerAutoRunHeartbeatProgram(run).catch(async (error) => {
|
|
77
|
-
await deps.context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
78
|
-
source: "mu-server.runs",
|
|
79
|
-
payload: {
|
|
80
|
-
action: "register_failed",
|
|
81
|
-
run_job_id: run.job_id,
|
|
82
|
-
error: deps.describeError(error),
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
65
|
return Response.json({ ok: true, run }, { status: 201, headers });
|
|
87
66
|
}
|
|
88
67
|
catch (err) {
|
|
@@ -109,61 +88,8 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
109
88
|
if (!result) {
|
|
110
89
|
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
111
90
|
}
|
|
112
|
-
if (!result.ok && result.reason === "not_running" && result.run) {
|
|
113
|
-
await deps.disableAutoRunHeartbeatProgram({
|
|
114
|
-
jobId: result.run.job_id,
|
|
115
|
-
status: result.run.status,
|
|
116
|
-
reason: "interrupt_not_running",
|
|
117
|
-
}).catch(() => {
|
|
118
|
-
// best effort cleanup only
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
91
|
return Response.json(result, { status: result.ok ? 200 : 404, headers });
|
|
122
92
|
}
|
|
123
|
-
if (path === "/api/runs/heartbeat") {
|
|
124
|
-
if (request.method !== "POST") {
|
|
125
|
-
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
126
|
-
}
|
|
127
|
-
let body;
|
|
128
|
-
try {
|
|
129
|
-
body = (await request.json());
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
133
|
-
}
|
|
134
|
-
const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
|
|
135
|
-
const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
|
|
136
|
-
const reason = typeof body.reason === "string" ? body.reason.trim() : null;
|
|
137
|
-
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
138
|
-
const result = await deps.controlPlaneProxy.heartbeatRun?.({
|
|
139
|
-
rootIssueId,
|
|
140
|
-
jobId,
|
|
141
|
-
reason,
|
|
142
|
-
wakeMode,
|
|
143
|
-
});
|
|
144
|
-
if (!result) {
|
|
145
|
-
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
146
|
-
}
|
|
147
|
-
if (!result.ok && result.reason === "not_running" && result.run) {
|
|
148
|
-
await deps.disableAutoRunHeartbeatProgram({
|
|
149
|
-
jobId: result.run.job_id,
|
|
150
|
-
status: result.run.status,
|
|
151
|
-
reason: "run_not_running",
|
|
152
|
-
}).catch(() => {
|
|
153
|
-
// best effort cleanup only
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
if (result.ok) {
|
|
157
|
-
return Response.json(result, { status: 200, headers });
|
|
158
|
-
}
|
|
159
|
-
if (result.reason === "missing_target") {
|
|
160
|
-
return Response.json(result, { status: 400, headers });
|
|
161
|
-
}
|
|
162
|
-
if (result.reason === "not_running") {
|
|
163
|
-
return Response.json(result, { status: 409, headers });
|
|
164
|
-
}
|
|
165
|
-
return Response.json(result, { status: 404, headers });
|
|
166
|
-
}
|
|
167
93
|
if (path.startsWith("/api/runs/")) {
|
|
168
94
|
const rest = path.slice("/api/runs/".length);
|
|
169
95
|
const [rawId, maybeSub] = rest.split("/");
|
|
@@ -192,15 +118,6 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
192
118
|
if (!run) {
|
|
193
119
|
return Response.json({ error: "run not found" }, { status: 404, headers });
|
|
194
120
|
}
|
|
195
|
-
if (run.status !== "running") {
|
|
196
|
-
await deps.disableAutoRunHeartbeatProgram({
|
|
197
|
-
jobId: run.job_id,
|
|
198
|
-
status: run.status,
|
|
199
|
-
reason: "run_terminal_snapshot",
|
|
200
|
-
}).catch(() => {
|
|
201
|
-
// best effort cleanup only
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
121
|
return Response.json(run, { headers });
|
|
205
122
|
}
|
|
206
123
|
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ServerRoutingDependencies } from "../server_routing.js";
|
|
2
|
+
export type SessionFlashStatus = "pending" | "delivered";
|
|
3
|
+
export type SessionFlashRecord = {
|
|
4
|
+
flash_id: string;
|
|
5
|
+
created_at_ms: number;
|
|
6
|
+
session_id: string;
|
|
7
|
+
session_kind: string | null;
|
|
8
|
+
body: string;
|
|
9
|
+
context_ids: string[];
|
|
10
|
+
source: string | null;
|
|
11
|
+
metadata: Record<string, unknown>;
|
|
12
|
+
from: {
|
|
13
|
+
channel: string | null;
|
|
14
|
+
channel_tenant_id: string | null;
|
|
15
|
+
channel_conversation_id: string | null;
|
|
16
|
+
actor_binding_id: string | null;
|
|
17
|
+
};
|
|
18
|
+
status: SessionFlashStatus;
|
|
19
|
+
delivered_at_ms: number | null;
|
|
20
|
+
delivered_by: string | null;
|
|
21
|
+
delivery_note: string | null;
|
|
22
|
+
};
|
|
23
|
+
export declare function getSessionFlashPath(repoRoot: string): string;
|
|
24
|
+
export declare function listSessionFlashRecords(opts: {
|
|
25
|
+
repoRoot: string;
|
|
26
|
+
sessionId?: string | null;
|
|
27
|
+
sessionKind?: string | null;
|
|
28
|
+
status?: SessionFlashStatus | "all";
|
|
29
|
+
contains?: string | null;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}): Promise<SessionFlashRecord[]>;
|
|
32
|
+
export declare function getSessionFlashRecord(opts: {
|
|
33
|
+
repoRoot: string;
|
|
34
|
+
flashId: string;
|
|
35
|
+
}): Promise<SessionFlashRecord | null>;
|
|
36
|
+
export declare function createSessionFlashRecord(opts: {
|
|
37
|
+
repoRoot: string;
|
|
38
|
+
sessionId: string;
|
|
39
|
+
body: string;
|
|
40
|
+
sessionKind?: string | null;
|
|
41
|
+
contextIds?: string[];
|
|
42
|
+
source?: string | null;
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
44
|
+
from?: {
|
|
45
|
+
channel?: string | null;
|
|
46
|
+
channel_tenant_id?: string | null;
|
|
47
|
+
channel_conversation_id?: string | null;
|
|
48
|
+
actor_binding_id?: string | null;
|
|
49
|
+
};
|
|
50
|
+
nowMs?: number;
|
|
51
|
+
}): Promise<SessionFlashRecord>;
|
|
52
|
+
export declare function ackSessionFlashRecord(opts: {
|
|
53
|
+
repoRoot: string;
|
|
54
|
+
flashId: string;
|
|
55
|
+
sessionId?: string | null;
|
|
56
|
+
deliveredBy?: string | null;
|
|
57
|
+
note?: string | null;
|
|
58
|
+
nowMs?: number;
|
|
59
|
+
}): Promise<SessionFlashRecord | null>;
|
|
60
|
+
export declare function sessionFlashRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;
|