mixdog 1.3.1 → 1.3.3

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 (468) hide show
  1. package/README.md +8 -4
  2. package/package.json +8 -8
  3. package/scripts/native-release-status.mjs +238 -0
  4. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  5. package/src/agents/reviewer/AGENT.md +5 -7
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +9 -1
  8. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  9. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  10. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  11. package/src/defaults/skills/setup/SKILL.md +23 -1
  12. package/src/defaults/skills/setup/references/actions.md +50 -3
  13. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  14. package/src/defaults/skills/setup-guide/SKILL.md +7 -3
  15. package/src/defaults/skills/visualize/SKILL.md +9 -6
  16. package/src/defaults/skills/visualize/references/craft.md +8 -1
  17. package/src/help.mjs +1 -1
  18. package/src/lib/keychain-cjs.cjs +4 -1
  19. package/src/lib/rules-builder.cjs +14 -7
  20. package/src/rules/agent-mode/AGENT_MODE.md +8 -5
  21. package/src/rules/agent-mode/MIXY.md +6 -8
  22. package/src/rules/lead/LEAD.md +7 -10
  23. package/src/rules/shared/10-tool-workflow.md +4 -0
  24. package/src/rules/shared/20-research.md +2 -0
  25. package/src/rules/shared/45-destructive.md +5 -1
  26. package/src/rules/shared/60-verification.md +6 -4
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  30. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  31. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  32. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  33. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  36. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  37. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  38. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +9 -2
  39. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -5
  40. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  41. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  42. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +84 -32
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  45. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +84 -42
  48. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  50. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  51. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +27 -8
  52. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  54. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  55. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  56. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  57. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  59. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +16 -0
  60. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  62. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  64. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  65. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +2 -1
  68. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  69. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  70. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  72. package/src/runtime/agent/orchestrator/providers/registry.mjs +19 -0
  73. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  74. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  75. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  78. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  79. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  81. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -6
  82. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  83. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +52 -6
  85. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +35 -13
  86. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +7 -6
  87. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +8 -3
  90. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  92. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  93. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  94. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -2
  95. package/src/runtime/agent/orchestrator/session/agent-room.mjs +196 -45
  96. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  97. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  98. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  99. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +45 -19
  100. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  102. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +185 -37
  103. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +6 -2
  107. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +4 -1
  108. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  110. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +240 -0
  111. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +102 -35
  112. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  113. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +19 -9
  114. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  115. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  116. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  117. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  118. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +199 -109
  119. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  120. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  123. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  124. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  125. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +5 -5
  126. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -0
  127. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  128. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +33 -8
  129. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  130. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +27 -0
  131. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  132. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  134. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  135. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  136. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  137. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  138. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +27 -14
  139. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +14 -0
  140. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  141. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  142. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  143. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  144. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +115 -0
  145. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  146. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  147. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  148. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  149. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  150. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  151. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  152. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  153. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  154. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  155. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +55 -27
  156. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +4 -9
  157. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +14 -1
  158. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  159. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  160. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  161. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  162. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +52 -11
  163. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  164. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +18 -3
  165. package/src/runtime/agent/orchestrator/session/routine-result.mjs +126 -12
  166. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  167. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +16 -3
  168. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  169. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  170. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  171. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  172. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  173. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  174. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  175. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  176. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +15 -5
  177. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +49 -1
  178. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +80 -1
  179. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  180. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  181. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  182. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  183. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  184. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  185. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  186. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  187. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  188. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +35 -5
  189. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +42 -2
  190. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +47 -14
  191. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  192. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +6 -3
  193. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  194. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  195. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  196. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  197. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  198. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  199. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +10 -12
  200. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  201. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  202. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  203. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  204. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  205. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +42 -23
  206. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  207. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  208. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  209. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  210. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  211. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  212. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  213. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  214. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  215. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  216. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  217. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  218. package/src/runtime/attachments/legacy-office.mjs +110 -0
  219. package/src/runtime/attachments/limits.mjs +5 -1
  220. package/src/runtime/attachments/office-extract.mjs +3 -6
  221. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  222. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  223. package/src/runtime/attachments/store.mjs +109 -33
  224. package/src/runtime/browser-bridge/client.mjs +73 -20
  225. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  226. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  227. package/src/runtime/channels/lib/state-file.mjs +15 -15
  228. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  229. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  230. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  231. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  232. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  233. package/src/runtime/effort-judge/judge-client.mjs +66 -16
  234. package/src/runtime/effort-judge/model-install.mjs +47 -7
  235. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  236. package/src/runtime/local-provider/catalog.mjs +5 -2
  237. package/src/runtime/local-provider/registered-models.mjs +64 -27
  238. package/src/runtime/local-provider/server.mjs +2 -3
  239. package/src/runtime/media/adapters/image-api.mjs +89 -0
  240. package/src/runtime/media/auth.mjs +64 -1
  241. package/src/runtime/media/catalog.mjs +66 -4
  242. package/src/runtime/media/jobs.mjs +4 -0
  243. package/src/runtime/media/lanes.mjs +46 -5
  244. package/src/runtime/media/media-usage.mjs +17 -0
  245. package/src/runtime/media/renditions.mjs +15 -15
  246. package/src/runtime/media/store.mjs +36 -35
  247. package/src/runtime/media/tool.mjs +8 -1
  248. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  249. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  250. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  251. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  252. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  253. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  254. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  255. package/src/runtime/office/core/journal.mjs +7 -14
  256. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  257. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  258. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  259. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  260. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  261. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  262. package/src/runtime/office/portable/portable-soffice.mjs +135 -29
  263. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  264. package/src/runtime/shared/agent-avatar.mjs +3 -2
  265. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  266. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  267. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  268. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  269. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  270. package/src/runtime/shared/agent-room-host.mjs +58 -0
  271. package/src/runtime/shared/agent-store.mjs +24 -7
  272. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  273. package/src/runtime/shared/atomic-file.d.mts +50 -0
  274. package/src/runtime/shared/atomic-file.mjs +394 -132
  275. package/src/runtime/shared/background-tasks.mjs +7 -4
  276. package/src/runtime/shared/bot-hops.mjs +35 -0
  277. package/src/runtime/shared/bot-split-migration.mjs +1 -1
  278. package/src/runtime/shared/bounded-download.mjs +4 -2
  279. package/src/runtime/shared/config.mjs +39 -9
  280. package/src/runtime/shared/defaults-separation.mjs +6 -2
  281. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  282. package/src/runtime/shared/err-text.mjs +4 -0
  283. package/src/runtime/shared/error-presentation.mjs +22 -4
  284. package/src/runtime/shared/file-has-data.mjs +28 -0
  285. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  286. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  287. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  288. package/src/runtime/shared/projects.mjs +50 -41
  289. package/src/runtime/shared/provider-accounts.mjs +17 -12
  290. package/src/runtime/shared/redact-directories.mjs +18 -0
  291. package/src/runtime/shared/remove-directory.mjs +28 -0
  292. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  293. package/src/runtime/shared/routine-trigger.mjs +14 -6
  294. package/src/runtime/shared/run-child.d.mts +3 -0
  295. package/src/runtime/shared/run-child.mjs +24 -3
  296. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  297. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  298. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  299. package/src/runtime/shared/singleton-owner.mjs +7 -6
  300. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  301. package/src/runtime/shared/staged-update.mjs +4 -2
  302. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  303. package/src/runtime/shared/tool-execution-contract.mjs +10 -0
  304. package/src/runtime/shared/tool-work-units.mjs +12 -9
  305. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  306. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  307. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  308. package/src/runtime/shared/turn-snapshot.mjs +277 -124
  309. package/src/runtime/shared/user-cwd.mjs +9 -4
  310. package/src/runtime/shared/user-data-guard.mjs +92 -61
  311. package/src/runtime/shared/utf8-cut.mjs +10 -0
  312. package/src/runtime/shared/worktrees/activity-gate.mjs +251 -61
  313. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  314. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  315. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  316. package/src/runtime/shared/worktrees/registry.mjs +29 -27
  317. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  318. package/src/runtime/tidy/core-install.mjs +26 -4
  319. package/src/runtime/tidy/install.mjs +91 -23
  320. package/src/runtime/tidy/process.mjs +41 -8
  321. package/src/runtime/tidy/resolve.mjs +10 -7
  322. package/src/runtime/web-search/lib/config.mjs +4 -16
  323. package/src/session-runtime/agent-home-delivery.mjs +542 -204
  324. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  325. package/src/session-runtime/agent-room-delivery.mjs +81 -35
  326. package/src/session-runtime/agent-task-sessions.mjs +811 -179
  327. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  328. package/src/session-runtime/boot/apis.mjs +48 -24
  329. package/src/session-runtime/boot/config.mjs +1 -0
  330. package/src/session-runtime/boot/facade.mjs +16 -7
  331. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  332. package/src/session-runtime/boot/providers.mjs +2 -2
  333. package/src/session-runtime/boot/shared.mjs +12 -6
  334. package/src/session-runtime/boot/tools.mjs +23 -3
  335. package/src/session-runtime/bot-status.mjs +432 -0
  336. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  337. package/src/session-runtime/config-lifecycle.mjs +60 -55
  338. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  339. package/src/session-runtime/held-requests.mjs +31 -33
  340. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  341. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  342. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +24 -26
  343. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  344. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  345. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  346. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  347. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  348. package/src/session-runtime/media-api.mjs +5 -2
  349. package/src/session-runtime/model-recency.mjs +1 -0
  350. package/src/session-runtime/model-route/route-tuning.mjs +4 -1
  351. package/src/session-runtime/model-route/set-route.mjs +61 -1
  352. package/src/session-runtime/owned-automation-run.mjs +8 -1
  353. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  354. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  355. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  356. package/src/session-runtime/route-resolution.mjs +16 -8
  357. package/src/session-runtime/route-state.mjs +1 -59
  358. package/src/session-runtime/routine-home-turn.mjs +5 -3
  359. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  360. package/src/session-runtime/runtime-paths.mjs +11 -5
  361. package/src/session-runtime/runtime-review-api.mjs +4 -1
  362. package/src/session-runtime/runtime-tunables.mjs +6 -0
  363. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  364. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  365. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  366. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  367. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
  368. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  369. package/src/session-runtime/services/agent-tool/send-flow.mjs +25 -21
  370. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +7 -0
  371. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  372. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +8 -5
  373. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  374. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  375. package/src/session-runtime/services/agent-tool.mjs +6 -0
  376. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  377. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  378. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  379. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  380. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  381. package/src/session-runtime/services/provider-admin.mjs +26 -6
  382. package/src/session-runtime/services/schedule-access.mjs +33 -4
  383. package/src/session-runtime/services/seeds.mjs +4 -3
  384. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  385. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  386. package/src/session-runtime/services/worktree-access.mjs +27 -0
  387. package/src/session-runtime/session-create/materialize.mjs +2 -3
  388. package/src/session-runtime/session-create/session-options.mjs +15 -2
  389. package/src/session-runtime/session-hooks.mjs +9 -17
  390. package/src/session-runtime/settings-builtin-tools-api.mjs +65 -1
  391. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  392. package/src/session-runtime/setup-tool/executor.mjs +12 -2
  393. package/src/session-runtime/setup-tool/settings-contract.mjs +34 -1
  394. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  395. package/src/session-runtime/skills-api.mjs +12 -5
  396. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +349 -0
  397. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  398. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  399. package/src/session-runtime/tool-policy/policy-stage.mjs +183 -0
  400. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  401. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  402. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  403. package/src/session-runtime/tool-policy-refresh.mjs +48 -369
  404. package/src/session-runtime/tool-surface.mjs +10 -4
  405. package/src/session-runtime/turn/session-ops.mjs +8 -0
  406. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +95 -88
  407. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  408. package/src/session-runtime/turn/turn-run.mjs +7 -2
  409. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  410. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +527 -333
  411. package/src/session-runtime/workflow-agents-api/agent-route.mjs +85 -63
  412. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  413. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  414. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  415. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  416. package/src/session-runtime/workspace/transitions.mjs +279 -67
  417. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  418. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  419. package/src/standalone/daemon-agent-control.mjs +4 -1
  420. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  421. package/src/standalone/daemon-session-control.mjs +173 -15
  422. package/src/standalone/daemon-telemetry.mjs +9 -0
  423. package/src/standalone/daemon.mjs +33 -10
  424. package/src/standalone/local-session-runtime.mjs +33 -16
  425. package/src/standalone/session-client.mjs +7 -2
  426. package/src/standalone/session-protocol.mjs +8 -0
  427. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  428. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  429. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  430. package/src/standalone/session-service.mjs +21 -0
  431. package/src/standalone/session-state-projection.mjs +19 -9
  432. package/src/standalone/session-tool-output.mjs +36 -9
  433. package/src/standalone/staged-import.mjs +47 -0
  434. package/src/tui/App.jsx +2 -0
  435. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  436. package/src/tui/app/message-selector.mjs +15 -2
  437. package/src/tui/app/model-options.mjs +16 -2
  438. package/src/tui/app/model-picker.mjs +2 -1
  439. package/src/tui/app/route-pickers.mjs +3 -1
  440. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  441. package/src/tui/app/slash-commands.mjs +1 -1
  442. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  443. package/src/tui/dist/index.mjs +617 -369
  444. package/src/tui/paste-attachments.mjs +0 -1
  445. package/src/tui/paste-text-policy.mjs +4 -5
  446. package/src/tui/prompt-history-store.mjs +16 -21
  447. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  448. package/src/tui/session/context-state.mjs +4 -0
  449. package/src/tui/session/notice-surface.mjs +3 -0
  450. package/src/tui/session/session-api/intake/abort.mjs +52 -18
  451. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  452. package/src/tui/session/session-api/media.mjs +2 -1
  453. package/src/tui/session/session-api/routes-agent-definitions.mjs +76 -0
  454. package/src/tui/session/session-api/routes-write.mjs +203 -0
  455. package/src/tui/session/session-api/routes.mjs +23 -247
  456. package/src/tui/session/session-api/settings.mjs +14 -1
  457. package/src/tui/session/session-flow/drain.mjs +28 -3
  458. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  459. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  460. package/src/tui/session/session-flow/steering.mjs +8 -3
  461. package/src/tui/session/tool-approval.mjs +142 -14
  462. package/src/tui/session/transcript-intake.mjs +2 -0
  463. package/src/tui/session/turn.mjs +23 -14
  464. package/src/tui/transcript-tool-failures.mjs +8 -2
  465. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  466. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  467. package/src/rules/agent-mode/ROUTINE.md +0 -6
  468. package/src/session-runtime/room-member-gate.mjs +0 -48
@@ -1,6 +1,23 @@
1
1
  import { join } from 'node:path';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+ import {
4
+ accessSync,
5
+ constants as fsConstants,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ renameSync,
11
+ rmSync,
12
+ statSync,
13
+ } from 'node:fs';
14
+ import {
15
+ holdsNoData,
16
+ parseJsonText,
17
+ readJsonStateSync,
18
+ writeFileAtomicSync,
19
+ writeJsonAtomicSync,
20
+ } from '../../runtime/shared/atomic-file.mjs';
4
21
  import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
5
22
  import { hasOwn } from '../../runtime/shared/object.mjs';
6
23
  import { serializeFrontmatterDoc } from '../../runtime/shared/markdown-frontmatter.mjs';
@@ -35,6 +52,7 @@ import {
35
52
  parseConnectorPolicy,
36
53
  readConnectorsFile,
37
54
  storedConnectorPolicy,
55
+ assertConnectorPolicyNotWidened,
38
56
  } from '../../runtime/agent/orchestrator/mcp/agent-connectors.mjs';
39
57
 
40
58
  /** A policy given to save → its stored form (null removes the field). Only a
@@ -45,6 +63,7 @@ function connectorPolicyInput(value) {
45
63
  }
46
64
  import { STANDALONE_ROOT } from '../runtime-paths.mjs';
47
65
  import { homeThreadEvidence } from '../../runtime/shared/agent-kind.mjs';
66
+ import { migrateAgentKinds } from '../../runtime/shared/agent-kind-migration.mjs';
48
67
  import { BOT_SPLIT_BACKUP_DIRS, migrateBotsToOwnStore } from '../../runtime/shared/bot-split-migration.mjs';
49
68
  import { backupUserData } from '../../runtime/shared/user-data-guard.mjs';
50
69
  import { normalizeWorkflowRoute } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
@@ -94,17 +113,75 @@ function readManifest(dir) {
94
113
  return readManifestStrict(join(dir, 'agent.json')) || {};
95
114
  }
96
115
 
97
- // The manifest object at `file`, or null when it is missing, unreadable or
98
- // not a JSON object.
116
+ // Definition files are user-authored: every write is atomic (a crash never
117
+ // leaves one torn or NUL-filled) and keeps the replaced copy as `<file>.bak`.
118
+ const DEFINITION_WRITE = Object.freeze({ backup: true, mode: 0o644 });
119
+ const writeDefinitionText = (file, text) => writeFileAtomicSync(file, text, DEFINITION_WRITE);
120
+ const writeManifest = (file, manifest) => writeJsonAtomicSync(file, manifest, DEFINITION_WRITE);
121
+
122
+ // The manifest object at `file`, or null when it is missing, unreadable, holds
123
+ // no data (empty, or NUL-filled by a crash) or is not a JSON object.
99
124
  function readManifestStrict(file) {
100
125
  try {
101
- const value = JSON.parse(readFileSync(file, 'utf8'));
126
+ const text = readFileSync(file, 'utf8');
127
+ if (holdsNoData(text)) return null;
128
+ const value = parseJsonText(text);
102
129
  return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
103
130
  } catch {
104
131
  return null;
105
132
  }
106
133
  }
107
134
 
135
+ // A read-only agent.json is a standing refusal, not a transient lock: the
136
+ // atomic write's rename onto it fails with EPERM and is retried for seconds,
137
+ // synchronously on the host's event loop. Refuse it up front (one stat, no wait),
138
+ // in the same synchronous stretch as the write so no-widen checks stay atomic.
139
+ function assertManifestWritable(file) {
140
+ try {
141
+ accessSync(file, fsConstants.W_OK);
142
+ } catch (error) {
143
+ if (error?.code === 'ENOENT') return;
144
+ throw new Error(`${file} is not writable (${error?.code || error}); make it writable, then save again`);
145
+ }
146
+ }
147
+
148
+ function requireManifestObject(value) {
149
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('not a JSON object');
150
+ return value;
151
+ }
152
+
153
+ // The manifest to modify and write back: {} when agent.json is missing. A
154
+ // damaged one (NUL-filled or torn by a crash, not an object) first gets its
155
+ // last good copy back (agent.json.bak; the damaged bytes are kept aside as
156
+ // agent.json.corrupt-<ts>). With no good copy it stays where it is and the
157
+ // write is refused: the keys it held (connectors, avatar, route…) are unknown,
158
+ // and rewriting from {} would drop a connector deny list — a manifest without
159
+ // one means "every connector on".
160
+ function readManifestForUpdate(dir) {
161
+ const file = join(dir, 'agent.json');
162
+ const manifest = readJsonStateSync(file, {
163
+ validate: requireManifestObject,
164
+ keepWithoutBackup: true,
165
+ mode: DEFINITION_WRITE.mode,
166
+ });
167
+ if (manifest) return manifest;
168
+ if (!existsSync(file)) return {};
169
+ throw new Error(
170
+ `${file} is damaged and has no good copy to restore; it was left untouched. Fix or delete the file, then save again`
171
+ );
172
+ }
173
+
174
+ // A text file's trimmed content; '' when it is missing, unreadable or holds no
175
+ // data (a crash can leave it NUL-filled, which trim() keeps).
176
+ function readTextOrEmpty(file) {
177
+ try {
178
+ const text = readFileSync(file, 'utf8');
179
+ return holdsNoData(text) ? '' : text.trim();
180
+ } catch {
181
+ return '';
182
+ }
183
+ }
184
+
108
185
  // Manifest keys the editor does not show (e.g. `avatar`). `entry` and `id`
109
186
  // go: the editor always writes AGENT.md, and the directory names the agent.
110
187
  function hiddenManifest(manifest) {
@@ -119,11 +196,7 @@ export const AGENT_DEFINITION_CONFLICT = 'AGENT_DEFINITION_CONFLICT';
119
196
  const LEAD_PERSONA_MAX_BYTES = 256 * 1024;
120
197
 
121
198
  function readPersona(dir) {
122
- try {
123
- return readFileSync(join(dir, PERSONA_FILE), 'utf8').trim();
124
- } catch {
125
- return '';
126
- }
199
+ return readTextOrEmpty(join(dir, PERSONA_FILE));
127
200
  }
128
201
 
129
202
  const IMAGE_DATA_URL = /^data:image\/(png|gif|webp);base64,/;
@@ -194,7 +267,7 @@ function avatarUpdate(value, dir) {
194
267
  function syncAvatarFiles(dir, avatar, writes) {
195
268
  for (const { path, bytes } of writes) {
196
269
  mkdirSync(join(dir, AGENT_AVATAR_DIR), { recursive: true });
197
- writeFileSync(join(dir, path), bytes);
270
+ writeFileAtomicSync(join(dir, path), bytes, { mode: 0o644 });
198
271
  }
199
272
  const keep = new Set(
200
273
  avatar?.kind === 'custom'
@@ -250,13 +323,401 @@ const stableJson = (value) =>
250
323
  function writeAgentFiles(dir, { name, description, body, extra = {}, manifest = {} }) {
251
324
  const meta = { name, ...(description ? { description } : {}) };
252
325
  mkdirSync(dir, { recursive: true });
253
- writeFileSync(join(dir, 'AGENT.md'), serializeFrontmatterDoc({ ...meta, ...extra }, body));
326
+ writeDefinitionText(join(dir, 'AGENT.md'), serializeFrontmatterDoc({ ...meta, ...extra }, body));
254
327
  // loadAgentDefinition prefers the manifest for name/description.
255
- writeFileSync(join(dir, 'agent.json'), `${JSON.stringify({ ...meta, ...manifest }, null, 2)}\n`);
328
+ writeManifest(join(dir, 'agent.json'), { ...meta, ...manifest });
256
329
  rmSync(join(dir, AGENT_DELETED_MARKER), { force: true });
257
330
  rmSync(join(dir, ARCHIVED_AGENT_FILE), { force: true });
258
331
  }
259
332
 
333
+ const isArchivedAgent = (dataDir, id) => existsSync(join(userDir(dataDir, id), AGENT_DELETED_MARKER));
334
+
335
+ // A save's avatar and connectors applied to `manifest` (in place); returns the
336
+ // avatar update (its image writes), null when the save gives no avatar.
337
+ function applyManifestEdits(manifest, payload, dir, options) {
338
+ // New custom images arrive as checked bytes, stored under content hashes.
339
+ const update = hasOwn(payload, 'avatar') ? avatarUpdate(payload.avatar, dir) : null;
340
+ if (update?.avatar) manifest.avatar = update.avatar;
341
+ else if (update) delete manifest.avatar;
342
+ // Connectors: a policy replaces the saved one, null removes the field
343
+ // (every connector on), absent leaves it as saved.
344
+ if (hasOwn(payload, 'connectors')) {
345
+ const connectors = connectorPolicyInput(payload.connectors);
346
+ // For a remote client: against the manifest just read, in the same synchronous stretch as its write.
347
+ if (options?.noWiden) assertConnectorPolicyNotWidened(manifest.connectors ?? null, connectors);
348
+ if (connectors) manifest.connectors = connectors;
349
+ else delete manifest.connectors;
350
+ }
351
+ return update;
352
+ }
353
+
354
+ // A saved definition's files in `dir`: AGENT.md and agent.json, PERSONA.md
355
+ // (`persona` '' removes it, null leaves it) and the avatar images.
356
+ function writeSavedDefinition(dir, files, persona, update) {
357
+ writeAgentFiles(dir, files);
358
+ if (persona) writeDefinitionText(join(dir, PERSONA_FILE), `${persona}\n`);
359
+ else if (persona === '') rmSync(join(dir, PERSONA_FILE), { force: true });
360
+ // Only the images the manifest names stay.
361
+ syncAvatarFiles(dir, normalizeAgentAvatar(files.manifest.avatar), update?.writes ?? []);
362
+ }
363
+
364
+ // A save's id, name and description: a new agent's id comes from its name.
365
+ function resolveSaveTarget(loadAgentDefinition, payload, dataDir) {
366
+ const id = agentEditorId(clean(payload.id));
367
+ if (hasOwn(payload, 'id') && !id) {
368
+ throw new Error('agent id must contain letters/numbers (dashes and dots allowed)');
369
+ }
370
+ if (id && isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and cannot be edited`);
371
+ const name = oneLine(payload.name);
372
+ if (!name && !id) throw new Error('agent name must not be empty');
373
+ return {
374
+ // An archived agent keeps its id: its chat is never handed to a new one.
375
+ id:
376
+ id ||
377
+ availableAgentId(
378
+ name,
379
+ (candidate) =>
380
+ Boolean(loadAgentDefinition(dataDir, candidate)) ||
381
+ isArchivedAgent(dataDir, candidate) ||
382
+ // An id the split copied stays the bot's, even once purged; a
383
+ // folder in either store keeps its id too.
384
+ migratedBotIds(dataDir).has(candidate) ||
385
+ (botStoreActive(dataDir) &&
386
+ (existsSync(join(dataDir, 'agents', candidate)) || existsSync(botDir(dataDir, candidate))))
387
+ ),
388
+ name,
389
+ description: oneLine(payload.description),
390
+ };
391
+ }
392
+
393
+ /** The name and face an archived (deleted) agent left, per id; null for
394
+ * any id that is not archived. Its chat lists under them. */
395
+ function archivedIdentities(dataDir, agentIds) {
396
+ const ids = Array.isArray(agentIds) ? agentIds.filter((id) => typeof id === 'string') : [];
397
+ return Object.fromEntries(
398
+ ids.map((agentId) => {
399
+ const id = agentEditorId(agentId);
400
+ if (!id || id !== agentId || !isArchivedAgent(dataDir, id)) return [agentId, null];
401
+ const manifest = readManifest(userDir(dataDir, id));
402
+ return [agentId, { name: oneLine(manifest.name) || id, avatar: normalizeAgentAvatar(manifest.avatar) ?? null }];
403
+ })
404
+ );
405
+ }
406
+
407
+ // A custom avatar's images as data URLs by state, read from `dir`; null
408
+ // without an idle image.
409
+ function customAvatarImages(dir, avatar) {
410
+ const states = {};
411
+ for (const [state, file] of Object.entries(avatar.states)) {
412
+ const image = readAvatarImage(dir, file);
413
+ if (image) states[state] = image;
414
+ }
415
+ return states.idle ? { states } : null;
416
+ }
417
+
418
+ // Mixy (the Lead) has no agent definition to edit: only its PERSONA.md
419
+ // (성격) and AGENT.md (일하는 방식, agent mode only), read by the runtime
420
+ // from Mixy's identity folder: bots/mixy once the bot store is active,
421
+ // agents/lead before (mixyIdentityDir, agent-store.mjs). Saved empty, the
422
+ // file goes. `dataDirOf()` resolves the data folder on each call.
423
+ function createLeadIdentityApi(dataDirOf, mainRoute) {
424
+ const leadDir = () => mixyIdentityDir(dataDirOf());
425
+ function readLeadFile(file) {
426
+ return readTextOrEmpty(join(leadDir(), file));
427
+ }
428
+ function saveLeadFile(file, text) {
429
+ const value = String(text ?? '').trim();
430
+ if (Buffer.byteLength(value) > LEAD_PERSONA_MAX_BYTES) throw new Error(`${file} is larger than 256 KB`);
431
+ const dir = leadDir();
432
+ if (value) {
433
+ mkdirSync(dir, { recursive: true });
434
+ writeDefinitionText(join(dir, file), `${value}\n`);
435
+ } else rmSync(join(dir, file), { force: true });
436
+ // Mixy's threads take it from their next turn (the save answers at once).
437
+ void publishAgentProfileChange();
438
+ return readLeadFile(file);
439
+ }
440
+
441
+ // Whether Mixy was created (the "대표 에이전트 만들기" first run): the
442
+ // explicit `created` flag in Mixy's agent.json (leadDir). null until decided —
443
+ // the desktop host decides once (an existing profile counts as created).
444
+ // Also Mixy's display name (`name`, read by the runtime's identity line) and
445
+ // face (`avatar`, as every agent manifest): '' / null mean the defaults.
446
+ // `homeChatted`: whether Mixy's home thread holds any conversation (the
447
+ // host reads it to tell v1.3.0's never-used automatic Mixy apart).
448
+ function getLeadStatus() {
449
+ const manifest = readManifest(leadDir());
450
+ // A v1.3.0 automatic Mixy the split left in agents/lead still counts as
451
+ // decided (its routines keep running) until bots/mixy says otherwise.
452
+ const legacy = readManifest(join(dataDirOf(), 'agents', LEAD_AGENT_ID)).created;
453
+ let created = null;
454
+ if (typeof manifest.created === 'boolean') created = manifest.created;
455
+ else if (typeof legacy === 'boolean') created = legacy;
456
+ return {
457
+ created,
458
+ name: oneLine(manifest.name),
459
+ avatar: normalizeAgentAvatar(manifest.avatar) ?? null,
460
+ homeChatted: homeThreadEvidence(dataDirOf(), LEAD_AGENT_ID).chatted,
461
+ // Mixy's own model (never the Lead's); null: its thread keeps its own.
462
+ route: readMixyRoute(dataDirOf()),
463
+ };
464
+ }
465
+ // The route Mixy's thread follows: its own, else Main's (an override cleared).
466
+ const getLeadHomeRoute = () => readMixyRoute(dataDirOf()) ?? mainRoute();
467
+ // Mixy's name and face, chosen in the 대표 에이전트 dialog or its profile:
468
+ // a name '' and an avatar null go back to the defaults; absent keys are kept.
469
+ function saveLeadIdentity(payload = {}) {
470
+ const dir = leadDir();
471
+ const manifest = readManifestForUpdate(dir);
472
+ if (hasOwn(payload, 'name')) {
473
+ const name = oneLine(payload.name);
474
+ if (name.length > 64) throw new Error('name must be 64 characters or fewer');
475
+ if (name) manifest.name = name;
476
+ else delete manifest.name;
477
+ }
478
+ const update = hasOwn(payload, 'avatar') ? avatarUpdate(payload.avatar, dir) : null;
479
+ if (update?.avatar) manifest.avatar = update.avatar;
480
+ else if (update) delete manifest.avatar;
481
+ mkdirSync(dir, { recursive: true });
482
+ writeManifest(join(dir, 'agent.json'), manifest);
483
+ syncAvatarFiles(dir, normalizeAgentAvatar(manifest.avatar), update?.writes ?? []);
484
+ // The name is in Mixy's prompt (its identity line); the face is not.
485
+ if (hasOwn(payload, 'name')) void publishAgentProfileChange();
486
+ return getLeadStatus();
487
+ }
488
+ // Sets the flag (and Mixy's 하는 일 when given); every other key is kept.
489
+ // A damaged Lead manifest (NUL-filled, torn, not an object) gets its last
490
+ // good copy back first. Without one it is left alone — nothing is written,
491
+ // the status stays as read — since a rewrite would drop its connector policy.
492
+ function setLeadCreated(created, description) {
493
+ if (typeof created !== 'boolean') throw new TypeError('created must be a boolean');
494
+ let manifest;
495
+ try {
496
+ manifest = readManifestForUpdate(leadDir());
497
+ } catch (error) {
498
+ process.stderr.write(`[agents] ${error.message}; Mixy's created flag was not saved\n`);
499
+ return getLeadStatus();
500
+ }
501
+ manifest.created = created;
502
+ const job = oneLine(description);
503
+ if (job) manifest.description = job;
504
+ mkdirSync(leadDir(), { recursive: true });
505
+ writeManifest(join(leadDir(), 'agent.json'), manifest);
506
+ return getLeadStatus();
507
+ }
508
+
509
+ return {
510
+ leadDir,
511
+ getLeadPersona: () => readLeadFile(PERSONA_FILE),
512
+ saveLeadPersona: (text) => saveLeadFile(PERSONA_FILE, text),
513
+ getLeadAgentBody: () => readLeadFile('AGENT.md'),
514
+ saveLeadAgentBody: (text) => saveLeadFile('AGENT.md', text),
515
+ getLeadStatus,
516
+ getLeadHomeRoute,
517
+ saveLeadIdentity,
518
+ setLeadCreated,
519
+ // Mixy's connector policy, in its agent.json (`next` null removes it).
520
+ getLeadConnectorPolicy: () => readConnectorsFile(join(leadDir(), 'agent.json')),
521
+ setLeadConnectorPolicy: (next) => {
522
+ const manifest = readManifestForUpdate(leadDir());
523
+ if (next) manifest.connectors = next;
524
+ else delete manifest.connectors;
525
+ mkdirSync(leadDir(), { recursive: true });
526
+ writeManifest(join(leadDir(), 'agent.json'), manifest);
527
+ },
528
+ };
529
+ }
530
+
531
+ // Whether each agent's definition is on disk, decided per id from the files
532
+ // themselves: 'deleted' only when every source is PROVEN absent (ENOENT) or
533
+ // tombstoned, 'unknown' on any other read fault. A list read swallows
534
+ // directory errors, so its absence never proves a deletion; this does.
535
+ function definitionPresence({ dataDir, rootDir, stat }, agentIds) {
536
+ const probe = (path) => {
537
+ try {
538
+ stat(path);
539
+ return 'present';
540
+ } catch (error) {
541
+ return error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? 'absent' : 'unknown';
542
+ }
543
+ };
544
+ const presence = (agentId) => {
545
+ const id = agentEditorId(agentId);
546
+ if (!id || id !== agentId) return 'unknown';
547
+ const user = probe(join(userDir(dataDir, id), 'AGENT.md'));
548
+ if (user !== 'absent') return user;
549
+ const tombstone = probe(join(userDir(dataDir, id), AGENT_DELETED_MARKER));
550
+ if (tombstone === 'present') return 'deleted';
551
+ if (tombstone === 'unknown') return 'unknown';
552
+ const shipped = probe(join(rootDir, 'agents', id, 'AGENT.md'));
553
+ return shipped === 'absent' ? 'deleted' : shipped;
554
+ };
555
+ const ids = Array.isArray(agentIds) ? agentIds.filter((id) => typeof id === 'string') : [];
556
+ return Object.fromEntries(ids.map((id) => [id, presence(id)]));
557
+ }
558
+
559
+ // A definition folder's revision: a hash of the three files an editor writes.
560
+ function definitionDirRevision(dir) {
561
+ const hash = createHash('sha256');
562
+ for (const name of ['agent.json', 'AGENT.md', PERSONA_FILE]) {
563
+ hash.update(`${name}\0`);
564
+ try {
565
+ hash.update(readFileSync(join(dir, name)));
566
+ } catch {
567
+ hash.update('\u0001absent');
568
+ }
569
+ hash.update('\0');
570
+ }
571
+ return hash.digest('hex').slice(0, 24);
572
+ }
573
+
574
+ /** The one-time split of bots into bots/ (bot-split-migration.mjs), from
575
+ * the config as it is now; the catalogs read the store from here on. */
576
+ function migrateBotStore(deps) {
577
+ const dataDir = resolveDataDir(deps);
578
+ const config = deps.getConfig();
579
+ const result = migrateBotsToOwnStore({
580
+ dataDir,
581
+ shippedRoot: deps.rootDir ?? STANDALONE_ROOT,
582
+ routeOf: (id) => deps.agentRouteFromConfig(config, id),
583
+ leadRoute: normalizeWorkflowRoute(deps.resolveRoute(config, {})),
584
+ // The definitions the split copies or a re-sync replaces go with it.
585
+ backup: () => backupUserData(dataDir, 'bot-split', { dirs: BOT_SPLIT_BACKUP_DIRS }),
586
+ log: (line) => process.stderr.write(`${line}\n`),
587
+ });
588
+ clearAgentDefinitionCache();
589
+ deps.listCustomAgentIds?.(dataDir);
590
+ return result;
591
+ }
592
+
593
+ /** The one-time `kind: agent` stamp for agents made before the marker
594
+ * (agent-kind-migration.mjs); runs before the bot split so a workflow
595
+ * agent is never copied to bots/. */
596
+ function migrateAgentKindMarkers(deps) {
597
+ const dataDir = resolveDataDir(deps);
598
+ const result = migrateAgentKinds({
599
+ dataDir,
600
+ shippedRoot: deps.rootDir ?? STANDALONE_ROOT,
601
+ backup: () => backupUserData(dataDir, 'agent-kind', { dirs: BOT_SPLIT_BACKUP_DIRS }),
602
+ // A restored bot's own route goes back to the config, where a workflow
603
+ // agent's lives (unless the config already holds that model).
604
+ restoreRoute: (id, route) => {
605
+ const config = deps.getConfig();
606
+ const current = deps.agentRouteFromConfig(config, id);
607
+ if (current?.provider === route.provider && current?.model === route.model) return;
608
+ deps.saveConfigAndAdopt(canonicalizeAgentRouteStorage({ ...config, agents: { ...config.agents, [id]: route } }));
609
+ },
610
+ log: (line) => process.stderr.write(`${line}\n`),
611
+ });
612
+ clearAgentDefinitionCache();
613
+ deps.listCustomAgentIds?.(dataDir);
614
+ return result;
615
+ }
616
+
617
+ // Deleting (archiving), restoring and purging agents, and the config cleanup
618
+ // a removed agent needs.
619
+ function createAgentArchiveApi({
620
+ dataDirOf,
621
+ getConfig,
622
+ saveConfigAndAdopt,
623
+ loadAgentDefinition,
624
+ shippedDir,
625
+ announceBotChange,
626
+ getAgentDefinition,
627
+ }) {
628
+ // A removed custom agent must not leave a dangling route/preset or a
629
+ // disabled-roster entry that would silently disable a later agent of the
630
+ // same id.
631
+ function dropAgentRoute(id) {
632
+ const nextConfig = withAgentDisabled(getConfig(), id, false);
633
+ if (nextConfig.agents && id in nextConfig.agents) {
634
+ const agents = { ...nextConfig.agents };
635
+ delete agents[id];
636
+ nextConfig.agents = agents;
637
+ }
638
+ saveConfigAndAdopt(canonicalizeAgentRouteStorage(nextConfig));
639
+ }
640
+
641
+ async function deleteAgentDefinition(agentId) {
642
+ const id = agentEditorId(agentId);
643
+ if (!id) throw new Error(`unknown agent "${agentId}"`);
644
+ if (isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and cannot be deleted`);
645
+ const builtIn = isFixedAgent(id);
646
+ const dataDir = dataDirOf();
647
+ const dir = userDir(dataDir, id);
648
+ if (!hasUserAgentFile(dataDir, id)) {
649
+ if (builtIn) throw new Error(`agent "${id}" has no user override to reset`);
650
+ if (!loadAgentDefinition(dataDir, id)) throw new Error(`agent "${id}" not found`);
651
+ }
652
+ // Agents are global (no workflow rosters): deleting a custom agent removes
653
+ // it from every surface at once — catalog, editor, routes, and spawn (the
654
+ // spawn path rejects unknown agent ids).
655
+ if (builtIn) {
656
+ rmSync(dir, { recursive: true, force: true });
657
+ clearAgentDefinitionCache(id);
658
+ const sourceFallback = loadAgentDefinition(dataDir, id);
659
+ // Its threads, and the Leads listing it, take the shipped definition.
660
+ await announceBotChange(dataDir);
661
+ return { id, deleted: true, revertedToBuiltIn: Boolean(sourceFallback) };
662
+ }
663
+ // A custom agent is archived: its AGENT.md (what makes it an agent
664
+ // anywhere) moves aside, the tombstone keeps the packaged copy of a
665
+ // starter from coming back, and the rest (agent.json: name and face,
666
+ // persona, memory, its model and on/off) stays for its archived chat —
667
+ // until restoreAgentDefinition brings it back or purgeAgentDefinition.
668
+ mkdirSync(dir, { recursive: true });
669
+ if (existsSync(join(dir, 'AGENT.md'))) renameSync(join(dir, 'AGENT.md'), join(dir, ARCHIVED_AGENT_FILE));
670
+ writeFileAtomicSync(join(dir, AGENT_DELETED_MARKER), 'deleted\n');
671
+ clearAgentDefinitionCache(id);
672
+ // The Leads listing it drop it from their roster from their next turn.
673
+ await announceBotChange(dataDir);
674
+ return { id, deleted: true, revertedToBuiltIn: false };
675
+ }
676
+
677
+ /** An archived agent back as it was, under its own id: its AGENT.md moved
678
+ * back (a starter without a copy of its own returns to the packaged one)
679
+ * and the tombstone gone. Its routines stay off (the host). */
680
+ async function restoreAgentDefinition(agentId) {
681
+ const id = agentEditorId(agentId);
682
+ if (!id || isHiddenAgent(id) || isFixedAgent(id)) throw new Error(`unknown agent "${agentId}"`);
683
+ const dataDir = dataDirOf();
684
+ if (!isArchivedAgent(dataDir, id)) throw new Error(`agent "${id}" is not deleted`);
685
+ const dir = userDir(dataDir, id);
686
+ const kept = join(dir, ARCHIVED_AGENT_FILE);
687
+ if (!existsSync(kept) && !existsSync(join(shippedDir(id), 'AGENT.md'))) {
688
+ throw new Error(`agent "${id}" was deleted before deleted agents were kept, so it cannot be restored`);
689
+ }
690
+ if (existsSync(kept)) renameSync(kept, join(dir, 'AGENT.md'));
691
+ rmSync(join(dir, AGENT_DELETED_MARKER), { force: true });
692
+ clearAgentDefinitionCache(id);
693
+ // Back in the roster of the Leads that list it, from their next turn.
694
+ await announceBotChange(dataDir);
695
+ return getAgentDefinition(id);
696
+ }
697
+
698
+ /** Deleting an archived agent again: everything it left goes for good. A
699
+ * starter keeps only its tombstone, so its packaged copy stays gone. */
700
+ async function purgeAgentDefinition(agentId) {
701
+ const id = agentEditorId(agentId);
702
+ if (!id || isHiddenAgent(id) || isFixedAgent(id)) throw new Error(`unknown agent "${agentId}"`);
703
+ const dataDir = dataDirOf();
704
+ // Never a live agent; one deleted before archiving existed left nothing.
705
+ if (!isArchivedAgent(dataDir, id)) return { id, purged: false };
706
+ const dir = userDir(dataDir, id);
707
+ rmSync(dir, { recursive: true, force: true });
708
+ clearAgentDefinitionCache(id);
709
+ dropAgentRoute(id);
710
+ if (loadAgentDefinition(dataDir, id)) {
711
+ mkdirSync(dir, { recursive: true });
712
+ writeFileAtomicSync(join(dir, AGENT_DELETED_MARKER), 'deleted\n');
713
+ clearAgentDefinitionCache(id);
714
+ }
715
+ return { id, purged: true };
716
+ }
717
+
718
+ return { dropAgentRoute, deleteAgentDefinition, restoreAgentDefinition, purgeAgentDefinition };
719
+ }
720
+
260
721
  // Agent catalog + editor surface: fixed services stay built-in; starter and
261
722
  // user-authored agents are custom definitions under agents/<id>/AGENT.md.
262
723
  export function createAgentEditorApi(deps) {
@@ -274,6 +735,10 @@ export function createAgentEditorApi(deps) {
274
735
  // The manifest in effect: the user's own copy if any, else the shipped one.
275
736
  const manifestDirOf = (dataDir, id) => (hasUserAgentFile(dataDir, id) ? userDir(dataDir, id) : shippedDir(id));
276
737
  const manifestOf = (dataDir, id) => readManifest(manifestDirOf(dataDir, id));
738
+ // The same, to be written back: the user's copy is recovered or refused when
739
+ // damaged (readManifestForUpdate); a shipped one is only read.
740
+ const manifestForUpdate = (dataDir, id) =>
741
+ hasUserAgentFile(dataDir, id) ? readManifestForUpdate(userDir(dataDir, id)) : readManifest(shippedDir(id));
277
742
  const avatarOf = (dataDir, id) => normalizeAgentAvatar(manifestOf(dataDir, id).avatar);
278
743
  // A saved, archived or restored bot reaches the prompts that embed it: the
279
744
  // fresh scan republishes the shared bot list the Lead rosters read (a new id
@@ -319,20 +784,8 @@ export function createAgentEditorApi(deps) {
319
784
  // The stored definition's revision: a hash of the three files an editor
320
785
  // writes, from the copy in effect. An editor that loaded it can send it back
321
786
  // as `expectedRevision`, and a save over a newer copy is refused.
322
- function definitionRevision(dataDir, id) {
323
- const dir = hasUserAgentFile(dataDir, id) ? userDir(dataDir, id) : shippedDir(id);
324
- const hash = createHash('sha256');
325
- for (const name of ['agent.json', 'AGENT.md', PERSONA_FILE]) {
326
- hash.update(`${name}\0`);
327
- try {
328
- hash.update(readFileSync(join(dir, name)));
329
- } catch {
330
- hash.update('\u0001absent');
331
- }
332
- hash.update('\0');
333
- }
334
- return hash.digest('hex').slice(0, 24);
335
- }
787
+ const definitionRevision = (dataDir, id) =>
788
+ definitionDirRevision(hasUserAgentFile(dataDir, id) ? userDir(dataDir, id) : shippedDir(id));
336
789
 
337
790
  function getAgentDefinition(agentId) {
338
791
  const id = agentEditorId(agentId);
@@ -359,39 +812,11 @@ export function createAgentEditorApi(deps) {
359
812
  };
360
813
  }
361
814
 
362
- function resolveSaveTarget(payload, dataDir) {
363
- const id = agentEditorId(clean(payload.id));
364
- if (hasOwn(payload, 'id') && !id) {
365
- throw new Error('agent id must contain letters/numbers (dashes and dots allowed)');
366
- }
367
- if (id && isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and cannot be edited`);
368
- const name = oneLine(payload.name);
369
- if (!name && !id) throw new Error('agent name must not be empty');
370
- return {
371
- // An archived agent keeps its id: its chat is never handed to a new one.
372
- id:
373
- id ||
374
- availableAgentId(
375
- name,
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))))
384
- ),
385
- name,
386
- description: oneLine(payload.description),
387
- };
388
- }
389
-
390
- async function saveAgentDefinition(payload = {}) {
815
+ async function saveAgentDefinition(payload = {}, options) {
391
816
  const body = String(payload.body || '').trim();
392
817
  if (!body) throw new Error('AGENT.md body must not be empty');
393
818
  const dataDir = resolveDataDir(deps);
394
- const { id, name, description } = resolveSaveTarget(payload, dataDir);
819
+ const { id, name, description } = resolveSaveTarget(loadAgentDefinition, payload, dataDir);
395
820
  // Opt-in (the agent profile): without `expectedRevision` the last write
396
821
  // wins, as for the Workflows editor and the setup tool.
397
822
  if (hasOwn(payload, 'expectedRevision') && String(payload.expectedRevision) !== definitionRevision(dataDir, id)) {
@@ -405,7 +830,7 @@ export function createAgentEditorApi(deps) {
405
830
  const reclaimed = !existed && isArchivedAgent(dataDir, id);
406
831
  const kind = creationKind(payload, existed);
407
832
  const definition = { name: name || id, description, body };
408
- const manifest = hiddenManifest(manifestOf(dataDir, id));
833
+ const manifest = hiddenManifest(manifestForUpdate(dataDir, id));
409
834
  // With the bot store, a new bot is made in bots/ and a new agent in
410
835
  // agents/; either keeps its `kind` marker, so its kind never rests on
411
836
  // the folder alone (a copy read without the store is still a bot).
@@ -421,17 +846,7 @@ export function createAgentEditorApi(deps) {
421
846
  }
422
847
  const dir = storeBot ? botDir(dataDir, id) : userDir(dataDir, id);
423
848
  const inStore = dir === botDir(dataDir, id);
424
- // New custom images arrive as checked bytes, stored under content hashes.
425
- const update = hasOwn(payload, 'avatar') ? avatarUpdate(payload.avatar, dir) : null;
426
- if (update?.avatar) manifest.avatar = update.avatar;
427
- else if (update) delete manifest.avatar;
428
- // Connectors: a policy replaces the saved one, null removes the field
429
- // (every connector on), absent leaves it as saved.
430
- if (hasOwn(payload, 'connectors')) {
431
- const connectors = connectorPolicyInput(payload.connectors);
432
- if (connectors) manifest.connectors = connectors;
433
- else delete manifest.connectors;
434
- }
849
+ const update = applyManifestEdits(manifest, payload, dir, options);
435
850
  // PERSONA.md (tone, how it addresses the user): written when given and
436
851
  // non-empty, removed when given empty, untouched when absent.
437
852
  const persona = hasOwn(payload, 'persona') ? String(payload.persona ?? '').trim() : null;
@@ -453,11 +868,7 @@ export function createAgentEditorApi(deps) {
453
868
  if (!absorbed) {
454
869
  // Hidden frontmatter (the `kind` marker included) survives every edit.
455
870
  const extra = kind ? { ...hiddenFrontmatter(current), kind } : hiddenFrontmatter(current);
456
- writeAgentFiles(dir, { ...definition, extra, manifest });
457
- if (persona) writeFileSync(join(dir, PERSONA_FILE), `${persona}\n`);
458
- else if (persona === '') rmSync(join(dir, PERSONA_FILE), { force: true });
459
- // Only the images the manifest names stay.
460
- syncAvatarFiles(dir, normalizeAgentAvatar(manifest.avatar), update?.writes ?? []);
871
+ writeSavedDefinition(dir, { ...definition, extra, manifest }, persona, update);
461
872
  }
462
873
  clearAgentDefinitionCache(id);
463
874
  if (reclaimed) dropAgentRoute(id);
@@ -471,143 +882,29 @@ export function createAgentEditorApi(deps) {
471
882
  return getAgentDefinition(id);
472
883
  }
473
884
 
474
- // A removed custom agent must not leave a dangling route/preset or a
475
- // disabled-roster entry that would silently disable a later agent of the
476
- // same id.
477
- function dropAgentRoute(id) {
478
- const nextConfig = withAgentDisabled(getConfig(), id, false);
479
- if (nextConfig.agents && id in nextConfig.agents) {
480
- const agents = { ...nextConfig.agents };
481
- delete agents[id];
482
- nextConfig.agents = agents;
483
- }
484
- saveConfigAndAdopt(canonicalizeAgentRouteStorage(nextConfig));
485
- }
486
-
487
- async function deleteAgentDefinition(agentId) {
488
- const id = agentEditorId(agentId);
489
- if (!id) throw new Error(`unknown agent "${agentId}"`);
490
- if (isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and cannot be deleted`);
491
- const builtIn = isFixedAgent(id);
492
- const dataDir = resolveDataDir(deps);
493
- const dir = userDir(dataDir, id);
494
- if (!hasUserAgentFile(dataDir, id)) {
495
- if (builtIn) throw new Error(`agent "${id}" has no user override to reset`);
496
- if (!loadAgentDefinition(dataDir, id)) throw new Error(`agent "${id}" not found`);
885
+ const { dropAgentRoute, deleteAgentDefinition, restoreAgentDefinition, purgeAgentDefinition } = createAgentArchiveApi(
886
+ {
887
+ dataDirOf: () => resolveDataDir(deps),
888
+ getConfig,
889
+ saveConfigAndAdopt,
890
+ loadAgentDefinition,
891
+ shippedDir,
892
+ announceBotChange,
893
+ getAgentDefinition,
497
894
  }
498
- // Agents are global (no workflow rosters): deleting a custom agent removes
499
- // it from every surface at once — catalog, editor, routes, and spawn (the
500
- // spawn path rejects unknown agent ids).
501
- if (builtIn) {
502
- rmSync(dir, { recursive: true, force: true });
503
- clearAgentDefinitionCache(id);
504
- const sourceFallback = loadAgentDefinition(dataDir, id);
505
- // Its threads, and the Leads listing it, take the shipped definition.
506
- await announceBotChange(dataDir);
507
- return { id, deleted: true, revertedToBuiltIn: Boolean(sourceFallback) };
508
- }
509
- // A custom agent is archived: its AGENT.md (what makes it an agent
510
- // anywhere) moves aside, the tombstone keeps the packaged copy of a
511
- // starter from coming back, and the rest (agent.json: name and face,
512
- // persona, memory, its model and on/off) stays for its archived chat —
513
- // until restoreAgentDefinition brings it back or purgeAgentDefinition.
514
- mkdirSync(dir, { recursive: true });
515
- if (existsSync(join(dir, 'AGENT.md'))) renameSync(join(dir, 'AGENT.md'), join(dir, ARCHIVED_AGENT_FILE));
516
- writeFileSync(join(dir, AGENT_DELETED_MARKER), 'deleted\n');
517
- clearAgentDefinitionCache(id);
518
- // The Leads listing it drop it from their roster from their next turn.
519
- await announceBotChange(dataDir);
520
- return { id, deleted: true, revertedToBuiltIn: false };
521
- }
522
-
523
- const isArchivedAgent = (dataDir, id) => existsSync(join(userDir(dataDir, id), AGENT_DELETED_MARKER));
895
+ );
524
896
 
525
- /** An archived agent back as it was, under its own id: its AGENT.md moved
526
- * back (a starter without a copy of its own returns to the packaged one)
527
- * and the tombstone gone. Its routines stay off (the host). */
528
- async function restoreAgentDefinition(agentId) {
529
- const id = agentEditorId(agentId);
530
- if (!id || isHiddenAgent(id) || isFixedAgent(id)) throw new Error(`unknown agent "${agentId}"`);
531
- const dataDir = resolveDataDir(deps);
532
- if (!isArchivedAgent(dataDir, id)) throw new Error(`agent "${id}" is not deleted`);
533
- const dir = userDir(dataDir, id);
534
- const kept = join(dir, ARCHIVED_AGENT_FILE);
535
- if (!existsSync(kept) && !existsSync(join(shippedDir(id), 'AGENT.md'))) {
536
- throw new Error(`agent "${id}" was deleted before deleted agents were kept, so it cannot be restored`);
537
- }
538
- if (existsSync(kept)) renameSync(kept, join(dir, 'AGENT.md'));
539
- rmSync(join(dir, AGENT_DELETED_MARKER), { force: true });
540
- clearAgentDefinitionCache(id);
541
- // Back in the roster of the Leads that list it, from their next turn.
542
- await announceBotChange(dataDir);
543
- return getAgentDefinition(id);
544
- }
897
+ const archivedAgentIdentities = (agentIds = []) => archivedIdentities(resolveDataDir(deps), agentIds);
545
898
 
546
- /** Deleting an archived agent again: everything it left goes for good. A
547
- * starter keeps only its tombstone, so its packaged copy stays gone. */
548
- async function purgeAgentDefinition(agentId) {
549
- const id = agentEditorId(agentId);
550
- if (!id || isHiddenAgent(id) || isFixedAgent(id)) throw new Error(`unknown agent "${agentId}"`);
551
- const dataDir = resolveDataDir(deps);
552
- // Never a live agent; one deleted before archiving existed left nothing.
553
- if (!isArchivedAgent(dataDir, id)) return { id, purged: false };
554
- const dir = userDir(dataDir, id);
555
- rmSync(dir, { recursive: true, force: true });
556
- clearAgentDefinitionCache(id);
557
- dropAgentRoute(id);
558
- if (loadAgentDefinition(dataDir, id)) {
559
- mkdirSync(dir, { recursive: true });
560
- writeFileSync(join(dir, AGENT_DELETED_MARKER), 'deleted\n');
561
- clearAgentDefinitionCache(id);
562
- }
563
- return { id, purged: true };
564
- }
565
-
566
- /** The name and face an archived (deleted) agent left, per id; null for
567
- * any id that is not archived. Its chat lists under them. */
568
- function archivedAgentIdentities(agentIds = []) {
569
- const dataDir = resolveDataDir(deps);
570
- const ids = Array.isArray(agentIds) ? agentIds.filter((id) => typeof id === 'string') : [];
571
- return Object.fromEntries(
572
- ids.map((agentId) => {
573
- const id = agentEditorId(agentId);
574
- if (!id || id !== agentId || !isArchivedAgent(dataDir, id)) return [agentId, null];
575
- const manifest = readManifest(userDir(dataDir, id));
576
- return [agentId, { name: oneLine(manifest.name) || id, avatar: normalizeAgentAvatar(manifest.avatar) ?? null }];
577
- })
899
+ const agentDefinitionPresence = (agentIds = []) =>
900
+ definitionPresence(
901
+ {
902
+ dataDir: resolveDataDir(deps),
903
+ rootDir: deps.rootDir ?? STANDALONE_ROOT,
904
+ stat: deps.statSync ?? statSync,
905
+ },
906
+ agentIds
578
907
  );
579
- }
580
-
581
- // Whether each agent's definition is on disk, decided per id from the files
582
- // themselves: 'deleted' only when every source is PROVEN absent (ENOENT) or
583
- // tombstoned, 'unknown' on any other read fault. A list read swallows
584
- // directory errors, so its absence never proves a deletion; this does.
585
- function agentDefinitionPresence(agentIds = []) {
586
- const dataDir = resolveDataDir(deps);
587
- const rootDir = deps.rootDir ?? STANDALONE_ROOT;
588
- const stat = deps.statSync ?? statSync;
589
- const probe = (path) => {
590
- try {
591
- stat(path);
592
- return 'present';
593
- } catch (error) {
594
- return error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? 'absent' : 'unknown';
595
- }
596
- };
597
- const presence = (agentId) => {
598
- const id = agentEditorId(agentId);
599
- if (!id || id !== agentId) return 'unknown';
600
- const user = probe(join(userDir(dataDir, id), 'AGENT.md'));
601
- if (user !== 'absent') return user;
602
- const tombstone = probe(join(userDir(dataDir, id), AGENT_DELETED_MARKER));
603
- if (tombstone === 'present') return 'deleted';
604
- if (tombstone === 'unknown') return 'unknown';
605
- const shipped = probe(join(rootDir, 'agents', id, 'AGENT.md'));
606
- return shipped === 'absent' ? 'deleted' : shipped;
607
- };
608
- const ids = Array.isArray(agentIds) ? agentIds.filter((id) => typeof id === 'string') : [];
609
- return Object.fromEntries(ids.map((id) => [id, presence(id)]));
610
- }
611
908
 
612
909
  // A custom avatar's images as data URLs by state, read on demand: the
613
910
  // catalog rows (which also reach the setup tool's model output) carry only
@@ -628,107 +925,25 @@ export function createAgentEditorApi(deps) {
628
925
  ? normalizeAgentAvatar(readManifest(dir).avatar)
629
926
  : avatarOf(dataDir, id);
630
927
  if (avatar?.kind !== 'custom' || (!lead && !archived && !hasUserAgentFile(dataDir, id))) return null;
631
- const states = {};
632
- for (const [state, file] of Object.entries(avatar.states)) {
633
- const image = readAvatarImage(dir, file);
634
- if (image) states[state] = image;
635
- }
636
- return states.idle ? { states } : null;
637
- }
638
-
639
- // Mixy (the Lead) has no agent definition to edit: only its PERSONA.md
640
- // (성격) and AGENT.md (일하는 방식, agent mode only), read by the runtime
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));
645
- function readLeadFile(file) {
646
- try {
647
- return readFileSync(join(leadDir(), file), 'utf8').trim();
648
- } catch {
649
- return '';
650
- }
651
- }
652
- function saveLeadFile(file, text) {
653
- const value = String(text ?? '').trim();
654
- if (Buffer.byteLength(value) > LEAD_PERSONA_MAX_BYTES) throw new Error(`${file} is larger than 256 KB`);
655
- const dir = leadDir();
656
- if (value) {
657
- mkdirSync(dir, { recursive: true });
658
- writeFileSync(join(dir, file), `${value}\n`);
659
- } else rmSync(join(dir, file), { force: true });
660
- // Mixy's threads take it from their next turn (the save answers at once).
661
- void publishAgentProfileChange();
662
- return readLeadFile(file);
928
+ return customAvatarImages(dir, avatar);
663
929
  }
664
- const getLeadPersona = () => readLeadFile(PERSONA_FILE);
665
- const saveLeadPersona = (text) => saveLeadFile(PERSONA_FILE, text);
666
- const getLeadAgentBody = () => readLeadFile('AGENT.md');
667
- const saveLeadAgentBody = (text) => saveLeadFile('AGENT.md', text);
668
930
 
669
- // Whether Mixy was created (the "대표 에이전트 만들기" first run): the
670
- // explicit `created` flag in Mixy's agent.json (leadDir). null until decided —
671
- // the desktop host decides once (an existing profile counts as created).
672
- // Also Mixy's display name (`name`, read by the runtime's identity line) and
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).
676
- function getLeadStatus() {
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;
684
- return {
685
- created,
686
- name: oneLine(manifest.name),
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)),
691
- };
692
- }
693
- // Mixy's name and face, chosen in the 대표 에이전트 dialog or its profile:
694
- // a name '' and an avatar null go back to the defaults; absent keys are kept.
695
- function saveLeadIdentity(payload = {}) {
696
- const dir = leadDir();
697
- const manifest = readManifest(dir);
698
- if (hasOwn(payload, 'name')) {
699
- const name = oneLine(payload.name);
700
- if (name.length > 64) throw new Error('name must be 64 characters or fewer');
701
- if (name) manifest.name = name;
702
- else delete manifest.name;
703
- }
704
- const update = hasOwn(payload, 'avatar') ? avatarUpdate(payload.avatar, dir) : null;
705
- if (update?.avatar) manifest.avatar = update.avatar;
706
- else if (update) delete manifest.avatar;
707
- mkdirSync(dir, { recursive: true });
708
- writeFileSync(join(dir, 'agent.json'), `${JSON.stringify(manifest, null, 2)}\n`);
709
- syncAvatarFiles(dir, normalizeAgentAvatar(manifest.avatar), update?.writes ?? []);
710
- // The name is in Mixy's prompt (its identity line); the face is not.
711
- if (hasOwn(payload, 'name')) void publishAgentProfileChange();
712
- return getLeadStatus();
713
- }
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.
717
- function setLeadCreated(created, description) {
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
- }
724
- const manifest = readManifest(leadDir());
725
- manifest.created = created;
726
- const job = oneLine(description);
727
- if (job) manifest.description = job;
728
- mkdirSync(leadDir(), { recursive: true });
729
- writeFileSync(join(leadDir(), 'agent.json'), `${JSON.stringify(manifest, null, 2)}\n`);
730
- return getLeadStatus();
731
- }
931
+ const {
932
+ leadDir,
933
+ getLeadPersona,
934
+ saveLeadPersona,
935
+ getLeadAgentBody,
936
+ saveLeadAgentBody,
937
+ getLeadStatus,
938
+ getLeadHomeRoute,
939
+ saveLeadIdentity,
940
+ setLeadCreated,
941
+ getLeadConnectorPolicy,
942
+ setLeadConnectorPolicy,
943
+ } = createLeadIdentityApi(
944
+ () => resolveDataDir(deps),
945
+ () => normalizeWorkflowRoute(deps.resolveRoute(getConfig(), {}))
946
+ );
732
947
 
733
948
  // The agent's connector policy (see runtime/.../mcp/agent-connectors.mjs),
734
949
  // or null when its definition has none: every connector is on. Read as the
@@ -739,7 +954,7 @@ export function createAgentEditorApi(deps) {
739
954
  if (!id) throw new Error(`unknown agent "${agentId}"`);
740
955
  if (isHiddenAgent(id)) throw new Error(`agent "${id}" is internal and cannot be edited`);
741
956
  const dataDir = resolveDataDir(deps);
742
- if (id === LEAD_AGENT_ID) return readConnectorsFile(join(leadDir(), 'agent.json'));
957
+ if (id === LEAD_AGENT_ID) return getLeadConnectorPolicy();
743
958
  if (!loadAgentDefinition(dataDir, id)) throw new Error(`agent "${agentId}" not found`);
744
959
  return readConnectorsFile(join(manifestDirOf(dataDir, id), 'agent.json'));
745
960
  }
@@ -749,40 +964,18 @@ export function createAgentEditorApi(deps) {
749
964
  const id = agentEditorId(agentId);
750
965
  const next = connectorPolicyInput(connectors);
751
966
  getAgentConnectorPolicy(id);
752
- if (id === LEAD_AGENT_ID) {
753
- const manifest = readManifest(leadDir());
754
- if (next) manifest.connectors = next;
755
- else delete manifest.connectors;
756
- mkdirSync(leadDir(), { recursive: true });
757
- writeFileSync(join(leadDir(), 'agent.json'), `${JSON.stringify(manifest, null, 2)}\n`);
758
- } else {
967
+ assertManifestWritable(join(id === LEAD_AGENT_ID ? leadDir() : userDir(resolveDataDir(deps), id), 'agent.json'));
968
+ if (id === LEAD_AGENT_ID) setLeadConnectorPolicy(next);
969
+ else {
759
970
  const { name, description, body } = loadAgentDefinition(resolveDataDir(deps), id);
760
971
  await saveAgentDefinition({ id, name, description, body, connectors: next });
761
972
  }
762
973
  return getAgentConnectorPolicy(id);
763
974
  }
764
975
 
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
-
784
976
  return {
785
- migrateBotStore,
977
+ migrateAgentKindMarkers: () => migrateAgentKindMarkers(deps),
978
+ migrateBotStore: () => migrateBotStore(deps),
786
979
  getAgentConnectorPolicy,
787
980
  setAgentConnectorPolicy,
788
981
  listAgents,
@@ -793,6 +986,7 @@ export function createAgentEditorApi(deps) {
793
986
  getLeadAgentBody,
794
987
  saveLeadAgentBody,
795
988
  getLeadStatus,
989
+ getLeadHomeRoute,
796
990
  setLeadCreated,
797
991
  saveLeadIdentity,
798
992
  saveAgentDefinition,