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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/agent-activities.ts
|
|
3
|
+
*
|
|
4
|
+
* Collect every sub-agent / background job the main agent has spawned this
|
|
5
|
+
* session, for the rail's Agents block.
|
|
6
|
+
*
|
|
7
|
+
* Derived entirely from state the UI already holds — the transcript entries and
|
|
8
|
+
* the in-flight tool calls. Spawning goes through tools (`task`, `delegate`,
|
|
9
|
+
* `bash background:true`), so the tool stream IS the record; there is no need
|
|
10
|
+
* for a parallel registry in core, and nothing here can drift out of sync with
|
|
11
|
+
* what the transcript shows.
|
|
12
|
+
*
|
|
13
|
+
* Running entries come from activeToolCalls (no result yet), finished ones from
|
|
14
|
+
* tool_result / tool_group items. A call that is both (still streaming while an
|
|
15
|
+
* earlier identical call finished) is keyed by tool-call id, so it appears once.
|
|
16
|
+
*/
|
|
17
|
+
import type { ChatEntry, ToolCall } from "../../types/index.js";
|
|
18
|
+
export type ActivityKind = "subagent" | "delegate" | "background";
|
|
19
|
+
export type ActivityStatus = "running" | "done" | "failed";
|
|
20
|
+
export interface AgentActivity {
|
|
21
|
+
/** Tool-call id — stable across the running → finished transition. */
|
|
22
|
+
id: string;
|
|
23
|
+
kind: ActivityKind;
|
|
24
|
+
/** Short line for the rail row, e.g. an explore agent's description. */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Sub-agent type ("explore", "general", …). Empty for background shells. */
|
|
27
|
+
agent: string;
|
|
28
|
+
status: ActivityStatus;
|
|
29
|
+
/** Full text shown when the row is opened: the prompt/command, then output. */
|
|
30
|
+
detail: string;
|
|
31
|
+
}
|
|
32
|
+
/** The activity kind a tool call represents, or null if it spawns nothing. */
|
|
33
|
+
export declare function activityKindFor(tc: ToolCall): ActivityKind | null;
|
|
34
|
+
/**
|
|
35
|
+
* Every activity in the session, oldest first. Later state for the same
|
|
36
|
+
* tool-call id wins, so a call that finishes replaces its own running row
|
|
37
|
+
* instead of adding a second one.
|
|
38
|
+
*/
|
|
39
|
+
export declare function collectAgentActivities(messages: ChatEntry[], activeToolCalls?: ToolCall[]): AgentActivity[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/agent-activities.ts
|
|
3
|
+
*
|
|
4
|
+
* Collect every sub-agent / background job the main agent has spawned this
|
|
5
|
+
* session, for the rail's Agents block.
|
|
6
|
+
*
|
|
7
|
+
* Derived entirely from state the UI already holds — the transcript entries and
|
|
8
|
+
* the in-flight tool calls. Spawning goes through tools (`task`, `delegate`,
|
|
9
|
+
* `bash background:true`), so the tool stream IS the record; there is no need
|
|
10
|
+
* for a parallel registry in core, and nothing here can drift out of sync with
|
|
11
|
+
* what the transcript shows.
|
|
12
|
+
*
|
|
13
|
+
* Running entries come from activeToolCalls (no result yet), finished ones from
|
|
14
|
+
* tool_result / tool_group items. A call that is both (still streaming while an
|
|
15
|
+
* earlier identical call finished) is keyed by tool-call id, so it appears once.
|
|
16
|
+
*/
|
|
17
|
+
const KIND_BY_TOOL = {
|
|
18
|
+
task: "subagent",
|
|
19
|
+
delegate: "delegate",
|
|
20
|
+
};
|
|
21
|
+
function parseArgs(tc) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(tc.function.arguments || "{}");
|
|
24
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
// Arguments stream in as partial JSON while a call assembles — an
|
|
28
|
+
// unparseable value here is expected mid-flight, not an error.
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function str(value) {
|
|
33
|
+
return typeof value === "string" ? value : "";
|
|
34
|
+
}
|
|
35
|
+
/** The activity kind a tool call represents, or null if it spawns nothing. */
|
|
36
|
+
export function activityKindFor(tc) {
|
|
37
|
+
const name = tc.function.name;
|
|
38
|
+
const known = KIND_BY_TOOL[name];
|
|
39
|
+
if (known)
|
|
40
|
+
return known;
|
|
41
|
+
// A backgrounded shell is a spawned job too — but only when it actually
|
|
42
|
+
// backgrounds. A normal bash call is just a tool, not an activity.
|
|
43
|
+
if (name === "bash" && parseArgs(tc).background === true)
|
|
44
|
+
return "background";
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function toActivity(tc, result) {
|
|
48
|
+
const kind = activityKindFor(tc);
|
|
49
|
+
if (!kind)
|
|
50
|
+
return null;
|
|
51
|
+
const args = parseArgs(tc);
|
|
52
|
+
const agent = kind === "background" ? "" : str(args.agent);
|
|
53
|
+
const command = str(args.command);
|
|
54
|
+
const label = (kind === "background" ? command : str(args.description) || str(args.prompt)) || tc.function.name;
|
|
55
|
+
const request = kind === "background" ? command : str(args.prompt);
|
|
56
|
+
let status = "running";
|
|
57
|
+
if (result)
|
|
58
|
+
status = result.success ? "done" : "failed";
|
|
59
|
+
const output = result ? (result.success ? result.output || "" : result.error || "") : "";
|
|
60
|
+
const detail = [request, output].filter((s) => s.trim().length > 0).join("\n\n");
|
|
61
|
+
return { id: tc.id, kind, label: label.replace(/\s+/g, " ").trim(), agent, status, detail };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Every activity in the session, oldest first. Later state for the same
|
|
65
|
+
* tool-call id wins, so a call that finishes replaces its own running row
|
|
66
|
+
* instead of adding a second one.
|
|
67
|
+
*/
|
|
68
|
+
export function collectAgentActivities(messages, activeToolCalls = []) {
|
|
69
|
+
const byId = new Map();
|
|
70
|
+
const add = (tc, result) => {
|
|
71
|
+
if (!tc)
|
|
72
|
+
return;
|
|
73
|
+
const activity = toActivity(tc, result);
|
|
74
|
+
if (activity)
|
|
75
|
+
byId.set(activity.id, activity);
|
|
76
|
+
};
|
|
77
|
+
for (const entry of messages) {
|
|
78
|
+
if (entry.type === "tool_result") {
|
|
79
|
+
add(entry.toolCall, entry.toolResult);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (entry.type === "tool_group") {
|
|
83
|
+
for (const item of entry.toolGroup?.items ?? [])
|
|
84
|
+
add(item.toolCall, item.result);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// In-flight calls fill in only what the transcript does not already have.
|
|
88
|
+
// activeToolCalls is not always cleared the instant a result lands, so adding
|
|
89
|
+
// one unconditionally would flip a finished row back to "running".
|
|
90
|
+
for (const tc of activeToolCalls) {
|
|
91
|
+
if (!byId.has(tc.id))
|
|
92
|
+
add(tc, undefined);
|
|
93
|
+
}
|
|
94
|
+
return [...byId.values()];
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=agent-activities.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/group-tool-entries.ts
|
|
3
|
+
*
|
|
4
|
+
* Rebuild the tool_group panels when a flat transcript is (re)loaded.
|
|
5
|
+
*
|
|
6
|
+
* WHY: the live turn renders tool calls inside a `tool_group` ChatEntry, but the
|
|
7
|
+
* persisted transcript stores one `tool_result` entry per call. Anything that
|
|
8
|
+
* replaces the message list with `agent.getChatEntries()` — the end-of-turn
|
|
9
|
+
* resync in finalizeActiveTurn, session load, resume — therefore DESTROYED the
|
|
10
|
+
* group and dropped the transcript back to one flat "→ <tool>" line per call.
|
|
11
|
+
* That is what a user sees after the answer lands, so the nice
|
|
12
|
+
* "Read 2 files, ran 1 shell command" recap only ever existed mid-turn.
|
|
13
|
+
*
|
|
14
|
+
* This folds consecutive tool_result entries back into a done/failed group.
|
|
15
|
+
*/
|
|
16
|
+
import type { ChatEntry } from "../../types/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Fold runs of consecutive groupable tool_result entries into one tool_group
|
|
19
|
+
* entry each. Non-tool entries and rich results pass through untouched, so the
|
|
20
|
+
* transcript keeps its order. A run of one is still grouped: the recap line
|
|
21
|
+
* ("Read 1 file") is what stays visible once collapsed.
|
|
22
|
+
*
|
|
23
|
+
* `tool_call` entries are dropped inside a run — they are the pending twin of a
|
|
24
|
+
* result that is already represented by the item.
|
|
25
|
+
*/
|
|
26
|
+
export declare function groupToolEntries(entries: ChatEntry[]): ChatEntry[];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/group-tool-entries.ts
|
|
3
|
+
*
|
|
4
|
+
* Rebuild the tool_group panels when a flat transcript is (re)loaded.
|
|
5
|
+
*
|
|
6
|
+
* WHY: the live turn renders tool calls inside a `tool_group` ChatEntry, but the
|
|
7
|
+
* persisted transcript stores one `tool_result` entry per call. Anything that
|
|
8
|
+
* replaces the message list with `agent.getChatEntries()` — the end-of-turn
|
|
9
|
+
* resync in finalizeActiveTurn, session load, resume — therefore DESTROYED the
|
|
10
|
+
* group and dropped the transcript back to one flat "→ <tool>" line per call.
|
|
11
|
+
* That is what a user sees after the answer lands, so the nice
|
|
12
|
+
* "Read 2 files, ran 1 shell command" recap only ever existed mid-turn.
|
|
13
|
+
*
|
|
14
|
+
* This folds consecutive tool_result entries back into a done/failed group.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Tool results whose own renderer shows something a tool-group item line cannot
|
|
18
|
+
* (a plan, a sub-agent run, an image, streaming process logs). These stay as
|
|
19
|
+
* standalone entries and also break a run, so ordering is preserved.
|
|
20
|
+
*/
|
|
21
|
+
function rendersRichResult(entry) {
|
|
22
|
+
const name = entry.toolCall?.function.name ?? "";
|
|
23
|
+
const r = entry.toolResult;
|
|
24
|
+
if (!r)
|
|
25
|
+
return true;
|
|
26
|
+
if (r.plan || r.task || r.delegation || r.backgroundProcess || (r.media?.length ?? 0) > 0)
|
|
27
|
+
return true;
|
|
28
|
+
return (name === "generate_plan" ||
|
|
29
|
+
name === "task" ||
|
|
30
|
+
name === "delegate" ||
|
|
31
|
+
name === "delegation_list" ||
|
|
32
|
+
name === "delegation_read" ||
|
|
33
|
+
name === "lsp" ||
|
|
34
|
+
name === "process_logs" ||
|
|
35
|
+
name === "process_stop" ||
|
|
36
|
+
name === "process_list");
|
|
37
|
+
}
|
|
38
|
+
function groupable(entry) {
|
|
39
|
+
return entry.type === "tool_result" && !!entry.toolCall && !rendersRichResult(entry);
|
|
40
|
+
}
|
|
41
|
+
function toItem(entry) {
|
|
42
|
+
const at = entry.timestamp instanceof Date ? entry.timestamp.getTime() : Date.now();
|
|
43
|
+
const failed = entry.toolResult?.success === false;
|
|
44
|
+
return {
|
|
45
|
+
toolCall: entry.toolCall,
|
|
46
|
+
result: entry.toolResult,
|
|
47
|
+
startedAt: at,
|
|
48
|
+
finishedAt: at,
|
|
49
|
+
...(failed ? { failed: true } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fold runs of consecutive groupable tool_result entries into one tool_group
|
|
54
|
+
* entry each. Non-tool entries and rich results pass through untouched, so the
|
|
55
|
+
* transcript keeps its order. A run of one is still grouped: the recap line
|
|
56
|
+
* ("Read 1 file") is what stays visible once collapsed.
|
|
57
|
+
*
|
|
58
|
+
* `tool_call` entries are dropped inside a run — they are the pending twin of a
|
|
59
|
+
* result that is already represented by the item.
|
|
60
|
+
*/
|
|
61
|
+
export function groupToolEntries(entries) {
|
|
62
|
+
const out = [];
|
|
63
|
+
let run = [];
|
|
64
|
+
const flush = () => {
|
|
65
|
+
if (run.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
const items = run.map(toItem);
|
|
68
|
+
const first = run[0];
|
|
69
|
+
const last = run[run.length - 1];
|
|
70
|
+
const startedAt = items[0].startedAt;
|
|
71
|
+
const finishedAt = items[items.length - 1].finishedAt ?? startedAt;
|
|
72
|
+
out.push({
|
|
73
|
+
type: "tool_group",
|
|
74
|
+
content: "",
|
|
75
|
+
timestamp: first.timestamp,
|
|
76
|
+
...(first.modeColor ? { modeColor: first.modeColor } : {}),
|
|
77
|
+
...(first.remoteKey ? { remoteKey: first.remoteKey } : {}),
|
|
78
|
+
...(first.sourceLabel ? { sourceLabel: first.sourceLabel } : {}),
|
|
79
|
+
toolGroup: {
|
|
80
|
+
// Deterministic id: the same transcript must rebuild to the same ids, or
|
|
81
|
+
// React remounts every group on each resync and expansion state is lost.
|
|
82
|
+
id: `tg-restored-${startedAt}-${items.length}-${last.toolCall?.id ?? "x"}`,
|
|
83
|
+
state: items.some((i) => i.failed) ? "failed" : "done",
|
|
84
|
+
items,
|
|
85
|
+
startedAt,
|
|
86
|
+
finishedAt,
|
|
87
|
+
// The persisted entries carry a write-time timestamp, not the tool's real
|
|
88
|
+
// wall-clock span, so any duration derived here is fiction (a 10s turn
|
|
89
|
+
// rebuilt as "23ms"). Flag it and let the header omit the number rather
|
|
90
|
+
// than print a confident lie.
|
|
91
|
+
restored: true,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
run = [];
|
|
95
|
+
};
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (groupable(entry)) {
|
|
98
|
+
run.push(entry);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// A pending tool_call adjacent to a run is already covered by the run's
|
|
102
|
+
// items; dropping it prevents a duplicate line above the group.
|
|
103
|
+
if (entry.type === "tool_call" && run.length > 0)
|
|
104
|
+
continue;
|
|
105
|
+
flush();
|
|
106
|
+
out.push(entry);
|
|
107
|
+
}
|
|
108
|
+
flush();
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=group-tool-entries.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/tool-summary.ts
|
|
3
|
+
*
|
|
4
|
+
* Phrasing for the tool-group panel. Pure string helpers, no React, so the
|
|
5
|
+
* wording is unit-testable and the component stays presentational.
|
|
6
|
+
*
|
|
7
|
+
* Active: "Reading 2 files…" (header, present participle)
|
|
8
|
+
* Done: "Read 2 files, ran 1 shell command" (past-tense recap)
|
|
9
|
+
*
|
|
10
|
+
* Counting is per CATEGORY, not per tool name: read_file and
|
|
11
|
+
* mcp__filesystem__read_text_file are both "files" to a reader.
|
|
12
|
+
*/
|
|
13
|
+
export declare function categoryForTool(name: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Header while the group runs: "Reading 2 files…".
|
|
16
|
+
* With a mixed batch the dominant category wins and the rest are summed into a
|
|
17
|
+
* trailing "+N more" so the header never claims work it isn't doing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function activeToolGroupHeader(toolNames: readonly string[]): string;
|
|
20
|
+
/** Past-tense recap once the group closes: "Read 2 files, ran 1 shell command". */
|
|
21
|
+
export declare function doneToolGroupSummary(toolNames: readonly string[]): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/tool-summary.ts
|
|
3
|
+
*
|
|
4
|
+
* Phrasing for the tool-group panel. Pure string helpers, no React, so the
|
|
5
|
+
* wording is unit-testable and the component stays presentational.
|
|
6
|
+
*
|
|
7
|
+
* Active: "Reading 2 files…" (header, present participle)
|
|
8
|
+
* Done: "Read 2 files, ran 1 shell command" (past-tense recap)
|
|
9
|
+
*
|
|
10
|
+
* Counting is per CATEGORY, not per tool name: read_file and
|
|
11
|
+
* mcp__filesystem__read_text_file are both "files" to a reader.
|
|
12
|
+
*/
|
|
13
|
+
import { verbForTool } from "./tools.js";
|
|
14
|
+
const CATEGORIES = {
|
|
15
|
+
read: { gerund: "Reading", past: "read", noun: "file", nounPlural: "files" },
|
|
16
|
+
write: { gerund: "Writing", past: "wrote", noun: "file", nounPlural: "files" },
|
|
17
|
+
edit: { gerund: "Editing", past: "edited", noun: "file", nounPlural: "files" },
|
|
18
|
+
bash: { gerund: "Running", past: "ran", noun: "shell command", nounPlural: "shell commands" },
|
|
19
|
+
search: { gerund: "Searching", past: "ran", noun: "search", nounPlural: "searches" },
|
|
20
|
+
agent: { gerund: "Exploring", past: "ran", noun: "sub-agent", nounPlural: "sub-agents" },
|
|
21
|
+
other: { gerund: "Working", past: "ran", noun: "tool", nounPlural: "tools" },
|
|
22
|
+
};
|
|
23
|
+
/** Stable phrase order so the recap doesn't reshuffle as items land. */
|
|
24
|
+
const ORDER = ["read", "write", "edit", "bash", "search", "agent", "other"];
|
|
25
|
+
export function categoryForTool(name) {
|
|
26
|
+
if (name === "write_file" || /^mcp_+filesystem__write_file$/.test(name))
|
|
27
|
+
return "write";
|
|
28
|
+
if (name === "edit_file" || /^mcp_+filesystem__edit_file$/.test(name))
|
|
29
|
+
return "edit";
|
|
30
|
+
if (name === "bash")
|
|
31
|
+
return "bash";
|
|
32
|
+
if (name === "task" || name === "delegate")
|
|
33
|
+
return "agent";
|
|
34
|
+
// Fall back to the existing verb vocabulary so a new tool inherits sane
|
|
35
|
+
// phrasing without being enumerated here twice.
|
|
36
|
+
const verb = verbForTool(name);
|
|
37
|
+
if (verb === "Reading")
|
|
38
|
+
return "read";
|
|
39
|
+
if (verb === "Searching")
|
|
40
|
+
return "search";
|
|
41
|
+
return "other";
|
|
42
|
+
}
|
|
43
|
+
function countByCategory(toolNames) {
|
|
44
|
+
const counts = new Map();
|
|
45
|
+
for (const n of toolNames) {
|
|
46
|
+
const c = categoryForTool(n);
|
|
47
|
+
counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
return counts;
|
|
50
|
+
}
|
|
51
|
+
function phrase(cat, n, past) {
|
|
52
|
+
const noun = n === 1 ? cat.noun : cat.nounPlural;
|
|
53
|
+
return past ? `${cat.past} ${n} ${noun}` : `${cat.gerund} ${n} ${noun}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Header while the group runs: "Reading 2 files…".
|
|
57
|
+
* With a mixed batch the dominant category wins and the rest are summed into a
|
|
58
|
+
* trailing "+N more" so the header never claims work it isn't doing.
|
|
59
|
+
*/
|
|
60
|
+
export function activeToolGroupHeader(toolNames) {
|
|
61
|
+
if (toolNames.length === 0)
|
|
62
|
+
return "Working…";
|
|
63
|
+
const counts = countByCategory(toolNames);
|
|
64
|
+
let bestKey = "other";
|
|
65
|
+
let best = 0;
|
|
66
|
+
for (const key of ORDER) {
|
|
67
|
+
const c = counts.get(key) ?? 0;
|
|
68
|
+
if (c > best) {
|
|
69
|
+
best = c;
|
|
70
|
+
bestKey = key;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const rest = toolNames.length - best;
|
|
74
|
+
const head = phrase(CATEGORIES[bestKey], best, false);
|
|
75
|
+
return rest > 0 ? `${head} +${rest} more…` : `${head}…`;
|
|
76
|
+
}
|
|
77
|
+
/** Past-tense recap once the group closes: "Read 2 files, ran 1 shell command". */
|
|
78
|
+
export function doneToolGroupSummary(toolNames) {
|
|
79
|
+
if (toolNames.length === 0)
|
|
80
|
+
return "Done";
|
|
81
|
+
const counts = countByCategory(toolNames);
|
|
82
|
+
const parts = [];
|
|
83
|
+
for (const key of ORDER) {
|
|
84
|
+
const n = counts.get(key) ?? 0;
|
|
85
|
+
if (n > 0)
|
|
86
|
+
parts.push(phrase(CATEGORIES[key], n, true));
|
|
87
|
+
}
|
|
88
|
+
const joined = parts.join(", ");
|
|
89
|
+
return joined.charAt(0).toUpperCase() + joined.slice(1);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=tool-summary.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/event-loop-monitor.ts
|
|
3
|
+
*
|
|
4
|
+
* Event-loop block detector.
|
|
5
|
+
*
|
|
6
|
+
* Root cause it addresses (observed live, 2026-07-16, session 90c3ff533826):
|
|
7
|
+
* the TUI froze for 304.5s mid-turn — keystrokes and mouse clicks produced
|
|
8
|
+
* nothing during the freeze, then ALL replayed at once when it ended (the
|
|
9
|
+
* signature of a blocked event loop, not a stalled render). Crucially the
|
|
10
|
+
* 120s provider stall watchdog (tool-engine.ts) did NOT rescue the turn at its
|
|
11
|
+
* deadline: `stall_rescue` landed at 09:05:47, 6.7s AFTER the loop recovered at
|
|
12
|
+
* 09:05:41. A setTimeout cannot fire while the loop is blocked, so every
|
|
13
|
+
* existing guard in this codebase is blind to this failure mode BY
|
|
14
|
+
* CONSTRUCTION — they are all loop-driven. Same pattern measured across 4
|
|
15
|
+
* models / 4 providers (grok-composer 304.5s, deepseek-v4-flash 320.2s,
|
|
16
|
+
* kimi-k2.7-code 329.7s + 314.8s), so it is not a provider quirk.
|
|
17
|
+
*
|
|
18
|
+
* This module is the missing instrument: a timer that measures its OWN lateness.
|
|
19
|
+
* It cannot prevent a block (it is loop-driven too), but the drift it observes
|
|
20
|
+
* on the first tick AFTER the loop frees up measures the block that just
|
|
21
|
+
* happened. Pair it with the CPU profiler in `loop-profiler.ts`, whose V8
|
|
22
|
+
* sampling thread runs INDEPENDENTLY of the JS thread and therefore captures
|
|
23
|
+
* the culprit stack DURING the block.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately dependency-free and cheap: one unref'd interval, no allocation
|
|
26
|
+
* per tick. `detectBlock` is pure so the drift maths is unit-testable without
|
|
27
|
+
* timers.
|
|
28
|
+
*/
|
|
29
|
+
/** A detected event-loop block, reported to `onBlock`. */
|
|
30
|
+
export interface EventLoopBlock {
|
|
31
|
+
/**
|
|
32
|
+
* How long the loop was blocked, in ms. Measured as the monitor tick's own
|
|
33
|
+
* lateness, so it UNDER-reports by up to one tick interval (a block shorter
|
|
34
|
+
* than the tick can hide entirely between two ticks). Treat it as a lower
|
|
35
|
+
* bound.
|
|
36
|
+
*/
|
|
37
|
+
blockedMs: number;
|
|
38
|
+
/** What the process last said it was doing — see {@link setLoopBreadcrumb}. */
|
|
39
|
+
breadcrumb: string | null;
|
|
40
|
+
/** ISO timestamp of the tick that observed the block (i.e. after it ended). */
|
|
41
|
+
detectedAt: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Decide whether an interval tick's lateness constitutes a block.
|
|
45
|
+
*
|
|
46
|
+
* A healthy tick fires ~`tickMs` after the previous one; the loop being busy
|
|
47
|
+
* pushes it later. Lateness beyond `thresholdMs` is reported as a block of that
|
|
48
|
+
* duration. Pure so the drift maths is testable without real timers.
|
|
49
|
+
*
|
|
50
|
+
* @param expectedAt when this tick was due (monotonic ms)
|
|
51
|
+
* @param actualAt when it actually ran (monotonic ms)
|
|
52
|
+
* @param thresholdMs report only blocks at least this long; <= 0 disables
|
|
53
|
+
* @returns block duration in ms, or null when the tick was on time
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectBlock(expectedAt: number, actualAt: number, thresholdMs: number): number | null;
|
|
56
|
+
/**
|
|
57
|
+
* Record what the process is about to do, so a block report can name a suspect
|
|
58
|
+
* instead of just a duration. Call it around anything that could plausibly run
|
|
59
|
+
* long and synchronously (tool execution, a stream step). Cheap — a single
|
|
60
|
+
* assignment; safe to call on a hot path.
|
|
61
|
+
*
|
|
62
|
+
* Pass null to clear. Not a stack: last writer wins, by design — the goal is a
|
|
63
|
+
* hint for the profile, and the profile is the real evidence.
|
|
64
|
+
*/
|
|
65
|
+
export declare function setLoopBreadcrumb(label: string | null): void;
|
|
66
|
+
/** The current breadcrumb, or null. */
|
|
67
|
+
export declare function getLoopBreadcrumb(): string | null;
|
|
68
|
+
/** Options for {@link startEventLoopMonitor}. */
|
|
69
|
+
export interface EventLoopMonitorOpts {
|
|
70
|
+
/** Report blocks at least this long. <= 0 disables the monitor entirely. */
|
|
71
|
+
thresholdMs: number;
|
|
72
|
+
/**
|
|
73
|
+
* How often to check. Bounds both the detection granularity and the amount a
|
|
74
|
+
* block can be under-reported by. 500ms costs ~2 wakeups/sec.
|
|
75
|
+
*/
|
|
76
|
+
tickMs?: number;
|
|
77
|
+
/** Called once per detected block. Must not throw (it is guarded anyway). */
|
|
78
|
+
onBlock: (block: EventLoopBlock) => void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Start watching for event-loop blocks. Returns a stop function (idempotent).
|
|
82
|
+
*
|
|
83
|
+
* The interval is unref'd so it never keeps the process alive on its own.
|
|
84
|
+
*/
|
|
85
|
+
export declare function startEventLoopMonitor(opts: EventLoopMonitorOpts): () => void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/event-loop-monitor.ts
|
|
3
|
+
*
|
|
4
|
+
* Event-loop block detector.
|
|
5
|
+
*
|
|
6
|
+
* Root cause it addresses (observed live, 2026-07-16, session 90c3ff533826):
|
|
7
|
+
* the TUI froze for 304.5s mid-turn — keystrokes and mouse clicks produced
|
|
8
|
+
* nothing during the freeze, then ALL replayed at once when it ended (the
|
|
9
|
+
* signature of a blocked event loop, not a stalled render). Crucially the
|
|
10
|
+
* 120s provider stall watchdog (tool-engine.ts) did NOT rescue the turn at its
|
|
11
|
+
* deadline: `stall_rescue` landed at 09:05:47, 6.7s AFTER the loop recovered at
|
|
12
|
+
* 09:05:41. A setTimeout cannot fire while the loop is blocked, so every
|
|
13
|
+
* existing guard in this codebase is blind to this failure mode BY
|
|
14
|
+
* CONSTRUCTION — they are all loop-driven. Same pattern measured across 4
|
|
15
|
+
* models / 4 providers (grok-composer 304.5s, deepseek-v4-flash 320.2s,
|
|
16
|
+
* kimi-k2.7-code 329.7s + 314.8s), so it is not a provider quirk.
|
|
17
|
+
*
|
|
18
|
+
* This module is the missing instrument: a timer that measures its OWN lateness.
|
|
19
|
+
* It cannot prevent a block (it is loop-driven too), but the drift it observes
|
|
20
|
+
* on the first tick AFTER the loop frees up measures the block that just
|
|
21
|
+
* happened. Pair it with the CPU profiler in `loop-profiler.ts`, whose V8
|
|
22
|
+
* sampling thread runs INDEPENDENTLY of the JS thread and therefore captures
|
|
23
|
+
* the culprit stack DURING the block.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately dependency-free and cheap: one unref'd interval, no allocation
|
|
26
|
+
* per tick. `detectBlock` is pure so the drift maths is unit-testable without
|
|
27
|
+
* timers.
|
|
28
|
+
*/
|
|
29
|
+
import { logger } from "./logger.js";
|
|
30
|
+
/**
|
|
31
|
+
* Decide whether an interval tick's lateness constitutes a block.
|
|
32
|
+
*
|
|
33
|
+
* A healthy tick fires ~`tickMs` after the previous one; the loop being busy
|
|
34
|
+
* pushes it later. Lateness beyond `thresholdMs` is reported as a block of that
|
|
35
|
+
* duration. Pure so the drift maths is testable without real timers.
|
|
36
|
+
*
|
|
37
|
+
* @param expectedAt when this tick was due (monotonic ms)
|
|
38
|
+
* @param actualAt when it actually ran (monotonic ms)
|
|
39
|
+
* @param thresholdMs report only blocks at least this long; <= 0 disables
|
|
40
|
+
* @returns block duration in ms, or null when the tick was on time
|
|
41
|
+
*/
|
|
42
|
+
export function detectBlock(expectedAt, actualAt, thresholdMs) {
|
|
43
|
+
if (!(thresholdMs > 0))
|
|
44
|
+
return null;
|
|
45
|
+
const lateBy = actualAt - expectedAt;
|
|
46
|
+
if (!Number.isFinite(lateBy) || lateBy < thresholdMs)
|
|
47
|
+
return null;
|
|
48
|
+
return Math.round(lateBy);
|
|
49
|
+
}
|
|
50
|
+
let _breadcrumb = null;
|
|
51
|
+
/**
|
|
52
|
+
* Record what the process is about to do, so a block report can name a suspect
|
|
53
|
+
* instead of just a duration. Call it around anything that could plausibly run
|
|
54
|
+
* long and synchronously (tool execution, a stream step). Cheap — a single
|
|
55
|
+
* assignment; safe to call on a hot path.
|
|
56
|
+
*
|
|
57
|
+
* Pass null to clear. Not a stack: last writer wins, by design — the goal is a
|
|
58
|
+
* hint for the profile, and the profile is the real evidence.
|
|
59
|
+
*/
|
|
60
|
+
export function setLoopBreadcrumb(label) {
|
|
61
|
+
_breadcrumb = label;
|
|
62
|
+
}
|
|
63
|
+
/** The current breadcrumb, or null. */
|
|
64
|
+
export function getLoopBreadcrumb() {
|
|
65
|
+
return _breadcrumb;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Start watching for event-loop blocks. Returns a stop function (idempotent).
|
|
69
|
+
*
|
|
70
|
+
* The interval is unref'd so it never keeps the process alive on its own.
|
|
71
|
+
*/
|
|
72
|
+
export function startEventLoopMonitor(opts) {
|
|
73
|
+
const { thresholdMs, onBlock } = opts;
|
|
74
|
+
const tickMs = opts.tickMs && opts.tickMs > 0 ? opts.tickMs : 500;
|
|
75
|
+
if (!(thresholdMs > 0))
|
|
76
|
+
return () => { };
|
|
77
|
+
let expectedAt = performance.now() + tickMs;
|
|
78
|
+
const timer = setInterval(() => {
|
|
79
|
+
const actualAt = performance.now();
|
|
80
|
+
const blockedMs = detectBlock(expectedAt, actualAt, thresholdMs);
|
|
81
|
+
expectedAt = actualAt + tickMs;
|
|
82
|
+
if (blockedMs === null)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
onBlock({
|
|
86
|
+
blockedMs,
|
|
87
|
+
breadcrumb: getLoopBreadcrumb(),
|
|
88
|
+
detectedAt: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
logger.error("cli", `[event-loop-monitor] onBlock handler threw: ${err?.message}`, {
|
|
93
|
+
error: err,
|
|
94
|
+
blockedMs,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}, tickMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
let stopped = false;
|
|
100
|
+
return () => {
|
|
101
|
+
if (stopped)
|
|
102
|
+
return;
|
|
103
|
+
stopped = true;
|
|
104
|
+
clearInterval(timer);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=event-loop-monitor.js.map
|
|
@@ -50,3 +50,17 @@ export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: nu
|
|
|
50
50
|
* The short grace lets the normal fetch-level abort win first when it's quick.
|
|
51
51
|
*/
|
|
52
52
|
abortSignal?: AbortSignal, abortGraceMs?: number): Promise<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Wall-clock backstop (ms) for an isolated sub-agent task (`runIsolatedTask`).
|
|
55
|
+
*
|
|
56
|
+
* The council `generate()` chokepoint and the sprint impl/verify stages already
|
|
57
|
+
* have deadlines, but the *other* `runIsolatedTask` await points — plan-adherence
|
|
58
|
+
* review + fix, council research, grounding-verify — were bare `await`s. A
|
|
59
|
+
* provider that hangs on the JS side after the sub-agent's stream finishes wedges
|
|
60
|
+
* the whole pipeline with no error (observed live: run mrhc43f0fb9b impl, and the
|
|
61
|
+
* scoping stall). Wrap those calls in `withDeadlineRace(fn, getIsolatedTaskDeadlineMs(), ...)`
|
|
62
|
+
* so a hang surfaces as a rejection the caller already handles. Generous by
|
|
63
|
+
* default so a legitimately long tool-using sub-agent is not cut short; override
|
|
64
|
+
* with MUONROI_IDEAL_ISOLATED_TASK_MS. Clamped to [60s, 30min].
|
|
65
|
+
*/
|
|
66
|
+
export declare function getIsolatedTaskDeadlineMs(): number;
|
|
@@ -106,4 +106,23 @@ abortSignal, abortGraceMs = 1500) {
|
|
|
106
106
|
abortSignal.removeEventListener("abort", abortListener);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Wall-clock backstop (ms) for an isolated sub-agent task (`runIsolatedTask`).
|
|
111
|
+
*
|
|
112
|
+
* The council `generate()` chokepoint and the sprint impl/verify stages already
|
|
113
|
+
* have deadlines, but the *other* `runIsolatedTask` await points — plan-adherence
|
|
114
|
+
* review + fix, council research, grounding-verify — were bare `await`s. A
|
|
115
|
+
* provider that hangs on the JS side after the sub-agent's stream finishes wedges
|
|
116
|
+
* the whole pipeline with no error (observed live: run mrhc43f0fb9b impl, and the
|
|
117
|
+
* scoping stall). Wrap those calls in `withDeadlineRace(fn, getIsolatedTaskDeadlineMs(), ...)`
|
|
118
|
+
* so a hang surfaces as a rejection the caller already handles. Generous by
|
|
119
|
+
* default so a legitimately long tool-using sub-agent is not cut short; override
|
|
120
|
+
* with MUONROI_IDEAL_ISOLATED_TASK_MS. Clamped to [60s, 30min].
|
|
121
|
+
*/
|
|
122
|
+
export function getIsolatedTaskDeadlineMs() {
|
|
123
|
+
const raw = Number.parseInt(process.env.MUONROI_IDEAL_ISOLATED_TASK_MS ?? "", 10);
|
|
124
|
+
if (Number.isFinite(raw) && raw >= 60_000 && raw <= 1_800_000)
|
|
125
|
+
return raw;
|
|
126
|
+
return 900_000; // 15 min backstop
|
|
127
|
+
}
|
|
109
128
|
//# sourceMappingURL=llm-deadline.js.map
|