mstro-app 0.5.1 → 0.5.5
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/PRIVACY.md +9 -9
- package/README.md +71 -28
- package/bin/commands/config.js +1 -1
- package/bin/mstro.js +55 -4
- package/dist/server/cli/eta-estimator.d.ts +55 -0
- package/dist/server/cli/eta-estimator.d.ts.map +1 -0
- package/dist/server/cli/eta-estimator.js +222 -0
- package/dist/server/cli/eta-estimator.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +64 -9
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +19 -12
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.js +5 -1
- package/dist/server/cli/improvisation-history-store.js.map +1 -1
- package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
- package/dist/server/cli/improvisation-output-queue.js +30 -7
- package/dist/server/cli/improvisation-output-queue.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +29 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +50 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +2 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/engines/EngineEvent.d.ts +126 -0
- package/dist/server/engines/EngineEvent.d.ts.map +1 -0
- package/dist/server/engines/EngineEvent.js +11 -0
- package/dist/server/engines/EngineEvent.js.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
- package/dist/server/engines/factory.d.ts +21 -0
- package/dist/server/engines/factory.d.ts.map +1 -0
- package/dist/server/engines/factory.js +152 -0
- package/dist/server/engines/factory.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
- package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
- package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
- package/dist/server/engines/opencode/model-catalog.js +141 -0
- package/dist/server/engines/opencode/model-catalog.js.map +1 -0
- package/dist/server/engines/types.d.ts +146 -0
- package/dist/server/engines/types.d.ts.map +1 -0
- package/dist/server/engines/types.js +4 -0
- package/dist/server/engines/types.js.map +1 -0
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +8 -124
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +45 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +69 -5
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/factory.d.ts +70 -0
- package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
- package/dist/server/mcp/classifier/factory.js +155 -0
- package/dist/server/mcp/classifier/factory.js.map +1 -0
- package/dist/server/services/plan/agent-resolver.d.ts +26 -0
- package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/agent-resolver.js +102 -0
- package/dist/server/services/plan/agent-resolver.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +59 -11
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +33 -1
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/settings.d.ts +76 -2
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +127 -4
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +19 -6
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +17 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +54 -2
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-complexity.js +78 -26
- package/dist/server/services/websocket/quality-complexity.js.map +1 -1
- package/dist/server/services/websocket/quality-eta.d.ts +47 -0
- package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-eta.js +110 -0
- package/dist/server/services/websocket/quality-eta.js.map +1 -0
- package/dist/server/services/websocket/quality-grading.d.ts +27 -4
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-grading.js +369 -201
- package/dist/server/services/websocket/quality-grading.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +145 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-operations.d.ts +34 -0
- package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-operations.js +47 -0
- package/dist/server/services/websocket/quality-operations.js.map +1 -0
- package/dist/server/services/websocket/quality-persistence.d.ts +9 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +10 -0
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +105 -56
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +9 -1
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +334 -14
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +21 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +49 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +35 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +1 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +3 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +57 -9
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.js +3 -0
- package/dist/server/services/websocket/session-history.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +158 -42
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +25 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +19 -0
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +35 -4
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.js +10 -2
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.js +138 -12
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.js +55 -2
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +47 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +28 -5
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +10 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +5 -3
- package/server/cli/eta-estimator.ts +249 -0
- package/server/cli/headless/stall-assessor.ts +93 -0
- package/server/cli/headless/tool-watchdog.ts +21 -0
- package/server/cli/improvisation-history-store.ts +4 -1
- package/server/cli/improvisation-output-queue.ts +29 -7
- package/server/cli/improvisation-session-manager.ts +54 -1
- package/server/cli/improvisation-types.ts +2 -0
- package/server/engines/EngineEvent.ts +156 -0
- package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
- package/server/engines/factory.ts +176 -0
- package/server/engines/opencode/OpenCodeEngine.ts +786 -0
- package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
- package/server/engines/opencode/model-catalog.ts +217 -0
- package/server/engines/types.ts +173 -0
- package/server/index.ts +1 -1
- package/server/mcp/bouncer-haiku.ts +21 -145
- package/server/mcp/bouncer-integration.ts +107 -5
- package/server/mcp/classifier/BouncerClassifier.ts +40 -0
- package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
- package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
- package/server/mcp/classifier/factory.ts +195 -0
- package/server/services/plan/agent-resolver.ts +115 -0
- package/server/services/plan/agents/code-review.md +38 -8
- package/server/services/plan/composer.ts +63 -11
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/issue-prompt-builder.ts +39 -1
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/types.ts +4 -0
- package/server/services/settings.ts +161 -4
- package/server/services/websocket/git-branch-handlers.ts +20 -6
- package/server/services/websocket/handler.ts +59 -2
- package/server/services/websocket/quality-complexity.ts +80 -26
- package/server/services/websocket/quality-eta.ts +155 -0
- package/server/services/websocket/quality-grading.ts +445 -222
- package/server/services/websocket/quality-handlers.ts +153 -7
- package/server/services/websocket/quality-operations.ts +72 -0
- package/server/services/websocket/quality-persistence.ts +17 -0
- package/server/services/websocket/quality-review-agent.ts +154 -64
- package/server/services/websocket/quality-service.ts +361 -13
- package/server/services/websocket/quality-tools.ts +51 -0
- package/server/services/websocket/quality-types.ts +41 -2
- package/server/services/websocket/session-handlers.ts +64 -10
- package/server/services/websocket/session-history.ts +3 -0
- package/server/services/websocket/session-initialization.ts +189 -46
- package/server/services/websocket/session-registry.ts +37 -0
- package/server/services/websocket/settings-handlers.ts +41 -4
- package/server/services/websocket/tab-broadcast.ts +10 -2
- package/server/services/websocket/tab-event-buffer.ts +143 -11
- package/server/services/websocket/tab-event-replay.ts +70 -3
- package/server/services/websocket/tab-handlers.ts +53 -5
- package/server/services/websocket/types.ts +37 -5
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* best result, error classification) live in haiku-assessments.ts.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import type { EngineEvent } from '../../engines/EngineEvent.js';
|
|
13
14
|
import { loadSkillPrompt } from '../../services/plan/agent-loader.js';
|
|
14
15
|
import { spawnHaikuRaw } from './haiku-assessments.js';
|
|
15
16
|
import { hlog } from './headless-logger.js';
|
|
@@ -36,6 +37,98 @@ export interface StallVerdict {
|
|
|
36
37
|
reason: string;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Mutable tool-activity accumulator fed by an engine-agnostic `EngineEvent`
|
|
42
|
+
* stream. Consumed by {@link buildStallContext} to produce the tool-related
|
|
43
|
+
* fields of a {@link StallContext} without coupling to any specific engine's
|
|
44
|
+
* internal shapes.
|
|
45
|
+
*/
|
|
46
|
+
export interface ToolActivityState {
|
|
47
|
+
/** Tool calls observed by `tool.start` but not yet ended. */
|
|
48
|
+
pendingToolIds: Set<string>;
|
|
49
|
+
/** Names of tools still pending (used by the stall heuristic). */
|
|
50
|
+
pendingToolNames: Set<string>;
|
|
51
|
+
/** Map of toolId -> toolName so `tool.end` can drop names when the last id goes. */
|
|
52
|
+
pendingToolNameById: Map<string, string>;
|
|
53
|
+
/** Last tool name seen via `tool.start`. */
|
|
54
|
+
lastToolName?: string;
|
|
55
|
+
/** Short summary of the last tool input (url/query/command/prompt). */
|
|
56
|
+
lastToolInputSummary?: string;
|
|
57
|
+
/** Total number of `tool.start` events observed this session. */
|
|
58
|
+
totalToolCalls: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Allocate a fresh, empty tool-activity state. */
|
|
62
|
+
export function createToolActivityState(): ToolActivityState {
|
|
63
|
+
return {
|
|
64
|
+
pendingToolIds: new Set(),
|
|
65
|
+
pendingToolNames: new Set(),
|
|
66
|
+
pendingToolNameById: new Map(),
|
|
67
|
+
totalToolCalls: 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update a {@link ToolActivityState} from a single engine event. Non-tool
|
|
73
|
+
* events are ignored. This lets the stall assessor operate on any
|
|
74
|
+
* CodingAgentEngine's event stream without knowing the engine's internals.
|
|
75
|
+
*/
|
|
76
|
+
export function applyEngineEventToActivity(state: ToolActivityState, event: EngineEvent): void {
|
|
77
|
+
if (event.kind === 'tool.start') {
|
|
78
|
+
state.pendingToolIds.add(event.toolCallId);
|
|
79
|
+
state.pendingToolNames.add(event.toolName);
|
|
80
|
+
state.pendingToolNameById.set(event.toolCallId, event.toolName);
|
|
81
|
+
state.lastToolName = event.toolName;
|
|
82
|
+
state.lastToolInputSummary = summarizeToolInput(event.input);
|
|
83
|
+
state.totalToolCalls++;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (event.kind === 'tool.end') {
|
|
87
|
+
state.pendingToolIds.delete(event.toolCallId);
|
|
88
|
+
state.pendingToolNameById.delete(event.toolCallId);
|
|
89
|
+
// Only drop the name from pendingToolNames if no other pending call uses it.
|
|
90
|
+
const stillPending = Array.from(state.pendingToolNameById.values()).includes(event.toolName);
|
|
91
|
+
if (!stillPending) state.pendingToolNames.delete(event.toolName);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build a {@link StallContext} from an engine-agnostic activity state plus
|
|
97
|
+
* the caller-owned timing fields. The stall heuristics and Haiku assessment
|
|
98
|
+
* in this module already operate on {@link StallContext}, so they are now
|
|
99
|
+
* fully drivable by any CodingAgentEngine's event stream.
|
|
100
|
+
*/
|
|
101
|
+
export function buildStallContext(
|
|
102
|
+
activity: ToolActivityState,
|
|
103
|
+
timing: {
|
|
104
|
+
originalPrompt: string;
|
|
105
|
+
silenceMs: number;
|
|
106
|
+
elapsedTotalMs: number;
|
|
107
|
+
tokenSilenceMs?: number;
|
|
108
|
+
},
|
|
109
|
+
): StallContext {
|
|
110
|
+
return {
|
|
111
|
+
originalPrompt: timing.originalPrompt,
|
|
112
|
+
silenceMs: timing.silenceMs,
|
|
113
|
+
elapsedTotalMs: timing.elapsedTotalMs,
|
|
114
|
+
tokenSilenceMs: timing.tokenSilenceMs,
|
|
115
|
+
lastToolName: activity.lastToolName,
|
|
116
|
+
lastToolInputSummary: activity.lastToolInputSummary,
|
|
117
|
+
pendingToolCount: activity.pendingToolIds.size,
|
|
118
|
+
pendingToolNames: new Set(activity.pendingToolNames),
|
|
119
|
+
totalToolCalls: activity.totalToolCalls,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function summarizeToolInput(input: Record<string, unknown>): string | undefined {
|
|
124
|
+
if (input.url) return `URL: ${String(input.url).slice(0, 200)}`;
|
|
125
|
+
if (input.query) return `Query: ${String(input.query).slice(0, 200)}`;
|
|
126
|
+
if (input.command) return `Command: ${String(input.command).slice(0, 200)}`;
|
|
127
|
+
if (input.prompt) return `Prompt: ${String(input.prompt).slice(0, 200)}`;
|
|
128
|
+
const serialized = JSON.stringify(input);
|
|
129
|
+
return serialized ? serialized.slice(0, 200) : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
39
132
|
// ========== Fast Heuristic ==========
|
|
40
133
|
|
|
41
134
|
function hasSubagentPending(pendingNames: Set<string>, lastToolName: string | undefined, hasPendingTools: boolean): boolean {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 3. Haiku tiebreaker: optional AI assessment before killing ambiguous cases
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import type { EngineEvent } from '../../engines/EngineEvent.js';
|
|
16
17
|
import { hlog } from './headless-logger.js';
|
|
17
18
|
import type {
|
|
18
19
|
ExecutionCheckpoint,
|
|
@@ -349,6 +350,26 @@ export class ToolWatchdog {
|
|
|
349
350
|
}, extensionMs);
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Drive the watchdog from an engine-agnostic `EngineEvent` stream.
|
|
355
|
+
* Routes `tool.start` to `startWatch`, and `tool.end` to `clearWatch` +
|
|
356
|
+
* `recordCompletion` — so any CodingAgentEngine (Claude Code, OpenCode)
|
|
357
|
+
* can feed this watchdog without leaking engine-specific shapes.
|
|
358
|
+
* Non-tool events are ignored.
|
|
359
|
+
*/
|
|
360
|
+
onEngineEvent(event: EngineEvent, onTimeout: (toolId: string) => void): void {
|
|
361
|
+
if (event.kind === 'tool.start') {
|
|
362
|
+
this.startWatch(event.toolCallId, event.toolName, event.input, () => onTimeout(event.toolCallId));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (event.kind === 'tool.end') {
|
|
366
|
+
this.clearWatch(event.toolCallId);
|
|
367
|
+
if (typeof event.durationMs === 'number' && event.durationMs >= 0) {
|
|
368
|
+
this.recordCompletion(event.toolName, event.durationMs);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
352
373
|
/** Stop watching a tool (it completed normally) */
|
|
353
374
|
clearWatch(toolId: string): void {
|
|
354
375
|
const watch = this.activeWatches.get(toolId);
|
|
@@ -39,7 +39,9 @@ export function loadHistory(historyPath: string, sessionId: string): SessionHist
|
|
|
39
39
|
if (existsSync(historyPath)) {
|
|
40
40
|
try {
|
|
41
41
|
const data = readFileSync(historyPath, 'utf-8');
|
|
42
|
-
|
|
42
|
+
const parsed = JSON.parse(data) as SessionHistory;
|
|
43
|
+
if (!parsed.engine) parsed.engine = 'claude-code';
|
|
44
|
+
return parsed;
|
|
43
45
|
} catch (error) {
|
|
44
46
|
herror('Failed to load history:', error);
|
|
45
47
|
}
|
|
@@ -51,6 +53,7 @@ export function loadHistory(historyPath: string, sessionId: string): SessionHist
|
|
|
51
53
|
lastActivityAt: now,
|
|
52
54
|
totalTokens: 0,
|
|
53
55
|
movements: [],
|
|
56
|
+
engine: 'claude-code',
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -4,12 +4,29 @@
|
|
|
4
4
|
* Small FIFO output buffer with a fixed-interval flush timer, used by the
|
|
5
5
|
* improvisation session manager to coalesce rapid stdout writes into
|
|
6
6
|
* steady `onOutput` emissions.
|
|
7
|
+
*
|
|
8
|
+
* ## Why coalesce inside `flush()`
|
|
9
|
+
*
|
|
10
|
+
* Claude's stdout arrives as many small chunks during streaming. Each chunk
|
|
11
|
+
* lands here via `queue_`. When `flush()` ran one `onEmit` per queued chunk,
|
|
12
|
+
* a streaming-heavy run produced thousands of `onOutput` events per minute,
|
|
13
|
+
* each becoming a tab-scoped broadcast that consumes a slot in the per-tab
|
|
14
|
+
* replay buffer (`tab-event-buffer.ts`). For 14-min runs with ~120 tool
|
|
15
|
+
* calls, that easily exceeded the buffer's 1000-event cap and triggered
|
|
16
|
+
* silent replay gaps on web reconnect.
|
|
17
|
+
*
|
|
18
|
+
* The flush window (50ms) is below the human-perceptible paint threshold and
|
|
19
|
+
* below WebSocket roundtrip latency, so concatenating all queued text into a
|
|
20
|
+
* single `onEmit` per tick is invisible to the user but cuts buffer pressure
|
|
21
|
+
* by 3-10× during streaming. No call site downstream depends on chunk
|
|
22
|
+
* boundaries — `onOutput` consumers (terminal renderer, history persistence)
|
|
23
|
+
* already treat the text as an opaque append.
|
|
7
24
|
*/
|
|
8
25
|
|
|
9
26
|
const FLUSH_INTERVAL_MS = 50;
|
|
10
27
|
|
|
11
28
|
export class OutputQueue {
|
|
12
|
-
private queue:
|
|
29
|
+
private queue: string[] = [];
|
|
13
30
|
private timer: NodeJS.Timeout | null = null;
|
|
14
31
|
|
|
15
32
|
constructor(private readonly onEmit: (text: string) => void) {}
|
|
@@ -20,15 +37,20 @@ export class OutputQueue {
|
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
queue_(text: string): void {
|
|
23
|
-
|
|
40
|
+
if (text.length === 0) return;
|
|
41
|
+
this.queue.push(text);
|
|
24
42
|
}
|
|
25
43
|
|
|
26
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Drain all buffered entries, emitting them as a single concatenated
|
|
46
|
+
* string via `onEmit`. Order is preserved (FIFO). No-op when the queue is
|
|
47
|
+
* empty so the periodic timer doesn't fire spurious empty-string emits.
|
|
48
|
+
*/
|
|
27
49
|
flush(): void {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
if (this.queue.length === 0) return;
|
|
51
|
+
const merged = this.queue.join('');
|
|
52
|
+
this.queue.length = 0;
|
|
53
|
+
this.onEmit(merged);
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
/** Stop the flush timer. Does NOT drain; call `flush()` first if needed. */
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
import { EventEmitter } from 'node:events';
|
|
17
17
|
import { existsSync, readFileSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
+
import { createEngine } from '../engines/factory.js';
|
|
20
|
+
import type { EngineId } from '../engines/types.js';
|
|
19
21
|
import { AnalyticsEvents, trackEvent } from '../services/analytics.js';
|
|
22
|
+
import { type EtaProfile, getEtaProfileCached } from './eta-estimator.js';
|
|
20
23
|
import { herror } from './headless/headless-logger.js';
|
|
21
24
|
import { cleanupAttachments, preparePromptAndAttachments } from './improvisation-attachments.js';
|
|
22
25
|
import {
|
|
@@ -55,6 +58,12 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
55
58
|
private history: SessionHistory;
|
|
56
59
|
private currentRunner: import('./headless/index.js').HeadlessRunner | null = null;
|
|
57
60
|
private options: ImprovisationOptions;
|
|
61
|
+
/**
|
|
62
|
+
* Coding-agent backend identifier. Routed through `createEngine` from
|
|
63
|
+
* `engines/factory.ts` so Epic 3 can swap in OpenCodeEngine by changing
|
|
64
|
+
* this value — the retry loop and headless runner remain unchanged.
|
|
65
|
+
*/
|
|
66
|
+
private engineId: EngineId = 'claude-code';
|
|
58
67
|
private pendingApproval?: {
|
|
59
68
|
plan: unknown;
|
|
60
69
|
resolve: (approved: boolean) => void;
|
|
@@ -73,6 +82,13 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
73
82
|
private _currentUserPrompt: string = '';
|
|
74
83
|
private _currentSequenceNumber: number = 0;
|
|
75
84
|
private _hasPersistedToDisk: boolean = false;
|
|
85
|
+
/**
|
|
86
|
+
* Cached duration-quantile profile used by the web "Composing" indicator
|
|
87
|
+
* to render an ETA. Built lazily on first executePrompt and refreshed by
|
|
88
|
+
* the eta-estimator's TTL cache. Null means "not enough history yet" — the
|
|
89
|
+
* web falls back to elapsed-only display.
|
|
90
|
+
*/
|
|
91
|
+
private _etaProfile: EtaProfile | null = null;
|
|
76
92
|
|
|
77
93
|
static resumeFromHistory(workingDir: string, historicalSessionId: string, overrides?: Partial<ImprovisationOptions>): ImprovisationSessionManager {
|
|
78
94
|
const historyDir = join(workingDir, '.mstro', 'history');
|
|
@@ -84,6 +100,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
const historyData = JSON.parse(readFileSync(historyPath, 'utf-8')) as SessionHistory;
|
|
103
|
+
if (!historyData.engine) historyData.engine = 'claude-code';
|
|
87
104
|
const manager = new ImprovisationSessionManager({
|
|
88
105
|
workingDir,
|
|
89
106
|
sessionId: historyData.sessionId,
|
|
@@ -126,6 +143,11 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
126
143
|
this.historyPath = paths.historyPath;
|
|
127
144
|
ensureHistoryDir(this.improviseDir);
|
|
128
145
|
|
|
146
|
+
// Validate the engine is available via the factory. Epic 3 will extend
|
|
147
|
+
// `createEngine` to also return OpenCodeEngine; today this asserts that
|
|
148
|
+
// a known engine id was requested and surfaces config errors early.
|
|
149
|
+
createEngine(this.engineId);
|
|
150
|
+
|
|
129
151
|
this.history = loadHistory(this.historyPath, this.sessionId);
|
|
130
152
|
// History is persisted lazily on the first `persistHistory` call (see
|
|
131
153
|
// `executePrompt`). Deferring the initial write keeps the Chat History
|
|
@@ -165,7 +187,8 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
165
187
|
const sequenceNumber = this.history.movements.length + 1;
|
|
166
188
|
this._currentUserPrompt = displayPrompt;
|
|
167
189
|
this._currentSequenceNumber = sequenceNumber;
|
|
168
|
-
this.
|
|
190
|
+
await this.refreshEtaProfile();
|
|
191
|
+
this.emit('onMovementStart', sequenceNumber, displayPrompt, isAutoContinue, this._etaProfile);
|
|
169
192
|
trackEvent(AnalyticsEvents.IMPROVISE_PROMPT_RECEIVED, {
|
|
170
193
|
prompt_length: userPrompt.length,
|
|
171
194
|
has_attachments: !!(attachments && attachments.length > 0),
|
|
@@ -499,6 +522,22 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
499
522
|
});
|
|
500
523
|
}
|
|
501
524
|
|
|
525
|
+
// ========== ETA profile ==========
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Resolve the duration-quantile profile before announcing the movement
|
|
529
|
+
* so the web indicator can render an ETA from t=0. The cache amortizes
|
|
530
|
+
* I/O across prompts; only the very first prompt of a project pays the
|
|
531
|
+
* ~50–100ms scan cost. Failures degrade silently to "no ETA".
|
|
532
|
+
*/
|
|
533
|
+
private async refreshEtaProfile(): Promise<void> {
|
|
534
|
+
try {
|
|
535
|
+
this._etaProfile = await getEtaProfileCached(this.improviseDir);
|
|
536
|
+
} catch {
|
|
537
|
+
this._etaProfile = null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
502
541
|
// ========== History I/O ==========
|
|
503
542
|
|
|
504
543
|
private persistHistory(): void {
|
|
@@ -588,10 +627,24 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
588
627
|
return this._isExecuting;
|
|
589
628
|
}
|
|
590
629
|
|
|
630
|
+
/**
|
|
631
|
+
* AI engine that produced this session. Read from SessionHistory (populated
|
|
632
|
+
* on load/creation). Defaults to 'claude-code' if the history record is
|
|
633
|
+
* missing the field (older sessions).
|
|
634
|
+
*/
|
|
635
|
+
get engine(): string {
|
|
636
|
+
return this.history.engine || 'claude-code';
|
|
637
|
+
}
|
|
638
|
+
|
|
591
639
|
get executionStartTimestamp(): number | undefined {
|
|
592
640
|
return this._executionStartTimestamp;
|
|
593
641
|
}
|
|
594
642
|
|
|
643
|
+
/** Most recently resolved ETA quantile profile, or null if none yet built. */
|
|
644
|
+
get etaProfile(): EtaProfile | null {
|
|
645
|
+
return this._etaProfile;
|
|
646
|
+
}
|
|
647
|
+
|
|
595
648
|
getExecutionEventLog(): Array<{ type: string; data: unknown; timestamp: number }> {
|
|
596
649
|
return this.executionEventLog;
|
|
597
650
|
}
|
|
@@ -66,6 +66,8 @@ export interface SessionHistory {
|
|
|
66
66
|
totalTokens: number;
|
|
67
67
|
movements: MovementRecord[];
|
|
68
68
|
claudeSessionId?: string;
|
|
69
|
+
/** AI engine that produced this session (e.g. 'claude-code', 'opencode'). Older histories default to 'claude-code' on read. */
|
|
70
|
+
engine: string;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/** Entry in the retry log for debugging recovery paths */
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EngineEvent — the engine-agnostic event stream produced by every
|
|
6
|
+
* CodingAgentEngine (see ./types.ts).
|
|
7
|
+
*
|
|
8
|
+
* Contract:
|
|
9
|
+
* - Every event has a `kind` discriminator, a `sessionId` (the engine's own
|
|
10
|
+
* session identifier — Claude Code session id, OpenCode session id, etc.),
|
|
11
|
+
* and a `timestamp` in Unix ms.
|
|
12
|
+
* - Payloads must carry enough information to populate OutputLine in the web
|
|
13
|
+
* client without the consumer knowing which engine produced them. Do not
|
|
14
|
+
* leak engine-specific fields (e.g. Claude tool_use_ids, OpenCode part ids)
|
|
15
|
+
* through typed event fields — keep them in `raw` when needed for debugging.
|
|
16
|
+
* - Ordering is guaranteed within a single session: for a given tool call,
|
|
17
|
+
* `tool.start` precedes `tool.end`; `usage.update` values are monotonic.
|
|
18
|
+
* - `session.idle` marks the end of a turn (assistant finished responding),
|
|
19
|
+
* not the end of the session. Multiple idle events per session are normal.
|
|
20
|
+
* - `engine.error` with `fatal: true` is terminal — the async iterator must
|
|
21
|
+
* complete after emitting it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Identifier for which concrete engine produced an event. */
|
|
25
|
+
export type EngineId = 'claude-code' | 'opencode';
|
|
26
|
+
|
|
27
|
+
/** Fields shared by every engine event. */
|
|
28
|
+
interface EngineEventBase {
|
|
29
|
+
/** Engine-reported session id (e.g. Claude Code session id, OpenCode session id). */
|
|
30
|
+
sessionId: string;
|
|
31
|
+
/** Unix epoch milliseconds when the engine observed this event. */
|
|
32
|
+
timestamp: number;
|
|
33
|
+
/** Optional raw payload from the engine for debugging/audit. Must not be used for business logic. */
|
|
34
|
+
raw?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Streaming assistant text (user-visible output). */
|
|
38
|
+
export interface MessageDeltaEvent extends EngineEventBase {
|
|
39
|
+
kind: 'message.delta';
|
|
40
|
+
/** Incremental chunk of assistant-visible text. Consumers concatenate. */
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Streaming assistant thinking/reasoning text (collapsed by default in UI). */
|
|
45
|
+
export interface MessageThinkingEvent extends EngineEventBase {
|
|
46
|
+
kind: 'message.thinking';
|
|
47
|
+
/** Incremental chunk of thinking text. */
|
|
48
|
+
text: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A tool invocation has started. */
|
|
52
|
+
export interface ToolStartEvent extends EngineEventBase {
|
|
53
|
+
kind: 'tool.start';
|
|
54
|
+
/** Engine-agnostic tool call id (unique within the session). */
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
/** Name of the tool (e.g. "Read", "Bash"). */
|
|
57
|
+
toolName: string;
|
|
58
|
+
/**
|
|
59
|
+
* Arguments passed to the tool. May be partial at start — some engines
|
|
60
|
+
* stream arguments. Consumers should treat this as best-effort until
|
|
61
|
+
* `tool.end` arrives with the authoritative input.
|
|
62
|
+
*/
|
|
63
|
+
input: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A tool invocation has completed (successfully or with an error). */
|
|
67
|
+
export interface ToolEndEvent extends EngineEventBase {
|
|
68
|
+
kind: 'tool.end';
|
|
69
|
+
toolCallId: string;
|
|
70
|
+
toolName: string;
|
|
71
|
+
/** Authoritative tool input as executed. */
|
|
72
|
+
input: Record<string, unknown>;
|
|
73
|
+
/** Serialized tool result (stdout, file contents, JSON, etc.). */
|
|
74
|
+
result: string;
|
|
75
|
+
/** True if the tool returned an error. */
|
|
76
|
+
isError: boolean;
|
|
77
|
+
/** Wall-clock duration in ms between tool.start and tool.end. */
|
|
78
|
+
durationMs: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The engine is asking whether a tool call should proceed. Consumed by
|
|
83
|
+
* the Bouncer in Epic 4 which must resolve the request via the engine's
|
|
84
|
+
* matching permission-response channel.
|
|
85
|
+
*/
|
|
86
|
+
export interface PermissionRequestEvent extends EngineEventBase {
|
|
87
|
+
kind: 'permission.request';
|
|
88
|
+
/** Opaque id the engine will expect echoed back in a decision. */
|
|
89
|
+
requestId: string;
|
|
90
|
+
toolName: string;
|
|
91
|
+
/** Tool arguments to be classified. */
|
|
92
|
+
input: Record<string, unknown>;
|
|
93
|
+
/** Engine-provided reason string, if any. */
|
|
94
|
+
reason?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Session returned to idle — the assistant finished its current turn.
|
|
99
|
+
* Not the end of the session; a new prompt may still be sent.
|
|
100
|
+
*/
|
|
101
|
+
export interface SessionIdleEvent extends EngineEventBase {
|
|
102
|
+
kind: 'session.idle';
|
|
103
|
+
/** Engine's stop reason if known (e.g. 'end_turn', 'max_tokens'). */
|
|
104
|
+
stopReason?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Running token counts. Values are cumulative across the session
|
|
109
|
+
* (not per-turn) and must be monotonically non-decreasing.
|
|
110
|
+
*/
|
|
111
|
+
export interface UsageUpdateEvent extends EngineEventBase {
|
|
112
|
+
kind: 'usage.update';
|
|
113
|
+
inputTokens: number;
|
|
114
|
+
outputTokens: number;
|
|
115
|
+
cacheCreationTokens?: number;
|
|
116
|
+
cacheReadTokens?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* An engine-level error occurred. With `fatal: true`, the session is
|
|
121
|
+
* unrecoverable and the async iterator completes after this event.
|
|
122
|
+
*/
|
|
123
|
+
export interface EngineErrorEvent extends EngineEventBase {
|
|
124
|
+
kind: 'engine.error';
|
|
125
|
+
/** Short error code for UI mapping (see ClaudeErrorCode in web/src/types/output.ts). */
|
|
126
|
+
code: string;
|
|
127
|
+
/** Human-readable message. */
|
|
128
|
+
message: string;
|
|
129
|
+
/** True if the session is unrecoverable and should be torn down. */
|
|
130
|
+
fatal: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Discriminated union of every event a CodingAgentEngine may emit. */
|
|
134
|
+
export type EngineEvent =
|
|
135
|
+
| MessageDeltaEvent
|
|
136
|
+
| MessageThinkingEvent
|
|
137
|
+
| ToolStartEvent
|
|
138
|
+
| ToolEndEvent
|
|
139
|
+
| PermissionRequestEvent
|
|
140
|
+
| SessionIdleEvent
|
|
141
|
+
| UsageUpdateEvent
|
|
142
|
+
| EngineErrorEvent;
|
|
143
|
+
|
|
144
|
+
/** Narrow helper — returns true for events that carry user-visible text. */
|
|
145
|
+
export function isMessageEvent(
|
|
146
|
+
event: EngineEvent,
|
|
147
|
+
): event is MessageDeltaEvent | MessageThinkingEvent {
|
|
148
|
+
return event.kind === 'message.delta' || event.kind === 'message.thinking';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Narrow helper — returns true for the tool lifecycle events. */
|
|
152
|
+
export function isToolEvent(
|
|
153
|
+
event: EngineEvent,
|
|
154
|
+
): event is ToolStartEvent | ToolEndEvent {
|
|
155
|
+
return event.kind === 'tool.start' || event.kind === 'tool.end';
|
|
156
|
+
}
|