@zq-silk/yui 0.15.7 → 0.15.8

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 (281) hide show
  1. package/ARCHITECTURE.md +192 -399
  2. package/README.md +127 -1149
  3. package/dist/agent/adapterCatalog.js +15 -2
  4. package/dist/agent/agent.js +23 -3
  5. package/dist/agent/argumentPolicy.js +7 -1
  6. package/dist/agent/connectionPlan.js +62 -0
  7. package/dist/agent/executionComponents.js +158 -0
  8. package/dist/agent/launchEnvironment.js +31 -3
  9. package/dist/agent/managedRuntimeEnvironment.js +3 -5
  10. package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
  11. package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
  12. package/dist/brief/taskBrief.js +12 -0
  13. package/dist/cli/agentConfigurationPicker.js +13 -0
  14. package/dist/cli/commandCatalog.js +157 -70
  15. package/dist/cli/interactionCandidates.js +5 -5
  16. package/dist/cli/interactionPolicy.js +38 -8
  17. package/dist/cli/invocationRouter.js +1 -1
  18. package/dist/cli/managedDiagnostics.js +28 -0
  19. package/dist/cli/operatorWizard.js +1 -7
  20. package/dist/cli/roleOptionOrder.js +27 -0
  21. package/dist/cli/roleWizard.js +50 -14
  22. package/dist/cli/updateOrchestrator.js +1 -1
  23. package/dist/cli/updatePorts.js +3 -4
  24. package/dist/cli.js +178 -95
  25. package/dist/commands/agentCommands.js +72 -14
  26. package/dist/commands/capabilityCommands.js +9 -6
  27. package/dist/commands/configCommands.js +20 -20
  28. package/dist/commands/deliveryGuardPreflight.js +2 -2
  29. package/dist/commands/executionAuditCommands.js +24 -24
  30. package/dist/commands/globalRoleCommands.js +1 -1
  31. package/dist/commands/grantCommands.js +4 -4
  32. package/dist/commands/operatorCommands.js +1 -7
  33. package/dist/commands/projectCommands.js +4 -4
  34. package/dist/commands/resourcesCommands.js +2 -2
  35. package/dist/commands/roleConfiguration.js +25 -5
  36. package/dist/commands/roleRuntimeGuard.js +4 -5
  37. package/dist/commands/sessionCommands.js +3 -7
  38. package/dist/commands/taskActivationCommands.js +259 -0
  39. package/dist/commands/taskActor.js +28 -49
  40. package/dist/commands/taskCommands.js +1188 -710
  41. package/dist/commands/taskContextCommand.js +39 -583
  42. package/dist/commands/taskExecutionCommands.js +32 -32
  43. package/dist/commands/taskInputCommands.js +40 -104
  44. package/dist/commands/taskIntegrationCommands.js +3 -2
  45. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  46. package/dist/commands/taskNextActionCommand.js +8 -8
  47. package/dist/commands/taskOverviewCommand.js +33 -45
  48. package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
  49. package/dist/commands/taskRoleRuntimeStatus.js +133 -102
  50. package/dist/commands/telemetryCommands.js +36 -38
  51. package/dist/config/configCatalog.js +4 -4
  52. package/dist/config/yuiConfig.js +8 -8
  53. package/dist/context/contextSnapshot.js +10 -10
  54. package/dist/context/dispatchContext.js +11 -11
  55. package/dist/context/roleSessionContext.js +6 -3
  56. package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
  57. package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
  58. package/dist/context/sessionBootstrapManifest.js +21 -2
  59. package/dist/context/sourceRunContext.js +30 -0
  60. package/dist/context/taskContext.js +458 -0
  61. package/dist/context/wakeNotification.js +27 -27
  62. package/dist/controller/agentRuntimeObserver.js +21 -24
  63. package/dist/controller/capabilityBridge.js +17 -6
  64. package/dist/controller/clientRuntime.js +65 -92
  65. package/dist/controller/controller.js +114 -188
  66. package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
  67. package/dist/controller/jobControl.js +54 -85
  68. package/dist/controller/resourceInventory.js +8 -27
  69. package/dist/controller/resourceInventoryLinux.js +12 -13
  70. package/dist/controller/runtime.js +529 -479
  71. package/dist/controller/runtimeEventInbox.js +55 -25
  72. package/dist/controller/runtimeEventProcessor.js +22 -31
  73. package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
  74. package/dist/controller/runtimeLaunchCoordinator.js +80 -426
  75. package/dist/controller/runtimeObservationHook.js +14 -18
  76. package/dist/controller/sessionNotify.js +16 -24
  77. package/dist/controller/sessionOwnerReconciliation.js +168 -50
  78. package/dist/controller/structuredProviderObservation.js +138 -99
  79. package/dist/coordination/workMailbox.js +3 -3
  80. package/dist/coordination/workMailboxQueue.js +36 -33
  81. package/dist/core/boundedRpc.js +8 -1
  82. package/dist/core/controllerClient.js +20 -1
  83. package/dist/core/controllerServer.js +4 -4
  84. package/dist/doctor/doctor.js +13 -2
  85. package/dist/domain/agentResultTransport.js +9 -9
  86. package/dist/execution/codexThreadNaming.js +2 -8
  87. package/dist/execution/executionHealth.js +51 -63
  88. package/dist/execution/reviewMainRun.js +137 -0
  89. package/dist/execution/workItemExecution.js +28 -29
  90. package/dist/execution/workItemExecutionProjection.js +99 -107
  91. package/dist/execution/workItemMainRun.js +141 -0
  92. package/dist/executor/agentAdapter.js +227 -20
  93. package/dist/executor/agentConfigurationCatalog.js +126 -4
  94. package/dist/executor/agentConfigurationProbe.js +162 -4
  95. package/dist/executor/agentExecutor.js +79 -78
  96. package/dist/executor/effectiveLaunch.js +105 -18
  97. package/dist/executor/executorRegistry.js +29 -44
  98. package/dist/executor/fileRoleLaunchPlanner.js +229 -154
  99. package/dist/executor/workspacePreflightClassification.js +16 -16
  100. package/dist/grant/capabilityGrant.js +6 -3
  101. package/dist/input/inputRequest.js +12 -10
  102. package/dist/integration/gitIntegrationService.js +4 -11
  103. package/dist/integration/integrationQueueService.js +4 -4
  104. package/dist/interaction/operatorPresentation.js +1 -1
  105. package/dist/kernel/builtinCapabilities.js +247 -12
  106. package/dist/kernel/capabilityRegistry.js +64 -18
  107. package/dist/kernel/instanceHost.js +12 -1
  108. package/dist/kernel/kernelPorts.js +2 -2
  109. package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
  110. package/dist/lifecycle/exactRunTerminalization.js +449 -0
  111. package/dist/message/message.js +62 -6
  112. package/dist/message/messageContinuation.js +204 -0
  113. package/dist/observability/executionAudit.js +70 -72
  114. package/dist/observability/faultClassification.js +2 -2
  115. package/dist/observability/orchestrationMetrics.js +8 -8
  116. package/dist/operator/operatorSessionHistory.js +1 -7
  117. package/dist/output/agentConfigurationPresentation.js +8 -3
  118. package/dist/output/agentRunConfigurationPresentation.js +128 -0
  119. package/dist/output/rolePresentation.js +54 -3
  120. package/dist/plugins/pluginChild.js +104 -0
  121. package/dist/plugins/pluginIntent.js +26 -0
  122. package/dist/plugins/pluginInterpreter.js +43 -0
  123. package/dist/plugins/pluginPackage.js +101 -0
  124. package/dist/plugins/pluginProcess.js +112 -0
  125. package/dist/plugins/pluginService.js +380 -0
  126. package/dist/profile/agentProfile.js +1 -1
  127. package/dist/repository/gitWorkspace.js +26 -4
  128. package/dist/repository/project.js +19 -4
  129. package/dist/repository/taskBaseFreshness.js +13 -13
  130. package/dist/repository/taskWorkspaceCoordinator.js +20 -27
  131. package/dist/repository/taskWorkspacePreparer.js +344 -83
  132. package/dist/resources/autoResourceGc.js +3 -3
  133. package/dist/resources/liveReferences.js +3 -3
  134. package/dist/resources/projectResource.js +123 -0
  135. package/dist/resources/projectResourceService.js +421 -0
  136. package/dist/resources/resourceDiscovery.js +6 -6
  137. package/dist/resources/resourceGc.js +1 -1
  138. package/dist/resources/resourceRegistrar.js +1 -1
  139. package/dist/resources/resourceTypes.js +1 -1
  140. package/dist/review/deltaRecheck.js +3 -3
  141. package/dist/review/reviewAcceptance.js +16 -16
  142. package/dist/review/reviewDecision.js +7 -7
  143. package/dist/review/reviewRound.js +21 -20
  144. package/dist/review/reviewerAvailability.js +2 -2
  145. package/dist/role/role.js +51 -7
  146. package/dist/role/taskRoleUpdate.js +30 -0
  147. package/dist/runtime/acpProtocol.js +425 -0
  148. package/dist/runtime/acpSession.js +731 -0
  149. package/dist/runtime/acpSessionConfiguration.js +260 -0
  150. package/dist/runtime/agentDriver.js +30 -11
  151. package/dist/runtime/agentEndpoint.js +278 -0
  152. package/dist/runtime/agentEndpointIdentity.js +86 -0
  153. package/dist/runtime/agentEndpointOwnership.js +239 -0
  154. package/dist/runtime/agentError.js +2 -10
  155. package/dist/runtime/agentHost.js +565 -314
  156. package/dist/runtime/agentRunConfiguration.js +258 -0
  157. package/dist/runtime/builtinAgentDrivers.js +134 -18
  158. package/dist/runtime/builtinAgentErrorMappers.js +55 -3
  159. package/dist/runtime/builtinTranscriptUsage.js +1 -1
  160. package/dist/runtime/claude-process-owner +0 -0
  161. package/dist/runtime/codexAppServerRuntime.js +38 -30
  162. package/dist/runtime/codexInteractiveHost.js +41 -6
  163. package/dist/runtime/continuationManager.js +2 -6
  164. package/dist/runtime/executionEnvironment.js +30 -0
  165. package/dist/runtime/firstProgressAdvisory.js +11 -11
  166. package/dist/runtime/index.js +4 -3
  167. package/dist/runtime/jsonLineChannel.js +109 -0
  168. package/dist/runtime/launchBroker.js +91 -16
  169. package/dist/runtime/launchDiagnostics.js +2 -2
  170. package/dist/runtime/lifecycleReservation.js +10 -18
  171. package/dist/runtime/managedCaller.js +61 -17
  172. package/dist/runtime/nativeSessionControl.js +102 -0
  173. package/dist/runtime/ports.js +6 -21
  174. package/dist/runtime/processExitObservation.js +8 -7
  175. package/dist/runtime/promptEnvelope.js +17 -6
  176. package/dist/runtime/providerContinuation.js +3 -9
  177. package/dist/runtime/providerContinuationReconciliationService.js +4 -13
  178. package/dist/runtime/providerControl.js +2 -7
  179. package/dist/runtime/providerRuntimeIdentity.js +110 -222
  180. package/dist/runtime/providerRuntimeReconciler.js +5 -9
  181. package/dist/runtime/runtimeBinding.js +0 -1
  182. package/dist/runtime/runtimeContinuationProjection.js +4 -7
  183. package/dist/runtime/runtimeDeadlines.js +9 -0
  184. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  185. package/dist/runtime/runtimeObservation.js +29 -65
  186. package/dist/runtime/runtimeProjection.js +43 -51
  187. package/dist/runtime/runtimeSessionCandidate.js +1 -3
  188. package/dist/runtime/sessionLaunchRequest.js +3 -7
  189. package/dist/runtime/sessionOwnerIdentity.js +7 -54
  190. package/dist/runtime/sessionOwnerRegistry.js +22 -17
  191. package/dist/runtime/sessionReconciliation.js +4 -8
  192. package/dist/runtime/sessionTerminationGuard.js +70 -259
  193. package/dist/runtime/sessionTokenMetrics.js +5 -16
  194. package/dist/runtime/structuredProviderHost.js +237 -117
  195. package/dist/runtime/taskRuntimeIsolation.js +39 -122
  196. package/dist/runtime/tmuxAdapters.js +39 -86
  197. package/dist/scheduler/activeRoleRunDelivery.js +354 -0
  198. package/dist/scheduler/leaderWakeupProcessor.js +75 -266
  199. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  200. package/dist/scheduler/ports.js +80 -9
  201. package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
  202. package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
  203. package/dist/scheduler/taskExecutionProjection.js +120 -124
  204. package/dist/scheduler/taskObservabilityProjection.js +29 -29
  205. package/dist/scheduler/taskWake.js +11 -4
  206. package/dist/scheduler/wakeReason.js +9 -1
  207. package/dist/setup/setupCommand.js +1 -0
  208. package/dist/storage/migrations/agentRunContract.js +159 -0
  209. package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
  210. package/dist/storage/sqliteSchema.js +431 -5
  211. package/dist/storage/sqliteStore.js +375 -220
  212. package/dist/storage/storageVersions.js +1 -1
  213. package/dist/storage/storeRpc.js +10 -5
  214. package/dist/storage/taskStore.js +13 -11
  215. package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
  216. package/dist/surface/surfaceContributions.js +102 -0
  217. package/dist/task/completionReadiness.js +32 -6
  218. package/dist/task/deliveryGuard.js +16 -16
  219. package/dist/task/draftPlan.js +72 -12
  220. package/dist/task/nextAction.js +144 -128
  221. package/dist/task/remoteDelivery.js +6 -6
  222. package/dist/task/task.js +184 -18
  223. package/dist/task/taskActivation.js +301 -0
  224. package/dist/task/taskActivationService.js +392 -0
  225. package/dist/task/taskRecordReference.js +5 -4
  226. package/dist/task/taskRecordRetirement.js +1 -1
  227. package/dist/telemetry/sqliteTelemetryStore.js +55 -68
  228. package/dist/telemetry/telemetryConfig.js +14 -14
  229. package/dist/telemetry/telemetryWiring.js +2 -2
  230. package/dist/web/assets/assetManifest.js +2 -0
  231. package/dist/web/assets/client/app.js +120 -20
  232. package/dist/web/assets/client/components.js +87 -54
  233. package/dist/web/assets/client/i18n.js +83 -41
  234. package/dist/web/assets/client/markdown.js +1 -1
  235. package/dist/web/assets/client/taskSurface.js +353 -0
  236. package/dist/web/assets/client/view.js +49 -44
  237. package/dist/web/assets/shell.js +1 -1
  238. package/dist/web/assets/styles/cards.js +22 -4
  239. package/dist/web/controllerWeb.js +60 -0
  240. package/dist/web/webMutation.js +28 -0
  241. package/dist/web/webServer.js +118 -8
  242. package/dist/web/webSnapshot.js +81 -74
  243. package/dist/web/webTaskSurface.js +64 -0
  244. package/dist/workItem/dependencyGate.js +1 -1
  245. package/dist/workItem/workItem.js +80 -48
  246. package/dist/workspace/workItemChangeSetManager.js +16 -9
  247. package/docs/agent-result-consumption.md +94 -0
  248. package/docs/agent-runtime-drivers.md +91 -0
  249. package/docs/architecture/README.md +38 -0
  250. package/docs/architecture/capabilities-and-resources.md +79 -0
  251. package/docs/managed-turn-and-session-runtime.md +222 -0
  252. package/docs/observability/README.md +81 -0
  253. package/docs/plugin-sdk.md +290 -0
  254. package/docs/provider-runtime.md +163 -0
  255. package/docs/release-workflow.md +303 -0
  256. package/docs/roles-and-configuration.md +113 -0
  257. package/docs/sqlite-control-plane-design.md +76 -0
  258. package/docs/task-dag-semantics.md +57 -0
  259. package/docs/task-delivery.md +103 -0
  260. package/docs/task-local-identity.md +6 -6
  261. package/docs/testing/verification-levels.md +86 -0
  262. package/i18n/README.zh-CN.md +98 -739
  263. package/package.json +2 -2
  264. package/skills/yui-leader/SKILL.md +130 -103
  265. package/skills/yui-leader/references/integration.md +39 -0
  266. package/skills/yui-leader/references/replicated-execution.md +42 -0
  267. package/skills/yui-leader/references/task-plugins.md +33 -0
  268. package/skills/yui-operator/SKILL.md +30 -59
  269. package/skills/yui-reviewer/SKILL.md +35 -36
  270. package/skills/yui-runtime/SKILL.md +88 -24
  271. package/skills/yui-runtime/references/publication.md +22 -0
  272. package/skills/yui-runtime/references/recovery.md +64 -0
  273. package/skills/yui-worker/SKILL.md +37 -39
  274. package/dist/cli/roleOptionCatalog.js +0 -68
  275. package/dist/context/sourceTurnContext.js +0 -30
  276. package/dist/execution/reviewMainTurn.js +0 -161
  277. package/dist/execution/workItemMainTurn.js +0 -164
  278. package/dist/lifecycle/exactTurnTerminalization.js +0 -407
  279. package/dist/runtime/preallocatedNativeSession.js +0 -13
  280. package/dist/runtime/runtimeStopReceipt.js +0 -42
  281. package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
@@ -1,47 +1,50 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { prepareMessageContinuations } from "../message/messageContinuation.js";
3
+ import { freezeRunContextSnapshot } from "../context/runContextPack.js";
4
+ import { contextSnapshotRef } from "../context/contextSnapshot.js";
2
5
  import { isDeepStrictEqual } from "node:util";
3
- import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
- import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, managedProviderTurnId, clearProviderGoal, providerGoalContinues, settleProviderTurnSubmission, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
6
+ import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
7
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime, selectNewTaskRoleSession, taskRoleControlTarget } from "../executor/agentExecutor.js";
8
+ import { acceptProviderTurn, cancelQuiescentProviderInput, beginProviderTurn, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, clearProviderGoal, settleProviderTurnSubmission, settleProviderTurn, transferProviderAuthority, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
5
9
  import { createTaskEvent } from "../event/taskEvent.js";
6
10
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
7
11
  import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
8
- import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
12
+ import { createTaskWake, fallbackWakeCursor, latestTaskWake, markTaskWakeConsumed } from "../scheduler/taskWake.js";
9
13
  import { answerInputRequest } from "../input/inputRequest.js";
10
14
  import { activeRoleAgentBinding } from "../role/role.js";
11
15
  import { effectiveLaunchWithTaskMainWorkspace, roleSessionMayContinue, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
12
16
  import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
13
- import { appendTurnInput, createTurn } from "../turn/turn.js";
17
+ import { appendRunInput, createRun, withRunContextSnapshot, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
14
18
  import { transportAgentResult } from "../domain/agentResultTransport.js";
15
- import { createTurnInput } from "../context/turnInputContract.js";
19
+ import { createRunInput } from "../context/runInputContract.js";
16
20
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
17
- import { terminalizeExactTaskTurn } from "../lifecycle/exactTurnTerminalization.js";
21
+ import { terminalizeExactTaskRun, cancelQuiescentRoleRuns } from "../lifecycle/exactRunTerminalization.js";
18
22
  import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
19
23
  import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
20
24
  import { recordLeaderAttentionRequired, routeRoleEvent } from "../scheduler/operatorEvent.js";
21
- import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
22
25
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
23
26
  import { wakeReason } from "../scheduler/wakeReason.js";
24
- import { foldTurnProgressFacts, latestTurnDurableProgressAt, latestTurnEventTime, latestStallEvidenceKey, isRoleTurnStalled, TURN_PROGRESS_EVENT, TURN_DIAGNOSTIC_FINISHED_EVENT, TURN_RECOVERED_EVENT, TURN_STALLED_EVENT } from "../scheduler/roleTurnStall.js";
27
+ import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_DIAGNOSTIC_FINISHED_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
25
28
  import { pendingWakeupProjection } from "../storage/taskStore.js";
26
29
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
27
30
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
28
- import { formatTurnReceiptId } from "../task/taskRecordReference.js";
29
- import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasPending, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
30
- import { enqueueWork, settleRoleTurnDispatch as settleRoleTurnDispatchMailbox } from "../coordination/workMailboxQueue.js";
31
- import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, RuntimeLifecycleBusyError, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, isRuntimeLaunchReservation, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
32
- import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
31
+ import { formatRunReceiptId } from "../task/taskRecordReference.js";
32
+ import { bindExecution, claimPending, completeProcessing, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
33
+ import { enqueueWork, settleRoleRunDispatch as settleRoleRunDispatchMailbox } from "../coordination/workMailboxQueue.js";
34
+ import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
33
35
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
34
36
  import { standardAgentError } from "../runtime/agentError.js";
35
- import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeTokenEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationTurnFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
36
- import { contextSnapshotRef } from "../context/contextSnapshot.js";
37
+ import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeTokenEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
37
38
  import { snapshotExecutionLaneWorkspaceSync } from "../repository/executionLaneGitSnapshot.js";
38
- import { contextSnapshotDeltaRefIds, freezeTurnContextSnapshot } from "../context/turnContextPack.js";
39
39
  export class AgentHostProviderTurnFenceError extends Error {
40
40
  constructor(message) {
41
41
  super(message);
42
42
  this.name = "AgentHostProviderTurnFenceError";
43
43
  }
44
44
  }
45
+ export class AgentHostProviderSessionBusyError extends AgentHostProviderTurnFenceError {
46
+ code = "SESSION_BUSY";
47
+ }
45
48
  /** Maps authoritative TaskStore records to the scheduler's narrow port. */
46
49
  export class FileSchedulerStoreAdapter {
47
50
  store;
@@ -55,23 +58,10 @@ export class FileSchedulerStoreAdapter {
55
58
  this.drivers = drivers;
56
59
  this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
57
60
  }
58
- freezeLeaderContextSnapshot(taskId, roleName, now) {
59
- return this.store.transaction((tx) => {
60
- const snapshot = freezeTurnContextSnapshot(tx, {
61
- taskId,
62
- roleName,
63
- purpose: "execution"
64
- }, now);
65
- return Object.freeze({
66
- ref: contextSnapshotRef(snapshot),
67
- deltaRefIds: contextSnapshotDeltaRefIds(tx, snapshot)
68
- });
69
- });
70
- }
71
61
  /**
72
62
  * Sole provider-independent ingress for structured runtime state. Driver
73
63
  * mapping has already happened before this boundary; this method validates
74
- * the exact durable Turn/session fence, applies the small workflow-relevant
64
+ * the exact durable AgentRun/session fence, applies the small workflow-relevant
75
65
  * transitions, and retains the canonical observation for status projection.
76
66
  */
77
67
  observeRuntimeObservation(raw, now = new Date()) {
@@ -86,10 +76,41 @@ export class FileSchedulerStoreAdapter {
86
76
  if (adapterId === null) {
87
77
  return this.recordObsoleteCanonicalObservation(input, "driver-or-turn-mismatch", now);
88
78
  }
79
+ if (input.kind === "input.accepted" && input.payload.input !== undefined
80
+ && (input.fence.receiptId?.startsWith("native-input:") === true
81
+ || input.fence.receiptId?.startsWith("steer:") === true)) {
82
+ return this.store.transaction((store) => {
83
+ const run = input.fence.runId === undefined ? null : store.getRun(taskId, input.fence.runId);
84
+ const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
85
+ const native = sessions?.providerBinding?.run;
86
+ if (run === null || run.roleName !== input.fence.roleName
87
+ || run.effective.agentId !== input.fence.agentId || native?.runId !== run.id
88
+ || native.nativeTurnId !== input.fence.nativeTurnId
89
+ || sessions?.sessions[input.fence.agentId]?.nativeSessionId !== input.fence.nativeSessionId)
90
+ return "obsolete";
91
+ if (!hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
92
+ const updated = appendRunInput(run, createRunInput({
93
+ source: input.fence.receiptId.startsWith("native-input:")
94
+ ? { type: "provider", channel: "visible-input" }
95
+ : { type: "yui", channel: "input-response" },
96
+ directive: input.payload.input, deltaRefIds: []
97
+ }), now);
98
+ store.saveRun(updated);
99
+ store.saveActiveRun(updated);
100
+ }
101
+ this.persistRuntimeObservation(input, now);
102
+ return "applied";
103
+ });
104
+ }
105
+ if ((input.kind === "input.accepted" || input.kind === "input.rejected" || input.kind === "input.delivery-unknown")
106
+ && input.payload.input !== undefined
107
+ && input.fence.receiptId?.startsWith("turn-input:") === true) {
108
+ return this.observeLeaderSteerReceipt(input, now);
109
+ }
89
110
  // Receipt dedupe is not business-result dedupe. Terminal folds revalidate
90
111
  // their exact binding and commit result + notification + observation
91
112
  // together, including a retry after a previously interrupted fold.
92
- if (!isTurnTerminalObservation(input)
113
+ if (!isRunTerminalObservation(input)
93
114
  && input.kind !== "turn.accepted" && input.kind !== "input.accepted"
94
115
  && hasPersistedRuntimeObservation(this.store.listEvents(taskId), input))
95
116
  return "applied";
@@ -108,8 +129,8 @@ export class FileSchedulerStoreAdapter {
108
129
  break;
109
130
  case "turn.failed": {
110
131
  // A Provider Turn is an activation boundary, not a Task outcome.
111
- // Keep the Turn and Session identity available for Agent-directed
112
- // restore + submit, even when a provider marks its Turn terminal.
132
+ // Keep the AgentRun and Session identity available for Agent-directed
133
+ // restore + submit, even when a provider marks its AgentRun terminal.
113
134
  outcome = this.foldProviderTurnBoundary(input, adapterId, "failed", now);
114
135
  break;
115
136
  }
@@ -124,16 +145,13 @@ export class FileSchedulerStoreAdapter {
124
145
  case "continuation.reported":
125
146
  case "continuation.settled":
126
147
  case "input.delivery-unknown":
127
- outcome = this.validateCanonicalTurnObservation(input, now);
148
+ outcome = this.validateCanonicalRunObservation(input, now);
128
149
  break;
129
150
  case "turn.cancelled": {
130
151
  outcome = this.foldProviderTurnBoundary(input, adapterId, "cancelled", now);
131
152
  break;
132
153
  }
133
154
  case "conversation.observed":
134
- case "activation.started":
135
- case "activation.ended":
136
- case "activation.failed":
137
155
  outcome = this.observeProviderRuntimeIdentity(input, now);
138
156
  break;
139
157
  case "goal.updated":
@@ -150,21 +168,86 @@ export class FileSchedulerStoreAdapter {
150
168
  default:
151
169
  outcome = "obsolete";
152
170
  }
153
- if (outcome === "applied" && !isTurnTerminalObservation(input)) {
171
+ if (outcome === "applied" && !isRunTerminalObservation(input)) {
154
172
  this.persistRuntimeObservation(input, now);
155
173
  }
156
174
  return outcome;
157
175
  }
176
+ /** A delayed steer receipt settles only its original claimed mailbox batch. */
177
+ observeLeaderSteerReceipt(input, now) {
178
+ return this.store.transaction((store) => {
179
+ const taskId = input.fence.taskId;
180
+ const run = input.fence.runId === undefined ? null : store.getRun(taskId, input.fence.runId);
181
+ const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
182
+ const session = sessions?.sessions[input.fence.agentId];
183
+ const binding = sessions?.providerBinding;
184
+ if (run === null || run.roleName !== "leader" || input.fence.roleName !== "leader"
185
+ || run.effective.agentId !== input.fence.agentId
186
+ || session === undefined
187
+ || session.nativeSessionId !== input.fence.nativeSessionId
188
+ || binding?.run?.runId !== run.id
189
+ || binding.run.nativeTurnId !== input.fence.nativeTurnId) {
190
+ recordCanonicalObservationObsolete(store, input, "steer-receipt-fence-mismatch", now);
191
+ return "obsolete";
192
+ }
193
+ if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
194
+ return "applied";
195
+ const target = { kind: "role", taskId, roleName: "leader" };
196
+ const mailbox = store.getWorkMailbox(target);
197
+ const processing = mailbox?.processing;
198
+ const exactBatch = processing?.owner === `leader-steer:${run.id}`
199
+ && input.fence.receiptId === `turn-input:${taskId}/${run.id}/${processing.batchId}`;
200
+ if (exactBatch && mailbox !== null && processing !== null && processing !== undefined) {
201
+ if (input.kind === "input.accepted") {
202
+ // Terminal AgentRuns remain immutable: the canonical receipt still
203
+ // retains their late input, without touching a successor pointer.
204
+ if (run.status === "active" && store.getActiveRun(taskId, "leader")?.id === run.id) {
205
+ const updated = appendRunInput(run, createRunInput({
206
+ source: { type: "yui", channel: "leader-forced-wakeup" },
207
+ directive: input.payload.input,
208
+ deltaRefIds: []
209
+ }), now);
210
+ store.saveRun(updated);
211
+ store.saveActiveRun(updated);
212
+ }
213
+ store.saveWorkMailbox(completeProcessing(mailbox, processing.batchId));
214
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "run.input-submitted", {
215
+ runId: run.id,
216
+ batchId: processing.batchId,
217
+ attemptId: input.fence.receiptId,
218
+ source: "yui/leader-forced-wakeup",
219
+ reasons: processing.batch.reasons.join(",")
220
+ }, now));
221
+ }
222
+ else if (input.payload.failure?.error.inputDisposition === "not-accepted") {
223
+ store.saveWorkMailbox(releaseProcessing(mailbox, processing.batchId));
224
+ }
225
+ }
226
+ if (input.kind !== "input.accepted" && input.payload.failure !== undefined) {
227
+ const error = input.payload.failure.error;
228
+ this.recordAgentError({
229
+ taskId, roleName: run.roleName, runId: run.id,
230
+ source: error.source, phase: error.phase,
231
+ message: error.message, raw: error.raw,
232
+ inputDisposition: error.inputDisposition,
233
+ sessionDisposition: error.sessionDisposition,
234
+ attemptId: input.fence.receiptId
235
+ }, now);
236
+ }
237
+ this.persistRuntimeObservation(input, now);
238
+ return "applied";
239
+ });
240
+ }
158
241
  adapterForRuntimeObservation(input) {
159
242
  const taskId = input.fence.taskId;
160
- const turnId = input.fence.turnId;
243
+ const runId = input.fence.runId;
161
244
  if (taskId === undefined)
162
245
  return null;
163
- if (turnId === undefined) {
246
+ if (runId === undefined) {
164
247
  const role = this.store.getRole(taskId, input.fence.roleName);
165
248
  const sessions = this.store.getTaskRoleSessionSet(taskId, input.fence.roleName);
166
249
  const session = sessions?.sessions[input.fence.agentId];
167
- if (role?.activeAgentId !== input.fence.agentId
250
+ if (role === null || sessions?.activeAgentId !== input.fence.agentId
168
251
  || session?.adapterId === undefined)
169
252
  return null;
170
253
  try {
@@ -176,7 +259,7 @@ export class FileSchedulerStoreAdapter {
176
259
  return null;
177
260
  }
178
261
  }
179
- const run = this.store.getTurn(taskId, turnId);
262
+ const run = this.store.getRun(taskId, runId);
180
263
  if (run === null
181
264
  || run.roleName !== input.fence.roleName
182
265
  || run.effective.agentId !== input.fence.agentId)
@@ -190,32 +273,31 @@ export class FileSchedulerStoreAdapter {
190
273
  return null;
191
274
  }
192
275
  }
193
- validateCanonicalTurnObservation(input, now) {
276
+ validateCanonicalRunObservation(input, now) {
194
277
  return this.store.transaction((store) => {
195
278
  if (hasPersistedRuntimeObservation(store.listEvents(input.fence.taskId), input)) {
196
279
  return "applied";
197
280
  }
198
- const run = store.getTurn(input.fence.taskId, input.fence.turnId);
199
- const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
281
+ const run = input.fence.runId === undefined ? null : store.getRun(input.fence.taskId, input.fence.runId);
282
+ const active = store.getActiveRun(input.fence.taskId, input.fence.roleName);
200
283
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
201
284
  const session = sessions?.sessions[input.fence.agentId];
202
- const knownContinuation = input.kind.startsWith("continuation.")
203
- && projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.turnId === input.fence.turnId
204
- && entry.identity.providerNamespace === input.fence.driverId
205
- && entry.identity.accountScope === input.fence.agentId
206
- && entry.identity.conversationId === input.fence.conversationId
207
- && entry.identity.activationId === input.fence.activationId
208
- && entry.identity.continuationId === input.fence.continuationId
209
- && entry.identity.generation === input.fence.continuationGeneration));
285
+ if (input.fence.runId === undefined
286
+ && ["operation.started", "operation.completed", "operation.failed", "activity.observed"].includes(input.kind)) {
287
+ const native = sessions?.providerBinding?.run;
288
+ return native?.runId === undefined && input.fence.receiptId !== undefined
289
+ && native?.attemptId === input.fence.receiptId && native.status === "accepted"
290
+ && session?.nativeSessionId === input.fence.nativeSessionId
291
+ && (native.nativeTurnId === undefined || native.nativeTurnId === input.fence.nativeTurnId)
292
+ ? "applied" : "obsolete";
293
+ }
294
+ const knownContinuation = input.kind.startsWith("continuation.") && projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.runId === input.fence.runId
295
+ && entry.identity.providerNamespace === input.fence.driverId
296
+ && entry.identity.accountScope === input.fence.agentId
297
+ && entry.identity.conversationId === input.fence.conversationId
298
+ && entry.identity.continuationId === input.fence.continuationId));
210
299
  const requiresCurrentRuntime = input.kind !== "turn.cancelled" && !knownContinuation;
211
- const valid = run !== null
212
- && (!requiresCurrentRuntime || (run.status === "active" && active?.id === run.id))
213
- && run.roleName === input.fence.roleName
214
- && run.effective.agentId === input.fence.agentId
215
- && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId
216
- && (knownContinuation || (session?.runtimeGenerationId === input.fence.runtimeGenerationId
217
- && session.nativeSessionId === input.fence.nativeSessionId))
218
- && runtimeReceiptBelongsToTurn(store, input);
300
+ const valid = run !== null && (!requiresCurrentRuntime || (run.status === "active" && active?.id === run.id)) && run.roleName === input.fence.roleName && run.effective.agentId === input.fence.agentId && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId && (knownContinuation || (session !== undefined && session.nativeSessionId === input.fence.nativeSessionId)) && runtimeReceiptBelongsToRun(store, input);
219
301
  if (!valid) {
220
302
  recordCanonicalObservationObsolete(store, input, "runtime-fence-not-current", now);
221
303
  return "obsolete";
@@ -234,9 +316,12 @@ export class FileSchedulerStoreAdapter {
234
316
  }
235
317
  : {
236
318
  status: "failed",
319
+ ...(typeof input.payload.output === "string"
320
+ && transportAgentResult(input.payload.output).status === "completed"
321
+ ? { output: input.payload.output } : {}),
237
322
  diagnostic: providerStatus === "cancelled"
238
- ? "Provider cancelled the Agent Turn."
239
- : `Provider Agent Turn failed: ${input.payload.failure?.error.message ?? "unknown provider failure"}`,
323
+ ? "Provider cancelled the Agent AgentRun."
324
+ : `Provider Agent AgentRun failed: ${input.payload.failure?.error.message ?? "unknown provider failure"}`,
240
325
  failureReason: providerStatus === "cancelled"
241
326
  ? "cancelled"
242
327
  : "runtime-failed"
@@ -246,57 +331,44 @@ export class FileSchedulerStoreAdapter {
246
331
  roleName: input.fence.roleName,
247
332
  agentId: input.fence.agentId,
248
333
  adapterId,
249
- runtimeGenerationId: input.fence.runtimeGenerationId,
250
- activationId: input.fence.activationId,
251
334
  conversationId: input.fence.conversationId,
252
335
  nativeSessionId: input.fence.nativeSessionId,
253
336
  nativeTurnId: input.fence.nativeTurnId,
254
337
  attemptId: input.fence.receiptId,
255
- turnId: input.fence.turnId,
338
+ runId: input.fence.runId,
256
339
  ...(input.payload.input === undefined ? {} : { input: input.payload.input }),
257
340
  providerStatus,
258
341
  outcome,
259
342
  observation: input
260
343
  };
261
- const classification = this.classifyRuntimeTurnTerminal(completed);
344
+ const classification = this.classifyRuntimeRunTerminal(completed);
262
345
  if (classification !== "apply")
263
346
  return classification;
264
- const result = this.observeRuntimeTurnTerminal(completed, now);
347
+ const result = this.observeRuntimeRunTerminal(completed, now);
265
348
  return result.disposition === "obsolete" ? "obsolete" : "applied";
266
349
  }
267
350
  validateCanonicalSessionObservation(input, now) {
268
351
  return this.store.transaction((store) => {
269
- const run = store.getTurn(input.fence.taskId, input.fence.turnId);
352
+ const run = input.fence.runId === undefined ? null : store.getRun(input.fence.taskId, input.fence.runId);
270
353
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
271
354
  const session = sessions?.sessions[input.fence.agentId];
272
- if (run === null
273
- || run.roleName !== input.fence.roleName
274
- || run.effective.agentId !== input.fence.agentId
275
- || this.drivers.requireByAdapterId(run.effective.adapterId).id !== input.fence.driverId
276
- || session?.runtimeGenerationId !== input.fence.runtimeGenerationId
277
- || session.nativeSessionId !== input.fence.nativeSessionId) {
355
+ const native = sessions?.providerBinding?.run;
356
+ const exactInput = input.fence.receiptId !== undefined && native?.attemptId === input.fence.receiptId;
357
+ const validRun = input.fence.runId === undefined ? exactInput
358
+ : run !== null && run.roleName === input.fence.roleName && run.effective.agentId === input.fence.agentId
359
+ && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId;
360
+ if (!validRun || session === undefined || session.nativeSessionId !== input.fence.nativeSessionId
361
+ || (input.fence.receiptId !== undefined && (!exactInput
362
+ || (native?.nativeTurnId !== undefined && native.nativeTurnId !== input.fence.nativeTurnId)))) {
278
363
  recordCanonicalObservationObsolete(store, input, "runtime-session-not-current", now);
279
364
  return "obsolete";
280
365
  }
281
366
  const status = "ended";
282
367
  let updatedSessions = updateRoleAgentSessionStatus(sessions, input.fence.agentId, status, now, input.kind === "session.failed" ? "failed" : "stopped");
283
- if (updatedSessions.providerBinding !== null) {
284
- const activationId = input.fence.activationId ?? input.fence.runtimeGenerationId;
285
- updatedSessions = updateTaskRoleProviderRuntime(updatedSessions, endProviderActivation(updatedSessions.providerBinding, activationId, {
286
- status: input.kind === "session.failed" ? "failed" : "ended",
287
- endedAt: input.observedAt ?? input.receivedAt,
288
- reason: input.kind
289
- }), now);
290
- }
291
368
  store.saveTaskRoleSessionSet(updatedSessions);
292
369
  for (const continuation of projectProviderContinuations(store.listEvents(input.fence.taskId))) {
293
- if (continuation.turnId !== input.fence.turnId
294
- || continuation.identity.conversationId
295
- !== (input.fence.conversationId ?? input.fence.nativeSessionId)
296
- || continuation.identity.activationId
297
- !== (input.fence.activationId ?? input.fence.runtimeGenerationId)
298
- || continuation.execution === "quiescent"
299
- || continuation.attachment === "detached")
370
+ if (continuation.runId !== input.fence.runId || continuation.identity.conversationId
371
+ !== (input.fence.conversationId ?? input.fence.nativeSessionId) || continuation.execution === "quiescent" || continuation.attachment === "detached")
300
372
  continue;
301
373
  const key = providerContinuationKey(continuation.identity);
302
374
  const identityDigest = createHash("sha256").update(key).digest("hex");
@@ -311,9 +383,7 @@ export class FileSchedulerStoreAdapter {
311
383
  fence: {
312
384
  ...input.fence,
313
385
  conversationId: continuation.identity.conversationId,
314
- activationId: continuation.identity.activationId,
315
386
  continuationId: continuation.identity.continuationId,
316
- continuationGeneration: continuation.identity.generation,
317
387
  ...(continuation.parentContinuationId === undefined
318
388
  ? {}
319
389
  : { parentContinuationId: continuation.parentContinuationId })
@@ -334,23 +404,23 @@ export class FileSchedulerStoreAdapter {
334
404
  store.saveEvent(input.fence.taskId, detachedEvent);
335
405
  routeContinuationResult(store, detached, detachedEvent, "provider-continuation-detached", now);
336
406
  }
337
- const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
407
+ const active = store.getActiveRun(input.fence.taskId, input.fence.roleName);
338
408
  const role = store.getRole(input.fence.taskId, input.fence.roleName);
339
409
  if ((input.kind === "session.ended" || input.kind === "session.failed")
340
410
  && active !== null
341
- && active.id === input.fence.turnId
411
+ && active.id === input.fence.runId
342
412
  && active.status === "active"
343
413
  && role !== null) {
344
414
  if (role.name === "leader") {
345
415
  const message = [
346
- `Leader native Session became unavailable while Turn ${active.id} remains active.`,
347
- "Yui preserved the Turn because Session/host termination is not a Task outcome.",
348
- "Retire the disposable Turn, or use Task execution stop/start if the current runtime cannot be settled normally."
416
+ `Leader native Session became unavailable while AgentRun ${active.id} remains active.`,
417
+ "Yui preserved the AgentRun because Session/host termination is not a Task outcome.",
418
+ "Retire the disposable AgentRun, or use Task execution stop/start if the current runtime cannot be settled normally."
349
419
  ].join(" ");
350
420
  recordLeaderAttentionRequired(store, {
351
421
  taskId: input.fence.taskId,
352
422
  reason: "leader-runtime-detached",
353
- payload: { message, turnId: active.id },
423
+ payload: { message, runId: active.id },
354
424
  now
355
425
  });
356
426
  }
@@ -360,7 +430,7 @@ export class FileSchedulerStoreAdapter {
360
430
  taskId: input.fence.taskId,
361
431
  roleName: "leader"
362
432
  }, "role-runtime-detached", now, [
363
- { type: "turn", taskId: input.fence.taskId, id: active.id }
433
+ { type: "run", taskId: input.fence.taskId, id: active.id }
364
434
  ]);
365
435
  }
366
436
  }
@@ -385,10 +455,7 @@ export class FileSchedulerStoreAdapter {
385
455
  && input.payload.snapshotComplete === true
386
456
  && input.payload.observationQuality === "exact") {
387
457
  for (const continuation of projectProviderContinuations(events)) {
388
- if (continuation.turnId !== input.fence.turnId
389
- || continuation.identity.conversationId !== input.fence.conversationId
390
- || continuation.identity.activationId !== input.fence.activationId
391
- || continuation.execution === "quiescent")
458
+ if (continuation.runId !== input.fence.runId || continuation.identity.conversationId !== input.fence.conversationId || continuation.execution === "quiescent")
392
459
  continue;
393
460
  const identityKey = providerContinuationKey(continuation.identity);
394
461
  const digest = createHash("sha256")
@@ -405,7 +472,6 @@ export class FileSchedulerStoreAdapter {
405
472
  fence: {
406
473
  ...input.fence,
407
474
  continuationId: continuation.identity.continuationId,
408
- continuationGeneration: continuation.identity.generation,
409
475
  ...(continuation.parentContinuationId === undefined
410
476
  ? {}
411
477
  : { parentContinuationId: continuation.parentContinuationId })
@@ -443,18 +509,17 @@ export class FileSchedulerStoreAdapter {
443
509
  if (input.kind === "turn.failed" && input.payload.failure !== undefined) {
444
510
  const failure = input.payload.failure;
445
511
  const error = failure.error;
446
- const run = input.fence.turnId === undefined
512
+ const run = input.fence.runId === undefined
447
513
  ? null
448
- : store.getTurn(taskId, input.fence.turnId);
514
+ : store.getRun(taskId, input.fence.runId);
449
515
  const errorEvent = createTaskEvent(store.nextEventId(taskId), taskId, "runtime.agent-error", {
450
516
  sourceEventId: input.eventId,
451
517
  observationEventId,
452
- turnId: input.fence.turnId ?? "",
518
+ runId: input.fence.runId ?? "",
453
519
  roleName: input.fence.roleName,
454
520
  agentId: input.fence.agentId,
455
521
  adapterId: run?.effective.adapterId ?? "unknown",
456
522
  driverId: input.fence.driverId,
457
- runtimeGenerationId: input.fence.runtimeGenerationId,
458
523
  nativeSessionId: input.fence.nativeSessionId ?? "",
459
524
  nativeTurnId: input.fence.nativeTurnId ?? "",
460
525
  source: error.source,
@@ -471,17 +536,7 @@ export class FileSchedulerStoreAdapter {
471
536
  ...(failure.lastOutput === undefined ? {} : { lastOutput: failure.lastOutput })
472
537
  }, now);
473
538
  store.saveEvent(taskId, errorEvent);
474
- enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, wakeReason("agent-error", errorEvent.id), now, [{ type: "event", taskId, id: errorEvent.id }], {
475
- source: input.fence.driverId,
476
- dedupeKey: `agent-error:${taskId}:${input.eventId}`
477
- });
478
- if (input.fence.roleName === "leader"
479
- && error.sessionDisposition === "unrecoverable") {
480
- enqueueWork(store, { kind: "operator" }, "leader-session-unrecoverable", now, [{ type: "event", taskId, id: errorEvent.id }], {
481
- source: input.fence.driverId,
482
- dedupeKey: `leader-session-unrecoverable:${taskId}:${input.eventId}`
483
- });
484
- }
539
+ routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
485
540
  }
486
541
  if ((input.kind === "operation.completed" || input.kind === "operation.failed")
487
542
  && input.payload.operation === "subagent") {
@@ -495,14 +550,14 @@ export class FileSchedulerStoreAdapter {
495
550
  }
496
551
  }
497
552
  });
498
- if (this.telemetry !== null && input.fence.turnId !== undefined) {
553
+ if (this.telemetry !== null && input.fence.runId !== undefined) {
499
554
  try {
500
555
  const entry = runtimeObservationTelemetryEntry(input);
501
556
  this.telemetry.sink.observe(entry);
502
- const run = this.store.getTurn(entry.taskId, input.fence.turnId);
557
+ const run = this.store.getRun(entry.taskId, input.fence.runId);
503
558
  if (run !== null && run.status !== "active") {
504
559
  void this.telemetry.retention.flush().then(() => {
505
- this.telemetry?.retention.pruneGeneration(entry.taskId, entry.roleName, entry.turnId, entry.generation);
560
+ this.telemetry?.retention.pruneRun(entry.taskId, entry.roleName, entry.runId);
506
561
  }).catch(() => undefined);
507
562
  }
508
563
  }
@@ -539,8 +594,8 @@ export class FileSchedulerStoreAdapter {
539
594
  const events = this.store.listEvents(taskId);
540
595
  task = {
541
596
  events,
542
- turnFacts: foldTurnProgressFacts(events),
543
- turns: this.store.listTurns(taskId),
597
+ runFacts: foldRunProgressFacts(events),
598
+ runs: this.store.listRuns(taskId),
544
599
  workItems: this.store.listWorkItems(taskId),
545
600
  reviewRounds: this.store.listReviewRounds(taskId),
546
601
  changeSets: this.store.listChangeSets(taskId),
@@ -560,6 +615,12 @@ export class FileSchedulerStoreAdapter {
560
615
  listActiveTaskIds() {
561
616
  return [...this.store.listActiveTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
562
617
  }
618
+ listPlanningDraftTaskIds() {
619
+ return [...this.store.listPlanningDraftTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
620
+ }
621
+ listPendingActivationRequestTaskIds() {
622
+ return [...this.store.listPendingActivationRequestTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
623
+ }
563
624
  getTask(taskId) { return this.store.getTask(taskId); }
564
625
  getTaskWorkspace(taskId) { return this.store.getTaskWorkspace(taskId); }
565
626
  getTaskBrief(taskId) { return this.store.getTaskBrief(taskId); }
@@ -576,7 +637,7 @@ export class FileSchedulerStoreAdapter {
576
637
  const latest = latestTaskWake(reader.listTaskWakes(taskId));
577
638
  const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
578
639
  taskCreatedAt: task.createdAt,
579
- leaderTurnCreatedAt: operationalTaskRecords(reader.listTurns(taskId), reader.listEvents(taskId), "turn")
640
+ leaderRunCreatedAt: operationalTaskRecords(reader.listRuns(taskId), reader.listEvents(taskId), "run")
580
641
  .filter((run) => run.roleName === "leader")
581
642
  .at(-1)?.createdAt
582
643
  });
@@ -595,8 +656,8 @@ export class FileSchedulerStoreAdapter {
595
656
  const role = this.store.getRole(taskId, roleName);
596
657
  return role === null ? null : mapRole(this.store, role);
597
658
  }
598
- getActiveTurn(taskId, roleName) {
599
- return this.store.getActiveTurn(taskId, roleName);
659
+ getActiveRun(taskId, roleName) {
660
+ return this.store.getActiveRun(taskId, roleName);
600
661
  }
601
662
  hasOpenInputRequest(taskId) {
602
663
  return this.store.listOpenInputRequests([taskId]).length > 0;
@@ -620,7 +681,7 @@ export class FileSchedulerStoreAdapter {
620
681
  adapterId: effectiveSession.effective.adapterId
621
682
  };
622
683
  }
623
- markOperatorTurnStarted(now) {
684
+ markOperatorRunStarted(now) {
624
685
  void now;
625
686
  }
626
687
  resolveExpiredInputRecommendations(now, taskIds) {
@@ -660,8 +721,8 @@ export class FileSchedulerStoreAdapter {
660
721
  listEvents(taskId) {
661
722
  return this.#taskReadProjection(taskId).events;
662
723
  }
663
- listTurns(taskId) {
664
- return this.#taskReadProjection(taskId).turns;
724
+ listRuns(taskId) {
725
+ return this.#taskReadProjection(taskId).runs;
665
726
  }
666
727
  listWorkItems(taskId) {
667
728
  return this.#taskReadProjection(taskId).workItems;
@@ -681,17 +742,17 @@ export class FileSchedulerStoreAdapter {
681
742
  listMessages(taskId) {
682
743
  return this.#taskReadProjection(taskId).messages;
683
744
  }
684
- getTurnProgressFacts(taskId, turnId) {
685
- return this.#taskReadProjection(taskId).turnFacts.get(turnId);
745
+ getRunProgressFacts(taskId, runId) {
746
+ return this.#taskReadProjection(taskId).runFacts.get(runId);
686
747
  }
687
- getTurnDurableProgress(taskId, roleName, turnId) {
748
+ getRunDurableProgress(taskId, roleName, runId) {
688
749
  const projected = this.#taskReadProjection(taskId);
689
750
  // Serve the related-record fold from the same revision's projection: the
690
751
  // event history and the WorkItem/Review/ChangeSet/Integration/Input lists
691
- // are read once per Task per revision, and the per-Turn checkpoint/activity
752
+ // are read once per Task per revision, and the per-AgentRun checkpoint/activity
692
753
  // facts come from the one-pass fold instead of per-candidate scans.
693
754
  const view = {
694
- getTurn: (id, turnId) => this.store.getTurn(id, turnId),
755
+ getRun: (id, runId) => this.store.getRun(id, runId),
695
756
  listEvents: () => projected.events,
696
757
  getWorkItem: (id, workItemId) => this.store.getWorkItem(id, workItemId),
697
758
  listReviewRounds: () => projected.reviewRounds,
@@ -702,22 +763,22 @@ export class FileSchedulerStoreAdapter {
702
763
  // A missing fold entry is an authoritative empty fold, not a signal to
703
764
  // re-scan the whole history. Pass {} so latestTurnDurableProgressAt treats
704
765
  // the fold as present and skips the per-candidate fallback scans.
705
- return latestTurnDurableProgressAt(view, taskId, roleName, turnId, projected.turnFacts.get(turnId) ?? {});
766
+ return latestRunDurableProgressAt(view, taskId, roleName, runId, projected.runFacts.get(runId) ?? {});
706
767
  }
707
- recordRoleTurnDiagnostic(input) {
768
+ recordRoleRunDiagnostic(input) {
708
769
  return this.store.transaction((store) => {
709
770
  const task = store.getTask(input.taskId);
710
- const run = store.getActiveTurn(input.taskId, input.roleName);
711
- if (task === null || task.status !== "active" || task.executionGate.state !== "enabled"
712
- || run === null || run.id !== input.turnId || run.status !== "active") {
771
+ const run = store.getActiveRun(input.taskId, input.roleName);
772
+ if (task === null || run === null || !runPurposeAdmitsTaskState(run.purpose, task)
773
+ || run.id !== input.runId || run.status !== "active") {
713
774
  return "state-changed";
714
775
  }
715
- const latest = latestTurnEventTime(store.listEvents(input.taskId), TURN_DIAGNOSTIC_FINISHED_EVENT, input.turnId);
776
+ const latest = latestRunEventTime(store.listEvents(input.taskId), RUN_DIAGNOSTIC_FINISHED_EVENT, input.runId);
716
777
  if (latest !== undefined && Date.parse(latest) >= Date.parse(input.startedAt)) {
717
778
  return "already-recorded";
718
779
  }
719
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, TURN_DIAGNOSTIC_FINISHED_EVENT, {
720
- turnId: input.turnId,
780
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, RUN_DIAGNOSTIC_FINISHED_EVENT, {
781
+ runId: input.runId,
721
782
  roleName: input.roleName,
722
783
  outcome: input.outcome,
723
784
  startedAt: input.startedAt
@@ -725,22 +786,21 @@ export class FileSchedulerStoreAdapter {
725
786
  return "recorded";
726
787
  });
727
788
  }
728
- recordRoleTurnStall(input) {
789
+ recordRoleRunStall(input) {
729
790
  return this.store.transaction((store) => {
730
791
  const task = store.getTask(input.taskId);
731
792
  const role = store.getRole(input.taskId, input.roleName);
732
- const run = store.getActiveTurn(input.taskId, input.roleName);
793
+ const run = store.getActiveRun(input.taskId, input.roleName);
733
794
  if (task === null
734
- || task.status !== "active"
735
- || task.executionGate.state !== "enabled"
736
795
  || role === null
737
796
  || run === null
738
- || run.id !== input.turnId
797
+ || !runPurposeAdmitsTaskState(run.purpose, task)
798
+ || run.id !== input.runId
739
799
  || run.status !== "active"
740
800
  || run.effective.agentId !== input.agentId
741
801
  || run.effective.adapterId !== input.adapterId)
742
802
  return "state-changed";
743
- const progress = latestTurnDurableProgressAt(store, input.taskId, input.roleName, input.turnId);
803
+ const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
744
804
  if (progress?.progressAt !== input.progressAt)
745
805
  return "state-changed";
746
806
  const session = store.getRoleSession(input.taskId, input.roleName);
@@ -749,8 +809,8 @@ export class FileSchedulerStoreAdapter {
749
809
  const existing = latestStallEvidenceKey(store.listEvents(task.id), run.id);
750
810
  if (existing?.progressAt === input.progressAt)
751
811
  return "already-raised";
752
- const event = createTaskEvent(store.nextEventId(task.id), task.id, TURN_STALLED_EVENT, {
753
- turnId: run.id,
812
+ const event = createTaskEvent(store.nextEventId(task.id), task.id, RUN_STALLED_EVENT, {
813
+ runId: run.id,
754
814
  roleName: role.name,
755
815
  kind: input.kind,
756
816
  classification: input.classification,
@@ -763,15 +823,14 @@ export class FileSchedulerStoreAdapter {
763
823
  return "raised";
764
824
  });
765
825
  }
766
- recordRoleTurnProgress(input) {
826
+ recordRoleRunProgress(input) {
767
827
  return this.store.transaction((store) => {
768
828
  const task = store.getTask(input.taskId);
769
- const run = store.getActiveTurn(input.taskId, input.roleName);
829
+ const run = store.getActiveRun(input.taskId, input.roleName);
770
830
  if (task === null
771
- || task.status !== "active"
772
- || task.executionGate.state !== "enabled"
773
831
  || run === null
774
- || run.id !== input.turnId
832
+ || !runPurposeAdmitsTaskState(run.purpose, task)
833
+ || run.id !== input.runId
775
834
  || run.status !== "active")
776
835
  return "state-changed";
777
836
  const events = store.listEvents(task.id);
@@ -782,18 +841,18 @@ export class FileSchedulerStoreAdapter {
782
841
  && Date.parse(input.progressAt) <= Date.parse(previousStall.progressAt)) {
783
842
  return "already-recorded";
784
843
  }
785
- const existing = events.some((event) => (event.type === TURN_PROGRESS_EVENT
786
- && event.payload.turnId === run.id
844
+ const existing = events.some((event) => (event.type === RUN_PROGRESS_EVENT
845
+ && event.payload.runId === run.id
787
846
  && (event.payload.progressAt === input.progressAt
788
847
  || (typeof event.payload.progressAt === "string"
789
848
  && Number.isFinite(Date.parse(event.payload.progressAt))
790
849
  && Date.parse(event.payload.progressAt) >= Date.parse(input.progressAt)))));
791
850
  // Advisory 30-minute diagnostics are intentionally not lifecycle
792
851
  // episodes and therefore must never synthesize turn.recovered.
793
- const recovered = isRoleTurnStalled(events, run.id);
852
+ const recovered = isRoleRunStalled(events, run.id);
794
853
  if (!existing) {
795
- store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, TURN_PROGRESS_EVENT, {
796
- turnId: run.id,
854
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_PROGRESS_EVENT, {
855
+ runId: run.id,
797
856
  roleName: input.roleName,
798
857
  kind: "durable-fold",
799
858
  progressAt: input.progressAt,
@@ -801,8 +860,8 @@ export class FileSchedulerStoreAdapter {
801
860
  }, input.now));
802
861
  }
803
862
  if (recovered) {
804
- store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, TURN_RECOVERED_EVENT, {
805
- turnId: run.id,
863
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, {
864
+ runId: run.id,
806
865
  roleName: input.roleName,
807
866
  progressAt: input.progressAt,
808
867
  kind: "durable-progress"
@@ -813,35 +872,44 @@ export class FileSchedulerStoreAdapter {
813
872
  }
814
873
  beginAgentHostProviderTurn(input) {
815
874
  this.store.transaction((store) => {
875
+ const task = store.getTask(input.taskId);
876
+ if (task === null || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled"
877
+ || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({
878
+ scope: "task", taskId: input.taskId, roleName: input.roleName
879
+ })))) {
880
+ throw new AgentHostProviderTurnFenceError("Session execution admission is stopped or being replaced.");
881
+ }
816
882
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
817
883
  const session = sessions?.sessions[input.agentId];
818
884
  const binding = sessions?.providerBinding;
819
- const active = store.getActiveTurn(input.taskId, input.roleName);
820
- if (sessions === null || sessions === undefined
821
- || binding === null || binding === undefined
822
- || (input.turnId !== undefined
823
- && (active?.id !== input.turnId || active.effective.agentId !== input.agentId))
824
- || session?.runtimeGenerationId !== input.runtimeGenerationId
825
- || session.nativeSessionId !== input.nativeSessionId
826
- || currentProviderConversation(binding).conversationId !== input.nativeSessionId
827
- || binding.authority.owner !== input.authorityOwner
828
- || binding.authority.epoch !== input.authorityEpoch
829
- || binding.authority.holderId !== input.holderId) {
885
+ const active = store.getActiveRun(input.taskId, input.roleName);
886
+ if (sessions === null || sessions === undefined || binding === null || binding === undefined || (input.runId !== undefined && (active?.id !== input.runId || active.effective.agentId !== input.agentId)) || session === undefined || session.nativeSessionId !== input.nativeSessionId || currentProviderConversation(binding).conversationId !== input.nativeSessionId || binding.authority.owner !== input.authorityOwner || binding.authority.epoch !== input.authorityEpoch || binding.authority.holderId !== input.holderId) {
830
887
  throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
831
888
  }
832
- const currentTurn = binding.turn;
833
- const exactReplay = currentTurn !== null
834
- && currentTurn.turnId === input.turnId
835
- && currentTurn.attemptId === input.attemptId
836
- && currentTurn.authorityEpoch === input.authorityEpoch
837
- && currentTurn.status === "submitting";
838
- if (!exactReplay && binding.turn !== null
889
+ const currentRun = binding.run;
890
+ if (session.effective.executionEnvironment !== undefined) {
891
+ assertExecutionEnvironmentCurrent(store, input.taskId, session.effective.executionEnvironment);
892
+ }
893
+ if (input.runId !== undefined
894
+ && !isDeepStrictEqual(active?.effective.executionEnvironment, session.effective.executionEnvironment)) {
895
+ throw new AgentHostProviderTurnFenceError("AgentRun and native Session execution environments differ; start a new Session.");
896
+ }
897
+ if (input.runId !== undefined
898
+ && active?.effective.executionAuthority !== session.effective.executionAuthority) {
899
+ throw new AgentHostProviderTurnFenceError("Execution authority differs from the fixed native Session.");
900
+ }
901
+ const exactReplay = currentRun !== null
902
+ && currentRun.runId === input.runId
903
+ && currentRun.attemptId === input.attemptId
904
+ && currentRun.authorityEpoch === input.authorityEpoch
905
+ && currentRun.status === "submitting";
906
+ if (!exactReplay && binding.run !== null
839
907
  && ["submitting", "accepted", "delivery-unknown"]
840
- .includes(binding.turn.status)) {
841
- throw new AgentHostProviderTurnFenceError("Provider Conversation already has an unsettled Turn.");
908
+ .includes(binding.run.status)) {
909
+ throw new AgentHostProviderSessionBusyError("Provider Conversation already has an unsettled AgentRun.");
842
910
  }
843
911
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
844
- ...(input.turnId === undefined ? {} : { turnId: input.turnId }),
912
+ ...(input.runId === undefined ? {} : { runId: input.runId }),
845
913
  attemptId: input.attemptId,
846
914
  authorityEpoch: input.authorityEpoch,
847
915
  submittedAt: input.now.toISOString()
@@ -854,8 +922,8 @@ export class FileSchedulerStoreAdapter {
854
922
  const binding = sessions?.providerBinding;
855
923
  if (sessions === null || sessions === undefined
856
924
  || binding === null || binding === undefined
857
- || binding.turn?.turnId !== input.turnId
858
- || binding.turn?.attemptId !== input.attemptId) {
925
+ || binding.run?.runId !== input.runId
926
+ || binding.run?.attemptId !== input.attemptId) {
859
927
  throw new Error("Agent Host Provider Turn submission is no longer current.");
860
928
  }
861
929
  const updated = settleProviderTurnSubmission(binding, {
@@ -865,9 +933,9 @@ export class FileSchedulerStoreAdapter {
865
933
  resolvedAt: input.now.toISOString()
866
934
  });
867
935
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
868
- if (input.turnId === undefined)
936
+ if (input.runId === undefined)
869
937
  return;
870
- const run = store.getTurn(input.taskId, input.turnId);
938
+ const run = store.getRun(input.taskId, input.runId);
871
939
  const session = sessions.sessions[sessions.activeAgentId];
872
940
  const driver = run === null
873
941
  ? null
@@ -881,11 +949,11 @@ export class FileSchedulerStoreAdapter {
881
949
  }),
882
950
  message: input.reason,
883
951
  raw: input.raw,
884
- inputDisposition: input.status === "rejected" ? "not-accepted" : "unknown"
952
+ inputDisposition: input.status === "delivery-unknown" ? "unknown" : "not-accepted"
885
953
  });
886
954
  const errorEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
887
955
  sourceEventId: input.attemptId,
888
- turnId: input.turnId,
956
+ runId: input.runId,
889
957
  roleName: input.roleName,
890
958
  agentId: run?.effective.agentId ?? sessions.activeAgentId,
891
959
  adapterId: run?.effective.adapterId ?? session?.adapterId ?? "unknown",
@@ -895,7 +963,6 @@ export class FileSchedulerStoreAdapter {
895
963
  // string reads back as a real value and cannot be told apart from
896
964
  // one the Provider genuinely reported.
897
965
  ...optionalEventFields({
898
- runtimeGenerationId: session?.runtimeGenerationId,
899
966
  nativeSessionId: session?.nativeSessionId
900
967
  }),
901
968
  source: error.source,
@@ -908,6 +975,8 @@ export class FileSchedulerStoreAdapter {
908
975
  sessionDisposition: error.sessionDisposition
909
976
  }, input.now);
910
977
  store.saveEvent(input.taskId, errorEvent);
978
+ if (input.status === "deferred")
979
+ return;
911
980
  const route = input.roleName === "leader"
912
981
  ? { kind: "operator" }
913
982
  : { kind: "role", taskId: input.taskId, roleName: "leader" };
@@ -923,22 +992,15 @@ export class FileSchedulerStoreAdapter {
923
992
  const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
924
993
  const session = sessions?.sessions[input.agentId];
925
994
  const binding = sessions?.providerBinding;
926
- if (binding === null || binding === undefined
927
- || session?.runtimeGenerationId !== input.runtimeGenerationId
928
- || session.nativeSessionId !== input.nativeSessionId
929
- || currentProviderConversation(binding).conversationId !== input.nativeSessionId)
930
- return null;
931
- const activation = currentProviderActivation(binding);
932
- if (activation === null)
995
+ if (binding === null || binding === undefined || session === undefined || session.nativeSessionId !== input.nativeSessionId || currentProviderConversation(binding).conversationId !== input.nativeSessionId)
933
996
  return null;
934
997
  return {
935
998
  conversationId: currentProviderConversation(binding).conversationId,
936
- activationId: activation.activationId,
937
999
  ...binding.authority
938
1000
  };
939
1001
  }
940
- peekNextTurnId(taskId) {
941
- return this.store.peekNextTurnId(taskId);
1002
+ peekNextRunId(taskId) {
1003
+ return this.store.peekNextRunId(taskId);
942
1004
  }
943
1005
  getWorkMailbox(target) { return this.store.getWorkMailbox(target); }
944
1006
  listWorkMailboxes() { return this.store.listWorkMailboxes(); }
@@ -961,7 +1023,7 @@ export class FileSchedulerStoreAdapter {
961
1023
  }
962
1024
  recordAgentError(input, now) {
963
1025
  return this.store.transaction((store) => {
964
- const run = store.getTurn(input.taskId, input.turnId);
1026
+ const run = store.getRun(input.taskId, input.runId);
965
1027
  const sessionSet = store.getTaskRoleSessionSet(input.taskId, input.roleName);
966
1028
  const sessionAgentId = run?.effective.agentId ?? sessionSet?.activeAgentId;
967
1029
  const session = sessionAgentId === undefined
@@ -987,7 +1049,7 @@ export class FileSchedulerStoreAdapter {
987
1049
  : { sessionDisposition: input.sessionDisposition })
988
1050
  });
989
1051
  const duplicate = [...store.listEvents(input.taskId)].reverse().find((event) => (event.type === "runtime.agent-error"
990
- && event.payload.turnId === input.turnId
1052
+ && event.payload.runId === input.runId
991
1053
  && event.payload.roleName === input.roleName
992
1054
  && event.payload.phase === input.phase
993
1055
  && event.payload.attemptId === input.attemptId
@@ -996,8 +1058,6 @@ export class FileSchedulerStoreAdapter {
996
1058
  && event.payload.sessionDisposition === error.sessionDisposition
997
1059
  && event.payload.registrationDisposition === input.registrationDisposition
998
1060
  && event.payload.hostState === input.hostState
999
- && event.payload.expectedRuntimeGenerationId === input.expectedRuntimeGenerationId
1000
- && event.payload.observedRuntimeGenerationId === input.observedRuntimeGenerationId
1001
1061
  && (input.attemptId === undefined
1002
1062
  ? event.payload.raw === error.raw
1003
1063
  : event.payload.message === error.message
@@ -1009,8 +1069,8 @@ export class FileSchedulerStoreAdapter {
1009
1069
  if (duplicate !== undefined)
1010
1070
  return duplicate.id;
1011
1071
  const event = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
1012
- sourceEventId: `${input.turnId}:${input.phase}${input.attemptId === undefined ? "" : `:${input.attemptId}`}`,
1013
- turnId: input.turnId,
1072
+ sourceEventId: `${input.runId}:${input.phase}${input.attemptId === undefined ? "" : `:${input.attemptId}`}`,
1073
+ runId: input.runId,
1014
1074
  roleName: input.roleName,
1015
1075
  agentId: run?.effective.agentId ?? session?.agentId ?? "unknown",
1016
1076
  adapterId: run?.effective.adapterId ?? session?.adapterId ?? "unknown",
@@ -1028,32 +1088,40 @@ export class FileSchedulerStoreAdapter {
1028
1088
  // "the Host did not report this" from "the Host reported nothing".
1029
1089
  // The Session identities follow the same rule: this failure can
1030
1090
  // happen before any Session record exists, and there is no native
1031
- // Turn to name at all.
1091
+ // AgentRun to name at all.
1032
1092
  ...optionalEventFields({
1033
- runtimeGenerationId: session?.runtimeGenerationId,
1034
1093
  nativeSessionId: session?.nativeSessionId,
1035
1094
  errorName: input.errorName,
1036
1095
  causeName: input.causeName,
1037
1096
  hostState: input.hostState,
1038
- expectedRuntimeGenerationId: input.expectedRuntimeGenerationId,
1039
- observedRuntimeGenerationId: input.observedRuntimeGenerationId,
1040
1097
  attemptId: input.attemptId,
1041
1098
  registrationDisposition: input.registrationDisposition
1042
1099
  })
1043
1100
  }, now);
1044
1101
  store.saveEvent(input.taskId, event);
1102
+ if (input.phase === "turn-submit" && error.inputDisposition === "unknown"
1103
+ && input.attemptId !== undefined && sessionSet?.providerBinding?.run?.attemptId === input.attemptId
1104
+ && sessionSet.providerBinding.run.status === "submitting") {
1105
+ // A pass clock can precede the asynchronous Host registration.
1106
+ const observedAt = new Date(Math.max(now.getTime(), Date.parse(sessionSet.providerBinding.run.updatedAt)));
1107
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessionSet, settleProviderTurnSubmission(sessionSet.providerBinding, { attemptId: input.attemptId,
1108
+ status: "delivery-unknown", reason: error.message, resolvedAt: observedAt.toISOString()
1109
+ }), observedAt));
1110
+ }
1111
+ if (input.errorName === "ProviderTurnBusyError" && error.inputDisposition === "not-accepted")
1112
+ return event.id;
1045
1113
  const leaderCannotReceive = input.roleName === "leader"
1046
1114
  && ["host-start", "session-start", "session-restore", "turn-submit"].includes(input.phase);
1047
1115
  if (!leaderCannotReceive) {
1048
1116
  enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, wakeReason("agent-error", event.id), now, [{ type: "event", taskId: input.taskId, id: event.id }], {
1049
1117
  source: driver?.id ?? input.source,
1050
- dedupeKey: `agent-error:${input.taskId}:${input.turnId}:${input.phase}:${event.id}`
1118
+ dedupeKey: `agent-error:${input.taskId}:${input.runId}:${input.phase}:${event.id}`
1051
1119
  });
1052
1120
  }
1053
1121
  if (leaderCannotReceive) {
1054
1122
  enqueueWork(store, { kind: "operator" }, "leader-agent-error", now, [{ type: "event", taskId: input.taskId, id: event.id }], {
1055
1123
  source: driver?.id ?? input.source,
1056
- dedupeKey: `leader-agent-error:${input.taskId}:${input.turnId}:${event.id}`
1124
+ dedupeKey: `leader-agent-error:${input.taskId}:${input.runId}:${event.id}`
1057
1125
  });
1058
1126
  }
1059
1127
  return event.id;
@@ -1063,7 +1131,7 @@ export class FileSchedulerStoreAdapter {
1063
1131
  * Records that a Leader wake was suppressed by scheduler single-flight
1064
1132
  * (the Role runtime lifecycle lane was busy). The wake stays durable and
1065
1133
  * is retried after the lane settles; this event is the audit trail that
1066
- * separates scheduler backpressure from real Turn failures.
1134
+ * separates scheduler backpressure from real AgentRun failures.
1067
1135
  */
1068
1136
  recordWakeSuppression(taskId, reason, now) {
1069
1137
  this.store.transaction((store) => {
@@ -1130,8 +1198,8 @@ export class FileSchedulerStoreAdapter {
1130
1198
  return { status: "claimed", processing: claimed.processing };
1131
1199
  });
1132
1200
  }
1133
- settleRoleTurnDispatch(input) {
1134
- return this.store.transaction((store) => (settleRoleTurnDispatchMailbox(store, input, input.expected)));
1201
+ settleRoleRunDispatch(input) {
1202
+ return this.store.transaction((store) => (settleRoleRunDispatchMailbox(store, input, input.expected)));
1135
1203
  }
1136
1204
  completeWorkMailbox(target, batchId) {
1137
1205
  return this.store.transaction((store) => {
@@ -1165,16 +1233,15 @@ export class FileSchedulerStoreAdapter {
1165
1233
  if (mailbox === null || disposition === null)
1166
1234
  return false;
1167
1235
  if (mailbox.processing !== null) {
1168
- if (!isRuntimeLaunchReservation(mailbox.processing)
1169
- && !mailbox.processing.batch.reasons.every(isRuntimeCleanupReason)) {
1236
+ if (disposition !== "replace-session" && !mailbox.processing.batch.reasons.every(isRuntimeCleanupReason)) {
1170
1237
  return false;
1171
1238
  }
1172
1239
  mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
1173
1240
  }
1174
1241
  const pending = mailbox.pending;
1175
1242
  if (pending !== null) {
1176
- if (pending.reasons.length === 0
1177
- || !pending.reasons.every(isRuntimeCleanupReason)) {
1243
+ if (disposition !== "replace-session" && (pending.reasons.length === 0
1244
+ || !pending.reasons.every(isRuntimeCleanupReason))) {
1178
1245
  return false;
1179
1246
  }
1180
1247
  const batchId = `runtime-cleanup-complete:${pending.fromSequence}-${pending.toSequence}`;
@@ -1185,9 +1252,46 @@ export class FileSchedulerStoreAdapter {
1185
1252
  }), batchId);
1186
1253
  }
1187
1254
  const owner = runtimeOwnerFromTarget(target);
1188
- if (disposition === "end-session") {
1255
+ if (disposition !== "detach-host" && owner.scope === "task") {
1256
+ // Physical quiescence was proven by stopOwner. Runs are engineering
1257
+ // attempts: cancel only this Role, never Task intent or other workers.
1258
+ cancelQuiescentRoleRuns(store, owner.taskId, owner.roleName, "Session execution stopped on request; durable Task context and workspace progress were preserved.", now);
1259
+ let set = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
1260
+ if (set !== null) {
1261
+ // Late observations cannot resurrect engineering occupancy after
1262
+ // the exact native/process stop just completed.
1263
+ if (set.providerBinding !== null) {
1264
+ let binding = set.providerBinding;
1265
+ if (binding.run !== null)
1266
+ binding = cancelQuiescentProviderInput(binding, {
1267
+ attemptId: binding.run.attemptId, cancelledAt: now.toISOString(),
1268
+ reason: "Session replacement after verified resource stop."
1269
+ });
1270
+ set = updateTaskRoleProviderRuntime(set, clearProviderGoal(binding), now);
1271
+ }
1272
+ store.saveTaskRoleSessionSet(set);
1273
+ }
1189
1274
  endRuntimeOwnerSession(store, owner, now);
1190
1275
  }
1276
+ if (disposition === "replace-session" && owner.scope === "task") {
1277
+ const set = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
1278
+ if (set !== null)
1279
+ store.saveTaskRoleSessionSet(selectNewTaskRoleSession(set, set.activeAgentId, now));
1280
+ const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "runtime.session-replaced", { roleName: owner.roleName }, now);
1281
+ store.saveEvent(owner.taskId, event);
1282
+ // Accepted notification batches are already consumed; unresolved old
1283
+ // claims belong to the discarded runtime, not to its successor.
1284
+ const leaderTarget = { kind: "role", taskId: owner.taskId, roleName: "leader" };
1285
+ const leaderMailbox = store.getWorkMailbox(leaderTarget);
1286
+ if (owner.roleName === "leader" && leaderMailbox?.processing?.owner.startsWith("leader-notification:")) {
1287
+ store.saveWorkMailbox(releaseProcessing(leaderMailbox, leaderMailbox.processing.batchId));
1288
+ }
1289
+ enqueueWork(store, leaderTarget, "session-replaced", now, [{ type: "event", taskId: owner.taskId, id: event.id }]);
1290
+ }
1291
+ else if (disposition === "end-session") {
1292
+ if (owner.scope === "global")
1293
+ endRuntimeOwnerSession(store, owner, now);
1294
+ }
1191
1295
  else {
1192
1296
  detachRuntimeOwnerHost(store, owner, now);
1193
1297
  }
@@ -1195,22 +1299,11 @@ export class FileSchedulerStoreAdapter {
1195
1299
  return true;
1196
1300
  });
1197
1301
  }
1198
- completeStoppedRuntimeReservation(target, batchId, now) {
1199
- return this.store.transaction((store) => {
1200
- const mailbox = store.getWorkMailbox(target);
1201
- if (!isRuntimeLaunchReservation(mailbox?.processing, batchId)) {
1202
- return false;
1203
- }
1204
- endRuntimeOwnerSession(store, runtimeOwnerFromTarget(target), now);
1205
- saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, batchId));
1206
- return true;
1207
- });
1208
- }
1209
1302
  listDormantRuntimeOwners() {
1210
1303
  return this.listRuntimeSessionCandidates().flatMap((candidate) => {
1211
1304
  if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(candidate.owner)))
1212
1305
  || (candidate.owner.scope === "task"
1213
- && this.store.getActiveTurn(candidate.owner.taskId, candidate.owner.roleName) !== null)) {
1306
+ && this.store.getActiveRun(candidate.owner.taskId, candidate.owner.roleName) !== null)) {
1214
1307
  return [];
1215
1308
  }
1216
1309
  return [{
@@ -1218,7 +1311,6 @@ export class FileSchedulerStoreAdapter {
1218
1311
  agentId: candidate.agentId,
1219
1312
  adapterId: candidate.adapterId,
1220
1313
  nativeSessionId: candidate.nativeSessionId,
1221
- ...(candidate.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: candidate.runtimeGenerationId }),
1222
1314
  sessionUpdatedAt: candidate.sessionUpdatedAt
1223
1315
  }];
1224
1316
  });
@@ -1226,11 +1318,11 @@ export class FileSchedulerStoreAdapter {
1226
1318
  listRuntimeSessionCandidates(query = {}) {
1227
1319
  return this.store.listRuntimeSessionCandidates(query);
1228
1320
  }
1229
- enqueueRuntimeCleanup(owner, now = new Date(), expectedDormantCandidate) {
1321
+ enqueueRuntimeCleanup(owner, now = new Date(), expectedDormantCandidate, allowActiveRun = false) {
1230
1322
  return this.store.transaction((store) => {
1231
1323
  if (expectedDormantCandidate !== undefined
1232
1324
  && (!sameRuntimeOwner(owner, expectedDormantCandidate.owner)
1233
- || !dormantRuntimeCandidateIsCurrent(store, expectedDormantCandidate))) {
1325
+ || !dormantRuntimeCandidateIsCurrent(store, expectedDormantCandidate, allowActiveRun))) {
1234
1326
  return null;
1235
1327
  }
1236
1328
  if (owner.scope === "task" && store.getTask(owner.taskId) === null) {
@@ -1263,122 +1355,169 @@ export class FileSchedulerStoreAdapter {
1263
1355
  }
1264
1356
  clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
1265
1357
  getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
1266
- saveLeaderDispatch(input) {
1267
- return this.store.transaction((store) => {
1268
- const task = store.getTask(input.task.id);
1269
- if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1270
- return "unavailable";
1271
- }
1272
- const role = requireRole(store, input.task.id, input.role.name);
1273
- const binding = activeRoleAgentBinding(role);
1274
- if (role.activeAgentId !== input.role.activeAgentId
1275
- || binding.adapterId !== input.role.adapterId
1276
- || binding.config.model !== input.role.model
1277
- || binding.config.effort !== input.role.effort
1278
- || role.workspace !== input.role.workspace) {
1279
- return "state-changed";
1280
- }
1281
- if (!isDeepStrictEqual(input.turn.effective, input.role.effective)) {
1282
- return "state-changed";
1283
- }
1284
- if (store.getTurn(input.task.id, input.turn.id) !== null)
1285
- return "state-changed";
1286
- if (store.getActiveTurn(input.task.id, input.role.name) !== null)
1287
- return "busy";
1288
- const pending = store.getPendingWakeup(input.task.id);
1289
- if (pending === null || !pendingWakeupsMatch(pending, input.wakeup)) {
1290
- return "state-changed";
1358
+ prepareMessageContinuations(taskId, now) {
1359
+ const roles = new Set(this.store.listMessages(taskId).flatMap((message) => message.recipient?.ownerRunId !== undefined && message.continuation?.runId === undefined ? [message.recipient.roleName] : []));
1360
+ for (const roleName of roles) {
1361
+ try {
1362
+ this.store.transaction((store) => prepareMessageContinuations(store, taskId, now, roleName));
1363
+ }
1364
+ catch (error) {
1365
+ const reason = `preparation-failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 1000);
1366
+ this.store.transaction((store) => {
1367
+ for (const message of store.listMessages(taskId)) {
1368
+ if (message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined && message.continuation?.runId === undefined
1369
+ && message.continuation?.notDeliveredReason !== reason) {
1370
+ store.updateMessage(taskId, { ...message, continuation: { notDeliveredReason: reason } });
1371
+ }
1372
+ }
1373
+ });
1291
1374
  }
1292
- const target = { kind: "role", taskId: input.task.id, roleName: input.role.name };
1375
+ }
1376
+ }
1377
+ /** The first Draft conversation is an explicit planning execution. Later
1378
+ * messages use the same notification path as direct Leader collaboration. */
1379
+ prepareDraftPlanning(taskId, now) {
1380
+ return this.store.transaction((store) => {
1381
+ const task = store.getTask(taskId);
1382
+ if (task?.status !== "draft" || task.executionGate.state !== "enabled")
1383
+ return false;
1384
+ if (store.getActiveRun(taskId, "leader") !== null)
1385
+ return true;
1386
+ if (store.listRuns(taskId).some(run => run.roleName === "leader" && run.purpose === "planning"))
1387
+ return false;
1388
+ const sessions = store.getTaskRoleSessionSet(taskId, "leader");
1389
+ if (activeLiveRoleAgentSession(sessions) !== null)
1390
+ return false;
1391
+ const role = requireRole(store, taskId, "leader");
1392
+ const target = { kind: "role", taskId, roleName: "leader" };
1293
1393
  const mailbox = store.getWorkMailbox(target);
1294
- if (mailbox === null || !mailboxHasPending(mailbox))
1295
- return "state-changed";
1296
- if (store.peekNextTurnId(input.task.id) !== input.turn.id) {
1297
- return "state-changed";
1298
- }
1299
- const allocatedTurnId = store.nextTurnId(input.task.id);
1300
- if (allocatedTurnId !== input.turn.id) {
1301
- throw new Error(`Leader Turn allocation changed unexpectedly: ${input.task.id}.`);
1302
- }
1303
- // The durable Turn row and its active pointer are separate projections;
1304
- // keep both writes in the same storage transaction.
1305
- store.saveTurn(input.turn);
1306
- store.saveActiveTurn(input.turn);
1307
- store.saveWorkMailbox(consumePendingBatch(mailbox));
1308
- store.clearPendingWakeup(input.task.id);
1309
- store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "turn.dispatched", turnLaunchEventPayload(input.turn), input.now));
1310
- if (input.wakeId !== undefined && input.wakeFromCursor !== undefined) {
1311
- if (store.peekNextTaskWakeId(input.task.id) !== input.wakeId) {
1312
- return "state-changed";
1313
- }
1314
- const allocatedWakeId = store.nextTaskWakeId(input.task.id);
1315
- if (allocatedWakeId !== input.wakeId) {
1316
- throw new Error(`Leader TaskWake allocation changed unexpectedly: ${input.task.id}.`);
1317
- }
1318
- store.saveTaskWake(input.task.id, createTaskWake({
1319
- id: allocatedWakeId,
1320
- taskId: input.task.id,
1321
- reasons: input.wakeup.reasons,
1322
- fromCursor: input.wakeFromCursor,
1323
- toCursor: input.now.toISOString(),
1324
- turnId: input.turn.id,
1325
- now: input.now
1326
- }));
1327
- }
1328
- if (input.turn.mode !== "new"
1329
- && input.session !== null
1330
- && input.session.nativeSessionId !== undefined) {
1331
- saveTaskSession(store, role, {
1332
- ...input.session,
1333
- nativeSessionId: input.session.nativeSessionId
1334
- }, "active", input.now);
1335
- }
1336
- store.clearLeaderFailure(input.task.id);
1337
- return "claimed";
1394
+ if (mailbox?.pending == null || mailbox.processing !== null)
1395
+ return false;
1396
+ const run = createRun(store.nextRunId(taskId), taskId, "leader", "new", createRunInput({ source: { type: "yui", channel: "task-dispatch" },
1397
+ directive: `Plan Task ${taskId} with the user. Persist requirements and decisions. Request activation explicitly; planning grants no delivery authority.`,
1398
+ deltaRefIds: [] }), now, {
1399
+ purpose: "planning", effective: resolveEffectiveLaunch({ role, purpose: "planning" })
1400
+ });
1401
+ const snapshot = freezeRunContextSnapshot(store, run, now);
1402
+ const frozen = withRunContextSnapshot(run, contextSnapshotRef(snapshot));
1403
+ store.saveRun(frozen);
1404
+ store.saveActiveRun(frozen);
1405
+ // Freeze the initial notification prefix against this planning Run.
1406
+ // Later messages remain pending; acceptance or an exact terminal settles
1407
+ // only this batch, so a failed initial launch cannot replay it forever.
1408
+ const batchId = formatRunReceiptId(taskId, run.id);
1409
+ store.saveWorkMailbox(bindExecution(claimPending(mailbox, {
1410
+ batchId, owner: `planning:${run.id}`, startedAt: now.toISOString()
1411
+ }), batchId, { type: "run", taskId, id: run.id }));
1412
+ return true;
1338
1413
  });
1339
1414
  }
1340
- saveLeaderSteer(input) {
1415
+ claimLeaderNotification(taskId, now) {
1341
1416
  return this.store.transaction((store) => {
1342
- const task = store.getTask(input.taskId);
1343
- const active = store.getActiveTurn(input.taskId, "leader");
1344
- if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1345
- return "unavailable";
1417
+ const task = store.getTask(taskId);
1418
+ if (task == null || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled")
1419
+ return null;
1420
+ const target = { kind: "role", taskId, roleName: "leader" };
1421
+ let mailbox = store.getWorkMailbox(target);
1422
+ if (mailbox === null)
1423
+ return null;
1424
+ const provider = store.getTaskRoleSessionSet(taskId, "leader")?.providerBinding?.run;
1425
+ const processing = mailbox.processing;
1426
+ if (processing !== null) {
1427
+ if (!processing.owner.startsWith("leader-notification:"))
1428
+ return null;
1429
+ const wakeId = processing.owner.slice("leader-notification:".length);
1430
+ const attemptId = processing.batchId;
1431
+ const previous = store.listEvents(taskId).filter((event) => event.type === "notification.delivery" && event.payload.attemptId === attemptId).at(-1);
1432
+ const native = provider?.attemptId === attemptId ? provider : undefined;
1433
+ const accepted = store.listEvents(taskId).some((event) => {
1434
+ const observation = runtimeObservationFromTaskEvent(event);
1435
+ return observation?.fence.roleName === "leader"
1436
+ && observation.fence.receiptId === attemptId
1437
+ && ((observation.kind === "turn.accepted" && observation.authority !== "transport")
1438
+ || isRunTerminalObservation(observation));
1439
+ });
1440
+ if (accepted || native?.status === "accepted" || previous?.payload.outcome === "accepted") {
1441
+ this.settleLeaderNotification(taskId, attemptId, "accepted", now);
1442
+ return null;
1443
+ }
1444
+ if (native?.status === "rejected" || previous?.payload.outcome === "rejected") {
1445
+ this.settleLeaderNotification(taskId, attemptId, "rejected", now);
1446
+ return null;
1447
+ }
1448
+ if (native?.status === "submitting")
1449
+ return { wakeId, attemptId, disposition: "pending" };
1450
+ if (native?.status !== "deferred" && previous?.payload.outcome !== "deferred") {
1451
+ this.settleLeaderNotification(taskId, attemptId, "unknown", now, "Acceptance could not be recovered. Preserve the fixed wake; no automatic replay.");
1452
+ return { wakeId, attemptId, disposition: "unknown" };
1453
+ }
1454
+ if (provider !== null && provider !== undefined && provider.attemptId !== attemptId
1455
+ && ["submitting", "accepted", "delivery-unknown"].includes(provider.status)) {
1456
+ return { wakeId, attemptId, disposition: "pending" };
1457
+ }
1458
+ const nextAttempt = `notification:${taskId}/${wakeId}/${randomUUID()}`;
1459
+ store.saveWorkMailbox({ ...mailbox, processing: { ...processing, batchId: nextAttempt } });
1460
+ return { wakeId, attemptId: nextAttempt, disposition: "submit" };
1346
1461
  }
1347
- if (active === null || active.id !== input.turnId || active.status !== "active")
1348
- return "busy";
1349
- const target = { kind: "role", taskId: input.taskId, roleName: "leader" };
1462
+ if (mailbox.pending === null || (provider !== null && provider !== undefined
1463
+ && ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
1464
+ return null;
1465
+ const latest = latestTaskWake(store.listTaskWakes(taskId));
1466
+ const wakeId = store.nextTaskWakeId(taskId);
1467
+ const wake = createTaskWake({
1468
+ id: wakeId, taskId, reasons: mailbox.pending.reasons,
1469
+ refs: mailbox.pending.refs,
1470
+ fromCursor: latest?.toCursor ?? task.createdAt,
1471
+ toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now
1472
+ });
1473
+ const attemptId = `notification:${taskId}/${wakeId}/${randomUUID()}`;
1474
+ mailbox = claimPending(mailbox, { batchId: attemptId,
1475
+ owner: `leader-notification:${wakeId}`, startedAt: now.toISOString() });
1476
+ store.saveTaskWake(taskId, wake);
1477
+ store.saveWorkMailbox(mailbox);
1478
+ return { wakeId, attemptId, disposition: "submit" };
1479
+ });
1480
+ }
1481
+ settleLeaderNotification(taskId, attemptId, outcome, now, detail) {
1482
+ this.store.transaction((store) => {
1483
+ const target = { kind: "role", taskId, roleName: "leader" };
1350
1484
  const mailbox = store.getWorkMailbox(target);
1351
1485
  const processing = mailbox?.processing;
1352
- if (mailbox === null
1353
- || processing?.batchId !== input.batchId
1354
- || processing.owner !== `leader-steer:${input.turnId}`)
1355
- return "state-changed";
1356
- const updated = appendTurnInput(active, input.input, input.now);
1357
- store.saveTurn(updated);
1358
- store.saveActiveTurn(updated);
1359
- store.saveWorkMailbox(completeProcessing(mailbox, input.batchId));
1360
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "turn.input-submitted", {
1361
- turnId: active.id,
1362
- sequence: String(updated.inputs.length),
1363
- source: `${input.input.source.type}/${input.input.source.channel}`,
1364
- reasons: processing.batch.reasons.join(",")
1365
- }, input.now));
1366
- return "claimed";
1486
+ if (mailbox == null || processing == null || processing.batchId !== attemptId
1487
+ || !processing.owner.startsWith("leader-notification:"))
1488
+ return;
1489
+ if (!store.listEvents(taskId).some((event) => event.type === "notification.delivery"
1490
+ && event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
1491
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "notification.delivery", { attemptId, outcome, ...(detail === undefined ? {} : { detail }) }, now));
1492
+ }
1493
+ if (outcome !== "accepted" && outcome !== "rejected")
1494
+ return;
1495
+ store.saveWorkMailbox(completeProcessing(mailbox, attemptId));
1496
+ if (outcome === "accepted") {
1497
+ const wakeId = processing.owner.slice("leader-notification:".length);
1498
+ const wake = store.listTaskWakes(taskId).find((entry) => entry.id === wakeId);
1499
+ if (wake !== undefined)
1500
+ store.saveTaskWake(taskId, markTaskWakeConsumed(wake, now));
1501
+ }
1367
1502
  });
1368
1503
  }
1369
- saveRoleTurnPrepared(input) {
1504
+ saveRoleRunPrepared(input) {
1370
1505
  this.store.transaction((store) => {
1371
1506
  const task = store.getTask(input.task.id);
1372
- if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
1373
- throw new Error(`Task is not active: ${input.task.id}.`);
1374
- }
1375
1507
  const role = requireRole(store, input.task.id, input.role.name);
1376
- const active = store.getActiveTurn(input.task.id, input.role.name);
1377
- if (active === null || active.id !== input.turn.id) {
1378
- throw new Error(`Active Turn changed before preparation was persisted: ${input.turn.id}.`);
1508
+ const active = store.getActiveRun(input.task.id, input.role.name);
1509
+ if (active === null || active.id !== input.run.id) {
1510
+ throw new Error(`Active AgentRun changed before preparation was persisted: ${input.run.id}.`);
1511
+ }
1512
+ // The Turn's own purpose decides which Task lifecycle admits it, so a
1513
+ // planning Turn on a Draft persists its Session while every other purpose
1514
+ // still requires an active Task. Read the durable active Turn, never the
1515
+ // caller's copy, so admission cannot be widened by a stale input.
1516
+ if (task === null || !runPurposeAdmitsTaskState(active.purpose, task)) {
1517
+ throw new Error(`Task is not active: ${input.task.id}.`);
1379
1518
  }
1380
1519
  if (store.getTaskRoleSessionSet(input.task.id, input.role.name) === null) {
1381
- store.saveTaskRoleSessionSet(createRoleSessionSet({ scope: "task", taskId: input.task.id, roleName: input.role.name }, input.turn.effective.agentId, input.now));
1520
+ store.saveTaskRoleSessionSet(createRoleSessionSet({ scope: "task", taskId: input.task.id, roleName: input.role.name }, input.run.effective.agentId, input.now));
1382
1521
  }
1383
1522
  if (input.session !== null && input.session.nativeSessionId !== undefined) {
1384
1523
  const existing = store.getRoleSession(input.task.id, input.role.name);
@@ -1390,63 +1529,61 @@ export class FileSchedulerStoreAdapter {
1390
1529
  && existing !== null
1391
1530
  && existing.nativeSessionId !== input.session.nativeSessionId
1392
1531
  && existing.status === "active";
1393
- if (!defersConversationReplacement
1394
- && (existing?.nativeSessionId !== input.session.nativeSessionId
1395
- || (input.runtimeGenerationId !== undefined && existing.runtimeGenerationId !== input.runtimeGenerationId)
1396
- || existing.status !== "active")) {
1532
+ if (!defersConversationReplacement && (existing?.nativeSessionId !== input.session.nativeSessionId || existing.status !== "active")) {
1397
1533
  saveTaskSession(store, role, {
1398
1534
  ...input.session,
1399
1535
  nativeSessionId: input.session.nativeSessionId
1400
- }, "active", input.now, input.runtimeGenerationId);
1536
+ }, "active", input.now);
1537
+ }
1538
+ if (!defersConversationReplacement && !store.listEvents(input.task.id).some((event) => event.type === "run.session-prepared" && event.payload.runId === input.run.id
1539
+ && event.payload.nativeSessionId === input.session.nativeSessionId)) {
1540
+ store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "run.session-prepared", { runId: input.run.id, roleName: role.name,
1541
+ nativeSessionId: input.session.nativeSessionId }, input.now));
1401
1542
  }
1402
1543
  }
1403
1544
  });
1404
1545
  }
1405
- saveRoleTurnDeliveryFailure(input) {
1546
+ saveRoleRunDeliveryFailure(input) {
1406
1547
  return this.store.transaction((store) => {
1407
1548
  const task = store.getTask(input.taskId);
1408
1549
  const role = store.getRole(input.taskId, input.roleName);
1409
- const active = store.getActiveTurn(input.taskId, input.roleName);
1550
+ const active = store.getActiveRun(input.taskId, input.roleName);
1410
1551
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1411
1552
  const session = sessions?.sessions[input.agentId];
1412
1553
  if (task === null
1413
- || task.status !== "active"
1414
- || task.executionGate.state !== "enabled"
1415
- || role === null
1416
1554
  || active === null
1417
- || active.id !== input.turnId
1555
+ || !runPurposeAdmitsTaskState(active.purpose, task)
1556
+ || role === null
1557
+ || active.id !== input.runId
1418
1558
  || active.status !== "active"
1419
1559
  || active.effective.agentId !== input.agentId
1420
1560
  || active.effective.adapterId !== input.adapterId) {
1421
1561
  return "state-changed";
1422
1562
  }
1423
1563
  const summary = input.summary
1424
- ?? `Role delivery failed conclusively before exact Turn input acceptance: ${input.turnId}.`;
1425
- const result = terminalizeExactTaskTurn(store, {
1564
+ ?? `Role delivery failed conclusively before exact AgentRun input acceptance: ${input.runId}.`;
1565
+ const result = terminalizeExactTaskRun(store, {
1426
1566
  taskId: input.taskId,
1427
1567
  roleName: input.roleName,
1428
1568
  agentId: input.agentId,
1429
- turnId: input.turnId,
1569
+ runId: input.runId,
1430
1570
  ...(session?.nativeSessionId === undefined
1431
1571
  ? {}
1432
1572
  : { nativeSessionId: session.nativeSessionId }),
1433
- ...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId }),
1434
1573
  outcome: { status: "failed", diagnostic: summary, failureReason: input.failureReason }
1435
1574
  }, input.now);
1436
- if (result.disposition !== "applied" || result.turn === null) {
1575
+ if (result.disposition !== "applied" || result.run === null) {
1437
1576
  return "state-changed";
1438
1577
  }
1439
- const terminal = result.turn;
1578
+ const terminal = result.run;
1440
1579
  const deliveryFailureEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.role-delivery-failed", {
1441
- turnId: terminal.id,
1580
+ runId: terminal.id,
1442
1581
  roleName: input.roleName,
1443
1582
  outcome: terminal.status
1444
1583
  }, input.now);
1445
1584
  store.saveEvent(input.taskId, deliveryFailureEvent);
1446
- if (input.roleName !== "leader") {
1447
- routeRoleEvent(store, deliveryFailureEvent, input.roleName, terminal.purpose === "review" ? "review-failed" : "role-turn-failed", input.now);
1448
- }
1449
- else {
1585
+ routeRoleEvent(store, deliveryFailureEvent, input.roleName, terminal.purpose === "review" ? "review-failed" : "role-turn-failed", input.now);
1586
+ if (input.roleName === "leader") {
1450
1587
  store.saveLeaderFailure(recordLeaderFailure(input.taskId, session?.nativeSessionId ?? "(unregistered)", summary, input.now, store.getLeaderFailure(input.taskId)));
1451
1588
  }
1452
1589
  return "failed";
@@ -1456,73 +1593,9 @@ export class FileSchedulerStoreAdapter {
1456
1593
  recordRuntimeNativeSession(input, now = new Date()) {
1457
1594
  return this.store.transaction((store) => (recordTaskRuntimeNativeSession(store, input, now)));
1458
1595
  }
1459
- reserveRuntimeLaunch(input, assertCurrent, now = new Date()) {
1596
+ recordLaunchedRuntimeNativeSession(input, assertCurrent, now = new Date()) {
1460
1597
  return this.store.transaction((store) => {
1461
1598
  assertCurrent();
1462
- const target = runtimeLifecycleTarget(input.owner);
1463
- const existing = store.getWorkMailbox(target);
1464
- if (isRuntimeLaunchReservation(existing?.processing)) {
1465
- if (hasRuntimeCleanupObligation(existing)) {
1466
- throw new Error("Runtime cleanup is still pending.");
1467
- }
1468
- return {
1469
- status: "existing",
1470
- runtimeGenerationId: existing.processing.batchId
1471
- };
1472
- }
1473
- if (hasRuntimeLifecycleWork(existing)) {
1474
- throw new RuntimeLifecycleBusyError("Runtime lifecycle work is already pending.");
1475
- }
1476
- enqueueWork(store, target, RUNTIME_LAUNCH_RESERVED_REASON, now, input.owner.scope === "task"
1477
- ? [{ type: "task", id: input.owner.taskId }]
1478
- : []);
1479
- const queued = store.getWorkMailbox(target);
1480
- if (queued === null
1481
- || queued.pending === null
1482
- || queued.processing !== null) {
1483
- throw new Error("Runtime launch reservation could not be queued.");
1484
- }
1485
- const claimed = claimPending(queued, {
1486
- batchId: input.runtimeGenerationId,
1487
- owner: RUNTIME_LIFECYCLE_OWNER,
1488
- startedAt: now.toISOString()
1489
- });
1490
- store.saveWorkMailbox(claimed);
1491
- return {
1492
- status: "reserved",
1493
- runtimeGenerationId: input.runtimeGenerationId
1494
- };
1495
- });
1496
- }
1497
- confirmRuntimeLaunchReservation(input, assertCurrent) {
1498
- return this.store.transaction((store) => {
1499
- assertCurrent();
1500
- const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(input.owner));
1501
- if (isRuntimeLaunchReservation(mailbox?.processing, input.runtimeGenerationId)) {
1502
- return "reserved";
1503
- }
1504
- if (hasRuntimeCleanupObligation(mailbox)) {
1505
- throw new Error("Runtime cleanup is still pending.");
1506
- }
1507
- // A synchronous Driver Hook may bind a fresh native Session
1508
- // and complete the reservation while the host start call is still
1509
- // unwinding. Preserve that exact hook-won generation instead of treating
1510
- // the already-settled reservation as a launch failure.
1511
- const sessions = runtimeOwnerSessionSet(store, input.owner);
1512
- const active = sessions?.sessions[sessions.activeAgentId];
1513
- if (active?.status === "active"
1514
- && active.runtimeGenerationId === input.runtimeGenerationId)
1515
- return "provider-bound";
1516
- throw new Error("Runtime launch reservation no longer matches the launch.");
1517
- });
1518
- }
1519
- recordReservedRuntimeNativeSession(input, assertCurrent, now = new Date()) {
1520
- return this.store.transaction((store) => {
1521
- assertCurrent();
1522
- const mailbox = requireRuntimeLaunchReservation(store, input.owner, input.runtimeGenerationId);
1523
- if (hasRuntimeCleanupObligation(mailbox)) {
1524
- throw new Error("Runtime cleanup is still pending.");
1525
- }
1526
1599
  const session = input.owner.scope === "task"
1527
1600
  ? recordTaskRuntimeNativeSession(store, {
1528
1601
  taskId: input.owner.taskId,
@@ -1530,7 +1603,6 @@ export class FileSchedulerStoreAdapter {
1530
1603
  agentId: input.agentId,
1531
1604
  adapterId: input.adapterId,
1532
1605
  nativeSessionId: input.nativeSessionId,
1533
- runtimeGenerationId: input.runtimeGenerationId,
1534
1606
  effective: input.effective
1535
1607
  }, now)
1536
1608
  : recordGlobalRuntimeNativeSession(store, {
@@ -1538,61 +1610,39 @@ export class FileSchedulerStoreAdapter {
1538
1610
  agentId: input.agentId,
1539
1611
  adapterId: input.adapterId,
1540
1612
  nativeSessionId: input.nativeSessionId,
1541
- runtimeGenerationId: input.runtimeGenerationId,
1542
1613
  effective: input.effective
1543
1614
  }, now);
1544
- saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, input.runtimeGenerationId));
1545
- return session;
1546
- });
1547
- }
1548
- completeRuntimeLaunchReservation(owner, runtimeGenerationId, beforeComplete) {
1549
- return this.store.transaction((store) => {
1550
- const target = runtimeLifecycleTarget(owner);
1551
- const mailbox = store.getWorkMailbox(target);
1552
- if (!isRuntimeLaunchReservation(mailbox?.processing, runtimeGenerationId))
1553
- return false;
1554
- beforeComplete?.();
1555
- saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, runtimeGenerationId));
1556
- return true;
1557
- });
1558
- }
1559
- /**
1560
- * Atomically settles a launch whose host is confirmed absent.
1561
- *
1562
- * The exact reservation is authoritative. If its matching Hook won the
1563
- * transaction race and already cleared that reservation, the session
1564
- * fallback is permitted only while the same Agent/native identity remains
1565
- * current and no later lifecycle work or Task Turn exists.
1566
- */
1567
- settleStoppedRuntimeLaunch(input, now = new Date()) {
1568
- return this.store.transaction((store) => {
1569
- const target = runtimeLifecycleTarget(input.owner);
1570
- const mailbox = store.getWorkMailbox(target);
1571
- if (isRuntimeLaunchReservation(mailbox?.processing, input.runtimeGenerationId)) {
1572
- const sessions = runtimeOwnerSessionSet(store, input.owner);
1573
- const active = sessions?.sessions[sessions.activeAgentId];
1574
- if (runtimeSessionMatchesSettledLaunch(active, input)) {
1575
- endRuntimeOwnerSession(store, input.owner, now);
1615
+ if (input.owner.scope === "task") {
1616
+ let sessions = store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName);
1617
+ if (sessions !== null && sessions.providerBinding === null) {
1618
+ sessions = bindTaskRoleProviderRuntime(sessions, createProviderRuntimeBinding({
1619
+ providerNamespace: this.drivers.requireByAdapterId(input.adapterId).id,
1620
+ accountScope: input.agentId, conversationId: input.nativeSessionId,
1621
+ startedAt: now.toISOString()
1622
+ }), now);
1623
+ store.saveTaskRoleSessionSet(sessions);
1624
+ }
1625
+ else if (sessions?.providerBinding != null
1626
+ && currentProviderConversation(sessions.providerBinding).conversationId !== input.nativeSessionId) {
1627
+ sessions = updateTaskRoleProviderRuntime(sessions, supersedeProviderConversation(sessions.providerBinding, {
1628
+ conversationId: input.nativeSessionId, switchedAt: now.toISOString(), basis: "terminal-session"
1629
+ }), now);
1630
+ store.saveTaskRoleSessionSet(sessions);
1631
+ }
1632
+ const binding = sessions?.providerBinding;
1633
+ if (sessions !== null && binding !== null && binding !== undefined
1634
+ && binding.authority.owner === "none"
1635
+ && currentProviderConversation(binding).conversationId === input.nativeSessionId) {
1636
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, transferProviderAuthority(binding, {
1637
+ expectedEpoch: binding.authority.epoch,
1638
+ expectedOwner: "none",
1639
+ owner: "controller",
1640
+ holderId: "controller",
1641
+ changedAt: now.toISOString()
1642
+ }), now));
1576
1643
  }
1577
- saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, input.runtimeGenerationId));
1578
- return true;
1579
- }
1580
- if (hasRuntimeLifecycleWork(mailbox))
1581
- return false;
1582
- if (input.owner.scope === "task"
1583
- && store.getActiveTurn(input.owner.taskId, input.owner.roleName) !== null) {
1584
- return false;
1585
- }
1586
- const sessions = runtimeOwnerSessionSet(store, input.owner);
1587
- if (sessions === null)
1588
- return true;
1589
- const active = sessions.sessions[sessions.activeAgentId];
1590
- if (!runtimeSessionMatchesSettledLaunch(active, input))
1591
- return false;
1592
- if (active.status !== "ended") {
1593
- endRuntimeOwnerSession(store, input.owner, now);
1594
1644
  }
1595
- return true;
1645
+ return session;
1596
1646
  });
1597
1647
  }
1598
1648
  /**
@@ -1600,66 +1650,66 @@ export class FileSchedulerStoreAdapter {
1600
1650
  * retained as an intermediate child wait or recorded as a ready boundary
1601
1651
  * for later mailbox input. It never performs tmux, workspace, or Controller I/O.
1602
1652
  */
1603
- classifyRuntimeTurnTerminal(input) {
1653
+ classifyRuntimeRunTerminal(input) {
1604
1654
  return resolveTerminalExecution(this.store, input) === null ? "obsolete" : "apply";
1605
1655
  }
1606
- observeRuntimeTurnTerminal(input, now = new Date()) {
1656
+ observeRuntimeRunTerminal(input, now = new Date()) {
1607
1657
  // Resource inspection is bounded but external to SQLite. Revalidate the
1608
- // immutable Turn workspace after acquiring the write transaction.
1609
- const preparedTurn = resolveTerminalExecution(this.store, input)?.turn;
1610
- const workspace = preparedTurn?.status === "active"
1611
- && preparedTurn.executionGroupId !== undefined
1612
- && preparedTurn.executionLaneId !== undefined
1613
- && preparedTurn.workspace !== undefined
1658
+ // immutable AgentRun workspace after acquiring the write transaction.
1659
+ const preparedRun = resolveTerminalExecution(this.store, input)?.run;
1660
+ const workspace = preparedRun?.status === "active"
1661
+ && preparedRun.executionGroupId !== undefined
1662
+ && preparedRun.executionLaneId !== undefined
1663
+ && preparedRun.workspace !== undefined
1614
1664
  && input.outcome.status === "completed"
1615
- ? this.snapshotExecutionLaneWorkspace(this.store, preparedTurn.workspace)
1665
+ ? this.snapshotExecutionLaneWorkspace(this.store, preparedRun.workspace)
1616
1666
  : undefined;
1617
1667
  return this.store.transaction((store) => {
1618
1668
  const task = store.getTask(input.taskId);
1619
1669
  if (task === null)
1620
1670
  throw new Error(`Task not found: ${input.taskId}.`);
1621
1671
  if (task.status === "archived") {
1622
- throw new Error(`Cannot complete a runtime turn for unavailable Task: ${input.taskId}.`);
1672
+ throw new Error(`Cannot complete a runtime run for unavailable Task: ${input.taskId}.`);
1623
1673
  }
1624
1674
  const resolved = resolveTerminalExecution(store, input);
1625
1675
  if (resolved === null)
1626
1676
  throw new Error("Runtime terminal has no exact execution binding.");
1627
- const { turn: observedTurn, current: recordedProviderTurn, attemptId } = resolved;
1628
- if (!isDeepStrictEqual(preparedTurn?.workspace, observedTurn?.workspace)) {
1677
+ const { run: observedRun, current: recordedProviderTurn, attemptId } = resolved;
1678
+ if (!isDeepStrictEqual(preparedRun?.workspace, observedRun?.workspace)) {
1629
1679
  throw new Error("Runtime terminal workspace changed during result preparation.");
1630
1680
  }
1631
- if (workspace !== undefined && observedTurn?.workspace !== undefined
1632
- && !isDeepStrictEqual(store.getManagedWorkspace(observedTurn.workspace.owner), observedTurn.workspace)) {
1681
+ if (workspace !== undefined && observedRun?.workspace !== undefined
1682
+ && !isDeepStrictEqual(store.getManagedWorkspace(observedRun.workspace.owner), observedRun.workspace)) {
1633
1683
  throw new Error("Runtime terminal workspace ownership changed during result preparation.");
1634
1684
  }
1635
1685
  let sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName)
1636
1686
  ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
1637
- const canonicalTurnId = input.nativeTurnId ?? resolved.nativeTurnId;
1687
+ const canonicalRunId = input.nativeTurnId ?? resolved.nativeTurnId;
1638
1688
  const existing = sessions.sessions[input.agentId];
1639
1689
  const owner = {
1640
1690
  scope: "task",
1641
1691
  taskId: input.taskId,
1642
1692
  roleName: input.roleName
1643
1693
  };
1644
- assertConsistentTerminal(store, input, observedTurn);
1694
+ assertConsistentTerminal(store, input, observedRun);
1645
1695
  // A historical operation failure stays failed. The immutable terminal
1646
1696
  // observation retains the late report for explicit Leader adoption.
1647
1697
  // Never touch a successor Session, active pointer, mailbox or Review.
1648
- if (!recordedProviderTurn || (observedTurn !== null && observedTurn.status !== "active")) {
1698
+ if (!recordedProviderTurn || (observedRun !== null && observedRun.status !== "active")) {
1649
1699
  // A failed operation and a subsequently confirmed Provider outcome are
1650
1700
  // distinct facts. Settle only the still-owned original attempt, never
1651
1701
  // rewriting the historical Task result or a successor's binding.
1652
- if (recordedProviderTurn && sessions.providerBinding?.turn !== null
1653
- && sessions.providerBinding?.turn !== undefined
1654
- && ["submitting", "accepted", "delivery-unknown"].includes(sessions.providerBinding.turn.status)) {
1655
- if (sessions.providerBinding.turn.status !== "accepted") {
1702
+ if (recordedProviderTurn && sessions.providerBinding?.run !== null
1703
+ && sessions.providerBinding?.run !== undefined
1704
+ && ["submitting", "accepted", "delivery-unknown"].includes(sessions.providerBinding.run.status)) {
1705
+ if (sessions.providerBinding.run.status !== "accepted") {
1656
1706
  sessions = updateTaskRoleProviderRuntime(sessions, acceptProviderTurn(sessions.providerBinding, {
1657
1707
  attemptId,
1658
- ...(canonicalTurnId === undefined ? {} : { nativeTurnId: canonicalTurnId }),
1708
+ ...(canonicalRunId === undefined ? {} : { nativeTurnId: canonicalRunId }),
1659
1709
  acceptedAt: now.toISOString()
1660
1710
  }), now);
1661
1711
  }
1662
- sessions = settleStructuredProviderTurn(sessions, canonicalTurnId, input.providerStatus, now, attemptId);
1712
+ sessions = settleStructuredProviderTurn(sessions, canonicalRunId, input.providerStatus, now, attemptId);
1663
1713
  store.saveTaskRoleSessionSet(sessions);
1664
1714
  }
1665
1715
  if (input.observation !== undefined)
@@ -1668,41 +1718,40 @@ export class FileSchedulerStoreAdapter {
1668
1718
  ...(sessions.sessions[input.agentId] === undefined
1669
1719
  ? {} : { session: sessions.sessions[input.agentId] }),
1670
1720
  duplicate: true,
1671
- ...(observedTurn === null ? {} : { turn: observedTurn })
1721
+ ...(observedRun === null ? {} : { run: observedRun })
1672
1722
  };
1673
1723
  }
1674
1724
  const pending = sessions.providerBinding;
1675
- if (pending.turn?.status === "submitting" || pending.turn?.status === "delivery-unknown") {
1725
+ if (pending.run?.status === "submitting" || pending.run?.status === "delivery-unknown") {
1676
1726
  sessions = updateTaskRoleProviderRuntime(sessions, acceptProviderTurn(pending, {
1677
1727
  attemptId,
1678
- ...(canonicalTurnId === undefined ? {} : { nativeTurnId: canonicalTurnId }),
1728
+ ...(canonicalRunId === undefined ? {} : { nativeTurnId: canonicalRunId }),
1679
1729
  acceptedAt: now.toISOString()
1680
1730
  }), now);
1681
1731
  }
1682
- sessions = settleStructuredProviderTurn(sessions, canonicalTurnId, input.providerStatus, now, attemptId);
1683
- if (canonicalTurnId !== undefined)
1684
- sessions = recordTaskRoleTurnBoundary(sessions, {
1732
+ sessions = settleStructuredProviderTurn(sessions, canonicalRunId, input.providerStatus, now, attemptId);
1733
+ if (canonicalRunId !== undefined)
1734
+ sessions = recordTaskRoleNativeTurnBoundary(sessions, {
1685
1735
  agentId: input.agentId,
1686
1736
  nativeSessionId: input.nativeSessionId,
1687
- turnId: canonicalTurnId
1737
+ turnId: canonicalRunId
1688
1738
  }, now);
1739
+ if (input.observation?.payload.failure?.error.sessionDisposition === "unrecoverable"
1740
+ && sessions.providerBinding !== null) {
1741
+ sessions = updateTaskRoleProviderRuntime(sessions, updateProviderConversationRecoverability(sessions.providerBinding, "unrecoverable"), now);
1742
+ }
1689
1743
  store.saveTaskRoleSessionSet(sessions);
1690
- completeRuntimeHookReservation(store, owner, input.runtimeGenerationId);
1691
- let terminalTurn;
1692
- if (recordedProviderTurn && observedTurn?.status === "active") {
1744
+ let terminalRun;
1745
+ if (recordedProviderTurn && observedRun?.status === "active") {
1693
1746
  const binding = sessions.providerBinding;
1694
1747
  const conversation = binding.conversations.find((entry) => entry.conversationId === input.nativeSessionId);
1695
- const activation = binding.activations.find((entry) => (entry.activationId === resolved.activationId));
1696
- if (activation === undefined) {
1697
- throw new Error("Provider Turn result has no matching Activation.");
1698
- }
1699
1748
  const providerStatus = input.providerStatus;
1700
1749
  let systemEvidence;
1701
1750
  let workspaceFailure;
1702
- if ((observedTurn.purpose === "execution" || observedTurn.purpose === "review")
1703
- && observedTurn.executionGroupId !== undefined
1704
- && observedTurn.executionLaneId !== undefined
1705
- && observedTurn.workspace !== undefined
1751
+ if ((observedRun.purpose === "execution" || observedRun.purpose === "review")
1752
+ && observedRun.executionGroupId !== undefined
1753
+ && observedRun.executionLaneId !== undefined
1754
+ && observedRun.workspace !== undefined
1706
1755
  && input.outcome.status === "completed") {
1707
1756
  const gitSnapshot = workspace;
1708
1757
  if (gitSnapshot.status === "captured") {
@@ -1719,21 +1768,19 @@ export class FileSchedulerStoreAdapter {
1719
1768
  };
1720
1769
  }
1721
1770
  }
1722
- const terminalized = terminalizeExactTaskTurn(store, {
1771
+ const terminalized = terminalizeExactTaskRun(store, {
1723
1772
  taskId: input.taskId,
1724
1773
  roleName: input.roleName,
1725
1774
  agentId: input.agentId,
1726
- turnId: observedTurn.id,
1775
+ runId: observedRun.id,
1727
1776
  nativeSessionId: input.nativeSessionId,
1728
- ...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId }),
1729
1777
  outcome: {
1730
1778
  ...input.outcome,
1731
1779
  provider: {
1732
1780
  providerNamespace: binding.providerNamespace,
1733
1781
  accountScope: binding.accountScope,
1734
1782
  conversationId: conversation.conversationId,
1735
- activationId: activation.activationId,
1736
- ...(canonicalTurnId === undefined ? {} : { nativeTurnId: canonicalTurnId }),
1783
+ ...(canonicalRunId === undefined ? {} : { nativeTurnId: canonicalRunId }),
1737
1784
  attemptId,
1738
1785
  status: providerStatus
1739
1786
  }
@@ -1741,20 +1788,24 @@ export class FileSchedulerStoreAdapter {
1741
1788
  ...(systemEvidence === undefined ? {} : { systemEvidence }),
1742
1789
  ...(workspaceFailure === undefined ? {} : { workspaceFailure })
1743
1790
  }, now);
1744
- if (terminalized.disposition !== "applied" || terminalized.turn === null) {
1745
- throw new Error(`Provider Turn terminal could not complete its exact Turn: ${terminalized.reason ?? "obsolete"}.`);
1791
+ if (terminalized.disposition !== "applied" || terminalized.run === null) {
1792
+ throw new Error(`Provider AgentRun terminal could not complete its exact AgentRun: ${terminalized.reason ?? "obsolete"}.`);
1746
1793
  }
1747
- terminalTurn = terminalized.turn;
1748
- const event = createTaskEvent(store.nextEventId(input.taskId), input.taskId, terminalTurn.status === "completed" ? "turn.completed" : "turn.failed", {
1749
- turnId: terminalTurn.id,
1750
- roleName: terminalTurn.roleName,
1751
- ...(canonicalTurnId === undefined ? {} : { providerTurnId: canonicalTurnId }),
1794
+ terminalRun = terminalized.run;
1795
+ const event = createTaskEvent(store.nextEventId(input.taskId), input.taskId, terminalRun.status === "completed" ? "run.completed" : "run.failed", {
1796
+ runId: terminalRun.id,
1797
+ roleName: terminalRun.roleName,
1798
+ ...(canonicalRunId === undefined ? {} : { providerTurnId: canonicalRunId }),
1752
1799
  providerStatus
1753
1800
  }, now);
1754
1801
  store.saveEvent(input.taskId, event);
1755
- if (terminalTurn.roleName !== "leader"
1756
- && (terminalTurn.status !== "completed" || !providerGoalContinues(binding.goal))) {
1757
- routeRoleEvent(store, event, terminalTurn.roleName, terminalTurn.purpose === "review" ? "review-result" : "role-turn-result", now);
1802
+ // A structured failure routes its original runtime.agent-error below;
1803
+ // the AgentRun terminal is the same fact, not a second notification.
1804
+ const structuredFailure = input.observation?.kind === "turn.failed"
1805
+ && input.observation.payload.failure !== undefined;
1806
+ if (!structuredFailure && terminalRun.roleName === "leader"
1807
+ && terminalRun.status === "failed") {
1808
+ routeRoleEvent(store, event, terminalRun.roleName, terminalRun.purpose === "review" ? "review-result" : "role-turn-result", now);
1758
1809
  }
1759
1810
  }
1760
1811
  if (input.observation !== undefined)
@@ -1762,7 +1813,7 @@ export class FileSchedulerStoreAdapter {
1762
1813
  return {
1763
1814
  session: sessions.sessions[input.agentId],
1764
1815
  duplicate: false,
1765
- ...(terminalTurn === undefined ? {} : { turn: terminalTurn })
1816
+ ...(terminalRun === undefined ? {} : { run: terminalRun })
1766
1817
  };
1767
1818
  });
1768
1819
  }
@@ -1771,8 +1822,7 @@ export class FileSchedulerStoreAdapter {
1771
1822
  const identity = [
1772
1823
  input.taskId,
1773
1824
  input.roleName,
1774
- input.turnId,
1775
- input.runtimeGenerationId ?? "unknown-runtime-generation",
1825
+ input.runId,
1776
1826
  String(input.deadStatus ?? "unknown-status")
1777
1827
  ].join("\0");
1778
1828
  const observationId = `tmux-host-exit-${createHash("sha256").update(identity).digest("hex")}`;
@@ -1780,26 +1830,19 @@ export class FileSchedulerStoreAdapter {
1780
1830
  if (events.some((event) => (event.type === "runtime.process-exit-observed"
1781
1831
  && event.payload.observationId === observationId)))
1782
1832
  return;
1783
- const stopReceipt = [...events].reverse().find((event) => (event.type === "runtime.session-termination"
1784
- && event.payload.roleName === input.roleName
1785
- && (input.runtimeGenerationId === undefined || event.payload.runtimeGenerationId === input.runtimeGenerationId)
1786
- && ["stop-requested", "graceful-stop", "forced-stop", "stop-confirmed"]
1787
- .includes(event.payload.outcome ?? "")));
1788
1833
  const observation = validateRuntimeProcessExitObservation({
1789
1834
  schemaVersion: 2,
1790
1835
  observationId,
1791
1836
  hostSequence: 1,
1792
- hostInstanceId: `tmux-${input.runtimeGenerationId ?? input.roleName}`,
1837
+ hostInstanceId: `tmux-${input.roleName}`,
1793
1838
  taskId: input.taskId,
1794
1839
  roleName: input.roleName,
1795
- turnId: input.turnId,
1796
- runtimeGenerationId: input.runtimeGenerationId ?? `unknown-${input.turnId}`,
1840
+ runId: input.runId,
1797
1841
  ...(input.nativeSessionId === undefined
1798
1842
  ? {}
1799
1843
  : { nativeSessionId: input.nativeSessionId }),
1800
1844
  processKind: "agent-host",
1801
1845
  ...(input.deadStatus === undefined ? {} : { exitCode: input.deadStatus }),
1802
- ...(stopReceipt === undefined ? {} : { stopReceiptId: stopReceipt.id }),
1803
1846
  observedAt: input.observedAt.toISOString()
1804
1847
  });
1805
1848
  const classification = classifyRuntimeProcessExit(observation, {});
@@ -1807,7 +1850,6 @@ export class FileSchedulerStoreAdapter {
1807
1850
  observationId,
1808
1851
  processKind: "agent-host",
1809
1852
  roleName: input.roleName,
1810
- runtimeGenerationId: observation.runtimeGenerationId,
1811
1853
  observedAt: observation.observedAt,
1812
1854
  classification,
1813
1855
  observation: JSON.stringify(observation)
@@ -1815,7 +1857,7 @@ export class FileSchedulerStoreAdapter {
1815
1857
  });
1816
1858
  }
1817
1859
  /**
1818
- * Issue 04: reopens each due retry on its original Native Session. A Turn
1860
+ * Issue 04: reopens each due retry on its original Native Session. A AgentRun
1819
1861
  * whose Session is proven dead terminalizes with an exact replacement
1820
1862
  * blocker; a live Session is reopened for the existing delivery path, which
1821
1863
  * re-pushes the exact same input in the same pass.
@@ -1838,22 +1880,6 @@ export class FileSchedulerStoreAdapter {
1838
1880
  if (input.kind === "conversation.observed") {
1839
1881
  binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
1840
1882
  }
1841
- else if (input.kind === "activation.started") {
1842
- const active = binding.activations.find((entry) => entry.status === "active");
1843
- if (active?.activationId !== input.fence.activationId) {
1844
- binding = startProviderActivation(binding, {
1845
- activationId: input.fence.activationId,
1846
- startedAt: input.observedAt ?? input.receivedAt
1847
- });
1848
- }
1849
- }
1850
- else {
1851
- binding = endProviderActivation(binding, input.fence.activationId, {
1852
- status: input.kind === "activation.failed" ? "failed" : "ended",
1853
- endedAt: input.observedAt ?? input.receivedAt,
1854
- reason: input.kind
1855
- });
1856
- }
1857
1883
  }
1858
1884
  catch {
1859
1885
  recordCanonicalObservationObsolete(store, input, "provider-identity-conflict", now);
@@ -1925,16 +1951,11 @@ export class FileSchedulerStoreAdapter {
1925
1951
  const taskId = input.fence.taskId;
1926
1952
  const role = store.getRole(taskId, input.fence.roleName);
1927
1953
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
1928
- const run = store.getTurn(taskId, input.fence.turnId);
1954
+ const run = store.getRun(taskId, input.fence.runId);
1929
1955
  if (role === null || sessions === null || run === null) {
1930
1956
  recordCanonicalObservationObsolete(store, input, "bind-state-missing", now);
1931
1957
  return "obsolete";
1932
1958
  }
1933
- const mailbox = store.getWorkMailbox({
1934
- kind: "role",
1935
- taskId,
1936
- roleName: input.fence.roleName
1937
- });
1938
1959
  const existingSession = sessions.sessions[input.fence.agentId];
1939
1960
  const existingNativeSessionId = existingSession?.nativeSessionId;
1940
1961
  const replacingNativeSession = existingNativeSessionId !== undefined
@@ -1948,7 +1969,6 @@ export class FileSchedulerStoreAdapter {
1948
1969
  agentId: input.fence.agentId,
1949
1970
  adapterId,
1950
1971
  nativeSessionId: decision.outcome.nativeSessionId,
1951
- runtimeGenerationId: input.fence.runtimeGenerationId,
1952
1972
  policy: "fixed",
1953
1973
  status: "active",
1954
1974
  effective: run.effective
@@ -1959,15 +1979,14 @@ export class FileSchedulerStoreAdapter {
1959
1979
  const withProvider = bindOrSupersedeProviderRuntime(bound, input, now, replacementBasis
1960
1980
  ?? terminalProviderReplacementBasis(bound, input, run.mode));
1961
1981
  store.saveTaskRoleSessionSet(withProvider);
1962
- completeRuntimeHookReservation(store, { scope: "task", taskId, roleName: input.fence.roleName }, input.fence.runtimeGenerationId);
1963
1982
  }
1964
1983
  const current = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
1965
1984
  const currentSession = current?.sessions[input.fence.agentId];
1966
1985
  if (current !== null && current !== undefined
1967
1986
  && currentSession?.nativeSessionId === input.fence.nativeSessionId) {
1968
- const run = input.fence.turnId === undefined
1987
+ const run = input.fence.runId === undefined
1969
1988
  ? null
1970
- : store.getTurn(input.fence.taskId, input.fence.turnId);
1989
+ : store.getRun(input.fence.taskId, input.fence.runId);
1971
1990
  const replacementBasis = run === null
1972
1991
  ? undefined
1973
1992
  : terminalSessionReplacementBasis(current, input, run)
@@ -1989,70 +2008,56 @@ export class FileSchedulerStoreAdapter {
1989
2008
  /** Provider acceptance is canonical before storage and remains receipt-fenced. */
1990
2009
  observeRuntimePromptAccepted(input, adapterId, now) {
1991
2010
  return this.store.transaction((store) => {
2011
+ // A pipe write is retained as transport evidence, but cannot acknowledge
2012
+ // the business input. A later native receipt/terminal proves acceptance.
2013
+ if (input.authority === "transport")
2014
+ return "applied";
1992
2015
  const continuation = input.fence.receiptId?.startsWith("turn-input:") === true;
1993
2016
  const ordinaryAttemptId = input.fence.receiptId;
1994
- if (!continuation && input.fence.turnId !== undefined) {
2017
+ if (!continuation && input.fence.runId !== undefined) {
1995
2018
  const exact = resolveTerminalExecution(store, {
1996
2019
  taskId: input.fence.taskId,
1997
2020
  roleName: input.fence.roleName,
1998
2021
  agentId: input.fence.agentId,
1999
2022
  adapterId,
2000
- runtimeGenerationId: input.fence.runtimeGenerationId,
2001
- activationId: input.fence.activationId,
2002
2023
  conversationId: input.fence.conversationId,
2003
2024
  nativeSessionId: input.fence.nativeSessionId,
2004
2025
  nativeTurnId: input.fence.nativeTurnId,
2005
2026
  attemptId: ordinaryAttemptId,
2006
- turnId: input.fence.turnId
2027
+ runId: input.fence.runId
2007
2028
  });
2008
2029
  if (exact === null)
2009
2030
  return "obsolete";
2010
2031
  // A terminal can prove acceptance before the delayed receipt. Retain
2011
2032
  // the receipt without recreating lifecycle state or touching a successor.
2012
- if (!exact.current || exact.turn?.status !== "active")
2033
+ if (!exact.current || exact.run?.status !== "active")
2013
2034
  return "applied";
2014
2035
  }
2015
- if (input.fence.turnId === undefined
2036
+ if (input.fence.runId === undefined
2016
2037
  && ordinaryAttemptId?.startsWith("direct:") === true) {
2017
2038
  const taskId = input.fence.taskId;
2018
2039
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2019
2040
  const session = sessions?.sessions[input.fence.agentId];
2020
2041
  const binding = sessions?.providerBinding;
2021
2042
  const nativeTurnId = input.fence.nativeTurnId;
2022
- const active = store.getActiveTurn(taskId, input.fence.roleName);
2023
- if (sessions === null || sessions === undefined
2024
- || binding === null || binding === undefined
2025
- || session?.runtimeGenerationId !== input.fence.runtimeGenerationId
2026
- || session.nativeSessionId !== input.fence.nativeSessionId
2027
- || currentProviderConversation(binding).conversationId !== input.fence.nativeSessionId) {
2043
+ if (sessions === null || sessions === undefined || binding === null || binding === undefined || session === undefined || session.nativeSessionId !== input.fence.nativeSessionId || currentProviderConversation(binding).conversationId !== input.fence.nativeSessionId) {
2028
2044
  recordCanonicalObservationObsolete(store, input, "direct-turn-session-mismatch", now);
2029
2045
  return "obsolete";
2030
2046
  }
2031
- if (binding.turn?.attemptId === ordinaryAttemptId
2032
- && binding.turn.nativeTurnId === nativeTurnId
2033
- && binding.turn.turnId !== undefined
2034
- && active?.id === binding.turn.turnId) {
2047
+ if (binding.run?.attemptId === ordinaryAttemptId
2048
+ && binding.run.nativeTurnId === nativeTurnId) {
2035
2049
  return "applied";
2036
2050
  }
2037
- if (active !== null) {
2038
- recordCanonicalObservationObsolete(store, input, "direct-turn-conflicts-with-active-turn", now);
2039
- return "obsolete";
2040
- }
2041
- const turnId = store.nextTurnId(taskId);
2042
- const goalContinuation = input.payload.input === undefined
2043
- && providerGoalContinues(binding.goal);
2044
- const direct = createTurn(turnId, taskId, input.fence.roleName, "resume", createTurnInput({
2045
- source: goalContinuation
2046
- ? { type: "provider", channel: "goal-continuation" }
2047
- : { type: "user", channel: "direct" },
2048
- ...(input.payload.input === undefined
2049
- ? {}
2050
- : { directive: input.payload.input }),
2051
- deltaRefIds: []
2052
- }), now, { effective: session.effective });
2051
+ // Native conversation is not an implicit managed assignment. Keep its
2052
+ // observation without borrowing or overwriting another request's
2053
+ // accepted, unknown, or waiting admission evidence.
2054
+ if (binding.run !== null
2055
+ && (["submitting", "accepted", "delivery-unknown"].includes(binding.run.status)
2056
+ || (binding.run.runId !== undefined
2057
+ && store.getRun(taskId, binding.run.runId)?.status === "active")))
2058
+ return "applied";
2053
2059
  const submittedAt = input.observedAt ?? input.receivedAt;
2054
2060
  const accepted = acceptProviderTurn(beginProviderTurn(binding, {
2055
- turnId,
2056
2061
  attemptId: ordinaryAttemptId,
2057
2062
  authorityEpoch: binding.authority.epoch,
2058
2063
  submittedAt
@@ -2061,10 +2066,7 @@ export class FileSchedulerStoreAdapter {
2061
2066
  nativeTurnId,
2062
2067
  acceptedAt: submittedAt
2063
2068
  });
2064
- store.saveTurn(direct);
2065
- store.saveActiveTurn(direct);
2066
2069
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, accepted, now));
2067
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "turn.dispatched", turnLaunchEventPayload(direct), now));
2068
2070
  return "applied";
2069
2071
  }
2070
2072
  if (ordinaryAttemptId !== undefined) {
@@ -2073,10 +2075,9 @@ export class FileSchedulerStoreAdapter {
2073
2075
  const binding = sessions?.providerBinding;
2074
2076
  if (sessions !== null && sessions !== undefined
2075
2077
  && binding !== null && binding !== undefined
2076
- && binding.turn?.turnId === undefined
2077
- && binding.turn?.attemptId === ordinaryAttemptId) {
2078
- if (session?.runtimeGenerationId !== input.fence.runtimeGenerationId
2079
- || session.nativeSessionId !== input.fence.nativeSessionId) {
2078
+ && binding.run?.runId === undefined
2079
+ && binding.run?.attemptId === ordinaryAttemptId) {
2080
+ if (session === undefined || session.nativeSessionId !== input.fence.nativeSessionId) {
2080
2081
  recordCanonicalObservationObsolete(store, input, "ordinary-turn-session-mismatch", now);
2081
2082
  return "obsolete";
2082
2083
  }
@@ -2085,25 +2086,18 @@ export class FileSchedulerStoreAdapter {
2085
2086
  }
2086
2087
  }
2087
2088
  if (continuation) {
2088
- const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
2089
+ const active = store.getActiveRun(input.fence.taskId, input.fence.roleName);
2089
2090
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2090
2091
  const session = sessions?.sessions[input.fence.agentId];
2091
- if (active === null
2092
- || active.id !== input.fence.turnId
2093
- || active.status !== "active"
2094
- || sessions === null
2095
- || sessions === undefined
2096
- || session?.runtimeGenerationId !== input.fence.runtimeGenerationId
2097
- || session.nativeSessionId !== input.fence.nativeSessionId) {
2092
+ if (active === null || active.id !== input.fence.runId || active.status !== "active" || sessions === null || sessions === undefined || session === undefined || session.nativeSessionId !== input.fence.nativeSessionId) {
2098
2093
  recordCanonicalObservationObsolete(store, input, "continuation-fence-mismatch", now);
2099
2094
  return "obsolete";
2100
2095
  }
2101
2096
  store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
2102
- store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "turn.input-delivered", {
2097
+ store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "run.input-delivered", {
2103
2098
  attemptId: input.fence.receiptId,
2104
- turnId: active.id,
2099
+ runId: active.id,
2105
2100
  conversationId: input.fence.conversationId ?? input.fence.nativeSessionId,
2106
- activationId: input.fence.activationId ?? input.fence.runtimeGenerationId,
2107
2101
  ...(input.fence.nativeTurnId === undefined
2108
2102
  ? {}
2109
2103
  : { nativeTurnId: input.fence.nativeTurnId })
@@ -2125,9 +2119,9 @@ export class FileSchedulerStoreAdapter {
2125
2119
  return "deferred";
2126
2120
  if (decision.kind === "idempotent")
2127
2121
  return "applied";
2128
- const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
2122
+ const active = store.getActiveRun(input.fence.taskId, input.fence.roleName);
2129
2123
  if (active === null
2130
- || active.id !== input.fence.turnId
2124
+ || active.id !== input.fence.runId
2131
2125
  || active.status !== "active") {
2132
2126
  recordCanonicalObservationObsolete(store, input, "turn-not-active", now);
2133
2127
  return "obsolete";
@@ -2140,7 +2134,7 @@ export class FileSchedulerStoreAdapter {
2140
2134
  });
2141
2135
  }
2142
2136
  foldRuntimeLifecycleEvent(store, event, observation) {
2143
- const expectation = this.projectTurnExpectation(store, event.fence, observation.fence.turnId);
2137
+ const expectation = this.projectRunExpectation(store, event.fence, observation.fence.runId);
2144
2138
  if (expectation === null) {
2145
2139
  return preallocatedRuntimeReadyAwaitingProjection(store, observation, this.drivers)
2146
2140
  ? { kind: "apply", outcome: { outcome: "mark-ready", preInputReady: true } }
@@ -2160,14 +2154,14 @@ export class FileSchedulerStoreAdapter {
2160
2154
  return { kind: "apply", outcome };
2161
2155
  }
2162
2156
  }
2163
- /** Reads durable Turn + Session state into the pure fold's expectation shape. */
2164
- projectTurnExpectation(store, fence, turnId) {
2157
+ /** Reads durable AgentRun + Session state into the pure fold's expectation shape. */
2158
+ projectRunExpectation(store, fence, runId) {
2165
2159
  const role = store.getRole(fence.taskId, fence.roleName);
2166
2160
  if (role === null)
2167
2161
  return null;
2168
2162
  const sessionSet = store.getTaskRoleSessionSet(fence.taskId, fence.roleName);
2169
2163
  const session = sessionSet?.sessions[fence.agentId] ?? null;
2170
- if (turnId === undefined) {
2164
+ if (runId === undefined) {
2171
2165
  return {
2172
2166
  fence,
2173
2167
  sessionStarted: false,
@@ -2180,45 +2174,30 @@ export class FileSchedulerStoreAdapter {
2180
2174
  : { boundNativeSessionId: session.nativeSessionId })
2181
2175
  };
2182
2176
  }
2183
- const run = store.getTurn(fence.taskId, turnId);
2177
+ const run = store.getRun(fence.taskId, runId);
2184
2178
  if (run === null || run.status !== "active")
2185
2179
  return null;
2186
- const owner = { scope: "task", taskId: fence.taskId, roleName: fence.roleName };
2187
- const freshConversationLaunch = run.mode === "new"
2188
- && runtimeHookMatchesReservation(store, owner, fence.runtimeGenerationId);
2180
+ const freshConversationLaunch = run.mode === "new" && session?.status !== "active";
2189
2181
  const boundNativeSessionId = freshConversationLaunch
2190
2182
  ? undefined
2191
2183
  : session?.nativeSessionId;
2192
- const runtimeGenerationId = freshConversationLaunch
2193
- ? fence.runtimeGenerationId
2194
- : session?.runtimeGenerationId
2195
- ?? (runtimeHookMatchesReservation(store, owner, fence.runtimeGenerationId) ? fence.runtimeGenerationId : undefined);
2196
- if (runtimeGenerationId === undefined)
2197
- return null;
2198
- const providerTurn = sessionSet?.providerBinding?.turn;
2199
- const managedTurnMatches = managedProviderTurnId(providerTurn) === run.id;
2184
+ const providerTurn = sessionSet?.providerBinding?.run;
2185
+ const managedRunMatches = managedProviderTurnId(providerTurn) === run.id;
2200
2186
  const expectedFence = {
2201
2187
  taskId: fence.taskId,
2202
2188
  roleName: fence.roleName,
2203
2189
  agentId: run.effective.agentId,
2204
2190
  adapterId: run.effective.adapterId,
2205
- turnId: run.id,
2206
- runtimeGenerationId,
2207
- receiptId: managedTurnMatches && providerTurn !== null && providerTurn !== undefined
2191
+ runId: run.id,
2192
+ receiptId: managedRunMatches && providerTurn !== null && providerTurn !== undefined
2208
2193
  ? providerTurn.attemptId
2209
- : formatTurnReceiptId(run.taskId, run.id),
2194
+ : formatRunReceiptId(run.taskId, run.id),
2210
2195
  ...(boundNativeSessionId === undefined ? {} : { nativeSessionId: boundNativeSessionId })
2211
2196
  };
2212
2197
  const driverId = this.drivers.requireByAdapterId(run.effective.adapterId).id;
2213
2198
  const lifecycleEvents = store.listEvents(fence.taskId).flatMap((event) => {
2214
2199
  const observation = runtimeObservationFromTaskEvent(event);
2215
- return observation !== null
2216
- && (observation.kind === "session.started" || observation.kind === "session.ready")
2217
- && observation.fence.roleName === fence.roleName
2218
- && observation.fence.agentId === run.effective.agentId
2219
- && observation.fence.driverId === driverId
2220
- && observation.fence.runtimeGenerationId === runtimeGenerationId
2221
- && observation.fence.nativeSessionId === (boundNativeSessionId ?? fence.nativeSessionId)
2200
+ return observation !== null && (observation.kind === "session.started" || observation.kind === "session.ready") && observation.fence.roleName === fence.roleName && observation.fence.agentId === run.effective.agentId && observation.fence.driverId === driverId && observation.fence.nativeSessionId === (boundNativeSessionId ?? fence.nativeSessionId)
2222
2201
  ? [observation]
2223
2202
  : [];
2224
2203
  });
@@ -2226,8 +2205,8 @@ export class FileSchedulerStoreAdapter {
2226
2205
  fence: expectedFence,
2227
2206
  sessionStarted: lifecycleEvents.length > 0,
2228
2207
  ready: lifecycleEvents.some((event) => event.kind === "session.ready"),
2229
- pushed: managedTurnMatches,
2230
- accepted: managedTurnMatches && providerTurn !== null && providerTurn !== undefined
2208
+ pushed: managedRunMatches,
2209
+ accepted: managedRunMatches && providerTurn !== null && providerTurn !== undefined
2231
2210
  && ["accepted", "completed", "failed", "cancelled"]
2232
2211
  .includes(providerTurn.status),
2233
2212
  terminal: run.status !== "active",
@@ -2244,7 +2223,7 @@ export class FileSchedulerStoreAdapter {
2244
2223
  recordGlobalRuntimeNativeSession(input, now = new Date()) {
2245
2224
  return this.store.transaction((store) => (recordGlobalRuntimeNativeSession(store, input, now)));
2246
2225
  }
2247
- observeGlobalRuntimeTurnTerminal(input, now = new Date()) {
2226
+ observeGlobalRuntimeRunTerminal(input, now = new Date()) {
2248
2227
  return this.store.transaction((store) => {
2249
2228
  const role = store.getGlobalRole(input.roleName);
2250
2229
  if (role === null)
@@ -2256,22 +2235,20 @@ export class FileSchedulerStoreAdapter {
2256
2235
  scope: "global",
2257
2236
  roleName: input.roleName
2258
2237
  };
2259
- if (existing === undefined
2260
- && !runtimeHookMatchesReservation(store, owner, input.runtimeGenerationId)) {
2261
- throw new Error("Runtime turn completion does not match the global launch reservation.");
2238
+ if (existing === undefined && !runtimeHookMatchesLaunchIntent(store, owner)) {
2239
+ throw new Error("Runtime turn completion has no matching global Session intent.");
2262
2240
  }
2263
- const nativeSessionId = globalCompletionNativeSessionId(existing, input);
2264
- const effectiveExisting = nativeTransitionExisting(store, owner, existing, nativeSessionId, input.runtimeGenerationId, "Runtime turn completion conflicts with the fixed global Role session.");
2241
+ const nativeSessionId = input.nativeSessionId;
2242
+ const effectiveExisting = nativeTransitionExisting(store, owner, existing, nativeSessionId, "Runtime turn completion conflicts with the fixed global Role session.");
2265
2243
  const effective = globalSessionEffective(role, effectiveExisting);
2266
2244
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
2267
- throw new Error("Runtime turn completion does not match the effective global runtime generation identity.");
2245
+ throw new Error("Runtime turn completion does not match the effective global runtime identity.");
2268
2246
  }
2269
2247
  const completedStatus = effectiveExisting?.status ?? "active";
2270
2248
  current = recordRoleAgentSession(current, {
2271
2249
  agentId: input.agentId,
2272
2250
  adapterId: input.adapterId,
2273
2251
  nativeSessionId,
2274
- ...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId }),
2275
2252
  title: effectiveExisting?.title ?? input.title,
2276
2253
  preview: effectiveExisting?.preview ?? sessionPreview(input.outcome.status === "completed"
2277
2254
  ? input.outcome.output
@@ -2285,7 +2262,6 @@ export class FileSchedulerStoreAdapter {
2285
2262
  }, now);
2286
2263
  current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.nativeTurnId, now);
2287
2264
  store.saveGlobalRoleSessionSet(current);
2288
- completeRuntimeHookReservation(store, owner, input.runtimeGenerationId);
2289
2265
  if (input.roleName === SYSTEM_OPERATOR_ROLE
2290
2266
  && input.adapterId === "codex"
2291
2267
  && completedStatus === "active") {
@@ -2300,7 +2276,7 @@ export class FileSchedulerStoreAdapter {
2300
2276
  return current.sessions[input.agentId];
2301
2277
  });
2302
2278
  }
2303
- classifyGlobalRuntimeTurnTerminal(input) {
2279
+ classifyGlobalRuntimeRunTerminal(input) {
2304
2280
  const role = this.store.getGlobalRole(input.roleName);
2305
2281
  if (role === null)
2306
2282
  return "obsolete";
@@ -2310,13 +2286,12 @@ export class FileSchedulerStoreAdapter {
2310
2286
  scope: "global",
2311
2287
  roleName: input.roleName
2312
2288
  };
2313
- if (existing === undefined
2314
- && !runtimeHookMatchesReservation(this.store, owner, input.runtimeGenerationId))
2289
+ if (existing === undefined && !runtimeHookMatchesLaunchIntent(this.store, owner))
2315
2290
  return "obsolete";
2316
- const nativeSessionId = globalCompletionNativeSessionId(existing, input);
2291
+ const nativeSessionId = input.nativeSessionId;
2317
2292
  let effectiveExisting;
2318
2293
  try {
2319
- effectiveExisting = nativeTransitionExisting(this.store, owner, existing, nativeSessionId, input.runtimeGenerationId, "Runtime turn completion conflicts with the fixed global Role session.");
2294
+ effectiveExisting = nativeTransitionExisting(this.store, owner, existing, nativeSessionId, "Runtime turn completion conflicts with the fixed global Role session.");
2320
2295
  }
2321
2296
  catch {
2322
2297
  return "obsolete";
@@ -2334,8 +2309,7 @@ function runtimeObservationLifecycleFence(input, adapterId) {
2334
2309
  roleName: input.fence.roleName,
2335
2310
  agentId: input.fence.agentId,
2336
2311
  adapterId,
2337
- runtimeGenerationId: input.fence.runtimeGenerationId,
2338
- ...(input.fence.turnId === undefined ? {} : { turnId: input.fence.turnId }),
2312
+ ...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
2339
2313
  ...(input.fence.nativeSessionId === undefined
2340
2314
  ? {}
2341
2315
  : { nativeSessionId: input.fence.nativeSessionId }),
@@ -2343,7 +2317,7 @@ function runtimeObservationLifecycleFence(input, adapterId) {
2343
2317
  };
2344
2318
  }
2345
2319
  /**
2346
- * A live parent Turn still owns its native child result. Only after that Turn
2320
+ * A live parent AgentRun still owns its native child result. Only after that AgentRun
2347
2321
  * is terminal or absent does Yui route the continuation fact to the original
2348
2322
  * Role's supervisor. Mailbox coalescing remains the downstream batching
2349
2323
  * mechanism; this guard decides ownership before any wake is enqueued.
@@ -2352,33 +2326,34 @@ function routeContinuationResult(store, observation, event, reason, now) {
2352
2326
  const taskId = observation.fence.taskId;
2353
2327
  if (taskId === undefined)
2354
2328
  return;
2355
- const parentTurn = observation.fence.turnId === undefined
2329
+ const parentRun = observation.fence.runId === undefined
2356
2330
  ? null
2357
- : store.getTurn(taskId, observation.fence.turnId);
2358
- if (parentTurn?.status === "active")
2331
+ : store.getRun(taskId, observation.fence.runId);
2332
+ if (parentRun?.status === "active")
2359
2333
  return;
2360
2334
  if (store.getRole(taskId, observation.fence.roleName) === null)
2361
2335
  return;
2362
2336
  routeRoleEvent(store, event, observation.fence.roleName, reason, now);
2363
2337
  }
2364
- /** Accepts the original Turn receipt or a later mailbox activation receipt. */
2365
- function runtimeReceiptBelongsToTurn(store, input) {
2338
+ /** Accepts the original AgentRun receipt or a later mailbox activation receipt. */
2339
+ function runtimeReceiptBelongsToRun(store, input) {
2366
2340
  const taskId = input.fence.taskId;
2367
- const turnId = input.fence.turnId;
2341
+ const runId = input.fence.runId;
2368
2342
  const receiptId = input.fence.receiptId;
2369
- if (receiptId === formatTurnReceiptId(taskId, turnId))
2343
+ if (receiptId === formatRunReceiptId(taskId, runId))
2370
2344
  return true;
2371
2345
  if (receiptId === undefined)
2372
2346
  return false;
2347
+ const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2348
+ const current = sessions?.providerBinding?.run;
2349
+ if (current?.runId === runId && current.attemptId === receiptId
2350
+ && current.status !== "rejected" && current.status !== "deferred"
2351
+ && sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId) {
2352
+ return true;
2353
+ }
2373
2354
  return store.listEvents(taskId).some((event) => {
2374
2355
  const accepted = runtimeObservationFromTaskEvent(event);
2375
- return accepted?.kind === "turn.accepted"
2376
- && accepted.fence.turnId === turnId
2377
- && accepted.fence.roleName === input.fence.roleName
2378
- && accepted.fence.agentId === input.fence.agentId
2379
- && accepted.fence.runtimeGenerationId === input.fence.runtimeGenerationId
2380
- && accepted.fence.nativeSessionId === input.fence.nativeSessionId
2381
- && accepted.fence.receiptId === receiptId;
2356
+ return accepted?.kind === "turn.accepted" && accepted.fence.runId === runId && accepted.fence.roleName === input.fence.roleName && accepted.fence.agentId === input.fence.agentId && accepted.fence.nativeSessionId === input.fence.nativeSessionId && accepted.fence.receiptId === receiptId;
2382
2357
  });
2383
2358
  }
2384
2359
  function recordCanonicalObservationObsolete(store, input, reason, now) {
@@ -2392,9 +2367,8 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
2392
2367
  taskId,
2393
2368
  roleName: input.fence.roleName,
2394
2369
  agentId: input.fence.agentId,
2395
- ...(input.fence.turnId === undefined ? {} : { turnId: input.fence.turnId }),
2396
- runtimeGenerationId: input.fence.runtimeGenerationId,
2397
- nativeSessionId: input.fence.nativeSessionId ?? input.fence.runtimeGenerationId
2370
+ ...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
2371
+ ...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId })
2398
2372
  }, reason, now);
2399
2373
  }
2400
2374
  function recordObsoleteRuntimeEvent(store, input, reason, now) {
@@ -2407,9 +2381,8 @@ function recordObsoleteRuntimeEvent(store, input, reason, now) {
2407
2381
  eventType: input.eventType ?? input.type ?? "unknown",
2408
2382
  roleName: input.roleName,
2409
2383
  agentId: input.agentId,
2410
- nativeSessionId: input.nativeSessionId,
2411
- ...(input.turnId === undefined ? {} : { turnId: input.turnId }),
2412
- ...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId }),
2384
+ ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
2385
+ ...(input.runId === undefined ? {} : { runId: input.runId }),
2413
2386
  reason
2414
2387
  }, now));
2415
2388
  }
@@ -2420,44 +2393,36 @@ function sessionPreview(value) {
2420
2393
  ? truncated.slice(0, -1)
2421
2394
  : truncated;
2422
2395
  }
2423
- function requireRuntimeLaunchReservation(store, owner, runtimeGenerationId) {
2424
- const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
2425
- if (!isRuntimeLaunchReservation(mailbox?.processing, runtimeGenerationId)) {
2426
- throw new Error("Runtime launch reservation no longer matches the launch.");
2427
- }
2428
- return mailbox;
2429
- }
2430
- function runtimeHookMatchesReservation(store, owner, runtimeGenerationId) {
2396
+ function runtimeHookMatchesLaunchIntent(store, owner) {
2431
2397
  const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
2432
2398
  if (hasRuntimeCleanupObligation(mailbox))
2433
2399
  return false;
2434
- const processing = mailbox?.processing;
2435
- return runtimeGenerationId !== undefined
2436
- && isRuntimeLaunchReservation(processing, runtimeGenerationId);
2400
+ if (owner.scope === "global")
2401
+ return true;
2402
+ return store.getActiveRun(owner.taskId, owner.roleName)?.mode === "new";
2437
2403
  }
2438
2404
  function preallocatedRuntimeReadyAwaitingProjection(store, observation, drivers) {
2439
2405
  const taskId = observation.fence.taskId;
2440
- const turnId = observation.fence.turnId;
2406
+ const runId = observation.fence.runId;
2441
2407
  const nativeSessionId = observation.fence.nativeSessionId;
2442
2408
  const driver = drivers.find(observation.fence.driverId);
2443
2409
  if (observation.kind !== "session.ready"
2444
2410
  || driver?.capabilities.observation.sessionBootstrap !== "preallocated"
2445
2411
  || taskId === undefined
2446
- || turnId === undefined
2412
+ || runId === undefined
2447
2413
  || nativeSessionId === undefined)
2448
2414
  return false;
2449
2415
  const task = store.getTask(taskId);
2450
2416
  const role = store.getRole(taskId, observation.fence.roleName);
2451
- const run = store.getActiveTurn(taskId, observation.fence.roleName);
2417
+ const run = store.getActiveRun(taskId, observation.fence.roleName);
2452
2418
  const sessions = store.getTaskRoleSessionSet(taskId, observation.fence.roleName);
2453
- if (task?.status !== "active"
2454
- || task.executionGate.state !== "enabled"
2419
+ if (task == null || run == null || !runPurposeAdmitsTaskState(run.purpose, task)
2455
2420
  || role?.activeAgentId !== observation.fence.agentId
2456
- || run?.id !== turnId
2421
+ || run?.id !== runId
2457
2422
  || run.status !== "active"
2458
2423
  || run.effective.agentId !== observation.fence.agentId
2459
2424
  || run.effective.adapterId !== driver.adapterId
2460
- || observation.fence.receiptId !== formatTurnReceiptId(run.taskId, run.id)
2425
+ || observation.fence.receiptId !== formatRunReceiptId(run.taskId, run.id)
2461
2426
  || sessions?.sessions[observation.fence.agentId] !== undefined)
2462
2427
  return false;
2463
2428
  const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
@@ -2465,17 +2430,7 @@ function preallocatedRuntimeReadyAwaitingProjection(store, observation, drivers)
2465
2430
  taskId,
2466
2431
  roleName: observation.fence.roleName
2467
2432
  }));
2468
- return isRuntimeLaunchReservation(mailbox?.processing, observation.fence.runtimeGenerationId)
2469
- && !hasRuntimeCleanupObligation(mailbox)
2470
- && nativeSessionId === nativeSessionIdForLaunch(store.rootDirectory(), observation.fence.runtimeGenerationId, observation.fence.agentId, driver.adapterId);
2471
- }
2472
- function completeRuntimeHookReservation(store, owner, runtimeGenerationId) {
2473
- if (runtimeGenerationId === undefined)
2474
- return;
2475
- const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
2476
- if (!isRuntimeLaunchReservation(mailbox?.processing, runtimeGenerationId))
2477
- return;
2478
- saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, runtimeGenerationId));
2433
+ return !hasRuntimeCleanupObligation(mailbox);
2479
2434
  }
2480
2435
  function saveRuntimeLifecycleMailbox(store, mailbox) {
2481
2436
  if (!mailboxHasWork(mailbox)) {
@@ -2503,24 +2458,18 @@ function sameRuntimeOwner(left, right) {
2503
2458
  && left.roleName === right.roleName
2504
2459
  : right.scope === "global" && left.roleName === right.roleName;
2505
2460
  }
2506
- function dormantRuntimeCandidateIsCurrent(store, candidate) {
2461
+ function dormantRuntimeCandidateIsCurrent(store, candidate, allowActiveRun = false) {
2507
2462
  const { owner } = candidate;
2508
- if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
2509
- return false;
2510
- }
2511
- if (owner.scope === "task"
2512
- && store.getActiveTurn(owner.taskId, owner.roleName) !== null) {
2463
+ if (!allowActiveRun && owner.scope === "task"
2464
+ && store.getActiveRun(owner.taskId, owner.roleName) !== null) {
2513
2465
  return false;
2514
2466
  }
2515
2467
  const sessions = runtimeOwnerSessionSet(store, owner);
2516
- const active = sessions?.sessions[sessions.activeAgentId];
2517
- return active !== undefined
2518
- && active.status === "active"
2519
- && active.agentId === candidate.agentId
2520
- && active.adapterId === candidate.adapterId
2521
- && active.nativeSessionId === candidate.nativeSessionId
2522
- && active.runtimeGenerationId === candidate.runtimeGenerationId
2523
- && active.updatedAt === candidate.sessionUpdatedAt;
2468
+ const active = allowActiveRun && owner.scope === "task"
2469
+ ? taskRoleControlTarget(store.getTaskRoleSessionSet(owner.taskId, owner.roleName))
2470
+ : sessions?.sessions[sessions.activeAgentId];
2471
+ return active !== undefined && active.agentId === candidate.agentId && active.adapterId === candidate.adapterId
2472
+ && active.nativeSessionId === candidate.nativeSessionId && (allowActiveRun || active.updatedAt === candidate.sessionUpdatedAt);
2524
2473
  }
2525
2474
  function endRuntimeOwnerSession(store, owner, now) {
2526
2475
  if (owner.scope === "task") {
@@ -2533,16 +2482,6 @@ function endRuntimeOwnerSession(store, owner, now) {
2533
2482
  let stopped = active.status === "ended"
2534
2483
  ? sessions
2535
2484
  : updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "ended", now, "stopped");
2536
- const activation = stopped.providerBinding === null
2537
- ? null
2538
- : currentProviderActivation(stopped.providerBinding);
2539
- if (activation !== null) {
2540
- stopped = updateTaskRoleProviderRuntime(stopped, endProviderActivation(stopped.providerBinding, activation.activationId, {
2541
- status: "ended",
2542
- endedAt: now.toISOString(),
2543
- reason: "session-ended"
2544
- }), now);
2545
- }
2546
2485
  if (stopped === sessions)
2547
2486
  return false;
2548
2487
  store.saveTaskRoleSessionSet(stopped);
@@ -2582,13 +2521,6 @@ function runtimeOwnerSessionSet(store, owner) {
2582
2521
  ? store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
2583
2522
  : store.getGlobalRoleSessionSet(owner.roleName);
2584
2523
  }
2585
- function runtimeSessionMatchesSettledLaunch(session, input) {
2586
- return session !== undefined
2587
- && session.agentId === input.agentId
2588
- && session.adapterId === input.adapterId
2589
- && (input.nativeSessionId === undefined
2590
- || session.nativeSessionId === input.nativeSessionId);
2591
- }
2592
2524
  function recordTaskRuntimeNativeSession(store, input, now) {
2593
2525
  const task = store.getTask(input.taskId);
2594
2526
  if (task === null)
@@ -2596,32 +2528,45 @@ function recordTaskRuntimeNativeSession(store, input, now) {
2596
2528
  if (task.status === "archived") {
2597
2529
  throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
2598
2530
  }
2599
- if (task.status !== "active" || task.executionGate.state !== "enabled") {
2531
+ // A Draft registers a Session only for its planning Turn. Admission is decided
2532
+ // by the active Turn's purpose through the one shared invariant, so a Draft
2533
+ // still cannot open an execution Session before activation.
2534
+ const admittingRun = store.getActiveRun(input.taskId, input.roleName);
2535
+ const existingPlanning = activeLiveRoleAgentSession(store.getTaskRoleSessionSet(input.taskId, input.roleName));
2536
+ if (!(admittingRun !== null
2537
+ && admittingRun.purpose === "planning"
2538
+ && runPurposeAdmitsTaskState(admittingRun.purpose, task))
2539
+ && !(task.status === "draft" && task.executionGate.state === "enabled" && input.roleName === "leader"
2540
+ && existingPlanning?.effective.executionAuthority === "planning"
2541
+ && existingPlanning.nativeSessionId === input.nativeSessionId)
2542
+ && (task.status !== "active" || task.executionGate.state !== "enabled")) {
2600
2543
  throw new Error(`Cannot register a native session for a Task that is not active: ${input.taskId}.`);
2601
2544
  }
2602
2545
  const role = requireRole(store, input.taskId, input.roleName);
2546
+ if (role.activeAgentId !== input.agentId
2547
+ || activeRoleAgentBinding(role).adapterId !== input.adapterId) {
2548
+ throw new Error("Native Session registration does not match the active Role Agent.");
2549
+ }
2603
2550
  const current = store.getRoleSessionSet(input.taskId, input.roleName)
2604
2551
  ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
2605
2552
  const existing = current.sessions[input.agentId];
2606
- const effectiveExisting = nativeTransitionExisting(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, existing, input.nativeSessionId, input.runtimeGenerationId, "Native session registration conflicts with the fixed Role session.");
2607
- if (existing?.status === "active"
2608
- && (input.runtimeGenerationId === undefined || existing.runtimeGenerationId === input.runtimeGenerationId))
2553
+ const effectiveExisting = nativeTransitionExisting(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, existing, input.nativeSessionId, "Native session registration conflicts with the fixed Role session.");
2554
+ if (existing?.status === "active")
2609
2555
  return existing;
2610
2556
  const resolvedEffective = taskSessionEffective(store, input.taskId, input.roleName, input.agentId, effectiveExisting);
2611
2557
  const effective = input.effective === undefined
2612
2558
  ? resolvedEffective
2613
2559
  : validateEffectiveLaunchSnapshot(input.effective);
2614
2560
  if (!roleSessionMayContinue(resolvedEffective, effective)) {
2615
- throw new Error("Reserved native Session effective launch changed before persistence.");
2561
+ throw new Error("Native Session effective launch changed before persistence.");
2616
2562
  }
2617
2563
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
2618
- throw new Error("Native session registration does not match the effective runtime generation identity.");
2564
+ throw new Error("Native session registration does not match the effective runtime identity.");
2619
2565
  }
2620
2566
  const updated = recordRoleAgentSession(current, {
2621
2567
  agentId: input.agentId,
2622
2568
  adapterId: input.adapterId,
2623
2569
  nativeSessionId: input.nativeSessionId,
2624
- ...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId }),
2625
2570
  policy: "fixed",
2626
2571
  status: "active",
2627
2572
  effective: effectiveExisting?.effective ?? effective
@@ -2633,28 +2578,30 @@ function recordGlobalRuntimeNativeSession(store, input, now) {
2633
2578
  const role = store.getGlobalRole(input.roleName);
2634
2579
  if (role === null)
2635
2580
  throw new Error(`Global Role not found: ${input.roleName}.`);
2581
+ if (role.activeAgentId !== input.agentId
2582
+ || role.agentBindings[role.activeAgentId]?.adapterId !== input.adapterId) {
2583
+ throw new Error("Native Session registration does not match the active global Role Agent.");
2584
+ }
2636
2585
  const current = store.getGlobalRoleSessionSet(input.roleName)
2637
2586
  ?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, input.agentId, now);
2638
2587
  const existing = current.sessions[input.agentId];
2639
- const effectiveExisting = nativeTransitionExisting(store, { scope: "global", roleName: input.roleName }, existing, input.nativeSessionId, input.runtimeGenerationId, "Native session registration conflicts with the fixed global Role session.");
2640
- if (existing?.status === "active"
2641
- && (input.runtimeGenerationId === undefined || existing.runtimeGenerationId === input.runtimeGenerationId))
2588
+ const effectiveExisting = nativeTransitionExisting(store, { scope: "global", roleName: input.roleName }, existing, input.nativeSessionId, "Native session registration conflicts with the fixed global Role session.");
2589
+ if (existing?.status === "active")
2642
2590
  return existing;
2643
2591
  const resolvedEffective = globalSessionEffective(role, effectiveExisting);
2644
2592
  const effective = input.effective === undefined
2645
2593
  ? resolvedEffective
2646
2594
  : validateEffectiveLaunchSnapshot(input.effective);
2647
2595
  if (!roleSessionMayContinue(resolvedEffective, effective)) {
2648
- throw new Error("Reserved global native Session effective launch changed before persistence.");
2596
+ throw new Error("Global native Session effective launch changed before persistence.");
2649
2597
  }
2650
2598
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
2651
- throw new Error("Native session registration does not match the effective global runtime generation identity.");
2599
+ throw new Error("Native session registration does not match the effective global runtime identity.");
2652
2600
  }
2653
2601
  const updated = recordRoleAgentSession(current, {
2654
2602
  agentId: input.agentId,
2655
2603
  adapterId: input.adapterId,
2656
2604
  nativeSessionId: input.nativeSessionId,
2657
- ...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId }),
2658
2605
  policy: "fixed",
2659
2606
  status: "active",
2660
2607
  effective
@@ -2662,31 +2609,20 @@ function recordGlobalRuntimeNativeSession(store, input, now) {
2662
2609
  store.saveGlobalRoleSessionSet(updated);
2663
2610
  return updated.sessions[input.agentId];
2664
2611
  }
2665
- function nativeTransitionExisting(store, owner, existing, nativeSessionId, runtimeGenerationId, conflictMessage) {
2612
+ function nativeTransitionExisting(store, owner, existing, nativeSessionId, conflictMessage) {
2666
2613
  if (existing === undefined || existing.nativeSessionId === nativeSessionId) {
2667
2614
  return existing;
2668
2615
  }
2669
- if (existing.status === "ended"
2670
- && runtimeHookMatchesReservation(store, owner, runtimeGenerationId)) {
2616
+ if (existing.status === "ended" && runtimeHookMatchesLaunchIntent(store, owner)) {
2671
2617
  return undefined;
2672
2618
  }
2673
2619
  throw new Error(conflictMessage);
2674
2620
  }
2675
- function globalCompletionNativeSessionId(existing, input) {
2676
- // Codex structured event sources can expose different native IDs for the
2677
- // same runtime generation. That generation remains the authoritative fence.
2678
- return input.adapterId === "codex"
2679
- && input.runtimeGenerationId !== undefined
2680
- && existing?.runtimeGenerationId === input.runtimeGenerationId
2681
- && existing.agentId === input.agentId
2682
- && existing.adapterId === input.adapterId
2683
- ? existing.nativeSessionId
2684
- : input.nativeSessionId;
2685
- }
2686
2621
  function mapRole(store, role) {
2687
2622
  const binding = activeRoleAgentBinding(role);
2623
+ const purpose = store.getTask(role.taskId)?.status === "draft" ? "planning" : "execution";
2688
2624
  const item = store.listWorkItems(role.taskId).find((candidate) => (candidate.assignee === role.name
2689
- && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
2625
+ && !["accepted", "retired"].includes(candidate.status))) ?? null;
2690
2626
  const workspace = (item === null
2691
2627
  ? store.getTaskWorkspace(role.taskId)
2692
2628
  : store.getWorkItemWorkspace(role.taskId, item.id))
@@ -2695,17 +2631,20 @@ function mapRole(store, role) {
2695
2631
  const reopened = role.name === "leader"
2696
2632
  && store.getPendingWakeup(role.taskId)?.reasons.includes("task-reopened") === true;
2697
2633
  const liveSession = activeLiveRoleAgentSession(store.getTaskRoleSessionSet(role.taskId, role.name));
2698
- const effective = reopened
2634
+ // This projection plans future dispatch. A live Session retains its actual
2635
+ // source, but cannot silently override an explicit next-Agent selection.
2636
+ // Active AgentRun delivery separately and exclusively uses turn.effective.
2637
+ const effective = reopened || (liveSession !== null && liveSession.agentId !== role.activeAgentId)
2699
2638
  ? resolveEffectiveLaunch({
2700
2639
  role,
2701
- purpose: "execution",
2640
+ purpose,
2702
2641
  ...(workspace === undefined ? {} : { workspace })
2703
2642
  })
2704
2643
  : liveSession !== null && workspace?.owner.type === "task"
2705
2644
  ? effectiveLaunchWithTaskMainWorkspace(liveSession.effective, workspace)
2706
2645
  : liveSession?.effective ?? resolveEffectiveLaunch({
2707
2646
  role,
2708
- purpose: "execution",
2647
+ purpose,
2709
2648
  ...(workspace === undefined ? {} : { workspace })
2710
2649
  });
2711
2650
  return {
@@ -2721,14 +2660,14 @@ function mapRole(store, role) {
2721
2660
  };
2722
2661
  }
2723
2662
  function taskSessionEffective(store, taskId, roleName, agentId, existing) {
2724
- const active = store.getActiveTurn(taskId, roleName);
2663
+ const active = store.getActiveRun(taskId, roleName);
2725
2664
  if (active !== null) {
2726
2665
  if (active.effective.agentId !== agentId) {
2727
- throw new Error(`Native Session registration does not match the effective Turn Agent: ${taskId}/${roleName}.`);
2666
+ throw new Error(`Native Session registration does not match the effective AgentRun Agent: ${taskId}/${roleName}.`);
2728
2667
  }
2729
2668
  if (existing !== undefined) {
2730
2669
  if (!roleSessionMayContinue(existing.effective, active.effective)) {
2731
- throw new Error(`Native Session effective launch does not match the active Turn: ${taskId}/${roleName}.`);
2670
+ throw new Error(`Native Session effective launch does not match the active AgentRun: ${taskId}/${roleName}.`);
2732
2671
  }
2733
2672
  return existing.effective;
2734
2673
  }
@@ -2740,7 +2679,7 @@ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
2740
2679
  if (role === null)
2741
2680
  throw new Error(`Role not found: ${taskId}/${roleName}.`);
2742
2681
  const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
2743
- && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
2682
+ && !["accepted", "retired"].includes(candidate.status))) ?? null;
2744
2683
  const workspace = (item === null
2745
2684
  ? store.getTaskWorkspace(taskId)
2746
2685
  : store.getWorkItemWorkspace(taskId, item.id))
@@ -2765,7 +2704,6 @@ function mapSession(session) {
2765
2704
  agentId: session.agentId,
2766
2705
  adapterId: session.adapterId,
2767
2706
  nativeSessionId: session.nativeSessionId,
2768
- ...(session.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: session.runtimeGenerationId }),
2769
2707
  ...(session.title === undefined ? {} : { title: session.title }),
2770
2708
  status: session.status,
2771
2709
  ...(session.endReason === undefined ? {} : { endReason: session.endReason }),
@@ -2773,14 +2711,13 @@ function mapSession(session) {
2773
2711
  updatedAt: session.updatedAt
2774
2712
  };
2775
2713
  }
2776
- function saveTaskSession(store, role, session, status, now, runtimeGenerationId) {
2714
+ function saveTaskSession(store, role, session, status, now) {
2777
2715
  const current = store.getRoleSessionSet(role.taskId, role.name)
2778
2716
  ?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, session.agentId, now);
2779
2717
  const updated = recordRoleAgentSession(current, {
2780
2718
  agentId: session.agentId,
2781
2719
  adapterId: session.adapterId,
2782
2720
  nativeSessionId: session.nativeSessionId,
2783
- ...(runtimeGenerationId === undefined ? {} : { runtimeGenerationId }),
2784
2721
  ...(session.title === undefined ? {} : { title: session.title }),
2785
2722
  policy: "fixed",
2786
2723
  status,
@@ -2794,11 +2731,7 @@ function saveTaskSession(store, role, session, status, now, runtimeGenerationId)
2794
2731
  function matchesStallSessionFence(current, expected) {
2795
2732
  if (current === null || expected === null)
2796
2733
  return current === expected;
2797
- return current.agentId === expected.agentId
2798
- && current.adapterId === expected.adapterId
2799
- && current.nativeSessionId === expected.nativeSessionId
2800
- && current.runtimeGenerationId === expected.runtimeGenerationId
2801
- && current.status === expected.status;
2734
+ return current.agentId === expected.agentId && current.adapterId === expected.adapterId && current.nativeSessionId === expected.nativeSessionId && current.status === expected.status;
2802
2735
  }
2803
2736
  function requireRole(store, taskId, roleName) {
2804
2737
  const role = store.getRole(taskId, roleName);
@@ -2806,17 +2739,18 @@ function requireRole(store, taskId, roleName) {
2806
2739
  throw new Error(`Role not found: ${taskId}/${roleName}.`);
2807
2740
  return role;
2808
2741
  }
2809
- function turnLaunchEventPayload(turn) {
2742
+ function runLaunchEventPayload(run) {
2810
2743
  return {
2811
- turnId: turn.id,
2812
- role: turn.roleName,
2813
- purpose: turn.purpose,
2814
- mode: turn.mode,
2815
- agent: `${turn.effective.agentId}/${turn.effective.adapterId}`,
2816
- effectiveRevision: String(turn.effective.sourceDesiredRevision),
2817
- profileAccess: turn.effective.profileAccess,
2818
- effectivePermission: turn.effective.permission.strategy,
2819
- writeProjectIds: turn.effective.writeProjectIds.join(",") || "none"
2744
+ runId: run.id,
2745
+ role: run.roleName,
2746
+ purpose: run.purpose,
2747
+ mode: run.mode,
2748
+ agent: `${run.effective.agentId}/${run.effective.adapterId}`,
2749
+ component: run.effective.component,
2750
+ effectiveRevision: String(run.effective.sourceDesiredRevision),
2751
+ profileAccess: run.effective.profileAccess,
2752
+ effectivePermission: run.effective.permission.strategy,
2753
+ writeProjectIds: run.effective.writeProjectIds.join(",") || "none"
2820
2754
  };
2821
2755
  }
2822
2756
  /**
@@ -2831,8 +2765,7 @@ function runtimeObservationTelemetryEntry(input) {
2831
2765
  return {
2832
2766
  taskId: input.fence.taskId,
2833
2767
  roleName: input.fence.roleName,
2834
- turnId: input.fence.turnId,
2835
- generation: input.fence.runtimeGenerationId,
2768
+ runId: input.fence.runId,
2836
2769
  progressId: [
2837
2770
  input.kind,
2838
2771
  input.payload.operationId ?? input.payload.activity ?? "state"
@@ -2843,7 +2776,6 @@ function runtimeObservationTelemetryEntry(input) {
2843
2776
  kind: input.kind,
2844
2777
  authority: input.authority,
2845
2778
  driverId: input.fence.driverId,
2846
- runtimeGenerationId: input.fence.runtimeGenerationId,
2847
2779
  nativeSessionId: input.fence.nativeSessionId ?? "",
2848
2780
  nativeTurnId: input.fence.nativeTurnId ?? "",
2849
2781
  ...(input.payload.operation === undefined
@@ -2870,7 +2802,7 @@ function compactedRuntimeObservationIds(events, incoming) {
2870
2802
  const observation = runtimeObservationFromTaskEvent(event);
2871
2803
  const matches = incoming.kind.startsWith("operation.")
2872
2804
  ? observation !== null
2873
- && runtimeObservationTurnFenceMatches(observation.fence, incoming.fence)
2805
+ && runtimeObservationRunFenceMatches(observation.fence, incoming.fence)
2874
2806
  : observation !== null
2875
2807
  && runtimeObservationFenceMatches(observation.fence, incoming.fence);
2876
2808
  return observation !== null
@@ -2922,18 +2854,18 @@ function compactedRuntimeObservationIds(events, incoming) {
2922
2854
  function recordStructuredProviderAcceptance(sessions, input, now) {
2923
2855
  const current = sessions.providerBinding;
2924
2856
  const attemptId = input.fence.receiptId;
2925
- const turnId = input.fence.nativeTurnId;
2857
+ const runId = input.fence.nativeTurnId;
2926
2858
  if (current === null || attemptId === undefined)
2927
2859
  return sessions;
2928
- if (current.turn === null
2929
- || current.turn.turnId !== input.fence.turnId)
2860
+ if (current.run === null
2861
+ || current.run.runId !== input.fence.runId)
2930
2862
  return sessions;
2931
- if (current.turn.attemptId === attemptId
2932
- && ["accepted", "completed", "failed", "cancelled"].includes(current.turn.status))
2863
+ if (current.run.attemptId === attemptId
2864
+ && ["accepted", "completed", "failed", "cancelled"].includes(current.run.status))
2933
2865
  return sessions;
2934
2866
  const binding = acceptProviderTurn(current, {
2935
2867
  attemptId,
2936
- nativeTurnId: turnId,
2868
+ nativeTurnId: runId,
2937
2869
  acceptedAt: input.observedAt ?? input.receivedAt
2938
2870
  });
2939
2871
  return updateTaskRoleProviderRuntime(sessions, binding, now);
@@ -2948,18 +2880,16 @@ function resolveTerminalExecution(store, input) {
2948
2880
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
2949
2881
  const session = sessions?.sessions[input.agentId];
2950
2882
  const binding = sessions?.providerBinding;
2951
- const pending = binding?.turn;
2883
+ const pending = binding?.run;
2952
2884
  const evidence = store.listEvents(input.taskId).map(runtimeObservationFromTaskEvent)
2953
2885
  .filter((event) => event !== null
2954
- && (event.kind === "turn.accepted" || isTurnTerminalObservation(event)));
2886
+ && (event.kind === "turn.accepted" || isRunTerminalObservation(event)));
2955
2887
  if (input.attemptId !== undefined && evidence.some((event) => (event.fence.roleName === input.roleName
2956
2888
  && event.fence.agentId === input.agentId
2957
2889
  && event.fence.nativeSessionId === input.nativeSessionId
2958
2890
  && event.fence.receiptId === input.attemptId
2959
- && (event.fence.runtimeGenerationId !== input.runtimeGenerationId
2960
- && input.runtimeGenerationId !== undefined
2961
- || input.nativeTurnId !== undefined && event.fence.nativeTurnId !== undefined
2962
- && event.fence.nativeTurnId !== input.nativeTurnId))))
2891
+ && input.nativeTurnId !== undefined && event.fence.nativeTurnId !== undefined
2892
+ && event.fence.nativeTurnId !== input.nativeTurnId)))
2963
2893
  return null;
2964
2894
  const identityMatches = (attemptId, nativeTurnId) => ((input.attemptId === undefined
2965
2895
  ? input.nativeTurnId !== undefined && input.nativeTurnId === nativeTurnId
@@ -2969,75 +2899,67 @@ function resolveTerminalExecution(store, input) {
2969
2899
  if (binding !== null && binding !== undefined && pending !== null && pending !== undefined
2970
2900
  && session?.adapterId === input.adapterId
2971
2901
  && session.nativeSessionId === input.nativeSessionId
2972
- && (input.runtimeGenerationId === undefined
2973
- || (pending.activationId ?? session.runtimeGenerationId) === input.runtimeGenerationId)
2974
2902
  && currentProviderConversation(binding).conversationId === input.nativeSessionId
2975
2903
  && identityMatches(pending.attemptId, pending.nativeTurnId)
2976
- && (input.turnId === undefined || input.turnId === pending.turnId)
2977
- && pending.status !== "rejected") {
2978
- const activation = binding.activations.find((entry) => (entry.activationId === (input.runtimeGenerationId ?? pending.activationId ?? session.runtimeGenerationId)
2979
- && (input.activationId === undefined || entry.activationId === input.activationId)
2980
- && entry.conversationId === input.nativeSessionId));
2981
- const turn = pending.turnId === undefined ? null : store.getTurn(input.taskId, pending.turnId);
2982
- if (activation !== undefined && (pending.turnId === undefined || (turn !== null && turn.roleName === input.roleName
2983
- && turn.effective.agentId === input.agentId && turn.effective.adapterId === input.adapterId)))
2904
+ && (input.runId === undefined || input.runId === pending.runId)
2905
+ && pending.status !== "rejected" && pending.status !== "deferred") {
2906
+ const run = pending.runId === undefined ? null : store.getRun(input.taskId, pending.runId);
2907
+ if (pending.runId === undefined || (run !== null && run.roleName === input.roleName
2908
+ && run.effective.agentId === input.agentId && run.effective.adapterId === input.adapterId))
2984
2909
  return {
2985
- turn, current: true, attemptId: pending.attemptId,
2986
- nativeTurnId: pending.nativeTurnId, activationId: activation.activationId
2910
+ run, current: true, attemptId: pending.attemptId,
2911
+ nativeTurnId: pending.nativeTurnId,
2987
2912
  };
2988
2913
  }
2989
2914
  // Old results use immutable acceptance/terminal evidence, not today's Role
2990
- // config, Session status, current generation or active Turn pointer.
2915
+ // config, Session status or active AgentRun pointer.
2991
2916
  const matches = evidence.filter((event) => event.fence.roleName === input.roleName
2992
2917
  && event.fence.agentId === input.agentId
2993
2918
  && event.fence.nativeSessionId === input.nativeSessionId
2994
- && input.runtimeGenerationId !== undefined
2995
- && event.fence.runtimeGenerationId === input.runtimeGenerationId
2996
- && (input.activationId === undefined
2997
- || (event.fence.activationId ?? event.fence.runtimeGenerationId) === input.activationId)
2998
2919
  && identityMatches(event.fence.receiptId, event.fence.nativeTurnId)
2999
- && event.fence.turnId !== undefined
3000
- && (input.turnId === undefined || event.fence.turnId === input.turnId));
2920
+ && (input.runId === undefined || event.fence.runId === input.runId));
3001
2921
  const accepted = matches[0];
3002
2922
  if (accepted === undefined || accepted.fence.receiptId === undefined)
3003
2923
  return null;
3004
- if (matches.some((event) => event.fence.turnId !== accepted.fence.turnId
2924
+ if (matches.some((event) => event.fence.runId !== accepted.fence.runId
3005
2925
  || event.fence.receiptId !== accepted.fence.receiptId)) {
3006
2926
  throw new Error("Runtime terminal has conflicting durable execution bindings.");
3007
2927
  }
3008
- const turn = store.getTurn(input.taskId, accepted.fence.turnId);
3009
- if (turn === null || turn.effective.adapterId !== input.adapterId
3010
- || turn.effective.agentId !== input.agentId || turn.roleName !== input.roleName)
2928
+ if (accepted.fence.runId === undefined) {
2929
+ return { run: null, current: false, attemptId: accepted.fence.receiptId,
2930
+ nativeTurnId: accepted.fence.nativeTurnId };
2931
+ }
2932
+ const run = store.getRun(input.taskId, accepted.fence.runId);
2933
+ if (run === null || run.effective.adapterId !== input.adapterId
2934
+ || run.effective.agentId !== input.agentId || run.roleName !== input.roleName)
3011
2935
  return null;
3012
2936
  return {
3013
- turn, current: false, attemptId: accepted.fence.receiptId,
2937
+ run, current: false, attemptId: accepted.fence.receiptId,
3014
2938
  nativeTurnId: accepted.fence.nativeTurnId,
3015
- activationId: accepted.fence.activationId ?? accepted.fence.runtimeGenerationId
3016
2939
  };
3017
2940
  }
3018
- function isTurnTerminalObservation(input) {
2941
+ function isRunTerminalObservation(input) {
3019
2942
  return ["turn.completed", "turn.failed", "turn.cancelled"].includes(input.kind);
3020
2943
  }
3021
- function assertConsistentTerminal(store, input, turn) {
2944
+ function assertConsistentTerminal(store, input, run) {
3022
2945
  const previous = store.listEvents(input.taskId).map(runtimeObservationFromTaskEvent)
3023
2946
  .filter((event) => event !== null
3024
- && isTurnTerminalObservation(event)
2947
+ && isRunTerminalObservation(event)
3025
2948
  && event.fence.roleName === input.roleName
3026
2949
  && event.fence.agentId === input.agentId
3027
2950
  && event.fence.nativeSessionId === input.nativeSessionId
3028
- && event.fence.runtimeGenerationId === input.runtimeGenerationId
3029
2951
  && (input.attemptId === undefined
3030
2952
  ? input.nativeTurnId !== undefined && event.fence.nativeTurnId === input.nativeTurnId
3031
2953
  : event.fence.receiptId === input.attemptId));
3032
2954
  if (input.observation !== undefined && previous.some((event) => (event.kind !== input.observation.kind
3033
2955
  || !isDeepStrictEqual(event.payload, input.observation.payload)
3034
- || event.fence.turnId !== input.observation.fence.turnId)))
2956
+ || event.fence.runId !== input.observation.fence.runId)))
3035
2957
  throw new Error("Conflicting terminal content for the same execution.");
3036
- if (turn?.result?.provider !== undefined) {
3037
- if (turn.result.provider.status !== input.providerStatus
3038
- || (input.outcome.status === "completed" && turn.result.output !== input.outcome.output)
2958
+ if (run?.result?.provider !== undefined) {
2959
+ if (run.result.provider.status !== input.providerStatus
2960
+ || (input.outcome.status === "completed" && run.result.output !== input.outcome.output)
3039
2961
  || (previous.length === 0 && input.outcome.status === "failed"
3040
- && turn.result.diagnostic !== input.outcome.diagnostic)) {
2962
+ && run.result.diagnostic !== input.outcome.diagnostic)) {
3041
2963
  throw new Error("Conflicting result for the same execution.");
3042
2964
  }
3043
2965
  }
@@ -3051,49 +2973,30 @@ function terminalSessionReplacementBasis(sessions, input, run) {
3051
2973
  || session === undefined
3052
2974
  || incomingConversationId === undefined
3053
2975
  || currentProviderConversation(binding).conversationId === incomingConversationId
3054
- || currentProviderActivation(binding) !== null
3055
- || binding.authority.owner !== "none") {
2976
+ || session.status !== "ended") {
3056
2977
  return undefined;
3057
2978
  }
3058
- // Pre-start persistence may already have replaced the terminal Role Session
3059
- // before Provider readiness arrives. A matching new Session plus the
3060
- // quiescent old Provider binding preserves the same terminal-session proof.
3061
- return session.status === "ended"
3062
- || session.nativeSessionId === input.fence.nativeSessionId
3063
- ? "terminal-session"
3064
- : undefined;
2979
+ return "terminal-session";
3065
2980
  }
3066
2981
  function bindOrSupersedeProviderRuntime(sessions, input, now, replacementBasis) {
3067
2982
  const conversationId = input.fence.conversationId ?? input.fence.nativeSessionId;
3068
- const activationId = input.fence.activationId ?? input.fence.runtimeGenerationId;
3069
2983
  if (sessions.providerBinding === null) {
3070
2984
  return bindTaskRoleProviderRuntime(sessions, createProviderRuntimeBinding({
3071
2985
  providerNamespace: input.fence.driverId,
3072
2986
  accountScope: input.fence.agentId,
3073
2987
  conversationId,
3074
- activationId,
3075
2988
  startedAt: input.observedAt ?? input.receivedAt
3076
2989
  }), now);
3077
2990
  }
3078
2991
  const current = currentProviderConversation(sessions.providerBinding);
3079
2992
  if (current.conversationId === conversationId) {
3080
- const active = currentProviderActivation(sessions.providerBinding);
3081
- if (active?.activationId === activationId)
3082
- return sessions;
3083
- if (active !== null) {
3084
- throw new Error("Provider Conversation cannot start a second Activation while the current one is active.");
3085
- }
3086
- return updateTaskRoleProviderRuntime(sessions, startProviderActivation(sessions.providerBinding, {
3087
- activationId,
3088
- startedAt: input.observedAt ?? input.receivedAt
3089
- }), now);
2993
+ return sessions;
3090
2994
  }
3091
2995
  if (replacementBasis === undefined) {
3092
2996
  throw new Error("A fresh Provider Conversation requires a terminal prior Session.");
3093
2997
  }
3094
2998
  return updateTaskRoleProviderRuntime(sessions, supersedeProviderConversation(sessions.providerBinding, {
3095
2999
  conversationId,
3096
- activationId,
3097
3000
  switchedAt: input.observedAt ?? input.receivedAt,
3098
3001
  basis: replacementBasis
3099
3002
  }), now);
@@ -3106,28 +3009,25 @@ function terminalProviderReplacementBasis(sessions, input, mode) {
3106
3009
  return undefined;
3107
3010
  }
3108
3011
  const current = sessions.sessions[input.fence.agentId];
3109
- if (current === undefined
3110
- || current.status !== "active"
3111
- || current.nativeSessionId !== input.fence.nativeSessionId
3112
- || current.runtimeGenerationId !== input.fence.runtimeGenerationId) {
3012
+ if (current === undefined || current.status !== "active" || current.nativeSessionId !== input.fence.nativeSessionId) {
3113
3013
  return undefined;
3114
3014
  }
3115
3015
  const replaced = [...(sessions.history ?? [])].reverse().find((session) => (session.agentId === current.agentId
3116
3016
  && session.adapterId === current.adapterId
3117
3017
  && session.status === "ended"
3118
- && session.nativeSessionId !== current.nativeSessionId));
3018
+ && session.nativeSessionId === currentProviderConversation(sessions.providerBinding).conversationId));
3119
3019
  return replaced === undefined ? undefined : "terminal-session";
3120
3020
  }
3121
- function settleStructuredProviderTurn(sessions, turnId, status, now, attemptId) {
3021
+ function settleStructuredProviderTurn(sessions, runId, status, now, attemptId) {
3122
3022
  const binding = sessions.providerBinding;
3123
- if (binding === null || binding.turn === null
3124
- || (attemptId === undefined ? binding.turn.nativeTurnId !== turnId : binding.turn.attemptId !== attemptId)) {
3023
+ if (binding === null || binding.run === null
3024
+ || (attemptId === undefined ? binding.run.nativeTurnId !== runId : binding.run.attemptId !== attemptId)) {
3125
3025
  return sessions;
3126
3026
  }
3127
- if (["completed", "failed", "cancelled"].includes(binding.turn.status))
3027
+ if (["completed", "failed", "cancelled"].includes(binding.run.status))
3128
3028
  return sessions;
3129
3029
  return updateTaskRoleProviderRuntime(sessions, settleProviderTurn(binding, {
3130
- nativeTurnId: turnId,
3030
+ nativeTurnId: runId,
3131
3031
  ...(attemptId === undefined ? {} : { attemptId }),
3132
3032
  status,
3133
3033
  settledAt: now.toISOString()