mstro-app 0.5.1 → 0.5.6
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/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +9 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +22 -5
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +7 -5
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +19 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- 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/headless/types.d.ts +16 -1
- package/dist/server/cli/headless/types.d.ts.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 +35 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +58 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +9 -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/cli/retry/retry-runner-factory.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.js +1 -0
- package/dist/server/cli/retry/retry-runner-factory.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 +9 -2
- 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/mcp/server.js +52 -0
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +1 -0
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/internal.d.ts +16 -0
- package/dist/server/routes/internal.d.ts.map +1 -0
- package/dist/server/routes/internal.js +94 -0
- package/dist/server/routes/internal.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/runtime-info.d.ts +3 -0
- package/dist/server/services/runtime-info.d.ts.map +1 -0
- package/dist/server/services/runtime-info.js +21 -0
- package/dist/server/services/runtime-info.js.map +1 -0
- 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/ask-user-question-bridge.d.ts +32 -0
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts.map +1 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js +115 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js.map +1 -0
- 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 +25 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +84 -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 +60 -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 +67 -7
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +12 -6
- 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/claude-invoker-process.ts +9 -1
- package/server/cli/headless/mcp-config.ts +30 -5
- package/server/cli/headless/runner.ts +21 -0
- package/server/cli/headless/stall-assessor.ts +93 -0
- package/server/cli/headless/tool-watchdog.ts +21 -0
- package/server/cli/headless/types.ts +16 -1
- 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 +63 -1
- package/server/cli/improvisation-types.ts +9 -0
- package/server/cli/retry/retry-runner-factory.ts +1 -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 +9 -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/mcp/server.ts +57 -0
- package/server/routes/index.ts +1 -0
- package/server/routes/internal.ts +112 -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/runtime-info.ts +24 -0
- package/server/services/settings.ts +161 -4
- package/server/services/websocket/ask-user-question-bridge.ts +148 -0
- package/server/services/websocket/git-branch-handlers.ts +20 -6
- package/server/services/websocket/handler.ts +89 -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 +67 -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 +85 -7
|
@@ -42,22 +42,59 @@ export interface BufferedEvent {
|
|
|
42
42
|
data: unknown
|
|
43
43
|
/** `Date.now()` at record time. Used for age-based eviction. */
|
|
44
44
|
timestamp: number
|
|
45
|
+
/**
|
|
46
|
+
* Approximate serialized byte size of `data`. Computed once at record
|
|
47
|
+
* time so eviction can enforce a memory cap without re-stringifying on
|
|
48
|
+
* every check. Type and seq overhead is small; we only bill `data` here.
|
|
49
|
+
*/
|
|
50
|
+
byteSize: number
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/**
|
|
48
54
|
* Bounded replay log for a single tab.
|
|
49
55
|
*
|
|
50
|
-
* Size/age limits are parameterised for testability but defaulted to
|
|
51
|
-
* that comfortably cover real-world reconnect windows
|
|
56
|
+
* Size/age/byte limits are parameterised for testability but defaulted to
|
|
57
|
+
* values that comfortably cover real-world reconnect windows for long-running
|
|
58
|
+
* coding-agent tasks (multi-tool, multi-minute).
|
|
59
|
+
*
|
|
60
|
+
* ## Replay-gap detection
|
|
61
|
+
*
|
|
62
|
+
* The buffer tracks `evictedThroughSeq` — the highest seq that has ever been
|
|
63
|
+
* evicted (0 if nothing has been evicted). A web client whose `lastSeenSeq`
|
|
64
|
+
* is below this value has missed events the buffer can no longer supply, and
|
|
65
|
+
* an incremental replay would produce a silent gap. Callers should consult
|
|
66
|
+
* `hasGapSince` before relying on `getSince` for incremental replay; on a
|
|
67
|
+
* gap they should fall back to a full snapshot path (e.g. `outputHistory`).
|
|
68
|
+
*
|
|
69
|
+
* ## Eviction is FIFO with three caps
|
|
70
|
+
*
|
|
71
|
+
* Events are evicted from the front when ANY of these limits is exceeded:
|
|
72
|
+
* - count: `maxEvents` (default 10k)
|
|
73
|
+
* - age: `maxAgeMs` (default 60 min)
|
|
74
|
+
* - bytes: `maxTotalBytes` (default 32 MB)
|
|
75
|
+
*
|
|
76
|
+
* The byte cap is the safety belt against pathological events (e.g. a 50 MB
|
|
77
|
+
* grep result streamed as one event). Without it, count- and age-based caps
|
|
78
|
+
* still allow a single tab to hoard arbitrary memory.
|
|
52
79
|
*/
|
|
53
80
|
export class TabEventBuffer {
|
|
54
81
|
private readonly events: BufferedEvent[] = []
|
|
55
82
|
private nextSeq = 1
|
|
83
|
+
/**
|
|
84
|
+
* Highest seq that has been evicted from the buffer. 0 means nothing has
|
|
85
|
+
* been evicted yet (buffer is operating within its window). Monotonically
|
|
86
|
+
* non-decreasing — eviction always happens from the front of the FIFO, in
|
|
87
|
+
* seq order, so the most recently evicted seq is always the highest.
|
|
88
|
+
*/
|
|
89
|
+
private evictedThroughSeq = 0
|
|
90
|
+
/** Approximate sum of `byteSize` over still-resident events. */
|
|
91
|
+
private totalBytes = 0
|
|
56
92
|
|
|
57
93
|
constructor(
|
|
58
94
|
private readonly maxEvents: number = DEFAULT_MAX_EVENTS,
|
|
59
95
|
private readonly maxAgeMs: number = DEFAULT_MAX_AGE_MS,
|
|
60
96
|
private readonly now: () => number = Date.now,
|
|
97
|
+
private readonly maxTotalBytes: number = DEFAULT_MAX_TOTAL_BYTES,
|
|
61
98
|
) {}
|
|
62
99
|
|
|
63
100
|
/**
|
|
@@ -69,7 +106,9 @@ export class TabEventBuffer {
|
|
|
69
106
|
*/
|
|
70
107
|
record(type: string, data: unknown): number {
|
|
71
108
|
const seq = this.nextSeq++
|
|
72
|
-
|
|
109
|
+
const byteSize = estimateByteSize(data)
|
|
110
|
+
this.events.push({ seq, type, data, timestamp: this.now(), byteSize })
|
|
111
|
+
this.totalBytes += byteSize
|
|
73
112
|
this.evict()
|
|
74
113
|
return seq
|
|
75
114
|
}
|
|
@@ -78,6 +117,11 @@ export class TabEventBuffer {
|
|
|
78
117
|
* Return all still-buffered events with `seq > afterSeq`, in original
|
|
79
118
|
* order. Returns an empty array if nothing newer is buffered (either the
|
|
80
119
|
* web is caught up or the window has rolled past).
|
|
120
|
+
*
|
|
121
|
+
* NOTE: This does not detect or signal replay gaps. Pair with
|
|
122
|
+
* `hasGapSince(afterSeq)` to know whether a returned array is a complete
|
|
123
|
+
* incremental replay or a partial one (events between `afterSeq` and the
|
|
124
|
+
* oldest surviving seq have been evicted and are no longer available).
|
|
81
125
|
*/
|
|
82
126
|
getSince(afterSeq: number): BufferedEvent[] {
|
|
83
127
|
this.evict()
|
|
@@ -88,6 +132,29 @@ export class TabEventBuffer {
|
|
|
88
132
|
return out
|
|
89
133
|
}
|
|
90
134
|
|
|
135
|
+
/**
|
|
136
|
+
* True when an incremental replay starting from `afterSeq` would silently
|
|
137
|
+
* skip events that the buffer has already evicted. Used by the replay
|
|
138
|
+
* orchestrator to decide whether to fall back to a full snapshot rather
|
|
139
|
+
* than emit a partial event stream the web can't reconstruct.
|
|
140
|
+
*
|
|
141
|
+
* `afterSeq < evictedThroughSeq` means the next event the caller expects
|
|
142
|
+
* (`afterSeq + 1`) is at or below the eviction frontier — that event has
|
|
143
|
+
* already been dropped from memory.
|
|
144
|
+
*/
|
|
145
|
+
hasGapSince(afterSeq: number): boolean {
|
|
146
|
+
this.evict()
|
|
147
|
+
return afterSeq < this.evictedThroughSeq
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Highest seq that has been evicted from this buffer; 0 if nothing has been
|
|
152
|
+
* evicted yet. Exposed for telemetry and gap-recovery decisions.
|
|
153
|
+
*/
|
|
154
|
+
getEvictedThroughSeq(): number {
|
|
155
|
+
return this.evictedThroughSeq
|
|
156
|
+
}
|
|
157
|
+
|
|
91
158
|
/** Current highest assigned seq (monotonic; not reset by eviction). */
|
|
92
159
|
currentSeq(): number {
|
|
93
160
|
return this.nextSeq - 1
|
|
@@ -98,20 +165,66 @@ export class TabEventBuffer {
|
|
|
98
165
|
return this.events.length
|
|
99
166
|
}
|
|
100
167
|
|
|
168
|
+
/** Approximate bytes held by `data` payloads currently in memory. For tests/telemetry. */
|
|
169
|
+
byteSize(): number {
|
|
170
|
+
return this.totalBytes
|
|
171
|
+
}
|
|
172
|
+
|
|
101
173
|
/**
|
|
102
174
|
* Drop events older than `maxAgeMs` from the front, then enforce
|
|
103
|
-
* `maxEvents` by trimming the front further if needed.
|
|
104
|
-
* newest events — they're the ones the web is most
|
|
175
|
+
* `maxEvents` and `maxTotalBytes` by trimming the front further if needed.
|
|
176
|
+
* Eviction keeps the newest events — they're the ones the web is most
|
|
177
|
+
* likely to still need.
|
|
178
|
+
*
|
|
179
|
+
* Each evicted seq advances `evictedThroughSeq` so callers can detect
|
|
180
|
+
* replay gaps. The FIFO ensures we always evict in seq order, so the last
|
|
181
|
+
* evicted seq is always the highest seen so far.
|
|
182
|
+
*
|
|
183
|
+
* The byte cap is enforced LAST so that count- and age-based eviction get
|
|
184
|
+
* a chance first; a chatty-but-small session evicts on age before it ever
|
|
185
|
+
* touches the byte cap, which keeps the usual case predictable.
|
|
105
186
|
*/
|
|
106
187
|
private evict(): void {
|
|
107
188
|
const cutoff = this.now() - this.maxAgeMs
|
|
108
189
|
while (this.events.length > 0 && this.events[0].timestamp < cutoff) {
|
|
109
|
-
this.
|
|
190
|
+
this.popOldest()
|
|
110
191
|
}
|
|
111
192
|
while (this.events.length > this.maxEvents) {
|
|
112
|
-
this.
|
|
193
|
+
this.popOldest()
|
|
194
|
+
}
|
|
195
|
+
while (this.events.length > 0 && this.totalBytes > this.maxTotalBytes) {
|
|
196
|
+
this.popOldest()
|
|
113
197
|
}
|
|
114
198
|
}
|
|
199
|
+
|
|
200
|
+
private popOldest(): void {
|
|
201
|
+
const evicted = this.events.shift()
|
|
202
|
+
if (!evicted) return
|
|
203
|
+
this.evictedThroughSeq = evicted.seq
|
|
204
|
+
this.totalBytes -= evicted.byteSize
|
|
205
|
+
if (this.totalBytes < 0) this.totalBytes = 0
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Estimate `data`'s serialized byte size for the eviction byte cap. Uses
|
|
211
|
+
* `JSON.stringify` because that's what hits the wire; falls back to a small
|
|
212
|
+
* default on circular structures so we don't crash the broadcast path.
|
|
213
|
+
*
|
|
214
|
+
* `Buffer.byteLength` would give us UTF-8 bytes vs UTF-16 code units, but on
|
|
215
|
+
* Node `JSON.stringify(...).length` is close enough (within a small constant
|
|
216
|
+
* factor for ASCII-heavy payloads) and avoids an extra allocation.
|
|
217
|
+
*/
|
|
218
|
+
function estimateByteSize(data: unknown): number {
|
|
219
|
+
if (data === undefined || data === null) return 0
|
|
220
|
+
try {
|
|
221
|
+
return JSON.stringify(data).length
|
|
222
|
+
} catch {
|
|
223
|
+
// Circular reference, BigInt, etc. — bill a small fixed cost so the
|
|
224
|
+
// byte cap still has SOME signal. We won't be able to wire-serialize
|
|
225
|
+
// this either, but that's a separate problem.
|
|
226
|
+
return 256
|
|
227
|
+
}
|
|
115
228
|
}
|
|
116
229
|
|
|
117
230
|
/**
|
|
@@ -152,7 +265,26 @@ export class TabEventBufferRegistry {
|
|
|
152
265
|
}
|
|
153
266
|
}
|
|
154
267
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
268
|
+
/**
|
|
269
|
+
* 10,000 events per tab.
|
|
270
|
+
*
|
|
271
|
+
* Sized for long-running coding-agent tasks (multi-tool, multi-minute) plus
|
|
272
|
+
* laptop sleep/wake reconnect windows. Worst-case observed: a 14-minute
|
|
273
|
+
* session with ~120 tool calls produces ~1.5–3k tab-scoped events; 10× that
|
|
274
|
+
* gives headroom for parallel agents and chatty improvisation. Memory
|
|
275
|
+
* footprint at ~500B/event = ~5MB per tab; the local-only single-tenant
|
|
276
|
+
* deployment makes this a non-issue.
|
|
277
|
+
*/
|
|
278
|
+
export const DEFAULT_MAX_EVENTS = 10_000
|
|
279
|
+
/**
|
|
280
|
+
* 60 minutes of history. Covers laptop sleep/wake, long meetings between
|
|
281
|
+
* sessions, and the largest plausible reconnect window that a tab might
|
|
282
|
+
* legitimately want to recover incrementally instead of starting fresh.
|
|
283
|
+
*/
|
|
284
|
+
export const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000
|
|
285
|
+
/**
|
|
286
|
+
* 32 MB safety belt against pathological events (large grep results, full
|
|
287
|
+
* file reads streamed inline). Eviction by bytes guarantees a single tab
|
|
288
|
+
* can't hoard arbitrary memory regardless of count/age limits.
|
|
289
|
+
*/
|
|
290
|
+
export const DEFAULT_MAX_TOTAL_BYTES = 32 * 1024 * 1024
|
|
@@ -13,23 +13,88 @@
|
|
|
13
13
|
* events live.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { captureException } from '../sentry.js'
|
|
16
17
|
import type { HandlerContext } from './handler-context.js'
|
|
17
18
|
import type { WebSocketResponse, WSContext } from './types.js'
|
|
18
19
|
|
|
20
|
+
/** Result of a replay attempt — used by callers (and tests) for telemetry. */
|
|
21
|
+
export interface ReplayResult {
|
|
22
|
+
/** Number of events sent to the web during this replay. */
|
|
23
|
+
sentCount: number
|
|
24
|
+
/**
|
|
25
|
+
* True when the buffer had already evicted events that fell between the
|
|
26
|
+
* web's `lastSeenSeq` and the oldest surviving seq. The replay is partial;
|
|
27
|
+
* the web's incremental state is now provably stale and the caller should
|
|
28
|
+
* fall back to a full snapshot path (e.g. `outputHistory`).
|
|
29
|
+
*/
|
|
30
|
+
hadGap: boolean
|
|
31
|
+
/**
|
|
32
|
+
* If `hadGap`, the highest seq that was evicted (so the gap range is
|
|
33
|
+
* `(lastSeenSeq + 1) .. evictedThroughSeq`). Undefined when no gap.
|
|
34
|
+
*/
|
|
35
|
+
evictedThroughSeq?: number
|
|
36
|
+
/**
|
|
37
|
+
* If `hadGap`, the seq the web requested replay from. Echoed into
|
|
38
|
+
* telemetry so log entries are self-contained.
|
|
39
|
+
*/
|
|
40
|
+
lastSeenSeq?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
/**
|
|
20
44
|
* Replay tab events with `seq > lastSeenSeq` to `ws`. Silently no-ops when
|
|
21
45
|
* the buffer is empty or `lastSeenSeq` is unset (full init, not a resume).
|
|
46
|
+
*
|
|
47
|
+
* Returns a `ReplayResult` so the caller can detect a partial replay (the
|
|
48
|
+
* buffer evicted events the web is asking about) and decide whether to send
|
|
49
|
+
* a recovery snapshot. This is the load-bearing telemetry surface for the
|
|
50
|
+
* "long-running task output disappears mid-stream" failure mode — a `hadGap`
|
|
51
|
+
* here is the smoking gun.
|
|
22
52
|
*/
|
|
23
53
|
export function replayTabEventsSince(
|
|
24
54
|
ctx: HandlerContext,
|
|
25
55
|
ws: WSContext,
|
|
26
56
|
tabId: string,
|
|
27
57
|
lastSeenSeq: number | undefined,
|
|
28
|
-
):
|
|
29
|
-
if (lastSeenSeq === undefined) return
|
|
58
|
+
): ReplayResult {
|
|
59
|
+
if (lastSeenSeq === undefined) return { sentCount: 0, hadGap: false }
|
|
30
60
|
|
|
31
61
|
const buffer = ctx.tabEventBuffers.get(tabId)
|
|
32
|
-
if (!buffer) return
|
|
62
|
+
if (!buffer) return { sentCount: 0, hadGap: false }
|
|
63
|
+
|
|
64
|
+
const hadGap = buffer.hasGapSince(lastSeenSeq)
|
|
65
|
+
const evictedThroughSeq = hadGap ? buffer.getEvictedThroughSeq() : undefined
|
|
66
|
+
|
|
67
|
+
if (hadGap) {
|
|
68
|
+
// Replay is structurally incomplete. Surface a single, structured warning
|
|
69
|
+
// so we can grep/Sentry-search for the failure mode without spamming logs
|
|
70
|
+
// on every event.
|
|
71
|
+
const message =
|
|
72
|
+
`[tab-replay] gap detected for tab=${tabId}: web requested replay from seq=${lastSeenSeq}, ` +
|
|
73
|
+
`but buffer has evicted through seq=${evictedThroughSeq}. ` +
|
|
74
|
+
`Events (${lastSeenSeq + 1}..${evictedThroughSeq}) are unavailable; the web's ` +
|
|
75
|
+
`incremental state is stale and a full snapshot will be sent instead.`
|
|
76
|
+
console.warn(message)
|
|
77
|
+
try {
|
|
78
|
+
captureException(new Error('TabEventBuffer replay gap'), {
|
|
79
|
+
context: 'tab-event-replay',
|
|
80
|
+
tabId,
|
|
81
|
+
lastSeenSeq,
|
|
82
|
+
evictedThroughSeq,
|
|
83
|
+
bufferCurrentSeq: buffer.currentSeq(),
|
|
84
|
+
gapSize: (evictedThroughSeq ?? 0) - lastSeenSeq,
|
|
85
|
+
})
|
|
86
|
+
} catch {
|
|
87
|
+
// Sentry transport errors must not break the replay path.
|
|
88
|
+
}
|
|
89
|
+
// CRITICAL: do NOT emit partial events. If we did, the web would advance
|
|
90
|
+
// its `tabSeqs` past the (lastSeenSeq+1 .. evictedThroughSeq) range and
|
|
91
|
+
// the subsequent snapshot would land in a tab that thinks it's caught up
|
|
92
|
+
// — silently rendering only the post-gap tail. Returning early without
|
|
93
|
+
// events forces the caller (`session-initialization.ts`) into the
|
|
94
|
+
// snapshot-fallback branch, which sends a fresh `outputHistory` payload
|
|
95
|
+
// with `replayGap: true` so the web can replace its tab state cleanly.
|
|
96
|
+
return { sentCount: 0, hadGap: true, evictedThroughSeq, lastSeenSeq }
|
|
97
|
+
}
|
|
33
98
|
|
|
34
99
|
const events = buffer.getSince(lastSeenSeq)
|
|
35
100
|
for (const event of events) {
|
|
@@ -38,4 +103,6 @@ export function replayTabEventsSince(
|
|
|
38
103
|
// `WebSocketResponse['type']`. Narrow here without an extra runtime check.
|
|
39
104
|
ctx.send(ws, { type: event.type as WebSocketResponse['type'], tabId, data: event.data, seq: event.seq })
|
|
40
105
|
}
|
|
106
|
+
|
|
107
|
+
return { sentCount: events.length, hadGap: false, lastSeenSeq }
|
|
41
108
|
}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
import { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
4
4
|
import { getEffortLevel, getModel } from '../settings.js';
|
|
5
5
|
import type { HandlerContext } from './handler-context.js';
|
|
6
|
-
import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
|
|
7
|
-
import type {
|
|
6
|
+
import { buildOutputHistory, resolveEngineForSession, setupSessionListeners } from './session-handlers.js';
|
|
7
|
+
import type { TabEngineOverride } from './session-registry.js';
|
|
8
|
+
import { DEFAULT_ENGINE_ID, type EngineId, type WebSocketMessage, type WSContext } from './types.js';
|
|
8
9
|
|
|
9
10
|
function buildActiveTabData(
|
|
10
|
-
regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string },
|
|
11
|
+
regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string; engineOverride?: TabEngineOverride },
|
|
11
12
|
session: ImprovisationSessionManager,
|
|
12
13
|
worktreePath: string | undefined,
|
|
13
14
|
worktreeBranch: string | undefined,
|
|
@@ -17,17 +18,19 @@ function buildActiveTabData(
|
|
|
17
18
|
createdAt: regTab.createdAt,
|
|
18
19
|
order: regTab.order,
|
|
19
20
|
hasUnviewedCompletion: regTab.hasUnviewedCompletion,
|
|
21
|
+
engine: resolveEngineForSession(session),
|
|
20
22
|
sessionInfo: session.getSessionInfo(),
|
|
21
23
|
isExecuting: session.isExecuting,
|
|
22
24
|
outputHistory: buildOutputHistory(session),
|
|
23
25
|
executionEvents: session.isExecuting ? session.getExecutionEventLog() : undefined,
|
|
24
26
|
...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
|
|
25
27
|
...(worktreePath ? { worktreePath, worktreeBranch } : {}),
|
|
28
|
+
...(regTab.engineOverride ? { engineOverride: regTab.engineOverride } : {}),
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
function buildInactiveTabData(
|
|
30
|
-
regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string },
|
|
33
|
+
regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string; engineOverride?: TabEngineOverride },
|
|
31
34
|
worktreePath: string | undefined,
|
|
32
35
|
worktreeBranch: string | undefined,
|
|
33
36
|
): Record<string, unknown> {
|
|
@@ -36,10 +39,12 @@ function buildInactiveTabData(
|
|
|
36
39
|
createdAt: regTab.createdAt,
|
|
37
40
|
order: regTab.order,
|
|
38
41
|
hasUnviewedCompletion: regTab.hasUnviewedCompletion,
|
|
42
|
+
engine: DEFAULT_ENGINE_ID,
|
|
39
43
|
sessionId: regTab.sessionId,
|
|
40
44
|
isExecuting: false,
|
|
41
45
|
outputHistory: [],
|
|
42
46
|
...(worktreePath ? { worktreePath, worktreeBranch } : {}),
|
|
47
|
+
...(regTab.engineOverride ? { engineOverride: regTab.engineOverride } : {}),
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -101,6 +106,41 @@ export function handleMarkTabViewed(ctx: HandlerContext, _ws: WSContext, tabId:
|
|
|
101
106
|
});
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Persist a per-tab engine override. `msg.data.override` is either a full
|
|
111
|
+
* `{ engine, model, effortLevel }` payload or `null` to clear the override.
|
|
112
|
+
* Persisted via the session registry so the override survives WebSocket
|
|
113
|
+
* disconnects — the core guarantee of IS-019. Broadcasts the change to all
|
|
114
|
+
* connected clients so multi-device sessions stay in sync.
|
|
115
|
+
*/
|
|
116
|
+
export function handleSetTabEngine(ctx: HandlerContext, _ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
|
|
117
|
+
const raw = msg.data?.override;
|
|
118
|
+
let override: TabEngineOverride | null;
|
|
119
|
+
if (raw === null || raw === undefined) {
|
|
120
|
+
override = null;
|
|
121
|
+
} else if (
|
|
122
|
+
typeof raw === 'object' &&
|
|
123
|
+
(raw.engine === 'claude-code' || raw.engine === 'opencode') &&
|
|
124
|
+
typeof raw.model === 'string' && raw.model.length > 0 &&
|
|
125
|
+
typeof raw.effortLevel === 'string' && raw.effortLevel.length > 0
|
|
126
|
+
) {
|
|
127
|
+
override = { engine: raw.engine, model: raw.model, effortLevel: raw.effortLevel };
|
|
128
|
+
} else {
|
|
129
|
+
// Malformed payload — ignore rather than crash. The client will re-emit
|
|
130
|
+
// from the canonical server-side value on the next reconnect.
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const registry = ctx.getRegistry(workingDir);
|
|
135
|
+
registry.updateTabEngineOverride(tabId, override);
|
|
136
|
+
|
|
137
|
+
ctx.broadcastToAll({
|
|
138
|
+
type: 'tabEngineOverride',
|
|
139
|
+
tabId,
|
|
140
|
+
data: { tabId, override },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workingDir: string, tabName?: string, optimisticTabId?: string): Promise<void> {
|
|
105
145
|
const registry = ctx.getRegistry(workingDir);
|
|
106
146
|
|
|
@@ -109,14 +149,18 @@ export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workin
|
|
|
109
149
|
const existingSession = registry.getTabSession(tabId);
|
|
110
150
|
if (existingSession) {
|
|
111
151
|
const regTab = registry.getTab(tabId);
|
|
152
|
+
const existingSessionObj = ctx.sessions.get(existingSession);
|
|
153
|
+
const engine: EngineId = resolveEngineForSession(existingSessionObj);
|
|
112
154
|
ctx.broadcastToAll({
|
|
113
155
|
type: 'tabCreated',
|
|
156
|
+
engine,
|
|
114
157
|
data: {
|
|
115
158
|
tabId,
|
|
116
159
|
tabName: regTab?.tabName || 'Chat',
|
|
117
160
|
createdAt: regTab?.createdAt,
|
|
118
161
|
order: regTab?.order,
|
|
119
|
-
|
|
162
|
+
engine,
|
|
163
|
+
sessionInfo: existingSessionObj?.getSessionInfo(),
|
|
120
164
|
}
|
|
121
165
|
});
|
|
122
166
|
return;
|
|
@@ -133,14 +177,17 @@ export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workin
|
|
|
133
177
|
|
|
134
178
|
registry.registerTab(tabId, sessionId, tabName);
|
|
135
179
|
const registeredTab = registry.getTab(tabId);
|
|
180
|
+
const engine: EngineId = resolveEngineForSession(session);
|
|
136
181
|
|
|
137
182
|
ctx.broadcastToAll({
|
|
138
183
|
type: 'tabCreated',
|
|
184
|
+
engine,
|
|
139
185
|
data: {
|
|
140
186
|
tabId,
|
|
141
187
|
tabName: registeredTab?.tabName || 'Chat',
|
|
142
188
|
createdAt: registeredTab?.createdAt,
|
|
143
189
|
order: registeredTab?.order,
|
|
190
|
+
engine,
|
|
144
191
|
sessionInfo: session.getSessionInfo(),
|
|
145
192
|
}
|
|
146
193
|
});
|
|
@@ -148,6 +195,7 @@ export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workin
|
|
|
148
195
|
ctx.send(ws, {
|
|
149
196
|
type: 'tabInitialized',
|
|
150
197
|
tabId,
|
|
198
|
+
engine,
|
|
151
199
|
data: session.getSessionInfo()
|
|
152
200
|
});
|
|
153
201
|
}
|
|
@@ -19,7 +19,7 @@ export interface WSContext {
|
|
|
19
19
|
_ws?: unknown
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const CoreMessages = ['execute', 'cancel', 'getHistory', 'getSessions', 'getSessionsCount', 'deleteSession', 'getSessionById', 'clearHistory', 'searchHistory', 'new', 'autocomplete', 'readFile', 'ping', 'initTab', 'resumeSession', 'approve', 'reject', 'recordSelection', 'requestNotificationSummary'] as const;
|
|
22
|
+
const CoreMessages = ['execute', 'cancel', 'getHistory', 'getSessions', 'getSessionsCount', 'deleteSession', 'getSessionById', 'clearHistory', 'searchHistory', 'new', 'autocomplete', 'readFile', 'ping', 'initTab', 'resumeSession', 'approve', 'reject', 'recordSelection', 'requestNotificationSummary', 'askUserQuestionResponse'] as const;
|
|
23
23
|
|
|
24
24
|
const TerminalMessages = ['terminalInit', 'terminalReconnect', 'terminalList', 'terminalInput', 'terminalResize', 'terminalClose'] as const;
|
|
25
25
|
|
|
@@ -37,11 +37,11 @@ const GitWorktreeMessages = ['gitWorktreeList', 'gitWorktreeCreate', 'gitWorktre
|
|
|
37
37
|
|
|
38
38
|
const GitMergeMessages = ['gitMergePreview', 'gitWorktreeMerge', 'gitMergeAbort', 'gitMergeComplete', 'gitMergeStashPop', 'gitMergeDiscardBlockers'] as const;
|
|
39
39
|
|
|
40
|
-
const SessionSyncMessages = ['getActiveTabs', 'createTab', 'reorderTabs', 'syncTabMeta', 'removeTab', 'markTabViewed'] as const;
|
|
40
|
+
const SessionSyncMessages = ['getActiveTabs', 'createTab', 'reorderTabs', 'syncTabMeta', 'removeTab', 'markTabViewed', 'setTabEngine'] as const;
|
|
41
41
|
|
|
42
42
|
const SettingsMessages = ['getSettings', 'updateSettings'] as const;
|
|
43
43
|
|
|
44
|
-
const QualityMessages = ['qualityDetectTools', 'qualityScan', 'qualityInstallTools', 'qualityCodeReview', 'qualityLoadState', 'qualitySaveDirectories'] as const;
|
|
44
|
+
const QualityMessages = ['qualityDetectTools', 'qualityScan', 'qualityInstallTools', 'qualityCodeReview', 'qualityCancel', 'qualityLoadState', 'qualityClearPending', 'qualitySaveDirectories'] as const;
|
|
45
45
|
|
|
46
46
|
const FileUploadMessages = ['fileUploadStart', 'fileUploadChunk', 'fileUploadComplete', 'fileUploadCancel'] as const;
|
|
47
47
|
|
|
@@ -55,6 +55,8 @@ const PlanSprintMessages = ['planCreateSprint', 'planActivateSprint', 'planCompl
|
|
|
55
55
|
|
|
56
56
|
const SkillMessages = ['listSkills', 'chatToBoard'] as const;
|
|
57
57
|
|
|
58
|
+
const InstanceMessages = ['shutdownInstance'] as const;
|
|
59
|
+
|
|
58
60
|
type WebSocketMessageType =
|
|
59
61
|
| typeof CoreMessages[number]
|
|
60
62
|
| typeof TerminalMessages[number]
|
|
@@ -73,19 +75,40 @@ type WebSocketMessageType =
|
|
|
73
75
|
| typeof PlanMessages[number]
|
|
74
76
|
| typeof PlanBoardMessages[number]
|
|
75
77
|
| typeof PlanSprintMessages[number]
|
|
76
|
-
| typeof SkillMessages[number]
|
|
78
|
+
| typeof SkillMessages[number]
|
|
79
|
+
| typeof InstanceMessages[number];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* AI engine identifier carried on WebSocket envelopes so the web client can
|
|
83
|
+
* render engine-specific affordances. Server relay forwards the field
|
|
84
|
+
* unchanged; missing values on inbound messages default to 'claude-code'.
|
|
85
|
+
*/
|
|
86
|
+
export type EngineId = 'claude-code' | 'opencode';
|
|
87
|
+
|
|
88
|
+
export const DEFAULT_ENGINE_ID: EngineId = 'claude-code';
|
|
89
|
+
|
|
90
|
+
/** Narrow an unknown engine value to a valid EngineId, defaulting to 'claude-code'. */
|
|
91
|
+
export function normalizeEngineId(value: unknown): EngineId {
|
|
92
|
+
return value === 'opencode' ? 'opencode' : DEFAULT_ENGINE_ID;
|
|
93
|
+
}
|
|
77
94
|
|
|
78
95
|
export interface WebSocketMessage {
|
|
79
96
|
type: WebSocketMessageType;
|
|
80
97
|
tabId?: string;
|
|
81
98
|
terminalId?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Engine that produced / should handle this message. Optional on the wire —
|
|
101
|
+
* missing field is treated as 'claude-code' by receivers. Populated on
|
|
102
|
+
* prompt-send, session-state, and tab-state messages.
|
|
103
|
+
*/
|
|
104
|
+
engine?: EngineId;
|
|
82
105
|
// biome-ignore lint/suspicious/noExplicitAny: message envelope carries heterogeneous payloads — typed per-handler via destructuring
|
|
83
106
|
data?: any;
|
|
84
107
|
/** Injected by server relay for view-only shared users */
|
|
85
108
|
_permission?: 'view';
|
|
86
109
|
}
|
|
87
110
|
|
|
88
|
-
const CoreResponseMessages = ['output', 'thinking', 'movementStart', 'movementComplete', 'movementError', 'sessionUpdate', 'history', 'sessions', 'sessionsCount', 'sessionDeleted', 'sessionData', 'historyCleared', 'searchResults', 'newSession', 'autocomplete', 'fileContent', 'error', 'pong', 'tabInitialized', 'approvalRequired', 'toolUse', 'streamingTokens', 'notificationSummary', 'executeAck', 'clientOffline', 'clientAuthExpired'] as const;
|
|
111
|
+
const CoreResponseMessages = ['output', 'thinking', 'movementStart', 'movementComplete', 'movementError', 'sessionUpdate', 'history', 'sessions', 'sessionsCount', 'sessionDeleted', 'sessionData', 'historyCleared', 'searchResults', 'newSession', 'autocomplete', 'fileContent', 'error', 'pong', 'tabInitialized', 'approvalRequired', 'toolUse', 'streamingTokens', 'notificationSummary', 'executeAck', 'clientOffline', 'clientAuthExpired', 'askUserQuestion', 'askUserQuestionDismissed'] as const;
|
|
89
112
|
|
|
90
113
|
const TerminalResponseMessages = ['terminalOutput', 'terminalReady', 'terminalExit', 'terminalError', 'terminalList', 'terminalScrollback', 'terminalCreated', 'terminalClosed'] as const;
|
|
91
114
|
|
|
@@ -103,7 +126,7 @@ const GitWorktreeResponseMessages = ['gitWorktreeListResult', 'gitWorktreeCreate
|
|
|
103
126
|
|
|
104
127
|
const GitMergeResponseMessages = ['gitMergePreviewResult', 'gitWorktreeMergeResult', 'gitMergeAborted', 'gitMergeCompleted', 'gitMergeStashPopped', 'gitMergeBlockersDiscarded'] as const;
|
|
105
128
|
|
|
106
|
-
const SessionSyncResponseMessages = ['activeTabs', 'tabCreated', 'tabRemoved', 'tabRenamed', 'tabsReordered', 'tabViewed', 'tabStateChanged'] as const;
|
|
129
|
+
const SessionSyncResponseMessages = ['activeTabs', 'tabCreated', 'tabRemoved', 'tabRenamed', 'tabsReordered', 'promptTextSync', 'tabViewed', 'tabStateChanged', 'tabEngineOverride'] as const;
|
|
107
130
|
|
|
108
131
|
const SettingsResponseMessages = ['settings', 'settingsUpdated'] as const;
|
|
109
132
|
|
|
@@ -121,6 +144,8 @@ const PlanSprintResponseMessages = ['planSprintCreated', 'planSprintUpdated', 'p
|
|
|
121
144
|
|
|
122
145
|
const SkillResponseMessages = ['skillsList', 'chatToBoardCreated'] as const;
|
|
123
146
|
|
|
147
|
+
const InstanceResponseMessages = ['shuttingDown'] as const;
|
|
148
|
+
|
|
124
149
|
type WebSocketResponseType =
|
|
125
150
|
| typeof CoreResponseMessages[number]
|
|
126
151
|
| typeof TerminalResponseMessages[number]
|
|
@@ -139,12 +164,19 @@ type WebSocketResponseType =
|
|
|
139
164
|
| typeof PlanResponseMessages[number]
|
|
140
165
|
| typeof PlanBoardResponseMessages[number]
|
|
141
166
|
| typeof PlanSprintResponseMessages[number]
|
|
142
|
-
| typeof SkillResponseMessages[number]
|
|
167
|
+
| typeof SkillResponseMessages[number]
|
|
168
|
+
| typeof InstanceResponseMessages[number];
|
|
143
169
|
|
|
144
170
|
export interface WebSocketResponse {
|
|
145
171
|
type: WebSocketResponseType;
|
|
146
172
|
tabId?: string;
|
|
147
173
|
terminalId?: string;
|
|
174
|
+
/**
|
|
175
|
+
* Engine that produced this response. Populated by CLI handlers for
|
|
176
|
+
* prompt-send, session-state, and tab-state message categories so the web
|
|
177
|
+
* client can render engine-specific affordances.
|
|
178
|
+
*/
|
|
179
|
+
engine?: EngineId;
|
|
148
180
|
// biome-ignore lint/suspicious/noExplicitAny: message envelope carries heterogeneous payloads — typed per-handler via destructuring
|
|
149
181
|
data?: any;
|
|
150
182
|
/**
|
|
@@ -161,6 +193,52 @@ export interface ConnectionData {
|
|
|
161
193
|
workingDir: string;
|
|
162
194
|
}
|
|
163
195
|
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// AskUserQuestion Types
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Single option Claude offers in an AskUserQuestion call. Mirrors the
|
|
202
|
+
* Claude Agent SDK shape so we can pass-through with no translation.
|
|
203
|
+
*/
|
|
204
|
+
export interface AskUserQuestionOption {
|
|
205
|
+
label: string;
|
|
206
|
+
description: string;
|
|
207
|
+
/** Optional HTML/Markdown preview snippet (when previewFormat is set). */
|
|
208
|
+
preview?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* One question in an AskUserQuestion call. Per Claude SDK: each call
|
|
213
|
+
* carries 1–4 questions, each question 2–4 options. Header is a short
|
|
214
|
+
* label (≤12 chars) used for compact display.
|
|
215
|
+
*/
|
|
216
|
+
export interface AskUserQuestionItem {
|
|
217
|
+
question: string;
|
|
218
|
+
header: string;
|
|
219
|
+
options: AskUserQuestionOption[];
|
|
220
|
+
multiSelect: boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Outbound payload (cli → web) when Claude pauses on an AskUserQuestion. */
|
|
224
|
+
export interface AskUserQuestionPayload {
|
|
225
|
+
toolUseId: string;
|
|
226
|
+
questions: AskUserQuestionItem[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Outbound payload (cli → web) when a pending question is no longer answerable. */
|
|
230
|
+
export interface AskUserQuestionDismissedPayload {
|
|
231
|
+
toolUseId: string;
|
|
232
|
+
reason: 'timeout' | 'cancelled' | 'session-ended';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Inbound payload (web → cli) carrying the user's selections. */
|
|
236
|
+
export interface AskUserQuestionResponseData {
|
|
237
|
+
toolUseId: string;
|
|
238
|
+
/** Map of question text → selected label(s) (joined with ", " for multi-select). */
|
|
239
|
+
answers: Record<string, string>;
|
|
240
|
+
}
|
|
241
|
+
|
|
164
242
|
export interface SkillEntry {
|
|
165
243
|
name: string;
|
|
166
244
|
displayName: string;
|