muonroi-cli 1.8.5 → 1.9.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 (206) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
  3. package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
  4. package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
  7. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  8. package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
  9. package/dist/src/agent-harness/mock-model.d.ts +10 -0
  10. package/dist/src/agent-harness/mock-model.js +6 -2
  11. package/dist/src/chat/chat-keychain.d.ts +7 -12
  12. package/dist/src/chat/chat-keychain.js +19 -86
  13. package/dist/src/cli/keys.d.ts +8 -45
  14. package/dist/src/cli/keys.js +18 -324
  15. package/dist/src/council/clarifier.d.ts +4 -2
  16. package/dist/src/council/clarifier.js +158 -36
  17. package/dist/src/council/debate-planner.js +3 -2
  18. package/dist/src/council/debate.js +59 -5
  19. package/dist/src/council/index.d.ts +23 -0
  20. package/dist/src/council/index.js +37 -2
  21. package/dist/src/council/llm.d.ts +62 -0
  22. package/dist/src/council/llm.js +123 -23
  23. package/dist/src/council/panel-select.js +13 -3
  24. package/dist/src/council/planner.js +17 -0
  25. package/dist/src/council/preflight.d.ts +10 -0
  26. package/dist/src/council/preflight.js +36 -0
  27. package/dist/src/council/prompts.d.ts +9 -1
  28. package/dist/src/council/prompts.js +23 -6
  29. package/dist/src/council/types.d.ts +10 -0
  30. package/dist/src/ee/auth.d.ts +19 -0
  31. package/dist/src/ee/auth.js +39 -0
  32. package/dist/src/ee/client.js +28 -3
  33. package/dist/src/ee/ee-onboarding.js +6 -26
  34. package/dist/src/flow/compaction/compress.d.ts +2 -2
  35. package/dist/src/flow/compaction/compress.js +21 -8
  36. package/dist/src/flow/compaction/extract.d.ts +3 -3
  37. package/dist/src/flow/compaction/extract.js +6 -6
  38. package/dist/src/flow/compaction/index.d.ts +2 -1
  39. package/dist/src/flow/compaction/index.js +29 -3
  40. package/dist/src/flow/compaction/progress.d.ts +35 -0
  41. package/dist/src/flow/compaction/progress.js +35 -0
  42. package/dist/src/generated/version.d.ts +1 -1
  43. package/dist/src/generated/version.js +1 -1
  44. package/dist/src/gsd/flags.d.ts +11 -0
  45. package/dist/src/gsd/flags.js +19 -0
  46. package/dist/src/gsd/plan-council.js +104 -72
  47. package/dist/src/gsd/verdict-schema.d.ts +1 -1
  48. package/dist/src/headless/council-answers.js +4 -0
  49. package/dist/src/index.js +129 -260
  50. package/dist/src/lsp/builtins.js +3 -1
  51. package/dist/src/lsp/manager.d.ts +5 -1
  52. package/dist/src/lsp/manager.js +249 -3
  53. package/dist/src/lsp/npm-cache.d.ts +11 -1
  54. package/dist/src/lsp/npm-cache.js +17 -1
  55. package/dist/src/lsp/runtime.d.ts +6 -1
  56. package/dist/src/lsp/runtime.js +17 -1
  57. package/dist/src/lsp/types.d.ts +83 -1
  58. package/dist/src/lsp/types.js +10 -0
  59. package/dist/src/mcp/client-pool.js +43 -15
  60. package/dist/src/mcp/lsp-tools.d.ts +5 -1
  61. package/dist/src/mcp/lsp-tools.js +93 -2
  62. package/dist/src/mcp/mcp-keychain.d.ts +3 -5
  63. package/dist/src/mcp/mcp-keychain.js +9 -49
  64. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  65. package/dist/src/mcp/setup-guide-text.js +22 -2
  66. package/dist/src/mcp/tools-server.d.ts +10 -0
  67. package/dist/src/mcp/tools-server.js +10 -2
  68. package/dist/src/models/catalog.json +19 -19
  69. package/dist/src/orchestrator/ask-user.d.ts +61 -0
  70. package/dist/src/orchestrator/ask-user.js +65 -0
  71. package/dist/src/orchestrator/compaction.d.ts +2 -3
  72. package/dist/src/orchestrator/compaction.js +8 -8
  73. package/dist/src/orchestrator/council-manager.js +9 -8
  74. package/dist/src/orchestrator/council-request.d.ts +49 -0
  75. package/dist/src/orchestrator/council-request.js +62 -0
  76. package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
  77. package/dist/src/orchestrator/interactive-pause.js +36 -0
  78. package/dist/src/orchestrator/message-processor.d.ts +4 -0
  79. package/dist/src/orchestrator/message-processor.js +26 -8
  80. package/dist/src/orchestrator/orchestrator.d.ts +25 -0
  81. package/dist/src/orchestrator/orchestrator.js +204 -50
  82. package/dist/src/orchestrator/preprocessor.js +2 -2
  83. package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
  84. package/dist/src/orchestrator/safety-askcard.js +5 -2
  85. package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
  86. package/dist/src/orchestrator/safety-intercept.js +7 -0
  87. package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
  88. package/dist/src/orchestrator/stall-watchdog.js +24 -3
  89. package/dist/src/orchestrator/stream-runner.d.ts +13 -3
  90. package/dist/src/orchestrator/stream-runner.js +54 -21
  91. package/dist/src/orchestrator/tool-engine.d.ts +19 -0
  92. package/dist/src/orchestrator/tool-engine.js +241 -25
  93. package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
  94. package/dist/src/orchestrator/turn-watchdog.js +38 -9
  95. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  96. package/dist/src/pil/agent-operating-contract.js +6 -4
  97. package/dist/src/pil/discovery.d.ts +1 -1
  98. package/dist/src/pil/discovery.js +2 -2
  99. package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
  100. package/dist/src/pil/layer1_5-complexity-size.js +31 -5
  101. package/dist/src/pil/llm-classify.d.ts +78 -3
  102. package/dist/src/pil/llm-classify.js +351 -111
  103. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  104. package/dist/src/pil/native-capabilities-workbook.js +7 -0
  105. package/dist/src/pil/pipeline.js +2 -0
  106. package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
  107. package/dist/src/pil/repo-grounding-probe.js +136 -0
  108. package/dist/src/pil/repo-structure-hints.d.ts +7 -0
  109. package/dist/src/pil/repo-structure-hints.js +45 -0
  110. package/dist/src/product-loop/artifact-io.js +4 -0
  111. package/dist/src/product-loop/criteria-seed.d.ts +51 -0
  112. package/dist/src/product-loop/criteria-seed.js +200 -0
  113. package/dist/src/product-loop/discovery-interview.d.ts +9 -0
  114. package/dist/src/product-loop/discovery-interview.js +37 -18
  115. package/dist/src/product-loop/discovery-recommender.js +2 -1
  116. package/dist/src/product-loop/discovery-schema.js +14 -1
  117. package/dist/src/product-loop/discovery-triage.d.ts +23 -0
  118. package/dist/src/product-loop/discovery-triage.js +109 -0
  119. package/dist/src/product-loop/gather.js +150 -2
  120. package/dist/src/product-loop/index.js +7 -0
  121. package/dist/src/product-loop/loop-driver.js +21 -8
  122. package/dist/src/product-loop/phase-plan.d.ts +16 -0
  123. package/dist/src/product-loop/phase-plan.js +42 -4
  124. package/dist/src/product-loop/phase-rituals.d.ts +3 -0
  125. package/dist/src/product-loop/phase-rituals.js +8 -3
  126. package/dist/src/product-loop/phase-runner.js +30 -11
  127. package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
  128. package/dist/src/product-loop/plan-adherence-review.js +144 -0
  129. package/dist/src/product-loop/sprint-runner.d.ts +62 -0
  130. package/dist/src/product-loop/sprint-runner.js +309 -8
  131. package/dist/src/product-loop/types.d.ts +25 -0
  132. package/dist/src/providers/anthropic.d.ts +9 -8
  133. package/dist/src/providers/anthropic.js +13 -47
  134. package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
  135. package/dist/src/providers/auth/grok-oauth.js +30 -5
  136. package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
  137. package/dist/src/providers/auth/openai-oauth.js +14 -0
  138. package/dist/src/providers/auth/token-store.d.ts +9 -9
  139. package/dist/src/providers/auth/token-store.js +8 -70
  140. package/dist/src/providers/auth/types.d.ts +8 -0
  141. package/dist/src/providers/env-store.d.ts +17 -0
  142. package/dist/src/providers/env-store.js +228 -0
  143. package/dist/src/providers/keychain.d.ts +21 -17
  144. package/dist/src/providers/keychain.js +124 -135
  145. package/dist/src/providers/runtime.d.ts +24 -9
  146. package/dist/src/providers/runtime.js +48 -37
  147. package/dist/src/providers/strategies/thinking-mode.js +9 -1
  148. package/dist/src/providers/strategies/xai.strategy.js +27 -0
  149. package/dist/src/providers/warm.d.ts +65 -0
  150. package/dist/src/providers/warm.js +145 -0
  151. package/dist/src/self-qa/agentic-loop.js +3 -2
  152. package/dist/src/storage/transcript.js +56 -2
  153. package/dist/src/tools/git-safety.d.ts +19 -0
  154. package/dist/src/tools/git-safety.js +168 -0
  155. package/dist/src/tools/native-tools.d.ts +1 -1
  156. package/dist/src/tools/native-tools.js +76 -1
  157. package/dist/src/tools/registry.d.ts +17 -0
  158. package/dist/src/tools/registry.js +116 -1
  159. package/dist/src/types/index.d.ts +29 -1
  160. package/dist/src/ui/app.js +67 -4
  161. package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
  162. package/dist/src/ui/components/agent-rail-activities.js +47 -0
  163. package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
  164. package/dist/src/ui/components/compact-progress-card.js +42 -0
  165. package/dist/src/ui/components/council-phase-timeline.js +17 -2
  166. package/dist/src/ui/components/council-question-card.js +1 -0
  167. package/dist/src/ui/components/message-view.d.ts +15 -0
  168. package/dist/src/ui/components/message-view.js +50 -1
  169. package/dist/src/ui/components/tool-group.d.ts +15 -3
  170. package/dist/src/ui/components/tool-group.js +69 -11
  171. package/dist/src/ui/containers/modals-layer.d.ts +2 -1
  172. package/dist/src/ui/containers/modals-layer.js +2 -2
  173. package/dist/src/ui/council-harness-event.d.ts +57 -0
  174. package/dist/src/ui/council-harness-event.js +46 -0
  175. package/dist/src/ui/heartbeat-debug.d.ts +29 -0
  176. package/dist/src/ui/heartbeat-debug.js +45 -0
  177. package/dist/src/ui/modals/api-key-modal.js +1 -1
  178. package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
  179. package/dist/src/ui/modals/model-picker-modal.js +8 -10
  180. package/dist/src/ui/slash/ee.js +81 -0
  181. package/dist/src/ui/slash/menu-items.js +11 -2
  182. package/dist/src/ui/use-app-logic.js +354 -224
  183. package/dist/src/ui/utils/agent-activities.d.ts +39 -0
  184. package/dist/src/ui/utils/agent-activities.js +96 -0
  185. package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
  186. package/dist/src/ui/utils/group-tool-entries.js +111 -0
  187. package/dist/src/ui/utils/tool-summary.d.ts +21 -0
  188. package/dist/src/ui/utils/tool-summary.js +91 -0
  189. package/dist/src/utils/event-loop-monitor.d.ts +85 -0
  190. package/dist/src/utils/event-loop-monitor.js +107 -0
  191. package/dist/src/utils/llm-deadline.d.ts +14 -0
  192. package/dist/src/utils/llm-deadline.js +19 -0
  193. package/dist/src/utils/loop-profiler.d.ts +102 -0
  194. package/dist/src/utils/loop-profiler.js +202 -0
  195. package/dist/src/utils/settings.d.ts +27 -0
  196. package/dist/src/utils/settings.js +38 -2
  197. package/dist/src/utils/side-question.d.ts +1 -2
  198. package/dist/src/utils/side-question.js +2 -2
  199. package/dist/src/verify/entrypoint.js +51 -16
  200. package/dist/src/verify/orchestrator.d.ts +1 -1
  201. package/dist/src/verify/orchestrator.js +20 -3
  202. package/package.json +1 -2
  203. package/dist/src/cli/bw-vault.d.ts +0 -55
  204. package/dist/src/cli/bw-vault.js +0 -133
  205. package/dist/src/mcp/ee-tools.d.ts +0 -46
  206. package/dist/src/mcp/ee-tools.js +0 -194
@@ -6,14 +6,7 @@ export type ProviderFactory = ((modelId: string) => any) & {
6
6
  defaultProviderOptions?: Record<string, unknown>;
7
7
  /** AI SDK top-level call params to strip (backend doesn't accept them). */
8
8
  unsupportedParams?: ReadonlyArray<"maxOutputTokens" | "temperature" | "topP">;
9
- /**
10
- * The provider this factory talks to. Stamped by createProviderFactory so a
11
- * caller that blindly reuses one provider's factory with a model from a
12
- * DIFFERENT provider can be detected and auto-corrected in resolveModelRuntime
13
- * (see the factory registry below) — otherwise the request would hit the wrong
14
- * endpoint (e.g. a gateway-routed model POSTed straight to a native API,
15
- * bypassing the configured gateway).
16
- */
9
+ /** The provider this factory talks to. Stamped by createProviderFactory. */
17
10
  providerId?: ProviderId;
18
11
  };
19
12
  export interface ProviderFactoryResult {
@@ -30,6 +23,15 @@ export interface ResolvedModelRuntime {
30
23
  }
31
24
  /** Test seam: clear the registry between cases so entries never leak across specs. */
32
25
  export declare function __resetProviderFactoryRegistry(): void;
26
+ /**
27
+ * Whether a factory for `id` was already built this session.
28
+ *
29
+ * Used by the boot warm-up to avoid CLOBBERING a factory that was built with
30
+ * session-specific options (custom baseURL, OAuth headers) with a plainer one.
31
+ */
32
+ export declare function hasProviderFactory(id: ProviderId): boolean;
33
+ /** The providers that have a factory this session. */
34
+ export declare function registeredProviderIds(): ProviderId[];
33
35
  /**
34
36
  * Phase 12.2-G4: thin dispatcher delegating to the provider strategy registry.
35
37
  * Each provider's SDK wiring + `factory.responses` + `defaultProviderOptions`
@@ -52,7 +54,20 @@ export declare function createProviderFactoryAsync(id: ProviderId, opts: {
52
54
  apiKey?: string;
53
55
  baseURL?: string;
54
56
  }): Promise<ProviderFactoryResult>;
55
- export declare function resolveModelRuntime(factory: ProviderFactory, modelId: string): ResolvedModelRuntime;
57
+ /**
58
+ * The factory for `modelId`'s OWN provider.
59
+ *
60
+ * Deriving the factory from the model is what makes a cross-wire structurally
61
+ * impossible: callers pass only a model id, so there is no second, independent
62
+ * value that can disagree with it. Previously the factory and the model id
63
+ * travelled separately and every sub-task path (compaction, classify,
64
+ * sub-agents) silently borrowed whatever factory its parent held — measured
65
+ * live 2026-07-16 (session 0c6728ba1a25): model `gpt-5.4` (openai) went out
66
+ * through an `xai` factory, and api.x.ai answered "The model gpt-5.4 does not
67
+ * exist", sending the user hunting a model-name problem that did not exist.
68
+ */
69
+ export declare function factoryForModel(modelId: string): ProviderFactory;
70
+ export declare function resolveModelRuntime(modelId: string): ResolvedModelRuntime;
56
71
  /**
57
72
  * F1: derive a stable OpenAI prompt-cache key from the session id.
58
73
  *
@@ -1,24 +1,36 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { getModelInfo } from "../models/registry.js";
3
- import { logger } from "../utils/logger.js";
4
3
  import { getReasoningEffortForModel } from "../utils/settings.js";
5
4
  import { getProviderCapabilities } from "./capabilities.js";
6
5
  import { getProviderStrategy } from "./strategies/registry.js";
7
6
  /**
8
7
  * Session-scoped registry of the most-recently-built factory per provider.
9
- * resolveModelRuntime consults this to auto-correct a factory/model provider
10
- * mismatch: when a caller reuses provider A's factory to run a model that
11
- * belongs to provider B (a real hazard in sub-task paths like compaction that
12
- * inherit the parent session's factory), we substitute B's real factory if one
13
- * was built this session. Single-orchestrator invariant (v1, see CQ-16a) makes
14
- * a module-level map safe. Last-built wins, so a /model key change that rebuilds
15
- * a provider's factory transparently refreshes the entry.
8
+ * It is the ONLY way a model reaches a factory: `factoryForModel` derives the
9
+ * factory from the model's own provider, so provider A's factory can never be
10
+ * paired with provider B's model (a real hazard in sub-task paths like
11
+ * compaction that used to inherit the parent session's factory). Boot warms an
12
+ * entry for every credentialed provider (see ./warm.ts). Single-orchestrator
13
+ * invariant (v1, see CQ-16a) makes a module-level map safe. Last-built wins, so
14
+ * a /model key change that rebuilds a provider's factory refreshes the entry.
16
15
  */
17
16
  const providerFactoryRegistry = new Map();
18
17
  /** Test seam: clear the registry between cases so entries never leak across specs. */
19
18
  export function __resetProviderFactoryRegistry() {
20
19
  providerFactoryRegistry.clear();
21
20
  }
21
+ /**
22
+ * Whether a factory for `id` was already built this session.
23
+ *
24
+ * Used by the boot warm-up to avoid CLOBBERING a factory that was built with
25
+ * session-specific options (custom baseURL, OAuth headers) with a plainer one.
26
+ */
27
+ export function hasProviderFactory(id) {
28
+ return providerFactoryRegistry.has(id);
29
+ }
30
+ /** The providers that have a factory this session. */
31
+ export function registeredProviderIds() {
32
+ return [...providerFactoryRegistry.keys()];
33
+ }
22
34
  /**
23
35
  * Phase 12.2-G4: thin dispatcher delegating to the provider strategy registry.
24
36
  * Each provider's SDK wiring + `factory.responses` + `defaultProviderOptions`
@@ -82,7 +94,31 @@ export async function createProviderFactoryAsync(id, opts) {
82
94
  }
83
95
  return createProviderFactory(id, opts);
84
96
  }
85
- export function resolveModelRuntime(factory, modelId) {
97
+ /**
98
+ * The factory for `modelId`'s OWN provider.
99
+ *
100
+ * Deriving the factory from the model is what makes a cross-wire structurally
101
+ * impossible: callers pass only a model id, so there is no second, independent
102
+ * value that can disagree with it. Previously the factory and the model id
103
+ * travelled separately and every sub-task path (compaction, classify,
104
+ * sub-agents) silently borrowed whatever factory its parent held — measured
105
+ * live 2026-07-16 (session 0c6728ba1a25): model `gpt-5.4` (openai) went out
106
+ * through an `xai` factory, and api.x.ai answered "The model gpt-5.4 does not
107
+ * exist", sending the user hunting a model-name problem that did not exist.
108
+ */
109
+ export function factoryForModel(modelId) {
110
+ const providerId = getModelInfo(modelId)?.provider;
111
+ if (!providerId) {
112
+ throw new Error(`Model "${modelId}" not found in catalog — cannot determine provider.`);
113
+ }
114
+ const factory = providerFactoryRegistry.get(providerId);
115
+ if (!factory) {
116
+ throw new Error(`No provider factory for "${providerId}" (model "${modelId}") — that provider is not authenticated this session. ` +
117
+ `Run /login for it, or pick a model from an authenticated provider.`);
118
+ }
119
+ return factory;
120
+ }
121
+ export function resolveModelRuntime(modelId) {
86
122
  // Resolve aliases (e.g. "deepseek-v4-flash") to the provider-native id
87
123
  // (e.g. "deepseek-v4-flash") BEFORE invoking the factory.
88
124
  // Without this, DeepSeek / xAI reject the request because
@@ -94,32 +130,6 @@ export function resolveModelRuntime(factory, modelId) {
94
130
  if (!providerId && !mockGlobals.__muonroiMockModel) {
95
131
  throw new Error(`Model "${modelId}" not found in catalog — cannot determine provider.`);
96
132
  }
97
- // Factory/model provider guard (Layer 2). A caller that reuses one provider's
98
- // factory to run a model from another provider (e.g. compaction inheriting the
99
- // parent session's native factory for a gateway-routed model) would otherwise
100
- // POST to the wrong endpoint. If the passed factory is stamped for a different
101
- // provider AND we built the correct one this session, substitute it so the
102
- // request lands on the right backend; otherwise keep going (Part 3's wire-id
103
- // normalization keeps it valid) but log the mismatch so it is never silent.
104
- let effectiveFactory = factory;
105
- if (providerId && factory.providerId && factory.providerId !== providerId) {
106
- const correct = providerFactoryRegistry.get(providerId);
107
- if (correct) {
108
- effectiveFactory = correct;
109
- logger.debug("orchestrator", "Redirected model to its own provider factory", {
110
- modelId: canonicalId,
111
- modelProvider: providerId,
112
- factoryProvider: factory.providerId,
113
- });
114
- }
115
- else {
116
- logger.warn("orchestrator", "Factory/model provider mismatch; no factory for model's provider", {
117
- modelId: canonicalId,
118
- modelProvider: providerId,
119
- factoryProvider: factory.providerId,
120
- });
121
- }
122
- }
123
133
  const userEffort = getReasoningEffortForModel(modelId);
124
134
  const mockModel = mockGlobals.__muonroiMockModel;
125
135
  // Determine the language model + unsupportedParams + provider-level defaults.
@@ -147,14 +157,15 @@ export function resolveModelRuntime(factory, modelId) {
147
157
  providerLevelDefaults = mockGlobals.__muonroiMockDefaultProviderOptions;
148
158
  }
149
159
  else {
160
+ const factory = factoryForModel(canonicalId);
150
161
  const strategy = getProviderStrategy(providerId);
151
162
  resolved = strategy.resolve({
152
- factory: effectiveFactory,
163
+ factory,
153
164
  modelId: canonicalId,
154
165
  modelInfo,
155
166
  reasoningEffort: userEffort,
156
167
  });
157
- providerLevelDefaults = effectiveFactory.defaultProviderOptions;
168
+ providerLevelDefaults = factory.defaultProviderOptions;
158
169
  }
159
170
  // Merge provider-level defaults from the factory (e.g. OAuth backends inject
160
171
  // `instructions` + `store: false` for the ChatGPT Codex API). This keeps
@@ -242,7 +242,15 @@ export function sanitizeToolCallArguments(messages) {
242
242
  }
243
243
  else {
244
244
  try {
245
- JSON.parse(trimmed);
245
+ const parsed = JSON.parse(trimmed);
246
+ // Tool arguments MUST be a JSON object. grok-composer (and some other
247
+ // models) occasionally emit a bare string / number / array / null —
248
+ // valid JSON but the wrong shape — which xAI rejects with 400
249
+ // "expected JSON object for tool arguments" (a non-transient error
250
+ // that wedges the retry loop). Normalize any non-object to {}.
251
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
252
+ repaired = "{}";
253
+ }
246
254
  }
247
255
  catch {
248
256
  repaired = "{}";
@@ -7,6 +7,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
7
7
  import { getProviderCapabilities } from "../capabilities.js";
8
8
  import { OPENAI_COMPATIBLE_BASE_URLS } from "../endpoints.js";
9
9
  import { BaseProviderStrategy } from "./base.strategy.js";
10
+ import { sanitizeToolCallArguments, splitParallelToolCalls } from "./thinking-mode.js";
10
11
  export class XAIStrategy extends BaseProviderStrategy {
11
12
  id = "xai";
12
13
  capabilities = getProviderCapabilities("xai");
@@ -22,6 +23,32 @@ export class XAIStrategy extends BaseProviderStrategy {
22
23
  baseURL: opts.baseURL ?? OPENAI_COMPATIBLE_BASE_URLS.xai,
23
24
  apiKey: opts.apiKey ?? (opts.headers ? "oauth" : undefined),
24
25
  ...(opts.headers ? { headers: opts.headers } : {}),
26
+ // grok-composer (Cursor Composer via xAI) emits BATCHES of parallel
27
+ // tool_calls per assistant turn, and some carry empty/truncated
28
+ // `arguments` strings. On the next multi-step request xAI rejects the
29
+ // replayed history with 400 `invalid-argument: expected JSON object for
30
+ // tool arguments` — a NON-transient error, so the bounded stream-retry
31
+ // re-sends the same malformed history and the turn wedges (observed live
32
+ // 2026-07-14, run mrkoeezk9a29: /ideal sprint impl made one tool-call
33
+ // response then never progressed, 0 files, across every path). Mirror the
34
+ // opencode-go/zai fix: split multi-tool-call assistant turns into
35
+ // sequential single-call turns and repair empty/truncated tool arguments
36
+ // to `{}` before they reach the upstream. Both helpers are no-ops unless
37
+ // the failing pattern is present, so healthy requests are untouched.
38
+ transformRequestBody: (body) => {
39
+ const out = { ...body };
40
+ if (Array.isArray(out.messages)) {
41
+ out.messages = splitParallelToolCalls(out.messages);
42
+ out.messages = sanitizeToolCallArguments(out.messages);
43
+ }
44
+ if ("response_format" in out) {
45
+ const rf = out.response_format;
46
+ if (rf == null || (typeof rf === "object" && Object.keys(rf).length === 0)) {
47
+ delete out.response_format;
48
+ }
49
+ }
50
+ return out;
51
+ },
25
52
  });
26
53
  return (modelId) => p(modelId);
27
54
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * src/providers/warm.ts
3
+ *
4
+ * Boot-time warm-up of the provider factory registry.
5
+ *
6
+ * Root cause it addresses (measured live, 2026-07-16, session 0c6728ba1a25):
7
+ * an xai session POSTed model `gpt-5.4` to api.x.ai and got 404 "The model
8
+ * gpt-5.4 does not exist" — an openai model sent to the xai endpoint. The
9
+ * factory/model guard in `runtime.ts` DID spot the mismatch and tried to
10
+ * redirect the request to openai's own factory, but the registry had no openai
11
+ * entry: a session only ever built the factory for ITS OWN provider, so any
12
+ * sub-task that resolved a model from another provider had nothing to redirect
13
+ * to and fell through to the wrong endpoint.
14
+ *
15
+ * That is the actual hole. Model ids and provider factories travel as two
16
+ * independent values, so "borrow the session's factory" is the silent default
17
+ * for every sub-task (compaction, classify, sub-agents, mode switches) and only
18
+ * some paths remember to re-derive the provider. Warming the registry for every
19
+ * credentialed provider closes it at the source: the factory for a model's own
20
+ * provider is then always present, so `resolveModelRuntime` can derive it from
21
+ * the model itself instead of trusting whatever factory a caller passed.
22
+ *
23
+ * Best-effort by construction: a provider that fails to warm is simply absent
24
+ * (the same state as today), never a boot failure.
25
+ */
26
+ import { type ProviderId } from "./types.js";
27
+ /** Outcome of {@link warmProviderFactories}, for logging and tests. */
28
+ export interface WarmResult {
29
+ /** Providers whose factory is now registered. */
30
+ warmed: ProviderId[];
31
+ /** Providers left out, with the reason (no credentials, disabled, or an error). */
32
+ skipped: Array<{
33
+ id: ProviderId;
34
+ reason: string;
35
+ }>;
36
+ }
37
+ /**
38
+ * Build and register a factory for every provider the user has credentials for.
39
+ *
40
+ * Runs providers concurrently — each is an independent credential read, and a
41
+ * serial loop would add every provider's latency to boot.
42
+ *
43
+ * Skips providers that already have a factory: the session builds its own with
44
+ * session-specific options (custom baseURL, OAuth headers), and rebuilding it
45
+ * from bare defaults here would silently downgrade the live session's wiring.
46
+ */
47
+ /**
48
+ * Rebuild ONE provider's factory after its credentials changed mid-session.
49
+ *
50
+ * `createProviderFactoryAsync` reads OAuth tokens at CONSTRUCTION and bakes the
51
+ * resulting auth headers into the factory (runtime.ts: `loadTokensWithRefresh()`
52
+ * then `authHeaders(tokens)`). Boot warms openai/xai even with no tokens —
53
+ * `getOAuthProviderConfig` only reports that a provider SUPPORTS OAuth — so the
54
+ * entry built at boot is an API-key-path factory. Signing in later saved tokens
55
+ * that nothing re-read: the stale factory stayed registered and the provider
56
+ * kept failing until the next process start. That is the "OAuth only works
57
+ * after I restart the session" report; the same held for a key set via `K` on a
58
+ * provider skipped at boot for "no credentials".
59
+ *
60
+ * warmProviderFactories() cannot be reused for this: it deliberately SKIPS a
61
+ * provider that already has a factory. The registry is last-built-wins, so
62
+ * building again is what replaces the stale entry.
63
+ */
64
+ export declare function rewarmProviderFactory(id: ProviderId): Promise<boolean>;
65
+ export declare function warmProviderFactories(): Promise<WarmResult>;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * src/providers/warm.ts
3
+ *
4
+ * Boot-time warm-up of the provider factory registry.
5
+ *
6
+ * Root cause it addresses (measured live, 2026-07-16, session 0c6728ba1a25):
7
+ * an xai session POSTed model `gpt-5.4` to api.x.ai and got 404 "The model
8
+ * gpt-5.4 does not exist" — an openai model sent to the xai endpoint. The
9
+ * factory/model guard in `runtime.ts` DID spot the mismatch and tried to
10
+ * redirect the request to openai's own factory, but the registry had no openai
11
+ * entry: a session only ever built the factory for ITS OWN provider, so any
12
+ * sub-task that resolved a model from another provider had nothing to redirect
13
+ * to and fell through to the wrong endpoint.
14
+ *
15
+ * That is the actual hole. Model ids and provider factories travel as two
16
+ * independent values, so "borrow the session's factory" is the silent default
17
+ * for every sub-task (compaction, classify, sub-agents, mode switches) and only
18
+ * some paths remember to re-derive the provider. Warming the registry for every
19
+ * credentialed provider closes it at the source: the factory for a model's own
20
+ * provider is then always present, so `resolveModelRuntime` can derive it from
21
+ * the model itself instead of trusting whatever factory a caller passed.
22
+ *
23
+ * Best-effort by construction: a provider that fails to warm is simply absent
24
+ * (the same state as today), never a boot failure.
25
+ */
26
+ import { logger } from "../utils/logger.js";
27
+ import { createProviderFactoryAsync, hasProviderFactory } from "./runtime.js";
28
+ import { ALL_PROVIDER_IDS } from "./types.js";
29
+ /**
30
+ * Build and register a factory for every provider the user has credentials for.
31
+ *
32
+ * Runs providers concurrently — each is an independent credential read, and a
33
+ * serial loop would add every provider's latency to boot.
34
+ *
35
+ * Skips providers that already have a factory: the session builds its own with
36
+ * session-specific options (custom baseURL, OAuth headers), and rebuilding it
37
+ * from bare defaults here would silently downgrade the live session's wiring.
38
+ */
39
+ /**
40
+ * Rebuild ONE provider's factory after its credentials changed mid-session.
41
+ *
42
+ * `createProviderFactoryAsync` reads OAuth tokens at CONSTRUCTION and bakes the
43
+ * resulting auth headers into the factory (runtime.ts: `loadTokensWithRefresh()`
44
+ * then `authHeaders(tokens)`). Boot warms openai/xai even with no tokens —
45
+ * `getOAuthProviderConfig` only reports that a provider SUPPORTS OAuth — so the
46
+ * entry built at boot is an API-key-path factory. Signing in later saved tokens
47
+ * that nothing re-read: the stale factory stayed registered and the provider
48
+ * kept failing until the next process start. That is the "OAuth only works
49
+ * after I restart the session" report; the same held for a key set via `K` on a
50
+ * provider skipped at boot for "no credentials".
51
+ *
52
+ * warmProviderFactories() cannot be reused for this: it deliberately SKIPS a
53
+ * provider that already has a factory. The registry is last-built-wins, so
54
+ * building again is what replaces the stale entry.
55
+ */
56
+ export async function rewarmProviderFactory(id) {
57
+ try {
58
+ const { loadUserSettings } = await import("../utils/settings.js");
59
+ const baseURL = loadUserSettings()?.providers?.[id]?.baseURL;
60
+ let apiKey;
61
+ try {
62
+ const { loadKeyForProvider } = await import("./keychain.js");
63
+ apiKey = (await loadKeyForProvider(id)) || undefined;
64
+ }
65
+ catch (err) {
66
+ // No stored key is the normal OAuth case, so this is not an error —
67
+ // but log it: a keychain fault here silently downgrades auth.
68
+ logger.debug("cli", `[provider-warm] key read failed for ${id}: ${err?.message}`, { error: err });
69
+ apiKey = undefined;
70
+ }
71
+ await createProviderFactoryAsync(id, { ...(apiKey ? { apiKey } : {}), ...(baseURL ? { baseURL } : {}) });
72
+ logger.info("cli", `[provider-warm] rebuilt ${id} factory after a credential change`, { providerId: id });
73
+ return true;
74
+ }
75
+ catch (err) {
76
+ logger.warn("cli", `[provider-warm] failed to rebuild factory for ${id}: ${err?.message}`, {
77
+ error: err,
78
+ providerId: id,
79
+ });
80
+ return false;
81
+ }
82
+ }
83
+ export async function warmProviderFactories() {
84
+ const warmed = [];
85
+ const skipped = [];
86
+ const { isProviderDisabled } = await import("../utils/settings.js");
87
+ const { loadUserSettings } = await import("../utils/settings.js");
88
+ const userSettings = loadUserSettings();
89
+ await Promise.all(ALL_PROVIDER_IDS.map(async (id) => {
90
+ try {
91
+ if (hasProviderFactory(id)) {
92
+ skipped.push({ id, reason: "already built" });
93
+ return;
94
+ }
95
+ if (isProviderDisabled(id)) {
96
+ skipped.push({ id, reason: "disabled" });
97
+ return;
98
+ }
99
+ let apiKey;
100
+ try {
101
+ const { loadKeyForProvider } = await import("./keychain.js");
102
+ apiKey = (await loadKeyForProvider(id)) || undefined;
103
+ }
104
+ catch {
105
+ // No stored key for this provider — normal; OAuth may still cover it.
106
+ apiKey = undefined;
107
+ }
108
+ let hasOAuth = false;
109
+ try {
110
+ const { getOAuthProviderConfig } = await import("./auth/registry.js");
111
+ hasOAuth = !!(await getOAuthProviderConfig(id));
112
+ }
113
+ catch (err) {
114
+ logger.debug("cli", `[provider-warm] OAuth probe failed for ${id}: ${err?.message}`, {
115
+ error: err,
116
+ });
117
+ }
118
+ if (!apiKey && !hasOAuth) {
119
+ skipped.push({ id, reason: "no credentials" });
120
+ return;
121
+ }
122
+ // Honour a user-configured endpoint; otherwise each strategy falls back
123
+ // to its own provider's apiBase, which is what a non-session provider
124
+ // should use.
125
+ const baseURL = userSettings?.providers?.[id]?.baseURL;
126
+ await createProviderFactoryAsync(id, { ...(apiKey ? { apiKey } : {}), ...(baseURL ? { baseURL } : {}) });
127
+ warmed.push(id);
128
+ }
129
+ catch (err) {
130
+ // Never let one provider's credential problem break boot: absent from
131
+ // the registry is exactly the pre-warm status quo.
132
+ logger.warn("cli", `[provider-warm] failed to build factory for ${id}: ${err?.message}`, {
133
+ error: err,
134
+ providerId: id,
135
+ });
136
+ skipped.push({ id, reason: `error: ${err?.message ?? "unknown"}` });
137
+ }
138
+ }));
139
+ logger.info("cli", `[provider-warm] registry warmed: ${warmed.join(", ") || "(none)"}`, {
140
+ warmed,
141
+ skipped,
142
+ });
143
+ return { warmed, skipped };
144
+ }
145
+ //# sourceMappingURL=warm.js.map
@@ -141,8 +141,9 @@ export async function createLLMBrain(opts) {
141
141
  const apiKey = await loadKeyForProvider(provider);
142
142
  if (!apiKey)
143
143
  throw new Error(`No API key found for provider '${provider}'`);
144
- const { factory } = createProviderFactory(provider, { apiKey });
145
- const runtime = resolveModelRuntime(factory, opts.modelId);
144
+ // Registers the factory so resolveModelRuntime can derive it from the model.
145
+ createProviderFactory(provider, { apiKey });
146
+ const runtime = resolveModelRuntime(opts.modelId);
146
147
  const systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
147
148
  const maxTokens = opts.maxTokens ?? 1024;
148
149
  const retryOnEmpty = opts.retryOnEmpty !== false;
@@ -604,13 +604,19 @@ export function buildChatEntries(sessionId) {
604
604
  catch (err) {
605
605
  logger.error("storage", "Failed to load session metadata for chain in buildChatEntries", { error: err });
606
606
  }
607
+ // One read per session in the chain — the prompt/answer reconciliation below
608
+ // and the render walk both need every session's records.
609
+ const recordsBySession = new Map();
610
+ for (const sid of chain) {
611
+ recordsBySession.set(sid, buildEffectiveMessageRecords(sid));
612
+ }
607
613
  // Map each child sub-session's initial user prompt to its creation timestamp
608
614
  const childPrompts = new Map();
609
615
  for (let i = 1; i < chain.length; i++) {
610
616
  const sid = chain[i];
611
617
  const meta = sessionMeta.get(sid);
612
618
  if (meta && meta.parent_session_id) {
613
- const records = buildEffectiveMessageRecords(sid);
619
+ const records = recordsBySession.get(sid) ?? [];
614
620
  const firstUser = records.find((r) => r.message.role === "user");
615
621
  if (firstUser) {
616
622
  const content = renderUserContent(firstUser.message.content);
@@ -620,14 +626,38 @@ export function buildChatEntries(sessionId) {
620
626
  }
621
627
  }
622
628
  }
629
+ // When a sub-session finishes, the orchestrator absorbs its LAST assistant
630
+ // message into the PARENT (salvageSubSessionOutput + appendCompletedTurn,
631
+ // orchestrator.ts) so the parent keeps context continuity. That content is
632
+ // therefore persisted in BOTH sessions, and the chain walk below would render
633
+ // the final answer twice. Keep the parent's copy — it sits in the timeline
634
+ // where the user actually asked — and drop the child's, mirroring how the
635
+ // child's duplicated user prompt is handled above.
636
+ const absorbedByChild = new Map();
637
+ for (let i = 1; i < chain.length; i++) {
638
+ const sid = chain[i];
639
+ const parentId = sessionMeta.get(sid)?.parent_session_id;
640
+ if (!parentId)
641
+ continue;
642
+ const parentRecords = recordsBySession.get(parentId);
643
+ if (!parentRecords)
644
+ continue;
645
+ const text = lastAssistantText(recordsBySession.get(sid) ?? []);
646
+ if (text && parentRecords.some((r) => r.message.role === "assistant" && assistantText(r.message) === text)) {
647
+ absorbedByChild.set(sid, text);
648
+ }
649
+ }
623
650
  const callMap = new Map();
624
651
  const entries = [];
625
652
  const renderedResponseCallIds = new Set();
626
653
  let lastTimestamp;
627
654
  for (let i = 0; i < chain.length; i++) {
628
655
  const sid = chain[i];
629
- const records = buildEffectiveMessageRecords(sid);
656
+ const records = recordsBySession.get(sid) ?? [];
630
657
  const isChildSession = i > 0;
658
+ // Only the single message the parent absorbed is dropped, so a child that
659
+ // legitimately repeats itself still renders its other turns.
660
+ let absorbedPending = absorbedByChild.get(sid);
631
661
  for (let j = 0; j < records.length; j++) {
632
662
  const { message, timestamp } = records[j];
633
663
  lastTimestamp = timestamp;
@@ -658,8 +688,15 @@ export function buildChatEntries(sessionId) {
658
688
  continue;
659
689
  }
660
690
  if (message.role === "assistant") {
691
+ // Call first regardless: it registers this message's tool-calls in
692
+ // callMap, which the tool-result branch below needs even when the
693
+ // message itself is skipped as an absorbed duplicate.
661
694
  const text = renderAssistantContent(message.content, callMap);
662
695
  if (text) {
696
+ if (absorbedPending !== undefined && text === absorbedPending) {
697
+ absorbedPending = undefined;
698
+ continue;
699
+ }
663
700
  entries.push({ type: "assistant", content: text, timestamp });
664
701
  }
665
702
  continue;
@@ -744,6 +781,23 @@ export function buildChatEntries(sessionId) {
744
781
  logger.debug("storage", "Built chat entries successfully", { sessionId, count: entries.length });
745
782
  return entries;
746
783
  }
784
+ /** Rendered text of an assistant message, with no callMap side effect. */
785
+ function assistantText(message) {
786
+ return renderAssistantContent(message.content, new Map());
787
+ }
788
+ /**
789
+ * Text of the session's LAST assistant message — the one the sub-session
790
+ * absorption path hands to the parent. Empty when that message carried only
791
+ * tool-calls (nothing renders, so nothing can duplicate).
792
+ */
793
+ function lastAssistantText(records) {
794
+ for (let i = records.length - 1; i >= 0; i--) {
795
+ const message = records[i].message;
796
+ if (message.role === "assistant")
797
+ return assistantText(message);
798
+ }
799
+ return "";
800
+ }
747
801
  function getNextSequence(db, sessionId) {
748
802
  const row = db
749
803
  .prepare(`
@@ -90,6 +90,25 @@ export interface StagingBlockResult {
90
90
  * 2. Setting MUONROI_ALLOW_BROAD_STAGE=1 (escape hatch, logged to decision-log).
91
91
  */
92
92
  export declare function checkSensitiveStaging(cwd: string): StagingBlockResult;
93
+ export type DestructiveKind = "checkout-discard" | "restore" | "reset-hard" | "clean" | "stash-drop" | "rm-tracked";
94
+ export interface DestructiveShape {
95
+ kind: DestructiveKind | null;
96
+ /** true when the risk is untracked files (clean) rather than tracked mods. */
97
+ untrackedRisk: boolean;
98
+ }
99
+ export declare function analyzeDestructiveGit(command: string): DestructiveShape;
100
+ export interface DestructiveBlockResult {
101
+ blocked: boolean;
102
+ kind: DestructiveKind | null;
103
+ message: string;
104
+ }
105
+ /**
106
+ * Pre-execution guard: block a working-tree-destroying command when it would
107
+ * actually discard uncommitted work. Returns blocked=false (no message) when
108
+ * the command is non-destructive, when nothing is at risk, or when the user set
109
+ * the escape hatch. Deterministic + cheap (a couple of git plumbing calls).
110
+ */
111
+ export declare function checkDestructiveOp(command: string, cwd: string): DestructiveBlockResult;
93
112
  /**
94
113
  * @deprecated Use checkSensitiveStaging() for hard-blocking.
95
114
  * Kept for call sites that emit a trailing warning (non-broad-stage contexts).