@femtomc/mu-agent 26.2.72 → 26.2.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +24 -45
  2. package/dist/extensions/branding.d.ts +1 -1
  3. package/dist/extensions/branding.js +3 -3
  4. package/dist/extensions/index.d.ts +3 -17
  5. package/dist/extensions/index.d.ts.map +1 -1
  6. package/dist/extensions/index.js +5 -19
  7. package/dist/extensions/mu-operator.d.ts +2 -2
  8. package/dist/extensions/mu-operator.d.ts.map +1 -1
  9. package/dist/extensions/mu-operator.js +2 -6
  10. package/dist/extensions/mu-serve.d.ts +2 -2
  11. package/dist/extensions/mu-serve.d.ts.map +1 -1
  12. package/dist/extensions/mu-serve.js +2 -14
  13. package/dist/extensions/shared.d.ts +2 -21
  14. package/dist/extensions/shared.d.ts.map +1 -1
  15. package/dist/extensions/shared.js +0 -90
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/operator.d.ts +15 -0
  20. package/dist/operator.d.ts.map +1 -1
  21. package/dist/operator.js +366 -14
  22. package/dist/session_factory.d.ts +12 -0
  23. package/dist/session_factory.d.ts.map +1 -1
  24. package/dist/session_factory.js +21 -2
  25. package/package.json +2 -3
  26. package/prompts/roles/operator.md +22 -45
  27. package/prompts/roles/orchestrator.md +17 -12
  28. package/prompts/roles/reviewer.md +7 -8
  29. package/prompts/roles/worker.md +6 -11
  30. package/dist/extensions/activities.d.ts +0 -7
  31. package/dist/extensions/activities.d.ts.map +0 -1
  32. package/dist/extensions/activities.js +0 -236
  33. package/dist/extensions/cron.d.ts +0 -7
  34. package/dist/extensions/cron.d.ts.map +0 -1
  35. package/dist/extensions/cron.js +0 -247
  36. package/dist/extensions/heartbeats.d.ts +0 -7
  37. package/dist/extensions/heartbeats.d.ts.map +0 -1
  38. package/dist/extensions/heartbeats.js +0 -192
  39. package/dist/extensions/messaging-setup/actions.d.ts +0 -22
  40. package/dist/extensions/messaging-setup/actions.d.ts.map +0 -1
  41. package/dist/extensions/messaging-setup/actions.js +0 -229
  42. package/dist/extensions/messaging-setup/adapters.d.ts +0 -24
  43. package/dist/extensions/messaging-setup/adapters.d.ts.map +0 -1
  44. package/dist/extensions/messaging-setup/adapters.js +0 -170
  45. package/dist/extensions/messaging-setup/index.d.ts +0 -17
  46. package/dist/extensions/messaging-setup/index.d.ts.map +0 -1
  47. package/dist/extensions/messaging-setup/index.js +0 -261
  48. package/dist/extensions/messaging-setup/parser.d.ts +0 -33
  49. package/dist/extensions/messaging-setup/parser.d.ts.map +0 -1
  50. package/dist/extensions/messaging-setup/parser.js +0 -240
  51. package/dist/extensions/messaging-setup/runtime.d.ts +0 -16
  52. package/dist/extensions/messaging-setup/runtime.d.ts.map +0 -1
  53. package/dist/extensions/messaging-setup/runtime.js +0 -110
  54. package/dist/extensions/messaging-setup/types.d.ts +0 -157
  55. package/dist/extensions/messaging-setup/types.d.ts.map +0 -1
  56. package/dist/extensions/messaging-setup/types.js +0 -4
  57. package/dist/extensions/messaging-setup/ui.d.ts +0 -15
  58. package/dist/extensions/messaging-setup/ui.d.ts.map +0 -1
  59. package/dist/extensions/messaging-setup/ui.js +0 -173
  60. package/dist/extensions/messaging-setup.d.ts +0 -3
  61. package/dist/extensions/messaging-setup.d.ts.map +0 -1
  62. package/dist/extensions/messaging-setup.js +0 -2
  63. package/dist/extensions/mu-full-tools.d.ts +0 -10
  64. package/dist/extensions/mu-full-tools.d.ts.map +0 -1
  65. package/dist/extensions/mu-full-tools.js +0 -25
  66. package/dist/extensions/mu-query-tools.d.ts +0 -10
  67. package/dist/extensions/mu-query-tools.d.ts.map +0 -1
  68. package/dist/extensions/mu-query-tools.js +0 -11
  69. package/dist/extensions/operator-command.d.ts +0 -14
  70. package/dist/extensions/operator-command.d.ts.map +0 -1
  71. package/dist/extensions/operator-command.js +0 -231
  72. package/dist/extensions/orchestration-runs-readonly.d.ts +0 -4
  73. package/dist/extensions/orchestration-runs-readonly.d.ts.map +0 -1
  74. package/dist/extensions/orchestration-runs-readonly.js +0 -226
  75. package/dist/extensions/orchestration-runs.d.ts +0 -4
  76. package/dist/extensions/orchestration-runs.d.ts.map +0 -1
  77. package/dist/extensions/orchestration-runs.js +0 -315
  78. package/dist/extensions/server-tools-readonly.d.ts +0 -4
  79. package/dist/extensions/server-tools-readonly.d.ts.map +0 -1
  80. package/dist/extensions/server-tools-readonly.js +0 -5
  81. package/dist/extensions/server-tools.d.ts +0 -25
  82. package/dist/extensions/server-tools.d.ts.map +0 -1
  83. package/dist/extensions/server-tools.js +0 -833
  84. package/prompts/skills/messaging-setup-brief.md +0 -25
package/dist/operator.js CHANGED
@@ -1,5 +1,6 @@
1
- import { appendJsonl } from "@femtomc/mu-core/node";
2
- import { join } from "node:path";
1
+ import { appendJsonl, readJsonl } from "@femtomc/mu-core/node";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
3
4
  import { z } from "zod";
4
5
  import { CommandContextResolver } from "./command_context.js";
5
6
  import { createMuSession } from "./session_factory.js";
@@ -182,6 +183,179 @@ function buildOperatorFailureFallbackMessage(code) {
182
183
  function conversationKey(inbound, binding) {
183
184
  return `${inbound.channel}:${inbound.channel_tenant_id}:${inbound.channel_conversation_id}:${binding.binding_id}`;
184
185
  }
186
+ function asRecord(value) {
187
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
188
+ return null;
189
+ }
190
+ return value;
191
+ }
192
+ function nonEmptyString(value) {
193
+ if (typeof value !== "string") {
194
+ return null;
195
+ }
196
+ const trimmed = value.trim();
197
+ return trimmed.length > 0 ? trimmed : null;
198
+ }
199
+ function finiteInt(value) {
200
+ if (typeof value !== "number" || !Number.isFinite(value)) {
201
+ return null;
202
+ }
203
+ return Math.trunc(value);
204
+ }
205
+ function stringList(value) {
206
+ if (!Array.isArray(value)) {
207
+ return [];
208
+ }
209
+ const out = [];
210
+ for (const item of value) {
211
+ const parsed = nonEmptyString(item);
212
+ if (parsed) {
213
+ out.push(parsed);
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+ function sessionFlashPath(repoRoot) {
219
+ return join(repoRoot, ".mu", "control-plane", "session_flash.jsonl");
220
+ }
221
+ async function loadPendingSessionFlashes(opts) {
222
+ const rows = await readJsonl(sessionFlashPath(opts.repoRoot));
223
+ const created = new Map();
224
+ const delivered = new Set();
225
+ for (const row of rows) {
226
+ const rec = asRecord(row);
227
+ if (!rec) {
228
+ continue;
229
+ }
230
+ const kind = nonEmptyString(rec.kind);
231
+ if (kind === "session_flash.create") {
232
+ const tsMs = finiteInt(rec.ts_ms) ?? Date.now();
233
+ const flashId = nonEmptyString(rec.flash_id);
234
+ const sessionId = nonEmptyString(rec.session_id);
235
+ const body = nonEmptyString(rec.body);
236
+ if (!flashId || !sessionId || !body || sessionId !== opts.sessionId) {
237
+ continue;
238
+ }
239
+ created.set(flashId, {
240
+ flash_id: flashId,
241
+ created_at_ms: tsMs,
242
+ session_id: sessionId,
243
+ session_kind: nonEmptyString(rec.session_kind),
244
+ body,
245
+ context_ids: stringList(rec.context_ids),
246
+ source: nonEmptyString(rec.source),
247
+ metadata: asRecord(rec.metadata) ?? {},
248
+ });
249
+ continue;
250
+ }
251
+ if (kind === "session_flash.delivery") {
252
+ const flashId = nonEmptyString(rec.flash_id);
253
+ const sessionId = nonEmptyString(rec.session_id);
254
+ if (!flashId || !sessionId || sessionId !== opts.sessionId) {
255
+ continue;
256
+ }
257
+ delivered.add(flashId);
258
+ }
259
+ }
260
+ const pending = [...created.values()]
261
+ .filter((row) => !delivered.has(row.flash_id))
262
+ .sort((a, b) => a.created_at_ms - b.created_at_ms);
263
+ const limit = Math.max(1, Math.trunc(opts.limit ?? 16));
264
+ if (pending.length <= limit) {
265
+ return pending;
266
+ }
267
+ return pending.slice(pending.length - limit);
268
+ }
269
+ async function markSessionFlashesDelivered(opts) {
270
+ if (opts.flashIds.length === 0) {
271
+ return;
272
+ }
273
+ const deduped = [...new Set(opts.flashIds.filter((id) => id.trim().length > 0))];
274
+ for (const flashId of deduped) {
275
+ const row = {
276
+ kind: "session_flash.delivery",
277
+ ts_ms: opts.nowMs,
278
+ flash_id: flashId,
279
+ session_id: opts.sessionId,
280
+ delivered_by: "messaging_operator_runtime",
281
+ note: null,
282
+ };
283
+ await appendJsonl(sessionFlashPath(opts.repoRoot), row);
284
+ }
285
+ }
286
+ export class JsonFileConversationSessionStore {
287
+ #path;
288
+ #loaded = false;
289
+ #bindings = new Map();
290
+ #persistQueue = Promise.resolve();
291
+ constructor(path) {
292
+ this.#path = path;
293
+ }
294
+ async #load() {
295
+ if (this.#loaded) {
296
+ return;
297
+ }
298
+ this.#loaded = true;
299
+ let raw = "";
300
+ try {
301
+ raw = await readFile(this.#path, "utf8");
302
+ }
303
+ catch {
304
+ return;
305
+ }
306
+ if (!raw.trim()) {
307
+ return;
308
+ }
309
+ try {
310
+ const parsed = JSON.parse(raw);
311
+ const bindings = parsed?.bindings;
312
+ if (!bindings || typeof bindings !== "object" || Array.isArray(bindings)) {
313
+ return;
314
+ }
315
+ for (const [key, value] of Object.entries(bindings)) {
316
+ if (typeof key === "string" && key.length > 0 && typeof value === "string" && value.length > 0) {
317
+ this.#bindings.set(key, value);
318
+ }
319
+ }
320
+ }
321
+ catch {
322
+ // Ignore malformed persistence snapshots.
323
+ }
324
+ }
325
+ async #persist() {
326
+ const snapshot = {
327
+ version: 1,
328
+ bindings: Object.fromEntries([...this.#bindings.entries()].sort(([a], [b]) => a.localeCompare(b))),
329
+ };
330
+ await mkdir(dirname(this.#path), { recursive: true });
331
+ const tempPath = `${this.#path}.tmp-${process.pid}-${crypto.randomUUID()}`;
332
+ await writeFile(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
333
+ await rename(tempPath, this.#path);
334
+ }
335
+ async #persistSoon() {
336
+ const runPersist = async () => {
337
+ await this.#persist();
338
+ };
339
+ this.#persistQueue = this.#persistQueue.then(runPersist, runPersist);
340
+ await this.#persistQueue;
341
+ }
342
+ async getSessionId(conversationKey) {
343
+ await this.#load();
344
+ return this.#bindings.get(conversationKey) ?? null;
345
+ }
346
+ async setSessionId(conversationKey, sessionId) {
347
+ await this.#load();
348
+ const current = this.#bindings.get(conversationKey);
349
+ if (current === sessionId) {
350
+ return;
351
+ }
352
+ this.#bindings.set(conversationKey, sessionId);
353
+ await this.#persistSoon();
354
+ }
355
+ async stop() {
356
+ await this.#persistQueue;
357
+ }
358
+ }
185
359
  export class MessagingOperatorRuntime {
186
360
  #backend;
187
361
  #broker;
@@ -189,6 +363,7 @@ export class MessagingOperatorRuntime {
189
363
  #enabledChannels;
190
364
  #sessionIdFactory;
191
365
  #turnIdFactory;
366
+ #conversationSessionStore;
192
367
  #sessionByConversation = new Map();
193
368
  constructor(opts) {
194
369
  this.#backend = opts.backend;
@@ -197,19 +372,40 @@ export class MessagingOperatorRuntime {
197
372
  this.#enabledChannels = opts.enabledChannels ? new Set(opts.enabledChannels.map((v) => v.toLowerCase())) : null;
198
373
  this.#sessionIdFactory = opts.sessionIdFactory ?? defaultSessionId;
199
374
  this.#turnIdFactory = opts.turnIdFactory ?? defaultTurnId;
375
+ this.#conversationSessionStore = opts.conversationSessionStore ?? null;
200
376
  }
201
- #resolveSessionId(inbound, binding) {
377
+ async #resolveSessionId(inbound, binding) {
202
378
  const key = conversationKey(inbound, binding);
203
379
  const existing = this.#sessionByConversation.get(key);
204
380
  if (existing) {
205
381
  return existing;
206
382
  }
383
+ if (this.#conversationSessionStore) {
384
+ try {
385
+ const persisted = await this.#conversationSessionStore.getSessionId(key);
386
+ if (persisted && persisted.length > 0) {
387
+ this.#sessionByConversation.set(key, persisted);
388
+ return persisted;
389
+ }
390
+ }
391
+ catch {
392
+ // Non-fatal persistence lookup failure.
393
+ }
394
+ }
207
395
  const created = this.#sessionIdFactory();
208
396
  this.#sessionByConversation.set(key, created);
397
+ if (this.#conversationSessionStore) {
398
+ try {
399
+ await this.#conversationSessionStore.setSessionId(key, created);
400
+ }
401
+ catch {
402
+ // Non-fatal persistence write failure.
403
+ }
404
+ }
209
405
  return created;
210
406
  }
211
407
  async handleInbound(opts) {
212
- const sessionId = this.#resolveSessionId(opts.inbound, opts.binding);
408
+ const sessionId = await this.#resolveSessionId(opts.inbound, opts.binding);
213
409
  const turnId = this.#turnIdFactory();
214
410
  if (!this.#enabled) {
215
411
  return {
@@ -227,14 +423,46 @@ export class MessagingOperatorRuntime {
227
423
  operatorTurnId: turnId,
228
424
  };
229
425
  }
426
+ let pendingFlashes = [];
427
+ try {
428
+ pendingFlashes = await loadPendingSessionFlashes({
429
+ repoRoot: opts.inbound.repo_root,
430
+ sessionId,
431
+ });
432
+ }
433
+ catch {
434
+ pendingFlashes = [];
435
+ }
436
+ const inboundForBackend = pendingFlashes.length > 0
437
+ ? {
438
+ ...opts.inbound,
439
+ metadata: {
440
+ ...opts.inbound.metadata,
441
+ session_flash_messages: pendingFlashes,
442
+ },
443
+ }
444
+ : opts.inbound;
230
445
  let backendResult;
231
446
  try {
232
447
  backendResult = OperatorBackendTurnResultSchema.parse(await this.#backend.runTurn({
233
448
  sessionId,
234
449
  turnId,
235
- inbound: opts.inbound,
450
+ inbound: inboundForBackend,
236
451
  binding: opts.binding,
237
452
  }));
453
+ if (pendingFlashes.length > 0) {
454
+ try {
455
+ await markSessionFlashesDelivered({
456
+ repoRoot: opts.inbound.repo_root,
457
+ sessionId,
458
+ flashIds: pendingFlashes.map((row) => row.flash_id),
459
+ nowMs: Date.now(),
460
+ });
461
+ }
462
+ catch {
463
+ // Best-effort delivery bookkeeping; do not fail the operator turn.
464
+ }
465
+ }
238
466
  }
239
467
  catch (err) {
240
468
  return {
@@ -284,19 +512,125 @@ export class MessagingOperatorRuntime {
284
512
  async stop() {
285
513
  this.#sessionByConversation.clear();
286
514
  await this.#backend.dispose?.();
515
+ await this.#conversationSessionStore?.stop?.();
287
516
  }
288
517
  }
289
518
  export { DEFAULT_OPERATOR_SYSTEM_PROMPT };
290
- const MU_COMMAND_TOOL_NAME = "mu_command";
519
+ const COMMAND_TOOL_NAME = "command";
520
+ const OPERATOR_PROMPT_CONTEXT_MAX_CHARS = 2_500;
521
+ function compactJsonPreview(value, maxChars = OPERATOR_PROMPT_CONTEXT_MAX_CHARS) {
522
+ let raw = "";
523
+ if (typeof value === "string") {
524
+ raw = value;
525
+ }
526
+ else {
527
+ try {
528
+ raw = JSON.stringify(value);
529
+ }
530
+ catch {
531
+ return null;
532
+ }
533
+ }
534
+ const compact = raw.replace(/\s+/g, " ").trim();
535
+ if (compact.length === 0) {
536
+ return null;
537
+ }
538
+ if (compact.length <= maxChars) {
539
+ return compact;
540
+ }
541
+ const keep = Math.max(1, maxChars - 1);
542
+ return `${compact.slice(0, keep)}…`;
543
+ }
544
+ function extractPromptContext(metadata) {
545
+ for (const key of ["client_context", "context", "editor_context"]) {
546
+ if (!(key in metadata)) {
547
+ continue;
548
+ }
549
+ const value = metadata[key];
550
+ if (value == null) {
551
+ continue;
552
+ }
553
+ if (typeof value === "object" || typeof value === "string") {
554
+ return value;
555
+ }
556
+ }
557
+ return null;
558
+ }
559
+ function buildOperatorPromptContextBlock(metadata) {
560
+ const context = extractPromptContext(metadata);
561
+ if (!context) {
562
+ return [];
563
+ }
564
+ const preview = compactJsonPreview(context);
565
+ if (!preview) {
566
+ return [];
567
+ }
568
+ return ["", "Client context (structured preview):", preview];
569
+ }
570
+ function extractSessionFlashPromptMessages(metadata) {
571
+ const raw = metadata.session_flash_messages;
572
+ if (!Array.isArray(raw)) {
573
+ return [];
574
+ }
575
+ const out = [];
576
+ for (const value of raw) {
577
+ const rec = asRecord(value);
578
+ if (!rec) {
579
+ continue;
580
+ }
581
+ const flashId = nonEmptyString(rec.flash_id);
582
+ const body = nonEmptyString(rec.body);
583
+ const sessionId = nonEmptyString(rec.session_id);
584
+ if (!flashId || !body || !sessionId) {
585
+ continue;
586
+ }
587
+ out.push({
588
+ flash_id: flashId,
589
+ created_at_ms: finiteInt(rec.created_at_ms) ?? 0,
590
+ session_id: sessionId,
591
+ session_kind: nonEmptyString(rec.session_kind),
592
+ body,
593
+ context_ids: stringList(rec.context_ids),
594
+ source: nonEmptyString(rec.source),
595
+ metadata: asRecord(rec.metadata) ?? {},
596
+ });
597
+ }
598
+ out.sort((a, b) => a.created_at_ms - b.created_at_ms);
599
+ return out;
600
+ }
601
+ function buildOperatorPromptFlashBlock(metadata) {
602
+ const flashes = extractSessionFlashPromptMessages(metadata);
603
+ if (flashes.length === 0) {
604
+ return [];
605
+ }
606
+ const lines = ["", `Session flash messages (${flashes.length}):`];
607
+ for (const flash of flashes) {
608
+ const source = flash.source ?? "unknown";
609
+ const contextIds = flash.context_ids.length > 0 ? ` | context_ids=${flash.context_ids.join(",")}` : "";
610
+ const bodyPreview = compactJsonPreview(flash.body, 400) ?? flash.body;
611
+ lines.push(`- [${flash.flash_id}] source=${source}${contextIds}`);
612
+ lines.push(` ${bodyPreview}`);
613
+ }
614
+ lines.push("Treat these as high-priority user-provided context for this session.");
615
+ return lines;
616
+ }
291
617
  function buildOperatorPrompt(input) {
292
- return [
618
+ const lines = [
293
619
  `[Messaging context]`,
294
620
  `channel: ${input.inbound.channel}`,
295
621
  `request_id: ${input.inbound.request_id}`,
296
622
  `repo_root: ${input.inbound.repo_root}`,
297
623
  ``,
298
624
  `User message: ${input.inbound.command_text}`,
299
- ].join("\n");
625
+ ...buildOperatorPromptContextBlock(input.inbound.metadata),
626
+ ...buildOperatorPromptFlashBlock(input.inbound.metadata),
627
+ ];
628
+ return lines.join("\n");
629
+ }
630
+ function sessionFileStem(sessionId) {
631
+ const normalized = sessionId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
632
+ const compact = normalized.replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
633
+ return compact.length > 0 ? compact : `operator-${crypto.randomUUID()}`;
300
634
  }
301
635
  export class PiMessagingOperatorBackend {
302
636
  #provider;
@@ -310,6 +644,8 @@ export class PiMessagingOperatorBackend {
310
644
  #sessionIdleTtlMs;
311
645
  #maxSessions;
312
646
  #auditTurns;
647
+ #persistSessions;
648
+ #sessionDirForRepoRoot;
313
649
  #sessions = new Map();
314
650
  constructor(opts = {}) {
315
651
  this.#provider = opts.provider;
@@ -323,7 +659,10 @@ export class PiMessagingOperatorBackend {
323
659
  this.#sessionIdleTtlMs = Math.max(60_000, Math.trunc(opts.sessionIdleTtlMs ?? 30 * 60 * 1_000));
324
660
  this.#maxSessions = Math.max(1, Math.trunc(opts.maxSessions ?? 32));
325
661
  this.#auditTurns = opts.auditTurns ?? true;
326
- // Command execution routes through the server command pipeline via mu_command.
662
+ this.#persistSessions = opts.persistSessions ?? true;
663
+ this.#sessionDirForRepoRoot =
664
+ opts.sessionDirForRepoRoot ?? ((repoRoot) => join(repoRoot, ".mu", "control-plane", "operator-sessions"));
665
+ // Operator turns can emit structured command proposals captured from tool events.
327
666
  }
328
667
  #disposeSession(sessionId) {
329
668
  const entry = this.#sessions.get(sessionId);
@@ -353,7 +692,19 @@ export class PiMessagingOperatorBackend {
353
692
  this.#disposeSession(sessionId);
354
693
  }
355
694
  }
356
- async #createSession(repoRoot, nowMs) {
695
+ #sessionPersistence(repoRoot, sessionId) {
696
+ if (!this.#persistSessions) {
697
+ return undefined;
698
+ }
699
+ const sessionDir = this.#sessionDirForRepoRoot(repoRoot);
700
+ const sessionFile = join(sessionDir, `${sessionFileStem(sessionId)}.jsonl`);
701
+ return {
702
+ mode: "open",
703
+ sessionDir,
704
+ sessionFile,
705
+ };
706
+ }
707
+ async #createSession(repoRoot, sessionId, nowMs) {
357
708
  const session = await this.#sessionFactory({
358
709
  cwd: repoRoot,
359
710
  systemPrompt: this.#systemPrompt,
@@ -361,6 +712,7 @@ export class PiMessagingOperatorBackend {
361
712
  model: this.#model,
362
713
  thinking: this.#thinking,
363
714
  extensionPaths: this.#extensionPaths,
715
+ session: this.#sessionPersistence(repoRoot, sessionId),
364
716
  });
365
717
  await session.bindExtensions({
366
718
  commandContextActions: {
@@ -391,7 +743,7 @@ export class PiMessagingOperatorBackend {
391
743
  if (existing && existing.repoRoot !== repoRoot) {
392
744
  this.#disposeSession(sessionId);
393
745
  }
394
- const created = await this.#createSession(repoRoot, nowMs);
746
+ const created = await this.#createSession(repoRoot, sessionId, nowMs);
395
747
  this.#sessions.set(sessionId, created);
396
748
  this.#pruneSessions(nowMs);
397
749
  return created;
@@ -447,8 +799,8 @@ export class PiMessagingOperatorBackend {
447
799
  assistantText = parts.join("\n");
448
800
  }
449
801
  }
450
- // Capture mu_command tool calls — structured command proposals.
451
- if (event?.type === "tool_execution_start" && event?.toolName === MU_COMMAND_TOOL_NAME) {
802
+ // Capture command tool calls — structured command proposals.
803
+ if (event?.type === "tool_execution_start" && event?.toolName === COMMAND_TOOL_NAME) {
452
804
  const parsed = OperatorApprovedCommandSchema.safeParse(event.args);
453
805
  if (parsed.success) {
454
806
  capturedCommand = parsed.data;
@@ -476,7 +828,7 @@ export class PiMessagingOperatorBackend {
476
828
  unsub();
477
829
  sessionRecord.lastUsedAtMs = Math.trunc(this.#nowMs());
478
830
  }
479
- // If the operator called mu_command, use the captured structured command.
831
+ // If the operator called command, use the captured structured command.
480
832
  if (capturedCommand) {
481
833
  await this.#auditTurn(input, {
482
834
  outcome: "command",
@@ -1,3 +1,9 @@
1
+ export type MuSessionPersistenceMode = "in-memory" | "continue-recent" | "new" | "open";
2
+ export type MuSessionPersistenceOpts = {
3
+ mode?: MuSessionPersistenceMode;
4
+ sessionDir?: string;
5
+ sessionFile?: string;
6
+ };
1
7
  export type CreateMuSessionOpts = {
2
8
  cwd: string;
3
9
  systemPrompt?: string;
@@ -5,6 +11,7 @@ export type CreateMuSessionOpts = {
5
11
  model?: string;
6
12
  thinking?: string;
7
13
  extensionPaths?: string[];
14
+ session?: MuSessionPersistenceOpts;
8
15
  };
9
16
  export type MuSession = {
10
17
  subscribe: (listener: (event: any) => void) => () => void;
@@ -16,6 +23,11 @@ export type MuSession = {
16
23
  agent: {
17
24
  waitForIdle: () => Promise<void>;
18
25
  };
26
+ sessionId?: string;
27
+ sessionFile?: string;
28
+ sessionManager?: {
29
+ getLeafId?: () => string | null;
30
+ };
19
31
  };
20
32
  export declare function createMuSession(opts: CreateMuSessionOpts): Promise<MuSession>;
21
33
  //# sourceMappingURL=session_factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session_factory.d.ts","sourceRoot":"","sources":["../src/session_factory.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,mBAAmB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACvB,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC1D,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;CAC5C,CAAC;AAEF,wBAAsB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,CAAC,CA2CnF"}
1
+ {"version":3,"file":"session_factory.d.ts","sourceRoot":"","sources":["../src/session_factory.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,wBAAwB,GAAG,WAAW,GAAG,iBAAiB,GAAG,KAAK,GAAG,MAAM,CAAC;AAExF,MAAM,MAAM,wBAAwB,GAAG;IACtC,IAAI,CAAC,EAAE,wBAAwB,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,wBAAwB,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACvB,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC1D,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE;QAChB,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;KAChC,CAAC;CACF,CAAC;AAkCF,wBAAsB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,CAAC,CA2CnF"}
@@ -1,6 +1,25 @@
1
- import { createBashTool, createEditTool, createReadTool, createWriteTool, } from "@mariozechner/pi-coding-agent";
1
+ import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent";
2
2
  import { createMuResourceLoader, resolveModel } from "./backend.js";
3
3
  import { MU_DEFAULT_THEME_NAME, MU_DEFAULT_THEME_PATH } from "./ui_defaults.js";
4
+ function createSessionManager(SessionManager, cwd, sessionOpts) {
5
+ const mode = sessionOpts?.mode ?? (sessionOpts?.sessionFile ? "open" : "continue-recent");
6
+ const sessionDir = sessionOpts?.sessionDir;
7
+ switch (mode) {
8
+ case "continue-recent":
9
+ return SessionManager.continueRecent(cwd, sessionDir);
10
+ case "new":
11
+ return SessionManager.create(cwd, sessionDir);
12
+ case "open": {
13
+ const sessionFile = sessionOpts?.sessionFile?.trim();
14
+ if (!sessionFile) {
15
+ throw new Error("session.mode=open requires session.sessionFile");
16
+ }
17
+ return SessionManager.open(sessionFile, sessionDir);
18
+ }
19
+ default:
20
+ return SessionManager.inMemory(cwd);
21
+ }
22
+ }
4
23
  export async function createMuSession(opts) {
5
24
  const { AuthStorage, createAgentSession, SessionManager, SettingsManager } = await import("@mariozechner/pi-coding-agent");
6
25
  const authStorage = AuthStorage.create();
@@ -31,7 +50,7 @@ export async function createMuSession(opts) {
31
50
  model,
32
51
  tools,
33
52
  thinkingLevel: (opts.thinking ?? "minimal"),
34
- sessionManager: SessionManager.inMemory(opts.cwd),
53
+ sessionManager: createSessionManager(SessionManager, opts.cwd, opts.session),
35
54
  settingsManager,
36
55
  resourceLoader,
37
56
  authStorage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.72",
3
+ "version": "26.2.74",
4
4
  "description": "Shared agent runtime for mu chat, orchestration roles, and serve extensions.",
5
5
  "keywords": [
6
6
  "mu",
@@ -24,11 +24,10 @@
24
24
  "themes/**"
25
25
  ],
26
26
  "dependencies": {
27
- "@femtomc/mu-core": "26.2.72",
27
+ "@femtomc/mu-core": "26.2.74",
28
28
  "@mariozechner/pi-agent-core": "^0.53.0",
29
29
  "@mariozechner/pi-ai": "^0.53.0",
30
30
  "@mariozechner/pi-coding-agent": "^0.53.0",
31
- "@sinclair/typebox": "^0.34.0",
32
31
  "zod": "^4.1.9"
33
32
  }
34
33
  }
@@ -5,57 +5,34 @@ Mission:
5
5
  - Help users with any coding tasks they ask you to handle directly.
6
6
  - Help users inspect repository/control-plane state.
7
7
  - Help users choose safe next actions.
8
- - When needed, propose approved commands using the mu_command tool.
8
+ - Execute reads and mutations through direct `mu` CLI invocation when managing mu state.
9
9
 
10
10
  Available tools:
11
11
  - read: Read file contents
12
- - bash: Execute bash commands
12
+ - bash: Execute shell commands (primary path for `mu` CLI)
13
13
  - edit: Make surgical edits to files
14
14
  - write: Create or overwrite files
15
15
 
16
- You also have access to specialized read/diagnostic tools:
17
- - `mu_status`
18
- - `mu_control_plane`
19
- - `mu_issues`
20
- - `mu_forum`
21
- - `mu_events`
22
- - `mu_runs`
23
- - `mu_activities`
24
- - `mu_heartbeats`
25
- - `mu_cron`
26
- - `mu_messaging_setup`
27
-
28
- Hard Constraints:
29
- - Never perform mutations directly through read/write tools.
30
- - Mutating actions must flow through the `mu_command` tool.
31
- - Use the `mu_command` tool to propose commands. It accepts structured parameters — do NOT emit raw JSON directives in your text output.
32
-
33
- mu_command tool usage:
34
- - Call `mu_command` with `kind` set to the command type and relevant parameters.
35
- - Example: `mu_command({ kind: "run_start", prompt: "ship release" })`
36
- - Example: `mu_command({ kind: "status" })`
37
- - Example: `mu_command({ kind: "issue_get", issue_id: "mu-abc123" })`
38
-
39
- Allowed command kinds:
40
- - `status`
41
- - `ready`
42
- - `issue_list`
43
- - `issue_get`
44
- - `forum_read`
45
- - `run_list`
46
- - `run_status`
47
- - `run_start`
48
- - `run_resume`
49
- - `run_interrupt`
50
-
51
- Efficiency:
52
- - Do NOT pre-fetch status, issues, control-plane, events, or runs at the start of a conversation. Only call diagnostic tools when the user's request specifically requires that information.
53
- - Respond directly to what the user asks. Avoid speculative tool calls.
54
-
55
- Context hygiene for `mu_*` query tools:
56
- - Prefer narrow discovery first (`limit` + filters like `contains`, `status`, `tag`, `source`).
57
- - Then do targeted retrieval by ID (`get`/`status`) with `fields` when available.
58
- - Avoid broad repeated scans when a precise lookup would answer the question.
16
+ CLI-first workflow:
17
+ - Use `bash` + `mu ...` for issue/forum/run/control-plane state operations.
18
+ - Prefer `--pretty` (or `--json` + targeted parsing) for clear, auditable output.
19
+ - Do not use bespoke query/command wrappers; call the CLI surface directly.
20
+
21
+ Example invocation patterns:
22
+ - `bash("mu status --pretty")`
23
+ - `bash("mu issues list --status open --limit 20 --pretty")`
24
+ - `bash("mu forum read issue:mu-abc123 --limit 20 --pretty")`
25
+ - `bash("mu runs start \"ship release\" --max-steps 25 --pretty")`
26
+ - `bash("mu issues close mu-abc123 --outcome success --pretty")`
27
+ - `bash("mu forum post issue:mu-abc123 -m \"done\" --author operator --pretty")`
28
+ - `bash("mu control reload --pretty")`
29
+
30
+ Guardrails:
31
+ - Never hand-edit `.mu/*.jsonl` for normal lifecycle actions; use `mu` CLI commands.
32
+ - Prefer bounded retrieval (`--limit`, scoped filters) before broad scans.
33
+ - Do NOT pre-fetch status/issues/events/runs at conversation start.
34
+ - Fetch only what the user request requires.
35
+ - Keep responses grounded in concrete command results.
59
36
 
60
37
  For normal answers:
61
38
  - Respond in plain text (no directive prefix).