@myagentroam/node 0.10.61 → 0.10.63

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/dist/connector.js CHANGED
@@ -3,8 +3,8 @@ import { isImageAttachment } from './util/node-operation-parsers.js';
3
3
  import { ModelUsageOutbox } from './service/model-usage-outbox.js';
4
4
  import { NodeModelUsageService } from './service/model-usage-service.js';
5
5
  import { dirname, resolve } from 'node:path';
6
- import { createEnvelope, composerVisibleText, directNodeCommandSchema } from '@myagentroam/protocol';
7
- import { hostedConversationStorePath, loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
6
+ import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
7
+ import { loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
8
8
  import { NodeMetrics, nodeLog } from './operational.js';
9
9
  import { assertLocalSqlitePath } from './storage.js';
10
10
  import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
@@ -12,9 +12,9 @@ import { NodeDatabase } from './database.js';
12
12
  import { TerminalManager } from './terminal.js';
13
13
  import { NodeRuntimeState } from './runtime-state.js';
14
14
  import { CommandStateStore } from './runner-command-engine.js';
15
- import { goalUnrestrictedAccess, GoalRuntime } from './service/goal-runtime.js';
15
+ import { GoalRuntime } from './service/goal-runtime.js';
16
+ import { GoalContinuationCoordinator } from './service/goal-continuation-coordinator.js';
16
17
  import { GoalMcpBridge } from './service/goal-mcp-bridge.js';
17
- import { NODE_MCP } from './mcp/naming.js';
18
18
  import { RunnerUsageReader } from './runner-usage.js';
19
19
  import { ClaudeCodeRunner } from './runner/claude-code-runner.js';
20
20
  import { ClaudeManagedRunController } from './runner/claude/managed-run-controller.js';
@@ -61,13 +61,12 @@ import { WorkspaceQueueWorkbenchService } from './service/workspace-queue-workbe
61
61
  import { ContinuousRunCoordinator } from './service/continuous-run-coordinator.js';
62
62
  import { SessionIdentityService } from './service/session-identity-service.js';
63
63
  import { NativeSessionProjectionService } from './service/native-session-projection-service.js';
64
- import { composeConversationRuntimePage, ConversationHistoryService, convergeConversationWatchPage, hasManagedActiveRun } from './service/conversation-history-service.js';
64
+ import { ConversationHistoryService, hasManagedActiveRun } from './service/conversation-history-service.js';
65
65
  import { ConversationSegmentService } from './service/conversation-segment-service.js';
66
66
  import { RunnerInteractionService } from './service/runner-interaction-service.js';
67
67
  import { SessionLifecycleService } from './service/session-lifecycle-service.js';
68
68
  import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
69
69
  import { SessionMessageService } from './service/session-message-service.js';
70
- import { composerResourceInputTags } from './service/composer-message-input.js';
71
70
  import { SessionScheduleService } from './service/session-schedule-service.js';
72
71
  import { ScheduledSessionCoordinator } from './service/scheduled-session-coordinator.js';
73
72
  import { ScheduledSessionStore } from './service/scheduled-session-store.js';
@@ -94,14 +93,8 @@ import { RunnerChannelMessageService } from './service/runner-channel-message-se
94
93
  import { ManagedRunnerSessionService } from './service/managed-runner-session-service.js';
95
94
  import { NodeConnectionLifecycleService } from './service/node-connection-lifecycle-service.js';
96
95
  import { NodeUpgradeCoordinator, nodeUpgradeRequestPath } from './service/node-upgrade-coordinator.js';
97
- import { HostedConversationStore } from './service/hosted-conversation-store.js';
98
- import { ConversationTemplateNodeService } from './service/conversation-template-node-service.js';
99
- import { HostedConversationNodeService } from './service/hosted-conversation-node-service.js';
100
- import { BubblewrapSandboxExecutor } from '@myagentroam/agent/sandbox/linux';
101
- import { hostedConversationSandboxProfile } from './service/hosted-conversation-sandbox-profile.js';
102
- import { MarAgentHostedConversationRuntime } from './runner/mar-agent/hosted-conversation-runtime.js';
103
- import { conversationHostingCapability, probeBubblewrap } from './service/conversation-hosting-capability-service.js';
104
- import { ConversationHostingMaintenanceService } from './service/conversation-hosting-maintenance-service.js';
96
+ import { probeBubblewrap } from './service/conversation-hosting-capability-service.js';
97
+ import { ConversationHostingLifecycleService } from './service/conversation-hosting-lifecycle-service.js';
105
98
  import { ConversationHostingEnablementService } from './service/conversation-hosting-enablement-service.js';
106
99
  import { ComputerUseEnablementService } from './service/computer-use-enablement-service.js';
107
100
  import { safeErrorCode } from './util/safe-error.js';
@@ -205,14 +198,7 @@ export class NodeConnector {
205
198
  capabilities;
206
199
  capabilityDetector;
207
200
  conversationHostingProbe;
208
- conversationHostingStorageReady = false;
209
- conversationHostingFailureCode;
210
- conversationTemplateOperations;
211
- hostedConversationOperations;
212
- conversationHostingMaintenanceOperations;
213
- hostedConversationRuntime;
214
- hostedConversationService;
215
- conversationHostingMaintenanceService;
201
+ conversationHosting;
216
202
  configPath;
217
203
  reconnect;
218
204
  claudeApprovalTimeoutMs;
@@ -283,25 +269,8 @@ export class NodeConnector {
283
269
  workspaceService;
284
270
  commandStates = new CommandStateStore();
285
271
  goalRuntime = new GoalRuntime(() => undefined);
286
- goalRetryTimers = new Map();
287
- goalContinuationPending = new Map();
288
- goalSubmissionAttempts = new Map();
289
- goalContinuationEpochs = new Map();
290
- goalExecutionProgress = new Set();
291
- nativeGoalBindings = new Map();
292
272
  sessionScheduleService;
293
- goalMcpBridge = new GoalMcpBridge(this.goalRuntime, (sessionId) => {
294
- const goal = this.goalRuntime.get(sessionId);
295
- const session = this.runtime.getAgentSession(sessionId);
296
- if (goal === undefined || session === undefined)
297
- return;
298
- if (goal.status !== 'active') {
299
- this.nativeGoalBindings.delete(goal.id);
300
- this.retireGoalContinuation(sessionId);
301
- this.continuousRunCoordinator.removeGoalQueue(sessionId);
302
- }
303
- this.publishGoalState(session, goal);
304
- });
273
+ goalMcpBridge = new GoalMcpBridge(this.goalRuntime, (sessionId) => this.goalCoordinator.changed(sessionId));
305
274
  scheduleMcpBridge = new ScheduleMcpBridge(() => {
306
275
  if (this.database === undefined)
307
276
  throw new Error('NODE_DATABASE_UNAVAILABLE');
@@ -313,7 +282,7 @@ export class NodeConnector {
313
282
  session: this.sessionPresentationService.present(session)
314
283
  });
315
284
  this.scheduledSessions.changed();
316
- }, (sessionId, proposalId) => this.emitScheduledProposalNotification(sessionId, proposalId), (input) => {
285
+ }, (sessionId, proposalId) => this.scheduledSessions.proposalCreated(sessionId, proposalId), (input) => {
317
286
  if (this.sessionScheduleService === undefined)
318
287
  throw new Error('SCHEDULE_SERVICE_UNAVAILABLE');
319
288
  return this.sessionScheduleService.createFromMcp(input);
@@ -355,6 +324,17 @@ export class NodeConnector {
355
324
  recentRunProjectionService = new RecentRunProjectionService((message) => this.send('recent-runs', message));
356
325
  /** Runtime/session state is intentionally not persisted to Node SQLite. */
357
326
  runtime = new NodeRuntimeState(() => this.config?.nodeId ?? 'runtime-node', (workspaceId) => this.database?.getWorkspace(workspaceId), Date.now, (run, session, userMessage) => this.recentRunProjectionService.record(run, session, userMessage));
327
+ goalCoordinator = new GoalContinuationCoordinator({
328
+ goals: this.goalRuntime,
329
+ runtime: this.runtime,
330
+ commands: this.commandStates,
331
+ queue: () => this.continuousRunCoordinator,
332
+ submit: (input) => this.sessionMessageService.submitGoal(input),
333
+ emitRun: (...args) => this.runEventBridge.emitRun(...args),
334
+ present: (session) => this.sessionPresentationService.present(session),
335
+ emitSession: (session) => this.emitSessionWorkbenchEvent({ session: this.sessionPresentationService.present(session) }),
336
+ database: () => this.database
337
+ });
358
338
  metrics = new NodeMetrics();
359
339
  runEventService = new RunEventService({
360
340
  runtime: this.runtime,
@@ -375,21 +355,8 @@ export class NodeConnector {
375
355
  scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
376
356
  refreshWorkspaceSessions: (workspaceId) => this.workspaceWorkbenchService.ensureTopicSnapshot(workspaceId, 'sessions'),
377
357
  onTerminalRun: (run, status) => {
378
- if (run.source !== 'SCHEDULED' || this.database === undefined)
379
- return;
380
- const store = new ScheduledSessionStore(this.database.raw);
381
- store.finish(run.clientMessageId, status === 'SUCCEEDED' ? 'SUCCEEDED' : 'FAILED', status === 'SUCCEEDED' ? undefined : `SCHEDULE_RUN_${status}`, run.id);
382
- const task = store.getById(run.clientMessageId.split(':', 1)[0]);
383
- if (task !== undefined) {
384
- if (task.notifyOnTerminal)
385
- this.publishScheduledTaskNotification(task, run.clientMessageId, {
386
- kind: 'SCHEDULE_TERMINAL',
387
- outcome: status,
388
- runId: run.id
389
- });
390
- else if (status !== 'SUCCEEDED')
391
- this.emitScheduledNotification(task, run.clientMessageId, `SCHEDULE_RUN_${status}`, 'FAILED');
392
- }
358
+ if (this.database !== undefined)
359
+ this.scheduledSessions.executionTerminated(run, status);
393
360
  },
394
361
  dispatchQueue: async (sessionId, delayMs = 0) => {
395
362
  if (this.upgradeCoordinator.pending())
@@ -401,77 +368,14 @@ export class NodeConnector {
401
368
  await this.continuousRunCoordinator.dispatch(sessionId);
402
369
  },
403
370
  executionStarted: (executionId) => this.continuousRunCoordinator.executionStarted(executionId),
404
- keepRunActive: (sessionId) => (this.goalRetryTimers.has(sessionId) || this.goalContinuationPending.has(sessionId)) &&
405
- this.goalRuntime.get(sessionId)?.status === 'active',
371
+ keepRunActive: (sessionId) => this.goalCoordinator.keepRunActive(sessionId),
406
372
  executionTerminating: (executionId, status, permissionStop) => {
407
373
  this.gitOperationService.executionFinished(executionId);
408
- if (this.stopped)
409
- return;
410
- const pending = this.continuousRunCoordinator.pending(executionId);
411
- if (pending?.goal === undefined)
412
- return;
413
- const started = this.continuousRunCoordinator.hasExecutionStarted(executionId);
414
- const progressed = this.goalExecutionProgress.delete(executionId);
415
- if (!this.goalGenerationCurrent(pending.sessionId, pending.goal))
416
- return;
417
- if (permissionStop !== undefined) {
418
- this.retireGoalContinuation(pending.sessionId);
419
- this.continuousRunCoordinator.removeGoalQueue(pending.sessionId);
420
- return;
421
- }
422
- const disposition = this.goalRuntime.recordExecutionResult(pending.sessionId, {
423
- failed: status === 'FAILED',
424
- progressed: status !== 'SUCCEEDED' || progressed
425
- });
426
- if (disposition === 'blocked' || disposition === 'completed') {
427
- this.nativeGoalBindings.delete(pending.goal.id);
428
- this.retireGoalContinuation(pending.sessionId);
429
- this.continuousRunCoordinator.removeGoalQueue(pending.sessionId);
430
- const terminalGoal = this.goalRuntime.get(pending.sessionId);
431
- const session = this.runtime.getAgentSession(pending.sessionId);
432
- if (terminalGoal !== undefined && session !== undefined)
433
- this.publishGoalState(session, terminalGoal);
434
- return;
435
- }
436
- if (status !== 'FAILED' || started)
437
- return;
438
- this.clearGoalRetry(pending.sessionId);
439
- const continuationEpoch = this.goalContinuationEpochs.get(pending.sessionId) ?? 0;
440
- const retry = {
441
- sessionId: pending.sessionId,
442
- timer: setTimeout(() => {
443
- this.goalRetryTimers.delete(retry.sessionId);
444
- void this.enqueueGoalContinuation({
445
- sessionId: retry.sessionId,
446
- goal: pending.goal,
447
- ...(pending.usageGrant === undefined ? {} : { usageGrant: pending.usageGrant }),
448
- secretEnvironment: pending.secretEnvironment,
449
- ...(pending.personalInstructions === undefined
450
- ? {}
451
- : { personalInstructions: pending.personalInstructions }),
452
- ...(pending.marAgentModel === undefined
453
- ? {}
454
- : { marAgentModel: pending.marAgentModel }),
455
- ...(pending.marAgentModels === undefined
456
- ? {}
457
- : { marAgentModels: pending.marAgentModels }),
458
- ...(pending.marAgentCompact === undefined
459
- ? {}
460
- : { marAgentCompact: pending.marAgentCompact }),
461
- ...(pending.marAgentRuntimeScopeId === undefined
462
- ? {}
463
- : { marAgentRuntimeScopeId: pending.marAgentRuntimeScopeId }),
464
- continuationEpoch,
465
- deliveryIntent: 'SEND'
466
- });
467
- }, 15_000)
468
- };
469
- this.goalRetryTimers.set(pending.sessionId, retry);
470
- retry.timer.unref();
374
+ this.goalCoordinator.executionTerminating(executionId, status, permissionStop);
471
375
  },
472
376
  releaseExecution: (executionId) => {
473
377
  this.modelUsage.release(executionId);
474
- this.goalExecutionProgress.delete(executionId);
378
+ this.goalCoordinator.releaseExecution(executionId);
475
379
  this.revokeExecutionMcps(executionId);
476
380
  this.continuousRunCoordinator.remove(executionId);
477
381
  }
@@ -491,8 +395,6 @@ export class NodeConnector {
491
395
  present: (session) => this.sessionPresentationService.present(session),
492
396
  readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
493
397
  readHistoryPage: (session, limit) => this.conversationHistoryService.readHistoryPage(session, { limit }),
494
- convergePage: convergeConversationWatchPage,
495
- composeRuntimePage: composeConversationRuntimePage,
496
398
  projectInitialPage: (session, page, _unit, limit) => {
497
399
  const context = this.sessionContextService.snapshot(session.id);
498
400
  this.sessionContextService.schedule(session);
@@ -526,6 +428,35 @@ export class NodeConnector {
526
428
  this.capabilitiesProvided = options.capabilities !== undefined;
527
429
  this.capabilities = options.capabilities ?? unavailableCapabilities();
528
430
  this.conversationHostingProbe = options.conversationHostingProbe ?? probeBubblewrap;
431
+ this.conversationHosting = new ConversationHostingLifecycleService({
432
+ config: () => this.config,
433
+ configPath: this.configPath,
434
+ database: () => this.database,
435
+ capabilities: () => this.capabilities,
436
+ probe: this.conversationHostingProbe,
437
+ runtime: {
438
+ webSearch: (input, signal) => this.webToolService.webSearch(input, signal),
439
+ webFetch: (input, signal) => this.webToolService.webFetch(input, signal, false),
440
+ modelFactory: this.marAgentRuntimeModels,
441
+ registerNativeImages: (runId, roots, images) => this.runAttachmentService.registerNativeImages(runId, roots, images, {
442
+ reuseCachedRunImages: false
443
+ })
444
+ },
445
+ host: {
446
+ modelUsage: (grant, usage) => {
447
+ const runId = usage.executionId ?? usage.requestId;
448
+ this.modelUsage.bind(runId, grant, usage.sessionId, 'mar-agent');
449
+ this.modelUsage.record(runId, normalizeMarAgentUsage(usage));
450
+ this.modelUsage.release(runId);
451
+ },
452
+ nodeGeneration: () => this.nodeGeneration,
453
+ sessionWatchSlots: this.sessionWatchSlots,
454
+ emitWatchEvent: (payload) => this.send('watch.event', payload),
455
+ nextWorkbenchEvent: (payload) => this.workbenchEventService.next('hosted-conversation', payload),
456
+ emitWorkbenchEvent: (event) => this.workbenchEventService.publish(event),
457
+ readImage: (runId, imageRef) => this.runAttachmentService.readNative(runId, imageRef)
458
+ }
459
+ });
529
460
  this.upgradeCoordinator = new NodeUpgradeCoordinator(() => {
530
461
  if (this.config === undefined)
531
462
  throw new Error('NODE_CONFIG_UNAVAILABLE');
@@ -547,7 +478,7 @@ export class NodeConnector {
547
478
  const detect = options.capabilityDetector ?? detectCapabilitiesAsync;
548
479
  this.capabilityDetector = async () => {
549
480
  const capabilities = await detect();
550
- return this.projectComputerUseCapability(await this.projectConversationHostingCapability({
481
+ return this.projectComputerUseCapability(await this.conversationHosting.project({
551
482
  ...capabilities,
552
483
  metadata: upgradeMetadata(capabilities.metadata, this.upgradeCoordinator.snapshot())
553
484
  }));
@@ -576,10 +507,10 @@ export class NodeConnector {
576
507
  promoteSession: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
577
508
  send: (type, payload) => this.send(type, payload),
578
509
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
579
- nativeGoalUpdated: (runId, goal) => this.handleNativeGoalUpdate(runId, goal),
510
+ nativeGoalUpdated: (runId, goal) => this.goalCoordinator.handleNativeGoalUpdate(runId, goal),
580
511
  cancel: (runId, threadId, turnId, cwd, status) => this.runnerInteractionService.cancelCodexTurn(runId, threadId, turnId, cwd, status),
581
512
  createApproval: (input) => this.runtime.createApproval(input),
582
- publishApproval: (input) => this.publishApprovalForRun(input),
513
+ publishApproval: (input) => this.runnerInteractionService.publishApproval(input),
583
514
  emitItem: (runId, item) => this.emitConversationItem(runId, item),
584
515
  appendImages: (runId, images) => this.runAttachmentService.append(runId, images.filter(isRunnerImageAttachment)),
585
516
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
@@ -620,7 +551,7 @@ export class NodeConnector {
620
551
  this.capabilities = this.projectComputerUseCapability(this.capabilities);
621
552
  this.marAgentRunController = new MarAgentManagedRunController(this.marAgentClient, {
622
553
  createApproval: (input) => this.runtime.createApproval(input),
623
- publishApproval: (input) => this.publishApprovalForRun(input),
554
+ publishApproval: (input) => this.runnerInteractionService.publishApproval(input),
624
555
  active: this.runState.active,
625
556
  adopt: (input) => this.runtime.adoptRun(input),
626
557
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
@@ -683,7 +614,7 @@ export class NodeConnector {
683
614
  expireApproval: (approvalId) => {
684
615
  this.runtime.expireApproval(approvalId);
685
616
  },
686
- publishApproval: (input) => this.publishApprovalForRun(input),
617
+ publishApproval: (input) => this.runnerInteractionService.publishApproval(input),
687
618
  commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
688
619
  setCommandState: (sessionId, state) => this.commandStates.set(sessionId, state),
689
620
  clearCommandState: (sessionId, commandId) => this.commandStates.clear(sessionId, commandId),
@@ -732,7 +663,7 @@ export class NodeConnector {
732
663
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
733
664
  emitItem: (runId, item) => this.emitConversationItem(runId, item),
734
665
  approvalTimeoutMs: this.claudeApprovalTimeoutMs,
735
- publishApproval: (input) => this.publishApprovalForRun(input)
666
+ publishApproval: (input) => this.runEventBridge.publishApproval(input)
736
667
  });
737
668
  this.runnerChannelMessages = new RunnerChannelMessageService({
738
669
  interactions: this.runnerInteractionService,
@@ -781,7 +712,7 @@ export class NodeConnector {
781
712
  respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
782
713
  pauseQueue: (sessionId) => this.workspaceQueueWorkbenchService.pauseSessionQueue(sessionId),
783
714
  runInterrupted: (sessionId) => {
784
- const goalExit = this.clearGoalState(sessionId, 'stopped');
715
+ const goalExit = this.goalCoordinator.clearGoalState(sessionId, 'stopped');
785
716
  return goalExit === undefined ? undefined : { goalExit };
786
717
  },
787
718
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
@@ -857,10 +788,11 @@ export class NodeConnector {
857
788
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
858
789
  mcps: (workspaceId) => this.database?.effectiveMcpInstallations(workspaceId) ?? [],
859
790
  retainEnvironment: (environment) => this.gitExecution().retain(environment),
860
- goalCurrent: (input) => input.goal === undefined || this.goalGenerationCurrent(input.sessionId, input.goal),
791
+ goalCurrent: (input) => input.goal === undefined ||
792
+ this.goalCoordinator.goalGenerationCurrent(input.sessionId, input.goal),
861
793
  goalActive: (sessionId) => this.goalRuntime.get(sessionId)?.status === 'active',
862
- afterExecutionStarted: (input) => this.maintainGoalContinuation(input, true),
863
- afterExecutionSteered: (input) => this.maintainGoalContinuation(input, false),
794
+ afterExecutionStarted: (input) => this.goalCoordinator.maintainGoalContinuation(input, true),
795
+ afterExecutionSteered: (input) => this.goalCoordinator.maintainGoalContinuation(input, false),
864
796
  afterExecutionStartFailed: (input, error) => {
865
797
  nodeLog('goal.execution-start.failed', {
866
798
  sessionId: input.sessionId,
@@ -939,35 +871,8 @@ export class NodeConnector {
939
871
  migrateCommands: (previousId, nextId) => this.commandStates.promoteSessionIdentity(previousId, nextId),
940
872
  migrateGoal: (previousId, nextId) => {
941
873
  this.runAttachmentService.migrateSession(previousId, nextId);
942
- this.goalRuntime.migrateSession(previousId, nextId);
874
+ this.goalCoordinator.migrateSession(previousId, nextId);
943
875
  this.goalMcpBridge.migrateSession(previousId, nextId);
944
- const retry = this.goalRetryTimers.get(previousId);
945
- if (retry !== undefined) {
946
- this.goalRetryTimers.delete(previousId);
947
- retry.sessionId = nextId;
948
- this.goalRetryTimers.set(nextId, retry);
949
- }
950
- const pendingContinuation = this.goalContinuationPending.get(previousId);
951
- if (pendingContinuation !== undefined) {
952
- this.goalContinuationPending.delete(previousId);
953
- pendingContinuation.sessionId = nextId;
954
- this.goalContinuationPending.set(nextId, pendingContinuation);
955
- }
956
- const submissionAttempts = this.goalSubmissionAttempts.get(previousId);
957
- if (submissionAttempts !== undefined) {
958
- this.goalSubmissionAttempts.delete(previousId);
959
- const currentAttempts = this.goalSubmissionAttempts.get(nextId) ?? new Set();
960
- for (const attempt of submissionAttempts) {
961
- attempt.sessionId = nextId;
962
- currentAttempts.add(attempt);
963
- }
964
- this.goalSubmissionAttempts.set(nextId, currentAttempts);
965
- }
966
- const continuationEpoch = this.goalContinuationEpochs.get(previousId);
967
- if (continuationEpoch !== undefined) {
968
- this.goalContinuationEpochs.delete(previousId);
969
- this.goalContinuationEpochs.set(nextId, continuationEpoch);
970
- }
971
876
  },
972
877
  migrateQueue: (previousId, nextId) => this.continuousRunCoordinator.migrateSession(previousId, nextId),
973
878
  migrateRunnerState: (previousId, nextId) => {
@@ -990,9 +895,12 @@ export class NodeConnector {
990
895
  projection: this.nativeSessionProjectionService,
991
896
  identity: this.sessionIdentityService,
992
897
  present: (session) => this.sessionPresentationService.present(session),
993
- emitSession: (session) => this.emitSessionWorkbenchEvent({
898
+ emitSession: (session) => this.publishWorkbenchEvent('session', {
994
899
  session: this.sessionPresentationService.present(session)
995
900
  }),
901
+ invalidateWorkspaceSessions: (workspaceId) => {
902
+ void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions');
903
+ },
996
904
  execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
997
905
  interceptedSessionId: (clientMessageId) => this.fakeRunner === undefined ? undefined : `fake:${clientMessageId}`
998
906
  });
@@ -1026,83 +934,9 @@ export class NodeConnector {
1026
934
  emitSession: (session) => this.emitSessionWorkbenchEvent({
1027
935
  session: this.sessionPresentationService.present(session)
1028
936
  }),
1029
- beginGoal: (session) => {
1030
- const existing = this.commandStates.get(session.id, 'goal');
1031
- if (existing?.active === true)
1032
- return {
1033
- session: this.sessionPresentationService.present(session),
1034
- goalAlreadyActive: true
1035
- };
1036
- if (existing !== undefined) {
1037
- const previousGoal = this.goalRuntime.get(session.id);
1038
- if (previousGoal !== undefined)
1039
- this.nativeGoalBindings.delete(previousGoal.id);
1040
- this.goalRuntime.clear(session.id);
1041
- this.commandStates.clear(session.id, 'goal');
1042
- }
1043
- const prepared = this.prepareGoalSession(session);
1044
- this.continuousRunCoordinator.removePendingQueue(session.id);
1045
- this.commandStates.set(session.id, this.pendingGoalCommandState());
1046
- this.emitWorkbenchEvent('session', {
1047
- session: this.sessionPresentationService.present(prepared.session)
1048
- });
1049
- return {
1050
- session: this.sessionPresentationService.present(prepared.session),
1051
- goalAccessChanged: prepared.changed
1052
- };
1053
- },
1054
- goal: async ({ session, objective, initiatedByUserId, suppressNotifications, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentCompact, marAgentRuntimeScopeId, usageGrant }) => {
1055
- this.invalidateGoalContinuation(session.id);
1056
- const prepared = this.prepareGoalSession(session);
1057
- const goalSession = prepared.session;
1058
- const activeRun = this.runtime.activeSessionRun(session.id);
1059
- const replacing = activeRun !== undefined && this.runtime.currentExecutionId(activeRun.id) !== undefined;
1060
- this.continuousRunCoordinator.removeGoalQueue(session.id);
1061
- const goal = this.goalRuntime.start(session.id, objective);
1062
- this.publishGoalState(goalSession, goal);
1063
- const attempt = this.beginGoalSubmission(session.id);
1064
- const goalRef = { id: goal.id, revision: goal.revision, epoch: attempt.epoch };
1065
- let result;
1066
- try {
1067
- result = await this.sessionMessageService.submitGoal({
1068
- sessionId: attempt.sessionId,
1069
- kind: 'start',
1070
- content: this.goalRuntime.prompt(attempt.sessionId, 'start'),
1071
- objective: goal.objective,
1072
- deliveryIntent: replacing ? 'REPLACE_CURRENT' : 'SEND',
1073
- goal: goalRef,
1074
- ...(suppressNotifications === true ? { suppressNotifications: true } : {}),
1075
- ...(usageGrant === undefined ? {} : { usageGrant }),
1076
- ...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
1077
- secretEnvironment,
1078
- ...(personalInstructions === undefined ? {} : { personalInstructions }),
1079
- ...(marAgentModel === undefined ? {} : { marAgentModel }),
1080
- ...(marAgentModels === undefined ? {} : { marAgentModels }),
1081
- ...(marAgentCompact === undefined ? {} : { marAgentCompact }),
1082
- ...(marAgentRuntimeScopeId === undefined ? {} : { marAgentRuntimeScopeId })
1083
- });
1084
- if (!this.goalGenerationCurrent(attempt.sessionId, goalRef)) {
1085
- this.removeStaleGoalSubmission(attempt.sessionId, result);
1086
- throw new Error('GOAL_REVISION_STALE');
1087
- }
1088
- }
1089
- finally {
1090
- this.finishGoalSubmissionAttempt(attempt);
1091
- }
1092
- return {
1093
- ...(isPlainRecord(result) ? result : { result }),
1094
- goalAccessChanged: prepared.changed
1095
- };
1096
- },
1097
- clearGoal: (session) => {
1098
- const goalExit = this.clearGoalState(session.id, 'removed');
1099
- this.finishIdleGoalRun(session.id, 'SUCCEEDED');
1100
- return {
1101
- session: this.sessionPresentationService.present(session),
1102
- cleared: true,
1103
- ...(goalExit === undefined ? {} : { goalExit })
1104
- };
1105
- }
937
+ beginGoal: (session) => this.goalCoordinator.begin(session),
938
+ goal: (input) => this.goalCoordinator.start(input),
939
+ clearGoal: (session) => this.goalCoordinator.clear(session)
1106
940
  });
1107
941
  this.sessionMessageService = new SessionMessageService({
1108
942
  runtime: this.runtime,
@@ -1121,53 +955,9 @@ export class NodeConnector {
1121
955
  const session = this.runtime.getAgentSession(sessionId);
1122
956
  return session?.channelToken ?? undefined;
1123
957
  },
1124
- goalMessage: (session, content, intent) => {
1125
- const state = this.commandStates.get(session.id, 'goal');
1126
- if (state?.active !== true)
1127
- return undefined;
1128
- const current = this.goalRuntime.get(session.id);
1129
- if (current?.status === 'active') {
1130
- const idlePausedGoal = this.runtime.activeSessionRun(session.id) === undefined &&
1131
- this.runtime.sessionQueue(session.id).paused;
1132
- if (intent !== 'REPLACE_CURRENT' && !idlePausedGoal)
1133
- return undefined;
1134
- this.invalidateGoalContinuation(session.id);
1135
- const resumed = this.goalRuntime.resume(session.id);
1136
- if (resumed !== current)
1137
- this.publishGoalState(session, resumed);
1138
- return {
1139
- runnerContent: content,
1140
- goal: {
1141
- id: resumed.id,
1142
- revision: resumed.revision,
1143
- epoch: this.goalContinuationEpochs.get(session.id) ?? 0
1144
- },
1145
- activated: false
1146
- };
1147
- }
1148
- const objective = composerVisibleText(content);
1149
- if (objective.length === 0)
1150
- return undefined;
1151
- this.invalidateGoalContinuation(session.id);
1152
- const goal = this.goalRuntime.start(session.id, objective, composerResourceInputTags(content));
1153
- this.publishGoalState(session, goal);
1154
- return {
1155
- runnerContent: this.goalRuntime.prompt(session.id, 'start'),
1156
- goal: {
1157
- id: goal.id,
1158
- revision: goal.revision,
1159
- epoch: this.goalContinuationEpochs.get(session.id) ?? 0
1160
- },
1161
- activated: true
1162
- };
1163
- },
1164
- prepareGoalInsertion: (sessionId, submissionId, goal) => {
1165
- if (!this.goalGenerationCurrent(sessionId, goal))
1166
- return false;
1167
- this.continuousRunCoordinator.removeGoalQueue(sessionId, undefined, submissionId);
1168
- return true;
1169
- },
1170
- goalPreparationFailed: (session, goal, activated) => this.handleGoalPreparationFailed(session, goal, activated)
958
+ goalMessage: (session, content, intent) => this.goalCoordinator.goalMessage(session, content, intent),
959
+ prepareGoalInsertion: (sessionId, submissionId, goal) => this.goalCoordinator.prepareInsertion(sessionId, submissionId, goal),
960
+ goalPreparationFailed: (session, goal, activated) => this.goalCoordinator.handleGoalPreparationFailed(session, goal, activated)
1171
961
  });
1172
962
  const inputState = (entry, message) => this.marAgentClient.restartInputState(entry.session.id, message.clientMessageId);
1173
963
  const restorer = new RestartSessionRestorer({
@@ -1180,7 +970,8 @@ export class NodeConnector {
1180
970
  authorize: (request) => this.restartAuthorization.authorize(request),
1181
971
  inputState,
1182
972
  withGrant: (context, work) => this.modelUsage.withGrant(context.usageGrant, work),
1183
- publishGoal: (session, goal) => this.publishGoalState(session, goal),
973
+ publishGoal: (session, goal) => this.goalCoordinator.publishGoalState(session, goal),
974
+ goalReference: (sessionId) => this.goalCoordinator.reference(sessionId),
1184
975
  publishQueue: (session) => this.continuousRunCoordinator.publish(session.id)
1185
976
  });
1186
977
  this.restartHandoff = new PlannedRestartHandoff({
@@ -1284,7 +1075,7 @@ export class NodeConnector {
1284
1075
  runtime: this.runtime,
1285
1076
  state: this.runState,
1286
1077
  emitRun: (runId, type, payload, status) => this.runEventBridge.emitRun(runId, type, payload, status),
1287
- publishApproval: (input) => this.publishApprovalForRun(input),
1078
+ publishApproval: (input) => this.runnerInteractionService.publishApproval(input),
1288
1079
  disconnect: () => this.controlChannel.close(4010, 'FAKE_RUNNER_DISCONNECT')
1289
1080
  });
1290
1081
  this.fakeRunner?.bindControl({
@@ -1437,7 +1228,8 @@ export class NodeConnector {
1437
1228
  heartbeatMs: HEARTBEAT_MS,
1438
1229
  refreshMs: CAPABILITY_REFRESH_MS,
1439
1230
  onRegistered: async () => {
1440
- await this.initializeConversationHosting();
1231
+ await this.conversationHosting.initialize();
1232
+ this.capabilities = await this.conversationHosting.project(this.capabilities);
1441
1233
  this.computerUseEnablement.initialize();
1442
1234
  },
1443
1235
  onConnected: () => {
@@ -1499,7 +1291,11 @@ export class NodeConnector {
1499
1291
  }));
1500
1292
  return { runId: result.run.id };
1501
1293
  },
1502
- notify: (task, occurrenceId, code, status) => this.emitScheduledNotification(task, occurrenceId, code, status)
1294
+ sessionTitle: (sessionId) => {
1295
+ const session = this.runtime.getAgentSession(sessionId);
1296
+ return session?.customTitle ?? session?.runnerTitle ?? sessionId;
1297
+ },
1298
+ emitNotification: (payload) => this.emitWorkbenchEvent('conversation', payload)
1503
1299
  });
1504
1300
  this.runnerService = new RunnerService(requireDatabase, () => this.capabilities, this.runners, this.runnerUsageReader);
1505
1301
  this.workbenchManifestService = new WorkbenchManifestService({
@@ -1588,21 +1384,21 @@ export class NodeConnector {
1588
1384
  this.operationRouter.registerAll(this.promptCompletion.operations());
1589
1385
  for (const operation of CONVERSATION_TEMPLATE_OPERATIONS)
1590
1386
  this.operationRouter.register(operation, (data) => {
1591
- const handler = this.conversationTemplateOperations?.[operation];
1387
+ const handler = this.conversationHosting.handler(operation);
1592
1388
  if (handler === undefined)
1593
1389
  throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1594
1390
  return handler(data);
1595
1391
  });
1596
1392
  for (const operation of HOSTED_CONVERSATION_OPERATIONS)
1597
1393
  this.operationRouter.register(operation, (data) => {
1598
- const handler = this.hostedConversationOperations?.[operation];
1394
+ const handler = this.conversationHosting.handler(operation);
1599
1395
  if (handler === undefined)
1600
1396
  throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1601
1397
  return handler(data);
1602
1398
  });
1603
1399
  for (const operation of CONVERSATION_HOSTING_MAINTENANCE_OPERATIONS)
1604
1400
  this.operationRouter.register(operation, (data) => {
1605
- const handler = this.conversationHostingMaintenanceOperations?.[operation];
1401
+ const handler = this.conversationHosting.handler(operation);
1606
1402
  if (handler === undefined)
1607
1403
  throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1608
1404
  return handler(data);
@@ -1630,7 +1426,7 @@ export class NodeConnector {
1630
1426
  session: this.sessionPresentationService.present(session)
1631
1427
  }),
1632
1428
  changed: () => this.scheduledSessions.changed(),
1633
- proposed: (sessionId, proposalId) => this.emitScheduledProposalNotification(sessionId, proposalId)
1429
+ proposed: (sessionId, proposalId) => this.scheduledSessions.proposalCreated(sessionId, proposalId)
1634
1430
  });
1635
1431
  this.operationRouter.registerAll(this.sessionScheduleService.operations());
1636
1432
  this.operationRouter.registerAll(this.sessionMessageService.operations());
@@ -1645,8 +1441,11 @@ export class NodeConnector {
1645
1441
  configPath: this.configPath,
1646
1442
  platform: () => this.capabilities.platform,
1647
1443
  capability: () => this.capabilities.conversationHosting,
1648
- initialize: () => this.initializeConversationHosting(),
1649
- prepareForDisable: () => this.hostedConversationService?.prepareForDisable() ?? (() => { }),
1444
+ initialize: async () => {
1445
+ await this.conversationHosting.initialize();
1446
+ this.capabilities = await this.conversationHosting.project(this.capabilities);
1447
+ },
1448
+ prepareForDisable: () => this.conversationHosting.prepareForDisable(),
1650
1449
  publishCapabilities: () => this.send('capabilities', this.capabilities)
1651
1450
  }).operations());
1652
1451
  this.operationRouter.registerAll(this.computerUseEnablement.operations());
@@ -1670,147 +1469,12 @@ export class NodeConnector {
1670
1469
  binaryTerminal: true
1671
1470
  }));
1672
1471
  }
1673
- async initializeConversationHosting() {
1674
- const previousRuntime = this.hostedConversationRuntime;
1675
- const previousService = this.hostedConversationService;
1676
- const previousMaintenance = this.conversationHostingMaintenanceService;
1677
- this.hostedConversationRuntime = undefined;
1678
- this.hostedConversationService = undefined;
1679
- this.conversationHostingMaintenanceService = undefined;
1680
- this.hostedConversationOperations = undefined;
1681
- this.conversationHostingMaintenanceOperations = undefined;
1682
- this.conversationTemplateOperations = undefined;
1683
- this.conversationHostingStorageReady = false;
1684
- this.conversationHostingFailureCode = undefined;
1685
- await previousMaintenance?.dispose();
1686
- previousService?.dispose();
1687
- await previousRuntime?.dispose().catch(() => undefined);
1688
- const config = this.config;
1689
- if (config?.conversationHosting?.enabled === true && this.capabilities.platform === 'linux') {
1690
- if (config.nodeId === undefined) {
1691
- this.conversationHostingFailureCode = 'NODE_UNAUTHENTICATED';
1692
- }
1693
- else {
1694
- let runtime;
1695
- let hostedService;
1696
- let maintenanceService;
1697
- try {
1698
- if (this.database === undefined)
1699
- throw new Error('NODE_DATABASE_UNAVAILABLE');
1700
- const rootDirectory = hostedConversationStorePath(config, this.configPath);
1701
- if (rootDirectory === undefined)
1702
- throw new Error('CONVERSATION_HOSTING_STORAGE_UNAVAILABLE');
1703
- const store = new HostedConversationStore({
1704
- dataDirectory: nodeDataDirectory(config, this.configPath),
1705
- rootDirectory,
1706
- nodeId: config.nodeId,
1707
- database: this.database
1708
- });
1709
- await store.initialize();
1710
- const templateService = new ConversationTemplateNodeService({
1711
- database: this.database,
1712
- store,
1713
- nodeId: () => this.config?.nodeId ?? 'unregistered'
1714
- });
1715
- await templateService.recoverInterruptedPublications();
1716
- await store.rebuildConversationIndex();
1717
- runtime = new MarAgentHostedConversationRuntime({
1718
- webSearch: (input, signal) => this.webToolService.webSearch(input, signal),
1719
- webFetch: (input, signal) => this.webToolService.webFetch(input, signal, false),
1720
- modelFactory: this.marAgentRuntimeModels,
1721
- registerNativeImages: (runId, roots, images) => this.runAttachmentService.registerNativeImages(runId, roots, images, {
1722
- reuseCachedRunImages: false
1723
- })
1724
- });
1725
- hostedService = new HostedConversationNodeService({
1726
- modelUsage: (grant, usage) => {
1727
- const runId = usage.executionId ?? usage.requestId;
1728
- this.modelUsage.bind(runId, grant, usage.sessionId, 'mar-agent');
1729
- this.modelUsage.record(runId, normalizeMarAgentUsage(usage));
1730
- this.modelUsage.release(runId);
1731
- },
1732
- database: this.database,
1733
- store,
1734
- runtime,
1735
- nodeId: () => this.config?.nodeId ?? 'unregistered',
1736
- nodeAppVersion: () => this.capabilities.nodeAppVersion ?? 'unknown',
1737
- nodeGeneration: () => this.nodeGeneration,
1738
- sessionWatchSlots: this.sessionWatchSlots,
1739
- emitWatchEvent: (payload) => this.send('watch.event', payload),
1740
- nextWorkbenchEvent: (payload) => this.workbenchEventService.next('hosted-conversation', payload),
1741
- emitWorkbenchEvent: (event) => this.workbenchEventService.publish(event),
1742
- readImage: (runId, imageRef) => this.runAttachmentService.readNative(runId, imageRef),
1743
- processExecutorFactory: (input) => new BubblewrapSandboxExecutor({
1744
- profile: hostedConversationSandboxProfile(input)
1745
- }),
1746
- assertCanCreate: () => {
1747
- if (maintenanceService === undefined)
1748
- throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1749
- return maintenanceService.assertCanCreate();
1750
- }
1751
- });
1752
- maintenanceService = new ConversationHostingMaintenanceService({
1753
- store,
1754
- inactiveRetentionDays: config.conversationHosting.inactiveRetentionDays ?? 60,
1755
- isConversationInUse: (conversationId) => hostedService?.isConversationInUse(conversationId) ?? true,
1756
- beginConversationCleanup: (conversationId) => hostedService?.beginConversationCleanup(conversationId),
1757
- releaseConversation: (conversation) => {
1758
- if (hostedService === undefined)
1759
- throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1760
- return hostedService.releaseConversationForCleanup(conversation);
1761
- },
1762
- onAutomaticCleanupError: (error) => nodeLog('conversation.hosting.cleanup.failed', {
1763
- nodeId: config.nodeId,
1764
- code: safeErrorCode(error, 'CONVERSATION_HOSTING_CLEANUP_FAILED')
1765
- })
1766
- });
1767
- this.conversationTemplateOperations = templateService.operations();
1768
- this.hostedConversationOperations = hostedService.operations();
1769
- this.conversationHostingMaintenanceOperations = maintenanceService.operations();
1770
- this.hostedConversationRuntime = runtime;
1771
- this.hostedConversationService = hostedService;
1772
- this.conversationHostingMaintenanceService = maintenanceService;
1773
- await maintenanceService.start();
1774
- this.conversationHostingStorageReady = true;
1775
- }
1776
- catch (error) {
1777
- await maintenanceService?.dispose();
1778
- hostedService?.dispose();
1779
- await runtime?.dispose().catch(() => undefined);
1780
- this.conversationHostingFailureCode = safeErrorCode(error, 'CONVERSATION_HOSTING_RECOVERY_FAILED');
1781
- nodeLog('conversation.hosting.initialization.failed', {
1782
- nodeId: config.nodeId,
1783
- code: this.conversationHostingFailureCode
1784
- });
1785
- }
1786
- }
1787
- }
1788
- this.capabilities = await this.projectConversationHostingCapability(this.capabilities);
1789
- }
1790
1472
  projectComputerUseCapability(capabilities) {
1791
1473
  return {
1792
1474
  ...capabilities,
1793
1475
  computerUse: this.computerUseEnablement.capability()
1794
1476
  };
1795
1477
  }
1796
- async projectConversationHostingCapability(capabilities) {
1797
- let storageReady = this.conversationHostingStorageReady;
1798
- let storageFailureCode = this.conversationHostingFailureCode;
1799
- let capacity = 'AVAILABLE';
1800
- if (storageReady && this.conversationHostingMaintenanceService !== undefined) {
1801
- try {
1802
- capacity = (await this.conversationHostingMaintenanceService.storageCapacity()).capacity;
1803
- }
1804
- catch (error) {
1805
- storageReady = false;
1806
- storageFailureCode = safeErrorCode(error, 'CONVERSATION_HOSTING_STORAGE_UNAVAILABLE');
1807
- }
1808
- }
1809
- return {
1810
- ...capabilities,
1811
- conversationHosting: await conversationHostingCapability(capabilities, this.config, storageReady, storageFailureCode, this.conversationHostingProbe, capacity)
1812
- };
1813
- }
1814
1478
  async start() {
1815
1479
  if (this.stopping !== undefined)
1816
1480
  await this.stopping;
@@ -1837,7 +1501,8 @@ export class NodeConnector {
1837
1501
  await this.restartHandoff.load();
1838
1502
  this.scheduledSessions.recoverInterrupted();
1839
1503
  this.scheduledSessions.start();
1840
- await this.initializeConversationHosting();
1504
+ await this.conversationHosting.initialize();
1505
+ this.capabilities = await this.conversationHosting.project(this.capabilities);
1841
1506
  this.computerUseEnablement.initialize();
1842
1507
  this.directTransferService ??= await this.createDirectTransferService();
1843
1508
  this.connectionLifecycle.start();
@@ -1914,287 +1579,12 @@ export class NodeConnector {
1914
1579
  };
1915
1580
  return this.directTransferService?.handleTerminal(command) ?? false;
1916
1581
  }
1917
- clearGoalRetry(sessionId) {
1918
- const retry = this.goalRetryTimers.get(sessionId);
1919
- if (retry !== undefined)
1920
- clearTimeout(retry.timer);
1921
- this.goalRetryTimers.delete(sessionId);
1922
- this.goalContinuationPending.delete(sessionId);
1923
- }
1924
- invalidateGoalContinuation(sessionId) {
1925
- this.goalContinuationEpochs.set(sessionId, (this.goalContinuationEpochs.get(sessionId) ?? 0) + 1);
1926
- this.clearGoalRetry(sessionId);
1927
- }
1928
- retireGoalContinuation(sessionId) {
1929
- this.invalidateGoalContinuation(sessionId);
1930
- this.goalContinuationEpochs.delete(sessionId);
1931
- }
1932
- goalGenerationCurrent(sessionId, goal) {
1933
- const current = this.goalRuntime.get(sessionId);
1934
- return ((this.goalContinuationEpochs.get(sessionId) ?? 0) === goal.epoch &&
1935
- current?.status === 'active' &&
1936
- current.id === goal.id &&
1937
- current.revision === goal.revision);
1938
- }
1939
- async maintainGoalContinuation(input, started) {
1940
- if (this.stopped || input.goal === undefined)
1941
- return;
1942
- if (!this.goalGenerationCurrent(input.sessionId, input.goal))
1943
- return;
1944
- const updated = started ? this.goalRuntime.recordTurn(input.sessionId) : undefined;
1945
- const session = this.runtime.getAgentSession(input.sessionId);
1946
- const run = this.runtime.getRun(input.runId);
1947
- await this.enqueueGoalContinuation({
1948
- sessionId: input.sessionId,
1949
- goal: { id: input.goal.id, revision: input.goal.revision },
1950
- continuationEpoch: input.goal.epoch,
1951
- ...(run?.initiatedByUserId === undefined ? {} : { initiatedByUserId: run.initiatedByUserId }),
1952
- ...(input.usageGrant === undefined ? {} : { usageGrant: input.usageGrant }),
1953
- secretEnvironment: input.secretEnvironment,
1954
- ...(input.personalInstructions === undefined
1955
- ? {}
1956
- : { personalInstructions: input.personalInstructions }),
1957
- ...(input.marAgentModel === undefined ? {} : { marAgentModel: input.marAgentModel }),
1958
- ...(input.marAgentModels === undefined ? {} : { marAgentModels: input.marAgentModels }),
1959
- ...(input.marAgentCompact === undefined ? {} : { marAgentCompact: input.marAgentCompact }),
1960
- ...(input.marAgentRuntimeScopeId === undefined
1961
- ? {}
1962
- : { marAgentRuntimeScopeId: input.marAgentRuntimeScopeId })
1963
- });
1964
- if (updated !== undefined && session !== undefined)
1965
- this.publishGoalState(session, updated);
1966
- }
1967
- handleNativeGoalUpdate(runId, nativeGoal) {
1968
- const pending = this.continuousRunCoordinator.pending(runId);
1969
- if (pending?.goal === undefined || !this.goalGenerationCurrent(pending.sessionId, pending.goal))
1970
- return;
1971
- const goal = this.goalRuntime.get(pending.sessionId);
1972
- if (nativeGoal.status === 'active') {
1973
- if ((nativeGoal.createdAt + 1) * 1_000 < goal.revisionStartedAt)
1974
- return;
1975
- const previous = this.nativeGoalBindings.get(goal.id);
1976
- if (previous !== undefined &&
1977
- previous.revision !== goal.revision &&
1978
- previous.threadId === nativeGoal.threadId &&
1979
- previous.createdAt === nativeGoal.createdAt &&
1980
- previous.objective === nativeGoal.objective)
1981
- return;
1982
- this.nativeGoalBindings.set(goal.id, {
1983
- revision: goal.revision,
1984
- threadId: nativeGoal.threadId,
1985
- createdAt: nativeGoal.createdAt,
1986
- objective: nativeGoal.objective
1987
- });
1988
- return;
1989
- }
1990
- const bound = this.nativeGoalBindings.get(goal.id);
1991
- if (bound?.revision !== goal.revision ||
1992
- bound.threadId !== nativeGoal.threadId ||
1993
- bound.createdAt !== nativeGoal.createdAt ||
1994
- bound.objective !== nativeGoal.objective)
1995
- return;
1996
- this.nativeGoalBindings.delete(goal.id);
1997
- const completed = this.goalRuntime.update(pending.sessionId, goal.id, goal.revision, 'completed');
1998
- this.retireGoalContinuation(pending.sessionId);
1999
- this.continuousRunCoordinator.removeGoalQueue(pending.sessionId);
2000
- const session = this.runtime.getAgentSession(pending.sessionId);
2001
- if (session !== undefined)
2002
- this.publishGoalState(session, completed);
2003
- }
2004
- beginGoalSubmission(sessionId, continuationEpoch = this.goalContinuationEpochs.get(sessionId) ?? 0) {
2005
- const attempt = { sessionId, epoch: continuationEpoch };
2006
- const attempts = this.goalSubmissionAttempts.get(sessionId) ?? new Set();
2007
- attempts.add(attempt);
2008
- this.goalSubmissionAttempts.set(sessionId, attempts);
2009
- return attempt;
2010
- }
2011
- finishGoalSubmissionAttempt(attempt) {
2012
- const attempts = this.goalSubmissionAttempts.get(attempt.sessionId);
2013
- attempts?.delete(attempt);
2014
- if (attempts?.size === 0)
2015
- this.goalSubmissionAttempts.delete(attempt.sessionId);
2016
- }
2017
- finishGoalContinuationAttempt(attempt) {
2018
- if (this.goalContinuationPending.get(attempt.sessionId) === attempt)
2019
- this.goalContinuationPending.delete(attempt.sessionId);
2020
- this.finishGoalSubmissionAttempt(attempt);
2021
- }
2022
- removeStaleGoalSubmission(sessionId, result) {
2023
- if (!isPlainRecord(result))
2024
- return;
2025
- if (typeof result.queueItemId === 'string') {
2026
- const item = this.runtime.getQueueItem(result.queueItemId);
2027
- if (item?.sessionId === sessionId)
2028
- this.continuousRunCoordinator.deleteQueueItem(item.id);
2029
- return;
2030
- }
2031
- if (!isPlainRecord(result.run) || typeof result.run.id !== 'string')
2032
- return;
2033
- const run = this.runtime.getRun(result.run.id);
2034
- if (run?.sessionId === sessionId && this.runtime.currentExecutionId(run.id) === undefined)
2035
- this.runEventBridge.emitRun(run.id, 'run.completed', { code: 'GOAL_SUBMISSION_STALE' }, 'INTERRUPTED');
2036
- }
2037
- async enqueueGoalContinuation(input) {
2038
- const attempt = this.beginGoalSubmission(input.sessionId, input.continuationEpoch);
2039
- const generation = { ...input.goal, epoch: attempt.epoch };
2040
- if (!this.goalGenerationCurrent(attempt.sessionId, generation)) {
2041
- this.finishGoalSubmissionAttempt(attempt);
2042
- return;
2043
- }
2044
- this.goalContinuationPending.set(input.sessionId, attempt);
2045
- try {
2046
- const result = await this.sessionMessageService.submitGoal({
2047
- sessionId: attempt.sessionId,
2048
- kind: 'continue',
2049
- content: this.goalRuntime.prompt(attempt.sessionId, 'continue'),
2050
- deliveryIntent: input.deliveryIntent ?? 'QUEUE',
2051
- goal: generation,
2052
- ...(input.initiatedByUserId === undefined
2053
- ? {}
2054
- : { initiatedByUserId: input.initiatedByUserId }),
2055
- ...(input.usageGrant === undefined ? {} : { usageGrant: input.usageGrant }),
2056
- secretEnvironment: input.secretEnvironment,
2057
- ...(input.personalInstructions === undefined
2058
- ? {}
2059
- : { personalInstructions: input.personalInstructions }),
2060
- ...(input.marAgentModel === undefined ? {} : { marAgentModel: input.marAgentModel }),
2061
- ...(input.marAgentModels === undefined ? {} : { marAgentModels: input.marAgentModels }),
2062
- ...(input.marAgentCompact === undefined ? {} : { marAgentCompact: input.marAgentCompact }),
2063
- ...(input.marAgentRuntimeScopeId === undefined
2064
- ? {}
2065
- : { marAgentRuntimeScopeId: input.marAgentRuntimeScopeId })
2066
- });
2067
- if (!this.goalGenerationCurrent(attempt.sessionId, generation)) {
2068
- this.removeStaleGoalSubmission(attempt.sessionId, result);
2069
- this.finishGoalContinuationAttempt(attempt);
2070
- return;
2071
- }
2072
- const retry = this.goalRetryTimers.get(attempt.sessionId);
2073
- if (retry !== undefined)
2074
- clearTimeout(retry.timer);
2075
- this.goalRetryTimers.delete(attempt.sessionId);
2076
- this.finishGoalContinuationAttempt(attempt);
2077
- }
2078
- catch (error) {
2079
- const failedAttempts = (input.failedAttempts ?? 0) + 1;
2080
- nodeLog('goal.continuation.enqueue.failed', {
2081
- sessionId: attempt.sessionId,
2082
- goalId: input.goal.id,
2083
- revision: input.goal.revision,
2084
- attempt: failedAttempts,
2085
- code: safeErrorCode(error, 'GOAL_CONTINUATION_ENQUEUE_FAILED')
2086
- });
2087
- if (!this.goalGenerationCurrent(attempt.sessionId, generation)) {
2088
- this.finishGoalContinuationAttempt(attempt);
2089
- return;
2090
- }
2091
- const session = this.runtime.getAgentSession(attempt.sessionId);
2092
- if (failedAttempts > 3 || session === undefined) {
2093
- this.clearGoalRetry(attempt.sessionId);
2094
- if (failedAttempts > 3) {
2095
- const blocked = this.goalRuntime.blockAfterRuntimeFailure(attempt.sessionId, input.goal.id, input.goal.revision);
2096
- this.nativeGoalBindings.delete(input.goal.id);
2097
- this.continuousRunCoordinator.removeGoalQueue(attempt.sessionId);
2098
- this.goalContinuationEpochs.delete(attempt.sessionId);
2099
- this.finishIdleGoalRun(attempt.sessionId, 'FAILED');
2100
- if (session !== undefined)
2101
- this.publishGoalState(session, blocked);
2102
- }
2103
- return;
2104
- }
2105
- this.finishGoalContinuationAttempt(attempt);
2106
- const previousRetry = this.goalRetryTimers.get(attempt.sessionId);
2107
- if (previousRetry !== undefined)
2108
- clearTimeout(previousRetry.timer);
2109
- const retry = {
2110
- sessionId: attempt.sessionId,
2111
- timer: setTimeout(() => {
2112
- if (!this.goalGenerationCurrent(retry.sessionId, generation))
2113
- return;
2114
- void this.enqueueGoalContinuation({
2115
- ...input,
2116
- sessionId: retry.sessionId,
2117
- failedAttempts,
2118
- continuationEpoch: attempt.epoch
2119
- });
2120
- }, 15_000)
2121
- };
2122
- this.goalRetryTimers.set(attempt.sessionId, retry);
2123
- retry.timer.unref();
2124
- }
2125
- }
2126
- handleGoalPreparationFailed(session, goal, activated) {
2127
- if (!activated || !this.goalGenerationCurrent(session.id, goal))
2128
- return;
2129
- this.goalRuntime.clear(session.id);
2130
- this.finishIdleGoalRun(session.id, 'FAILED');
2131
- this.commandStates.set(session.id, this.pendingGoalCommandState());
2132
- this.emitWorkbenchEvent('session', {
2133
- session: this.sessionPresentationService.present(session)
2134
- });
2135
- }
2136
- finishIdleGoalRun(sessionId, status) {
2137
- const run = this.runtime.activeSessionRun(sessionId);
2138
- if (run === undefined || this.runtime.currentExecutionId(run.id) !== undefined)
2139
- return;
2140
- this.runEventBridge.emitRun(run.id, status === 'SUCCEEDED' ? 'run.completed' : 'run.failed', status === 'SUCCEEDED' ? {} : { code: 'GOAL_CONTINUATION_FAILED' }, status);
2141
- }
2142
- clearGoalState(sessionId, status) {
2143
- const goalExit = this.goalRuntime.exitSummary(sessionId, status);
2144
- const goal = this.goalRuntime.get(sessionId);
2145
- if (goal !== undefined)
2146
- this.nativeGoalBindings.delete(goal.id);
2147
- this.retireGoalContinuation(sessionId);
2148
- const removed = this.goalRuntime.clear(sessionId);
2149
- this.continuousRunCoordinator.removeGoalQueue(sessionId);
2150
- const commandState = this.commandStates.get(sessionId, 'goal');
2151
- if (commandState !== undefined)
2152
- this.commandStates.clear(sessionId, 'goal');
2153
- if (!removed && commandState === undefined)
2154
- return goalExit;
2155
- const session = this.runtime.getAgentSession(sessionId);
2156
- if (session !== undefined)
2157
- this.emitWorkbenchEvent('session', {
2158
- session: this.sessionPresentationService.present(session)
2159
- });
2160
- return goalExit;
2161
- }
2162
- prepareGoalSession(session) {
2163
- const access = goalUnrestrictedAccess(session.runner, session.access);
2164
- const requiresUpdate = session.access !== access;
2165
- const changed = session.runner !== 'mar-agent' && requiresUpdate;
2166
- const prepared = requiresUpdate
2167
- ? this.runtime.updateAgentSessionAccess(session.id, access)
2168
- : session;
2169
- if (changed &&
2170
- prepared.externalSessionId !== null &&
2171
- prepared.model !== null &&
2172
- prepared.effort !== null)
2173
- this.database?.saveNativeSessionMetadata({
2174
- runner: prepared.runner,
2175
- externalSessionId: prepared.externalSessionId,
2176
- workspaceId: prepared.workspaceId,
2177
- model: prepared.model,
2178
- effort: prepared.effort,
2179
- access
2180
- });
2181
- return { session: prepared, changed };
2182
- }
2183
- pendingGoalCommandState() {
2184
- return {
2185
- commandId: 'goal',
2186
- active: true,
2187
- label: 'Goal',
2188
- detail: null,
2189
- closable: true,
2190
- closeInput: '/goal --clear'
2191
- };
2192
- }
2193
1582
  stop(options = {}) {
2194
1583
  if (this.stopping !== undefined)
2195
1584
  return this.stopping;
2196
1585
  this.stopped = true;
2197
1586
  this.continuousRunCoordinator.suspend();
1587
+ this.goalCoordinator.freeze();
2198
1588
  this.scheduledSessions.stop();
2199
1589
  this.stopping = (async () => {
2200
1590
  let snapshot;
@@ -2219,43 +1609,18 @@ export class NodeConnector {
2219
1609
  this.recentRunProjectionService.dispose();
2220
1610
  this.upgradeCoordinator.stop();
2221
1611
  this.connectionLifecycle.stop();
2222
- for (const approval of this.claudeClient.execution.approvals.values()) {
2223
- clearTimeout(approval.timer);
2224
- approval.resolve('REJECTED');
2225
- }
2226
- this.claudeClient.execution.approvals.clear();
2227
- for (const pending of this.claudeClient.execution.userInputs.values()) {
2228
- clearTimeout(pending.timer);
2229
- pending.reject('NODE_STOPPED');
2230
- }
2231
- this.claudeClient.execution.userInputs.clear();
2232
1612
  this.nativeSessionWatchService.clear();
2233
1613
  this.promptCompletion.dispose();
2234
1614
  this.sessionIdentityService.dispose();
2235
1615
  this.sessionLifecycleService.clear();
2236
1616
  this.continuousRunCoordinator.clear();
2237
1617
  this.gitOperationService.clear();
2238
- for (const retry of this.goalRetryTimers.values())
2239
- clearTimeout(retry.timer);
2240
- this.goalRetryTimers.clear();
2241
- this.goalContinuationPending.clear();
2242
- this.goalSubmissionAttempts.clear();
2243
- this.goalContinuationEpochs.clear();
1618
+ this.goalCoordinator.dispose();
2244
1619
  this.runEventService.clear();
2245
1620
  this.sessionContextService.dispose();
2246
- for (const run of this.claudeClient.execution.runs.values())
2247
- run.handle.cancel();
2248
- this.claudeClient.execution.runs.clear();
2249
1621
  this.runAttachmentService.dispose();
2250
1622
  const marAgentDrain = this.marAgentRunController.dispose();
2251
- const conversationHostingMaintenanceDrain = this.conversationHostingMaintenanceService?.dispose();
2252
- this.conversationHostingMaintenanceService = undefined;
2253
- this.hostedConversationService?.dispose();
2254
- this.hostedConversationService = undefined;
2255
- const hostedConversationRuntime = this.hostedConversationRuntime;
2256
- this.hostedConversationRuntime = undefined;
2257
- this.hostedConversationOperations = undefined;
2258
- this.conversationHostingMaintenanceOperations = undefined;
1623
+ const conversationHostingDrain = this.conversationHosting.dispose();
2259
1624
  const uploadDrain = this.workspaceUploadService.shutdown();
2260
1625
  this.workspaceFileService.dispose();
2261
1626
  this.workspaceWatchService.clearAll();
@@ -2271,7 +1636,7 @@ export class NodeConnector {
2271
1636
  this.terminalChannel.stop();
2272
1637
  this.runtimeCredentials.close();
2273
1638
  this.chatgptLogins.close();
2274
- this.scheduledSessions.stop();
1639
+ const scheduledDrain = this.scheduledSessions.stop();
2275
1640
  this.scheduledRunAuthorization.close();
2276
1641
  this.restartAuthorization.close();
2277
1642
  this.mailNotificationControl.close();
@@ -2281,8 +1646,8 @@ export class NodeConnector {
2281
1646
  this.directTransferService = undefined;
2282
1647
  this.controlChannel.stop();
2283
1648
  const database = this.database;
2284
- this.database = undefined;
2285
1649
  const drains = Promise.allSettled([
1650
+ scheduledDrain,
2286
1651
  marAgentDrain,
2287
1652
  runnerDrain,
2288
1653
  terminalDrain,
@@ -2295,11 +1660,11 @@ export class NodeConnector {
2295
1660
  return (async () => {
2296
1661
  let failed;
2297
1662
  try {
2298
- await conversationHostingMaintenanceDrain;
1663
+ await conversationHostingDrain;
2299
1664
  }
2300
1665
  finally {
2301
- await hostedConversationRuntime?.dispose().catch(() => undefined);
2302
1666
  const results = await drains;
1667
+ this.database = undefined;
2303
1668
  await this.gitExecutionService?.close();
2304
1669
  this.usageOutbox?.disconnected();
2305
1670
  this.usageOutbox = undefined;
@@ -2313,97 +1678,9 @@ export class NodeConnector {
2313
1678
  }
2314
1679
  /** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
2315
1680
  handleMessage = (raw) => this.controlMessages.handle(raw);
2316
- publishApprovalForRun = (input) => {
2317
- const run = this.runtime.getRun(this.runtime.canonicalRunId(input.runId));
2318
- if (run?.source !== 'SCHEDULED') {
2319
- this.runEventBridge.publishApproval(input);
2320
- return;
2321
- }
2322
- this.runEventBridge.emitRun(input.runId, 'approval.request', input);
2323
- queueMicrotask(() => {
2324
- try {
2325
- this.runtime.decideApproval(input.approvalId, 'APPROVED');
2326
- this.runEventBridge.emitRun(input.runId, 'approval.decided', {
2327
- approvalId: input.approvalId,
2328
- decision: 'APPROVED',
2329
- source: 'SCHEDULED'
2330
- });
2331
- this.runners.decideApproval({
2332
- runId: input.runId,
2333
- approvalId: input.approvalId,
2334
- decision: 'APPROVED'
2335
- });
2336
- }
2337
- catch {
2338
- // A cancelled execution cannot receive an approval after its pending request expires.
2339
- }
2340
- });
2341
- };
2342
- emitScheduledNotification = (task, occurrenceId, code, status) => this.publishScheduledTaskNotification(task, occurrenceId, {
2343
- kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
2344
- code
2345
- });
2346
- publishScheduledTaskNotification = (task, occurrenceId, event) => {
2347
- const occurredAt = Date.now();
2348
- const recipient = (this.database === undefined
2349
- ? undefined
2350
- : new ScheduledSessionStore(this.database.raw).getById(task.id))?.ownerUserId ?? task.ownerUserId;
2351
- const session = this.runtime.getAgentSession(task.sessionId);
2352
- this.emitWorkbenchEvent('conversation', {
2353
- event: {
2354
- runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
2355
- sequence: 1,
2356
- eventType: 'notification',
2357
- payload: {
2358
- targetType: 'WORKSPACE_SESSION',
2359
- userId: recipient,
2360
- workspaceId: task.workspaceId,
2361
- sessionId: task.sessionId,
2362
- sessionTitle: session?.customTitle ?? session?.runnerTitle ?? task.sessionId,
2363
- runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
2364
- notificationId: event.kind === 'SCHEDULE_TERMINAL'
2365
- ? `schedule-terminal:${task.id}:${occurrenceId}`
2366
- : `schedule:${task.id}:${occurrenceId}`,
2367
- ...event,
2368
- occurredAt
2369
- },
2370
- createdAt: occurredAt
2371
- }
2372
- });
2373
- };
2374
- emitScheduledProposalNotification = (sessionId, proposalId) => {
2375
- if (this.database === undefined)
2376
- return;
2377
- const task = new ScheduledSessionStore(this.database.raw).get(sessionId);
2378
- if (task === undefined)
2379
- return;
2380
- const occurredAt = Date.now();
2381
- const session = this.runtime.getAgentSession(sessionId);
2382
- this.emitWorkbenchEvent('conversation', {
2383
- event: {
2384
- runId: proposalId,
2385
- sequence: 1,
2386
- eventType: 'notification',
2387
- payload: {
2388
- targetType: 'WORKSPACE_SESSION',
2389
- userId: task.ownerUserId,
2390
- workspaceId: task.workspaceId,
2391
- sessionId,
2392
- sessionTitle: session?.customTitle ?? session?.runnerTitle ?? sessionId,
2393
- runId: proposalId,
2394
- notificationId: `schedule-proposal:${task.id}:${proposalId}`,
2395
- kind: 'SCHEDULE_PROPOSAL',
2396
- occurredAt
2397
- },
2398
- createdAt: occurredAt
2399
- }
2400
- });
2401
- };
2402
1681
  emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
2403
1682
  emitConversationItem = (runId, item) => {
2404
- if (this.continuousRunCoordinator.pending(runId)?.goal !== undefined &&
2405
- isGoalProgressItem(item))
2406
- this.goalExecutionProgress.add(runId);
1683
+ this.goalCoordinator.executionProgress(runId, item);
2407
1684
  this.runEventBridge.emitConversationItem(runId, item);
2408
1685
  };
2409
1686
  emitConversationTurn = (turn) => {
@@ -2483,33 +1760,6 @@ export class NodeConnector {
2483
1760
  });
2484
1761
  return this.gitExecutionService;
2485
1762
  }
2486
- publishGoalState(session, goal) {
2487
- if (goal.status !== 'active') {
2488
- this.commandStates.clear(session.id, 'goal');
2489
- this.emitWorkbenchEvent('session', {
2490
- session: this.sessionPresentationService.present(session)
2491
- });
2492
- return;
2493
- }
2494
- this.commandStates.set(session.id, {
2495
- commandId: 'goal',
2496
- active: goal.status === 'active',
2497
- label: 'Goal',
2498
- detail: goal.objective.slice(0, 500),
2499
- closable: true,
2500
- closeInput: '/goal',
2501
- goal: {
2502
- id: goal.id,
2503
- objective: goal.objective,
2504
- revision: goal.revision,
2505
- status: goal.status,
2506
- turns: goal.turns
2507
- }
2508
- });
2509
- this.emitWorkbenchEvent('session', {
2510
- session: this.sessionPresentationService.present(session)
2511
- });
2512
- }
2513
1763
  send(type, payload, replyTo) {
2514
1764
  this.controlChannel.send(type, payload, replyTo);
2515
1765
  }
@@ -2517,20 +1767,3 @@ export class NodeConnector {
2517
1767
  this.controlChannel.sendEnvelope(envelope);
2518
1768
  }
2519
1769
  }
2520
- const goalReadToolNames = new Set([
2521
- `${NODE_MCP.goal.name}.${NODE_MCP.goal.tools.get}`,
2522
- `${NODE_MCP.goal.name}_${NODE_MCP.goal.tools.get}`,
2523
- `mcp__${NODE_MCP.goal.name}__${NODE_MCP.goal.tools.get}`
2524
- ]);
2525
- function isGoalProgressItem(item) {
2526
- if (!isPlainRecord(item) || typeof item.kind !== 'string')
2527
- return false;
2528
- if (item.kind === 'command_execution' || item.kind === 'file_change')
2529
- return true;
2530
- if (item.kind !== 'tool_call' || !isPlainRecord(item.payload))
2531
- return false;
2532
- const toolName = item.payload.toolName;
2533
- return (typeof toolName === 'string' &&
2534
- !goalReadToolNames.has(toolName) &&
2535
- !(item.payload.namespace === NODE_MCP.goal.name && toolName === NODE_MCP.goal.tools.get));
2536
- }