@zq-silk/yui 0.15.11 → 0.15.12

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 (101) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/cli/commandCatalog.js +38 -10
  6. package/dist/cli/interactionPolicy.js +4 -0
  7. package/dist/cli/managedDiagnostics.js +1 -1
  8. package/dist/cli.js +46 -15
  9. package/dist/commands/executionAuditCommands.js +10 -0
  10. package/dist/commands/globalRoleCommands.js +27 -2
  11. package/dist/commands/projectCommands.js +44 -15
  12. package/dist/commands/taskCommands.js +95 -33
  13. package/dist/commands/taskIntegrationCommands.js +3 -1
  14. package/dist/commands/taskOverviewCommand.js +4 -3
  15. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  16. package/dist/commands/taskPublicationCommands.js +11 -2
  17. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  18. package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
  19. package/dist/context/runContextPack.js +3 -0
  20. package/dist/context/taskCatalog.js +187 -0
  21. package/dist/context/taskContext.js +17 -4
  22. package/dist/controller/controller.js +11 -2
  23. package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
  24. package/dist/controller/globalInputDelivery.js +13 -0
  25. package/dist/controller/providerRetryAdmission.js +100 -0
  26. package/dist/controller/providerRetryDelivery.js +218 -0
  27. package/dist/controller/runtime.js +36 -1
  28. package/dist/integration/gitIntegrationService.js +19 -6
  29. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  30. package/dist/message/globalProviderRetry.js +15 -0
  31. package/dist/observability/executionAudit.js +19 -0
  32. package/dist/repository/gitWorkspace.js +371 -105
  33. package/dist/repository/projectMaintenanceLock.js +75 -18
  34. package/dist/repository/taskWorkspaceCoordinator.js +71 -124
  35. package/dist/repository/taskWorkspacePreparer.js +86 -24
  36. package/dist/repository/workspaceCleanupInspection.js +187 -0
  37. package/dist/runtime/agentError.js +5 -3
  38. package/dist/runtime/agentHost.js +28 -11
  39. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  40. package/dist/runtime/codexAppServerRuntime.js +34 -3
  41. package/dist/runtime/providerControl.js +5 -1
  42. package/dist/runtime/providerRetry.js +198 -0
  43. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  44. package/dist/runtime/sessionTokenMetrics.js +15 -5
  45. package/dist/runtime/structuredProviderHost.js +6 -2
  46. package/dist/runtime/taskUsageMetrics.js +275 -0
  47. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  48. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  49. package/dist/scheduler/taskExecutionProjection.js +26 -5
  50. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  51. package/dist/storage/sqliteSchema.js +31 -0
  52. package/dist/storage/sqliteStore.js +17 -0
  53. package/dist/storage/storageVersions.js +1 -1
  54. package/dist/storage/storeRpc.js +1 -0
  55. package/dist/storage/taskCatalog.js +123 -0
  56. package/dist/storage/taskStore.js +2 -0
  57. package/dist/task/archiveDiagnostics.js +1 -0
  58. package/dist/task/archivePreflight.js +124 -0
  59. package/dist/task/publicationAdoption.js +56 -0
  60. package/dist/task/publicationReference.js +10 -0
  61. package/dist/task/remoteDelivery.js +31 -16
  62. package/dist/web/assets/client/app.js +92 -17
  63. package/dist/web/assets/client/components.js +55 -13
  64. package/dist/web/assets/client/i18n.js +72 -4
  65. package/dist/web/assets/client/taskSurface.js +2 -1
  66. package/dist/web/assets/client/view.js +35 -7
  67. package/dist/web/assets/shell.js +6 -0
  68. package/dist/web/assets/styles/layout.js +7 -0
  69. package/dist/web/webServer.js +14 -3
  70. package/dist/web/webSnapshot.js +12 -3
  71. package/dist/workspace/cleanupInspection.js +63 -0
  72. package/dist/workspace/workItemChangeSetManager.js +110 -50
  73. package/docs/agent-result-consumption.md +4 -0
  74. package/docs/agent-result-consumption.zh-CN.md +3 -0
  75. package/docs/agent-runtime-drivers.md +7 -0
  76. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  77. package/docs/architecture/README.md +2 -0
  78. package/docs/architecture/README.zh-CN.md +3 -1
  79. package/docs/architecture/capabilities-and-resources.md +30 -5
  80. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  81. package/docs/managed-turn-and-session-runtime.md +47 -0
  82. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  83. package/docs/observability/README.md +62 -0
  84. package/docs/observability/README.zh-CN.md +47 -0
  85. package/docs/project-refresh.md +77 -0
  86. package/docs/project-refresh.zh-CN.md +59 -0
  87. package/docs/provider-retry.md +70 -0
  88. package/docs/task-delivery.md +133 -13
  89. package/docs/task-delivery.zh-CN.md +99 -10
  90. package/docs/task-discovery.md +102 -0
  91. package/docs/task-discovery.zh-CN.md +86 -0
  92. package/docs/testing/verification-levels.md +16 -0
  93. package/docs/testing/verification-levels.zh-CN.md +13 -1
  94. package/i18n/README.zh-CN.md +13 -7
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +3 -2
  97. package/skills/yui-operator/SKILL.md +13 -2
  98. package/skills/yui-reviewer/SKILL.md +4 -0
  99. package/skills/yui-runtime/SKILL.md +17 -2
  100. package/skills/yui-runtime/references/publication.md +26 -4
  101. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -0,0 +1,275 @@
1
+ import { builtinDriverIdForAdapter } from "./builtinAgentDrivers.js";
2
+ import { isRuntimeTokenEvidence, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
3
+ import { projectSessionTokenObservations, unobservedSessionTokenMetrics } from "./sessionTokenMetrics.js";
4
+ /**
5
+ * Pure lifetime projection. No transcript reads, collection, persistence,
6
+ * lifecycle decisions, guessed Run identities, or cumulative counter allocation.
7
+ */
8
+ export function projectTaskUsageMetrics(input) {
9
+ const now = input.now ?? new Date();
10
+ const events = input.events.filter((event) => event.taskId === input.task.id);
11
+ const observations = events.flatMap((event) => {
12
+ const observation = runtimeObservationFromTaskEvent(event);
13
+ return observation !== null
14
+ && observation.fence.taskId === input.task.id
15
+ && Date.parse(observation.receivedAt) <= now.getTime()
16
+ ? [observation] : [];
17
+ }).sort(compare);
18
+ const runs = new Map(input.runs.filter((run) => run.taskId === input.task.id).map((run) => [run.id, run]));
19
+ const inScope = (observation) => {
20
+ if (input.workItemId === undefined)
21
+ return true;
22
+ const run = exactRun(observation, runs);
23
+ return run?.workItemId === input.workItemId;
24
+ };
25
+ const scoped = observations.filter(inScope);
26
+ const buckets = new Map();
27
+ for (const observation of observations) {
28
+ const identity = sessionIdentity(observation);
29
+ if (identity === null)
30
+ continue;
31
+ const key = identityKey(identity);
32
+ const bucket = buckets.get(key) ?? { identity, observations: [] };
33
+ bucket.observations.push(observation);
34
+ buckets.set(key, bucket);
35
+ }
36
+ const resourceOwners = new Map();
37
+ for (const { identity, observations: all } of buckets.values()) {
38
+ if (!all.some((observation) => !child(observation) && usageAuthority(observation)))
39
+ continue;
40
+ const resource = resourceKey(identity);
41
+ const owners = resourceOwners.get(resource) ?? new Set();
42
+ owners.add(identity.roleName);
43
+ resourceOwners.set(resource, owners);
44
+ }
45
+ const sessions = [...buckets.values()]
46
+ .sort((a, b) => identityKey(a.identity).localeCompare(identityKey(b.identity)))
47
+ .flatMap(({ identity, observations: all }) => {
48
+ if (!all.some(inScope))
49
+ return [];
50
+ const main = all.filter((observation) => !child(observation) && usageAuthority(observation));
51
+ const evidence = main.filter(isRuntimeTokenEvidence);
52
+ const metrics = projectSessionTokenObservations(main, identity);
53
+ let displayedMetrics = input.workItemId === undefined ? metrics : unobservedSessionTokenMetrics(identity);
54
+ const reasons = [];
55
+ if (all.some(child))
56
+ reasons.push("child-usage-overlap-unproven");
57
+ if (evidence.some(({ payload }) => payload.observationQuality === "partial"
58
+ || payload.observationQuality === "unavailable"))
59
+ reasons.push("source-coverage-partial");
60
+ let tokens = attributableTokens(evidence, metrics);
61
+ if (input.workItemId !== undefined) {
62
+ // Establish semantics and request ownership before filtering. Filtering
63
+ // first could hide a mixed stream or allocate a revised request twice.
64
+ if (metrics.cumulativeTotal.status !== "observed") {
65
+ tokens = metric(null, ["session-usage-unavailable"]);
66
+ }
67
+ else if (evidence.some(({ payload }) => payload.usage?.semantics === "cumulative-session")) {
68
+ tokens = metric(null, ["cumulative-not-allocatable-to-work-item"]);
69
+ }
70
+ else {
71
+ const requests = new Map();
72
+ for (const observation of evidence) {
73
+ if (observation.payload.usage?.semantics !== "request-context")
74
+ continue;
75
+ const key = observation.payload.activityId ?? observation.semanticKey;
76
+ requests.set(key, [...(requests.get(key) ?? []), observation]);
77
+ }
78
+ const selected = [];
79
+ for (const versions of requests.values()) {
80
+ if (!versions.some(inScope))
81
+ continue;
82
+ const owners = new Set(versions.map((observation) => exactRun(observation, runs)?.id));
83
+ if (owners.size !== 1 || owners.has(undefined) || !versions.every(inScope)) {
84
+ reasons.push("request-run-attribution-conflict");
85
+ }
86
+ else {
87
+ selected.push(...versions);
88
+ }
89
+ }
90
+ displayedMetrics = projectSessionTokenObservations(selected, identity);
91
+ const subtotal = displayedMetrics.cumulativeTotal;
92
+ tokens = metric(subtotal.status === "observed" ? subtotal.totalTokens : null, subtotal.status === "observed" ? [] : ["no-attributable-request-usage"]);
93
+ }
94
+ }
95
+ if ((resourceOwners.get(resourceKey(identity))?.size ?? 0) > 1) {
96
+ tokens = metric(null, ["native-session-role-overlap"]);
97
+ displayedMetrics = unobservedSessionTokenMetrics(identity);
98
+ }
99
+ tokens = metric(tokens.value, [...tokens.reasons, ...reasons]);
100
+ return [{
101
+ identity,
102
+ semantics: [...new Set(evidence.flatMap(({ payload }) => payload.usage === undefined ? [] : [payload.usage.semantics]))].sort(),
103
+ sources: [...new Set(evidence.map(({ authority, payload }) => payload.observerSource?.sourceId ?? authority))].sort(),
104
+ observedThrough: all.filter(inScope).at(-1).receivedAt,
105
+ tokens,
106
+ metrics: displayedMetrics
107
+ }];
108
+ });
109
+ const reasons = [];
110
+ if (scoped.some((observation) => sessionIdentity(observation) === null))
111
+ reasons.push("session-identity-missing");
112
+ for (const run of runs.values()) {
113
+ if (input.workItemId !== undefined && run.workItemId !== input.workItemId)
114
+ continue;
115
+ if (!observations.some((observation) => exactRun(observation, runs)?.id === run.id
116
+ && usageAuthority(observation) && isRuntimeTokenEvidence(observation)))
117
+ reasons.push("run-usage-unobserved");
118
+ }
119
+ const tokenValues = sessions.flatMap(({ tokens }) => tokens.value === null ? [] : [tokens.value]);
120
+ const sum = safeSum(tokenValues);
121
+ const tokens = metric(tokenValues.length === 0 ? null : sum, [...reasons, ...sessions.flatMap(({ tokens }) => tokens.reasons),
122
+ ...(sum === null ? ["counter-overflow"] : []),
123
+ ...(sessions.length === 0 ? ["no-token-evidence"] : [])]);
124
+ const operations = new Map();
125
+ for (const observation of observations) {
126
+ const identity = sessionIdentity(observation);
127
+ const { nativeTurnId } = observation.fence;
128
+ if (!usageAuthority(observation) || child(observation) || identity === null || nativeTurnId === undefined
129
+ || !observation.kind.startsWith("operation.")
130
+ || observation.payload.operation !== "tool" || observation.payload.operationId === undefined)
131
+ continue;
132
+ const key = JSON.stringify([resourceKey(identity), nativeTurnId, observation.payload.operationId]);
133
+ const versions = operations.get(key) ?? [];
134
+ versions.push(observation);
135
+ operations.set(key, versions);
136
+ }
137
+ const toolCount = [...operations.values()].filter((versions) => {
138
+ if (input.workItemId === undefined)
139
+ return true;
140
+ const owners = new Set(versions.map((observation) => exactRun(observation, runs)?.id));
141
+ return owners.size === 1 && !owners.has(undefined) && versions.every(inScope);
142
+ }).length;
143
+ return {
144
+ scope: "task-lifetime", taskId: input.task.id,
145
+ ...(input.workItemId === undefined ? {} : { workItemId: input.workItemId }),
146
+ observedThrough: scoped.at(-1)?.receivedAt ?? null,
147
+ tokens,
148
+ toolCalls: metric(toolCount === 0 ? null : toolCount, ["tool-history-compacted"]),
149
+ elapsedSeconds: input.workItemId === undefined
150
+ ? taskElapsed(input.task, events, now) : metric(null, ["work-item-lifecycle-not-task-elapsed"]),
151
+ executionSeconds: executionDuration(scoped, now),
152
+ coverage: {
153
+ basis: "task-fenced-observations",
154
+ observedSessions: sessions.length,
155
+ tokenSessions: tokenValues.length,
156
+ completeness: "observed-sources-only"
157
+ },
158
+ sessions
159
+ };
160
+ }
161
+ function attributableTokens(evidence, metrics) {
162
+ const total = metrics.cumulativeTotal;
163
+ if (total.status !== "observed")
164
+ return metric(null, ["session-usage-unavailable"]);
165
+ const cumulative = evidence.filter(({ payload }) => payload.usage?.semantics === "cumulative-session");
166
+ if (cumulative.length === 0)
167
+ return metric(total.totalTokens);
168
+ const first = cumulative[0].payload.usage;
169
+ const baseline = first.inputTokens + first.outputTokens;
170
+ if (baseline === 0)
171
+ return metric(total.totalTokens);
172
+ // session.started also occurs on resume: it cannot prove a fresh counter.
173
+ // A nonzero first counter may predate this Task, so exclude it. A single
174
+ // snapshot does not prove even a zero delta.
175
+ return cumulative.length < 2
176
+ ? metric(null, ["nonzero-session-baseline"])
177
+ : metric(total.totalTokens - baseline, ["nonzero-session-baseline"]);
178
+ }
179
+ function exactRun(observation, runs) {
180
+ const { fence } = observation;
181
+ const run = fence.runId === undefined ? undefined : runs.get(fence.runId);
182
+ if (run === undefined || fence.nativeSessionId === undefined || run.roleName !== fence.roleName
183
+ || run.effective.agentId !== fence.agentId)
184
+ return undefined;
185
+ try {
186
+ return builtinDriverIdForAdapter(run.effective.adapterId) === fence.driverId ? run : undefined;
187
+ }
188
+ catch {
189
+ return undefined;
190
+ }
191
+ }
192
+ function sessionIdentity({ fence }) {
193
+ return fence.taskId === undefined || fence.nativeSessionId === undefined ? null : {
194
+ taskId: fence.taskId, roleName: fence.roleName, agentId: fence.agentId,
195
+ driverId: fence.driverId, nativeSessionId: fence.nativeSessionId
196
+ };
197
+ }
198
+ function identityKey(identity) {
199
+ return JSON.stringify([identity.taskId, identity.roleName, identity.agentId, identity.driverId, identity.nativeSessionId]);
200
+ }
201
+ function resourceKey(identity) {
202
+ return JSON.stringify([identity.agentId, identity.driverId, identity.nativeSessionId]);
203
+ }
204
+ function child({ fence }) {
205
+ return fence.continuationId !== undefined || fence.parentContinuationId !== undefined;
206
+ }
207
+ function usageAuthority(observation) {
208
+ return observation.authority === "provider-structured" || observation.authority === "driver-inferred";
209
+ }
210
+ function compare(left, right) {
211
+ return left.receivedAt.localeCompare(right.receivedAt)
212
+ || (left.sequence ?? -1) - (right.sequence ?? -1)
213
+ || (left.ordinal ?? -1) - (right.ordinal ?? -1)
214
+ || left.eventId.localeCompare(right.eventId);
215
+ }
216
+ function metric(value, reasons = []) {
217
+ return {
218
+ value, status: value === null ? "unknown" : reasons.length === 0 ? "known" : "partial",
219
+ reasons: [...new Set(reasons)].sort()
220
+ };
221
+ }
222
+ function safeSum(values) {
223
+ const sum = values.reduce((a, b) => a + b, 0);
224
+ return Number.isSafeInteger(sum) && sum >= 0 ? sum : null;
225
+ }
226
+ function taskElapsed(task, events, now) {
227
+ const terminal = task.status === "completed"
228
+ || task.status === "cancelled" || task.status === "archived";
229
+ const cancelledAt = events.filter((event) => event.type === "task.cancelled")
230
+ .map(({ createdAt }) => createdAt).sort().at(-1);
231
+ const end = terminal ? task.completedAt ?? task.retiredAt ?? cancelledAt : now.toISOString();
232
+ const elapsed = end === undefined ? NaN : (Date.parse(end) - Date.parse(task.createdAt)) / 1000;
233
+ return Number.isFinite(elapsed) && elapsed >= 0
234
+ ? metric(elapsed) : metric(null, ["task-lifecycle-time-missing"]);
235
+ }
236
+ function executionDuration(observations, now) {
237
+ const turns = new Map();
238
+ for (const observation of observations) {
239
+ const identity = sessionIdentity(observation);
240
+ const { nativeTurnId } = observation.fence;
241
+ if (identity === null || nativeTurnId === undefined || child(observation) || !usageAuthority(observation))
242
+ continue;
243
+ const resource = resourceKey(identity);
244
+ const key = JSON.stringify([resource, nativeTurnId]);
245
+ const turn = turns.get(key) ?? { resource };
246
+ const time = Date.parse(observation.observedAt ?? observation.receivedAt);
247
+ if (observation.kind === "turn.accepted")
248
+ turn.start = Math.min(turn.start ?? time, time);
249
+ if (["turn.completed", "turn.failed", "turn.cancelled"].includes(observation.kind)) {
250
+ turn.end = Math.max(turn.end ?? time, time);
251
+ }
252
+ turns.set(key, turn);
253
+ }
254
+ const intervals = new Map();
255
+ for (const turn of turns.values()) {
256
+ if (turn.start === undefined || turn.end === undefined || turn.end < turn.start || turn.end > now.getTime())
257
+ continue;
258
+ intervals.set(turn.resource, [...(intervals.get(turn.resource) ?? []), [turn.start, turn.end]]);
259
+ }
260
+ let milliseconds = 0;
261
+ for (const ranges of intervals.values()) {
262
+ ranges.sort((a, b) => a[0] - b[0]);
263
+ let end = -Infinity;
264
+ for (const range of ranges) {
265
+ milliseconds += Math.max(0, range[1] - Math.max(end, range[0]));
266
+ end = Math.max(end, range[1]);
267
+ }
268
+ }
269
+ return metric(intervals.size === 0 ? null : milliseconds / 1000, ["native-turn-history-compacted"]);
270
+ }
271
+ /** CLI/audit formatting shares the same metric contract; Web localizes labels. */
272
+ export function formatUsageMetric(metric, suffix = "") {
273
+ return metric.value === null ? `unknown (${metric.reasons.join(", ")})`
274
+ : `${metric.value}${suffix}${metric.status === "partial" ? ` (partial: ${metric.reasons.join(", ")})` : ""}`;
275
+ }
@@ -51,6 +51,18 @@ async function deliverActiveRun(store, delivery, task, role, run, now) {
51
51
  });
52
52
  const sessionSet = store.getTaskRoleSessionSet?.(task.id, role.name) ?? null;
53
53
  const binding = sessionSet?.providerBinding ?? null;
54
+ if (binding?.retry !== undefined && (binding.retry.successorRunId === run.id
55
+ || binding.retry.successorReviewRoundId !== undefined && binding.retry.successorReviewRoundId === run.reviewRoundId)
56
+ && ["cancelled", "exhausted", "needs-attention"].includes(binding.retry.status)
57
+ && binding.run?.runId !== run.id) {
58
+ return failRunDelivery(store, run, now, "runtime-failed", `Scheduled Provider retry ${binding.retry.status}: ${binding.retry.reason ?? "not admitted"}`);
59
+ }
60
+ if (binding?.retry?.status === "waiting"
61
+ && (binding.retry.successorRunId === run.id || binding.retry.successorReviewRoundId !== undefined
62
+ && binding.retry.successorReviewRoundId === run.reviewRoundId)
63
+ && now.getTime() < Date.parse(binding.retry.nextEligibleAt)) {
64
+ return { ...base, status: "skipped", reason: "not-ready" };
65
+ }
54
66
  const observedRun = binding?.run ?? null;
55
67
  const initialAttemptId = formatRunReceiptId(task.id, run.id);
56
68
  const currentProviderTurn = managedProviderTurnId(observedRun) === run.id ? observedRun : null;
@@ -3,6 +3,7 @@ import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
3
3
  import { isSchedulerTaskWorkspaceReady } from "./ports.js";
4
4
  import { taskOwnsManagedWorkspace } from "../task/task.js";
5
5
  import { hasImmediateWakeReason } from "./wakeReason.js";
6
+ import { providerRetryPending } from "../runtime/providerRetry.js";
6
7
  export const LEADER_WAKE_AGGREGATION_MS = 60_000;
7
8
  /** Deliver a durable mailbox notification, not an implicit execution assignment.
8
9
  * Acceptance settles only its claimed batch; no Agent report is required.
@@ -34,6 +35,10 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
34
35
  }
35
36
  const sessions = store.getTaskRoleSessionSet?.(task.id, role.name) ?? null;
36
37
  const provider = sessions?.providerBinding;
38
+ if (providerRetryPending(provider)) {
39
+ results.push({ ...base, status: "skipped", reason: "not-ready" });
40
+ continue;
41
+ }
37
42
  if (provider?.authority.owner === "human" || provider?.authority.owner === "unknown") {
38
43
  results.push({ ...base, status: "skipped", reason: "busy" });
39
44
  continue;
@@ -1,5 +1,6 @@
1
1
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
2
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
3
+ import { providerRetryProjection } from "../runtime/providerRetry.js";
3
4
  import { taskOwnsManagedWorkspace } from "../task/task.js";
4
5
  import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
5
6
  import { mailboxBatches } from "../coordination/workMailbox.js";
@@ -19,7 +20,8 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride, now =
19
20
  return null;
20
21
  const roles = store.listRoles?.(taskId) ?? [];
21
22
  const events = store.listEvents?.(taskId) ?? [];
22
- const runs = operationalTaskRecords(store.listRuns?.(taskId) ?? [], events, "run");
23
+ const usageRuns = store.listRuns?.(taskId) ?? [];
24
+ const runs = operationalTaskRecords(usageRuns, events, "run");
23
25
  const leaderMailbox = store.getWorkMailbox?.({
24
26
  kind: "role",
25
27
  taskId,
@@ -35,6 +37,11 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride, now =
35
37
  task,
36
38
  roles,
37
39
  runs,
40
+ usageRuns,
41
+ providerRetries: roles.flatMap(role => {
42
+ const retry = providerRetryProjection(store.getTaskRoleSessionSet?.(taskId, role.name)?.providerBinding);
43
+ return retry === null ? [] : [retry];
44
+ }),
38
45
  runDelivery: Object.fromEntries(runs.map((run) => {
39
46
  const observed = store.getTaskRoleSessionSet?.(taskId, run.roleName)?.providerBinding?.run;
40
47
  return [run.id, observed?.runId === run.id ? observed.status : "unobserved"];
@@ -79,6 +86,7 @@ export function projectTaskExecutionFromFacts(facts) {
79
86
  return projectTaskExecution({
80
87
  ...facts,
81
88
  runs,
89
+ usageRuns: facts.usageRuns ?? facts.runs,
82
90
  executionGroups
83
91
  });
84
92
  }
@@ -107,9 +115,10 @@ export function projectTaskExecution(facts) {
107
115
  });
108
116
  });
109
117
  const observability = buildTaskObservabilityProjection({
118
+ task,
110
119
  workItems,
111
120
  executionGroups: observabilityGroups,
112
- runs,
121
+ runs: facts.usageRuns ?? runs,
113
122
  events,
114
123
  contextSnapshots: facts.contextSnapshots,
115
124
  sessionTokens,
@@ -206,8 +215,12 @@ export function projectTaskExecution(facts) {
206
215
  const hasPendingLeaderWork = pendingWakeup !== null
207
216
  || leaderMailbox?.pending !== null;
208
217
  const recoveryPending = isRecoveryPending(pendingWakeup, leaderMailbox, leaderFailure);
209
- const failedWork = workItems.some((item) => item.status === "open"
210
- && runs.filter((run) => run.workItemId === item.id).at(-1)?.status === "failed");
218
+ const providerRecoveries = (facts.providerRetries ?? []).filter(retry => retry.status === "waiting" || retry.status === "in-flight");
219
+ const recoveringRunIds = new Set(providerRecoveries.flatMap(retry => [retry.previousRunId, retry.successorRunId].filter((id) => id !== undefined)));
220
+ const failedWork = workItems.some((item) => {
221
+ const last = runs.filter((run) => run.workItemId === item.id).at(-1);
222
+ return item.status === "open" && last?.status === "failed" && !recoveringRunIds.has(last.id);
223
+ });
211
224
  const candidateReady = workItems.some((item) => (item.status === "open" && item.currentCandidateId !== undefined));
212
225
  const blockedIntegration = integrations.some((attempt) => attempt.status === "blocked");
213
226
  const conflictedIntegration = integrations.find((attempt) => attempt.status === "conflicted");
@@ -258,7 +271,7 @@ export function projectTaskExecution(facts) {
258
271
  pendingWakeup
259
272
  });
260
273
  }
261
- if (laneRecovery !== undefined) {
274
+ if (laneRecovery !== undefined && (laneRecovery.runId === undefined || !recoveringRunIds.has(laneRecovery.runId))) {
262
275
  return render({
263
276
  task,
264
277
  status: laneRecovery.runtimeHealth === "confirmed-dead"
@@ -306,6 +319,14 @@ export function projectTaskExecution(facts) {
306
319
  pendingWakeup
307
320
  });
308
321
  }
322
+ if (providerRecoveries.length > 0 && healthyActiveRuns.length === 0) {
323
+ return render({
324
+ task, status: "recovering", owner: "none", action: "none",
325
+ summary: `${providerRecoveries.length} Provider recovery chain(s) are cooling down or awaiting their exact result; existing work is preserved. Do not dispatch a duplicate.`,
326
+ reason: "provider-recovery", monitoring, failClosed: false,
327
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
328
+ });
329
+ }
309
330
  if (recoveryPending) {
310
331
  return render({
311
332
  task,
@@ -1,3 +1,4 @@
1
+ import { projectTaskUsageMetrics } from "../runtime/taskUsageMetrics.js";
1
2
  /**
2
3
  * Build the read-only DAG, execution, cost, and context view consumed by CLI
3
4
  * and Web. It deliberately derives every value from existing Task records and
@@ -8,7 +9,7 @@ export function buildTaskObservabilityProjection(input) {
8
9
  const dag = projectDag(input.workItems);
9
10
  const workItems = input.workItems.map((item) => {
10
11
  const groups = item.executionGroups;
11
- const itemCost = projectWorkItemCost(groups, input.runs, now);
12
+ const itemCost = projectCost({ ...input, now }, groups, item.id);
12
13
  const itemContext = projectContext(groups, input.runs, input.contextSnapshots);
13
14
  const producerObservability = projectWorkItemProducerObservability(item, input.runs);
14
15
  return Object.freeze({
@@ -21,7 +22,7 @@ export function buildTaskObservabilityProjection(input) {
21
22
  resultCount: producerObservability?.resultCount ?? null
22
23
  });
23
24
  });
24
- const cost = projectCost(input.executionGroups, input.runs, input.events, now);
25
+ const cost = projectCost({ ...input, now }, input.executionGroups);
25
26
  const context = projectContext(input.executionGroups, input.runs, input.contextSnapshots);
26
27
  return Object.freeze({
27
28
  dag,
@@ -140,17 +141,13 @@ function dependencyEdgeStatus(id, byId) {
140
141
  return "active";
141
142
  return "active";
142
143
  }
143
- function projectCost(groups, runs, _events, now) {
144
+ function projectCost(input, groups, workItemId) {
144
145
  const uniqueGroups = [...new Map(groups.map((group) => [group.id, group])).values()];
145
146
  const groupIds = new Set(uniqueGroups.map(({ id }) => id));
146
- const attempts = runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
147
+ const attempts = input.runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
147
148
  const laneCount = uniqueGroups.reduce((total, group) => total + group.lanes.length, 0);
148
149
  return Object.freeze({
149
- tokens: 0,
150
- toolCalls: 0,
151
- wallClockSeconds: uniqueGroups.reduce((total, group) => total + groupDurationSeconds(group, now), 0),
152
- tokensObservable: false,
153
- toolCallsObservable: false,
150
+ ...projectTaskUsageMetrics({ ...input, workItemId }),
154
151
  laneCount,
155
152
  groupCount: uniqueGroups.length,
156
153
  retryCount: Math.max(0, attempts.length - laneCount),
@@ -158,32 +155,6 @@ function projectCost(groups, runs, _events, now) {
158
155
  marginalValueStatus: "unavailable"
159
156
  });
160
157
  }
161
- function projectWorkItemCost(groups, runs, now) {
162
- const groupIds = new Set(groups.map(({ id }) => id));
163
- const attempts = runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
164
- const wallClockSeconds = groups.reduce((total, group) => {
165
- const started = Date.parse(group.createdAt);
166
- const ended = group.lanes.some(({ disposition }) => disposition === "open")
167
- ? now.getTime()
168
- : Date.parse(group.updatedAt);
169
- return total + (Number.isFinite(started) && Number.isFinite(ended)
170
- ? Math.max(0, Math.floor((ended - started) / 1_000))
171
- : 0);
172
- }, 0);
173
- const laneCount = groups.reduce((total, group) => total + group.lanes.length, 0);
174
- return Object.freeze({
175
- tokens: 0,
176
- toolCalls: 0,
177
- wallClockSeconds,
178
- tokensObservable: false,
179
- toolCallsObservable: false,
180
- laneCount,
181
- groupCount: groups.length,
182
- retryCount: Math.max(0, attempts.length - laneCount),
183
- marginalValuePercent: null,
184
- marginalValueStatus: "unavailable"
185
- });
186
- }
187
158
  function projectContext(groups, runs, snapshots) {
188
159
  const refs = new Map();
189
160
  for (const group of groups) {
@@ -223,12 +194,3 @@ function projectContext(groups, runs, snapshots) {
223
194
  compressionStatus: "unavailable"
224
195
  });
225
196
  }
226
- function groupDurationSeconds(group, now) {
227
- const started = Date.parse(group.createdAt);
228
- const ended = group.lanes.some(({ disposition }) => disposition === "open")
229
- ? now.getTime()
230
- : Date.parse(group.updatedAt);
231
- return Number.isFinite(started) && Number.isFinite(ended)
232
- ? Math.max(0, Math.floor((ended - started) / 1_000))
233
- : 0;
234
- }
@@ -1264,6 +1264,37 @@ UPDATE review_rounds SET payload = json_set(payload, '$.executionGroup.lanes', j
1264
1264
  // The data/filesystem transform is the substantive step; the SQL is inert.
1265
1265
  sql: COLLAPSE_WORKTREE_LAYOUT_SQL,
1266
1266
  migrateData: migrateCollapseWorktreeLayout
1267
+ },
1268
+ {
1269
+ version: 26,
1270
+ name: "publication-candidate-adoption",
1271
+ introducedIn: "0.15.12",
1272
+ // Add optional Publication.headCommit (the observed PR/MR source head) and
1273
+ // publication.candidate-adopted events binding publicationId, completionEventId,
1274
+ // projectId, acceptedCommit/localCommit, acceptedTree/candidateTree, diffDigest,
1275
+ // acceptance and by, optionally integrationId/integrationDigest. Derived
1276
+ // task.archived mergeCoverage may also be uncovered or unverified.
1277
+ // No stored delivery index and no inferred adoption or verification:
1278
+ // valid old Publication, completion and archive payloads remain byte-identical.
1279
+ sql: "SELECT 1; -- fixed acceptance-to-publication evidence; preserve all historical facts"
1280
+ },
1281
+ {
1282
+ version: 27,
1283
+ name: "bounded-provider-input-retry",
1284
+ introducedIn: "0.16.0",
1285
+ // Optional owned input/recovery records on the existing Provider binding;
1286
+ // positive Driver transient evidence on StandardAgentError. No historical
1287
+ // failures are scanned, scheduled, reclassified or replayed by upgrading.
1288
+ sql: `
1289
+ -- ProviderTurn.input/retrySupported/failure; ProviderRuntimeBinding.retry/retryHistory/retryDisabled;
1290
+ -- optional error retryable and retryAfterMs facts. Index existing records only.
1291
+ CREATE INDEX idx_task_provider_retry ON role_session_sets(
1292
+ json_extract(payload, '$.providerBinding.retry.status')
1293
+ ) WHERE json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight');
1294
+ CREATE INDEX idx_global_provider_retry ON global_role_session_sets(
1295
+ json_extract(payload, '$.providerBinding.retry.status')
1296
+ ) WHERE json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight');
1297
+ `
1267
1298
  }
1268
1299
  ]);
1269
1300
  for (let index = 0; index < MIGRATIONS.length; index += 1) {
@@ -38,6 +38,7 @@ import { existsSync, mkdirSync } from "node:fs";
38
38
  import { join } from "node:path";
39
39
  import { isDeepStrictEqual } from "node:util";
40
40
  import Database from "better-sqlite3";
41
+ import { queryTaskCatalog } from "./taskCatalog.js";
41
42
  import { validatePluginValidation } from "../plugins/pluginPackage.js";
42
43
  import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
43
44
  import { validateLocalResource, validateEnvironmentPreparation } from "../resources/projectResource.js";
@@ -921,6 +922,15 @@ export class SqliteTaskStore {
921
922
  const tasks = this.#listPayload("task_records", "1=1", []);
922
923
  return this.#sortById(tasks, (task) => task.id);
923
924
  }
925
+ queryTaskCatalog(query) {
926
+ return queryTaskCatalog(this.#db, query);
927
+ }
928
+ listTaskChoices() {
929
+ const rows = this.#db.prepare(`SELECT c.task_id AS id, c.status,
930
+ json_extract(r.payload, '$.title') AS title
931
+ FROM tasks_catalog c JOIN task_records r ON r.task_id = c.task_id`).all();
932
+ return this.#sortById(rows, task => task.id);
933
+ }
924
934
  getTask(id) {
925
935
  return this.#getPayload("task_records", "task_id = ?", [id]);
926
936
  }
@@ -1267,6 +1277,13 @@ export class SqliteTaskStore {
1267
1277
  listRoleSessionSets(taskId) {
1268
1278
  return this.#sortById(this.#listPayload("role_session_sets", "task_id = ?", [taskId]), (set) => set.owner.roleName);
1269
1279
  }
1280
+ listProviderRetrySessions() {
1281
+ const pending = "json_extract(payload, '$.providerBinding.retry.status') IN ('waiting', 'in-flight')";
1282
+ return [
1283
+ ...this.#listPayload("role_session_sets", pending, []),
1284
+ ...this.#listPayload("global_role_session_sets", pending, [])
1285
+ ];
1286
+ }
1270
1287
  listRuntimeSessionCandidates(query = {}) {
1271
1288
  const taskIds = query.taskIds === undefined
1272
1289
  ? undefined
@@ -13,4 +13,4 @@
13
13
  * intermediate Yui releases.
14
14
  */
15
15
  export const MIN_SUPPORTED_STORAGE_VERSION = 1;
16
- export const CURRENT_STORAGE_VERSION = 25;
16
+ export const CURRENT_STORAGE_VERSION = 27;
@@ -72,6 +72,7 @@ const READ_ONLY_STORE_METHODS = new Set([
72
72
  "getRoleSessionSet",
73
73
  "getTaskRoleSessionSet",
74
74
  "listRoleSessionSets",
75
+ "listProviderRetrySessions",
75
76
  "listRuntimeSessionCandidates",
76
77
  "getRoleSession",
77
78
  "getWorkItem",