mixdog 0.9.85 → 0.9.86
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.
- package/README.md +7 -1
- package/package.json +3 -2
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +25 -14
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +34 -9
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +37 -85
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +442 -0
- package/src/standalone/agent-tool.mjs +222 -0
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +9 -10
- package/src/tui/App.jsx +16 -9
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +0 -45
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +243 -323
- package/src/tui/engine/session-api-ext.mjs +24 -9
- package/src/tui/engine.mjs +7 -1
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- 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
|
|
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.
|
|
3
|
+
"version": "0.9.86",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -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",
|
package/src/defaults/agents.json
CHANGED
|
@@ -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
|
|
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
|
|
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 /
|
|
43
|
+
| Memory / core memories | `/memory`, config |
|
|
43
44
|
| MCP server | `/mcp`, config |
|
|
44
45
|
| Provider secret | provider login flow or secret store |
|
|
45
|
-
| Discord /
|
|
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
|
|
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
|
|
111
|
+
### Memory
|
|
93
112
|
|
|
94
|
-
1.
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
132
|
+
### Discord / Telegram / voice channels
|
|
112
133
|
|
|
113
|
-
1. Check
|
|
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 [
|
|
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
|
|
18
|
-
' /
|
|
19
|
-
' /
|
|
20
|
-
' /
|
|
21
|
-
' /
|
|
22
|
-
' /
|
|
23
|
-
' /
|
|
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
|
];
|
package/src/lib/keychain-cjs.cjs
CHANGED
|
@@ -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
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
}, {
|
|
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
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Extracted from openai-oauth-ws.mjs, which now owns transport flow only.
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function _cleanMetaString(value) {
|
|
8
8
|
return typeof value === 'string' ? value.trim() : '';
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -38,7 +38,7 @@ function _codexInstallationId(sendOpts) {
|
|
|
38
38
|
// The identity block codex rebuilds per request (responses_metadata.rs
|
|
39
39
|
// client_metadata()): never cached on the pooled socket, or a later turn would
|
|
40
40
|
// replay the first turn's identity.
|
|
41
|
-
|
|
41
|
+
function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = false } = {}) {
|
|
42
42
|
const sessionId = _cleanMetaString(sendOpts?.codexSessionId || sendOpts?.session?.codexSessionId || poolKey || cacheKey)
|
|
43
43
|
|| 'mixdog-session';
|
|
44
44
|
const threadId = _cleanMetaString(sendOpts?.threadId || sendOpts?.codexThreadId || sendOpts?.session?.threadId || cacheKey || sessionId)
|