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