@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
@@ -1,10 +1,15 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { prepareMessageContinuations } from "../message/messageContinuation.js";
2
+ import { prepareMessageContinuations, interruptThenTerminalState } from "../message/messageContinuation.js";
3
3
  import { freezeRunContextSnapshot } from "../context/runContextPack.js";
4
4
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
7
- import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime, selectNewTaskRoleSession, taskRoleControlTarget } from "../executor/agentExecutor.js";
7
+ import { resolveAgentHostObservation, recordAgentHostConnection, AgentHostObservationDeferred } from "./agentHostObservation.js";
8
+ import { RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
9
+ import { admitOwnedProviderInput, ownedProviderInput } from "./providerRetryAdmission.js";
10
+ import { recordProviderFailure, providerRetryPending, deferProviderRetry } from "../runtime/providerRetry.js";
11
+ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
12
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindGlobalRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime, updateGlobalRoleProviderRuntime, selectNewTaskRoleSession, taskRoleControlTarget } from "../executor/agentExecutor.js";
8
13
  import { acceptProviderTurn, cancelQuiescentProviderInput, beginProviderTurn, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, clearProviderGoal, settleProviderTurnSubmission, settleProviderTurn, transferProviderAuthority, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
9
14
  import { createTaskEvent } from "../event/taskEvent.js";
10
15
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
@@ -17,6 +22,7 @@ import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
17
22
  import { appendRunInput, createRun, withRunContextSnapshot, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
18
23
  import { transportAgentResult } from "../domain/agentResultTransport.js";
19
24
  import { createRunInput } from "../context/runInputContract.js";
25
+ import { recordTaskMessageControlOutcome, markGlobalRoleMessageDelivered, markGlobalRoleMessageNotDelivered } from "../message/message.js";
20
26
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
21
27
  import { terminalizeExactTaskRun, cancelQuiescentRoleRuns } from "../lifecycle/exactRunTerminalization.js";
22
28
  import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
@@ -29,7 +35,7 @@ import { pendingWakeupProjection } from "../storage/taskStore.js";
29
35
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
30
36
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
31
37
  import { formatRunReceiptId } from "../task/taskRecordReference.js";
32
- import { bindExecution, claimPending, completeProcessing, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
38
+ import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
33
39
  import { enqueueWork, settleRoleRunDispatch as settleRoleRunDispatchMailbox } from "../coordination/workMailboxQueue.js";
34
40
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
35
41
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
@@ -58,6 +64,57 @@ export class FileSchedulerStoreAdapter {
58
64
  this.drivers = drivers;
59
65
  this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
60
66
  }
67
+ observeAgentHostObservation(event, now = new Date()) {
68
+ // Match canonical ingress: archive and Host fact settlement share the
69
+ // same write boundary, including the original transport envelope.
70
+ return this.store.transaction(() => this.foldAgentHostObservation(event, now));
71
+ }
72
+ foldAgentHostObservation(event, now) {
73
+ const raw = createRuntimeObservation(event.observation);
74
+ const task = raw.fence.taskId === undefined ? null : this.store.getTask(raw.fence.taskId);
75
+ if (raw.fence.taskId !== undefined && task === null)
76
+ return "obsolete";
77
+ if (task?.status === "archived") {
78
+ this.observeObsoleteRuntimeEvent({
79
+ eventId: event.id, eventType: event.type, taskId: task.id,
80
+ roleName: raw.fence.roleName, agentId: raw.fence.agentId,
81
+ nativeSessionId: raw.fence.nativeSessionId ?? "unknown",
82
+ reason: "task-archived", originalEvent: event
83
+ }, now);
84
+ return "obsolete";
85
+ }
86
+ if (Date.parse(raw.receivedAt) > now.getTime()) {
87
+ return this.recordObsoleteCanonicalObservation(raw, "received-at-in-future", now);
88
+ }
89
+ let input;
90
+ try {
91
+ if (event.observation.kind === "host.observed") {
92
+ return this.store.transaction(store => {
93
+ const connection = resolveAgentHostObservation(store, event);
94
+ recordAgentHostConnection(store, event, connection, now);
95
+ if (connection.fence.taskId !== undefined)
96
+ this.persistRuntimeObservation(connection, now);
97
+ return "applied";
98
+ });
99
+ }
100
+ input = resolveAgentHostObservation(this.store, event);
101
+ if (input.kind === "input.accepted" && input.fence.runId === undefined
102
+ && input.fence.receiptId?.startsWith("native-input:")) {
103
+ // A valid ordinary conversation has no managed Run input to mirror.
104
+ // Consuming this fact is not rejection, nor assignment acceptance.
105
+ return "applied";
106
+ }
107
+ }
108
+ catch (error) {
109
+ if (error instanceof AgentHostObservationDeferred)
110
+ return "deferred";
111
+ if (!(error instanceof RuntimeHookRunFenceError))
112
+ throw error;
113
+ // Rejection is durable diagnostic evidence, never business acceptance.
114
+ return this.recordObsoleteCanonicalObservation(event.observation, error.message, now);
115
+ }
116
+ return this.observeRuntimeObservation(input, now);
117
+ }
61
118
  /**
62
119
  * Sole provider-independent ingress for structured runtime state. Driver
63
120
  * mapping has already happened before this boundary; this method validates
@@ -65,10 +122,19 @@ export class FileSchedulerStoreAdapter {
65
122
  * transitions, and retains the canonical observation for status projection.
66
123
  */
67
124
  observeRuntimeObservation(raw, now = new Date()) {
125
+ // Archive and observation settlement share the same write boundary.
126
+ // An ingress read outside this transaction must never authorize clearing
127
+ // a claim after another CLI has committed archive.
128
+ return this.store.transaction(() => this.foldRuntimeObservation(raw, now));
129
+ }
130
+ foldRuntimeObservation(raw, now) {
68
131
  const input = createRuntimeObservation(raw);
69
132
  const taskId = input.fence.taskId;
70
133
  if (taskId === undefined)
71
- return "obsolete";
134
+ return this.observeGlobalProviderObservation(input, now);
135
+ if (this.store.getTask(taskId)?.status === "archived") {
136
+ return this.recordObsoleteCanonicalObservation(input, "task-archived", now);
137
+ }
72
138
  if (Date.parse(input.receivedAt) > now.getTime()) {
73
139
  return this.recordObsoleteCanonicalObservation(input, "received-at-in-future", now);
74
140
  }
@@ -83,12 +149,18 @@ export class FileSchedulerStoreAdapter {
83
149
  const run = input.fence.runId === undefined ? null : store.getRun(taskId, input.fence.runId);
84
150
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
85
151
  const native = sessions?.providerBinding?.run;
86
- if (run === null || run.roleName !== input.fence.roleName
87
- || run.effective.agentId !== input.fence.agentId || native?.runId !== run.id
88
- || native.nativeTurnId !== input.fence.nativeTurnId
89
- || sessions?.sessions[input.fence.agentId]?.nativeSessionId !== input.fence.nativeSessionId)
152
+ const isSteer = input.fence.receiptId.startsWith("steer:");
153
+ // A Worker/Reviewer steer's accepted input is folded onto the owning
154
+ // AgentRun. A no-Run Leader steer (decision-3 §9) has no run to append to,
155
+ // but its accepted disposition is still a real fact that gap D records on
156
+ // the Message; only the Worker append requires the exact Run/Turn fence.
157
+ const runFolded = run !== null && run.roleName === input.fence.roleName
158
+ && run.effective.agentId === input.fence.agentId && native?.runId === run.id
159
+ && native.nativeTurnId === input.fence.nativeTurnId
160
+ && sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId;
161
+ if (!runFolded && !(isSteer && input.fence.runId === undefined))
90
162
  return "obsolete";
91
- if (!hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
163
+ if (runFolded && !hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
92
164
  const updated = appendRunInput(run, createRunInput({
93
165
  source: input.fence.receiptId.startsWith("native-input:")
94
166
  ? { type: "provider", channel: "visible-input" }
@@ -98,6 +170,31 @@ export class FileSchedulerStoreAdapter {
98
170
  store.saveRun(updated);
99
171
  store.saveActiveRun(updated);
100
172
  }
173
+ // decision-3 §3/§8, message-5 gap D: promote the Message's independent
174
+ // control op to its proven terminal from the Host's own settlement, keyed
175
+ // by this exact steer receipt. Monotonic and idempotent — never a rewind
176
+ // of an already-proven outcome, and a replayed observation is absorbed.
177
+ if (isSteer)
178
+ this.recordSteerControlOutcome(store, input.fence.receiptId, "accepted", now);
179
+ this.persistRuntimeObservation(input, now);
180
+ return "applied";
181
+ });
182
+ }
183
+ // A steer's rejected/delivery-unknown disposition is the Host's proven
184
+ // terminal for the one live control attempt (message-5 gap D). It carries no
185
+ // AgentRun append — a rejected input changed no execution — so it is recorded
186
+ // only on the Message's control op, for a Worker/Reviewer or a no-Run Leader
187
+ // steer alike, keyed by the exact steer receipt.
188
+ if ((input.kind === "input.rejected" || input.kind === "input.delivery-unknown")
189
+ && input.fence.receiptId?.startsWith("steer:") === true) {
190
+ return this.store.transaction((store) => {
191
+ if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
192
+ return "applied";
193
+ const recorded = this.recordSteerControlOutcome(store, input.fence.receiptId, input.kind === "input.rejected" ? "rejected" : "delivery-unknown", now);
194
+ if (!recorded) {
195
+ recordCanonicalObservationObsolete(store, input, "steer-message-not-found", now);
196
+ return "obsolete";
197
+ }
101
198
  this.persistRuntimeObservation(input, now);
102
199
  return "applied";
103
200
  });
@@ -173,6 +270,38 @@ export class FileSchedulerStoreAdapter {
173
270
  }
174
271
  return outcome;
175
272
  }
273
+ /**
274
+ * Promote a steer Message's independent control op to a proven outcome from the
275
+ * Host's own settlement (decision-3 §3/§8, message-5 gap D). The `steer:` fence
276
+ * names the exact `steer:<taskId>/<messageId>` receipt, so the outcome binds to
277
+ * that one Message and never another; the requestId is read from the Message's
278
+ * own steer identity (its live input, or the reused-steer provenance a handoff
279
+ * preserved) so the recorded op matches the same idempotency key the live edge
280
+ * used. Returns false when the receipt names no such steer Message, so the
281
+ * caller can mark the observation obsolete rather than silently drop it. The
282
+ * record is monotonic and idempotent inside {@link recordTaskMessageControlOutcome}.
283
+ */
284
+ recordSteerControlOutcome(store, receiptId, outcome, now) {
285
+ const parsed = /^steer:([^/]+)\/(.+)$/.exec(receiptId);
286
+ if (parsed === null)
287
+ return false;
288
+ const [, taskId, messageId] = parsed;
289
+ const message = store.listMessages(taskId).find((entry) => entry.id === messageId);
290
+ const requestId = message?.inputControl?.requestId ?? message?.interruptThen?.reusedInput?.requestId;
291
+ const wasSteer = message?.inputControl?.action === "steer"
292
+ || message?.interruptThen?.reusedInput?.action === "steer";
293
+ if (message === undefined || requestId === undefined || !wasSteer)
294
+ return false;
295
+ const recorded = recordTaskMessageControlOutcome(message, {
296
+ requestId, receiptId, outcome, observedAt: now
297
+ });
298
+ // The monotonic guard returns the same Message when the record is absorbed
299
+ // (an idempotent repeat, or a stale outcome that must not rewind a proven
300
+ // terminal); only persist a real change.
301
+ if (recorded !== message)
302
+ store.updateMessage(taskId, recorded);
303
+ return true;
304
+ }
176
305
  /** A delayed steer receipt settles only its original claimed mailbox batch. */
177
306
  observeLeaderSteerReceipt(input, now) {
178
307
  return this.store.transaction((store) => {
@@ -238,6 +367,105 @@ export class FileSchedulerStoreAdapter {
238
367
  return "applied";
239
368
  });
240
369
  }
370
+ observeGlobalProviderObservation(input, now) {
371
+ return this.store.transaction((store) => {
372
+ const fence = input.fence;
373
+ let sessions = store.getGlobalRoleSessionSet(fence.roleName);
374
+ const session = sessions?.sessions[fence.agentId];
375
+ let binding = sessions?.providerBinding;
376
+ if (sessions == null || session == null || binding == null || fence.runId !== undefined
377
+ || sessions.activeAgentId !== fence.agentId
378
+ || session.nativeSessionId !== fence.nativeSessionId
379
+ || this.drivers.requireByAdapterId(session.adapterId).id !== fence.driverId
380
+ || currentProviderConversation(binding).conversationId !== fence.nativeSessionId
381
+ || Date.parse(input.receivedAt) > now.getTime())
382
+ return "obsolete";
383
+ const at = input.observedAt ?? input.receivedAt;
384
+ const receipt = fence.receiptId;
385
+ const matches = receipt !== undefined && binding.run?.attemptId === receipt
386
+ && (fence.nativeTurnId === undefined || binding.run.nativeTurnId === undefined
387
+ || binding.run.nativeTurnId === fence.nativeTurnId);
388
+ if (input.kind === "turn.accepted") {
389
+ if (input.authority === "transport" && matches) {
390
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
391
+ if (message !== undefined)
392
+ store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at), "transport"));
393
+ return "applied";
394
+ }
395
+ if (input.authority !== "provider-structured")
396
+ return "applied";
397
+ if (!matches) {
398
+ if (receipt?.startsWith("direct:") !== true || binding.run != null
399
+ && ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status))
400
+ return "obsolete";
401
+ binding = beginProviderTurn(binding, { attemptId: receipt,
402
+ authorityEpoch: binding.authority.epoch, submittedAt: at });
403
+ }
404
+ if (binding.run?.status === "submitting" || binding.run?.status === "delivery-unknown") {
405
+ binding = acceptProviderTurn(binding, { attemptId: receipt,
406
+ nativeTurnId: fence.nativeTurnId, acceptedAt: at });
407
+ }
408
+ else if (binding.run?.status !== "accepted")
409
+ return "applied";
410
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
411
+ || binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
412
+ && binding.retry.input.messageId === entry.id);
413
+ if (message !== undefined)
414
+ store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
415
+ }
416
+ else if (["turn.completed", "turn.cancelled", "turn.failed"].includes(input.kind)) {
417
+ if (!matches || input.authority !== "provider-structured")
418
+ return "obsolete";
419
+ // A provider final result proves this exact submitted input ran (Claude
420
+ // may emit its result without an earlier assistant acceptance event).
421
+ // Process exit/failure alone must not turn unconfirmed input into proof.
422
+ if (input.kind === "turn.completed" && binding.run != null
423
+ && ["submitting", "delivery-unknown"].includes(binding.run.status)) {
424
+ binding = acceptProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId, acceptedAt: at });
425
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
426
+ || binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
427
+ && binding.retry.input.messageId === entry.id);
428
+ if (message !== undefined)
429
+ store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
430
+ }
431
+ if (binding.run?.status !== "accepted")
432
+ return binding.run != null
433
+ && ["completed", "cancelled", "failed"].includes(binding.run.status) ? "applied" : "obsolete";
434
+ binding = settleProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId,
435
+ status: input.kind === "turn.completed" ? "completed" : input.kind === "turn.cancelled" ? "cancelled" : "failed",
436
+ settledAt: at });
437
+ if (fence.nativeTurnId !== undefined)
438
+ sessions = rememberRoleAgentCompletedTurn(sessions, fence.agentId, fence.nativeSessionId, fence.nativeTurnId, new Date(at));
439
+ }
440
+ else if (["input.accepted", "input.rejected", "input.delivery-unknown"].includes(input.kind)) {
441
+ if (input.authority !== "provider-structured" || binding.run?.nativeTurnId !== fence.nativeTurnId
442
+ || binding.run?.status !== "accepted")
443
+ return "obsolete";
444
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `steer:${fence.roleName}/${entry.id}`);
445
+ if (message !== undefined && message.inputControl !== undefined) {
446
+ const outcome = input.kind === "input.accepted" ? "accepted"
447
+ : input.kind === "input.rejected" ? "rejected" : "delivery-unknown";
448
+ if (message.control?.outcome !== "accepted" && message.control?.outcome !== "rejected") {
449
+ const updated = { ...message, control: { requestId: message.inputControl.requestId,
450
+ receiptId: receipt, outcome, observedAt: at } };
451
+ store.updateGlobalRoleMessage(outcome === "accepted"
452
+ ? markGlobalRoleMessageDelivered(updated, new Date(at)) : updated);
453
+ }
454
+ }
455
+ }
456
+ else if (input.kind === "conversation.observed" && input.payload.recoverability !== undefined) {
457
+ binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
458
+ }
459
+ if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined) {
460
+ binding = recordProviderFailure(binding, {
461
+ error: input.payload.failure.error, failureRef: input.eventId, at: now.getTime()
462
+ });
463
+ }
464
+ store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
465
+ settleGlobalRetryInput(store, fence.roleName, binding, now);
466
+ return "applied";
467
+ });
468
+ }
241
469
  adapterForRuntimeObservation(input) {
242
470
  const taskId = input.fence.taskId;
243
471
  const runId = input.fence.runId;
@@ -536,7 +764,8 @@ export class FileSchedulerStoreAdapter {
536
764
  ...(failure.lastOutput === undefined ? {} : { lastOutput: failure.lastOutput })
537
765
  }, now);
538
766
  store.saveEvent(taskId, errorEvent);
539
- routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
767
+ if (!providerRetryPending(store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding))
768
+ routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
540
769
  }
541
770
  if ((input.kind === "operation.completed" || input.kind === "operation.failed")
542
771
  && input.payload.operation === "subagent") {
@@ -871,6 +1100,72 @@ export class FileSchedulerStoreAdapter {
871
1100
  });
872
1101
  }
873
1102
  beginAgentHostProviderTurn(input) {
1103
+ if (input.taskId === undefined) {
1104
+ this.store.transaction((store) => {
1105
+ if (input.runId !== undefined || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: input.roleName })))) {
1106
+ throw new AgentHostProviderTurnFenceError("Global Session admission is stopped.");
1107
+ }
1108
+ const sessions = store.getGlobalRoleSessionSet(input.roleName);
1109
+ let binding = sessions?.providerBinding;
1110
+ const session = sessions?.sessions[input.agentId];
1111
+ if (sessions === null || binding == null || session === undefined
1112
+ || sessions.activeAgentId !== input.agentId || session.status !== "active"
1113
+ || session.nativeSessionId !== input.nativeSessionId
1114
+ || currentProviderConversation(binding).conversationId !== input.nativeSessionId
1115
+ || binding.authority.owner !== input.authorityOwner
1116
+ || binding.authority.epoch !== input.authorityEpoch
1117
+ || binding.authority.holderId !== input.holderId) {
1118
+ throw new AgentHostProviderTurnFenceError("Global input carries a stale Session or writer fence.");
1119
+ }
1120
+ const turn = binding.run;
1121
+ if (turn?.attemptId === input.attemptId && turn.status === "submitting") {
1122
+ admitOwnedProviderInput(store, binding, input);
1123
+ return;
1124
+ }
1125
+ if (turn != null && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)) {
1126
+ throw new AgentHostProviderSessionBusyError("Global Conversation has an unsettled input.");
1127
+ }
1128
+ const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1129
+ const reserved = store.listGlobalRoleMessages(input.roleName).find(entry => entry.delivery === undefined && entry.notDelivered === undefined
1130
+ && entry.interruptThen?.targetNativeSessionId === input.nativeSessionId
1131
+ && entry.interruptThen.targetAgentId === input.agentId
1132
+ && entry.interruptThen.targetAuthorityEpoch === input.authorityEpoch
1133
+ && entry.interruptThen.targetAuthorityHolderId === input.holderId);
1134
+ if (reserved !== undefined && reserved.id !== message?.id) {
1135
+ throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
1136
+ }
1137
+ if (message !== undefined) {
1138
+ if (message.delivery !== undefined || message.notDelivered !== undefined
1139
+ || message.control !== undefined && message.control.outcome !== "rejected"
1140
+ || message.deliveryTarget?.agentId !== input.agentId
1141
+ || message.deliveryTarget?.nativeSessionId !== input.nativeSessionId) {
1142
+ throw new AgentHostProviderTurnFenceError("Global Message is settled, unconfirmed, or bound to another Session.");
1143
+ }
1144
+ const claim = message.interruptThen;
1145
+ if (claim !== undefined && (claim.targetAgentId !== input.agentId
1146
+ || claim.targetNativeSessionId !== input.nativeSessionId
1147
+ || claim.targetAuthorityEpoch !== input.authorityEpoch
1148
+ || claim.targetAuthorityHolderId !== input.holderId
1149
+ || turn?.attemptId !== claim.targetAttemptId
1150
+ || !["completed", "failed", "cancelled"].includes(turn.status))) {
1151
+ throw new AgentHostProviderTurnFenceError("Global then target no longer has exact terminal proof.");
1152
+ }
1153
+ store.updateGlobalRoleMessage({ ...message, control: {
1154
+ requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
1155
+ receiptId: input.attemptId, outcome: "pending", observedAt: input.now.toISOString()
1156
+ } });
1157
+ }
1158
+ binding = admitOwnedProviderInput(store, binding, input);
1159
+ store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, beginProviderTurn(binding, { attemptId: input.attemptId,
1160
+ input: binding.retry?.currentAttemptId === input.attemptId ? binding.retry.input : ownedProviderInput(input),
1161
+ retrySupported: input.retrySupported,
1162
+ authorityEpoch: input.authorityEpoch, submittedAt: input.now.toISOString() }), input.now));
1163
+ });
1164
+ return;
1165
+ }
1166
+ this.beginTaskAgentHostProviderTurn({ ...input, taskId: input.taskId });
1167
+ }
1168
+ beginTaskAgentHostProviderTurn(input) {
874
1169
  this.store.transaction((store) => {
875
1170
  const task = store.getTask(input.taskId);
876
1171
  if (task === null || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled"
@@ -881,7 +1176,7 @@ export class FileSchedulerStoreAdapter {
881
1176
  }
882
1177
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
883
1178
  const session = sessions?.sessions[input.agentId];
884
- const binding = sessions?.providerBinding;
1179
+ let binding = sessions?.providerBinding;
885
1180
  const active = store.getActiveRun(input.taskId, input.roleName);
886
1181
  if (sessions === null || sessions === undefined || binding === null || binding === undefined || (input.runId !== undefined && (active?.id !== input.runId || active.effective.agentId !== input.agentId)) || session === undefined || session.nativeSessionId !== input.nativeSessionId || currentProviderConversation(binding).conversationId !== input.nativeSessionId || binding.authority.owner !== input.authorityOwner || binding.authority.epoch !== input.authorityEpoch || binding.authority.holderId !== input.holderId) {
887
1182
  throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
@@ -908,15 +1203,76 @@ export class FileSchedulerStoreAdapter {
908
1203
  .includes(binding.run.status)) {
909
1204
  throw new AgentHostProviderSessionBusyError("Provider Conversation already has an unsettled AgentRun.");
910
1205
  }
1206
+ if (!exactReplay) {
1207
+ const claims = store.listMessages(input.taskId).filter(message => message.interruptThen?.targetRoleName === input.roleName
1208
+ && message.interruptThen.targetNativeSessionId === input.nativeSessionId
1209
+ && message.interruptThen.targetAgentId === input.agentId
1210
+ && message.interruptThen.targetAuthorityEpoch === input.authorityEpoch
1211
+ && message.interruptThen.targetAuthorityHolderId === input.holderId
1212
+ && message.interruptThen.notDeliveredReason === undefined
1213
+ && message.continuation?.notDeliveredReason === undefined);
1214
+ for (const message of claims) {
1215
+ if (input.roleName === "leader") {
1216
+ const wakes = store.listTaskWakes(input.taskId).filter(wake => wake.refs?.some(ref => ref.type === "message" && ref.id === message.id));
1217
+ if (wakes.some(wake => wake.status === "consumed"))
1218
+ continue;
1219
+ const mailbox = store.getWorkMailbox({ kind: "role", taskId: input.taskId, roleName: "leader" });
1220
+ const selected = wakes.some(wake => mailbox?.processing?.owner === `leader-notification:${wake.id}`
1221
+ && mailbox.processing.batchId === input.attemptId);
1222
+ if (!selected)
1223
+ throw new AgentHostProviderSessionBusyError("The next Leader input belongs to an explicit then handoff.");
1224
+ }
1225
+ else {
1226
+ const continuationId = message.continuation?.runId;
1227
+ const continuation = continuationId === undefined ? null : store.getRun(input.taskId, continuationId);
1228
+ if (continuation !== null && (continuation.status !== "active" || currentRun?.runId === continuation.id))
1229
+ continue;
1230
+ if (input.runId !== continuationId || continuationId === undefined) {
1231
+ throw new AgentHostProviderSessionBusyError("The next execution input belongs to an explicit then handoff.");
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ binding = admitOwnedProviderInput(store, binding, input);
911
1237
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
912
1238
  ...(input.runId === undefined ? {} : { runId: input.runId }),
913
1239
  attemptId: input.attemptId,
914
1240
  authorityEpoch: input.authorityEpoch,
915
- submittedAt: input.now.toISOString()
1241
+ submittedAt: input.now.toISOString(),
1242
+ input: binding.retry?.currentAttemptId === input.attemptId ? binding.retry.input : ownedProviderInput(input),
1243
+ retrySupported: input.retrySupported
916
1244
  }), input.now));
917
1245
  });
918
1246
  }
919
1247
  resolveAgentHostProviderTurnSubmission(input) {
1248
+ if (input.taskId === undefined) {
1249
+ this.store.transaction((store) => {
1250
+ const sessions = store.getGlobalRoleSessionSet(input.roleName);
1251
+ const binding = sessions?.providerBinding;
1252
+ if (input.runId !== undefined || sessions == null || binding?.run?.attemptId !== input.attemptId) {
1253
+ throw new Error("Global input submission is no longer current.");
1254
+ }
1255
+ let resolved = settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
1256
+ reason: input.reason, resolvedAt: input.now.toISOString() });
1257
+ resolved = this.retrySubmissionResolution(resolved, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
1258
+ store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, resolved, input.now));
1259
+ settleGlobalRetryInput(store, input.roleName, resolved, input.now);
1260
+ const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1261
+ if (message !== undefined && message.delivery?.via !== "provider") {
1262
+ const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
1263
+ const updated = { ...message, control: {
1264
+ requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
1265
+ receiptId: input.attemptId, outcome, observedAt: input.now.toISOString()
1266
+ } };
1267
+ store.updateGlobalRoleMessage(input.status === "rejected" && updated.delivery === undefined && !providerRetryPending(resolved)
1268
+ ? markGlobalRoleMessageNotDelivered(updated, input.reason, input.now) : updated);
1269
+ }
1270
+ });
1271
+ return;
1272
+ }
1273
+ this.resolveTaskAgentHostProviderTurnSubmission({ ...input, taskId: input.taskId });
1274
+ }
1275
+ resolveTaskAgentHostProviderTurnSubmission(input) {
920
1276
  this.store.transaction((store) => {
921
1277
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
922
1278
  const binding = sessions?.providerBinding;
@@ -926,20 +1282,18 @@ export class FileSchedulerStoreAdapter {
926
1282
  || binding.run?.attemptId !== input.attemptId) {
927
1283
  throw new Error("Agent Host Provider Turn submission is no longer current.");
928
1284
  }
929
- const updated = settleProviderTurnSubmission(binding, {
1285
+ let updated = settleProviderTurnSubmission(binding, {
930
1286
  attemptId: input.attemptId,
931
1287
  status: input.status,
932
1288
  reason: input.reason,
933
1289
  resolvedAt: input.now.toISOString()
934
1290
  });
1291
+ updated = this.retrySubmissionResolution(updated, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
935
1292
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
936
- if (input.runId === undefined)
937
- return;
938
- const run = store.getRun(input.taskId, input.runId);
1293
+ const run = input.runId === undefined ? null : store.getRun(input.taskId, input.runId);
939
1294
  const session = sessions.sessions[sessions.activeAgentId];
940
- const driver = run === null
941
- ? null
942
- : this.drivers.findByAdapterId(run.effective.adapterId);
1295
+ const adapterId = run?.effective.adapterId ?? session?.adapterId;
1296
+ const driver = adapterId === undefined ? null : this.drivers.findByAdapterId(adapterId);
943
1297
  const error = standardAgentError({
944
1298
  source: "driver",
945
1299
  phase: "turn-submit",
@@ -953,7 +1307,7 @@ export class FileSchedulerStoreAdapter {
953
1307
  });
954
1308
  const errorEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
955
1309
  sourceEventId: input.attemptId,
956
- runId: input.runId,
1310
+ runId: input.runId ?? "",
957
1311
  roleName: input.roleName,
958
1312
  agentId: run?.effective.agentId ?? sessions.activeAgentId,
959
1313
  adapterId: run?.effective.adapterId ?? session?.adapterId ?? "unknown",
@@ -975,7 +1329,7 @@ export class FileSchedulerStoreAdapter {
975
1329
  sessionDisposition: error.sessionDisposition
976
1330
  }, input.now);
977
1331
  store.saveEvent(input.taskId, errorEvent);
978
- if (input.status === "deferred")
1332
+ if (input.status === "deferred" || providerRetryPending(updated))
979
1333
  return;
980
1334
  const route = input.roleName === "leader"
981
1335
  ? { kind: "operator" }
@@ -988,6 +1342,30 @@ export class FileSchedulerStoreAdapter {
988
1342
  });
989
1343
  });
990
1344
  }
1345
+ retrySubmissionResolution(binding, input, adapterId) {
1346
+ if (input.status === "deferred")
1347
+ return deferProviderRetry(binding, input.now.getTime());
1348
+ const driver = adapterId === undefined ? null : this.drivers.findByAdapterId(adapterId);
1349
+ const classification = driver?.runtime.mapError({ message: input.reason, raw: input.raw });
1350
+ const error = standardAgentError({
1351
+ source: "driver", phase: "turn-submit", classification,
1352
+ message: input.reason, raw: input.raw,
1353
+ inputDisposition: input.status === "delivery-unknown" ? "unknown" : "not-accepted"
1354
+ });
1355
+ // Native preflight issued no mutation. Transient metadata failures may
1356
+ // cool down within the same deadline; unavailable proof needs an Agent.
1357
+ const noProviderWrite = input.status === "rejected" && input.noProviderWrite === true;
1358
+ if (noProviderWrite && classification?.retryable !== true) {
1359
+ const recorded = binding.run === null ? binding : {
1360
+ ...binding, run: { ...binding.run, failure: { error, ref: input.attemptId } }
1361
+ };
1362
+ return deferProviderRetry(recorded, input.now.getTime(), input.reason);
1363
+ }
1364
+ return recordProviderFailure(binding, {
1365
+ error,
1366
+ failureRef: input.attemptId, at: input.now.getTime(), noProviderWrite
1367
+ });
1368
+ }
991
1369
  getProviderAuthorityFence(input) {
992
1370
  const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
993
1371
  const session = sessions?.sessions[input.agentId];
@@ -1014,6 +1392,8 @@ export class FileSchedulerStoreAdapter {
1014
1392
  }
1015
1393
  enqueueLeaderWakeup(taskId, reason, now) {
1016
1394
  return this.store.transaction((store) => {
1395
+ if (store.getTask(taskId)?.status === "archived")
1396
+ return null;
1017
1397
  const mailbox = enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, reason, now);
1018
1398
  const wakeup = pendingWakeupProjection(mailbox);
1019
1399
  if (wakeup === null)
@@ -1462,11 +1842,47 @@ export class FileSchedulerStoreAdapter {
1462
1842
  if (mailbox.pending === null || (provider !== null && provider !== undefined
1463
1843
  && ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
1464
1844
  return null;
1845
+ const pending = mailbox.pending;
1846
+ const pendingMessageIds = new Set(pending.refs.filter((ref) => ref.type === "message").map((ref) => ref.id));
1847
+ const claimedMessages = store.listMessages(taskId).filter((message) => pendingMessageIds.has(message.id) && message.interruptThen?.targetRoleName === "leader");
1848
+ const sessions = store.getTaskRoleSessionSet(taskId, "leader");
1849
+ const session = sessions?.sessions[sessions.activeAgentId];
1850
+ const obsoleteIds = new Set();
1851
+ for (const message of claimedMessages) {
1852
+ const claim = message.interruptThen;
1853
+ if (claim.notDeliveredReason !== undefined
1854
+ || session?.nativeSessionId !== claim.targetNativeSessionId
1855
+ || session.agentId !== claim.targetAgentId
1856
+ || session.adapterId !== claim.targetAdapterId
1857
+ || (sessions?.providerBinding != null && (sessions.providerBinding.authority.epoch !== claim.targetAuthorityEpoch
1858
+ || sessions.providerBinding.authority.holderId !== claim.targetAuthorityHolderId))) {
1859
+ obsoleteIds.add(message.id);
1860
+ if (claim.notDeliveredReason === undefined)
1861
+ store.updateMessage(taskId, {
1862
+ ...message, interruptThen: { ...claim, notDeliveredReason: "target-session-or-authority-changed" }
1863
+ });
1864
+ }
1865
+ }
1866
+ const availableRefs = pending.refs.filter(ref => !(ref.type === "message" && obsoleteIds.has(ref.id)));
1867
+ const handoffs = claimedMessages.filter(message => !obsoleteIds.has(message.id));
1868
+ // A quiet/new Session is not evidence that the claimed old Turn stopped.
1869
+ // Keep the exact pending intent until the original fence is provable.
1870
+ if (handoffs.some((message) => interruptThenTerminalState(store, message) !== "ready"))
1871
+ return null;
1872
+ const handoffIds = new Set(handoffs.map((message) => message.id));
1873
+ if (availableRefs.length === 0 && obsoleteIds.size > 0) {
1874
+ store.saveWorkMailbox(consumePendingBatch(mailbox, pending));
1875
+ return null;
1876
+ }
1877
+ const selectedRefs = handoffs.length === 0 ? availableRefs
1878
+ : availableRefs.filter((ref) => ref.type === "message" && handoffIds.has(ref.id));
1879
+ const remainingRefs = handoffs.length === 0 ? []
1880
+ : availableRefs.filter((ref) => !(ref.type === "message" && handoffIds.has(ref.id)));
1465
1881
  const latest = latestTaskWake(store.listTaskWakes(taskId));
1466
1882
  const wakeId = store.nextTaskWakeId(taskId);
1467
1883
  const wake = createTaskWake({
1468
1884
  id: wakeId, taskId, reasons: mailbox.pending.reasons,
1469
- refs: mailbox.pending.refs,
1885
+ refs: selectedRefs,
1470
1886
  fromCursor: latest?.toCursor ?? task.createdAt,
1471
1887
  toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now
1472
1888
  });
@@ -1475,6 +1891,10 @@ export class FileSchedulerStoreAdapter {
1475
1891
  owner: `leader-notification:${wakeId}`, startedAt: now.toISOString() });
1476
1892
  store.saveTaskWake(taskId, wake);
1477
1893
  store.saveWorkMailbox(mailbox);
1894
+ // The mailbox is only a hint. Carry ordinary refs into its next pending
1895
+ // batch, rather than delivering them with the chosen immediate successor.
1896
+ if (remainingRefs.length > 0)
1897
+ enqueueWork(store, target, "user-message", now, remainingRefs, { source: "controller", dedupeKey: `then-remainder:${wakeId}` });
1478
1898
  return { wakeId, attemptId, disposition: "submit" };
1479
1899
  });
1480
1900
  }
@@ -1490,6 +1910,8 @@ export class FileSchedulerStoreAdapter {
1490
1910
  && event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
1491
1911
  store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "notification.delivery", { attemptId, outcome, ...(detail === undefined ? {} : { detail }) }, now));
1492
1912
  }
1913
+ if (store.getTask(taskId)?.status === "archived")
1914
+ return;
1493
1915
  if (outcome !== "accepted" && outcome !== "rejected")
1494
1916
  return;
1495
1917
  store.saveWorkMailbox(completeProcessing(mailbox, attemptId));
@@ -1612,7 +2034,32 @@ export class FileSchedulerStoreAdapter {
1612
2034
  nativeSessionId: input.nativeSessionId,
1613
2035
  effective: input.effective
1614
2036
  }, now);
1615
- if (input.owner.scope === "task") {
2037
+ if (input.owner.scope === "global") {
2038
+ let sessions = store.getGlobalRoleSessionSet(input.owner.roleName);
2039
+ let binding = sessions.providerBinding;
2040
+ if (binding == null) {
2041
+ sessions = bindGlobalRoleProviderRuntime(sessions, createProviderRuntimeBinding({
2042
+ providerNamespace: this.drivers.requireByAdapterId(input.adapterId).id,
2043
+ accountScope: input.agentId, conversationId: input.nativeSessionId,
2044
+ startedAt: now.toISOString()
2045
+ }), now);
2046
+ }
2047
+ else {
2048
+ if (currentProviderConversation(binding).conversationId !== input.nativeSessionId) {
2049
+ binding = supersedeProviderConversation(binding, {
2050
+ conversationId: input.nativeSessionId, switchedAt: now.toISOString(), basis: "terminal-session"
2051
+ });
2052
+ }
2053
+ if (binding.authority.owner === "none")
2054
+ binding = transferProviderAuthority(binding, {
2055
+ expectedEpoch: binding.authority.epoch, expectedOwner: "none",
2056
+ owner: "controller", holderId: "controller", changedAt: now.toISOString()
2057
+ });
2058
+ sessions = updateGlobalRoleProviderRuntime(sessions, binding, now);
2059
+ }
2060
+ store.saveGlobalRoleSessionSet(sessions);
2061
+ }
2062
+ else {
1616
2063
  let sessions = store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName);
1617
2064
  if (sessions !== null && sessions.providerBinding === null) {
1618
2065
  sessions = bindTaskRoleProviderRuntime(sessions, createProviderRuntimeBinding({
@@ -1740,6 +2187,12 @@ export class FileSchedulerStoreAdapter {
1740
2187
  && sessions.providerBinding !== null) {
1741
2188
  sessions = updateTaskRoleProviderRuntime(sessions, updateProviderConversationRecoverability(sessions.providerBinding, "unrecoverable"), now);
1742
2189
  }
2190
+ if (input.observation?.kind === "turn.failed" && input.observation.payload.failure !== undefined
2191
+ && sessions.providerBinding !== null) {
2192
+ sessions = updateTaskRoleProviderRuntime(sessions, recordProviderFailure(sessions.providerBinding, {
2193
+ error: input.observation.payload.failure.error, failureRef: input.observation.eventId, at: now.getTime()
2194
+ }), now);
2195
+ }
1743
2196
  store.saveTaskRoleSessionSet(sessions);
1744
2197
  let terminalRun;
1745
2198
  if (recordedProviderTurn && observedRun?.status === "active") {
@@ -2082,6 +2535,14 @@ export class FileSchedulerStoreAdapter {
2082
2535
  return "obsolete";
2083
2536
  }
2084
2537
  store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
2538
+ const retry = binding.retry;
2539
+ if (retry?.currentAttemptId === ordinaryAttemptId && retry.input.kind === "wake") {
2540
+ const originalWakeId = retry.input.wakeId;
2541
+ const original = store.listTaskWakes(input.fence.taskId).find(w => w.id === originalWakeId);
2542
+ if (original !== undefined && original.status !== "consumed") {
2543
+ store.saveTaskWake(input.fence.taskId, markTaskWakeConsumed(original, now));
2544
+ }
2545
+ }
2085
2546
  return "applied";
2086
2547
  }
2087
2548
  }
@@ -2261,6 +2722,14 @@ export class FileSchedulerStoreAdapter {
2261
2722
  effective
2262
2723
  }, now);
2263
2724
  current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.nativeTurnId, now);
2725
+ // decision-3 §9: when a controller-owned native Turn is actually bound for
2726
+ // this Global Role, settle it to the observed terminal so the scope-generic
2727
+ // resolver and the interrupt-then gate see the real stop on the live binding
2728
+ // — the same edge Task settles via settleStructuredProviderTurn. This is a
2729
+ // strict guarded no-op for an older unmanaged Session or a
2730
+ // binding holding a different/already-terminal Turn, and the
2731
+ // durable native terminal recorded just above remains the proof of record.
2732
+ current = settleGlobalRoleRuntimeTurn(current, input.nativeTurnId, input.providerStatus, now);
2264
2733
  store.saveGlobalRoleSessionSet(current);
2265
2734
  if (input.roleName === SYSTEM_OPERATOR_ROLE
2266
2735
  && input.adapterId === "codex"
@@ -2368,7 +2837,8 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
2368
2837
  roleName: input.fence.roleName,
2369
2838
  agentId: input.fence.agentId,
2370
2839
  ...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
2371
- ...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId })
2840
+ ...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId }),
2841
+ ...(reason === "task-archived" ? { originalEvent: input } : {})
2372
2842
  }, reason, now);
2373
2843
  }
2374
2844
  function recordObsoleteRuntimeEvent(store, input, reason, now) {
@@ -2383,7 +2853,8 @@ function recordObsoleteRuntimeEvent(store, input, reason, now) {
2383
2853
  agentId: input.agentId,
2384
2854
  ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
2385
2855
  ...(input.runId === undefined ? {} : { runId: input.runId }),
2386
- reason
2856
+ reason,
2857
+ ...(input.originalEvent === undefined ? {} : { originalEvent: JSON.stringify(input.originalEvent) })
2387
2858
  }, now));
2388
2859
  }
2389
2860
  function sessionPreview(value) {
@@ -3033,6 +3504,29 @@ function settleStructuredProviderTurn(sessions, runId, status, now, attemptId) {
3033
3504
  settledAt: now.toISOString()
3034
3505
  }), now);
3035
3506
  }
3507
+ /**
3508
+ * The Global twin of {@link settleStructuredProviderTurn}: settle a Global Role's
3509
+ * own live Provider Turn to the observed native terminal, keyed by the exact
3510
+ * native Turn id the real Provider Stop/StopFailure hook carries (decision-3 §9).
3511
+ * It is a strict guarded no-op unless a controller-owned Turn with that exact
3512
+ * native id is bound and still in flight (`accepted`) — an absent binding, a
3513
+ * different/only-submitting Turn, or an already-terminal one is left untouched, so
3514
+ * an older unmanaged Session without a controlled binding settles nothing
3515
+ * and the durable `recentCompletedTurnIds` terminal remains the proof of record.
3516
+ */
3517
+ function settleGlobalRoleRuntimeTurn(sessions, nativeTurnId, status, now) {
3518
+ const binding = sessions.providerBinding;
3519
+ if (binding === null || binding === undefined || binding.run === null
3520
+ || binding.run.nativeTurnId !== nativeTurnId
3521
+ || binding.run.status !== "accepted") {
3522
+ return sessions;
3523
+ }
3524
+ return updateGlobalRoleProviderRuntime(sessions, settleProviderTurn(binding, {
3525
+ nativeTurnId,
3526
+ status,
3527
+ settledAt: now.toISOString()
3528
+ }), now);
3529
+ }
3036
3530
  function usageSnapshotIsSuperseded(events, incoming) {
3037
3531
  if (incoming.kind !== "activity.observed" || incoming.payload.usage === undefined) {
3038
3532
  return false;