@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
@@ -0,0 +1,132 @@
1
+ import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
2
+ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
3
+ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
+ import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
5
+ import { providerRetryPending } from "../runtime/providerRetry.js";
6
+ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
+ /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
8
+ export async function deliverGlobalInputs(home, store, ensure, onError) {
9
+ for (const role of store.listGlobalRoles()) {
10
+ try {
11
+ let pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
12
+ && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
13
+ if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
14
+ continue;
15
+ let sessions = store.getGlobalRoleSessionSet(role.name);
16
+ if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
17
+ store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
18
+ pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
19
+ && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
20
+ if (pending.length === 0)
21
+ continue;
22
+ }
23
+ if (providerRetryPending(sessions?.providerBinding))
24
+ continue;
25
+ const current = sessions?.providerBinding?.run;
26
+ const obsolete = pending.filter(message => {
27
+ const claim = message.interruptThen;
28
+ return claim !== undefined && (sessions?.activeAgentId !== claim.targetAgentId
29
+ || sessions.sessions[claim.targetAgentId]?.nativeSessionId !== claim.targetNativeSessionId
30
+ || sessions.providerBinding?.authority.epoch !== claim.targetAuthorityEpoch
31
+ || sessions.providerBinding.authority.holderId !== claim.targetAuthorityHolderId);
32
+ });
33
+ for (const message of obsolete)
34
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-or-authority-changed", new Date()));
35
+ pending = pending.filter(message => !obsolete.includes(message));
36
+ if (pending.length === 0)
37
+ continue;
38
+ if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
39
+ continue;
40
+ const claims = pending.filter(message => message.interruptThen !== undefined);
41
+ let next;
42
+ if (claims.length > 0) {
43
+ next = claims.find(message => {
44
+ const claim = message.interruptThen;
45
+ const binding = sessions?.providerBinding;
46
+ return current?.attemptId === claim.targetAttemptId
47
+ && ["completed", "failed", "cancelled"].includes(current.status)
48
+ && sessions?.activeAgentId === claim.targetAgentId
49
+ && sessions.sessions[claim.targetAgentId]?.nativeSessionId === claim.targetNativeSessionId
50
+ && binding?.authority.owner === "controller"
51
+ && binding.authority.epoch === claim.targetAuthorityEpoch
52
+ && binding.authority.holderId === claim.targetAuthorityHolderId;
53
+ });
54
+ if (next === undefined)
55
+ continue;
56
+ }
57
+ else
58
+ next = pending[0];
59
+ const attemptId = `global-input:${role.name}/${next.id}`;
60
+ if (current?.attemptId === attemptId)
61
+ continue;
62
+ if (next.control !== undefined && next.control.outcome !== "rejected")
63
+ continue;
64
+ const matchesTarget = () => next.deliveryTarget === undefined
65
+ || sessions?.activeAgentId === next.deliveryTarget.agentId
66
+ && sessions.sessions[next.deliveryTarget.agentId]?.nativeSessionId === next.deliveryTarget.nativeSessionId;
67
+ if (!matchesTarget()) {
68
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
69
+ continue;
70
+ }
71
+ await ensure(role.name);
72
+ sessions = store.getGlobalRoleSessionSet(role.name);
73
+ const binding = sessions?.providerBinding;
74
+ if (binding == null || binding.authority.owner !== "controller")
75
+ continue;
76
+ const nativeSessionId = currentProviderConversation(binding).conversationId;
77
+ const persisted = store.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
78
+ if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
79
+ continue;
80
+ const claim = next.interruptThen;
81
+ if (!matchesTarget() || claim !== undefined
82
+ && (sessions?.activeAgentId !== claim.targetAgentId
83
+ || nativeSessionId !== claim.targetNativeSessionId
84
+ || binding.authority.epoch !== claim.targetAuthorityEpoch
85
+ || binding.authority.holderId !== claim.targetAuthorityHolderId
86
+ || binding.run?.attemptId !== claim.targetAttemptId
87
+ || !["completed", "failed", "cancelled"].includes(binding.run.status))) {
88
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
89
+ continue;
90
+ }
91
+ if (next.deliveryTarget === undefined) {
92
+ next = { ...next, deliveryTarget: { agentId: sessions.activeAgentId, nativeSessionId } };
93
+ store.updateGlobalRoleMessage(next);
94
+ }
95
+ const result = await sendAgentHostRunControl({
96
+ home, scope: "global", roleName: role.name,
97
+ control: {
98
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId,
99
+ authority: { epoch: binding.authority.epoch, owner: "controller",
100
+ holderId: binding.authority.holderId },
101
+ run: { attemptId, boundedText: [
102
+ `Yui Global Message: role=${role.name} message=${next.id}.`,
103
+ "Read your Session Context and the referenced Message in full, then act on its durable input."
104
+ ].join("\n") }
105
+ }
106
+ });
107
+ if (result.outcome === "rejected") {
108
+ store.transaction(tx => {
109
+ const latest = tx.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
110
+ if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
111
+ return;
112
+ const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
113
+ if (providerRetryPending(tx.getGlobalRoleSessionSet(role.name)?.providerBinding))
114
+ return;
115
+ // A later canonical acceptance wins over transport diagnostics.
116
+ if (turn?.attemptId === attemptId && ["accepted", "completed", "failed", "cancelled"].includes(turn.status))
117
+ return;
118
+ const unknown = result.failure?.inputDisposition === "unknown" || result.snapshot.state === "delivery-unknown";
119
+ const updated = { ...latest, control: {
120
+ requestId: latest.inputControl?.requestId ?? latest.interruptThen.requestId,
121
+ receiptId: attemptId, outcome: unknown ? "delivery-unknown" : "rejected",
122
+ observedAt: new Date().toISOString()
123
+ } };
124
+ tx.updateGlobalRoleMessage(unknown ? updated : markGlobalRoleMessageNotDelivered(updated, result.failure?.detail ?? result.snapshot.detail ?? "provider-input-rejected", new Date()));
125
+ });
126
+ }
127
+ }
128
+ catch (error) {
129
+ onError(error);
130
+ }
131
+ }
132
+ }
@@ -253,10 +253,14 @@ function validateJobTarget(store, params) {
253
253
  if (attempt === null) {
254
254
  throw jobDomainError(`Integration Attempt not found: ${params.taskId}/${params.owner.integrationAttemptId}.`);
255
255
  }
256
- if (attempt.status === "committed" || attempt.status === "failed") {
257
- throw jobDomainError(`DurableJob owner Integration Attempt is terminal: `
256
+ if (attempt.status !== "running") {
257
+ throw jobDomainError(`DurableJob owner Integration Attempt must be running: `
258
258
  + `${params.taskId}/${params.owner.integrationAttemptId} is ${attempt.status}.`);
259
259
  }
260
+ if (attempt.candidateCommit !== params.head || attempt.checkInputDigest === undefined
261
+ || attempt.checkInputDigest !== durableJobIdempotencyKey(params)) {
262
+ throw jobDomainError("Integration Job has no matching admitted candidate/check specification.");
263
+ }
260
264
  }
261
265
  const project = store.getProject(params.projectId);
262
266
  if (project === null) {
@@ -0,0 +1,100 @@
1
+ import { admitProviderRetry, cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
2
+ import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
3
+ import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ export function ownedProviderInput(input) {
6
+ if (input.runId !== undefined && input.taskId !== undefined) {
7
+ return { kind: "run", taskId: input.taskId, runId: input.runId };
8
+ }
9
+ const wake = /^notification:([^/]+)\/([^/]+)\//u.exec(input.attemptId);
10
+ if (wake !== null && wake[1] === input.taskId) {
11
+ return { kind: "wake", taskId: wake[1], wakeId: wake[2] };
12
+ }
13
+ const message = /^global-input:([^/]+)\/([^/]+)$/u.exec(input.attemptId);
14
+ if (message !== null && message[1] === input.roleName && input.taskId === undefined) {
15
+ return { kind: "message", roleName: input.roleName, messageId: message[2] };
16
+ }
17
+ return input.boundedText === undefined ? undefined : { kind: "text", text: input.boundedText };
18
+ }
19
+ export function retryIntentBlocker(store, input, retry) {
20
+ if (input.taskId === undefined) {
21
+ const role = store.getGlobalRole(input.roleName);
22
+ const sessions = store.getGlobalRoleSessionSet(input.roleName);
23
+ if (role === null || sessions?.activeAgentId !== role.activeAgentId)
24
+ return "role-changed";
25
+ if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
26
+ return "role-configuration-changed";
27
+ if (store.listGlobalRoleMessages(input.roleName).some(message => Date.parse(message.createdAt) > Date.parse(retry.intentAt)
28
+ && message.delivery === undefined && message.notDelivered === undefined))
29
+ return "new-user-input";
30
+ return undefined;
31
+ }
32
+ const task = store.getTask(input.taskId);
33
+ if (task === null || !["active", "draft"].includes(task.status)
34
+ || task.executionGate.state !== "enabled")
35
+ return "task-execution-stopped";
36
+ const role = store.getRole(input.taskId, input.roleName);
37
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
38
+ if (role === null || sessions?.activeAgentId !== role.activeAgentId)
39
+ return "role-changed";
40
+ if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
41
+ return "role-configuration-changed";
42
+ if (store.listMessages(input.taskId).some(message => (message.kind === "operator" || message.kind === "user")
43
+ && Date.parse(message.createdAt) > Date.parse(retry.intentAt)))
44
+ return "new-user-input";
45
+ if (blockingProviderContinuations(store.listEvents(input.taskId)).some(c => c.roleName === input.roleName && c.identity.conversationId === retry.nativeSessionId)) {
46
+ return "background-work-unsettled";
47
+ }
48
+ return undefined;
49
+ }
50
+ /** Shared by automatic recovery and every ordinary Host submit. The native
51
+ * writer CAS remains the only grant; a scheduled successor is not a grant. */
52
+ export function admitOwnedProviderInput(store, binding, input) {
53
+ const retry = binding.retry;
54
+ const sameReviewSuccessor = retry?.successorReviewRoundId !== undefined && input.taskId !== undefined && input.runId !== undefined
55
+ && store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId;
56
+ if (retry !== undefined && !providerRetryPending(binding)
57
+ && (input.attemptId.startsWith("provider-retry:") || input.runId !== undefined && input.runId === retry.successorRunId
58
+ || sameReviewSuccessor)) {
59
+ throw new Error(`Provider retry is ${retry.status}; this scheduled successor cannot regain admission.`);
60
+ }
61
+ if (!providerRetryPending(binding) || retry === undefined)
62
+ return binding;
63
+ const automatic = input.attemptId === providerRetryAttemptId(retry)
64
+ || input.runId !== undefined && input.runId === retry.successorRunId
65
+ || input.runId !== undefined && input.taskId !== undefined && retry.successorReviewRoundId !== undefined
66
+ && store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId
67
+ || retry.status === "in-flight" && input.attemptId === retry.currentAttemptId;
68
+ if (!automatic)
69
+ return cancelProviderRetry(binding, "superseded-by-explicit-input", input.now.getTime());
70
+ const blocker = retryIntentBlocker(store, input, retry);
71
+ if (blocker !== undefined)
72
+ throw new Error(`Provider retry admission stopped: ${blocker}.`);
73
+ if (input.retrySupported !== true)
74
+ throw new Error("This Host cannot prove safe same-Session recovery; use explicit recovery.");
75
+ if (binding.goal?.status === "active")
76
+ throw new Error("Provider still owns an autonomous Goal; inspect its exact continuation.");
77
+ if (input.taskId !== undefined && input.runId !== undefined) {
78
+ const run = store.getRun(input.taskId, input.runId);
79
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
80
+ if (run === null || run.status !== "active"
81
+ || !isDeepStrictEqual(run.effective, sessions?.sessions[input.agentId]?.effective)) {
82
+ throw new Error("Retry execution no longer matches its fixed native Session.");
83
+ }
84
+ if (run.workspace !== undefined && !isDeepStrictEqual(store.getManagedWorkspace(run.workspace.owner), run.workspace)) {
85
+ throw new Error("Retry workspace authority changed.");
86
+ }
87
+ if (run.workItemId !== undefined) {
88
+ const item = store.getWorkItem(input.taskId, run.workItemId);
89
+ if (item?.status !== "open" || run.purpose !== "review" && run.executionLaneId === undefined
90
+ && item.assignee !== run.roleName)
91
+ throw new Error("Retry WorkItem responsibility changed.");
92
+ }
93
+ if (validateExactRunReviewRound(store, run).disposition !== "applied") {
94
+ throw new Error("Retry Review no longer matches its exact Round and frozen candidate.");
95
+ }
96
+ }
97
+ const admitted = admitProviderRetry(binding, { attemptId: input.attemptId, at: input.now.getTime() });
98
+ return input.runId === undefined ? admitted
99
+ : { ...admitted, retry: { ...admitted.retry, successorRunId: input.runId } };
100
+ }
@@ -0,0 +1,218 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { runTaskCommand } from "../commands/taskCommands.js";
3
+ import { retryIntentBlocker } from "./providerRetryAdmission.js";
4
+ import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
5
+ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
6
+ import { runtimeLifecycleTarget, hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
7
+ import { sendAgentHostRunControl, inspectAgentHostSocket, agentHostControlSocketPath, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
8
+ import { redactAgentErrorText } from "../runtime/agentError.js";
9
+ import { createTaskEvent } from "../event/taskEvent.js";
10
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
11
+ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
12
+ /** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
13
+ * current Provider input is the only pending intent; nothing is queued here. */
14
+ export function createProviderRetryHooks(home, store, ports = {}) {
15
+ const anchors = new Map();
16
+ const inspectedUnknown = new Set();
17
+ const inspect = ports.inspect ?? ((target) => inspectAgentHostSocket(agentHostControlSocketPath(target), 250));
18
+ const now = (retry) => {
19
+ const wall = (ports.now ?? (() => new Date()))().getTime();
20
+ const monotonic = (ports.monotonicNow ?? (() => performance.now()))();
21
+ let anchor = anchors.get(retry.chainId);
22
+ if (anchor === undefined) {
23
+ anchor = { wall: Math.max(wall, Date.parse(retry.updatedAt)), monotonic };
24
+ anchors.set(retry.chainId, anchor);
25
+ }
26
+ return Math.max(wall, anchor.wall + Math.max(0, monotonic - anchor.monotonic));
27
+ };
28
+ const load = (owner) => owner.scope === "task"
29
+ ? store.getTaskRoleSessionSet(owner.taskId, owner.roleName) : store.getGlobalRoleSessionSet(owner.roleName);
30
+ const save = (set) => {
31
+ if (set.owner.scope === "task")
32
+ store.saveTaskRoleSessionSet(set);
33
+ else
34
+ store.saveGlobalRoleSessionSet(set);
35
+ };
36
+ const stop = (set, reason, at, exhausted = false) => {
37
+ store.transaction(() => {
38
+ const current = load(set.owner);
39
+ if (current === null || current.providerBinding?.retry?.chainId !== set.providerBinding?.retry?.chainId)
40
+ return;
41
+ const binding = current.providerBinding;
42
+ if (!providerRetryPending(binding))
43
+ return;
44
+ const cancelled = cancelProviderRetry(binding, reason, at);
45
+ save({ ...current, providerBinding: exhausted
46
+ ? { ...cancelled, retry: { ...cancelled.retry, status: "exhausted" } } : cancelled });
47
+ const owner = set.owner;
48
+ if (owner.scope === "global")
49
+ settleGlobalRetryInput(store, owner.roleName, load(owner)?.providerBinding, new Date(at));
50
+ if (owner.scope === "task") {
51
+ const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-stopped", {
52
+ roleName: owner.roleName, chainId: binding.retry.chainId, reason
53
+ }, new Date(at));
54
+ store.saveEvent(owner.taskId, event);
55
+ enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
56
+ : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
57
+ }
58
+ });
59
+ };
60
+ const key = (set) => set.owner.scope === "task"
61
+ ? `role:${encodeURIComponent(set.owner.taskId)}/${encodeURIComponent(set.owner.roleName)}`
62
+ : `global-role:${encodeURIComponent(set.owner.roleName)}`;
63
+ return {
64
+ deadlines() {
65
+ return store.listProviderRetrySessions().flatMap(set => {
66
+ const retry = set.providerBinding.retry;
67
+ if (retry.status !== "waiting")
68
+ return [];
69
+ // Busy or already-queued work awaits the next ordinary pass without a
70
+ // zero-delay timer loop. Waiting never consumes an execution attempt.
71
+ const wall = (ports.now ?? (() => new Date()))().getTime();
72
+ const effective = now(retry);
73
+ return [{ key: key(set), at: wall + Math.max(250, (retry.successorAutomatic === false ? Date.parse(retry.nextEligibleAt)
74
+ : Math.min(Date.parse(retry.nextEligibleAt), Date.parse(retry.deadline))) - effective) }];
75
+ });
76
+ },
77
+ async reconcile() {
78
+ const current = store.listProviderRetrySessions();
79
+ const activeChains = new Set(current.map(set => set.providerBinding.retry.chainId));
80
+ for (const id of anchors.keys())
81
+ if (!activeChains.has(id))
82
+ anchors.delete(id);
83
+ const pendingAttempts = new Set(current.map(set => set.providerBinding.retry.currentAttemptId));
84
+ for (const id of inspectedUnknown)
85
+ if (!pendingAttempts.has(id))
86
+ inspectedUnknown.delete(id);
87
+ for (const set of current) {
88
+ const owner = set.owner;
89
+ const binding = set.providerBinding;
90
+ const retry = binding.retry;
91
+ const at = now(retry);
92
+ const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
93
+ const session = set.sessions[set.activeAgentId];
94
+ const blocker = retryIntentBlocker(store, scoped, retry);
95
+ if (blocker !== undefined && blocker !== "background-work-unsettled") {
96
+ stop(set, blocker, at);
97
+ continue;
98
+ }
99
+ if (binding.retryDisabled && retry.successorAutomatic !== false || binding.authority.owner !== "controller"
100
+ || binding.authority.epoch !== retry.authorityEpoch
101
+ || currentProviderConversation(binding).conversationId !== retry.nativeSessionId
102
+ || session?.status !== "active" || session.nativeSessionId !== retry.nativeSessionId
103
+ || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
104
+ stop(set, "session-or-authority-changed", at);
105
+ continue;
106
+ }
107
+ // Never replay an admitted attempt. Existing exact runtime queries and
108
+ // terminal ingress resolve it, even across Controller restart.
109
+ if (retry.status === "in-flight") {
110
+ if (binding.run?.status === "delivery-unknown" && retry.currentAttemptId !== undefined
111
+ && !inspectedUnknown.has(retry.currentAttemptId)) {
112
+ inspectedUnknown.add(retry.currentAttemptId);
113
+ let detail;
114
+ try {
115
+ const host = await inspect({ home, ...scoped, scope: owner.scope });
116
+ detail = host.nativeSessionId === retry.nativeSessionId && host.attemptId === retry.currentAttemptId
117
+ ? `Exact Host query: ${host.state}; awaiting original canonical Provider evidence.`
118
+ : "Host query did not prove the original Session/input identity; no replay.";
119
+ }
120
+ catch (error) {
121
+ detail = `Original Host query unavailable; no replay: ${redactAgentErrorText(error instanceof Error ? error.message : String(error))}`;
122
+ }
123
+ store.transaction(() => {
124
+ const latest = load(owner);
125
+ if (latest === null || latest.providerBinding == null || latest.providerBinding.run === null
126
+ || latest.providerBinding.run.attemptId !== retry.currentAttemptId
127
+ || latest.providerBinding.run.status !== "delivery-unknown")
128
+ return;
129
+ save({ ...latest, providerBinding: { ...latest.providerBinding,
130
+ retry: { ...latest.providerBinding.retry, reason: detail } } });
131
+ if (owner.scope === "task") {
132
+ const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
133
+ roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
134
+ }, new Date(at));
135
+ store.saveEvent(owner.taskId, event);
136
+ enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
137
+ : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
138
+ }
139
+ });
140
+ }
141
+ continue;
142
+ }
143
+ if (retry.successorAutomatic !== false && at >= Date.parse(retry.deadline)) {
144
+ stop(set, "deadline-exhausted", at, true);
145
+ continue;
146
+ }
147
+ if (at < Date.parse(retry.nextEligibleAt) || blocker !== undefined || binding.goal?.status === "active")
148
+ continue;
149
+ if (binding.run?.attemptId !== retry.failedAttemptId) {
150
+ stop(set, "superseded-input", at);
151
+ continue;
152
+ }
153
+ try {
154
+ if (owner.scope === "task" && retry.previousRunId !== undefined) {
155
+ if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
156
+ continue;
157
+ const previous = store.getRun(owner.taskId, retry.previousRunId);
158
+ if (previous?.status === "active")
159
+ continue; // ordinary rejected-delivery terminal fold
160
+ if (previous?.status !== "failed") {
161
+ stop(set, "run-no-longer-failed", at);
162
+ continue;
163
+ }
164
+ const actualTaskReviewCandidate = await ports.snapshotTaskCandidate?.(previous);
165
+ store.transaction(() => {
166
+ const latest = load(owner)?.providerBinding?.retry;
167
+ if (latest?.chainId !== retry.chainId || latest.failedAttemptId !== retry.failedAttemptId
168
+ || latest.status !== "waiting")
169
+ return;
170
+ const reason = retryIntentBlocker(store, scoped, latest);
171
+ if (reason !== undefined)
172
+ throw new Error(reason);
173
+ runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
174
+ now: () => new Date(at), environment: {}, providerRetryChainId: retry.chainId,
175
+ ...(actualTaskReviewCandidate === undefined ? {} : { actualTaskReviewCandidate })
176
+ });
177
+ });
178
+ }
179
+ else {
180
+ const original = retry.input.kind === "text" ? retry.input.text
181
+ : retry.input.kind === "wake"
182
+ ? `Yui Task notification: task=${retry.input.taskId} wake=${retry.input.wakeId}.\nRead current Task context and that fixed wake window, including original Messages in full. This is not an execution assignment.`
183
+ : retry.input.kind === "message"
184
+ ? `Yui Global Message: role=${retry.input.roleName} message=${retry.input.messageId}.\nRead your Session Context and referenced original Message in full.`
185
+ : undefined;
186
+ if (original === undefined) {
187
+ stop(set, "original-input-unavailable", at);
188
+ continue;
189
+ }
190
+ const result = await (ports.submit ?? sendAgentHostRunControl)({
191
+ home, ...scoped, scope: owner.scope,
192
+ control: {
193
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId: retry.nativeSessionId,
194
+ authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId },
195
+ run: { attemptId: providerRetryAttemptId(retry), boundedText: original }
196
+ }
197
+ });
198
+ // Durable settlement belongs to Host/Controller ingress. In
199
+ // particular a lost response is never a reason to replay.
200
+ if (result.outcome === "rejected" && result.failure?.registrationDisposition === "not-committed"
201
+ && result.failure.errorName !== "ProviderTurnBusyError") {
202
+ stop(set, result.failure.detail, at);
203
+ }
204
+ }
205
+ }
206
+ catch (error) {
207
+ // A send failure may have applied. A durable submitting/unknown
208
+ // identity fences it; do not manufacture a successor to uncertainty.
209
+ const latest = load(owner);
210
+ if (latest?.providerBinding?.run?.attemptId === retry.failedAttemptId) {
211
+ stop(set, error instanceof Error ? error.message : String(error), at);
212
+ }
213
+ ports.onError?.(error);
214
+ }
215
+ }
216
+ }
217
+ };
218
+ }
@@ -1,4 +1,11 @@
1
1
  import { resolve } from "node:path";
2
+ /** Archiving may retain execution; a Task lifecycle label is not exit proof. */
3
+ export function runtimeRoleProtectsDomain(role) {
4
+ return (role.ownerKind === "task-role"
5
+ && (role.taskStatus === "active" || role.taskStatus === "archived"))
6
+ // Global Roles have no Task lifecycle; require a current native Session.
7
+ || (role.ownerKind === "global-role" && role.nativeSessionId !== undefined);
8
+ }
2
9
  export function createRuntimeResourceActivityTracker() {
3
10
  const previous = new Map();
4
11
  return (identity, resource) => {
@@ -112,8 +119,7 @@ export function buildControllerResourceInventory(facts) {
112
119
  for (const process of paneProcesses)
113
120
  claimed.add(process.pid);
114
121
  const role = findRole(homeFact.roles, pane);
115
- const terminalIsolation = (role?.taskStatus === "cancelled" && role.taskRetirementIsolated === true)
116
- || role?.taskStatus === "archived";
122
+ const terminalIsolation = role?.taskStatus === "cancelled" && role.taskRetirementIsolated === true;
117
123
  resources.push(processResource({
118
124
  kind: "agent-session",
119
125
  state: pane.dead ? "dead" : role === undefined ? "orphaned" : "running",
@@ -122,7 +128,8 @@ export function buildControllerResourceInventory(facts) {
122
128
  : terminalIsolation ? "safe" : "protected",
123
129
  reasonCode: role === undefined
124
130
  ? pane.dead ? "dead-orphan-pane" : "orphan-pane"
125
- : terminalIsolation ? (role.taskStatus === "cancelled" ? "retired-task-pane" : "archived-task-pane") : "owned-role-pane",
131
+ : terminalIsolation ? "retired-task-pane"
132
+ : role.taskStatus === "archived" ? "archived-task-pane" : "owned-role-pane",
126
133
  yuiHome,
127
134
  owner: role === undefined ? { kind: "none" } : roleOwner(role),
128
135
  processes: paneProcesses,
@@ -358,7 +365,10 @@ function artifactResource(artifact, yuiHome, domain, homeId) {
358
365
  };
359
366
  }
360
367
  function domainDisposition(base, reason, domain, target) {
361
- if (reason === "retired-task-pane" || reason === "archived-task-pane") {
368
+ // Even an expired test domain cannot authorize deleting retained execution.
369
+ if (reason === "archived-task-pane")
370
+ return "protected";
371
+ if (reason === "retired-task-pane") {
362
372
  return "safe";
363
373
  }
364
374
  if (domain === undefined)
@@ -11,7 +11,7 @@ import { readControllerDiscovery } from "../core/controllerClient.js";
11
11
  import { CONTROLLER_DISCOVERY_PATH } from "../core/protocol.js";
12
12
  import { controllerSocketPath } from "../core/controllerEndpoint.js";
13
13
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
14
- import { buildControllerResourceInventory } from "./resourceInventory.js";
14
+ import { buildControllerResourceInventory, runtimeRoleProtectsDomain } from "./resourceInventory.js";
15
15
  import { CONTROLLER_DOMAIN_PATH, EPHEMERAL_DOMAIN_GRACE_MS, ephemeralDomainFingerprint, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "./domainIdentity.js";
16
16
  const LINUX_PROCESS_SCAN_BATCH_SIZE = 64;
17
17
  export const INVENTORY_EVENT_LOOP_RUN_BUDGET_MS = 25;
@@ -518,11 +518,7 @@ function inspectRuntimeDomain(home, processes, roles, storageStatus, now) {
518
518
  graceMs: EPHEMERAL_DOMAIN_GRACE_MS
519
519
  };
520
520
  }
521
- const activeRole = roles.some((role) => ((role.ownerKind === "task-role" && role.taskStatus === "active")
522
- // Global Roles have no Task status. Only a live native Session is a
523
- // durable liveness fact for that scope; stopped/broken history must not
524
- // protect an expired disposable domain merely because it has an ID.
525
- || (role.ownerKind === "global-role" && role.nativeSessionId !== undefined)));
521
+ const activeRole = roles.some(runtimeRoleProtectsDomain);
526
522
  if (!storageSafe) {
527
523
  return {
528
524
  kind: "ephemeral-test",