@zq-silk/yui 0.15.9 → 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 (160) 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/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -65,7 +65,9 @@ export class FileTaskRuntimeIsolation {
65
65
  this.#resourceRegistrar().registerTaskRuntimeIsolation(descriptor);
66
66
  return;
67
67
  }
68
- ensureDirectoryChain(this.#runtimeRoot, dirname(root));
68
+ ensureDirectoryChain(this.#controlPlane.managedRuntimeRoot === undefined
69
+ ? this.#runtimeRoot
70
+ : this.#controlPlane.yuiHome, dirname(root));
69
71
  mkdirSync(root, { mode: 0o700 });
70
72
  try {
71
73
  const marker = {
@@ -255,10 +257,22 @@ export function assertTaskRuntimeIsolationPreflight(input) {
255
257
  }
256
258
  }
257
259
  const control = normalizeControlBoundary(input.controlPlane);
258
- const protectedPaths = [control.yuiHome, ...control.globalInstallPaths];
259
260
  for (const root of Object.values(descriptor.roots)) {
260
- if (protectedPaths.some((path) => pathsOverlap(root, path))) {
261
- throw new Error("Task runtime roots overlap the control YUI_HOME or a global install path.");
261
+ if (control.globalInstallPaths.some((path) => pathsOverlap(root, path))) {
262
+ throw new Error("Task runtime roots overlap a global install path.");
263
+ }
264
+ // Precise partition isolation: a runtime root may occupy Home only when it
265
+ // is fully within the single designated managed runtime partition. Every
266
+ // other part of Home — the database, managed Git workspaces, projects —
267
+ // stays protected. Both Task and integration runtimes now supply such a
268
+ // partition; a caller that supplies none keeps the whole-Home overlap ban.
269
+ if (pathsOverlap(root, control.yuiHome)) {
270
+ const partition = control.managedRuntimeRoot;
271
+ const withinPartition = partition !== undefined
272
+ && (root === partition || isWithin(partition, root));
273
+ if (!withinPartition) {
274
+ throw new Error("Task runtime roots overlap the control YUI_HOME outside the managed runtime partition.");
275
+ }
262
276
  }
263
277
  if (pathsOverlap(root, descriptor.workspace.root)) {
264
278
  throw new Error("Task runtime roots must not dirty the managed workspace.");
@@ -441,11 +455,21 @@ function taskRuntimeServiceNamespace(taskId, owner, runtimeRoot) {
441
455
  ])).slice(0, 24)}`;
442
456
  }
443
457
  function normalizeControlBoundary(value) {
458
+ const yuiHome = canonicalPath(value.yuiHome, "Control YUI_HOME");
459
+ const managedRuntimeRoot = value.managedRuntimeRoot === undefined
460
+ ? undefined
461
+ : canonicalPath(value.managedRuntimeRoot, "Managed runtime root");
462
+ if (managedRuntimeRoot !== undefined
463
+ && managedRuntimeRoot !== yuiHome
464
+ && !isWithin(yuiHome, managedRuntimeRoot)) {
465
+ throw new Error("Managed runtime root must be within the control YUI_HOME.");
466
+ }
444
467
  return Object.freeze({
445
- yuiHome: canonicalPath(value.yuiHome, "Control YUI_HOME"),
468
+ yuiHome,
446
469
  controllerSocketPath: canonicalPath(value.controllerSocketPath, "Controller socket path"),
447
470
  tmuxNamespace: requireIdentity(value.tmuxNamespace, "Controller tmux namespace"),
448
- globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path")))
471
+ globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path"))),
472
+ ...(managedRuntimeRoot === undefined ? {} : { managedRuntimeRoot })
449
473
  });
450
474
  }
451
475
  function validateObservation(resource) {
@@ -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
+ }
@@ -209,7 +209,8 @@ export class TmuxSessionHost {
209
209
  throw toRuntimeLaunchFailure(error, "validation", launchContext);
210
210
  }
211
211
  }
212
- const interactiveCodex = request.owner.scope === "global" && request.adapterId === "codex";
212
+ const interactiveCodex = request.owner.scope === "global" && request.adapterId === "codex"
213
+ && planned.launch.providerControl === undefined;
213
214
  let reuseInteractivePane = false;
214
215
  if (interactiveCodex) {
215
216
  let status;
@@ -310,6 +311,7 @@ export class TmuxSessionHost {
310
311
  }
311
312
  const reservation = broker.reserve(Object.freeze({
312
313
  schemaVersion: 2,
314
+ ...(request.runId === undefined ? {} : { startupRunId: request.runId }),
313
315
  command: planned.launch.command,
314
316
  args: [...planned.launch.args],
315
317
  environment: { ...planned.launch.env },
@@ -577,8 +579,8 @@ export class AgentHostPromptPushAdapter {
577
579
  try {
578
580
  const result = await sendAgentHostSteerControl({
579
581
  home: this.home,
580
- scope: "task",
581
- taskId: request.owner.taskId,
582
+ scope: request.owner.scope,
583
+ ...(request.owner.scope === "task" ? { taskId: request.owner.taskId } : {}),
582
584
  roleName: request.owner.roleName,
583
585
  control: {
584
586
  protocol: AGENT_HOST_CONTROL_PROTOCOL,
@@ -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;
@@ -4,6 +4,8 @@ import { queueLeaderWakeup } from "./wakeupQueue.js";
4
4
  export const LEADER_ATTENTION_REQUIRED_EVENT = "leader.attention-required";
5
5
  /** The sole Task-event boundary into the global Operator mailbox. */
6
6
  export function enqueueOperatorEvent(store, event, reason, now) {
7
+ if (store.getTask?.(event.taskId)?.status === "archived")
8
+ return;
7
9
  enqueueWork(store, { kind: "operator" }, reason, now, [
8
10
  { type: "event", taskId: event.taskId, id: event.id }
9
11
  ], {
@@ -13,6 +15,8 @@ export function enqueueOperatorEvent(store, event, reason, now) {
13
15
  }
14
16
  /** Routes one Role fact to its supervisor without exposing Provider details to Operator code. */
15
17
  export function routeRoleEvent(store, event, roleName, reason, now) {
18
+ if (store.getTask?.(event.taskId)?.status === "archived")
19
+ return;
16
20
  if (roleName === "leader") {
17
21
  enqueueOperatorEvent(store, event, reason, now);
18
22
  return;
@@ -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,13 +215,19 @@ 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");
226
+ const conflictedIntegration = integrations.find((attempt) => attempt.status === "conflicted");
213
227
  const unresolvedIntegration = integrations.some((attempt) => (attempt.status === "running"
214
228
  || attempt.status === "validating"
215
- || attempt.status === "blocked"));
229
+ || attempt.status === "blocked"
230
+ || attempt.status === "conflicted"));
216
231
  const hasLeaderMismatch = attention.some((item) => item.kind === "identity-mismatch");
217
232
  const renderAttention = () => {
218
233
  const first = attention[0];
@@ -256,7 +271,7 @@ export function projectTaskExecution(facts) {
256
271
  pendingWakeup
257
272
  });
258
273
  }
259
- if (laneRecovery !== undefined) {
274
+ if (laneRecovery !== undefined && (laneRecovery.runId === undefined || !recoveringRunIds.has(laneRecovery.runId))) {
260
275
  return render({
261
276
  task,
262
277
  status: laneRecovery.runtimeHealth === "confirmed-dead"
@@ -279,6 +294,15 @@ export function projectTaskExecution(facts) {
279
294
  if (attention.length > 0) {
280
295
  return renderAttention();
281
296
  }
297
+ if (conflictedIntegration !== undefined) {
298
+ return render({
299
+ task, status: "needs-leader-action", owner: "leader",
300
+ action: "resolve-integration-conflicts",
301
+ summary: `Integration ${conflictedIntegration.id} has Git conflicts; resolve its workspace and continue.`,
302
+ reason: "integration-conflicted", monitoring, failClosed: false,
303
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
304
+ });
305
+ }
282
306
  if (blockedIntegration || failedWork || hasLeaderMismatch) {
283
307
  return render({
284
308
  task,
@@ -295,6 +319,14 @@ export function projectTaskExecution(facts) {
295
319
  pendingWakeup
296
320
  });
297
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
+ }
298
330
  if (recoveryPending) {
299
331
  return render({
300
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
- }
@@ -37,7 +37,13 @@ const IMMEDIATE_KINDS = new Set([
37
37
  "activation-failed",
38
38
  "task-reopened",
39
39
  "leader-turn-failed",
40
- "role-turn-failed"
40
+ "role-turn-failed",
41
+ // A claimed interrupt-then handoff for a no-Run Leader turn: the operator/Leader
42
+ // explicitly redirected, so the release wake bypasses the aggregation debounce.
43
+ // It is still held by the leader-mailbox busy-gate until the interrupted native
44
+ // Turn reaches its proven terminal (decision-3 §4), so "immediate" only removes
45
+ // the extra 60s aggregation delay once that terminal actually opens the gate.
46
+ "interrupt-then"
41
47
  ]);
42
48
  export function wakeReason(kind, ref) {
43
49
  if (ref === undefined)
@@ -1,5 +1,7 @@
1
1
  import { mergePendingWakeup } from "./pendingWakeup.js";
2
2
  export function queueLeaderWakeup(store, taskId, reason, now) {
3
+ if (store.getTask?.(taskId)?.status === "archived")
4
+ return null;
3
5
  if (store.enqueueLeaderWakeup !== undefined) {
4
6
  return store.enqueueLeaderWakeup(taskId, reason, now);
5
7
  }