mixdog 0.9.85 → 0.9.87

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 (102) hide show
  1. package/README.md +7 -1
  2. package/package.json +4 -3
  3. package/src/defaults/agents.json +12 -0
  4. package/src/defaults/skills/setup/SKILL.md +44 -12
  5. package/src/help.mjs +40 -9
  6. package/src/lib/keychain-cjs.cjs +11 -1
  7. package/src/rules/agent/43-title-agent.md +22 -0
  8. package/src/rules/shared/01-tool.md +3 -3
  9. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
  11. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
  12. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
  13. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
  15. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
  16. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
  18. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
  19. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
  20. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
  21. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
  22. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
  23. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
  24. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  25. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
  26. package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
  27. package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
  29. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
  30. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
  32. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
  33. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  34. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
  35. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  36. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
  37. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
  38. package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
  39. package/src/runtime/agent/orchestrator/tools/token-manifest.json +26 -0
  40. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
  41. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  42. package/src/runtime/media/renditions.mjs +1 -1
  43. package/src/runtime/media/store.mjs +1 -1
  44. package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
  45. package/src/runtime/memory/tool-defs.mjs +19 -3
  46. package/src/runtime/shared/atomic-file.mjs +25 -14
  47. package/src/runtime/shared/automation-attachments.mjs +3 -3
  48. package/src/runtime/shared/child-guardian.mjs +15 -5
  49. package/src/runtime/shared/child-spawn-gate.mjs +6 -12
  50. package/src/runtime/shared/resource-admission.mjs +7 -3
  51. package/src/runtime/shared/turn-snapshot.mjs +0 -3
  52. package/src/session-runtime/lifecycle-api.mjs +49 -9
  53. package/src/session-runtime/prewarm.mjs +35 -0
  54. package/src/session-runtime/provider-auth-api.mjs +8 -0
  55. package/src/session-runtime/provider-usage.mjs +30 -5
  56. package/src/session-runtime/remote-control.mjs +8 -47
  57. package/src/session-runtime/remote-transcript.mjs +3 -7
  58. package/src/session-runtime/route-preparation.mjs +24 -0
  59. package/src/session-runtime/runtime-core.mjs +286 -548
  60. package/src/session-runtime/session-lifecycle.mjs +378 -0
  61. package/src/session-runtime/session-turn-api.mjs +9 -2
  62. package/src/session-runtime/workflow-agents-api.mjs +3 -0
  63. package/src/standalone/agent-shard/shard-child.mjs +300 -0
  64. package/src/standalone/agent-shard/shard-pool.mjs +443 -0
  65. package/src/standalone/agent-tool/job-views.mjs +329 -0
  66. package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
  67. package/src/standalone/agent-tool/tag-registry.mjs +340 -0
  68. package/src/standalone/agent-tool.mjs +112 -941
  69. package/src/standalone/channel-daemon-transport.mjs +126 -185
  70. package/src/standalone/channel-daemon.mjs +9 -10
  71. package/src/standalone/usage-dashboard.mjs +23 -1
  72. package/src/tui/App.jsx +362 -2569
  73. package/src/tui/app/app-view.jsx +504 -0
  74. package/src/tui/app/channel-pickers.mjs +3 -8
  75. package/src/tui/app/create-app-pickers.mjs +313 -0
  76. package/src/tui/app/prompt-submit.mjs +501 -0
  77. package/src/tui/app/route-pickers.mjs +117 -152
  78. package/src/tui/app/settings-picker.mjs +90 -89
  79. package/src/tui/app/shell-layout.mjs +563 -0
  80. package/src/tui/app/slash-commands.mjs +7 -7
  81. package/src/tui/app/slash-dispatch.mjs +0 -45
  82. package/src/tui/app/usage-context-panels.mjs +288 -0
  83. package/src/tui/app/use-copy-selection.mjs +56 -0
  84. package/src/tui/app/use-global-key-input.mjs +151 -0
  85. package/src/tui/app/use-pasted-buffers.mjs +115 -0
  86. package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
  87. package/src/tui/app/use-prompt-hint.mjs +58 -0
  88. package/src/tui/app/use-prompt-queue-history.mjs +100 -0
  89. package/src/tui/app/use-terminal-chrome.mjs +72 -0
  90. package/src/tui/app/use-transcript-activity.mjs +131 -0
  91. package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
  92. package/src/tui/components/StatusLine.jsx +1 -1
  93. package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
  94. package/src/tui/dist/index.mjs +9857 -9159
  95. package/src/tui/engine/session-api-ext.mjs +47 -9
  96. package/src/tui/engine.mjs +7 -1
  97. package/src/tui/figures.mjs +0 -1
  98. package/src/ui/statusline-format.mjs +0 -1
  99. package/src/hooks/lib/permission-evaluator.cjs +0 -24
  100. package/src/lib/config-cjs.cjs +0 -61
  101. package/src/runtime/shared/launcher-control.mjs +0 -258
  102. package/src/runtime/shared/workspace-router.mjs +0 -259
package/README.md CHANGED
@@ -138,7 +138,7 @@ Common slash commands:
138
138
  /setting open the runtime settings hub
139
139
  /mcp manage MCP servers and tools
140
140
  /skills choose a skill for the next request
141
- /channels manage Discord/Telegram, schedules, webhooks, voice
141
+ /channels manage Discord, Telegram, and voice
142
142
  /compact compact older conversation context
143
143
  /clear reset the conversation and screen
144
144
  /OutputStyle show or switch Lead output style
@@ -149,6 +149,12 @@ route. The model picker warms the provider catalog in the background and keeps
149
149
  Claude families such as Opus, Sonnet, Haiku, and Fable separate when filtering
150
150
  current Anthropic models.
151
151
 
152
+ Workflows and agents are Markdown definition packs (`WORKFLOW.md`,
153
+ `AGENT.md`). Built-ins ship with mixdog; custom packs live under the data
154
+ directory (`workflows/<id>/`, `agents/<id>/`) and are edited on the desktop
155
+ app's Workflows page. Schedules and webhooks are also managed in the desktop
156
+ app.
157
+
152
158
  ## Scripts
153
159
 
154
160
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.85",
3
+ "version": "0.9.87",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -55,7 +55,7 @@
55
55
  "test:tool-contracts": "node scripts/tool-smoke.mjs",
56
56
  "smoke:patch": "node scripts/apply-patch-edit-smoke.mjs",
57
57
  "smoke:output": "node scripts/output-style-smoke.mjs",
58
- "smoke:tui": "node scripts/tui-render-smoke.mjs && npm run test:tui-input-render && npm run test:tui-streaming-window && npm run test:tui-queue",
58
+ "smoke:tui": "node scripts/build-tui.mjs && node scripts/tui-render-smoke.mjs && npm run test:tui-input-render && npm run test:tui-streaming-window && npm run test:tui-queue",
59
59
  "smoke:freevars": "node scripts/freevar-smoke.mjs",
60
60
  "smoke:logguard": "node scripts/log-writer-guard-smoke.mjs",
61
61
  "smoke:live-worker": "node scripts/live-worker-smoke.mjs",
@@ -73,6 +73,7 @@
73
73
  "test:anthropic-oauth-race": "node --test scripts/anthropic-oauth-refresh-race-test.mjs",
74
74
  "test:grok-oauth-race": "node --test scripts/grok-oauth-refresh-race-test.mjs",
75
75
  "test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
76
+ "test:keychain": "node --test scripts/keychain-prewarm-test.mjs",
76
77
  "test:memory-routing": "node --test scripts/memory-cycle-routing-test.mjs scripts/maintenance-default-routes-test.mjs scripts/embedding-worker-exit-test.mjs scripts/embedding-runtime-prune-test.mjs",
77
78
  "test:embedding-runtime": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs",
78
79
  "test:embedding-runtime:core": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs --core",
@@ -87,7 +88,7 @@
87
88
  "test:release-focused": "npm run test:release-assets && npm run test:tool-contracts && npm run test:placeholder && npm run smoke:patch && npm run test:patch-binary-cache && npm run test:providers && npm run test:deferred-tools && npm run smoke:compact && node --test scripts/code-graph-root-federation-test.mjs scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs && npm run test:shellhardening && node --test scripts/windows-hide-spawn-options-test.mjs && npm run test:session && npm run test:workflow-editor && npm run test:embedding-runtime && node --test scripts/tui-transcript-perf-test.mjs",
88
89
  "test:native-edit-wire": "node --test scripts/native-edit-wire-test.mjs",
89
90
  "test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
90
- "test:session": "node --test scripts/session-orphan-sweep-test.mjs scripts/interrupted-turn-history-test.mjs scripts/session-heartbeat-lifecycle-test.mjs scripts/remote-transition-order-test.mjs",
91
+ "test:session": "node --test scripts/session-orphan-sweep-test.mjs scripts/interrupted-turn-history-test.mjs scripts/turn-checkpoint-crash-test.mjs scripts/session-heartbeat-lifecycle-test.mjs scripts/remote-transition-order-test.mjs",
91
92
  "test:rebindtail": "node --test scripts/forwarder-rebind-tail-test.mjs",
92
93
  "test:workflow-editor": "node --test scripts/workflow-id-test.mjs scripts/workflow-pack-editor-test.mjs",
93
94
  "test:route-scope": "node --test scripts/route-scope-isolation-test.mjs",
@@ -48,6 +48,18 @@
48
48
  "permission": "read",
49
49
  "stallCap": { "idleSeconds": 300, "toolRunningSeconds": 300 }
50
50
  },
51
+ {
52
+ "agent": "title-agent",
53
+ "slot": "title",
54
+ "systemFile": "rules/agent/43-title-agent.md",
55
+ "description": "One-shot session title generator invoked by the desktop host",
56
+ "invokedBy": "desktop-title",
57
+ "maintKey": "memory",
58
+ "toolSchemaProfile": "none",
59
+ "kind": "maintenance",
60
+ "permission": "read",
61
+ "stallCap": { "idleSeconds": 90, "toolRunningSeconds": 90 }
62
+ },
51
63
  {
52
64
  "agent": "scheduler-task",
53
65
  "slot": "scheduler",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup
3
- description: Use this skill to configure a mixdog installation — request-driven recipes for models, MCP, channels, output style, memory/recap, skills, secrets, and workflow packs. Triggers on "setup", "configure", "change model", "add MCP", "output style", "Discord token", "workflow".
3
+ description: Use this skill to configure a mixdog installation — request-driven recipes for models, MCP, channels, remote, output style, memory, skills, secrets, and workflow/agent packs. Triggers on "setup", "configure", "change model", "add MCP", "output style", "Discord token", "workflow", "remote".
4
4
  ---
5
5
 
6
6
  # Setup Skill
@@ -34,16 +34,21 @@ apply it, and verify the result.
34
34
  |---|---|
35
35
  | Main model | `/model` |
36
36
  | Agent model | `/agents` |
37
- | Workflow pack | `/workflow` |
37
+ | Workflow switch | `/workflow` |
38
+ | Workflow/agent definition | desktop Workflows page or pack files |
38
39
  | Search model | `/search` |
39
40
  | Reasoning effort | `/effort [level]` |
40
41
  | Fast mode | `/fast [on|off]` |
41
42
  | Output style | `/style` |
42
- | Memory / recap | `/memory`, `/recap`, config |
43
+ | Memory / core memories | `/memory`, config |
43
44
  | MCP server | `/mcp`, config |
44
45
  | Provider secret | provider login flow or secret store |
45
- | Discord / channel | config and channel runtime status |
46
+ | Discord / Telegram / voice | `/channels`, config |
47
+ | Remote (channel control) | `/remote` (manual claim) |
48
+ | Theme | `/theme` |
49
+ | Profile (title/language) | `/profile` |
46
50
  | Skill add/update | project or global `skills/<name>/SKILL.md` |
51
+ | Schedules / webhooks | desktop app only |
47
52
 
48
53
  ## Recipes
49
54
 
@@ -59,12 +64,26 @@ apply it, and verify the result.
59
64
  2. Change the target agent route through the agent picker.
60
65
  3. Verify the target agent reports the new route on the next run.
61
66
 
62
- ### Workflow pack
67
+ ### Workflow switch
63
68
 
64
69
  1. Check `/workflow` or `/setting` for the active workflow.
65
70
  2. Change with `/workflow`; this updates `config.workflow.active`.
66
71
  3. Verify the notice and active marker.
67
72
 
73
+ ### Workflow / agent definitions
74
+
75
+ 1. Workflows and agents are Markdown packs. Built-ins ship with mixdog
76
+ (workflows `default`, `solo`; agents worker, heavy-worker, reviewer,
77
+ debugger, maintainer); custom packs live at
78
+ `<mixdogData>/workflows/<id>/WORKFLOW.md` and
79
+ `<mixdogData>/agents/<id>/AGENT.md`.
80
+ 2. Saving a built-in id writes a user override; deleting the override
81
+ reverts to the built-in.
82
+ 3. The desktop app (Workflows page) is the full editor. The TUI only
83
+ switches the active workflow (`/workflow`) and per-agent model routes
84
+ (`/agents`).
85
+ 4. Verify with `/workflow` / `/agents` listings after the change.
86
+
68
87
  ### Search model
69
88
 
70
89
  1. Check `searchRoute` in config and the status line.
@@ -89,11 +108,13 @@ apply it, and verify the result.
89
108
  2. Change with `/style` and the picker.
90
109
  3. Verify the active style and run a short response if needed.
91
110
 
92
- ### Memory and recap
111
+ ### Memory
93
112
 
94
- 1. Inspect memory/recap status via commands or config.
95
- 2. Update the requested setting only.
96
- 3. Verify status output and, when relevant, run a small recall/recap check.
113
+ 1. `/memory` is the one-stop surface: memory on/off toggle plus core-memory
114
+ list, add, edit, and delete.
115
+ 2. Compact type is fixed to Fast-track; only auto-compact toggles in
116
+ `/setting`.
117
+ 3. Verify status output and, when relevant, run a small recall check.
97
118
 
98
119
  ### MCP server
99
120
 
@@ -108,13 +129,24 @@ apply it, and verify the result.
108
129
  2. Use the provider login flow, secret store, or environment variable path.
109
130
  3. Verify presence only; never print the secret value.
110
131
 
111
- ### Discord or channel configuration
132
+ ### Discord / Telegram / voice channels
112
133
 
113
- 1. Check config and runtime channel status.
114
- 2. Update the requested channel setting.
134
+ 1. Check `/channels` (or `/setting` → Channel) and runtime channel status.
135
+ 2. Update the requested backend, credential, or main channel/chat setting.
115
136
  3. Restart/reconnect only when the channel implementation requires it.
116
137
  4. Verify channel presence and a non-secret status signal.
117
138
 
139
+ Schedules and webhooks are managed in the desktop app, not the TUI.
140
+
141
+ ### Remote
142
+
143
+ 1. Remote is manual-only: `/remote` claims remote for the current session
144
+ and takes over from any other session. There is no auto-start, and
145
+ session handoffs never transfer ownership implicitly.
146
+ 2. Turning remote OFF (or exiting the owning session) stops messaging;
147
+ schedules and webhooks keep running.
148
+ 3. Verify via the status line or `/setting` → Remote Runtime.
149
+
118
150
  ### Skills
119
151
 
120
152
  1. Check existing skills with `/skills` or `skillsStatus()`.
package/src/help.mjs CHANGED
@@ -5,22 +5,53 @@ export const HELP_LINES = [
5
5
  'mixdog — standalone mixdog CLI/TUI coding agent.',
6
6
  '',
7
7
  'Usage:',
8
- ' mixdog [--provider <name>] [--model <name>] [--readonly]',
9
- ' mixdog [--onboarding] re-run the first-run setup wizard',
8
+ ' mixdog [options] start the TUI in the current project',
10
9
  ' mixdog --provider <name> --model <name> [--effort <level>] [--fast] <role> <message...>',
11
10
  ' mixdog --help',
12
11
  '',
12
+ 'Options:',
13
+ ' --provider <name> provider route for this session',
14
+ ' --model <name> model route for this session',
15
+ ' --effort <level> reasoning effort for the selected model',
16
+ ' --fast enable Fast mode for the selected model',
17
+ ' --workflow <name> start with the given workflow active',
18
+ ' --readonly read-only tool surface',
19
+ ' --remote enable remote/channel mode for this session',
20
+ ' --onboarding re-run the first-run setup wizard',
21
+ '',
13
22
  'Headless role commands require an explicit provider/model pair and run with',
14
23
  'ephemeral config/data; host behavioral config and personal state are not loaded.',
24
+ 'Roles: explore, worker, heavy-worker, reviewer, debugger, maintainer, web-researcher.',
15
25
  '',
16
26
  'Slash commands (inside mixdog):',
17
- ' /clear reset the conversation and clear the screen',
18
- ' /compact compact older conversation context',
19
- ' /model <name> switch model/preset for subsequent turns',
20
- ' /OutputStyle [name] show or switch Lead output style',
21
- ' /providers manage provider auth and local endpoints',
22
- ' /agents show available workflow agents',
23
- ' /quit quit (aliases: /exit, /q)',
27
+ ' /clear start a fresh chat (alias: /new)',
28
+ ' /project [path] switch working directory (project)',
29
+ ' /compact compact older conversation context',
30
+ ' /autoclear [on|off|duration] reduce cache-miss cost after long idle gaps',
31
+ ' /resume [id] resume a saved chat',
32
+ ' /context show current context surface',
33
+ ' /usage [refresh] show total provider quota / balance',
34
+ ' /model [name|refresh] switch model for subsequent turns',
35
+ ' /search set the web search provider/model',
36
+ ' /workflow [name] switch the active workflow',
37
+ ' /OutputStyle [name] switch Lead output style (alias: /style)',
38
+ ' /theme [id] change the TUI color theme',
39
+ ' /agents [refresh] show available workflow agents',
40
+ ' /effort [level] set reasoning effort for the current model',
41
+ ' /fast [on|off] toggle Fast mode for the current model',
42
+ ' /mcp manage MCP servers and tools',
43
+ ' /skills choose a skill for the next request',
44
+ ' /memory [status|core ...] list and edit core memories',
45
+ ' /plugins manage local plugin integrations',
46
+ ' /hooks manage before-tool hook rules and events',
47
+ ' /providers manage auth, API keys, OAuth, and local endpoints',
48
+ ' /channels manage Discord, Telegram, and voice',
49
+ ' /remote claim remote for this session',
50
+ ' /setting open runtime settings (aliases: /settings, /config)',
51
+ ' /profile set your title and response language',
52
+ ' /update check version and update mixdog',
53
+ ' /doctor diagnose installation health',
54
+ ' /quit quit the TUI (aliases: /exit, /q)',
24
55
  '',
25
56
  'History: use ↑ / ↓ to recall previous inputs.',
26
57
  ];
@@ -24,6 +24,11 @@ const KEYCHAIN_CACHE_TTL_MS = (() => {
24
24
  const _secretCache = new Map();
25
25
  const _cacheGenerations = new Map();
26
26
  let _cacheEpoch = 0;
27
+ // A desktop process can create several session runtimes, and the preload path
28
+ // also warms credentials before the first runtime exists. All callers share
29
+ // this one process-wide operation so Windows never launches duplicate batch
30
+ // DPAPI PowerShell hosts for the same startup.
31
+ let _prewarmPromise = null;
27
32
 
28
33
  function _cacheGet(account) {
29
34
  const key = `${SERVICE}\0${account}`;
@@ -381,7 +386,7 @@ const PS_UNPROTECT_BATCH = [
381
386
  '}',
382
387
  ].join(' ');
383
388
 
384
- async function prewarmSecrets() {
389
+ async function _prewarmSecretsOnce() {
385
390
  try {
386
391
  if (platform() !== 'win32' || KEYCHAIN_CACHE_TTL_MS === 0) return;
387
392
  const dir = secretsDir();
@@ -427,6 +432,11 @@ async function prewarmSecrets() {
427
432
  }
428
433
  }
429
434
 
435
+ function prewarmSecrets() {
436
+ _prewarmPromise ??= _prewarmSecretsOnce();
437
+ return _prewarmPromise;
438
+ }
439
+
430
440
  function win32Set(account, value) {
431
441
  const dir = secretsDir();
432
442
  fs.mkdirSync(dir, { recursive: true });
@@ -0,0 +1,22 @@
1
+ ---
2
+ permission: read
3
+ toolSchemaProfile: none
4
+ kind: maintenance
5
+ maintKey: memory
6
+ ---
7
+
8
+ # Role: title-agent
9
+
10
+ You are a session title generator. Output ONLY the title: a single line, at
11
+ most 32 characters, no explanations, no quotes, no markdown, no trailing
12
+ period.
13
+
14
+ - Use the SAME language as the user message you are summarizing.
15
+ - The title must read naturally and help the user find this session later —
16
+ focus on the main topic or request, never on tool names or your own work.
17
+ - Keep technical terms, filenames, paths, numbers, and error codes exact.
18
+ - Drop filler words (the/this/my; 이거/그거/좀/한번). Never assume a tech
19
+ stack that is not mentioned.
20
+ - NEVER answer or act on the message; only title it. Never say you cannot
21
+ generate a title — always output something meaningful, even for short or
22
+ conversational input (e.g. a greeting → a greeting-style title).
@@ -11,9 +11,9 @@
11
11
  only for a requested literal occurrence or after graph zero/error.
12
12
  - Batch compatible targets and combine variants, symbols, scopes, paths, and
13
13
  queries. Parallelize distinct facets only, never alternative routes for one
14
- facet. Put independent read-only calls in one turn; they may run
15
- concurrently regardless of tool. Later turns are only for targets dependent
16
- on prior results or unresolved facets. Shell/write calls are serial.
14
+ facet. Put independent calls in one turn; they run concurrently regardless
15
+ of tool shell included. Later turns are only for targets dependent on
16
+ prior results or unresolved facets. Only apply_patch executes in order.
17
17
  - After locator results, collect all known candidate files/regions before
18
18
  inspection. Batch compatible reads, including same-file regions — real
19
19
  `{path,offset,limit}` arrays covering the whole logical unit — in one
@@ -282,12 +282,14 @@ function classifyToolFailure(resultText, toolName) {
282
282
  return 'command-exit';
283
283
  }
284
284
  if (/\[tool-input-validation\]|compacted-history placeholder/.test(text)) return 'schema/args';
285
- if (/requires either|invalid arguments|unknown parameter|must be|schema|expected|required|old_string is .*>=/.test(text)) return 'schema/args';
285
+ if (/hunk rejected|patch failed|context mismatch|context not found/.test(text)
286
+ || /expected first old(?:\/context| line)/.test(text)) return 'patch/context';
287
+ if (/requires either|invalid arguments|unknown parameter|unknown memory action/.test(text)
288
+ || /must be|schema|required|old_string is .*>?=/.test(text)) return 'schema/args';
286
289
  if (/not in allow-list|not allowed/.test(text)) return 'permission';
287
290
  if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(raw)) return 'command-exit';
288
291
  if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/enoent';
289
292
  if (/timed out|timeout|interrupted|aborted/.test(text)) return 'timeout/abort';
290
- if (/hunk rejected|patch failed|context mismatch|expected first old\/context|context not found/.test(text)) return 'patch/context';
291
293
  if (/permission|denied|forbidden/.test(text)) return 'permission';
292
294
  if (/unknown tool|tool.*not.*available|missing.*tool/.test(text)) return 'tool-surface';
293
295
  return 'runtime/failure';
@@ -24,6 +24,18 @@ const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
24
24
  // --- State ---
25
25
  const servers = new Map();
26
26
  let mcpSdkPromise = null;
27
+ // Agent-shard proxy mode (P2, docs/agent-shard-design.md): a shard child has
28
+ // no MCP connections of its own — the lead process owns the singleton server
29
+ // registry. When a proxy is installed, tool DEFINITIONS come from the
30
+ // lead-supplied snapshot and every call is forwarded over IPC; the local
31
+ // `servers` map stays empty and untouched.
32
+ let _mcpToolProxy = null;
33
+ export function setMcpToolProxy(proxy) {
34
+ _mcpToolProxy = proxy && typeof proxy.call === 'function'
35
+ ? { tools: Array.isArray(proxy.tools) ? proxy.tools : [], call: proxy.call }
36
+ : null;
37
+ _invalidateMcpToolFieldMemo();
38
+ }
27
39
  // Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
28
40
  // The lookup (regex parse + servers Map get + tools.find + schema property
29
41
  // inspection) runs on every MCP tool invocation but its result only changes
@@ -139,6 +151,7 @@ export async function connectMcpServers(config) {
139
151
  * Tool names are prefixed: `mcp__{serverName}__{toolName}`
140
152
  */
141
153
  export function getMcpTools() {
154
+ if (_mcpToolProxy) return _mcpToolProxy.tools;
142
155
  const tools = [];
143
156
  for (const server of servers.values()) {
144
157
  tools.push(...server.tools);
@@ -182,6 +195,9 @@ export async function executeMcpTool(name, args) {
182
195
  const match = name.match(/^mcp__(.+?)__(.+)$/);
183
196
  if (!match)
184
197
  throw new Error(`Not an MCP tool name: ${name}`);
198
+ // Proxy mode: the lead executes (including its own reconnect/timeout/cap
199
+ // handling) and returns the final model-visible result/envelope.
200
+ if (_mcpToolProxy) return _mcpToolProxy.call(name, args);
185
201
  const [, serverName, toolName] = match;
186
202
  const server = servers.get(serverName);
187
203
  if (!server)
@@ -355,6 +371,7 @@ export function isMcpTool(name) {
355
371
  /** True when the prefixed name exists on a connected MCP server. */
356
372
  export function isRegisteredMcpTool(name) {
357
373
  if (!isMcpTool(name)) return false;
374
+ if (_mcpToolProxy) return _mcpToolProxy.tools.some((t) => t?.name === name);
358
375
  const match = name.match(/^mcp__(.+?)__(.+)$/);
359
376
  if (!match) return false;
360
377
  const [, serverName] = match;
@@ -372,6 +389,13 @@ export function mcpToolHasField(name, field) {
372
389
  const memoKey = `${name}|${field}`;
373
390
  const memoized = _mcpToolFieldMemo.get(memoKey);
374
391
  if (memoized !== undefined) return memoized;
392
+ if (_mcpToolProxy) {
393
+ const proxyTool = _mcpToolProxy.tools.find((t) => t?.name === name);
394
+ const proxyProps = proxyTool?.inputSchema?.properties;
395
+ const proxyResult = Boolean(proxyProps && Object.prototype.hasOwnProperty.call(proxyProps, field));
396
+ _mcpToolFieldMemo.set(memoKey, proxyResult);
397
+ return proxyResult;
398
+ }
375
399
  const match = name.match(/^mcp__(.+?)__(.+)$/);
376
400
  if (!match) { _mcpToolFieldMemo.set(memoKey, false); return false; }
377
401
  const [, serverName] = match;
@@ -1,7 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
 
4
- export const PROVIDER_ACCOUNT_CONCURRENCY = 64;
4
+ // Unbounded by design: provider requests must never serialize behind a local
5
+ // concurrency cap. Rate-limit cooldowns below (server-mandated retry-after)
6
+ // remain the only thing that parks/rejects requests; with an infinite limit
7
+ // the adaptive halving/recovery below is inert.
8
+ export const PROVIDER_ACCOUNT_CONCURRENCY = Infinity;
5
9
  export const PROVIDER_ACCOUNT_MAX_QUEUE = 1024;
6
10
  // A cooldown longer than this is a quota-window block (subscription limit),
7
11
  // not a transient burst: parking requests silently for it would look like a
@@ -119,9 +123,59 @@ export class ProviderAdmissionScheduler {
119
123
  this.reportedRateLimits = new WeakSet();
120
124
  this.closedReason = null;
121
125
  this.context = currentAdmission;
126
+ // Cross-process cooldown fan-out (agent shard pool): listeners observe
127
+ // locally-recorded 429 cooldowns and credential-driven resets so one
128
+ // account's quota window parks every shard process.
129
+ this.cooldownListeners = new Set();
122
130
  }
123
131
 
124
- run(key, task, { signal = null } = {}) {
132
+ /** Observe cooldown/reset events. Returns an unsubscribe function. */
133
+ onCooldownEvent(listener) {
134
+ if (typeof listener !== 'function') return () => {};
135
+ this.cooldownListeners.add(listener);
136
+ return () => this.cooldownListeners.delete(listener);
137
+ }
138
+
139
+ _emitCooldownEvent(event) {
140
+ for (const listener of [...this.cooldownListeners]) {
141
+ try { listener(event); } catch { /* observer must not break admission */ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Apply a cooldown recorded by ANOTHER process (shard broadcast). Extends
147
+ * the local lane's park window without re-emitting (no feedback loops) and
148
+ * without touching the adaptive limit — only the reporting process saw the
149
+ * actual 429. Returns true when the lane's window moved forward.
150
+ */
151
+ applyExternalCooldown(key, cooldownUntil) {
152
+ const laneKey = String(key || '');
153
+ const until = Number(cooldownUntil) || 0;
154
+ const now = this.now();
155
+ if (!laneKey || until <= now || this.closedReason) return false;
156
+ const lane = this.lanes.get(laneKey) || {
157
+ active: 0,
158
+ queue: [],
159
+ adaptive: isAnthropicLane(laneKey),
160
+ limit: this.concurrency,
161
+ cooldownUntil: 0,
162
+ recoverySuccesses: 0,
163
+ cooldownTimer: null,
164
+ };
165
+ if (!lane.adaptive || until <= lane.cooldownUntil) return false;
166
+ this.lanes.set(laneKey, lane);
167
+ lane.cooldownUntil = until;
168
+ lane.recoverySuccesses = 0;
169
+ this._scheduleCooldown(laneKey, lane);
170
+ // Same queue semantics as a locally-recorded quota window: parked
171
+ // requests must not hang silently for hours.
172
+ if (until - now > PROVIDER_COOLDOWN_FAIL_FAST_MS) {
173
+ this._rejectQueueForCooldown(laneKey, lane);
174
+ }
175
+ return true;
176
+ }
177
+
178
+ run(key, task, { signal = null, onCooldownWait = null } = {}) {
125
179
  const laneKey = String(key || 'provider:default');
126
180
  // Provider-local recovery may recursively call this.send(). It already
127
181
  // owns a slot, so reacquiring the same lane could deadlock a full wave.
@@ -168,6 +222,17 @@ export class ProviderAdmissionScheduler {
168
222
  }
169
223
  lane.queue.push(item);
170
224
  this._drain(laneKey, lane);
225
+ // Short-cooldown visibility: this request just parked behind an
226
+ // active burst cooldown (long quota windows fail fast above). Tell
227
+ // the caller how long the silent wait will be so the UI can show
228
+ // "rate-limited" instead of looking stalled.
229
+ if (!item.started && !item.canceled && lane.adaptive
230
+ && typeof onCooldownWait === 'function') {
231
+ const waitMs = lane.cooldownUntil - this.now();
232
+ if (waitMs > 250) {
233
+ try { onCooldownWait(waitMs); } catch { /* display-only */ }
234
+ }
235
+ }
171
236
  });
172
237
  }
173
238
 
@@ -249,6 +314,7 @@ export class ProviderAdmissionScheduler {
249
314
  if (lane.cooldownUntil - now > PROVIDER_COOLDOWN_FAIL_FAST_MS) {
250
315
  this._rejectQueueForCooldown(key, lane);
251
316
  }
317
+ this._emitCooldownEvent({ type: 'cooldown', key, cooldownUntil: lane.cooldownUntil });
252
318
  return true;
253
319
  }
254
320
 
@@ -289,6 +355,11 @@ export class ProviderAdmissionScheduler {
289
355
  resetCount += 1;
290
356
  this._drain(key, lane);
291
357
  }
358
+ // resetCount gating terminates cross-process echo: a broadcast-driven
359
+ // second reset finds nothing to clear and emits nothing.
360
+ if (resetCount > 0) {
361
+ this._emitCooldownEvent({ type: 'reset', provider: providerName ? String(providerName) : null });
362
+ }
292
363
  return resetCount;
293
364
  }
294
365
 
@@ -432,7 +503,17 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
432
503
  ...opts,
433
504
  signal: admissionSignal,
434
505
  });
435
- }, { signal });
506
+ }, {
507
+ signal,
508
+ onCooldownWait: (waitMs) => {
509
+ // Display-only: the TUI/desktop 'reconnecting' stage already
510
+ // renders a custom verb, so no new stage vocabulary is needed.
511
+ const secs = Math.max(1, Math.ceil(waitMs / 1000));
512
+ try {
513
+ opts.onStageChange?.('reconnecting', { message: `Rate-limited — waiting ~${secs}s for the provider window` });
514
+ } catch { /* display-only */ }
515
+ },
516
+ });
436
517
  };
437
518
  return provider;
438
519
  }