@zq-silk/yui 0.13.3 → 0.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +8 -8
  2. package/dist/cli/commandCatalog.js +22 -21
  3. package/dist/cli/interactionPolicy.js +0 -14
  4. package/dist/cli.js +42 -0
  5. package/dist/commands/executionAuditCommands.js +1 -1
  6. package/dist/commands/taskCommands.js +60 -326
  7. package/dist/commands/taskContextCommand.js +3 -14
  8. package/dist/commands/taskExecutionCommands.js +254 -0
  9. package/dist/commands/taskNextActionCommand.js +1 -3
  10. package/dist/commands/taskOverviewCommand.js +9 -2
  11. package/dist/commands/taskRoleRuntimeStatus.js +2 -25
  12. package/dist/controller/agentRuntimeObserver.js +4 -2
  13. package/dist/controller/clientRuntime.js +45 -2
  14. package/dist/controller/controller.js +6 -3
  15. package/dist/controller/fileSchedulerStoreAdapter.js +70 -188
  16. package/dist/controller/jobControl.js +3 -2
  17. package/dist/controller/runtime.js +6 -33
  18. package/dist/controller/runtimeEventProcessor.js +8 -4
  19. package/dist/controller/runtimeHookRunFence.js +4 -10
  20. package/dist/execution/executionHealth.js +8 -16
  21. package/dist/executor/agentExecutor.js +13 -14
  22. package/dist/executor/fileRoleLaunchPlanner.js +9 -16
  23. package/dist/lifecycle/exactRunTerminalization.js +24 -322
  24. package/dist/repository/taskWorkspaceCoordinator.js +0 -9
  25. package/dist/runtime/agentHost.js +20 -80
  26. package/dist/runtime/exactControlPlane.js +15 -9
  27. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  28. package/dist/runtime/providerRecoveryDecision.js +1 -1
  29. package/dist/runtime/providerRuntimeIdentity.js +25 -15
  30. package/dist/scheduler/activeRoleRunDelivery.js +1 -19
  31. package/dist/scheduler/leaderWakeupProcessor.js +12 -63
  32. package/dist/scheduler/ports.js +3 -2
  33. package/dist/scheduler/roleRunLiveness.js +4 -1
  34. package/dist/scheduler/roleRunStall.js +0 -2
  35. package/dist/scheduler/taskExecutionProjection.js +18 -1
  36. package/dist/scheduler/wakeupQueue.js +2 -1
  37. package/dist/storage/migration/productionRegistry.js +65 -0
  38. package/dist/storage/sqliteStore.js +10 -2
  39. package/dist/storage/taskStore.js +11 -3
  40. package/dist/task/completionReadiness.js +0 -67
  41. package/dist/task/nextAction.js +16 -32
  42. package/dist/task/task.js +38 -3
  43. package/dist/web/assets/client/i18n.js +0 -4
  44. package/dist/web/assets/client/view.js +0 -18
  45. package/dist/web/webSnapshot.js +7 -13
  46. package/package.json +1 -1
  47. package/dist/run/recoveryProjection.js +0 -252
  48. package/dist/runtime/conversationSwitch.js +0 -277
@@ -1,8 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, replaceTaskRoleAgentSessionForConversationSwitch, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, stopTaskRoleRuntimeAfterPhysicalExit, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
3
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, stopTaskRoleRuntimeAfterPhysicalExit, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
4
  import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurnSubmission, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
5
- import { CONVERSATION_SWITCH_DETACHED_EVENT, CONVERSATION_SWITCH_RESOLVED_EVENT, conversationDetachmentBasis, conversationReplacementBasis, pendingConversationSwitch, roleSessionDispatchModeWithConversationSwitch } from "../runtime/conversationSwitch.js";
6
5
  import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
7
6
  import { hasRecentTurnId } from "../executor/turnCompletion.js";
8
7
  import { createTaskEvent } from "../event/taskEvent.js";
@@ -29,7 +28,7 @@ import { wakeReason } from "../scheduler/wakeReason.js";
29
28
  import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_DIAGNOSTIC_FINISHED_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
30
29
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
31
30
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
32
- import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup, updateWorkItemStatus, workItemOwnsUnresolvedExecutionLane } from "../workItem/workItem.js";
31
+ import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup } from "../workItem/workItem.js";
33
32
  import { recordExecutionLaneResult } from "../execution/executionGroup.js";
34
33
  import { formatAgentRunReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
35
34
  import { bindExecution, claimPending, claimInputDelivery as claimMailboxInputDelivery, completeInputDelivery as completeMailboxInputDelivery, completeProcessing, mailboxHasPending, mailboxHasWork, pendingLane, markInputDeliveryPushed as markMailboxInputDeliveryPushed, markInputDeliveryUnknown as markMailboxInputDeliveryUnknown, releaseInputDelivery as releaseMailboxInputDelivery, resolveInputDeliveryNotAccepted as resolveMailboxInputDeliveryNotAccepted, releaseProcessing } from "../coordination/workMailbox.js";
@@ -387,7 +386,7 @@ export class FileSchedulerStoreAdapter {
387
386
  const message = [
388
387
  `Leader native Session became unavailable while Run ${active.id} remains active.`,
389
388
  "Yui preserved the Run because Session/host termination is not a workflow outcome.",
390
- "Inspect native-child and mailbox facts, then explicitly diagnose, recover the Run, request a Conversation switch, or request user input."
389
+ "Retire the disposable Run, or use Task execution stop/start if the current runtime cannot be settled normally."
391
390
  ].join(" ");
392
391
  recordLeaderAttentionRequired(store, {
393
392
  taskId: input.fence.taskId,
@@ -650,7 +649,9 @@ export class FileSchedulerStoreAdapter {
650
649
  const resolved = [];
651
650
  for (const request of expired) {
652
651
  const task = store.getTask(request.taskId);
653
- if (task?.status !== "active" || request.policy.kind !== "recommended")
652
+ if (task?.status !== "active"
653
+ || task.executionGate.state !== "enabled"
654
+ || request.policy.kind !== "recommended")
654
655
  continue;
655
656
  const choiceKey = request.policy.recommendedChoiceKey;
656
657
  const answered = answerInputRequest(request, { choiceKey }, "agent-timeout", now);
@@ -723,7 +724,7 @@ export class FileSchedulerStoreAdapter {
723
724
  return this.store.transaction((store) => {
724
725
  const task = store.getTask(input.taskId);
725
726
  const run = store.getActiveAgentRun(input.taskId, input.roleName);
726
- if (task === null || task.status !== "active"
727
+ if (task === null || task.status !== "active" || task.executionGate.state !== "enabled"
727
728
  || run === null || run.id !== input.runId || run.status !== "active") {
728
729
  return "state-changed";
729
730
  }
@@ -747,6 +748,7 @@ export class FileSchedulerStoreAdapter {
747
748
  const run = store.getActiveAgentRun(input.taskId, input.roleName);
748
749
  if (task === null
749
750
  || task.status !== "active"
751
+ || task.executionGate.state !== "enabled"
750
752
  || role === null
751
753
  || run === null
752
754
  || run.id !== input.runId
@@ -783,6 +785,7 @@ export class FileSchedulerStoreAdapter {
783
785
  const run = store.getActiveAgentRun(input.taskId, input.roleName);
784
786
  if (task === null
785
787
  || task.status !== "active"
788
+ || task.executionGate.state !== "enabled"
786
789
  || run === null
787
790
  || run.id !== input.runId
788
791
  || run.status !== "active")
@@ -828,82 +831,6 @@ export class FileSchedulerStoreAdapter {
828
831
  const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
829
832
  return sessions !== null && sessions.inFlight !== null;
830
833
  }
831
- detachAgentHostProviderForConversationSwitch(input) {
832
- return this.store.transaction((store) => {
833
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
834
- const run = store.getAgentRun(input.taskId, input.runId);
835
- const mailbox = store.getWorkMailbox({
836
- kind: "role",
837
- taskId: input.taskId,
838
- roleName: input.roleName
839
- });
840
- const binding = sessions?.providerBinding;
841
- const session = sessions?.sessions[input.agentId];
842
- const task = store.getTask(input.taskId);
843
- const role = store.getRole(input.taskId, input.roleName);
844
- const activeRun = store.getActiveAgentRun(input.taskId, input.roleName);
845
- if (task === null || task.status !== "active"
846
- || role === null || role.activeAgentId !== input.agentId
847
- || sessions === null || sessions === undefined
848
- || run === null || run.status !== "active" || run.mode !== "new"
849
- || activeRun?.id !== run.id
850
- || run.effective.agentId !== input.agentId
851
- || session?.nativeSessionId !== input.previousNativeSessionId
852
- || binding === null || binding === undefined
853
- || currentProviderConversation(binding).conversationId !== input.previousConversationId
854
- || sessions.inFlight?.runId !== input.runId
855
- || mailbox?.processing?.batchId !== sessions.inFlight.receiptId
856
- || mailbox.processing.owner !== "controller"
857
- || mailbox.processing.executionRef?.type !== "run"
858
- || mailbox.processing.executionRef.taskId !== input.taskId
859
- || mailbox.processing.executionRef.id !== input.runId
860
- || mailbox.inputDelivery !== null
861
- || pendingConversationSwitch(store.listEvents(input.taskId), input.roleName, sessions) === null
862
- || !runtimeHookMatchesReservation(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, input.launchId)
863
- || input.nextAuthorityHolderId !== input.launchId) {
864
- throw new Error("Provider Conversation detachment no longer matches the exact switch Run.");
865
- }
866
- const target = binding.activations.find((entry) => (entry.activationId === input.previousActivationId
867
- && entry.conversationId === input.previousConversationId));
868
- if (target === undefined) {
869
- throw new Error("Provider Conversation detachment targets an unknown Activation.");
870
- }
871
- if (target.status !== "active") {
872
- if (binding.authority.owner !== "none"
873
- || input.nextAuthorityEpoch !== binding.authority.epoch + 1) {
874
- throw new Error("Provider Conversation detachment replay has a stale authority fence.");
875
- }
876
- return "already-detached";
877
- }
878
- if (conversationDetachmentBasis({
879
- sessions,
880
- events: store.listEvents(input.taskId),
881
- mailbox,
882
- roleName: input.roleName,
883
- runId: input.runId,
884
- runMode: run.mode
885
- }) !== "actor-request"
886
- || binding.authority.owner !== "controller"
887
- || binding.authority.holderId !== target.activationId
888
- || input.nextAuthorityEpoch !== binding.authority.epoch + 2) {
889
- throw new Error("Provider Conversation detachment is not authorized at this boundary.");
890
- }
891
- const detached = endProviderActivation(binding, target.activationId, {
892
- status: "ended",
893
- endedAt: input.now.toISOString(),
894
- reason: "conversation-switch"
895
- });
896
- store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, detached, input.now));
897
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, CONVERSATION_SWITCH_DETACHED_EVENT, {
898
- runId: input.runId,
899
- roleName: input.roleName,
900
- launchId: input.launchId,
901
- conversationId: input.previousConversationId,
902
- activationId: input.previousActivationId
903
- }, input.now));
904
- return "detached";
905
- });
906
- }
907
834
  beginAgentHostProviderTurn(input) {
908
835
  this.store.transaction((store) => {
909
836
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
@@ -1307,7 +1234,7 @@ export class FileSchedulerStoreAdapter {
1307
1234
  saveLeaderDispatch(input) {
1308
1235
  return this.store.transaction((store) => {
1309
1236
  const task = store.getTask(input.task.id);
1310
- if (task === null || task.status !== "active") {
1237
+ if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1311
1238
  return "unavailable";
1312
1239
  }
1313
1240
  const role = requireRole(store, input.task.id, input.role.name);
@@ -1391,7 +1318,7 @@ export class FileSchedulerStoreAdapter {
1391
1318
  saveRoleRunDelivery(input) {
1392
1319
  this.store.transaction((store) => {
1393
1320
  const task = store.getTask(input.task.id);
1394
- if (task === null || task.status !== "active") {
1321
+ if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1395
1322
  throw new Error(`Task is not active: ${input.task.id}.`);
1396
1323
  }
1397
1324
  const role = requireRole(store, input.task.id, input.role.name);
@@ -1464,7 +1391,7 @@ export class FileSchedulerStoreAdapter {
1464
1391
  saveRoleRunPrepared(input) {
1465
1392
  this.store.transaction((store) => {
1466
1393
  const task = store.getTask(input.task.id);
1467
- if (task === null || task.status !== "active") {
1394
+ if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1468
1395
  throw new Error(`Task is not active: ${input.task.id}.`);
1469
1396
  }
1470
1397
  const role = requireRole(store, input.task.id, input.role.name);
@@ -1475,9 +1402,15 @@ export class FileSchedulerStoreAdapter {
1475
1402
  if (input.session !== null && input.session.nativeSessionId !== undefined) {
1476
1403
  const existing = store.getRoleSession(input.task.id, input.role.name);
1477
1404
  matchingPreparedRuntimeReservation(store, input);
1405
+ // A terminal Session is audit history, not the current execution
1406
+ // identity. Preallocated providers must publish the replacement fence
1407
+ // before the new Agent Host starts so its exact-runtime preflight sees
1408
+ // the new launch. Only a still-live conflicting Session is deferred.
1478
1409
  const defersConversationReplacement = active.mode === "new"
1479
1410
  && existing !== null
1480
- && existing.nativeSessionId !== input.session.nativeSessionId;
1411
+ && existing.nativeSessionId !== input.session.nativeSessionId
1412
+ && existing.status !== "stopped"
1413
+ && existing.status !== "broken";
1481
1414
  if (!defersConversationReplacement
1482
1415
  && (existing?.nativeSessionId !== input.session.nativeSessionId
1483
1416
  || (input.launchId !== undefined && existing.launchId !== input.launchId)
@@ -1510,6 +1443,7 @@ export class FileSchedulerStoreAdapter {
1510
1443
  }));
1511
1444
  if (task === null
1512
1445
  || task.status !== "active"
1446
+ || task.executionGate.state !== "enabled"
1513
1447
  || role === null
1514
1448
  || active === null
1515
1449
  || active.id !== input.runId
@@ -1556,16 +1490,6 @@ export class FileSchedulerStoreAdapter {
1556
1490
  outcome: terminal.status
1557
1491
  }, input.now);
1558
1492
  store.saveEvent(input.taskId, deliveryFailureEvent);
1559
- if (terminal.purpose === "execution" && terminal.workItemId !== undefined) {
1560
- const item = store.getWorkItem(input.taskId, terminal.workItemId);
1561
- if (item !== null && ![
1562
- "completed", "failed", "retired"
1563
- ].includes(item.status)) {
1564
- if (!workItemOwnsUnresolvedExecutionLane(item, terminal.executionGroupId, terminal.executionLaneId)) {
1565
- store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", input.now, summary));
1566
- }
1567
- }
1568
- }
1569
1493
  if (input.roleName !== "leader") {
1570
1494
  routeRoleEvent(store, deliveryFailureEvent, input.roleName, terminal.purpose === "review" ? "review-failed" : "role-run-failed", input.now);
1571
1495
  }
@@ -1593,6 +1517,7 @@ export class FileSchedulerStoreAdapter {
1593
1517
  const mailbox = store.getWorkMailbox(target);
1594
1518
  if (task === null
1595
1519
  || task.status !== "active"
1520
+ || task.executionGate.state !== "enabled"
1596
1521
  || role === null
1597
1522
  || active === null
1598
1523
  || active.id !== input.claimed.run.id
@@ -1606,7 +1531,9 @@ export class FileSchedulerStoreAdapter {
1606
1531
  store.saveAgentRun(failAgentRun(active, input.failure.message, input.now));
1607
1532
  store.clearActiveAgentRun(input.task.id, input.role.name);
1608
1533
  clearTaskRoleRunInFlight(store, role, active, input.now);
1609
- store.saveWorkMailbox(releaseProcessing(mailbox, mailbox.processing.batchId));
1534
+ // Consume this failed delivery attempt. Releasing it would immediately
1535
+ // redispatch the same wake and manufacture another Leader Session.
1536
+ store.saveWorkMailbox(completeProcessing(mailbox, mailbox.processing.batchId));
1610
1537
  store.saveRole(input.task.id, updateRoleStatus(role, "failed", input.now));
1611
1538
  breakTaskSessionIfPresent(store, input.task.id, role.name, active.effective.agentId, input.now);
1612
1539
  const failure = recordLeaderFailure(input.task.id, input.failure.nativeSessionId, input.failure.message, input.now, store.getLeaderFailure(input.task.id));
@@ -1630,6 +1557,7 @@ export class FileSchedulerStoreAdapter {
1630
1557
  const currentRun = store.getActiveAgentRun(input.task.id, input.role.name);
1631
1558
  if (task === null
1632
1559
  || task.status !== "active"
1560
+ || task.executionGate.state !== "enabled"
1633
1561
  || role === null
1634
1562
  || currentRun === null
1635
1563
  || currentRun.id !== input.run.id
@@ -1708,17 +1636,12 @@ export class FileSchedulerStoreAdapter {
1708
1636
  const group = currentWorkItemExecutionGroup(workItem);
1709
1637
  store.saveWorkItem(task.id, updateWorkItemExecutionGroup(workItem, recordExecutionLaneResult(group, currentRun.executionLaneId, { summary: input.summary }, "failed", input.now), input.now));
1710
1638
  }
1711
- const laneUpdated = store.getWorkItem(task.id, currentRun.workItemId);
1712
- if (!workItemOwnsUnresolvedExecutionLane(laneUpdated, currentRun.executionGroupId, currentRun.executionLaneId)) {
1713
- store.saveWorkItem(task.id, updateWorkItemStatus(laneUpdated, "failed", input.now, input.summary));
1714
- }
1715
1639
  }
1716
1640
  }
1717
- const blocksAutomaticLeaderRecovery = role.name === "leader"
1718
- && input.leaderRecovery === "blocked";
1719
- store.saveRole(task.id, updateRoleStatus(role, blocksAutomaticLeaderRecovery ? "failed" : "exited", input.now));
1641
+ const leaderFailed = role.name === "leader";
1642
+ store.saveRole(task.id, updateRoleStatus(role, leaderFailed ? "failed" : "exited", input.now));
1720
1643
  stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
1721
- if (blocksAutomaticLeaderRecovery) {
1644
+ if (leaderFailed) {
1722
1645
  const failure = recordLeaderFailure(task.id, input.session?.nativeSessionId ?? "(unregistered)", input.summary, input.now, store.getLeaderFailure(task.id));
1723
1646
  store.saveLeaderFailure(failure);
1724
1647
  recordLeaderAttentionRequired(store, {
@@ -1732,7 +1655,7 @@ export class FileSchedulerStoreAdapter {
1732
1655
  });
1733
1656
  return "failed";
1734
1657
  }
1735
- queueLeaderWakeup(store, task.id, wakeReason(role.name === "leader" ? "leader-run-failed" : "role-run-failed"), input.now);
1658
+ queueLeaderWakeup(store, task.id, wakeReason("role-run-failed"), input.now);
1736
1659
  return "failed";
1737
1660
  });
1738
1661
  }
@@ -1963,7 +1886,7 @@ export class FileSchedulerStoreAdapter {
1963
1886
  const task = store.getTask(input.taskId);
1964
1887
  if (task === null)
1965
1888
  throw new Error(`Task not found: ${input.taskId}.`);
1966
- if (task.status !== "active") {
1889
+ if (task.status !== "active" || task.executionGate.state !== "enabled") {
1967
1890
  throw new Error(`Cannot complete a runtime turn for inactive Task: ${input.taskId}.`);
1968
1891
  }
1969
1892
  const role = requireRole(store, input.taskId, input.roleName);
@@ -2061,7 +1984,7 @@ export class FileSchedulerStoreAdapter {
2061
1984
  }
2062
1985
  classifyRuntimeTurnFailed(input) {
2063
1986
  const task = this.store.getTask(input.taskId);
2064
- if (task?.status !== "active")
1987
+ if (task?.status !== "active" || task.executionGate.state !== "enabled")
2065
1988
  return "obsolete";
2066
1989
  const role = this.store.getRole(input.taskId, input.roleName);
2067
1990
  if (role === null)
@@ -2122,14 +2045,6 @@ export class FileSchedulerStoreAdapter {
2122
2045
  roleName: input.roleName
2123
2046
  }), RUNTIME_CLEANUP_REQUIRED_REASON, now, [{ type: "task", id: input.taskId }]);
2124
2047
  const terminal = result.run;
2125
- if (before.purpose === "execution" && before.workItemId !== undefined) {
2126
- const item = store.getWorkItem(input.taskId, before.workItemId);
2127
- if (item !== null && ![
2128
- "completed", "failed", "retired"
2129
- ].includes(item.status) && !workItemOwnsUnresolvedExecutionLane(item, before.executionGroupId, before.executionLaneId)) {
2130
- store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", now, summary));
2131
- }
2132
- }
2133
2048
  const role = store.getRole(input.taskId, input.roleName);
2134
2049
  if (role !== null) {
2135
2050
  store.saveRole(input.taskId, updateRoleStatus(role, input.roleName === "leader" ? "failed" : "idle", now));
@@ -2513,8 +2428,6 @@ export class FileSchedulerStoreAdapter {
2513
2428
  }
2514
2429
  const sessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
2515
2430
  const providerBinding = sessions?.providerBinding;
2516
- let recoveryMode = "resume";
2517
- let replacementRequired = false;
2518
2431
  if (providerBinding === null || providerBinding === undefined) {
2519
2432
  deferUnprovenProviderRecovery(store, run, entry.roleName, "Provider Conversation identity is missing; automatic recovery is fenced.", now);
2520
2433
  return;
@@ -2554,12 +2467,19 @@ export class FileSchedulerStoreAdapter {
2554
2467
  }
2555
2468
  return;
2556
2469
  }
2557
- if (recovery.action === "replace") {
2558
- if (conversation.recoverability !== "unrecoverable") {
2559
- deferUnprovenProviderRecovery(store, run, entry.roleName, "Provider replacement lacks an exact missing-Conversation observation.", now);
2560
- return;
2470
+ if (recovery.action === "restart-run") {
2471
+ const finalized = finalizeProviderRetryDeadline(store, run, "Provider Conversation is unavailable; retry the work with a fresh Run and Session.", "provider-conversation-unavailable", now);
2472
+ if (finalized) {
2473
+ const currentSessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
2474
+ const currentSession = currentSessions?.sessions[run.effective.agentId];
2475
+ if (currentSessions !== null && currentSessions !== undefined
2476
+ && currentSession !== undefined
2477
+ && currentSession.status !== "stopped"
2478
+ && currentSession.status !== "broken") {
2479
+ store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(currentSessions, run.effective.agentId, "broken", now));
2480
+ }
2561
2481
  }
2562
- replacementRequired = true;
2482
+ return;
2563
2483
  }
2564
2484
  const session = sessions?.sessions[run.effective.agentId];
2565
2485
  const identityMatches = session !== undefined
@@ -2570,9 +2490,8 @@ export class FileSchedulerStoreAdapter {
2570
2490
  || session.launchId === run.providerRetry.launchId);
2571
2491
  if (!identityMatches || session?.nativeSessionId === undefined) {
2572
2492
  // Local Session projection loss is not native-session-dead evidence.
2573
- // Preserve the Run and episode for explicit same-generation recovery;
2574
- // only an exact Provider-native resume response may terminalize it as
2575
- // unrecoverable.
2493
+ // Keep the bounded retry episode; if native identity never returns,
2494
+ // the episode fails and a later Run can continue from Task progress.
2576
2495
  const deferred = deferProviderRetry(run.providerRetry, now);
2577
2496
  if (deferred === null) {
2578
2497
  finalizeProviderRetryDeadline(store, run, "Provider retry safety could not be re-established before the bounded episode deadline; native Session recoverability remains unproven.", "native-resume-unproven", now);
@@ -2606,25 +2525,10 @@ export class FileSchedulerStoreAdapter {
2606
2525
  store.saveAgentRun(withProviderRetry(run, deferred));
2607
2526
  return;
2608
2527
  }
2609
- if (replacementRequired) {
2610
- try {
2611
- recoveryMode = roleSessionDispatchModeWithConversationSwitch(sessions, store.listEvents(entry.taskId), roleMailbox, entry.roleName, run.effective.agentId, run.effective);
2612
- }
2613
- catch (error) {
2614
- deferUnprovenProviderRecovery(store, run, entry.roleName, error instanceof Error
2615
- ? error.message
2616
- : "Independent Conversation-switch evaluation failed closed.", now);
2617
- return;
2618
- }
2619
- if (recoveryMode !== "new") {
2620
- deferUnprovenProviderRecovery(store, run, entry.roleName, "Independent Conversation-switch evaluation kept the current Conversation.", now);
2621
- return;
2622
- }
2623
- }
2624
2528
  // Reopen the Run on its original Session: reset transport markers,
2625
2529
  // switch to resume, and mark the projection in-flight. The delivery
2626
2530
  // pass re-pushes the exact same input in this same pass.
2627
- const reopenedRun = reopenRunForProviderRetry(run, `provider-retry-${run.providerRetry.episodeId}-${run.providerRetry.dispatchedRetries + 1}`, now, recoveryMode);
2531
+ const reopenedRun = reopenRunForProviderRetry(run, `provider-retry-${run.providerRetry.episodeId}-${run.providerRetry.dispatchedRetries + 1}`, now, "resume");
2628
2532
  store.saveAgentRun(reopenedRun);
2629
2533
  if (sessions !== null && sessions.inFlight !== null) {
2630
2534
  store.saveTaskRoleSessionSet(prepareTaskRoleRunRedispatch(sessions, {
@@ -2735,22 +2639,17 @@ export class FileSchedulerStoreAdapter {
2735
2639
  taskId,
2736
2640
  roleName: input.fence.roleName
2737
2641
  });
2738
- const existingNativeSessionId = sessions.sessions[input.fence.agentId]
2739
- ?.nativeSessionId;
2642
+ const existingSession = sessions.sessions[input.fence.agentId];
2643
+ const existingNativeSessionId = existingSession?.nativeSessionId;
2740
2644
  const replacingNativeSession = existingNativeSessionId !== undefined
2741
2645
  && existingNativeSessionId !== decision.outcome.nativeSessionId;
2742
2646
  const replacementBasis = replacingNativeSession
2743
- ? conversationReplacementBasis({
2744
- sessions,
2745
- events: store.listEvents(taskId),
2746
- mailbox,
2747
- roleName: input.fence.roleName,
2748
- runId: run.id,
2749
- runMode: run.mode
2750
- })
2647
+ && run.mode === "new"
2648
+ && (existingSession?.status === "stopped" || existingSession?.status === "broken")
2649
+ ? "terminal-session"
2751
2650
  : null;
2752
2651
  if (replacingNativeSession && replacementBasis === null) {
2753
- recordCanonicalObservationObsolete(store, input, "conversation-switch-not-authorized", now);
2652
+ recordCanonicalObservationObsolete(store, input, "session-replacement-not-terminal", now);
2754
2653
  return "obsolete";
2755
2654
  }
2756
2655
  const sessionInput = {
@@ -2764,17 +2663,9 @@ export class FileSchedulerStoreAdapter {
2764
2663
  };
2765
2664
  const bound = replacementBasis === null
2766
2665
  ? recordRoleAgentSession(sessions, sessionInput, now)
2767
- : replaceTaskRoleAgentSessionForConversationSwitch(sessions, sessionInput, now);
2666
+ : replaceTaskRoleAgentSession(sessions, sessionInput, now);
2768
2667
  const withProvider = bindOrSupersedeProviderRuntime(bound, input, now, replacementBasis ?? undefined);
2769
2668
  store.saveTaskRoleSessionSet(withProvider);
2770
- if (replacementBasis === "actor-request") {
2771
- resolveConversationSwitchRequest(store, taskId, input.fence.roleName, sessions, "applied", now, {
2772
- conversationId: input.fence.conversationId
2773
- ?? decision.outcome.nativeSessionId,
2774
- activationId: input.fence.activationId ?? input.fence.launchId,
2775
- launchId: input.fence.launchId
2776
- });
2777
- }
2778
2669
  const driver = this.drivers.require(input.fence.driverId);
2779
2670
  if (driver.capabilities.observation.sessionBootstrap === "discovered"
2780
2671
  && run.pushedAt === undefined) {
@@ -3111,6 +3002,17 @@ export class FileSchedulerStoreAdapter {
3111
3002
  current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.turnId, now);
3112
3003
  store.saveGlobalRoleSessionSet(current);
3113
3004
  completeRuntimeHookReservation(store, owner, input.launchId);
3005
+ if (input.roleName === SYSTEM_OPERATOR_ROLE
3006
+ && input.adapterId === "codex"
3007
+ && completedStatus === "ready") {
3008
+ const operatorMailbox = store.getWorkMailbox({ kind: "operator" });
3009
+ // A completed foreground Codex Turn leaves its native TUI attached to
3010
+ // the thread. Stop only that idle Role runtime so Desktop can become
3011
+ // the writer; the durable nativeSessionId remains available to resume.
3012
+ if (operatorMailbox === null || !mailboxHasWork(operatorMailbox)) {
3013
+ enqueueWork(store, runtimeLifecycleTarget(owner), RUNTIME_CLEANUP_REQUIRED_REASON, now);
3014
+ }
3015
+ }
3114
3016
  return current.sessions[input.agentId];
3115
3017
  });
3116
3018
  }
@@ -3286,6 +3188,7 @@ function preallocatedRuntimeReadyAwaitingProjection(store, observation, drivers)
3286
3188
  const run = store.getActiveAgentRun(taskId, observation.fence.roleName);
3287
3189
  const sessions = store.getTaskRoleSessionSet(taskId, observation.fence.roleName);
3288
3190
  if (task?.status !== "active"
3191
+ || task.executionGate.state !== "enabled"
3289
3192
  || role?.activeAgentId !== observation.fence.agentId
3290
3193
  || run?.id !== runId
3291
3194
  || run.status !== "active"
@@ -3436,14 +3339,6 @@ function finalizeProviderRetryDeadline(store, run, summary, reason, now) {
3436
3339
  elapsedMs: String(Math.max(0, now.getTime() - Date.parse(retry.firstFailureAt)))
3437
3340
  }, now);
3438
3341
  store.saveEvent(run.taskId, exhaustionEvent);
3439
- if (terminal.purpose === "execution" && terminal.workItemId !== undefined) {
3440
- const item = store.getWorkItem(run.taskId, terminal.workItemId);
3441
- if (item !== null
3442
- && !["completed", "failed", "retired"].includes(item.status)
3443
- && !workItemOwnsUnresolvedExecutionLane(item, terminal.executionGroupId, terminal.executionLaneId)) {
3444
- store.saveWorkItem(run.taskId, updateWorkItemStatus(item, "failed", now, summary));
3445
- }
3446
- }
3447
3342
  const role = store.getRole(run.taskId, run.roleName);
3448
3343
  if (role !== null) {
3449
3344
  store.saveRole(run.taskId, updateRoleStatus(role, run.roleName === "leader" ? "failed" : "idle", now));
@@ -3511,7 +3406,7 @@ function recordTaskRuntimeNativeSession(store, input, now) {
3511
3406
  if (task.status === "archived") {
3512
3407
  throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
3513
3408
  }
3514
- if (task.status !== "active") {
3409
+ if (task.status !== "active" || task.executionGate.state !== "enabled") {
3515
3410
  throw new Error(`Cannot register a native session for a Task that is not active: ${input.taskId}.`);
3516
3411
  }
3517
3412
  const role = requireRole(store, input.taskId, input.roleName);
@@ -3979,29 +3874,16 @@ function bindOrSupersedeProviderRuntime(sessions, input, now, replacementBasis)
3979
3874
  startedAt: input.observedAt ?? input.receivedAt
3980
3875
  }), now);
3981
3876
  }
3982
- const turn = sessions.providerBinding.turn;
3983
- const noUnsettledInputDelivery = turn === null
3984
- || ["completed", "failed", "cancelled", "rejected"].includes(turn.status);
3877
+ if (replacementBasis === undefined) {
3878
+ throw new Error("A fresh Provider Conversation requires a terminal prior Session.");
3879
+ }
3985
3880
  return updateTaskRoleProviderRuntime(sessions, supersedeProviderConversation(sessions.providerBinding, {
3986
3881
  conversationId,
3987
3882
  activationId,
3988
3883
  switchedAt: input.observedAt ?? input.receivedAt,
3989
- noUnsettledInputDelivery,
3990
- ...(replacementBasis === undefined ? {} : { basis: replacementBasis })
3884
+ basis: replacementBasis
3991
3885
  }), now);
3992
3886
  }
3993
- function resolveConversationSwitchRequest(store, taskId, roleName, previousSessions, status, now, replacement) {
3994
- const request = pendingConversationSwitch(store.listEvents(taskId), roleName, previousSessions);
3995
- if (request === null)
3996
- return;
3997
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, CONVERSATION_SWITCH_RESOLVED_EVENT, {
3998
- requestId: request.requestId,
3999
- roleName,
4000
- generation: request.generation,
4001
- status,
4002
- ...(replacement === undefined ? {} : replacement)
4003
- }, now));
4004
- }
4005
3887
  function rejectSubmittingProviderTurn(store, target, delivery, now, reason) {
4006
3888
  if (target.kind !== "role"
4007
3889
  || delivery.executionRef.type !== "run"
@@ -99,8 +99,9 @@ function validateStartParams(store, params) {
99
99
  if (task === null) {
100
100
  throw jobDomainError(`Task not found: ${params.taskId}.`);
101
101
  }
102
- if (task.status !== "active") {
103
- throw jobDomainError(`DurableJob requires an active Task; ${params.taskId} is ${task.status}.`);
102
+ if (task.status !== "active" || task.executionGate.state !== "enabled") {
103
+ throw jobDomainError(`DurableJob requires enabled Task execution; ${params.taskId} is `
104
+ + `${task.status}/${task.executionGate.state}.`);
104
105
  }
105
106
  // The owner record must exist and must not be terminal. A terminal owner
106
107
  // cannot run new jobs — its workspace is eligible for cleanup.
@@ -584,11 +584,6 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
584
584
  }
585
585
  return { recorded: true };
586
586
  }
587
- if (method === "runtime.conversation-switch-detach") {
588
- const value = conversationSwitchDetachParams(params);
589
- const outcome = schedulerStore.detachAgentHostProviderForConversationSwitch(value);
590
- return { recorded: outcome === "detached", outcome };
591
- }
592
587
  if (method === "runtime.provider-turn-submission-resolve") {
593
588
  const value = providerTurnControlParams(params);
594
589
  const status = params.status;
@@ -719,10 +714,11 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
719
714
  const task = request.scope === "task"
720
715
  ? store.getTask(request.taskId)
721
716
  : null;
722
- if (request.scope === "task" && task?.status !== "active") {
717
+ if (request.scope === "task"
718
+ && (task?.status !== "active" || task.executionGate.state !== "enabled")) {
723
719
  throw applicationError("INVALID_PARAMS", task === null
724
720
  ? `Task not found: ${request.taskId}.`
725
- : `Task is not active: ${request.taskId}.`);
721
+ : `Task execution is not enabled: ${request.taskId}.`);
726
722
  }
727
723
  if (request.scope === "task" && task !== null) {
728
724
  const taskWorkspace = store.getTaskWorkspace(task.id);
@@ -852,7 +848,9 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
852
848
  let activeRun = null;
853
849
  if (request.owner.scope === "task") {
854
850
  const task = store.getTask(request.owner.taskId);
855
- if (task === null || task.status !== "active") {
851
+ if (task === null
852
+ || task.status !== "active"
853
+ || task.executionGate.state !== "enabled") {
856
854
  throw new Error(`Task is no longer active: ${request.owner.taskId}.`);
857
855
  }
858
856
  activeRun = store.getActiveAgentRun(request.owner.taskId, request.owner.roleName);
@@ -1135,31 +1133,6 @@ function providerTurnControlParams(params) {
1135
1133
  now: new Date(observedAt)
1136
1134
  };
1137
1135
  }
1138
- function conversationSwitchDetachParams(params) {
1139
- if (typeof params !== "object" || params === null || Array.isArray(params)) {
1140
- throw applicationError("INVALID_PARAMS", "Provider Conversation detachment params are invalid.");
1141
- }
1142
- const value = params;
1143
- const nextAuthorityEpoch = value.nextAuthorityEpoch;
1144
- const observedAt = requiredParam(value.observedAt);
1145
- if (!Number.isSafeInteger(nextAuthorityEpoch) || nextAuthorityEpoch < 1
1146
- || !Number.isFinite(Date.parse(observedAt))) {
1147
- throw applicationError("INVALID_PARAMS", "Provider Conversation detachment fence is invalid.");
1148
- }
1149
- return {
1150
- taskId: requiredParam(value.taskId),
1151
- roleName: requiredParam(value.roleName),
1152
- runId: requiredParam(value.runId),
1153
- agentId: requiredParam(value.agentId),
1154
- launchId: requiredParam(value.launchId),
1155
- previousConversationId: requiredParam(value.previousConversationId),
1156
- previousNativeSessionId: requiredParam(value.previousNativeSessionId),
1157
- previousActivationId: requiredParam(value.previousActivationId),
1158
- nextAuthorityEpoch: nextAuthorityEpoch,
1159
- nextAuthorityHolderId: requiredParam(value.nextAuthorityHolderId),
1160
- now: new Date(observedAt)
1161
- };
1162
- }
1163
1136
  function abortableDelay(milliseconds, signal) {
1164
1137
  return new Promise((resolve, reject) => {
1165
1138
  const timer = setTimeout(() => {
@@ -137,7 +137,9 @@ export class FileRuntimeEventProcessor {
137
137
  const taskId = event.observation.fence.taskId;
138
138
  if (taskId !== undefined) {
139
139
  const task = this.observer.getTask(taskId);
140
- if (task === null || task.status !== "active")
140
+ if (task === null
141
+ || task.status !== "active"
142
+ || task.executionGate.state !== "enabled")
141
143
  return "obsolete";
142
144
  }
143
145
  return this.observer.observeRuntimeObservation?.(event.observation, now) ?? "obsolete";
@@ -217,7 +219,7 @@ export class FileRuntimeEventProcessor {
217
219
  };
218
220
  if (task === null)
219
221
  return "obsolete";
220
- if (task.status !== "active") {
222
+ if (task.status !== "active" || task.executionGate.state !== "enabled") {
221
223
  this.recordObsolete(event, task.status === "archived" ? "task-archived" : "task-retired", now);
222
224
  return "obsolete";
223
225
  }
@@ -622,7 +624,9 @@ export class AsyncRuntimeEventProcessor {
622
624
  const taskId = event.observation.fence.taskId;
623
625
  if (taskId !== undefined) {
624
626
  const task = await this.observer.getTask(taskId);
625
- if (task === null || task.status !== "active")
627
+ if (task === null
628
+ || task.status !== "active"
629
+ || task.executionGate.state !== "enabled")
626
630
  return "obsolete";
627
631
  }
628
632
  const outcome = (await this.observer.observeRuntimeObservation?.(event.observation, now))
@@ -662,7 +666,7 @@ export class AsyncRuntimeEventProcessor {
662
666
  };
663
667
  if (task === null)
664
668
  return "obsolete";
665
- if (task.status !== "active") {
669
+ if (task.status !== "active" || task.executionGate.state !== "enabled") {
666
670
  await this.recordObsolete(event, task.status === "archived" ? "task-archived" : "task-retired", now);
667
671
  return "obsolete";
668
672
  }