@femtomc/mu-server 26.2.75 → 26.2.76
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 +24 -35
- package/dist/api/control_plane.js +35 -0
- package/dist/api/events.js +7 -3
- package/dist/api/heartbeats.js +19 -2
- package/dist/api/identities.js +3 -3
- package/dist/api/runs.js +6 -6
- package/dist/api/session_turn.d.ts +0 -36
- package/dist/api/session_turn.js +32 -372
- package/dist/cli.js +4 -4
- package/dist/config.d.ts +15 -0
- package/dist/config.js +70 -1
- package/dist/control_plane.js +1 -1
- package/dist/control_plane_bootstrap_helpers.js +3 -2
- package/dist/control_plane_contract.d.ts +1 -1
- package/dist/cron_programs.js +3 -2
- package/dist/heartbeat_programs.d.ts +4 -0
- package/dist/heartbeat_programs.js +17 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/memory_index_maintainer.d.ts +15 -0
- package/dist/memory_index_maintainer.js +165 -0
- package/dist/run_queue.js +2 -2
- package/dist/run_supervisor.d.ts +4 -4
- package/dist/run_supervisor.js +6 -5
- package/dist/server.d.ts +0 -3
- package/dist/server.js +26 -23
- package/dist/server_program_orchestration.js +6 -1
- package/dist/server_routing.d.ts +0 -2
- package/dist/server_routing.js +1 -43
- package/package.json +4 -4
- package/dist/activity_supervisor.d.ts +0 -81
- package/dist/activity_supervisor.js +0 -306
- package/dist/api/activities.d.ts +0 -2
- package/dist/api/activities.js +0 -160
- package/dist/api/session_flash.d.ts +0 -60
- package/dist/api/session_flash.js +0 -326
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
HTTP API server for mu control-plane infrastructure. Powers `mu serve`, messaging frontend transport routes, and control-plane/session coordination endpoints.
|
|
4
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/
|
|
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/heartbeats/cron) stays server-owned.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -37,7 +37,7 @@ Bun.serve(server);
|
|
|
37
37
|
|
|
38
38
|
### Status
|
|
39
39
|
|
|
40
|
-
- `GET /api/status` - Returns repository + control-plane runtime status
|
|
40
|
+
- `GET /api/control-plane/status` - Returns repository + control-plane runtime status
|
|
41
41
|
```json
|
|
42
42
|
{
|
|
43
43
|
"repo_root": "/path/to/repo",
|
|
@@ -76,8 +76,8 @@ Bun.serve(server);
|
|
|
76
76
|
|
|
77
77
|
### Config + Control Plane Admin
|
|
78
78
|
|
|
79
|
-
- `GET /api/config` - Read redacted `.mu/config.json` plus presence booleans
|
|
80
|
-
- `POST /api/config` - Apply a partial patch to `.mu/config.json`
|
|
79
|
+
- `GET /api/control-plane/config` - Read redacted `.mu/config.json` plus presence booleans
|
|
80
|
+
- `POST /api/control-plane/config` - Apply a partial patch to `.mu/config.json`
|
|
81
81
|
- Body:
|
|
82
82
|
```json
|
|
83
83
|
{
|
|
@@ -85,6 +85,10 @@ Bun.serve(server);
|
|
|
85
85
|
"control_plane": {
|
|
86
86
|
"adapters": {
|
|
87
87
|
"slack": { "signing_secret": "..." }
|
|
88
|
+
},
|
|
89
|
+
"memory_index": {
|
|
90
|
+
"enabled": true,
|
|
91
|
+
"every_ms": 300000
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
}
|
|
@@ -101,26 +105,10 @@ Bun.serve(server);
|
|
|
101
105
|
- `POST /api/control-plane/rollback` - Explicit rollback trigger (same pipeline, reason=`rollback`)
|
|
102
106
|
- `GET /api/control-plane/channels` - Capability/discovery snapshot for mounted adapter channels (route, verification contract, configured/active/frontend flags)
|
|
103
107
|
|
|
104
|
-
### Session
|
|
108
|
+
### Session Turn Injection (control-plane)
|
|
105
109
|
|
|
106
|
-
- `POST /api/
|
|
107
|
-
|
|
108
|
-
{
|
|
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"
|
|
114
|
-
}
|
|
115
|
-
```
|
|
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
|
|
110
|
+
- `POST /api/control-plane/turn` - Run a real turn in an existing target session and return reply + new context cursor
|
|
111
|
+
- Requires Neovim frontend shared-secret header (`x-mu-neovim-secret`)
|
|
124
112
|
```json
|
|
125
113
|
{
|
|
126
114
|
"session_id": "operator-abc123",
|
|
@@ -135,24 +123,25 @@ Bun.serve(server);
|
|
|
135
123
|
### Control-plane Coordination Endpoints
|
|
136
124
|
|
|
137
125
|
- 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`
|
|
126
|
+
- `GET /api/control-plane/runs`
|
|
127
|
+
- `POST /api/control-plane/runs/start`
|
|
128
|
+
- `POST /api/control-plane/runs/resume`
|
|
129
|
+
- `POST /api/control-plane/runs/interrupt`
|
|
130
|
+
- `GET /api/control-plane/runs/:id`
|
|
131
|
+
- `GET /api/control-plane/runs/:id/trace`
|
|
144
132
|
- Scheduling + orchestration:
|
|
145
133
|
- `GET|POST|PATCH|DELETE /api/heartbeats...`
|
|
146
134
|
- `GET|POST|PATCH|DELETE /api/cron...`
|
|
147
|
-
- `
|
|
135
|
+
- Heartbeat programs support an optional free-form `prompt` field; when present it becomes the primary wake instruction sent to the operator turn path.
|
|
148
136
|
- Heartbeat/cron ticks dispatch operator wake turns and broadcast the resulting operator reply.
|
|
137
|
+
- Built-in memory-index maintenance runs on the server heartbeat scheduler (config: `control_plane.memory_index`).
|
|
149
138
|
- Identity bindings:
|
|
150
|
-
- `GET /api/identities`
|
|
151
|
-
- `POST /api/identities/link`
|
|
152
|
-
- `POST /api/identities/unlink`
|
|
139
|
+
- `GET /api/control-plane/identities`
|
|
140
|
+
- `POST /api/control-plane/identities/link`
|
|
141
|
+
- `POST /api/control-plane/identities/unlink`
|
|
153
142
|
- Observability:
|
|
154
|
-
- `GET /api/events`
|
|
155
|
-
- `GET /api/events/tail`
|
|
143
|
+
- `GET /api/control-plane/events`
|
|
144
|
+
- `GET /api/control-plane/events/tail`
|
|
156
145
|
|
|
157
146
|
## Running the Server
|
|
158
147
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { CONTROL_PLANE_CHANNEL_ADAPTER_SPECS } from "@femtomc/mu-control-plane";
|
|
2
|
+
import { configRoutes } from "./config.js";
|
|
3
|
+
import { eventRoutes } from "./events.js";
|
|
4
|
+
import { identityRoutes } from "./identities.js";
|
|
5
|
+
import { runRoutes } from "./runs.js";
|
|
6
|
+
import { sessionTurnRoutes } from "./session_turn.js";
|
|
2
7
|
function asRecord(value) {
|
|
3
8
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4
9
|
return null;
|
|
@@ -32,6 +37,30 @@ function configuredForChannel(config, channel) {
|
|
|
32
37
|
}
|
|
33
38
|
export async function controlPlaneRoutes(request, url, deps, headers) {
|
|
34
39
|
const path = url.pathname;
|
|
40
|
+
if (path === "/api/control-plane" || path === "/api/control-plane/status") {
|
|
41
|
+
if (request.method !== "GET") {
|
|
42
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
43
|
+
}
|
|
44
|
+
return Response.json({
|
|
45
|
+
repo_root: deps.context.repoRoot,
|
|
46
|
+
control_plane: deps.getControlPlaneStatus(),
|
|
47
|
+
}, { headers });
|
|
48
|
+
}
|
|
49
|
+
if (path === "/api/control-plane/config") {
|
|
50
|
+
return configRoutes(request, url, deps, headers);
|
|
51
|
+
}
|
|
52
|
+
if (path === "/api/control-plane/identities" ||
|
|
53
|
+
path === "/api/control-plane/identities/link" ||
|
|
54
|
+
path === "/api/control-plane/identities/unlink") {
|
|
55
|
+
return identityRoutes(request, url, deps, headers);
|
|
56
|
+
}
|
|
57
|
+
if (path.startsWith("/api/control-plane/events")) {
|
|
58
|
+
const response = await eventRoutes(request, deps.context);
|
|
59
|
+
headers.forEach((value, key) => {
|
|
60
|
+
response.headers.set(key, value);
|
|
61
|
+
});
|
|
62
|
+
return response;
|
|
63
|
+
}
|
|
35
64
|
if (path === "/api/control-plane/reload") {
|
|
36
65
|
if (request.method !== "POST") {
|
|
37
66
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
@@ -80,5 +109,11 @@ export async function controlPlaneRoutes(request, url, deps, headers) {
|
|
|
80
109
|
channels,
|
|
81
110
|
}, { headers });
|
|
82
111
|
}
|
|
112
|
+
if (path === "/api/control-plane/runs" || path.startsWith("/api/control-plane/runs/")) {
|
|
113
|
+
return runRoutes(request, url, deps, headers);
|
|
114
|
+
}
|
|
115
|
+
if (path === "/api/control-plane/turn") {
|
|
116
|
+
return sessionTurnRoutes(request, url, deps, headers);
|
|
117
|
+
}
|
|
83
118
|
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
84
119
|
}
|
package/dist/api/events.js
CHANGED
|
@@ -64,7 +64,11 @@ function parseSinceMs(value) {
|
|
|
64
64
|
}
|
|
65
65
|
export async function eventRoutes(request, context) {
|
|
66
66
|
const url = new URL(request.url);
|
|
67
|
-
const
|
|
67
|
+
const pathname = url.pathname;
|
|
68
|
+
if (!pathname.startsWith("/api/control-plane/events")) {
|
|
69
|
+
return new Response("Not Found", { status: 404 });
|
|
70
|
+
}
|
|
71
|
+
const path = pathname.slice("/api/control-plane/events".length) || "/";
|
|
68
72
|
const method = request.method;
|
|
69
73
|
if (method !== "GET") {
|
|
70
74
|
return new Response("Method Not Allowed", { status: 405 });
|
|
@@ -79,13 +83,13 @@ export async function eventRoutes(request, context) {
|
|
|
79
83
|
sinceMs: parseSinceMs(url.searchParams.get("since")),
|
|
80
84
|
contains: trimOrNull(url.searchParams.get("contains")),
|
|
81
85
|
};
|
|
82
|
-
// Tail - GET /api/events/tail?n=50
|
|
86
|
+
// Tail - GET /api/control-plane/events/tail?n=50
|
|
83
87
|
if (path === "/tail") {
|
|
84
88
|
const n = Math.min(Math.max(1, parseInt(url.searchParams.get("n") ?? "50", 10) || 50), 500);
|
|
85
89
|
const filtered = applyEventFilters(allEvents, filters);
|
|
86
90
|
return Response.json(filtered.slice(-n));
|
|
87
91
|
}
|
|
88
|
-
// Query - GET /api/events?type=...&source=...&issue_id=...&run_id=...&since=...&contains=...&limit=50
|
|
92
|
+
// Query - GET /api/control-plane/events?type=...&source=...&issue_id=...&run_id=...&since=...&contains=...&limit=50
|
|
89
93
|
if (path === "/") {
|
|
90
94
|
const limit = Math.min(Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50), 500);
|
|
91
95
|
const filtered = applyEventFilters(allEvents, filters);
|
package/dist/api/heartbeats.js
CHANGED
|
@@ -26,6 +26,10 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
26
26
|
if (!title) {
|
|
27
27
|
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
28
28
|
}
|
|
29
|
+
if ("prompt" in body && typeof body.prompt !== "string" && body.prompt !== null) {
|
|
30
|
+
return Response.json({ error: "prompt must be string or null" }, { status: 400, headers });
|
|
31
|
+
}
|
|
32
|
+
const prompt = typeof body.prompt === "string" ? body.prompt : body.prompt === null ? null : undefined;
|
|
29
33
|
const everyMs = typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
|
|
30
34
|
? Math.max(0, Math.trunc(body.every_ms))
|
|
31
35
|
: undefined;
|
|
@@ -34,6 +38,7 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
34
38
|
try {
|
|
35
39
|
const program = await deps.heartbeatPrograms.create({
|
|
36
40
|
title,
|
|
41
|
+
prompt,
|
|
37
42
|
everyMs,
|
|
38
43
|
reason,
|
|
39
44
|
enabled,
|
|
@@ -63,7 +68,7 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
63
68
|
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
64
69
|
}
|
|
65
70
|
try {
|
|
66
|
-
const
|
|
71
|
+
const updateOpts = {
|
|
67
72
|
programId,
|
|
68
73
|
title: typeof body.title === "string" ? body.title : undefined,
|
|
69
74
|
everyMs: typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
|
|
@@ -74,7 +79,19 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
74
79
|
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
75
80
|
? body.metadata
|
|
76
81
|
: undefined,
|
|
77
|
-
}
|
|
82
|
+
};
|
|
83
|
+
if ("prompt" in body) {
|
|
84
|
+
if (typeof body.prompt === "string") {
|
|
85
|
+
updateOpts.prompt = body.prompt;
|
|
86
|
+
}
|
|
87
|
+
else if (body.prompt === null) {
|
|
88
|
+
updateOpts.prompt = null;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
return Response.json({ error: "prompt must be string or null" }, { status: 400, headers });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const result = await deps.heartbeatPrograms.update(updateOpts);
|
|
78
95
|
if (result.ok) {
|
|
79
96
|
return Response.json(result, { headers });
|
|
80
97
|
}
|
package/dist/api/identities.js
CHANGED
|
@@ -4,7 +4,7 @@ export async function identityRoutes(request, url, deps, headers) {
|
|
|
4
4
|
const cpPaths = getControlPlanePaths(deps.context.repoRoot);
|
|
5
5
|
const identityStore = new IdentityStore(cpPaths.identitiesPath);
|
|
6
6
|
await identityStore.load();
|
|
7
|
-
if (path === "/api/identities") {
|
|
7
|
+
if (path === "/api/control-plane/identities") {
|
|
8
8
|
if (request.method !== "GET") {
|
|
9
9
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
10
10
|
}
|
|
@@ -12,7 +12,7 @@ export async function identityRoutes(request, url, deps, headers) {
|
|
|
12
12
|
const bindings = identityStore.listBindings({ includeInactive });
|
|
13
13
|
return Response.json({ count: bindings.length, bindings }, { headers });
|
|
14
14
|
}
|
|
15
|
-
if (path === "/api/identities/link") {
|
|
15
|
+
if (path === "/api/control-plane/identities/link") {
|
|
16
16
|
if (request.method !== "POST") {
|
|
17
17
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
18
18
|
}
|
|
@@ -64,7 +64,7 @@ export async function identityRoutes(request, url, deps, headers) {
|
|
|
64
64
|
return Response.json({ ok: false, kind: "principal_already_linked", binding: decision.binding }, { status: 409, headers });
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
if (path === "/api/identities/unlink") {
|
|
67
|
+
if (path === "/api/control-plane/identities/unlink") {
|
|
68
68
|
if (request.method !== "POST") {
|
|
69
69
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
70
70
|
}
|
package/dist/api/runs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export async function runRoutes(request, url, deps, headers) {
|
|
2
2
|
const path = url.pathname;
|
|
3
|
-
if (path === "/api/runs") {
|
|
3
|
+
if (path === "/api/control-plane/runs") {
|
|
4
4
|
if (request.method !== "GET") {
|
|
5
5
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
6
6
|
}
|
|
@@ -10,7 +10,7 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
10
10
|
const runs = await deps.controlPlaneProxy.listRuns?.({ status, limit });
|
|
11
11
|
return Response.json({ count: runs?.length ?? 0, runs: runs ?? [] }, { headers });
|
|
12
12
|
}
|
|
13
|
-
if (path === "/api/runs/start") {
|
|
13
|
+
if (path === "/api/control-plane/runs/start") {
|
|
14
14
|
if (request.method !== "POST") {
|
|
15
15
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
16
16
|
}
|
|
@@ -39,7 +39,7 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
39
39
|
return Response.json({ error: deps.describeError(err) }, { status: 500, headers });
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
if (path === "/api/runs/resume") {
|
|
42
|
+
if (path === "/api/control-plane/runs/resume") {
|
|
43
43
|
if (request.method !== "POST") {
|
|
44
44
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
45
45
|
}
|
|
@@ -68,7 +68,7 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
68
68
|
return Response.json({ error: deps.describeError(err) }, { status: 500, headers });
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
if (path === "/api/runs/interrupt") {
|
|
71
|
+
if (path === "/api/control-plane/runs/interrupt") {
|
|
72
72
|
if (request.method !== "POST") {
|
|
73
73
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
74
74
|
}
|
|
@@ -90,8 +90,8 @@ export async function runRoutes(request, url, deps, headers) {
|
|
|
90
90
|
}
|
|
91
91
|
return Response.json(result, { status: result.ok ? 200 : 404, headers });
|
|
92
92
|
}
|
|
93
|
-
if (path.startsWith("/api/runs/")) {
|
|
94
|
-
const rest = path.slice("/api/runs/".length);
|
|
93
|
+
if (path.startsWith("/api/control-plane/runs/")) {
|
|
94
|
+
const rest = path.slice("/api/control-plane/runs/".length);
|
|
95
95
|
const [rawId, maybeSub] = rest.split("/");
|
|
96
96
|
const idOrRoot = decodeURIComponent(rawId ?? "").trim();
|
|
97
97
|
if (idOrRoot.length === 0) {
|
|
@@ -1,38 +1,2 @@
|
|
|
1
|
-
import { type CreateMuSessionOpts, type MuSession } from "@femtomc/mu-agent";
|
|
2
1
|
import type { ServerRoutingDependencies } from "../server_routing.js";
|
|
3
|
-
export type SessionTurnRequest = {
|
|
4
|
-
session_id: string;
|
|
5
|
-
session_kind: string | null;
|
|
6
|
-
body: string;
|
|
7
|
-
source: string | null;
|
|
8
|
-
provider: string | null;
|
|
9
|
-
model: string | null;
|
|
10
|
-
thinking: string | null;
|
|
11
|
-
session_file: string | null;
|
|
12
|
-
session_dir: string | null;
|
|
13
|
-
extension_profile: string | null;
|
|
14
|
-
};
|
|
15
|
-
export type SessionTurnResult = {
|
|
16
|
-
session_id: string;
|
|
17
|
-
session_kind: string | null;
|
|
18
|
-
session_file: string;
|
|
19
|
-
context_entry_id: string | null;
|
|
20
|
-
reply: string;
|
|
21
|
-
source: string | null;
|
|
22
|
-
completed_at_ms: number;
|
|
23
|
-
};
|
|
24
|
-
export declare class SessionTurnError extends Error {
|
|
25
|
-
readonly status: number;
|
|
26
|
-
constructor(status: number, message: string);
|
|
27
|
-
}
|
|
28
|
-
export declare function parseSessionTurnRequest(body: Record<string, unknown>): {
|
|
29
|
-
request: SessionTurnRequest | null;
|
|
30
|
-
error: string | null;
|
|
31
|
-
};
|
|
32
|
-
export declare function executeSessionTurn(opts: {
|
|
33
|
-
repoRoot: string;
|
|
34
|
-
request: SessionTurnRequest;
|
|
35
|
-
sessionFactory?: (opts: CreateMuSessionOpts) => Promise<MuSession>;
|
|
36
|
-
nowMs?: () => number;
|
|
37
|
-
}): Promise<SessionTurnResult>;
|
|
38
2
|
export declare function sessionTurnRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;
|