@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
@@ -2,8 +2,8 @@ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeout
2
2
  import { resolve } from "node:path";
3
3
  import { isDeepStrictEqual } from "node:util";
4
4
  import { controllerSocketPath } from "../core/controllerEndpoint.js";
5
- import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
6
- import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
5
+ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
6
+ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
7
7
  import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
8
8
  import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
9
9
  import { roleSessionMayContinue, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
@@ -18,15 +18,20 @@ import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
18
18
  import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
19
19
  import { AgentHostPromptPushAdapter, FileTaskRuntimeIsolation, TmuxSessionHost, ProviderContinuationReconciliationService } from "../runtime/index.js";
20
20
  import { startFileTaskController } from "./controller.js";
21
- import { AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
21
+ import { AgentHostProviderTurnFenceError, AgentHostProviderSessionBusyError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
22
22
  import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
23
23
  import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
24
24
  import { authorizeJobStart } from "./jobControl.js";
25
25
  import { createKernelPorts } from "../kernel/kernelPorts.js";
26
26
  import { createCapabilityDispatcher } from "./capabilityBridge.js";
27
+ import { createControllerWeb } from "../web/controllerWeb.js";
28
+ import { createWebTaskSurface } from "../web/webTaskSurface.js";
29
+ import { SurfaceContributions } from "../surface/surfaceContributions.js";
30
+ import { TmuxWebTerminalService } from "../web/tmuxWebTerminal.js";
31
+ import { FileTaskWorkflowRuntime } from "./clientRuntime.js";
27
32
  import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
28
33
  import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
29
- import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver, } from "./runtimeEventProcessor.js";
34
+ import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver } from "./runtimeEventProcessor.js";
30
35
  import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
31
36
  import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./domainIdentity.js";
32
37
  import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
@@ -42,18 +47,22 @@ import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExit
42
47
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
43
48
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
44
49
  import { createTaskEvent } from "../event/taskEvent.js";
50
+ import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
51
+ import { taskOwnsManagedWorkspace } from "../task/task.js";
45
52
  /** Production composition root for the current SQLite TaskStore + tmux Controller. */
46
53
  export async function startFileTaskControllerRuntime(home, options = {}) {
47
54
  // The current Home always uses SQLite. The persistence worker is enabled by
48
55
  // default; YUI_STORE_WORKER=0/false keeps the same database in-process.
49
56
  const useWorker = resolveStoreWorkerEnabledForHome(home, options.environment ?? process.env);
50
- const store = options.store
51
- ?? (useWorker
57
+ const ownedStore = options.store === undefined
58
+ ? (useWorker
52
59
  // The worker owns the event-processing hot path while the scheduler uses
53
60
  // the synchronous connection. Both point at the same WAL database and
54
61
  // serialize via BEGIN IMMEDIATE + busy_timeout.
55
62
  ? new SqliteTaskStore(home)
56
- : openCurrentTaskStore(home));
63
+ : openCurrentTaskStore(home))
64
+ : undefined;
65
+ const store = options.store ?? ownedStore;
57
66
  const homeId = store.getHomeIdentity().homeId;
58
67
  const durableConfig = store.getConfig();
59
68
  // When the worker backend is active, the db-touching observer folds run in
@@ -70,445 +79,470 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
70
79
  const inventoryClient = useWorker
71
80
  ? new ResourceInventoryClient()
72
81
  : undefined;
73
- const schedulerStore = options.schedulerStore
74
- ?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, store.getConfig()));
75
- const domainIdentity = options.domainIdentity
76
- ?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
77
- const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
78
- environment: options.environment
79
- });
80
- const tmux = options.tmux ?? new TmuxManager(resolveTmuxBin(durableConfig.tmuxBin), new NodeCommandExecutor(), {
81
- yuiHome: home,
82
- historyLimit: resolveTmuxHistoryLimit(durableConfig.tmuxHistoryLimit),
83
- ...(domainIdentity === undefined
84
- ? {}
85
- : {
86
- onRoleTargetRecorded: (target) => {
87
- if (!recordEphemeralTmuxTarget(home, domainIdentity.token, target)) {
88
- throw new Error(`Ephemeral tmux target fence could not be recorded: ${target}.`);
82
+ let closeKernel = async () => { };
83
+ let closeWeb = async () => { };
84
+ try {
85
+ const schedulerStore = options.schedulerStore
86
+ ?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, store.getConfig()));
87
+ const domainIdentity = options.domainIdentity
88
+ ?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
89
+ const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
90
+ environment: options.environment
91
+ });
92
+ const tmux = options.tmux ?? new TmuxManager(resolveTmuxBin(durableConfig.tmuxBin), new NodeCommandExecutor(), {
93
+ yuiHome: home,
94
+ historyLimit: resolveTmuxHistoryLimit(durableConfig.tmuxHistoryLimit),
95
+ ...(domainIdentity === undefined
96
+ ? {}
97
+ : {
98
+ onRoleTargetRecorded: (target) => {
99
+ if (!recordEphemeralTmuxTarget(home, domainIdentity.token, target)) {
100
+ throw new Error(`Ephemeral tmux target fence could not be recorded: ${target}.`);
101
+ }
89
102
  }
90
- }
91
- })
92
- });
93
- const sessionOwners = new SessionOwnerReconciliation({
94
- home,
95
- store,
96
- environment: options.environment,
97
- tmux,
98
- onWarning: options.onError
99
- });
100
- // The runtime inbox is also the low-latency discovery source during a
101
- // scheduler-owned launch. Waiting only for the post-pass durable projection
102
- // would deadlock behind the same pass that is currently starting the host.
103
- const runtimeEventInbox = new FileRuntimeEventInbox(home);
104
- const catalogs = options.catalogs
105
- ?? new AgentConfigurationCatalogService(home, {
106
- environment: options.environment ?? process.env
103
+ })
104
+ });
105
+ const sessionOwners = new SessionOwnerReconciliation({
106
+ home,
107
+ store,
108
+ environment: options.environment,
109
+ tmux,
110
+ nativeConnection: (taskId, roleName) => planner.planNativeControl(taskId, roleName),
111
+ onWarning: options.onError
107
112
  });
108
- const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
109
- validateLaunch: async (request) => {
110
- const agent = store.getConfiguredAgent(request.agentId);
111
- if (agent === null)
112
- return;
113
- const resolved = await catalogs.resolve({
114
- agent,
115
- cwd: request.workspace,
116
- config: effectiveLaunchConfig(request.effective)
113
+ // The runtime inbox is also the low-latency discovery source during a
114
+ // scheduler-owned launch. Waiting only for the post-pass durable projection
115
+ // would deadlock behind the same pass that is currently starting the host.
116
+ const runtimeEventInbox = new FileRuntimeEventInbox(home);
117
+ const catalogs = options.catalogs
118
+ ?? new AgentConfigurationCatalogService(home, {
119
+ environment: options.environment ?? process.env
117
120
  });
118
- validateAgentLaunchConfiguration(resolved.catalog, effectiveLaunchConfig(request.effective));
119
- },
120
- waitForNativeSession: async (request, signal) => {
121
- const owner = request.owner;
122
- if (owner.scope !== "task") {
123
- throw new Error("Native session discovery requires a Task runtime owner.");
124
- }
125
- while (!signal.aborted) {
126
- const session = schedulerStore.getRoleSession(owner.taskId, owner.roleName, request.agentId);
127
- if (session !== null
128
- && session.runtimeGenerationId === request.runtimeGenerationId
129
- && typeof session.nativeSessionId === "string"
130
- && session.nativeSessionId.trim().length > 0) {
131
- return session.nativeSessionId;
121
+ const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
122
+ validateLaunch: async (request) => {
123
+ const agent = store.getConfiguredAgent(request.agentId);
124
+ if (agent === null)
125
+ return;
126
+ const resolved = await catalogs.resolve({
127
+ agent,
128
+ cwd: request.workspace,
129
+ config: effectiveLaunchConfig(request.effective)
130
+ });
131
+ validateAgentLaunchConfiguration(resolved.catalog, effectiveLaunchConfig(request.effective));
132
+ },
133
+ waitForNativeSession: async (request, signal) => {
134
+ const owner = request.owner;
135
+ if (owner.scope !== "task") {
136
+ throw new Error("Native session discovery requires a Task runtime owner.");
132
137
  }
133
- for (const event of runtimeEventInbox.list()) {
134
- if (event.type === "runtime-observation"
135
- && event.observation.kind === "session.started"
136
- && event.observation.fence.taskId === owner.taskId
137
- && event.observation.fence.roleName === owner.roleName
138
- && event.observation.fence.agentId === request.agentId
139
- && event.observation.fence.runtimeGenerationId === request.runtimeGenerationId
140
- && typeof event.observation.fence.nativeSessionId === "string"
141
- && event.observation.fence.nativeSessionId.trim().length > 0) {
142
- return event.observation.fence.nativeSessionId;
138
+ while (!signal.aborted) {
139
+ const session = schedulerStore.getRoleSession(owner.taskId, owner.roleName, request.agentId);
140
+ if (session !== null && session.status === "active"
141
+ && session.adapterId === request.adapterId
142
+ && typeof session.nativeSessionId === "string" && session.nativeSessionId.trim().length > 0) {
143
+ return session.nativeSessionId;
144
+ }
145
+ for (const event of runtimeEventInbox.list()) {
146
+ if (event.type === "runtime-observation" && event.observation.kind === "session.started"
147
+ && event.observation.fence.taskId === owner.taskId
148
+ && event.observation.fence.roleName === owner.roleName
149
+ && event.observation.fence.agentId === request.agentId
150
+ && event.observation.fence.runId === request.runId
151
+ && typeof event.observation.fence.nativeSessionId === "string"
152
+ && event.observation.fence.nativeSessionId.trim().length > 0) {
153
+ return event.observation.fence.nativeSessionId;
154
+ }
143
155
  }
156
+ await abortableDelay(50, signal);
144
157
  }
145
- await abortableDelay(50, signal);
146
- }
147
- throw new Error("Native session discovery was aborted.");
148
- },
149
- inactivityTimeoutMs: resolveAgentLaunchInactivityTimeoutSeconds(durableConfig.agentLaunchInactivityTimeoutSeconds) * 1_000,
150
- onHostCreated: ({ binding, pane }) => {
151
- sessionOwners.recordHostOwner({
152
- owner: binding.owner,
153
- agentId: binding.agentId,
154
- adapterId: binding.adapterId,
155
- runtimeGenerationId: binding.runtimeGenerationId,
156
- ...(binding.nativeSessionId === undefined
157
- ? {}
158
- : { nativeSessionId: binding.nativeSessionId }),
159
- ...(pane.pid === undefined ? {} : { panePid: pane.pid })
160
- });
161
- }
162
- });
163
- const promptPush = options.promptPush
164
- ?? new AgentHostPromptPushAdapter(home);
165
- const runtimeIsolation = options.runtimeIsolation
166
- ?? new FileTaskRuntimeIsolation({
167
- // A sibling of the exact control Home keeps provider data/cache/tmp out
168
- // of both the shared control plane and every managed Git workspace.
169
- runtimeRoot: `${resolve(home)}.task-runtimes`,
170
- controlPlane: {
171
- yuiHome: home,
172
- controllerSocketPath: controllerSocketPath(homeId),
173
- tmuxNamespace: yuiTmuxServerName(home),
174
- globalInstallPaths: [process.execPath]
158
+ throw new Error("Native session discovery was aborted.");
159
+ },
160
+ inactivityTimeoutMs: resolveAgentLaunchInactivityTimeoutSeconds(durableConfig.agentLaunchInactivityTimeoutSeconds) * 1_000,
161
+ onHostCreated: ({ binding, pane }) => {
162
+ sessionOwners.recordHostOwner({
163
+ owner: binding.owner,
164
+ agentId: binding.agentId,
165
+ adapterId: binding.adapterId,
166
+ ...(binding.nativeSessionId === undefined
167
+ ? {}
168
+ : { nativeSessionId: binding.nativeSessionId }),
169
+ ...(pane.pid === undefined ? {} : { panePid: pane.pid })
170
+ });
175
171
  }
176
172
  });
177
- const lifecycleHost = {
178
- inspectOwner: (owner) => (sessionHost.inspectOwner(owner)),
179
- ...(sessionHost.inspectOwners === undefined
180
- ? {}
181
- : {
182
- inspectOwners: (owners) => sessionHost.inspectOwners(owners)
183
- }),
184
- stopOwner: (owner) => {
185
- // Issue 03: the durable `stopped` transition is gated on physical
186
- // exit proof. A blocked result keeps the Session non-terminal and
187
- // preserves owner records for Operator recovery.
188
- return sessionOwners.terminateOwner(owner).then((result) => {
189
- if (result.outcome === "stop-blocked") {
190
- (options.onError ?? (() => undefined))(new Error(`Role runtime cleanup could not prove physical exit: ${result.remaining
191
- .map(({ record, detail }) => `${record.runtimeGenerationId}: ${detail}`)
192
- .join("; ")}`));
173
+ const promptPush = options.promptPush
174
+ ?? new AgentHostPromptPushAdapter(home);
175
+ const runtimeIsolation = options.runtimeIsolation
176
+ ?? new FileTaskRuntimeIsolation({
177
+ // A sibling of the exact control Home keeps provider data/cache/tmp out
178
+ // of both the shared control plane and every managed Git workspace.
179
+ runtimeRoot: `${resolve(home)}.task-runtimes`,
180
+ controlPlane: {
181
+ yuiHome: home,
182
+ controllerSocketPath: controllerSocketPath(homeId),
183
+ tmuxNamespace: yuiTmuxServerName(home),
184
+ globalInstallPaths: [process.execPath]
193
185
  }
194
- return result.outcome === "stop-confirmed";
195
186
  });
196
- },
197
- ...(runtimeIsolation.cleanupTaskLaunch === undefined
198
- ? {}
199
- : {
200
- cleanupTaskLaunch: (input) => runtimeIsolation.cleanupTaskLaunch(input)
201
- })
202
- };
203
- const resourceActivity = createRuntimeResourceActivityTracker();
204
- let runningRuntime;
205
- let runningController;
206
- const signalRuntimeCleanup = (target) => {
207
- runningRuntime?.signal(runtimeLifecycleSignalKey(target.kind === "role-runtime"
208
- ? {
209
- scope: "task",
210
- taskId: target.taskId,
211
- roleName: target.roleName
212
- }
213
- : { scope: "global", roleName: target.roleName }));
214
- };
215
- const launchCoordinator = new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
216
- ...(options.now === undefined ? {} : { now: options.now }),
217
- assertCurrent: (request) => {
218
- assertRuntimeLaunchRequestCurrent(store, request);
219
- },
220
- onCleanupRequired: signalRuntimeCleanup,
221
- runtimeIsolation
222
- });
223
- // One inventory scan per scheduler pass. When the inventory worker is active
224
- // the blocking /proc scan runs there; otherwise it runs on the main thread.
225
- const scanInventory = (panes) => inventoryClient !== undefined
226
- ? inventoryClient.scan({
227
- currentHome: home,
228
- scope: "current",
229
- panes,
230
- ...(options.environment === undefined
231
- ? {}
232
- : { environment: options.environment })
233
- })
234
- : scanControllerResourceInventory({
235
- currentHome: home,
236
- scope: "current",
237
- panes,
238
- tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
239
- ...(options.environment === undefined
187
+ const lifecycleHost = {
188
+ inspectOwner: (owner) => (sessionHost.inspectOwner(owner)),
189
+ ...(sessionHost.inspectOwners === undefined
240
190
  ? {}
241
- : { environment: options.environment })
191
+ : {
192
+ inspectOwners: (owners) => sessionHost.inspectOwners(owners)
193
+ }),
194
+ stopOwner: (owner) => {
195
+ // Issue 03: the durable `stopped` transition is gated on physical
196
+ // exit proof. A blocked result keeps the Session non-terminal and
197
+ // preserves owner records for Operator recovery.
198
+ return sessionOwners.terminateOwner(owner).then((result) => {
199
+ if (result.outcome === "stop-blocked") {
200
+ (options.onError ?? (() => undefined))(new Error(`Role runtime cleanup could not prove physical exit: ${result.remaining
201
+ .map(({ record, detail }) => `PID ${record.providerRoot.pid}: ${detail}`)
202
+ .join("; ")}`));
203
+ }
204
+ return result.outcome === "stop-confirmed";
205
+ });
206
+ },
207
+ };
208
+ const resourceActivity = createRuntimeResourceActivityTracker();
209
+ let runningRuntime;
210
+ let runningController;
211
+ const launchCoordinator = new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
212
+ ...(options.now === undefined ? {} : { now: options.now }),
213
+ assertCurrent: (request) => {
214
+ assertRuntimeLaunchRequestCurrent(store, request);
215
+ },
216
+ runtimeIsolation
242
217
  });
243
- const delivery = options.delivery ?? new ExecutorRegistry(planner, tmux, agentProcessReadinessProbe, {
244
- sessionHost,
245
- promptPush,
246
- launchCoordinator,
247
- roleResourceInventory: async (panes, inputs) => {
248
- const inventory = await scanInventory(panes);
249
- return inventory.resources.flatMap((resource) => {
250
- if (resource.kind !== "agent-session")
251
- return [];
252
- const owner = resource.owner;
253
- if (owner.kind !== "task-role")
254
- return [];
255
- const active = resource.state === "running" || resource.state === "current";
256
- const input = inputs.find((candidate) => (candidate.taskId === owner.taskId
257
- && candidate.roleName === owner.roleName));
258
- if (input === undefined)
259
- return [];
260
- const identity = owner.turnId === undefined
261
- || owner.adapterId === undefined
262
- || (owner.nativeSessionId === undefined && owner.runtimeGenerationId === undefined)
263
- || (owner.nativeSessionId !== undefined && owner.nativeSessionId.trim().length === 0)
264
- || (owner.runtimeGenerationId !== undefined && owner.runtimeGenerationId.trim().length === 0)
265
- ? undefined
266
- : {
267
- taskId: owner.taskId,
268
- roleName: owner.roleName,
269
- turnId: owner.turnId,
270
- agentId: owner.agentId,
271
- adapterId: owner.adapterId,
272
- ...(owner.nativeSessionId === undefined
273
- ? {}
274
- : { nativeSessionId: owner.nativeSessionId }),
275
- ...(owner.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: owner.runtimeGenerationId })
276
- };
277
- const changed = !active || input.turnId === undefined
278
- ? false
279
- : resourceActivity({
280
- taskId: input.taskId,
281
- roleName: input.roleName,
282
- turnId: input.turnId,
283
- agentId: input.agentId,
284
- adapterId: input.adapterId,
285
- ...(input.nativeSessionId === undefined
286
- ? {}
287
- : { nativeSessionId: input.nativeSessionId }),
288
- ...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId })
289
- }, resource);
290
- return [{
291
- taskId: owner.taskId,
292
- roleName: owner.roleName,
293
- resource: {
294
- observedAt: inventory.observedAt,
295
- active,
296
- changed: active && changed,
297
- ...(identity === undefined ? {} : { identity }),
298
- ...(input.progressAt === undefined ? {} : { progressAt: input.progressAt }),
299
- cpuTimeMs: resource.cpuTimeMs,
300
- ...(resource.ioReadBytes === undefined
301
- ? {}
302
- : { ioReadBytes: resource.ioReadBytes }),
303
- ...(resource.ioWriteBytes === undefined
304
- ? {}
305
- : { ioWriteBytes: resource.ioWriteBytes }),
306
- rssBytes: resource.rssBytes
307
- }
308
- }];
309
- });
310
- }
311
- });
312
- const workspacePreparer = options.workspacePreparer
313
- ?? new FileTaskWorkspacePreparer(home, store);
314
- const resourceReaper = options.resourceReaper
315
- ?? (domainIdentity === undefined
316
- ? undefined
317
- : createEphemeralResourceReaper({
218
+ // One inventory scan per scheduler pass. When the inventory worker is active
219
+ // the blocking /proc scan runs there; otherwise it runs on the main thread.
220
+ const scanInventory = (panes) => inventoryClient !== undefined
221
+ ? inventoryClient.scan({
222
+ currentHome: home,
223
+ scope: "current",
224
+ panes,
225
+ ...(options.environment === undefined
226
+ ? {}
227
+ : { environment: options.environment })
228
+ })
229
+ : scanControllerResourceInventory({
318
230
  currentHome: home,
319
- // The detached Controller owns one YUI_HOME. Keep automatic
320
- // recovery bounded to that domain; cross-home cleanup remains an
321
- // explicit `controller cleanup --all` inventory operation.
322
231
  scope: "current",
323
- environment: options.environment,
232
+ panes,
324
233
  tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
325
- // When the worker backend is active, the reaper's scan runs in the
326
- // inventory worker too (same cadence, same inventory shape).
327
- ...(inventoryClient === undefined
234
+ ...(options.environment === undefined
328
235
  ? {}
329
- : {
330
- scan: () => inventoryClient.scan({
331
- currentHome: home,
332
- scope: "current",
333
- ...(options.environment === undefined
236
+ : { environment: options.environment })
237
+ });
238
+ const delivery = options.delivery ?? new ExecutorRegistry(planner, tmux, agentProcessReadinessProbe, {
239
+ sessionHost,
240
+ promptPush,
241
+ launchCoordinator,
242
+ roleResourceInventory: async (panes, inputs) => {
243
+ const inventory = await scanInventory(panes);
244
+ return inventory.resources.flatMap((resource) => {
245
+ if (resource.kind !== "agent-session")
246
+ return [];
247
+ const owner = resource.owner;
248
+ if (owner.kind !== "task-role")
249
+ return [];
250
+ const active = resource.state === "running" || resource.state === "current";
251
+ const input = inputs.find((candidate) => (candidate.taskId === owner.taskId
252
+ && candidate.roleName === owner.roleName));
253
+ if (input === undefined)
254
+ return [];
255
+ const identity = owner.runId === undefined || owner.adapterId === undefined || (owner.nativeSessionId === undefined) || (owner.nativeSessionId !== undefined && owner.nativeSessionId.trim().length === 0)
256
+ ? undefined
257
+ : {
258
+ taskId: owner.taskId,
259
+ roleName: owner.roleName,
260
+ runId: owner.runId,
261
+ agentId: owner.agentId,
262
+ adapterId: owner.adapterId,
263
+ ...(owner.nativeSessionId === undefined
264
+ ? {}
265
+ : { nativeSessionId: owner.nativeSessionId }),
266
+ };
267
+ const changed = !active || input.runId === undefined
268
+ ? false
269
+ : resourceActivity({
270
+ taskId: input.taskId,
271
+ roleName: input.roleName,
272
+ runId: input.runId,
273
+ agentId: input.agentId,
274
+ adapterId: input.adapterId,
275
+ ...(input.nativeSessionId === undefined
334
276
  ? {}
335
- : { environment: options.environment })
277
+ : { nativeSessionId: input.nativeSessionId }),
278
+ }, resource);
279
+ return [{
280
+ taskId: owner.taskId,
281
+ roleName: owner.roleName,
282
+ resource: {
283
+ observedAt: inventory.observedAt,
284
+ active,
285
+ changed: active && changed,
286
+ ...(identity === undefined ? {} : { identity }),
287
+ ...(input.progressAt === undefined ? {} : { progressAt: input.progressAt }),
288
+ cpuTimeMs: resource.cpuTimeMs,
289
+ ...(resource.ioReadBytes === undefined
290
+ ? {}
291
+ : { ioReadBytes: resource.ioReadBytes }),
292
+ ...(resource.ioWriteBytes === undefined
293
+ ? {}
294
+ : { ioWriteBytes: resource.ioWriteBytes }),
295
+ rssBytes: resource.rssBytes
296
+ }
297
+ }];
298
+ });
299
+ }
300
+ });
301
+ const workspacePreparer = options.workspacePreparer
302
+ ?? new FileTaskWorkspacePreparer(home, store);
303
+ const resourceReaper = options.resourceReaper
304
+ ?? (domainIdentity === undefined
305
+ ? undefined
306
+ : createEphemeralResourceReaper({
307
+ currentHome: home,
308
+ // The detached Controller owns one YUI_HOME. Keep automatic
309
+ // recovery bounded to that domain; cross-home cleanup remains an
310
+ // explicit `controller cleanup --all` inventory operation.
311
+ scope: "current",
312
+ environment: options.environment,
313
+ tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
314
+ // When the worker backend is active, the reaper's scan runs in the
315
+ // inventory worker too (same cadence, same inventory shape).
316
+ ...(inventoryClient === undefined
317
+ ? {}
318
+ : {
319
+ scan: () => inventoryClient.scan({
320
+ currentHome: home,
321
+ scope: "current",
322
+ ...(options.environment === undefined
323
+ ? {}
324
+ : { environment: options.environment })
325
+ })
336
326
  })
337
- })
338
- }));
339
- // Issue 10: automatic Resource GC. The runner self-skips unless
340
- // resourcesGcMode=quarantine and resourcesGcAutoQuarantine=true, so wiring
341
- // it unconditionally costs one config read per full pass when disabled.
342
- const resourceAutoGc = options.resourceAutoGc
343
- ?? createResourceAutoGc({
344
- home,
345
- store,
346
- environment: options.environment
327
+ }));
328
+ // Issue 10: automatic Resource GC. The runner self-skips unless
329
+ // resourcesGcMode=quarantine and resourcesGcAutoQuarantine=true, so wiring
330
+ // it unconditionally costs one config read per full pass when disabled.
331
+ const resourceAutoGc = options.resourceAutoGc
332
+ ?? createResourceAutoGc({
333
+ home,
334
+ store,
335
+ environment: options.environment
336
+ });
337
+ const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, launchCoordinator, planner);
338
+ // Process-exit observations are persisted by the Agent Host before socket
339
+ // delivery. Drain them while this Controller is still the only storage
340
+ // writer and before it begins accepting new work after a handover.
341
+ await replayRuntimeProcessExitOutbox(home, async (observation) => {
342
+ await lifecycleDispatcher("runtime.process-exit-observe", observation);
347
343
  });
348
- const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, signalRuntimeCleanup, launchCoordinator, planner);
349
- // Process-exit observations are persisted by the Agent Host before socket
350
- // delivery. Drain them while this Controller is still the only storage
351
- // writer and before it begins accepting new work after a handover.
352
- await replayRuntimeProcessExitOutbox(home, async (observation) => {
353
- await lifecycleDispatcher("runtime.process-exit-observe", observation);
354
- });
355
- // f7/rr5: This same inbox feeds the supervisor's terminal channel and the
356
- // runtime event processor. When a Job reaches a terminal state, the
357
- // supervisor enqueues a durable-job-terminal event; the processor drains it
358
- // on the next pass, waking the Controller immediately instead of waiting for
359
- // the poll interval.
360
- const kernel = createKernelPorts(store, createLinuxProcessPort(), (taskId) => {
361
- runningRuntime?.signal(`task:${taskId}`);
362
- });
363
- const jobSupervisor = new DurableJobSupervisor({
364
- store: schedulerStore,
365
- process: kernel.runner,
366
- artifacts: createFileArtifactPort(home),
367
- authorizeStart: (job) => authorizeJobStart(store, job),
368
- // rr6/f1: Bounded supervision wake. The supervisor signals the Controller
369
- // after spawning a runner (queued→running adoption) and when a runner
370
- // exits (terminal harvest), so a quick job converges without waiting for
371
- // the recovery interval. Closes over runningRuntime, which is assigned
372
- // once startFileTaskController resolves; a wake during shutdown is a
373
- // no-op. The recovery interval stays the cross-restart fallback.
374
- wake: (taskId) => {
375
- try {
376
- runningRuntime?.signal(`task:${taskId}`);
377
- }
378
- catch {
379
- // Controller stopped; the recovery interval remains the fallback.
380
- }
381
- },
382
- terminalEvents: {
383
- deliverTerminalEvent(notice) {
344
+ // f7/rr5: This same inbox feeds the supervisor's terminal channel and the
345
+ // runtime event processor. When a Job reaches a terminal state, the
346
+ // supervisor enqueues a durable-job-terminal event; the processor drains it
347
+ // on the next pass, waking the Controller immediately instead of waiting for
348
+ // the poll interval.
349
+ const kernel = createKernelPorts(store, createLinuxProcessPort(), (taskId) => {
350
+ runningRuntime?.signal(`task:${taskId}`);
351
+ });
352
+ closeKernel = () => kernel.close();
353
+ const webWorkflow = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
354
+ environment: options.environment ?? process.env, onError: options.onError
355
+ });
356
+ const webSurface = createWebTaskSurface(store, { runtime: {
357
+ notifyStateChanged: (taskId) => runningRuntime?.signal(`task:${taskId}`),
358
+ notifyMailboxChanged: (target) => {
359
+ if (target.kind === "role")
360
+ runningRuntime?.signal(`role:${target.taskId}/${target.roleName}`);
361
+ else if (target.kind === "task")
362
+ runningRuntime?.signal(`task:${target.taskId}`);
363
+ },
364
+ reconcileTask: (taskId) => runningRuntime?.signal(`task:${taskId}`)
365
+ }, yuiHome: home });
366
+ const surfaces = new SurfaceContributions(kernel.capabilities.registry);
367
+ const web = createControllerWeb(store, {
368
+ surface: webSurface,
369
+ answerInput: async ({ taskId, inputId, answer }) => webSurface.answer(taskId, inputId, answer),
370
+ panels: {
371
+ list: (taskId) => surfaces.listPanels(kernel.capabilities.authenticateWebQuery(taskId)),
372
+ read: (taskId, ref, input) => surfaces.readPanel(kernel.capabilities.authenticateWebQuery(taskId), ref, input)
373
+ },
374
+ terminal: new TmuxWebTerminalService({
375
+ yuiHome: home, tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin), tmux,
376
+ prepareGlobalRole: (roleName) => webWorkflow.prepareGlobalRoleEnter(roleName),
377
+ environment: options.environment ?? process.env, onError: options.onError
378
+ })
379
+ });
380
+ closeWeb = () => web.close();
381
+ const jobSupervisor = new DurableJobSupervisor({
382
+ store: schedulerStore,
383
+ process: kernel.runner,
384
+ artifacts: createFileArtifactPort(home),
385
+ authorizeStart: (job) => authorizeJobStart(store, job),
386
+ // rr6/f1: Bounded supervision wake. The supervisor signals the Controller
387
+ // after spawning a runner (queued→running adoption) and when a runner
388
+ // exits (terminal harvest), so a quick job converges without waiting for
389
+ // the recovery interval. Closes over runningRuntime, which is assigned
390
+ // once startFileTaskController resolves; a wake during shutdown is a
391
+ // no-op. The recovery interval stays the cross-restart fallback.
392
+ wake: (taskId) => {
384
393
  try {
385
- runtimeEventInbox.enqueueDurableJobTerminal({
386
- scope: "task",
387
- taskId: notice.taskId,
388
- jobId: notice.jobId,
389
- status: notice.status,
390
- outcome: notice.outcome
391
- });
394
+ runningRuntime?.signal(`task:${taskId}`);
392
395
  }
393
- catch (error) {
394
- // Best-effort terminal channel: the terminal transition already
395
- // committed. A delivery failure must not fail the reconcile pass.
396
- (options.onError ?? (() => undefined))(error);
396
+ catch {
397
+ // Controller stopped; the recovery interval remains the fallback.
397
398
  }
398
- }
399
- },
400
- onError: options.onError
401
- });
402
- const jobControl = kernel.jobs;
403
- const continuationReconciler = options.continuationMetadata === undefined
404
- ? undefined
405
- : new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata);
406
- const running = await startFileTaskController(home, schedulerStore, delivery, lifecycleDispatcher, {
407
- intervalMs: options.intervalMs
408
- ?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
409
- signalWindowMs: options.signalWindowMs,
410
- taskConcurrency: options.taskConcurrency
411
- ?? resolveControllerTaskConcurrency(durableConfig.controllerTaskConcurrency),
412
- deliveryRetryMs: options.deliveryRetryMs,
413
- deliveryRetryLimit: options.deliveryRetryLimit,
414
- deliveryTimeoutMs: options.deliveryTimeoutMs
415
- ?? resolveDeliveryTimeoutSeconds(durableConfig.deliveryTimeoutSeconds) * 1_000,
416
- stallWindowMs: options.stallWindowMs
417
- ?? resolveRuntimeHealth(durableConfig.runtimeHealth).stallWindowMs,
418
- diagnosticAfterMs: options.diagnosticAfterMs
419
- ?? resolveRuntimeHealth(durableConfig.runtimeHealth).diagnosticAfterMs,
420
- now: options.now,
421
- onError: options.onError,
422
- lifecycleHost,
423
- jobSupervisor,
424
- jobControl,
425
- capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
426
- ...(continuationReconciler === undefined ? {} : { continuationReconciler }),
427
- ...(resourceReaper === undefined ? {} : { resourceReaper }),
428
- resourceAutoGc,
429
- onExpiredEphemeralDomain: (domain) => {
430
- if (domain.yuiHome !== home)
431
- return;
432
- void runningController?.close().catch(options.onError ?? (() => undefined));
433
- },
434
- workspacePreparer,
435
- runtimeEventProcessor: options.runtimeEventProcessor
436
- ?? (useWorker && asyncStoreClient !== undefined
437
- ? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)))
438
- : new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore)),
439
- runtimeObserver: options.runtimeObserver
440
- ?? new AgentRuntimeObserver(store, runtimeEventInbox),
441
- domainIdentity,
442
- ...(options.configuration !== undefined
443
- ? { configuration: options.configuration }
444
- : options.intervalMs === undefined
445
- ? {
446
- configuration: {
447
- reconciliationIntervalMs: () => reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds)
399
+ },
400
+ terminalEvents: {
401
+ deliverTerminalEvent(notice) {
402
+ try {
403
+ runtimeEventInbox.enqueueDurableJobTerminal({
404
+ scope: "task",
405
+ taskId: notice.taskId,
406
+ jobId: notice.jobId,
407
+ status: notice.status,
408
+ outcome: notice.outcome
409
+ });
410
+ }
411
+ catch (error) {
412
+ // Best-effort terminal channel: the terminal transition already
413
+ // committed. A delivery failure must not fail the reconcile pass.
414
+ (options.onError ?? (() => undefined))(error);
448
415
  }
449
416
  }
450
- : {})
451
- });
452
- runningController = running;
453
- runningRuntime = running.runtime;
454
- // Issue 03: read-only startup reconciliation. Surfaces durable/physical
455
- // Session mismatches (including generations whose durable map was cleared)
456
- // without changing stop or archive behavior. Cleanup stays an explicit
457
- // Operator action in exact-owner-cleanup mode.
458
- try {
459
- const startupReport = sessionOwners.report();
460
- if (startupReport.summary.livePhysicalRoots > 0) {
461
- (options.onError ?? (() => undefined))(new Error(`Session reconciliation: ${startupReport.summary.livePhysicalRoots} `
462
- + `live physical root(s) across ${startupReport.summary.owners} owner record(s); `
463
- + "run `yui session reconcile --report` for details."));
417
+ },
418
+ onError: options.onError
419
+ });
420
+ const jobControl = kernel.jobs;
421
+ const continuationReconciler = options.continuationMetadata === undefined
422
+ ? undefined
423
+ : new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata);
424
+ const running = await startFileTaskController(home, schedulerStore, delivery, (method, params) => method === "web.start" || method === "web.stop" || method === "web.status"
425
+ ? web.dispatch(method, params) : lifecycleDispatcher(method, params), {
426
+ intervalMs: options.intervalMs
427
+ ?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
428
+ signalWindowMs: options.signalWindowMs,
429
+ taskConcurrency: options.taskConcurrency
430
+ ?? resolveControllerTaskConcurrency(durableConfig.controllerTaskConcurrency),
431
+ deliveryRetryMs: options.deliveryRetryMs,
432
+ deliveryRetryLimit: options.deliveryRetryLimit,
433
+ deliveryTimeoutMs: options.deliveryTimeoutMs
434
+ ?? resolveDeliveryTimeoutSeconds(durableConfig.deliveryTimeoutSeconds) * 1_000,
435
+ stallWindowMs: options.stallWindowMs
436
+ ?? resolveRuntimeHealth(durableConfig.runtimeHealth).stallWindowMs,
437
+ diagnosticAfterMs: options.diagnosticAfterMs
438
+ ?? resolveRuntimeHealth(durableConfig.runtimeHealth).diagnosticAfterMs,
439
+ now: options.now,
440
+ onError: options.onError,
441
+ lifecycleHost,
442
+ jobSupervisor,
443
+ jobControl,
444
+ capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
445
+ ...(continuationReconciler === undefined ? {} : { continuationReconciler }),
446
+ ...(resourceReaper === undefined ? {} : { resourceReaper }),
447
+ resourceAutoGc,
448
+ onExpiredEphemeralDomain: (domain) => {
449
+ if (domain.yuiHome !== home)
450
+ return;
451
+ void runningController?.close().catch(options.onError ?? (() => undefined));
452
+ },
453
+ workspacePreparer,
454
+ runtimeEventProcessor: options.runtimeEventProcessor
455
+ ?? (useWorker && asyncStoreClient !== undefined
456
+ ? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)))
457
+ : new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore)),
458
+ runtimeObserver: options.runtimeObserver
459
+ ?? new AgentRuntimeObserver(store, runtimeEventInbox),
460
+ domainIdentity,
461
+ ...(options.configuration !== undefined
462
+ ? { configuration: options.configuration }
463
+ : options.intervalMs === undefined
464
+ ? {
465
+ configuration: {
466
+ reconciliationIntervalMs: () => reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds)
467
+ }
468
+ }
469
+ : {})
470
+ });
471
+ runningController = running;
472
+ runningRuntime = running.runtime;
473
+ // Issue 03: read-only startup reconciliation. Surfaces durable/physical
474
+ // Session mismatches (including generations whose durable map was cleared)
475
+ // without changing stop or archive behavior. Cleanup stays an explicit
476
+ // Operator action in exact-owner-cleanup mode.
477
+ try {
478
+ const startupReport = sessionOwners.report();
479
+ if (startupReport.summary.livePhysicalRoots > 0) {
480
+ (options.onError ?? (() => undefined))(new Error(`Session reconciliation: ${startupReport.summary.livePhysicalRoots} `
481
+ + `live physical root(s) across ${startupReport.summary.owners} owner record(s); `
482
+ + "run `yui session reconcile --report` for details."));
483
+ }
484
+ }
485
+ catch (error) {
486
+ (options.onError ?? (() => undefined))(error);
464
487
  }
488
+ let resourceClose;
489
+ const closeResources = () => {
490
+ resourceClose ??= Promise.all([
491
+ web.close(),
492
+ kernel.close(),
493
+ asyncStoreClient?.close() ?? Promise.resolve(),
494
+ inventoryClient?.close() ?? Promise.resolve()
495
+ ]).then(() => undefined).finally(() => {
496
+ ownedStore?.close();
497
+ });
498
+ return resourceClose;
499
+ };
500
+ const closed = running.closed.then(closeResources);
501
+ return {
502
+ ...running,
503
+ kernel,
504
+ closed,
505
+ close: async () => {
506
+ try {
507
+ await running.close();
508
+ }
509
+ finally {
510
+ // RPC-driven Controller stops resolve `running.closed` without calling
511
+ // this wrapper. Share one cleanup promise so both lifecycle paths
512
+ // release the worker connections before the process can linger.
513
+ await closeResources();
514
+ }
515
+ },
516
+ store,
517
+ schedulerStore,
518
+ planner,
519
+ tmux,
520
+ delivery,
521
+ sessionHost,
522
+ promptPush,
523
+ runtimeIsolation,
524
+ workspacePreparer
525
+ };
465
526
  }
466
527
  catch (error) {
467
- (options.onError ?? (() => undefined))(error);
468
- }
469
- let resourceClose;
470
- const closeResources = () => {
471
- resourceClose ??= Promise.all([
472
- kernel.close(),
528
+ // The socket may never have opened. Startup failure must still release
529
+ // this attempt's workers/instances instead of leaving an uncallable process.
530
+ await Promise.allSettled([
531
+ closeWeb(),
532
+ closeKernel(),
473
533
  asyncStoreClient?.close() ?? Promise.resolve(),
474
534
  inventoryClient?.close() ?? Promise.resolve()
475
- ]).then(() => undefined);
476
- return resourceClose;
477
- };
478
- const closed = running.closed.then(closeResources);
479
- return {
480
- ...running,
481
- kernel,
482
- closed,
483
- close: async () => {
484
- try {
485
- await running.close();
486
- }
487
- finally {
488
- // RPC-driven Controller stops resolve `running.closed` without calling
489
- // this wrapper. Share one cleanup promise so both lifecycle paths
490
- // release the worker connections before the process can linger.
491
- await closeResources();
492
- }
493
- },
494
- store,
495
- schedulerStore,
496
- planner,
497
- tmux,
498
- delivery,
499
- sessionHost,
500
- promptPush,
501
- runtimeIsolation,
502
- workspacePreparer
503
- };
535
+ ]);
536
+ ownedStore?.close();
537
+ throw error;
538
+ }
504
539
  }
505
- export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, onCleanupRequired, sharedLaunchCoordinator, environmentRefresher) {
540
+ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, sharedLaunchCoordinator, environmentRefresher) {
506
541
  const launchCoordinator = sharedLaunchCoordinator
507
542
  ?? new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
508
543
  assertCurrent: (request) => {
509
544
  assertRuntimeLaunchRequestCurrent(store, request);
510
545
  },
511
- onCleanupRequired
512
546
  });
513
547
  const lifecycleTails = new Map();
514
548
  return async (method, params) => {
@@ -528,9 +562,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
528
562
  schedulerStore.beginAgentHostProviderTurn({
529
563
  taskId: value.taskId,
530
564
  roleName: value.roleName,
531
- ...(value.turnId === undefined ? {} : { turnId: value.turnId }),
565
+ ...(value.runId === undefined ? {} : { runId: value.runId }),
532
566
  agentId: value.agentId,
533
- runtimeGenerationId: value.runtimeGenerationId,
534
567
  nativeSessionId: value.nativeSessionId,
535
568
  attemptId: value.attemptId,
536
569
  authorityEpoch: value.authorityEpoch,
@@ -540,6 +573,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
540
573
  });
541
574
  }
542
575
  catch (error) {
576
+ if (error instanceof AgentHostProviderSessionBusyError)
577
+ throw error;
543
578
  if (error instanceof AgentHostProviderTurnFenceError) {
544
579
  throw applicationError("INVALID_PARAMS", error.message);
545
580
  }
@@ -552,7 +587,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
552
587
  const status = params.status;
553
588
  const reason = params.reason;
554
589
  const raw = params.raw;
555
- if ((status !== "rejected" && status !== "delivery-unknown")
590
+ if ((status !== "rejected" && status !== "deferred" && status !== "delivery-unknown")
556
591
  || typeof reason !== "string" || reason.trim().length === 0
557
592
  || typeof raw !== "string" || raw.trim().length === 0) {
558
593
  throw applicationError("INVALID_PARAMS", "Provider Turn resolution is invalid.");
@@ -560,7 +595,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
560
595
  schedulerStore.resolveAgentHostProviderTurnSubmission({
561
596
  taskId: value.taskId,
562
597
  roleName: value.roleName,
563
- ...(value.turnId === undefined ? {} : { turnId: value.turnId }),
598
+ ...(value.runId === undefined ? {} : { runId: value.runId }),
564
599
  attemptId: value.attemptId,
565
600
  status,
566
601
  reason,
@@ -571,9 +606,9 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
571
606
  }
572
607
  if (method === "runtime.process-exit-observe") {
573
608
  const observation = validateRuntimeProcessExitObservation(params);
574
- const run = observation.taskId === undefined || observation.turnId === undefined
609
+ const run = observation.taskId === undefined || observation.runId === undefined
575
610
  ? null
576
- : store.getTurn(observation.taskId, observation.turnId);
611
+ : store.getRun(observation.taskId, observation.runId);
577
612
  const globalRole = observation.taskId === undefined
578
613
  ? store.getGlobalRole(observation.roleName)
579
614
  : null;
@@ -582,31 +617,29 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
582
617
  const driver = adapterId === undefined
583
618
  ? null
584
619
  : builtinAgentDriverRegistry().findByAdapterId(adapterId);
585
- const turnTerminalObserved = observation.taskId !== undefined
586
- && observation.turnId !== undefined
620
+ const runTerminalObserved = observation.taskId !== undefined
621
+ && observation.runId !== undefined
587
622
  && store.listEvents(observation.taskId).some((event) => {
588
623
  const runtime = runtimeObservationFromTaskEvent(event);
589
624
  return runtime !== null
590
- && runtime.fence.turnId === observation.turnId
625
+ && runtime.fence.runId === observation.runId
591
626
  && ["turn.completed", "turn.failed", "turn.cancelled"].includes(runtime.kind)
592
627
  && Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
593
628
  });
594
- const turnFailureObserved = observation.taskId !== undefined
595
- && observation.turnId !== undefined
596
- && store.listEvents(observation.taskId).some((event) => {
597
- const runtime = runtimeObservationFromTaskEvent(event);
598
- return runtime !== null
599
- && runtime.fence.turnId === observation.turnId
600
- && runtime.fence.runtimeGenerationId === observation.runtimeGenerationId
601
- && runtime.kind === "turn.failed"
602
- && Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
603
- });
629
+ const runFailureObserved = observation.taskId !== undefined && observation.runId !== undefined && store.listEvents(observation.taskId).some((event) => {
630
+ const runtime = runtimeObservationFromTaskEvent(event);
631
+ return runtime !== null
632
+ && runtime.fence.runId === observation.runId
633
+ && runtime.fence.nativeSessionId === observation.nativeSessionId
634
+ && runtime.kind === "turn.failed"
635
+ && Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
636
+ });
604
637
  const classification = classifyRuntimeProcessExit(observation, {
605
638
  ...(driver === null
606
639
  ? {}
607
640
  : { childLifecycle: driver.capabilities.lifecycle.providerProcess }),
608
- turnTerminalObserved,
609
- turnFailureObserved
641
+ runTerminalObserved,
642
+ runFailureObserved
610
643
  });
611
644
  if (observation.taskId === undefined) {
612
645
  const recorded = appendGlobalProcessExitObservation(store.rootDirectory(), observation, classification);
@@ -624,7 +657,6 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
624
657
  observationId: observation.observationId,
625
658
  processKind: observation.processKind,
626
659
  roleName: observation.roleName,
627
- runtimeGenerationId: observation.runtimeGenerationId,
628
660
  observedAt: observation.observedAt,
629
661
  classification,
630
662
  observation: JSON.stringify(observation)
@@ -637,16 +669,14 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
637
669
  if (params === null || typeof params !== "object" || Array.isArray(params)) {
638
670
  throw applicationError("INVALID_PARAMS", "Launch redemption params are invalid.");
639
671
  }
640
- const runtimeGenerationId = params.runtimeGenerationId;
641
672
  const ticket = params.ticket;
642
673
  const hostPid = params.hostPid;
643
- if (typeof runtimeGenerationId !== "string" || typeof ticket !== "string"
644
- || !Number.isSafeInteger(hostPid) || hostPid <= 0) {
674
+ if (typeof ticket !== "string" || !Number.isSafeInteger(hostPid) || hostPid <= 0) {
645
675
  throw applicationError("INVALID_PARAMS", "Launch redemption identity is invalid.");
646
676
  }
647
677
  // The launch payload is validated at reservation time and every member
648
678
  // of its discriminated Provider-control union is JSON serializable.
649
- return launchBrokerForHome(store.rootDirectory()).redeem(runtimeGenerationId, ticket);
679
+ return launchBrokerForHome(store.rootDirectory()).redeem(ticket);
650
680
  }
651
681
  if (method === "runtime.replace-agent-environment") {
652
682
  if (environmentRefresher === undefined) {
@@ -680,13 +710,31 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
680
710
  const task = request.scope === "task"
681
711
  ? store.getTask(request.taskId)
682
712
  : null;
713
+ // Resolve the admitted Turn first: its purpose decides which Task
714
+ // lifecycle and workspace fences apply. A Draft Leader planning Turn runs
715
+ // without a Task workspace; every delivery Turn keeps the full fence.
716
+ const activeRun = request.scope === "task"
717
+ ? store.getActiveRun(request.taskId, request.roleName)
718
+ : null;
719
+ if (request.scope === "task" && activeRun === null) {
720
+ throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Turn; it cannot create an empty Provider Conversation.");
721
+ }
683
722
  if (request.scope === "task"
684
- && (task?.status !== "active" || task.executionGate.state !== "enabled")) {
723
+ && (task === null || !runPurposeAdmitsTaskState(activeRun.purpose, task))) {
685
724
  throw applicationError("INVALID_PARAMS", task === null
686
725
  ? `Task not found: ${request.taskId}.`
687
726
  : `Task execution is not enabled: ${request.taskId}.`);
688
727
  }
689
- if (request.scope === "task" && task !== null) {
728
+ const planningDraft = request.scope === "task"
729
+ && activeRun?.purpose === "planning"
730
+ && task?.status === "draft";
731
+ // A Task activated with an empty environment plan owns no managed
732
+ // workspace, so there is nothing to prove ready. Every Task that does own
733
+ // one keeps the full ownership fence.
734
+ if (request.scope === "task"
735
+ && task !== null
736
+ && !planningDraft
737
+ && taskOwnsManagedWorkspace(task)) {
690
738
  const taskWorkspace = store.getTaskWorkspace(task.id);
691
739
  if (!isTaskOwnedWorkspace(taskWorkspace, task.id, task.cwd, task.projectBindings.map(({ projectId, directory }) => ({ projectId, directory })))) {
692
740
  throw new Error(`Task workspace is not ready: ${task.id}.`);
@@ -708,20 +756,24 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
708
756
  ? `Role not found: ${request.taskId}/${request.roleName}.`
709
757
  : `Global Role not found: ${request.roleName}.`);
710
758
  }
711
- const activeTurn = request.scope === "task"
712
- ? store.getActiveTurn(request.taskId, request.roleName)
713
- : null;
714
- if (request.scope === "task" && activeTurn === null) {
715
- throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Turn; it cannot create an empty Provider Conversation.");
716
- }
717
759
  const managedWorkspace = request.scope === "task"
718
- ? activeTurn?.workspace
760
+ ? activeRun?.workspace
719
761
  ?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
720
762
  : undefined;
763
+ // Distinguish "this Task owns no workspace by design" from "the
764
+ // authoritative workspace is missing". Only the former may launch without
765
+ // one. Two cases qualify: an empty plan binding no Project, and a Draft
766
+ // planning conversation, which may bind a Project but has not activated, so
767
+ // no worktree exists or is owed. Without the planning term a Project-bound
768
+ // Draft fails closed on a workspace activation was never asked to create.
769
+ const workspaceFree = request.scope === "task"
770
+ && managedWorkspace === undefined
771
+ && task !== null
772
+ && (planningDraft || !taskOwnsManagedWorkspace(task));
721
773
  const sessions = request.scope === "task"
722
774
  ? store.getTaskRoleSessionSet(request.taskId, request.roleName)
723
775
  : store.getGlobalRoleSessionSet(request.roleName);
724
- const effective = activeTurn?.effective
776
+ const effective = activeRun?.effective
725
777
  ?? activeLiveRoleAgentSession(sessions)?.effective
726
778
  ?? resolveRuntimeDesiredEffective(store, request, role);
727
779
  const binding = role.agentBindings[effective.agentId];
@@ -734,12 +786,12 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
734
786
  }
735
787
  validateLifecycleEnvironment(request.environment, agent);
736
788
  const session = sessions?.sessions[effective.agentId];
737
- // An ensure call may reattach the already-admitted Turn to its existing
789
+ // An ensure call may reattach the already-admitted AgentRun to its existing
738
790
  // Conversation, but it may not manufacture a fresh Conversation without a
739
- // pending managed Turn. Fresh replacement remains owned by Turn dispatch.
740
- const mode = activeTurn === null
791
+ // pending managed AgentRun. Fresh replacement remains owned by AgentRun dispatch.
792
+ const mode = activeRun === null
741
793
  ? roleAgentSessionResumeMode(sessions, effective.agentId, effective)
742
- : session?.nativeSessionId === undefined ? activeTurn.mode : "resume";
794
+ : session?.nativeSessionId === undefined ? activeRun.mode : "resume";
743
795
  if (request.scope === "task" && mode === "resume"
744
796
  && (session?.nativeSessionId === undefined
745
797
  || session.nativeSessionId.trim().length === 0)) {
@@ -755,7 +807,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
755
807
  effective,
756
808
  workspace: effective.workspace.root,
757
809
  ...(managedWorkspace === undefined ? {} : { managedWorkspace }),
758
- ...(activeTurn === null ? {} : { turnId: activeTurn.id }),
810
+ ...(workspaceFree ? { workspaceFree: true } : {}),
811
+ ...(activeRun === null ? {} : { runId: activeRun.id }),
759
812
  ...(request.environment === undefined
760
813
  ? {}
761
814
  : { environment: request.environment })
@@ -768,7 +821,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
768
821
  nativeSessionId: session.nativeSessionId
769
822
  };
770
823
  const assertCurrent = () => assertRuntimeLaunchRequestCurrent(store, launchRequest);
771
- const runtimeBinding = await launchCoordinator.prepare(launchRequest, "immediate", assertCurrent);
824
+ const runtimeBinding = await launchCoordinator.prepare(launchRequest, assertCurrent);
772
825
  return {
773
826
  ensured: true,
774
827
  sessionStarted: runtimeBinding.hostCreated === true,
@@ -794,7 +847,7 @@ function resolveRuntimeDesiredEffective(store, request, role) {
794
847
  }
795
848
  const taskRole = role;
796
849
  const item = store.listWorkItems(request.taskId).find((candidate) => (candidate.assignee === request.roleName
797
- && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
850
+ && !["accepted", "retired"].includes(candidate.status))) ?? null;
798
851
  const workspace = (item === null
799
852
  ? store.getTaskWorkspace(request.taskId)
800
853
  : store.getWorkItemWorkspace(request.taskId, item.id))
@@ -802,24 +855,28 @@ function resolveRuntimeDesiredEffective(store, request, role) {
802
855
  ?? undefined;
803
856
  return resolveEffectiveLaunch({
804
857
  role: taskRole,
805
- purpose: "execution",
858
+ purpose: store.getTask(request.taskId)?.status === "draft" && request.roleName === "leader"
859
+ ? "planning" : "execution",
806
860
  ...(workspace === undefined ? {} : { workspace }),
807
861
  ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
808
862
  });
809
863
  }
810
864
  function assertRuntimeLaunchRequestCurrent(store, request) {
811
- let activeTurn = null;
865
+ let activeRun = null;
812
866
  if (request.owner.scope === "task") {
813
867
  const task = store.getTask(request.owner.taskId);
814
- if (task === null
815
- || task.status !== "active"
816
- || task.executionGate.state !== "enabled") {
868
+ if (task === null) {
817
869
  throw new Error(`Task is no longer active: ${request.owner.taskId}.`);
818
870
  }
819
- activeTurn = store.getActiveTurn(request.owner.taskId, request.owner.roleName);
820
- if (request.turnId !== undefined
821
- && activeTurn?.id !== request.turnId) {
822
- throw new Error(`Role Turn is no longer current: ${request.turnId}.`);
871
+ activeRun = store.getActiveRun(request.owner.taskId, request.owner.roleName);
872
+ const purpose = activeRun?.purpose
873
+ ?? (task.status === "draft" && request.owner.roleName === "leader" ? "planning" : "execution");
874
+ if (!runPurposeAdmitsTaskState(purpose, task)) {
875
+ throw new Error(`Task is no longer active: ${request.owner.taskId}.`);
876
+ }
877
+ if (request.runId !== undefined
878
+ && activeRun?.id !== request.runId) {
879
+ throw new Error(`Role AgentRun is no longer current: ${request.runId}.`);
823
880
  }
824
881
  }
825
882
  const role = request.owner.scope === "task"
@@ -831,8 +888,8 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
831
888
  const sessions = request.owner.scope === "task"
832
889
  ? store.getTaskRoleSessionSet(request.owner.taskId, request.owner.roleName)
833
890
  : store.getGlobalRoleSessionSet(request.owner.roleName);
834
- const expectedEffective = request.owner.scope === "task" && request.turnId !== undefined
835
- ? activeTurn?.effective
891
+ const expectedEffective = request.owner.scope === "task" && request.runId !== undefined
892
+ ? activeRun?.effective
836
893
  : activeLiveRoleAgentSession(sessions)?.effective
837
894
  ?? currentDesiredEffective(store, request, role);
838
895
  if (expectedEffective === undefined
@@ -843,7 +900,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
843
900
  throw new Error(`Role launch state changed: ${request.owner.roleName}.`);
844
901
  }
845
902
  if (request.owner.scope === "task" && request.managedWorkspace !== undefined) {
846
- const expectedWorkspace = activeTurn?.workspace
903
+ const expectedWorkspace = activeRun?.workspace
847
904
  ?? currentDesiredManagedWorkspace(store, request.owner.taskId, request.owner.roleName);
848
905
  if (expectedWorkspace === undefined
849
906
  || !sameManagedWorkspaceIdentity(expectedWorkspace, request.managedWorkspace)) {
@@ -868,13 +925,6 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
868
925
  || session.nativeSessionId !== request.nativeSessionId) {
869
926
  throw new Error(`Native session changed: ${request.owner.roleName}.`);
870
927
  }
871
- // The Role's durable Session record is the only authority for which Host
872
- // activation may be restored. Targeting anything else would revive a
873
- // historical activation.
874
- if (request.hostActivationId !== undefined
875
- && session.runtimeGenerationId !== request.hostActivationId) {
876
- throw new Error(`Session restore does not target the Role's current Host activation: ${request.owner.roleName}.`);
877
- }
878
928
  if (!roleSessionMayContinue(session.effective, request.effective)) {
879
929
  throw new Error(`Native session cannot continue under this launch: ${request.owner.roleName}.`);
880
930
  }
@@ -885,7 +935,7 @@ function currentDesiredEffective(store, request, role) {
885
935
  return resolveEffectiveLaunch({ role: role, purpose: "execution" });
886
936
  }
887
937
  const item = store.listWorkItems(request.owner.taskId).find((candidate) => (candidate.assignee === request.owner.roleName
888
- && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
938
+ && !["accepted", "retired"].includes(candidate.status))) ?? null;
889
939
  const workspace = (item === null
890
940
  ? store.getTaskWorkspace(request.owner.taskId)
891
941
  : store.getWorkItemWorkspace(request.owner.taskId, item.id))
@@ -893,14 +943,15 @@ function currentDesiredEffective(store, request, role) {
893
943
  ?? undefined;
894
944
  return resolveEffectiveLaunch({
895
945
  role: role,
896
- purpose: "execution",
946
+ purpose: store.getTask(request.owner.taskId)?.status === "draft" && request.owner.roleName === "leader"
947
+ ? "planning" : "execution",
897
948
  ...(workspace === undefined ? {} : { workspace }),
898
949
  ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
899
950
  });
900
951
  }
901
952
  function currentDesiredManagedWorkspace(store, taskId, roleName) {
902
953
  const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
903
- && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
954
+ && !["accepted", "retired"].includes(candidate.status))) ?? null;
904
955
  return (item === null
905
956
  ? store.getTaskWorkspace(taskId)
906
957
  : store.getWorkItemWorkspace(taskId, item.id))
@@ -1044,7 +1095,7 @@ function validateEnvironmentRefreshSources(store, refresh) {
1044
1095
  throw applicationError("INVALID_PARAMS", `Runtime native Agent environment source is not declared: ${name}.`);
1045
1096
  }
1046
1097
  }
1047
- const allowedNative = new Set(["CODEX_HOME", "CLAUDE_CONFIG_DIR"]);
1098
+ const allowedNative = new Set(NATIVE_AGENT_ENVIRONMENT_NAMES);
1048
1099
  if (refresh.nativeNames.some((name) => !allowedNative.has(name))) {
1049
1100
  throw applicationError("INVALID_PARAMS", "Runtime native Agent environment scope is invalid.");
1050
1101
  }
@@ -1074,9 +1125,8 @@ function providerTurnControlParams(params) {
1074
1125
  return {
1075
1126
  taskId: requiredParam(value.taskId),
1076
1127
  roleName: requiredParam(value.roleName),
1077
- ...(value.turnId === undefined ? {} : { turnId: requiredParam(value.turnId) }),
1128
+ ...(value.runId === undefined ? {} : { runId: requiredParam(value.runId) }),
1078
1129
  agentId: requiredParam(value.agentId),
1079
- runtimeGenerationId: requiredParam(value.runtimeGenerationId),
1080
1130
  nativeSessionId: requiredParam(value.nativeSessionId),
1081
1131
  attemptId: requiredParam(value.attemptId),
1082
1132
  authorityEpoch: authorityEpoch,