mstro-app 0.4.52 → 0.5.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/README.md +10 -5
- package/bin/mstro.js +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.js +7 -2
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +1 -1
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +63 -67
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +9 -4
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts +16 -0
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
- package/dist/server/cli/improvisation-history-store.js +52 -0
- package/dist/server/cli/improvisation-history-store.js.map +1 -0
- package/dist/server/cli/improvisation-movements.d.ts +31 -0
- package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
- package/dist/server/cli/improvisation-movements.js +93 -0
- package/dist/server/cli/improvisation-movements.js.map +1 -0
- package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
- package/dist/server/cli/improvisation-output-queue.js +40 -0
- package/dist/server/cli/improvisation-output-queue.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +21 -51
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +18 -433
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +53 -148
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
- package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-best-result.js +61 -0
- package/dist/server/cli/retry/retry-best-result.js.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.js +68 -0
- package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.js +81 -0
- package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js +166 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.js +22 -0
- package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.js +60 -0
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.js +24 -0
- package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
- package/dist/server/cli/retry/retry-types.d.ts +30 -0
- package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-types.js +4 -0
- package/dist/server/cli/retry/retry-types.js.map +1 -0
- package/dist/server/index.js +21 -109
- package/dist/server/index.js.map +1 -1
- package/dist/server/server-setup.d.ts +16 -1
- package/dist/server/server-setup.d.ts.map +1 -1
- package/dist/server/server-setup.js +107 -0
- package/dist/server/server-setup.js.map +1 -1
- package/dist/server/services/plan/board-config.d.ts +21 -0
- package/dist/server/services/plan/board-config.d.ts.map +1 -0
- package/dist/server/services/plan/board-config.js +112 -0
- package/dist/server/services/plan/board-config.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +7 -5
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +48 -48
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +157 -455
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-loader.d.ts +16 -0
- package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
- package/dist/server/services/plan/issue-loader.js +46 -0
- package/dist/server/services/plan/issue-loader.js.map +1 -0
- package/dist/server/services/plan/issue-writer.d.ts +34 -0
- package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
- package/dist/server/services/plan/issue-writer.js +110 -0
- package/dist/server/services/plan/issue-writer.js.map +1 -0
- package/dist/server/services/plan/output-manager.d.ts.map +1 -1
- package/dist/server/services/plan/output-manager.js +2 -1
- package/dist/server/services/plan/output-manager.js.map +1 -1
- package/dist/server/services/plan/progress-log.d.ts +11 -0
- package/dist/server/services/plan/progress-log.d.ts.map +1 -0
- package/dist/server/services/plan/progress-log.js +81 -0
- package/dist/server/services/plan/progress-log.js.map +1 -0
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/prompt-builder.js +48 -31
- package/dist/server/services/plan/prompt-builder.js.map +1 -1
- package/dist/server/services/plan/readiness-planner.d.ts +15 -0
- package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
- package/dist/server/services/plan/readiness-planner.js +41 -0
- package/dist/server/services/plan/readiness-planner.js.map +1 -0
- package/dist/server/services/plan/review-gate.d.ts +31 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +52 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/platform.d.ts +56 -0
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +154 -52
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
- package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
- package/dist/server/services/websocket/file-download-handler.js +165 -0
- package/dist/server/services/websocket/file-download-handler.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +28 -2
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +15 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +7 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +73 -11
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
- package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
- package/dist/server/services/websocket/msg-id-tracker.js +77 -0
- package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.js +15 -3
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +2 -2
- package/dist/server/services/websocket/session-handlers.d.ts +48 -2
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +204 -65
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts +2 -2
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +75 -17
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +29 -1
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +53 -4
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-broadcast.js +13 -0
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.js +107 -0
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.js +21 -0
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +2 -9
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +15 -6
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +6 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/README.md +1 -1
- package/server/cli/headless/claude-invoker-stall.ts +7 -2
- package/server/cli/headless/claude-invoker.ts +1 -1
- package/server/cli/headless/runner.ts +67 -72
- package/server/cli/headless/stall-assessor.ts +9 -4
- package/server/cli/headless/types.ts +1 -1
- package/server/cli/improvisation-history-store.ts +62 -0
- package/server/cli/improvisation-movements.ts +120 -0
- package/server/cli/improvisation-output-queue.ts +42 -0
- package/server/cli/improvisation-retry.ts +25 -600
- package/server/cli/improvisation-session-manager.ts +74 -160
- package/server/cli/retry/retry-best-result.ts +70 -0
- package/server/cli/retry/retry-context-loss.ts +87 -0
- package/server/cli/retry/retry-premature-completion.ts +113 -0
- package/server/cli/retry/retry-recovery-strategies.ts +247 -0
- package/server/cli/retry/retry-resume-strategy.ts +33 -0
- package/server/cli/retry/retry-runner-factory.ts +70 -0
- package/server/cli/retry/retry-tool-results.ts +31 -0
- package/server/cli/retry/retry-types.ts +32 -0
- package/server/index.ts +37 -123
- package/server/server-setup.ts +126 -1
- package/server/services/plan/agents/assess-stall.md +11 -4
- package/server/services/plan/board-config.ts +122 -0
- package/server/services/plan/composer.ts +7 -5
- package/server/services/plan/executor.ts +214 -467
- package/server/services/plan/issue-loader.ts +64 -0
- package/server/services/plan/issue-writer.ts +137 -0
- package/server/services/plan/output-manager.ts +2 -1
- package/server/services/plan/progress-log.ts +92 -0
- package/server/services/plan/prompt-builder.ts +73 -35
- package/server/services/plan/readiness-planner.ts +50 -0
- package/server/services/plan/review-gate.ts +102 -2
- package/server/services/platform.ts +163 -58
- package/server/services/websocket/file-download-handler.ts +191 -0
- package/server/services/websocket/git-worktree-handlers.ts +29 -2
- package/server/services/websocket/handler-context.ts +15 -0
- package/server/services/websocket/handler.ts +76 -12
- package/server/services/websocket/msg-id-tracker.ts +84 -0
- package/server/services/websocket/quality-handlers.ts +16 -3
- package/server/services/websocket/quality-review-agent.ts +2 -2
- package/server/services/websocket/session-handlers.ts +213 -68
- package/server/services/websocket/session-initialization.ts +83 -19
- package/server/services/websocket/session-registry.ts +61 -4
- package/server/services/websocket/tab-broadcast.ts +38 -0
- package/server/services/websocket/tab-event-buffer.ts +159 -0
- package/server/services/websocket/tab-event-replay.ts +42 -0
- package/server/services/websocket/tab-handlers.ts +2 -9
- package/server/services/websocket/types.ts +17 -4
|
@@ -14,20 +14,24 @@ import { homedir } from 'node:os';
|
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import type { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
16
16
|
import { captureException } from '../sentry.js';
|
|
17
|
+
import { getPTYManager } from '../terminal/pty-manager.js';
|
|
17
18
|
import { AutocompleteService } from './autocomplete.js';
|
|
19
|
+
import { FileDownloadHandler } from './file-download-handler.js';
|
|
18
20
|
import { handleFileExplorerMessage, handleFileMessage } from './file-explorer-handlers.js';
|
|
19
21
|
import { FileUploadHandler } from './file-upload-handler.js';
|
|
20
22
|
import { handleGitMessage } from './git-handlers.js';
|
|
21
23
|
import { GitHeadWatcher } from './git-head-watcher.js';
|
|
22
24
|
import type { HandlerContext, UsageReporter } from './handler-context.js';
|
|
25
|
+
import { MsgIdTracker } from './msg-id-tracker.js';
|
|
23
26
|
import { handlePlanMessage } from './plan-handlers.js';
|
|
24
27
|
import { handleQualityMessage } from './quality-handlers.js';
|
|
25
|
-
import { handleHistoryMessage, handleSessionMessage, initializeTab, resumeHistoricalSession } from './session-handlers.js';
|
|
28
|
+
import { handleHistoryMessage, handleSessionMessage, initializeTab, restoreWorktreeFromRegistry, resumeHistoricalSession } from './session-handlers.js';
|
|
26
29
|
import { SessionRegistry } from './session-registry.js';
|
|
27
30
|
import { generateNotificationSummary, handleGetSettings, handleUpdateSettings } from './settings-handlers.js';
|
|
28
31
|
import { handleListSkills } from './skill-handlers.js';
|
|
29
32
|
import { SkillsWatcher } from './skill-watcher.js';
|
|
30
|
-
import {
|
|
33
|
+
import { TabEventBufferRegistry } from './tab-event-buffer.js';
|
|
34
|
+
import { handleCreateTab, handleGetActiveTabs, handleMarkTabViewed, handleRemoveTab, handleReorderTabs, handleSyncTabMeta } from './tab-handlers.js';
|
|
31
35
|
import { cleanupTerminalSubscribers, handleTerminalMessage } from './terminal-handlers.js';
|
|
32
36
|
import type { FrecencyData, WebSocketMessage, WebSocketResponse, WSContext } from './types.js';
|
|
33
37
|
|
|
@@ -47,8 +51,11 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
47
51
|
terminalListenerCleanups: Map<string, () => void> = new Map();
|
|
48
52
|
terminalSubscribers: Map<string, Set<WSContext>> = new Map();
|
|
49
53
|
fileUploadHandler: FileUploadHandler | null = null;
|
|
54
|
+
fileDownloadHandler: FileDownloadHandler | null = null;
|
|
50
55
|
gitHeadWatcher: GitHeadWatcher | null = null;
|
|
51
56
|
skillsWatcher: SkillsWatcher | null = null;
|
|
57
|
+
tabEventBuffers: TabEventBufferRegistry = new TabEventBufferRegistry();
|
|
58
|
+
msgIdTracker: MsgIdTracker = new MsgIdTracker();
|
|
52
59
|
|
|
53
60
|
constructor() {
|
|
54
61
|
this.frecencyPath = join(homedir(), '.mstro', 'autocomplete-frecency.json');
|
|
@@ -146,7 +153,7 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
/** Dispatch table mapping message types to domain handlers. Built once, looked up per message. */
|
|
149
|
-
private static readonly DISPATCH: Record<string, 'session' | 'history' | 'file' | 'terminal' | 'fileExplorer' | 'git' | 'quality' | 'plan' | 'fileUpload'> = {
|
|
156
|
+
private static readonly DISPATCH: Record<string, 'session' | 'history' | 'file' | 'terminal' | 'fileExplorer' | 'git' | 'quality' | 'plan' | 'fileUpload' | 'fileDownload'> = {
|
|
150
157
|
// Session
|
|
151
158
|
execute: 'session', cancel: 'session', getHistory: 'session', new: 'session', approve: 'session', reject: 'session',
|
|
152
159
|
// History
|
|
@@ -165,6 +172,8 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
165
172
|
planInit: 'plan', planGetState: 'plan', planListIssues: 'plan', planGetIssue: 'plan', planGetSprint: 'plan', planGetMilestone: 'plan', planCreateIssue: 'plan', planUpdateIssue: 'plan', planDeleteIssue: 'plan', planScaffold: 'plan', planPrompt: 'plan', planExecute: 'plan', planExecuteEpic: 'plan', planPause: 'plan', planStop: 'plan', planResume: 'plan', planCreateBoard: 'plan', planUpdateBoard: 'plan', planArchiveBoard: 'plan', planGetBoard: 'plan', planGetBoardState: 'plan', planReorderBoards: 'plan', planSetActiveBoard: 'plan', planGetBoardArtifacts: 'plan', planCreateSprint: 'plan', planActivateSprint: 'plan', planCompleteSprint: 'plan', planGetSprintArtifacts: 'plan', chatToBoard: 'plan',
|
|
166
173
|
// File upload
|
|
167
174
|
fileUploadStart: 'fileUpload', fileUploadChunk: 'fileUpload', fileUploadComplete: 'fileUpload', fileUploadCancel: 'fileUpload',
|
|
175
|
+
// File download (chunked streaming for large binaries)
|
|
176
|
+
fileDownloadStart: 'fileDownload', fileDownloadCancel: 'fileDownload',
|
|
168
177
|
};
|
|
169
178
|
|
|
170
179
|
private async dispatchMessage(ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: 'view'): Promise<void> {
|
|
@@ -174,10 +183,10 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
174
183
|
this.send(ws, { type: 'pong', tabId });
|
|
175
184
|
return;
|
|
176
185
|
case 'initTab':
|
|
177
|
-
return void await initializeTab(this, ws, tabId, workingDir, msg.data?.tabName);
|
|
186
|
+
return void await initializeTab(this, ws, tabId, workingDir, msg.data?.tabName, msg.data);
|
|
178
187
|
case 'resumeSession':
|
|
179
188
|
if (!msg.data?.historicalSessionId) throw new Error('Historical session ID is required');
|
|
180
|
-
return void await resumeHistoricalSession(this, ws, tabId, workingDir, msg.data.historicalSessionId);
|
|
189
|
+
return void await resumeHistoricalSession(this, ws, tabId, workingDir, msg.data.historicalSessionId, msg.data);
|
|
181
190
|
case 'requestNotificationSummary':
|
|
182
191
|
if (!msg.data?.prompt || !msg.data?.output) throw new Error('Prompt and output are required for notification summary');
|
|
183
192
|
return void await generateNotificationSummary(this, ws, tabId, msg.data.prompt, msg.data.output, workingDir);
|
|
@@ -189,8 +198,6 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
189
198
|
return handleReorderTabs(this, ws, workingDir, msg.data?.tabOrder);
|
|
190
199
|
case 'syncTabMeta':
|
|
191
200
|
return handleSyncTabMeta(this, ws, msg, tabId, workingDir);
|
|
192
|
-
case 'syncPromptText':
|
|
193
|
-
return handleSyncPromptText(this, ws, msg, tabId);
|
|
194
201
|
case 'removeTab':
|
|
195
202
|
return handleRemoveTab(this, ws, tabId, workingDir);
|
|
196
203
|
case 'markTabViewed':
|
|
@@ -208,6 +215,14 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
208
215
|
const domain = WebSocketImproviseHandler.DISPATCH[msg.type];
|
|
209
216
|
if (!domain) throw new Error(`Unknown message type: ${msg.type}`);
|
|
210
217
|
|
|
218
|
+
// Hydrate worktree state from the registry before any domain handler
|
|
219
|
+
// reads it, so git/file/autocomplete ops route to the tab's worktree
|
|
220
|
+
// even when they arrive before the initTab handshake completes. The
|
|
221
|
+
// registry is authoritative; the in-memory Map is just a cache.
|
|
222
|
+
if (msg.tabId && !this.gitDirectories.has(tabId)) {
|
|
223
|
+
restoreWorktreeFromRegistry(this, this.getRegistry(workingDir), tabId);
|
|
224
|
+
}
|
|
225
|
+
|
|
211
226
|
// Resolve effective working directory: use worktree path if tab is on a worktree
|
|
212
227
|
const effectiveDir = this.gitDirectories.get(tabId) || workingDir;
|
|
213
228
|
|
|
@@ -221,6 +236,24 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
221
236
|
case 'quality': return handleQualityMessage(this, ws, msg, tabId, workingDir, permission);
|
|
222
237
|
case 'plan': return handlePlanMessage(this, ws, msg, tabId, workingDir, permission);
|
|
223
238
|
case 'fileUpload': return this.handleFileUploadMessage(ws, msg, tabId, workingDir, permission);
|
|
239
|
+
case 'fileDownload': return this.handleFileDownloadMessage(ws, msg, tabId, effectiveDir);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private handleFileDownloadMessage(ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
|
|
244
|
+
if (!this.fileDownloadHandler) {
|
|
245
|
+
this.fileDownloadHandler = new FileDownloadHandler(workingDir);
|
|
246
|
+
}
|
|
247
|
+
const handler = this.fileDownloadHandler;
|
|
248
|
+
const send = this.send.bind(this);
|
|
249
|
+
|
|
250
|
+
switch (msg.type) {
|
|
251
|
+
case 'fileDownloadStart':
|
|
252
|
+
handler.handleDownloadStart(ws, send, tabId, msg.data);
|
|
253
|
+
break;
|
|
254
|
+
case 'fileDownloadCancel':
|
|
255
|
+
handler.handleDownloadCancel(ws, send, tabId, msg.data);
|
|
256
|
+
break;
|
|
224
257
|
}
|
|
225
258
|
}
|
|
226
259
|
|
|
@@ -262,6 +295,10 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
262
295
|
this.fileUploadHandler.destroy();
|
|
263
296
|
this.fileUploadHandler = null;
|
|
264
297
|
}
|
|
298
|
+
if (this.fileDownloadHandler) {
|
|
299
|
+
this.fileDownloadHandler.destroy();
|
|
300
|
+
this.fileDownloadHandler = null;
|
|
301
|
+
}
|
|
265
302
|
if (this.gitHeadWatcher) {
|
|
266
303
|
this.gitHeadWatcher.stop();
|
|
267
304
|
this.gitHeadWatcher = null;
|
|
@@ -270,18 +307,45 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
270
307
|
this.skillsWatcher.stop();
|
|
271
308
|
this.skillsWatcher = null;
|
|
272
309
|
}
|
|
310
|
+
|
|
311
|
+
// Close orphan PTYs when no web client is watching any more.
|
|
312
|
+
//
|
|
313
|
+
// Prior behavior: PTYs survived until the user typed `exit` or the
|
|
314
|
+
// CLI process died. A page refresh / browser tab close / view-switch
|
|
315
|
+
// would leak the pty, and after a few of these the user would have
|
|
316
|
+
// tens of zombie shells competing for I/O bandwidth, which manifests
|
|
317
|
+
// as the app feeling "unresponsive" — interactive operations starve
|
|
318
|
+
// because the relay socket is saturated streaming output for ptys
|
|
319
|
+
// that no UI is rendering.
|
|
320
|
+
//
|
|
321
|
+
// The active-session preservation in `cleanupConnectionResources`
|
|
322
|
+
// above is intentionally separate: improvise sessions can produce
|
|
323
|
+
// useful work while the browser is closed (Claude Code keeps running
|
|
324
|
+
// in the background); a shell process can't, by construction. So we
|
|
325
|
+
// preserve the former and reap the latter.
|
|
326
|
+
const ptyManager = getPTYManager();
|
|
327
|
+
const activeTerminals = ptyManager.getActiveTerminals();
|
|
328
|
+
if (activeTerminals.length > 0) {
|
|
329
|
+
console.log(`[handler] No web subscribers — closing ${activeTerminals.length} orphan PTY${activeTerminals.length === 1 ? '' : 's'}`);
|
|
330
|
+
ptyManager.closeAll();
|
|
331
|
+
}
|
|
273
332
|
}
|
|
274
333
|
}
|
|
275
334
|
|
|
276
335
|
private cleanupConnectionResources(tabMap: Map<string, string>): void {
|
|
277
|
-
//
|
|
336
|
+
// Preserve actively-executing sessions across web reconnects. The runner
|
|
337
|
+
// is still producing output, and the new web connection will re-attach
|
|
338
|
+
// via session-handlers.ts::getSession (or initializeTab → reattachSession)
|
|
339
|
+
// which rebinds listeners and replays executionEventLog. Destroying the
|
|
340
|
+
// session here would orphan the runner and silently drop all streamed
|
|
341
|
+
// output for the rest of the prompt.
|
|
278
342
|
const sessionIds = new Set(tabMap.values());
|
|
279
343
|
for (const sessionId of sessionIds) {
|
|
280
344
|
const session = this.sessions.get(sessionId);
|
|
281
|
-
if (session)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
345
|
+
if (!session) continue;
|
|
346
|
+
if (session.isExecuting) continue;
|
|
347
|
+
session.destroy();
|
|
348
|
+
this.sessions.delete(sessionId);
|
|
285
349
|
}
|
|
286
350
|
// Kill search processes owned by this connection's tabs
|
|
287
351
|
for (const tabId of tabMap.keys()) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Idempotency tracker for client-sent message IDs.
|
|
6
|
+
*
|
|
7
|
+
* The web client assigns a UUID `msgId` to every `execute` message and
|
|
8
|
+
* stores it in a persistent outbox until an `executeAck` arrives. If the
|
|
9
|
+
* web reconnects before the ack lands it replays the same `msgId`; without
|
|
10
|
+
* dedupe the CLI would run the prompt twice (burning tokens, confusing
|
|
11
|
+
* the user). This tracker gives handlers an atomic
|
|
12
|
+
* "first time we've seen this msgId?" check with a bounded TTL so old
|
|
13
|
+
* ids don't leak forever.
|
|
14
|
+
*
|
|
15
|
+
* Design notes:
|
|
16
|
+
* - TTL (not LRU) — web promises to stop replaying once it gets an ack
|
|
17
|
+
* or times out the outbox, so a fixed window covers the realistic
|
|
18
|
+
* replay horizon. 15 minutes matches `tab-event-buffer.ts`.
|
|
19
|
+
* - Per-tab partitioning so a tab removal can cheaply clear that tab's
|
|
20
|
+
* history without scanning the whole map.
|
|
21
|
+
* - Lazy eviction on every check — no timers, cheap in the hot path.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const DEFAULT_TTL_MS = 15 * 60 * 1000;
|
|
25
|
+
const DEFAULT_MAX_PER_TAB = 512;
|
|
26
|
+
|
|
27
|
+
interface TabEntry {
|
|
28
|
+
ids: Map<string, number>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class MsgIdTracker {
|
|
32
|
+
private readonly tabs = new Map<string, TabEntry>();
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly ttlMs: number = DEFAULT_TTL_MS,
|
|
36
|
+
private readonly maxPerTab: number = DEFAULT_MAX_PER_TAB,
|
|
37
|
+
private readonly now: () => number = Date.now,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns `true` if `msgId` is being recorded for the first time on
|
|
42
|
+
* `tabId`, `false` if it was already seen inside the TTL window. Callers
|
|
43
|
+
* should skip re-execution when this returns `false` but still re-ack
|
|
44
|
+
* so the client's outbox drains.
|
|
45
|
+
*/
|
|
46
|
+
recordIfFirst(tabId: string, msgId: string): boolean {
|
|
47
|
+
const entry = this.tabs.get(tabId) ?? { ids: new Map<string, number>() };
|
|
48
|
+
this.evictExpired(entry);
|
|
49
|
+
if (entry.ids.has(msgId)) {
|
|
50
|
+
// Refresh the timestamp so a long stall still counts as "seen".
|
|
51
|
+
entry.ids.set(msgId, this.now());
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
entry.ids.set(msgId, this.now());
|
|
55
|
+
this.enforceSizeCap(entry);
|
|
56
|
+
this.tabs.set(tabId, entry);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Drop all msgIds for a removed tab. */
|
|
61
|
+
forget(tabId: string): void {
|
|
62
|
+
this.tabs.delete(tabId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Test helper — visible for unit tests only. */
|
|
66
|
+
size(tabId: string): number {
|
|
67
|
+
return this.tabs.get(tabId)?.ids.size ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private evictExpired(entry: TabEntry): void {
|
|
71
|
+
const cutoff = this.now() - this.ttlMs;
|
|
72
|
+
for (const [id, ts] of entry.ids) {
|
|
73
|
+
if (ts < cutoff) entry.ids.delete(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private enforceSizeCap(entry: TabEntry): void {
|
|
78
|
+
while (entry.ids.size > this.maxPerTab) {
|
|
79
|
+
const firstKey = entry.ids.keys().next().value;
|
|
80
|
+
if (firstKey === undefined) break;
|
|
81
|
+
entry.ids.delete(firstKey);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -173,6 +173,15 @@ async function handleSaveDirectories(
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
persistence.saveConfig(directories);
|
|
176
|
+
|
|
177
|
+
// Broadcast the updated set so every paired web (on any Fly instance)
|
|
178
|
+
// reflects the change. This is how Quality subdirectory tabs stay in
|
|
179
|
+
// sync across devices — the type is listed in the cross-instance
|
|
180
|
+
// set in `server/src/relay/handlers/clientHandlers.ts`.
|
|
181
|
+
ctx.broadcastToAll({
|
|
182
|
+
type: 'qualityDirectoriesUpdated',
|
|
183
|
+
data: { directories },
|
|
184
|
+
});
|
|
176
185
|
} catch (error) {
|
|
177
186
|
ctx.send(ws, {
|
|
178
187
|
type: 'qualityError',
|
|
@@ -245,9 +254,11 @@ async function handleScan(
|
|
|
245
254
|
|
|
246
255
|
const resultData = { path: reportPath, results };
|
|
247
256
|
try {
|
|
248
|
-
|
|
257
|
+
// Broadcast so every device sees the new scan — the Quality view on
|
|
258
|
+
// another device otherwise stays stuck on the previous scan results.
|
|
259
|
+
ctx.broadcastToAll({ type: 'qualityScanResults', data: resultData });
|
|
249
260
|
} catch {
|
|
250
|
-
//
|
|
261
|
+
// Broadcast failed — save as pending for delivery on reconnect
|
|
251
262
|
persistence.addPendingResult({
|
|
252
263
|
type: 'scanResults',
|
|
253
264
|
path: reportPath,
|
|
@@ -288,7 +299,9 @@ async function handleInstallTools(
|
|
|
288
299
|
|
|
289
300
|
const { tools, ecosystem } = await installTools(dirPath, toolNames);
|
|
290
301
|
|
|
291
|
-
|
|
302
|
+
// Broadcast so every device sees the install result (status of tools
|
|
303
|
+
// changes orchestra-wide once installed on the machine).
|
|
304
|
+
ctx.broadcastToAll({
|
|
292
305
|
type: 'qualityInstallComplete',
|
|
293
306
|
data: { path: reportPath, tools, ecosystem },
|
|
294
307
|
});
|
|
@@ -428,7 +428,7 @@ async function runVerificationPass(
|
|
|
428
428
|
policy: 'STANDARD',
|
|
429
429
|
stallWarningMs: 300_000,
|
|
430
430
|
stallKillMs: 900_000,
|
|
431
|
-
stallHardCapMs:
|
|
431
|
+
stallHardCapMs: 3_600_000,
|
|
432
432
|
toolUseCallback: makeToolCallback(send, 'Verifying: '),
|
|
433
433
|
logLabel: 'code-review-verify',
|
|
434
434
|
});
|
|
@@ -505,7 +505,7 @@ export async function handleCodeReview(
|
|
|
505
505
|
policy: 'STANDARD',
|
|
506
506
|
stallWarningMs: 300_000,
|
|
507
507
|
stallKillMs: 1_200_000,
|
|
508
|
-
stallHardCapMs:
|
|
508
|
+
stallHardCapMs: 7_200_000,
|
|
509
509
|
toolUseCallback: makeToolCallback(send),
|
|
510
510
|
logLabel: 'code-review',
|
|
511
511
|
});
|