mixdog 1.0.12 → 1.1.0

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 (256) hide show
  1. package/README.md +75 -181
  2. package/package.json +4 -2
  3. package/scripts/build-shipped-history.mjs +131 -0
  4. package/scripts/lib/run-node-tests.mjs +235 -13
  5. package/scripts/lib/test-failure-records.mjs +5 -0
  6. package/scripts/lib/test-failure-summary.mjs +9 -3
  7. package/scripts/lib/test-spawn-ledger.mjs +19 -0
  8. package/scripts/prune-embedding-runtime.mjs +9 -5
  9. package/scripts/test-direct.mjs +16 -1
  10. package/src/agents/advisor/AGENT.md +38 -0
  11. package/src/agents/advisor/agent.json +6 -0
  12. package/src/agents/reviewer/AGENT.md +5 -2
  13. package/src/agents/reviewer/agent.json +1 -1
  14. package/src/defaults/shipped-history.json +381 -0
  15. package/src/defaults/skills/goal-management/SKILL.md +11 -5
  16. package/src/defaults/skills/setup/SKILL.md +5 -8
  17. package/src/defaults/skills/setup/references/actions.md +61 -31
  18. package/src/defaults/skills/setup/references/surfaces.md +12 -10
  19. package/src/lib/rules-builder.cjs +1 -1
  20. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  22. package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
  23. package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
  24. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
  25. package/src/runtime/agent/orchestrator/config.mjs +102 -21
  26. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
  27. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
  30. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
  32. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
  33. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
  34. package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
  35. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
  36. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
  38. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
  43. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
  44. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
  47. package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
  48. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
  49. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
  50. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
  51. package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
  52. package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
  56. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
  58. package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
  63. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
  64. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
  65. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
  66. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
  67. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
  68. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  69. package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
  72. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
  73. package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
  77. package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
  82. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
  83. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
  85. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
  86. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
  87. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  88. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
  90. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
  91. package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
  92. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
  93. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
  94. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
  95. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
  97. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  98. package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
  99. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
  100. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
  101. package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
  104. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
  105. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
  106. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
  107. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
  108. package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
  109. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
  110. package/src/runtime/channels/lib/config.mjs +3 -8
  111. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
  112. package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
  113. package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
  114. package/src/runtime/computer-bridge/actions.mjs +2 -0
  115. package/src/runtime/effort-judge/judge-client.mjs +12 -3
  116. package/src/runtime/effort-judge/model-manifest.json +7 -7
  117. package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
  118. package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
  119. package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
  120. package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
  121. package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
  122. package/src/runtime/memory/lib/memory-embed.mjs +5 -0
  123. package/src/runtime/shared/agent-route-config.mjs +50 -2
  124. package/src/runtime/shared/bounded-file-read.mjs +166 -0
  125. package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
  126. package/src/runtime/shared/config.mjs +24 -4
  127. package/src/runtime/shared/defaults-separation.mjs +263 -0
  128. package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
  129. package/src/runtime/shared/llm/model-catalog.mjs +23 -0
  130. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
  131. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  132. package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
  133. package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
  134. package/src/runtime/shared/orchestration.mjs +14 -6
  135. package/src/runtime/shared/projects.mjs +65 -48
  136. package/src/runtime/shared/provider-accounts.mjs +47 -6
  137. package/src/runtime/shared/quota-resume-store.mjs +255 -0
  138. package/src/runtime/shared/shipped-definitions.mjs +275 -0
  139. package/src/runtime/shared/tool-card-model.golden.json +3 -3
  140. package/src/runtime/shared/tool-result-summary.mjs +86 -15
  141. package/src/runtime/shared/transcript-metadata.mjs +2 -0
  142. package/src/runtime/shared/turn-review-reply.mjs +2 -0
  143. package/src/runtime/shared/turn-snapshot.mjs +65 -6
  144. package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
  145. package/src/session-runtime/boot/apis.mjs +19 -0
  146. package/src/session-runtime/boot/config.mjs +4 -0
  147. package/src/session-runtime/boot/facade.mjs +4 -1
  148. package/src/session-runtime/boot/infrastructure.mjs +21 -2
  149. package/src/session-runtime/boot/session.mjs +3 -0
  150. package/src/session-runtime/channel-config-api.mjs +5 -0
  151. package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
  152. package/src/session-runtime/config-lifecycle.mjs +31 -6
  153. package/src/session-runtime/goal-facade-api.mjs +4 -0
  154. package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
  155. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
  156. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
  157. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
  158. package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
  159. package/src/session-runtime/media-api.mjs +32 -22
  160. package/src/session-runtime/model-recency.mjs +14 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +3 -5
  162. package/src/session-runtime/model-route/set-route.mjs +49 -21
  163. package/src/session-runtime/model-route-api.mjs +2 -1
  164. package/src/session-runtime/notification-bus.mjs +31 -6
  165. package/src/session-runtime/provider-auth/login.mjs +6 -0
  166. package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
  167. package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
  168. package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
  169. package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
  170. package/src/session-runtime/resource-api.mjs +8 -1
  171. package/src/session-runtime/runtime-core.mjs +4 -0
  172. package/src/session-runtime/runtime-facade.mjs +20 -1
  173. package/src/session-runtime/runtime-review-api.mjs +12 -1
  174. package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
  175. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
  176. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
  177. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
  178. package/src/session-runtime/services/channel-admin.mjs +87 -13
  179. package/src/session-runtime/services/provider-admin.mjs +256 -63
  180. package/src/session-runtime/services/seeds.mjs +73 -142
  181. package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
  182. package/src/session-runtime/services/usage-dashboard.mjs +7 -0
  183. package/src/session-runtime/session-address.mjs +125 -0
  184. package/src/session-runtime/session-create.mjs +57 -2
  185. package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
  186. package/src/session-runtime/settings-compaction-api.mjs +13 -0
  187. package/src/session-runtime/setup-tool/executor.mjs +92 -40
  188. package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
  189. package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
  190. package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
  191. package/src/session-runtime/tool-surface.mjs +16 -9
  192. package/src/session-runtime/turn/session-ops.mjs +3 -0
  193. package/src/session-runtime/turn/share-ops.mjs +4 -1
  194. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
  195. package/src/session-runtime/usage-stats-api.mjs +15 -0
  196. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
  197. package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
  198. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
  199. package/src/standalone/daemon-boot-coordinator.mjs +10 -0
  200. package/src/standalone/daemon-drain-policy.mjs +9 -0
  201. package/src/standalone/daemon-stored-session-views.mjs +7 -8
  202. package/src/standalone/daemon.mjs +21 -5
  203. package/src/standalone/local-session-runtime.mjs +11 -0
  204. package/src/standalone/session-client.mjs +52 -7
  205. package/src/standalone/session-protocol.mjs +8 -0
  206. package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
  207. package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
  208. package/src/standalone/session-service/entries/session-loads.mjs +9 -0
  209. package/src/standalone/session-service/projection/revisions.mjs +28 -4
  210. package/src/standalone/session-service/projection.mjs +1 -0
  211. package/src/standalone/session-service/quota-resume.mjs +140 -0
  212. package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
  213. package/src/standalone/session-service/session-calls.mjs +10 -2
  214. package/src/standalone/session-service/stored-reader.mjs +26 -1
  215. package/src/standalone/session-service.mjs +21 -0
  216. package/src/standalone/session-transport/routes.mjs +2 -0
  217. package/src/tui/app/doctor.mjs +7 -11
  218. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
  219. package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
  220. package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
  221. package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
  222. package/src/tui/components/ContextPanel.jsx +5 -1
  223. package/src/tui/components/Spinner.jsx +34 -319
  224. package/src/tui/components/SpinnerByline.jsx +256 -0
  225. package/src/tui/components/use-spinner-progress.mjs +113 -0
  226. package/src/tui/dist/index.mjs +15540 -14452
  227. package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
  228. package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
  229. package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
  230. package/src/tui/session/agent-job-feed.mjs +4 -0
  231. package/src/tui/session/context-state.mjs +13 -1
  232. package/src/tui/session/initial-state.mjs +6 -0
  233. package/src/tui/session/quota-resume.mjs +263 -0
  234. package/src/tui/session/session-api/intake/abort.mjs +39 -35
  235. package/src/tui/session/session-api/intake/submission.mjs +13 -21
  236. package/src/tui/session/session-api/intake.mjs +3 -3
  237. package/src/tui/session/session-api/integrations.mjs +5 -1
  238. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  239. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
  240. package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
  241. package/src/tui/session/session-api/lifecycle.mjs +71 -2
  242. package/src/tui/session/session-api/media.mjs +2 -2
  243. package/src/tui/session/session-api/quota-resume.mjs +21 -0
  244. package/src/tui/session/session-api/routes.mjs +2 -0
  245. package/src/tui/session/session-api/settings.mjs +1 -0
  246. package/src/tui/session/session-api.mjs +2 -0
  247. package/src/tui/session/session-flow/auto-clear.mjs +14 -6
  248. package/src/tui/session/session-flow/drain.mjs +52 -5
  249. package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
  250. package/src/tui/session/session-flow/queue.mjs +3 -0
  251. package/src/tui/session/session-flow/steering.mjs +11 -0
  252. package/src/tui/session/session-flow.mjs +1 -0
  253. package/src/tui/session/tool-approval.mjs +7 -2
  254. package/src/tui/session/transcript-intake.mjs +1 -0
  255. package/src/tui/session/turn.mjs +40 -3
  256. package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
@@ -41,6 +41,7 @@ export async function consumeCompatChatCompletionStream(
41
41
  parseToolCalls,
42
42
  knownToolNames,
43
43
  semanticIdleTimeoutMs,
44
+ firstByteTimeoutMs,
44
45
  } = {}
45
46
  ) {
46
47
  const idleOverrideEnabled = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0;
@@ -57,7 +58,11 @@ export async function consumeCompatChatCompletionStream(
57
58
  reportTransport(state);
58
59
  const iterator = stream[Symbol.asyncIterator]();
59
60
  let iteratorDone = false;
60
- const firstByteTimeout = createTimeoutSignal(signal, PROVIDER_FIRST_BYTE_TIMEOUT_MS, `${label} first byte`);
61
+ const firstByteTimeout = createTimeoutSignal(
62
+ signal,
63
+ Number(firstByteTimeoutMs) > 0 ? Number(firstByteTimeoutMs) : PROVIDER_FIRST_BYTE_TIMEOUT_MS,
64
+ `${label} first byte`
65
+ );
61
66
  try {
62
67
  while (true) {
63
68
  const { value: chunk, done } = await nextAsyncWithWatchdog(iterator, {
@@ -7,13 +7,15 @@ const MODEL_LIST_TIMEOUT_MS = resolveTimeoutMs('MIXDOG_COMPAT_MODEL_LIST_TIMEOUT
7
7
  maxMs: PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
8
8
  });
9
9
 
10
- export async function fetchCompatModelItems(provider) {
11
- const timeout = createTimeoutSignal(null, MODEL_LIST_TIMEOUT_MS, `${provider.name} model list`);
10
+ // `keyless` omits the Authorization header entirely (keyless endpoints);
11
+ // `signal` aborts the request together with the list timeout.
12
+ export async function fetchCompatModelItems(provider, { signal = null, keyless = false } = {}) {
13
+ const timeout = createTimeoutSignal(signal, MODEL_LIST_TIMEOUT_MS, `${provider.name} model list`);
12
14
  try {
13
15
  const res = await fetch(`${String(provider.baseURL || '').replace(/\/+$/, '')}/models`, {
14
16
  method: 'GET',
15
17
  headers: {
16
- Authorization: `Bearer ${provider.apiKey || 'no-key'}`,
18
+ ...(keyless ? {} : { Authorization: `Bearer ${provider.apiKey || 'no-key'}` }),
17
19
  ...(provider.defaultHeaders || {}),
18
20
  },
19
21
  signal: timeout.signal,
@@ -28,9 +30,9 @@ export async function fetchCompatModelItems(provider) {
28
30
  }
29
31
  }
30
32
 
31
- export async function listCompatModels(provider, { throwOnError = false } = {}) {
33
+ export async function listCompatModels(provider, { throwOnError = false, signal = null } = {}) {
32
34
  try {
33
- const list = await provider._fetchModelItems();
35
+ const list = await provider._fetchModelItems({ signal });
34
36
  const models = [];
35
37
  for (const m of list) {
36
38
  const contextWindow = Number(
@@ -3,6 +3,7 @@ import {
3
3
  xaiModelSupportsReasoningEffort,
4
4
  normalizeOpencodeGoReasoningEffort,
5
5
  } from './openai-compat-xai.mjs';
6
+ import { isCustomProviderName, resolveCustomReasoning } from './custom-reasoning.mjs';
6
7
 
7
8
  function normalizeReasoningEffort(value, allowed) {
8
9
  const effort = String(value ?? '')
@@ -15,6 +16,17 @@ function normalizeReasoningEffort(value, allowed) {
15
16
  // backend are frequently rejected by another even when the core Chat schema
16
17
  // is shared.
17
18
  export function applyCompatProviderChatOptions(params, providerName, opts = {}, config = {}, modelInfo = null) {
19
+ if (isCustomProviderName(providerName)) {
20
+ // User-defined provider: the model row's reasoning settings decide the
21
+ // level and its wire shape (custom-reasoning.mjs).
22
+ const { fields } = resolveCustomReasoning({
23
+ protocol: 'openai-chat',
24
+ model: modelInfo,
25
+ effort: opts.effort ?? config?.reasoningEffort,
26
+ });
27
+ Object.assign(params, fields);
28
+ return params;
29
+ }
18
30
  if (providerName === 'openrouter') {
19
31
  const effort = normalizeReasoningEffort(opts.openRouterReasoningEffort ?? opts.effort ?? config?.reasoningEffort, [
20
32
  'none',
@@ -19,9 +19,11 @@ import {
19
19
  responseOutputText,
20
20
  collectCompatResponseSearchSources,
21
21
  toXaiResponsesInput,
22
+ xaiSystemInstructions,
22
23
  } from './openai-compat-wire.mjs';
23
24
  import { preparePdfTextForProvider } from './media-normalization.mjs';
24
25
  import { normalizeOpencodeGoReasoningEffort } from './openai-compat-xai.mjs';
26
+ import { isCustomProviderName, resolveCustomReasoning } from './custom-reasoning.mjs';
25
27
  import { createProviderReplay } from './lib/provider-replay.mjs';
26
28
  import {
27
29
  applyCompatToolChoice,
@@ -38,11 +40,17 @@ import {
38
40
  // provider switch never replays foreign encrypted items into this gateway.
39
41
  const COMPAT_RESPONSES_STATE_KEY = 'compatResponses';
40
42
 
41
- function resolveReasoningEffort(provider, useModel, opts) {
42
- const modelInfo =
43
- (typeof provider.getCachedModelInfo === 'function' && provider.getCachedModelInfo(useModel)) ||
44
- getModelMetadataSync(useModel, provider.name);
45
- return normalizeOpencodeGoReasoningEffort(opts.effort ?? provider.config?.reasoningEffort, modelInfo);
43
+ // The request's `reasoning` field, or null. Custom providers snap onto their
44
+ // own model row (never promoting); gateway brands keep the catalog promotion.
45
+ function resolveReasoning(provider, useModel, opts) {
46
+ const effort = opts.effort ?? provider.config?.reasoningEffort;
47
+ const cached = (typeof provider.getCachedModelInfo === 'function' && provider.getCachedModelInfo(useModel)) || null;
48
+ if (isCustomProviderName(provider.name)) {
49
+ return resolveCustomReasoning({ protocol: 'openai-responses', model: cached, effort }).fields.reasoning || null;
50
+ }
51
+ const modelInfo = cached || getModelMetadataSync(useModel, provider.name);
52
+ const reasoningEffort = normalizeOpencodeGoReasoningEffort(effort, modelInfo);
53
+ return reasoningEffort ? { effort: reasoningEffort } : null;
46
54
  }
47
55
 
48
56
  /** The request body: lowered transcript plus the per-turn knobs (output cap,
@@ -63,8 +71,14 @@ function buildCompatResponsesParams({ provider, useModel, input, previousRespons
63
71
  params.parallel_tool_calls = true;
64
72
  }
65
73
  applyCompatToolChoice(params, opts);
66
- const reasoningEffort = resolveReasoningEffort(provider, useModel, opts);
67
- if (reasoningEffort) params.reasoning = { effort: reasoningEffort };
74
+ const reasoning = resolveReasoning(provider, useModel, opts);
75
+ if (reasoning) params.reasoning = reasoning;
76
+ // Opt-in per-provider final shaping (a provider passes the hook in its config).
77
+ const hook = provider.config?.responsesParamsHook;
78
+ if (typeof hook === 'function') {
79
+ const modelInfo = (typeof provider.getCachedModelInfo === 'function' && provider.getCachedModelInfo(useModel)) || null;
80
+ hook(params, { useModel, opts, modelInfo });
81
+ }
68
82
  return params;
69
83
  }
70
84
 
@@ -146,8 +160,14 @@ export async function sendCompatResponses(provider, messages, useModel, tools, o
146
160
  replayProvider,
147
161
  providerName: provider.name,
148
162
  nativePdf: provider.nativePdf,
163
+ // Opt-in: system prompts travel as top-level `instructions`, not input items.
164
+ includeSystem: provider.config?.responsesSystemAsInstructions !== true,
149
165
  });
150
166
  const params = buildCompatResponsesParams({ provider, useModel, input, previousResponseId, tools, opts });
167
+ if (provider.config?.responsesSystemAsInstructions === true) {
168
+ const instructions = xaiSystemInstructions(messages);
169
+ if (instructions) params.instructions = instructions;
170
+ }
151
171
 
152
172
  try {
153
173
  opts.onStageChange?.('requesting');
@@ -46,7 +46,13 @@ export function toOpenAIMessages(messages, providerName, options = {}) {
46
46
  // their official multi-turn shape and cache prefix stability when prior
47
47
  // assistant reasoning_content is replayed; reasoning_effort itself remains
48
48
  // caller/user-selected.
49
- const replaysReasoningContent = options.replaysReasoningContent === true || providerName === 'xai';
49
+ //
50
+ // Custom providers pass options.customReplay (custom-reasoning.mjs replay
51
+ // eligibility). When it is a boolean it alone decides; a replaying custom
52
+ // endpoint also needs a non-empty reasoning_content on every assistant
53
+ // tool-call turn, so missing/empty reasoning there is padded with " ".
54
+ const customReplay = typeof options.customReplay === 'boolean' ? options.customReplay : null;
55
+ const replaysReasoningContent = customReplay ?? (options.replaysReasoningContent === true || providerName === 'xai');
50
56
  const out = [];
51
57
  const pendingToolMedia = [];
52
58
  const openRouterReasoningDetails = (message) => {
@@ -56,7 +62,11 @@ export function toOpenAIMessages(messages, providerName, options = {}) {
56
62
  return Array.isArray(details) && details.length ? details : null;
57
63
  };
58
64
  const attachAssistantReasoning = (msg, message) => {
59
- if (replaysReasoningContent && typeof message.reasoningContent === 'string') {
65
+ if (customReplay) {
66
+ const stored = typeof message.reasoningContent === 'string' ? message.reasoningContent : '';
67
+ if (stored) msg.reasoning_content = stored;
68
+ else if (Array.isArray(msg.tool_calls) && msg.tool_calls.length) msg.reasoning_content = ' ';
69
+ } else if (replaysReasoningContent && typeof message.reasoningContent === 'string') {
60
70
  msg.reasoning_content = message.reasoningContent;
61
71
  }
62
72
  const reasoningDetails = openRouterReasoningDetails(message);
@@ -99,11 +99,16 @@ export class OpenAICompatProvider {
99
99
  return new (loadOpenAI())({
100
100
  baseURL: this.baseURL,
101
101
  apiKey,
102
- defaultHeaders: this.defaultHeaders,
102
+ // A keyless endpoint (config.keyless) gets no Authorization header: the
103
+ // SDK drops a header whose default is null.
104
+ defaultHeaders: this.config?.keyless ? { ...this.defaultHeaders, Authorization: null } : this.defaultHeaders,
103
105
  // The SDK's own retry loop (default 2) would nest underneath our
104
106
  // withRetry wrapper and multiply tail latency on a transient
105
107
  // backend. We own retry/backoff via withRetry, so disable the SDK's.
106
108
  maxRetries: 0,
109
+ // Opt-in SDK constructor overrides (e.g. an explicit null organization so
110
+ // the SDK does not inherit OPENAI_ORG_ID from the environment).
111
+ ...(this.config?.clientOptions || {}),
107
112
  // Force the shared long-keepalive undici dispatcher to be installed
108
113
  // globally (setGlobalDispatcher) so the SDK's global fetch rides a
109
114
  // warm socket pool instead of Node's short-keepalive default. The
@@ -81,7 +81,7 @@ export function createOpenAIOAuthLogin(deps) {
81
81
  return tokens;
82
82
  }
83
83
 
84
- async function beginOAuthLogin() {
84
+ async function beginOAuthLogin({ openBrowser = true } = {}) {
85
85
  const pkce = createOAuthPkce(64);
86
86
  const state = randomBytes(16).toString('hex');
87
87
  const url = new URL(AUTHORIZE_URL);
@@ -141,6 +141,7 @@ export function createOpenAIOAuthLogin(deps) {
141
141
  process.stderr.write(
142
142
  `\n[openai-oauth] Open this URL to log in to ChatGPT (OpenAI OAuth):\n${url.toString()}\n\n`
143
143
  );
144
+ if (!openBrowser) return;
144
145
  try {
145
146
  const { openInBrowser } = await import('../../../shared/open-url.mjs');
146
147
  openInBrowser(url.toString());
@@ -0,0 +1,70 @@
1
+ /**
2
+ * quota-reading.mjs — the authoritative "can this provider serve this model
3
+ * now?" answer a quota resume asks before it fires. Every account is
4
+ * re-measured with a forced usage reading (the account pool's own probe), and
5
+ * the answer is the pool's own choice over the fresh readings, so a resume
6
+ * fires exactly when a send would find a usable account.
7
+ */
8
+ import {
9
+ ACCOUNT_PROVIDERS,
10
+ chooseProviderAccount,
11
+ quotaWindowAppliesToModel,
12
+ readProviderAccountPool,
13
+ } from '../../../shared/provider-accounts.mjs';
14
+ import { fetchOAuthUsageSnapshot } from './oauth-usage.mjs';
15
+ import { getProvider } from './registry.mjs';
16
+
17
+ const forcedReading = (routeInfo, providerObj) =>
18
+ fetchOAuthUsageSnapshot(routeInfo, providerObj, () => {}, { force: true }).catch(() => null);
19
+
20
+ const positive = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0;
21
+
22
+ /** Measured evidence in this one reading that `model` can be served: an
23
+ * applicable window with a numeric meter and none of them spent, or, when
24
+ * the account pays with credits, a positive measured balance. A failed,
25
+ * unknown or balance-only reading is no evidence. */
26
+ function readingServesModel(reading, model, useCredits) {
27
+ if (!reading) return false;
28
+ const balance = reading.balance;
29
+ if (
30
+ useCredits &&
31
+ balance &&
32
+ (balance.unlimited === true || positive(balance.remainingCredits) || positive(balance.remainingUsd))
33
+ ) {
34
+ return true;
35
+ }
36
+ const measured = (reading.quotaWindows || []).filter(
37
+ (window) =>
38
+ quotaWindowAppliesToModel(window, model) && typeof window?.usedPct === 'number' && Number.isFinite(window.usedPct)
39
+ );
40
+ return measured.length > 0 && measured.every((window) => window.usedPct < 100);
41
+ }
42
+
43
+ export async function providerQuotaUsable(providerName, model = '', { provider = getProvider(providerName) } = {}) {
44
+ // Only an account-pooled provider reports exhausted quota at all.
45
+ if (!ACCOUNT_PROVIDERS.includes(providerName)) return true;
46
+ const pool = readProviderAccountPool(providerName);
47
+ const useCredits = pool.useCredits === true;
48
+ // Only this reading counts as evidence: an account whose forced reading
49
+ // failed or measured nothing for this model stays exhausted however its
50
+ // older record reads; the pool's own choice (selection, blocks) decides
51
+ // among the rest.
52
+ if (pool.accounts.length > 0) {
53
+ const readings = await Promise.all(
54
+ pool.accounts.map((row) =>
55
+ forcedReading(
56
+ { provider: providerName, accountId: row.id },
57
+ typeof provider?.forAccount === 'function' ? provider.forAccount(row.id) : provider
58
+ )
59
+ )
60
+ );
61
+ const unproven = new Set(
62
+ pool.accounts.filter((_, index) => !readingServesModel(readings[index], model, useCredits)).map((row) => row.id)
63
+ );
64
+ if (unproven.size === pool.accounts.length) return false;
65
+ return chooseProviderAccount(readProviderAccountPool(providerName), unproven, Date.now(), model) !== null;
66
+ }
67
+ // No roster: the single credential's fresh meter decides.
68
+ const reading = await forcedReading({ provider: providerName, model: '' }, provider);
69
+ return readingServesModel(reading, model, useCredits);
70
+ }
@@ -256,21 +256,50 @@ async function _initProvidersUnsynchronized(config, signal = null) {
256
256
  return { name, inst, sig };
257
257
  } catch (err) {
258
258
  const msg = err instanceof Error ? err.message : String(err);
259
- return { name, error: new Error(`[provider] Failed to init "${name}": ${msg}`) };
259
+ const error = new Error(`[provider] Failed to init "${name}": ${msg}`);
260
+ // One invalid custom provider is isolated: it is skipped (never
261
+ // partially loaded) and reported, the rest of the registry builds.
262
+ if (isCustomProviderConfig(name, cfg) && !signal?.aborted) return { name, error, isolated: true };
263
+ return { name, error };
260
264
  }
261
265
  })
262
266
  );
267
+ const nextInitErrors = new Map();
263
268
  for (const result of enabledResults) {
264
269
  throwIfAborted(signal);
265
270
  if (!result) continue;
271
+ if (result.isolated) {
272
+ nextInitErrors.set(result.name, result.error.message);
273
+ process.stderr.write(`${result.error.message} (provider skipped; fix its configuration)\n`);
274
+ continue;
275
+ }
266
276
  if (result.error) throw result.error;
267
277
  next.set(result.name, result.inst);
268
278
  if (result.sig !== null) nextSignatures.set(result.name, result.sig);
269
279
  }
280
+ _providerInitErrors = nextInitErrors;
270
281
  // Second guard: every entry was disabled. Same reasoning — keep the
271
- // existing registry rather than going dark.
282
+ // existing registry rather than going dark. Isolation still holds: a live
283
+ // instance of a provider that is now an invalid custom config is removed
284
+ // (the rest of the registry stays), and the recorded reasons are reported.
272
285
  if (next.size === 0) {
273
- throw noEnabledProvidersError('[provider] all providers disabled in config — refusing to clear registry');
286
+ let removed = false;
287
+ for (const name of nextInitErrors.keys()) {
288
+ removed = providers.delete(name) || removed;
289
+ signatures.delete(name);
290
+ }
291
+ if (removed) {
292
+ _providerCatalogRevision += 1;
293
+ _startupCatalogRefreshPromise = null;
294
+ // The live registry no longer reflects the last applied config.
295
+ _lastAppliedSig = null;
296
+ }
297
+ const reasons = [...nextInitErrors.values()];
298
+ throw noEnabledProvidersError(
299
+ `[provider] all providers disabled in config — refusing to clear registry${
300
+ reasons.length ? `; invalid custom providers skipped: ${reasons.join('; ')}` : ''
301
+ }`
302
+ );
274
303
  }
275
304
  // OAuth preservation guard. anthropic-oauth / openai-oauth are NOT stored
276
305
  // in mixdog-config.json — buildDefaultConfig injects them at runtime by
@@ -320,6 +349,12 @@ async function _initProvidersUnsynchronized(config, signal = null) {
320
349
  signatures.clear();
321
350
  for (const [k, v] of nextSignatures) signatures.set(k, v);
322
351
  }
352
+
353
+ // Custom providers skipped by the last applied init, by name → error message.
354
+ let _providerInitErrors = new Map();
355
+ export function getProviderInitErrors() {
356
+ return new Map(_providerInitErrors);
357
+ }
323
358
  // Register a lazily self-healed OAuth instance. Bumps the catalog revision and
324
359
  // drops the settled startup-refresh epoch so the newcomer's catalog is fetched
325
360
  // instead of inheriting a refresh that finished before it existed.
@@ -21,6 +21,8 @@ import { readBridgeDiscovery } from '../../../bridge-discovery.mjs';
21
21
  import { HEADLESS_MODEL_TOOL_NAMES, HEADLESS_TOOL_PROFILE, normalizeToolProfile } from './tool-profile.mjs';
22
22
  import { DEFERRED_DEFAULT_LEAD_TOOLS } from './tool-catalog-data.mjs';
23
23
  import { gitExecutablePresent } from '../../../shared/path-executable.mjs';
24
+ import { onnxRuntimeSupported } from '../../../shared/onnx-runtime-support.mjs';
25
+ import { setBuiltinInstalledInConfig, withGrandfatheredBuiltins } from '../../../shared/builtin-grandfathering.mjs';
24
26
 
25
27
  // Browser Use / Computer Use have no install marker: the desktop app publishes
26
28
  // a loopback bridge discovery file while the feature is on. The same file
@@ -37,8 +39,14 @@ function bridgePresent(file) {
37
39
  }
38
40
  }
39
41
 
40
- export const INSTALLABLE_BUILTIN_IDS = Object.freeze(['git', 'memory', 'office', 'tidy', 'localProvider', 'autoEffort']);
41
- const GRANDFATHERED_BUILTIN_IDS = Object.freeze(['git', 'memory', 'office']);
42
+ export const INSTALLABLE_BUILTIN_IDS = Object.freeze([
43
+ 'git',
44
+ 'memory',
45
+ 'office',
46
+ 'tidy',
47
+ 'localProvider',
48
+ 'autoEffort',
49
+ ]);
42
50
 
43
51
  /** Model-facing activation for one gated feature: an explicit MIXDOG_FEATURE_*
44
52
  * env override (headless/bench) wins, otherwise the install marker and the
@@ -85,6 +93,7 @@ export function builtinFeatureActive(configLike, id) {
85
93
  // the judge model downloads at boot, and until it is on disk every call keeps
86
94
  // the user's effort.
87
95
  if (id === 'autoEffort') {
96
+ if (!onnxRuntimeSupported()) return false;
88
97
  return featureEnvOverride('MIXDOG_FEATURE_AUTO_EFFORT') ?? moduleEnabled(configLike, 'autoEffort', true);
89
98
  }
90
99
  // Media Studio is a hidden built-in like setup: no Settings card, no install
@@ -117,7 +126,13 @@ export function builtinFeatureActive(configLike, id) {
117
126
  * overrides), which the caller passes in. */
118
127
  export function featureDisallowedToolsFor(
119
128
  configLike,
120
- { browserAvailable = false, computerAvailable = false, terminalAvailable = false, toolProfile = 'interactive', gitAvailable } = {}
129
+ {
130
+ browserAvailable = false,
131
+ computerAvailable = false,
132
+ terminalAvailable = false,
133
+ toolProfile = 'interactive',
134
+ gitAvailable,
135
+ } = {}
121
136
  ) {
122
137
  const browser = featureEnvOverride('MIXDOG_FEATURE_BROWSER') ?? browserAvailable === true;
123
138
  const computer = featureEnvOverride('MIXDOG_FEATURE_COMPUTER') ?? computerAvailable === true;
@@ -162,35 +177,4 @@ export function localGitToolsActive(configLike, { gitAvailable } = {}) {
162
177
  );
163
178
  }
164
179
 
165
- export function setBuiltinInstalledInConfig(configLike, id, installed = true) {
166
- const next = { ...(configLike || {}) };
167
- next.builtins = { ...(next.builtins || {}) };
168
- if (installed === true) {
169
- next.builtins[id] = { ...(next.builtins[id] || {}), installed: true };
170
- } else {
171
- const entry = { ...(next.builtins[id] || {}) };
172
- delete entry.installed;
173
- next.builtins[id] = entry;
174
- }
175
- return next;
176
- }
177
-
178
- /** Stamp or grandfather the `builtins` section on config adoption. Returns the
179
- * same object when the section already exists, so callers can use identity to
180
- * decide whether a persist is needed. */
181
- export function withGrandfatheredBuiltins(configLike) {
182
- const config = configLike && typeof configLike === 'object' ? configLike : {};
183
- if (config.builtins && typeof config.builtins === 'object') return config;
184
- let next = { ...config, builtins: {} };
185
- // User-mark keys only: a default in-memory config may carry harmless
186
- // structural keys before onboarding ever writes, and must stay "fresh".
187
- const existingProfile = ['presets', 'providers', 'modules', 'default', 'memoryTools', 'recap'].some(
188
- (key) => config[key] !== undefined
189
- );
190
- if (existingProfile) {
191
- for (const id of GRANDFATHERED_BUILTIN_IDS) {
192
- next = setBuiltinInstalledInConfig(next, id, true);
193
- }
194
- }
195
- return next;
196
- }
180
+ export { setBuiltinInstalledInConfig, withGrandfatheredBuiltins };
@@ -1,5 +1,6 @@
1
1
  // Reasoning-effort catalogs and coercion. Pure helpers.
2
2
  import { clean } from './session-text.mjs';
3
+ import { customEffortLevels, snapCustomEffort } from '../providers/custom-reasoning.mjs';
3
4
 
4
5
  const TOOL_MODES = new Set(['full', 'readonly', 'lead']);
5
6
  const ALL_EFFORT_LEVELS = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
@@ -71,6 +72,9 @@ export function normalizeEffortInput(value) {
71
72
  }
72
73
 
73
74
  export function effortOptionsFor(provider, model) {
75
+ // Custom-provider rows declare their levels authoritatively: an explicit []
76
+ // (or no declaration) means none, with no family/provider fallback.
77
+ if (model?.customReasoning) return customEffortLevels(model) || [];
74
78
  const providerAllowed = EFFORT_OPTIONS_BY_PROVIDER[provider] || null;
75
79
  const normalizeCatalogValues = (values) => {
76
80
  const seen = new Set();
@@ -106,6 +110,7 @@ export function effortOptionsFor(provider, model) {
106
110
 
107
111
  export function coerceEffortFor(provider, model, effort) {
108
112
  if (!effort) return null;
113
+ if (model?.customReasoning) return snapCustomEffort(customEffortLevels(model), effort);
109
114
  const allowed = effortOptionsFor(provider, model);
110
115
  if (!allowed || allowed.length === 0) return null;
111
116
  if (allowed.includes(effort)) return effort;
@@ -117,19 +117,6 @@ function assertCompletionEvidence(goal, at) {
117
117
  }
118
118
  }
119
119
 
120
- // Three consecutive turns naming the same blocker confirm it; true once the
121
- // audit reaches that count.
122
- function recordBlockAudit(goal, blocker) {
123
- const turn = goal.turnCount;
124
- const previous = goal.blockAudit;
125
- const sameBlocker = previous?.reason === blocker;
126
- let count = 1;
127
- if (sameBlocker && previous.turn === turn) count = previous.count;
128
- else if (sameBlocker && previous.turn === turn - 1) count = previous.count + 1;
129
- goal.blockAudit = { reason: blocker, turn, count };
130
- return count >= 3;
131
- }
132
-
133
120
  export function createGoalMutations(ctx) {
134
121
  const { root, now, defaultTimeLimitMs, turnGoalIds, turnStartedAt } = ctx;
135
122
 
@@ -201,10 +188,6 @@ export function createGoalMutations(ctx) {
201
188
  clearTurnFailures(goal);
202
189
  } else {
203
190
  const blocker = validateGoalBlocker(args.blocker);
204
- if (!recordBlockAudit(goal, blocker)) {
205
- goal.updatedAt = at;
206
- return ctx.commit(id, goal);
207
- }
208
191
  settleActiveClock(id, goal, at);
209
192
  goal.status = 'blocked';
210
193
  goal.blocker = blocker;
@@ -149,7 +149,6 @@ export function normalizeStoredGoal(value, sessionId, resumedAt = Date.now()) {
149
149
  tasks: normalizeGoalTasks(value.tasks ?? value.criteria ?? []),
150
150
  blocker: clean(value.blocker),
151
151
  pauseReason: ['waiting', 'cancelled'].includes(value.pauseReason) ? value.pauseReason : 'user',
152
- blockAudit: value.blockAudit && typeof value.blockAudit === 'object' ? value.blockAudit : null,
153
152
  failureReason: clean(value.failureReason),
154
153
  failureCount: Math.max(0, Math.floor(Number(value.failureCount) || 0)),
155
154
  // Observations report progress; they never decide when work is complete.
@@ -207,7 +206,6 @@ export function publicGoal(goal, now = Date.now()) {
207
206
  ...goalTaskProgress(tasks),
208
207
  blocker: goal.blocker || '',
209
208
  pauseReason: goal.pauseReason || 'user',
210
- blockAudit: goal.blockAudit || null,
211
209
  timeLimitMs: goal.timeLimitMs,
212
210
  timeMode: goal.timeMode,
213
211
  timeUsedMs,
@@ -288,7 +286,6 @@ export function activateGoal(goal, now = Date.now()) {
288
286
  goal.completedAt = null;
289
287
  goal.archivedAt = null;
290
288
  goal.blocker = '';
291
- goal.blockAudit = null;
292
289
  // A running Goal has no pause to explain. Leaving the previous reason behind
293
290
  // left active records reading as a wait that had already ended.
294
291
  goal.pauseReason = 'user';
@@ -27,7 +27,6 @@ const REVISION_STATE_FIELDS = [
27
27
  'tasksObjectiveRevision',
28
28
  'timeMode',
29
29
  'pauseReason',
30
- 'blockAudit',
31
30
  ];
32
31
 
33
32
  function revisionStateKey(goal) {
@@ -137,7 +137,7 @@ export function continuationPrompt(goal, { idleReview = false, includeRules = tr
137
137
  '- Before completing, audit each user condition on its own: name the evidence that would prove it, inspect current state for it, and match the check to the claim. The audit must prove completion, not merely fail to find remaining work.',
138
138
  '- Missing or insufficient evidence means incomplete; keep working. Complete only when every user condition is proven met and no required work remains. Existing checks need not be repeated and verification need not be a separate task row.',
139
139
  '- Only the user retires a condition: drop a task because the user changed the objective, never to reach completion — a task dropped this turn blocks completion.',
140
- '- Report an impasse that only external state or the user can clear with block once per turn using the same stable blocker description. The runtime keeps the Goal active until 3 consecutive turns confirm it. Treat equivalent blockers as the same condition even when their wording or stated next step changes. Never block for difficulty, uncertainty, merely incomplete work, a choice you can make, confirmation for in-scope work, or clarification that would only help.',
140
+ '- block stops the Goal immediately. Do not block the first time a blocker appears: block only after the same blocking condition has repeated for at least 3 consecutive Goal turns, counting the user-triggered turn and automatic continuations; after a resume, count only turns since that resume. Until then, re-check the blocker against current state with a tool each turn rather than restating it. Exception: when current state verifies an impasse that only the user can clear (their action, approval, credentials, or a resource they control), block on that turn. Once the threshold is met, block instead of reporting the same blocker again. Treat equivalent blockers as the same condition even when their wording or stated next step changes. Never block for difficulty, uncertainty, merely incomplete work, a choice you can make, confirmation for in-scope work, or clarification that would only help.',
141
141
  '- Never complete or block merely because time is low or the turn is ending.',
142
142
  '</system-reminder>',
143
143
  ].join('\n');
@@ -99,7 +99,7 @@ export const GOAL_TOOL_DEFS = Object.freeze([
99
99
  minLength: 1,
100
100
  maxLength: 1_000,
101
101
  description:
102
- 'block: stable description of the same impasse only external state or the user can clear, reported once per turn; the runtime stops after 3.',
102
+ 'block: stable description of an impasse only external state or the user can clear; stops the Goal immediately.',
103
103
  },
104
104
  },
105
105
  required: ['action'],
@@ -27,7 +27,6 @@ function toolReply(ctx, id, goal, { full = false, previousIds = null } = {}) {
27
27
  timeUsedMs: goal.timeUsedMs,
28
28
  };
29
29
  if (goal.blocker) goalView.blocker = goal.blocker;
30
- if (goal.blockAudit) goalView.blockAudit = goal.blockAudit;
31
30
  if (goal.needsTaskReview) goalView.needsTaskReview = true;
32
31
  }
33
32
  const result = { goal: goalView, remaining_ms: goal?.remainingMs ?? null };
@@ -76,7 +75,7 @@ async function pauseOrResume(ctx, id, args, action) {
76
75
  throw new Error(`a ${current.status === 'complete' ? 'completed' : 'stopped'} Goal cannot be paused or resumed`);
77
76
  }
78
77
  // The model pauses only at the user's explicit request, so its pause is the
79
- // user's pause; waits on the user go through the consecutive-turn block audit.
78
+ // user's pause; waits on the user go through block.
80
79
  const change =
81
80
  action === 'resume'
82
81
  ? { updates: args.updates, tasks: args.tasks, duration: timeLimitMs, timeMode: args.time_mode }
@@ -10,6 +10,7 @@ import {
10
10
  checkpointActiveClock,
11
11
  clearTurnFailures,
12
12
  publicGoal,
13
+ resumeGoalState,
13
14
  runningAgentWork,
14
15
  startActiveClock,
15
16
  stopActiveClock,
@@ -28,8 +29,7 @@ function settledDurationWait(goal) {
28
29
  goal.remainingMs > 0 &&
29
30
  !goal.needsTaskReview &&
30
31
  goal.tasks.length > 0 &&
31
- goal.tasks.every((task) => GOAL_TASK_SETTLED.includes(task.status)) &&
32
- goal.blockAudit?.turn !== goal.turnCount
32
+ goal.tasks.every((task) => GOAL_TASK_SETTLED.includes(task.status))
33
33
  );
34
34
  }
35
35
 
@@ -89,8 +89,7 @@ function applyTurnOutcome(goal, detail, at) {
89
89
  }
90
90
  if (status === 'failed' && goal.status === 'active') {
91
91
  // A failed turn has exhausted its recovery. Starting another Goal turn
92
- // retries the same terminal error with a larger transcript. The model's
93
- // separate external-blocker audit still spans 3 turns.
92
+ // retries the same terminal error with a larger transcript.
94
93
  stopActiveClock(goal, at);
95
94
  goal.status = 'blocked';
96
95
  goal.failureReason = clean(detail?.error) || 'Goal turn failed';
@@ -169,6 +168,21 @@ export function createGoalTurnLifecycle(ctx) {
169
168
  return published;
170
169
  });
171
170
  },
171
+ // A quota resume revives exactly the Goal its failure suspended, and only
172
+ // while that Goal is still waiting on the limit: a Goal the user paused,
173
+ // stopped, replaced or completed meanwhile stays as it is.
174
+ reactivateUsageLimited(sessionId, goalId) {
175
+ return withMutation(sessionId, async (id) => {
176
+ const goal = readRecord(id).goal;
177
+ if (!goal || goal.archivedAt || goal.id !== clean(goalId) || goal.status !== 'usage_limited') return null;
178
+ try {
179
+ resumeGoalState(goal, now());
180
+ } catch {
181
+ return null;
182
+ }
183
+ return commit(id, goal);
184
+ });
185
+ },
172
186
  continuation(sessionId, { agentStatus = null } = {}) {
173
187
  const goal = visibleSnapshot(sessionId);
174
188
  if (goal?.status !== 'active') return { run: false, reason: goal?.status || 'missing', goal };
@@ -14,9 +14,15 @@ output dependency. Prefer parallel scopes over sequential slices in one agent.
14
14
  If the plan has only one scope, Lead executes it directly.`,
15
15
  };
16
16
 
17
+ const SOLO = `# Orchestration Mode: Solo
18
+
19
+ Lead executes all work directly and never delegates on its own judgment.
20
+ Only when the user explicitly asks for delegation, load the deferred \`agent\`
21
+ tool and brief each agent as the Lead rules describe.`;
22
+
17
23
  export function orchestrationInstructions(value) {
18
24
  const mode = normalizeOrchestrationMode(value);
19
- if (mode === 'none') return '';
25
+ if (mode === 'none') return SOLO;
20
26
  return `# Orchestration Mode: ${mode[0].toUpperCase()}${mode.slice(1)}
21
27
 
22
28
  ${BATCHING[mode]}