mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -262,6 +262,44 @@ export function parseGrepCoverage(resultText, toolName, toolArgs, resultKind) {
262
262
  return out.length ? out : null;
263
263
  }
264
264
 
265
+ // Codex-style patch failures all arrive as "apply_patch … failed" prose, but
266
+ // a malformed envelope, a rejected hunk, a preflight veto and a size/lock
267
+ // guard need different operator responses. Returning null means "nothing
268
+ // patch-specific here" and lets the generic rules (path/enoent, schema/args,
269
+ // runtime/failure) finish the classification.
270
+ // Emitted by the sequence/wave reporters whenever section writes reached disk
271
+ // and were NOT rolled back cleanly (committed-by-design, partial mode, or a
272
+ // rollback that itself failed). Any of these means the working tree state is
273
+ // committed or uncertain, which outranks every other failure detail.
274
+ const PATCH_COMMITTED_WRITES_RE = /already applied to disk \(writes committed\)|applied to disk \(committed\) and left in place|applied \(committed to disk\)|rollback was incomplete|--- rollback incomplete ---|patch partially (?:applied|created|updated|written)/;
275
+
276
+ function classifyPatchFailure(text) {
277
+ // Writes already committed outranks every other patch detail: the tree is
278
+ // no longer in its pre-patch state and needs inspection before a retry.
279
+ if (PATCH_COMMITTED_WRITES_RE.test(text)) return 'patch/partial-apply';
280
+ // Resource guards (byte cap) are deliberate rejections, not tool defects.
281
+ if (/patch too large|byte cap/.test(text)) return 'patch/limit';
282
+ if (/parse failed|invalid patch|malformed patch|missing \*\*\* (?:begin|end) patch|patch body is empty|unsupported patch format/.test(text)
283
+ // `patch contained no file sections` / `contains an empty file path`:
284
+ // the envelope parsed but produced nothing applicable — a patch-text
285
+ // defect, not a context miss.
286
+ || /contained no file sections|contains an empty file path/.test(text)) return 'patch/parse';
287
+ if (/hunk rejected|context not found|context mismatch|anchor not found|expected first old(?:\/context| line)/.test(text)) {
288
+ // Stale context requires REAL evidence that the region exists but has
289
+ // moved/changed (nearest line, divergent line, rejected hunk). The
290
+ // `expected first old line: …` fragment is printed for every context
291
+ // miss, so on its own it means only "the context is not there".
292
+ return /nearest line|first divergent line|hunk rejected/.test(text)
293
+ ? 'patch/stale-context'
294
+ : 'patch/context';
295
+ }
296
+ // Missing/unreadable targets are path problems even when a preflight
297
+ // wrapper reports them; fall through to the generic path rules.
298
+ if (/enoent|no such file|target unreadable|source (?:missing or )?unreadable|destination unreadable|cannot find/.test(text)) return null;
299
+ if (/preflight rejected|does not support|cannot be combined|only one v4a rename|missing parsed entry|internal js dispatch|rollback snapshot target/.test(text)) return 'patch/verification';
300
+ return null;
301
+ }
302
+
265
303
  function classifyToolFailure(resultText, toolName) {
266
304
  const raw = String(resultText ?? '');
267
305
  const text = raw.toLowerCase();
@@ -281,16 +319,31 @@ function classifyToolFailure(resultText, toolName) {
281
319
  if (/\[signal:\s*[^\]]+/i.test(leading)) return 'process/signal';
282
320
  return 'command-exit';
283
321
  }
284
- if (/\[tool-input-validation\]|compacted-history placeholder/.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';
322
+ if (/compacted-history placeholder/.test(text)) return 'expected-preflight';
323
+ if (/\[tool-input-validation\]/.test(text)) return 'schema/args';
324
+ // `shell` results quote git/patch output verbatim (e.g. `git apply`
325
+ // rejects); only real patch surfaces may claim the patch taxonomy, so an
326
+ // ordinary command exit is never rewritten into a patch failure.
327
+ const patchSurface = String(toolName || '') === 'apply_patch'
328
+ || (String(toolName || '') !== 'shell'
329
+ && /apply_patch|native patch|v4a|\*\*\* begin patch|hunk rejected|context not found/.test(text));
330
+ // Committed/uncertain on-disk state outranks the lock or permission detail
331
+ // that triggered it: the operator must inspect the tree before retrying,
332
+ // and a "denied"/"lock held" word further down must not hide that.
333
+ if (patchSurface && PATCH_COMMITTED_WRITES_RE.test(text)) return 'patch/partial-apply';
334
+ // Write-lock contention is a resource guard, not a timeout or a patch bug.
335
+ if (/advisory lock (?:timeout|busy)|lock held by pid|\.mixdog-lock/.test(text)) return 'resource/lock';
336
+ if (/eacces|eperm|permission|denied|forbidden|operation not permitted/.test(text)) return 'path/permission';
337
+ if (patchSurface) {
338
+ const patchCategory = classifyPatchFailure(text);
339
+ if (patchCategory) return patchCategory;
340
+ }
287
341
  if (/requires either|invalid arguments|unknown parameter|unknown memory action/.test(text)
288
342
  || /must be|schema|required|old_string is .*>?=/.test(text)) return 'schema/args';
289
343
  if (/not in allow-list|not allowed/.test(text)) return 'permission';
290
344
  if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(raw)) return 'command-exit';
291
345
  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';
292
346
  if (/timed out|timeout|interrupted|aborted/.test(text)) return 'timeout/abort';
293
- if (/permission|denied|forbidden/.test(text)) return 'permission';
294
347
  if (/unknown tool|tool.*not.*available|missing.*tool/.test(text)) return 'tool-surface';
295
348
  return 'runtime/failure';
296
349
  }
@@ -305,8 +305,8 @@ function _scheduleFlush(immediate = false) {
305
305
  // Never let the periodic trace flush keep the event loop alive: a
306
306
  // ref'd 5s timer prevented short-lived processes (smoke scripts, agent
307
307
  // tasks) from exiting naturally after their work was done — they hung
308
- // until an external timeout/kill. exit/beforeExit drains still flush
309
- // the buffer, so unref loses no data.
308
+ // until an external timeout/kill. The synchronous exit/beforeExit drain
309
+ // still flushes the local spool; explicit shutdown awaits remote drain.
310
310
  _flushTimer.unref?.();
311
311
  }
312
312
  }
@@ -331,7 +331,6 @@ async function drainAgentTrace() {
331
331
  await _flush();
332
332
  }
333
333
  }
334
- process.on('exit', drainAgentTrace);
335
334
 
336
335
  function appendAgentTrace(record = {}) {
337
336
  // Test isolation — when run-all-tests.mjs sets this env, skip entirely.
@@ -366,6 +366,26 @@ export function loadConfig(options = {}) {
366
366
  .map(p => normalizePreset(p))
367
367
  .filter(Boolean)
368
368
  .filter(p => p.id !== 'workflow-search');
369
+ // `fastModels` was the pre-modelSettings Fast preference map.
370
+ // Fold any still-meaningful true entries into the canonical map,
371
+ // without overriding an explicit modelSettings.fast value. Keep
372
+ // the compatibility read here, but never expose/persist the
373
+ // legacy container again.
374
+ const modelSettings = raw.modelSettings && typeof raw.modelSettings === 'object'
375
+ ? { ...raw.modelSettings }
376
+ : {};
377
+ const legacyFastModels = raw.fastModels && typeof raw.fastModels === 'object'
378
+ ? raw.fastModels
379
+ : {};
380
+ for (const [key, enabled] of Object.entries(legacyFastModels)) {
381
+ if (enabled !== true) continue;
382
+ const current = modelSettings[key];
383
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
384
+ modelSettings[key] = { fast: true };
385
+ } else if (!Object.prototype.hasOwnProperty.call(current, 'fast')) {
386
+ modelSettings[key] = { ...current, fast: true };
387
+ }
388
+ }
369
389
  const workflowRoutes = raw.workflowRoutes && typeof raw.workflowRoutes === 'object' ? { ...raw.workflowRoutes } : {};
370
390
  delete workflowRoutes.search;
371
391
  // Normalize maintenance slots to routes, then overlay onto the
@@ -382,12 +402,10 @@ export function loadConfig(options = {}) {
382
402
  maintenance: { ...DEFAULT_MAINTENANCE, ...normalizedMaint },
383
403
  workflowRoutes,
384
404
  searchRoute: normalizeSearchRoute(raw.searchRoute),
385
- fastModels: raw.fastModels && typeof raw.fastModels === 'object' ? raw.fastModels : {},
386
- modelSettings: raw.modelSettings && typeof raw.modelSettings === 'object' ? raw.modelSettings : {},
405
+ modelSettings,
387
406
  onboarding: raw.onboarding && typeof raw.onboarding === 'object' ? raw.onboarding : {},
388
407
  agents: raw.agents && typeof raw.agents === 'object' ? raw.agents : {},
389
408
  workflow: raw.workflow && typeof raw.workflow === 'object' ? { active: String(raw.workflow.active || 'default') } : { active: 'default' },
390
- agentMaintenance: { enabled: true, interval: '1h', ...raw.agentMaintenance },
391
409
  profile: normalizeProfileConfig(raw.profile),
392
410
  skills: normalizeSkillsConfig(raw.skills),
393
411
  // No idleMs default here: absent idleMs means "provider default"
@@ -396,7 +414,6 @@ export function loadConfig(options = {}) {
396
414
  // bypass the per-provider auto-clear table.
397
415
  autoClear: { enabled: true, ...raw.autoClear },
398
416
  compaction: raw.compaction && typeof raw.compaction === 'object' ? { ...raw.compaction } : {},
399
- runtime: raw.runtime && typeof raw.runtime === 'object' ? raw.runtime : {},
400
417
  shell: raw.shell && typeof raw.shell === 'object' ? raw.shell : {},
401
418
  update: raw.update && typeof raw.update === 'object' ? { ...raw.update } : {},
402
419
  recap: recapConfig,
@@ -415,17 +432,14 @@ export function loadConfig(options = {}) {
415
432
  maintenance: { ...DEFAULT_MAINTENANCE },
416
433
  workflowRoutes: {},
417
434
  searchRoute: null,
418
- fastModels: {},
419
435
  modelSettings: {},
420
436
  onboarding: {},
421
437
  agents: {},
422
438
  workflow: { active: 'default' },
423
- agentMaintenance: { enabled: true, interval: '1h' },
424
439
  profile: normalizeProfileConfig(null),
425
440
  skills: normalizeSkillsConfig(null),
426
441
  autoClear: { enabled: true },
427
442
  compaction: {},
428
- runtime: {},
429
443
  shell: {},
430
444
  update: {},
431
445
  recap: { enabled: true },
@@ -543,7 +557,7 @@ function buildAgentSaveBuilder(config, appliedDirty) {
543
557
  }
544
558
  if (Object.keys(mergedProject).length > 0) mcpProjectOverrides[projectKey] = mergedProject;
545
559
  }
546
- return {
560
+ const next = {
547
561
  ...existingRaw,
548
562
  guide: config.guide || existingRaw.guide || undefined,
549
563
  providers: persistedProviders,
@@ -554,22 +568,26 @@ function buildAgentSaveBuilder(config, appliedDirty) {
554
568
  maintenance: config.maintenance || {},
555
569
  workflowRoutes,
556
570
  searchRoute: normalizeSearchRoute(config.searchRoute),
557
- fastModels: config.fastModels || {},
558
571
  modelSettings: config.modelSettings || {},
559
572
  onboarding: config.onboarding || {},
560
573
  agents: config.agents || {},
561
574
  workflow: config.workflow || { active: 'default' },
562
- agentMaintenance: config.agentMaintenance || {},
563
575
  profile: normalizeProfileConfig(config.profile),
564
576
  skills: normalizeSkillsConfig(config.skills),
565
577
  autoClear: config.autoClear || {},
566
578
  compaction: config.compaction || {},
567
- runtime: config.runtime || {},
568
579
  shell: config.shell || {},
569
580
  update: config.update || {},
570
581
  recap: config.recap || {},
571
582
  modules: config.modules || existingRaw.modules || {},
572
583
  };
584
+ // These keys were previously round-tripped despite having no runtime
585
+ // consumer. Explicitly remove them from the in-lock baseline so a
586
+ // normal settings save also finishes migration of older installs.
587
+ delete next.fastModels;
588
+ delete next.agentMaintenance;
589
+ delete next.runtime;
590
+ return next;
573
591
  };
574
592
  }
575
593
  export function saveConfig(config) {
@@ -714,7 +714,7 @@ export function loadScopedRoleInstructions(agent, provider = null) {
714
714
  if (useUnified) {
715
715
  // Explicit-cache providers — every agent sees the same all-in-one
716
716
  // instruction surface. Cross-agent calls hit the same provider-side prefix
717
- // shard, eliminating the agent-shard miss seen on Pool C
717
+ // prefix, eliminating the cross-agent cache miss seen on Pool C
718
718
  // transitions for openai-oauth/openai. This branch is disabled by the
719
719
  // empty provider set above; BP2 remains the active agent surface.
720
720
  agentRuleSectionsToEmit = hiddenPairs.map(p => `## ${p.name}\n\n${p.body}`);
@@ -191,7 +191,7 @@ export function listHiddenAgentNames() {
191
191
 
192
192
  /**
193
193
  * List hidden agent names matching a given kind ('retrieval' | 'maintenance').
194
- * Consumed by collect.mjs to drive BP2 agent-shard classification dynamically
194
+ * Consumed by collect.mjs to drive BP2 agent classification dynamically
195
195
  * instead of hard-coding agent-name sets.
196
196
  */
197
197
  export function listHiddenAgentsByKind(kind) {
@@ -24,18 +24,6 @@ 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
- }
39
27
  // Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
40
28
  // The lookup (regex parse + servers Map get + tools.find + schema property
41
29
  // inspection) runs on every MCP tool invocation but its result only changes
@@ -151,7 +139,6 @@ export async function connectMcpServers(config) {
151
139
  * Tool names are prefixed: `mcp__{serverName}__{toolName}`
152
140
  */
153
141
  export function getMcpTools() {
154
- if (_mcpToolProxy) return _mcpToolProxy.tools;
155
142
  const tools = [];
156
143
  for (const server of servers.values()) {
157
144
  tools.push(...server.tools);
@@ -195,9 +182,6 @@ export async function executeMcpTool(name, args) {
195
182
  const match = name.match(/^mcp__(.+?)__(.+)$/);
196
183
  if (!match)
197
184
  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);
201
185
  const [, serverName, toolName] = match;
202
186
  const server = servers.get(serverName);
203
187
  if (!server)
@@ -371,7 +355,6 @@ export function isMcpTool(name) {
371
355
  /** True when the prefixed name exists on a connected MCP server. */
372
356
  export function isRegisteredMcpTool(name) {
373
357
  if (!isMcpTool(name)) return false;
374
- if (_mcpToolProxy) return _mcpToolProxy.tools.some((t) => t?.name === name);
375
358
  const match = name.match(/^mcp__(.+?)__(.+)$/);
376
359
  if (!match) return false;
377
360
  const [, serverName] = match;
@@ -389,13 +372,6 @@ export function mcpToolHasField(name, field) {
389
372
  const memoKey = `${name}|${field}`;
390
373
  const memoized = _mcpToolFieldMemo.get(memoKey);
391
374
  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
- }
399
375
  const match = name.match(/^mcp__(.+?)__(.+)$/);
400
376
  if (!match) { _mcpToolFieldMemo.set(memoKey, false); return false; }
401
377
  const [, serverName] = match;
@@ -123,9 +123,9 @@ export class ProviderAdmissionScheduler {
123
123
  this.reportedRateLimits = new WeakSet();
124
124
  this.closedReason = null;
125
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.
126
+ // Cooldown listeners observe locally-recorded 429 cooldowns and
127
+ // credential-driven resets so every runtime using one account sees
128
+ // the same quota window.
129
129
  this.cooldownListeners = new Set();
130
130
  }
131
131
 
@@ -59,6 +59,7 @@ import {
59
59
  parseSSEStream,
60
60
  _classifyMidstreamError,
61
61
  _midstreamSleepWithAbort,
62
+ stampAnthropicStreamOutcome,
62
63
  } from './anthropic-sse.mjs';
63
64
  import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
64
65
  import {
@@ -718,13 +719,19 @@ export class AnthropicOAuthProvider {
718
719
  const quotaText = await result.response.text().catch(() => '');
719
720
  cleanupCancelHandler(result.cancelHandler);
720
721
  try { result.controller?.abort?.(); } catch {}
721
- throw anthropicQuotaError(status, result?.response?.headers, this.scrubTokens(quotaText));
722
+ // Initial-response failure: nothing was sampled, so the
723
+ // typed retry rules upstream own the decision.
724
+ throw Object.assign(
725
+ anthropicQuotaError(status, result?.response?.headers, this.scrubTokens(quotaText)),
726
+ { initialResponseError: true },
727
+ );
722
728
  }
723
729
  const err = new Error(`Anthropic OAuth API ${status}`);
724
730
  err.httpStatus = status;
725
731
  err.status = status;
726
732
  err.headers = result?.response?.headers;
727
733
  err.response = { status, headers: result?.response?.headers };
734
+ err.initialResponseError = true;
728
735
  const retryAfterMs = retryAfterMsFromError(err);
729
736
  if (transientStatus || retryAfterMs != null) {
730
737
  try { await result.response.text(); } catch {}
@@ -972,6 +979,15 @@ export class AnthropicOAuthProvider {
972
979
  } catch { /* ignore non-extensible result */ }
973
980
  return result;
974
981
  } catch (err) {
982
+ // Canonical stream-outcome contract: stamp before ANY safety
983
+ // decision below. The parser's stamped verdict (when present)
984
+ // is authoritative — coarse midState.partialToolCall must not
985
+ // overwrite an idempotent pending-input truncation — while
986
+ // genuinely new wrapper-observed exposure is still merged.
987
+ let _outcome = null;
988
+ try {
989
+ _outcome = stampAnthropicStreamOutcome(err, midState, { provider: 'anthropic-oauth' });
990
+ } catch { /* stamping is best-effort */ }
975
991
  // Acknowledged reset semantics let the owner tombstone this
976
992
  // attempt before the full request is restarted non-streaming.
977
993
  // Without that acknowledgement, recoverNonStreaming stamps
@@ -979,16 +995,12 @@ export class AnthropicOAuthProvider {
979
995
  if (midState.emittedText) {
980
996
  return await recoverNonStreaming(midState, err, controller);
981
997
  }
982
- // Completed/partial tools and thinking blocks are also replay
983
- // boundaries. Stamp the current (latest) error so no upstream
984
- // retry owner can reissue this turn.
985
- if (midState.emittedToolCall || midState.partialToolCall || midState.emittedThinking) {
986
- try {
987
- err.emittedToolCall = !!midState.emittedToolCall;
988
- err.partialToolCall = !!midState.partialToolCall;
989
- err.emittedThinking = !!midState.emittedThinking;
990
- err.unsafeToRetry = true;
991
- } catch {}
998
+ // Dispatched tools and exposed thinking are replay boundaries;
999
+ // the canonical merge above already recorded them and wrote the
1000
+ // aliases. Coarse midState flags are NOT written here: they
1001
+ // would downgrade the parser's authoritative verdict that an
1002
+ // incomplete, never dispatched tool input stays replay-safe.
1003
+ if (_outcome?.replayUnsafe === true) {
992
1004
  try { controller?.abort?.(err); } catch {}
993
1005
  throw err;
994
1006
  }
@@ -1006,9 +1018,7 @@ export class AnthropicOAuthProvider {
1006
1018
  }
1007
1019
  if (classifyError(err) === 'transient'
1008
1020
  && !midState.sawMessageStart
1009
- && !midState.emittedToolCall
1010
- && !midState.partialToolCall
1011
- && !midState.emittedThinking
1021
+ && _outcome?.replayUnsafe !== true
1012
1022
  && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1013
1023
  firstAttemptError = err;
1014
1024
  firstAttemptClassifier = err?.providerErrorType || 'sse_transient';
@@ -1032,9 +1042,7 @@ export class AnthropicOAuthProvider {
1032
1042
  // yet; otherwise let the error surface.
1033
1043
  if ((err?.truncatedStream === true || err?.code === 'TRUNCATED_STREAM')
1034
1044
  && classifyError(err) === 'transient'
1035
- && !midState.emittedToolCall
1036
- && !midState.partialToolCall
1037
- && !midState.emittedThinking
1045
+ && _outcome?.replayUnsafe !== true
1038
1046
  && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1039
1047
  firstAttemptError = err;
1040
1048
  firstAttemptClassifier = 'truncated_stream';