@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
@@ -0,0 +1,572 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { completeProcessing } from "../coordination/workMailbox.js";
3
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
4
+ import { settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
5
+ import { terminalizeTaskRoleRunSession } from "../executor/agentExecutor.js";
6
+ import { updateRoleStatus } from "../role/role.js";
7
+ import { createTaskEvent } from "../event/taskEvent.js";
8
+ import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
9
+ import { failAgentRun, yieldAgentRun } from "../run/agentRun.js";
10
+ import { recordExecutionLaneResult } from "../execution/executionGroup.js";
11
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
12
+ import { isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
13
+ import { clearMatchingLeaderStallAttention, latestRunDurableProgressAt, RUN_RECOVERY_APPLIED_EVENT, RUN_RECOVERY_REQUESTED_EVENT } from "../scheduler/roleRunStall.js";
14
+ import { workItemExecutionGroupById, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
15
+ /**
16
+ * Validate every immutable identity and frozen Project head needed before a
17
+ * review Run can settle any mailbox or Round state. This is deliberately
18
+ * read-only: callers use it as the compare-and-swap fence immediately before
19
+ * their aggregate mutation.
20
+ */
21
+ export function validateExactRunReviewRound(store, run, options = {}) {
22
+ if (run.purpose !== "review")
23
+ return { disposition: "applied", round: null };
24
+ if (run.reviewRoundId === undefined) {
25
+ return { disposition: "obsolete", round: null, reason: "review-round-missing" };
26
+ }
27
+ const round = store.getReviewRound(run.taskId, run.reviewRoundId);
28
+ if (round === null) {
29
+ return { disposition: "obsolete", round: null, reason: "review-round-missing" };
30
+ }
31
+ if (!options.allowTerminal
32
+ && round.status !== "pending" && round.status !== "running") {
33
+ return { disposition: "obsolete", round, reason: "review-round-terminal" };
34
+ }
35
+ const lane = round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
36
+ const exactReviewerRun = round.reviewerRunId === run.id || lane?.runId === run.id;
37
+ const exactReviewerRole = round.reviewerRoleName === run.roleName || lane?.roleName === run.roleName;
38
+ if (!exactReviewerRun
39
+ || !exactReviewerRole
40
+ || round.workItemId !== run.workItemId
41
+ || round.reviewBaseCommit !== run.effective.reviewBaseCommit) {
42
+ return { disposition: "obsolete", round, reason: "review-round-mismatch" };
43
+ }
44
+ const laneWorkspaceRoot = lane?.workspace?.root;
45
+ if (run.workspace === undefined || round.workspace === undefined
46
+ || (laneWorkspaceRoot === undefined && !isDeepStrictEqual(run.workspace, round.workspace))
47
+ || (laneWorkspaceRoot !== undefined && run.workspace.root !== laneWorkspaceRoot)) {
48
+ return { disposition: "obsolete", round, reason: "review-workspace-mismatch" };
49
+ }
50
+ const storedWorkspace = run.workspace.owner.type === "execution-lane"
51
+ ? store.getManagedWorkspace(run.workspace.owner)
52
+ : store.getReviewRoundWorkspace(run.taskId, round.id);
53
+ if (storedWorkspace === null
54
+ || (run.workspace.owner.type !== "execution-lane"
55
+ && !isDeepStrictEqual(storedWorkspace, round.workspace))
56
+ || !isDeepStrictEqual(storedWorkspace, run.workspace)) {
57
+ return { disposition: "obsolete", round, reason: "review-workspace-drift" };
58
+ }
59
+ if (run.workspace.owner.type !== "execution-lane") {
60
+ if (storedWorkspace.owner.type !== "review-round"
61
+ || storedWorkspace.owner.taskId !== run.taskId
62
+ || storedWorkspace.owner.reviewRoundId !== round.id) {
63
+ return { disposition: "obsolete", round, reason: "review-workspace-owner-mismatch" };
64
+ }
65
+ }
66
+ if (run.workspace.owner.type === "execution-lane"
67
+ && (run.workspace.owner.purpose !== "review"
68
+ || run.workspace.owner.executionGroupId !== run.executionGroupId
69
+ || run.workspace.owner.executionLaneId !== run.executionLaneId
70
+ || run.workspace.owner.reviewRoundId !== round.id)) {
71
+ return { disposition: "obsolete", round, reason: "review-lane-workspace-owner-mismatch" };
72
+ }
73
+ if (run.workspace.owner.type === "execution-lane") {
74
+ if (lane === undefined
75
+ || lane.runId !== run.id
76
+ || lane.roleName !== run.roleName
77
+ || lane.workspace?.root !== run.workspace.root
78
+ || lane.workspace.writableProjectIds.length !== run.workspace.entries.length
79
+ || run.workspace.entries.some((entry) => (entry.access !== "write"
80
+ || !lane.workspace.writableProjectIds.includes(entry.projectId)))) {
81
+ return { disposition: "obsolete", round, reason: "review-lane-workspace-lineage-mismatch" };
82
+ }
83
+ }
84
+ const item = store.getWorkItem(run.taskId, round.workItemId);
85
+ if (item === null) {
86
+ return { disposition: "obsolete", round, reason: "review-work-item-missing" };
87
+ }
88
+ const candidate = item.candidates.find(({ id }) => id === round.candidateId);
89
+ if (candidate === undefined) {
90
+ return { disposition: "obsolete", round, reason: "review-candidate-missing" };
91
+ }
92
+ const task = store.getTask(run.taskId);
93
+ const taskScope = (round.scope ?? "work-item") === "task";
94
+ const frozenProjects = taskScope
95
+ ? round.taskCandidate?.projects
96
+ : candidate.gitSnapshot?.projects;
97
+ if (taskScope) {
98
+ if (task === null || round.taskCandidate === undefined) {
99
+ return { disposition: "obsolete", round, reason: "review-task-candidate-missing" };
100
+ }
101
+ if (frozenProjects === undefined
102
+ || new Set(frozenProjects.map(({ projectId }) => projectId)).size
103
+ !== task.projectBindings.length
104
+ || task.projectBindings.some(({ projectId }) => (!frozenProjects.some((project) => project.projectId === projectId)))) {
105
+ return { disposition: "obsolete", round, reason: "review-frozen-project-scope-drift" };
106
+ }
107
+ }
108
+ else if (candidate.gitSnapshot !== undefined
109
+ && candidate.gitSnapshot.reviewBaseCommit !== round.reviewBaseCommit) {
110
+ return { disposition: "obsolete", round, reason: "review-candidate-snapshot-drift" };
111
+ }
112
+ if (!taskScope && frozenProjects === undefined) {
113
+ return { disposition: "applied", round };
114
+ }
115
+ if (frozenProjects === undefined
116
+ || storedWorkspace.entries.length !== frozenProjects.length) {
117
+ return { disposition: "obsolete", round, reason: "review-frozen-project-scope-drift" };
118
+ }
119
+ const frozenByProject = new Map(frozenProjects.map(({ projectId, commit }) => [projectId, commit]));
120
+ if (storedWorkspace.entries.some((entry) => (entry.access !== "write"
121
+ || frozenByProject.get(entry.projectId) !== entry.baseCommit
122
+ || entry.baseRef !== entry.baseCommit))) {
123
+ return { disposition: "obsolete", round, reason: "review-frozen-head-drift" };
124
+ }
125
+ return { disposition: "applied", round };
126
+ }
127
+ /**
128
+ * Atomically terminalizes the exact ReviewRound bound to a review AgentRun.
129
+ * The round must still be pending/running and its reviewer identity must match
130
+ * the Run exactly. This is the sole review-round convergence primitive shared
131
+ * by the exact Run terminalization path and the pre-delivery launch-failure
132
+ * path: a failed review Run must never leave its Round stranded.
133
+ */
134
+ export function terminalizeExactRunReviewRound(store, input, now) {
135
+ const validation = validateExactRunReviewRound(store, input.run);
136
+ if (validation.disposition !== "applied" || validation.round === null) {
137
+ return validation;
138
+ }
139
+ const reviewRound = validation.round;
140
+ const groupedRound = input.run.executionGroupId !== undefined
141
+ && input.run.executionLaneId !== undefined
142
+ && reviewRound.executionGroup !== undefined
143
+ ? updateReviewExecutionGroup(reviewRound, recordExecutionLaneResult(reviewRound.executionGroup, input.run.executionLaneId, {
144
+ summary: input.outcome.summary,
145
+ ...(input.reviewResult?.report === undefined ? {} : { report: input.reviewResult.report }),
146
+ ...(input.reviewResult?.checks === undefined
147
+ ? {}
148
+ : {
149
+ checks: input.reviewResult.checks.map(({ name, outcome, details }) => ({
150
+ name,
151
+ outcome,
152
+ ...(details === undefined ? {} : { details })
153
+ }))
154
+ }),
155
+ ...(input.reviewResult?.findings === undefined
156
+ ? {}
157
+ : { findings: input.reviewResult.findings }),
158
+ ...(input.reviewResult?.evidence === undefined
159
+ ? {}
160
+ : { evidence: input.reviewResult.evidence }),
161
+ ...(input.reviewResult?.evidenceCommit === undefined
162
+ ? {}
163
+ : { evidenceCommit: input.reviewResult.evidenceCommit }),
164
+ ...(input.reviewResult?.gitSnapshot === undefined
165
+ ? {}
166
+ : { gitSnapshot: input.reviewResult.gitSnapshot })
167
+ }, input.outcome.status === "yielded" ? "completed" : "failed", now))
168
+ : reviewRound;
169
+ const groupedMultiLane = groupedRound.executionGroup !== undefined
170
+ && (groupedRound.executionGroup.lanes.length > 1
171
+ || groupedRound.executionGroup.strategy.mode === "adaptive");
172
+ if (groupedMultiLane && groupedRound.executionGroup !== undefined) {
173
+ // A panel Lane only contributes evidence. The Leader must see every
174
+ // terminal Lane and explicitly resolve the Group before this ReviewRound
175
+ // can become terminal.
176
+ store.saveReviewRound(input.taskId, groupedRound);
177
+ return { disposition: "applied", round: groupedRound };
178
+ }
179
+ const terminal = finishReviewRound(groupedRound, input.outcome.status === "yielded" ? "completed" : "failed", input.outcome.summary, now, input.reviewResult);
180
+ store.saveReviewRound(input.taskId, terminal);
181
+ return { disposition: "applied", round: terminal };
182
+ }
183
+ /**
184
+ * Applies one exact application-level terminal fact inside the caller's
185
+ * FileTaskStore transaction. All caller-owned outcome records can therefore
186
+ * be saved in the same aggregate commit.
187
+ */
188
+ export function terminalizeExactTaskRun(store, input, now) {
189
+ const run = store.getAgentRun(input.taskId, input.runId);
190
+ if (run === null)
191
+ return obsolete(null, "run-missing");
192
+ if (run.status !== "active")
193
+ return obsolete(run, "run-terminal");
194
+ if (run.taskId !== input.taskId || run.roleName !== input.roleName) {
195
+ return obsolete(run, "run-owner-mismatch");
196
+ }
197
+ if (run.effective.agentId !== input.agentId) {
198
+ return obsolete(run, "run-agent-mismatch");
199
+ }
200
+ const role = store.getRole(input.taskId, input.roleName);
201
+ if (role === null)
202
+ return obsolete(run, "role-missing");
203
+ const active = run.executionGroupId !== undefined && run.executionLaneId !== undefined
204
+ ? store.getActiveExecutionLaneRun(input.taskId, run.executionGroupId, run.executionLaneId)
205
+ : store.getActiveAgentRun(input.taskId, input.roleName);
206
+ if (active?.id !== run.id)
207
+ return obsolete(run, "active-run-mismatch");
208
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
209
+ if (!matchesSessionFence(sessions, input)) {
210
+ return obsolete(run, "session-fence-mismatch");
211
+ }
212
+ if (!matchesLaunchFence(store, sessions, input)) {
213
+ return obsolete(run, "launch-fence-mismatch");
214
+ }
215
+ // Validate the exact ReviewRound, Candidate, stored workspace, and frozen
216
+ // Project heads before any mailbox or Round write.
217
+ const reviewValidation = validateExactRunReviewRound(store, run);
218
+ if (reviewValidation.disposition !== "applied") {
219
+ return obsolete(run, reviewValidation.reason ?? "review-round-mismatch");
220
+ }
221
+ const roleTarget = {
222
+ kind: "role",
223
+ taskId: input.taskId,
224
+ roleName: input.roleName
225
+ };
226
+ if (input.mailboxDisposition === "discard") {
227
+ store.removeWorkMailbox(roleTarget);
228
+ }
229
+ else {
230
+ // For review Runs the mailbox must carry an exact pending or processing
231
+ // entry for this Run: an absent or unrelated mailbox means the review
232
+ // dispatch was never recorded and the terminalization must fail closed.
233
+ // Execution/Leader Runs keep the historical absent-is-clean behavior so
234
+ // direct Leader completion and normal execution are unaffected.
235
+ const isReview = run.purpose === "review";
236
+ const mailbox = store.getWorkMailbox(roleTarget);
237
+ const processing = mailbox?.processing;
238
+ if (mailbox !== null && processing !== null && processing !== undefined) {
239
+ if (processing.executionRef === undefined
240
+ || processing.executionRef.type !== "run"
241
+ || processing.executionRef.taskId !== run.taskId
242
+ || processing.executionRef.id !== run.id) {
243
+ return obsolete(run, "mailbox-busy");
244
+ }
245
+ store.saveWorkMailbox(completeProcessing(mailbox, processing.batchId));
246
+ }
247
+ else {
248
+ const mailboxSettlement = settleExactWorkExecution(store, roleTarget, { type: "run", taskId: run.taskId, id: run.id });
249
+ if (mailboxSettlement === "absent" && isReview) {
250
+ return obsolete(run, "review-mailbox-missing");
251
+ }
252
+ }
253
+ }
254
+ // All Run, active-pointer, Session, launch, and mailbox fences have passed.
255
+ // Only now may the exact ReviewRound be terminalized alongside the Run so a
256
+ // stale fence never leaves a Round written while the Run stays active.
257
+ const reviewRoundTerminalization = terminalizeExactRunReviewRound(store, {
258
+ taskId: input.taskId,
259
+ run,
260
+ outcome: input.outcome,
261
+ reviewResult: input.reviewResult
262
+ }, now);
263
+ if (reviewRoundTerminalization.disposition !== "applied") {
264
+ return obsolete(run, reviewRoundTerminalization.reason ?? "review-round-mismatch");
265
+ }
266
+ const terminal = input.outcome.status === "yielded"
267
+ ? yieldAgentRun(run, input.outcome.summary, now)
268
+ : failAgentRun(run, input.outcome.summary, now);
269
+ if (run.executionGroupId !== undefined
270
+ && run.executionLaneId !== undefined
271
+ && run.purpose === "execution"
272
+ && run.workItemId !== undefined) {
273
+ const item = store.getWorkItem(input.taskId, run.workItemId);
274
+ const group = item === null
275
+ ? undefined
276
+ : workItemExecutionGroupById(item, run.executionGroupId);
277
+ if (item !== null && group !== undefined) {
278
+ const grouped = recordExecutionLaneResult(group, run.executionLaneId, {
279
+ summary: input.outcome.summary,
280
+ ...(input.reviewResult?.report === undefined ? {} : { report: input.reviewResult.report }),
281
+ ...(input.reviewResult?.checks === undefined ? {} : { checks: input.reviewResult.checks }),
282
+ ...(input.reviewResult?.findings === undefined ? {} : { findings: input.reviewResult.findings }),
283
+ ...(input.reviewResult?.evidence === undefined ? {} : { evidence: input.reviewResult.evidence }),
284
+ ...(input.reviewResult?.evidenceCommit === undefined ? {} : { evidenceCommit: input.reviewResult.evidenceCommit }),
285
+ ...(input.reviewResult?.gitSnapshot === undefined ? {} : { gitSnapshot: input.reviewResult.gitSnapshot })
286
+ }, input.outcome.status === "yielded" ? "completed" : "failed", now);
287
+ store.saveWorkItem(input.taskId, updateWorkItemExecutionGroup(item, grouped, now));
288
+ }
289
+ }
290
+ store.saveAgentRun(terminal);
291
+ if (terminal.executionGroupId !== undefined && terminal.executionLaneId !== undefined) {
292
+ store.clearActiveExecutionLaneRun(input.taskId, terminal.executionGroupId, terminal.executionLaneId);
293
+ }
294
+ else {
295
+ store.clearActiveAgentRun(input.taskId, input.roleName);
296
+ }
297
+ store.saveRole(input.taskId, updateRoleStatus(role, "idle", now));
298
+ if (sessions !== null) {
299
+ store.saveTaskRoleSessionSet(terminalizeTaskRoleRunSession(sessions, {
300
+ agentId: input.agentId,
301
+ runId: input.runId,
302
+ receiptId: input.receiptId
303
+ }, now));
304
+ }
305
+ settleLaunchReservation(store, sessions, input);
306
+ return { disposition: "applied", run: terminal };
307
+ }
308
+ /**
309
+ * Leader-controlled recovery boundary for one active AgentRun. This primitive
310
+ * validates every durable fence in one transaction and records only a
311
+ * structured request for retry/session replacement. It never writes terminal
312
+ * bytes, retries a provider input, kills a host, or silently rebinds a native
313
+ * generation. Explicit termination is the sole action that changes Run state.
314
+ */
315
+ export function recoverExactAgentRun(store, input) {
316
+ return store.transaction((tx) => recoverExactAgentRunInTransaction(tx, input));
317
+ }
318
+ /** Alias named after the existing exact terminalization primitive. */
319
+ export const recoverExactTaskRun = recoverExactAgentRun;
320
+ function recoverExactAgentRunInTransaction(store, input) {
321
+ const current = store.getAgentRun(input.taskId, input.runId);
322
+ const stateChanged = (reason) => ({
323
+ disposition: "state-changed",
324
+ action: input.action,
325
+ run: current,
326
+ ...(current === null ? {} : { progressAt: latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId)?.progressAt }),
327
+ reason
328
+ });
329
+ const task = store.getTask(input.taskId);
330
+ if (task === null)
331
+ return stateChanged("task-missing");
332
+ if (task.status !== "active")
333
+ return stateChanged("task-terminal");
334
+ if (current === null)
335
+ return stateChanged("run-missing");
336
+ if (current.status !== "active")
337
+ return stateChanged("run-terminal");
338
+ if (current.taskId !== input.taskId || current.roleName !== input.roleName) {
339
+ return stateChanged("run-owner-mismatch");
340
+ }
341
+ if (current.effective.agentId !== input.agentId
342
+ || current.effective.adapterId !== input.adapterId) {
343
+ return stateChanged("run-launch-identity-mismatch");
344
+ }
345
+ const role = store.getRole(input.taskId, input.roleName);
346
+ if (role === null)
347
+ return stateChanged("role-missing");
348
+ const active = current.executionGroupId !== undefined && current.executionLaneId !== undefined
349
+ ? store.getActiveExecutionLaneRun(input.taskId, current.executionGroupId, current.executionLaneId)
350
+ : store.getActiveAgentRun(input.taskId, input.roleName);
351
+ if (active?.id !== current.id) {
352
+ return stateChanged("active-run-mismatch");
353
+ }
354
+ const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
355
+ if (progress === null)
356
+ return stateChanged("progress-unavailable");
357
+ if (progress.progressAt !== input.expectedProgressAt) {
358
+ return {
359
+ ...stateChanged("progress-fence-mismatch"),
360
+ progressAt: progress.progressAt
361
+ };
362
+ }
363
+ // Acceptance is a durable delivery boundary, not a Leader assertion. An
364
+ // accepted request must match the Run's persisted receipt; an already
365
+ // delivered Run cannot be reclassified as provider-rejected.
366
+ if ((input.providerAcceptance === "accepted" && current.deliveredAt === undefined)
367
+ || (input.providerAcceptance === "rejected" && current.deliveredAt !== undefined)) {
368
+ return {
369
+ disposition: "blocked",
370
+ action: input.action,
371
+ run: current,
372
+ progressAt: progress.progressAt,
373
+ reason: "provider-acceptance-mismatch"
374
+ };
375
+ }
376
+ if (!matchesRecoverySessionFence(store, input)) {
377
+ return stateChanged("session-or-launch-fence-mismatch");
378
+ }
379
+ if (input.providerAcceptance === "ambiguous"
380
+ && input.action !== "diagnose") {
381
+ return {
382
+ disposition: "blocked",
383
+ action: input.action,
384
+ run: current,
385
+ progressAt: progress.progressAt,
386
+ reason: "provider-acceptance-ambiguous"
387
+ };
388
+ }
389
+ if (!hasRecoveryReason(input.reason)) {
390
+ return {
391
+ disposition: "blocked",
392
+ action: input.action,
393
+ run: current,
394
+ progressAt: progress.progressAt,
395
+ reason: "recovery-reason-required"
396
+ };
397
+ }
398
+ const eventPayload = {
399
+ runId: current.id,
400
+ roleName: current.roleName,
401
+ action: input.action,
402
+ providerAcceptance: input.providerAcceptance,
403
+ progressAt: progress.progressAt,
404
+ ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
405
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
406
+ reason: input.reason
407
+ };
408
+ const events = store.listEvents(input.taskId);
409
+ const alreadyRequested = events.some((event) => (event.type === RUN_RECOVERY_REQUESTED_EVENT
410
+ && event.payload.runId === current.id
411
+ && event.payload.action === input.action
412
+ && event.payload.progressAt === progress.progressAt
413
+ && event.payload.nativeSessionId === input.nativeSessionId
414
+ && event.payload.launchId === input.launchId));
415
+ if (input.action !== "terminate") {
416
+ if (!alreadyRequested) {
417
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, RUN_RECOVERY_REQUESTED_EVENT, eventPayload, input.now));
418
+ // A non-Leader request is surfaced through the existing Leader mailbox;
419
+ // it is not a Task Message and is coalesced by the mailbox reason.
420
+ if (input.roleName !== "leader") {
421
+ enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, "run-recovery-requested", input.now, [{ type: "run", taskId: input.taskId, id: current.id }]);
422
+ }
423
+ }
424
+ return {
425
+ disposition: "applied",
426
+ action: input.action,
427
+ run: current,
428
+ progressAt: progress.progressAt,
429
+ requiresExplicitFollowup: true
430
+ };
431
+ }
432
+ const terminalization = terminalizeExactTaskRun(store, {
433
+ taskId: input.taskId,
434
+ roleName: input.roleName,
435
+ agentId: input.agentId,
436
+ runId: input.runId,
437
+ receiptId: formatAgentRunReceiptId(input.taskId, input.runId),
438
+ ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
439
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
440
+ outcome: { status: "failed", summary: input.reason }
441
+ }, input.now);
442
+ if (terminalization.disposition !== "applied" || terminalization.run === null) {
443
+ return stateChanged(terminalization.reason ?? "terminalization-fence-mismatch");
444
+ }
445
+ const terminal = terminalization.run;
446
+ if (terminal.purpose === "execution" && terminal.workItemId !== undefined) {
447
+ const item = store.getWorkItem(input.taskId, terminal.workItemId);
448
+ if (item !== null && !["completed", "failed", "retired"].includes(item.status)) {
449
+ store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", input.now, input.reason));
450
+ }
451
+ }
452
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, RUN_RECOVERY_APPLIED_EVENT, { ...eventPayload, status: "terminated" }, input.now));
453
+ clearMatchingLeaderStallAttention(store, input.taskId, input.runId);
454
+ if (input.roleName !== "leader") {
455
+ enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, "run-recovery-terminated", input.now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
456
+ }
457
+ return {
458
+ disposition: "applied",
459
+ action: input.action,
460
+ run: terminal,
461
+ progressAt: progress.progressAt
462
+ };
463
+ }
464
+ function matchesRecoverySessionFence(store, input) {
465
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
466
+ // Recovery must never proceed without a durable Session fence. A missing
467
+ // nativeSessionId is supported for an opaque host, but its exact launchId
468
+ // is then the only identity that can fence the action.
469
+ if (sessions === null)
470
+ return false;
471
+ if (sessions.activeAgentId !== input.agentId)
472
+ return false;
473
+ const session = sessions.sessions[input.agentId];
474
+ if (session === undefined)
475
+ return false;
476
+ if (session.agentId !== input.agentId || session.adapterId !== input.adapterId)
477
+ return false;
478
+ if (session.status === "stopped" || session.status === "broken")
479
+ return false;
480
+ const sessionNativeSessionId = session.nativeSessionId;
481
+ if (sessionNativeSessionId === undefined) {
482
+ if (input.nativeSessionId !== undefined)
483
+ return false;
484
+ }
485
+ else if (input.nativeSessionId !== sessionNativeSessionId) {
486
+ return false;
487
+ }
488
+ let launchMatches = false;
489
+ if (input.launchId !== undefined && session.launchId === input.launchId) {
490
+ launchMatches = true;
491
+ }
492
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
493
+ scope: "task",
494
+ taskId: input.taskId,
495
+ roleName: input.roleName
496
+ }));
497
+ if (!launchMatches
498
+ && input.launchId !== undefined
499
+ && isRuntimeLaunchReservation(mailbox?.processing, input.launchId)) {
500
+ launchMatches = true;
501
+ }
502
+ // An opaque Session has no native identity to compare, so an exact launch
503
+ // identity is mandatory. Native Sessions may retain the older no-launch
504
+ // shape; their exact native identity remains a valid fence.
505
+ if (sessionNativeSessionId === undefined)
506
+ return launchMatches;
507
+ if (input.nativeSessionId === undefined)
508
+ return launchMatches;
509
+ return session.launchId === undefined
510
+ ? input.launchId === undefined || launchMatches
511
+ : launchMatches;
512
+ }
513
+ function hasRecoveryReason(value) {
514
+ return typeof value === "string" && value.includes("\0") === false && value.trim().length > 0;
515
+ }
516
+ function matchesSessionFence(sessions, input) {
517
+ if (sessions === null)
518
+ return input.nativeSessionId === undefined;
519
+ if (sessions.activeAgentId !== input.agentId)
520
+ return false;
521
+ const session = sessions.sessions[input.agentId];
522
+ if (input.nativeSessionId !== undefined) {
523
+ if (session?.nativeSessionId !== input.nativeSessionId)
524
+ return false;
525
+ }
526
+ const pending = sessions.pendingTurnCompletion;
527
+ if (pending !== null && (pending.agentId !== input.agentId
528
+ || pending.runId !== input.runId
529
+ || (input.nativeSessionId !== undefined
530
+ && pending.nativeSessionId !== input.nativeSessionId)))
531
+ return false;
532
+ const inFlight = sessions.inFlight;
533
+ return inFlight === null || (inFlight.agentId === input.agentId
534
+ && inFlight.runId === input.runId
535
+ && inFlight.receiptId === input.receiptId);
536
+ }
537
+ function matchesLaunchFence(store, sessions, input) {
538
+ if (input.launchId === undefined)
539
+ return true;
540
+ const session = sessions?.sessions[input.agentId];
541
+ if (session?.launchId === input.launchId)
542
+ return true;
543
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
544
+ scope: "task",
545
+ taskId: input.taskId,
546
+ roleName: input.roleName
547
+ }));
548
+ return isRuntimeLaunchReservation(mailbox?.processing, input.launchId);
549
+ }
550
+ function settleLaunchReservation(store, sessions, input) {
551
+ const target = runtimeLifecycleTarget({
552
+ scope: "task",
553
+ taskId: input.taskId,
554
+ roleName: input.roleName
555
+ });
556
+ const mailbox = store.getWorkMailbox(target);
557
+ const reservation = mailbox?.processing;
558
+ const session = sessions?.sessions[input.agentId];
559
+ const launchId = input.launchId ?? session?.launchId;
560
+ if (launchId === undefined || !isRuntimeLaunchReservation(reservation, launchId))
561
+ return;
562
+ const settled = completeProcessing(mailbox, reservation.batchId);
563
+ if (settled.processing === null && settled.pending === null) {
564
+ store.removeWorkMailbox(target);
565
+ }
566
+ else {
567
+ store.saveWorkMailbox(settled);
568
+ }
569
+ }
570
+ function obsolete(run, reason) {
571
+ return { disposition: "obsolete", run, reason };
572
+ }