mixdog 0.9.4 → 0.9.5

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 (121) hide show
  1. package/README.md +112 -38
  2. package/package.json +4 -1
  3. package/scripts/bench/r5-orchestrated-task.json +7 -0
  4. package/scripts/bench/round-r13-clientmeta.json +464 -0
  5. package/scripts/bench/round-r14-betafeatures.json +466 -0
  6. package/scripts/bench/round-r15-fulldefault.json +462 -0
  7. package/scripts/bench/round-r16-sessionid.json +466 -0
  8. package/scripts/bench/round-r17-wirebytes.json +456 -0
  9. package/scripts/bench/round-r18-prewarm.json +468 -0
  10. package/scripts/bench/round-r19-clean.json +472 -0
  11. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  12. package/scripts/bench/round-r21-delta-retry.json +473 -0
  13. package/scripts/bench/round-r22-full-probe.json +693 -0
  14. package/scripts/bench/round-r23-itemprobe.json +701 -0
  15. package/scripts/bench/round-r24-shapefix.json +677 -0
  16. package/scripts/bench/round-r25-serial.json +464 -0
  17. package/scripts/bench/round-r26-parallel3.json +671 -0
  18. package/scripts/bench/round-r27-parallel10.json +894 -0
  19. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  20. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  21. package/scripts/bench/round-r30-instid.json +253 -0
  22. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  23. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  24. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  25. package/scripts/bench/round-r34-orchestrated.json +120 -0
  26. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  27. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  28. package/scripts/bench-run.mjs +40 -7
  29. package/scripts/internal-comms-bench.mjs +2 -4
  30. package/scripts/model-catalog-audit.mjs +209 -0
  31. package/scripts/model-list-sanitize-test.mjs +37 -0
  32. package/scripts/recall-bench.mjs +76 -13
  33. package/scripts/recall-quality-cases.json +12 -0
  34. package/scripts/tool-smoke.mjs +2 -2
  35. package/src/agents/heavy-worker/AGENT.md +7 -7
  36. package/src/agents/scheduler-task/AGENT.md +2 -3
  37. package/src/agents/webhook-handler/AGENT.md +2 -3
  38. package/src/agents/worker/AGENT.md +9 -10
  39. package/src/app.mjs +12 -1
  40. package/src/headless-role.mjs +7 -1
  41. package/src/mixdog-session-runtime.mjs +15 -14
  42. package/src/output-styles/default.md +14 -18
  43. package/src/output-styles/minimal.md +4 -5
  44. package/src/output-styles/simple.md +7 -8
  45. package/src/rules/agent/00-common.md +6 -5
  46. package/src/rules/agent/30-explorer.md +16 -5
  47. package/src/rules/lead/01-general.md +5 -5
  48. package/src/rules/lead/lead-brief.md +6 -3
  49. package/src/rules/lead/lead-tool.md +6 -4
  50. package/src/rules/shared/01-tool.md +17 -21
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  52. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +19 -0
  53. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -3
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +9 -1
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +53 -9
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +23 -10
  57. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +33 -4
  59. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  60. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +93 -0
  61. package/src/runtime/agent/orchestrator/session/loop.mjs +27 -5
  62. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  65. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  66. package/src/runtime/channels/backends/discord-gateway.mjs +2 -2
  67. package/src/runtime/channels/backends/discord.mjs +15 -26
  68. package/src/runtime/channels/backends/telegram.mjs +8 -12
  69. package/src/runtime/channels/index.mjs +23 -43
  70. package/src/runtime/channels/lib/backend-dispatch.mjs +5 -3
  71. package/src/runtime/channels/lib/config.mjs +37 -149
  72. package/src/runtime/channels/lib/event-pipeline.mjs +4 -4
  73. package/src/runtime/channels/lib/event-queue.mjs +15 -9
  74. package/src/runtime/channels/lib/inbound-routing.mjs +2 -39
  75. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  76. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  77. package/src/runtime/channels/lib/webhook/deliveries.mjs +3 -2
  78. package/src/runtime/channels/lib/webhook.mjs +8 -47
  79. package/src/runtime/memory/index.mjs +9 -0
  80. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  81. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  82. package/src/runtime/memory/lib/memory-recall-store.mjs +47 -10
  83. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  84. package/src/runtime/memory/lib/query-handlers.mjs +27 -5
  85. package/src/runtime/memory/tool-defs.mjs +2 -2
  86. package/src/runtime/shared/config.mjs +14 -4
  87. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  88. package/src/runtime/shared/schedules-store.mjs +13 -3
  89. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  90. package/src/session-runtime/config-helpers.mjs +45 -10
  91. package/src/session-runtime/quick-model-rows.mjs +46 -11
  92. package/src/session-runtime/quick-search-models.mjs +28 -27
  93. package/src/session-runtime/settings-api.mjs +34 -1
  94. package/src/session-runtime/tool-defs.mjs +1 -1
  95. package/src/session-runtime/workflow.mjs +1 -1
  96. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  97. package/src/standalone/agent-tool.mjs +30 -0
  98. package/src/standalone/channel-admin.mjs +102 -90
  99. package/src/standalone/explore-tool.mjs +39 -10
  100. package/src/tui/App.jsx +126 -28
  101. package/src/tui/app/app-format.mjs +11 -4
  102. package/src/tui/app/channel-pickers.mjs +2 -4
  103. package/src/tui/app/maintenance-pickers.mjs +60 -28
  104. package/src/tui/app/model-options.mjs +5 -1
  105. package/src/tui/app/settings-picker.mjs +0 -1
  106. package/src/tui/app/transcript-window.mjs +8 -2
  107. package/src/tui/app/use-transcript-scroll.mjs +3 -1
  108. package/src/tui/app/use-transcript-window.mjs +19 -1
  109. package/src/tui/components/ConfirmBar.jsx +10 -7
  110. package/src/tui/components/Picker.jsx +32 -11
  111. package/src/tui/components/PromptInput.jsx +10 -1
  112. package/src/tui/components/StatusLine.jsx +6 -3
  113. package/src/tui/dist/index.mjs +440 -293
  114. package/src/tui/engine.mjs +48 -12
  115. package/src/tui/index.jsx +11 -52
  116. package/src/ui/statusline.mjs +10 -0
  117. package/src/workflows/default/WORKFLOW.md +15 -17
  118. package/src/workflows/sequential/WORKFLOW.md +15 -17
  119. package/vendor/ink/build/ink.js +9 -1
  120. package/scripts/recall-usecase-probe.json +0 -6
  121. package/src/runtime/channels/lib/holidays.mjs +0 -138
@@ -0,0 +1,128 @@
1
+ {
2
+ "round": "36",
3
+ "runner": "lead",
4
+ "opts": {
5
+ "provider": "openai-oauth",
6
+ "model": "gpt-5.5",
7
+ "effort": null,
8
+ "fast": false,
9
+ "workflow": "default",
10
+ "env": {
11
+ "MIXDOG_DATA_DIR": "C:\\Users\\tempe\\.mixdog\\bench-data"
12
+ }
13
+ },
14
+ "tasks": 1,
15
+ "completed": 1,
16
+ "completion_rate": 100,
17
+ "sessions": [
18
+ "sess_54872_1783078178435_1783078178685_d27922b6978571ab4599bcc38f64fa0f"
19
+ ],
20
+ "results": [
21
+ {
22
+ "id": "r5-orchestrated-audit",
23
+ "agent": "lead",
24
+ "ok": true,
25
+ "ms": 255448,
26
+ "sessionId": "sess_54872_1783078178435_1783078178685_d27922b6978571ab4599bcc38f64fa0f",
27
+ "output": "sessionId: sess_54872_1783078178435_1783078178685_d27922b6978571ab4599bcc38f64fa0f\n완료했습니다. 분석만 수행했고 파일은 수정하지 않았습니다.\n\n**Scope A — classifier bucket reachability**\n\n- `classifyError()`는 `auth|permanent|transient|unknown`만 반환합니다(`retry-classifier.mjs:100-135`). `openai-oauth-ws.mjs`는 이 함수를 import하지 않으므로 WS 경로에서는 직접 도달하지 않지만(`openai-oauth-ws.mjs:33-40`), 전역적으로는 `withRetry()`가 사용합니다(`retry-classifier.mjs:515-570`).\n\n- `classifyHandshakeError()`는 `timeout|reset|dns|refused|network|acquire_timeout|http_${status}|null`을 반환합니다(`retry-classifier.mjs:471-494`). `openai-oauth-ws`는 `_classifyHandshakeError()`로 래핑해 acquire 실패마다 소비합니다(`openai-oauth-ws.mjs:112-114`, `openai-oauth-ws.mjs:403-430`).\n\n- WS midstream classifier는 `first_byte_timeout|ws_send_failed|agent_stall|stream_stalled|ws_1006|ws_1011|ws_1012|ws_4000|ws_1000|http_${status}|response_failed_disconnected|response_failed_network|response_failed_auth_expired|null`을 반환합니다(`retry-classifier.mjs:286-343`). `openai-oauth-ws`는 항상 `mode:'ws'`로만 호출합니다(`openai-oauth-ws.mjs:87-91`, `openai-oauth-ws.mjs:157-159`).\n\n- SSE midstream bucket `agent_stall|stream_stalled|reset|timeout|dns|sse_idle_timeout|stream_terminated|fetch_failed`는 `retry-classifier.mjs` 안에는 존재하지만(`retry-classifier.mjs:348-381`), `openai-oauth-ws` 경로에서는 `mode:'sse'`를 넘기지 않아 도달 불가입니다.\n\n- “생성 불가”로 볼 수 있는 것은 현재 WS classifier가 반환하지 않는 레거시/문서성 `http_5xx` 문자열입니다. 실제 반환은 `http_500` 같은 `http_${status}`이고(`retry-classifier.mjs:309`, `retry-classifier.mjs:480-482`), fallback 쪽만 `http_5xx`와 regex 둘 다 허용합니다(`retry-classifier.mjs:386-410`).\n\n- `response_failed_auth_expired`는 repo 내부가 문자열을 생성하진 않지만, 서버의 `response.failed`/failed `response.done` payload를 그대로 저장해 classifier가 sniff하므로 런타임상 완전한 dead bucket은 아닙니다(`openai-ws-stream.mjs:887-896`, `openai-ws-stream.mjs:953-963`, `retry-classifier.mjs:332-339`).\n\n**Scope B — retry loop budgets/backoff**\n\n- Handshake retry는 `HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS = 5`라서 최초 1회 + 최대 4회 재시도입니다(`openai-oauth-ws.mjs:104`, `stall-policy.mjs:200-201`). backoff는 `500ms * 2^(attempt-1)`, cap `5000ms`, `jitterDelayMs()` 적용입니다(`openai-oauth-ws.mjs:175-178`, `stall-policy.mjs:202-206`).\n\n- Midstream retry는 outer loop가 `attemptIndex <= 4`까지 돌 수 있지만(`openai-oauth-ws.mjs:514`, `openai-oauth-ws.mjs:557`), 실제 허용 횟수는 classifier별입니다. `ws_1006|ws_1011`은 4회, 나머지 WS bucket은 2회입니다(`retry-classifier.mjs:245-268`, `openai-oauth-ws.mjs:164-168`, `openai-oauth-ws.mjs:843-860`).\n\n- Midstream backoff는 retry 번호별 `[250,1000,2000,4000]`를 쓰고 jitter를 적용합니다(`retry-classifier.mjs:245-247`, `openai-oauth-ws.mjs:170-173`). 단, 로컬 주석은 “two retries / one retry”라고 되어 있어 실제 4/2 budget과 불일치합니다(`openai-oauth-ws.mjs:503-508`).\n\n- 같은 fault가 두 계층에서 이중 재시도되지는 않습니다. acquire 실패는 midstream classifier로 넘어가지 않고 바로 throw되며(`openai-oauth-ws.mjs:578-601`), midstream 실패만 `_classifyMidstreamError()` 후 재시도되고 다음 시도에서 fresh acquire를 새로 수행합니다(`openai-oauth-ws.mjs:511-513`, `openai-oauth-ws.mjs:564-577`, `openai-oauth-ws.mjs:843-860`).\n\n- 다만 한 사용자 요청 안에서 “midstream fault 재시도 → 새 acquire 중 handshake fault 재시도”는 가능하므로, 두 budget은 같은 error class에 중첩되는 것이 아니라 서로 다른 단계의 fault에 순차적으로 소비됩니다.",
28
+ "card": {
29
+ "session": "sess_54872_1783078178435_1783078178685_d27922b6978571ab4599bcc38f64fa0f",
30
+ "provider": "openai-oauth",
31
+ "model": "gpt-5.5",
32
+ "wall_ms": 244855,
33
+ "turns": 44,
34
+ "tool_calls": 42,
35
+ "tools_per_turn": 1,
36
+ "total_tool_ms": 2277,
37
+ "llm_stream_ms": 305080,
38
+ "cache_ratio": 0.7561787493012467,
39
+ "cached_tokens": 597906,
40
+ "prompt_tokens": 790694,
41
+ "uncached_tokens": 35297,
42
+ "output_tokens": 14439,
43
+ "cache_weighted_input_10": 95087.6,
44
+ "cache_weighted_input_25": 184773.5,
45
+ "prompt_growth": 33692,
46
+ "antipatterns": 4,
47
+ "issues": 22,
48
+ "issue_types": [
49
+ "cache",
50
+ "cache_break",
51
+ "cache_miss",
52
+ "readonly_stall",
53
+ "broad_tool_result",
54
+ "service_tier",
55
+ "read_fragmentation",
56
+ "missed_parallelism",
57
+ "tool_churn_cluster"
58
+ ],
59
+ "read_fragmentation_paths": [
60
+ "src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs"
61
+ ],
62
+ "grep_sweep_count": 0
63
+ }
64
+ }
65
+ ],
66
+ "task_complete": true,
67
+ "task_errors": [],
68
+ "score_complete": true,
69
+ "score_errors": [],
70
+ "group": {
71
+ "n": 1,
72
+ "wall_ms": 244855,
73
+ "turns": 44,
74
+ "tool_calls": 42,
75
+ "tools_per_turn": 1,
76
+ "total_tool_ms": 2277,
77
+ "llm_stream_ms": 305080,
78
+ "cache_ratio": 0.8,
79
+ "cached_tokens": 597906,
80
+ "prompt_tokens": 790694,
81
+ "uncached_tokens": 35297,
82
+ "output_tokens": 14439,
83
+ "cache_weighted_input_10": 95087.6,
84
+ "cache_weighted_input_25": 184773.5,
85
+ "prompt_growth": 33692,
86
+ "antipatterns": 4,
87
+ "issues": 22,
88
+ "grep_sweep_count": 0
89
+ },
90
+ "cards": [
91
+ {
92
+ "session": "sess_54872_1783078178435_1783078178685_d27922b6978571ab4599bcc38f64fa0f",
93
+ "provider": "openai-oauth",
94
+ "model": "gpt-5.5",
95
+ "wall_ms": 244855,
96
+ "turns": 44,
97
+ "tool_calls": 42,
98
+ "tools_per_turn": 1,
99
+ "total_tool_ms": 2277,
100
+ "llm_stream_ms": 305080,
101
+ "cache_ratio": 0.7561787493012467,
102
+ "cached_tokens": 597906,
103
+ "prompt_tokens": 790694,
104
+ "uncached_tokens": 35297,
105
+ "output_tokens": 14439,
106
+ "cache_weighted_input_10": 95087.6,
107
+ "cache_weighted_input_25": 184773.5,
108
+ "prompt_growth": 33692,
109
+ "antipatterns": 4,
110
+ "issues": 22,
111
+ "issue_types": [
112
+ "cache",
113
+ "cache_break",
114
+ "cache_miss",
115
+ "readonly_stall",
116
+ "broad_tool_result",
117
+ "service_tier",
118
+ "read_fragmentation",
119
+ "missed_parallelism",
120
+ "tool_churn_cluster"
121
+ ],
122
+ "read_fragmentation_paths": [
123
+ "src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs"
124
+ ],
125
+ "grep_sweep_count": 0
126
+ }
127
+ ]
128
+ }
@@ -24,6 +24,7 @@ import { dirname, resolve } from 'node:path';
24
24
 
25
25
  const __dir = dirname(fileURLToPath(import.meta.url));
26
26
  const HEADLESS = pathToFileURL(resolve(__dir, '../src/headless-role.mjs')).href;
27
+ const WORKFLOW_MOD = pathToFileURL(resolve(__dir, '../src/session-runtime/workflow.mjs')).href;
27
28
  const TASK_BENCH = resolve(__dir, 'task-bench.mjs');
28
29
 
29
30
  function argValue(name, fallback = null) {
@@ -65,11 +66,15 @@ const RUNNERS = {
65
66
  const RUNTIME_URL = pathToFileURL(resolve(__dir, '../src/mixdog-session-runtime.mjs')).href;
66
67
  const driver = [
67
68
  `import { createMixdogSessionRuntime } from ${JSON.stringify(RUNTIME_URL)};`,
69
+ `import { normalizeWorkflowId } from ${JSON.stringify(WORKFLOW_MOD)};`,
68
70
  `process.chdir(${JSON.stringify(task.cwd ? resolve(task.cwd) : process.cwd())});`,
69
71
  `const rt = await createMixdogSessionRuntime({`,
70
72
  ` provider: ${JSON.stringify(opts.provider || null)} || undefined,`,
71
73
  ` model: ${JSON.stringify(opts.model || null)} || undefined,`,
72
74
  `});`,
75
+ `if (${JSON.stringify(opts.workflow || null)}) {`,
76
+ ` await rt.setWorkflow(normalizeWorkflowId(${JSON.stringify(opts.workflow || null)}));`,
77
+ `}`,
73
78
  `let text = '';`,
74
79
  `// Mirror the TUI auto-resume EXACTLY (engine/agent-job-feed.mjs):`,
75
80
  `// runtime notifications enqueue the model-visible completion into the`,
@@ -145,7 +150,6 @@ const RUNNERS = {
145
150
  maxBuffer: 64 * 1024 * 1024,
146
151
  env: {
147
152
  ...process.env,
148
- ...(opts.effort ? { MIXDOG_AGENT_EFFORT: opts.effort } : {}),
149
153
  ...(opts.env || {}),
150
154
  },
151
155
  }, (err, stdout, stderrOut) => {
@@ -164,6 +168,8 @@ const RUNNERS = {
164
168
  ` message: ${JSON.stringify(task.prompt || '')},`,
165
169
  ` provider: ${JSON.stringify(opts.provider || null)},`,
166
170
  ` model: ${JSON.stringify(opts.model || null)},`,
171
+ ` effort: ${JSON.stringify(opts.effort || null)},`,
172
+ ` fast: ${JSON.stringify(opts.fast === true)},`,
167
173
  ` cwd: ${JSON.stringify(task.cwd ? resolve(task.cwd) : process.cwd())},`,
168
174
  ` write: (t) => out.push(t),`,
169
175
  ` writeErr: (t) => process.stderr.write(t),`,
@@ -178,8 +184,6 @@ const RUNNERS = {
178
184
  maxBuffer: 64 * 1024 * 1024,
179
185
  env: {
180
186
  ...process.env,
181
- ...(opts.effort ? { MIXDOG_AGENT_EFFORT: opts.effort } : {}),
182
- ...(opts.fast ? { MIXDOG_AGENT_FAST: '1' } : {}),
183
187
  ...(opts.env || {}),
184
188
  },
185
189
  }, (err, stdout, stderrOut) => {
@@ -198,6 +202,7 @@ const RUNNERS = {
198
202
  '-C', task.cwd ? resolve(task.cwd) : process.cwd()];
199
203
  if (opts.model) args.push('-m', opts.model);
200
204
  if (opts.effort) args.push('-c', `model_reasoning_effort="${opts.effort}"`);
205
+ if (opts.fast) args.push('-c', 'service_tier="fast"');
201
206
  args.push('-');
202
207
  const started = Date.now();
203
208
  const { raw, ok } = await new Promise((resolveRun) => {
@@ -241,6 +246,7 @@ const RUNNERS = {
241
246
  out_tokens: usage.out_tokens,
242
247
  cache_ratio: totalIn > 0 ? Math.round((usage.cached_in / totalIn) * 1000) / 1000 : 0,
243
248
  completed: ok ? 1 : 0,
249
+ usd_cost: usdCost(opts.model, usage.uncached_in, usage.cached_in, usage.out_tokens),
244
250
  };
245
251
  return { sessionId: card.session, ok, ms, raw, card, finalMessage };
246
252
  },
@@ -290,6 +296,23 @@ function num(v) {
290
296
  return Number.isFinite(n) ? n : 0;
291
297
  }
292
298
 
299
+ // USD list pricing per 1M tokens (input / cached input / output), 2026-07.
300
+ // Cross-harness cost comparison needs a real currency: OAuth lanes bill
301
+ // quota, not dollars, but token counts priced at API list rates are the
302
+ // only harness-neutral metric. Unknown models fall back to gpt-5.5 rates.
303
+ const USD_PER_1M = {
304
+ 'gpt-5.5': { in: 5.00, cachedIn: 0.50, out: 30.00 },
305
+ 'claude-sonnet-5': { in: 2.00, cachedIn: 0.20, out: 10.00 },
306
+ 'claude-fable-5': { in: 5.00, cachedIn: 0.50, out: 25.00 },
307
+ };
308
+ function usdCost(model, uncachedIn, cachedIn, outTokens) {
309
+ const key = String(model || '').toLowerCase();
310
+ const p = USD_PER_1M[key]
311
+ || USD_PER_1M[Object.keys(USD_PER_1M).find((k) => key.includes(k))]
312
+ || USD_PER_1M['gpt-5.5'];
313
+ return Math.round(((num(uncachedIn) * p.in + num(cachedIn) * p.cachedIn + num(outTokens) * p.out) / 1e6) * 10000) / 10000;
314
+ }
315
+
293
316
  function averageCards(cards) {
294
317
  if (!cards.length) return null;
295
318
  const keys = Object.keys(cards[0]).filter((k) => cards.some((c) => typeof c[k] === 'number' && Number.isFinite(c[k])));
@@ -334,9 +357,10 @@ const opts = {
334
357
  model: _mo.model,
335
358
  effort: argValue('--effort', null),
336
359
  fast: hasFlag('--fast'),
360
+ workflow: argValue('--workflow', null),
337
361
  env: _env,
338
362
  };
339
- process.stderr.write(`[bench-run] model=${opts.model || '(agent default)'} provider=${opts.provider || '(agent default)'} effort=${opts.effort || '-'} fast=${opts.fast} env=${JSON.stringify(_env)}\n`);
363
+ process.stderr.write(`[bench-run] model=${opts.model || '(agent default)'} provider=${opts.provider || '(agent default)'} effort=${opts.effort || '-'} fast=${opts.fast} workflow=${opts.workflow || '-'} env=${JSON.stringify(_env)}\n`);
340
364
 
341
365
  // --parallel N: run up to N tasks concurrently (default: all). Scoring stays
342
366
  // sequential after runs so task-bench reads a settled trace.
@@ -344,7 +368,11 @@ const parallel = Math.max(1, Number.parseInt(argValue('--parallel', String(tasks
344
368
  // --repeat N: run the whole task set N times (repeat r appended to task id as
345
369
  // "id#r") so group averages settle over more samples.
346
370
  const repeat = Math.max(1, Number.parseInt(argValue('--repeat', '1'), 10) || 1);
347
- process.stderr.write(`[bench-run] parallel=${parallel} repeat=${repeat}\n`);
371
+ // --stagger MS: delay each parallel lane's start by laneIndex*MS so session
372
+ // first-calls don't race the server prompt-cache write window (A/B for
373
+ // parallel fan-out cache misses).
374
+ const staggerMs = Math.max(0, Number.parseInt(argValue('--stagger', '0'), 10) || 0);
375
+ process.stderr.write(`[bench-run] parallel=${parallel} repeat=${repeat} stagger=${staggerMs}ms\n`);
348
376
  const runList = [];
349
377
  for (let r = 1; r <= repeat; r += 1) {
350
378
  for (const task of tasks) {
@@ -373,14 +401,15 @@ async function runTask(task) {
373
401
  const results = new Array(runList.length);
374
402
  {
375
403
  let next = 0;
376
- async function drain() {
404
+ async function drain(lane) {
405
+ if (staggerMs > 0 && lane > 0) await new Promise((res) => setTimeout(res, lane * staggerMs));
377
406
  for (;;) {
378
407
  const i = next++;
379
408
  if (i >= runList.length) return;
380
409
  results[i] = await runTask(runList[i]);
381
410
  }
382
411
  }
383
- await Promise.all(Array.from({ length: Math.min(parallel, runList.length) }, drain));
412
+ await Promise.all(Array.from({ length: Math.min(parallel, runList.length) }, (_, lane) => drain(lane)));
384
413
  }
385
414
 
386
415
  for (const result of results) {
@@ -389,6 +418,10 @@ for (const result of results) {
389
418
  const scored = scoreSession(result.sessionId, { env: opts.env });
390
419
  if (scored.ok) {
391
420
  result.card = scored.card;
421
+ // task-bench cards expose uncached/cached/output token fields; price at
422
+ // the session's own model when present, else the round's --model.
423
+ const c = scored.card;
424
+ c.usd_cost = usdCost(c.model || opts.model, c.uncached_tokens, c.cached_tokens, c.output_tokens);
392
425
  process.stderr.write(`[bench-run] -> scored ${result.sessionId.slice(0, 22)} turns=${scored.card.turns} tools=${scored.card.tool_calls}\n`);
393
426
  } else {
394
427
  result.scoreError = scored.error;
@@ -212,6 +212,8 @@ function runHeadlessWorker({ pluginRoot, dataDir, taskCwd, prompt, provider, mod
212
212
  ` message: ${JSON.stringify(prompt)},`,
213
213
  ` provider: ${JSON.stringify(provider || null)},`,
214
214
  ` model: ${JSON.stringify(model || null)},`,
215
+ ` effort: ${JSON.stringify(effort || null)},`,
216
+ ` fast: ${JSON.stringify(fast === true)},`,
215
217
  ` cwd: ${JSON.stringify(taskCwd)},`,
216
218
  ` write: (t) => out.push(t),`,
217
219
  ` writeErr: (t) => process.stderr.write(t),`,
@@ -230,8 +232,6 @@ function runHeadlessWorker({ pluginRoot, dataDir, taskCwd, prompt, provider, mod
230
232
  ...process.env,
231
233
  MIXDOG_ROOT: pluginRoot,
232
234
  MIXDOG_DATA_DIR: dataDir,
233
- ...(effort ? { MIXDOG_AGENT_EFFORT: effort } : {}),
234
- ...(fast ? { MIXDOG_AGENT_FAST: '1' } : {}),
235
235
  },
236
236
  });
237
237
  ok = true;
@@ -478,8 +478,6 @@ function runLiveLeadDelegation({ pluginRoot, dataDir, taskCwd, prompt, provider,
478
478
  ...process.env,
479
479
  MIXDOG_ROOT: pluginRoot,
480
480
  MIXDOG_DATA_DIR: dataDir,
481
- ...(effort ? { MIXDOG_AGENT_EFFORT: effort } : {}),
482
- ...(fast ? { MIXDOG_AGENT_FAST: '1' } : {}),
483
481
  },
484
482
  });
485
483
  const j = raw.lastIndexOf('{');
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from '../src/runtime/agent/orchestrator/config.mjs';
3
+ import { initProviders, getAllProviders } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
4
+ import {
5
+ loadModelsDevCatalog,
6
+ warmModelMetadataCatalogs,
7
+ } from '../src/runtime/agent/orchestrator/providers/model-catalog.mjs';
8
+ import { QUICK_SEARCH_MODELS } from '../src/session-runtime/quick-search-models.mjs';
9
+
10
+ const args = new Set(process.argv.slice(2));
11
+ const json = args.has('--json');
12
+ const allRows = args.has('--all');
13
+ const refresh = !args.has('--no-refresh');
14
+
15
+ const PROVIDER_ALIAS = Object.freeze({
16
+ 'openai-oauth': 'openai',
17
+ openai: 'openai',
18
+ 'grok-oauth': 'xai',
19
+ xai: 'xai',
20
+ gemini: 'google',
21
+ google: 'google',
22
+ 'anthropic-oauth': 'anthropic',
23
+ anthropic: 'anthropic',
24
+ });
25
+
26
+ const LIT_PREFIXES = Object.freeze({
27
+ openai: ['openai/'],
28
+ xai: ['xai/'],
29
+ google: ['gemini/', 'google/'],
30
+ anthropic: ['anthropic/'],
31
+ deepseek: ['deepseek/'],
32
+ });
33
+
34
+ function n(value) {
35
+ const num = Number(value);
36
+ return Number.isFinite(num) && num > 0 ? Math.floor(num) : null;
37
+ }
38
+
39
+ function contextLabel(value) {
40
+ const v = n(value);
41
+ if (!v) return '-';
42
+ if (v >= 1_000_000) {
43
+ const m = v / 1_000_000;
44
+ const label = Number.isInteger(m)
45
+ ? m.toFixed(0)
46
+ : m.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
47
+ return `${label}M`;
48
+ }
49
+ return `${Math.round(v / 1000)}k`;
50
+ }
51
+
52
+ function isInteresting(id) {
53
+ const s = String(id || '').toLowerCase();
54
+ return /^(gpt-5|gpt-4\.1|claude-|gemini-|grok-|deepseek-v4|glm-|qwen|minimax|kimi)/.test(s);
55
+ }
56
+
57
+ function litellmRow(litellm, provider, id) {
58
+ const p = PROVIDER_ALIAS[provider] || provider;
59
+ const keys = [id, ...(LIT_PREFIXES[p] || []).map(prefix => prefix + id)];
60
+ if (p === 'anthropic') keys.push(`anthropic.${id}-v1:0`, `bedrock/anthropic.${id}-v1:0`);
61
+ for (const key of keys) {
62
+ if (litellm?.[key]) return { key, row: litellm[key] };
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function externalMeta(modelsDev, litellm, provider, id) {
68
+ const p = PROVIDER_ALIAS[provider] || provider;
69
+ const md = modelsDev?.[p]?.models?.[id] || null;
70
+ const lit = litellmRow(litellm, provider, id);
71
+ return {
72
+ modelsDevContext: n(md?.limit?.context),
73
+ modelsDevOutput: n(md?.limit?.output),
74
+ litellmContext: n(lit?.row?.max_input_tokens ?? lit?.row?.max_tokens),
75
+ litellmOutput: n(lit?.row?.max_output_tokens),
76
+ externalName: md?.name || null,
77
+ litellmKey: lit?.key || null,
78
+ };
79
+ }
80
+
81
+ function classifyOneMillion(value) {
82
+ const v = n(value);
83
+ if (!v) return 'unknown';
84
+ if (v === 1_000_000) return 'exact-1M';
85
+ if (v > 900_000 && v < 1_100_000) return 'near-1M-not-exact';
86
+ return 'not-1M';
87
+ }
88
+
89
+ async function listProviderModels(name, provider) {
90
+ let models = null;
91
+ let refreshed = false;
92
+ let error = null;
93
+ try {
94
+ if (refresh && typeof provider?._refreshModelCache === 'function') {
95
+ models = await provider._refreshModelCache();
96
+ refreshed = true;
97
+ }
98
+ if (!Array.isArray(models) && typeof provider?.listModels === 'function') {
99
+ models = await provider.listModels();
100
+ }
101
+ } catch (err) {
102
+ error = err?.message || String(err);
103
+ }
104
+ return { provider: name, refreshed, error, models: Array.isArray(models) ? models : [] };
105
+ }
106
+
107
+ const cfg = loadConfig();
108
+ try {
109
+ await initProviders(cfg.providers || {});
110
+ } catch (err) {
111
+ if (!json) console.error(`[model-audit] init failed: ${err?.message || err}`);
112
+ }
113
+
114
+ const [modelsDev, litellm] = await Promise.all([
115
+ loadModelsDevCatalog().catch(() => ({})),
116
+ warmModelMetadataCatalogs().catch(() => ({})),
117
+ ]);
118
+
119
+ const providerResults = [];
120
+ for (const [name, provider] of getAllProviders()) {
121
+ if (typeof provider?.listModels !== 'function') continue;
122
+ const listed = await listProviderModels(name, provider);
123
+ const rows = listed.models
124
+ .filter(row => row?.id && (allRows || isInteresting(row.id)))
125
+ .map(row => {
126
+ const liveContext = n(row.contextWindow ?? row.maxContextWindow);
127
+ const liveOutput = n(row.outputTokens);
128
+ const external = externalMeta(modelsDev, litellm, name, row.id);
129
+ const externalContext = external.modelsDevContext ?? external.litellmContext;
130
+ const externalOutput = external.modelsDevOutput ?? external.litellmOutput;
131
+ return {
132
+ provider: name,
133
+ id: row.id,
134
+ display: row.display || row.name || null,
135
+ liveContext,
136
+ liveContextLabel: contextLabel(liveContext),
137
+ liveOutput,
138
+ externalContext,
139
+ externalContextLabel: contextLabel(externalContext),
140
+ externalOutput,
141
+ contextVerdict: classifyOneMillion(liveContext),
142
+ contextMismatch: liveContext != null && externalContext != null && liveContext !== externalContext,
143
+ outputMismatch: liveOutput != null && externalOutput != null && liveOutput !== externalOutput,
144
+ source: listed.refreshed ? 'live-refresh' : 'live-list',
145
+ ...external,
146
+ };
147
+ });
148
+ providerResults.push({
149
+ provider: name,
150
+ refreshed: listed.refreshed,
151
+ error: listed.error,
152
+ count: listed.models.length,
153
+ rows,
154
+ });
155
+ }
156
+
157
+ const quickRows = [];
158
+ for (const [provider, models] of Object.entries(QUICK_SEARCH_MODELS)) {
159
+ for (const model of models) {
160
+ if (model?.contextWindow == null) continue;
161
+ const external = externalMeta(modelsDev, litellm, provider, model.id);
162
+ const externalContext = external.modelsDevContext ?? external.litellmContext;
163
+ quickRows.push({
164
+ provider,
165
+ id: model.id,
166
+ staticContext: n(model.contextWindow),
167
+ externalContext,
168
+ mismatch: n(model.contextWindow) != null && externalContext != null && n(model.contextWindow) !== externalContext,
169
+ });
170
+ }
171
+ }
172
+
173
+ const mismatches = providerResults.flatMap(p => p.rows.filter(r => r.contextMismatch || r.outputMismatch));
174
+ const exactOneM = providerResults.flatMap(p => p.rows.filter(r => r.liveContext === 1_000_000));
175
+ const nearOneM = providerResults.flatMap(p => p.rows.filter(r => r.contextVerdict === 'near-1M-not-exact'));
176
+
177
+ const report = {
178
+ generatedAt: new Date().toISOString(),
179
+ enabledProviders: Object.entries(cfg.providers || {}).filter(([, v]) => v?.enabled).map(([k]) => k),
180
+ providerResults,
181
+ mismatches,
182
+ exactOneM,
183
+ nearOneM,
184
+ quickStaticContextRows: quickRows,
185
+ };
186
+
187
+ if (json) {
188
+ console.log(JSON.stringify(report, null, 2));
189
+ } else {
190
+ console.log(`model catalog audit @ ${report.generatedAt}`);
191
+ console.log(`enabled providers: ${report.enabledProviders.join(', ') || '-'}`);
192
+ for (const p of providerResults) {
193
+ console.log(`\n[${p.provider}] count=${p.count} refreshed=${p.refreshed}${p.error ? ` error=${p.error}` : ''}`);
194
+ for (const r of p.rows) {
195
+ const flags = [
196
+ r.contextMismatch ? `context ${r.liveContextLabel} != external ${r.externalContextLabel}` : '',
197
+ r.outputMismatch ? `output ${r.liveOutput} != external ${r.externalOutput}` : '',
198
+ ].filter(Boolean).join('; ');
199
+ console.log(` ${r.id}: ${r.liveContextLabel} ctx, out=${r.liveOutput ?? '-'}${flags ? ` [${flags}]` : ''}`);
200
+ }
201
+ }
202
+ console.log(`\nsummary: mismatches=${mismatches.length}, exact1M=${exactOneM.length}, near1MNotExact=${nearOneM.length}, staticContextRows=${quickRows.length}`);
203
+ if (mismatches.length) {
204
+ console.log('mismatches:');
205
+ for (const r of mismatches.slice(0, 50)) {
206
+ console.log(` ${r.provider}/${r.id}: live=${r.liveContextLabel}/${r.liveOutput ?? '-'} external=${r.externalContextLabel}/${r.externalOutput ?? '-'}`);
207
+ }
208
+ }
209
+ }
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { sanitizeModelList } from '../src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs';
4
+
5
+ test('Anthropic staleness keeps Claude subfamilies separate', () => {
6
+ const prev = process.env.MIXDOG_MODEL_STALE_MONTHS;
7
+ process.env.MIXDOG_MODEL_STALE_MONTHS = '0';
8
+ try {
9
+ const models = [
10
+ { id: 'claude-opus-4-8', mode: 'chat' },
11
+ { id: 'claude-sonnet-4-6', mode: 'chat' },
12
+ { id: 'claude-haiku-4-5-20251001', mode: 'chat' },
13
+ ];
14
+ const catalogRow = (release_date) => ({
15
+ family: 'claude',
16
+ release_date,
17
+ tool_call: true,
18
+ modalities: { output: ['text'] },
19
+ });
20
+ const out = sanitizeModelList(models, {
21
+ provider: 'anthropic',
22
+ _testCatalog: {
23
+ anthropic: {
24
+ models: {
25
+ 'claude-opus-4-8': catalogRow('2025-12-01'),
26
+ 'claude-sonnet-4-6': catalogRow('2025-11-01'),
27
+ 'claude-haiku-4-5-20251001': catalogRow('2025-10-15'),
28
+ },
29
+ },
30
+ },
31
+ });
32
+ assert.deepEqual(out.map((m) => m.id), models.map((m) => m.id));
33
+ } finally {
34
+ if (prev == null) delete process.env.MIXDOG_MODEL_STALE_MONTHS;
35
+ else process.env.MIXDOG_MODEL_STALE_MONTHS = prev;
36
+ }
37
+ });