@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,951 @@
1
+ import { selectedSchedulerRoles, selectedSchedulerTasks } from "./ports.js";
2
+ /**
3
+ * Default window of no durable progress before a live-but-idle Run becomes a
4
+ * traceable needs-attention signal. It is deliberately long: a healthy Run that
5
+ * is simply slow keeps its structured checkpoint fresh and never crosses it.
6
+ */
7
+ export const DEFAULT_STALL_WINDOW_MS = 30 * 60_000;
8
+ /** Cheap execution-stall candidate filter; the real threshold remains 30m. */
9
+ export const DEFAULT_EXECUTION_STALL_CANDIDATE_AGE_MS = 10 * 60_000;
10
+ /** Resource activity may postpone attention only within this semantic gap. */
11
+ export const DEFAULT_RESOURCE_ONLY_SEMANTIC_GAP_MS = 90 * 60_000;
12
+ export const RUN_PROGRESS_EVENT = "run.progress";
13
+ export const RUN_STALLED_EVENT = "run.stalled";
14
+ export const RUN_RECOVERED_EVENT = "run.recovered";
15
+ /** Durable one-shot advisory resource evidence; never a progress fact. */
16
+ export const RUN_RESOURCE_SUPPRESSED_EVENT = "run.resource-suppressed";
17
+ /** Structured, non-Message recovery evidence written by an explicit Leader. */
18
+ export const RUN_RECOVERY_REQUESTED_EVENT = "run.recovery-requested";
19
+ export const RUN_RECOVERY_APPLIED_EVENT = "run.recovery-applied";
20
+ /**
21
+ * Clear only the Operator projection raised for this exact Leader Run. A
22
+ * later Run (or a recovery notification of another kind) must remain intact;
23
+ * callers use this helper from every supported recovery/terminalization path
24
+ * so attention cleanup cannot drift between CLI and runtime code.
25
+ */
26
+ export function clearMatchingLeaderStallAttention(store, taskId, runId) {
27
+ const notification = store.getOperatorNotification(taskId);
28
+ if (notification?.type !== "leader-stalled" || notification.runId !== runId) {
29
+ return false;
30
+ }
31
+ store.clearOperatorNotification(taskId);
32
+ return true;
33
+ }
34
+ /**
35
+ * One pure projection used by every Role. Resource activity can suppress a
36
+ * false positive only together with a live host and matching native Session
37
+ * or launch generation;
38
+ * it never changes the durable progress clock or authorizes recovery.
39
+ */
40
+ export function projectRoleRunHealth(input) {
41
+ const windowMs = input.windowMs ?? DEFAULT_STALL_WINDOW_MS;
42
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
43
+ throw new Error("Role run stall window must be a positive number of milliseconds.");
44
+ }
45
+ const evaluation = evaluateRoleRunStall({
46
+ progressAt: input.progressAt,
47
+ now: input.now,
48
+ windowMs
49
+ });
50
+ const idleMs = evaluation.idleMs;
51
+ const candidateStart = input.deliveredAt ?? input.createdAt;
52
+ const candidateAge = input.now.getTime() - Date.parse(candidateStart);
53
+ const candidate = Number.isFinite(candidateAge)
54
+ && (input.deliveredAt === undefined
55
+ ? candidateAge >= windowMs
56
+ : candidateAge >= DEFAULT_EXECUTION_STALL_CANDIDATE_AGE_MS);
57
+ const providerAcceptance = input.providerAcceptance
58
+ ?? (input.deliveredAt === undefined ? "ambiguous" : "accepted");
59
+ const hostLiveness = input.hostLiveness;
60
+ const session = input.nativeSession;
61
+ const nativeSession = session === null || session === undefined
62
+ ? "missing"
63
+ : session.status === "stopped"
64
+ ? "stopped"
65
+ : session.status === "broken"
66
+ ? "broken"
67
+ : session.status !== "ready" && session.status !== "running"
68
+ ? "unknown"
69
+ : !hasResourceIdentityText(session.nativeSessionId)
70
+ && !hasResourceIdentityText(session.launchId)
71
+ ? "unknown"
72
+ : "matching";
73
+ const resourceActivity = hostLiveness === "present"
74
+ && nativeSession === "matching"
75
+ && resourceEvidenceIsFresh(input.resource, input.now, windowMs)
76
+ // Residency/RSS and an unchanged cumulative counter are not progress.
77
+ && input.resource?.active === true
78
+ && input.resource?.changed === true;
79
+ const waitingUser = input.waitingUser === true;
80
+ const waitingOnWorkers = input.waitingOnWorkers === true && !input.staleLeaderMailbox;
81
+ // A TmuxSessionHost binding may intentionally be opaque and therefore have
82
+ // no persisted nativeSessionId. With a present host, durable no-progress
83
+ // evidence is still actionable in that case. Only an explicit stopped or
84
+ // broken Session blocks the projection; identity mismatches are fenced by
85
+ // reconcileStalledRoleRuns below before this projection is routed.
86
+ const executionStall = candidate
87
+ && evaluation.stalled
88
+ && hostLiveness === "present"
89
+ && nativeSession !== "stopped"
90
+ && nativeSession !== "broken"
91
+ && providerAcceptance !== "ambiguous"
92
+ && !resourceActivity;
93
+ const stalled = executionStall
94
+ && !waitingUser
95
+ && !waitingOnWorkers;
96
+ const classification = waitingUser
97
+ ? "waiting-user"
98
+ : waitingOnWorkers
99
+ ? "waiting-on-workers"
100
+ : stalled
101
+ ? "truly-stalled"
102
+ : candidate && input.roleName === "leader"
103
+ ? "working"
104
+ : "working";
105
+ return {
106
+ candidate,
107
+ stalled,
108
+ classification,
109
+ providerAcceptance,
110
+ hostLiveness,
111
+ nativeSession,
112
+ resourceActivity,
113
+ progressAt: input.progressAt,
114
+ idleMs
115
+ };
116
+ }
117
+ /**
118
+ * Pure stall decision. Given the last durable progress timestamp and whatever
119
+ * progress point the previous attention (if any) recorded, decide whether the
120
+ * Run is stalled and whether this is new evidence worth surfacing again.
121
+ */
122
+ export function evaluateRoleRunStall(input) {
123
+ const windowMs = input.windowMs ?? DEFAULT_STALL_WINDOW_MS;
124
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
125
+ throw new Error("Role run stall window must be a positive number of milliseconds.");
126
+ }
127
+ const progressMs = Date.parse(input.progressAt);
128
+ if (!Number.isFinite(progressMs)) {
129
+ throw new Error("Role run stall progress timestamp is invalid.");
130
+ }
131
+ const idleMs = input.now.getTime() - progressMs;
132
+ const stalled = idleMs >= windowMs;
133
+ const isNewEpisode = stalled && input.lastAttentionProgressAt !== input.progressAt;
134
+ return { stalled, isNewEpisode, idleMs, progressAt: input.progressAt };
135
+ }
136
+ /**
137
+ * Latest durable progress timestamp for an active Run. Provider acceptance
138
+ * (deliveredAt), explicit structured checkpoints, and semantic Run-scoped
139
+ * activity all count as progress; bookkeeping-only Run/Session timestamps are
140
+ * intentionally excluded. CPU/memory are never inputs here.
141
+ */
142
+ export function latestDurableProgressAt(input) {
143
+ const candidates = [
144
+ input.deliveredAt,
145
+ input.latestCheckpointAt,
146
+ input.latestActivityAt
147
+ ].filter((value) => (typeof value === "string" && Number.isFinite(Date.parse(value))));
148
+ if (candidates.length === 0) {
149
+ throw new Error("No durable progress timestamp is available for the Run.");
150
+ }
151
+ return candidates.reduce((latest, value) => (Date.parse(value) > Date.parse(latest) ? value : latest));
152
+ }
153
+ /** Most recent semantic timestamp carried by a Run progress event. */
154
+ export function latestRunProgressAt(events, runId) {
155
+ let latest;
156
+ for (const event of events) {
157
+ if (event.type !== RUN_PROGRESS_EVENT
158
+ && event.type !== "runtime.provider-turn-progress")
159
+ continue;
160
+ if (event.payload.runId !== runId)
161
+ continue;
162
+ if (event.type === "runtime.provider-turn-progress"
163
+ && (typeof event.payload.progressAt !== "string"
164
+ || !Number.isFinite(Date.parse(event.payload.progressAt))))
165
+ continue;
166
+ const progressAt = typeof event.payload.progressAt === "string"
167
+ && Number.isFinite(Date.parse(event.payload.progressAt))
168
+ ? event.payload.progressAt
169
+ : event.createdAt;
170
+ // The semantic timestamp is the CAS value. A late/stale event must not
171
+ // move the durable progress projection backwards merely because it was
172
+ // appended later than a newer checkpoint.
173
+ if (latest === undefined || Date.parse(progressAt) > Date.parse(latest)) {
174
+ latest = progressAt;
175
+ }
176
+ }
177
+ return latest;
178
+ }
179
+ /**
180
+ * Resolve the exact semantic progress fence shared by resource production and
181
+ * stall consumption for one active Run. The richer adapter fold remains
182
+ * authoritative when available; the retained Run progress/activity events are
183
+ * the provider-neutral fallback. Resource evidence carries this value as an
184
+ * exact fence, but never advances it.
185
+ */
186
+ export function currentRoleRunProgressAt(store, taskId, roleName, run, events = store.listEvents?.(taskId) ?? []) {
187
+ let richerProgress;
188
+ try {
189
+ richerProgress = store.getRunDurableProgress?.(taskId, roleName, run.id);
190
+ }
191
+ catch {
192
+ // A related-record fold is advisory. The retained Run/Event fence remains
193
+ // usable when that optional read is unavailable.
194
+ richerProgress = undefined;
195
+ }
196
+ if (run.deliveredAt === undefined)
197
+ return { progressAt: run.createdAt };
198
+ const fallbackProgressAt = latestDurableProgressAt({
199
+ deliveredAt: run.deliveredAt,
200
+ latestCheckpointAt: latestRunProgressAt(events, run.id),
201
+ latestActivityAt: latestRunActivityAt(events, run.id)
202
+ });
203
+ return {
204
+ progressAt: richerProgress?.progressAt ?? fallbackProgressAt,
205
+ ...(richerProgress?.evidence === undefined
206
+ ? {}
207
+ : { evidence: richerProgress.evidence })
208
+ };
209
+ }
210
+ /**
211
+ * Computes the current semantic progress fence for an exact Run. The optional
212
+ * related-record readers mirror the adapter's Work/Review/Integration fold;
213
+ * narrow scheduler ports can omit them and still retain Run/Event evidence.
214
+ */
215
+ export function latestRunDurableProgressAt(store, taskId, roleName, runId) {
216
+ const run = store.getAgentRun(taskId, runId);
217
+ if (run === null || run.taskId !== taskId || run.roleName !== roleName)
218
+ return null;
219
+ const events = store.listEvents(taskId);
220
+ const latestCheckpointAt = latestRunProgressAt(events, run.id);
221
+ const latestActivityAt = latestRunActivityAt(events, run.id);
222
+ const baseline = run.deliveredAt === undefined
223
+ ? run.createdAt
224
+ : latestDurableProgressAt({
225
+ deliveredAt: run.deliveredAt,
226
+ latestCheckpointAt,
227
+ latestActivityAt
228
+ });
229
+ if (run.workItemId === undefined) {
230
+ return { progressAt: baseline };
231
+ }
232
+ const workItem = store.getWorkItem?.(taskId, run.workItemId) ?? null;
233
+ const reviewRounds = store.listReviewRounds?.(taskId)
234
+ .filter(({ workItemId }) => workItemId === run.workItemId)
235
+ ?? [];
236
+ const changeSets = store.listChangeSets?.(taskId)
237
+ .filter(({ workItemId }) => workItemId === run.workItemId)
238
+ ?? [];
239
+ const changeSetIds = new Set(changeSets.map(({ id }) => id));
240
+ const integrations = store.listIntegrationAttempts?.(taskId)
241
+ .filter(({ changeSetIds: ids }) => ids.some((id) => changeSetIds.has(id)))
242
+ ?? [];
243
+ const inputProgress = store.listInputRequests?.(taskId)
244
+ .filter((request) => (request.requester.runId === run.id
245
+ || request.blockedRefs.some((ref) => ref.type === "run" && ref.id === run.id)))
246
+ ?? [];
247
+ const related = [
248
+ workItem?.updatedAt,
249
+ ...reviewRounds.map(({ endedAt, createdAt }) => endedAt ?? createdAt),
250
+ ...changeSets.map(({ createdAt }) => createdAt),
251
+ ...integrations.map(({ updatedAt }) => updatedAt),
252
+ ...inputProgress.map(({ updatedAt }) => updatedAt),
253
+ ...(workItem?.candidates ?? []).map(({ createdAt }) => createdAt)
254
+ ].filter((value) => (typeof value === "string" && Number.isFinite(Date.parse(value))));
255
+ const latestRelatedAt = related.reduce((latest, value) => latest === undefined || Date.parse(value) > Date.parse(latest)
256
+ ? value
257
+ : latest, undefined);
258
+ // Before provider acceptance the Run clock remains anchored to creation,
259
+ // while related records are retained as diagnostic evidence only.
260
+ return run.deliveredAt !== undefined
261
+ && latestRelatedAt !== undefined
262
+ && Date.parse(latestRelatedAt) > Date.parse(baseline)
263
+ ? { progressAt: latestRelatedAt, evidence: "work-review-integration" }
264
+ : {
265
+ progressAt: baseline,
266
+ ...(latestRelatedAt === undefined ? {} : { evidence: "work-review-integration" })
267
+ };
268
+ }
269
+ /** Most recent createdAt of a Run-scoped event of one type, if any. */
270
+ export function latestRunEventTime(events, type, runId) {
271
+ let latest;
272
+ for (const event of events) {
273
+ if (event.type !== type || event.payload.runId !== runId)
274
+ continue;
275
+ if (latest === undefined || Date.parse(event.createdAt) > Date.parse(latest)) {
276
+ latest = event.createdAt;
277
+ }
278
+ }
279
+ return latest;
280
+ }
281
+ /** Most recent non-control event carrying a Run identity. */
282
+ export function latestRunActivityAt(events, runId) {
283
+ const semanticTypes = new Set([
284
+ RUN_PROGRESS_EVENT,
285
+ "runtime.provider-turn-progress",
286
+ "message.sent",
287
+ "input.answered",
288
+ "input.auto-answered",
289
+ "input.cancelled",
290
+ "work.accepted",
291
+ "work.updated",
292
+ "review.completed",
293
+ "review.failed",
294
+ "integration.updated",
295
+ "integration.completed",
296
+ "integration.failed"
297
+ ]);
298
+ let latest;
299
+ for (const event of events) {
300
+ if (!semanticTypes.has(event.type)
301
+ || event.payload.runId !== runId
302
+ || event.type === RUN_STALLED_EVENT
303
+ || event.type === RUN_RECOVERED_EVENT)
304
+ continue;
305
+ if (event.type === "runtime.provider-turn-progress"
306
+ && (typeof event.payload.progressAt !== "string"
307
+ || !Number.isFinite(Date.parse(event.payload.progressAt))))
308
+ continue;
309
+ const activityAt = (event.type === RUN_PROGRESS_EVENT
310
+ || event.type === "runtime.provider-turn-progress")
311
+ && typeof event.payload.progressAt === "string"
312
+ && Number.isFinite(Date.parse(event.payload.progressAt))
313
+ ? event.payload.progressAt
314
+ : event.createdAt;
315
+ if (latest === undefined || Date.parse(activityAt) > Date.parse(latest)) {
316
+ latest = activityAt;
317
+ }
318
+ }
319
+ return latest;
320
+ }
321
+ /** The progress point recorded by the most recent stall attention for a Run. */
322
+ export function latestStallProgressAt(events, runId) {
323
+ let latest;
324
+ for (const event of events) {
325
+ if (event.type !== RUN_STALLED_EVENT || event.payload.runId !== runId)
326
+ continue;
327
+ if (latest === undefined || Date.parse(event.createdAt) > Date.parse(latest.createdAt)) {
328
+ latest = event;
329
+ }
330
+ }
331
+ return latest?.payload.progressAt;
332
+ }
333
+ /** Latest stall episode identity used for source-idempotent attention. */
334
+ export function latestStallEvidenceKey(events, runId) {
335
+ let latest;
336
+ for (const event of events) {
337
+ if (event.type !== RUN_STALLED_EVENT || event.payload.runId !== runId)
338
+ continue;
339
+ if (latest === undefined || Date.parse(event.createdAt) > Date.parse(latest.createdAt)) {
340
+ latest = event;
341
+ }
342
+ }
343
+ if (latest?.payload.progressAt === undefined)
344
+ return undefined;
345
+ return {
346
+ progressAt: latest.payload.progressAt,
347
+ evidenceKey: latest.payload.evidenceKey ?? "live-pane-no-progress"
348
+ };
349
+ }
350
+ /**
351
+ * True while a Run remains in an unresolved stall episode: a stall was raised
352
+ * and no later checkpoint has advanced its durable progress since. This is the
353
+ * projection the runtime-health view reads to surface needs-attention.
354
+ */
355
+ export function isRoleRunStalled(events, runId) {
356
+ const stalledAt = latestRunEventTime(events, RUN_STALLED_EVENT, runId);
357
+ if (stalledAt === undefined)
358
+ return false;
359
+ const stalled = [...events]
360
+ .filter((event) => event.type === RUN_STALLED_EVENT && event.payload.runId === runId)
361
+ .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
362
+ const stalledProgressAt = typeof stalled?.payload.progressAt === "string"
363
+ && Number.isFinite(Date.parse(stalled.payload.progressAt))
364
+ ? stalled.payload.progressAt
365
+ : stalledAt;
366
+ const recoveredAt = latestRunEventTime(events, RUN_RECOVERED_EVENT, runId);
367
+ if (recoveredAt !== undefined && Date.parse(recoveredAt) > Date.parse(stalledAt)) {
368
+ return false;
369
+ }
370
+ const progressAt = latestRunProgressAt(events, runId);
371
+ return progressAt === undefined || Date.parse(progressAt) <= Date.parse(stalledProgressAt);
372
+ }
373
+ /**
374
+ * Low-frequency health pass for active Task Role Runs. An unaccepted Run is
375
+ * watched as delivery-stalled after the reasonable delivery window; an
376
+ * accepted Run enters the execution-stall candidate scan after ten minutes,
377
+ * while the actual no-progress threshold remains thirty minutes.
378
+ * Leader Runs are only persisted when classification reaches truly-stalled —
379
+ * healthy downstream work, open user input, and recent own progress remain
380
+ * structured waiting/working facts. No branch sends terminal bytes, retries,
381
+ * replaces a Session, or changes Run status.
382
+ */
383
+ export async function reconcileStalledRoleRuns(store, delivery, now, selection, windowMs = DEFAULT_STALL_WINDOW_MS, liveStatuses, resourceEvidence, resourceSuppressionKeys) {
384
+ // Dirty mailbox passes are intentionally not a second scheduler. Full
385
+ // reconcile owns the all-active-Run scan; dirty passes may still route the
386
+ // existing mailbox work without manufacturing another episode.
387
+ if (selection !== undefined && !selection.full)
388
+ return [];
389
+ if (store.listEvents === undefined || store.recordRoleRunStall === undefined)
390
+ return [];
391
+ const candidates = selectedSchedulerTasks(store, selection).flatMap((task) => (task.status !== "active"
392
+ ? []
393
+ : selectedSchedulerRoles(store, task.id, selection).flatMap((role) => {
394
+ const run = store.getActiveAgentRun(task.id, role.name);
395
+ if (run === null || run.status !== "active")
396
+ return [];
397
+ return [{
398
+ task,
399
+ role,
400
+ run,
401
+ session: store.getRoleSession(task.id, role.name, run.effective.agentId)
402
+ }];
403
+ })));
404
+ const stallCandidates = candidates.filter(({ run }) => isStallCandidate(run, now, windowMs));
405
+ if (stallCandidates.length === 0)
406
+ return [];
407
+ const stallCandidateKeys = new Set(stallCandidates.map(({ task, role }) => (`${task.id}\0${role.name}`)));
408
+ const observed = new Map();
409
+ // Recent active Runs are known healthy enough for Leader classification from
410
+ // their exact acceptance/creation boundary, but are deliberately not read
411
+ // from tmux or Event history until the 10-minute execution candidate window.
412
+ for (const candidate of candidates) {
413
+ const key = `${candidate.task.id}\0${candidate.role.name}`;
414
+ if (stallCandidateKeys.has(key))
415
+ continue;
416
+ observed.set(key, {
417
+ candidate,
418
+ live: liveStatuses?.get(key) ?? "present",
419
+ progressAt: candidate.run.deliveredAt ?? candidate.run.createdAt,
420
+ idleMs: 0,
421
+ stalled: false,
422
+ resourceActivity: false,
423
+ stallCandidate: false
424
+ });
425
+ }
426
+ let statuses = null;
427
+ if (liveStatuses !== undefined) {
428
+ statuses = null;
429
+ }
430
+ else if (delivery.inspectRoles !== undefined) {
431
+ try {
432
+ statuses = await delivery.inspectRoles(stallCandidates.map(({ task, role, run, session }) => ({
433
+ taskId: task.id,
434
+ roleName: role.name,
435
+ runId: run.id,
436
+ progressAt: run.deliveredAt ?? run.createdAt,
437
+ agentId: session?.agentId ?? run.effective.agentId,
438
+ adapterId: session?.adapterId ?? run.effective.adapterId,
439
+ ...(session?.launchId === undefined ? {} : { launchId: session.launchId }),
440
+ ...(session?.nativeSessionId === undefined
441
+ ? {}
442
+ : { nativeSessionId: session.nativeSessionId })
443
+ })), stallCandidates.flatMap(({ task, role, run, session }) => (run.deliveredAt === undefined
444
+ ? []
445
+ : [{
446
+ taskId: task.id,
447
+ roleName: role.name,
448
+ runId: run.id,
449
+ progressAt: currentRoleRunProgressAt(store, task.id, role.name, run).progressAt,
450
+ agentId: session?.agentId ?? run.effective.agentId,
451
+ adapterId: session?.adapterId ?? run.effective.adapterId,
452
+ ...(session?.launchId === undefined ? {} : { launchId: session.launchId }),
453
+ ...(session?.nativeSessionId === undefined
454
+ ? {}
455
+ : { nativeSessionId: session.nativeSessionId })
456
+ }])));
457
+ }
458
+ catch {
459
+ // Health inspection is advisory. Unknown host state must leave the exact
460
+ // Run/Session fence untouched and let the next full pass retry.
461
+ return [];
462
+ }
463
+ }
464
+ let byRole = null;
465
+ if (statuses !== null) {
466
+ try {
467
+ byRole = exactLiveStatuses(statuses, stallCandidates);
468
+ }
469
+ catch {
470
+ // A malformed/partial provider snapshot is not evidence of a stall.
471
+ // Leave all fences untouched and let the next full pass retry.
472
+ return [];
473
+ }
474
+ }
475
+ const raised = [];
476
+ for (const candidate of stallCandidates) {
477
+ const key = `${candidate.task.id}\0${candidate.role.name}`;
478
+ let live;
479
+ try {
480
+ if (liveStatuses !== undefined && !liveStatuses.has(key))
481
+ return [];
482
+ live = liveStatuses !== undefined
483
+ ? liveStatuses.get(key)
484
+ : byRole === null
485
+ ? await delivery.inspectRole({
486
+ taskId: candidate.task.id,
487
+ roleName: candidate.role.name,
488
+ agentId: candidate.session?.agentId ?? candidate.run.effective.agentId,
489
+ adapterId: candidate.session?.adapterId ?? candidate.run.effective.adapterId,
490
+ ...(candidate.session?.nativeSessionId === undefined
491
+ ? {}
492
+ : { nativeSessionId: candidate.session.nativeSessionId })
493
+ })
494
+ : byRole.get(key);
495
+ }
496
+ catch {
497
+ // Without a complete live snapshot, especially for downstream Runs,
498
+ // Leader classification would be unsafe. Treat the whole advisory pass
499
+ // as unknown instead of escalating a false stall.
500
+ return [];
501
+ }
502
+ // A missing process is handled by the existing fenced liveness pass. This
503
+ // monitor must not race it or synthesize a second failure disposition.
504
+ if (live !== "present") {
505
+ observed.set(key, {
506
+ candidate,
507
+ live,
508
+ progressAt: candidate.run.createdAt,
509
+ idleMs: 0,
510
+ stalled: false,
511
+ resourceActivity: false,
512
+ stallCandidate: true
513
+ });
514
+ continue;
515
+ }
516
+ const events = store.listEvents(candidate.task.id);
517
+ // Before exact acceptance there is no execution progress clock. Keep the
518
+ // delivery watch anchored to the Run creation/transport boundary even if
519
+ // checkpoints, output, or related WorkItem/Review/Integration records are
520
+ // newer; those facts are useful evidence but cannot prove provider
521
+ // acceptance or reset delivery timeout. Once accepted, deliveredAt is the
522
+ // semantic baseline and the durable fold may advance it.
523
+ const progress = currentRoleRunProgressAt(store, candidate.task.id, candidate.role.name, candidate.run, events);
524
+ const progressAt = progress.progressAt;
525
+ const evaluation = evaluateRoleRunStall({
526
+ progressAt,
527
+ now,
528
+ windowMs,
529
+ lastAttentionProgressAt: latestStallProgressAt(events, candidate.run.id)
530
+ });
531
+ const runAgentId = candidate.run.effective.agentId;
532
+ const runAdapterId = candidate.run.effective.adapterId;
533
+ const sessionMatchesRun = candidate.session === null
534
+ || (candidate.session.agentId === runAgentId
535
+ && candidate.session.adapterId === runAdapterId);
536
+ const sessionUsable = candidate.session === null
537
+ || (candidate.session.status !== "stopped" && candidate.session.status !== "broken");
538
+ const expectedResourceIdentity = candidate.session === null
539
+ ? undefined
540
+ : {
541
+ taskId: candidate.task.id,
542
+ roleName: candidate.role.name,
543
+ runId: candidate.run.id,
544
+ agentId: runAgentId,
545
+ adapterId: runAdapterId,
546
+ ...(candidate.session.nativeSessionId === undefined
547
+ ? {}
548
+ : { nativeSessionId: candidate.session.nativeSessionId }),
549
+ ...(candidate.session.launchId === undefined
550
+ ? {}
551
+ : { launchId: candidate.session.launchId })
552
+ };
553
+ const resourceSnapshot = resourceForRun(resourceEvidence, candidate.task.id, candidate.role.name, candidate.run.id);
554
+ const resourceCanSuppress = live === "present"
555
+ && sessionMatchesRun
556
+ && sessionUsable
557
+ && expectedResourceIdentity !== undefined
558
+ && resourceEvidenceMatchesCurrentRun(resourceSnapshot, expectedResourceIdentity, progressAt)
559
+ && resourceEvidenceIsFresh(resourceSnapshot, now, windowMs);
560
+ const resource = resourceCanSuppress
561
+ ? await consumeResourceEvidence(store, resourceEvidence, candidate.task.id, candidate.role.name, candidate.run.id, progressAt, expectedResourceIdentity, resourceSuppressionKeys, now)
562
+ : undefined;
563
+ const health = projectRoleRunHealth({
564
+ progressAt,
565
+ createdAt: candidate.run.createdAt,
566
+ ...(candidate.run.deliveredAt === undefined
567
+ ? {}
568
+ : { deliveredAt: candidate.run.deliveredAt }),
569
+ now,
570
+ windowMs,
571
+ hostLiveness: live,
572
+ nativeSession: candidate.session,
573
+ providerAcceptance: candidate.run.deliveredAt === undefined
574
+ ? "ambiguous"
575
+ : "accepted",
576
+ resource,
577
+ roleName: candidate.role.name
578
+ });
579
+ // Delivery-stalled Runs retain the existing delivery clock and immediate
580
+ // provider-uncertainty path. Accepted execution Runs use the shared
581
+ // projection, including its exact Session/host and advisory resource
582
+ // conditions.
583
+ const resourceActivity = health.resourceActivity;
584
+ const stalled = candidate.run.deliveredAt === undefined
585
+ ? evaluation.stalled && live === "present"
586
+ : health.stalled && sessionMatchesRun && sessionUsable;
587
+ observed.set(key, {
588
+ candidate,
589
+ live,
590
+ progressAt,
591
+ idleMs: evaluation.idleMs,
592
+ // Resource activity is advisory: with a live, matching native Session it
593
+ // keeps a long inference/remote-IO Run in the working projection, but it
594
+ // never advances progress or creates a recovered event by itself.
595
+ stalled,
596
+ resourceActivity,
597
+ evidence: [
598
+ ...(progress.evidence === undefined ? [] : [progress.evidence]),
599
+ ...(resourceActivity ? ["resource-activity"] : [])
600
+ ].join(",") || undefined,
601
+ stallCandidate: true
602
+ });
603
+ }
604
+ for (const current of observed.values()) {
605
+ if (current.live !== "present" || !current.stalled || !current.stallCandidate)
606
+ continue;
607
+ const { candidate, progressAt } = current;
608
+ const events = store.listEvents(candidate.task.id);
609
+ const previous = latestStallEvidenceKey(events, candidate.run.id);
610
+ if (previous !== undefined
611
+ && Date.parse(progressAt) > Date.parse(previous.progressAt)) {
612
+ // A new semantic progress point closes the previous episode first. It
613
+ // may itself already be older than the window, in which case the same
614
+ // pass records the next Run+progressAt episode below.
615
+ store.recordRoleRunProgress?.({
616
+ taskId: candidate.task.id,
617
+ roleName: candidate.role.name,
618
+ runId: candidate.run.id,
619
+ progressAt,
620
+ ...(current.evidence === undefined ? {} : { evidence: current.evidence }),
621
+ now
622
+ });
623
+ }
624
+ const kind = candidate.run.deliveredAt === undefined
625
+ ? "delivery-stalled"
626
+ : "execution-stalled";
627
+ const classification = candidate.role.name === "leader"
628
+ ? classifyLeaderStall(store, candidate.task.id, observed, now, windowMs)
629
+ : "truly-stalled";
630
+ // A Leader waiting on the user or healthy downstream work is not a stall
631
+ // episode. Its durable InputRequest/worker facts remain the evidence.
632
+ if (classification !== "truly-stalled")
633
+ continue;
634
+ const evidenceKey = [
635
+ kind,
636
+ stallEvidenceKey(candidate.role.status, candidate.session?.status),
637
+ classification,
638
+ ...(candidate.role.name === "leader"
639
+ ? [leaderStallEvidence(store, candidate.task.id, observed, now, windowMs)]
640
+ : []),
641
+ ...(current.evidence === undefined ? [] : [current.evidence])
642
+ ].join(":");
643
+ // The final contract is one episode per Run + semantic progress point.
644
+ // New role/session/provider evidence is retained in the Leader's next
645
+ // diagnostic context, not duplicated as another Task-level alert.
646
+ if (previous?.progressAt === progressAt)
647
+ continue;
648
+ const persisted = store.recordRoleRunStall({
649
+ taskId: candidate.task.id,
650
+ roleName: candidate.role.name,
651
+ runId: candidate.run.id,
652
+ agentId: candidate.run.effective.agentId,
653
+ adapterId: candidate.run.effective.adapterId,
654
+ session: candidate.session,
655
+ kind,
656
+ classification,
657
+ progressAt,
658
+ idleMs: current.idleMs,
659
+ evidenceKey,
660
+ now
661
+ });
662
+ if (persisted === "raised" || persisted === "already-raised") {
663
+ raised.push({
664
+ taskId: candidate.task.id,
665
+ roleName: candidate.role.name,
666
+ runId: candidate.run.id,
667
+ status: persisted,
668
+ kind,
669
+ classification,
670
+ idleMs: current.idleMs
671
+ });
672
+ }
673
+ }
674
+ // A related WorkItem/Review/Integration fold may advance progress without
675
+ // carrying the Run id. Materialize it once so context/web can clear the
676
+ // attention projection and the event history records the recovery boundary.
677
+ for (const current of observed.values()) {
678
+ if (current.live !== "present" || current.stalled || !current.stallCandidate)
679
+ continue;
680
+ const { candidate, progressAt } = current;
681
+ const events = store.listEvents(candidate.task.id);
682
+ const previous = latestStallEvidenceKey(events, candidate.run.id);
683
+ if (previous !== undefined
684
+ && Date.parse(progressAt) > Date.parse(previous.progressAt)) {
685
+ store.recordRoleRunProgress?.({
686
+ taskId: candidate.task.id,
687
+ roleName: candidate.role.name,
688
+ runId: candidate.run.id,
689
+ progressAt,
690
+ ...(current.evidence === undefined ? {} : { evidence: current.evidence }),
691
+ now
692
+ });
693
+ }
694
+ }
695
+ return raised;
696
+ }
697
+ function isStallCandidate(run, now, windowMs) {
698
+ const startAt = run.deliveredAt ?? run.createdAt;
699
+ const ageMs = now.getTime() - Date.parse(startAt);
700
+ if (!Number.isFinite(ageMs))
701
+ return false;
702
+ // Undelivered Runs are watched for a delivery stall on the same reasonable
703
+ // window, but they never enter execution-stall candidate filtering.
704
+ return run.deliveredAt === undefined
705
+ ? ageMs >= windowMs
706
+ : ageMs >= DEFAULT_EXECUTION_STALL_CANDIDATE_AGE_MS;
707
+ }
708
+ function classifyLeaderStall(store, taskId, observed, now, windowMs) {
709
+ if (store.hasOpenInputRequest(taskId))
710
+ return "waiting-user";
711
+ const downstream = [...observed.values()].filter((entry) => (entry.candidate.task.id === taskId && entry.candidate.role.name !== "leader"));
712
+ // A present downstream Run keeps recovery Leader-owned. If that Run is
713
+ // itself stalled, this pass has just routed its structured attention to the
714
+ // Leader; escalating the Leader to the Operator in the same pass would skip
715
+ // the intended recovery owner.
716
+ const downstreamPresent = downstream.some((entry) => entry.live === "present");
717
+ const leader = [...observed.values()].find((entry) => (entry.candidate.task.id === taskId && entry.candidate.role.name === "leader"));
718
+ const mailbox = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" });
719
+ const pending = mailbox?.pending;
720
+ const processing = mailbox?.processing;
721
+ const processingCurrent = processing?.executionRef?.type === "run"
722
+ && processing.executionRef.taskId === taskId
723
+ && leader !== undefined
724
+ && processing.executionRef.id === leader.candidate.run.id;
725
+ const currentRunId = leader?.candidate.run.id;
726
+ if (processingCurrent && currentRunId !== undefined && processing !== null) {
727
+ const actionProgressAt = latestLeaderActionProgressAt(store, taskId, currentRunId, processing.startedAt, processing.batch, now);
728
+ const actionProgressMs = Date.parse(actionProgressAt);
729
+ const actionStalled = Number.isFinite(actionProgressMs)
730
+ && now.getTime() - actionProgressMs >= windowMs;
731
+ if (actionStalled)
732
+ return "truly-stalled";
733
+ return downstreamPresent ? "waiting-on-workers" : "working";
734
+ }
735
+ const pendingStalled = pending !== null
736
+ && pending !== undefined
737
+ && Number.isFinite(Date.parse(pending.lastQueuedAt))
738
+ && now.getTime() - Date.parse(pending.lastQueuedAt) >= windowMs;
739
+ if (pendingStalled)
740
+ return "truly-stalled";
741
+ if (downstreamPresent)
742
+ return "waiting-on-workers";
743
+ return "truly-stalled";
744
+ }
745
+ const LEADER_ACTION_PROGRESS_TYPES = new Set([
746
+ RUN_PROGRESS_EVENT,
747
+ "runtime.provider-turn-progress",
748
+ "input.answered",
749
+ "input.auto-answered",
750
+ "input.cancelled",
751
+ "work.accepted",
752
+ "work.updated",
753
+ "work.retired",
754
+ "review.completed",
755
+ "review.failed",
756
+ "integration.updated",
757
+ "integration.completed",
758
+ "integration.failed",
759
+ "decision.recorded",
760
+ "decision.superseded",
761
+ "milestone.added"
762
+ ]);
763
+ function latestLeaderActionProgressAt(store, taskId, runId, startedAt, batch, now) {
764
+ const startedMs = Date.parse(startedAt);
765
+ if (!Number.isFinite(startedMs))
766
+ return "";
767
+ let latest = startedAt;
768
+ let latestMs = startedMs;
769
+ for (const event of store.listEvents?.(taskId) ?? []) {
770
+ if (!LEADER_ACTION_PROGRESS_TYPES.has(event.type))
771
+ continue;
772
+ if (event.payload.taskId !== undefined && event.payload.taskId !== taskId)
773
+ continue;
774
+ if (!leaderActionEventMatches(event, taskId, runId, batch))
775
+ continue;
776
+ const createdMs = Date.parse(event.createdAt);
777
+ if (!Number.isFinite(createdMs)
778
+ || createdMs < startedMs
779
+ || createdMs > now.getTime())
780
+ continue;
781
+ const value = (event.type === RUN_PROGRESS_EVENT
782
+ || event.type === "runtime.provider-turn-progress")
783
+ && typeof event.payload.progressAt === "string"
784
+ && Number.isFinite(Date.parse(event.payload.progressAt))
785
+ ? event.payload.progressAt
786
+ : event.createdAt;
787
+ const valueMs = Date.parse(value);
788
+ if (!Number.isFinite(valueMs)
789
+ || valueMs < startedMs
790
+ || valueMs > now.getTime()
791
+ || valueMs <= latestMs)
792
+ continue;
793
+ latest = value;
794
+ latestMs = valueMs;
795
+ }
796
+ return latest;
797
+ }
798
+ function leaderActionEventMatches(event, taskId, runId, batch) {
799
+ if (event.taskId !== taskId)
800
+ return false;
801
+ if (event.payload.leaderRunId === runId)
802
+ return true;
803
+ if (event.payload.runId === runId)
804
+ return true;
805
+ const refs = batch.refs ?? [];
806
+ const refMatches = (type, payloadKey) => {
807
+ const id = event.payload[payloadKey];
808
+ return typeof id === "string" && refs.some((ref) => (ref.type === type
809
+ && ref.id === id
810
+ && (ref.taskId === undefined || ref.taskId === taskId)));
811
+ };
812
+ if (refMatches("run", "runId"))
813
+ return true;
814
+ if (refMatches("work-item", "workItemId"))
815
+ return true;
816
+ const reason = event.type.replaceAll(".", "-");
817
+ // Mailbox reasons are opaque exact coalescing keys. A prefix or suffix
818
+ // resemblance can belong to another action in the same Task and is not a
819
+ // durable ownership fence.
820
+ const reasonMatches = batch.reasons.some((candidate) => (candidate === reason));
821
+ return reasonMatches && refs.some((ref) => (ref.type === "task" && (ref.taskId === undefined || ref.taskId === taskId)));
822
+ }
823
+ function leaderStallEvidence(store, taskId, observed, now, windowMs) {
824
+ const downstream = [...observed.values()].filter((entry) => (entry.candidate.task.id === taskId && entry.candidate.role.name !== "leader"));
825
+ const active = downstream.filter((entry) => entry.candidate.run !== null).length;
826
+ const healthy = downstream.filter((entry) => entry.live === "present" && !entry.stalled).length;
827
+ const stalled = downstream.filter((entry) => entry.live === "present" && entry.stalled).length;
828
+ const pending = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" })?.pending;
829
+ const pendingAge = pending === undefined || pending === null
830
+ ? "none"
831
+ : Number.isFinite(Date.parse(pending.lastQueuedAt))
832
+ && now.getTime() - Date.parse(pending.lastQueuedAt) >= windowMs
833
+ ? "stale"
834
+ : "recent";
835
+ const processing = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" })?.processing;
836
+ const leader = [...observed.values()].find((entry) => (entry.candidate.task.id === taskId && entry.candidate.role.name === "leader"));
837
+ const processingCurrent = processing?.executionRef?.type === "run"
838
+ && processing.executionRef.taskId === taskId
839
+ && leader !== undefined
840
+ && processing.executionRef.id === leader.candidate.run.id;
841
+ const processingAge = processing === undefined || processing === null
842
+ ? "none"
843
+ : !processingCurrent
844
+ ? "mismatched"
845
+ : Number.isFinite(Date.parse(processing.startedAt))
846
+ && now.getTime() - Date.parse(processing.startedAt) >= windowMs
847
+ ? "stale"
848
+ : "recent";
849
+ return `downstream=active:${active},healthy:${healthy},stalled:${stalled}:leader-mailbox=${pendingAge},leader-processing=${processingAge}`;
850
+ }
851
+ function stallEvidenceKey(roleStatus, sessionStatus) {
852
+ return `live-pane-no-progress:role=${roleStatus}:session=${sessionStatus ?? "unknown"}`;
853
+ }
854
+ function exactLiveStatuses(statuses, candidates) {
855
+ const expected = new Set(candidates.map(({ task, role }) => `${task.id}\0${role.name}`));
856
+ const result = new Map();
857
+ for (const status of statuses) {
858
+ const key = `${status.taskId}\0${status.roleName}`;
859
+ if (!expected.has(key) || result.has(key)) {
860
+ throw new Error("Tmux Role stall snapshot is invalid.");
861
+ }
862
+ result.set(key, status.status);
863
+ }
864
+ if (result.size !== expected.size)
865
+ throw new Error("Tmux Role stall snapshot is incomplete.");
866
+ return result;
867
+ }
868
+ function resourceForRun(snapshot, taskId, roleName, runId) {
869
+ if (snapshot === undefined)
870
+ return undefined;
871
+ return snapshot.get(`${taskId}\0${roleName}\0${runId}`);
872
+ }
873
+ function resourceEvidenceMatchesCurrentRun(resource, expected, progressAt) {
874
+ if (resource === undefined || expected === undefined)
875
+ return false;
876
+ const identity = resource.identity;
877
+ if (identity === undefined
878
+ || resource.progressAt !== progressAt
879
+ || identity.taskId !== expected.taskId
880
+ || identity.roleName !== expected.roleName
881
+ || identity.runId !== expected.runId
882
+ || identity.agentId !== expected.agentId
883
+ || identity.adapterId !== expected.adapterId
884
+ || identity.nativeSessionId !== expected.nativeSessionId
885
+ || identity.launchId !== expected.launchId)
886
+ return false;
887
+ return hasResourceIdentityText(identity.nativeSessionId)
888
+ || hasResourceIdentityText(identity.launchId);
889
+ }
890
+ function hasResourceIdentityText(value) {
891
+ return typeof value === "string" && value.trim().length > 0;
892
+ }
893
+ /** Persist the first advisory sample and keep it bounded by the semantic gap. */
894
+ async function consumeResourceEvidence(store, snapshot, taskId, roleName, runId, progressAt, expectedIdentity, suppressionKeys, now) {
895
+ const resource = resourceForRun(snapshot, taskId, roleName, runId);
896
+ if (resource === undefined)
897
+ return undefined;
898
+ if (!resourceEvidenceMatchesCurrentRun(resource, expectedIdentity, progressAt)) {
899
+ return resource;
900
+ }
901
+ if (resource.active !== true || resource.changed !== true)
902
+ return resource;
903
+ const key = `${taskId}\0${roleName}\0${runId}\0${progressAt}`;
904
+ if (store.recordRoleRunResourceSuppression !== undefined) {
905
+ const persisted = store.recordRoleRunResourceSuppression({
906
+ taskId,
907
+ roleName,
908
+ runId,
909
+ agentId: expectedIdentity.agentId,
910
+ adapterId: expectedIdentity.adapterId,
911
+ ...(expectedIdentity.nativeSessionId === undefined
912
+ ? {}
913
+ : { nativeSessionId: expectedIdentity.nativeSessionId }),
914
+ ...(expectedIdentity.launchId === undefined
915
+ ? {}
916
+ : { launchId: expectedIdentity.launchId }),
917
+ progressAt,
918
+ observedAt: resource.observedAt,
919
+ now
920
+ });
921
+ if (persisted === "recorded" || persisted === "already-recorded") {
922
+ return resourceFallsWithinSemanticGap(progressAt, now)
923
+ ? resource
924
+ : { ...resource, active: false, changed: false };
925
+ }
926
+ // A concurrent Run/session change invalidates this sample. Do not let the
927
+ // stale changed bit suppress the current Run's attention episode.
928
+ return { ...resource, active: false, changed: false };
929
+ }
930
+ if (suppressionKeys === undefined || !suppressionKeys.has(key)) {
931
+ suppressionKeys?.add(key);
932
+ return resource;
933
+ }
934
+ return { ...resource, active: false, changed: false };
935
+ }
936
+ function resourceFallsWithinSemanticGap(progressAt, now) {
937
+ const progressMs = Date.parse(progressAt);
938
+ return Number.isFinite(progressMs)
939
+ && progressMs <= now.getTime()
940
+ && now.getTime() - progressMs < DEFAULT_RESOURCE_ONLY_SEMANTIC_GAP_MS;
941
+ }
942
+ function resourceEvidenceIsFresh(evidence, now, windowMs) {
943
+ if (evidence === undefined)
944
+ return false;
945
+ const observedAt = Date.parse(evidence.observedAt);
946
+ if (!Number.isFinite(observedAt))
947
+ return false;
948
+ // A sample from an earlier scheduler window is not a current health signal.
949
+ return observedAt <= now.getTime()
950
+ && now.getTime() - observedAt < Math.max(windowMs, DEFAULT_EXECUTION_STALL_CANDIDATE_AGE_MS);
951
+ }