@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.
@@ -1,398 +1,58 @@
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
- }
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 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 {
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 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
- }
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
- sessionFile,
250
- sessionDir: opts.request.session_dir ? sessionDir : dirname(sessionFile),
23
+ ok: false,
24
+ status: 503,
25
+ error: "neovim shared secret is not configured",
251
26
  };
252
27
  }
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)) {
28
+ const provided = request.headers.get(NEOVIM_SHARED_SECRET_HEADER)?.trim() ?? "";
29
+ if (provided.length === 0) {
276
30
  return {
277
- request: null,
278
- error: "extension_profile must be one of operator|worker|orchestrator|reviewer|none",
31
+ ok: false,
32
+ status: 401,
33
+ error: `missing ${NEOVIM_SHARED_SECRET_HEADER} header`,
279
34
  };
280
35
  }
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");
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/session-turn") {
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 find .mu directory. Run 'mu serve' or 'mu run' once to initialize it.");
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, ".mu", "control-plane", "server.json");
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, ".mu", "config.json");
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
  };
@@ -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 .mu/config.json" };
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, ".mu", "control-plane", "operator_conversations.json"));
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
- * Compatibility adapters may dispatch immediately after enqueue, but must preserve queue invariants.
139
+ * Queue coordinators may dispatch immediately after enqueue, but must preserve queue invariants.
140
140
  */
141
141
  startRun?(opts: {
142
142
  prompt: string;
@@ -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 ?? new FsJsonlStore(join(opts.repoRoot, ".mu", CRON_PROGRAMS_FILENAME));
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
  });