@femtomc/mu-server 26.2.73 → 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 -66
- 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_contract.d.ts +1 -7
- 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/outbound_delivery_router.d.ts +12 -0
- package/dist/outbound_delivery_router.js +29 -0
- package/dist/run_supervisor.d.ts +1 -16
- package/dist/run_supervisor.js +0 -70
- 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 -654
- 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 -9
- package/dist/api/context.d.ts +0 -5
- package/dist/api/context.js +0 -1147
- 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
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { createMuSession, DEFAULT_OPERATOR_SYSTEM_PROMPT, DEFAULT_ORCHESTRATOR_PROMPT, DEFAULT_REVIEWER_PROMPT, DEFAULT_WORKER_PROMPT, operatorExtensionPaths, orchestratorToolExtensionPaths, workerToolExtensionPaths, } from "@femtomc/mu-agent";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
export class SessionTurnError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
constructor(status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function nonEmptyString(value) {
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
17
|
+
}
|
|
18
|
+
function asRecord(value) {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function normalizeSessionKind(value) {
|
|
25
|
+
if (!value) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
29
|
+
if (normalized === "cpoperator" || normalized === "control_plane_operator") {
|
|
30
|
+
return "cp_operator";
|
|
31
|
+
}
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
function normalizeExtensionProfile(value) {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const normalized = value.trim().toLowerCase();
|
|
39
|
+
if (normalized === "operator" ||
|
|
40
|
+
normalized === "worker" ||
|
|
41
|
+
normalized === "orchestrator" ||
|
|
42
|
+
normalized === "reviewer" ||
|
|
43
|
+
normalized === "none") {
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function sessionFileStem(sessionId) {
|
|
49
|
+
const normalized = sessionId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
50
|
+
const compact = normalized.replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
51
|
+
return compact.length > 0 ? compact : "session";
|
|
52
|
+
}
|
|
53
|
+
function resolveRepoPath(repoRoot, candidate) {
|
|
54
|
+
return isAbsolute(candidate) ? resolve(candidate) : resolve(repoRoot, candidate);
|
|
55
|
+
}
|
|
56
|
+
function defaultSessionDirForKind(repoRoot, sessionKind) {
|
|
57
|
+
switch (sessionKind) {
|
|
58
|
+
case "operator":
|
|
59
|
+
return join(repoRoot, ".mu", "operator", "sessions");
|
|
60
|
+
case "cp_operator":
|
|
61
|
+
return join(repoRoot, ".mu", "control-plane", "operator-sessions");
|
|
62
|
+
case "orchestrator":
|
|
63
|
+
return join(repoRoot, ".mu", "orchestrator", "sessions");
|
|
64
|
+
case "worker":
|
|
65
|
+
return join(repoRoot, ".mu", "worker", "sessions");
|
|
66
|
+
case "reviewer":
|
|
67
|
+
return join(repoRoot, ".mu", "reviewer", "sessions");
|
|
68
|
+
default:
|
|
69
|
+
return join(repoRoot, ".mu", "control-plane", "operator-sessions");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function pathExists(path) {
|
|
73
|
+
try {
|
|
74
|
+
await stat(path);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function directoryExists(path) {
|
|
82
|
+
try {
|
|
83
|
+
const info = await stat(path);
|
|
84
|
+
return info.isDirectory();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readSessionHeaderId(sessionFile) {
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = await readFile(sessionFile, "utf8");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const firstLine = raw
|
|
99
|
+
.split("\n")
|
|
100
|
+
.map((line) => line.trim())
|
|
101
|
+
.find((line) => line.length > 0);
|
|
102
|
+
if (!firstLine) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(firstLine);
|
|
107
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const header = parsed;
|
|
111
|
+
if (header.type !== "session") {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return nonEmptyString(header.id);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function resolveSessionFileById(opts) {
|
|
121
|
+
const direct = join(opts.sessionDir, `${sessionFileStem(opts.sessionId)}.jsonl`);
|
|
122
|
+
if (await pathExists(direct)) {
|
|
123
|
+
const headerId = await readSessionHeaderId(direct);
|
|
124
|
+
if (headerId === opts.sessionId) {
|
|
125
|
+
return direct;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const entries = await readdir(opts.sessionDir, { withFileTypes: true });
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const filePath = join(opts.sessionDir, entry.name);
|
|
134
|
+
if (filePath === direct) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const headerId = await readSessionHeaderId(filePath);
|
|
138
|
+
if (headerId === opts.sessionId) {
|
|
139
|
+
return filePath;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function extensionPathsForTurn(opts) {
|
|
145
|
+
if (opts.extensionProfile === "none") {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
if (opts.extensionProfile === "operator") {
|
|
149
|
+
return [...operatorExtensionPaths];
|
|
150
|
+
}
|
|
151
|
+
if (opts.extensionProfile === "orchestrator") {
|
|
152
|
+
return [...orchestratorToolExtensionPaths];
|
|
153
|
+
}
|
|
154
|
+
if (opts.extensionProfile === "worker" || opts.extensionProfile === "reviewer") {
|
|
155
|
+
return [...workerToolExtensionPaths];
|
|
156
|
+
}
|
|
157
|
+
if (opts.sessionKind === "operator" || opts.sessionKind === "cp_operator") {
|
|
158
|
+
return [...operatorExtensionPaths];
|
|
159
|
+
}
|
|
160
|
+
if (opts.sessionKind === "orchestrator") {
|
|
161
|
+
return [...orchestratorToolExtensionPaths];
|
|
162
|
+
}
|
|
163
|
+
if (opts.sessionKind === "worker" || opts.sessionKind === "reviewer") {
|
|
164
|
+
return [...workerToolExtensionPaths];
|
|
165
|
+
}
|
|
166
|
+
return [...operatorExtensionPaths];
|
|
167
|
+
}
|
|
168
|
+
function systemPromptForTurn(opts) {
|
|
169
|
+
const role = opts.extensionProfile ?? opts.sessionKind;
|
|
170
|
+
if (role === "operator" || role === "cp_operator") {
|
|
171
|
+
return DEFAULT_OPERATOR_SYSTEM_PROMPT;
|
|
172
|
+
}
|
|
173
|
+
if (role === "orchestrator") {
|
|
174
|
+
return DEFAULT_ORCHESTRATOR_PROMPT;
|
|
175
|
+
}
|
|
176
|
+
if (role === "reviewer") {
|
|
177
|
+
return DEFAULT_REVIEWER_PROMPT;
|
|
178
|
+
}
|
|
179
|
+
if (role === "worker") {
|
|
180
|
+
return DEFAULT_WORKER_PROMPT;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function extractAssistantText(message) {
|
|
185
|
+
if (!message || typeof message !== "object") {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
const record = message;
|
|
189
|
+
if (typeof record.text === "string") {
|
|
190
|
+
return record.text;
|
|
191
|
+
}
|
|
192
|
+
if (typeof record.content === "string") {
|
|
193
|
+
return record.content;
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(record.content)) {
|
|
196
|
+
const parts = [];
|
|
197
|
+
for (const item of record.content) {
|
|
198
|
+
if (typeof item === "string") {
|
|
199
|
+
if (item.trim().length > 0) {
|
|
200
|
+
parts.push(item);
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!item || typeof item !== "object") {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const text = nonEmptyString(item.text);
|
|
208
|
+
if (text) {
|
|
209
|
+
parts.push(text);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return parts.join("\n");
|
|
213
|
+
}
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
function safeLeafId(session) {
|
|
217
|
+
const manager = session.sessionManager;
|
|
218
|
+
if (!manager || typeof manager.getLeafId !== "function") {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const value = manager.getLeafId();
|
|
222
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
223
|
+
}
|
|
224
|
+
function safeSessionId(session) {
|
|
225
|
+
const value = session.sessionId;
|
|
226
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
227
|
+
}
|
|
228
|
+
function safeSessionFile(session) {
|
|
229
|
+
const value = session.sessionFile;
|
|
230
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
231
|
+
}
|
|
232
|
+
async function resolveSessionTarget(opts) {
|
|
233
|
+
const sessionDir = opts.request.session_dir
|
|
234
|
+
? resolveRepoPath(opts.repoRoot, opts.request.session_dir)
|
|
235
|
+
: defaultSessionDirForKind(opts.repoRoot, opts.normalizedSessionKind);
|
|
236
|
+
if (opts.request.session_file) {
|
|
237
|
+
const sessionFile = resolveRepoPath(opts.repoRoot, opts.request.session_file);
|
|
238
|
+
if (!(await pathExists(sessionFile))) {
|
|
239
|
+
throw new SessionTurnError(404, `session_file not found: ${sessionFile}`);
|
|
240
|
+
}
|
|
241
|
+
const headerId = await readSessionHeaderId(sessionFile);
|
|
242
|
+
if (!headerId) {
|
|
243
|
+
throw new SessionTurnError(400, `session_file is missing a valid session header: ${sessionFile}`);
|
|
244
|
+
}
|
|
245
|
+
if (headerId !== opts.request.session_id) {
|
|
246
|
+
throw new SessionTurnError(409, `session_file header id mismatch (expected ${opts.request.session_id}, found ${headerId})`);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
sessionFile,
|
|
250
|
+
sessionDir: opts.request.session_dir ? sessionDir : dirname(sessionFile),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (!(await directoryExists(sessionDir))) {
|
|
254
|
+
throw new SessionTurnError(404, `session directory not found: ${sessionDir}`);
|
|
255
|
+
}
|
|
256
|
+
const sessionFile = await resolveSessionFileById({
|
|
257
|
+
sessionDir,
|
|
258
|
+
sessionId: opts.request.session_id,
|
|
259
|
+
});
|
|
260
|
+
if (!sessionFile) {
|
|
261
|
+
throw new SessionTurnError(404, `session_id not found in ${sessionDir}: ${opts.request.session_id}`);
|
|
262
|
+
}
|
|
263
|
+
return { sessionFile, sessionDir };
|
|
264
|
+
}
|
|
265
|
+
export function parseSessionTurnRequest(body) {
|
|
266
|
+
const sessionId = nonEmptyString(body.session_id);
|
|
267
|
+
if (!sessionId) {
|
|
268
|
+
return { request: null, error: "session_id is required" };
|
|
269
|
+
}
|
|
270
|
+
const messageBody = nonEmptyString(body.body) ?? nonEmptyString(body.message) ?? nonEmptyString(body.prompt);
|
|
271
|
+
if (!messageBody) {
|
|
272
|
+
return { request: null, error: "body (or message/prompt) is required" };
|
|
273
|
+
}
|
|
274
|
+
const extensionProfileRaw = nonEmptyString(body.extension_profile);
|
|
275
|
+
if (extensionProfileRaw && !normalizeExtensionProfile(extensionProfileRaw)) {
|
|
276
|
+
return {
|
|
277
|
+
request: null,
|
|
278
|
+
error: "extension_profile must be one of operator|worker|orchestrator|reviewer|none",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
request: {
|
|
283
|
+
session_id: sessionId,
|
|
284
|
+
session_kind: nonEmptyString(body.session_kind),
|
|
285
|
+
body: messageBody,
|
|
286
|
+
source: nonEmptyString(body.source),
|
|
287
|
+
provider: nonEmptyString(body.provider),
|
|
288
|
+
model: nonEmptyString(body.model),
|
|
289
|
+
thinking: nonEmptyString(body.thinking),
|
|
290
|
+
session_file: nonEmptyString(body.session_file),
|
|
291
|
+
session_dir: nonEmptyString(body.session_dir),
|
|
292
|
+
extension_profile: extensionProfileRaw,
|
|
293
|
+
},
|
|
294
|
+
error: null,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export async function executeSessionTurn(opts) {
|
|
298
|
+
const normalizedSessionKind = normalizeSessionKind(opts.request.session_kind);
|
|
299
|
+
const extensionProfile = normalizeExtensionProfile(opts.request.extension_profile);
|
|
300
|
+
const target = await resolveSessionTarget({
|
|
301
|
+
repoRoot: opts.repoRoot,
|
|
302
|
+
request: opts.request,
|
|
303
|
+
normalizedSessionKind,
|
|
304
|
+
});
|
|
305
|
+
const sessionFactory = opts.sessionFactory ?? createMuSession;
|
|
306
|
+
const session = await sessionFactory({
|
|
307
|
+
cwd: opts.repoRoot,
|
|
308
|
+
systemPrompt: systemPromptForTurn({
|
|
309
|
+
sessionKind: normalizedSessionKind,
|
|
310
|
+
extensionProfile,
|
|
311
|
+
}),
|
|
312
|
+
provider: opts.request.provider ?? undefined,
|
|
313
|
+
model: opts.request.model ?? undefined,
|
|
314
|
+
thinking: opts.request.thinking ?? undefined,
|
|
315
|
+
extensionPaths: extensionPathsForTurn({
|
|
316
|
+
sessionKind: normalizedSessionKind,
|
|
317
|
+
extensionProfile,
|
|
318
|
+
}),
|
|
319
|
+
session: {
|
|
320
|
+
mode: "open",
|
|
321
|
+
sessionDir: target.sessionDir,
|
|
322
|
+
sessionFile: target.sessionFile,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
let assistantText = "";
|
|
326
|
+
let contextEntryId = null;
|
|
327
|
+
let resolvedSessionId = null;
|
|
328
|
+
let resolvedSessionFile = null;
|
|
329
|
+
const nowMs = opts.nowMs ?? Date.now;
|
|
330
|
+
try {
|
|
331
|
+
await session.bindExtensions({
|
|
332
|
+
commandContextActions: {
|
|
333
|
+
waitForIdle: () => session.agent.waitForIdle(),
|
|
334
|
+
newSession: async () => ({ cancelled: true }),
|
|
335
|
+
fork: async () => ({ cancelled: true }),
|
|
336
|
+
navigateTree: async () => ({ cancelled: true }),
|
|
337
|
+
switchSession: async () => ({ cancelled: true }),
|
|
338
|
+
reload: async () => { },
|
|
339
|
+
},
|
|
340
|
+
onError: () => { },
|
|
341
|
+
});
|
|
342
|
+
const unsubscribe = session.subscribe((event) => {
|
|
343
|
+
const rec = asRecord(event);
|
|
344
|
+
if (!rec || rec.type !== "message_end") {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const message = asRecord(rec.message);
|
|
348
|
+
if (!message || message.role !== "assistant") {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const text = extractAssistantText(message);
|
|
352
|
+
if (text.trim().length > 0) {
|
|
353
|
+
assistantText = text;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
try {
|
|
357
|
+
await session.prompt(opts.request.body, { expandPromptTemplates: false });
|
|
358
|
+
await session.agent.waitForIdle();
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
unsubscribe();
|
|
362
|
+
}
|
|
363
|
+
contextEntryId = safeLeafId(session);
|
|
364
|
+
resolvedSessionId = safeSessionId(session) ?? opts.request.session_id;
|
|
365
|
+
resolvedSessionFile = safeSessionFile(session) ?? target.sessionFile;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
try {
|
|
369
|
+
session.dispose();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
// Best effort cleanup.
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const reply = assistantText.trim();
|
|
376
|
+
if (reply.length === 0) {
|
|
377
|
+
throw new SessionTurnError(502, "session turn completed without an assistant reply");
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
session_id: resolvedSessionId ?? opts.request.session_id,
|
|
381
|
+
session_kind: normalizedSessionKind,
|
|
382
|
+
session_file: resolvedSessionFile ?? target.sessionFile,
|
|
383
|
+
context_entry_id: contextEntryId,
|
|
384
|
+
reply,
|
|
385
|
+
source: opts.request.source ?? null,
|
|
386
|
+
completed_at_ms: Math.trunc(nowMs()),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
export async function sessionTurnRoutes(request, url, deps, headers) {
|
|
390
|
+
if (url.pathname !== "/api/session-turn") {
|
|
391
|
+
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
392
|
+
}
|
|
393
|
+
if (request.method !== "POST") {
|
|
394
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
395
|
+
}
|
|
396
|
+
let body;
|
|
397
|
+
try {
|
|
398
|
+
const parsed = (await request.json());
|
|
399
|
+
const rec = asRecord(parsed);
|
|
400
|
+
if (!rec) {
|
|
401
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
402
|
+
}
|
|
403
|
+
body = rec;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
407
|
+
}
|
|
408
|
+
const parsedRequest = parseSessionTurnRequest(body);
|
|
409
|
+
if (!parsedRequest.request) {
|
|
410
|
+
return Response.json({ error: parsedRequest.error ?? "invalid session turn request" }, { status: 400, headers });
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const turn = await executeSessionTurn({
|
|
414
|
+
repoRoot: deps.context.repoRoot,
|
|
415
|
+
request: parsedRequest.request,
|
|
416
|
+
});
|
|
417
|
+
return Response.json({ ok: true, turn }, { headers });
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
const status = error instanceof SessionTurnError ? error.status : 500;
|
|
421
|
+
return Response.json({ error: deps.describeError(error) }, { status, headers });
|
|
422
|
+
}
|
|
423
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export type WakeTurnMode = "off" | "shadow" | "active";
|
|
2
1
|
export type MuConfig = {
|
|
3
2
|
version: 1;
|
|
4
3
|
control_plane: {
|
|
@@ -14,11 +13,13 @@ export type MuConfig = {
|
|
|
14
13
|
bot_token: string | null;
|
|
15
14
|
bot_username: string | null;
|
|
16
15
|
};
|
|
16
|
+
neovim: {
|
|
17
|
+
shared_secret: string | null;
|
|
18
|
+
};
|
|
17
19
|
};
|
|
18
20
|
operator: {
|
|
19
21
|
enabled: boolean;
|
|
20
22
|
run_triggers_enabled: boolean;
|
|
21
|
-
wake_turn_mode: WakeTurnMode;
|
|
22
23
|
provider: string | null;
|
|
23
24
|
model: string | null;
|
|
24
25
|
};
|
|
@@ -38,11 +39,13 @@ export type MuConfigPatch = {
|
|
|
38
39
|
bot_token?: string | null;
|
|
39
40
|
bot_username?: string | null;
|
|
40
41
|
};
|
|
42
|
+
neovim?: {
|
|
43
|
+
shared_secret?: string | null;
|
|
44
|
+
};
|
|
41
45
|
};
|
|
42
46
|
operator?: {
|
|
43
47
|
enabled?: boolean;
|
|
44
48
|
run_triggers_enabled?: boolean;
|
|
45
|
-
wake_turn_mode?: WakeTurnMode;
|
|
46
49
|
provider?: string | null;
|
|
47
50
|
model?: string | null;
|
|
48
51
|
};
|
|
@@ -62,11 +65,13 @@ export type MuConfigPresence = {
|
|
|
62
65
|
bot_token: boolean;
|
|
63
66
|
bot_username: boolean;
|
|
64
67
|
};
|
|
68
|
+
neovim: {
|
|
69
|
+
shared_secret: boolean;
|
|
70
|
+
};
|
|
65
71
|
};
|
|
66
72
|
operator: {
|
|
67
73
|
enabled: boolean;
|
|
68
74
|
run_triggers_enabled: boolean;
|
|
69
|
-
wake_turn_mode: WakeTurnMode;
|
|
70
75
|
provider: boolean;
|
|
71
76
|
model: boolean;
|
|
72
77
|
};
|
package/dist/config.js
CHANGED
|
@@ -15,11 +15,13 @@ export const DEFAULT_MU_CONFIG = {
|
|
|
15
15
|
bot_token: null,
|
|
16
16
|
bot_username: null,
|
|
17
17
|
},
|
|
18
|
+
neovim: {
|
|
19
|
+
shared_secret: null,
|
|
20
|
+
},
|
|
18
21
|
},
|
|
19
22
|
operator: {
|
|
20
23
|
enabled: true,
|
|
21
24
|
run_triggers_enabled: true,
|
|
22
|
-
wake_turn_mode: "off",
|
|
23
25
|
provider: null,
|
|
24
26
|
model: null,
|
|
25
27
|
},
|
|
@@ -54,19 +56,6 @@ function normalizeBoolean(value, fallback) {
|
|
|
54
56
|
}
|
|
55
57
|
return fallback;
|
|
56
58
|
}
|
|
57
|
-
function normalizeWakeTurnMode(value, fallback) {
|
|
58
|
-
if (typeof value !== "string") {
|
|
59
|
-
return fallback;
|
|
60
|
-
}
|
|
61
|
-
const normalized = value.trim().toLowerCase();
|
|
62
|
-
if (normalized === "shadow") {
|
|
63
|
-
return "shadow";
|
|
64
|
-
}
|
|
65
|
-
if (normalized === "active") {
|
|
66
|
-
return "active";
|
|
67
|
-
}
|
|
68
|
-
return "off";
|
|
69
|
-
}
|
|
70
59
|
export function normalizeMuConfig(input) {
|
|
71
60
|
const next = cloneDefault();
|
|
72
61
|
const root = asRecord(input);
|
|
@@ -97,6 +86,10 @@ export function normalizeMuConfig(input) {
|
|
|
97
86
|
next.control_plane.adapters.telegram.bot_username = normalizeNullableString(telegram.bot_username);
|
|
98
87
|
}
|
|
99
88
|
}
|
|
89
|
+
const neovim = asRecord(adapters.neovim);
|
|
90
|
+
if (neovim && "shared_secret" in neovim) {
|
|
91
|
+
next.control_plane.adapters.neovim.shared_secret = normalizeNullableString(neovim.shared_secret);
|
|
92
|
+
}
|
|
100
93
|
}
|
|
101
94
|
const operator = asRecord(controlPlane.operator);
|
|
102
95
|
if (operator) {
|
|
@@ -106,9 +99,6 @@ export function normalizeMuConfig(input) {
|
|
|
106
99
|
if ("run_triggers_enabled" in operator) {
|
|
107
100
|
next.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, next.control_plane.operator.run_triggers_enabled);
|
|
108
101
|
}
|
|
109
|
-
if ("wake_turn_mode" in operator) {
|
|
110
|
-
next.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, next.control_plane.operator.wake_turn_mode);
|
|
111
|
-
}
|
|
112
102
|
if ("provider" in operator) {
|
|
113
103
|
next.control_plane.operator.provider = normalizeNullableString(operator.provider);
|
|
114
104
|
}
|
|
@@ -158,6 +148,16 @@ function normalizeMuConfigPatch(input) {
|
|
|
158
148
|
patch.control_plane.adapters.telegram = telegramPatch;
|
|
159
149
|
}
|
|
160
150
|
}
|
|
151
|
+
const neovim = asRecord(adapters.neovim);
|
|
152
|
+
if (neovim) {
|
|
153
|
+
const neovimPatch = {};
|
|
154
|
+
if ("shared_secret" in neovim) {
|
|
155
|
+
neovimPatch.shared_secret = normalizeNullableString(neovim.shared_secret);
|
|
156
|
+
}
|
|
157
|
+
if (Object.keys(neovimPatch).length > 0) {
|
|
158
|
+
patch.control_plane.adapters.neovim = neovimPatch;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
161
|
}
|
|
162
162
|
const operator = asRecord(controlPlane.operator);
|
|
163
163
|
if (operator) {
|
|
@@ -168,9 +168,6 @@ function normalizeMuConfigPatch(input) {
|
|
|
168
168
|
if ("run_triggers_enabled" in operator) {
|
|
169
169
|
patch.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, DEFAULT_MU_CONFIG.control_plane.operator.run_triggers_enabled);
|
|
170
170
|
}
|
|
171
|
-
if ("wake_turn_mode" in operator) {
|
|
172
|
-
patch.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, DEFAULT_MU_CONFIG.control_plane.operator.wake_turn_mode);
|
|
173
|
-
}
|
|
174
171
|
if ("provider" in operator) {
|
|
175
172
|
patch.control_plane.operator.provider = normalizeNullableString(operator.provider);
|
|
176
173
|
}
|
|
@@ -214,6 +211,9 @@ export function applyMuConfigPatch(base, patchInput) {
|
|
|
214
211
|
next.control_plane.adapters.telegram.bot_username = adapters.telegram.bot_username ?? null;
|
|
215
212
|
}
|
|
216
213
|
}
|
|
214
|
+
if (adapters.neovim && "shared_secret" in adapters.neovim) {
|
|
215
|
+
next.control_plane.adapters.neovim.shared_secret = adapters.neovim.shared_secret ?? null;
|
|
216
|
+
}
|
|
217
217
|
}
|
|
218
218
|
const operator = patch.control_plane.operator;
|
|
219
219
|
if (operator) {
|
|
@@ -223,9 +223,6 @@ export function applyMuConfigPatch(base, patchInput) {
|
|
|
223
223
|
if ("run_triggers_enabled" in operator && typeof operator.run_triggers_enabled === "boolean") {
|
|
224
224
|
next.control_plane.operator.run_triggers_enabled = operator.run_triggers_enabled;
|
|
225
225
|
}
|
|
226
|
-
if ("wake_turn_mode" in operator) {
|
|
227
|
-
next.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, next.control_plane.operator.wake_turn_mode);
|
|
228
|
-
}
|
|
229
226
|
if ("provider" in operator) {
|
|
230
227
|
next.control_plane.operator.provider = operator.provider ?? null;
|
|
231
228
|
}
|
|
@@ -276,6 +273,7 @@ export function redactMuConfigSecrets(config) {
|
|
|
276
273
|
next.control_plane.adapters.discord.signing_secret = redacted(next.control_plane.adapters.discord.signing_secret);
|
|
277
274
|
next.control_plane.adapters.telegram.webhook_secret = redacted(next.control_plane.adapters.telegram.webhook_secret);
|
|
278
275
|
next.control_plane.adapters.telegram.bot_token = redacted(next.control_plane.adapters.telegram.bot_token);
|
|
276
|
+
next.control_plane.adapters.neovim.shared_secret = redacted(next.control_plane.adapters.neovim.shared_secret);
|
|
279
277
|
return next;
|
|
280
278
|
}
|
|
281
279
|
function isPresent(value) {
|
|
@@ -296,11 +294,13 @@ export function muConfigPresence(config) {
|
|
|
296
294
|
bot_token: isPresent(config.control_plane.adapters.telegram.bot_token),
|
|
297
295
|
bot_username: isPresent(config.control_plane.adapters.telegram.bot_username),
|
|
298
296
|
},
|
|
297
|
+
neovim: {
|
|
298
|
+
shared_secret: isPresent(config.control_plane.adapters.neovim.shared_secret),
|
|
299
|
+
},
|
|
299
300
|
},
|
|
300
301
|
operator: {
|
|
301
302
|
enabled: config.control_plane.operator.enabled,
|
|
302
303
|
run_triggers_enabled: config.control_plane.operator.run_triggers_enabled,
|
|
303
|
-
wake_turn_mode: config.control_plane.operator.wake_turn_mode,
|
|
304
304
|
provider: isPresent(config.control_plane.operator.provider),
|
|
305
305
|
model: isPresent(config.control_plane.operator.model),
|
|
306
306
|
},
|
package/dist/control_plane.d.ts
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import type { MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
|
|
2
2
|
import { type GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
|
|
3
3
|
import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type InterRootQueuePolicy, type TelegramGenerationSwapHooks, type WakeDeliveryObserver } from "./control_plane_contract.js";
|
|
4
|
-
import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
5
4
|
import { type ControlPlaneRunSupervisorOpts } from "./run_supervisor.js";
|
|
5
|
+
import { detectAdapters } from "./control_plane_adapter_registry.js";
|
|
6
6
|
export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
|
|
7
|
-
|
|
8
|
-
name: "slack";
|
|
9
|
-
signingSecret: string;
|
|
10
|
-
} | {
|
|
11
|
-
name: "discord";
|
|
12
|
-
signingSecret: string;
|
|
13
|
-
} | {
|
|
14
|
-
name: "telegram";
|
|
15
|
-
webhookSecret: string;
|
|
16
|
-
botToken: string | null;
|
|
17
|
-
botUsername: string | null;
|
|
18
|
-
};
|
|
19
|
-
export declare function detectAdapters(config: ControlPlaneConfig): DetectedAdapter[];
|
|
7
|
+
export { detectAdapters };
|
|
20
8
|
export type TelegramSendMessagePayload = {
|
|
21
9
|
chat_id: string;
|
|
22
10
|
text: string;
|
|
@@ -40,9 +28,7 @@ export type BootstrapControlPlaneOpts = {
|
|
|
40
28
|
config?: ControlPlaneConfig;
|
|
41
29
|
operatorRuntime?: MessagingOperatorRuntime | null;
|
|
42
30
|
operatorBackend?: MessagingOperatorBackend;
|
|
43
|
-
heartbeatScheduler?: ActivityHeartbeatScheduler;
|
|
44
31
|
runSupervisorSpawnProcess?: ControlPlaneRunSupervisorOpts["spawnProcess"];
|
|
45
|
-
runSupervisorHeartbeatIntervalMs?: number;
|
|
46
32
|
sessionLifecycle: ControlPlaneSessionLifecycle;
|
|
47
33
|
generation?: ControlPlaneGenerationContext;
|
|
48
34
|
telemetry?: GenerationTelemetryRecorder | null;
|