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.
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
- package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
- package/dist/src/agent-harness/mock-model.d.ts +10 -0
- package/dist/src/agent-harness/mock-model.js +6 -2
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/keys.d.ts +8 -45
- package/dist/src/cli/keys.js +18 -324
- package/dist/src/council/clarifier.d.ts +4 -2
- package/dist/src/council/clarifier.js +158 -36
- package/dist/src/council/debate-planner.js +3 -2
- package/dist/src/council/debate.js +59 -5
- package/dist/src/council/index.d.ts +23 -0
- package/dist/src/council/index.js +37 -2
- package/dist/src/council/llm.d.ts +62 -0
- package/dist/src/council/llm.js +123 -23
- package/dist/src/council/panel-select.js +13 -3
- package/dist/src/council/planner.js +17 -0
- package/dist/src/council/preflight.d.ts +10 -0
- package/dist/src/council/preflight.js +36 -0
- package/dist/src/council/prompts.d.ts +9 -1
- package/dist/src/council/prompts.js +23 -6
- package/dist/src/council/types.d.ts +10 -0
- package/dist/src/ee/auth.d.ts +19 -0
- package/dist/src/ee/auth.js +39 -0
- package/dist/src/ee/client.js +28 -3
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/flow/compaction/compress.d.ts +2 -2
- package/dist/src/flow/compaction/compress.js +21 -8
- package/dist/src/flow/compaction/extract.d.ts +3 -3
- package/dist/src/flow/compaction/extract.js +6 -6
- package/dist/src/flow/compaction/index.d.ts +2 -1
- package/dist/src/flow/compaction/index.js +29 -3
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/flags.d.ts +11 -0
- package/dist/src/gsd/flags.js +19 -0
- package/dist/src/gsd/plan-council.js +104 -72
- package/dist/src/gsd/verdict-schema.d.ts +1 -1
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/index.js +129 -260
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/mcp/client-pool.js +43 -15
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog.json +19 -19
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/compaction.d.ts +2 -3
- package/dist/src/orchestrator/compaction.js +8 -8
- package/dist/src/orchestrator/council-manager.js +9 -8
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +26 -8
- package/dist/src/orchestrator/orchestrator.d.ts +25 -0
- package/dist/src/orchestrator/orchestrator.js +204 -50
- package/dist/src/orchestrator/preprocessor.js +2 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
- package/dist/src/orchestrator/safety-intercept.js +7 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
- package/dist/src/orchestrator/stall-watchdog.js +24 -3
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +54 -21
- package/dist/src/orchestrator/tool-engine.d.ts +19 -0
- package/dist/src/orchestrator/tool-engine.js +241 -25
- package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
- package/dist/src/orchestrator/turn-watchdog.js +38 -9
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +2 -2
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/llm-classify.d.ts +78 -3
- package/dist/src/pil/llm-classify.js +351 -111
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +7 -0
- package/dist/src/pil/pipeline.js +2 -0
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +37 -18
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +14 -1
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/index.js +7 -0
- package/dist/src/product-loop/loop-driver.js +21 -8
- package/dist/src/product-loop/phase-plan.d.ts +16 -0
- package/dist/src/product-loop/phase-plan.js +42 -4
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +30 -11
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +62 -0
- package/dist/src/product-loop/sprint-runner.js +309 -8
- package/dist/src/product-loop/types.d.ts +25 -0
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +14 -0
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -70
- package/dist/src/providers/auth/types.d.ts +8 -0
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +21 -17
- package/dist/src/providers/keychain.js +124 -135
- package/dist/src/providers/runtime.d.ts +24 -9
- package/dist/src/providers/runtime.js +48 -37
- package/dist/src/providers/strategies/thinking-mode.js +9 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/self-qa/agentic-loop.js +3 -2
- package/dist/src/storage/transcript.js +56 -2
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +76 -1
- package/dist/src/tools/registry.d.ts +17 -0
- package/dist/src/tools/registry.js +116 -1
- package/dist/src/types/index.d.ts +29 -1
- package/dist/src/ui/app.js +67 -4
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/council-phase-timeline.js +17 -2
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/menu-items.js +11 -2
- package/dist/src/ui/use-app-logic.js +354 -224
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/settings.d.ts +27 -0
- package/dist/src/utils/settings.js +38 -2
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/verify/entrypoint.js +51 -16
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/package.json +1 -2
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- 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
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* inherit the parent session's factory)
|
|
13
|
-
*
|
|
14
|
-
* a module-level map safe. Last-built wins, so
|
|
15
|
-
* a provider's factory
|
|
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
|
-
|
|
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
|
|
163
|
+
factory,
|
|
153
164
|
modelId: canonicalId,
|
|
154
165
|
modelInfo,
|
|
155
166
|
reasoningEffort: userEffort,
|
|
156
167
|
});
|
|
157
|
-
providerLevelDefaults =
|
|
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
|
-
|
|
145
|
-
|
|
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 =
|
|
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 =
|
|
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).
|