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
package/NOTICE.md CHANGED
@@ -125,15 +125,58 @@ downloads it from this repository's `effort-judge-v*` GitHub releases, as
125
125
  listed in `src/runtime/effort-judge/model-manifest.json`.
126
126
 
127
127
  Statement of changes (Apache-2.0 section 4(b)): only the text encoder is kept,
128
- reduced to 12 of its layers; a four-level classification head is added and the
129
- model is fine-tuned to rate the reasoning effort a request needs; the weights
130
- are exported to ONNX and quantized to 4 bits. Full license text:
131
- `LICENSES/Apache-2.0.txt`.
132
-
133
- Its training data includes requests from SWE-chat
134
- (<https://huggingface.co/datasets/cfahlgren1/SWE-chat>), made available under
135
- the Open Data Commons Attribution License 1.0, and from DataClaw exports that
136
- their authors published on the Hugging Face Hub under the MIT License.
128
+ reduced to 18 of its layers; a four-level classification head is added and the
129
+ model is fine-tuned to rate the reasoning effort a request or an agent step
130
+ needs; the weights are exported to ONNX and quantized to 4 bits. Full license
131
+ text: `LICENSES/Apache-2.0.txt`.
132
+
133
+ Its training data includes requests and agent steps from SWE-chat
134
+ (<https://huggingface.co/datasets/SALT-NLP/SWE-chat>, Open Data Commons
135
+ Attribution License 1.0); OpenAssistant oasst2
136
+ (<https://huggingface.co/datasets/OpenAssistant/oasst2>, Apache License 2.0);
137
+ DataClaw exports that their authors published on the Hugging Face Hub under the
138
+ MIT License (<https://huggingface.co/datasets/di-zhang-fdu/ShareDataClaw>);
139
+ DevGPT by Tao Xiao, Christoph Treude, Hideaki Hata and Kenichi Matsumoto
140
+ (<https://doi.org/10.5281/zenodo.16392320>, CC BY 4.0); issue texts from the
141
+ SWE-bench training split (<https://github.com/SWE-bench/SWE-bench>, MIT
142
+ License); and agent steps from NVIDIA's SWE-Hero OpenHands trajectories
143
+ (<https://huggingface.co/datasets/nvidia/SWE-Hero-openhands-trajectories>,
144
+ CC BY 4.0).
145
+
146
+ Contains information from WildChat-4.8M
147
+ (<https://huggingface.co/datasets/allenai/WildChat-4.8M>) which is made
148
+ available under the ODC Attribution License
149
+ (<https://opendatacommons.org/licenses/by/1-0/>).
150
+
151
+ Since `effort-judge-v7` it also includes questions from Stack Exchange sites
152
+ (via <https://huggingface.co/datasets/HuggingFaceTB/stackexchange_2025_md>;
153
+ each post is licensed CC BY-SA 2.5, 3.0 or 4.0 by its author, and its URL and
154
+ author are kept with the training item); GitHub issues and pull requests of
155
+ permissively licensed repositories (via
156
+ <https://huggingface.co/datasets/common-pile/github_archive>, with each item's
157
+ repository, URL, license and authors kept); issues and pull requests from
158
+ Nebius SWE-rebench-V2 (<https://huggingface.co/datasets/nebius/SWE-rebench-V2>,
159
+ <https://huggingface.co/datasets/nebius/SWE-rebench-V2-PRs>, CC BY 4.0,
160
+ permissively licensed repositories only); and agent steps from Nebius
161
+ SWE-agent trajectories
162
+ (<https://huggingface.co/datasets/nebius/SWE-agent-trajectories>, CC BY 4.0),
163
+ Nebius SWE-rebench OpenHands trajectories
164
+ (<https://huggingface.co/datasets/nebius/SWE-rebench-openhands-trajectories>,
165
+ CC BY 4.0), Kwai-Klear SWE-smith mini-swe-agent-plus trajectories
166
+ (<https://huggingface.co/datasets/Kwai-Klear/SWE-smith-mini_swe_agent_plus-trajectories-66k>,
167
+ MIT License), SWE-Factory DeepSWE-Agent Kimi-K2 trajectories
168
+ (<https://huggingface.co/datasets/SWE-Factory/DeepSWE-Agent-Kimi-K2-Trajectories-2.8K>,
169
+ MIT License), SWE-Gym OpenHands SFT trajectories
170
+ (<https://huggingface.co/datasets/SWE-Gym/OpenHands-SFT-Trajectories>, MIT
171
+ License), SWE-ZERO-12M trajectories
172
+ (<https://huggingface.co/datasets/AlienKevin/SWE-ZERO-12M-trajectories>, Apache
173
+ License 2.0), SWE-Lego real and synthetic data
174
+ (<https://huggingface.co/datasets/Lego-X/SWE-Lego-Real-Data>,
175
+ <https://huggingface.co/datasets/Lego-X/SWE-Lego-Synthetic-Data>, Apache
176
+ License 2.0) and TIGER-Lab SWE-Next SFT trajectories
177
+ (<https://huggingface.co/datasets/TIGER-Lab/SWE-Next-SFT-Trajectories>, MIT
178
+ License). Each `effort-judge-v*` release lists its files, hashes and
179
+ attributions.
137
180
 
138
181
  ## CC0-1.0
139
182
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Open-source coding agent that matches Codex on Terminal-Bench 2.1 with 63% fewer tokens.",
@@ -116,6 +116,7 @@
116
116
  "8d343d0a406fd5817b7fd98a3ac47010",
117
117
  "a1b73ce404125d3ce0a5762e4a1e1555",
118
118
  "a28fb01bb21ceebbde1bc6c4a02d3db6",
119
+ "c7fdabb946eed7283db656493d95749b",
119
120
  "d40c23517c4803afdac3fc056bb65cda",
120
121
  "db1af5417849abc2f6bb1d1d2b63939f"
121
122
  ],
@@ -344,6 +345,7 @@
344
345
  "0d6926aaca2890da9347f15a826546e3",
345
346
  "0f14ba0a3d8b3896b717dc144bbe49a4",
346
347
  "15d86146cfbd58d85adf0c25cf1d683f",
348
+ "18dc665b96ee0ba5a1645180e173b1af",
347
349
  "1c317db86e1ff80a5c4102e09e36f5fb",
348
350
  "242f11864abdef90fa0cc9cb4599a579",
349
351
  "29a347c4d3a2f773fa92c627260301ba",
@@ -374,7 +376,8 @@
374
376
  "ff18ac1a9f5961925aec27ffb870bae0"
375
377
  ],
376
378
  "setup-guide": [
377
- "633e15b84dbe22f2b578b67cc35853d4"
379
+ "633e15b84dbe22f2b578b67cc35853d4",
380
+ "920d5b29ccfe6c88f623cf0892b29285"
378
381
  ],
379
382
  "skill-creator": [
380
383
  "020817866da73a69544ba35086bdcd5f",
@@ -52,7 +52,7 @@ Read this when choosing a `setup` mutation.
52
52
  | Remove stored provider authentication (one OAuth account at a time) | `forget_provider_auth` |
53
53
  | Select, rename, or reorder OAuth accounts, set the switching policy, or allow paid credits | `set_provider_account` |
54
54
  | Developer options such as risk-gated OAuth providers | `set_developer_option` |
55
- | Connect a catalog service (Slack, Notion, GitHub, …) for the user: shows a one-click connect card | `connect_connector` |
55
+ | Connect a catalog service (Notion, Linear, Gmail, …; not GitHub, which is the GitHub CLI) for the user: shows a one-click connect card | `connect_connector` |
56
56
  | Add an MCP server | `add_mcp_server` |
57
57
  | Edit or rename an MCP server | `save_mcp_server` |
58
58
  | Remove an MCP server | `remove_mcp_server` |
@@ -112,6 +112,9 @@ workflow` before changing the corresponding domain.
112
112
  `originalName` and `name`. Skill deletion is not exposed; disable it instead.
113
113
  Built-in and plugin skills accept only `toolDependencies` edits (`null`
114
114
  restores their declared dependencies).
115
+ - A created agent is a bot (`definition.kind: "bot"`) or a workflow agent
116
+ (`"agent"`); omitted, the representative bot's chat creates a bot and any other chat a
117
+ workflow agent. The kind is fixed after creation.
115
118
  - Changing orchestration does not rewrite workflow instructions or agents.
116
119
 
117
120
  ## Providers and authentication
@@ -16,7 +16,7 @@ through file edits. One named setting the user already chose belongs to `setup`.
16
16
 
17
17
  ## Style
18
18
 
19
- Reply as in agent mode: one step or one concept per message, short plain
19
+ Reply as in bot mode: one step or one concept per message, short plain
20
20
  sentences, no tables or headings, in the user's language. In Korean use the
21
21
  formal polite style (~합니다 / ~하세요, never ~해요). Say "봇" for Mixy and
22
22
  other contacts, "에이전트" only for workflow agents. Never print ids, file
@@ -67,8 +67,8 @@ connector links an outside service so bots and agents can read and act in it,
67
67
  and anything that sends or posts asks first through an approval card. Then
68
68
  suggest by what the user does, only these: Gmail (with a Google app password;
69
69
  it needs 2-Step Verification on the Google account, and the connect window
70
- opens the app-password page in the browser), GitHub (comes with Git & GitHub),
71
- Notion, Linear or Jira. Do not suggest Google Calendar or Google
70
+ opens the app-password page in the browser), Notion, Linear or Jira. GitHub is
71
+ not a connector; it is the signed-in GitHub CLI of Git & GitHub. Do not suggest Google Calendar or Google
72
72
  Drive; their Google sign-in is still under review. Connect one with
73
73
  `connect_connector`; it only leaves a card, so tell the user to press it and
74
74
  confirm with `status mcp`.
@@ -142,7 +142,7 @@ step; do not insist.
142
142
 
143
143
  ## Glossary
144
144
 
145
- - Bot (봇): an agent-mode contact you chat with. Mixy is the representative
145
+ - Bot (봇): a contact you chat with in bot mode (봇 모드). Mixy is the representative
146
146
  bot; the user can have it make others, such as a GitHub Watcher.
147
147
  - Agent (에이전트): a workflow sub-agent such as Worker or Reviewer that
148
148
  does part of a task. Users add their own on the Workflows page with the
@@ -150,12 +150,15 @@ step; do not insist.
150
150
  - Default agents (기본 에이전트): built-in helpers listed on Workflows, such as
151
151
  Web Search (runs the web search tool) and the Maintainer (memory upkeep and
152
152
  session titles). They need a model route to work.
153
- - Sessions and projects: a coding session runs in a Project, a folder whose
153
+ - Agent mode (에이전트 모드): a coding work session run by Lead and agents.
154
+ A session a bot starts for a job is an agent task session (에이전트 작업
155
+ 세션); others are just sessions (세션).
156
+ - Sessions and projects: an agent-mode session runs in a Project, a folder whose
154
157
  instructions and memories it loads.
155
158
  - Workflows and work rules: a workflow sets how a request gets done; work
156
159
  rules are the instructions it applies.
157
- - Connectors and extensions: connectors are MCP services such as GitHub or
158
- Notion (`connect_connector`, the user presses the card); extensions are
160
+ - Connectors and extensions: connectors are MCP services such as Notion or
161
+ Linear (`connect_connector`, the user presses the card); extensions are
159
162
  plugins that add tools and skills that teach how to work.
160
163
  - Group rooms (그룹 대화방): one chat with several bots. `@name` or "봇 부르기"
161
164
  addresses one bot; otherwise the Maintainer decides who answers next.
@@ -20,15 +20,20 @@ partial: true
20
20
  destructive-action confirmations; quote needed error text exactly.
21
21
 
22
22
  Layout (GitHub-flavored Markdown, `$$…$$` for display math): the content picks
23
- the format at every depth; depth sets only how much is said.
23
+ the format at every depth; depth sets only how much is said. There is no fixed
24
+ template: a reply's shape follows what it says, so consecutive replies look
25
+ different when their content differs.
24
26
 
25
- - Mixed content gets one short block per kind (result, cause, finding,
26
- decision needed, remaining work) under a plain label in the reader's
27
- language, never one running paragraph; what the reader must decide or do
28
- comes last, on its own.
29
- - A single answer or cause is short prose paragraphs without labels. Three or
30
- more parallel items are a flat one-line list, numbered only for order, never
31
- nested. A comparison across items is a small table with short cells.
27
+ - An explanation, a single answer, or a cause and its effect is short prose.
28
+ - Items that share two or more attributes (status of several tasks, options
29
+ and their costs, before/after, provider × behavior) are a small table with
30
+ short cells, not a list of labelled bullets.
31
+ - Ordered steps are a numbered list; three or more parallel, independent
32
+ points are a flat list; never nested.
33
+ - Mixed content opens with one sentence carrying the outcome, then uses
34
+ whichever of the forms above fits each part. Plain labels only separate
35
+ parts that are long and genuinely different; never label every short part.
36
+ - What the reader must decide or do comes last, on its own.
32
37
  - Commands, paths, identifiers, and literals go in code; multi-line code,
33
38
  diffs, and output in fenced blocks.
34
39
  - Bold only what the reader scans for: the decision or warning they must not
@@ -1,4 +1,4 @@
1
- # Agent Mode
1
+ # Bot Mode
2
2
 
3
3
  - Speak naturally in your persona, like a capable colleague texting: plain
4
4
  everyday sentences, technical detail only when asked. No opener, filler,
@@ -12,10 +12,17 @@
12
12
  requested scope and existing permissions. You alone create, edit or remove
13
13
  bots, and you manage every bot's routines and settings; other bots manage
14
14
  only their own.
15
- - Users call agent-mode contacts (you and the ones made for them) "bots"
15
+ - Users call bot-mode contacts (you and the ones made for them) "bots"
16
16
  (봇), and the workflow sub-agents (Worker, Reviewer, …) "agents"
17
- (에이전트); use those words with the user.
17
+ (에이전트). Chatting with a bot is "bot mode" (봇 모드); a coding work
18
+ session run by Lead and agents is "agent mode" (에이전트 모드). A session
19
+ you start with `session create` is an "agent task session" (에이전트 작업
20
+ 세션); any other is a "session" (세션). Use those words with the user.
18
21
  - You coordinate the user's bots and agents: route a request to the one best
19
- suited for it (a bot, a sub-agent, or a new coding-mode task session for
22
+ suited for it (a bot, a sub-agent, or a new agent task session for
20
23
  substantial coding) with a self-contained brief, start it when already
21
- requested, otherwise suggest it, and follow up with the result.
24
+ requested, otherwise suggest it, and follow up with the result. Promise a
25
+ follow-up only for work that reports back: a bot, a sub-agent or an agent
26
+ task session you created, or a `session send` whose result says
27
+ reportsBack: true. Otherwise say it was sent and that the user can check
28
+ that session.
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { createHash } from 'node:crypto';
4
4
  import { existsSync, readdirSync } from 'node:fs';
5
- import { join } from 'node:path';
5
+ import { basename, dirname, join } from 'node:path';
6
6
  import { compareCodePoints } from '../../../shared/code-point-order.mjs';
7
+ import { BOTS_DIR, MIXY_BOT_ID, botStoreActive, isStoreBot, mixyIdentityDir } from '../../../shared/agent-store.mjs';
7
8
  import { mixdogRoot } from '../../../shared/plugin-paths.mjs';
8
9
  import { readMarkdownDocument } from '../../../shared/markdown-frontmatter.mjs';
9
10
  import { listHiddenAgentsByKind, getAgentCatalogShareAgents } from '../internal-agents.mjs';
@@ -89,9 +90,35 @@ function agentSectionDirs(pluginRoot) {
89
90
  } catch {
90
91
  /* unresolvable data dir — built-in roles still load */
91
92
  }
93
+ // Bots of the store (agent-store.mjs), last: a bot's own folder wins over
94
+ // the agents/ original the split left behind.
95
+ const root = userDataRoot();
96
+ if (root && botStoreActive(root)) dirs.push(join(root, BOTS_DIR));
92
97
  return dirs;
93
98
  }
94
99
 
100
+ // The user data dir (parent of agents/), or '' when it cannot be resolved.
101
+ function userDataRoot() {
102
+ try {
103
+ const userDir = mixdogGlobalDir('agents');
104
+ return userDir ? dirname(userDir) : '';
105
+ } catch {
106
+ return '';
107
+ }
108
+ }
109
+
110
+ // A store bot's own folder, or ''.
111
+ function storeBotDir(agent) {
112
+ const root = userDataRoot();
113
+ return root && isStoreBot(root, agent) ? join(root, BOTS_DIR, agent) : '';
114
+ }
115
+
116
+ // Mixy's identity folder: bots/mixy with the store, agents/lead before.
117
+ function mixyDir() {
118
+ const root = userDataRoot();
119
+ return root ? mixyIdentityDir(root) : '';
120
+ }
121
+
95
122
  function loadAgentSections(pluginRoot) {
96
123
  // agents/ accepts both the compatibility flat layout and the current
97
124
  // nested agents/<agent>/AGENT.md layout.
@@ -117,6 +144,7 @@ function loadAgentSections(pluginRoot) {
117
144
  // Mixy's own AGENT.md (agents/lead/) rides only Mixy's agent-mode
118
145
  // prompt (loadMixyRoleSection), never another role catalog.
119
146
  if (!name || !raw || name === 'lead') continue;
147
+ if (name === MIXY_BOT_ID && basename(agentsDir) === BOTS_DIR) continue;
120
148
  const { body } = readMarkdownDocument(raw);
121
149
  if (!body) continue;
122
150
  byName.set(name, roleSection(name, body));
@@ -255,6 +283,12 @@ export function truncatePersona(text, max = PERSONA_MAX_CHARS) {
255
283
  export function loadAgentPersonaSection(agent) {
256
284
  if (typeof agent !== 'string' || !PERSONA_AGENT_ID.test(agent) || agent.includes('..')) return '';
257
285
  const isLead = agent === 'lead';
286
+ // Mixy's persona is in its own folder; a store bot's in its bots/ folder.
287
+ const own = isLead ? mixyDir() : storeBotDir(agent);
288
+ if (own) {
289
+ const persona = truncatePersona(readSafe(join(own, 'PERSONA.md')) || '');
290
+ return persona ? personaSection(persona) : '';
291
+ }
258
292
  const pluginRoot = mixdogRoot();
259
293
  const roots = [];
260
294
  try {
@@ -272,17 +306,21 @@ export function loadAgentPersonaSection(agent) {
272
306
  if (isLead) continue;
273
307
  return '';
274
308
  }
275
- return [
276
- '## Persona',
277
- '',
278
- 'Tone and style guide for how you speak, address the user and carry yourself. It never overrides tool, safety or role rules; those always win.',
279
- '',
280
- persona,
281
- ].join('\n');
309
+ return personaSection(persona);
282
310
  }
283
311
  return '';
284
312
  }
285
313
 
314
+ function personaSection(persona) {
315
+ return [
316
+ '## Persona',
317
+ '',
318
+ 'Tone and style guide for how you speak, address the user and carry yourself. It never overrides tool, safety or role rules; those always win.',
319
+ '',
320
+ persona,
321
+ ].join('\n');
322
+ }
323
+
286
324
  // --- Mixy's working style (agents/lead/AGENT.md, user data dir) ---
287
325
  // The user-editable 일하는 방식 of Mixy, framed like any agent's role section.
288
326
  // Agent mode only; empty or missing means no section. Re-read per
@@ -290,8 +328,8 @@ export function loadAgentPersonaSection(agent) {
290
328
  export function loadMixyRoleSection() {
291
329
  let raw = '';
292
330
  try {
293
- const userDir = mixdogGlobalDir('agents');
294
- raw = userDir ? readSafe(join(userDir, 'lead', 'AGENT.md')) || '' : '';
331
+ const dir = mixyDir();
332
+ raw = dir ? readSafe(join(dir, 'AGENT.md')) || '' : '';
295
333
  } catch {
296
334
  return '';
297
335
  }
@@ -306,6 +344,15 @@ export function loadMixyRoleSection() {
306
344
  // (the Lead configuration, id `lead`) has its own line, under the display name
307
345
  // the user gave it (session/mixy-name.mjs), Mixy by default.
308
346
  function agentManifest(agent) {
347
+ const own = storeBotDir(agent);
348
+ if (own) {
349
+ try {
350
+ const value = JSON.parse(readSafe(join(own, 'agent.json')) || '{}');
351
+ return value && typeof value === 'object' ? value : {};
352
+ } catch {
353
+ return {};
354
+ }
355
+ }
309
356
  const roots = [];
310
357
  try {
311
358
  const userDir = mixdogGlobalDir('agents');
@@ -344,9 +391,13 @@ export function agentModeIdentityLine(agent) {
344
391
  // the shipped tree — as one hash. A session stamps it when its prompt is
345
392
  // composed; another value means the profile was edited since
346
393
  // (session-runtime/tool-policy-refresh.mjs refreshAgentProfile). '' for an
347
- // invalid id.
348
- export function agentProfileRevision(agent) {
394
+ // invalid id. A coding Lead (`lead` outside Mixy's own thread) embeds no
395
+ // profile file at all, so its stamp is a constant: editing Mixy never
396
+ // rebuilds a coding session's prompt.
397
+ export const CODING_LEAD_PROFILE_REVISION = 'coding-lead';
398
+ export function agentProfileRevision(agent, { mixy = true } = {}) {
349
399
  if (typeof agent !== 'string' || !PERSONA_AGENT_ID.test(agent) || agent.includes('..')) return '';
400
+ if (agent === 'lead' && !mixy) return CODING_LEAD_PROFILE_REVISION;
350
401
  const roots = [mixdogGlobalDir('agents')];
351
402
  const pluginRoot = mixdogRoot();
352
403
  if (pluginRoot) roots.push(join(pluginRoot, 'agents'));
@@ -356,6 +407,13 @@ export function agentProfileRevision(agent) {
356
407
  hash.update(`${root}\0${file}\0${readSafe(join(root, agent, file)) ?? '\u0001'}\0`);
357
408
  }
358
409
  }
410
+ // Mixy's or a store bot's own folder, when it has one.
411
+ const own = agent === 'lead' ? mixyDir() : storeBotDir(agent);
412
+ if (own && own !== join(roots[0] || '', agent)) {
413
+ for (const file of ['agent.json', 'AGENT.md', 'PERSONA.md']) {
414
+ hash.update(`${own}\0${file}\0${readSafe(join(own, file)) ?? '\u0001'}\0`);
415
+ }
416
+ }
359
417
  return hash.digest('hex').slice(0, 24);
360
418
  }
361
419
 
@@ -417,8 +475,8 @@ export function composeSystemPrompt(opts) {
417
475
  ? loadMixyRoleSection()
418
476
  : loadScopedRoleInstructions(opts.agent || null, opts.provider || null);
419
477
  }
420
- // Persona follows the role; the Lead has no role section but may own a
421
- // persona through agents/lead/PERSONA.md.
478
+ // Persona follows the role; Mixy's own thread carries Mixy's persona
479
+ // (bots/mixy/PERSONA.md) — a coding Lead's prompt never does (skipPersona).
422
480
  const personaContext = opts.agent && !opts.skipPersona ? loadAgentPersonaSection(opts.agent) : '';
423
481
  const coreMemory = skip.memory ? '' : trimmedPromptSlice(opts.coreMemoryContext);
424
482
  // User AGENTS.md / CLAUDE.md block (context/instruction-files.mjs), already
@@ -14,6 +14,7 @@ import { readFileSync, statSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
  import { cwdWithinProjects } from '../../../shared/extension-scopes.mjs';
16
16
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
17
+ import { userDefinitionDir } from '../../../shared/agent-store.mjs';
17
18
  import { isSelfMcpToolName, parseMcpToolName } from '../../../shared/tool-primitives.mjs';
18
19
  import { connectorToolDefault } from '../../../shared/connector-catalog.mjs';
19
20
  import { serverConnector } from './server-connectors.mjs';
@@ -149,7 +150,7 @@ export function readConnectorsFile(path) {
149
150
  export function agentConnectors(agentId, dataDir = resolvePluginData()) {
150
151
  const id = clean(agentId);
151
152
  if (!id || !AGENT_DIR_ID.test(id)) return null;
152
- return cachedRead(join(dataDir, 'agents', id, 'agent.json'), readConnectorsFile);
153
+ return cachedRead(join(userDefinitionDir(dataDir, id), 'agent.json'), readConnectorsFile);
153
154
  }
154
155
 
155
156
  /** Uncached: the project policy file → `[{ root, off }]`, null without one,
@@ -187,9 +187,14 @@ export function normalizeAntigravityCatalog(rawModels) {
187
187
  return models.sort((a, b) => a.display.localeCompare(b.display));
188
188
  }
189
189
 
190
- /** Catalog to resolve wire ids against: in-memory mirror, disk cache, then the curated fallback. */
190
+ /**
191
+ * Catalog to resolve wire ids against: in-memory mirror, disk cache, then the
192
+ * curated fallback. Sends never refresh the catalog, so the account's last live
193
+ * list is used even past its TTL: it names the wire ids this account was
194
+ * offered, which the curated list can only approximate.
195
+ */
191
196
  function antigravityCatalogModels() {
192
- if (!_mirror) _mirror = antigravityModelCache.loadSync();
197
+ if (!_mirror) _mirror = antigravityModelCache.loadSync({ allowStale: true });
193
198
  return _mirror || ANTIGRAVITY_MODELS;
194
199
  }
195
200
 
@@ -31,14 +31,14 @@ export const AUTO_EFFORT_LEVELS = Object.freeze(['easy', 'normal', 'hard', 'very
31
31
  const STEP_OF_LEVEL = [-1, 0, 1, 2];
32
32
 
33
33
  // The chosen level minimises the expected cost under the judge distribution.
34
- // A miss costs one per level, plus extra for an opposite call and for a
35
- // needless "very hard" (xhigh). Opposite calls are asymmetric: "easy" for a
36
- // hard turn degrades the answer, "hard" for an easy turn only costs tokens.
37
- // On the held-out test splits the heavier under-call cost cut hard-as-easy
38
- // calls by a third (20 -> 13 of 7,180) for -0.2 points of exact accuracy.
34
+ // A miss costs one per level, plus extra for an opposite call. Opposite calls
35
+ // are asymmetric: "easy" for a hard turn degrades the answer, "hard" for an
36
+ // easy turn only costs tokens. On the held-out test splits the heavier
37
+ // under-call cost cut hard-as-easy calls by a third (20 -> 13 of 7,180) for
38
+ // -0.2 points of exact accuracy. "Very hard" carries no extra cost: an extra
39
+ // penalty there let judge v7 pick xhigh on only 17% of very hard turns.
39
40
  export const AUTO_EFFORT_UNDER_OPPOSITE_COST = 3;
40
41
  export const AUTO_EFFORT_OVER_OPPOSITE_COST = 1;
41
- export const AUTO_EFFORT_FALSE_VERY_HARD_COST = 1;
42
42
  // Ties keep the default first, then the cheaper side.
43
43
  const TIE_ORDER = [1, 0, 2, 3];
44
44
 
@@ -49,8 +49,7 @@ function expectedCost(values, level) {
49
49
  p *
50
50
  (Math.abs(level - actual) +
51
51
  (level === 0 && actual >= 2 ? AUTO_EFFORT_UNDER_OPPOSITE_COST : 0) +
52
- (actual === 0 && level >= 2 ? AUTO_EFFORT_OVER_OPPOSITE_COST : 0) +
53
- (level === 3 && actual < 3 ? AUTO_EFFORT_FALSE_VERY_HARD_COST : 0));
52
+ (actual === 0 && level >= 2 ? AUTO_EFFORT_OVER_OPPOSITE_COST : 0));
54
53
  });
55
54
  return cost;
56
55
  }
@@ -10,6 +10,7 @@ import { mergeFlatSchemaProperty } from './flat-schema-merge.mjs';
10
10
  import { reportedCostUsd } from './usage-primitives.mjs';
11
11
  import { reportInvalidTool, wireInputSchema } from './claude-bridge-schema.mjs';
12
12
  import { normalizeServerToolUse } from './anthropic-server-tool-usage.mjs';
13
+ import { ANTHROPIC_TOOL_CALL_IDS, withWireToolCallIds } from './tool-call-wire-ids.mjs';
13
14
  export const ANTHROPIC_CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' };
14
15
  const ANTHROPIC_CACHE_TTL_VOLATILE = { type: 'ephemeral' };
15
16
 
@@ -232,7 +233,8 @@ const ANTHROPIC_NATIVE_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
232
233
 
233
234
  // `provider`: the engine's provider name. A gateway on a native Messages
234
235
  // route (wire-route.mjs) also replays the references it tagged itself.
235
- export function toAnthropicMessages(messages, availableTools, provider = '') {
236
+ export function toAnthropicMessages(transcript, availableTools, provider = '') {
237
+ const messages = withWireToolCallIds(transcript, ANTHROPIC_TOOL_CALL_IDS);
236
238
  const ownProvider = String(provider || '').toLowerCase();
237
239
  const availableNames = anthropicToolNameSet(availableTools);
238
240
  const result = [];
@@ -0,0 +1,65 @@
1
+ // Tool-call ids at the wire. A transcript keeps each tool call's id as the
2
+ // provider that produced it minted it; after a model switch another provider
3
+ // may refuse it (Anthropic accepts only [A-Za-z0-9_-]; OpenAI chat caps the
4
+ // length). Each request builder projects the ids onto its own rule just
5
+ // before lowering: a valid id passes unchanged (so native replay items and
6
+ // the provider's own ids stay byte-exact), an invalid one becomes a
7
+ // deterministic, collision-free stand-in, and a call and its result always
8
+ // map to the same value. Persisted history is never rewritten.
9
+ import { createHash } from 'node:crypto';
10
+
11
+ export const ANTHROPIC_TOOL_CALL_IDS = Object.freeze({ allowed: /^[A-Za-z0-9_-]+$/, maxLength: 64 });
12
+ export const OPENAI_RESPONSES_TOOL_CALL_IDS = Object.freeze({ allowed: /^[A-Za-z0-9_.:-]+$/, maxLength: 64 });
13
+ export const OPENAI_CHAT_TOOL_CALL_IDS = Object.freeze({ allowed: /^[A-Za-z0-9_.:-]+$/, maxLength: 40 });
14
+
15
+ const HASH_CHARS = 10;
16
+
17
+ function standIn(id, rule, salt) {
18
+ const hash = createHash('sha256').update(`${salt}\u0000${id}`).digest('hex').slice(0, HASH_CHARS);
19
+ const stem = String(id)
20
+ .replace(/[^A-Za-z0-9_-]+/g, '_')
21
+ .replace(/^_+|_+$/g, '')
22
+ .slice(0, Math.max(0, rule.maxLength - HASH_CHARS - 1));
23
+ return stem ? `${stem}_${hash}` : `call_${hash}`.slice(0, rule.maxLength);
24
+ }
25
+
26
+ const isValid = (id, rule) => id.length > 0 && id.length <= rule.maxLength && rule.allowed.test(id);
27
+
28
+ /** id → wire id for every tool call and result in `messages` under `rule`. */
29
+ export function toolCallWireIdMap(messages, rule) {
30
+ const ids = new Set();
31
+ for (const message of Array.isArray(messages) ? messages : []) {
32
+ if (message?.role === 'assistant')
33
+ for (const call of message.toolCalls || []) if (call?.id) ids.add(String(call.id));
34
+ if (message?.role === 'tool' && message.toolCallId) ids.add(String(message.toolCallId));
35
+ }
36
+ const map = new Map();
37
+ const taken = new Set([...ids].filter((id) => isValid(id, rule)));
38
+ for (const id of ids) {
39
+ if (isValid(id, rule)) continue;
40
+ let salt = 0;
41
+ let wire = standIn(id, rule, salt);
42
+ while (taken.has(wire)) wire = standIn(id, rule, ++salt);
43
+ taken.add(wire);
44
+ map.set(id, wire);
45
+ }
46
+ return map;
47
+ }
48
+
49
+ /** `messages` with tool-call ids projected onto `rule`; the same array when none needs it. */
50
+ export function withWireToolCallIds(messages, rule) {
51
+ const map = toolCallWireIdMap(messages, rule);
52
+ if (!map.size) return messages;
53
+ return messages.map((message) => {
54
+ if (message?.role === 'tool' && map.has(String(message.toolCallId))) {
55
+ return { ...message, toolCallId: map.get(String(message.toolCallId)) };
56
+ }
57
+ if (message?.role !== 'assistant' || !message.toolCalls?.some((call) => map.has(String(call?.id)))) return message;
58
+ return {
59
+ ...message,
60
+ toolCalls: message.toolCalls.map((call) =>
61
+ map.has(String(call?.id)) ? { ...call, id: map.get(String(call.id)) } : call
62
+ ),
63
+ };
64
+ });
65
+ }
@@ -104,10 +104,16 @@ export const COMPAT_RETRY_PROFILES = Object.freeze({
104
104
  }),
105
105
  });
106
106
 
107
- /** withRetry options (attempt budget, schedule, jitter) for a provider; {} for the shared defaults. */
107
+ // A provider without a profile (custom endpoints, gateways, local servers)
108
+ // keeps the shared ladder, but a Retry-After beyond a minute is not waited
109
+ // out: it surfaces with its wait so the account pool can rotate and the
110
+ // notice can name when to try again.
111
+ export const COMPAT_DEFAULT_MAX_RETRY_AFTER_MS = 60_000;
112
+
113
+ /** withRetry options (attempt budget, schedule, jitter) for a provider; only the advice ceiling for the shared defaults. */
108
114
  export function compatRetryOptions(providerName) {
109
115
  const profile = COMPAT_RETRY_PROFILES[providerName];
110
- if (!profile) return {};
116
+ if (!profile) return { maxRetryAfterMs: COMPAT_DEFAULT_MAX_RETRY_AFTER_MS };
111
117
  return {
112
118
  maxAttempts: profile.maxAttempts,
113
119
  backoffMs: profile.backoffMs,
@@ -17,6 +17,11 @@ import {
17
17
  } from './media-normalization.mjs';
18
18
  import { customToolCallFromResponseItem, nativeToolSearchCallFromArguments } from './custom-tool-wire.mjs';
19
19
  import { providerReplayItems } from './lib/provider-replay.mjs';
20
+ import {
21
+ OPENAI_CHAT_TOOL_CALL_IDS,
22
+ OPENAI_RESPONSES_TOOL_CALL_IDS,
23
+ withWireToolCallIds,
24
+ } from './lib/tool-call-wire-ids.mjs';
20
25
  import { acceptedWireTools } from './lib/claude-bridge-schema.mjs';
21
26
  import { ensureResponsesCallOutputs } from './lib/wire-pairing.mjs';
22
27
  import { responsesEffortUpdates } from './effort-configuration.mjs';
@@ -34,7 +39,8 @@ export function resolveCompatMaxOutputTokens(opts = {}) {
34
39
  );
35
40
  }
36
41
 
37
- export function toOpenAIMessages(messages, providerName, options = {}) {
42
+ export function toOpenAIMessages(transcript, providerName, options = {}) {
43
+ const messages = withWireToolCallIds(transcript, OPENAI_CHAT_TOOL_CALL_IDS);
38
44
  // NOTE: chat.completions has no equivalent slot for replaying reasoning
39
45
  // encrypted_content the way the Responses API does (no `type:'reasoning'`
40
46
  // input item). Whatever reasoningItems may be attached to assistant
@@ -421,7 +427,8 @@ export function xaiSystemInstructions(messages) {
421
427
  return instructions || undefined;
422
428
  }
423
429
 
424
- export function toXaiResponsesInput(messages, providerState, options = {}) {
430
+ export function toXaiResponsesInput(transcript, providerState, options = {}) {
431
+ const messages = withWireToolCallIds(transcript, OPENAI_RESPONSES_TOOL_CALL_IDS);
425
432
  const includeSystem = options.includeSystem !== false;
426
433
  // Generic Responses callers (opencode-go gateway) keep their own state
427
434
  // slot and replay tag so a provider switch never replays foreign items.
@@ -1,6 +1,7 @@
1
1
  // Transcript lowering is independent of request/turn metadata. A retained
2
2
  // message must have the same wire representation in every subsequent request.
3
3
  import { providerReplayItems } from './lib/provider-replay.mjs';
4
+ import { OPENAI_RESPONSES_TOOL_CALL_IDS, withWireToolCallIds } from './lib/tool-call-wire-ids.mjs';
4
5
  import { responsesEffortUpdates } from './effort-configuration.mjs';
5
6
  import { ensureResponsesCallOutputs } from './lib/wire-pairing.mjs';
6
7
  import { normalizeContentForOpenAIResponses, splitToolContentForOpenAIResponses } from './media-normalization.mjs';
@@ -147,7 +148,8 @@ function pushAssistantToolCalls(toolCalls, { out, customToolCallIds, nativeSearc
147
148
  * - `includeSystem`, `nativePdf`, `splitToolContent`, `startIndex`: the
148
149
  * gateway's system/media/continuation handling.
149
150
  */
150
- export function convertMessagesToResponsesInput(messages, opts = {}) {
151
+ export function convertMessagesToResponsesInput(transcript, opts = {}) {
152
+ const messages = withWireToolCallIds(transcript, OPENAI_RESPONSES_TOOL_CALL_IDS);
151
153
  const out = [];
152
154
  const pendingToolMedia = [];
153
155
  const pendingToolLoads = [];