@minionry/minion 0.7.36 → 0.7.39
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/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { dirname } from 'node:path';
|
|
@@ -8,6 +9,7 @@ import { machineFile } from '../../../bin/machine-layout.js';
|
|
|
8
9
|
import { setAgentBrowserHostAttached } from '../../cli/headless/mcp-config.js';
|
|
9
10
|
import { getBrowserDriverState } from '../browser/driver-state.js';
|
|
10
11
|
import { toBrowserPreflightPayload } from '../browser/preflight.js';
|
|
12
|
+
import { getChainEngine } from '../chain/chain-engine.js';
|
|
11
13
|
import { CollabSessionRegistry } from '../collab/registry.js';
|
|
12
14
|
import { COLLAB_EVENT_BUFFER_MAX_AGE_MS, COLLAB_EVENT_BUFFER_MAX_BYTES, COLLAB_EVENT_BUFFER_MAX_EVENTS, } from '../collab/types.js';
|
|
13
15
|
import { respawnInstance } from '../instances.js';
|
|
@@ -16,6 +18,7 @@ import { parseDeclaredEnvironment, resolveCliEnvironment } from '../platform-url
|
|
|
16
18
|
import { getAgentScheduler } from '../schedule/agent-scheduler.js';
|
|
17
19
|
import { getQualityScheduler } from '../schedule/quality-scheduler.js';
|
|
18
20
|
import { getScheduler } from '../schedule/scheduler.js';
|
|
21
|
+
import { holdAppToolCallsFor, resumeAppToolCallsFor } from '../sdk/app-tools.js';
|
|
19
22
|
import { attachSdkBrowserTransport } from '../sdk/browser-transport.js';
|
|
20
23
|
import { teardownSdkForViewer, teardownSdkOnTransportClose } from '../sdk/revoke.js';
|
|
21
24
|
import { captureException } from '../sentry.js';
|
|
@@ -28,6 +31,7 @@ import { resolvePendingQuestion } from './ask-user-question-bridge.js';
|
|
|
28
31
|
import { AutocompleteService } from './autocomplete.js';
|
|
29
32
|
import { activeBrowserAgentTarget } from './browser-agent-runtime.js';
|
|
30
33
|
import { attachBrowserHostBroadcast, broadcastAgentTabState, browserHostPolicy, handleBrowserHostMessage, onViewerDisconnected as onBrowserViewerDisconnected, onWsContextClosed as onBrowserWsContextClosed, } from './browser-handlers.js';
|
|
34
|
+
import { moveViewerToContext } from './browser-viewer-lifecycle.js';
|
|
31
35
|
import { addressToViewer } from './browser-viewer-registry.js';
|
|
32
36
|
import { handleCollabMessage } from './collab-handlers.js';
|
|
33
37
|
import { createCommitSuggestionService, GIT_VIEW_TAB_ID, isAnyExecutionActive } from './commit-suggestion-service.js';
|
|
@@ -42,7 +46,7 @@ import { FileUploadHandler } from './file-upload-handler.js';
|
|
|
42
46
|
import { handleGitMessage } from './git-handlers.js';
|
|
43
47
|
import { GitHeadWatcher } from './git-head-watcher.js';
|
|
44
48
|
import { hasScopeBinding, resolveScopeDir } from './handler-context.js';
|
|
45
|
-
import { MsgIdTracker } from './msg-id-tracker.js';
|
|
49
|
+
import { MsgIdTracker, MUTATION_SCOPE, mutationMsgId, ReplyRecorder } from './msg-id-tracker.js';
|
|
46
50
|
import { maybeStartAutoImplement } from './plan-execution-handlers.js';
|
|
47
51
|
import { handlePlanMessage } from './plan-handlers.js';
|
|
48
52
|
import { executorCache } from './plan-helpers.js';
|
|
@@ -64,7 +68,9 @@ import { buildSnippetListResultData, handleSnippetMessage } from './snippet-hand
|
|
|
64
68
|
import { SeqEventBuffer, SeqEventBufferRegistry, TabEventBufferRegistry } from './tab-event-buffer.js';
|
|
65
69
|
import { handleCreateTab, handleGetActiveTabs, handleMarkTabViewed, handleRemoveTab, handleReorderTabs, handleSetTabEngine, handleSyncTabMeta } from './tab-handlers.js';
|
|
66
70
|
import { cleanupTerminalSubscribers, handleTerminalMessage } from './terminal-handlers.js';
|
|
71
|
+
import { parseStreamInterest, ViewerInterestRegistry } from './viewer-interest.js';
|
|
67
72
|
const log = debugLog('handler');
|
|
73
|
+
export const VIEWER_DETACH_GRACE_MS = 60_000;
|
|
68
74
|
export class WebSocketImproviseHandler {
|
|
69
75
|
sessions = new Map();
|
|
70
76
|
connections = new Map();
|
|
@@ -87,12 +93,14 @@ export class WebSocketImproviseHandler {
|
|
|
87
93
|
snippetExpander = null;
|
|
88
94
|
snippetWatcher = null;
|
|
89
95
|
tabEventBuffers = new TabEventBufferRegistry();
|
|
96
|
+
viewerInterest = new ViewerInterestRegistry();
|
|
90
97
|
chatTimelines = new ChatTimelineRegistry();
|
|
91
98
|
pmEventBuffers = new SeqEventBufferRegistry();
|
|
92
99
|
pmTimelines = new PmTimelineRegistry();
|
|
93
100
|
collabEventBuffers = new SeqEventBufferRegistry(() => new SeqEventBuffer(COLLAB_EVENT_BUFFER_MAX_EVENTS, COLLAB_EVENT_BUFFER_MAX_AGE_MS, Date.now, COLLAB_EVENT_BUFFER_MAX_BYTES));
|
|
94
101
|
collabSessions = new CollabSessionRegistry(this);
|
|
95
102
|
msgIdTracker = new MsgIdTracker();
|
|
103
|
+
replyRecorders = new AsyncLocalStorage();
|
|
96
104
|
commitSuggestions = null;
|
|
97
105
|
instanceRegistry;
|
|
98
106
|
shutdownInProgress = false;
|
|
@@ -104,6 +112,9 @@ export class WebSocketImproviseHandler {
|
|
|
104
112
|
browserDriverStateDispose = null;
|
|
105
113
|
currentWorkingDir = null;
|
|
106
114
|
sessionRoles = new SessionRoleDirectory();
|
|
115
|
+
contextViewers = new Map();
|
|
116
|
+
detachedContexts = new Map();
|
|
117
|
+
deferredCloses = new Map();
|
|
107
118
|
constructor(instanceRegistry = null, browserHost = null) {
|
|
108
119
|
this.frecencyPath = machineFile('frecency');
|
|
109
120
|
const frecencyData = this.loadFrecencyData();
|
|
@@ -283,6 +294,7 @@ export class WebSocketImproviseHandler {
|
|
|
283
294
|
}
|
|
284
295
|
if (workingDir) {
|
|
285
296
|
getScheduler(workingDir, this).start();
|
|
297
|
+
getChainEngine(workingDir, this).start();
|
|
286
298
|
getAgentScheduler(workingDir, this).start();
|
|
287
299
|
getQualityScheduler(workingDir).start();
|
|
288
300
|
}
|
|
@@ -316,16 +328,59 @@ export class WebSocketImproviseHandler {
|
|
|
316
328
|
const viewerId = msg._viewer ?? ws._viewerId;
|
|
317
329
|
delete msg._viewer;
|
|
318
330
|
this.sessionRoles.noteMessage(viewerId, role, ws, { explicitViewerStamp, messageType: msg.type });
|
|
319
|
-
|
|
331
|
+
const msgId = mutationMsgId(msg);
|
|
332
|
+
if (msgId === undefined) {
|
|
333
|
+
await this.dispatchMessage(ws, msg, tabId, workingDir, role, viewerId);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
await this.dispatchMutationOnce(ws, msg, msgId, viewerId, () => this.dispatchMessage(ws, msg, tabId, workingDir, role, viewerId));
|
|
337
|
+
}
|
|
320
338
|
}
|
|
321
339
|
catch (error) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
340
|
+
this.replyWithHandlerError(ws, error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
replyWithHandlerError(ws, error) {
|
|
344
|
+
console.error('[WebSocketImproviseHandler] Error handling message:', error);
|
|
345
|
+
captureException(error, { context: 'websocket.handleMessage' });
|
|
346
|
+
this.send(ws, {
|
|
347
|
+
type: 'error',
|
|
348
|
+
data: { message: error instanceof Error ? error.message : String(error) }
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async dispatchMutationOnce(ws, msg, msgId, viewerId, dispatch) {
|
|
352
|
+
const repeat = this.msgIdTracker.admit(MUTATION_SCOPE, msgId);
|
|
353
|
+
if (repeat) {
|
|
354
|
+
this.answerRepeatedMutation(ws, msg, msgId, repeat, viewerId);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const recorder = new ReplyRecorder(ws, msg.type);
|
|
358
|
+
try {
|
|
359
|
+
await this.replyRecorders.run(recorder, async () => {
|
|
360
|
+
try {
|
|
361
|
+
await dispatch();
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
this.replyWithHandlerError(ws, error);
|
|
365
|
+
}
|
|
327
366
|
});
|
|
328
367
|
}
|
|
368
|
+
finally {
|
|
369
|
+
this.msgIdTracker.settle(MUTATION_SCOPE, msgId, recorder.close());
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
answerRepeatedMutation(ws, msg, msgId, repeat, viewerId) {
|
|
373
|
+
console.log(`[WebSocketImproviseHandler] ${msg.type} duplicate msgId=${msgId} — ${repeat.replies ? 'replaying its result' : 'ignored'}`);
|
|
374
|
+
if (repeat.replies) {
|
|
375
|
+
for (const frame of repeat.replies)
|
|
376
|
+
this.deliver(ws, frame);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.send(ws, addressToViewer({
|
|
380
|
+
type: 'duplicateIgnored',
|
|
381
|
+
...(msg.tabId ? { tabId: msg.tabId } : {}),
|
|
382
|
+
data: { msgId, type: msg.type, ...(repeat.pending ? { pending: true } : {}) },
|
|
383
|
+
}, ws, viewerId));
|
|
329
384
|
}
|
|
330
385
|
static DISPATCH = {
|
|
331
386
|
execute: 'session', cancel: 'session', getHistory: 'session', new: 'session', approve: 'session', reject: 'session', queueEnqueuePrompt: 'session', queueRemovePrompt: 'session', queueClearPrompts: 'session',
|
|
@@ -337,13 +392,13 @@ export class WebSocketImproviseHandler {
|
|
|
337
392
|
listDirectory: 'fileExplorer', writeFile: 'fileExplorer', createFile: 'fileExplorer', createDirectory: 'fileExplorer', deleteFile: 'fileExplorer', renameFile: 'fileExplorer', notifyFileOpened: 'fileExplorer', searchFileContents: 'fileExplorer', cancelSearch: 'fileExplorer', findDefinition: 'fileExplorer',
|
|
338
393
|
collabJoin: 'collab', collabLeave: 'collab', collabPush: 'collab', collabCursor: 'collab', collabFlush: 'collab',
|
|
339
394
|
gitStatus: 'git', gitStage: 'git', gitUnstage: 'git', gitCommit: 'git', gitCommitWithAI: 'git', gitPush: 'git', gitPull: 'git', gitLog: 'git', gitDiscoverRepos: 'git', gitSetDirectory: 'git', gitGetRemoteInfo: 'git', gitCreatePR: 'git', gitGeneratePRDescription: 'git', gitListBranches: 'git', gitCheckout: 'git', gitCreateBranch: 'git', gitDeleteBranch: 'git', gitDiff: 'git', gitShowCommit: 'git', gitCommitDiff: 'git', gitListTags: 'git', gitCreateTag: 'git', gitPushTag: 'git', gitWorktreeList: 'git', gitWorktreeCreate: 'git', gitWorktreeCreateAndAssign: 'git', gitWorktreeRemove: 'git', tabWorktreeSwitch: 'git', tabRepoWorktreeSwitch: 'git', gitWorktreePush: 'git', gitWorktreeCreatePR: 'git', gitMergePreview: 'git', gitWorktreeMerge: 'git', gitMergeAbort: 'git', gitMergeComplete: 'git', gitMergeStashPop: 'git', gitMergeDiscardBlockers: 'git',
|
|
340
|
-
planInit: 'plan', planGetState: 'plan', planListIssues: 'plan', planGetIssue: 'plan', planGetSprint: 'plan', planGetMilestone: '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', planRestoreBoard: 'plan', planGetBoard: 'plan', planGetBoardState: 'plan', planReorderBoards: 'plan', planSetActiveBoard: 'plan', planGetBoardArtifacts: 'plan', planExportBoard: 'plan', planImportBoard: 'plan', planDownloadBoard: 'plan', planInstantiateTemplate: 'plan', planCreateSprint: 'plan', planActivateSprint: 'plan', planCompleteSprint: 'plan', planGetSprintArtifacts: 'plan', chatToBoard: 'plan', queueEnqueueComposerPrompt: 'plan', queueRemoveComposerPrompt: 'plan', queueClearComposerPrompts: 'plan',
|
|
395
|
+
planInit: 'plan', planGetState: 'plan', planListIssues: 'plan', planGetIssue: 'plan', planGetSprint: 'plan', planGetMilestone: 'plan', planUpdateIssue: 'plan', planReviewDecision: 'plan', planDeleteIssue: 'plan', planScaffold: 'plan', planPrompt: 'plan', planExecute: 'plan', planExecuteEpic: 'plan', planPause: 'plan', planStop: 'plan', planResume: 'plan', planCreateBoard: 'plan', planUpdateBoard: 'plan', planArchiveBoard: 'plan', planRestoreBoard: 'plan', planGetBoard: 'plan', planGetBoardState: 'plan', planReorderBoards: 'plan', planSetActiveBoard: 'plan', planGetBoardArtifacts: 'plan', planExportBoard: 'plan', planImportBoard: 'plan', planDownloadBoard: 'plan', planInstantiateTemplate: 'plan', planCreateSprint: 'plan', planActivateSprint: 'plan', planCompleteSprint: 'plan', planGetSprintArtifacts: 'plan', chatToBoard: 'plan', queueEnqueueComposerPrompt: 'plan', queueRemoveComposerPrompt: 'plan', queueClearComposerPrompts: 'plan',
|
|
341
396
|
getPmTimeline: 'plan',
|
|
342
397
|
planScheduleCreate: 'schedule', planScheduleUpdate: 'schedule', planScheduleDelete: 'schedule', planScheduleList: 'schedule', planScheduleValidateSource: 'schedule',
|
|
343
398
|
agentScheduleCreate: 'agentSchedule', agentScheduleUpdate: 'agentSchedule', agentScheduleDelete: 'agentSchedule', agentScheduleList: 'agentSchedule',
|
|
344
399
|
fileUploadStart: 'fileUpload', fileUploadChunk: 'fileUpload', fileUploadComplete: 'fileUpload', fileUploadCancel: 'fileUpload',
|
|
345
400
|
fileDownloadStart: 'fileDownload', fileDownloadCancel: 'fileDownload',
|
|
346
|
-
sdkConnect: 'sdk', sdkRequest: 'sdk', sdkAgentRun: 'sdk', sdkPmWrite: 'sdk', sdkPmExecute: 'sdk', sdkPmSchedule: 'sdk', sdkFilesWrite: 'sdk', sdkStorageWrite: 'sdk', sdkCancel: 'sdk', sdkRevoke: 'sdk', sdkConsentDecision: 'sdk', sdkAgentSession: 'sdk', sdkAgentInteract: 'sdk', sdkGitWrite: 'sdk', sdkTerminalWrite: 'sdk', sdkToolsServe: 'sdk', sdkQualityWrite: 'sdk', sdkBrowserWrite: 'sdk', sdkInferenceRun: 'sdk', sdkEndpointsWrite: 'sdk',
|
|
401
|
+
sdkConnect: 'sdk', sdkRequest: 'sdk', sdkAgentRun: 'sdk', sdkPmWrite: 'sdk', sdkPmExecute: 'sdk', sdkPmSchedule: 'sdk', sdkPmChain: 'sdk', sdkFilesWrite: 'sdk', sdkStorageWrite: 'sdk', sdkCancel: 'sdk', sdkRevoke: 'sdk', sdkConsentDecision: 'sdk', sdkAgentSession: 'sdk', sdkAgentInteract: 'sdk', sdkGitWrite: 'sdk', sdkTerminalWrite: 'sdk', sdkToolsServe: 'sdk', sdkQualityWrite: 'sdk', sdkBrowserWrite: 'sdk', sdkInferenceRun: 'sdk', sdkEndpointsWrite: 'sdk',
|
|
347
402
|
appInstallLocal: 'apps', appServeStatus: 'apps', appStop: 'apps', appUpdate: 'apps', appGreenlightDecision: 'apps', appSpaceList: 'apps', appSpaceAdd: 'apps', appSpaceRemove: 'apps', appSpaceConfigure: 'apps',
|
|
348
403
|
browser_preflight: 'browser', browser_snapshot: 'browser', browser_resume_driver: 'browser',
|
|
349
404
|
browser_navigate: 'browser', browser_back: 'browser', browser_forward: 'browser', browser_reload: 'browser', browser_stop: 'browser',
|
|
@@ -411,7 +466,7 @@ export class WebSocketImproviseHandler {
|
|
|
411
466
|
this.sendPermissionDenied(ws, msg.type);
|
|
412
467
|
return;
|
|
413
468
|
}
|
|
414
|
-
return handleTerminalMessage(this, ws, msg, tabId, workingDir);
|
|
469
|
+
return handleTerminalMessage(this, ws, msg, tabId, workingDir, viewerId);
|
|
415
470
|
case 'fileExplorer': return handleFileExplorerMessage(this, ws, msg, tabId, effectiveDir, role);
|
|
416
471
|
case 'collab': return handleCollabMessage(this, ws, msg, effectiveDir, role, viewerId);
|
|
417
472
|
case 'git':
|
|
@@ -420,7 +475,7 @@ export class WebSocketImproviseHandler {
|
|
|
420
475
|
return;
|
|
421
476
|
}
|
|
422
477
|
return handleGitMessage(this, ws, msg, tabId, workingDir);
|
|
423
|
-
case 'plan': return handlePlanMessage(this, ws, msg, tabId, workingDir, role);
|
|
478
|
+
case 'plan': return handlePlanMessage(this, ws, msg, tabId, workingDir, role, viewerId);
|
|
424
479
|
case 'schedule': return handleScheduleMessage(this, ws, msg, tabId, workingDir, role);
|
|
425
480
|
case 'agentSchedule': return handleAgentScheduleMessage(this, ws, msg, workingDir, role);
|
|
426
481
|
case 'fileUpload': return this.handleFileUploadMessage(ws, msg, tabId, effectiveDir, role);
|
|
@@ -549,6 +604,12 @@ export class WebSocketImproviseHandler {
|
|
|
549
604
|
if (can(role, 'editSettings'))
|
|
550
605
|
handleSetTabEngine(this, ws, msg, tabId, workingDir);
|
|
551
606
|
return true;
|
|
607
|
+
case 'viewerInterest': {
|
|
608
|
+
const interest = parseStreamInterest(msg.data?.interest);
|
|
609
|
+
if (interest)
|
|
610
|
+
this.viewerInterest.declare(ws, interest);
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
552
613
|
default:
|
|
553
614
|
return false;
|
|
554
615
|
}
|
|
@@ -772,12 +833,25 @@ export class WebSocketImproviseHandler {
|
|
|
772
833
|
}
|
|
773
834
|
}
|
|
774
835
|
handleClose(ws) {
|
|
836
|
+
const deferred = this.deferredCloses.get(ws);
|
|
837
|
+
if (deferred) {
|
|
838
|
+
clearTimeout(deferred);
|
|
839
|
+
this.deferredCloses.delete(ws);
|
|
840
|
+
}
|
|
841
|
+
this.contextViewers.delete(ws);
|
|
842
|
+
const detached = this.detachedContexts.get(ws);
|
|
843
|
+
if (detached) {
|
|
844
|
+
clearTimeout(detached.timer);
|
|
845
|
+
this.detachedContexts.delete(ws);
|
|
846
|
+
this.killDetachedSearches(detached);
|
|
847
|
+
}
|
|
775
848
|
const tabMap = this.connections.get(ws);
|
|
776
849
|
if (tabMap) {
|
|
777
850
|
this.cleanupConnectionResources(tabMap);
|
|
778
851
|
}
|
|
779
852
|
this.connections.delete(ws);
|
|
780
853
|
this.allConnections.delete(ws);
|
|
854
|
+
this.viewerInterest.forget(ws);
|
|
781
855
|
cleanupTerminalSubscribers(this, ws);
|
|
782
856
|
this.sessionRoles.dropWs(ws);
|
|
783
857
|
if (ws._viewerId) {
|
|
@@ -802,34 +876,90 @@ export class WebSocketImproviseHandler {
|
|
|
802
876
|
}
|
|
803
877
|
}
|
|
804
878
|
handleTransportDetach(ws) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
catch { }
|
|
814
|
-
this.activeSearches.delete(tabId);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
879
|
+
if (this.detachedContexts.has(ws))
|
|
880
|
+
return;
|
|
881
|
+
const searches = new Map();
|
|
882
|
+
for (const tabId of this.connections.get(ws)?.keys() ?? []) {
|
|
883
|
+
const search = this.activeSearches.get(tabId);
|
|
884
|
+
if (search)
|
|
885
|
+
searches.set(tabId, search);
|
|
817
886
|
}
|
|
818
887
|
this.connections.delete(ws);
|
|
819
888
|
this.allConnections.delete(ws);
|
|
889
|
+
this.viewerInterest.forget(ws);
|
|
820
890
|
cleanupTerminalSubscribers(this, ws);
|
|
821
891
|
this.sessionRoles.dropWs(ws);
|
|
892
|
+
this.collabSessions.onTransportDetach();
|
|
893
|
+
for (const viewerId of this.contextViewers.get(ws) ?? [])
|
|
894
|
+
holdAppToolCallsFor(viewerId);
|
|
895
|
+
this.contextViewers.delete(ws);
|
|
896
|
+
const timer = setTimeout(() => this.expireDetachedContext(ws), VIEWER_DETACH_GRACE_MS);
|
|
897
|
+
timer.unref?.();
|
|
898
|
+
this.detachedContexts.set(ws, { searches, resumed: false, timer });
|
|
899
|
+
}
|
|
900
|
+
expireDetachedContext(ws) {
|
|
901
|
+
const detached = this.detachedContexts.get(ws);
|
|
902
|
+
if (!detached)
|
|
903
|
+
return;
|
|
904
|
+
this.detachedContexts.delete(ws);
|
|
905
|
+
if (!detached.resumed)
|
|
906
|
+
this.killDetachedSearches(detached);
|
|
822
907
|
if (this.browserHost) {
|
|
823
908
|
try {
|
|
824
909
|
onBrowserWsContextClosed(this.browserHost, ws);
|
|
825
910
|
}
|
|
826
911
|
catch (err) {
|
|
827
|
-
captureException(err, { context: 'websocket.
|
|
912
|
+
captureException(err, { context: 'websocket.detachGrace.browserRelease' });
|
|
828
913
|
}
|
|
829
914
|
}
|
|
830
|
-
|
|
915
|
+
}
|
|
916
|
+
killDetachedSearches(detached) {
|
|
917
|
+
for (const [tabId, search] of detached.searches) {
|
|
918
|
+
if (this.activeSearches.get(tabId) !== search)
|
|
919
|
+
continue;
|
|
920
|
+
try {
|
|
921
|
+
search.kill();
|
|
922
|
+
}
|
|
923
|
+
catch { }
|
|
924
|
+
this.activeSearches.delete(tabId);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
handleViewerConnected(ws, viewerId) {
|
|
928
|
+
let known = this.contextViewers.get(ws);
|
|
929
|
+
if (!known) {
|
|
930
|
+
known = new Set();
|
|
931
|
+
this.contextViewers.set(ws, known);
|
|
932
|
+
}
|
|
933
|
+
known.add(viewerId);
|
|
934
|
+
this.handleWebConnected();
|
|
935
|
+
for (const from of this.detachedContexts.keys())
|
|
936
|
+
moveViewerToContext(viewerId, from, ws);
|
|
937
|
+
resumeAppToolCallsFor(viewerId);
|
|
938
|
+
}
|
|
939
|
+
handleWebConnected() {
|
|
940
|
+
if (this.deferredCloses.size > 0) {
|
|
941
|
+
console.log('[handler] A viewer is back — the pending web_disconnected teardown is cancelled');
|
|
942
|
+
}
|
|
943
|
+
for (const timer of this.deferredCloses.values())
|
|
944
|
+
clearTimeout(timer);
|
|
945
|
+
this.deferredCloses.clear();
|
|
946
|
+
for (const detached of this.detachedContexts.values())
|
|
947
|
+
detached.resumed = true;
|
|
948
|
+
}
|
|
949
|
+
handleWebDisconnected(ws, onClosed) {
|
|
950
|
+
if (this.deferredCloses.has(ws))
|
|
951
|
+
return;
|
|
952
|
+
const timer = setTimeout(() => {
|
|
953
|
+
this.deferredCloses.delete(ws);
|
|
954
|
+
this.handleClose(ws);
|
|
955
|
+
onClosed?.();
|
|
956
|
+
}, VIEWER_DETACH_GRACE_MS);
|
|
957
|
+
timer.unref?.();
|
|
958
|
+
this.deferredCloses.set(ws, timer);
|
|
831
959
|
}
|
|
832
960
|
handleBrowserViewerDisconnected(viewerId) {
|
|
961
|
+
for (const known of this.contextViewers.values())
|
|
962
|
+
known.delete(viewerId);
|
|
833
963
|
teardownSdkForViewer(viewerId);
|
|
834
964
|
this.sessionRoles.dropViewer(viewerId);
|
|
835
965
|
this.collabSessions.dropViewer(viewerId);
|
|
@@ -952,23 +1082,45 @@ export class WebSocketImproviseHandler {
|
|
|
952
1082
|
}
|
|
953
1083
|
}
|
|
954
1084
|
send(ws, response) {
|
|
955
|
-
|
|
956
|
-
ws.send(JSON.stringify(response));
|
|
957
|
-
}
|
|
958
|
-
catch (error) {
|
|
959
|
-
console.error('[WebSocketImproviseHandler] Error sending message:', error);
|
|
960
|
-
}
|
|
1085
|
+
this.write(ws, response, this.replyRecorders.getStore());
|
|
961
1086
|
}
|
|
962
1087
|
broadcastToOthers(sender, response) {
|
|
963
1088
|
for (const ws of this.allConnections) {
|
|
964
1089
|
if (ws !== sender) {
|
|
965
|
-
this.
|
|
1090
|
+
this.write(ws, response);
|
|
966
1091
|
}
|
|
967
1092
|
}
|
|
968
1093
|
}
|
|
969
1094
|
broadcastToAll(response) {
|
|
970
1095
|
for (const ws of this.allConnections) {
|
|
971
|
-
this.
|
|
1096
|
+
this.write(ws, response);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
broadcastStream(response) {
|
|
1100
|
+
for (const ws of this.allConnections) {
|
|
1101
|
+
if (!this.viewerInterest.wantsStreams(ws))
|
|
1102
|
+
continue;
|
|
1103
|
+
this.write(ws, response);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
write(ws, response, recorder) {
|
|
1107
|
+
let frame;
|
|
1108
|
+
try {
|
|
1109
|
+
frame = JSON.stringify(response);
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
console.error('[WebSocketImproviseHandler] Error sending message:', error);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
recorder?.record(ws, response.type, frame, response._target);
|
|
1116
|
+
this.deliver(ws, frame);
|
|
1117
|
+
}
|
|
1118
|
+
deliver(ws, frame) {
|
|
1119
|
+
try {
|
|
1120
|
+
ws.send(frame);
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
console.error('[WebSocketImproviseHandler] Error sending message:', error);
|
|
972
1124
|
}
|
|
973
1125
|
}
|
|
974
1126
|
cleanupSession(sessionId) {
|
|
@@ -2,27 +2,89 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
const DEFAULT_TTL_MS = 15 * 60 * 1000;
|
|
4
4
|
const DEFAULT_MAX_PER_TAB = 512;
|
|
5
|
+
const DEFAULT_MAX_MUTATIONS = 4096;
|
|
6
|
+
export const IDEMPOTENT_MUTATION_TYPES = new Set([
|
|
7
|
+
'writeFile', 'createFile', 'createDirectory', 'deleteFile', 'renameFile',
|
|
8
|
+
'fileUploadStart', 'fileUploadChunk', 'fileUploadComplete', 'fileUploadCancel',
|
|
9
|
+
'gitStage', 'gitUnstage', 'gitCommit', 'gitCommitWithAI', 'gitPush', 'gitPull', 'gitSetDirectory',
|
|
10
|
+
'gitCheckout', 'gitCreateBranch', 'gitDeleteBranch', 'gitCreatePR', 'gitCreateTag', 'gitPushTag',
|
|
11
|
+
'gitWorktreeCreate', 'gitWorktreeCreateAndAssign', 'gitWorktreeRemove', 'gitWorktreePush', 'gitWorktreeCreatePR',
|
|
12
|
+
'gitWorktreeMerge', 'gitMergeAbort', 'gitMergeComplete', 'gitMergeStashPop', 'gitMergeDiscardBlockers',
|
|
13
|
+
'tabWorktreeSwitch', 'tabRepoWorktreeSwitch',
|
|
14
|
+
'cancel', 'approve', 'reject',
|
|
15
|
+
'removeTab',
|
|
16
|
+
'askUserQuestionResponse',
|
|
17
|
+
'planReviewDecision',
|
|
18
|
+
'sdkPmWrite', 'sdkFilesWrite', 'sdkStorageWrite', 'sdkGitWrite', 'sdkTerminalWrite', 'sdkQualityWrite',
|
|
19
|
+
'sdkBrowserWrite', 'sdkEndpointsWrite',
|
|
20
|
+
'sdkPmChain',
|
|
21
|
+
]);
|
|
22
|
+
export const MUTATION_SCOPE = '\u0000mutations';
|
|
23
|
+
const MAX_MSG_ID_LENGTH = 128;
|
|
24
|
+
export function mutationMsgId(msg) {
|
|
25
|
+
if (!IDEMPOTENT_MUTATION_TYPES.has(msg.type))
|
|
26
|
+
return undefined;
|
|
27
|
+
const msgId = msg.data?.msgId;
|
|
28
|
+
return typeof msgId === 'string' && msgId.length > 0 && msgId.length <= MAX_MSG_ID_LENGTH ? msgId : undefined;
|
|
29
|
+
}
|
|
30
|
+
const MAX_CACHED_REPLIES = 8;
|
|
31
|
+
const MAX_CACHED_REPLY_BYTES = 4 * 1024;
|
|
32
|
+
export class ReplyRecorder {
|
|
33
|
+
requester;
|
|
34
|
+
requestType;
|
|
35
|
+
frames = [];
|
|
36
|
+
bytes = 0;
|
|
37
|
+
overflowed = false;
|
|
38
|
+
open = true;
|
|
39
|
+
constructor(requester, requestType) {
|
|
40
|
+
this.requester = requester;
|
|
41
|
+
this.requestType = requestType;
|
|
42
|
+
}
|
|
43
|
+
record(to, replyType, frame, target) {
|
|
44
|
+
if (!this.open || this.overflowed || to !== this.requester || target !== undefined)
|
|
45
|
+
return;
|
|
46
|
+
if (this.requestType.startsWith('sdk') ? replyType !== 'sdkResponse' : replyType.startsWith('sdk'))
|
|
47
|
+
return;
|
|
48
|
+
this.bytes += frame.length;
|
|
49
|
+
if (this.frames.length >= MAX_CACHED_REPLIES || this.bytes > MAX_CACHED_REPLY_BYTES) {
|
|
50
|
+
this.overflowed = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.frames.push(frame);
|
|
54
|
+
}
|
|
55
|
+
close() {
|
|
56
|
+
this.open = false;
|
|
57
|
+
return this.overflowed || this.frames.length === 0 ? null : this.frames;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
5
60
|
export class MsgIdTracker {
|
|
6
61
|
ttlMs;
|
|
7
62
|
maxPerTab;
|
|
8
63
|
now;
|
|
64
|
+
maxMutations;
|
|
9
65
|
tabs = new Map();
|
|
10
|
-
constructor(ttlMs = DEFAULT_TTL_MS, maxPerTab = DEFAULT_MAX_PER_TAB, now = Date.now) {
|
|
66
|
+
constructor(ttlMs = DEFAULT_TTL_MS, maxPerTab = DEFAULT_MAX_PER_TAB, now = Date.now, maxMutations = DEFAULT_MAX_MUTATIONS) {
|
|
11
67
|
this.ttlMs = ttlMs;
|
|
12
68
|
this.maxPerTab = maxPerTab;
|
|
13
69
|
this.now = now;
|
|
70
|
+
this.maxMutations = maxMutations;
|
|
14
71
|
}
|
|
15
72
|
recordIfFirst(tabId, msgId) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
73
|
+
return this.seeIfFirst(tabId, msgId, this.maxPerTab) === null;
|
|
74
|
+
}
|
|
75
|
+
admit(scope, msgId) {
|
|
76
|
+
const seen = this.seeIfFirst(scope, msgId, this.maxMutations, true);
|
|
77
|
+
if (seen === null)
|
|
78
|
+
return null;
|
|
79
|
+
return { replies: seen.replies ?? null, pending: seen.pending === true };
|
|
80
|
+
}
|
|
81
|
+
settle(scope, msgId, replies) {
|
|
82
|
+
const seen = this.tabs.get(scope)?.ids.get(msgId);
|
|
83
|
+
if (!seen)
|
|
84
|
+
return;
|
|
85
|
+
seen.pending = false;
|
|
86
|
+
seen.replies = replies;
|
|
87
|
+
seen.at = this.now();
|
|
26
88
|
}
|
|
27
89
|
forget(tabId) {
|
|
28
90
|
this.tabs.delete(tabId);
|
|
@@ -30,19 +92,32 @@ export class MsgIdTracker {
|
|
|
30
92
|
size(tabId) {
|
|
31
93
|
return this.tabs.get(tabId)?.ids.size ?? 0;
|
|
32
94
|
}
|
|
95
|
+
seeIfFirst(tabId, msgId, cap, pending = false) {
|
|
96
|
+
const entry = this.tabs.get(tabId) ?? { ids: new Map() };
|
|
97
|
+
this.evictExpired(entry);
|
|
98
|
+
const seen = entry.ids.get(msgId);
|
|
99
|
+
if (seen) {
|
|
100
|
+
seen.at = this.now();
|
|
101
|
+
return seen;
|
|
102
|
+
}
|
|
103
|
+
entry.ids.set(msgId, pending ? { at: this.now(), pending } : { at: this.now() });
|
|
104
|
+
this.enforceSizeCap(entry, cap);
|
|
105
|
+
this.tabs.set(tabId, entry);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
33
108
|
evictExpired(entry) {
|
|
34
109
|
const cutoff = this.now() - this.ttlMs;
|
|
35
|
-
for (const [id,
|
|
36
|
-
if (
|
|
110
|
+
for (const [id, seen] of entry.ids) {
|
|
111
|
+
if (seen.at < cutoff && !seen.pending)
|
|
37
112
|
entry.ids.delete(id);
|
|
38
113
|
}
|
|
39
114
|
}
|
|
40
|
-
enforceSizeCap(entry) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
115
|
+
enforceSizeCap(entry, cap) {
|
|
116
|
+
for (const [id, seen] of entry.ids) {
|
|
117
|
+
if (entry.ids.size <= cap)
|
|
118
|
+
return;
|
|
119
|
+
if (!seen.pending)
|
|
120
|
+
entry.ids.delete(id);
|
|
46
121
|
}
|
|
47
122
|
}
|
|
48
123
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { sendAgentPush } from '../agent-push.js';
|
|
3
4
|
import { handlePlanPrompt, parseModelDirective, parseRunModel } from '../plan/composer.js';
|
|
5
|
+
import { resolveReadyToWork } from '../plan/dependency-resolver.js';
|
|
4
6
|
import { parseBoardDirectory, parsePlanDirectory, resolvePmDir } from '../plan/parser.js';
|
|
5
7
|
import { PM_BOARD_ISSUE_ID, PM_COMPOSER_ISSUE_ID, pmTimelineIssueId } from '../plan/pm-timeline.js';
|
|
8
|
+
import { broadcastPlanStateIfChanged } from '../plan/watcher.js';
|
|
6
9
|
import { scopeExecutionDir } from './handler-context.js';
|
|
7
|
-
import { denyIfViewOnly, executorCache, getExecutor } from './plan-helpers.js';
|
|
10
|
+
import { denyIfViewOnly, executorCache, getExecutor, notifyChainExecutorStatus, notifyChainRunEnded, } from './plan-helpers.js';
|
|
8
11
|
import { broadcastPmEvent, pmTimelineKey } from './pm-timeline-broadcast.js';
|
|
9
12
|
import { mergePreUploadedAttachments } from './session-handlers.js';
|
|
10
13
|
export function handlePrompt(ctx, ws, msg, workingDir, permission) {
|
|
@@ -42,6 +45,7 @@ export function wireExecutorEvents(executor, ctx, workingDir, boardId, agentTabI
|
|
|
42
45
|
const timelineKey = (issueId) => pmTimelineKey(boardId ?? null, pmTimelineIssueId(issueId));
|
|
43
46
|
executor.on('statusChanged', (status) => {
|
|
44
47
|
ctx.broadcastToAll({ type: 'planExecutionProgress', data: { status, ...tag } });
|
|
48
|
+
notifyChainExecutorStatus(workingDir, status);
|
|
45
49
|
});
|
|
46
50
|
executor.on('issueStarted', (issue) => {
|
|
47
51
|
ctx.broadcastToAll({
|
|
@@ -80,7 +84,7 @@ export function wireExecutorEvents(executor, ctx, workingDir, boardId, agentTabI
|
|
|
80
84
|
ctx.broadcastToAll({ type: 'planExecutionMetrics', data: executor.getMetrics() });
|
|
81
85
|
const fullState = parsePlanDirectory(workingDir);
|
|
82
86
|
if (fullState) {
|
|
83
|
-
ctx
|
|
87
|
+
broadcastPlanStateIfChanged(ctx, workingDir, fullState);
|
|
84
88
|
}
|
|
85
89
|
});
|
|
86
90
|
executor.on('issueError', (data) => {
|
|
@@ -110,22 +114,34 @@ export function wireExecutorEvents(executor, ctx, workingDir, boardId, agentTabI
|
|
|
110
114
|
executor.on('stateUpdated', () => {
|
|
111
115
|
const fullState = parsePlanDirectory(workingDir);
|
|
112
116
|
if (fullState) {
|
|
113
|
-
ctx
|
|
117
|
+
broadcastPlanStateIfChanged(ctx, workingDir, fullState);
|
|
114
118
|
}
|
|
115
119
|
});
|
|
116
120
|
executor.on('reviewProgress', (data) => {
|
|
117
121
|
broadcastPmEvent(ctx, timelineKey(data.issueId), 'planReviewProgress', { ...data, ...tag });
|
|
122
|
+
if (data.status === 'awaiting_approval') {
|
|
123
|
+
sendAgentPush({
|
|
124
|
+
event: 'approval_needed',
|
|
125
|
+
tabId: `${boardId ?? 'pm'}::${data.issueId}`,
|
|
126
|
+
workingDirectory: workingDir,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
executor.on('issueAbandoned', (data) => {
|
|
131
|
+
broadcastPmEvent(ctx, timelineKey(data.issueId), 'planIssueAbandoned', { ...data, ...tag });
|
|
118
132
|
});
|
|
119
133
|
executor.on('complete', (reason) => {
|
|
120
134
|
ctx.broadcastToAll({ type: 'planExecutionComplete', data: { reason, ...tag, metrics: executor.getMetrics() } });
|
|
121
135
|
ctx.commitSuggestions?.notifyExecutionSettled(settleDir);
|
|
136
|
+
notifyChainRunEnded(workingDir, boardId ?? null, 'complete');
|
|
122
137
|
});
|
|
123
138
|
executor.on('error', (error) => {
|
|
124
139
|
ctx.broadcastToAll({ type: 'planExecutionError', data: { error, ...tag } });
|
|
125
140
|
ctx.commitSuggestions?.notifyExecutionSettled(settleDir);
|
|
141
|
+
notifyChainRunEnded(workingDir, boardId ?? null, 'error');
|
|
126
142
|
});
|
|
127
143
|
}
|
|
128
|
-
function broadcastBoardRunPrompt(ctx, boardId, prompt) {
|
|
144
|
+
export function broadcastBoardRunPrompt(ctx, boardId, prompt) {
|
|
129
145
|
broadcastPmEvent(ctx, pmTimelineKey(boardId, PM_BOARD_ISSUE_ID), 'planExecutionPrompt', {
|
|
130
146
|
issueId: PM_BOARD_ISSUE_ID,
|
|
131
147
|
prompt,
|
|
@@ -139,12 +155,24 @@ export function maybeStartAutoImplement(ctx, workingDir, boardId) {
|
|
|
139
155
|
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
140
156
|
if (!boardState?.board.autoImplement)
|
|
141
157
|
return;
|
|
158
|
+
startBoardRunIfIdle(ctx, workingDir, boardId, `Auto-implement board ${boardId}`);
|
|
159
|
+
}
|
|
160
|
+
export function maybeContinueAfterApproval(ctx, workingDir, boardId) {
|
|
161
|
+
const pmDir = resolvePmDir(workingDir);
|
|
162
|
+
if (!pmDir)
|
|
163
|
+
return;
|
|
164
|
+
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
165
|
+
if (!boardState || resolveReadyToWork(boardState.issues).length === 0)
|
|
166
|
+
return;
|
|
167
|
+
startBoardRunIfIdle(ctx, workingDir, boardId, `Continue board ${boardId} after an approval`);
|
|
168
|
+
}
|
|
169
|
+
function startBoardRunIfIdle(ctx, workingDir, boardId, launchPrompt) {
|
|
142
170
|
const executor = getExecutor(workingDir);
|
|
143
171
|
if (executor.getStatus() === 'executing' || executor.getStatus() === 'starting')
|
|
144
172
|
return;
|
|
145
173
|
const executionDir = scopeExecutionDir(ctx, boardId);
|
|
146
174
|
wireExecutorEvents(executor, ctx, workingDir, boardId, undefined, executionDir);
|
|
147
|
-
broadcastBoardRunPrompt(ctx, boardId,
|
|
175
|
+
broadcastBoardRunPrompt(ctx, boardId, launchPrompt);
|
|
148
176
|
ctx.broadcastToAll({ type: 'planExecutionStarted', data: { status: 'executing', boardId } });
|
|
149
177
|
executor.startBoard(boardId, executionDir).catch(error => {
|
|
150
178
|
ctx.broadcastToAll({
|
|
@@ -4,12 +4,12 @@ import { can } from '@minionry/roles';
|
|
|
4
4
|
import { dispatchByType } from './dispatch-util.js';
|
|
5
5
|
import { handleArchiveBoard, handleChatToBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleExportBoard as handlePublishExportBoard, handleReorderBoards, handleRestoreBoard, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
|
|
6
6
|
import { handleExecute, handleExecuteEpic, handleGetPmTimeline, handlePause, handlePrompt, handleResume, handleStop } from './plan-execution-handlers.js';
|
|
7
|
-
import { handleDeleteIssue, handleGetIssue, handleGetMilestone, handleGetSprint, handleListIssues, handlePlanInit, handleScaffold, handleUpdateIssue } from './plan-issue-handlers.js';
|
|
7
|
+
import { handleDeleteIssue, handleGetIssue, handleGetMilestone, handleGetSprint, handleListIssues, handlePlanInit, handleReviewDecision, handleScaffold, handleUpdateIssue } from './plan-issue-handlers.js';
|
|
8
8
|
import { handleActivateSprint, handleCompleteSprint, handleCreateSprint, handleGetSprintArtifacts } from './plan-sprint-handlers.js';
|
|
9
9
|
import { handleDownloadBoard, handleImportBoard, handleInstantiateTemplate, handleExportBoard as handleTransferExportBoard } from './plan-transfer-handlers.js';
|
|
10
10
|
import { handleQueueClearComposerPrompts, handleQueueEnqueueComposerPrompt, handleQueueRemoveComposerPrompt } from './prompt-queue-handlers.js';
|
|
11
11
|
export { cleanupPlanResources } from './plan-helpers.js';
|
|
12
|
-
export function buildPlanHandlers(ctx, ws, msg, workingDir, editPlanGate) {
|
|
12
|
+
export function buildPlanHandlers(ctx, ws, msg, workingDir, editPlanGate, decidedBy = { role: 'owner' }) {
|
|
13
13
|
return {
|
|
14
14
|
planInit: () => handlePlanInit(ctx, ws, workingDir),
|
|
15
15
|
planGetState: () => handlePlanInit(ctx, ws, workingDir),
|
|
@@ -17,7 +17,8 @@ export function buildPlanHandlers(ctx, ws, msg, workingDir, editPlanGate) {
|
|
|
17
17
|
planGetIssue: () => handleGetIssue(ctx, ws, msg, workingDir),
|
|
18
18
|
planGetSprint: () => handleGetSprint(ctx, ws, msg, workingDir),
|
|
19
19
|
planGetMilestone: () => handleGetMilestone(ctx, ws, msg, workingDir),
|
|
20
|
-
planUpdateIssue: () => handleUpdateIssue(ctx, ws, msg, workingDir, editPlanGate),
|
|
20
|
+
planUpdateIssue: () => handleUpdateIssue(ctx, ws, msg, workingDir, editPlanGate, decidedBy),
|
|
21
|
+
planReviewDecision: () => handleReviewDecision(ctx, ws, msg, workingDir, editPlanGate, decidedBy),
|
|
21
22
|
planDeleteIssue: () => handleDeleteIssue(ctx, ws, msg, workingDir, editPlanGate),
|
|
22
23
|
planScaffold: () => handleScaffold(ctx, ws, msg, workingDir, editPlanGate),
|
|
23
24
|
planPrompt: () => handlePrompt(ctx, ws, msg, workingDir, editPlanGate),
|
|
@@ -52,9 +53,13 @@ export function buildPlanHandlers(ctx, ws, msg, workingDir, editPlanGate) {
|
|
|
52
53
|
queueClearComposerPrompts: () => handleQueueClearComposerPrompts(ctx, ws, msg, workingDir, editPlanGate),
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
|
-
export function handlePlanMessage(ctx, ws, msg, _tabId, workingDir, role
|
|
56
|
+
export function handlePlanMessage(ctx, ws, msg, _tabId, workingDir, role,
|
|
57
|
+
viewerId) {
|
|
56
58
|
const editPlanGate = can(role, 'editPlan') ? undefined : 'view';
|
|
57
|
-
const handlers = buildPlanHandlers(ctx, ws, msg, workingDir, editPlanGate
|
|
59
|
+
const handlers = buildPlanHandlers(ctx, ws, msg, workingDir, editPlanGate, {
|
|
60
|
+
role,
|
|
61
|
+
...(viewerId ? { viewerId } : {}),
|
|
62
|
+
});
|
|
58
63
|
try {
|
|
59
64
|
dispatchByType(handlers, 'plan', msg.type, (type) => {
|
|
60
65
|
ctx.send(ws, { type: 'planError', data: { error: `Unknown plan message type: ${type}` } });
|