mixdog 1.3.0 → 1.3.1

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 (103) hide show
  1. package/NOTICE.md +52 -9
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -1
  4. package/src/defaults/skills/setup/references/actions.md +4 -1
  5. package/src/defaults/skills/setup-guide/SKILL.md +10 -7
  6. package/src/output-styles/common.md +13 -8
  7. package/src/rules/agent-mode/AGENT_MODE.md +1 -1
  8. package/src/rules/agent-mode/MIXY.md +11 -4
  9. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +72 -14
  10. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +2 -1
  11. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  12. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  13. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  14. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  15. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  16. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  17. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  18. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  19. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  20. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -2
  21. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +14 -4
  22. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +37 -7
  23. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +5 -3
  24. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +4 -0
  25. package/src/runtime/agent/orchestrator/session/agent-room.mjs +17 -8
  26. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +32 -5
  27. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  28. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  29. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +8 -14
  30. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  31. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  32. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  33. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -20
  34. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  36. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  38. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +17 -1
  39. package/src/runtime/agent/orchestrator/session/routine-result.mjs +61 -9
  40. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +14 -9
  41. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  42. package/src/runtime/attachments/store.mjs +6 -0
  43. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  44. package/src/runtime/effort-judge/model-manifest.json +7 -7
  45. package/src/runtime/shared/agent-kind.mjs +152 -0
  46. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  47. package/src/runtime/shared/agent-route-config.mjs +22 -8
  48. package/src/runtime/shared/agent-store.mjs +132 -0
  49. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  50. package/src/runtime/shared/config-patch.mjs +20 -1
  51. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  52. package/src/runtime/shared/connector-catalog.mjs +2 -4
  53. package/src/runtime/shared/connector-triggers.mjs +2 -0
  54. package/src/runtime/shared/err-text.mjs +3 -1
  55. package/src/runtime/shared/error-presentation.mjs +91 -6
  56. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  57. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  58. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  59. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  60. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  61. package/src/runtime/shared/plain-preview.mjs +22 -17
  62. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  63. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  64. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  65. package/src/runtime/shared/user-data-guard.mjs +5 -2
  66. package/src/session-runtime/agent-home-delivery.mjs +423 -56
  67. package/src/session-runtime/agent-room-delivery.mjs +45 -4
  68. package/src/session-runtime/agent-task-sessions.mjs +299 -39
  69. package/src/session-runtime/boot/apis.mjs +13 -0
  70. package/src/session-runtime/boot/providers.mjs +14 -4
  71. package/src/session-runtime/config-lifecycle.mjs +43 -2
  72. package/src/session-runtime/handoff-attachments.mjs +593 -0
  73. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +27 -3
  74. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  75. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  76. package/src/session-runtime/model-route/set-route.mjs +21 -1
  77. package/src/session-runtime/native-web-search.mjs +2 -2
  78. package/src/session-runtime/new-session-config.mjs +6 -1
  79. package/src/session-runtime/route-resolution.mjs +5 -11
  80. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  81. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  82. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  83. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  84. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  85. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  86. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  87. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -0
  88. package/src/session-runtime/tool-policy-refresh.mjs +13 -2
  89. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +11 -0
  90. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +123 -32
  91. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  92. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  93. package/src/standalone/daemon-session-control.mjs +13 -5
  94. package/src/standalone/daemon.mjs +16 -4
  95. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  96. package/src/standalone/session-service/agent-tree.mjs +4 -3
  97. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  98. package/src/tui/dist/index.mjs +70 -9
  99. package/src/tui/session/notice-surface.mjs +3 -4
  100. package/src/tui/session/session-api/routes.mjs +14 -3
  101. package/src/tui/session/transcript-intake.mjs +2 -0
  102. package/src/tui/session/turn-live-call.mjs +11 -3
  103. package/src/tui/session/turn.mjs +13 -3
@@ -12,6 +12,7 @@ import {
12
12
  clearAgentDefinitionCache,
13
13
  } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
14
14
  import {
15
+ BOT_AGENT_KIND,
15
16
  canonicalizeAgentRouteStorage,
16
17
  isAgentDisabled,
17
18
  WORKFLOW_AGENT_KIND,
@@ -43,9 +44,23 @@ function connectorPolicyInput(value) {
43
44
  return storedConnectorPolicy(parseConnectorPolicy(value));
44
45
  }
45
46
  import { STANDALONE_ROOT } from '../runtime-paths.mjs';
47
+ import { homeThreadEvidence } from '../../runtime/shared/agent-kind.mjs';
48
+ import { BOT_SPLIT_BACKUP_DIRS, migrateBotsToOwnStore } from '../../runtime/shared/bot-split-migration.mjs';
49
+ import { backupUserData } from '../../runtime/shared/user-data-guard.mjs';
50
+ import { normalizeWorkflowRoute } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
51
+ import {
52
+ botDir,
53
+ botStoreActive,
54
+ migratedBotIds,
55
+ mixyIdentityDir,
56
+ readBotRoute,
57
+ readMixyRoute,
58
+ userDefinitionDir,
59
+ } from '../../runtime/shared/agent-store.mjs';
46
60
 
47
61
  const isFixedAgent = (id) => FIXED_AGENT_SLOTS.some((agent) => agent.id === id);
48
- const hasUserAgentFile = (dataDir, id) => existsSync(join(dataDir, 'agents', id, 'AGENT.md'));
62
+ const userDir = userDefinitionDir;
63
+ const hasUserAgentFile = (dataDir, id) => existsSync(join(userDir(dataDir, id), 'AGENT.md'));
49
64
  // An archived (deleted) agent's AGENT.md, moved aside beside the tombstone:
50
65
  // no loader reads it, and restoring moves it back.
51
66
  const ARCHIVED_AGENT_FILE = 'AGENT.md.archived';
@@ -57,28 +72,36 @@ function hiddenFrontmatter(definition) {
57
72
  return rest;
58
73
  }
59
74
 
60
- // The definition's persisted `kind` marker (isContactAgentId reads it), or
61
- // null: only the Workflows page's create writes one (`kind: agent`).
75
+ // The definition's kind (isContactAgentId reads it), or null: the explicit
76
+ // marker bot creation (`kind: bot`) or the Workflows page (`kind: agent`)
77
+ // wrote, else the read-only decision for an unmarked one (agent-kind.mjs).
62
78
  function kindOf(definition) {
63
- const kind = definition?.frontmatter?.kind;
64
- return typeof kind === 'string' && kind.trim() ? kind.trim() : null;
79
+ return definition?.kind || null;
65
80
  }
66
81
 
67
- // A save's `kind`: given only when creating, and only as the workflow-agent
68
- // marker. Unmarked agents are contacts (봇); a kind never changes later.
82
+ // A save's `kind`: given only when creating, as the bot or workflow-agent
83
+ // marker. Unmarked agents are workflow agents; a kind never changes later.
69
84
  function creationKind(payload, existed) {
70
85
  if (!hasOwn(payload, 'kind')) return null;
71
86
  if (existed) throw new Error("an agent's kind is set when it is created and cannot change");
72
- if (payload.kind !== WORKFLOW_AGENT_KIND) throw new Error(`agent kind must be "${WORKFLOW_AGENT_KIND}" or absent`);
73
- return WORKFLOW_AGENT_KIND;
87
+ if (payload.kind !== BOT_AGENT_KIND && payload.kind !== WORKFLOW_AGENT_KIND) {
88
+ throw new Error(`agent kind must be "${BOT_AGENT_KIND}", "${WORKFLOW_AGENT_KIND}" or absent`);
89
+ }
90
+ return payload.kind;
74
91
  }
75
92
 
76
93
  function readManifest(dir) {
94
+ return readManifestStrict(join(dir, 'agent.json')) || {};
95
+ }
96
+
97
+ // The manifest object at `file`, or null when it is missing, unreadable or
98
+ // not a JSON object.
99
+ function readManifestStrict(file) {
77
100
  try {
78
- const value = JSON.parse(readFileSync(join(dir, 'agent.json'), 'utf8'));
79
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
101
+ const value = JSON.parse(readFileSync(file, 'utf8'));
102
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
80
103
  } catch {
81
- return {};
104
+ return null;
82
105
  }
83
106
  }
84
107
 
@@ -238,13 +261,18 @@ function writeAgentFiles(dir, { name, description, body, extra = {}, manifest =
238
261
  // user-authored agents are custom definitions under agents/<id>/AGENT.md.
239
262
  export function createAgentEditorApi(deps) {
240
263
  const { getConfig, loadAgentDefinition, listCustomAgentIds, saveConfigAndAdopt, setAgentRoute } = deps;
241
- const routeFor = (config, id) => effectiveAgentRoute(deps, config, id);
264
+ // A store bot's route lives in its folder; every other agent's in the config.
265
+ const ownRoute = (config, id) => {
266
+ const bot = readBotRoute(resolveDataDir(deps), id);
267
+ return bot !== undefined ? bot : deps.agentRouteFromConfig(config, id);
268
+ };
269
+ const routeFor = (config, id) => effectiveAgentRoute({ ...deps, agentRouteFromConfig: ownRoute }, config, id);
242
270
  // `route` falls back to Main; this says whether the agent has a route of
243
271
  // its own, so a surface never shows (or saves) the Main fallback as one.
244
- const routeConfigured = (config, id) => Boolean(deps.agentRouteFromConfig(config, id));
272
+ const routeConfigured = (config, id) => Boolean(ownRoute(config, id));
245
273
  const shippedDir = (id) => join(deps.rootDir ?? STANDALONE_ROOT, 'agents', id);
246
274
  // The manifest in effect: the user's own copy if any, else the shipped one.
247
- const manifestDirOf = (dataDir, id) => (hasUserAgentFile(dataDir, id) ? join(dataDir, 'agents', id) : shippedDir(id));
275
+ const manifestDirOf = (dataDir, id) => (hasUserAgentFile(dataDir, id) ? userDir(dataDir, id) : shippedDir(id));
248
276
  const manifestOf = (dataDir, id) => readManifest(manifestDirOf(dataDir, id));
249
277
  const avatarOf = (dataDir, id) => normalizeAgentAvatar(manifestOf(dataDir, id).avatar);
250
278
  // A saved, archived or restored bot reaches the prompts that embed it: the
@@ -292,7 +320,7 @@ export function createAgentEditorApi(deps) {
292
320
  // writes, from the copy in effect. An editor that loaded it can send it back
293
321
  // as `expectedRevision`, and a save over a newer copy is refused.
294
322
  function definitionRevision(dataDir, id) {
295
- const dir = hasUserAgentFile(dataDir, id) ? join(dataDir, 'agents', id) : shippedDir(id);
323
+ const dir = hasUserAgentFile(dataDir, id) ? userDir(dataDir, id) : shippedDir(id);
296
324
  const hash = createHash('sha256');
297
325
  for (const name of ['agent.json', 'AGENT.md', PERSONA_FILE]) {
298
326
  hash.update(`${name}\0`);
@@ -326,7 +354,7 @@ export function createAgentEditorApi(deps) {
326
354
  kind: kindOf(definition),
327
355
  disabled: isAgentDisabled(getConfig(), id),
328
356
  avatar: avatarOf(dataDir, id),
329
- persona: readPersona(hasUserAgentFile(dataDir, id) ? join(dataDir, 'agents', id) : shippedDir(id)),
357
+ persona: readPersona(hasUserAgentFile(dataDir, id) ? userDir(dataDir, id) : shippedDir(id)),
330
358
  revision: definitionRevision(dataDir, id),
331
359
  };
332
360
  }
@@ -345,7 +373,14 @@ export function createAgentEditorApi(deps) {
345
373
  id ||
346
374
  availableAgentId(
347
375
  name,
348
- (candidate) => Boolean(loadAgentDefinition(dataDir, candidate)) || isArchivedAgent(dataDir, candidate)
376
+ (candidate) =>
377
+ Boolean(loadAgentDefinition(dataDir, candidate)) ||
378
+ isArchivedAgent(dataDir, candidate) ||
379
+ // An id the split copied stays the bot's, even once purged; a
380
+ // folder in either store keeps its id too.
381
+ migratedBotIds(dataDir).has(candidate) ||
382
+ (botStoreActive(dataDir) &&
383
+ (existsSync(join(dataDir, 'agents', candidate)) || existsSync(botDir(dataDir, candidate))))
349
384
  ),
350
385
  name,
351
386
  description: oneLine(payload.description),
@@ -371,7 +406,21 @@ export function createAgentEditorApi(deps) {
371
406
  const kind = creationKind(payload, existed);
372
407
  const definition = { name: name || id, description, body };
373
408
  const manifest = hiddenManifest(manifestOf(dataDir, id));
374
- const dir = join(dataDir, 'agents', id);
409
+ // With the bot store, a new bot is made in bots/ and a new agent in
410
+ // agents/; either keeps its `kind` marker, so its kind never rests on
411
+ // the folder alone (a copy read without the store is still a bot).
412
+ const storeBot = !existed && kind === BOT_AGENT_KIND && botStoreActive(dataDir);
413
+ // An id is one definition's in one store: a new bot never takes an id
414
+ // agents/ holds (an archived agent's included), a new agent never one of
415
+ // bots/ (or one the split moved there, even if since deleted).
416
+ if (!existed && botStoreActive(dataDir)) {
417
+ const taken = storeBot
418
+ ? existsSync(join(dataDir, 'agents', id))
419
+ : existsSync(botDir(dataDir, id)) || migratedBotIds(dataDir).has(id);
420
+ if (taken) throw new Error(`agent id "${id}" is already in use; pick another name`);
421
+ }
422
+ const dir = storeBot ? botDir(dataDir, id) : userDir(dataDir, id);
423
+ const inStore = dir === botDir(dataDir, id);
375
424
  // New custom images arrive as checked bytes, stored under content hashes.
376
425
  const update = hasOwn(payload, 'avatar') ? avatarUpdate(payload.avatar, dir) : null;
377
426
  if (update?.avatar) manifest.avatar = update.avatar;
@@ -389,6 +438,7 @@ export function createAgentEditorApi(deps) {
389
438
  // Equal to the shipped agent: keep no copy so it follows the shipped
390
439
  // version — unless the manifest (an avatar) or a persona differs.
391
440
  const absorbed =
441
+ !inStore &&
392
442
  !persona &&
393
443
  stableJson(manifest) === stableJson(hiddenManifest(readManifest(shippedDir(id)))) &&
394
444
  absorbSaveEqualToShipped({
@@ -440,7 +490,7 @@ export function createAgentEditorApi(deps) {
440
490
  if (isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and cannot be deleted`);
441
491
  const builtIn = isFixedAgent(id);
442
492
  const dataDir = resolveDataDir(deps);
443
- const dir = join(dataDir, 'agents', id);
493
+ const dir = userDir(dataDir, id);
444
494
  if (!hasUserAgentFile(dataDir, id)) {
445
495
  if (builtIn) throw new Error(`agent "${id}" has no user override to reset`);
446
496
  if (!loadAgentDefinition(dataDir, id)) throw new Error(`agent "${id}" not found`);
@@ -470,7 +520,7 @@ export function createAgentEditorApi(deps) {
470
520
  return { id, deleted: true, revertedToBuiltIn: false };
471
521
  }
472
522
 
473
- const isArchivedAgent = (dataDir, id) => existsSync(join(dataDir, 'agents', id, AGENT_DELETED_MARKER));
523
+ const isArchivedAgent = (dataDir, id) => existsSync(join(userDir(dataDir, id), AGENT_DELETED_MARKER));
474
524
 
475
525
  /** An archived agent back as it was, under its own id: its AGENT.md moved
476
526
  * back (a starter without a copy of its own returns to the packaged one)
@@ -480,7 +530,7 @@ export function createAgentEditorApi(deps) {
480
530
  if (!id || isHiddenAgent(id) || isFixedAgent(id)) throw new Error(`unknown agent "${agentId}"`);
481
531
  const dataDir = resolveDataDir(deps);
482
532
  if (!isArchivedAgent(dataDir, id)) throw new Error(`agent "${id}" is not deleted`);
483
- const dir = join(dataDir, 'agents', id);
533
+ const dir = userDir(dataDir, id);
484
534
  const kept = join(dir, ARCHIVED_AGENT_FILE);
485
535
  if (!existsSync(kept) && !existsSync(join(shippedDir(id), 'AGENT.md'))) {
486
536
  throw new Error(`agent "${id}" was deleted before deleted agents were kept, so it cannot be restored`);
@@ -501,7 +551,7 @@ export function createAgentEditorApi(deps) {
501
551
  const dataDir = resolveDataDir(deps);
502
552
  // Never a live agent; one deleted before archiving existed left nothing.
503
553
  if (!isArchivedAgent(dataDir, id)) return { id, purged: false };
504
- const dir = join(dataDir, 'agents', id);
554
+ const dir = userDir(dataDir, id);
505
555
  rmSync(dir, { recursive: true, force: true });
506
556
  clearAgentDefinitionCache(id);
507
557
  dropAgentRoute(id);
@@ -522,7 +572,7 @@ export function createAgentEditorApi(deps) {
522
572
  ids.map((agentId) => {
523
573
  const id = agentEditorId(agentId);
524
574
  if (!id || id !== agentId || !isArchivedAgent(dataDir, id)) return [agentId, null];
525
- const manifest = readManifest(join(dataDir, 'agents', id));
575
+ const manifest = readManifest(userDir(dataDir, id));
526
576
  return [agentId, { name: oneLine(manifest.name) || id, avatar: normalizeAgentAvatar(manifest.avatar) ?? null }];
527
577
  })
528
578
  );
@@ -547,9 +597,9 @@ export function createAgentEditorApi(deps) {
547
597
  const presence = (agentId) => {
548
598
  const id = agentEditorId(agentId);
549
599
  if (!id || id !== agentId) return 'unknown';
550
- const user = probe(join(dataDir, 'agents', id, 'AGENT.md'));
600
+ const user = probe(join(userDir(dataDir, id), 'AGENT.md'));
551
601
  if (user !== 'absent') return user;
552
- const tombstone = probe(join(dataDir, 'agents', id, AGENT_DELETED_MARKER));
602
+ const tombstone = probe(join(userDir(dataDir, id), AGENT_DELETED_MARKER));
553
603
  if (tombstone === 'present') return 'deleted';
554
604
  if (tombstone === 'unknown') return 'unknown';
555
605
  const shipped = probe(join(rootDir, 'agents', id, 'AGENT.md'));
@@ -566,9 +616,10 @@ export function createAgentEditorApi(deps) {
566
616
  const id = agentEditorId(agentId);
567
617
  if (!id || isHiddenAgent(id)) return null;
568
618
  const dataDir = resolveDataDir(deps);
569
- // Mixy's face lives in agents/lead/agent.json, beside no AGENT.md of its own.
619
+ // Mixy's face lives in its identity folder's agent.json (mixyIdentityDir:
620
+ // bots/mixy with the bot store, agents/lead before), beside no AGENT.md.
570
621
  const lead = id === LEAD_AGENT_ID;
571
- const dir = join(dataDir, 'agents', id);
622
+ const dir = lead ? leadDir() : userDir(dataDir, id);
572
623
  // An archived agent's face stays readable for its archived chat.
573
624
  const archived = !lead && isArchivedAgent(dataDir, id);
574
625
  const avatar = lead
@@ -587,8 +638,10 @@ export function createAgentEditorApi(deps) {
587
638
 
588
639
  // Mixy (the Lead) has no agent definition to edit: only its PERSONA.md
589
640
  // (성격) and AGENT.md (일하는 방식, agent mode only), read by the runtime
590
- // from agents/lead/. Saved empty, the file goes.
591
- const leadDir = () => join(resolveDataDir(deps), 'agents', LEAD_AGENT_ID);
641
+ // from Mixy's identity folder: bots/mixy once the bot store is active,
642
+ // agents/lead before (mixyIdentityDir, agent-store.mjs). Saved empty, the
643
+ // file goes.
644
+ const leadDir = () => mixyIdentityDir(resolveDataDir(deps));
592
645
  function readLeadFile(file) {
593
646
  try {
594
647
  return readFileSync(join(leadDir(), file), 'utf8').trim();
@@ -614,16 +667,27 @@ export function createAgentEditorApi(deps) {
614
667
  const saveLeadAgentBody = (text) => saveLeadFile('AGENT.md', text);
615
668
 
616
669
  // Whether Mixy was created (the "대표 에이전트 만들기" first run): the
617
- // explicit `created` flag in agents/lead/agent.json. null until decided —
670
+ // explicit `created` flag in Mixy's agent.json (leadDir). null until decided —
618
671
  // the desktop host decides once (an existing profile counts as created).
619
672
  // Also Mixy's display name (`name`, read by the runtime's identity line) and
620
673
  // face (`avatar`, as every agent manifest): '' / null mean the defaults.
674
+ // `homeChatted`: whether Mixy's home thread holds any conversation (the
675
+ // host reads it to tell v1.3.0's never-used automatic Mixy apart).
621
676
  function getLeadStatus() {
622
677
  const manifest = readManifest(leadDir());
678
+ // A v1.3.0 automatic Mixy the split left in agents/lead still counts as
679
+ // decided (its routines keep running) until bots/mixy says otherwise.
680
+ const legacy = readManifest(join(resolveDataDir(deps), 'agents', LEAD_AGENT_ID)).created;
681
+ let created = null;
682
+ if (typeof manifest.created === 'boolean') created = manifest.created;
683
+ else if (typeof legacy === 'boolean') created = legacy;
623
684
  return {
624
- created: typeof manifest.created === 'boolean' ? manifest.created : null,
685
+ created,
625
686
  name: oneLine(manifest.name),
626
687
  avatar: normalizeAgentAvatar(manifest.avatar) ?? null,
688
+ homeChatted: homeThreadEvidence(resolveDataDir(deps), LEAD_AGENT_ID).chatted,
689
+ // Mixy's own model (never the Lead's); null: its thread keeps its own.
690
+ route: readMixyRoute(resolveDataDir(deps)),
627
691
  };
628
692
  }
629
693
  // Mixy's name and face, chosen in the 대표 에이전트 dialog or its profile:
@@ -648,8 +712,15 @@ export function createAgentEditorApi(deps) {
648
712
  return getLeadStatus();
649
713
  }
650
714
  // Sets the flag (and Mixy's 하는 일 when given); every other key is kept.
715
+ // A Lead manifest that exists but cannot be read as an object is the
716
+ // user's and is left alone: nothing is written, the status stays as read.
651
717
  function setLeadCreated(created, description) {
652
718
  if (typeof created !== 'boolean') throw new TypeError('created must be a boolean');
719
+ const file = join(leadDir(), 'agent.json');
720
+ if (existsSync(file) && !readManifestStrict(file)) {
721
+ process.stderr.write(`[agents] ${file} is not a readable JSON object; Mixy's created flag was not saved\n`);
722
+ return getLeadStatus();
723
+ }
653
724
  const manifest = readManifest(leadDir());
654
725
  manifest.created = created;
655
726
  const job = oneLine(description);
@@ -691,7 +762,27 @@ export function createAgentEditorApi(deps) {
691
762
  return getAgentConnectorPolicy(id);
692
763
  }
693
764
 
765
+ /** The one-time split of bots into bots/ (bot-split-migration.mjs), from
766
+ * the config as it is now; the catalogs read the store from here on. */
767
+ function migrateBotStore() {
768
+ const dataDir = resolveDataDir(deps);
769
+ const config = getConfig();
770
+ const result = migrateBotsToOwnStore({
771
+ dataDir,
772
+ shippedRoot: deps.rootDir ?? STANDALONE_ROOT,
773
+ routeOf: (id) => deps.agentRouteFromConfig(config, id),
774
+ leadRoute: normalizeWorkflowRoute(deps.resolveRoute(config, {})),
775
+ // The definitions the split copies or a re-sync replaces go with it.
776
+ backup: () => backupUserData(dataDir, 'bot-split', { dirs: BOT_SPLIT_BACKUP_DIRS }),
777
+ log: (line) => process.stderr.write(`${line}\n`),
778
+ });
779
+ clearAgentDefinitionCache();
780
+ listCustomAgentIds?.(dataDir);
781
+ return result;
782
+ }
783
+
694
784
  return {
785
+ migrateBotStore,
695
786
  getAgentConnectorPolicy,
696
787
  setAgentConnectorPolicy,
697
788
  listAgents,
@@ -4,8 +4,22 @@ import { isHiddenAgent } from '../../runtime/agent/orchestrator/internal-agents.
4
4
  import { normalizeWorkflowRoute } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
5
5
  import { ensureProviderEnabled } from '../../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
6
6
  import { resolveRouteContextState, selectionFastCapable } from '../route-state.mjs';
7
- import { canonicalizeAgentRouteStorage, withAgentDisabled } from '../../runtime/shared/agent-route-config.mjs';
8
- import { agentEditorId } from './shared.mjs';
7
+ import {
8
+ canonicalizeAgentRouteStorage,
9
+ isAgentDisabled,
10
+ withAgentDisabled,
11
+ } from '../../runtime/shared/agent-route-config.mjs';
12
+ import { agentEditorId, resolveDataDir } from './shared.mjs';
13
+ import {
14
+ isStoreBot,
15
+ readBotRoute,
16
+ readMixyRoute,
17
+ writeBotRoute,
18
+ writeMixyRoute,
19
+ } from '../../runtime/shared/agent-store.mjs';
20
+
21
+ /** Mixy's runtime id (agent-home.mjs MIXY_AGENT_ID). */
22
+ const MIXY_RUNTIME_ID = 'lead';
9
23
  import { routeChain, validateRouteChainProviders } from '../../runtime/shared/model-route-chain.mjs';
10
24
 
11
25
  // Agent route persistence. Route-scope isolation: an agent route owns its
@@ -23,8 +37,35 @@ export function createAgentRouteApi(deps) {
23
37
  saveConfigAndAdopt(canonicalizeAgentRouteStorage(withAgentDisabled(getConfig(), id, disabled)));
24
38
  }
25
39
 
40
+ // A bot of the store keeps its route in its own folder (agent-store.mjs);
41
+ // Mixy (`lead` here, its runtime id) keeps its own in bots/mixy — never the
42
+ // Lead's route, which the Main route setter owns.
43
+ const dataDir = () => (deps.cfgMod || deps.STANDALONE_DATA_DIR ? resolveDataDir(deps) : '');
44
+ const isMixy = (id) => id === MIXY_RUNTIME_ID && Boolean(dataDir());
45
+ const storeBot = (id) => isStoreBot(dataDir(), id);
46
+ function saveRoute(id, route, keepOff) {
47
+ if (isMixy(id)) {
48
+ writeMixyRoute(dataDir(), route);
49
+ return;
50
+ }
51
+ if (storeBot(id)) {
52
+ writeBotRoute(dataDir(), id, route);
53
+ saveDisabled(id, keepOff);
54
+ return;
55
+ }
56
+ const nextConfig = withAgentDisabled(getConfig(), id, keepOff);
57
+ nextConfig.agents = { ...(nextConfig.agents || {}), [id]: route };
58
+ saveConfigAndAdopt(canonicalizeAgentRouteStorage(nextConfig));
59
+ }
60
+
26
61
  // Only an explicitly empty provider removes an override: the agent inherits Main.
27
62
  function clearOverride(id) {
63
+ if (isMixy(id) || storeBot(id)) {
64
+ if (isMixy(id)) writeMixyRoute(dataDir(), null);
65
+ else writeBotRoute(dataDir(), id, null);
66
+ const inherited = normalizeWorkflowRoute(resolveRoute(getConfig(), {}));
67
+ return { ...(inherited || {}), inherited: true };
68
+ }
28
69
  const nextConfig = { ...getConfig() };
29
70
  const agents = { ...(nextConfig.agents || {}) };
30
71
  const hadOverride = Object.hasOwn(agents, id);
@@ -83,7 +124,20 @@ export function createAgentRouteApi(deps) {
83
124
  if (!id) throw new Error(`unknown agent "${agentId}"`);
84
125
  if (isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and has no configurable route`);
85
126
  const requested = { ...(next || {}) };
86
- const stored = agentRouteFromConfig(getConfig(), id) || {};
127
+ // Compare-and-set: fill the route only while the agent has none. A route
128
+ // picked while the model resolved below stays (answered with `skipped`).
129
+ const onlyIfEmpty = requested.onlyIfEmpty === true;
130
+ delete requested.onlyIfEmpty;
131
+ const readStored = () => {
132
+ if (isMixy(id)) return readMixyRoute(dataDir()) || {};
133
+ if (storeBot(id)) return readBotRoute(dataDir(), id) || {};
134
+ return agentRouteFromConfig(getConfig(), id) || {};
135
+ };
136
+ const kept = () => ({ ...readStored(), id, skipped: true });
137
+ const stored = readStored();
138
+ if (onlyIfEmpty && clean(stored.provider)) return kept();
139
+ // Mixy is never off: its route request is a model pick alone.
140
+ if (isMixy(id)) delete requested.disabled;
87
141
  // Off is an explicit state, stored apart from the route so the model the
88
142
  // user picked survives and comes back when the agent is switched on. An
89
143
  // off request that also carries a model saves that model and stays off.
@@ -126,11 +180,17 @@ export function createAgentRouteApi(deps) {
126
180
  const [primary, ...fallbacks] = resolved;
127
181
  const routeToSave = normalizeWorkflowRoute({ ...primary, fallbacks });
128
182
  if (!routeToSave) throw new Error('agent route requires provider and model');
183
+ // No await from here to the write: the emptiness checked is the emptiness
184
+ // written over. An off agent stays off when it was only back-filled.
185
+ // A store bot's or Mixy's route is read from its folder on disk right here.
186
+ // An agent routed in the config file may have been written by another
187
+ // process whose change notification is still pending: re-read it first.
188
+ if (onlyIfEmpty && !isMixy(id) && !storeBot(id)) deps.reloadFullConfig?.();
189
+ if (onlyIfEmpty && clean(readStored().provider)) return kept();
190
+ const stayOff = onlyIfEmpty ? isAgentDisabled(getConfig(), id) : keepOff;
129
191
  // Picking a model is also the "on" switch unless the request keeps it off.
130
- const nextConfig = withAgentDisabled(getConfig(), id, keepOff);
131
- nextConfig.agents = { ...(nextConfig.agents || {}), [id]: routeToSave };
132
- saveConfigAndAdopt(canonicalizeAgentRouteStorage(nextConfig));
133
- return keepOff ? { ...routeToSave, id, disabled: true } : routeToSave;
192
+ saveRoute(id, routeToSave, stayOff);
193
+ return stayOff ? { ...routeToSave, id, disabled: true } : routeToSave;
134
194
  }
135
195
 
136
196
  return { setAgentRoute };
@@ -25,6 +25,7 @@ function applyWorkflowSlots(workflowInput, draft) {
25
25
  if (!normalized) continue;
26
26
  draft.presets = upsertWorkflowPreset(draft.presets, 'lead', normalized);
27
27
  draft.defaultPreset = workflowPresetId('lead');
28
+ draft.leadRoute = normalized;
28
29
  } else if (slot === 'agent') {
29
30
  if (normalized) draft.agentRoutes.worker = normalized;
30
31
  else delete draft.agentRoutes.worker;
@@ -72,10 +73,13 @@ export function createOnboardingApi(deps) {
72
73
  presets: Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [],
73
74
  defaultPreset: nextConfig.default,
74
75
  agentRoutes: { ...(nextConfig.agents || {}) },
76
+ // The Lead route this save writes, from `defaultRoute` or `workflowRoutes.lead`.
77
+ leadRoute: null,
75
78
  };
76
79
  if (defaultRoute) {
77
80
  draft.presets = upsertWorkflowPreset(draft.presets, 'lead', defaultRoute);
78
81
  draft.defaultPreset = workflowPresetId('lead');
82
+ draft.leadRoute = defaultRoute;
79
83
  }
80
84
  const workflowInput =
81
85
  payload.workflowRoutes && typeof payload.workflowRoutes === 'object' ? payload.workflowRoutes : {};
@@ -104,12 +108,15 @@ export function createOnboardingApi(deps) {
104
108
  }
105
109
 
106
110
  saveConfigAndAdopt(canonicalizeAgentRouteStorage(nextConfig));
107
- if (defaultRoute) {
111
+ // Whatever wrote the Lead route also moves the runtime Main route, in the
112
+ // same synchronous step as the save: a pending conditional backfill then
113
+ // finds a route that is not empty instead of overwriting this choice.
114
+ if (draft.leadRoute) {
108
115
  setRouteState(
109
116
  resolveRoute(getConfig(), {
110
- provider: defaultRoute.provider,
111
- model: defaultRoute.model,
112
- effort: defaultRoute.effort,
117
+ provider: draft.leadRoute.provider,
118
+ model: draft.leadRoute.model,
119
+ effort: draft.leadRoute.effort,
113
120
  })
114
121
  );
115
122
  deps.invalidatePreSessionToolSurface?.();
@@ -1,6 +1,7 @@
1
1
  import { SESSION_ID_PATTERN } from '../runtime/agent/orchestrator/runtime-core/session-id.mjs';
2
2
  import { sessionListOutput, sessionReadOutput, SESSION_TOOL_OUTPUT_BYTES } from './session-tool-output.mjs';
3
3
  import { createAgentTaskSessions } from '../session-runtime/agent-task-sessions.mjs';
4
+ import { handoffContent } from '../session-runtime/handoff-attachments.mjs';
4
5
 
5
6
  const TARGET_ACTIONS = new Set(['status', 'read', 'send', 'cancel', 'delete']);
6
7
  const MUTATING_ACTIONS = new Set(['send', 'cancel', 'delete']);
@@ -51,15 +52,22 @@ export function createSessionControl({
51
52
  if (typeof args.prompt !== 'string' || !args.prompt.trim()) {
52
53
  throw new Error('session: prompt is required for action=send');
53
54
  }
54
- // The agent's follow-up in a task session it created reports back too.
55
- const origin = await agentTasks.originFor(sessionId, context.callerAgentId);
56
- if (origin) return { sessionId, accepted: await agentTasks.send(sessionId, args.prompt, origin) };
55
+ // An agent conversation's send reports its turn back to the agent's
56
+ // home: a task session it created by its stored origin, any other
57
+ // session by an origin for this send only.
58
+ // Forwarded files ride along as stored attachment parts (workspace-tools.mjs).
59
+ const prompt = handoffContent(args.prompt, context.attachmentParts);
60
+ const origin = await agentTasks.reportOrigin(sessionId, context.callerAgentId, caller);
61
+ if (origin) {
62
+ const { accepted, reportsBack } = await agentTasks.send(sessionId, prompt, origin);
63
+ return { sessionId, accepted, reportsBack };
64
+ }
57
65
  const result = await service.submitSession({
58
66
  sessionId,
59
- prompt: args.prompt,
67
+ prompt,
60
68
  options: { mode: 'prompt', transcriptMeta: { sender: 'lead' } },
61
69
  });
62
- return { sessionId, accepted: result.accepted === true };
70
+ return { sessionId, accepted: result.accepted === true, reportsBack: false };
63
71
  }
64
72
  if (action === 'cancel') {
65
73
  const result = await service.abortSession({ sessionId });
@@ -96,6 +96,7 @@ import { createChannelsRuntimeLoader } from './daemon-channels-loader.mjs';
96
96
  import { createDesktopRuntime } from './daemon-desktop-runtime.mjs';
97
97
  import { createCanonicalAgentControl } from './daemon-agent-control.mjs';
98
98
  import { createSessionControl } from './daemon-session-control.mjs';
99
+ import { createAgentTaskSessions } from '../session-runtime/agent-task-sessions.mjs';
99
100
  import { flushAllIndexWrites } from '../session-runtime/services/agent-tool/index-write-queue.mjs';
100
101
  import {
101
102
  compareRuntimeVersions,
@@ -926,6 +927,8 @@ async function main() {
926
927
  cwd: CWD,
927
928
  });
928
929
  canonicalAgentControl = agentControl;
930
+ // Agents' cross-session sends and task sessions, and their report watches.
931
+ const agentTasks = createAgentTaskSessions({ getSessionService: () => sessionService });
929
932
  const requestedRuntimeMode = String(process.env.MIXDOG_SESSION_RUNTIME_MODE || '')
930
933
  .trim()
931
934
  .toLowerCase();
@@ -939,7 +942,7 @@ async function main() {
939
942
  log,
940
943
  measureBootPhase: bootPhases.measure,
941
944
  executeAgentControl: agentControl.execute,
942
- executeSessionControl: createSessionControl({ getSessionService: () => sessionService }),
945
+ executeSessionControl: createSessionControl({ getSessionService: () => sessionService, agentTasks }),
943
946
  });
944
947
  // Codex-style runtime: daemon routing and independent async session actors
945
948
  // share one V8 isolate and module graph. CPU-heavy work stays in bounded
@@ -986,9 +989,16 @@ async function main() {
986
989
  setAgentHomeLiveSurface({
987
990
  busy: (sessionId) => sessionService.liveSessionBusy(sessionId),
988
991
  notice: (sessionId, notice) => sessionService.pushLiveSessionNotice(sessionId, notice),
989
- // A task session's result (session create): the agent relays it in one turn.
990
- wake: (sessionId, prompt) =>
991
- sessionService.submitSession({ sessionId, prompt, options: { mode: 'prompt', kickoff: true } }),
992
+ // A task session's result (session create): the agent relays it in one
993
+ // turn. Resolves whether the turn was taken.
994
+ wake: async (sessionId, prompt, { id } = {}) =>
995
+ (
996
+ await sessionService.submitSession({
997
+ sessionId,
998
+ prompt,
999
+ options: { mode: 'prompt', kickoff: true, ...(id ? { id } : {}) },
1000
+ })
1001
+ )?.accepted === true,
992
1002
  // A routine fire: one queued turn in its bot's thread (routine-home-turn.mjs).
993
1003
  submit: async (sessionId, prompt, options) =>
994
1004
  (await sessionService.submitSession({ sessionId, prompt, options }))?.accepted === true,
@@ -997,6 +1007,8 @@ async function main() {
997
1007
  // Results queued before a restart are posted now; results a finished run
998
1008
  // could not queue (kept on its routine's record) are queued again.
999
1009
  recoverScheduleResults().catch((error) => log(`routine results recovery failed: ${error?.message || error}`));
1010
+ // Agents' sends whose report an earlier process had not queued yet.
1011
+ agentTasks.recover().catch((error) => log(`agent task report recovery failed: ${error?.message || error}`));
1000
1012
  recoverRoutineDeliveries().catch((error) => log(`routine delivery recovery failed: ${error?.message || error}`));
1001
1013
  sessionTransport = createSessionTransport({
1002
1014
  // ctx carries the CLIENT token: the session service refcounts views across
@@ -4,6 +4,43 @@
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { createAgentChildFactory } from './agent-child.mjs';
6
6
  import { partialHandoffTextFromSession } from '../../../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
7
+ import { attachmentsForIntake } from '../../../runtime/attachments/store.mjs';
8
+ import { artifactLines } from '../../../runtime/shared/handoff-artifacts.mjs';
9
+ import { collectTurnArtifacts, handoffContent, sessionRoots } from '../../../session-runtime/handoff-attachments.mjs';
10
+
11
+ /** An agent turn's prompt: text, or text with forwarded attachment parts
12
+ * (handoff-attachments.mjs), never any other shape. */
13
+ export function agentTurnContent(prompt) {
14
+ if (!Array.isArray(prompt)) return String(prompt || '');
15
+ const text = prompt
16
+ .filter((part) => part?.type === 'text' && !part.attachmentRef)
17
+ .map((part) => String(part.text || ''))
18
+ .join('\n');
19
+ return handoffContent(text, prompt);
20
+ }
21
+
22
+ /** The prompt row's metadata: a Lead's message, with the files it carries. */
23
+ export function agentTurnTranscriptMeta(content) {
24
+ const attachments = Array.isArray(content) ? attachmentsForIntake(content) : [];
25
+ return { sender: 'lead', ...(attachments.length ? { attachments } : {}) };
26
+ }
27
+
28
+ /** The artifact lines of the turn that began at `messageStart`; [] on any
29
+ * failure to read it (the handoff still goes out). */
30
+ async function turnArtifactLines(runtime, messageStart, descriptor) {
31
+ try {
32
+ const { messages } = await runtime.readModelMessages(messageStart);
33
+ const session = runtime.session || descriptor;
34
+ const { files, omitted } = collectTurnArtifacts(messages, {
35
+ cwd: descriptor.cwd,
36
+ roots: sessionRoots(session, descriptor.cwd),
37
+ });
38
+ // Text only: the caller forwards these by path (an id in text is never trusted).
39
+ return artifactLines(files, omitted, { ids: false });
40
+ } catch {
41
+ return [];
42
+ }
43
+ }
7
44
 
8
45
  export function createAgentTurns({
9
46
  registry,
@@ -67,15 +104,16 @@ export function createAgentTurns({
67
104
  descriptor.stage = 'running';
68
105
  descriptor.updatedAt = Date.now();
69
106
  try {
107
+ const content = agentTurnContent(prompt);
70
108
  const options = {
71
109
  id: `agent-turn-${randomUUID()}`,
72
110
  mode: 'prompt',
73
111
  priority: 'next',
74
112
  context,
75
- transcriptMeta: { sender: 'lead' },
113
+ transcriptMeta: agentTurnTranscriptMeta(content),
76
114
  ...(entry.runtime.isWireSafe === true || typeof onToolResult !== 'function' ? {} : { onToolResult }),
77
115
  };
78
- const detail = await target.call(entry.runtime, String(prompt || ''), options);
116
+ const detail = await target.call(entry.runtime, content, options);
79
117
  if (detail?.status === 'failed') {
80
118
  throw new Error(String(detail.error || 'agent session turn failed'));
81
119
  }
@@ -91,7 +129,12 @@ export function createAgentTurns({
91
129
  const reason = String(detail?.reason || detail?.error || '').trim();
92
130
  throw new Error(`agent turn ${status}${reason ? `: ${reason}` : ''}`);
93
131
  }
94
- const result = detail?.result || { content: '' };
132
+ let result = detail?.result || { content: '' };
133
+ // The files the turn produced reach the owner with its handoff.
134
+ const files = await turnArtifactLines(entry.runtime, messageStart, descriptor);
135
+ if (files.length) {
136
+ result = { ...result, content: [String(result.content || ''), files.join('\n')].join('\n\n') };
137
+ }
95
138
  descriptor.status = 'idle';
96
139
  descriptor.stage = 'idle';
97
140
  descriptor.lastHandoff = typeof result?.content === 'string' ? result.content : '';