@bbigbang/agent-node 0.1.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.
Files changed (47) hide show
  1. package/dist/agentHost.js +483 -0
  2. package/dist/appVersion.js +14 -0
  3. package/dist/assetCachePaths.js +35 -0
  4. package/dist/attachmentInput.js +588 -0
  5. package/dist/attachmentMaterializer.js +230 -0
  6. package/dist/bigbangCli.js +17 -0
  7. package/dist/bigbangMessageSendDetection.js +284 -0
  8. package/dist/builtinSkillRoots.js +54 -0
  9. package/dist/claudeConfig.js +32 -0
  10. package/dist/claudeDirectRuntime.js +1960 -0
  11. package/dist/claudeSessionControls.js +78 -0
  12. package/dist/claudeTranscriptFs.js +147 -0
  13. package/dist/codexAppServerClient.js +188 -0
  14. package/dist/codexAppServerEnv.js +14 -0
  15. package/dist/codexAppServerRpc.js +273 -0
  16. package/dist/codexAppServerRuntime.js +3495 -0
  17. package/dist/codexBuiltinPrompt.js +117 -0
  18. package/dist/codexConversationSummarizer.js +76 -0
  19. package/dist/codexTranscriptFs.js +145 -0
  20. package/dist/config.js +129 -0
  21. package/dist/connection.js +151 -0
  22. package/dist/dispatchQueueStore.js +39 -0
  23. package/dist/dreamEnv.js +1 -0
  24. package/dist/dreamMemoryFallback.js +118 -0
  25. package/dist/dreamToolPolicy.js +293 -0
  26. package/dist/droidMissionRunner.js +808 -0
  27. package/dist/executor.js +1078 -0
  28. package/dist/hostRuntime.js +1 -0
  29. package/dist/libraryAuthorityFs.js +74 -0
  30. package/dist/libraryMirror.js +183 -0
  31. package/dist/main.js +1659 -0
  32. package/dist/native-worker/native-worker.mjs +475 -0
  33. package/dist/nativeMissionAgentDispatch.js +463 -0
  34. package/dist/nativeMissionRunner.js +461 -0
  35. package/dist/nativeSkillMounts.js +204 -0
  36. package/dist/nativeWorkerHost.js +142 -0
  37. package/dist/nodeSink.js +142 -0
  38. package/dist/panelHttpFetch.js +334 -0
  39. package/dist/runtimeDrivers.js +62 -0
  40. package/dist/skillFs.js +229 -0
  41. package/dist/soloHost.js +165 -0
  42. package/dist/soloNodeSink.js +138 -0
  43. package/dist/terminalManager.js +254 -0
  44. package/dist/workspaceFs.js +1020 -0
  45. package/dist/workspaceGit.js +694 -0
  46. package/dist/workspaceInspect.js +22 -0
  47. package/package.json +49 -0
@@ -0,0 +1,1078 @@
1
+ import { createRequire } from 'node:module';
2
+ import { randomUUID } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
+ import { ToolAuth, WorkspaceLockManager, clearAcpSessionId, createSession, finishRun, getSession, upsertBinding, log, } from '@bbigbang/runtime-acp';
7
+ import { getRuntimeDriver, } from '@bbigbang/protocol';
8
+ import { AgentHost, HostDispatchRejectedError } from './agentHost.js';
9
+ import { ensureIsolatedClaudeConfig } from './claudeConfig.js';
10
+ import { ensureNativeSkillMounts } from './nativeSkillMounts.js';
11
+ import { ensureWorkspaceScaffold, migrateWorkspaceLegacyFiles } from './workspaceFs.js';
12
+ import { enqueueDispatch, listPendingDispatches, removeDispatch, removeDispatchesForHostKey, removeDispatchesForSessionKey, updateDispatchState, } from './dispatchQueueStore.js';
13
+ import { buildHostAssetCacheRoot } from './assetCachePaths.js';
14
+ import { resolveBigbangCliEntry } from './bigbangCli.js';
15
+ import { mergeDreamRuntimeEnv } from './dreamEnv.js';
16
+ function isDreamHostKey(hostKey) {
17
+ return hostKey.startsWith('dream:');
18
+ }
19
+ import { SoloHost } from './soloHost.js';
20
+ function resolveClaudeStartupModel(params) {
21
+ const explicitModel = params.explicitModel?.trim();
22
+ if (explicitModel)
23
+ return explicitModel;
24
+ const envModel = params.env?.ANTHROPIC_MODEL?.trim();
25
+ return envModel ? envModel : null;
26
+ }
27
+ function appendClaudeStartupArgs(agentArgs, params) {
28
+ const model = resolveClaudeStartupModel(params);
29
+ if (model) {
30
+ agentArgs.push('--model', model);
31
+ }
32
+ }
33
+ export class Executor {
34
+ db;
35
+ config;
36
+ toolAuth;
37
+ hosts = new Map();
38
+ hostBridgeSignatures = new Map();
39
+ hostAgentCommandEnvOverlays = new Map();
40
+ hostMetadata = new Map();
41
+ runToHost = new Map();
42
+ steerChains = new Map();
43
+ send;
44
+ terminalRunIds = new Set();
45
+ createHost;
46
+ createSoloHost;
47
+ hostSweepTimer;
48
+ workspaceLockManager;
49
+ restoredDispatchContext = new AsyncLocalStorage();
50
+ soloHosts = new Map();
51
+ closingSoloHosts = new Set();
52
+ constructor(params) {
53
+ this.db = params.db;
54
+ this.config = params.config;
55
+ this.toolAuth = new ToolAuth(params.db);
56
+ this.send = (msg) => {
57
+ if (msg.type === 'run.end') {
58
+ this.terminalRunIds.add(msg.runId);
59
+ }
60
+ params.send(msg);
61
+ };
62
+ this.createHost = params.createHost ?? ((hostParams) => new AgentHost(hostParams));
63
+ this.createSoloHost = params.createSoloHost ?? ((soloHostParams) => new SoloHost(soloHostParams));
64
+ this.workspaceLockManager = params.workspaceLockManager ?? new WorkspaceLockManager();
65
+ this.hostSweepTimer = setInterval(() => {
66
+ this.reapIdleHosts();
67
+ this.reapIdleSoloHosts();
68
+ }, this.config.hostSweepIntervalMs);
69
+ this.hostSweepTimer.unref?.();
70
+ }
71
+ buildHostConfigSignature(params) {
72
+ return JSON.stringify({
73
+ agentCommand: params.agentCommand,
74
+ agentArgs: params.agentArgs,
75
+ env: params.env,
76
+ model: params.agentType === 'claude_sdk' || params.agentType === 'codex_app_server' ? null : params.model ?? null,
77
+ reasoningEffort: params.agentType === 'claude_sdk' || params.agentType === 'codex_app_server' ? null : params.reasoningEffort ?? null,
78
+ claudePermissionMode: null,
79
+ codexServiceTier: params.agentType === 'codex_app_server' ? null : params.codexServiceTier ?? null,
80
+ systemPromptText: params.systemPromptText ?? null,
81
+ disabledToolKinds: [...(params.disabledToolKinds ?? [])].sort(),
82
+ assetAccessConfig: params.assetAccessConfig ?? null,
83
+ agentSurfaceMode: params.agentSurfaceMode,
84
+ workspaceRoot: params.workspaceRoot,
85
+ });
86
+ }
87
+ buildChannelBridgeMcpEntry(params) {
88
+ try {
89
+ const req = createRequire(import.meta.url);
90
+ const binPath = req.resolve('@bbigbang/channel-bridge');
91
+ const args = [
92
+ binPath,
93
+ '--agent-id',
94
+ params.agentId,
95
+ '--conversation-id',
96
+ params.conversationId,
97
+ '--server-url',
98
+ params.serverUrl,
99
+ '--asset-cache-root',
100
+ params.assetCacheRoot,
101
+ '--run-context-path',
102
+ path.join(params.assetCacheRoot, 'run-context.json'),
103
+ '--workspace-path',
104
+ params.workspaceRoot,
105
+ ];
106
+ return {
107
+ name: 'chat',
108
+ command: 'node',
109
+ args,
110
+ env: params.authToken ? [{ name: 'CHANNEL_BRIDGE_AUTH_TOKEN', value: params.authToken }] : [],
111
+ };
112
+ }
113
+ catch {
114
+ log.warn('[executor] channel-bridge package not found, skipping MCP injection');
115
+ return undefined;
116
+ }
117
+ }
118
+ buildAgentCommandEnvOverlay(params) {
119
+ if (!params.assetAccessConfig || !params.assetCacheRoot) {
120
+ return undefined;
121
+ }
122
+ const overlay = {
123
+ vars: {
124
+ BIGBANG_AGENT_ID: params.assetAccessConfig.agentId,
125
+ BIGBANG_CONVERSATION_ID: params.assetAccessConfig.conversationId,
126
+ BIGBANG_SERVER_URL: params.assetAccessConfig.serverUrl,
127
+ BIGBANG_RUN_CONTEXT_PATH: path.join(params.assetCacheRoot, 'run-context.json'),
128
+ },
129
+ };
130
+ if (params.assetAccessConfig.authToken) {
131
+ try {
132
+ fs.mkdirSync(params.assetCacheRoot, { recursive: true });
133
+ const tokenPath = path.join(params.assetCacheRoot, 'agent-command-token');
134
+ fs.writeFileSync(tokenPath, `${params.assetAccessConfig.authToken}\n`, { encoding: 'utf8', mode: 0o600 });
135
+ overlay.vars.BIGBANG_AUTH_TOKEN_FILE = tokenPath;
136
+ }
137
+ catch (error) {
138
+ log.warn('[executor] failed to write agent command token file; falling back to env token', {
139
+ agentId: params.assetAccessConfig.agentId,
140
+ conversationId: params.assetAccessConfig.conversationId,
141
+ error: String(error?.message ?? error),
142
+ });
143
+ overlay.vars.BIGBANG_AUTH_TOKEN = params.assetAccessConfig.authToken;
144
+ }
145
+ }
146
+ try {
147
+ const bigbangEntry = resolveBigbangCliEntry();
148
+ if (!bigbangEntry) {
149
+ throw new Error('Bigbang CLI entrypoint not found');
150
+ }
151
+ const binDir = path.join(params.assetCacheRoot, 'bin');
152
+ fs.mkdirSync(binDir, { recursive: true });
153
+ const wrapperPath = path.join(binDir, process.platform === 'win32' ? 'bigbang.cmd' : 'bigbang');
154
+ if (process.platform === 'win32') {
155
+ fs.writeFileSync(wrapperPath, `@echo off\r\n"${process.execPath}" "${bigbangEntry}" %*\r\n`, { encoding: 'utf8', mode: 0o755 });
156
+ }
157
+ else {
158
+ fs.writeFileSync(wrapperPath, `#!/usr/bin/env bash\nexec "${process.execPath}" "${bigbangEntry}" "$@"\n`, { encoding: 'utf8', mode: 0o755 });
159
+ }
160
+ overlay.pathPrefix = binDir;
161
+ overlay.vars.BIGBANG_BIGBANG_BIN = wrapperPath;
162
+ }
163
+ catch (error) {
164
+ const message = String(error?.message ?? error);
165
+ log.warn('[executor] failed to expose bigbang CLI on runtime PATH', {
166
+ agentId: params.assetAccessConfig.agentId,
167
+ conversationId: params.assetAccessConfig.conversationId,
168
+ error: message,
169
+ });
170
+ throw new Error(`Failed to expose Bigbang CLI on runtime PATH: ${message}`);
171
+ }
172
+ return overlay;
173
+ }
174
+ mergeAgentCommandEnv(baseEnv, overlay) {
175
+ if (!overlay)
176
+ return baseEnv;
177
+ const env = { ...(baseEnv ?? {}), ...overlay.vars };
178
+ if (overlay.vars.BIGBANG_AUTH_TOKEN_FILE) {
179
+ delete env.BIGBANG_AUTH_TOKEN;
180
+ }
181
+ if (overlay.pathPrefix) {
182
+ env.PATH = `${overlay.pathPrefix}${path.delimiter}${baseEnv?.PATH ?? process.env.PATH ?? ''}`;
183
+ }
184
+ return env;
185
+ }
186
+ getOrCreateHost(params) {
187
+ ensureWorkspaceScaffold(params.workspaceRoot, params.agentName);
188
+ migrateWorkspaceLegacyFiles(params.workspaceRoot);
189
+ const hostConfigSignature = this.buildHostConfigSignature({
190
+ agentType: params.agentType,
191
+ agentCommand: params.agentCommand,
192
+ agentArgs: params.agentArgs,
193
+ env: params.env,
194
+ model: params.model,
195
+ reasoningEffort: params.reasoningEffort,
196
+ claudePermissionMode: params.claudePermissionMode,
197
+ codexServiceTier: params.codexServiceTier,
198
+ systemPromptText: params.systemPromptText,
199
+ disabledToolKinds: params.disabledToolKinds,
200
+ assetAccessConfig: params.assetAccessConfig,
201
+ agentSurfaceMode: params.agentSurfaceMode,
202
+ workspaceRoot: params.workspaceRoot,
203
+ });
204
+ let host = this.hosts.get(params.runtimeKey);
205
+ if (host?.getState() === 'failed') {
206
+ log.warn('[executor] recreating failed host', {
207
+ runtimeKey: params.runtimeKey,
208
+ sessionKey: params.sessionKey,
209
+ previousRunId: host.getCurrentRunId(),
210
+ lastError: host.getLastError(),
211
+ });
212
+ host.close();
213
+ this.hosts.delete(params.runtimeKey);
214
+ this.hostBridgeSignatures.delete(params.runtimeKey);
215
+ this.hostAgentCommandEnvOverlays.delete(params.runtimeKey);
216
+ this.hostMetadata.delete(params.runtimeKey);
217
+ host = undefined;
218
+ }
219
+ const previousBridgeSignature = this.hostBridgeSignatures.get(params.runtimeKey);
220
+ if (host &&
221
+ previousBridgeSignature !== hostConfigSignature &&
222
+ host.getState() === 'idle' &&
223
+ !host.getCurrentRunId() &&
224
+ !host.hasPendingApproval()) {
225
+ log.info('[executor] recreating host to refresh config', {
226
+ runtimeKey: params.runtimeKey,
227
+ sessionKey: params.sessionKey,
228
+ });
229
+ host.close();
230
+ this.hosts.delete(params.runtimeKey);
231
+ this.hostBridgeSignatures.delete(params.runtimeKey);
232
+ this.hostAgentCommandEnvOverlays.delete(params.runtimeKey);
233
+ this.hostMetadata.delete(params.runtimeKey);
234
+ host = undefined;
235
+ }
236
+ if (!host) {
237
+ const hostInstanceId = randomUUID();
238
+ const assetCacheRoot = params.assetAccessConfig
239
+ ? buildHostAssetCacheRoot(params.assetAccessConfig.agentId, params.assetAccessConfig.conversationId, hostInstanceId)
240
+ : undefined;
241
+ const channelBridgeMcpEntry = params.agentSurfaceMode === 'mcp' && params.assetAccessConfig && assetCacheRoot
242
+ ? this.buildChannelBridgeMcpEntry({
243
+ ...params.assetAccessConfig,
244
+ assetCacheRoot,
245
+ workspaceRoot: params.workspaceRoot,
246
+ })
247
+ : undefined;
248
+ const agentCommandEnvOverlay = params.agentSurfaceMode === 'bigbang'
249
+ ? this.buildAgentCommandEnvOverlay({
250
+ assetAccessConfig: params.assetAccessConfig,
251
+ assetCacheRoot,
252
+ })
253
+ : undefined;
254
+ const hostEnv = this.mergeAgentCommandEnv(params.env, agentCommandEnvOverlay);
255
+ host = this.createHost({
256
+ hostKey: params.runtimeKey,
257
+ sessionKey: params.sessionKey,
258
+ bindingKey: params.bindingKey,
259
+ db: this.db,
260
+ config: { ...this.config, agentSurfaceMode: params.agentSurfaceMode },
261
+ toolAuth: this.toolAuth,
262
+ workspaceRoot: params.workspaceRoot,
263
+ agentType: params.agentType,
264
+ agentCommand: params.agentCommand,
265
+ agentArgs: params.agentArgs,
266
+ env: hostEnv,
267
+ model: params.model,
268
+ reasoningEffort: params.reasoningEffort,
269
+ claudePermissionMode: params.claudePermissionMode,
270
+ codexServiceTier: params.codexServiceTier,
271
+ disabledToolKinds: params.disabledToolKinds,
272
+ hostInstanceId,
273
+ channelBridgeMcpEntry,
274
+ assetAccessConfig: params.assetAccessConfig,
275
+ assetCacheRoot,
276
+ workspaceLockManager: this.workspaceLockManager,
277
+ send: this.send,
278
+ hooks: {
279
+ onRunStart: (dispatchMsg) => {
280
+ updateDispatchState(this.db, dispatchMsg.runId, 'running');
281
+ },
282
+ onAwaitingApproval: (dispatchMsg) => {
283
+ updateDispatchState(this.db, dispatchMsg.runId, 'awaiting_approval');
284
+ },
285
+ onRunFinish: (dispatchMsg) => {
286
+ removeDispatch(this.db, dispatchMsg.runId);
287
+ },
288
+ },
289
+ });
290
+ this.hosts.set(params.runtimeKey, host);
291
+ this.hostBridgeSignatures.set(params.runtimeKey, hostConfigSignature);
292
+ this.hostAgentCommandEnvOverlays.set(params.runtimeKey, agentCommandEnvOverlay);
293
+ log.info('[executor] created runtime host', {
294
+ runtimeKey: params.runtimeKey,
295
+ sessionKey: params.sessionKey,
296
+ agentSurfaceMode: params.agentSurfaceMode,
297
+ channelBridgeMcpInjected: Boolean(channelBridgeMcpEntry),
298
+ agentCommandEnvInjected: Boolean(agentCommandEnvOverlay),
299
+ });
300
+ }
301
+ this.hostMetadata.set(params.runtimeKey, {
302
+ sessionKey: params.sessionKey,
303
+ workspaceRoot: params.workspaceRoot,
304
+ agentType: params.agentType,
305
+ });
306
+ return host;
307
+ }
308
+ ensureLocalSession(params) {
309
+ const existingSession = getSession(this.db, params.sessionKey);
310
+ if (!existingSession) {
311
+ createSession(this.db, {
312
+ sessionKey: params.sessionKey,
313
+ agentCommand: params.agentCommand,
314
+ agentArgs: params.agentArgs,
315
+ cwd: params.cwd,
316
+ loadSupported: false,
317
+ });
318
+ return;
319
+ }
320
+ if (existingSession.agentCommand !== params.agentCommand ||
321
+ existingSession.agentArgsJson !== JSON.stringify(params.agentArgs)) {
322
+ this.db.prepare(`UPDATE sessions
323
+ SET agent_command = ?, agent_args_json = ?, acp_session_id = NULL, system_prompt_text = NULL, load_supported = 0, updated_at = ?
324
+ WHERE session_key = ?`).run(params.agentCommand, JSON.stringify(params.agentArgs), Date.now(), params.sessionKey);
325
+ }
326
+ }
327
+ buildClaudeControlHost(target) {
328
+ const driver = getRuntimeDriver('claude_sdk');
329
+ const runtimeKey = target.hostKey || target.sessionKey;
330
+ const bindingKey = `node:${target.conversationId}:-:node_user`;
331
+ const workspaceRoot = target.workspaceRoot || this.config.workspaceRoot;
332
+ const runtimeEnv = {
333
+ ...(driver.defaultEnv ?? {}),
334
+ ...(target.envVars ?? {}),
335
+ };
336
+ const agentArgs = [...driver.args];
337
+ appendClaudeStartupArgs(agentArgs, { env: runtimeEnv });
338
+ const claudeConfigDir = ensureIsolatedClaudeConfig(workspaceRoot);
339
+ runtimeEnv.CLAUDE_CONFIG_DIR = claudeConfigDir;
340
+ ensureNativeSkillMounts({
341
+ agentType: 'claude_sdk',
342
+ workspaceRoot,
343
+ });
344
+ this.ensureLocalSession({
345
+ sessionKey: target.sessionKey,
346
+ agentCommand: driver.command,
347
+ agentArgs,
348
+ cwd: workspaceRoot,
349
+ });
350
+ upsertBinding(this.db, { platform: 'node', chatId: target.conversationId, threadId: null, userId: 'node_user' }, target.sessionKey);
351
+ return this.getOrCreateHost({
352
+ runtimeKey,
353
+ sessionKey: target.sessionKey,
354
+ bindingKey,
355
+ workspaceRoot,
356
+ agentType: 'claude_sdk',
357
+ agentCommand: driver.command,
358
+ agentArgs,
359
+ env: runtimeEnv,
360
+ disabledToolKinds: target.disabledToolKinds,
361
+ agentSurfaceMode: this.config.agentSurfaceMode,
362
+ });
363
+ }
364
+ async dispatch(msg, options) {
365
+ const shouldPersist = options?.persist !== false;
366
+ const isDreamDispatch = msg.dispatchMode === 'dream';
367
+ const effectiveAgentType = isDreamDispatch ? 'claude_sdk' : msg.agentType;
368
+ const effectiveMsg = isDreamDispatch
369
+ ? {
370
+ ...msg,
371
+ agentType: 'claude_sdk',
372
+ disabledToolKinds: [
373
+ ...(msg.disabledToolKinds ?? []),
374
+ 'execute',
375
+ 'edit',
376
+ 'delete',
377
+ 'move',
378
+ ],
379
+ }
380
+ : msg;
381
+ const { runId, conversationId, sessionKey, prompt, hostKey } = effectiveMsg;
382
+ const agentSurfaceMode = effectiveMsg.agentSurfaceMode ?? this.config.agentSurfaceMode;
383
+ const bindingKey = `node:${conversationId}:-:node_user`;
384
+ const runtimeKey = hostKey || sessionKey;
385
+ const driver = getRuntimeDriver(effectiveAgentType);
386
+ const workspaceRoot = effectiveMsg.workspacePath ?? this.config.workspaceRoot;
387
+ const runtimeEnv = isDreamDispatch
388
+ ? mergeDreamRuntimeEnv({
389
+ ...(driver.defaultEnv ?? {}),
390
+ ...(effectiveMsg.envVars ?? {}),
391
+ })
392
+ : {
393
+ ...(driver.defaultEnv ?? {}),
394
+ ...(effectiveMsg.envVars ?? {}),
395
+ };
396
+ const agentArgs = [...driver.args];
397
+ if (msg.agentType === 'codex_acp' && msg.model?.trim()) {
398
+ agentArgs.push('-c', `model=${JSON.stringify(msg.model.trim())}`);
399
+ }
400
+ if (msg.agentType === 'codex_acp' && msg.reasoningEffort?.trim()) {
401
+ agentArgs.push('-c', `model_reasoning_effort=${JSON.stringify(msg.reasoningEffort.trim())}`);
402
+ }
403
+ if (msg.agentType === 'claude_acp') {
404
+ appendClaudeStartupArgs(agentArgs, { explicitModel: msg.model, env: runtimeEnv });
405
+ }
406
+ try {
407
+ if (msg.agentType === 'claude_acp' || msg.agentType === 'claude_sdk') {
408
+ const claudeConfigDir = ensureIsolatedClaudeConfig(workspaceRoot);
409
+ runtimeEnv.CLAUDE_CONFIG_DIR = claudeConfigDir;
410
+ ensureNativeSkillMounts({
411
+ agentType: msg.agentType,
412
+ workspaceRoot,
413
+ skillRoots: msg.skillRoots,
414
+ enabledSkillPaths: msg.enabledSkillPaths,
415
+ });
416
+ }
417
+ else {
418
+ ensureNativeSkillMounts({
419
+ agentType: msg.agentType,
420
+ workspaceRoot,
421
+ skillRoots: msg.skillRoots,
422
+ enabledSkillPaths: msg.enabledSkillPaths,
423
+ });
424
+ }
425
+ }
426
+ catch (error) {
427
+ const message = String(error?.message ?? error);
428
+ log.warn('[executor] failed to prepare native skill mounts', {
429
+ runId,
430
+ conversationId,
431
+ agentType: msg.agentType,
432
+ error: message,
433
+ });
434
+ this.send({
435
+ type: 'run.end',
436
+ runId,
437
+ conversationId,
438
+ error: `Failed to prepare native skill mounts: ${message}`,
439
+ });
440
+ throw error;
441
+ }
442
+ log.info('[executor] dispatch received', {
443
+ runId,
444
+ conversationId,
445
+ sessionKey,
446
+ runtimeKey,
447
+ dispatchMode: msg.dispatchMode,
448
+ agentType: msg.agentType,
449
+ agentSurfaceMode,
450
+ });
451
+ this.terminalRunIds.delete(runId);
452
+ if (shouldPersist) {
453
+ enqueueDispatch(this.db, effectiveMsg, 'queued');
454
+ }
455
+ // Ensure local session row exists (bindings has FK → sessions)
456
+ this.ensureLocalSession({
457
+ sessionKey,
458
+ agentCommand: driver.command,
459
+ agentArgs,
460
+ cwd: workspaceRoot,
461
+ });
462
+ // Ensure binding exists
463
+ upsertBinding(this.db, { platform: 'node', chatId: conversationId, threadId: null, userId: 'node_user' }, sessionKey);
464
+ const assetAccessConfig = effectiveMsg.channelBridgeConfig
465
+ ? {
466
+ agentId: effectiveMsg.channelBridgeConfig.agentId,
467
+ conversationId: effectiveMsg.channelBridgeConfig.conversationId,
468
+ serverUrl: effectiveMsg.channelBridgeConfig.serverUrl,
469
+ authToken: effectiveMsg.channelBridgeConfig.authToken,
470
+ }
471
+ : undefined;
472
+ const codexServiceTier = effectiveAgentType === 'codex_app_server' ? effectiveMsg.codexServiceTier : undefined;
473
+ let host;
474
+ try {
475
+ host = this.getOrCreateHost({
476
+ runtimeKey,
477
+ sessionKey,
478
+ bindingKey,
479
+ workspaceRoot,
480
+ agentName: effectiveMsg.agentName,
481
+ agentType: effectiveAgentType,
482
+ agentCommand: driver.command,
483
+ agentArgs,
484
+ env: runtimeEnv,
485
+ model: effectiveMsg.model,
486
+ reasoningEffort: effectiveMsg.reasoningEffort,
487
+ claudePermissionMode: effectiveMsg.claudePermissionMode,
488
+ codexServiceTier,
489
+ systemPromptText: effectiveMsg.systemPromptText,
490
+ disabledToolKinds: effectiveMsg.disabledToolKinds,
491
+ assetAccessConfig,
492
+ agentSurfaceMode,
493
+ });
494
+ }
495
+ catch (error) {
496
+ const message = String(error?.message ?? error);
497
+ log.warn('[executor] failed to prepare runtime host', {
498
+ runId,
499
+ conversationId,
500
+ runtimeKey,
501
+ agentSurfaceMode,
502
+ error: message,
503
+ });
504
+ removeDispatch(this.db, runId);
505
+ this.send({
506
+ type: 'run.end',
507
+ runId,
508
+ conversationId,
509
+ error: message,
510
+ });
511
+ throw error;
512
+ }
513
+ this.runToHost.set(runId, runtimeKey);
514
+ this.send({
515
+ type: 'run.accepted',
516
+ runId,
517
+ conversationId,
518
+ });
519
+ let dispatchSucceeded = false;
520
+ try {
521
+ const dispatchEnvVars = this.mergeAgentCommandEnv(runtimeEnv, this.hostAgentCommandEnvOverlays.get(runtimeKey));
522
+ await host.dispatch({
523
+ ...msg,
524
+ agentSurfaceMode,
525
+ envVars: dispatchEnvVars,
526
+ });
527
+ dispatchSucceeded = true;
528
+ }
529
+ catch (error) {
530
+ if (error instanceof HostDispatchRejectedError) {
531
+ log.warn('[executor] host rejected dispatch before run start', {
532
+ runId,
533
+ conversationId,
534
+ runtimeKey,
535
+ code: error.code,
536
+ error: error.message,
537
+ });
538
+ this.send({
539
+ type: 'run.end',
540
+ runId,
541
+ conversationId,
542
+ error: error.message,
543
+ });
544
+ }
545
+ throw error;
546
+ }
547
+ finally {
548
+ this.runToHost.delete(runId);
549
+ if (!dispatchSucceeded || host.getState() === 'failed') {
550
+ removeDispatch(this.db, runId);
551
+ }
552
+ }
553
+ }
554
+ resumePendingDispatches() {
555
+ const pending = listPendingDispatches(this.db);
556
+ if (pending.length === 0)
557
+ return;
558
+ log.warn('[executor] restoring pending dispatches after node restart', {
559
+ count: pending.length,
560
+ });
561
+ for (const entry of pending) {
562
+ if (entry.state === 'awaiting_approval') {
563
+ this.failApprovalRecovery(entry);
564
+ continue;
565
+ }
566
+ if (isDreamHostKey(entry.hostKey) && entry.state === 'running') {
567
+ this.failDreamDispatchRecovery(entry);
568
+ continue;
569
+ }
570
+ const existingHost = this.hosts.get(entry.hostKey);
571
+ if (existingHost?.getCurrentRunId() === entry.runId && existingHost.getState() !== 'failed') {
572
+ log.info('[executor] pending dispatch already attached to live host, skipping redispatch', {
573
+ runId: entry.runId,
574
+ hostKey: entry.hostKey,
575
+ state: entry.state,
576
+ });
577
+ this.send({
578
+ type: 'run.event',
579
+ runId: entry.runId,
580
+ conversationId: entry.payload.conversationId,
581
+ event: {
582
+ type: 'conversation.status',
583
+ conversationId: entry.payload.conversationId,
584
+ status: 'recovering',
585
+ },
586
+ });
587
+ continue;
588
+ }
589
+ const restoredMsg = !isDreamHostKey(entry.hostKey) && entry.state === 'running'
590
+ ? { ...entry.payload, dispatchMode: 'resume' }
591
+ : entry.payload;
592
+ this.send({
593
+ type: 'run.event',
594
+ runId: restoredMsg.runId,
595
+ conversationId: restoredMsg.conversationId,
596
+ event: {
597
+ type: 'conversation.status',
598
+ conversationId: restoredMsg.conversationId,
599
+ status: 'recovering',
600
+ },
601
+ });
602
+ void this.restoredDispatchContext
603
+ .run({ runId: restoredMsg.runId }, () => this.dispatch(restoredMsg, { persist: false }))
604
+ .catch((error) => {
605
+ const errorMessage = String(error?.message ?? error);
606
+ log.warn('[executor] failed to restore pending dispatch', {
607
+ runId: restoredMsg.runId,
608
+ hostKey: restoredMsg.hostKey,
609
+ conversationId: restoredMsg.conversationId,
610
+ state: entry.state,
611
+ error: errorMessage,
612
+ });
613
+ removeDispatch(this.db, restoredMsg.runId);
614
+ this.runToHost.delete(restoredMsg.runId);
615
+ this.sendRestoreFailureEndIfNeeded(restoredMsg.runId, restoredMsg.conversationId, errorMessage);
616
+ });
617
+ }
618
+ }
619
+ failRestoredDispatch(runId, error) {
620
+ const pending = listPendingDispatches(this.db).find((entry) => entry.runId === runId);
621
+ if (!pending)
622
+ return false;
623
+ const errorMessage = String(error?.message ?? error);
624
+ log.warn('[executor] failing restored dispatch', {
625
+ runId: pending.runId,
626
+ hostKey: pending.hostKey,
627
+ conversationId: pending.payload.conversationId,
628
+ state: pending.state,
629
+ error: errorMessage,
630
+ });
631
+ const host = this.hosts.get(pending.hostKey);
632
+ if (host) {
633
+ const currentRunId = host.getCurrentRunId();
634
+ if (currentRunId) {
635
+ this.runToHost.delete(currentRunId);
636
+ }
637
+ host.close();
638
+ this.hosts.delete(pending.hostKey);
639
+ this.hostBridgeSignatures.delete(pending.hostKey);
640
+ this.hostAgentCommandEnvOverlays.delete(pending.hostKey);
641
+ this.hostMetadata.delete(pending.hostKey);
642
+ }
643
+ removeDispatch(this.db, pending.runId);
644
+ this.runToHost.delete(pending.runId);
645
+ this.sendRestoreFailureEndIfNeeded(pending.runId, pending.payload.conversationId, errorMessage);
646
+ return true;
647
+ }
648
+ failCurrentRestoredDispatch(error) {
649
+ const runId = this.restoredDispatchContext.getStore()?.runId;
650
+ if (!runId)
651
+ return false;
652
+ return this.failRestoredDispatch(runId, error);
653
+ }
654
+ sendRestoreFailureEndIfNeeded(runId, conversationId, errorMessage) {
655
+ if (this.terminalRunIds.has(runId))
656
+ return;
657
+ this.send({
658
+ type: 'run.end',
659
+ runId,
660
+ conversationId,
661
+ error: `Failed to restore pending dispatch after node restart: ${errorMessage}`,
662
+ });
663
+ }
664
+ failDreamDispatchRecovery(entry) {
665
+ const errorMessage = 'Dream dispatch cannot be resumed after node restart.';
666
+ log.warn('[executor] dream dispatch cannot be restored after node restart', {
667
+ runId: entry.runId,
668
+ hostKey: entry.hostKey,
669
+ state: entry.state,
670
+ });
671
+ const host = this.hosts.get(entry.hostKey);
672
+ if (host?.getCurrentRunId() === entry.runId) {
673
+ this.runToHost.delete(entry.runId);
674
+ host.close();
675
+ this.hosts.delete(entry.hostKey);
676
+ this.hostBridgeSignatures.delete(entry.hostKey);
677
+ this.hostAgentCommandEnvOverlays.delete(entry.hostKey);
678
+ this.hostMetadata.delete(entry.hostKey);
679
+ }
680
+ else {
681
+ this.runToHost.delete(entry.runId);
682
+ }
683
+ finishRun(this.db, { runId: entry.runId, error: errorMessage });
684
+ removeDispatch(this.db, entry.runId);
685
+ this.send({
686
+ type: 'run.end',
687
+ runId: entry.runId,
688
+ conversationId: entry.payload.conversationId,
689
+ error: errorMessage,
690
+ });
691
+ }
692
+ failApprovalRecovery(entry) {
693
+ log.warn('[executor] approval request cannot be restored after reconnect/restart', {
694
+ runId: entry.runId,
695
+ hostKey: entry.hostKey,
696
+ });
697
+ const host = this.hosts.get(entry.hostKey);
698
+ if (host) {
699
+ const currentRunId = host.getCurrentRunId();
700
+ if (currentRunId) {
701
+ this.runToHost.delete(currentRunId);
702
+ }
703
+ host.close();
704
+ this.hosts.delete(entry.hostKey);
705
+ this.hostBridgeSignatures.delete(entry.hostKey);
706
+ this.hostAgentCommandEnvOverlays.delete(entry.hostKey);
707
+ this.hostMetadata.delete(entry.hostKey);
708
+ }
709
+ this.runToHost.delete(entry.runId);
710
+ removeDispatch(this.db, entry.runId);
711
+ this.send({
712
+ type: 'run.end',
713
+ runId: entry.runId,
714
+ conversationId: entry.payload.conversationId,
715
+ error: 'Approval request lost during reconnect. Re-run required.',
716
+ });
717
+ }
718
+ reapIdleHosts() {
719
+ const now = Date.now();
720
+ for (const [hostKey, host] of this.hosts.entries()) {
721
+ if (!host.isIdleExpired(now, this.config.hostIdleTimeoutMs))
722
+ continue;
723
+ log.info('[executor] reaping idle host', {
724
+ hostKey,
725
+ lastSleepAt: host.getLastSleepAt(),
726
+ });
727
+ host.close();
728
+ this.hosts.delete(hostKey);
729
+ this.hostBridgeSignatures.delete(hostKey);
730
+ this.hostAgentCommandEnvOverlays.delete(hostKey);
731
+ this.hostMetadata.delete(hostKey);
732
+ }
733
+ }
734
+ reapIdleSoloHosts() {
735
+ for (const [soloSessionId, host] of this.soloHosts.entries()) {
736
+ if (!host.isIdleExpired())
737
+ continue;
738
+ if (this.closingSoloHosts.has(soloSessionId))
739
+ continue;
740
+ log.info('[executor] reaping idle solo host', { soloSessionId });
741
+ this.closingSoloHosts.add(soloSessionId);
742
+ void host.close()
743
+ .then(() => {
744
+ if (this.soloHosts.get(soloSessionId) === host) {
745
+ this.soloHosts.delete(soloSessionId);
746
+ }
747
+ })
748
+ .catch((error) => {
749
+ log.warn('[executor] failed to close idle solo host', {
750
+ soloSessionId,
751
+ error: String(error?.message ?? error),
752
+ });
753
+ })
754
+ .finally(() => {
755
+ this.closingSoloHosts.delete(soloSessionId);
756
+ });
757
+ }
758
+ }
759
+ async handleSoloDispatch(msg) {
760
+ if (msg.agentType !== 'codex_app_server') {
761
+ this.send({
762
+ type: 'solo.run.end',
763
+ soloSessionId: msg.soloSessionId,
764
+ stopReason: 'failed',
765
+ error: `Solo mode is not supported for agent type ${msg.agentType}.`,
766
+ });
767
+ return;
768
+ }
769
+ let host = this.soloHosts.get(msg.soloSessionId);
770
+ if (host && this.closingSoloHosts.has(msg.soloSessionId)) {
771
+ this.send({
772
+ type: 'solo.run.end',
773
+ soloSessionId: msg.soloSessionId,
774
+ stopReason: 'failed',
775
+ error: 'Solo session is closing. Try again in a moment.',
776
+ });
777
+ return;
778
+ }
779
+ if (host?.isClosed()) {
780
+ this.soloHosts.delete(msg.soloSessionId);
781
+ host = undefined;
782
+ }
783
+ if (!host) {
784
+ host = this.createSoloHost({
785
+ msg,
786
+ db: this.db,
787
+ config: this.config,
788
+ toolAuth: this.toolAuth,
789
+ workspaceLockManager: this.workspaceLockManager,
790
+ send: this.send,
791
+ });
792
+ this.soloHosts.set(msg.soloSessionId, host);
793
+ }
794
+ else {
795
+ host.touch();
796
+ }
797
+ if (host.isRunning()) {
798
+ this.send({
799
+ type: 'solo.run.end',
800
+ soloSessionId: msg.soloSessionId,
801
+ stopReason: 'failed',
802
+ error: 'Solo run already active for this session.',
803
+ });
804
+ return;
805
+ }
806
+ try {
807
+ await host.prompt(msg.prompt, msg.systemPromptText);
808
+ }
809
+ catch (error) {
810
+ const message = String(error?.message ?? error);
811
+ this.send({
812
+ type: 'solo.run.end',
813
+ soloSessionId: msg.soloSessionId,
814
+ stopReason: 'failed',
815
+ error: message,
816
+ });
817
+ }
818
+ }
819
+ async handleSoloCancel(msg) {
820
+ const host = this.soloHosts.get(msg.soloSessionId);
821
+ if (!host)
822
+ return;
823
+ host.touch();
824
+ await host.cancel();
825
+ }
826
+ async handleSoloClose(msg) {
827
+ const host = this.soloHosts.get(msg.soloSessionId);
828
+ if (!host)
829
+ return;
830
+ if (this.closingSoloHosts.has(msg.soloSessionId))
831
+ return;
832
+ this.closingSoloHosts.add(msg.soloSessionId);
833
+ try {
834
+ await host.close();
835
+ if (this.soloHosts.get(msg.soloSessionId) === host) {
836
+ this.soloHosts.delete(msg.soloSessionId);
837
+ }
838
+ }
839
+ finally {
840
+ this.closingSoloHosts.delete(msg.soloSessionId);
841
+ }
842
+ }
843
+ async handleSoloSteer(msg) {
844
+ const host = this.soloHosts.get(msg.soloSessionId);
845
+ if (!host)
846
+ return false;
847
+ return host.steer(msg.prompt);
848
+ }
849
+ async handleSoloPermissionResponse(msg) {
850
+ const host = this.soloHosts.get(msg.soloSessionId);
851
+ if (!host)
852
+ return false;
853
+ return host.respondToPermission(msg.requestId, msg.decision, msg.selectedActionId, msg.responseText, msg.answers);
854
+ }
855
+ async cancelRun(runId) {
856
+ const runtimeKey = this.runToHost.get(runId);
857
+ if (!runtimeKey)
858
+ return false;
859
+ const host = this.hosts.get(runtimeKey);
860
+ if (!host)
861
+ return false;
862
+ return host.cancelRun(runId);
863
+ }
864
+ async steerRun(runId, promptText, attachments) {
865
+ return this.withSteerSerial(runId, async () => {
866
+ const runtimeKey = this.runToHost.get(runId);
867
+ if (!runtimeKey)
868
+ return false;
869
+ const host = this.hosts.get(runtimeKey);
870
+ if (!host)
871
+ return false;
872
+ if (!host.steerRun)
873
+ return false;
874
+ return host.steerRun(runId, promptText, attachments);
875
+ });
876
+ }
877
+ isRunActiveInWorkspace(runId, workspaceRoot) {
878
+ const normalizedRunId = runId?.trim();
879
+ if (!normalizedRunId)
880
+ return false;
881
+ const runtimeKey = this.runToHost.get(normalizedRunId);
882
+ if (!runtimeKey)
883
+ return false;
884
+ const host = this.hosts.get(runtimeKey);
885
+ if (!host || host.getCurrentRunId() !== normalizedRunId)
886
+ return false;
887
+ return path.resolve(host.getWorkspaceRoot()) === path.resolve(workspaceRoot);
888
+ }
889
+ async handlePermissionResponse(requestId, decision, selectedActionId, responseText, answers) {
890
+ for (const host of this.hosts.values()) {
891
+ const handled = await host.handlePermissionResponse(requestId, decision, selectedActionId, responseText, answers);
892
+ if (handled)
893
+ return true;
894
+ }
895
+ return false;
896
+ }
897
+ async getClaudeControls(target) {
898
+ const host = this.buildClaudeControlHost(target);
899
+ return host.getClaudeControls();
900
+ }
901
+ async setClaudeMode(target, modeId) {
902
+ const host = this.buildClaudeControlHost(target);
903
+ if (host.getCurrentRunId() || host.hasPendingApproval()) {
904
+ throw new Error('Claude session is busy.');
905
+ }
906
+ return host.setClaudeMode(modeId);
907
+ }
908
+ async setClaudeModel(target, modelId) {
909
+ const host = this.buildClaudeControlHost(target);
910
+ if (host.getCurrentRunId() || host.hasPendingApproval()) {
911
+ throw new Error('Claude session is busy.');
912
+ }
913
+ return host.setClaudeModel(modelId);
914
+ }
915
+ async executeClaudeCommand(target, commandName, args) {
916
+ const host = this.buildClaudeControlHost(target);
917
+ if (host.getCurrentRunId() || host.hasPendingApproval()) {
918
+ throw new Error('Claude session is busy.');
919
+ }
920
+ const result = await host.executeClaudeCommand(commandName, args);
921
+ const controls = await host.getClaudeControls();
922
+ return { controls, result };
923
+ }
924
+ listHostSnapshots() {
925
+ const pendingByHost = new Map();
926
+ for (const pending of listPendingDispatches(this.db)) {
927
+ pendingByHost.set(pending.hostKey, (pendingByHost.get(pending.hostKey) ?? 0) + 1);
928
+ }
929
+ const snapshots = [];
930
+ for (const [hostKey, host] of this.hosts.entries()) {
931
+ const meta = this.hostMetadata.get(hostKey);
932
+ if (!meta)
933
+ continue;
934
+ const state = host.getState();
935
+ const currentRunId = host.getCurrentRunId();
936
+ const hasPendingApproval = host.hasPendingApproval();
937
+ const pendingDispatchCount = pendingByHost.get(hostKey) ?? 0;
938
+ const hostWithMetrics = host;
939
+ const inboxSize = typeof hostWithMetrics.getInboxSize === 'function' ? hostWithMetrics.getInboxSize() : 0;
940
+ const lastWakeAt = typeof hostWithMetrics.getLastWakeAt === 'function' ? hostWithMetrics.getLastWakeAt() : null;
941
+ const lastSleepAt = host.getLastSleepAt();
942
+ const lastError = host.getLastError();
943
+ snapshots.push({
944
+ hostKey,
945
+ hostInstanceId: host.getHostInstanceId(),
946
+ sessionKey: meta.sessionKey,
947
+ workspaceRoot: meta.workspaceRoot,
948
+ agentType: meta.agentType,
949
+ state,
950
+ currentRunId,
951
+ hasPendingApproval,
952
+ inboxSize,
953
+ pendingDispatchCount,
954
+ lastWakeAt,
955
+ lastSleepAt,
956
+ lastError,
957
+ resumable: state !== 'failed' && (currentRunId !== null || hasPendingApproval || pendingDispatchCount > 0 || state === 'idle'),
958
+ });
959
+ }
960
+ return snapshots.sort((a, b) => a.hostKey.localeCompare(b.hostKey));
961
+ }
962
+ closeHost(hostKey) {
963
+ const host = this.hosts.get(hostKey);
964
+ if (!host)
965
+ return;
966
+ const currentRunId = host.getCurrentRunId();
967
+ if (currentRunId) {
968
+ this.runToHost.delete(currentRunId);
969
+ }
970
+ removeDispatchesForHostKey(this.db, hostKey);
971
+ host.close();
972
+ this.hosts.delete(hostKey);
973
+ this.hostBridgeSignatures.delete(hostKey);
974
+ this.hostAgentCommandEnvOverlays.delete(hostKey);
975
+ this.hostMetadata.delete(hostKey);
976
+ }
977
+ async cleanupAgentRuntime(hostKeys, sessionKeys = []) {
978
+ let closedHosts = 0;
979
+ let removedDispatches = 0;
980
+ const sessionKeysToClear = new Set(sessionKeys.filter(Boolean));
981
+ for (const hostKey of [...new Set(hostKeys.filter(Boolean))]) {
982
+ const meta = this.hostMetadata.get(hostKey);
983
+ if (meta?.sessionKey)
984
+ sessionKeysToClear.add(meta.sessionKey);
985
+ const host = this.hosts.get(hostKey);
986
+ if (host) {
987
+ const currentRunId = host.getCurrentRunId();
988
+ if (currentRunId) {
989
+ this.runToHost.delete(currentRunId);
990
+ }
991
+ if (host.closeAndWait) {
992
+ await host.closeAndWait();
993
+ }
994
+ else {
995
+ await Promise.resolve(host.close());
996
+ }
997
+ this.hosts.delete(hostKey);
998
+ this.hostBridgeSignatures.delete(hostKey);
999
+ this.hostAgentCommandEnvOverlays.delete(hostKey);
1000
+ this.hostMetadata.delete(hostKey);
1001
+ closedHosts += 1;
1002
+ }
1003
+ for (const [runId, mappedHostKey] of this.runToHost.entries()) {
1004
+ if (mappedHostKey === hostKey) {
1005
+ this.runToHost.delete(runId);
1006
+ }
1007
+ }
1008
+ removedDispatches += removeDispatchesForHostKey(this.db, hostKey);
1009
+ }
1010
+ for (const sessionKey of sessionKeysToClear) {
1011
+ for (const pending of listPendingDispatches(this.db)) {
1012
+ if (pending.payload.sessionKey === sessionKey) {
1013
+ this.runToHost.delete(pending.runId);
1014
+ }
1015
+ }
1016
+ removedDispatches += removeDispatchesForSessionKey(this.db, sessionKey);
1017
+ clearAcpSessionId(this.db, sessionKey);
1018
+ }
1019
+ return { closedHosts, removedDispatches };
1020
+ }
1021
+ resetWorkspace(workspaceRoot) {
1022
+ const resolvedRoot = path.resolve(workspaceRoot);
1023
+ for (const [hostKey, host] of this.hosts.entries()) {
1024
+ if (path.resolve(host.getWorkspaceRoot()) !== resolvedRoot)
1025
+ continue;
1026
+ const currentRunId = host.getCurrentRunId();
1027
+ if (currentRunId) {
1028
+ this.runToHost.delete(currentRunId);
1029
+ removeDispatch(this.db, currentRunId);
1030
+ }
1031
+ host.close();
1032
+ this.hosts.delete(hostKey);
1033
+ this.hostBridgeSignatures.delete(hostKey);
1034
+ this.hostAgentCommandEnvOverlays.delete(hostKey);
1035
+ this.hostMetadata.delete(hostKey);
1036
+ }
1037
+ for (const pending of listPendingDispatches(this.db)) {
1038
+ const pendingRoot = path.resolve(pending.payload.workspacePath ?? this.config.workspaceRoot);
1039
+ if (pendingRoot !== resolvedRoot)
1040
+ continue;
1041
+ this.runToHost.delete(pending.runId);
1042
+ removeDispatch(this.db, pending.runId);
1043
+ }
1044
+ }
1045
+ close() {
1046
+ clearInterval(this.hostSweepTimer);
1047
+ this.closeSoloHosts();
1048
+ for (const host of this.hosts.values()) {
1049
+ host.close();
1050
+ }
1051
+ this.hosts.clear();
1052
+ this.hostBridgeSignatures.clear();
1053
+ this.hostAgentCommandEnvOverlays.clear();
1054
+ this.hostMetadata.clear();
1055
+ this.steerChains.clear();
1056
+ }
1057
+ closeSoloHosts() {
1058
+ for (const host of this.soloHosts.values()) {
1059
+ void host.close();
1060
+ }
1061
+ this.soloHosts.clear();
1062
+ this.closingSoloHosts.clear();
1063
+ }
1064
+ async withSteerSerial(runId, task) {
1065
+ const previous = this.steerChains.get(runId) ?? Promise.resolve();
1066
+ const current = previous.catch(() => undefined).then(task);
1067
+ const tracked = current.catch(() => undefined);
1068
+ this.steerChains.set(runId, tracked);
1069
+ try {
1070
+ return await current;
1071
+ }
1072
+ finally {
1073
+ if (this.steerChains.get(runId) === tracked) {
1074
+ this.steerChains.delete(runId);
1075
+ }
1076
+ }
1077
+ }
1078
+ }