@zq-silk/yui 0.10.0 → 0.11.0

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 (46) hide show
  1. package/README.md +53 -0
  2. package/dist/cli/commandCatalog.js +28 -7
  3. package/dist/cli.js +50 -47
  4. package/dist/commands/projectCommands.js +69 -6
  5. package/dist/commands/taskCommands.js +639 -89
  6. package/dist/commands/taskContextCommand.js +78 -27
  7. package/dist/commands/taskNextActionCommand.js +13 -2
  8. package/dist/commands/taskOverviewCommand.js +21 -5
  9. package/dist/commands/taskUpstreamCommands.js +136 -0
  10. package/dist/context/runContextPack.js +184 -17
  11. package/dist/controller/agentRuntimeObserver.js +31 -20
  12. package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
  13. package/dist/execution/candidateConvergence.js +623 -0
  14. package/dist/execution/executionGroup.js +255 -13
  15. package/dist/execution/executionHealth.js +324 -0
  16. package/dist/execution/resourceBroker.js +425 -0
  17. package/dist/executor/fileRoleLaunchPlanner.js +6 -9
  18. package/dist/executor/workspacePreflightClassification.js +117 -0
  19. package/dist/lifecycle/exactRunTerminalization.js +13 -2
  20. package/dist/lifecycle/taskRoleSessionReset.js +4 -2
  21. package/dist/repository/taskBaseFreshness.js +26 -1
  22. package/dist/repository/taskWorkspacePreparer.js +17 -1
  23. package/dist/review/reviewRound.js +27 -6
  24. package/dist/run/agentRun.js +2 -2
  25. package/dist/run/recoveryProjection.js +15 -0
  26. package/dist/runtime/runtimeContinuationProjection.js +7 -0
  27. package/dist/runtime/tmuxAdapters.js +8 -2
  28. package/dist/scheduler/actionability.js +169 -3
  29. package/dist/scheduler/activeTaskProgress.js +15 -10
  30. package/dist/scheduler/leaderWakeupProcessor.js +17 -1
  31. package/dist/scheduler/taskExecutionProjection.js +105 -8
  32. package/dist/scheduler/taskObservabilityProjection.js +282 -0
  33. package/dist/storage/migration/productionRegistry.js +14 -0
  34. package/dist/storage/sqliteStore.js +12 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/task/completionReadiness.js +1 -1
  37. package/dist/task/nextAction.js +314 -2
  38. package/dist/web/assets/client/components.js +116 -0
  39. package/dist/web/assets/client/i18n.js +66 -0
  40. package/dist/web/assets/client/view.js +15 -0
  41. package/dist/web/assets/styles/cards.js +23 -0
  42. package/dist/web/assets/styles/responsive.js +2 -0
  43. package/dist/web/webSnapshot.js +8 -2
  44. package/dist/workItem/workItem.js +262 -5
  45. package/i18n/README.zh-CN.md +42 -0
  46. package/package.json +1 -1
@@ -61,11 +61,29 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
61
61
  const task = requireTask(store, taskId);
62
62
  const git = options.git ?? newGitWorkspace();
63
63
  const workspace = store.getTaskWorkspace(taskId);
64
+ // Collect active Run workspace snapshots once so every Project entry can
65
+ // report whether a live Run is pinned to a different commit.
66
+ const activeRuns = store.listAgentRuns(taskId)
67
+ .filter((run) => run.status === "active" && run.workspace !== undefined);
64
68
  const entries = await Promise.all(task.projectBindings.map(async (binding) => {
65
69
  const project = requireProject(store, binding.projectId);
66
70
  const entry = workspace === null ? undefined : workspaceProjectEntry(workspace, project.id);
67
71
  const baseCommit = await resolveBaseCommit(git, project, binding, entry);
68
72
  const workspacePath = entry?.path ?? project.path;
73
+ let physicalHead;
74
+ try {
75
+ physicalHead = (await git.inspect(workspacePath)).baseCommit;
76
+ }
77
+ catch {
78
+ physicalHead = undefined;
79
+ }
80
+ // Find the first active Run whose workspace snapshot covers this Project.
81
+ const runSnapshot = activeRuns.find((run) => {
82
+ if (run.workspace === undefined)
83
+ return false;
84
+ const runEntry = workspaceProjectEntry(run.workspace, project.id);
85
+ return runEntry !== undefined;
86
+ });
69
87
  const provenance = latestBaseProvenance(store.listEvents(taskId), project.id);
70
88
  const tracked = await resolveTracked(git, project, workspacePath, options.refresh === true);
71
89
  const status = await classifyStatus(git, workspacePath, project, baseCommit, tracked);
@@ -80,6 +98,13 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
80
98
  source: tracked?.source ?? (project.remoteUrl === undefined ? "not-applicable" : "compatibility-projection"),
81
99
  workspacePath,
82
100
  workspaceClean,
101
+ ...(physicalHead === undefined ? {} : { physicalHead }),
102
+ ...(runSnapshot?.workspace === undefined
103
+ ? {}
104
+ : {
105
+ runSnapshotCommit: workspaceProjectEntry(runSnapshot.workspace, project.id)?.baseCommit,
106
+ runSnapshotRunId: runSnapshot.id
107
+ }),
83
108
  ...(tracked === undefined ? {} : {
84
109
  trackedRef: tracked.ref,
85
110
  trackedCommit: tracked.commit,
@@ -136,7 +161,7 @@ export function renderTaskBaseFreshnessReport(report) {
136
161
  `Task base freshness: ${report.taskId} (source: ${report.refreshed ? "remote refresh" : "local tracking"})`
137
162
  ];
138
163
  for (const entry of report.entries) {
139
- lines.push(`- ${entry.directory}: ${entry.projectId} @ ${entry.baseCommit}`, ` status: ${entry.status}; workspace: ${entry.workspaceClean === null ? "unknown" : entry.workspaceClean ? "clean" : "dirty"}`, ` observed remote: ${entry.observedRemoteUrl ?? "-"}`, ` tracked: ${entry.trackedCommit ?? "-"}${entry.trackedRef === undefined ? "" : ` (${entry.trackedRef})`}`, ` observed: ${entry.observedTrackingCommit ?? "-"}${entry.observedAt === undefined ? "" : ` at ${entry.observedAt}`}`);
164
+ lines.push(`- ${entry.directory}: ${entry.projectId} @ ${entry.baseCommit}`, ` status: ${entry.status}; workspace: ${entry.workspaceClean === null ? "unknown" : entry.workspaceClean ? "clean" : "dirty"}`, ` physical HEAD: ${entry.physicalHead ?? "-"}`, ` Run snapshot: ${entry.runSnapshotCommit ?? "-"}${entry.runSnapshotRunId === undefined ? "" : ` (${entry.runSnapshotRunId})`}`, ` observed remote: ${entry.observedRemoteUrl ?? "-"}`, ` tracked: ${entry.trackedCommit ?? "-"}${entry.trackedRef === undefined ? "" : ` (${entry.trackedRef})`}`, ` observed: ${entry.observedTrackingCommit ?? "-"}${entry.observedAt === undefined ? "" : ` at ${entry.observedAt}`}`);
140
165
  if (entry.risk !== undefined)
141
166
  lines.push(` risk: ${entry.risk}`);
142
167
  if (entry.error !== undefined)
@@ -1028,7 +1028,7 @@ export class FileTaskWorkspacePreparer {
1028
1028
  : this.store.getReviewRound(taskId, lineage.reviewRoundId)?.executionGroup;
1029
1029
  const lane = group?.lanes.find(({ id }) => id === executionLaneId);
1030
1030
  if (group === undefined
1031
- || lane === undefined || !["completed", "failed", "yielded"].includes(lane.status)) {
1031
+ || lane === undefined || !["completed", "failed", "yielded", "skipped"].includes(lane.status)) {
1032
1032
  throw new Error(`Execution Lane is not terminally resolved: ${taskId}/${executionLaneId}.`);
1033
1033
  }
1034
1034
  const workspace = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane"
@@ -1895,6 +1895,22 @@ export class FileTaskWorkspacePreparer {
1895
1895
  }
1896
1896
  async #rebuildTaskWorkspaceLocked(task, options) {
1897
1897
  if (task.workspaceIdentity !== undefined) {
1898
+ // Quick Win (EXE-08): an identity-bearing Task cannot silently "resume"
1899
+ // when --latest is requested. The resume branch only cleans legacy
1900
+ // refs and reclaims orphans; it never re-pins the baseline. Reporting
1901
+ // success here would manufacture a split-brain state where the physical
1902
+ // Task branch moves but the binding, ManagedWorkspace, and Run snapshot
1903
+ // stay on the old commit. Refuse explicitly and point at the correct
1904
+ // tools instead.
1905
+ if (options.latestRemote === true) {
1906
+ throw new Error(`Task ${task.id} already owns a workspace identity; `
1907
+ + "`task rebuild --latest` cannot re-pin its baseline in place. "
1908
+ + "Use `yui task base status " + task.id + "` to inspect the current "
1909
+ + "binding, ManagedWorkspace, physical HEAD, and Run snapshot. "
1910
+ + "For an Active Task, integrate upstream changes through the "
1911
+ + "normal Leader-driven integration path; for a Draft Task with no "
1912
+ + "execution evidence, recreate the Task workspace.");
1913
+ }
1898
1914
  const current = this.store.getTaskWorkspace(task.id);
1899
1915
  if (current !== null && current.owner.type === "task") {
1900
1916
  await ensureWorkspaceView(current.root, current.entries);
@@ -138,7 +138,7 @@ export function finishReviewRound(round, status, summary, now, result = {}) {
138
138
  * returns to pending so infrastructure retries do not manufacture a new
139
139
  * semantic ReviewRound or duplicate findings.
140
140
  */
141
- export function retryTaskReviewRound(round) {
141
+ export function retryTaskReviewRound(round, executionLaneId) {
142
142
  validateReviewRound(round);
143
143
  if ((round.scope ?? "work-item") !== "task") {
144
144
  throw new Error(`Only a Task-final ReviewRound can be retried in place: ${round.id}.`);
@@ -148,7 +148,7 @@ export function retryTaskReviewRound(round) {
148
148
  }
149
149
  const retryExecutionGroup = round.executionGroup === undefined
150
150
  ? undefined
151
- : retryReviewExecutionGroup(round);
151
+ : retryReviewExecutionGroup(round, executionLaneId);
152
152
  return validateReviewRound({
153
153
  schemaVersion: round.schemaVersion,
154
154
  id: round.id,
@@ -186,11 +186,32 @@ export function retryTaskReviewRound(round) {
186
186
  createdAt: round.createdAt
187
187
  });
188
188
  }
189
- function retryReviewExecutionGroup(round) {
189
+ /** Keep a running panel Round active while retrying only its exact failed Lane. */
190
+ export function retryRunningReviewExecutionLane(round, executionLaneId, runId, now) {
191
+ validateReviewRound(round);
192
+ if (round.status !== "running" || round.executionGroup === undefined) {
193
+ throw new Error(`ReviewRound ${round.id} has no running ExecutionGroup.`);
194
+ }
195
+ const lane = round.executionGroup.lanes.find(({ id }) => id === executionLaneId);
196
+ if (lane === undefined || lane.status !== "failed" || lane.runId !== runId) {
197
+ throw new Error(`Review retry does not target the current failed Lane attempt: `
198
+ + `${round.executionGroup.id}/${executionLaneId}/${runId}.`);
199
+ }
200
+ return updateReviewExecutionGroup(round, retryReviewExecutionGroup(round, executionLaneId, now));
201
+ }
202
+ function retryReviewExecutionGroup(round, executionLaneId, retryAt) {
190
203
  const previous = round.executionGroup;
191
- const attemptTime = Date.parse(round.endedAt ?? round.createdAt);
192
- const now = new Date(attemptTime);
193
- const lanes = previous.lanes.map((lane) => (resetReviewExecutionLane(previous, lane.id, now)));
204
+ if (executionLaneId !== undefined) {
205
+ const exactLane = previous.lanes.find(({ id }) => id === executionLaneId);
206
+ if (exactLane === undefined || exactLane.status !== "failed") {
207
+ throw new Error(`Review retry Lane is not failed: ${previous.id}/${executionLaneId}.`);
208
+ }
209
+ }
210
+ const now = retryAt ?? new Date(Date.parse(round.endedAt ?? round.createdAt));
211
+ const lanes = previous.lanes.map((lane) => (lane.status === "failed"
212
+ && (executionLaneId === undefined || lane.id === executionLaneId)
213
+ ? resetReviewExecutionLane(previous, lane.id, now)
214
+ : lane));
194
215
  return validateExecutionGroup({
195
216
  ...previous,
196
217
  lanes,
@@ -175,8 +175,8 @@ export function validateAgentRun(run) {
175
175
  }
176
176
  }
177
177
  if (run.purpose === "review") {
178
- if (run.workItemId === undefined || run.reviewRoundId === undefined) {
179
- throw new Error("A review Agent run requires WorkItem and ReviewRound references.");
178
+ if (run.reviewRoundId === undefined) {
179
+ throw new Error("A review Agent run requires a ReviewRound reference.");
180
180
  }
181
181
  if (run.workspace === undefined
182
182
  || !((run.workspace.owner.type === "review-round"
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
3
+ import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
3
4
  export const RUN_RECOVERY_ACTIONS = [
4
5
  "diagnose",
5
6
  "retry",
@@ -25,6 +26,20 @@ export function readRunRecoveryFacts(store, taskId, runId) {
25
26
  latestProviderObservation: latestRunProviderObservation(store.listEvents(taskId), runId)
26
27
  };
27
28
  }
29
+ /**
30
+ * Resolve the exact live-Run recovery plans referenced by Lane health. Failed
31
+ * terminal Lanes use `task run retry` directly and therefore need no live-Run
32
+ * recovery projection here.
33
+ */
34
+ export function projectExecutionLaneRunRecoveries(store, taskId, groups) {
35
+ const runIds = new Set(actionableExecutionLaneRecoveries(groups).flatMap((lane) => (lane.runId === undefined || lane.recovery === "retry-new-agent-run"
36
+ ? []
37
+ : [lane.runId])));
38
+ return [...runIds].flatMap((runId) => {
39
+ const facts = readRunRecoveryFacts(store, taskId, runId);
40
+ return facts === null ? [] : [projectRunRecovery(facts)];
41
+ });
42
+ }
28
43
  /**
29
44
  * Latest Provider observation for a Run. Provider timestamps are evidence:
30
45
  * they explain why a stale fence was supplied but never authorize recovery.
@@ -32,3 +32,10 @@ export function projectProviderContinuations(events) {
32
32
  export function blockingProviderContinuations(events) {
33
33
  return projectProviderContinuations(events).filter((entry) => (continuationOwnsWriterUmbrella(entry) || entry.identityConflict));
34
34
  }
35
+ /** Exact Run-scoped writer fence shared by terminalization and retry paths. */
36
+ export function runOwnsBlockingProviderContinuation(events, owner) {
37
+ return blockingProviderContinuations(events).some((continuation) => (continuation.taskId === owner.taskId
38
+ && continuation.roleName === owner.roleName
39
+ && continuation.runId === owner.runId
40
+ && continuation.identity.accountScope === owner.agentId));
41
+ }
@@ -281,11 +281,14 @@ export class TmuxSessionHost {
281
281
  return binding;
282
282
  }
283
283
  const broker = launchBrokerForHome(yuiHome);
284
+ const sessionManifest = planned.launch.env.YUI_SESSION_MANIFEST;
284
285
  const frozenControlPlane = planned.launch.env[YUI_CONTROL_PLANE_DESCRIPTOR];
285
286
  const frozenTaskRuntime = planned.launch.env[YUI_TASK_RUNTIME_DESCRIPTOR];
286
287
  if (request.owner.scope === "task" && request.runId !== undefined
287
- && (frozenControlPlane === undefined || frozenTaskRuntime === undefined)) {
288
- throw new Error("Managed Task Agent Host launch is missing its frozen control descriptors.");
288
+ && (sessionManifest === undefined
289
+ || frozenControlPlane === undefined
290
+ || frozenTaskRuntime === undefined)) {
291
+ throw new Error("Managed Task Agent Host launch is missing its Session Manifest or frozen control descriptors.");
289
292
  }
290
293
  const reservation = broker.reserve(Object.freeze({
291
294
  schemaVersion: 1,
@@ -324,6 +327,9 @@ export class TmuxSessionHost {
324
327
  ...(planned.launch.env.YUI_WORKSPACE === undefined
325
328
  ? {}
326
329
  : { YUI_WORKSPACE: planned.launch.env.YUI_WORKSPACE }),
330
+ ...(sessionManifest === undefined
331
+ ? {}
332
+ : { YUI_SESSION_MANIFEST: sessionManifest }),
327
333
  ...(frozenControlPlane === undefined
328
334
  ? {}
329
335
  : { [YUI_CONTROL_PLANE_DESCRIPTOR]: frozenControlPlane }),
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
2
3
  import { isDurableJobTerminal } from "../job/durableJob.js";
3
4
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
5
+ import { executionStageSpendClosed, observedExecutionResourceUsage, projectExecutionStageResources } from "../execution/resourceBroker.js";
4
6
  /**
5
7
  * Canonical SHA-256 digest over the normalized actionable facts. Pure and
6
8
  * deterministic: the same facts always produce the same digest regardless of
@@ -19,6 +21,92 @@ export function computeActionabilityDigest(input) {
19
21
  }
20
22
  const TERMINAL_WORK_ITEM_STATUSES = new Set(["completed", "retired"]);
21
23
  const CONSUMED_INTEGRATION_STATUSES = new Set(["committed", "superseded"]);
24
+ /** Exact durable Broker queue shared by admission and wake actionability. */
25
+ export function projectQueuedResourceLaneIdentities(store, now) {
26
+ return store.listActiveTaskIds().flatMap((taskId) => {
27
+ const taskRuns = store.listAgentRuns(taskId);
28
+ const taskEvents = store.listEvents(taskId);
29
+ const workItemLanes = store.listWorkItems(taskId).flatMap((item) => {
30
+ if (item.status !== "running")
31
+ return [];
32
+ const group = currentWorkItemExecutionGroup(item);
33
+ if (group === undefined || group.resolution !== undefined)
34
+ return [];
35
+ if (group.stage !== undefined) {
36
+ const resources = projectExecutionStageResources({
37
+ group,
38
+ stageGroups: item.executionGroups,
39
+ usage: observedExecutionResourceUsage({
40
+ group,
41
+ stageGroups: item.executionGroups,
42
+ runs: taskRuns,
43
+ events: taskEvents
44
+ }),
45
+ now
46
+ });
47
+ if (executionStageSpendClosed(resources))
48
+ return [];
49
+ }
50
+ return group.lanes.flatMap((lane) => {
51
+ if (lane.status !== "pending"
52
+ || lane.effective === undefined
53
+ || lane.runId !== undefined)
54
+ return [];
55
+ return [{
56
+ taskId,
57
+ workItemId: item.id,
58
+ executionGroupId: group.id,
59
+ executionLaneId: lane.id,
60
+ providerId: lane.effective.adapterId,
61
+ agentId: lane.effective.agentId,
62
+ ...(lane.effective.model === undefined ? {} : { model: lane.effective.model }),
63
+ requestedAt: lane.updatedAt
64
+ }];
65
+ });
66
+ });
67
+ const reviewLanes = store.listReviewRounds(taskId).flatMap((round) => {
68
+ if (round.status !== "pending" && round.status !== "running")
69
+ return [];
70
+ const group = round.executionGroup;
71
+ if (group === undefined || group.resolution !== undefined)
72
+ return [];
73
+ return group.lanes.flatMap((lane) => {
74
+ if (lane.status !== "pending"
75
+ || lane.effective === undefined
76
+ || lane.runId !== undefined)
77
+ return [];
78
+ return [{
79
+ taskId,
80
+ ...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
81
+ executionGroupId: group.id,
82
+ executionLaneId: lane.id,
83
+ providerId: lane.effective.adapterId,
84
+ agentId: lane.effective.agentId,
85
+ ...(lane.effective.model === undefined ? {} : { model: lane.effective.model }),
86
+ requestedAt: lane.updatedAt
87
+ }];
88
+ });
89
+ });
90
+ return [...workItemLanes, ...reviewLanes];
91
+ });
92
+ }
93
+ /**
94
+ * True only when the Task still owns a Resource-Broker-queued Lane that a
95
+ * later Leader dispatch may start. Terminal owners retain their immutable
96
+ * Group history, but can never reserve live queue capacity.
97
+ */
98
+ export function hasDispatchableQueuedResourceLane(store, taskId) {
99
+ const workItemGroups = (store.listWorkItems?.(taskId) ?? [])
100
+ .filter(({ status }) => status === "running")
101
+ .flatMap(({ executionGroups }) => executionGroups);
102
+ const reviewGroups = (store.listReviewRounds?.(taskId) ?? [])
103
+ .filter(({ status }) => status === "pending" || status === "running")
104
+ .flatMap(({ executionGroup }) => executionGroup === undefined ? [] : [executionGroup]);
105
+ return [...workItemGroups, ...reviewGroups].some((group) => (group.resolution === undefined
106
+ && group.lanes.some((lane) => (lane.status === "pending"
107
+ && lane.effective !== undefined
108
+ && lane.runId === undefined))));
109
+ }
22
110
  /**
23
111
  * Fold a Task's durable records into the normalized actionable facts. This
24
112
  * function only reads; it never starts a Controller, queues a wake, writes a
@@ -41,7 +129,7 @@ const CONSUMED_INTEGRATION_STATUSES = new Set(["committed", "superseded"]);
41
129
  * an unchanged blocker, read-only status requests, and waits already owned by
42
130
  * an active Worker/Reviewer/Job (the active-Run facts above capture ownership).
43
131
  */
44
- export function collectTaskActionability(store, taskId) {
132
+ export function collectTaskActionability(store, taskId, now = new Date()) {
45
133
  const task = store.getTask(taskId);
46
134
  if (task === null) {
47
135
  throw new Error(`Task not found for actionability projection: ${taskId}.`);
@@ -59,15 +147,93 @@ export function collectTaskActionability(store, taskId) {
59
147
  ].join("|")
60
148
  });
61
149
  }
62
- for (const item of store.listWorkItems?.(taskId) ?? []) {
150
+ const workItems = store.listWorkItems?.(taskId) ?? [];
151
+ const reviewRounds = store.listReviewRounds?.(taskId) ?? [];
152
+ for (const item of workItems) {
63
153
  if (TERMINAL_WORK_ITEM_STATUSES.has(item.status))
64
154
  continue;
65
155
  facts.push({
66
156
  key: `work-item:${item.id}`,
67
157
  value: `${item.status}|${item.updatedAt}`
68
158
  });
159
+ for (const group of item.executionGroups) {
160
+ if (group.resolution !== undefined || group.stage?.resources === undefined)
161
+ continue;
162
+ facts.push({
163
+ key: `resource-deadline:${group.id}`,
164
+ value: now.getTime() >= Date.parse(group.stage.resources.deadlineAt)
165
+ ? "reached"
166
+ : "open"
167
+ });
168
+ }
169
+ }
170
+ // A Resource-Broker-queued Lane is durable but deliberately has no active
171
+ // Run. Include the global capacity and fair-queue projection only for Tasks
172
+ // that are actually queued, so either capacity or an older reservation being
173
+ // released changes their digest without polling or a second scheduler.
174
+ const queueProjectionAvailable = store.listActiveTaskIds !== undefined
175
+ && store.listWorkItems !== undefined
176
+ && store.listReviewRounds !== undefined
177
+ && store.listEvents !== undefined;
178
+ const queuedResourceLanes = queueProjectionAvailable
179
+ ? projectQueuedResourceLaneIdentities({
180
+ listActiveTaskIds: () => store.listActiveTaskIds(),
181
+ listAgentRuns: (candidateTaskId) => store.listAgentRuns(candidateTaskId),
182
+ listWorkItems: (candidateTaskId) => store.listWorkItems(candidateTaskId),
183
+ listReviewRounds: (candidateTaskId) => store.listReviewRounds(candidateTaskId),
184
+ listEvents: (candidateTaskId) => store.listEvents(candidateTaskId)
185
+ }, now)
186
+ : [];
187
+ const hasQueuedResourceLane = queueProjectionAvailable
188
+ ? queuedResourceLanes.some((lane) => lane.taskId === taskId)
189
+ : hasDispatchableQueuedResourceLane(store, taskId);
190
+ if (hasQueuedResourceLane && store.listActiveTaskIds !== undefined) {
191
+ const activeResourceScopes = store.listActiveTaskIds().flatMap((activeTaskId) => (operationalTaskRecords(store.listAgentRuns(activeTaskId), store.listEvents?.(activeTaskId) ?? [], "agent-run").flatMap((run) => (run.status !== "active"
192
+ || run.executionGroupId === undefined
193
+ || run.executionLaneId === undefined
194
+ ? []
195
+ : [
196
+ "home",
197
+ `task:${activeTaskId}`,
198
+ ...(run.workItemId === undefined
199
+ ? []
200
+ : [`work-item:${activeTaskId}/${run.workItemId}`]),
201
+ `group:${activeTaskId}/${run.executionGroupId}`,
202
+ `provider:${run.effective.adapterId}`,
203
+ `agent:${run.effective.agentId}`,
204
+ `model:${run.effective.adapterId}/${run.effective.model ?? "default"}`
205
+ ]))));
206
+ const activeResourceCounts = new Map();
207
+ for (const scope of activeResourceScopes) {
208
+ activeResourceCounts.set(scope, (activeResourceCounts.get(scope) ?? 0) + 1);
209
+ }
210
+ facts.push({
211
+ key: "resource-broker:active-capacity",
212
+ value: [...activeResourceCounts]
213
+ .sort(([left], [right]) => left.localeCompare(right))
214
+ .map(([scope, count]) => `${scope}=${count}`)
215
+ .join("|")
216
+ });
217
+ if (queueProjectionAvailable) {
218
+ facts.push({
219
+ key: "resource-broker:fair-queue",
220
+ value: queuedResourceLanes
221
+ .map((lane) => [
222
+ lane.requestedAt,
223
+ lane.taskId,
224
+ lane.workItemId ?? "",
225
+ lane.executionGroupId,
226
+ lane.executionLaneId,
227
+ lane.providerId,
228
+ lane.agentId,
229
+ lane.model ?? "default"
230
+ ].join("|"))
231
+ .sort()
232
+ .join("\n")
233
+ });
234
+ }
69
235
  }
70
- for (const round of store.listReviewRounds?.(taskId) ?? []) {
236
+ for (const round of reviewRounds) {
71
237
  if (round.status === "completed")
72
238
  continue;
73
239
  facts.push({
@@ -2,7 +2,7 @@ import { selectedActiveSchedulerTasks, isSchedulerTaskWorkspaceReady } from "./p
2
2
  import { queueLeaderWakeup } from "./wakeupQueue.js";
3
3
  import { wakeReason } from "./wakeReason.js";
4
4
  import { projectTaskExecution } from "./taskExecutionProjection.js";
5
- import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake } from "./actionability.js";
5
+ import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake, hasDispatchableQueuedResourceLane } from "./actionability.js";
6
6
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
7
7
  /**
8
8
  * Repairs an active Task that has no durable owner capable of advancing it.
@@ -26,6 +26,7 @@ export function repairOrphanedActiveTasks(store, now, selection) {
26
26
  return run === null ? [] : [run];
27
27
  });
28
28
  const hasInFlightTurn = roles.some((role) => store.hasInFlightTurn(task.id, role.name));
29
+ const hasLeaderInFlightTurn = store.hasInFlightTurn(task.id, "leader");
29
30
  const leaderTarget = { kind: "role", taskId: task.id, roleName: "leader" };
30
31
  const leaderMailbox = store.getWorkMailbox(leaderTarget);
31
32
  const projection = projectTaskExecution({
@@ -37,19 +38,23 @@ export function repairOrphanedActiveTasks(store, now, selection) {
37
38
  leaderFailure: store.getLeaderFailure(task.id),
38
39
  operatorNotification: store.getOperatorNotification(task.id)
39
40
  });
40
- if (hasInFlightTurn
41
+ const queuedAlongsideActiveSibling = hasDispatchableQueuedResourceLane(store, task.id)
42
+ && !activeRuns.some(({ roleName }) => roleName === "leader")
43
+ && activeRuns.some(({ roleName }) => roleName !== "leader")
44
+ && projection.status === "waiting-on-agents";
45
+ if ((queuedAlongsideActiveSibling ? hasLeaderInFlightTurn : hasInFlightTurn)
41
46
  || store.hasOpenInputRequest(task.id)
42
47
  || store.getLeaderFailure(task.id) !== null
43
48
  || store.getOperatorNotification(task.id) !== null
44
- || projection.status !== "needs-leader-action"
49
+ || (projection.status !== "needs-leader-action" && !queuedAlongsideActiveSibling)
45
50
  || hasUnclaimedLeaderWork(store, task.id, leaderMailbox)) {
46
51
  continue;
47
52
  }
48
- // Issue 05: digest-based admission. Only the "no-executor" orphan path
49
- // reaches here; every other needs-leader-action state already has a
50
- // durable owner (candidate, integration, or pending wake) and is exempt.
51
- if (projection.reason === "no-executor") {
52
- if (admitOrphanWake(store, task.id) === "suppress")
53
+ // Digest admission covers both an ownerless Task and a resource-queued
54
+ // Task with active siblings. The latter wakes only when capacity/deadline
55
+ // actionability changes, so repeated full scans remain silent.
56
+ if (projection.reason === "no-executor" || queuedAlongsideActiveSibling) {
57
+ if (admitOrphanWake(store, task.id, now) === "suppress")
53
58
  continue;
54
59
  }
55
60
  const taskWorkspace = store.getTaskWorkspace(task.id);
@@ -76,10 +81,10 @@ export function repairOrphanedActiveTasks(store, now, selection) {
76
81
  * `"suppress"` without writing anything when the digest is unchanged since
77
82
  * the last waiting/blocked Leader Run. Computation errors fail open.
78
83
  */
79
- function admitOrphanWake(store, taskId) {
84
+ function admitOrphanWake(store, taskId, now) {
80
85
  let digest;
81
86
  try {
82
- const input = collectTaskActionability(store, taskId);
87
+ const input = collectTaskActionability(store, taskId, now);
83
88
  digest = computeActionabilityDigest(input);
84
89
  }
85
90
  catch (error) {
@@ -126,7 +126,23 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
126
126
  const resumableSession = hasNativeSession(existingSession)
127
127
  && existingSession.status !== "stopped"
128
128
  && existingSession.status !== "broken";
129
- const mode = resumableSession && compatibleSession ? "resume" : "new";
129
+ // Quick Win (EXE-03): a resume Run that failed before durable Provider
130
+ // acceptance must not be retried against the same native Session. The
131
+ // Session is proven unusable for this delivery; the next launch must be
132
+ // a fresh Session after exact cleanup/reset. The guard only applies
133
+ // while the failed resume Run is the *latest* Run for the Role: once a
134
+ // newer Run exists (the fresh-Session launch), the historical failure
135
+ // is stale and must not permanently disable resume for the Role.
136
+ const latestRoleRun = store.listAgentRuns?.(task.id)
137
+ ?.filter((run) => run.roleName === role.name)
138
+ .at(-1);
139
+ const failedResumeWithoutAcceptance = latestRoleRun !== undefined
140
+ && latestRoleRun.mode === "resume"
141
+ && latestRoleRun.status === "failed"
142
+ && latestRoleRun.deliveredAt === undefined;
143
+ const mode = resumableSession && compatibleSession && !failedResumeWithoutAcceptance
144
+ ? "resume"
145
+ : "new";
130
146
  const runId = store.peekNextAgentRunId(task.id);
131
147
  const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
132
148
  const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);