@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
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The smallest stable, provider-neutral Yui lifecycle vocabulary.
3
+ *
4
+ * Each phase is a durable semantic fact about a managed Agent Session or the
5
+ * AgentRun whose prompt it carries. Agent Drivers translate native signals into
6
+ * these phases; the scheduler and durable fold reason about phases, sources,
7
+ * and evidence — never about provider names.
8
+ */
9
+ import { isAgentAdapterId } from "../agent/adapterCatalog.js";
1
10
  export function isPreInputReadinessSupported(capability) {
2
11
  return capability.status === "supported";
3
12
  }
@@ -9,17 +18,17 @@ export class CanonicalLifecycleError extends Error {
9
18
  this.name = "CanonicalLifecycleError";
10
19
  }
11
20
  }
12
- const TURN_SCOPED_PHASES = new Set([
21
+ const RUN_SCOPED_PHASES = new Set([
13
22
  "prompt-pushed",
14
23
  "provider-accepted",
15
24
  "turn-progress",
16
25
  "turn-terminal"
17
26
  ]);
18
27
  /**
19
- * Phases that assert durable provider truth about a Turn. Every one must carry
20
- * the complete turn-scoped identity — turnId AND the generation fences
21
- * (nativeSessionId + runtimeGenerationId) AND, where the transport owns it, the receiptId —
22
- * because a missing or wrong fence must fail closed rather than advance a Turn or
28
+ * Phases that assert durable provider truth about a AgentRun. Every one must carry
29
+ * the complete turn-scoped identity — turnId AND the Session fences
30
+ * (nativeSessionId) AND, where the transport owns it, the receiptId —
31
+ * because a missing or wrong fence must fail closed rather than advance a AgentRun or
23
32
  * its successor.
24
33
  */
25
34
  const PROVIDER_TRUTH_PHASES = new Set([
@@ -34,7 +43,7 @@ const PROVIDER_TRUTH_PHASES = new Set([
34
43
  * (acceptance/progress/terminal), so neither a transport/liveness signal nor a
35
44
  * shape-only `adapter-mapped` observation can ever forge or terminalize a turn.
36
45
  * `adapter-mapped` remains permissible only for the pre-acceptance
37
- * session-started/ready phases, whose fold never advances Turn truth.
46
+ * session-started/ready phases, whose fold never advances AgentRun truth.
38
47
  */
39
48
  const PHASE_EVIDENCE_MATRIX = {
40
49
  "host-process-created": { sources: ["controller"], evidence: ["controller"] },
@@ -77,18 +86,15 @@ export function createCanonicalLifecycleEvent(input) {
77
86
  throw new CanonicalLifecycleError(`Phase ${input.phase} rejects evidence ${input.evidence}; only ${rule.evidence.join("/")} may prove it.`);
78
87
  }
79
88
  const fence = normalizeFence(input.fence);
80
- if (TURN_SCOPED_PHASES.has(input.phase) && fence.turnId === undefined) {
81
- throw new CanonicalLifecycleError(`Phase ${input.phase} requires a turnId fence.`);
89
+ if (RUN_SCOPED_PHASES.has(input.phase) && fence.runId === undefined) {
90
+ throw new CanonicalLifecycleError(`Phase ${input.phase} requires a runId fence.`);
82
91
  }
83
- // Provider-truth phases must carry the complete generation fence so a stale or
84
- // wrong generation can never advance the Turn.
92
+ // Provider-truth phases must carry the complete Session fence so a stale or
93
+ // wrong Session can never advance the AgentRun.
85
94
  if (PROVIDER_TRUTH_PHASES.has(input.phase)) {
86
95
  if (fence.nativeSessionId === undefined) {
87
96
  throw new CanonicalLifecycleError(`Phase ${input.phase} requires a nativeSessionId fence.`);
88
97
  }
89
- if (fence.runtimeGenerationId === undefined) {
90
- throw new CanonicalLifecycleError(`Phase ${input.phase} requires a runtimeGenerationId fence.`);
91
- }
92
98
  }
93
99
  if (input.phase === "prompt-pushed" && fence.receiptId === undefined) {
94
100
  throw new CanonicalLifecycleError("A prompt-pushed event requires a transport receiptId.");
@@ -133,7 +139,7 @@ export function createCanonicalLifecycleEvent(input) {
133
139
  * The provider-neutral decision. Given a validated canonical event and the
134
140
  * consumer's current expectation, it returns what should happen — idempotent for
135
141
  * duplicates, deferred for not-yet-applicable facts, obsolete for stale/wrong
136
- * generation, fail-closed for contract violations. It reads only phase / source
142
+ * Session, fail-closed for contract violations. It reads only phase / source
137
143
  * / evidence / identity, never the adapter name, and mutates nothing.
138
144
  */
139
145
  export function foldCanonicalLifecycleEvent(event, expectation) {
@@ -154,10 +160,7 @@ export function foldCanonicalLifecycleEvent(event, expectation) {
154
160
  }
155
161
  // Discovery may bind a previously-unknown native id, but only under the
156
162
  // exact launch fence and only when none is bound yet.
157
- if (expectation.boundNativeSessionId === undefined
158
- && event.fence.nativeSessionId !== undefined
159
- && event.fence.runtimeGenerationId !== undefined
160
- && event.fence.runtimeGenerationId === expectation.fence.runtimeGenerationId) {
163
+ if (expectation.boundNativeSessionId === undefined && event.fence.nativeSessionId !== undefined) {
161
164
  return { outcome: "bind-native-session", nativeSessionId: event.fence.nativeSessionId };
162
165
  }
163
166
  return { outcome: "mark-session-started" };
@@ -176,7 +179,7 @@ export function foldCanonicalLifecycleEvent(event, expectation) {
176
179
  return { outcome: "obsolete", reason: "push-after-terminal" };
177
180
  if (expectation.accepted)
178
181
  return { outcome: "obsolete", reason: "push-after-accepted" };
179
- // Exactly one push: a repeat for an already-pushed Turn is a no-op, never a
182
+ // Exactly one push: a repeat for an already-pushed AgentRun is a no-op, never a
180
183
  // second Enter.
181
184
  if (expectation.pushed)
182
185
  return { outcome: "idempotent", reason: "already-pushed" };
@@ -209,7 +212,7 @@ export function foldCanonicalLifecycleEvent(event, expectation) {
209
212
  return { outcome: "idempotent", reason: "already-terminal" };
210
213
  if (expectation.accepted)
211
214
  return { outcome: "advance-terminal" };
212
- // A terminal for a pushed-but-unaccepted Turn must NOT promote acceptance;
215
+ // A terminal for a pushed-but-unaccepted AgentRun must NOT promote acceptance;
213
216
  // hold the immutable fact until (or unless) acceptance folds first.
214
217
  if (expectation.pushed)
215
218
  return { outcome: "deferred", reason: "terminal-before-accept" };
@@ -219,12 +222,12 @@ export function foldCanonicalLifecycleEvent(event, expectation) {
219
222
  }
220
223
  }
221
224
  /**
222
- * Returns null when the event belongs to the expected generation/Turn, or a
225
+ * Returns null when the event belongs to the expected Session/AgentRun, or a
223
226
  * mismatch reason otherwise. Owner/adapter and turn-scoped turnId are always
224
227
  * compared. Provider-truth phases (acceptance/progress/terminal) require the
225
- * COMPLETE generation fence — nativeSessionId, runtimeGenerationId, and (for acceptance)
226
- * receiptId must be present and equal, so a missing or wrong generation/receipt
227
- * fails closed rather than advancing a Turn or its successor. Where a native id
228
+ * COMPLETE Session fence — nativeSessionId, and (for acceptance)
229
+ * receiptId must be present and equal, so a missing or wrong Session/receipt
230
+ * fails closed rather than advancing a AgentRun or its successor. Where a native id
228
231
  * is already bound, every turn-scoped fact must match it.
229
232
  */
230
233
  function matchesFence(expectation, event) {
@@ -239,29 +242,23 @@ function matchesFence(expectation, event) {
239
242
  return "fence-mismatch:agentId";
240
243
  if (expected.adapterId !== actual.adapterId)
241
244
  return "fence-mismatch:adapterId";
242
- // Turn-scoped phases must name the exact expected Turn.
243
- if (TURN_SCOPED_PHASES.has(phase)) {
244
- if (expected.turnId === undefined)
245
+ // AgentRun-scoped phases must name the exact expected AgentRun.
246
+ if (RUN_SCOPED_PHASES.has(phase)) {
247
+ if (expected.runId === undefined)
245
248
  return "fence-mismatch:expected-turn-missing";
246
- if (actual.turnId !== expected.turnId)
247
- return "fence-mismatch:turnId";
249
+ if (actual.runId !== expected.runId)
250
+ return "fence-mismatch:runId";
248
251
  }
249
- else if (actual.turnId !== undefined
250
- && expected.turnId !== undefined
251
- && actual.turnId !== expected.turnId) {
252
- return "fence-mismatch:turnId";
252
+ else if (actual.runId !== undefined
253
+ && expected.runId !== undefined
254
+ && actual.runId !== expected.runId) {
255
+ return "fence-mismatch:runId";
253
256
  }
254
- // Provider-truth phases require the complete, equal generation fence. A
257
+ // Provider-truth phases require the complete, equal Session fence. A
255
258
  // provider-accepted event additionally must carry the exact receipt it accepts.
256
259
  if (PROVIDER_TRUTH_PHASES.has(phase)) {
257
260
  if (actual.nativeSessionId === undefined)
258
261
  return "fence-mismatch:missing-native-session";
259
- if (actual.runtimeGenerationId === undefined) {
260
- return "fence-mismatch:missing-runtime-generation";
261
- }
262
- if (expected.runtimeGenerationId !== undefined && actual.runtimeGenerationId !== expected.runtimeGenerationId) {
263
- return "fence-mismatch:runtimeGenerationId";
264
- }
265
262
  // Once a native id is bound, every provider-truth fact must match it. Before
266
263
  // binding (discovery still open) the expected fence's own nativeSessionId, if
267
264
  // present, still constrains it.
@@ -279,7 +276,7 @@ function matchesFence(expectation, event) {
279
276
  return null;
280
277
  }
281
278
  // Pre-acceptance phases (host/session/ready and the transport push): compare
282
- // the generation dimensions that are present on both sides. A push must match
279
+ // the Session dimensions that are present on both sides. A push must match
283
280
  // its expected receipt when both name one.
284
281
  if (actual.nativeSessionId !== undefined
285
282
  && expectation.boundNativeSessionId !== undefined
@@ -291,11 +288,6 @@ function matchesFence(expectation, event) {
291
288
  && actual.nativeSessionId !== expected.nativeSessionId) {
292
289
  return "fence-mismatch:nativeSessionId";
293
290
  }
294
- if (actual.runtimeGenerationId !== undefined
295
- && expected.runtimeGenerationId !== undefined
296
- && actual.runtimeGenerationId !== expected.runtimeGenerationId) {
297
- return "fence-mismatch:runtimeGenerationId";
298
- }
299
291
  if (phase === "prompt-pushed"
300
292
  && actual.receiptId !== undefined
301
293
  && expected.receiptId !== undefined
@@ -308,7 +300,11 @@ function normalizeFence(fence) {
308
300
  const taskId = requireFenceText(fence.taskId, "taskId");
309
301
  const roleName = requireFenceText(fence.roleName, "roleName");
310
302
  const agentId = requireFenceText(fence.agentId, "agentId");
311
- if (fence.adapterId !== "codex" && fence.adapterId !== "claude") {
303
+ // The fence names an adapter, and the catalog is what defines which adapters
304
+ // exist. Re-listing them here would silently exclude a catalogued adapter
305
+ // from the durable fold, which reads as an identity mismatch rather than as
306
+ // the missing registration it actually is.
307
+ if (!isAgentAdapterId(fence.adapterId)) {
312
308
  throw new CanonicalLifecycleError(`Unsupported adapter fence: ${String(fence.adapterId)}.`);
313
309
  }
314
310
  return Object.freeze({
@@ -316,11 +312,10 @@ function normalizeFence(fence) {
316
312
  roleName,
317
313
  agentId,
318
314
  adapterId: fence.adapterId,
319
- ...(fence.turnId === undefined ? {} : { turnId: requireFenceText(fence.turnId, "turnId") }),
315
+ ...(fence.runId === undefined ? {} : { runId: requireFenceText(fence.runId, "runId") }),
320
316
  ...(fence.nativeSessionId === undefined
321
317
  ? {}
322
318
  : { nativeSessionId: requireFenceText(fence.nativeSessionId, "nativeSessionId") }),
323
- ...(fence.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: requireFenceText(fence.runtimeGenerationId, "runtimeGenerationId") }),
324
319
  ...(fence.receiptId === undefined ? {} : { receiptId: requireFenceText(fence.receiptId, "receiptId") })
325
320
  });
326
321
  }
@@ -0,0 +1,449 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { createTaskMessage } from "../message/message.js";
3
+ import { captureRoleRunDispatch, enqueueWork, settleRoleRunDispatch } from "../coordination/workMailboxQueue.js";
4
+ import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
5
+ import { completeRun, failRun, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
6
+ import { boundedRunFailureDiagnostic } from "../domain/agentResultTransport.js";
7
+ import { updateExecutionLane, updateWorkItemExecutionLane } from "../execution/workItemExecution.js";
8
+ import { managedProviderTurnId } from "../runtime/providerRuntimeIdentity.js";
9
+ import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
10
+ import { markTaskWakeConsumed } from "../scheduler/taskWake.js";
11
+ import { workItemExecutionGroupById, updateWorkItemExecutionGroup } from "../workItem/workItem.js";
12
+ /**
13
+ * Validate every immutable identity and frozen Project head needed before a
14
+ * review AgentRun can settle any mailbox or Round state. This is deliberately
15
+ * read-only: callers use it as the compare-and-swap fence immediately before
16
+ * their aggregate mutation.
17
+ */
18
+ export function validateExactRunReviewRound(store, run, options = {}) {
19
+ if (run.purpose !== "review")
20
+ return { disposition: "applied", round: null };
21
+ if (run.reviewRoundId === undefined) {
22
+ return { disposition: "obsolete", round: null, reason: "review-round-missing" };
23
+ }
24
+ const round = store.getReviewRound(run.taskId, run.reviewRoundId);
25
+ if (round === null) {
26
+ return { disposition: "obsolete", round: null, reason: "review-round-missing" };
27
+ }
28
+ if (!options.allowTerminal && run.executionLaneId === undefined
29
+ && round.status !== "pending" && round.status !== "running") {
30
+ return { disposition: "obsolete", round, reason: "review-round-terminal" };
31
+ }
32
+ const lane = round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
33
+ const exactReviewerRun = round.reviewerRunId === run.id
34
+ || lane?.currentRunId === run.id;
35
+ const exactReviewerRole = round.reviewerRoleName === run.roleName || lane?.roleName === run.roleName;
36
+ if (!exactReviewerRun
37
+ || !exactReviewerRole
38
+ || round.workItemId !== run.workItemId
39
+ || round.reviewBaseCommit !== run.effective.reviewBaseCommit) {
40
+ return { disposition: "obsolete", round, reason: "review-round-mismatch" };
41
+ }
42
+ const laneWorkspaceRoot = lane?.workspace?.root;
43
+ if (run.workspace === undefined || round.workspace === undefined
44
+ || (laneWorkspaceRoot === undefined && !isDeepStrictEqual(run.workspace, round.workspace))
45
+ || (laneWorkspaceRoot !== undefined && run.workspace.root !== laneWorkspaceRoot)) {
46
+ return { disposition: "obsolete", round, reason: "review-workspace-mismatch" };
47
+ }
48
+ const storedWorkspace = run.workspace.owner.type === "execution-lane"
49
+ ? store.getManagedWorkspace(run.workspace.owner)
50
+ : store.getReviewRoundWorkspace(run.taskId, round.id);
51
+ if (storedWorkspace === null
52
+ || (run.workspace.owner.type !== "execution-lane"
53
+ && !isDeepStrictEqual(storedWorkspace, round.workspace))
54
+ || !isDeepStrictEqual(storedWorkspace, run.workspace)) {
55
+ return { disposition: "obsolete", round, reason: "review-workspace-drift" };
56
+ }
57
+ if (run.workspace.owner.type !== "execution-lane") {
58
+ if (storedWorkspace.owner.type !== "review-round"
59
+ || storedWorkspace.owner.taskId !== run.taskId
60
+ || storedWorkspace.owner.reviewRoundId !== round.id) {
61
+ return { disposition: "obsolete", round, reason: "review-workspace-owner-mismatch" };
62
+ }
63
+ }
64
+ if (run.workspace.owner.type === "execution-lane"
65
+ && (run.workspace.owner.purpose !== "review"
66
+ || run.workspace.owner.executionGroupId !== run.executionGroupId
67
+ || run.workspace.owner.executionLaneId !== run.executionLaneId
68
+ || run.workspace.owner.reviewRoundId !== round.id)) {
69
+ return { disposition: "obsolete", round, reason: "review-lane-workspace-owner-mismatch" };
70
+ }
71
+ if (run.workspace.owner.type === "execution-lane") {
72
+ if (lane === undefined
73
+ || lane.currentRunId !== run.id
74
+ || lane.roleName !== run.roleName
75
+ || lane.workspace?.root !== run.workspace.root
76
+ || lane.workspace.writableProjectIds.length !== run.workspace.entries.length
77
+ || run.workspace.entries.some((entry) => (entry.access !== "write"
78
+ || !lane.workspace.writableProjectIds.includes(entry.projectId)))) {
79
+ return { disposition: "obsolete", round, reason: "review-lane-workspace-lineage-mismatch" };
80
+ }
81
+ }
82
+ const task = store.getTask(run.taskId);
83
+ const taskScope = (round.scope ?? "work-item") === "task";
84
+ const item = taskScope || round.workItemId === undefined
85
+ ? null
86
+ : store.getWorkItem(run.taskId, round.workItemId);
87
+ if (!taskScope && item === null) {
88
+ return { disposition: "obsolete", round, reason: "review-work-item-missing" };
89
+ }
90
+ const candidate = taskScope
91
+ ? undefined
92
+ : item.candidates.find(({ id }) => id === round.candidateId);
93
+ if (!taskScope && candidate === undefined) {
94
+ return { disposition: "obsolete", round, reason: "review-candidate-missing" };
95
+ }
96
+ const frozenProjects = taskScope
97
+ ? round.taskCandidate?.projects
98
+ : candidate?.gitSnapshot?.projects;
99
+ if (taskScope) {
100
+ if (task === null || round.taskCandidate === undefined) {
101
+ return { disposition: "obsolete", round, reason: "review-task-candidate-missing" };
102
+ }
103
+ if (frozenProjects === undefined
104
+ || new Set(frozenProjects.map(({ projectId }) => projectId)).size
105
+ !== task.projectBindings.length
106
+ || task.projectBindings.some(({ projectId }) => (!frozenProjects.some((project) => project.projectId === projectId)))) {
107
+ return { disposition: "obsolete", round, reason: "review-frozen-project-scope-drift" };
108
+ }
109
+ }
110
+ else if (candidate?.gitSnapshot !== undefined
111
+ && candidate.gitSnapshot.reviewBaseCommit !== round.reviewBaseCommit) {
112
+ return { disposition: "obsolete", round, reason: "review-candidate-snapshot-drift" };
113
+ }
114
+ if (!taskScope && frozenProjects === undefined) {
115
+ return { disposition: "applied", round };
116
+ }
117
+ if (frozenProjects === undefined
118
+ || storedWorkspace.entries.length !== frozenProjects.length) {
119
+ return { disposition: "obsolete", round, reason: "review-frozen-project-scope-drift" };
120
+ }
121
+ const frozenByProject = new Map(frozenProjects.map(({ projectId, commit }) => [projectId, commit]));
122
+ if (storedWorkspace.entries.some((entry) => (entry.access !== "write"
123
+ || frozenByProject.get(entry.projectId) !== entry.baseCommit
124
+ || entry.baseRef !== entry.baseCommit))) {
125
+ return { disposition: "obsolete", round, reason: "review-frozen-head-drift" };
126
+ }
127
+ return { disposition: "applied", round };
128
+ }
129
+ /**
130
+ * Atomically terminalizes the exact ReviewRound bound to a review AgentRun.
131
+ * The round must still be pending/running and its reviewer identity must match
132
+ * the AgentRun exactly. This is the sole review-round convergence primitive shared
133
+ * by the exact AgentRun terminalization path and the pre-delivery launch-failure
134
+ * path: a failed review AgentRun must never leave its Round stranded.
135
+ */
136
+ export function terminalizeExactRunReviewRound(store, input, now) {
137
+ const validation = validateExactRunReviewRound(store, input.run);
138
+ if (validation.disposition !== "applied" || validation.round === null) {
139
+ return validation;
140
+ }
141
+ const reviewRound = validation.round;
142
+ if (input.run.executionGroupId !== undefined
143
+ && input.run.executionLaneId !== undefined) {
144
+ // Producer AgentRuns own only their immutable AgentRun result. The unified Group
145
+ // is advanced after that result is validated and stored below.
146
+ return { disposition: "applied", round: reviewRound };
147
+ }
148
+ const terminal = finishReviewRound(reviewRound, input.outcome.status, now, input.outcome.status === "failed"
149
+ ? { kind: "execution", message: input.outcome.diagnostic }
150
+ : undefined);
151
+ store.saveReviewRound(input.taskId, terminal);
152
+ return { disposition: "applied", round: terminal };
153
+ }
154
+ /** Explicit runtime replacement has already proved this Role's resources
155
+ * stopped. A stale active-pointer/Review projection cannot veto cancellation
156
+ * of its engineering attempts. Never accept work or rewrite an old result. */
157
+ export function cancelQuiescentRoleRuns(store, taskId, roleName, reason, now) {
158
+ for (const run of store.listRuns(taskId).filter(run => run.roleName === roleName && run.status === "active")) {
159
+ const outcome = { status: "failed", failureReason: "cancelled", diagnostic: reason };
160
+ const settled = terminalizeExactTaskRun(store, {
161
+ taskId, roleName, agentId: run.effective.agentId, runId: run.id, outcome
162
+ }, now);
163
+ if (settled.disposition === "applied")
164
+ continue;
165
+ store.saveRun(failRun(run, "cancelled", reason, now));
166
+ settleRoleRunDispatch(store, { taskId, roleName, runId: run.id });
167
+ if (store.getActiveRun(taskId, roleName)?.id === run.id)
168
+ store.clearActiveRun(taskId, roleName);
169
+ if (run.executionGroupId !== undefined && run.executionLaneId !== undefined
170
+ && store.getActiveExecutionLaneRun(taskId, run.executionGroupId, run.executionLaneId)?.id === run.id) {
171
+ store.clearActiveExecutionLaneRun(taskId, run.executionGroupId, run.executionLaneId);
172
+ }
173
+ if (run.reviewRoundId !== undefined && run.executionGroupId === undefined) {
174
+ const round = store.getReviewRound(taskId, run.reviewRoundId);
175
+ if (round?.reviewerRunId === run.id && (round.status === "pending" || round.status === "running")) {
176
+ store.saveReviewRound(taskId, finishReviewRound(round, "failed", now, { kind: "execution", message: reason }));
177
+ }
178
+ }
179
+ if (roleName !== "leader") {
180
+ const message = createTaskMessage(store.nextMessageId(taskId), taskId, `Execution ${run.id} cancelled after runtime stop.`, "role-result", { type: "role", roleName }, now, { resultRef: { type: "agent-run-result", runId: run.id } });
181
+ store.saveMessage(taskId, message);
182
+ enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, "role-result", now, [{ type: "message", taskId, id: message.id }]);
183
+ }
184
+ }
185
+ }
186
+ /**
187
+ * Retire one stranded active AgentRun only after its exact Provider Turn is
188
+ * terminal and every durable execution fence is quiet. The caller owns the
189
+ * surrounding aggregate transaction so the AgentRun, ReviewRound/Lane, mailbox,
190
+ * Session, and append-only retirement record commit together.
191
+ */
192
+ export function retireExactActiveRun(store, input, now) {
193
+ const current = store.getRun(input.taskId, input.runId);
194
+ const stateChanged = (reason) => ({
195
+ disposition: "state-changed",
196
+ run: current,
197
+ ...(current === null ? {} : { progressAt: latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId)?.progressAt }),
198
+ reason
199
+ });
200
+ const task = store.getTask(input.taskId);
201
+ if (task === null)
202
+ return stateChanged("task-missing");
203
+ if (current === null)
204
+ return stateChanged("turn-missing");
205
+ if (current.status !== "active")
206
+ return stateChanged("turn-terminal");
207
+ // The Turn's purpose decides which Task lifecycle admits it: a planning Turn
208
+ // is admitted on a Draft, so its retirement must land there too. Otherwise a
209
+ // Draft planning Turn could never be retired and would leak an active Turn.
210
+ if (!runPurposeAdmitsTaskState(current.purpose, task)) {
211
+ return stateChanged("task-terminal");
212
+ }
213
+ if (current.taskId !== input.taskId || current.roleName !== input.roleName) {
214
+ return stateChanged("turn-owner-mismatch");
215
+ }
216
+ if (current.effective.agentId !== input.agentId
217
+ || current.effective.adapterId !== input.adapterId) {
218
+ return stateChanged("turn-launch-identity-mismatch");
219
+ }
220
+ const active = current.executionGroupId !== undefined && current.executionLaneId !== undefined
221
+ ? store.getActiveExecutionLaneRun(input.taskId, current.executionGroupId, current.executionLaneId)
222
+ : store.getActiveRun(input.taskId, input.roleName);
223
+ if (active?.id !== current.id)
224
+ return stateChanged("active-turn-mismatch");
225
+ const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
226
+ if (progress === null)
227
+ return stateChanged("progress-unavailable");
228
+ if (progress.progressAt !== input.expectedProgressAt) {
229
+ return { ...stateChanged("progress-fence-mismatch"), progressAt: progress.progressAt };
230
+ }
231
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
232
+ const terminalInput = {
233
+ taskId: input.taskId,
234
+ roleName: input.roleName,
235
+ agentId: input.agentId,
236
+ runId: input.runId,
237
+ ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
238
+ outcome: { status: "failed", diagnostic: input.reason, failureReason: "missing-result" }
239
+ };
240
+ const session = sessions?.sessions[input.agentId];
241
+ const providerBinding = sessions?.providerBinding;
242
+ const providerTurn = providerBinding?.run;
243
+ const providerSettled = managedProviderTurnId(providerTurn) === current.id
244
+ && (providerTurn?.status === "completed"
245
+ || providerTurn?.status === "failed"
246
+ || providerTurn?.status === "cancelled"
247
+ || providerTurn?.status === "rejected"
248
+ || providerTurn?.status === "deferred");
249
+ if (session?.status === "active" && !providerSettled) {
250
+ return {
251
+ disposition: "blocked",
252
+ run: current,
253
+ progressAt: progress.progressAt,
254
+ reason: "runtime-not-terminal"
255
+ };
256
+ }
257
+ const terminal = terminalizeExactTaskRun(store, terminalInput, now);
258
+ if (terminal.disposition !== "applied" || terminal.run === null) {
259
+ return stateChanged(terminal.reason ?? "terminalization-fence-mismatch");
260
+ }
261
+ return {
262
+ disposition: "applied",
263
+ run: terminal.run,
264
+ progressAt: progress.progressAt
265
+ };
266
+ }
267
+ /**
268
+ * Applies one exact application-level terminal fact inside the caller's
269
+ * TaskStore transaction. All caller-owned outcome records can therefore
270
+ * be saved in the same aggregate commit.
271
+ */
272
+ export function terminalizeExactTaskRun(store, input, now) {
273
+ const run = store.getRun(input.taskId, input.runId);
274
+ if (run === null)
275
+ return obsolete(null, "turn-missing");
276
+ if (run.status !== "active")
277
+ return obsolete(run, "turn-terminal");
278
+ if (run.taskId !== input.taskId || run.roleName !== input.roleName) {
279
+ return obsolete(run, "turn-owner-mismatch");
280
+ }
281
+ if (run.effective.agentId !== input.agentId) {
282
+ return obsolete(run, "turn-agent-mismatch");
283
+ }
284
+ const role = store.getRole(input.taskId, input.roleName);
285
+ if (role === null)
286
+ return obsolete(run, "role-missing");
287
+ const active = run.executionGroupId !== undefined && run.executionLaneId !== undefined
288
+ ? store.getActiveExecutionLaneRun(input.taskId, run.executionGroupId, run.executionLaneId)
289
+ : store.getActiveRun(input.taskId, input.roleName);
290
+ if (active?.id !== run.id)
291
+ return obsolete(run, "active-turn-mismatch");
292
+ // Validate the exact ReviewRound, Candidate, stored workspace, and frozen
293
+ // Project heads before any mailbox or Round write.
294
+ const reviewValidation = validateExactRunReviewRound(store, run);
295
+ if (reviewValidation.disposition !== "applied") {
296
+ return obsolete(run, reviewValidation.reason ?? "review-round-mismatch");
297
+ }
298
+ const requiredSnapshotProjects = [...new Set(run.effective.writeProjectIds)].sort();
299
+ const observedSnapshotProjects = input.systemEvidence?.workspaceSnapshot?.projects
300
+ .map(({ projectId }) => projectId)
301
+ .sort();
302
+ const laneSnapshotRequired = input.outcome.status === "completed"
303
+ && run.executionGroupId !== undefined
304
+ && run.executionLaneId !== undefined
305
+ && requiredSnapshotProjects.length > 0;
306
+ const missingLaneSnapshot = laneSnapshotRequired
307
+ && (observedSnapshotProjects === undefined
308
+ || !isDeepStrictEqual(observedSnapshotProjects, requiredSnapshotProjects));
309
+ const workspaceFailure = laneSnapshotRequired
310
+ ? input.workspaceFailure ?? (missingLaneSnapshot
311
+ ? {
312
+ failureReason: "workspace-unavailable",
313
+ diagnostic: "Core could not freeze the exact clean writable Lane workspace."
314
+ }
315
+ : undefined)
316
+ : undefined;
317
+ const effectiveOutcome = input.outcome.status === "completed" && workspaceFailure !== undefined
318
+ ? {
319
+ status: "failed",
320
+ output: input.outcome.output,
321
+ diagnostic: workspaceFailure.diagnostic,
322
+ failureReason: workspaceFailure.failureReason,
323
+ ...(input.outcome.provider === undefined ? {} : { provider: input.outcome.provider })
324
+ }
325
+ : input.outcome;
326
+ const terminalOutcome = effectiveOutcome.status === "failed"
327
+ ? {
328
+ ...effectiveOutcome,
329
+ diagnostic: boundedRunFailureDiagnostic(effectiveOutcome.diagnostic)
330
+ }
331
+ : effectiveOutcome;
332
+ // All AgentRun, active-pointer, Session, launch, and mailbox fences have passed.
333
+ // Only now may the exact ReviewRound be terminalized alongside the AgentRun so a
334
+ // stale fence never leaves a Round written while the AgentRun stays active.
335
+ const reviewRoundTerminalization = terminalizeExactRunReviewRound(store, {
336
+ taskId: input.taskId,
337
+ run,
338
+ outcome: terminalOutcome
339
+ }, now);
340
+ if (reviewRoundTerminalization.disposition !== "applied") {
341
+ return obsolete(run, reviewRoundTerminalization.reason ?? "review-round-mismatch");
342
+ }
343
+ const terminal = terminalOutcome.status === "completed"
344
+ ? completeRun(run, terminalOutcome.output, now, terminalOutcome.provider, input.systemEvidence)
345
+ : failRun(run, terminalOutcome.failureReason, terminalOutcome.diagnostic, now, terminalOutcome.provider, terminalOutcome.output);
346
+ if (run.executionGroupId !== undefined
347
+ && run.executionLaneId !== undefined
348
+ && run.purpose === "execution"
349
+ && run.workItemId !== undefined) {
350
+ const item = store.getWorkItem(input.taskId, run.workItemId);
351
+ const group = item === null
352
+ ? undefined
353
+ : workItemExecutionGroupById(item, run.executionGroupId);
354
+ if (item !== null && group !== undefined) {
355
+ if (terminal.status === "completed") {
356
+ const grouped = updateWorkItemExecutionLane(group, run.executionLaneId, {
357
+ currentRunId: run.id,
358
+ successfulRunId: run.id,
359
+ disposition: "succeeded"
360
+ }, now);
361
+ store.saveWorkItem(input.taskId, updateWorkItemExecutionGroup(item, grouped, now));
362
+ }
363
+ }
364
+ }
365
+ if (run.executionGroupId !== undefined
366
+ && run.executionLaneId !== undefined
367
+ && run.purpose === "review"
368
+ && run.reviewRoundId !== undefined) {
369
+ const round = store.getReviewRound(input.taskId, run.reviewRoundId);
370
+ const group = round?.executionGroup;
371
+ if (round !== null
372
+ && round !== undefined
373
+ && group !== undefined
374
+ && group.id === run.executionGroupId) {
375
+ if (terminal.status === "completed") {
376
+ const grouped = updateExecutionLane(group, run.executionLaneId, {
377
+ currentRunId: run.id,
378
+ successfulRunId: run.id,
379
+ disposition: "succeeded"
380
+ }, now);
381
+ store.saveReviewRound(input.taskId, updateReviewExecutionGroup(round, grouped));
382
+ }
383
+ }
384
+ }
385
+ store.saveRun(terminal);
386
+ const pendingMessages = store.listMessages(terminal.taskId).filter((message) => message.recipient?.roleName === terminal.roleName && message.recipient.ownerRunId !== undefined
387
+ && message.continuation?.runId === undefined);
388
+ if (pendingMessages.length > 0) {
389
+ enqueueWork(store, { kind: "task", taskId: terminal.taskId }, "message-continuation", now, pendingMessages.slice(0, 16).map((message) => ({ type: "message", taskId: terminal.taskId, id: message.id })), { dedupeKey: `message-after-result:${terminal.taskId}/${terminal.id}` });
390
+ }
391
+ if (terminal.roleName !== "leader") {
392
+ // Keep the report in its execution record; atomically publish only a
393
+ // collaboration reference. Duplicate terminals have already returned.
394
+ const message = createTaskMessage(store.nextMessageId(terminal.taskId), terminal.taskId, `Execution ${terminal.id} ${terminal.status}.`, "role-result", { type: "role", roleName: terminal.roleName }, now, { resultRef: { type: "agent-run-result", runId: terminal.id }, ...(terminal.workItemId === undefined
395
+ ? {} : { workItemId: terminal.workItemId }) });
396
+ store.saveMessage(terminal.taskId, message);
397
+ enqueueWork(store, { kind: "role", taskId: terminal.taskId, roleName: "leader" }, "role-result", now, [{ type: "message", taskId: terminal.taskId, id: message.id }], { source: "task-event", dedupeKey: `result:${terminal.taskId}/${terminal.id}` });
398
+ }
399
+ const dispatchIdentity = {
400
+ taskId: terminal.taskId,
401
+ roleName: terminal.roleName,
402
+ runId: terminal.id
403
+ };
404
+ const dispatchToken = captureRoleRunDispatch(store.getWorkMailbox({
405
+ kind: "role",
406
+ taskId: terminal.taskId,
407
+ roleName: terminal.roleName
408
+ }), dispatchIdentity);
409
+ settleRoleRunDispatch(store, dispatchIdentity, dispatchToken);
410
+ if (terminal.roleName === "leader") {
411
+ const wake = store.listTaskWakes(input.taskId)
412
+ .find((candidate) => candidate.runId === terminal.id && candidate.status === "dispatched");
413
+ if (wake !== undefined) {
414
+ store.saveTaskWake(input.taskId, markTaskWakeConsumed(wake, now));
415
+ }
416
+ }
417
+ if (terminal.executionGroupId !== undefined && terminal.executionLaneId !== undefined) {
418
+ store.clearActiveExecutionLaneRun(input.taskId, terminal.executionGroupId, terminal.executionLaneId);
419
+ }
420
+ else {
421
+ store.clearActiveRun(input.taskId, input.roleName);
422
+ }
423
+ releaseDeferredTaskActivation(store, terminal, now);
424
+ return { disposition: "applied", run: terminal };
425
+ }
426
+ /**
427
+ * A planning Turn that deferred an activation has just ended, so signal the
428
+ * Task mailbox to look at the request now.
429
+ *
430
+ * The signal carries no decision. Whether the request still applies is re-read
431
+ * at the adoption boundary from facts that are current then, so a request
432
+ * cancelled or a Task retired while this Turn was still running is never
433
+ * replayed. Enqueuing inside the terminalization transaction keeps the release
434
+ * atomic with the Turn becoming non-active: there is no window in which the
435
+ * deferral is unblocked but nothing will look at it.
436
+ */
437
+ function releaseDeferredTaskActivation(store, terminal, now) {
438
+ if (terminal.purpose !== "planning")
439
+ return;
440
+ const request = store.getTask(terminal.taskId)?.activationRequest;
441
+ if (request?.disposition !== "pending")
442
+ return;
443
+ if (request.afterPlanningRun !== terminal.id)
444
+ return;
445
+ enqueueWork(store, { kind: "task", taskId: terminal.taskId }, "activation-deferral-released", now, [{ type: "task", id: terminal.taskId }]);
446
+ }
447
+ function obsolete(run, reason) {
448
+ return { disposition: "obsolete", run, reason };
449
+ }