@myagentroam/node 0.10.61 → 0.10.62

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,7 +895,7 @@ 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
  }),
996
901
  execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
@@ -1026,83 +931,9 @@ export class NodeConnector {
1026
931
  emitSession: (session) => this.emitSessionWorkbenchEvent({
1027
932
  session: this.sessionPresentationService.present(session)
1028
933
  }),
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
- }
934
+ beginGoal: (session) => this.goalCoordinator.begin(session),
935
+ goal: (input) => this.goalCoordinator.start(input),
936
+ clearGoal: (session) => this.goalCoordinator.clear(session)
1106
937
  });
1107
938
  this.sessionMessageService = new SessionMessageService({
1108
939
  runtime: this.runtime,
@@ -1121,53 +952,9 @@ export class NodeConnector {
1121
952
  const session = this.runtime.getAgentSession(sessionId);
1122
953
  return session?.channelToken ?? undefined;
1123
954
  },
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)
955
+ goalMessage: (session, content, intent) => this.goalCoordinator.goalMessage(session, content, intent),
956
+ prepareGoalInsertion: (sessionId, submissionId, goal) => this.goalCoordinator.prepareInsertion(sessionId, submissionId, goal),
957
+ goalPreparationFailed: (session, goal, activated) => this.goalCoordinator.handleGoalPreparationFailed(session, goal, activated)
1171
958
  });
1172
959
  const inputState = (entry, message) => this.marAgentClient.restartInputState(entry.session.id, message.clientMessageId);
1173
960
  const restorer = new RestartSessionRestorer({
@@ -1180,7 +967,8 @@ export class NodeConnector {
1180
967
  authorize: (request) => this.restartAuthorization.authorize(request),
1181
968
  inputState,
1182
969
  withGrant: (context, work) => this.modelUsage.withGrant(context.usageGrant, work),
1183
- publishGoal: (session, goal) => this.publishGoalState(session, goal),
970
+ publishGoal: (session, goal) => this.goalCoordinator.publishGoalState(session, goal),
971
+ goalReference: (sessionId) => this.goalCoordinator.reference(sessionId),
1184
972
  publishQueue: (session) => this.continuousRunCoordinator.publish(session.id)
1185
973
  });
1186
974
  this.restartHandoff = new PlannedRestartHandoff({
@@ -1284,7 +1072,7 @@ export class NodeConnector {
1284
1072
  runtime: this.runtime,
1285
1073
  state: this.runState,
1286
1074
  emitRun: (runId, type, payload, status) => this.runEventBridge.emitRun(runId, type, payload, status),
1287
- publishApproval: (input) => this.publishApprovalForRun(input),
1075
+ publishApproval: (input) => this.runnerInteractionService.publishApproval(input),
1288
1076
  disconnect: () => this.controlChannel.close(4010, 'FAKE_RUNNER_DISCONNECT')
1289
1077
  });
1290
1078
  this.fakeRunner?.bindControl({
@@ -1437,7 +1225,8 @@ export class NodeConnector {
1437
1225
  heartbeatMs: HEARTBEAT_MS,
1438
1226
  refreshMs: CAPABILITY_REFRESH_MS,
1439
1227
  onRegistered: async () => {
1440
- await this.initializeConversationHosting();
1228
+ await this.conversationHosting.initialize();
1229
+ this.capabilities = await this.conversationHosting.project(this.capabilities);
1441
1230
  this.computerUseEnablement.initialize();
1442
1231
  },
1443
1232
  onConnected: () => {
@@ -1499,7 +1288,11 @@ export class NodeConnector {
1499
1288
  }));
1500
1289
  return { runId: result.run.id };
1501
1290
  },
1502
- notify: (task, occurrenceId, code, status) => this.emitScheduledNotification(task, occurrenceId, code, status)
1291
+ sessionTitle: (sessionId) => {
1292
+ const session = this.runtime.getAgentSession(sessionId);
1293
+ return session?.customTitle ?? session?.runnerTitle ?? sessionId;
1294
+ },
1295
+ emitNotification: (payload) => this.emitWorkbenchEvent('conversation', payload)
1503
1296
  });
1504
1297
  this.runnerService = new RunnerService(requireDatabase, () => this.capabilities, this.runners, this.runnerUsageReader);
1505
1298
  this.workbenchManifestService = new WorkbenchManifestService({
@@ -1588,21 +1381,21 @@ export class NodeConnector {
1588
1381
  this.operationRouter.registerAll(this.promptCompletion.operations());
1589
1382
  for (const operation of CONVERSATION_TEMPLATE_OPERATIONS)
1590
1383
  this.operationRouter.register(operation, (data) => {
1591
- const handler = this.conversationTemplateOperations?.[operation];
1384
+ const handler = this.conversationHosting.handler(operation);
1592
1385
  if (handler === undefined)
1593
1386
  throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1594
1387
  return handler(data);
1595
1388
  });
1596
1389
  for (const operation of HOSTED_CONVERSATION_OPERATIONS)
1597
1390
  this.operationRouter.register(operation, (data) => {
1598
- const handler = this.hostedConversationOperations?.[operation];
1391
+ const handler = this.conversationHosting.handler(operation);
1599
1392
  if (handler === undefined)
1600
1393
  throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1601
1394
  return handler(data);
1602
1395
  });
1603
1396
  for (const operation of CONVERSATION_HOSTING_MAINTENANCE_OPERATIONS)
1604
1397
  this.operationRouter.register(operation, (data) => {
1605
- const handler = this.conversationHostingMaintenanceOperations?.[operation];
1398
+ const handler = this.conversationHosting.handler(operation);
1606
1399
  if (handler === undefined)
1607
1400
  throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
1608
1401
  return handler(data);
@@ -1630,7 +1423,7 @@ export class NodeConnector {
1630
1423
  session: this.sessionPresentationService.present(session)
1631
1424
  }),
1632
1425
  changed: () => this.scheduledSessions.changed(),
1633
- proposed: (sessionId, proposalId) => this.emitScheduledProposalNotification(sessionId, proposalId)
1426
+ proposed: (sessionId, proposalId) => this.scheduledSessions.proposalCreated(sessionId, proposalId)
1634
1427
  });
1635
1428
  this.operationRouter.registerAll(this.sessionScheduleService.operations());
1636
1429
  this.operationRouter.registerAll(this.sessionMessageService.operations());
@@ -1645,8 +1438,11 @@ export class NodeConnector {
1645
1438
  configPath: this.configPath,
1646
1439
  platform: () => this.capabilities.platform,
1647
1440
  capability: () => this.capabilities.conversationHosting,
1648
- initialize: () => this.initializeConversationHosting(),
1649
- prepareForDisable: () => this.hostedConversationService?.prepareForDisable() ?? (() => { }),
1441
+ initialize: async () => {
1442
+ await this.conversationHosting.initialize();
1443
+ this.capabilities = await this.conversationHosting.project(this.capabilities);
1444
+ },
1445
+ prepareForDisable: () => this.conversationHosting.prepareForDisable(),
1650
1446
  publishCapabilities: () => this.send('capabilities', this.capabilities)
1651
1447
  }).operations());
1652
1448
  this.operationRouter.registerAll(this.computerUseEnablement.operations());
@@ -1670,147 +1466,12 @@ export class NodeConnector {
1670
1466
  binaryTerminal: true
1671
1467
  }));
1672
1468
  }
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
1469
  projectComputerUseCapability(capabilities) {
1791
1470
  return {
1792
1471
  ...capabilities,
1793
1472
  computerUse: this.computerUseEnablement.capability()
1794
1473
  };
1795
1474
  }
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
1475
  async start() {
1815
1476
  if (this.stopping !== undefined)
1816
1477
  await this.stopping;
@@ -1837,7 +1498,8 @@ export class NodeConnector {
1837
1498
  await this.restartHandoff.load();
1838
1499
  this.scheduledSessions.recoverInterrupted();
1839
1500
  this.scheduledSessions.start();
1840
- await this.initializeConversationHosting();
1501
+ await this.conversationHosting.initialize();
1502
+ this.capabilities = await this.conversationHosting.project(this.capabilities);
1841
1503
  this.computerUseEnablement.initialize();
1842
1504
  this.directTransferService ??= await this.createDirectTransferService();
1843
1505
  this.connectionLifecycle.start();
@@ -1914,287 +1576,12 @@ export class NodeConnector {
1914
1576
  };
1915
1577
  return this.directTransferService?.handleTerminal(command) ?? false;
1916
1578
  }
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
1579
  stop(options = {}) {
2194
1580
  if (this.stopping !== undefined)
2195
1581
  return this.stopping;
2196
1582
  this.stopped = true;
2197
1583
  this.continuousRunCoordinator.suspend();
1584
+ this.goalCoordinator.freeze();
2198
1585
  this.scheduledSessions.stop();
2199
1586
  this.stopping = (async () => {
2200
1587
  let snapshot;
@@ -2219,43 +1606,18 @@ export class NodeConnector {
2219
1606
  this.recentRunProjectionService.dispose();
2220
1607
  this.upgradeCoordinator.stop();
2221
1608
  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
1609
  this.nativeSessionWatchService.clear();
2233
1610
  this.promptCompletion.dispose();
2234
1611
  this.sessionIdentityService.dispose();
2235
1612
  this.sessionLifecycleService.clear();
2236
1613
  this.continuousRunCoordinator.clear();
2237
1614
  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();
1615
+ this.goalCoordinator.dispose();
2244
1616
  this.runEventService.clear();
2245
1617
  this.sessionContextService.dispose();
2246
- for (const run of this.claudeClient.execution.runs.values())
2247
- run.handle.cancel();
2248
- this.claudeClient.execution.runs.clear();
2249
1618
  this.runAttachmentService.dispose();
2250
1619
  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;
1620
+ const conversationHostingDrain = this.conversationHosting.dispose();
2259
1621
  const uploadDrain = this.workspaceUploadService.shutdown();
2260
1622
  this.workspaceFileService.dispose();
2261
1623
  this.workspaceWatchService.clearAll();
@@ -2271,7 +1633,7 @@ export class NodeConnector {
2271
1633
  this.terminalChannel.stop();
2272
1634
  this.runtimeCredentials.close();
2273
1635
  this.chatgptLogins.close();
2274
- this.scheduledSessions.stop();
1636
+ const scheduledDrain = this.scheduledSessions.stop();
2275
1637
  this.scheduledRunAuthorization.close();
2276
1638
  this.restartAuthorization.close();
2277
1639
  this.mailNotificationControl.close();
@@ -2281,8 +1643,8 @@ export class NodeConnector {
2281
1643
  this.directTransferService = undefined;
2282
1644
  this.controlChannel.stop();
2283
1645
  const database = this.database;
2284
- this.database = undefined;
2285
1646
  const drains = Promise.allSettled([
1647
+ scheduledDrain,
2286
1648
  marAgentDrain,
2287
1649
  runnerDrain,
2288
1650
  terminalDrain,
@@ -2295,11 +1657,11 @@ export class NodeConnector {
2295
1657
  return (async () => {
2296
1658
  let failed;
2297
1659
  try {
2298
- await conversationHostingMaintenanceDrain;
1660
+ await conversationHostingDrain;
2299
1661
  }
2300
1662
  finally {
2301
- await hostedConversationRuntime?.dispose().catch(() => undefined);
2302
1663
  const results = await drains;
1664
+ this.database = undefined;
2303
1665
  await this.gitExecutionService?.close();
2304
1666
  this.usageOutbox?.disconnected();
2305
1667
  this.usageOutbox = undefined;
@@ -2313,97 +1675,9 @@ export class NodeConnector {
2313
1675
  }
2314
1676
  /** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
2315
1677
  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
1678
  emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
2403
1679
  emitConversationItem = (runId, item) => {
2404
- if (this.continuousRunCoordinator.pending(runId)?.goal !== undefined &&
2405
- isGoalProgressItem(item))
2406
- this.goalExecutionProgress.add(runId);
1680
+ this.goalCoordinator.executionProgress(runId, item);
2407
1681
  this.runEventBridge.emitConversationItem(runId, item);
2408
1682
  };
2409
1683
  emitConversationTurn = (turn) => {
@@ -2483,33 +1757,6 @@ export class NodeConnector {
2483
1757
  });
2484
1758
  return this.gitExecutionService;
2485
1759
  }
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
1760
  send(type, payload, replyTo) {
2514
1761
  this.controlChannel.send(type, payload, replyTo);
2515
1762
  }
@@ -2517,20 +1764,3 @@ export class NodeConnector {
2517
1764
  this.controlChannel.sendEnvelope(envelope);
2518
1765
  }
2519
1766
  }
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
- }