@myagentroam/node 0.9.89 → 0.9.91

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
@@ -244,13 +244,18 @@ export class NodeConnector {
244
244
  missionRuntime = new MissionRuntime(() => undefined);
245
245
  missionRetryTimers = new Map();
246
246
  missionContinuationPending = new Map();
247
+ missionSubmissionAttempts = new Map();
248
+ missionContinuationEpochs = new Map();
249
+ missionExecutionProgress = new Set();
247
250
  missionMcpServer = new MissionMcpServer(this.missionRuntime, (sessionId) => {
248
251
  const mission = this.missionRuntime.get(sessionId);
249
252
  const session = this.runtime.getAgentSession(sessionId);
250
253
  if (mission === undefined || session === undefined)
251
254
  return;
252
- if (mission.status !== 'active')
255
+ if (mission.status !== 'active') {
256
+ this.retireMissionContinuation(sessionId);
253
257
  this.continuousRunCoordinator.removeMissionQueue(sessionId);
258
+ }
254
259
  this.publishMissionState(session, mission);
255
260
  });
256
261
  runnerUsageReader;
@@ -317,9 +322,18 @@ export class NodeConnector {
317
322
  if (pending?.mission === undefined)
318
323
  return;
319
324
  const started = this.continuousRunCoordinator.hasExecutionStarted(executionId);
320
- const blocked = this.missionRuntime.recordExecutionResult(pending.sessionId, status === 'FAILED');
321
- if (blocked) {
322
- this.clearMissionRetry(pending.sessionId);
325
+ const progressed = this.missionExecutionProgress.delete(executionId);
326
+ if (!this.missionGenerationCurrent(pending.sessionId, pending.mission))
327
+ return;
328
+ const disposition = this.missionRuntime.recordExecutionResult(pending.sessionId, {
329
+ failed: status === 'FAILED',
330
+ progressed: status !== 'SUCCEEDED' || progressed
331
+ });
332
+ if (disposition === 'blocked' || disposition === 'paused') {
333
+ if (disposition === 'blocked')
334
+ this.retireMissionContinuation(pending.sessionId);
335
+ else
336
+ this.invalidateMissionContinuation(pending.sessionId);
323
337
  this.continuousRunCoordinator.removeMissionQueue(pending.sessionId);
324
338
  const terminalMission = this.missionRuntime.get(pending.sessionId);
325
339
  const session = this.runtime.getAgentSession(pending.sessionId);
@@ -330,22 +344,14 @@ export class NodeConnector {
330
344
  if (status !== 'FAILED' || started)
331
345
  return;
332
346
  this.clearMissionRetry(pending.sessionId);
347
+ const continuationEpoch = this.missionContinuationEpochs.get(pending.sessionId) ?? 0;
333
348
  const retry = {
334
349
  sessionId: pending.sessionId,
335
350
  timer: setTimeout(() => {
336
351
  this.missionRetryTimers.delete(retry.sessionId);
337
- const mission = this.missionRuntime.get(retry.sessionId);
338
- if (mission?.status !== 'active' ||
339
- mission.id !== pending.mission?.id ||
340
- mission.revision !== pending.mission.revision)
341
- return;
342
- void this.sessionMessageService
343
- .submitMission({
352
+ void this.enqueueMissionContinuation({
344
353
  sessionId: retry.sessionId,
345
- kind: 'continue',
346
- content: this.missionRuntime.prompt(retry.sessionId, 'continue'),
347
- deliveryIntent: 'SEND',
348
- mission: { id: mission.id, revision: mission.revision },
354
+ mission: pending.mission,
349
355
  secretEnvironment: pending.secretEnvironment,
350
356
  ...(pending.personalInstructions === undefined
351
357
  ? {}
@@ -358,15 +364,17 @@ export class NodeConnector {
358
364
  : { marAgentModels: pending.marAgentModels }),
359
365
  ...(pending.marAgentRuntimeScopeId === undefined
360
366
  ? {}
361
- : { marAgentRuntimeScopeId: pending.marAgentRuntimeScopeId })
362
- })
363
- .catch(() => undefined);
367
+ : { marAgentRuntimeScopeId: pending.marAgentRuntimeScopeId }),
368
+ continuationEpoch,
369
+ deliveryIntent: 'SEND'
370
+ });
364
371
  }, 15_000)
365
372
  };
366
373
  this.missionRetryTimers.set(pending.sessionId, retry);
367
374
  retry.timer.unref();
368
375
  },
369
376
  releaseExecution: (executionId) => {
377
+ this.missionExecutionProgress.delete(executionId);
370
378
  this.missionMcpServer.revoke(executionId);
371
379
  this.continuousRunCoordinator.remove(executionId);
372
380
  }
@@ -634,6 +642,7 @@ export class NodeConnector {
634
642
  (await this.runAttachmentService.readNative(runId, imageIndex)),
635
643
  respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
636
644
  pauseQueue: (sessionId) => this.workspaceQueueWorkbenchService.pauseSessionQueue(sessionId),
645
+ runInterrupted: (sessionId) => this.clearMissionState(sessionId),
637
646
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
638
647
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
639
648
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId)
@@ -662,20 +671,19 @@ export class NodeConnector {
662
671
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
663
672
  mcps: (workspaceId) => this.database?.effectiveMcpInstallations(workspaceId) ?? [],
664
673
  retainEnvironment: (environment) => this.gitExecution().retain(environment),
674
+ missionCurrent: (input) => input.mission === undefined ||
675
+ this.missionGenerationCurrent(input.sessionId, input.mission),
665
676
  afterExecutionStarted: async (input) => {
666
677
  if (input.mission === undefined)
667
678
  return;
668
- const mission = this.missionRuntime.get(input.sessionId);
669
- if (mission?.status !== 'active' ||
670
- mission.id !== input.mission.id ||
671
- mission.revision !== input.mission.revision)
679
+ if (!this.missionGenerationCurrent(input.sessionId, input.mission))
672
680
  return;
673
681
  const updated = this.missionRuntime.recordTurn(input.sessionId);
674
682
  const session = this.runtime.getAgentSession(input.sessionId);
675
683
  const run = this.runtime.getRun(input.runId);
676
684
  await this.enqueueMissionContinuation({
677
685
  sessionId: input.sessionId,
678
- mission: { id: mission.id, revision: mission.revision },
686
+ mission: { id: input.mission.id, revision: input.mission.revision },
679
687
  ...(run?.initiatedByUserId === undefined
680
688
  ? {}
681
689
  : { initiatedByUserId: run.initiatedByUserId }),
@@ -761,8 +769,24 @@ export class NodeConnector {
761
769
  const pendingContinuation = this.missionContinuationPending.get(previousId);
762
770
  if (pendingContinuation !== undefined) {
763
771
  this.missionContinuationPending.delete(previousId);
772
+ pendingContinuation.sessionId = nextId;
764
773
  this.missionContinuationPending.set(nextId, pendingContinuation);
765
774
  }
775
+ const submissionAttempts = this.missionSubmissionAttempts.get(previousId);
776
+ if (submissionAttempts !== undefined) {
777
+ this.missionSubmissionAttempts.delete(previousId);
778
+ const currentAttempts = this.missionSubmissionAttempts.get(nextId) ?? new Set();
779
+ for (const attempt of submissionAttempts) {
780
+ attempt.sessionId = nextId;
781
+ currentAttempts.add(attempt);
782
+ }
783
+ this.missionSubmissionAttempts.set(nextId, currentAttempts);
784
+ }
785
+ const continuationEpoch = this.missionContinuationEpochs.get(previousId);
786
+ if (continuationEpoch !== undefined) {
787
+ this.missionContinuationEpochs.delete(previousId);
788
+ this.missionContinuationEpochs.set(nextId, continuationEpoch);
789
+ }
766
790
  },
767
791
  migrateQueue: (previousId, nextId) => this.continuousRunCoordinator.migrateSession(previousId, nextId),
768
792
  migrateRunnerState: (previousId, nextId) => this.runEventBridge.migrateRunnerSessionIdentity(previousId, nextId),
@@ -855,7 +879,7 @@ export class NodeConnector {
855
879
  };
856
880
  },
857
881
  mission: async ({ session, objective, initiatedByUserId, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentRuntimeScopeId }) => {
858
- this.clearMissionRetry(session.id);
882
+ this.invalidateMissionContinuation(session.id);
859
883
  const prepared = this.prepareMissionSession(session);
860
884
  const missionSession = prepared.session;
861
885
  const activeRun = this.runtime.activeSessionRun(session.id);
@@ -863,33 +887,39 @@ export class NodeConnector {
863
887
  this.continuousRunCoordinator.removeMissionQueue(session.id);
864
888
  const mission = this.missionRuntime.start(session.id, objective);
865
889
  this.publishMissionState(missionSession, mission);
866
- const result = await this.sessionMessageService.submitMission({
867
- sessionId: session.id,
868
- kind: 'start',
869
- content: this.missionRuntime.prompt(session.id, 'start'),
870
- deliveryIntent: replacing ? 'REPLACE_CURRENT' : 'SEND',
871
- mission: { id: mission.id, revision: mission.revision },
872
- ...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
873
- secretEnvironment,
874
- ...(personalInstructions === undefined ? {} : { personalInstructions }),
875
- ...(marAgentModel === undefined ? {} : { marAgentModel }),
876
- ...(marAgentModels === undefined ? {} : { marAgentModels }),
877
- ...(marAgentRuntimeScopeId === undefined ? {} : { marAgentRuntimeScopeId })
878
- });
890
+ const attempt = this.beginMissionSubmission(session.id);
891
+ const missionRef = { id: mission.id, revision: mission.revision, epoch: attempt.epoch };
892
+ let result;
893
+ try {
894
+ result = await this.sessionMessageService.submitMission({
895
+ sessionId: attempt.sessionId,
896
+ kind: 'start',
897
+ content: this.missionRuntime.prompt(attempt.sessionId, 'start'),
898
+ deliveryIntent: replacing ? 'REPLACE_CURRENT' : 'SEND',
899
+ mission: missionRef,
900
+ ...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
901
+ secretEnvironment,
902
+ ...(personalInstructions === undefined ? {} : { personalInstructions }),
903
+ ...(marAgentModel === undefined ? {} : { marAgentModel }),
904
+ ...(marAgentModels === undefined ? {} : { marAgentModels }),
905
+ ...(marAgentRuntimeScopeId === undefined ? {} : { marAgentRuntimeScopeId })
906
+ });
907
+ if (!this.missionGenerationCurrent(attempt.sessionId, missionRef)) {
908
+ this.removeStaleMissionSubmission(attempt.sessionId, result);
909
+ throw new Error('MISSION_REVISION_STALE');
910
+ }
911
+ }
912
+ finally {
913
+ this.finishMissionSubmissionAttempt(attempt);
914
+ }
879
915
  return {
880
916
  ...(isPlainRecord(result) ? result : { result }),
881
917
  missionAccessChanged: prepared.changed
882
918
  };
883
919
  },
884
920
  clearMission: (session) => {
885
- this.clearMissionRetry(session.id);
886
- this.missionRuntime.clear(session.id);
887
- this.continuousRunCoordinator.removeMissionQueue(session.id);
921
+ this.clearMissionState(session.id);
888
922
  this.finishIdleMissionRun(session.id, 'SUCCEEDED');
889
- this.commandStates.clear(session.id, 'mission');
890
- this.emitWorkbenchEvent('session', {
891
- session: this.sessionPresentationService.present(session)
892
- });
893
923
  return { session: this.sessionPresentationService.present(session), cleared: true };
894
924
  }
895
925
  });
@@ -915,34 +945,40 @@ export class NodeConnector {
915
945
  return undefined;
916
946
  const current = this.missionRuntime.get(session.id);
917
947
  if (current?.status === 'active') {
948
+ this.invalidateMissionContinuation(session.id);
949
+ const resumed = this.missionRuntime.resume(session.id);
950
+ if (resumed !== current)
951
+ this.publishMissionState(session, resumed);
918
952
  return {
919
953
  runnerContent: content,
920
- mission: { id: current.id, revision: current.revision },
954
+ mission: {
955
+ id: resumed.id,
956
+ revision: resumed.revision,
957
+ epoch: this.missionContinuationEpochs.get(session.id) ?? 0
958
+ },
921
959
  activated: false
922
960
  };
923
961
  }
962
+ this.invalidateMissionContinuation(session.id);
924
963
  const mission = this.missionRuntime.start(session.id, content);
925
964
  this.publishMissionState(session, mission);
926
965
  return {
927
966
  runnerContent: this.missionRuntime.prompt(session.id, 'start'),
928
- mission: { id: mission.id, revision: mission.revision },
967
+ mission: {
968
+ id: mission.id,
969
+ revision: mission.revision,
970
+ epoch: this.missionContinuationEpochs.get(session.id) ?? 0
971
+ },
929
972
  activated: true
930
973
  };
931
974
  },
932
- prepareMissionInsertion: (sessionId, submissionId) => this.continuousRunCoordinator.removePendingQueue(sessionId, submissionId),
933
- missionPreparationFailed: (session, mission, activated) => {
934
- if (!activated)
935
- return;
936
- const current = this.missionRuntime.get(session.id);
937
- if (current?.id !== mission.id || current.revision !== mission.revision)
938
- return;
939
- this.missionRuntime.clear(session.id);
940
- this.finishIdleMissionRun(session.id, 'FAILED');
941
- this.commandStates.set(session.id, this.pendingMissionCommandState());
942
- this.emitWorkbenchEvent('session', {
943
- session: this.sessionPresentationService.present(session)
944
- });
945
- }
975
+ prepareMissionInsertion: (sessionId, submissionId, mission) => {
976
+ if (!this.missionGenerationCurrent(sessionId, mission))
977
+ return false;
978
+ this.continuousRunCoordinator.removePendingQueue(sessionId, submissionId);
979
+ return true;
980
+ },
981
+ missionPreparationFailed: (session, mission, activated) => this.handleMissionPreparationFailed(session, mission, activated)
946
982
  });
947
983
  this.sessionCatalogService = new SessionCatalogService({
948
984
  database: () => {
@@ -1461,16 +1497,70 @@ export class NodeConnector {
1461
1497
  this.missionRetryTimers.delete(sessionId);
1462
1498
  this.missionContinuationPending.delete(sessionId);
1463
1499
  }
1500
+ invalidateMissionContinuation(sessionId) {
1501
+ this.missionContinuationEpochs.set(sessionId, (this.missionContinuationEpochs.get(sessionId) ?? 0) + 1);
1502
+ this.clearMissionRetry(sessionId);
1503
+ }
1504
+ retireMissionContinuation(sessionId) {
1505
+ this.invalidateMissionContinuation(sessionId);
1506
+ this.missionContinuationEpochs.delete(sessionId);
1507
+ }
1508
+ missionGenerationCurrent(sessionId, mission) {
1509
+ const current = this.missionRuntime.get(sessionId);
1510
+ return ((this.missionContinuationEpochs.get(sessionId) ?? 0) === mission.epoch &&
1511
+ current?.status === 'active' &&
1512
+ !current.continuationPaused &&
1513
+ current.id === mission.id &&
1514
+ current.revision === mission.revision);
1515
+ }
1516
+ beginMissionSubmission(sessionId, continuationEpoch = this.missionContinuationEpochs.get(sessionId) ?? 0) {
1517
+ const attempt = { sessionId, epoch: continuationEpoch };
1518
+ const attempts = this.missionSubmissionAttempts.get(sessionId) ?? new Set();
1519
+ attempts.add(attempt);
1520
+ this.missionSubmissionAttempts.set(sessionId, attempts);
1521
+ return attempt;
1522
+ }
1523
+ finishMissionSubmissionAttempt(attempt) {
1524
+ const attempts = this.missionSubmissionAttempts.get(attempt.sessionId);
1525
+ attempts?.delete(attempt);
1526
+ if (attempts?.size === 0)
1527
+ this.missionSubmissionAttempts.delete(attempt.sessionId);
1528
+ }
1529
+ finishMissionContinuationAttempt(attempt) {
1530
+ if (this.missionContinuationPending.get(attempt.sessionId) === attempt)
1531
+ this.missionContinuationPending.delete(attempt.sessionId);
1532
+ this.finishMissionSubmissionAttempt(attempt);
1533
+ }
1534
+ removeStaleMissionSubmission(sessionId, result) {
1535
+ if (!isPlainRecord(result))
1536
+ return;
1537
+ if (typeof result.queueItemId === 'string') {
1538
+ const item = this.runtime.getQueueItem(result.queueItemId);
1539
+ if (item?.sessionId === sessionId)
1540
+ this.continuousRunCoordinator.deleteQueueItem(item.id);
1541
+ return;
1542
+ }
1543
+ if (!isPlainRecord(result.run) || typeof result.run.id !== 'string')
1544
+ return;
1545
+ const run = this.runtime.getRun(result.run.id);
1546
+ if (run?.sessionId === sessionId && this.runtime.currentExecutionId(run.id) === undefined)
1547
+ this.runEventBridge.emitRun(run.id, 'run.completed', { code: 'MISSION_SUBMISSION_STALE' }, 'INTERRUPTED');
1548
+ }
1464
1549
  async enqueueMissionContinuation(input) {
1465
- const attemptKey = `${input.mission.id}:${input.mission.revision}`;
1466
- this.missionContinuationPending.set(input.sessionId, attemptKey);
1550
+ const attempt = this.beginMissionSubmission(input.sessionId, input.continuationEpoch);
1551
+ const generation = { ...input.mission, epoch: attempt.epoch };
1552
+ if (!this.missionGenerationCurrent(attempt.sessionId, generation)) {
1553
+ this.finishMissionSubmissionAttempt(attempt);
1554
+ return;
1555
+ }
1556
+ this.missionContinuationPending.set(input.sessionId, attempt);
1467
1557
  try {
1468
- await this.sessionMessageService.submitMission({
1469
- sessionId: input.sessionId,
1558
+ const result = await this.sessionMessageService.submitMission({
1559
+ sessionId: attempt.sessionId,
1470
1560
  kind: 'continue',
1471
- content: this.missionRuntime.prompt(input.sessionId, 'continue'),
1472
- deliveryIntent: 'QUEUE',
1473
- mission: input.mission,
1561
+ content: this.missionRuntime.prompt(attempt.sessionId, 'continue'),
1562
+ deliveryIntent: input.deliveryIntent ?? 'QUEUE',
1563
+ mission: generation,
1474
1564
  ...(input.initiatedByUserId === undefined
1475
1565
  ? {}
1476
1566
  : { initiatedByUserId: input.initiatedByUserId }),
@@ -1484,72 +1574,95 @@ export class NodeConnector {
1484
1574
  ? {}
1485
1575
  : { marAgentRuntimeScopeId: input.marAgentRuntimeScopeId })
1486
1576
  });
1487
- const current = this.missionRuntime.get(input.sessionId);
1488
- if (current?.status !== 'active' ||
1489
- current.id !== input.mission.id ||
1490
- current.revision !== input.mission.revision) {
1491
- this.continuousRunCoordinator.removeMissionQueue(input.sessionId, current?.status === 'active' ? { id: current.id, revision: current.revision } : undefined);
1492
- if (this.missionContinuationPending.get(input.sessionId) === attemptKey)
1493
- this.missionContinuationPending.delete(input.sessionId);
1577
+ if (!this.missionGenerationCurrent(attempt.sessionId, generation)) {
1578
+ this.removeStaleMissionSubmission(attempt.sessionId, result);
1579
+ this.finishMissionContinuationAttempt(attempt);
1494
1580
  return;
1495
1581
  }
1496
- this.clearMissionRetry(input.sessionId);
1582
+ const retry = this.missionRetryTimers.get(attempt.sessionId);
1583
+ if (retry !== undefined)
1584
+ clearTimeout(retry.timer);
1585
+ this.missionRetryTimers.delete(attempt.sessionId);
1586
+ this.finishMissionContinuationAttempt(attempt);
1497
1587
  }
1498
1588
  catch (error) {
1499
- const current = this.missionRuntime.get(input.sessionId);
1500
1589
  const failedAttempts = (input.failedAttempts ?? 0) + 1;
1501
1590
  nodeLog('mission.continuation.enqueue.failed', {
1502
- sessionId: input.sessionId,
1591
+ sessionId: attempt.sessionId,
1503
1592
  missionId: input.mission.id,
1504
1593
  revision: input.mission.revision,
1505
1594
  attempt: failedAttempts,
1506
1595
  code: safeErrorCode(error, 'MISSION_CONTINUATION_ENQUEUE_FAILED')
1507
1596
  });
1508
- if (current?.status !== 'active' ||
1509
- current.id !== input.mission.id ||
1510
- current.revision !== input.mission.revision) {
1511
- if (this.missionContinuationPending.get(input.sessionId) === attemptKey)
1512
- this.missionContinuationPending.delete(input.sessionId);
1597
+ if (!this.missionGenerationCurrent(attempt.sessionId, generation)) {
1598
+ this.finishMissionContinuationAttempt(attempt);
1513
1599
  return;
1514
1600
  }
1515
- const session = this.runtime.getAgentSession(input.sessionId);
1601
+ const session = this.runtime.getAgentSession(attempt.sessionId);
1516
1602
  if (failedAttempts > 3 || session === undefined) {
1517
- this.clearMissionRetry(input.sessionId);
1603
+ this.clearMissionRetry(attempt.sessionId);
1518
1604
  if (failedAttempts > 3) {
1519
- const blocked = this.missionRuntime.blockAfterRuntimeFailure(input.sessionId, input.mission.id, input.mission.revision);
1520
- this.continuousRunCoordinator.removeMissionQueue(input.sessionId);
1521
- this.finishIdleMissionRun(input.sessionId, 'FAILED');
1605
+ const blocked = this.missionRuntime.blockAfterRuntimeFailure(attempt.sessionId, input.mission.id, input.mission.revision);
1606
+ this.continuousRunCoordinator.removeMissionQueue(attempt.sessionId);
1607
+ this.missionContinuationEpochs.delete(attempt.sessionId);
1608
+ this.finishIdleMissionRun(attempt.sessionId, 'FAILED');
1522
1609
  if (session !== undefined)
1523
1610
  this.publishMissionState(session, blocked);
1524
1611
  }
1525
1612
  return;
1526
1613
  }
1527
- this.clearMissionRetry(input.sessionId);
1614
+ this.finishMissionContinuationAttempt(attempt);
1615
+ const previousRetry = this.missionRetryTimers.get(attempt.sessionId);
1616
+ if (previousRetry !== undefined)
1617
+ clearTimeout(previousRetry.timer);
1528
1618
  const retry = {
1529
- sessionId: input.sessionId,
1619
+ sessionId: attempt.sessionId,
1530
1620
  timer: setTimeout(() => {
1531
- const current = this.missionRuntime.get(retry.sessionId);
1532
- if (current?.status !== 'active' ||
1533
- current.id !== input.mission.id ||
1534
- current.revision !== input.mission.revision)
1621
+ if (!this.missionGenerationCurrent(retry.sessionId, generation))
1535
1622
  return;
1536
1623
  void this.enqueueMissionContinuation({
1537
1624
  ...input,
1538
1625
  sessionId: retry.sessionId,
1539
- failedAttempts
1626
+ failedAttempts,
1627
+ continuationEpoch: attempt.epoch
1540
1628
  });
1541
1629
  }, 15_000)
1542
1630
  };
1543
- this.missionRetryTimers.set(input.sessionId, retry);
1631
+ this.missionRetryTimers.set(attempt.sessionId, retry);
1544
1632
  retry.timer.unref();
1545
1633
  }
1546
1634
  }
1635
+ handleMissionPreparationFailed(session, mission, activated) {
1636
+ if (!activated || !this.missionGenerationCurrent(session.id, mission))
1637
+ return;
1638
+ this.missionRuntime.clear(session.id);
1639
+ this.finishIdleMissionRun(session.id, 'FAILED');
1640
+ this.commandStates.set(session.id, this.pendingMissionCommandState());
1641
+ this.emitWorkbenchEvent('session', {
1642
+ session: this.sessionPresentationService.present(session)
1643
+ });
1644
+ }
1547
1645
  finishIdleMissionRun(sessionId, status) {
1548
1646
  const run = this.runtime.activeSessionRun(sessionId);
1549
1647
  if (run === undefined || this.runtime.currentExecutionId(run.id) !== undefined)
1550
1648
  return;
1551
1649
  this.runEventBridge.emitRun(run.id, status === 'SUCCEEDED' ? 'run.completed' : 'run.failed', status === 'SUCCEEDED' ? {} : { code: 'MISSION_CONTINUATION_FAILED' }, status);
1552
1650
  }
1651
+ clearMissionState(sessionId) {
1652
+ this.retireMissionContinuation(sessionId);
1653
+ const removed = this.missionRuntime.clear(sessionId);
1654
+ this.continuousRunCoordinator.removeMissionQueue(sessionId);
1655
+ const commandState = this.commandStates.get(sessionId, 'mission');
1656
+ if (commandState !== undefined)
1657
+ this.commandStates.clear(sessionId, 'mission');
1658
+ if (!removed && commandState === undefined)
1659
+ return;
1660
+ const session = this.runtime.getAgentSession(sessionId);
1661
+ if (session !== undefined)
1662
+ this.emitWorkbenchEvent('session', {
1663
+ session: this.sessionPresentationService.present(session)
1664
+ });
1665
+ }
1553
1666
  prepareMissionSession(session) {
1554
1667
  const access = missionUnrestrictedAccess(session.runner);
1555
1668
  const changed = session.access !== access;
@@ -1604,6 +1717,8 @@ export class NodeConnector {
1604
1717
  clearTimeout(retry.timer);
1605
1718
  this.missionRetryTimers.clear();
1606
1719
  this.missionContinuationPending.clear();
1720
+ this.missionSubmissionAttempts.clear();
1721
+ this.missionContinuationEpochs.clear();
1607
1722
  this.runEventService.clear();
1608
1723
  this.sessionContextService.dispose();
1609
1724
  for (const run of this.claudeClient.execution.runs.values())
@@ -1655,6 +1770,9 @@ export class NodeConnector {
1655
1770
  emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
1656
1771
  emitConversationItem = (runId, item) => {
1657
1772
  this.runEventService.flushPendingText(runId);
1773
+ if (this.continuousRunCoordinator.pending(runId)?.mission !== undefined &&
1774
+ isMissionProgressItem(item))
1775
+ this.missionExecutionProgress.add(runId);
1658
1776
  const itemId = isPlainRecord(item) && typeof item.itemId === 'string' ? item.itemId : undefined;
1659
1777
  const itemKind = isPlainRecord(item) && typeof item.kind === 'string' ? item.kind : undefined;
1660
1778
  const turn = this.runtime.conversationTurnForRun(runId);
@@ -1832,3 +1950,13 @@ export class NodeConnector {
1832
1950
  this.controlChannel.sendEnvelope(envelope);
1833
1951
  }
1834
1952
  }
1953
+ function isMissionProgressItem(item) {
1954
+ if (!isPlainRecord(item) || typeof item.kind !== 'string')
1955
+ return false;
1956
+ if (item.kind === 'command_execution' || item.kind === 'file_change')
1957
+ return true;
1958
+ if (item.kind !== 'tool_call' || !isPlainRecord(item.payload))
1959
+ return false;
1960
+ const toolName = item.payload.toolName;
1961
+ return typeof toolName === 'string' && !toolName.endsWith('mar_get_mission');
1962
+ }
@@ -30,7 +30,7 @@ export class MarAgentManagedRunController {
30
30
  constructor(runner, host, options = {}) {
31
31
  this.runner = runner;
32
32
  this.host = host;
33
- this.#resourceIdleTtlMs = options.resourceIdleTtlMs ?? 60_000;
33
+ this.#resourceIdleTtlMs = options.resourceIdleTtlMs ?? 10 * 60_000;
34
34
  this.#createAgent = options.createAgent ?? createMarAgent;
35
35
  this.#modelFactory = options.modelFactory ?? {
36
36
  create: (configurations) => configurations.map((configuration) => {
@@ -241,6 +241,8 @@ export class MarAgentManagedRunController {
241
241
  });
242
242
  this.host.emit(input.runId, 'run.started', {}, 'STARTING');
243
243
  let runtime;
244
+ let executionMcpLease;
245
+ let executionMcpBinding;
244
246
  try {
245
247
  const sessionKey = input.externalSessionId ?? input.sessionId;
246
248
  const acquisitionKeys = [...new Set([sessionKey, input.sessionId])];
@@ -268,6 +270,17 @@ export class MarAgentManagedRunController {
268
270
  throw new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE');
269
271
  runtime.activeRunId = input.runId;
270
272
  runtime.activeAccess = input.access;
273
+ const executionMcpServers = input.mcpServers.filter((server) => server.lifecycle === 'EXECUTION');
274
+ if (executionMcpServers.length > 0) {
275
+ executionMcpLease = await this.#mcpClients.acquire(executionMcpServers, input.cwd, {
276
+ reusable: false
277
+ });
278
+ const mcp = executionMcpLease.pool;
279
+ executionMcpBinding = runtime.host.bindExecutionMcp({
280
+ tools: mcp.definitions(),
281
+ call: async (call) => mcp.call(call.name, call.arguments)
282
+ });
283
+ }
271
284
  const session = runtime.session;
272
285
  const reasoningEffort = input.effort
273
286
  ? marAgentReasoningEffortSchema.parse(input.effort)
@@ -332,6 +345,8 @@ export class MarAgentManagedRunController {
332
345
  this.#normalizers.delete(input.runId);
333
346
  this.host.active.delete(input.runId);
334
347
  this.#executions.delete(input.runId);
348
+ runtime?.host.unbindExecutionMcp(executionMcpBinding);
349
+ await executionMcpLease?.release().catch(() => undefined);
335
350
  if (runtime !== undefined)
336
351
  await this.#finishRuntimeExecution(input.runId, runtime).catch(() => undefined);
337
352
  }
@@ -341,20 +356,14 @@ export class MarAgentManagedRunController {
341
356
  delete runtime.activeRunId;
342
357
  delete runtime.activeAccess;
343
358
  }
344
- if (runtime.retainable)
345
- this.#scheduleRuntimeRelease(runtime);
346
- else
347
- void this.#releaseRuntime(runtime).catch(() => undefined);
359
+ this.#scheduleRuntimeRelease(runtime);
348
360
  }
349
361
  async #finishRuntimeExecution(runId, runtime) {
350
362
  if (runtime.activeRunId === runId) {
351
363
  delete runtime.activeRunId;
352
364
  delete runtime.activeAccess;
353
365
  }
354
- if (runtime.retainable)
355
- this.#scheduleRuntimeRelease(runtime);
356
- else
357
- await this.#releaseRuntime(runtime);
366
+ this.#scheduleRuntimeRelease(runtime);
358
367
  }
359
368
  async #acquireRuntime(input) {
360
369
  if (this.#disposed)
@@ -424,10 +433,8 @@ export class MarAgentManagedRunController {
424
433
  async #createRuntime(input, sessionKey, fingerprint, hostConfiguration) {
425
434
  const dynamicCredentials = input.models.some((model) => model.credential?.type === 'RUNTIME_TICKET' ||
426
435
  model.imageGeneration?.credential?.type === 'RUNTIME_TICKET');
427
- const retainable = !executionScopedMcp(input.mcpServers);
428
- const mcpLease = await this.#mcpClients.acquire(input.mcpServers, input.cwd, {
429
- reusable: retainable
430
- });
436
+ const sharedMcpServers = input.mcpServers.filter((server) => server.lifecycle !== 'EXECUTION');
437
+ const mcpLease = await this.#mcpClients.acquire(sharedMcpServers, input.cwd);
431
438
  let agent;
432
439
  let runtime;
433
440
  try {
@@ -473,8 +480,8 @@ export class MarAgentManagedRunController {
473
480
  fingerprint,
474
481
  agent,
475
482
  session,
483
+ host: localHost,
476
484
  mcpLease,
477
- retainable,
478
485
  dynamicCredentials,
479
486
  released: false
480
487
  };
@@ -672,7 +679,7 @@ function sessionRuntimeFingerprint(input, hostConfiguration) {
672
679
  defaultModelId: input.model.id,
673
680
  models: input.models.map(runtimeModelFingerprint),
674
681
  personalInstructions: input.personalInstructions ?? '',
675
- mcpServers: input.mcpServers,
682
+ mcpServers: input.mcpServers.filter((server) => server.lifecycle !== 'EXECUTION'),
676
683
  environment: Object.entries(hostConfiguration.environment)
677
684
  .filter((entry) => entry[1] !== undefined)
678
685
  .sort(([left], [right]) => left.localeCompare(right)),
@@ -683,9 +690,6 @@ function sessionRuntimeFingerprint(input, hostConfiguration) {
683
690
  export function allowsUntrustedWebFetchTls(environment) {
684
691
  return environment['MAR_AGENT_WEB_FETCH_ALLOW_UNTRUSTED_TLS'] !== '0';
685
692
  }
686
- function executionScopedMcp(servers) {
687
- return servers.some((server) => server.lifecycle === 'EXECUTION');
688
- }
689
693
  async function resolveMarAgentHostRuntimeConfiguration(secretEnvironment = {}) {
690
694
  const environment = { ...process.env, ...secretEnvironment };
691
695
  const ripgrep = await availableBundledRipgrep();
@@ -242,6 +242,11 @@ export class ContinuousRunCoordinator {
242
242
  const item = this.options.runtime.getQueueItem(itemId);
243
243
  if (item === undefined)
244
244
  return;
245
+ if (!this.missionCurrent(pending)) {
246
+ this.deleteQueueItem(item.id);
247
+ this.schedule(sessionId);
248
+ return;
249
+ }
245
250
  if (this.options.runtime.claimNextQueueItem(sessionId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths)) === undefined)
246
251
  return;
247
252
  const execution = this.options.runtime.createExecution(active.id);
@@ -257,6 +262,10 @@ export class ContinuousRunCoordinator {
257
262
  this.options.emitConversation(turn);
258
263
  }
259
264
  else if (executionId === undefined && pending !== undefined) {
265
+ if (!this.missionCurrent(pending)) {
266
+ this.rejectStaleMission(active.id);
267
+ return;
268
+ }
260
269
  const execution = this.options.runtime.createExecution(active.id);
261
270
  executionId = execution.id;
262
271
  this.preparedInputs.delete(active.id);
@@ -268,11 +277,20 @@ export class ContinuousRunCoordinator {
268
277
  return;
269
278
  if (this.dispatchedExecutions.has(executionId))
270
279
  return;
280
+ if (!this.missionCurrent(pending)) {
281
+ this.rejectStaleMission(executionId);
282
+ return;
283
+ }
271
284
  this.dispatchedExecutions.add(executionId);
272
285
  try {
273
286
  const missionMcp = pending.mission === undefined
274
287
  ? undefined
275
288
  : await this.options.missionMcp?.(pending);
289
+ if (!this.missionCurrent(pending)) {
290
+ this.dispatchedExecutions.delete(executionId);
291
+ this.rejectStaleMission(executionId);
292
+ return;
293
+ }
276
294
  this.options.control('run.start', {
277
295
  runId: executionId,
278
296
  sessionId: pending.sessionId,
@@ -395,6 +413,11 @@ export class ContinuousRunCoordinator {
395
413
  const item = this.options.runtime.getQueueItem(itemId);
396
414
  if (item === undefined)
397
415
  throw new Error('QUEUE_ITEM_INVALID');
416
+ if (!this.missionCurrent(pending)) {
417
+ this.deleteQueueItem(item.id);
418
+ this.startIdleQueue(sessionId);
419
+ return;
420
+ }
398
421
  const { run, turn } = this.options.runtime.startRunFromQueueItem(itemId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths));
399
422
  this.preparedInputs.delete(itemId);
400
423
  this.preparedInputs.set(run.id, { ...pending, runId: run.id });
@@ -410,6 +433,14 @@ export class ContinuousRunCoordinator {
410
433
  throw new Error('QUEUE_INPUT_LOST');
411
434
  return item;
412
435
  }
436
+ missionCurrent(pending) {
437
+ return (pending.mission === undefined ||
438
+ this.options.missionCurrent?.(pending) !== false);
439
+ }
440
+ rejectStaleMission(runId) {
441
+ this.remove(runId);
442
+ this.options.emitRun(runId, 'run.completed', { code: 'MISSION_SUBMISSION_STALE' }, 'INTERRUPTED');
443
+ }
413
444
  isMessageRun(run) {
414
445
  return this.options.runtime.findMessageRun(run.sessionId, run.clientMessageId)?.id === run.id;
415
446
  }
@@ -62,7 +62,9 @@ export class MissionRuntime {
62
62
  status: 'active',
63
63
  turns: 0,
64
64
  retryRequired: false,
65
- retryStartedAtTurn: null
65
+ retryStartedAtTurn: null,
66
+ consecutiveNoProgressTurns: 0,
67
+ continuationPaused: false
66
68
  };
67
69
  this.missions.set(sessionId, mission);
68
70
  this.executionFailures.delete(sessionId);
@@ -77,22 +79,70 @@ export class MissionRuntime {
77
79
  this.missions.set(sessionId, mission);
78
80
  return mission;
79
81
  }
80
- recordExecutionResult(sessionId, failed) {
82
+ recordExecutionResult(sessionId, input) {
81
83
  const current = this.missions.get(sessionId);
82
84
  if (current?.status !== 'active')
83
- return false;
84
- if (!failed) {
85
+ return 'continue';
86
+ if (!input.failed && !input.progressed) {
85
87
  this.executionFailures.delete(sessionId);
86
- return false;
88
+ const consecutiveNoProgressTurns = current.consecutiveNoProgressTurns + 1;
89
+ const paused = consecutiveNoProgressTurns >= 3;
90
+ const mission = {
91
+ ...current,
92
+ consecutiveNoProgressTurns,
93
+ continuationPaused: paused
94
+ };
95
+ this.missions.set(sessionId, mission);
96
+ this.changed(mission);
97
+ return paused ? 'paused' : 'continue';
98
+ }
99
+ if (!input.failed) {
100
+ this.executionFailures.delete(sessionId);
101
+ if (current.consecutiveNoProgressTurns > 0 || current.continuationPaused) {
102
+ const mission = {
103
+ ...current,
104
+ consecutiveNoProgressTurns: 0,
105
+ continuationPaused: false
106
+ };
107
+ this.missions.set(sessionId, mission);
108
+ this.changed(mission);
109
+ }
110
+ return 'continue';
111
+ }
112
+ if (current.consecutiveNoProgressTurns > 0 || current.continuationPaused) {
113
+ const mission = {
114
+ ...current,
115
+ consecutiveNoProgressTurns: 0,
116
+ continuationPaused: false
117
+ };
118
+ this.missions.set(sessionId, mission);
87
119
  }
88
120
  const failures = (this.executionFailures.get(sessionId) ?? 0) + 1;
89
121
  this.executionFailures.set(sessionId, failures);
90
122
  if (failures <= 3)
91
- return false;
92
- const blocked = { ...current, status: 'blocked' };
123
+ return 'continue';
124
+ const blocked = {
125
+ ...(this.missions.get(sessionId) ?? current),
126
+ status: 'blocked'
127
+ };
93
128
  this.missions.set(sessionId, blocked);
94
129
  this.changed(blocked);
95
- return true;
130
+ return 'blocked';
131
+ }
132
+ resume(sessionId) {
133
+ const current = this.missions.get(sessionId);
134
+ if (current?.status !== 'active')
135
+ return current;
136
+ if (!current.continuationPaused && current.consecutiveNoProgressTurns === 0)
137
+ return current;
138
+ const mission = {
139
+ ...current,
140
+ consecutiveNoProgressTurns: 0,
141
+ continuationPaused: false
142
+ };
143
+ this.missions.set(sessionId, mission);
144
+ this.changed(mission);
145
+ return mission;
96
146
  }
97
147
  blockAfterRuntimeFailure(sessionId, missionId, revision) {
98
148
  const current = this.requireCurrent(sessionId, missionId, revision);
@@ -137,7 +187,7 @@ export class MissionRuntime {
137
187
  if (mission?.status !== 'active')
138
188
  throw new Error('MISSION_NOT_ACTIVE');
139
189
  const heading = kind === 'start' ? 'A Mission has started or changed.' : 'Continue the active Mission.';
140
- return `${START_OPEN}\n${heading}\n\nOriginal objective:\n${mission.objective}\n\nMission rules:\n- The Mission persists across turns; ending this turn does not end it.\n- Do not narrow or rewrite the original objective to finish early. Continue making concrete progress while it is incomplete.\n- Before completion, review the full original objective and verify it with actual evidence such as code, tests, commands, or runtime results.\n- Call mar_update_mission with status=completed only after every part is complete and verified.\n- A single failed command, test, or edit is not a blocker. Analyze it, adapt, and retry.\n- Request blocked only after the same blocker has persisted for three Mission turns. One additional real retry is required before blocked can be accepted.\n- Use mar_get_mission whenever you need to reread the authoritative objective or status.\n${START_CLOSE}`;
190
+ return `${START_OPEN}\n${heading}\n\nOriginal objective:\n${mission.objective}\n\nContinue executing this Mission across turns:\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_mission with status=completed.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_mission only when you need to reread the authoritative objective or status.\n${START_CLOSE}`;
141
191
  }
142
192
  requireCurrent(sessionId, missionId, revision) {
143
193
  const current = this.missions.get(sessionId);
@@ -55,10 +55,11 @@ export class RunWorkbenchService {
55
55
  const run = this.options.runtime.activeSessionRun(session.id);
56
56
  if (run === undefined || isTerminalRunStatus(run.status))
57
57
  throw new Error('RUN_NOT_FOUND');
58
- return this.cancelOrInterrupt('run.interrupt', {
58
+ const result = this.cancelOrInterrupt('run.interrupt', {
59
59
  runId: run.id,
60
60
  __requestUserId: input.__requestUserId
61
61
  });
62
+ return result;
62
63
  }
63
64
  cancelOrInterrupt(operation, input) {
64
65
  const run = this.requireOwnedRun(input.runId, input.__requestUserId);
@@ -66,14 +67,19 @@ export class RunWorkbenchService {
66
67
  const executionId = this.options.runtime.currentExecutionId(run.id);
67
68
  const turn = this.options.runtime.conversationTurnForRun(run.id);
68
69
  const controlRunId = executionId ?? (turn === undefined || turn.userItemId === null ? run.id : undefined);
69
- this.options.pauseQueue(run.sessionId);
70
- if (controlRunId === undefined) {
71
- this.options.markInterrupted(run.id);
72
- this.options.emitRun(run.id, 'run.interrupted', { code: 'USER_INTERRUPTED' }, 'INTERRUPTED');
70
+ try {
71
+ this.options.pauseQueue(run.sessionId);
72
+ if (controlRunId === undefined) {
73
+ this.options.markInterrupted(run.id);
74
+ this.options.emitRun(run.id, 'run.interrupted', { code: 'USER_INTERRUPTED' }, 'INTERRUPTED');
75
+ }
76
+ else {
77
+ this.options.markInterrupted(controlRunId);
78
+ this.options.control(operation, { runId: controlRunId });
79
+ }
73
80
  }
74
- else {
75
- this.options.markInterrupted(controlRunId);
76
- this.options.control(operation, { runId: controlRunId });
81
+ finally {
82
+ this.options.runInterrupted(run.sessionId);
77
83
  }
78
84
  return { run: next };
79
85
  }
@@ -168,8 +168,9 @@ export class SessionMessageService {
168
168
  ...(runner.serviceTier() === 'fast' ? { serviceTier: 'fast' } : {})
169
169
  });
170
170
  this.options.attachments.cache(submissionId, imageAttachments);
171
- if (missionMessage !== undefined)
172
- this.options.prepareMissionInsertion(session.id, submissionId);
171
+ if (missionMessage !== undefined &&
172
+ !this.options.prepareMissionInsertion(session.id, submissionId, missionMessage.mission))
173
+ throw new Error('MISSION_REVISION_STALE');
173
174
  const delivery = this.options.coordinator.settle({
174
175
  submissionId,
175
176
  intent,
@@ -269,5 +270,6 @@ function isMissionRef(value) {
269
270
  return (value !== null &&
270
271
  typeof value === 'object' &&
271
272
  typeof value.id === 'string' &&
272
- Number.isInteger(value.revision));
273
+ Number.isInteger(value.revision) &&
274
+ Number.isInteger(value.epoch));
273
275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.89",
3
+ "version": "0.9.91",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  "node-pty": "1.1.0",
29
29
  "ws": "^8.21.3",
30
30
  "zod": "4.4.3",
31
- "@myagentroam/agent": "0.9.89",
32
- "@myagentroam/protocol": "0.9.89"
31
+ "@myagentroam/agent": "0.9.91",
32
+ "@myagentroam/protocol": "0.9.91"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/ws": "^8.18.1"