@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/dist/api/session_turn.js
CHANGED
|
@@ -1,398 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
}
|
|
1
|
+
import { executeSessionTurn, parseSessionTurnRequest, SessionTurnError } from "@femtomc/mu-agent";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
const NEOVIM_SHARED_SECRET_HEADER = "x-mu-neovim-secret";
|
|
18
4
|
function asRecord(value) {
|
|
19
5
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
20
6
|
return null;
|
|
21
7
|
}
|
|
22
8
|
return value;
|
|
23
9
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
10
|
+
function secureSecretEqual(expected, provided) {
|
|
11
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
12
|
+
const providedBuf = Buffer.from(provided, "utf8");
|
|
13
|
+
if (expectedBuf.length !== providedBuf.length) {
|
|
78
14
|
return false;
|
|
79
15
|
}
|
|
16
|
+
return timingSafeEqual(expectedBuf, providedBuf);
|
|
80
17
|
}
|
|
81
|
-
async function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
}
|
|
18
|
+
async function verifySessionTurnAuth(request, deps) {
|
|
19
|
+
const config = await deps.loadConfigFromDisk();
|
|
20
|
+
const expected = config.control_plane.adapters.neovim.shared_secret?.trim() ?? "";
|
|
21
|
+
if (expected.length === 0) {
|
|
248
22
|
return {
|
|
249
|
-
|
|
250
|
-
|
|
23
|
+
ok: false,
|
|
24
|
+
status: 503,
|
|
25
|
+
error: "neovim shared secret is not configured",
|
|
251
26
|
};
|
|
252
27
|
}
|
|
253
|
-
|
|
254
|
-
|
|
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)) {
|
|
28
|
+
const provided = request.headers.get(NEOVIM_SHARED_SECRET_HEADER)?.trim() ?? "";
|
|
29
|
+
if (provided.length === 0) {
|
|
276
30
|
return {
|
|
277
|
-
|
|
278
|
-
|
|
31
|
+
ok: false,
|
|
32
|
+
status: 401,
|
|
33
|
+
error: `missing ${NEOVIM_SHARED_SECRET_HEADER} header`,
|
|
279
34
|
};
|
|
280
35
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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");
|
|
36
|
+
if (!secureSecretEqual(expected, provided)) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 401,
|
|
40
|
+
error: `invalid ${NEOVIM_SHARED_SECRET_HEADER}`,
|
|
41
|
+
};
|
|
378
42
|
}
|
|
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
|
-
};
|
|
43
|
+
return { ok: true };
|
|
388
44
|
}
|
|
389
45
|
export async function sessionTurnRoutes(request, url, deps, headers) {
|
|
390
|
-
if (url.pathname !== "/api/
|
|
46
|
+
if (url.pathname !== "/api/control-plane/turn") {
|
|
391
47
|
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
392
48
|
}
|
|
393
49
|
if (request.method !== "POST") {
|
|
394
50
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
395
51
|
}
|
|
52
|
+
const auth = await verifySessionTurnAuth(request, deps);
|
|
53
|
+
if (!auth.ok) {
|
|
54
|
+
return Response.json({ error: auth.error }, { status: auth.status, headers });
|
|
55
|
+
}
|
|
396
56
|
let body;
|
|
397
57
|
try {
|
|
398
58
|
const parsed = (await request.json());
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { findRepoRoot } from "@femtomc/mu-core/node";
|
|
4
|
+
import { findRepoRoot, getStorePaths } from "@femtomc/mu-core/node";
|
|
5
5
|
import { composeServerRuntime, createServerFromRuntime } from "./server.js";
|
|
6
6
|
// Parse CLI flags: --port N, --repo-root PATH
|
|
7
7
|
function parseArgs(argv) {
|
|
@@ -30,11 +30,11 @@ try {
|
|
|
30
30
|
repoRoot = args.repoRoot ?? findRepoRoot();
|
|
31
31
|
}
|
|
32
32
|
catch {
|
|
33
|
-
console.error("Error: Could not
|
|
33
|
+
console.error("Error: Could not resolve a repository root for mu server startup.");
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
36
36
|
const port = args.port;
|
|
37
|
-
const discoveryPath = join(repoRoot
|
|
37
|
+
const discoveryPath = join(getStorePaths(repoRoot).storeDir, "control-plane", "server.json");
|
|
38
38
|
console.log(`Starting mu-server on port ${port}...`);
|
|
39
39
|
console.log(`Repository root: ${repoRoot}`);
|
|
40
40
|
const runtime = await composeServerRuntime({ repoRoot });
|
|
@@ -86,7 +86,7 @@ if (runtime.controlPlane && runtime.controlPlane.activeAdapters.length > 0) {
|
|
|
86
86
|
}
|
|
87
87
|
else {
|
|
88
88
|
console.log(`Health check: http://localhost:${port}/healthz`);
|
|
89
|
-
console.log(`API Status: http://localhost:${port}/api/status`);
|
|
89
|
+
console.log(`API Status: http://localhost:${port}/api/control-plane/status`);
|
|
90
90
|
}
|
|
91
91
|
const cleanup = async () => {
|
|
92
92
|
try {
|
package/dist/config.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export type MuConfig = {
|
|
|
22
22
|
run_triggers_enabled: boolean;
|
|
23
23
|
provider: string | null;
|
|
24
24
|
model: string | null;
|
|
25
|
+
thinking: string | null;
|
|
26
|
+
};
|
|
27
|
+
memory_index: {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
every_ms: number;
|
|
25
30
|
};
|
|
26
31
|
};
|
|
27
32
|
};
|
|
@@ -48,6 +53,11 @@ export type MuConfigPatch = {
|
|
|
48
53
|
run_triggers_enabled?: boolean;
|
|
49
54
|
provider?: string | null;
|
|
50
55
|
model?: string | null;
|
|
56
|
+
thinking?: string | null;
|
|
57
|
+
};
|
|
58
|
+
memory_index?: {
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
every_ms?: number;
|
|
51
61
|
};
|
|
52
62
|
};
|
|
53
63
|
};
|
|
@@ -74,6 +84,11 @@ export type MuConfigPresence = {
|
|
|
74
84
|
run_triggers_enabled: boolean;
|
|
75
85
|
provider: boolean;
|
|
76
86
|
model: boolean;
|
|
87
|
+
thinking: boolean;
|
|
88
|
+
};
|
|
89
|
+
memory_index: {
|
|
90
|
+
enabled: boolean;
|
|
91
|
+
every_ms: number;
|
|
77
92
|
};
|
|
78
93
|
};
|
|
79
94
|
};
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getStorePaths } from "@femtomc/mu-core/node";
|
|
1
2
|
import { chmod, mkdir } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
export const DEFAULT_MU_CONFIG = {
|
|
@@ -24,6 +25,11 @@ export const DEFAULT_MU_CONFIG = {
|
|
|
24
25
|
run_triggers_enabled: true,
|
|
25
26
|
provider: null,
|
|
26
27
|
model: null,
|
|
28
|
+
thinking: null,
|
|
29
|
+
},
|
|
30
|
+
memory_index: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
every_ms: 300_000,
|
|
27
33
|
},
|
|
28
34
|
},
|
|
29
35
|
};
|
|
@@ -56,6 +62,21 @@ function normalizeBoolean(value, fallback) {
|
|
|
56
62
|
}
|
|
57
63
|
return fallback;
|
|
58
64
|
}
|
|
65
|
+
function normalizeInteger(value, fallback, opts = {}) {
|
|
66
|
+
const min = opts.min ?? Number.NEGATIVE_INFINITY;
|
|
67
|
+
const max = opts.max ?? Number.POSITIVE_INFINITY;
|
|
68
|
+
const clamp = (next) => Math.min(max, Math.max(min, Math.trunc(next)));
|
|
69
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
70
|
+
return clamp(value);
|
|
71
|
+
}
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
74
|
+
if (Number.isFinite(parsed)) {
|
|
75
|
+
return clamp(parsed);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return clamp(fallback);
|
|
79
|
+
}
|
|
59
80
|
export function normalizeMuConfig(input) {
|
|
60
81
|
const next = cloneDefault();
|
|
61
82
|
const root = asRecord(input);
|
|
@@ -105,6 +126,18 @@ export function normalizeMuConfig(input) {
|
|
|
105
126
|
if ("model" in operator) {
|
|
106
127
|
next.control_plane.operator.model = normalizeNullableString(operator.model);
|
|
107
128
|
}
|
|
129
|
+
if ("thinking" in operator) {
|
|
130
|
+
next.control_plane.operator.thinking = normalizeNullableString(operator.thinking);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const memoryIndex = asRecord(controlPlane.memory_index);
|
|
134
|
+
if (memoryIndex) {
|
|
135
|
+
if ("enabled" in memoryIndex) {
|
|
136
|
+
next.control_plane.memory_index.enabled = normalizeBoolean(memoryIndex.enabled, next.control_plane.memory_index.enabled);
|
|
137
|
+
}
|
|
138
|
+
if ("every_ms" in memoryIndex) {
|
|
139
|
+
next.control_plane.memory_index.every_ms = normalizeInteger(memoryIndex.every_ms, next.control_plane.memory_index.every_ms, { min: 1_000, max: 86_400_000 });
|
|
140
|
+
}
|
|
108
141
|
}
|
|
109
142
|
return next;
|
|
110
143
|
}
|
|
@@ -174,10 +207,29 @@ function normalizeMuConfigPatch(input) {
|
|
|
174
207
|
if ("model" in operator) {
|
|
175
208
|
patch.control_plane.operator.model = normalizeNullableString(operator.model);
|
|
176
209
|
}
|
|
210
|
+
if ("thinking" in operator) {
|
|
211
|
+
patch.control_plane.operator.thinking = normalizeNullableString(operator.thinking);
|
|
212
|
+
}
|
|
177
213
|
if (Object.keys(patch.control_plane.operator).length === 0) {
|
|
178
214
|
delete patch.control_plane.operator;
|
|
179
215
|
}
|
|
180
216
|
}
|
|
217
|
+
const memoryIndex = asRecord(controlPlane.memory_index);
|
|
218
|
+
if (memoryIndex) {
|
|
219
|
+
const memoryIndexPatch = {};
|
|
220
|
+
if ("enabled" in memoryIndex) {
|
|
221
|
+
memoryIndexPatch.enabled = normalizeBoolean(memoryIndex.enabled, DEFAULT_MU_CONFIG.control_plane.memory_index.enabled);
|
|
222
|
+
}
|
|
223
|
+
if ("every_ms" in memoryIndex) {
|
|
224
|
+
memoryIndexPatch.every_ms = normalizeInteger(memoryIndex.every_ms, DEFAULT_MU_CONFIG.control_plane.memory_index.every_ms, {
|
|
225
|
+
min: 1_000,
|
|
226
|
+
max: 86_400_000,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (Object.keys(memoryIndexPatch).length > 0) {
|
|
230
|
+
patch.control_plane.memory_index = memoryIndexPatch;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
181
233
|
if (patch.control_plane.adapters && Object.keys(patch.control_plane.adapters).length === 0) {
|
|
182
234
|
delete patch.control_plane.adapters;
|
|
183
235
|
}
|
|
@@ -229,11 +281,23 @@ export function applyMuConfigPatch(base, patchInput) {
|
|
|
229
281
|
if ("model" in operator) {
|
|
230
282
|
next.control_plane.operator.model = operator.model ?? null;
|
|
231
283
|
}
|
|
284
|
+
if ("thinking" in operator) {
|
|
285
|
+
next.control_plane.operator.thinking = operator.thinking ?? null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const memoryIndex = patch.control_plane.memory_index;
|
|
289
|
+
if (memoryIndex) {
|
|
290
|
+
if ("enabled" in memoryIndex && typeof memoryIndex.enabled === "boolean") {
|
|
291
|
+
next.control_plane.memory_index.enabled = memoryIndex.enabled;
|
|
292
|
+
}
|
|
293
|
+
if ("every_ms" in memoryIndex && typeof memoryIndex.every_ms === "number" && Number.isFinite(memoryIndex.every_ms)) {
|
|
294
|
+
next.control_plane.memory_index.every_ms = normalizeInteger(memoryIndex.every_ms, next.control_plane.memory_index.every_ms, { min: 1_000, max: 86_400_000 });
|
|
295
|
+
}
|
|
232
296
|
}
|
|
233
297
|
return next;
|
|
234
298
|
}
|
|
235
299
|
export function getMuConfigPath(repoRoot) {
|
|
236
|
-
return join(repoRoot
|
|
300
|
+
return join(getStorePaths(repoRoot).storeDir, "config.json");
|
|
237
301
|
}
|
|
238
302
|
export async function readMuConfigFile(repoRoot) {
|
|
239
303
|
const path = getMuConfigPath(repoRoot);
|
|
@@ -303,6 +367,11 @@ export function muConfigPresence(config) {
|
|
|
303
367
|
run_triggers_enabled: config.control_plane.operator.run_triggers_enabled,
|
|
304
368
|
provider: isPresent(config.control_plane.operator.provider),
|
|
305
369
|
model: isPresent(config.control_plane.operator.model),
|
|
370
|
+
thinking: isPresent(config.control_plane.operator.thinking),
|
|
371
|
+
},
|
|
372
|
+
memory_index: {
|
|
373
|
+
enabled: config.control_plane.memory_index.enabled,
|
|
374
|
+
every_ms: config.control_plane.memory_index.every_ms,
|
|
306
375
|
},
|
|
307
376
|
},
|
|
308
377
|
};
|
package/dist/control_plane.js
CHANGED
|
@@ -421,7 +421,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
421
421
|
deliver: async (record) => {
|
|
422
422
|
const telegramBotToken = telegramManager.activeBotToken();
|
|
423
423
|
if (!telegramBotToken) {
|
|
424
|
-
return { kind: "retry", error: "telegram bot token not configured in
|
|
424
|
+
return { kind: "retry", error: "telegram bot token not configured in mu workspace config" };
|
|
425
425
|
}
|
|
426
426
|
const richPayload = buildTelegramSendMessagePayload({
|
|
427
427
|
chatId: record.envelope.channel_conversation_id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApprovedCommandBroker, CommandContextResolver, JsonFileConversationSessionStore, MessagingOperatorRuntime, operatorExtensionPaths, PiMessagingOperatorBackend, } from "@femtomc/mu-agent";
|
|
2
|
+
import { ControlPlaneOutboxDispatcher, getControlPlanePaths, } from "@femtomc/mu-control-plane";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
import { ControlPlaneOutboxDispatcher, } from "@femtomc/mu-control-plane";
|
|
4
4
|
const OUTBOX_DRAIN_INTERVAL_MS = 500;
|
|
5
5
|
export function buildMessagingOperatorRuntime(opts) {
|
|
6
6
|
if (!opts.config.operator.enabled) {
|
|
@@ -10,9 +10,10 @@ export function buildMessagingOperatorRuntime(opts) {
|
|
|
10
10
|
new PiMessagingOperatorBackend({
|
|
11
11
|
provider: opts.config.operator.provider ?? undefined,
|
|
12
12
|
model: opts.config.operator.model ?? undefined,
|
|
13
|
+
thinking: opts.config.operator.thinking ?? undefined,
|
|
13
14
|
extensionPaths: operatorExtensionPaths,
|
|
14
15
|
});
|
|
15
|
-
const conversationSessionStore = new JsonFileConversationSessionStore(join(opts.repoRoot
|
|
16
|
+
const conversationSessionStore = new JsonFileConversationSessionStore(join(getControlPlanePaths(opts.repoRoot).controlPlaneDir, "operator_conversations.json"));
|
|
16
17
|
return new MessagingOperatorRuntime({
|
|
17
18
|
backend,
|
|
18
19
|
broker: new ApprovedCommandBroker({
|
|
@@ -136,7 +136,7 @@ export type ControlPlaneHandle = {
|
|
|
136
136
|
getRun?(idOrRoot: string): Promise<ControlPlaneRunSnapshot | null>;
|
|
137
137
|
/**
|
|
138
138
|
* Run lifecycle boundary: accepts start intent into the default queue/reconcile path.
|
|
139
|
-
*
|
|
139
|
+
* Queue coordinators may dispatch immediately after enqueue, but must preserve queue invariants.
|
|
140
140
|
*/
|
|
141
141
|
startRun?(opts: {
|
|
142
142
|
prompt: string;
|
package/dist/cron_programs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { FsJsonlStore } from "@femtomc/mu-core/node";
|
|
2
|
+
import { FsJsonlStore, getStorePaths } from "@femtomc/mu-core/node";
|
|
3
3
|
import { computeNextScheduleRunAtMs, normalizeCronSchedule } from "./cron_schedule.js";
|
|
4
4
|
import { CronTimerRegistry } from "./cron_timer.js";
|
|
5
5
|
const CRON_PROGRAMS_FILENAME = "cron.jsonl";
|
|
@@ -86,7 +86,8 @@ export class CronProgramRegistry {
|
|
|
86
86
|
this.#nowMs = opts.nowMs ?? defaultNowMs;
|
|
87
87
|
this.#timer = opts.timer ?? new CronTimerRegistry({ nowMs: this.#nowMs });
|
|
88
88
|
this.#store =
|
|
89
|
-
opts.store ??
|
|
89
|
+
opts.store ??
|
|
90
|
+
new FsJsonlStore(join(getStorePaths(opts.repoRoot).storeDir, CRON_PROGRAMS_FILENAME));
|
|
90
91
|
void this.#ensureLoaded().catch(() => {
|
|
91
92
|
// Best effort eager load for startup re-arming.
|
|
92
93
|
});
|