@zq-silk/yui 0.15.9 → 0.15.11

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 (100) hide show
  1. package/dist/agent/launchEnvironment.js +7 -0
  2. package/dist/cli/commandCatalog.js +26 -6
  3. package/dist/cli/interactionPolicy.js +3 -3
  4. package/dist/cli/updateOrchestrator.js +24 -1
  5. package/dist/cli/updatePorts.js +7 -3
  6. package/dist/cli/upgradeCommand.js +42 -2
  7. package/dist/cli.js +336 -93
  8. package/dist/commands/globalRoleCommands.js +314 -4
  9. package/dist/commands/projectCommands.js +6 -7
  10. package/dist/commands/releaseCommands.js +18 -0
  11. package/dist/commands/taskActor.js +25 -0
  12. package/dist/commands/taskCommands.js +511 -83
  13. package/dist/commands/taskIntegrationCommands.js +16 -38
  14. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  15. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  16. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  17. package/dist/context/taskContext.js +38 -2
  18. package/dist/controller/agentHostObservation.js +155 -0
  19. package/dist/controller/clientRuntime.js +17 -2
  20. package/dist/controller/controller.js +3 -0
  21. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  22. package/dist/controller/globalInputDelivery.js +119 -0
  23. package/dist/controller/jobControl.js +6 -2
  24. package/dist/controller/resourceInventory.js +14 -4
  25. package/dist/controller/resourceInventoryLinux.js +2 -6
  26. package/dist/controller/runtime.js +81 -6
  27. package/dist/controller/runtimeEventInbox.js +32 -3
  28. package/dist/controller/runtimeEventProcessor.js +26 -6
  29. package/dist/controller/runtimeHookRunFence.js +75 -19
  30. package/dist/controller/structuredProviderObservation.js +133 -70
  31. package/dist/coordination/workMailboxQueue.js +5 -0
  32. package/dist/execution/workItemExecutionProjection.js +1 -1
  33. package/dist/executor/agentExecutor.js +64 -4
  34. package/dist/executor/executorRegistry.js +3 -0
  35. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  36. package/dist/integration/deliveryObligation.js +2 -1
  37. package/dist/integration/gitIntegrationService.js +312 -382
  38. package/dist/integration/integrationAttempt.js +30 -4
  39. package/dist/integration/integrationQueueService.js +7 -7
  40. package/dist/integration/integrationSourceApplication.js +323 -0
  41. package/dist/message/globalInterrupt.js +33 -0
  42. package/dist/message/inputControlResolution.js +106 -0
  43. package/dist/message/message.js +367 -0
  44. package/dist/message/messageContinuation.js +126 -3
  45. package/dist/message/taskInterrupt.js +34 -0
  46. package/dist/observability/orchestrationMetrics.js +1 -1
  47. package/dist/release/releaseHandover.js +22 -0
  48. package/dist/release/releaseWorkflowPorts.js +15 -7
  49. package/dist/repository/gitWorkspace.js +72 -15
  50. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  51. package/dist/repository/taskWorkspacePreparer.js +120 -49
  52. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  53. package/dist/resources/resourceDiscovery.js +3 -2
  54. package/dist/runtime/agentHost.js +152 -72
  55. package/dist/runtime/agentHostCompatibility.js +127 -0
  56. package/dist/runtime/agentHostProtocol.js +53 -0
  57. package/dist/runtime/executionEnvironment.js +0 -19
  58. package/dist/runtime/launchBroker.js +6 -0
  59. package/dist/runtime/sessionReconciliation.js +4 -4
  60. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  61. package/dist/runtime/tmuxAdapters.js +5 -3
  62. package/dist/scheduler/operatorEvent.js +4 -0
  63. package/dist/scheduler/taskExecutionProjection.js +12 -1
  64. package/dist/scheduler/wakeReason.js +7 -1
  65. package/dist/scheduler/wakeupQueue.js +2 -0
  66. package/dist/setup/setupCommand.js +26 -8
  67. package/dist/storage/homeLayout.js +130 -0
  68. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  69. package/dist/storage/migrations/integrationContinuation.js +104 -0
  70. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  71. package/dist/storage/sqliteSchema.js +136 -4
  72. package/dist/storage/sqliteStore.js +40 -1
  73. package/dist/storage/storageVersions.js +1 -1
  74. package/dist/storage/storeRpc.js +1 -0
  75. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  76. package/dist/task/archiveDiagnostics.js +128 -0
  77. package/dist/task/nextAction.js +44 -11
  78. package/dist/web/assets/client/app.js +55 -0
  79. package/dist/web/assets/client/components.js +1 -0
  80. package/dist/web/assets/client/i18n.js +6 -0
  81. package/dist/web/assets/client/taskSurface.js +106 -0
  82. package/dist/web/assets/client/view.js +7 -4
  83. package/dist/web/assets/shell.js +23 -0
  84. package/dist/web/assets/styles/layout.js +1 -1
  85. package/dist/web/assets/styles/widgets.js +12 -0
  86. package/dist/web/webServer.js +117 -1
  87. package/dist/web/webSnapshot.js +4 -3
  88. package/dist/web/webTaskSurface.js +222 -5
  89. package/dist/workspace/workItemChangeSetManager.js +18 -2
  90. package/docs/release-workflow.md +39 -0
  91. package/docs/release-workflow.zh-CN.md +29 -0
  92. package/docs/sqlite-control-plane-design.md +223 -1
  93. package/docs/testing/verification-levels.md +24 -0
  94. package/docs/testing/verification-levels.zh-CN.md +11 -0
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +151 -49
  97. package/skills/yui-leader/references/integration.md +52 -2
  98. package/skills/yui-operator/SKILL.md +6 -1
  99. package/skills/yui-runtime/SKILL.md +27 -0
  100. package/skills/yui-runtime/references/publication.md +20 -0
@@ -1,10 +1,12 @@
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 { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindGlobalRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime, updateGlobalRoleProviderRuntime, selectNewTaskRoleSession, taskRoleControlTarget } from "../executor/agentExecutor.js";
8
10
  import { acceptProviderTurn, cancelQuiescentProviderInput, beginProviderTurn, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, clearProviderGoal, settleProviderTurnSubmission, settleProviderTurn, transferProviderAuthority, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
9
11
  import { createTaskEvent } from "../event/taskEvent.js";
10
12
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
@@ -17,6 +19,7 @@ import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
17
19
  import { appendRunInput, createRun, withRunContextSnapshot, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
18
20
  import { transportAgentResult } from "../domain/agentResultTransport.js";
19
21
  import { createRunInput } from "../context/runInputContract.js";
22
+ import { recordTaskMessageControlOutcome, markGlobalRoleMessageDelivered, markGlobalRoleMessageNotDelivered } from "../message/message.js";
20
23
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
21
24
  import { terminalizeExactTaskRun, cancelQuiescentRoleRuns } from "../lifecycle/exactRunTerminalization.js";
22
25
  import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
@@ -29,7 +32,7 @@ import { pendingWakeupProjection } from "../storage/taskStore.js";
29
32
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
30
33
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
31
34
  import { formatRunReceiptId } from "../task/taskRecordReference.js";
32
- import { bindExecution, claimPending, completeProcessing, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
35
+ import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
33
36
  import { enqueueWork, settleRoleRunDispatch as settleRoleRunDispatchMailbox } from "../coordination/workMailboxQueue.js";
34
37
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
35
38
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
@@ -58,6 +61,57 @@ export class FileSchedulerStoreAdapter {
58
61
  this.drivers = drivers;
59
62
  this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
60
63
  }
64
+ observeAgentHostObservation(event, now = new Date()) {
65
+ // Match canonical ingress: archive and Host fact settlement share the
66
+ // same write boundary, including the original transport envelope.
67
+ return this.store.transaction(() => this.foldAgentHostObservation(event, now));
68
+ }
69
+ foldAgentHostObservation(event, now) {
70
+ const raw = createRuntimeObservation(event.observation);
71
+ const task = raw.fence.taskId === undefined ? null : this.store.getTask(raw.fence.taskId);
72
+ if (raw.fence.taskId !== undefined && task === null)
73
+ return "obsolete";
74
+ if (task?.status === "archived") {
75
+ this.observeObsoleteRuntimeEvent({
76
+ eventId: event.id, eventType: event.type, taskId: task.id,
77
+ roleName: raw.fence.roleName, agentId: raw.fence.agentId,
78
+ nativeSessionId: raw.fence.nativeSessionId ?? "unknown",
79
+ reason: "task-archived", originalEvent: event
80
+ }, now);
81
+ return "obsolete";
82
+ }
83
+ if (Date.parse(raw.receivedAt) > now.getTime()) {
84
+ return this.recordObsoleteCanonicalObservation(raw, "received-at-in-future", now);
85
+ }
86
+ let input;
87
+ try {
88
+ if (event.observation.kind === "host.observed") {
89
+ return this.store.transaction(store => {
90
+ const connection = resolveAgentHostObservation(store, event);
91
+ recordAgentHostConnection(store, event, connection, now);
92
+ if (connection.fence.taskId !== undefined)
93
+ this.persistRuntimeObservation(connection, now);
94
+ return "applied";
95
+ });
96
+ }
97
+ input = resolveAgentHostObservation(this.store, event);
98
+ if (input.kind === "input.accepted" && input.fence.runId === undefined
99
+ && input.fence.receiptId?.startsWith("native-input:")) {
100
+ // A valid ordinary conversation has no managed Run input to mirror.
101
+ // Consuming this fact is not rejection, nor assignment acceptance.
102
+ return "applied";
103
+ }
104
+ }
105
+ catch (error) {
106
+ if (error instanceof AgentHostObservationDeferred)
107
+ return "deferred";
108
+ if (!(error instanceof RuntimeHookRunFenceError))
109
+ throw error;
110
+ // Rejection is durable diagnostic evidence, never business acceptance.
111
+ return this.recordObsoleteCanonicalObservation(event.observation, error.message, now);
112
+ }
113
+ return this.observeRuntimeObservation(input, now);
114
+ }
61
115
  /**
62
116
  * Sole provider-independent ingress for structured runtime state. Driver
63
117
  * mapping has already happened before this boundary; this method validates
@@ -65,10 +119,19 @@ export class FileSchedulerStoreAdapter {
65
119
  * transitions, and retains the canonical observation for status projection.
66
120
  */
67
121
  observeRuntimeObservation(raw, now = new Date()) {
122
+ // Archive and observation settlement share the same write boundary.
123
+ // An ingress read outside this transaction must never authorize clearing
124
+ // a claim after another CLI has committed archive.
125
+ return this.store.transaction(() => this.foldRuntimeObservation(raw, now));
126
+ }
127
+ foldRuntimeObservation(raw, now) {
68
128
  const input = createRuntimeObservation(raw);
69
129
  const taskId = input.fence.taskId;
70
130
  if (taskId === undefined)
71
- return "obsolete";
131
+ return this.observeGlobalProviderObservation(input, now);
132
+ if (this.store.getTask(taskId)?.status === "archived") {
133
+ return this.recordObsoleteCanonicalObservation(input, "task-archived", now);
134
+ }
72
135
  if (Date.parse(input.receivedAt) > now.getTime()) {
73
136
  return this.recordObsoleteCanonicalObservation(input, "received-at-in-future", now);
74
137
  }
@@ -83,12 +146,18 @@ export class FileSchedulerStoreAdapter {
83
146
  const run = input.fence.runId === undefined ? null : store.getRun(taskId, input.fence.runId);
84
147
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
85
148
  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)
149
+ const isSteer = input.fence.receiptId.startsWith("steer:");
150
+ // A Worker/Reviewer steer's accepted input is folded onto the owning
151
+ // AgentRun. A no-Run Leader steer (decision-3 §9) has no run to append to,
152
+ // but its accepted disposition is still a real fact that gap D records on
153
+ // the Message; only the Worker append requires the exact Run/Turn fence.
154
+ const runFolded = run !== null && run.roleName === input.fence.roleName
155
+ && run.effective.agentId === input.fence.agentId && native?.runId === run.id
156
+ && native.nativeTurnId === input.fence.nativeTurnId
157
+ && sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId;
158
+ if (!runFolded && !(isSteer && input.fence.runId === undefined))
90
159
  return "obsolete";
91
- if (!hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
160
+ if (runFolded && !hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
92
161
  const updated = appendRunInput(run, createRunInput({
93
162
  source: input.fence.receiptId.startsWith("native-input:")
94
163
  ? { type: "provider", channel: "visible-input" }
@@ -98,6 +167,31 @@ export class FileSchedulerStoreAdapter {
98
167
  store.saveRun(updated);
99
168
  store.saveActiveRun(updated);
100
169
  }
170
+ // decision-3 §3/§8, message-5 gap D: promote the Message's independent
171
+ // control op to its proven terminal from the Host's own settlement, keyed
172
+ // by this exact steer receipt. Monotonic and idempotent — never a rewind
173
+ // of an already-proven outcome, and a replayed observation is absorbed.
174
+ if (isSteer)
175
+ this.recordSteerControlOutcome(store, input.fence.receiptId, "accepted", now);
176
+ this.persistRuntimeObservation(input, now);
177
+ return "applied";
178
+ });
179
+ }
180
+ // A steer's rejected/delivery-unknown disposition is the Host's proven
181
+ // terminal for the one live control attempt (message-5 gap D). It carries no
182
+ // AgentRun append — a rejected input changed no execution — so it is recorded
183
+ // only on the Message's control op, for a Worker/Reviewer or a no-Run Leader
184
+ // steer alike, keyed by the exact steer receipt.
185
+ if ((input.kind === "input.rejected" || input.kind === "input.delivery-unknown")
186
+ && input.fence.receiptId?.startsWith("steer:") === true) {
187
+ return this.store.transaction((store) => {
188
+ if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
189
+ return "applied";
190
+ const recorded = this.recordSteerControlOutcome(store, input.fence.receiptId, input.kind === "input.rejected" ? "rejected" : "delivery-unknown", now);
191
+ if (!recorded) {
192
+ recordCanonicalObservationObsolete(store, input, "steer-message-not-found", now);
193
+ return "obsolete";
194
+ }
101
195
  this.persistRuntimeObservation(input, now);
102
196
  return "applied";
103
197
  });
@@ -173,6 +267,38 @@ export class FileSchedulerStoreAdapter {
173
267
  }
174
268
  return outcome;
175
269
  }
270
+ /**
271
+ * Promote a steer Message's independent control op to a proven outcome from the
272
+ * Host's own settlement (decision-3 §3/§8, message-5 gap D). The `steer:` fence
273
+ * names the exact `steer:<taskId>/<messageId>` receipt, so the outcome binds to
274
+ * that one Message and never another; the requestId is read from the Message's
275
+ * own steer identity (its live input, or the reused-steer provenance a handoff
276
+ * preserved) so the recorded op matches the same idempotency key the live edge
277
+ * used. Returns false when the receipt names no such steer Message, so the
278
+ * caller can mark the observation obsolete rather than silently drop it. The
279
+ * record is monotonic and idempotent inside {@link recordTaskMessageControlOutcome}.
280
+ */
281
+ recordSteerControlOutcome(store, receiptId, outcome, now) {
282
+ const parsed = /^steer:([^/]+)\/(.+)$/.exec(receiptId);
283
+ if (parsed === null)
284
+ return false;
285
+ const [, taskId, messageId] = parsed;
286
+ const message = store.listMessages(taskId).find((entry) => entry.id === messageId);
287
+ const requestId = message?.inputControl?.requestId ?? message?.interruptThen?.reusedInput?.requestId;
288
+ const wasSteer = message?.inputControl?.action === "steer"
289
+ || message?.interruptThen?.reusedInput?.action === "steer";
290
+ if (message === undefined || requestId === undefined || !wasSteer)
291
+ return false;
292
+ const recorded = recordTaskMessageControlOutcome(message, {
293
+ requestId, receiptId, outcome, observedAt: now
294
+ });
295
+ // The monotonic guard returns the same Message when the record is absorbed
296
+ // (an idempotent repeat, or a stale outcome that must not rewind a proven
297
+ // terminal); only persist a real change.
298
+ if (recorded !== message)
299
+ store.updateMessage(taskId, recorded);
300
+ return true;
301
+ }
176
302
  /** A delayed steer receipt settles only its original claimed mailbox batch. */
177
303
  observeLeaderSteerReceipt(input, now) {
178
304
  return this.store.transaction((store) => {
@@ -238,6 +364,95 @@ export class FileSchedulerStoreAdapter {
238
364
  return "applied";
239
365
  });
240
366
  }
367
+ observeGlobalProviderObservation(input, now) {
368
+ return this.store.transaction((store) => {
369
+ const fence = input.fence;
370
+ let sessions = store.getGlobalRoleSessionSet(fence.roleName);
371
+ const session = sessions?.sessions[fence.agentId];
372
+ let binding = sessions?.providerBinding;
373
+ if (sessions == null || session == null || binding == null || fence.runId !== undefined
374
+ || sessions.activeAgentId !== fence.agentId
375
+ || session.nativeSessionId !== fence.nativeSessionId
376
+ || this.drivers.requireByAdapterId(session.adapterId).id !== fence.driverId
377
+ || currentProviderConversation(binding).conversationId !== fence.nativeSessionId
378
+ || Date.parse(input.receivedAt) > now.getTime())
379
+ return "obsolete";
380
+ const at = input.observedAt ?? input.receivedAt;
381
+ const receipt = fence.receiptId;
382
+ const matches = receipt !== undefined && binding.run?.attemptId === receipt
383
+ && (fence.nativeTurnId === undefined || binding.run.nativeTurnId === undefined
384
+ || binding.run.nativeTurnId === fence.nativeTurnId);
385
+ if (input.kind === "turn.accepted") {
386
+ if (input.authority === "transport" && matches) {
387
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
388
+ if (message !== undefined)
389
+ store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at), "transport"));
390
+ return "applied";
391
+ }
392
+ if (input.authority !== "provider-structured")
393
+ return "applied";
394
+ if (!matches) {
395
+ if (receipt?.startsWith("direct:") !== true || binding.run != null
396
+ && ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status))
397
+ return "obsolete";
398
+ binding = beginProviderTurn(binding, { attemptId: receipt,
399
+ authorityEpoch: binding.authority.epoch, submittedAt: at });
400
+ }
401
+ if (binding.run?.status === "submitting" || binding.run?.status === "delivery-unknown") {
402
+ binding = acceptProviderTurn(binding, { attemptId: receipt,
403
+ nativeTurnId: fence.nativeTurnId, acceptedAt: at });
404
+ }
405
+ else if (binding.run?.status !== "accepted")
406
+ return "applied";
407
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
408
+ if (message !== undefined)
409
+ store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
410
+ }
411
+ else if (["turn.completed", "turn.cancelled", "turn.failed"].includes(input.kind)) {
412
+ if (!matches || input.authority !== "provider-structured")
413
+ return "obsolete";
414
+ // A provider final result proves this exact submitted input ran (Claude
415
+ // may emit its result without an earlier assistant acceptance event).
416
+ // Process exit/failure alone must not turn unconfirmed input into proof.
417
+ if (input.kind === "turn.completed" && binding.run != null
418
+ && ["submitting", "delivery-unknown"].includes(binding.run.status)) {
419
+ binding = acceptProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId, acceptedAt: at });
420
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
421
+ if (message !== undefined)
422
+ store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
423
+ }
424
+ if (binding.run?.status !== "accepted")
425
+ return binding.run != null
426
+ && ["completed", "cancelled", "failed"].includes(binding.run.status) ? "applied" : "obsolete";
427
+ binding = settleProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId,
428
+ status: input.kind === "turn.completed" ? "completed" : input.kind === "turn.cancelled" ? "cancelled" : "failed",
429
+ settledAt: at });
430
+ if (fence.nativeTurnId !== undefined)
431
+ sessions = rememberRoleAgentCompletedTurn(sessions, fence.agentId, fence.nativeSessionId, fence.nativeTurnId, new Date(at));
432
+ }
433
+ else if (["input.accepted", "input.rejected", "input.delivery-unknown"].includes(input.kind)) {
434
+ if (input.authority !== "provider-structured" || binding.run?.nativeTurnId !== fence.nativeTurnId
435
+ || binding.run?.status !== "accepted")
436
+ return "obsolete";
437
+ const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `steer:${fence.roleName}/${entry.id}`);
438
+ if (message !== undefined && message.inputControl !== undefined) {
439
+ const outcome = input.kind === "input.accepted" ? "accepted"
440
+ : input.kind === "input.rejected" ? "rejected" : "delivery-unknown";
441
+ if (message.control?.outcome !== "accepted" && message.control?.outcome !== "rejected") {
442
+ const updated = { ...message, control: { requestId: message.inputControl.requestId,
443
+ receiptId: receipt, outcome, observedAt: at } };
444
+ store.updateGlobalRoleMessage(outcome === "accepted"
445
+ ? markGlobalRoleMessageDelivered(updated, new Date(at)) : updated);
446
+ }
447
+ }
448
+ }
449
+ else if (input.kind === "conversation.observed" && input.payload.recoverability !== undefined) {
450
+ binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
451
+ }
452
+ store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
453
+ return "applied";
454
+ });
455
+ }
241
456
  adapterForRuntimeObservation(input) {
242
457
  const taskId = input.fence.taskId;
243
458
  const runId = input.fence.runId;
@@ -871,6 +1086,67 @@ export class FileSchedulerStoreAdapter {
871
1086
  });
872
1087
  }
873
1088
  beginAgentHostProviderTurn(input) {
1089
+ if (input.taskId === undefined) {
1090
+ this.store.transaction((store) => {
1091
+ if (input.runId !== undefined || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: input.roleName })))) {
1092
+ throw new AgentHostProviderTurnFenceError("Global Session admission is stopped.");
1093
+ }
1094
+ const sessions = store.getGlobalRoleSessionSet(input.roleName);
1095
+ const binding = sessions?.providerBinding;
1096
+ const session = sessions?.sessions[input.agentId];
1097
+ if (sessions === null || binding == null || session === undefined
1098
+ || sessions.activeAgentId !== input.agentId || session.status !== "active"
1099
+ || session.nativeSessionId !== input.nativeSessionId
1100
+ || currentProviderConversation(binding).conversationId !== input.nativeSessionId
1101
+ || binding.authority.owner !== input.authorityOwner
1102
+ || binding.authority.epoch !== input.authorityEpoch
1103
+ || binding.authority.holderId !== input.holderId) {
1104
+ throw new AgentHostProviderTurnFenceError("Global input carries a stale Session or writer fence.");
1105
+ }
1106
+ const turn = binding.run;
1107
+ if (turn?.attemptId === input.attemptId && turn.status === "submitting")
1108
+ return;
1109
+ if (turn != null && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)) {
1110
+ throw new AgentHostProviderSessionBusyError("Global Conversation has an unsettled input.");
1111
+ }
1112
+ const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1113
+ const reserved = store.listGlobalRoleMessages(input.roleName).find(entry => entry.delivery === undefined && entry.notDelivered === undefined
1114
+ && entry.interruptThen?.targetNativeSessionId === input.nativeSessionId
1115
+ && entry.interruptThen.targetAgentId === input.agentId
1116
+ && entry.interruptThen.targetAuthorityEpoch === input.authorityEpoch
1117
+ && entry.interruptThen.targetAuthorityHolderId === input.holderId);
1118
+ if (reserved !== undefined && reserved.id !== message?.id) {
1119
+ throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
1120
+ }
1121
+ if (message !== undefined) {
1122
+ if (message.delivery !== undefined || message.notDelivered !== undefined
1123
+ || message.control !== undefined && message.control.outcome !== "rejected"
1124
+ || message.deliveryTarget?.agentId !== input.agentId
1125
+ || message.deliveryTarget?.nativeSessionId !== input.nativeSessionId) {
1126
+ throw new AgentHostProviderTurnFenceError("Global Message is settled, unconfirmed, or bound to another Session.");
1127
+ }
1128
+ const claim = message.interruptThen;
1129
+ if (claim !== undefined && (claim.targetAgentId !== input.agentId
1130
+ || claim.targetNativeSessionId !== input.nativeSessionId
1131
+ || claim.targetAuthorityEpoch !== input.authorityEpoch
1132
+ || claim.targetAuthorityHolderId !== input.holderId
1133
+ || turn?.attemptId !== claim.targetAttemptId
1134
+ || !["completed", "failed", "cancelled"].includes(turn.status))) {
1135
+ throw new AgentHostProviderTurnFenceError("Global then target no longer has exact terminal proof.");
1136
+ }
1137
+ store.updateGlobalRoleMessage({ ...message, control: {
1138
+ requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
1139
+ receiptId: input.attemptId, outcome: "pending", observedAt: input.now.toISOString()
1140
+ } });
1141
+ }
1142
+ store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, beginProviderTurn(binding, { attemptId: input.attemptId,
1143
+ authorityEpoch: input.authorityEpoch, submittedAt: input.now.toISOString() }), input.now));
1144
+ });
1145
+ return;
1146
+ }
1147
+ this.beginTaskAgentHostProviderTurn({ ...input, taskId: input.taskId });
1148
+ }
1149
+ beginTaskAgentHostProviderTurn(input) {
874
1150
  this.store.transaction((store) => {
875
1151
  const task = store.getTask(input.taskId);
876
1152
  if (task === null || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled"
@@ -908,6 +1184,36 @@ export class FileSchedulerStoreAdapter {
908
1184
  .includes(binding.run.status)) {
909
1185
  throw new AgentHostProviderSessionBusyError("Provider Conversation already has an unsettled AgentRun.");
910
1186
  }
1187
+ if (!exactReplay) {
1188
+ const claims = store.listMessages(input.taskId).filter(message => message.interruptThen?.targetRoleName === input.roleName
1189
+ && message.interruptThen.targetNativeSessionId === input.nativeSessionId
1190
+ && message.interruptThen.targetAgentId === input.agentId
1191
+ && message.interruptThen.targetAuthorityEpoch === input.authorityEpoch
1192
+ && message.interruptThen.targetAuthorityHolderId === input.holderId
1193
+ && message.interruptThen.notDeliveredReason === undefined
1194
+ && message.continuation?.notDeliveredReason === undefined);
1195
+ for (const message of claims) {
1196
+ if (input.roleName === "leader") {
1197
+ const wakes = store.listTaskWakes(input.taskId).filter(wake => wake.refs?.some(ref => ref.type === "message" && ref.id === message.id));
1198
+ if (wakes.some(wake => wake.status === "consumed"))
1199
+ continue;
1200
+ const mailbox = store.getWorkMailbox({ kind: "role", taskId: input.taskId, roleName: "leader" });
1201
+ const selected = wakes.some(wake => mailbox?.processing?.owner === `leader-notification:${wake.id}`
1202
+ && mailbox.processing.batchId === input.attemptId);
1203
+ if (!selected)
1204
+ throw new AgentHostProviderSessionBusyError("The next Leader input belongs to an explicit then handoff.");
1205
+ }
1206
+ else {
1207
+ const continuationId = message.continuation?.runId;
1208
+ const continuation = continuationId === undefined ? null : store.getRun(input.taskId, continuationId);
1209
+ if (continuation !== null && (continuation.status !== "active" || currentRun?.runId === continuation.id))
1210
+ continue;
1211
+ if (input.runId !== continuationId || continuationId === undefined) {
1212
+ throw new AgentHostProviderSessionBusyError("The next execution input belongs to an explicit then handoff.");
1213
+ }
1214
+ }
1215
+ }
1216
+ }
911
1217
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
912
1218
  ...(input.runId === undefined ? {} : { runId: input.runId }),
913
1219
  attemptId: input.attemptId,
@@ -917,6 +1223,31 @@ export class FileSchedulerStoreAdapter {
917
1223
  });
918
1224
  }
919
1225
  resolveAgentHostProviderTurnSubmission(input) {
1226
+ if (input.taskId === undefined) {
1227
+ this.store.transaction((store) => {
1228
+ const sessions = store.getGlobalRoleSessionSet(input.roleName);
1229
+ const binding = sessions?.providerBinding;
1230
+ if (input.runId !== undefined || sessions == null || binding?.run?.attemptId !== input.attemptId) {
1231
+ throw new Error("Global input submission is no longer current.");
1232
+ }
1233
+ store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
1234
+ reason: input.reason, resolvedAt: input.now.toISOString() }), input.now));
1235
+ const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1236
+ if (message !== undefined && message.delivery?.via !== "provider") {
1237
+ const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
1238
+ const updated = { ...message, control: {
1239
+ requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
1240
+ receiptId: input.attemptId, outcome, observedAt: input.now.toISOString()
1241
+ } };
1242
+ store.updateGlobalRoleMessage(input.status === "rejected" && updated.delivery === undefined
1243
+ ? markGlobalRoleMessageNotDelivered(updated, input.reason, input.now) : updated);
1244
+ }
1245
+ });
1246
+ return;
1247
+ }
1248
+ this.resolveTaskAgentHostProviderTurnSubmission({ ...input, taskId: input.taskId });
1249
+ }
1250
+ resolveTaskAgentHostProviderTurnSubmission(input) {
920
1251
  this.store.transaction((store) => {
921
1252
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
922
1253
  const binding = sessions?.providerBinding;
@@ -1014,6 +1345,8 @@ export class FileSchedulerStoreAdapter {
1014
1345
  }
1015
1346
  enqueueLeaderWakeup(taskId, reason, now) {
1016
1347
  return this.store.transaction((store) => {
1348
+ if (store.getTask(taskId)?.status === "archived")
1349
+ return null;
1017
1350
  const mailbox = enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, reason, now);
1018
1351
  const wakeup = pendingWakeupProjection(mailbox);
1019
1352
  if (wakeup === null)
@@ -1462,11 +1795,47 @@ export class FileSchedulerStoreAdapter {
1462
1795
  if (mailbox.pending === null || (provider !== null && provider !== undefined
1463
1796
  && ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
1464
1797
  return null;
1798
+ const pending = mailbox.pending;
1799
+ const pendingMessageIds = new Set(pending.refs.filter((ref) => ref.type === "message").map((ref) => ref.id));
1800
+ const claimedMessages = store.listMessages(taskId).filter((message) => pendingMessageIds.has(message.id) && message.interruptThen?.targetRoleName === "leader");
1801
+ const sessions = store.getTaskRoleSessionSet(taskId, "leader");
1802
+ const session = sessions?.sessions[sessions.activeAgentId];
1803
+ const obsoleteIds = new Set();
1804
+ for (const message of claimedMessages) {
1805
+ const claim = message.interruptThen;
1806
+ if (claim.notDeliveredReason !== undefined
1807
+ || session?.nativeSessionId !== claim.targetNativeSessionId
1808
+ || session.agentId !== claim.targetAgentId
1809
+ || session.adapterId !== claim.targetAdapterId
1810
+ || (sessions?.providerBinding != null && (sessions.providerBinding.authority.epoch !== claim.targetAuthorityEpoch
1811
+ || sessions.providerBinding.authority.holderId !== claim.targetAuthorityHolderId))) {
1812
+ obsoleteIds.add(message.id);
1813
+ if (claim.notDeliveredReason === undefined)
1814
+ store.updateMessage(taskId, {
1815
+ ...message, interruptThen: { ...claim, notDeliveredReason: "target-session-or-authority-changed" }
1816
+ });
1817
+ }
1818
+ }
1819
+ const availableRefs = pending.refs.filter(ref => !(ref.type === "message" && obsoleteIds.has(ref.id)));
1820
+ const handoffs = claimedMessages.filter(message => !obsoleteIds.has(message.id));
1821
+ // A quiet/new Session is not evidence that the claimed old Turn stopped.
1822
+ // Keep the exact pending intent until the original fence is provable.
1823
+ if (handoffs.some((message) => interruptThenTerminalState(store, message) !== "ready"))
1824
+ return null;
1825
+ const handoffIds = new Set(handoffs.map((message) => message.id));
1826
+ if (availableRefs.length === 0 && obsoleteIds.size > 0) {
1827
+ store.saveWorkMailbox(consumePendingBatch(mailbox, pending));
1828
+ return null;
1829
+ }
1830
+ const selectedRefs = handoffs.length === 0 ? availableRefs
1831
+ : availableRefs.filter((ref) => ref.type === "message" && handoffIds.has(ref.id));
1832
+ const remainingRefs = handoffs.length === 0 ? []
1833
+ : availableRefs.filter((ref) => !(ref.type === "message" && handoffIds.has(ref.id)));
1465
1834
  const latest = latestTaskWake(store.listTaskWakes(taskId));
1466
1835
  const wakeId = store.nextTaskWakeId(taskId);
1467
1836
  const wake = createTaskWake({
1468
1837
  id: wakeId, taskId, reasons: mailbox.pending.reasons,
1469
- refs: mailbox.pending.refs,
1838
+ refs: selectedRefs,
1470
1839
  fromCursor: latest?.toCursor ?? task.createdAt,
1471
1840
  toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now
1472
1841
  });
@@ -1475,6 +1844,10 @@ export class FileSchedulerStoreAdapter {
1475
1844
  owner: `leader-notification:${wakeId}`, startedAt: now.toISOString() });
1476
1845
  store.saveTaskWake(taskId, wake);
1477
1846
  store.saveWorkMailbox(mailbox);
1847
+ // The mailbox is only a hint. Carry ordinary refs into its next pending
1848
+ // batch, rather than delivering them with the chosen immediate successor.
1849
+ if (remainingRefs.length > 0)
1850
+ enqueueWork(store, target, "user-message", now, remainingRefs, { source: "controller", dedupeKey: `then-remainder:${wakeId}` });
1478
1851
  return { wakeId, attemptId, disposition: "submit" };
1479
1852
  });
1480
1853
  }
@@ -1490,6 +1863,8 @@ export class FileSchedulerStoreAdapter {
1490
1863
  && event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
1491
1864
  store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "notification.delivery", { attemptId, outcome, ...(detail === undefined ? {} : { detail }) }, now));
1492
1865
  }
1866
+ if (store.getTask(taskId)?.status === "archived")
1867
+ return;
1493
1868
  if (outcome !== "accepted" && outcome !== "rejected")
1494
1869
  return;
1495
1870
  store.saveWorkMailbox(completeProcessing(mailbox, attemptId));
@@ -1612,7 +1987,32 @@ export class FileSchedulerStoreAdapter {
1612
1987
  nativeSessionId: input.nativeSessionId,
1613
1988
  effective: input.effective
1614
1989
  }, now);
1615
- if (input.owner.scope === "task") {
1990
+ if (input.owner.scope === "global") {
1991
+ let sessions = store.getGlobalRoleSessionSet(input.owner.roleName);
1992
+ let binding = sessions.providerBinding;
1993
+ if (binding == null) {
1994
+ sessions = bindGlobalRoleProviderRuntime(sessions, createProviderRuntimeBinding({
1995
+ providerNamespace: this.drivers.requireByAdapterId(input.adapterId).id,
1996
+ accountScope: input.agentId, conversationId: input.nativeSessionId,
1997
+ startedAt: now.toISOString()
1998
+ }), now);
1999
+ }
2000
+ else {
2001
+ if (currentProviderConversation(binding).conversationId !== input.nativeSessionId) {
2002
+ binding = supersedeProviderConversation(binding, {
2003
+ conversationId: input.nativeSessionId, switchedAt: now.toISOString(), basis: "terminal-session"
2004
+ });
2005
+ }
2006
+ if (binding.authority.owner === "none")
2007
+ binding = transferProviderAuthority(binding, {
2008
+ expectedEpoch: binding.authority.epoch, expectedOwner: "none",
2009
+ owner: "controller", holderId: "controller", changedAt: now.toISOString()
2010
+ });
2011
+ sessions = updateGlobalRoleProviderRuntime(sessions, binding, now);
2012
+ }
2013
+ store.saveGlobalRoleSessionSet(sessions);
2014
+ }
2015
+ else {
1616
2016
  let sessions = store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName);
1617
2017
  if (sessions !== null && sessions.providerBinding === null) {
1618
2018
  sessions = bindTaskRoleProviderRuntime(sessions, createProviderRuntimeBinding({
@@ -2261,6 +2661,14 @@ export class FileSchedulerStoreAdapter {
2261
2661
  effective
2262
2662
  }, now);
2263
2663
  current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.nativeTurnId, now);
2664
+ // decision-3 §9: when a controller-owned native Turn is actually bound for
2665
+ // this Global Role, settle it to the observed terminal so the scope-generic
2666
+ // resolver and the interrupt-then gate see the real stop on the live binding
2667
+ // — the same edge Task settles via settleStructuredProviderTurn. This is a
2668
+ // strict guarded no-op for an older unmanaged Session or a
2669
+ // binding holding a different/already-terminal Turn, and the
2670
+ // durable native terminal recorded just above remains the proof of record.
2671
+ current = settleGlobalRoleRuntimeTurn(current, input.nativeTurnId, input.providerStatus, now);
2264
2672
  store.saveGlobalRoleSessionSet(current);
2265
2673
  if (input.roleName === SYSTEM_OPERATOR_ROLE
2266
2674
  && input.adapterId === "codex"
@@ -2368,7 +2776,8 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
2368
2776
  roleName: input.fence.roleName,
2369
2777
  agentId: input.fence.agentId,
2370
2778
  ...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
2371
- ...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId })
2779
+ ...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId }),
2780
+ ...(reason === "task-archived" ? { originalEvent: input } : {})
2372
2781
  }, reason, now);
2373
2782
  }
2374
2783
  function recordObsoleteRuntimeEvent(store, input, reason, now) {
@@ -2383,7 +2792,8 @@ function recordObsoleteRuntimeEvent(store, input, reason, now) {
2383
2792
  agentId: input.agentId,
2384
2793
  ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
2385
2794
  ...(input.runId === undefined ? {} : { runId: input.runId }),
2386
- reason
2795
+ reason,
2796
+ ...(input.originalEvent === undefined ? {} : { originalEvent: JSON.stringify(input.originalEvent) })
2387
2797
  }, now));
2388
2798
  }
2389
2799
  function sessionPreview(value) {
@@ -3033,6 +3443,29 @@ function settleStructuredProviderTurn(sessions, runId, status, now, attemptId) {
3033
3443
  settledAt: now.toISOString()
3034
3444
  }), now);
3035
3445
  }
3446
+ /**
3447
+ * The Global twin of {@link settleStructuredProviderTurn}: settle a Global Role's
3448
+ * own live Provider Turn to the observed native terminal, keyed by the exact
3449
+ * native Turn id the real Provider Stop/StopFailure hook carries (decision-3 §9).
3450
+ * It is a strict guarded no-op unless a controller-owned Turn with that exact
3451
+ * native id is bound and still in flight (`accepted`) — an absent binding, a
3452
+ * different/only-submitting Turn, or an already-terminal one is left untouched, so
3453
+ * an older unmanaged Session without a controlled binding settles nothing
3454
+ * and the durable `recentCompletedTurnIds` terminal remains the proof of record.
3455
+ */
3456
+ function settleGlobalRoleRuntimeTurn(sessions, nativeTurnId, status, now) {
3457
+ const binding = sessions.providerBinding;
3458
+ if (binding === null || binding === undefined || binding.run === null
3459
+ || binding.run.nativeTurnId !== nativeTurnId
3460
+ || binding.run.status !== "accepted") {
3461
+ return sessions;
3462
+ }
3463
+ return updateGlobalRoleProviderRuntime(sessions, settleProviderTurn(binding, {
3464
+ nativeTurnId,
3465
+ status,
3466
+ settledAt: now.toISOString()
3467
+ }), now);
3468
+ }
3036
3469
  function usageSnapshotIsSuperseded(events, incoming) {
3037
3470
  if (incoming.kind !== "activity.observed" || incoming.payload.usage === undefined) {
3038
3471
  return false;