@zq-silk/yui 0.2.0 → 0.4.2

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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -1,29 +1,48 @@
1
- import { createRoleSessionSet, recordRoleAgentSession, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { activeLiveRoleAgentSession, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, recordObservedTaskRoleCompletion, recordRoleAgentSession, rememberRoleAgentCompletedTurn, settleTaskRoleCompletion, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
3
+ import { createPendingTurnCompletion, hasRecentTurnId } from "../executor/turnCompletion.js";
2
4
  import { createTaskEvent } from "../event/taskEvent.js";
3
5
  import { answerInputRequest } from "../input/inputRequest.js";
4
6
  import { activeRoleAgentBinding, updateRoleStatus } from "../role/role.js";
7
+ import { effectiveLaunchSnapshotsCompatible, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
5
8
  import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
6
- import { failAgentRun, markAgentRunDelivered } from "../run/agentRun.js";
9
+ import { failAgentRun, markAgentRunDelivered, markAgentRunPushed } from "../run/agentRun.js";
10
+ import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
11
+ import { foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
12
+ import { mapNativeLifecycleSignal } from "../lifecycle/providerLifecycleMapping.js";
13
+ import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
14
+ import { createLeaderRecoveryNotification, createLeaderStallNotification } from "../scheduler/operatorNotification.js";
7
15
  import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
8
16
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
9
- import { updateWorkItemStatus } from "../workItem/workItem.js";
17
+ import { latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_RESOURCE_SUPPRESSED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
18
+ import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
19
+ import { recordExecutionLaneResult } from "../execution/executionGroup.js";
20
+ import { formatAgentRunReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
21
+ import { bindExecution, claimPending, completeProcessing, releaseProcessing } from "../coordination/workMailbox.js";
22
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
23
+ import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
24
+ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
10
25
  /** Maps the authoritative FileTaskStore records to the scheduler's narrow port. */
11
26
  export class FileSchedulerStoreAdapter {
12
27
  store;
13
28
  constructor(store) {
14
29
  this.store = store;
15
30
  }
31
+ getPresentationContext() {
32
+ return { timeZone: this.store.getConfig().timeZone };
33
+ }
16
34
  listTasks() { return this.store.listTasks(); }
17
35
  getTask(taskId) { return this.store.getTask(taskId); }
36
+ getTaskWorkspace(taskId) { return this.store.getTaskWorkspace(taskId); }
18
37
  getTaskBrief(taskId) { return this.store.getTaskBrief(taskId); }
19
38
  listDecisions(taskId) { return this.store.listDecisions(taskId); }
20
39
  listMilestones(taskId) { return this.store.listMilestones(taskId); }
21
40
  listRoles(taskId) {
22
- return this.store.listRoles(taskId).map(mapRole);
41
+ return this.store.listRoles(taskId).map((role) => mapRole(this.store, role));
23
42
  }
24
43
  getRole(taskId, roleName) {
25
44
  const role = this.store.getRole(taskId, roleName);
26
- return role === null ? null : mapRole(role);
45
+ return role === null ? null : mapRole(this.store, role);
27
46
  }
28
47
  getActiveAgentRun(taskId, roleName) {
29
48
  return this.store.getActiveAgentRun(taskId, roleName);
@@ -34,20 +53,28 @@ export class FileSchedulerStoreAdapter {
34
53
  listOpenInputRequests() {
35
54
  return this.store.listAllInputRequests().filter((request) => request.status === "open");
36
55
  }
56
+ getInputRequest(taskId, inputRequestId) {
57
+ return this.store.getInputRequest(taskId, inputRequestId);
58
+ }
37
59
  getOperatorDeliveryTarget() {
38
60
  const role = this.store.getGlobalRole(SYSTEM_OPERATOR_ROLE);
39
61
  if (role === null)
40
62
  return null;
63
+ const sessions = this.store.getGlobalRoleSessionSet(SYSTEM_OPERATOR_ROLE);
64
+ const effectiveSession = sessions?.sessions[sessions.activeAgentId];
65
+ if (effectiveSession?.status !== "ready")
66
+ return null;
41
67
  return {
42
68
  roleName: SYSTEM_OPERATOR_ROLE,
43
- adapterId: activeRoleAgentBinding(role).adapterId
69
+ adapterId: effectiveSession.effective.adapterId
44
70
  };
45
71
  }
46
- resolveExpiredInputRecommendations(now) {
72
+ resolveExpiredInputRecommendations(now, taskIds) {
47
73
  return this.store.transaction((store) => {
48
74
  const expired = store.listAllInputRequests().filter((request) => (request.status === "open"
49
75
  && request.policy.kind === "recommended"
50
- && Date.parse(request.policy.timeoutAt) <= now.getTime()));
76
+ && Date.parse(request.policy.timeoutAt) <= now.getTime()
77
+ && (taskIds === undefined || taskIds.has(request.taskId))));
51
78
  const resolved = [];
52
79
  for (const request of expired) {
53
80
  const task = store.getTask(request.taskId);
@@ -56,18 +83,341 @@ export class FileSchedulerStoreAdapter {
56
83
  const choiceKey = request.policy.recommendedChoiceKey;
57
84
  const answered = answerInputRequest(request, { choiceKey }, "agent-timeout", now);
58
85
  store.saveInputRequest(task.id, answered);
59
- store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), "input.auto-answered", { requestId: request.id, choiceKey }, now));
86
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, "input.auto-answered", { requestId: request.id, choiceKey }, now));
60
87
  queueLeaderWakeup(store, task.id, `input-timeout:${request.id}`, now);
61
88
  resolved.push({ inputRequestId: request.id, taskId: task.id, choiceKey });
62
89
  }
63
90
  return resolved;
64
91
  });
65
92
  }
66
- getRoleSession(taskId, roleName) {
67
- const session = this.store.getRoleSession(taskId, roleName);
68
- return session === null ? null : mapSession(session);
93
+ getRoleSession(taskId, roleName, agentId) {
94
+ const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
95
+ const session = agentId === undefined
96
+ ? sessions?.sessions[sessions.activeAgentId]
97
+ : sessions?.sessions[agentId];
98
+ return session === undefined ? null : mapSession(session);
99
+ }
100
+ listEvents(taskId) {
101
+ return this.store.listEvents(taskId);
102
+ }
103
+ getRunDurableProgress(taskId, roleName, runId) {
104
+ return latestRunDurableProgressAt(this.store, taskId, roleName, runId);
105
+ }
106
+ recordRoleRunStall(input) {
107
+ return this.store.transaction((store) => {
108
+ const task = store.getTask(input.taskId);
109
+ const role = store.getRole(input.taskId, input.roleName);
110
+ const run = store.getActiveAgentRun(input.taskId, input.roleName);
111
+ if (task === null
112
+ || task.status !== "active"
113
+ || role === null
114
+ || run === null
115
+ || run.id !== input.runId
116
+ || run.status !== "active"
117
+ || run.effective.agentId !== input.agentId
118
+ || run.effective.adapterId !== input.adapterId)
119
+ return "state-changed";
120
+ const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
121
+ if (progress?.progressAt !== input.progressAt)
122
+ return "state-changed";
123
+ const session = store.getRoleSession(input.taskId, input.roleName);
124
+ if (!matchesStallSessionFence(session, input.session))
125
+ return "state-changed";
126
+ const existing = latestStallEvidenceKey(store.listEvents(task.id), run.id);
127
+ if (existing?.progressAt === input.progressAt)
128
+ return "already-raised";
129
+ const event = createTaskEvent(store.nextEventId(task.id), task.id, RUN_STALLED_EVENT, {
130
+ runId: run.id,
131
+ roleName: role.name,
132
+ kind: input.kind,
133
+ classification: input.classification,
134
+ progressAt: input.progressAt,
135
+ idleMs: String(Math.max(0, Math.floor(input.idleMs))),
136
+ evidenceKey: input.evidenceKey,
137
+ status: "needs-attention"
138
+ }, input.now);
139
+ store.saveEvent(task.id, event);
140
+ if (role.name === "leader") {
141
+ store.saveOperatorNotification(createLeaderStallNotification(task.id, run.id, input.progressAt, input.evidenceKey, input.now, store.getOperatorNotification(task.id)));
142
+ enqueueWork(store, { kind: "operator" }, "leader-run-stalled", input.now, [
143
+ { type: "task", id: task.id }
144
+ ]);
145
+ }
146
+ else {
147
+ queueLeaderWakeup(store, task.id, "role-run-stalled", input.now);
148
+ }
149
+ return "raised";
150
+ });
151
+ }
152
+ recordRoleRunProgress(input) {
153
+ return this.store.transaction((store) => {
154
+ const task = store.getTask(input.taskId);
155
+ const run = store.getActiveAgentRun(input.taskId, input.roleName);
156
+ if (task === null
157
+ || task.status !== "active"
158
+ || run === null
159
+ || run.id !== input.runId
160
+ || run.status !== "active")
161
+ return "state-changed";
162
+ const events = store.listEvents(task.id);
163
+ const previousStall = latestStallEvidenceKey(events, run.id);
164
+ // A progress fact can close only the matching, older stall episode. A
165
+ // stale/native observation must never clear a newer attention point.
166
+ if (previousStall !== undefined
167
+ && Date.parse(input.progressAt) <= Date.parse(previousStall.progressAt)) {
168
+ return "already-recorded";
169
+ }
170
+ const existing = events.some((event) => (event.type === RUN_PROGRESS_EVENT
171
+ && event.payload.runId === run.id
172
+ && (event.payload.progressAt === input.progressAt
173
+ || (typeof event.payload.progressAt === "string"
174
+ && Number.isFinite(Date.parse(event.payload.progressAt))
175
+ && Date.parse(event.payload.progressAt) >= Date.parse(input.progressAt)))));
176
+ const stalledAt = latestRunEventTime(events, RUN_STALLED_EVENT, run.id);
177
+ const recoveredAt = latestRunEventTime(events, RUN_RECOVERED_EVENT, run.id);
178
+ const recovered = stalledAt !== undefined
179
+ && (recoveredAt === undefined || Date.parse(recoveredAt) <= Date.parse(stalledAt));
180
+ if (!existing) {
181
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_PROGRESS_EVENT, {
182
+ runId: run.id,
183
+ roleName: input.roleName,
184
+ kind: "durable-fold",
185
+ progressAt: input.progressAt,
186
+ evidence: input.evidence ?? ""
187
+ }, input.now));
188
+ }
189
+ if (recovered) {
190
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, {
191
+ runId: run.id,
192
+ roleName: input.roleName,
193
+ progressAt: input.progressAt,
194
+ kind: "durable-progress"
195
+ }, input.now));
196
+ clearMatchingLeaderStallAttention(store, task.id, run.id);
197
+ }
198
+ return existing && !recovered ? "already-recorded" : "recorded";
199
+ });
200
+ }
201
+ recordRoleRunResourceSuppression(input) {
202
+ return this.store.transaction((store) => {
203
+ const task = store.getTask(input.taskId);
204
+ const run = store.getActiveAgentRun(input.taskId, input.roleName);
205
+ if (task === null
206
+ || task.status !== "active"
207
+ || run === null
208
+ || run.id !== input.runId
209
+ || run.status !== "active")
210
+ return "state-changed";
211
+ const session = store.getRoleSession(input.taskId, input.roleName);
212
+ if (session === null
213
+ || session.agentId !== input.agentId
214
+ || session.adapterId !== input.adapterId
215
+ || session.status === "stopped"
216
+ || session.status === "broken"
217
+ || session.nativeSessionId !== input.nativeSessionId
218
+ || session.launchId !== input.launchId)
219
+ return "state-changed";
220
+ const existing = store.listEvents(task.id).some((event) => (event.type === RUN_RESOURCE_SUPPRESSED_EVENT
221
+ && event.payload.runId === run.id
222
+ && event.payload.roleName === input.roleName
223
+ && event.payload.progressAt === input.progressAt));
224
+ if (existing)
225
+ return "already-recorded";
226
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RESOURCE_SUPPRESSED_EVENT, {
227
+ runId: run.id,
228
+ roleName: input.roleName,
229
+ progressAt: input.progressAt,
230
+ observedAt: input.observedAt,
231
+ kind: "advisory-resource"
232
+ }, input.now));
233
+ return "recorded";
234
+ });
235
+ }
236
+ hasInFlightTurn(taskId, roleName) {
237
+ const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
238
+ return sessions !== null && sessions.inFlight !== null;
239
+ }
240
+ isRoleGenerationProviderReady(input) {
241
+ // A provider-ready fact for this exact generation is proven by a folded
242
+ // runtime.provider-session-lifecycle event with preInputReady true and a
243
+ // matching agent/launch/nativeSession fence. This reads durable folded truth
244
+ // only — no liveness, screen, or pane/PID inference.
245
+ return this.store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-session-lifecycle"
246
+ && event.payload.roleName === input.roleName
247
+ && event.payload.agentId === input.agentId
248
+ && event.payload.preInputReady === "true"
249
+ && (input.launchId === undefined || event.payload.launchId === input.launchId)
250
+ && (input.nativeSessionId === undefined
251
+ || event.payload.nativeSessionId === input.nativeSessionId)));
252
+ }
253
+ peekNextAgentRunId(taskId) {
254
+ return this.store.peekNextAgentRunId(taskId);
255
+ }
256
+ getWorkMailbox(target) { return this.store.getWorkMailbox(target); }
257
+ listWorkMailboxes() { return this.store.listWorkMailboxes(); }
258
+ queueTaskProgress(taskId, reason, now) {
259
+ this.store.transaction((store) => {
260
+ enqueueWork(store, { kind: "task", taskId }, reason, now, [
261
+ { type: "task", id: taskId }
262
+ ]);
263
+ });
264
+ }
265
+ enqueueLeaderWakeup(taskId, reason, now) {
266
+ return this.store.transaction((store) => (queueLeaderWakeup(store, taskId, reason, now)));
267
+ }
268
+ releaseLeaderWakeupAndEnqueue(taskId, batchId, reason, now) {
269
+ const target = { kind: "role", taskId, roleName: "leader" };
270
+ return this.store.transaction((store) => {
271
+ const mailbox = store.getWorkMailbox(target);
272
+ if (mailbox?.processing?.batchId !== batchId)
273
+ return false;
274
+ store.saveWorkMailbox(releaseProcessing(mailbox, batchId));
275
+ enqueueWork(store, target, reason, now);
276
+ return true;
277
+ });
278
+ }
279
+ claimWorkMailbox(input) {
280
+ return this.store.transaction((store) => {
281
+ const mailbox = store.getWorkMailbox(input.target);
282
+ if (mailbox === null || (mailbox.processing === null && mailbox.pending === null)) {
283
+ return { status: "empty" };
284
+ }
285
+ if (mailbox.processing !== null) {
286
+ return { status: "processing", processing: mailbox.processing };
287
+ }
288
+ let claimed = claimPending(mailbox, {
289
+ batchId: input.batchId,
290
+ owner: input.owner,
291
+ startedAt: input.now.toISOString()
292
+ });
293
+ if (input.executionRef !== undefined) {
294
+ claimed = bindExecution(claimed, input.batchId, input.executionRef);
295
+ }
296
+ store.saveWorkMailbox(claimed);
297
+ return { status: "claimed", processing: claimed.processing };
298
+ });
299
+ }
300
+ completeWorkMailbox(target, batchId) {
301
+ return this.store.transaction((store) => {
302
+ const mailbox = store.getWorkMailbox(target);
303
+ if (mailbox?.processing?.batchId !== batchId)
304
+ return false;
305
+ const completed = completeProcessing(mailbox, batchId);
306
+ if (target.kind === "role-runtime"
307
+ || target.kind === "global-role-runtime") {
308
+ saveRuntimeLifecycleMailbox(store, completed);
309
+ }
310
+ else {
311
+ store.saveWorkMailbox(completed);
312
+ }
313
+ return true;
314
+ });
315
+ }
316
+ releaseWorkMailbox(target, batchId) {
317
+ return this.store.transaction((store) => {
318
+ const mailbox = store.getWorkMailbox(target);
319
+ if (mailbox?.processing?.batchId !== batchId)
320
+ return false;
321
+ store.saveWorkMailbox(releaseProcessing(mailbox, batchId));
322
+ return true;
323
+ });
324
+ }
325
+ completeRuntimeCleanup(target, now) {
326
+ return this.store.transaction((store) => {
327
+ let mailbox = store.getWorkMailbox(target);
328
+ if (mailbox === null || !hasRuntimeCleanupObligation(mailbox))
329
+ return false;
330
+ if (mailbox.processing !== null) {
331
+ if (!isRuntimeLaunchReservation(mailbox.processing)
332
+ && !mailbox.processing.batch.reasons.includes(RUNTIME_CLEANUP_REQUIRED_REASON)) {
333
+ return false;
334
+ }
335
+ mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
336
+ }
337
+ if (mailbox.pending !== null) {
338
+ if (mailbox.pending.reasons.length !== 1
339
+ || mailbox.pending.reasons[0] !== RUNTIME_CLEANUP_REQUIRED_REASON) {
340
+ return false;
341
+ }
342
+ const batchId = `runtime-cleanup-complete:${mailbox.pending.fromSequence}-${mailbox.pending.toSequence}`;
343
+ mailbox = completeProcessing(claimPending(mailbox, {
344
+ batchId,
345
+ owner: RUNTIME_LIFECYCLE_OWNER,
346
+ startedAt: now.toISOString()
347
+ }), batchId);
348
+ }
349
+ markRuntimeOwnerSessionStopped(store, runtimeOwnerFromTarget(target), now);
350
+ saveRuntimeLifecycleMailbox(store, mailbox);
351
+ return true;
352
+ });
353
+ }
354
+ completeStoppedRuntimeReservation(target, batchId, now) {
355
+ return this.store.transaction((store) => {
356
+ const mailbox = store.getWorkMailbox(target);
357
+ if (!isRuntimeLaunchReservation(mailbox?.processing, batchId)) {
358
+ return false;
359
+ }
360
+ markRuntimeOwnerSessionStopped(store, runtimeOwnerFromTarget(target), now);
361
+ saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, batchId));
362
+ return true;
363
+ });
364
+ }
365
+ listDormantRuntimeOwners() {
366
+ const taskOwners = this.store.listTasks().flatMap((task) => (this.store.listRoleSessionSets(task.id).flatMap((sessions) => {
367
+ const active = sessions.sessions[sessions.activeAgentId];
368
+ return active !== undefined
369
+ && active.status !== "stopped"
370
+ && !hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(sessions.owner)))
371
+ && this.store.getActiveAgentRun(task.id, sessions.owner.roleName) === null
372
+ ? [{
373
+ owner: sessions.owner,
374
+ agentId: active.agentId,
375
+ adapterId: active.adapterId,
376
+ nativeSessionId: active.nativeSessionId,
377
+ ...(active.launchId === undefined ? {} : { launchId: active.launchId }),
378
+ sessionUpdatedAt: active.updatedAt
379
+ }]
380
+ : [];
381
+ })));
382
+ const globalOwners = this.store.listGlobalRoleSessionSets().flatMap((sessions) => {
383
+ const active = sessions.sessions[sessions.activeAgentId];
384
+ return active !== undefined
385
+ && active.status !== "stopped"
386
+ && !hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(sessions.owner)))
387
+ ? [{
388
+ owner: sessions.owner,
389
+ agentId: active.agentId,
390
+ adapterId: active.adapterId,
391
+ nativeSessionId: active.nativeSessionId,
392
+ ...(active.launchId === undefined ? {} : { launchId: active.launchId }),
393
+ sessionUpdatedAt: active.updatedAt
394
+ }]
395
+ : [];
396
+ });
397
+ return [...taskOwners, ...globalOwners];
398
+ }
399
+ markRuntimeOwnerStopped(candidate, now) {
400
+ return this.store.transaction((store) => {
401
+ if (!dormantRuntimeCandidateIsCurrent(store, candidate))
402
+ return false;
403
+ return markRuntimeOwnerSessionStopped(store, candidate.owner, now);
404
+ });
405
+ }
406
+ enqueueRuntimeCleanup(owner, now = new Date(), expectedDormantCandidate) {
407
+ return this.store.transaction((store) => {
408
+ if (expectedDormantCandidate !== undefined
409
+ && (!sameRuntimeOwner(owner, expectedDormantCandidate.owner)
410
+ || !dormantRuntimeCandidateIsCurrent(store, expectedDormantCandidate))) {
411
+ return null;
412
+ }
413
+ if (owner.scope === "task" && store.getTask(owner.taskId) === null) {
414
+ return null;
415
+ }
416
+ const target = runtimeLifecycleTarget(owner);
417
+ enqueueWork(store, target, RUNTIME_CLEANUP_REQUIRED_REASON, now, owner.scope === "task" ? [{ type: "task", id: owner.taskId }] : []);
418
+ return target;
419
+ });
69
420
  }
70
- nextAgentRunId(taskId) { return this.store.nextAgentRunId(taskId); }
71
421
  getPendingWakeup(taskId) { return this.store.getPendingWakeup(taskId); }
72
422
  listPendingWakeups() { return this.store.listPendingWakeups(); }
73
423
  savePendingWakeup(wakeup) {
@@ -83,18 +433,49 @@ export class FileSchedulerStoreAdapter {
83
433
  return "unavailable";
84
434
  }
85
435
  const role = requireRole(store, input.task.id, input.role.name);
436
+ const binding = activeRoleAgentBinding(role);
86
437
  if (role.activeAgentId !== input.role.activeAgentId
87
- || activeRoleAgentBinding(role).adapterId !== input.role.adapterId) {
438
+ || binding.adapterId !== input.role.adapterId
439
+ || binding.config.model !== input.role.model
440
+ || binding.config.effort !== input.role.effort
441
+ || role.workspace !== input.role.workspace) {
88
442
  return "state-changed";
89
443
  }
444
+ if (!isDeepStrictEqual(input.run.effective, input.role.effective)) {
445
+ return "state-changed";
446
+ }
447
+ if (store.getAgentRun(input.task.id, input.run.id) !== null)
448
+ return "state-changed";
90
449
  if (store.getActiveAgentRun(input.task.id, input.role.name) !== null)
91
450
  return "busy";
92
451
  const pending = store.getPendingWakeup(input.task.id);
93
452
  if (pending === null || !pendingWakeupsMatch(pending, input.wakeup)) {
94
453
  return "state-changed";
95
454
  }
455
+ const target = { kind: "role", taskId: input.task.id, roleName: input.role.name };
456
+ const mailbox = store.getWorkMailbox(target);
457
+ if (mailbox === null || mailbox.pending === null)
458
+ return "state-changed";
459
+ if (mailbox.processing !== null)
460
+ return "busy";
461
+ if (store.peekNextAgentRunId(input.task.id) !== input.run.id) {
462
+ return "state-changed";
463
+ }
464
+ const allocatedRunId = store.nextAgentRunId(input.task.id);
465
+ if (allocatedRunId !== input.run.id) {
466
+ throw new Error(`Leader Run allocation changed unexpectedly: ${input.task.id}.`);
467
+ }
468
+ const batchId = formatAgentRunReceiptId(input.task.id, input.run.id);
469
+ const claimed = bindExecution(claimPending(mailbox, {
470
+ batchId,
471
+ owner: "controller",
472
+ startedAt: input.now.toISOString()
473
+ }), batchId, { type: "run", taskId: input.task.id, id: input.run.id });
96
474
  store.saveActiveAgentRun(input.run);
475
+ store.saveWorkMailbox(claimed);
97
476
  store.saveRole(input.task.id, updateRoleStatus(role, "running", input.now));
477
+ bindTaskRoleRunInFlight(store, role, input.run, input.now);
478
+ store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "run.dispatched", runLaunchEventPayload(input.run), input.now));
98
479
  if (input.session !== null && input.session.nativeSessionId !== undefined) {
99
480
  saveTaskSession(store, role, {
100
481
  ...input.session,
@@ -103,7 +484,6 @@ export class FileSchedulerStoreAdapter {
103
484
  }
104
485
  store.clearLeaderFailure(input.task.id);
105
486
  store.clearOperatorNotification(input.task.id);
106
- store.clearPendingWakeup(input.task.id);
107
487
  return "claimed";
108
488
  });
109
489
  }
@@ -118,187 +498,1826 @@ export class FileSchedulerStoreAdapter {
118
498
  if (active === null || active.id !== input.run.id) {
119
499
  throw new Error(`Active Agent run changed before delivery was persisted: ${input.run.id}.`);
120
500
  }
121
- if (active.deliveredAt === undefined) {
122
- store.saveAgentRun(markAgentRunDelivered(active, input.now));
501
+ // Transport success records prompt-pushed ONLY. Acceptance (deliveredAt)
502
+ // is written exclusively by an exact provider-accepted fold, never by the
503
+ // transport receipt — that removes the transport-to-delivered false path.
504
+ if (active.pushedAt === undefined) {
505
+ store.saveAgentRun(markAgentRunPushed(active, input.now));
506
+ markTaskRoleRunPushedInFlight(store, role, input.run, input.now);
507
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, "run.pushed", runLaunchEventPayload(active), input.now));
508
+ }
509
+ else {
510
+ const sessions = store.getTaskRoleSessionSet(input.task.id, input.role.name);
511
+ if (sessions?.inFlight?.runId === input.run.id
512
+ && sessions.inFlight.pushedAt === undefined) {
513
+ markTaskRoleRunPushedInFlight(store, role, input.run, input.now);
514
+ }
123
515
  }
124
516
  if (role.status !== "running") {
125
517
  store.saveRole(input.task.id, updateRoleStatus(role, "running", input.now));
126
518
  }
127
519
  if (input.session !== null && input.session.nativeSessionId !== undefined) {
128
520
  const existing = store.getRoleSession(input.task.id, input.role.name);
521
+ const reservation = matchingPreparedRuntimeReservation(store, input);
129
522
  if (existing?.agentId !== input.session.agentId
130
523
  || existing.adapterId !== input.session.adapterId
131
524
  || existing.nativeSessionId !== input.session.nativeSessionId
525
+ || (input.launchId !== undefined && existing.launchId !== input.launchId)
132
526
  || existing.status !== "running") {
133
527
  saveTaskSession(store, role, {
134
528
  ...input.session,
135
529
  nativeSessionId: input.session.nativeSessionId
136
- }, "running", input.now);
530
+ }, "running", input.now, input.launchId);
137
531
  }
532
+ completePreparedRuntimeReservation(store, reservation);
138
533
  }
139
534
  });
140
535
  }
141
- saveLeaderDispatchFailure(input) {
536
+ saveRoleRunPrepared(input) {
142
537
  this.store.transaction((store) => {
143
538
  const task = store.getTask(input.task.id);
144
- if (task === null || task.status !== "active")
145
- return;
539
+ if (task === null || task.status !== "active") {
540
+ throw new Error(`Task is not active: ${input.task.id}.`);
541
+ }
146
542
  const role = requireRole(store, input.task.id, input.role.name);
147
- if (input.claimed !== undefined) {
148
- const active = store.getActiveAgentRun(input.task.id, input.role.name);
149
- if (active?.id === input.claimed.run.id) {
150
- store.saveAgentRun(failAgentRun(active, input.failure.message, input.now));
151
- store.clearActiveAgentRun(input.task.id, input.role.name);
543
+ const active = store.getActiveAgentRun(input.task.id, input.role.name);
544
+ if (active === null || active.id !== input.run.id) {
545
+ throw new Error(`Active Agent run changed before preparation was persisted: ${input.run.id}.`);
546
+ }
547
+ if (input.session !== null && input.session.nativeSessionId !== undefined) {
548
+ const existing = store.getRoleSession(input.task.id, input.role.name);
549
+ matchingPreparedRuntimeReservation(store, input);
550
+ if (existing?.nativeSessionId !== input.session.nativeSessionId
551
+ || (input.launchId !== undefined && existing.launchId !== input.launchId)
552
+ || existing.status !== "running") {
553
+ saveTaskSession(store, role, {
554
+ ...input.session,
555
+ nativeSessionId: input.session.nativeSessionId
556
+ }, "running", input.now, input.launchId);
152
557
  }
153
- store.savePendingWakeup(restorePendingWakeup(input.claimed.wakeup, store.getPendingWakeup(input.task.id)));
154
558
  }
155
- store.saveRole(input.task.id, updateRoleStatus(role, "failed", input.now));
156
- breakTaskSessionIfPresent(store, input.task.id, role.name, role.activeAgentId, input.now);
157
- store.saveLeaderFailure(input.failure);
158
- store.saveOperatorNotification(input.notification);
559
+ bindTaskRoleRunInFlight(store, role, input.run, input.now);
159
560
  });
160
561
  }
161
- saveExitedRoleRun(input) {
162
- this.store.transaction((store) => {
163
- const role = requireRole(store, input.task.id, input.role.name);
164
- store.saveAgentRun(failAgentRun(input.run, input.summary, input.now));
165
- store.clearActiveAgentRun(input.task.id, input.role.name);
166
- if (input.run.workItemId !== undefined) {
167
- const workItem = store.getWorkItem(input.task.id, input.run.workItemId);
168
- if (workItem !== null && !["completed", "failed", "cancelled", "superseded"].includes(workItem.status)) {
169
- store.saveWorkItem(input.task.id, updateWorkItemStatus(workItem, "failed", input.summary, input.now));
562
+ saveRoleRunDeliveryFailure(input) {
563
+ return this.store.transaction((store) => {
564
+ const task = store.getTask(input.taskId);
565
+ const role = store.getRole(input.taskId, input.roleName);
566
+ const active = store.getActiveAgentRun(input.taskId, input.roleName);
567
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
568
+ const session = sessions?.sessions[input.agentId];
569
+ const mailbox = store.getWorkMailbox({
570
+ kind: "role",
571
+ taskId: input.taskId,
572
+ roleName: input.roleName
573
+ });
574
+ const runtimeMailbox = store.getWorkMailbox(runtimeLifecycleTarget({
575
+ scope: "task",
576
+ taskId: input.taskId,
577
+ roleName: input.roleName
578
+ }));
579
+ if (task === null
580
+ || task.status !== "active"
581
+ || role === null
582
+ || active === null
583
+ || active.id !== input.runId
584
+ || active.status !== "active"
585
+ || active.pushedAt !== undefined
586
+ || active.effective.agentId !== input.agentId
587
+ || active.effective.adapterId !== input.adapterId
588
+ || mailbox?.processing?.batchId !== input.mailboxBatchId
589
+ || mailbox.processing.executionRef?.type !== "run"
590
+ || mailbox.processing.executionRef.taskId !== input.taskId
591
+ || mailbox.processing.executionRef.id !== input.runId
592
+ || !preparedDeliveryFailureSessionFenceMatches(sessions, session, input)
593
+ || !preparedReservationMatches(runtimeMailbox, input.taskId, input.runId, input.launchId)) {
594
+ return "state-changed";
595
+ }
596
+ const summary = `Role delivery retry limit exhausted before exact Run input delivery: ${input.runId}.`;
597
+ const result = terminalizeExactTaskRun(store, {
598
+ taskId: input.taskId,
599
+ roleName: input.roleName,
600
+ agentId: input.agentId,
601
+ runId: input.runId,
602
+ receiptId: formatAgentRunReceiptId(input.taskId, input.runId),
603
+ ...(session?.nativeSessionId === undefined
604
+ ? {}
605
+ : { nativeSessionId: session.nativeSessionId }),
606
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
607
+ outcome: { status: "failed", summary }
608
+ }, input.now);
609
+ if (result.disposition !== "applied" || result.run === null) {
610
+ return "state-changed";
611
+ }
612
+ enqueueWork(store, runtimeLifecycleTarget({
613
+ scope: "task",
614
+ taskId: input.taskId,
615
+ roleName: input.roleName
616
+ }), RUNTIME_CLEANUP_REQUIRED_REASON, input.now, [{ type: "task", id: input.taskId }]);
617
+ const terminal = result.run;
618
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.role-delivery-failed", {
619
+ runId: terminal.id,
620
+ roleName: input.roleName,
621
+ outcome: terminal.status
622
+ }, input.now));
623
+ if (terminal.purpose === "execution" && terminal.workItemId !== undefined) {
624
+ const item = store.getWorkItem(input.taskId, terminal.workItemId);
625
+ if (item !== null && ![
626
+ "completed", "failed", "retired"
627
+ ].includes(item.status)) {
628
+ const group = currentWorkItemExecutionGroup(item);
629
+ const groupedPanel = group !== undefined
630
+ && (group.lanes.length > 1 || group.strategy.mode === "adaptive")
631
+ && group.resolution === undefined;
632
+ if (!groupedPanel) {
633
+ store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", input.now, summary));
634
+ }
635
+ }
636
+ }
637
+ if (input.roleName !== "leader") {
638
+ enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, terminal.purpose === "review" ? "review-failed" : "role-run-failed", input.now, [
639
+ { type: "run", taskId: input.taskId, id: terminal.id },
640
+ ...(terminal.workItemId === undefined
641
+ ? []
642
+ : [{ type: "work-item", taskId: input.taskId, id: terminal.workItemId }])
643
+ ]);
644
+ }
645
+ else {
646
+ const failedRole = store.getRole(input.taskId, input.roleName);
647
+ if (failedRole !== null) {
648
+ store.saveRole(input.taskId, updateRoleStatus(failedRole, "failed", input.now));
170
649
  }
650
+ store.saveLeaderFailure(recordLeaderFailure(input.taskId, session?.nativeSessionId ?? "(unregistered)", summary, input.now, store.getLeaderFailure(input.taskId)));
651
+ store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, summary, input.now, store.getOperatorNotification(input.taskId)));
652
+ enqueueWork(store, { kind: "operator" }, "leader-run-failed", input.now, [
653
+ { type: "task", id: input.taskId },
654
+ { type: "run", taskId: input.taskId, id: terminal.id }
655
+ ]);
171
656
  }
172
- store.saveRole(input.task.id, updateRoleStatus(role, "exited", input.now));
173
- stopTaskSessionIfPresent(store, input.task.id, role.name, role.activeAgentId, input.now);
657
+ return "failed";
174
658
  });
175
659
  }
176
- saveArchivedTaskStopped(taskId, now) {
177
- this.store.transaction((store) => {
178
- const task = store.getTask(taskId);
179
- if (task === null || task.status !== "archived")
180
- return;
181
- for (const role of store.listRoles(taskId)) {
182
- if (role.status !== "idle") {
183
- store.saveRole(taskId, updateRoleStatus(role, "idle", now));
660
+ saveLeaderDispatchFailure(input) {
661
+ return this.store.transaction((store) => {
662
+ const task = store.getTask(input.task.id);
663
+ const role = store.getRole(input.task.id, input.role.name);
664
+ const active = store.getActiveAgentRun(input.task.id, input.role.name);
665
+ const target = {
666
+ kind: "role",
667
+ taskId: input.task.id,
668
+ roleName: input.role.name
669
+ };
670
+ const mailbox = store.getWorkMailbox(target);
671
+ if (task === null
672
+ || task.status !== "active"
673
+ || role === null
674
+ || active === null
675
+ || active.id !== input.claimed.run.id
676
+ || active.status !== "active"
677
+ || mailbox?.processing?.executionRef?.type !== "run"
678
+ || mailbox.processing.executionRef.taskId !== input.task.id
679
+ || mailbox.processing.executionRef.id !== input.claimed.run.id
680
+ || !schedulerRoleSessionsMatch(store.getRoleSession(input.task.id, input.role.name), input.session)) {
681
+ return "state-changed";
682
+ }
683
+ store.saveAgentRun(failAgentRun(active, input.failure.message, input.now));
684
+ store.clearActiveAgentRun(input.task.id, input.role.name);
685
+ clearTaskRoleRunInFlight(store, role, active, input.now);
686
+ store.saveWorkMailbox(releaseProcessing(mailbox, mailbox.processing.batchId));
687
+ store.saveRole(input.task.id, updateRoleStatus(role, "failed", input.now));
688
+ breakTaskSessionIfPresent(store, input.task.id, role.name, active.effective.agentId, input.now);
689
+ const failure = recordLeaderFailure(input.task.id, input.failure.nativeSessionId, input.failure.message, input.now, store.getLeaderFailure(input.task.id));
690
+ store.saveLeaderFailure(failure);
691
+ store.saveOperatorNotification(createLeaderRecoveryNotification(input.task.id, input.notification.message, input.now, store.getOperatorNotification(input.task.id)));
692
+ enqueueWork(store, { kind: "operator" }, "leader-recovery-failed", input.now, [
693
+ { type: "task", id: input.task.id }
694
+ ]);
695
+ return "failed";
696
+ });
697
+ }
698
+ saveExitedRoleRun(input) {
699
+ return this.store.transaction((store) => {
700
+ const task = store.getTask(input.task.id);
701
+ const role = store.getRole(input.task.id, input.role.name);
702
+ const currentRun = store.getActiveAgentRun(input.task.id, input.role.name);
703
+ if (task === null
704
+ || task.status !== "active"
705
+ || role === null
706
+ || currentRun === null
707
+ || currentRun.id !== input.run.id
708
+ || currentRun.status !== "active") {
709
+ return "state-changed";
710
+ }
711
+ const sessions = store.getTaskRoleSessionSet(task.id, role.name);
712
+ if (sessions?.inFlight !== null
713
+ && sessions !== null
714
+ && (sessions.inFlight.agentId !== currentRun.effective.agentId
715
+ || sessions.inFlight.runId !== currentRun.id
716
+ || sessions.inFlight.receiptId !== formatAgentRunReceiptId(task.id, currentRun.id))) {
717
+ return "state-changed";
718
+ }
719
+ // Review launch failure uses the aggregate exact-terminalization path.
720
+ // It owns mailbox settlement and ReviewRound convergence together, so a
721
+ // pending pre-delivery dispatch cannot be consumed before its Run and
722
+ // Round pass the same immutable identity fences.
723
+ if (currentRun.purpose === "review") {
724
+ if (input.run.taskId !== currentRun.taskId
725
+ || input.run.roleName !== currentRun.roleName
726
+ || input.run.purpose !== "review"
727
+ || input.run.reviewRoundId !== currentRun.reviewRoundId
728
+ || input.run.workItemId !== currentRun.workItemId
729
+ || input.run.effective.agentId !== currentRun.effective.agentId
730
+ || input.run.effective.adapterId !== currentRun.effective.adapterId) {
731
+ return "state-changed";
184
732
  }
733
+ const terminal = terminalizeExactTaskRun(store, {
734
+ taskId: task.id,
735
+ roleName: role.name,
736
+ agentId: currentRun.effective.agentId,
737
+ runId: currentRun.id,
738
+ receiptId: formatAgentRunReceiptId(task.id, currentRun.id),
739
+ ...(input.session?.nativeSessionId === undefined
740
+ ? {}
741
+ : { nativeSessionId: input.session.nativeSessionId }),
742
+ outcome: { status: "failed", summary: input.summary }
743
+ }, input.now);
744
+ if (terminal.disposition !== "applied")
745
+ return "state-changed";
746
+ clearMatchingLeaderStallAttention(store, task.id, currentRun.id);
747
+ store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
748
+ stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
749
+ queueLeaderWakeup(store, task.id, "review-failed", input.now);
750
+ return "failed";
185
751
  }
186
- for (const current of store.listRoleSessionSets(taskId)) {
187
- if (Object.values(current.sessions).every((session) => session.status === "stopped")) {
188
- continue;
752
+ const target = { kind: "role", taskId: task.id, roleName: role.name };
753
+ const mailbox = store.getWorkMailbox(target);
754
+ const processing = mailbox?.processing;
755
+ if (mailbox !== null && processing !== null && processing !== undefined) {
756
+ if (processing.executionRef === undefined
757
+ || processing.executionRef.type !== "run"
758
+ || processing.executionRef.taskId !== task.id
759
+ || processing.executionRef.id !== currentRun.id) {
760
+ return "state-changed";
189
761
  }
190
- let updated = current;
191
- for (const agentId of Object.keys(current.sessions)) {
192
- updated = updateRoleAgentSessionStatus(updated, agentId, "stopped", now);
762
+ }
763
+ store.saveAgentRun(failAgentRun(currentRun, input.summary, input.now));
764
+ clearMatchingLeaderStallAttention(store, task.id, currentRun.id);
765
+ store.clearActiveAgentRun(task.id, role.name);
766
+ clearTaskRoleRunInFlight(store, role, currentRun, input.now);
767
+ if (mailbox !== null && processing !== null && processing !== undefined) {
768
+ store.saveWorkMailbox(completeProcessing(mailbox, processing.batchId));
769
+ }
770
+ if (currentRun.purpose === "execution" && currentRun.workItemId !== undefined) {
771
+ const workItem = store.getWorkItem(task.id, currentRun.workItemId);
772
+ if (workItem !== null && !["completed", "failed", "retired"].includes(workItem.status)) {
773
+ // Terminalize the bound execution lane in the same transaction that
774
+ // fails the Run. Without this, a scheduler-driven Worker failure
775
+ // leaves the lane "running" behind a "failed" WorkItem, and a later
776
+ // `yui task run retry` is rejected because the lane is not terminal.
777
+ // The lane result and the WorkItem status are two ordered single-step
778
+ // record revisions, matching the aggregate terminalization path.
779
+ if (currentRun.executionGroupId !== undefined
780
+ && currentRun.executionLaneId !== undefined
781
+ && currentWorkItemExecutionGroup(workItem) !== undefined) {
782
+ const group = currentWorkItemExecutionGroup(workItem);
783
+ store.saveWorkItem(task.id, updateWorkItemExecutionGroup(workItem, recordExecutionLaneResult(group, currentRun.executionLaneId, { summary: input.summary }, "failed", input.now), input.now));
784
+ }
785
+ const laneUpdated = store.getWorkItem(task.id, currentRun.workItemId);
786
+ const laneGroup = currentWorkItemExecutionGroup(laneUpdated);
787
+ const groupedPanel = laneGroup !== undefined
788
+ && (laneGroup.lanes.length > 1 || laneGroup.strategy.mode === "adaptive")
789
+ && laneGroup.resolution === undefined;
790
+ if (!groupedPanel) {
791
+ store.saveWorkItem(task.id, updateWorkItemStatus(laneUpdated, "failed", input.now, input.summary));
792
+ }
193
793
  }
194
- store.saveRoleSessionSet(updated);
195
794
  }
795
+ store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
796
+ stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
797
+ queueLeaderWakeup(store, task.id, role.name === "leader" ? "leader-run-failed" : "role-run-failed", input.now);
798
+ return "failed";
196
799
  });
197
800
  }
198
801
  /** Called by the internal Codex notify hook, never by an LLM prompt. */
199
802
  recordRuntimeNativeSession(input, now = new Date()) {
803
+ return this.store.transaction((store) => (recordTaskRuntimeNativeSession(store, input, now)));
804
+ }
805
+ reserveRuntimeLaunch(input, assertCurrent, now = new Date()) {
806
+ return this.store.transaction((store) => {
807
+ assertCurrent();
808
+ const target = runtimeLifecycleTarget(input.owner);
809
+ const existing = store.getWorkMailbox(target);
810
+ if (isRuntimeLaunchReservation(existing?.processing)) {
811
+ if (hasRuntimeCleanupObligation(existing)) {
812
+ throw new Error("Runtime cleanup is still pending.");
813
+ }
814
+ const executionRef = existing.processing.executionRef;
815
+ if (executionRef?.type === "run"
816
+ && (input.owner.scope !== "task"
817
+ || executionRef.taskId !== input.owner.taskId)) {
818
+ throw new Error("Runtime launch reservation belongs to another Task Run.");
819
+ }
820
+ return {
821
+ status: "existing",
822
+ launchId: existing.processing.batchId,
823
+ ...(executionRef?.type === "run"
824
+ ? { runId: executionRef.id }
825
+ : {})
826
+ };
827
+ }
828
+ if (existing !== null
829
+ && (existing.processing !== null || existing.pending !== null)) {
830
+ throw new Error("Runtime lifecycle work is already pending.");
831
+ }
832
+ enqueueWork(store, target, RUNTIME_LAUNCH_RESERVED_REASON, now, input.owner.scope === "task"
833
+ ? [{ type: "task", id: input.owner.taskId }]
834
+ : []);
835
+ const queued = store.getWorkMailbox(target);
836
+ if (queued === null || queued.pending === null || queued.processing !== null) {
837
+ throw new Error("Runtime launch reservation could not be queued.");
838
+ }
839
+ let claimed = claimPending(queued, {
840
+ batchId: input.launchId,
841
+ owner: RUNTIME_LIFECYCLE_OWNER,
842
+ startedAt: now.toISOString()
843
+ });
844
+ if (input.runId !== undefined) {
845
+ if (input.owner.scope !== "task") {
846
+ throw new Error("A Run-bound runtime reservation requires a Task owner.");
847
+ }
848
+ claimed = bindExecution(claimed, input.launchId, { type: "run", taskId: input.owner.taskId, id: input.runId });
849
+ }
850
+ store.saveWorkMailbox(claimed);
851
+ return {
852
+ status: "reserved",
853
+ launchId: input.launchId,
854
+ ...(input.runId === undefined ? {} : { runId: input.runId })
855
+ };
856
+ });
857
+ }
858
+ confirmRuntimeLaunchReservation(input, assertCurrent) {
859
+ return this.store.transaction((store) => {
860
+ assertCurrent();
861
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(input.owner));
862
+ if (isRuntimeLaunchReservation(mailbox?.processing, input.launchId)) {
863
+ return "reserved";
864
+ }
865
+ if (hasRuntimeCleanupObligation(mailbox)) {
866
+ throw new Error("Runtime cleanup is still pending.");
867
+ }
868
+ // A synchronous provider lifecycle Hook may bind a fresh native Session
869
+ // and complete the reservation while the host start call is still
870
+ // unwinding. Preserve that exact hook-won generation instead of treating
871
+ // the already-settled reservation as a launch failure.
872
+ const sessions = runtimeOwnerSessionSet(store, input.owner);
873
+ const active = sessions?.sessions[sessions.activeAgentId];
874
+ if (active?.status === "running"
875
+ && active.launchId === input.launchId)
876
+ return "provider-bound";
877
+ throw new Error("Runtime launch reservation no longer matches the launch.");
878
+ });
879
+ }
880
+ recordReservedRuntimeNativeSession(input, assertCurrent, now = new Date()) {
881
+ return this.store.transaction((store) => {
882
+ assertCurrent();
883
+ const mailbox = requireRuntimeLaunchReservation(store, input.owner, input.launchId);
884
+ if (hasRuntimeCleanupObligation(mailbox)) {
885
+ throw new Error("Runtime cleanup is still pending.");
886
+ }
887
+ const session = input.owner.scope === "task"
888
+ ? recordTaskRuntimeNativeSession(store, {
889
+ taskId: input.owner.taskId,
890
+ roleName: input.owner.roleName,
891
+ agentId: input.agentId,
892
+ adapterId: input.adapterId,
893
+ nativeSessionId: input.nativeSessionId,
894
+ launchId: input.launchId,
895
+ effective: input.effective
896
+ }, now)
897
+ : recordGlobalRuntimeNativeSession(store, {
898
+ roleName: input.owner.roleName,
899
+ agentId: input.agentId,
900
+ adapterId: input.adapterId,
901
+ nativeSessionId: input.nativeSessionId,
902
+ launchId: input.launchId,
903
+ effective: input.effective
904
+ }, now);
905
+ saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, input.launchId));
906
+ return session;
907
+ });
908
+ }
909
+ completeRuntimeLaunchReservation(owner, launchId, expectedTerminalRunId, beforeComplete) {
910
+ return this.store.transaction((store) => {
911
+ const target = runtimeLifecycleTarget(owner);
912
+ const mailbox = store.getWorkMailbox(target);
913
+ if (!isRuntimeLaunchReservation(mailbox?.processing, launchId))
914
+ return false;
915
+ if (expectedTerminalRunId !== undefined) {
916
+ const expectedRun = owner.scope === "task"
917
+ ? store.getAgentRun(owner.taskId, expectedTerminalRunId)
918
+ : null;
919
+ if (owner.scope !== "task"
920
+ || mailbox?.processing?.executionRef?.type !== "run"
921
+ || mailbox.processing.executionRef.taskId !== owner.taskId
922
+ || mailbox.processing.executionRef.id !== expectedTerminalRunId
923
+ || expectedRun === null
924
+ || expectedRun.status === "active") {
925
+ return false;
926
+ }
927
+ }
928
+ beforeComplete?.();
929
+ saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, launchId));
930
+ return true;
931
+ });
932
+ }
933
+ /**
934
+ * Atomically settles a launch whose host is confirmed absent.
935
+ *
936
+ * The exact reservation is authoritative. If its matching Hook won the
937
+ * transaction race and already cleared that reservation, the session
938
+ * fallback is permitted only while the same Agent/native identity remains
939
+ * current and no later lifecycle work or Task Run exists.
940
+ */
941
+ settleStoppedRuntimeLaunch(input, now = new Date()) {
942
+ return this.store.transaction((store) => {
943
+ const target = runtimeLifecycleTarget(input.owner);
944
+ const mailbox = store.getWorkMailbox(target);
945
+ if (isRuntimeLaunchReservation(mailbox?.processing, input.launchId)) {
946
+ const sessions = runtimeOwnerSessionSet(store, input.owner);
947
+ const active = sessions?.sessions[sessions.activeAgentId];
948
+ if (runtimeSessionMatchesSettledLaunch(active, input)) {
949
+ markRuntimeOwnerSessionStopped(store, input.owner, now);
950
+ }
951
+ saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, input.launchId));
952
+ return true;
953
+ }
954
+ if (hasRuntimeLifecycleWork(mailbox))
955
+ return false;
956
+ if (input.owner.scope === "task"
957
+ && store.getActiveAgentRun(input.owner.taskId, input.owner.roleName) !== null) {
958
+ return false;
959
+ }
960
+ const sessions = runtimeOwnerSessionSet(store, input.owner);
961
+ if (sessions === null)
962
+ return true;
963
+ const active = sessions.sessions[sessions.activeAgentId];
964
+ if (!runtimeSessionMatchesSettledLaunch(active, input))
965
+ return false;
966
+ if (active.status !== "stopped") {
967
+ markRuntimeOwnerSessionStopped(store, input.owner, now);
968
+ }
969
+ return true;
970
+ });
971
+ }
972
+ /**
973
+ * Fast hook path: durably records the native Turn boundary and a two-second
974
+ * closure deadline. It never performs tmux, workspace, or Controller I/O.
975
+ */
976
+ classifyRuntimeTurnCompleted(input) {
977
+ const task = this.store.getTask(input.taskId);
978
+ const role = this.store.getRole(input.taskId, input.roleName);
979
+ if (task === null || task.status === "archived" || role === null)
980
+ return "obsolete";
981
+ const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
982
+ const existing = sessions?.sessions[input.agentId];
983
+ const owner = {
984
+ scope: "task",
985
+ taskId: input.taskId,
986
+ roleName: input.roleName
987
+ };
988
+ if (existing === undefined
989
+ && !runtimeHookMatchesReservation(this.store, owner, input.launchId))
990
+ return "obsolete";
991
+ try {
992
+ const effectiveExisting = nativeTransitionExisting(this.store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
993
+ const effective = taskSessionEffective(this.store, input.taskId, input.roleName, input.agentId, effectiveExisting);
994
+ if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
995
+ return "obsolete";
996
+ }
997
+ }
998
+ catch {
999
+ return "obsolete";
1000
+ }
1001
+ if (sessions?.pendingTurnCompletion !== null && sessions !== null) {
1002
+ return sessions.pendingTurnCompletion.turnId === input.turnId
1003
+ ? "apply"
1004
+ : "deferred";
1005
+ }
1006
+ // A normal explicit CLI yield may precede its native Hook; that Hook still
1007
+ // owns the turn fact. A forced cleanup boundary or stopped process instead
1008
+ // makes the old generation obsolete.
1009
+ if (sessions?.inFlight === null || sessions === null) {
1010
+ const cleanup = hasRuntimeCleanupObligation(this.store.getWorkMailbox(runtimeLifecycleTarget({
1011
+ scope: "task",
1012
+ taskId: input.taskId,
1013
+ roleName: input.roleName
1014
+ })));
1015
+ return cleanup || isObsoleteTerminalRuntimeRun(this.store, input)
1016
+ ? "obsolete"
1017
+ : "apply";
1018
+ }
1019
+ if (input.runId === undefined || sessions.inFlight.runId !== input.runId) {
1020
+ return "obsolete";
1021
+ }
1022
+ // Provider completion is not a transport acknowledgement. Retain the
1023
+ // immutable Hook until the exact pane receipt independently reconciles
1024
+ // run.pushed; never infer transport from provider progress/terminal facts.
1025
+ return sessions.inFlight.pushedAt === undefined ? "deferred" : "apply";
1026
+ }
1027
+ observeRuntimeTurnCompleted(input, now = new Date()) {
200
1028
  return this.store.transaction((store) => {
201
1029
  const task = store.getTask(input.taskId);
202
1030
  if (task === null)
203
1031
  throw new Error(`Task not found: ${input.taskId}.`);
204
- if (task.status === "archived") {
205
- throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
206
- }
207
1032
  if (task.status !== "active") {
208
- throw new Error(`Cannot register a native session for a Task that is not active: ${input.taskId}.`);
1033
+ throw new Error(`Cannot complete a runtime turn for inactive Task: ${input.taskId}.`);
209
1034
  }
210
1035
  const role = requireRole(store, input.taskId, input.roleName);
211
- const binding = activeRoleAgentBinding(role);
212
- if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
213
- throw new Error("Native session registration does not match the active Role Agent binding.");
1036
+ let sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName)
1037
+ ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
1038
+ const existing = sessions.sessions[input.agentId];
1039
+ const owner = {
1040
+ scope: "task",
1041
+ taskId: input.taskId,
1042
+ roleName: input.roleName
1043
+ };
1044
+ if (existing === undefined
1045
+ && !runtimeHookMatchesReservation(store, owner, input.launchId)) {
1046
+ throw new Error("Runtime turn completion does not match the launch reservation.");
214
1047
  }
215
- const current = store.getRoleSessionSet(input.taskId, input.roleName)
216
- ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, role.activeAgentId, now);
217
- const existing = current.sessions[input.agentId];
218
- if (existing !== undefined && existing.nativeSessionId !== input.nativeSessionId) {
219
- throw new Error("Native session registration conflicts with the fixed Role session.");
1048
+ const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
1049
+ const effective = taskSessionEffective(store, task.id, role.name, input.agentId, effectiveExisting);
1050
+ if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
1051
+ throw new Error("Runtime turn completion does not match the effective launch identity.");
1052
+ }
1053
+ if (effectiveExisting !== undefined
1054
+ && hasRecentTurnId(effectiveExisting.recentCompletedTurnIds, input.turnId)) {
1055
+ return { session: effectiveExisting, duplicate: true };
1056
+ }
1057
+ const pending = sessions.pendingTurnCompletion;
1058
+ if (effectiveExisting !== undefined
1059
+ && pending !== null
1060
+ && pending.agentId === input.agentId
1061
+ && pending.nativeSessionId === input.nativeSessionId
1062
+ && pending.turnId === input.turnId
1063
+ && pending.runId === input.runId) {
1064
+ return {
1065
+ session: effectiveExisting,
1066
+ duplicate: true,
1067
+ pendingRunId: pending.runId
1068
+ };
1069
+ }
1070
+ if (sessions.inFlight === null
1071
+ && existing !== undefined
1072
+ && (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))
1073
+ || isObsoleteTerminalRuntimeRun(store, input))) {
1074
+ return { session: existing, duplicate: false, disposition: "obsolete" };
1075
+ }
1076
+ // Classification is only an optimization. Revalidate the Run inside the
1077
+ // authoritative transaction before a Hook may claim a native identity.
1078
+ if (sessions.inFlight !== null
1079
+ && (input.runId === undefined
1080
+ || sessions.inFlight.runId !== input.runId)) {
1081
+ if (effectiveExisting === undefined) {
1082
+ throw new Error("Runtime turn completion no longer matches the in-flight Run.");
1083
+ }
1084
+ return { session: effectiveExisting, duplicate: false };
1085
+ }
1086
+ if (sessions.inFlight !== null && sessions.inFlight.pushedAt === undefined) {
1087
+ throw new Error("Runtime turn completion requires an independently committed transport receipt.");
220
1088
  }
221
- if (existing?.status === "running")
222
- return existing;
223
- const updated = recordRoleAgentSession(current, {
1089
+ const idleStatus = effectiveExisting?.status === "stopped"
1090
+ || effectiveExisting?.status === "broken"
1091
+ ? effectiveExisting.status
1092
+ : "ready";
1093
+ sessions = recordRoleAgentSession(sessions, {
224
1094
  agentId: input.agentId,
225
1095
  adapterId: input.adapterId,
226
1096
  nativeSessionId: input.nativeSessionId,
1097
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
227
1098
  policy: "fixed",
228
- status: "running"
1099
+ status: sessions.inFlight === null ? idleStatus : "running",
1100
+ effective
229
1101
  }, now);
230
- store.saveRoleSessionSet(updated);
231
- return updated.sessions[input.agentId];
1102
+ if (sessions.inFlight === null) {
1103
+ sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, input.turnId, now);
1104
+ store.saveTaskRoleSessionSet(sessions);
1105
+ completeRuntimeHookReservation(store, owner, input.launchId);
1106
+ return { session: sessions.sessions[input.agentId], duplicate: false };
1107
+ }
1108
+ const fence = {
1109
+ agentId: sessions.inFlight.agentId,
1110
+ runId: sessions.inFlight.runId,
1111
+ receiptId: sessions.inFlight.receiptId
1112
+ };
1113
+ const completion = createPendingTurnCompletion({
1114
+ taskId: task.id,
1115
+ roleName: role.name,
1116
+ agentId: input.agentId,
1117
+ nativeSessionId: input.nativeSessionId,
1118
+ turnId: input.turnId,
1119
+ runId: fence.runId,
1120
+ summary: input.summary,
1121
+ observedAt: now,
1122
+ dueAt: new Date(now.getTime() + 2_000)
1123
+ });
1124
+ const wasStalled = isRoleRunStalled(store.listEvents(task.id), fence.runId);
1125
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_PROGRESS_EVENT, {
1126
+ runId: fence.runId,
1127
+ roleName: role.name,
1128
+ kind: "native-turn",
1129
+ turnId: input.turnId,
1130
+ note: boundedRuntimeNote(input.summary)
1131
+ }, now));
1132
+ if (wasStalled) {
1133
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, {
1134
+ runId: fence.runId,
1135
+ roleName: role.name,
1136
+ progressAt: now.toISOString(),
1137
+ kind: "native-turn"
1138
+ }, now));
1139
+ }
1140
+ clearMatchingLeaderStallAttention(store, task.id, fence.runId);
1141
+ sessions = recordObservedTaskRoleCompletion(sessions, completion);
1142
+ const active = store.getActiveAgentRun(task.id, role.name);
1143
+ if (active === null || active.id !== fence.runId || active.status !== "active") {
1144
+ sessions = settleTaskRoleCompletion(sessions, {
1145
+ agentId: input.agentId,
1146
+ runId: fence.runId,
1147
+ turnId: input.turnId
1148
+ }, now);
1149
+ sessions = updateRoleAgentSessionStatus(sessions, input.agentId, "ready", now);
1150
+ store.saveTaskRoleSessionSet(sessions);
1151
+ completeRuntimeHookReservation(store, owner, input.launchId);
1152
+ return { session: sessions.sessions[input.agentId], duplicate: false };
1153
+ }
1154
+ store.saveTaskRoleSessionSet(sessions);
1155
+ completeRuntimeHookReservation(store, owner, input.launchId);
1156
+ return {
1157
+ session: sessions.sessions[input.agentId],
1158
+ duplicate: false,
1159
+ pendingRunId: fence.runId
1160
+ };
1161
+ });
1162
+ }
1163
+ listPendingRuntimeTurnCompletions() {
1164
+ return this.store.listTasks().flatMap((task) => (this.store.listRoleSessionSets(task.id).flatMap((sessions) => (sessions.pendingTurnCompletion === null ? [] : [sessions.pendingTurnCompletion]))));
1165
+ }
1166
+ /** Resolves only completions whose grace deadline has elapsed. */
1167
+ resolveDueRuntimeTurnCompletions(now, taskIds) {
1168
+ const due = this.listPendingRuntimeTurnCompletions().filter((completion) => (Date.parse(completion.dueAt) <= now.getTime()
1169
+ && (taskIds === undefined || taskIds.has(completion.taskId))));
1170
+ const finalized = [];
1171
+ for (const completion of due) {
1172
+ this.store.transaction((store) => {
1173
+ const current = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
1174
+ if (current === null)
1175
+ return;
1176
+ const pending = current.pendingTurnCompletion;
1177
+ if (pending === null
1178
+ || pending.turnId !== completion.turnId
1179
+ || pending.runId !== completion.runId)
1180
+ return;
1181
+ const active = store.getActiveAgentRun(completion.taskId, completion.roleName);
1182
+ if (active?.id === completion.runId) {
1183
+ const result = this.recordRuntimeTurnCompleted({
1184
+ taskId: completion.taskId,
1185
+ roleName: completion.roleName,
1186
+ agentId: completion.agentId,
1187
+ adapterId: current.sessions[completion.agentId].adapterId,
1188
+ nativeSessionId: completion.nativeSessionId,
1189
+ turnId: completion.turnId,
1190
+ expectedRunId: completion.runId,
1191
+ summary: completion.summary
1192
+ }, now);
1193
+ if (result.finalizedRunId !== undefined) {
1194
+ finalized.push(formatTaskRecordReference(completion.taskId, result.finalizedRunId, "agentRun"));
1195
+ }
1196
+ }
1197
+ let settled = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
1198
+ if (settled?.pendingTurnCompletion?.turnId !== completion.turnId)
1199
+ return;
1200
+ settled = settleTaskRoleCompletion(settled, {
1201
+ agentId: completion.agentId,
1202
+ runId: completion.runId,
1203
+ turnId: completion.turnId
1204
+ }, now);
1205
+ settled = updateRoleAgentSessionStatus(settled, completion.agentId, "ready", now);
1206
+ store.saveTaskRoleSessionSet(settled);
1207
+ });
1208
+ }
1209
+ return finalized;
1210
+ }
1211
+ /** A structured native Turn-completion event terminalizes its exact Run fence. */
1212
+ recordRuntimeTurnCompleted(input, now = new Date()) {
1213
+ return this.store.transaction((store) => {
1214
+ const task = store.getTask(input.taskId);
1215
+ if (task === null)
1216
+ throw new Error(`Task not found: ${input.taskId}.`);
1217
+ if (task.status !== "active") {
1218
+ throw new Error(`Cannot complete a runtime turn for inactive Task: ${input.taskId}.`);
1219
+ }
1220
+ const role = requireRole(store, input.taskId, input.roleName);
1221
+ const current = store.getRoleSessionSet(input.taskId, input.roleName)
1222
+ ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
1223
+ const existing = current.sessions[input.agentId];
1224
+ const owner = {
1225
+ scope: "task",
1226
+ taskId: input.taskId,
1227
+ roleName: input.roleName
1228
+ };
1229
+ if (existing === undefined
1230
+ && !runtimeHookMatchesReservation(store, owner, input.launchId)) {
1231
+ throw new Error("Runtime turn completion does not match the launch reservation.");
1232
+ }
1233
+ const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
1234
+ const effective = taskSessionEffective(store, task.id, role.name, input.agentId, effectiveExisting);
1235
+ if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
1236
+ throw new Error("Runtime turn completion does not match the effective launch identity.");
1237
+ }
1238
+ const sessions = effectiveExisting?.status === "ready"
1239
+ ? current
1240
+ : recordRoleAgentSession(current, {
1241
+ agentId: input.agentId,
1242
+ adapterId: input.adapterId,
1243
+ nativeSessionId: input.nativeSessionId,
1244
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1245
+ policy: "fixed",
1246
+ status: "ready",
1247
+ effective
1248
+ }, now);
1249
+ if (input.expectedRunId !== undefined
1250
+ && store.getActiveAgentRun(task.id, role.name)?.id !== input.expectedRunId) {
1251
+ return { session: sessions.sessions[input.agentId] };
1252
+ }
1253
+ if (sessions !== current)
1254
+ store.saveRoleSessionSet(sessions);
1255
+ completeRuntimeHookReservation(store, owner, input.launchId);
1256
+ const session = sessions.sessions[input.agentId];
1257
+ const active = store.getActiveAgentRun(task.id, role.name);
1258
+ if (task.status !== "active"
1259
+ || active === null
1260
+ || active.pushedAt === undefined
1261
+ || (input.expectedRunId !== undefined && active.id !== input.expectedRunId)) {
1262
+ return { session };
1263
+ }
1264
+ if (role.name !== "leader" || active.workItemId !== undefined) {
1265
+ const summary = `Role turn completed without yui task run yield. Last assistant message: ${input.summary}`;
1266
+ const result = terminalizeExactTaskRun(store, {
1267
+ taskId: task.id,
1268
+ roleName: role.name,
1269
+ agentId: active.effective.agentId,
1270
+ runId: active.id,
1271
+ receiptId: formatAgentRunReceiptId(task.id, active.id),
1272
+ nativeSessionId: input.nativeSessionId,
1273
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1274
+ outcome: { status: "failed", summary }
1275
+ }, now);
1276
+ if (result.disposition !== "applied" || result.run === null) {
1277
+ throw new Error(`Role Run changed during native Turn completion: ${active.id}/${result.reason}.`);
1278
+ }
1279
+ const terminal = result.run;
1280
+ if (active.purpose === "execution" && active.workItemId !== undefined) {
1281
+ const item = store.getWorkItem(task.id, active.workItemId);
1282
+ if (item !== null && !["completed", "failed", "retired"].includes(item.status)) {
1283
+ store.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", now, summary));
1284
+ }
1285
+ }
1286
+ store.saveRole(task.id, updateRoleStatus(role, "idle", now));
1287
+ clearMatchingLeaderStallAttention(store, task.id, terminal.id);
1288
+ enqueueWork(store, { kind: "role", taskId: task.id, roleName: "leader" }, "role-run-failed", now, [
1289
+ { type: "run", taskId: task.id, id: terminal.id },
1290
+ ...(terminal.workItemId === undefined
1291
+ ? []
1292
+ : [{
1293
+ type: "work-item",
1294
+ taskId: task.id,
1295
+ id: terminal.workItemId
1296
+ }])
1297
+ ]);
1298
+ return { session, finalizedRunId: terminal.id };
1299
+ }
1300
+ const leaderTarget = { kind: "role", taskId: task.id, roleName: role.name };
1301
+ const leaderMailbox = store.getWorkMailbox(leaderTarget);
1302
+ const recoveryTurn = leaderMailbox?.processing?.executionRef?.type === "run"
1303
+ && leaderMailbox.processing.executionRef.taskId === task.id
1304
+ && leaderMailbox.processing.executionRef.id === active.id
1305
+ && leaderMailbox.processing.batch.reasons.includes("leader-turn-unclosed");
1306
+ const quiescent = leaderMailbox?.pending === null
1307
+ && !store.listRoles(task.id).some((candidate) => (candidate.name !== "leader"
1308
+ && store.getActiveAgentRun(task.id, candidate.name) !== null))
1309
+ && !store.listWorkItems(task.id).some((item) => item.status === "running")
1310
+ && !store.listInputRequests(task.id).some((request) => request.status === "open");
1311
+ const result = terminalizeExactTaskRun(store, {
1312
+ taskId: task.id,
1313
+ roleName: role.name,
1314
+ agentId: active.effective.agentId,
1315
+ runId: active.id,
1316
+ receiptId: formatAgentRunReceiptId(task.id, active.id),
1317
+ nativeSessionId: input.nativeSessionId,
1318
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1319
+ outcome: { status: "yielded", summary: input.summary }
1320
+ }, now);
1321
+ if (result.disposition !== "applied" || result.run === null) {
1322
+ throw new Error(`Leader Run changed during native Turn completion: ${active.id}/${result.reason}.`);
1323
+ }
1324
+ const terminal = result.run;
1325
+ if (input.origin === "recovery") {
1326
+ store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, { runId: terminal.id, roleName: role.name, summary: input.summary }, now));
1327
+ }
1328
+ clearMatchingLeaderStallAttention(store, task.id, terminal.id);
1329
+ if (quiescent) {
1330
+ if (recoveryTurn) {
1331
+ const message = "Leader ended a recovery Turn without completing, blocking, or continuing the Task.";
1332
+ store.saveLeaderFailure(recordLeaderFailure(task.id, input.nativeSessionId, message, now, store.getLeaderFailure(task.id)));
1333
+ store.saveOperatorNotification(createLeaderRecoveryNotification(task.id, message, now, store.getOperatorNotification(task.id)));
1334
+ store.saveRole(task.id, updateRoleStatus(role, "failed", now));
1335
+ enqueueWork(store, { kind: "operator" }, "leader-recovery-failed", now, [
1336
+ { type: "task", id: task.id },
1337
+ { type: "run", taskId: task.id, id: terminal.id }
1338
+ ]);
1339
+ }
1340
+ else {
1341
+ enqueueWork(store, leaderTarget, "leader-turn-unclosed", now, [
1342
+ { type: "task", id: task.id },
1343
+ { type: "run", taskId: task.id, id: terminal.id }
1344
+ ]);
1345
+ }
1346
+ }
1347
+ return { session, finalizedRunId: terminal.id };
1348
+ });
1349
+ }
1350
+ classifyClaudeStopFailureEvent(input) {
1351
+ const task = this.store.getTask(input.taskId);
1352
+ if (task?.status !== "active")
1353
+ return "obsolete";
1354
+ const role = this.store.getRole(input.taskId, input.roleName);
1355
+ if (role === null)
1356
+ return "obsolete";
1357
+ const run = this.store.getAgentRun(input.taskId, input.runId);
1358
+ const active = this.store.getActiveAgentRun(input.taskId, input.roleName);
1359
+ if (run === null
1360
+ || run.status !== "active"
1361
+ || active?.id !== run.id
1362
+ || run.roleName !== input.roleName
1363
+ || run.effective.agentId !== input.agentId
1364
+ || run.effective.adapterId !== "claude"
1365
+ || input.adapterId !== "claude")
1366
+ return "obsolete";
1367
+ const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
1368
+ const session = sessions?.sessions[input.agentId];
1369
+ if (sessions === null
1370
+ || session?.nativeSessionId !== input.nativeSessionId
1371
+ || session.launchId !== input.launchId
1372
+ || sessions.inFlight?.agentId !== input.agentId
1373
+ || sessions.inFlight.runId !== input.runId
1374
+ || sessions.inFlight.receiptId !== `agent-run:${input.taskId}/${input.runId}`)
1375
+ return "obsolete";
1376
+ return "apply";
1377
+ }
1378
+ observeClaudeStopFailureEvent(input, now = new Date()) {
1379
+ return this.store.transaction((store) => {
1380
+ const before = store.getAgentRun(input.taskId, input.runId);
1381
+ if (before === null) {
1382
+ recordObsoleteRuntimeEvent(store, input, "run-missing", now);
1383
+ return { disposition: "obsolete", runId: input.runId };
1384
+ }
1385
+ const summary = claudeStopFailureSummary(input);
1386
+ const result = terminalizeExactTaskRun(store, {
1387
+ taskId: input.taskId,
1388
+ roleName: input.roleName,
1389
+ agentId: input.agentId,
1390
+ runId: input.runId,
1391
+ receiptId: `agent-run:${input.taskId}/${input.runId}`,
1392
+ nativeSessionId: input.nativeSessionId,
1393
+ launchId: input.launchId,
1394
+ outcome: {
1395
+ status: "failed",
1396
+ summary
1397
+ }
1398
+ }, now);
1399
+ if (result.disposition === "obsolete" || result.run === null) {
1400
+ recordObsoleteRuntimeEvent(store, input, result.reason ?? "identity-mismatch-or-terminal", now);
1401
+ return { disposition: "obsolete", runId: input.runId };
1402
+ }
1403
+ enqueueWork(store, runtimeLifecycleTarget({
1404
+ scope: "task",
1405
+ taskId: input.taskId,
1406
+ roleName: input.roleName
1407
+ }), RUNTIME_CLEANUP_REQUIRED_REASON, now, [{ type: "task", id: input.taskId }]);
1408
+ const terminal = result.run;
1409
+ if (before.purpose === "execution" && before.workItemId !== undefined) {
1410
+ const item = store.getWorkItem(input.taskId, before.workItemId);
1411
+ if (item !== null && ![
1412
+ "completed", "failed", "retired"
1413
+ ].includes(item.status)) {
1414
+ store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", now, summary));
1415
+ }
1416
+ }
1417
+ const role = store.getRole(input.taskId, input.roleName);
1418
+ if (role !== null) {
1419
+ store.saveRole(input.taskId, updateRoleStatus(role, input.roleName === "leader" ? "failed" : "idle", now));
1420
+ }
1421
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.claude-stop-failure", {
1422
+ eventId: input.eventId,
1423
+ runId: input.runId,
1424
+ roleName: input.roleName,
1425
+ outcome: terminal.status
1426
+ }, now));
1427
+ if (input.roleName !== "leader") {
1428
+ enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, before.purpose === "review" ? "review-failed" : "role-run-failed", now, [
1429
+ { type: "run", taskId: input.taskId, id: terminal.id },
1430
+ ...(terminal.workItemId === undefined
1431
+ ? []
1432
+ : [{ type: "work-item", taskId: input.taskId, id: terminal.workItemId }])
1433
+ ]);
1434
+ }
1435
+ else {
1436
+ store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, summary, now, store.getOperatorNotification(input.taskId)));
1437
+ enqueueWork(store, { kind: "operator" }, "leader-run-failed", now, [
1438
+ { type: "task", id: input.taskId },
1439
+ { type: "run", taskId: input.taskId, id: terminal.id }
1440
+ ]);
1441
+ }
1442
+ return { disposition: "applied", runId: input.runId };
1443
+ });
1444
+ }
1445
+ /**
1446
+ * Folds a provider SessionStart lifecycle fact
1447
+ * through the canonical contract. Session-started/ready never advance Run
1448
+ * acceptance; this only records generation facts and binds a discovered native
1449
+ * id under the exact launch fence. Idempotent replays are no-ops.
1450
+ */
1451
+ observeProviderSessionLifecycle(input, now = new Date()) {
1452
+ return this.store.transaction((store) => {
1453
+ const decision = this.foldProviderSignal(store, {
1454
+ kind: "native-session-start",
1455
+ ...(input.sessionSource === undefined ? {} : { sessionSource: input.sessionSource }),
1456
+ fence: providerFence(input)
1457
+ }, input.runId);
1458
+ switch (decision.kind) {
1459
+ case "obsolete":
1460
+ recordCanonicalObsolete(store, input, "native-session-lifecycle", decision.reason, now);
1461
+ return "obsolete";
1462
+ case "deferred":
1463
+ return "deferred";
1464
+ case "idempotent":
1465
+ return "applied";
1466
+ case "apply":
1467
+ if (decision.outcome.outcome === "mark-ready"
1468
+ && input.adapterId === "claude"
1469
+ && input.sessionSource === "startup"
1470
+ && store.getTaskRoleSessionSet(input.taskId, input.roleName)
1471
+ ?.sessions[input.agentId] === undefined) {
1472
+ // Claude emits startup readiness after native process creation but
1473
+ // before the Controller projects its preallocated Session. Keep
1474
+ // the immutable fact queued until that exact Session is durable.
1475
+ return "deferred";
1476
+ }
1477
+ if (decision.outcome.outcome === "bind-native-session") {
1478
+ const role = store.getRole(input.taskId, input.roleName);
1479
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1480
+ const run = input.runId === undefined
1481
+ ? null
1482
+ : store.getAgentRun(input.taskId, input.runId);
1483
+ if (role === null || sessions === null || run === null) {
1484
+ recordCanonicalObsolete(store, input, "native-session-lifecycle", "bind-state-missing", now);
1485
+ return "obsolete";
1486
+ }
1487
+ const bound = recordRoleAgentSession(sessions, {
1488
+ agentId: input.agentId,
1489
+ adapterId: input.adapterId,
1490
+ nativeSessionId: decision.outcome.nativeSessionId,
1491
+ launchId: input.launchId,
1492
+ policy: "fixed",
1493
+ status: "running",
1494
+ effective: run.effective
1495
+ }, now);
1496
+ store.saveTaskRoleSessionSet(bound);
1497
+ // Codex 0.145 emits SessionStart from inside run_turn(input), so a
1498
+ // runtime-discovered bind is durable evidence that this Run's
1499
+ // initial transport input already crossed the provider boundary.
1500
+ // Preserve that truth before clearing the launch reservation: a
1501
+ // restarted Controller must not allocate a second generation or
1502
+ // push the same first input again. UserPromptSubmit remains the
1503
+ // sole acceptance/delivered transition.
1504
+ if (input.adapterId === "codex" && run.pushedAt === undefined) {
1505
+ const pushed = markAgentRunPushed(run, now);
1506
+ store.saveAgentRun(pushed);
1507
+ markTaskRoleRunPushedInFlight(store, role, pushed, now);
1508
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "run.pushed", runLaunchEventPayload(pushed), now));
1509
+ }
1510
+ completeRuntimeHookReservation(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, input.launchId);
1511
+ }
1512
+ // Session-started/ready/bind facts are recorded provider-neutrally as
1513
+ // an auditable event. The
1514
+ // payload carries the generation fence + preInputReady so the fresh
1515
+ // push gate can read the exact provider-ready fact for this generation.
1516
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-session-lifecycle", {
1517
+ eventId: input.eventId,
1518
+ roleName: input.roleName,
1519
+ agentId: input.agentId,
1520
+ adapterId: input.adapterId,
1521
+ launchId: input.launchId,
1522
+ nativeSessionId: input.nativeSessionId,
1523
+ outcome: decision.outcome.outcome,
1524
+ preInputReady: (decision.outcome.outcome === "mark-ready"
1525
+ && decision.outcome.preInputReady) ? "true" : "false",
1526
+ ...(input.runId === undefined ? {} : { runId: input.runId })
1527
+ }, now));
1528
+ return "applied";
1529
+ }
1530
+ });
1531
+ }
1532
+ /**
1533
+ * Folds a provider UserPromptSubmit acceptance fact. This is the ONLY path
1534
+ * that may advance a Run to accepted/delivered, and only after an exact
1535
+ * transport receipt has independently committed run.pushed.
1536
+ */
1537
+ observeProviderPromptAccepted(input, now = new Date()) {
1538
+ return this.store.transaction((store) => {
1539
+ const decision = this.foldProviderSignal(store, {
1540
+ kind: "native-prompt-submit",
1541
+ fence: providerFence(input)
1542
+ }, input.runId);
1543
+ if (decision.kind === "obsolete") {
1544
+ recordCanonicalObsolete(store, input, "native-prompt-accepted", decision.reason, now);
1545
+ return "obsolete";
1546
+ }
1547
+ if (decision.kind === "deferred")
1548
+ return "deferred";
1549
+ if (decision.kind === "idempotent")
1550
+ return "applied";
1551
+ // decision.kind === "apply" && outcome advance-accepted
1552
+ const active = store.getActiveAgentRun(input.taskId, input.roleName);
1553
+ if (active === null || active.id !== input.runId || active.status !== "active") {
1554
+ recordCanonicalObsolete(store, input, "native-prompt-accepted", "run-not-active", now);
1555
+ return "obsolete";
1556
+ }
1557
+ const role = store.getRole(input.taskId, input.roleName);
1558
+ if (role === null) {
1559
+ recordCanonicalObsolete(store, input, "native-prompt-accepted", "role-missing", now);
1560
+ return "obsolete";
1561
+ }
1562
+ if (active.deliveredAt === undefined) {
1563
+ const delivered = markAgentRunDelivered(active, now);
1564
+ store.saveAgentRun(delivered);
1565
+ markTaskRoleRunDeliveredInFlight(store, role, delivered, now);
1566
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "run.delivered", runLaunchEventPayload(delivered), now));
1567
+ }
1568
+ return "applied";
1569
+ });
1570
+ }
1571
+ /**
1572
+ * Folds provider-native in-turn progress through the canonical lifecycle
1573
+ * contract. Progress is an advisory semantic fence only: it never advances
1574
+ * Run delivery/acceptance and never reads pane text or process counters.
1575
+ */
1576
+ observeProviderTurnProgress(input, now = new Date()) {
1577
+ return this.store.transaction((store) => {
1578
+ const receivedAtMs = Date.parse(input.receivedAt);
1579
+ if (!Number.isFinite(receivedAtMs) || receivedAtMs > now.getTime()) {
1580
+ recordCanonicalObsolete(store, input, "native-turn-progress", "received-at-invalid", now);
1581
+ return "obsolete";
1582
+ }
1583
+ const decision = this.foldProviderSignal(store, {
1584
+ kind: "native-turn-progress",
1585
+ ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
1586
+ fence: providerFence(input)
1587
+ }, input.runId);
1588
+ switch (decision.kind) {
1589
+ case "obsolete":
1590
+ recordCanonicalObsolete(store, input, "native-turn-progress", decision.reason, now);
1591
+ return "obsolete";
1592
+ case "deferred":
1593
+ return "deferred";
1594
+ case "idempotent":
1595
+ return "applied";
1596
+ case "apply":
1597
+ if (decision.outcome.outcome !== "advance-progress")
1598
+ return "obsolete";
1599
+ if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-turn-progress"
1600
+ && event.payload.eventId === input.eventId)))
1601
+ return "applied";
1602
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-turn-progress", {
1603
+ eventId: input.eventId,
1604
+ roleName: input.roleName,
1605
+ agentId: input.agentId,
1606
+ adapterId: input.adapterId,
1607
+ launchId: input.launchId,
1608
+ nativeSessionId: input.nativeSessionId,
1609
+ runId: input.runId,
1610
+ progressId: input.progressId,
1611
+ progressAt: input.receivedAt,
1612
+ ...(input.sequence === undefined ? {} : { sequence: String(input.sequence) })
1613
+ }, now));
1614
+ return "applied";
1615
+ }
1616
+ });
1617
+ }
1618
+ /**
1619
+ * Shared projection + canonical fold for provider-native lifecycle signals.
1620
+ * Reads current durable Run/session state into a CanonicalRunExpectation,
1621
+ * maps the neutral signal through the per-adapter registry, and folds. The
1622
+ * fold owns every ordering/identity/evidence decision; this method only reads
1623
+ * durable truth and never branches on the provider name.
1624
+ */
1625
+ foldProviderSignal(store, signal, runId) {
1626
+ let event;
1627
+ try {
1628
+ event = mapNativeLifecycleSignal(signal);
1629
+ }
1630
+ catch (error) {
1631
+ return { kind: "obsolete", reason: error instanceof Error ? error.message : "map-failed" };
1632
+ }
1633
+ const expectation = this.projectRunExpectation(store, signal.fence, runId);
1634
+ if (expectation === null) {
1635
+ if (preallocatedClaudeStartupAwaitingProjection(store, signal, runId)) {
1636
+ return { kind: "deferred", reason: "preallocated-session-not-yet-projected" };
1637
+ }
1638
+ return { kind: "obsolete", reason: "run-or-role-missing" };
1639
+ }
1640
+ const outcome = foldCanonicalLifecycleEvent(event, expectation);
1641
+ switch (outcome.outcome) {
1642
+ case "obsolete":
1643
+ return { kind: "obsolete", reason: outcome.reason };
1644
+ case "fail-closed":
1645
+ return { kind: "obsolete", reason: `fail-closed:${outcome.reason}` };
1646
+ case "deferred":
1647
+ return { kind: "deferred", reason: outcome.reason };
1648
+ case "idempotent":
1649
+ return { kind: "idempotent", reason: outcome.reason };
1650
+ default:
1651
+ return { kind: "apply", outcome };
1652
+ }
1653
+ }
1654
+ /** Reads durable Run + session state into the pure fold's expectation shape. */
1655
+ projectRunExpectation(store, fence, runId) {
1656
+ const role = store.getRole(fence.taskId, fence.roleName);
1657
+ if (role === null)
1658
+ return null;
1659
+ const sessionSet = store.getTaskRoleSessionSet(fence.taskId, fence.roleName);
1660
+ const session = sessionSet?.sessions[fence.agentId] ?? null;
1661
+ const boundNativeSessionId = session?.nativeSessionId;
1662
+ if (runId === undefined) {
1663
+ return {
1664
+ fence,
1665
+ sessionStarted: false,
1666
+ ready: false,
1667
+ pushed: false,
1668
+ accepted: false,
1669
+ terminal: false,
1670
+ ...(boundNativeSessionId === undefined ? {} : { boundNativeSessionId })
1671
+ };
1672
+ }
1673
+ const run = store.getAgentRun(fence.taskId, runId);
1674
+ const inFlight = sessionSet?.inFlight;
1675
+ if (run === null
1676
+ || inFlight === null
1677
+ || inFlight === undefined
1678
+ || inFlight.runId !== run.id
1679
+ || inFlight.agentId !== run.effective.agentId)
1680
+ return null;
1681
+ const owner = { scope: "task", taskId: fence.taskId, roleName: fence.roleName };
1682
+ const launchId = session?.launchId
1683
+ ?? (runtimeHookMatchesReservation(store, owner, fence.launchId) ? fence.launchId : undefined);
1684
+ if (launchId === undefined)
1685
+ return null;
1686
+ const expectedFence = {
1687
+ taskId: fence.taskId,
1688
+ roleName: fence.roleName,
1689
+ agentId: run.effective.agentId,
1690
+ adapterId: run.effective.adapterId,
1691
+ runId: run.id,
1692
+ launchId,
1693
+ receiptId: inFlight.receiptId,
1694
+ ...(boundNativeSessionId === undefined ? {} : { nativeSessionId: boundNativeSessionId })
1695
+ };
1696
+ const lifecycleEvents = store.listEvents(fence.taskId).filter((event) => (event.type === "runtime.provider-session-lifecycle"
1697
+ && event.payload.roleName === fence.roleName
1698
+ && event.payload.agentId === run.effective.agentId
1699
+ && event.payload.adapterId === run.effective.adapterId
1700
+ && event.payload.launchId === launchId
1701
+ && event.payload.nativeSessionId === (boundNativeSessionId ?? fence.nativeSessionId)));
1702
+ return {
1703
+ fence: expectedFence,
1704
+ sessionStarted: lifecycleEvents.length > 0,
1705
+ ready: lifecycleEvents.some((event) => event.payload.preInputReady === "true"),
1706
+ pushed: run.pushedAt !== undefined,
1707
+ accepted: run.deliveredAt !== undefined,
1708
+ terminal: run.status !== "active",
1709
+ ...(boundNativeSessionId === undefined ? {} : { boundNativeSessionId })
1710
+ };
1711
+ }
1712
+ observeObsoleteRuntimeEvent(input, now = new Date()) {
1713
+ this.store.transaction((store) => {
1714
+ if (store.getTask(input.taskId) === null)
1715
+ return;
1716
+ recordObsoleteRuntimeEvent(store, input, input.reason, now);
232
1717
  });
233
1718
  }
234
1719
  recordGlobalRuntimeNativeSession(input, now = new Date()) {
1720
+ return this.store.transaction((store) => (recordGlobalRuntimeNativeSession(store, input, now)));
1721
+ }
1722
+ observeGlobalRuntimeTurnCompleted(input, now = new Date()) {
235
1723
  return this.store.transaction((store) => {
236
1724
  const role = store.getGlobalRole(input.roleName);
237
1725
  if (role === null)
238
1726
  throw new Error(`Global Role not found: ${input.roleName}.`);
239
- const binding = activeRoleAgentBinding(role);
240
- if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
241
- throw new Error("Native session registration does not match the active global Role Agent binding.");
242
- }
243
- const current = store.getGlobalRoleSessionSet(input.roleName)
244
- ?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, role.activeAgentId, now);
1727
+ let current = store.getGlobalRoleSessionSet(input.roleName)
1728
+ ?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, input.agentId, now);
245
1729
  const existing = current.sessions[input.agentId];
246
- if (existing !== undefined && existing.nativeSessionId !== input.nativeSessionId) {
247
- throw new Error("Native session registration conflicts with the fixed global Role session.");
1730
+ const owner = {
1731
+ scope: "global",
1732
+ roleName: input.roleName
1733
+ };
1734
+ if (existing === undefined
1735
+ && !runtimeHookMatchesReservation(store, owner, input.launchId)) {
1736
+ throw new Error("Runtime turn completion does not match the global launch reservation.");
248
1737
  }
249
- if (existing?.status === "running")
250
- return existing;
251
- const updated = recordRoleAgentSession(current, {
1738
+ const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
1739
+ const effective = globalSessionEffective(role, effectiveExisting);
1740
+ if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
1741
+ throw new Error("Runtime turn completion does not match the effective global launch identity.");
1742
+ }
1743
+ const completedStatus = effectiveExisting?.status === "stopped"
1744
+ || effectiveExisting?.status === "broken"
1745
+ ? effectiveExisting.status
1746
+ : "ready";
1747
+ current = recordRoleAgentSession(current, {
252
1748
  agentId: input.agentId,
253
1749
  adapterId: input.adapterId,
254
1750
  nativeSessionId: input.nativeSessionId,
1751
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1752
+ title: effectiveExisting?.title ?? input.title,
1753
+ preview: effectiveExisting?.preview ?? (input.summary === undefined ? undefined : sessionPreview(input.summary)),
255
1754
  policy: "fixed",
256
- status: "running"
1755
+ status: completedStatus,
1756
+ effective
257
1757
  }, now);
258
- store.saveGlobalRoleSessionSet(updated);
259
- return updated.sessions[input.agentId];
1758
+ current = rememberRoleAgentCompletedTurn(current, input.agentId, input.nativeSessionId, input.turnId, now);
1759
+ store.saveGlobalRoleSessionSet(current);
1760
+ completeRuntimeHookReservation(store, owner, input.launchId);
1761
+ return current.sessions[input.agentId];
260
1762
  });
261
1763
  }
1764
+ classifyGlobalRuntimeTurnCompleted(input) {
1765
+ const role = this.store.getGlobalRole(input.roleName);
1766
+ if (role === null)
1767
+ return "obsolete";
1768
+ const existing = this.store.getGlobalRoleSessionSet(input.roleName)
1769
+ ?.sessions[input.agentId];
1770
+ const owner = {
1771
+ scope: "global",
1772
+ roleName: input.roleName
1773
+ };
1774
+ if (existing === undefined
1775
+ && !runtimeHookMatchesReservation(this.store, owner, input.launchId))
1776
+ return "obsolete";
1777
+ let effectiveExisting;
1778
+ try {
1779
+ effectiveExisting = nativeTransitionExisting(this.store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
1780
+ }
1781
+ catch {
1782
+ return "obsolete";
1783
+ }
1784
+ const effective = globalSessionEffective(role, effectiveExisting);
1785
+ if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
1786
+ return "obsolete";
1787
+ }
1788
+ return "apply";
1789
+ }
262
1790
  }
263
- function restorePendingWakeup(claimed, current) {
264
- if (current === null)
265
- return claimed;
1791
+ function claudeStopFailureSummary(input) {
1792
+ return [
1793
+ "Claude StopFailure.",
1794
+ `error: ${input.error}`,
1795
+ ...(input.errorDetails === undefined
1796
+ ? []
1797
+ : [`error_details: ${input.errorDetails}`]),
1798
+ ...(input.lastAssistantMessage === undefined
1799
+ ? []
1800
+ : [`last_assistant_message: ${input.lastAssistantMessage}`])
1801
+ ].join("\n");
1802
+ }
1803
+ function providerFence(input) {
266
1804
  return {
267
- schemaVersion: 1,
268
- taskId: claimed.taskId,
269
- reasons: [...new Set([...claimed.reasons, ...current.reasons])],
270
- requestCount: claimed.requestCount + current.requestCount,
271
- firstRequestedAt: claimed.firstRequestedAt,
272
- lastRequestedAt: current.lastRequestedAt
1805
+ taskId: input.taskId,
1806
+ roleName: input.roleName,
1807
+ agentId: input.agentId,
1808
+ adapterId: input.adapterId,
1809
+ launchId: input.launchId,
1810
+ nativeSessionId: input.nativeSessionId,
1811
+ ...(input.runId === undefined ? {} : { runId: input.runId }),
1812
+ ...(input.receiptId === undefined ? {} : { receiptId: input.receiptId })
273
1813
  };
274
1814
  }
275
- function mapRole(role) {
1815
+ function recordCanonicalObsolete(store, input, eventType, reason, now) {
1816
+ recordObsoleteRuntimeEvent(store, { ...input, eventType }, reason, now);
1817
+ }
1818
+ function recordObsoleteRuntimeEvent(store, input, reason, now) {
1819
+ if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.event-obsolete"
1820
+ && event.payload.eventId === input.eventId)))
1821
+ return;
1822
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.event-obsolete", {
1823
+ eventId: input.eventId,
1824
+ eventType: input.eventType ?? input.type ?? "unknown",
1825
+ roleName: input.roleName,
1826
+ agentId: input.agentId,
1827
+ nativeSessionId: input.nativeSessionId,
1828
+ ...(input.runId === undefined ? {} : { runId: input.runId }),
1829
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1830
+ reason
1831
+ }, now));
1832
+ }
1833
+ function isLeaderDisposedWorkItemRun(store, input) {
1834
+ if (input.runId === undefined)
1835
+ return false;
1836
+ const run = store.getAgentRun(input.taskId, input.runId);
1837
+ if (run === null
1838
+ || run.status !== "failed"
1839
+ || run.roleName !== input.roleName
1840
+ || run.effective.agentId !== input.agentId
1841
+ || run.workItemId === undefined) {
1842
+ return false;
1843
+ }
1844
+ return store.getWorkItem(input.taskId, run.workItemId)?.disposition !== undefined;
1845
+ }
1846
+ function isObsoleteTerminalRuntimeRun(store, input) {
1847
+ if (isLeaderDisposedWorkItemRun(store, input))
1848
+ return true;
1849
+ if (input.runId === undefined)
1850
+ return false;
1851
+ const run = store.getAgentRun(input.taskId, input.runId);
1852
+ if (run === null
1853
+ || run.status !== "failed"
1854
+ || run.roleName !== input.roleName
1855
+ || run.effective.agentId !== input.agentId) {
1856
+ return false;
1857
+ }
1858
+ return store.listEvents(input.taskId).some((event) => (event.type === "runtime.role-delivery-failed"
1859
+ && event.payload.runId === input.runId));
1860
+ }
1861
+ function sessionPreview(value) {
1862
+ const normalized = value.trim().replaceAll(/\s+/g, " ");
1863
+ const truncated = normalized.slice(0, 1_024);
1864
+ return /[\uD800-\uDBFF]$/.test(truncated)
1865
+ ? truncated.slice(0, -1)
1866
+ : truncated;
1867
+ }
1868
+ function boundedRuntimeNote(value) {
1869
+ const normalized = value.trim().replaceAll(/\s+/gu, " ");
1870
+ return normalized.length <= 280 ? normalized : `${normalized.slice(0, 279)}…`;
1871
+ }
1872
+ function requireRuntimeLaunchReservation(store, owner, launchId) {
1873
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
1874
+ if (!isRuntimeLaunchReservation(mailbox?.processing, launchId)) {
1875
+ throw new Error("Runtime launch reservation no longer matches the launch.");
1876
+ }
1877
+ return mailbox;
1878
+ }
1879
+ function runtimeHookMatchesReservation(store, owner, launchId) {
1880
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
1881
+ if (hasRuntimeCleanupObligation(mailbox))
1882
+ return false;
1883
+ const processing = mailbox?.processing;
1884
+ return launchId !== undefined
1885
+ && isRuntimeLaunchReservation(processing, launchId);
1886
+ }
1887
+ /**
1888
+ * Claude may publish SessionStart(startup) synchronously from host.start(),
1889
+ * before the Controller can atomically project the returned Session and bind
1890
+ * the Run. Preserve that exact immutable fact only while the matching launch
1891
+ * reservation is still the durable owner of the active Run. Everything else
1892
+ * remains obsolete/fail-closed.
1893
+ */
1894
+ function preallocatedClaudeStartupAwaitingProjection(store, signal, runId) {
1895
+ if (signal.kind !== "native-session-start"
1896
+ || signal.sessionSource !== "startup"
1897
+ || signal.fence.adapterId !== "claude"
1898
+ || runId === undefined
1899
+ || signal.fence.runId !== runId
1900
+ || signal.fence.launchId === undefined
1901
+ || signal.fence.nativeSessionId === undefined)
1902
+ return false;
1903
+ const task = store.getTask(signal.fence.taskId);
1904
+ const role = store.getRole(signal.fence.taskId, signal.fence.roleName);
1905
+ const run = store.getActiveAgentRun(signal.fence.taskId, signal.fence.roleName);
1906
+ const sessions = store.getTaskRoleSessionSet(signal.fence.taskId, signal.fence.roleName);
1907
+ if (task === null
1908
+ || task.status !== "active"
1909
+ || role === null
1910
+ || role.activeAgentId !== signal.fence.agentId
1911
+ || run === null
1912
+ || run.id !== runId
1913
+ || run.status !== "active"
1914
+ || run.effective.agentId !== signal.fence.agentId
1915
+ || run.effective.adapterId !== "claude"
1916
+ || (sessions?.inFlight !== null && sessions?.inFlight !== undefined)
1917
+ || sessions?.sessions[signal.fence.agentId] !== undefined)
1918
+ return false;
1919
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
1920
+ scope: "task",
1921
+ taskId: signal.fence.taskId,
1922
+ roleName: signal.fence.roleName
1923
+ }));
1924
+ const executionRef = mailbox?.processing?.executionRef;
1925
+ return isRuntimeLaunchReservation(mailbox?.processing, signal.fence.launchId)
1926
+ && !hasRuntimeCleanupObligation(mailbox)
1927
+ && executionRef?.type === "run"
1928
+ && executionRef.taskId === signal.fence.taskId
1929
+ && executionRef.id === runId
1930
+ && signal.fence.nativeSessionId === nativeSessionIdForLaunch(store.rootDirectory(), signal.fence.launchId, signal.fence.agentId, "claude");
1931
+ }
1932
+ function completeRuntimeHookReservation(store, owner, launchId) {
1933
+ if (launchId === undefined)
1934
+ return;
1935
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
1936
+ if (!isRuntimeLaunchReservation(mailbox?.processing, launchId))
1937
+ return;
1938
+ saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, launchId));
1939
+ }
1940
+ function saveRuntimeLifecycleMailbox(store, mailbox) {
1941
+ if (mailbox.processing === null && mailbox.pending === null) {
1942
+ store.removeWorkMailbox(mailbox.target);
1943
+ return;
1944
+ }
1945
+ store.saveWorkMailbox(mailbox);
1946
+ }
1947
+ function runtimeOwnerFromTarget(target) {
1948
+ return target.kind === "role-runtime"
1949
+ ? {
1950
+ scope: "task",
1951
+ taskId: target.taskId,
1952
+ roleName: target.roleName
1953
+ }
1954
+ : {
1955
+ scope: "global",
1956
+ roleName: target.roleName
1957
+ };
1958
+ }
1959
+ function sameRuntimeOwner(left, right) {
1960
+ return left.scope === "task"
1961
+ ? right.scope === "task"
1962
+ && left.taskId === right.taskId
1963
+ && left.roleName === right.roleName
1964
+ : right.scope === "global" && left.roleName === right.roleName;
1965
+ }
1966
+ function dormantRuntimeCandidateIsCurrent(store, candidate) {
1967
+ const { owner } = candidate;
1968
+ if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
1969
+ return false;
1970
+ }
1971
+ if (owner.scope === "task"
1972
+ && store.getActiveAgentRun(owner.taskId, owner.roleName) !== null) {
1973
+ return false;
1974
+ }
1975
+ const sessions = runtimeOwnerSessionSet(store, owner);
1976
+ const active = sessions?.sessions[sessions.activeAgentId];
1977
+ return active !== undefined
1978
+ && active.status !== "stopped"
1979
+ && active.agentId === candidate.agentId
1980
+ && active.adapterId === candidate.adapterId
1981
+ && active.nativeSessionId === candidate.nativeSessionId
1982
+ && active.launchId === candidate.launchId
1983
+ && active.updatedAt === candidate.sessionUpdatedAt;
1984
+ }
1985
+ function markRuntimeOwnerSessionStopped(store, owner, now) {
1986
+ if (owner.scope === "task") {
1987
+ const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
1988
+ if (sessions === null)
1989
+ return false;
1990
+ const active = sessions.sessions[sessions.activeAgentId];
1991
+ if (active === undefined || active.status === "stopped")
1992
+ return false;
1993
+ store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now));
1994
+ return true;
1995
+ }
1996
+ const sessions = store.getGlobalRoleSessionSet(owner.roleName);
1997
+ if (sessions === null)
1998
+ return false;
1999
+ const active = sessions.sessions[sessions.activeAgentId];
2000
+ if (active === undefined || active.status === "stopped")
2001
+ return false;
2002
+ store.saveGlobalRoleSessionSet(updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now));
2003
+ return true;
2004
+ }
2005
+ function runtimeOwnerSessionSet(store, owner) {
2006
+ return owner.scope === "task"
2007
+ ? store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
2008
+ : store.getGlobalRoleSessionSet(owner.roleName);
2009
+ }
2010
+ function runtimeSessionMatchesSettledLaunch(session, input) {
2011
+ return session !== undefined
2012
+ && session.agentId === input.agentId
2013
+ && session.adapterId === input.adapterId
2014
+ && (input.nativeSessionId === undefined
2015
+ || session.nativeSessionId === input.nativeSessionId);
2016
+ }
2017
+ function preparedSessionMatches(session, agentId, adapterId, nativeSessionId, launchId) {
2018
+ if (session === undefined)
2019
+ return nativeSessionId === undefined;
2020
+ if (session.agentId !== agentId
2021
+ || session.adapterId !== adapterId
2022
+ || session.status === "stopped"
2023
+ || session.status === "broken"
2024
+ || (nativeSessionId !== undefined
2025
+ && session.nativeSessionId !== nativeSessionId)) {
2026
+ return false;
2027
+ }
2028
+ return launchId === undefined
2029
+ ? session.launchId === undefined
2030
+ : session.launchId === launchId;
2031
+ }
2032
+ function preparedDeliveryFailureSessionFenceMatches(sessions, session, input) {
2033
+ if (sessions === null)
2034
+ return input.nativeSessionId === undefined;
2035
+ if (sessions.activeAgentId !== input.agentId)
2036
+ return false;
2037
+ if (sessions.inFlight === null) {
2038
+ return input.nativeSessionId === undefined
2039
+ && session === undefined
2040
+ && sessions.pendingTurnCompletion === null;
2041
+ }
2042
+ return sessions.inFlight.agentId === input.agentId
2043
+ && sessions.inFlight.runId === input.runId
2044
+ && sessions.inFlight.receiptId === formatAgentRunReceiptId(input.taskId, input.runId)
2045
+ && sessions.inFlight.pushedAt === undefined
2046
+ && preparedSessionMatches(session, input.agentId, input.adapterId, input.nativeSessionId, input.launchId);
2047
+ }
2048
+ function preparedReservationMatches(mailbox, taskId, runId, launchId) {
2049
+ if (launchId === undefined) {
2050
+ return !isRuntimeLaunchReservation(mailbox?.processing);
2051
+ }
2052
+ const processing = mailbox?.processing;
2053
+ return isRuntimeLaunchReservation(processing, launchId)
2054
+ && !hasRuntimeCleanupObligation(mailbox)
2055
+ && processing?.executionRef?.type === "run"
2056
+ && processing.executionRef.taskId === taskId
2057
+ && processing.executionRef.id === runId;
2058
+ }
2059
+ function matchingPreparedRuntimeReservation(store, input) {
2060
+ if (input.launchId === undefined
2061
+ || input.session?.nativeSessionId === undefined) {
2062
+ return null;
2063
+ }
2064
+ const owner = {
2065
+ scope: "task",
2066
+ taskId: input.task.id,
2067
+ roleName: input.role.name
2068
+ };
2069
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
2070
+ if (isRuntimeLaunchReservation(mailbox?.processing, input.launchId)
2071
+ && mailbox?.processing?.executionRef?.type === "run"
2072
+ && mailbox.processing.executionRef.id === input.run.id
2073
+ && !hasRuntimeCleanupObligation(mailbox)) {
2074
+ return mailbox;
2075
+ }
2076
+ throw new Error("Prepared Role session does not match its launch generation.");
2077
+ }
2078
+ function completePreparedRuntimeReservation(store, mailbox) {
2079
+ if (mailbox === null || mailbox.processing === null)
2080
+ return;
2081
+ saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, mailbox.processing.batchId));
2082
+ }
2083
+ function recordTaskRuntimeNativeSession(store, input, now) {
2084
+ const task = store.getTask(input.taskId);
2085
+ if (task === null)
2086
+ throw new Error(`Task not found: ${input.taskId}.`);
2087
+ if (task.status === "archived") {
2088
+ throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
2089
+ }
2090
+ if (task.status !== "active") {
2091
+ throw new Error(`Cannot register a native session for a Task that is not active: ${input.taskId}.`);
2092
+ }
2093
+ const role = requireRole(store, input.taskId, input.roleName);
2094
+ const current = store.getRoleSessionSet(input.taskId, input.roleName)
2095
+ ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
2096
+ const existing = current.sessions[input.agentId];
2097
+ const effectiveExisting = nativeTransitionExisting(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, existing, input.nativeSessionId, input.launchId, "Native session registration conflicts with the fixed Role session.");
2098
+ if (existing?.status === "running"
2099
+ && (input.launchId === undefined || existing.launchId === input.launchId))
2100
+ return existing;
2101
+ const resolvedEffective = taskSessionEffective(store, input.taskId, input.roleName, input.agentId, effectiveExisting);
2102
+ const effective = input.effective === undefined
2103
+ ? resolvedEffective
2104
+ : validateEffectiveLaunchSnapshot(input.effective);
2105
+ if (!effectiveLaunchSnapshotsCompatible(resolvedEffective, effective)) {
2106
+ throw new Error("Reserved native Session effective launch changed before persistence.");
2107
+ }
2108
+ if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
2109
+ throw new Error("Native session registration does not match the effective launch identity.");
2110
+ }
2111
+ const updated = recordRoleAgentSession(current, {
2112
+ agentId: input.agentId,
2113
+ adapterId: input.adapterId,
2114
+ nativeSessionId: input.nativeSessionId,
2115
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
2116
+ policy: "fixed",
2117
+ status: "running",
2118
+ effective
2119
+ }, now);
2120
+ store.saveRoleSessionSet(updated);
2121
+ return updated.sessions[input.agentId];
2122
+ }
2123
+ function recordGlobalRuntimeNativeSession(store, input, now) {
2124
+ const role = store.getGlobalRole(input.roleName);
2125
+ if (role === null)
2126
+ throw new Error(`Global Role not found: ${input.roleName}.`);
2127
+ const current = store.getGlobalRoleSessionSet(input.roleName)
2128
+ ?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, input.agentId, now);
2129
+ const existing = current.sessions[input.agentId];
2130
+ const effectiveExisting = nativeTransitionExisting(store, { scope: "global", roleName: input.roleName }, existing, input.nativeSessionId, input.launchId, "Native session registration conflicts with the fixed global Role session.");
2131
+ if (existing?.status === "running"
2132
+ && (input.launchId === undefined || existing.launchId === input.launchId))
2133
+ return existing;
2134
+ const resolvedEffective = globalSessionEffective(role, effectiveExisting);
2135
+ const effective = input.effective === undefined
2136
+ ? resolvedEffective
2137
+ : validateEffectiveLaunchSnapshot(input.effective);
2138
+ if (!effectiveLaunchSnapshotsCompatible(resolvedEffective, effective)) {
2139
+ throw new Error("Reserved global native Session effective launch changed before persistence.");
2140
+ }
2141
+ if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
2142
+ throw new Error("Native session registration does not match the effective global launch identity.");
2143
+ }
2144
+ const updated = recordRoleAgentSession(current, {
2145
+ agentId: input.agentId,
2146
+ adapterId: input.adapterId,
2147
+ nativeSessionId: input.nativeSessionId,
2148
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
2149
+ policy: "fixed",
2150
+ status: "running",
2151
+ effective
2152
+ }, now);
2153
+ store.saveGlobalRoleSessionSet(updated);
2154
+ return updated.sessions[input.agentId];
2155
+ }
2156
+ function nativeTransitionExisting(store, owner, existing, nativeSessionId, launchId, conflictMessage) {
2157
+ if (existing === undefined || existing.nativeSessionId === nativeSessionId) {
2158
+ return existing;
2159
+ }
2160
+ if ((existing.status === "stopped" || existing.status === "broken")
2161
+ && runtimeHookMatchesReservation(store, owner, launchId)) {
2162
+ return undefined;
2163
+ }
2164
+ throw new Error(conflictMessage);
2165
+ }
2166
+ function mapRole(store, role) {
276
2167
  const binding = activeRoleAgentBinding(role);
2168
+ const item = store.listWorkItems(role.taskId).find((candidate) => (candidate.assignee === role.name
2169
+ && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
2170
+ const workspace = (item === null
2171
+ ? store.getTaskWorkspace(role.taskId)
2172
+ : store.getWorkItemWorkspace(role.taskId, item.id))
2173
+ ?? store.getTaskWorkspace(role.taskId)
2174
+ ?? undefined;
2175
+ const reopened = role.name === "leader"
2176
+ && store.getPendingWakeup(role.taskId)?.reasons.includes("task-reopened") === true;
2177
+ const effective = reopened
2178
+ ? resolveEffectiveLaunch({
2179
+ role,
2180
+ purpose: "execution",
2181
+ ...(workspace === undefined ? {} : { workspace })
2182
+ })
2183
+ : activeLiveRoleAgentSession(store.getTaskRoleSessionSet(role.taskId, role.name))?.effective ?? resolveEffectiveLaunch({
2184
+ role,
2185
+ purpose: "execution",
2186
+ ...(workspace === undefined ? {} : { workspace })
2187
+ });
277
2188
  return {
278
2189
  taskId: role.taskId,
279
2190
  name: role.name,
280
2191
  activeAgentId: role.activeAgentId,
281
2192
  adapterId: binding.adapterId,
2193
+ ...(binding.config.model === undefined ? {} : { model: binding.config.model }),
2194
+ ...(binding.config.effort === undefined ? {} : { effort: binding.config.effort }),
2195
+ effective,
2196
+ workspace: role.workspace,
2197
+ ...(workspace === undefined ? {} : { managedWorkspace: workspace }),
282
2198
  status: role.status
283
2199
  };
284
2200
  }
2201
+ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
2202
+ const active = store.getActiveAgentRun(taskId, roleName);
2203
+ if (active !== null) {
2204
+ if (active.effective.agentId !== agentId) {
2205
+ throw new Error(`Native Session registration does not match the effective Run Agent: ${taskId}/${roleName}.`);
2206
+ }
2207
+ return active.effective;
2208
+ }
2209
+ if (existing !== undefined)
2210
+ return existing.effective;
2211
+ const role = store.getRole(taskId, roleName);
2212
+ if (role === null)
2213
+ throw new Error(`Role not found: ${taskId}/${roleName}.`);
2214
+ const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
2215
+ && !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
2216
+ const workspace = (item === null
2217
+ ? store.getTaskWorkspace(taskId)
2218
+ : store.getWorkItemWorkspace(taskId, item.id))
2219
+ ?? store.getTaskWorkspace(taskId)
2220
+ ?? undefined;
2221
+ const effective = resolveEffectiveLaunch({
2222
+ role,
2223
+ purpose: "execution",
2224
+ ...(workspace === undefined ? {} : { workspace }),
2225
+ ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
2226
+ });
2227
+ if (effective.agentId !== agentId) {
2228
+ throw new Error(`Native Session registration does not match Role desired Agent: ${agentId}.`);
2229
+ }
2230
+ return effective;
2231
+ }
2232
+ function globalSessionEffective(role, existing) {
2233
+ return existing?.effective ?? resolveEffectiveLaunch({ role, purpose: "execution" });
2234
+ }
285
2235
  function mapSession(session) {
286
2236
  return {
287
2237
  agentId: session.agentId,
288
2238
  adapterId: session.adapterId,
289
2239
  nativeSessionId: session.nativeSessionId,
290
- status: session.status
2240
+ ...(session.launchId === undefined ? {} : { launchId: session.launchId }),
2241
+ status: session.status,
2242
+ effective: session.effective,
2243
+ updatedAt: session.updatedAt
291
2244
  };
292
2245
  }
293
- function saveTaskSession(store, role, session, status, now) {
2246
+ function saveTaskSession(store, role, session, status, now, launchId) {
294
2247
  const current = store.getRoleSessionSet(role.taskId, role.name)
295
- ?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, role.activeAgentId, now);
2248
+ ?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, session.agentId, now);
296
2249
  const updated = recordRoleAgentSession(current, {
297
2250
  agentId: session.agentId,
298
2251
  adapterId: session.adapterId,
299
2252
  nativeSessionId: session.nativeSessionId,
2253
+ ...(launchId === undefined ? {} : { launchId }),
300
2254
  policy: "fixed",
301
- status
2255
+ status,
2256
+ effective: session.effective
2257
+ }, now);
2258
+ store.saveRoleSessionSet(updated);
2259
+ }
2260
+ function bindTaskRoleRunInFlight(store, role, run, now) {
2261
+ const agentId = run.effective.agentId;
2262
+ let current = store.getRoleSessionSet(role.taskId, role.name)
2263
+ ?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, agentId, now);
2264
+ if (current.activeAgentId !== agentId) {
2265
+ if (current.inFlight !== null || current.pendingTurnCompletion !== null) {
2266
+ throw new Error("Task Role Session identity cannot change with an unsettled Run fence.");
2267
+ }
2268
+ const liveConflict = Object.values(current.sessions).find((session) => (session.agentId !== agentId
2269
+ && session.status !== "stopped"
2270
+ && session.status !== "broken"));
2271
+ if (liveConflict !== undefined) {
2272
+ throw new Error(`Task Role Session identity cannot change while ${liveConflict.agentId} is live.`);
2273
+ }
2274
+ current = {
2275
+ ...current,
2276
+ activeAgentId: agentId,
2277
+ updatedAt: now.toISOString()
2278
+ };
2279
+ }
2280
+ const updated = bindTaskRoleRun(current, {
2281
+ agentId,
2282
+ runId: run.id,
2283
+ receiptId: formatAgentRunReceiptId(role.taskId, run.id)
2284
+ }, now);
2285
+ store.saveRoleSessionSet(updated);
2286
+ }
2287
+ function markTaskRoleRunPushedInFlight(store, role, run, now) {
2288
+ const current = store.getRoleSessionSet(role.taskId, role.name);
2289
+ if (current === null) {
2290
+ throw new Error(`Task Role session set is missing for pushed Run: ${run.id}.`);
2291
+ }
2292
+ const updated = markTaskRoleRunPushed(current, {
2293
+ agentId: run.effective.agentId,
2294
+ runId: run.id,
2295
+ receiptId: formatAgentRunReceiptId(role.taskId, run.id)
2296
+ }, now);
2297
+ store.saveRoleSessionSet(updated);
2298
+ }
2299
+ function markTaskRoleRunDeliveredInFlight(store, role, run, now) {
2300
+ const current = store.getRoleSessionSet(role.taskId, role.name);
2301
+ if (current === null) {
2302
+ throw new Error(`Task Role session set is missing for delivered Run: ${run.id}.`);
2303
+ }
2304
+ const updated = markTaskRoleRunDelivered(current, {
2305
+ agentId: run.effective.agentId,
2306
+ runId: run.id,
2307
+ receiptId: formatAgentRunReceiptId(role.taskId, run.id)
2308
+ }, now);
2309
+ store.saveRoleSessionSet(updated);
2310
+ }
2311
+ function clearTaskRoleRunInFlight(store, role, run, now) {
2312
+ const current = store.getRoleSessionSet(role.taskId, role.name);
2313
+ if (current === null)
2314
+ return;
2315
+ if (current.inFlight?.runId !== run.id)
2316
+ return;
2317
+ const updated = clearTaskRoleRun(current, {
2318
+ agentId: run.effective.agentId,
2319
+ runId: run.id,
2320
+ receiptId: formatAgentRunReceiptId(role.taskId, run.id)
302
2321
  }, now);
303
2322
  store.saveRoleSessionSet(updated);
304
2323
  }
@@ -314,9 +2333,40 @@ function updateTaskSessionStatusIfPresent(store, taskId, roleName, agentId, stat
314
2333
  return;
315
2334
  store.saveRoleSessionSet(updateRoleAgentSessionStatus(set, agentId, status, now));
316
2335
  }
2336
+ function schedulerRoleSessionsMatch(current, expected) {
2337
+ if (current === null || expected === null)
2338
+ return current === expected;
2339
+ return current.agentId === expected.agentId
2340
+ && current.adapterId === expected.adapterId
2341
+ && current.nativeSessionId === expected.nativeSessionId
2342
+ && current.launchId === expected.launchId
2343
+ && isDeepStrictEqual(current.effective, expected.effective);
2344
+ }
2345
+ function matchesStallSessionFence(current, expected) {
2346
+ if (current === null || expected === null)
2347
+ return current === expected;
2348
+ return current.agentId === expected.agentId
2349
+ && current.adapterId === expected.adapterId
2350
+ && current.nativeSessionId === expected.nativeSessionId
2351
+ && current.launchId === expected.launchId
2352
+ && current.status === expected.status;
2353
+ }
317
2354
  function requireRole(store, taskId, roleName) {
318
2355
  const role = store.getRole(taskId, roleName);
319
2356
  if (role === null)
320
2357
  throw new Error(`Role not found: ${taskId}/${roleName}.`);
321
2358
  return role;
322
2359
  }
2360
+ function runLaunchEventPayload(run) {
2361
+ return {
2362
+ runId: run.id,
2363
+ role: run.roleName,
2364
+ purpose: run.purpose,
2365
+ mode: run.mode,
2366
+ agent: `${run.effective.agentId}/${run.effective.adapterId}`,
2367
+ effectiveRevision: String(run.effective.sourceDesiredRevision),
2368
+ profileAccess: run.effective.profileAccess,
2369
+ effectivePermission: run.effective.permission.strategy,
2370
+ writeProjectIds: run.effective.writeProjectIds.join(",") || "none"
2371
+ };
2372
+ }