@zq-silk/yui 2.21.0 → 2.23.0

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 (145) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +87 -27
  3. package/dist/cli/dynamicCompletion.js +4 -1
  4. package/dist/cli/helpRenderer.js +12 -1
  5. package/dist/cli/interactionPolicy.js +2 -0
  6. package/dist/cli/invocationRouter.js +5 -5
  7. package/dist/cli/roleWizard.js +5 -1
  8. package/dist/cli/updateCommand.js +20 -3
  9. package/dist/cli/updateOrchestrator.js +13 -0
  10. package/dist/cli/updatePorts.js +27 -0
  11. package/dist/commands/globalRoleCommands.js +33 -8
  12. package/dist/commands/inboxCommands.js +39 -12
  13. package/dist/commands/operatorCommands.js +2 -5
  14. package/dist/commands/releaseCommands.js +16 -2
  15. package/dist/commands/roleConfiguration.js +25 -2
  16. package/dist/commands/sessionCommands.js +5 -2
  17. package/dist/commands/taskCommands.js +217 -64
  18. package/dist/commands/taskContextCommand.js +26 -4
  19. package/dist/commands/taskFactCommands.js +15 -2
  20. package/dist/commands/taskIntegrationCommands.js +3 -3
  21. package/dist/commands/taskRoleRuntimeStatus.js +19 -5
  22. package/dist/commands/textInput.js +2 -2
  23. package/dist/context/contextFamilies.js +10 -0
  24. package/dist/context/currentSkills.js +37 -4
  25. package/dist/context/runContextPack.js +44 -5
  26. package/dist/context/runInputContract.js +1 -1
  27. package/dist/context/sessionBootstrapManifest.js +21 -6
  28. package/dist/context/sessionSkillNotice.js +74 -0
  29. package/dist/context/taskContext.js +100 -16
  30. package/dist/controlPlaneCli.js +17 -7
  31. package/dist/controller/agentRuntimeObserver.js +57 -12
  32. package/dist/controller/clientRuntime.js +66 -18
  33. package/dist/controller/controller.js +19 -2
  34. package/dist/controller/diagnosticJournal.js +10 -2
  35. package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
  36. package/dist/controller/hostLifecycle.js +153 -0
  37. package/dist/controller/hostLifecycleChecks.js +27 -0
  38. package/dist/controller/hostUpgrade.js +344 -0
  39. package/dist/controller/messageHealth.js +4 -3
  40. package/dist/controller/providerRetryAdmission.js +15 -1
  41. package/dist/controller/providerRetryDelivery.js +18 -2
  42. package/dist/controller/runtime.js +82 -21
  43. package/dist/controller/runtimeEventInbox.js +107 -13
  44. package/dist/controller/runtimeEventProcessor.js +3 -3
  45. package/dist/controller/sessionNotify.js +5 -46
  46. package/dist/controller/taskRunRecovery.js +153 -0
  47. package/dist/core/controllerClient.js +16 -2
  48. package/dist/core/controllerServer.js +14 -5
  49. package/dist/core/controllerTelemetry.js +51 -5
  50. package/dist/errors/cliFailure.js +14 -5
  51. package/dist/execution/codexThreadNaming.js +54 -43
  52. package/dist/executor/agentAdapter.js +32 -4
  53. package/dist/executor/agentConfigurationFields.js +2 -0
  54. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  55. package/dist/integration/gitIntegrationService.js +3 -2
  56. package/dist/integration/integrationSourceApplication.js +12 -1
  57. package/dist/interaction/notificationPresentation.js +11 -16
  58. package/dist/interaction/operatorPresentation.js +1 -1
  59. package/dist/message/messageContinuation.js +3 -1
  60. package/dist/output/boundedRead.js +36 -7
  61. package/dist/output/rolePresentation.js +3 -1
  62. package/dist/runtime/agentEndpoint.js +11 -1
  63. package/dist/runtime/agentEndpointIdentity.js +2 -0
  64. package/dist/runtime/agentHost.js +174 -40
  65. package/dist/runtime/agentHostImplementation.js +3 -1
  66. package/dist/runtime/codexAppServerRuntime.js +113 -24
  67. package/dist/runtime/codexInteractiveHost.js +102 -11
  68. package/dist/runtime/codexNativeAccess.js +1 -1
  69. package/dist/runtime/contextRecovery.js +15 -0
  70. package/dist/runtime/hostSessionLifecycle.js +83 -0
  71. package/dist/runtime/hostUpgradeGate.js +43 -0
  72. package/dist/runtime/lifecycleChecks.js +47 -0
  73. package/dist/runtime/lifecycleReservation.js +9 -1
  74. package/dist/runtime/nativeTitleReceipt.js +31 -0
  75. package/dist/runtime/nativeTitleSync.js +192 -0
  76. package/dist/runtime/providerRetry.js +9 -6
  77. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  78. package/dist/runtime/structuredProviderHost.js +65 -35
  79. package/dist/scheduler/wakeupQueue.js +10 -4
  80. package/dist/storage/sqliteSchema.js +29 -0
  81. package/dist/storage/storageVersions.js +1 -1
  82. package/dist/tmux/tmuxManager.js +4 -2
  83. package/dist/web/webSettings.js +2 -0
  84. package/docs/authorization.md +18 -3
  85. package/docs/cli-information-contract.md +228 -6
  86. package/docs/cli-information-contract.zh-CN.md +95 -5
  87. package/docs/managed-turn-and-session-runtime.md +177 -21
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
  89. package/docs/provider-retry.md +11 -0
  90. package/docs/release-workflow.md +65 -0
  91. package/docs/release-workflow.zh-CN.md +48 -0
  92. package/docs/roles-and-configuration.md +28 -0
  93. package/docs/roles-and-configuration.zh-CN.md +21 -0
  94. package/docs/storage-baseline.md +13 -1
  95. package/docs/storage-baseline.zh-CN.md +5 -1
  96. package/docs/task-delivery.md +7 -0
  97. package/docs/task-delivery.zh-CN.md +5 -0
  98. package/docs/testing/review-evaluation.md +7 -7
  99. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  100. package/docs/testing/verification-levels.md +45 -2
  101. package/docs/testing/verification-levels.zh-CN.md +21 -2
  102. package/i18n/README.zh-CN.md +6 -0
  103. package/package.json +1 -1
  104. package/skills/yui-leader/SKILL.md +98 -193
  105. package/skills/yui-leader/VERSION +1 -1
  106. package/skills/yui-leader/references/artifacts.md +41 -0
  107. package/skills/yui-leader/references/assignment-execution.md +140 -0
  108. package/skills/yui-leader/references/completion.md +79 -0
  109. package/skills/yui-leader/references/delivery-scope.md +26 -0
  110. package/skills/yui-leader/references/execution.md +16 -487
  111. package/skills/yui-leader/references/implementation.md +81 -0
  112. package/skills/yui-leader/references/planning.md +7 -45
  113. package/skills/yui-leader/references/review.md +90 -0
  114. package/skills/yui-leader/references/task-progress.md +23 -0
  115. package/skills/yui-leader/references/work-allocation.md +118 -0
  116. package/skills/yui-operator/SKILL.md +77 -333
  117. package/skills/yui-operator/VERSION +1 -1
  118. package/skills/yui-operator/references/body-transport.md +62 -0
  119. package/skills/yui-operator/references/configuration.md +47 -0
  120. package/skills/yui-operator/references/lifecycle.md +41 -0
  121. package/skills/yui-operator/references/progress.md +39 -0
  122. package/skills/yui-operator/references/routing.md +57 -0
  123. package/skills/yui-operator/references/submission.md +56 -0
  124. package/skills/yui-operator/references/task-delivery.md +1 -1
  125. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  126. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  127. package/skills/yui-runtime/SKILL.md +182 -531
  128. package/skills/yui-runtime/VERSION +1 -1
  129. package/skills/yui-runtime/references/archive.md +51 -0
  130. package/skills/yui-runtime/references/command-discovery.md +41 -0
  131. package/skills/yui-runtime/references/completed-followup.md +65 -0
  132. package/skills/yui-runtime/references/context-ordering.md +3 -34
  133. package/skills/yui-runtime/references/context-reads.md +61 -0
  134. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  135. package/skills/yui-runtime/references/execution-failure.md +105 -0
  136. package/skills/yui-runtime/references/global-context.md +18 -0
  137. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  138. package/skills/yui-runtime/references/host-custody.md +30 -0
  139. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  140. package/skills/yui-runtime/references/incremental-context.md +40 -0
  141. package/skills/yui-runtime/references/publication.md +4 -111
  142. package/skills/yui-runtime/references/recovery.md +32 -192
  143. package/skills/yui-runtime/references/session-identity.md +18 -0
  144. package/skills/yui-runtime/references/session-replacement.md +45 -0
  145. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
@@ -0,0 +1,344 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { isEnabled } from "../role/availability.js";
4
+ import { createWorkMailbox, enqueueSignal, claimPending, completeProcessing, mailboxHasWork } from "../coordination/workMailbox.js";
5
+ import { RUNTIME_HOST_UPGRADE_REASON, RUNTIME_HOST_UPGRADE_DRAINED as DRAINED, RUNTIME_HOST_UPGRADE_FAILED as FAILED, runtimeLifecycleTarget, runtimeCleanupDisposition } from "../runtime/lifecycleReservation.js";
6
+ const SOURCE = "host-upgrade/v1:";
7
+ /** Reuse the durable role-runtime lane and its fixed claim/settlement contract.
8
+ * The versioned signal source is a bounded exact request, not a second queue
9
+ * or writable Session state. No environment, credentials or inputs are copied.
10
+ * The drain signal is persisted BEFORE asking the old process to exit. */
11
+ export class HostUpgradeCoordinator {
12
+ store;
13
+ target;
14
+ ports;
15
+ #tail = Promise.resolve();
16
+ #closed = false;
17
+ constructor(store, target, ports) {
18
+ this.store = store;
19
+ this.target = target;
20
+ this.ports = ports;
21
+ }
22
+ hasPending() {
23
+ return !this.#closed && this.store.listWorkMailboxes().some(mailbox => reasons(mailbox).includes(RUNTIME_HOST_UPGRADE_REASON));
24
+ }
25
+ stop() { this.#closed = true; }
26
+ async close() {
27
+ this.stop();
28
+ await this.#tail;
29
+ }
30
+ /** Read-only follow-up: never admits, drains, restores, or settles a request. */
31
+ async status() {
32
+ const results = [];
33
+ for (const { owner, nativeSessionId, adapterId } of this.store.listRuntimeSessionCandidates()) {
34
+ if (this.#closed)
35
+ break;
36
+ const result = (status, detail, snapshot) => ({
37
+ owner, nativeSessionId, status, detail, hostImplementation: snapshot?.hostImplementation,
38
+ endpointImplementation: snapshot?.endpointImplementation
39
+ });
40
+ try {
41
+ const mailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
42
+ const request = readRequest(mailbox);
43
+ if (!this.enabled(owner)) {
44
+ results.push(result("paused", "Role/Task remains paused."));
45
+ continue;
46
+ }
47
+ const state = await this.ports.hostState(owner);
48
+ const live = state === "running" ? await this.ports.inspect(owner) : undefined;
49
+ if (live && this.verified(live, nativeSessionId, adapterId)) {
50
+ results.push(result("current", "Loaded Host and Endpoint match this Controller.", live));
51
+ }
52
+ else if (request && (request.nativeSessionId !== nativeSessionId || live
53
+ && (live.nativeSessionId !== request.nativeSessionId || live.processInstanceId !== request.processInstanceId))) {
54
+ results.push(result("unknown", `Request ${request.requestId} does not match the observed Host/Session.`, live));
55
+ }
56
+ else if (request) {
57
+ results.push(result(reasons(mailbox).includes(FAILED) || live?.hostUpgrade?.state === "failed" ? "failed"
58
+ : state === "unavailable" ? "unknown" : "waiting", `Request ${request.requestId}; target=${request.targetGeneration}; Host=${state}; drain=${live?.hostUpgrade?.state
59
+ ?? (reasons(mailbox).includes(DRAINED) ? "persisted" : "unconfirmed")}. ${live?.hostUpgrade?.detail ?? ""}`, live));
60
+ }
61
+ else {
62
+ results.push(result(live && (!live.controlCapabilities?.includes("prepare-upgrade")
63
+ || !live.controlCapabilities.includes("finish-upgrade")) ? "unsupported" : "unknown", "No confirmed switch to this Controller's generation.", live));
64
+ }
65
+ }
66
+ catch (error) {
67
+ results.push(result("unknown", message(error)));
68
+ }
69
+ }
70
+ return results;
71
+ }
72
+ request() {
73
+ return this.serial(async () => {
74
+ const results = [];
75
+ for (const candidate of this.store.listRuntimeSessionCandidates()) {
76
+ if (this.#closed)
77
+ break;
78
+ const { owner, nativeSessionId } = candidate;
79
+ const result = (status, detail, snapshot) => ({
80
+ owner, nativeSessionId, status, detail, hostImplementation: snapshot?.hostImplementation,
81
+ endpointImplementation: snapshot?.endpointImplementation
82
+ });
83
+ try {
84
+ const existing = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
85
+ if (readRequest(existing)) {
86
+ results.push(await this.step(owner));
87
+ continue;
88
+ }
89
+ if (!this.enabled(owner)) {
90
+ results.push(result("paused", "Role/Task remains paused; no Host is started."));
91
+ continue;
92
+ }
93
+ const snapshot = await this.ports.inspect(owner);
94
+ if (this.#closed)
95
+ break;
96
+ if (snapshot.nativeSessionId !== nativeSessionId) {
97
+ results.push(result("unknown", "Live Host does not match the recorded native Session.", snapshot));
98
+ continue;
99
+ }
100
+ if (this.verified(snapshot, nativeSessionId, candidate.adapterId)) {
101
+ results.push(result("current", undefined, snapshot));
102
+ continue;
103
+ }
104
+ if (this.sameImplementations(snapshot, candidate.adapterId)) {
105
+ results.push(result("waiting", "Loaded implementations match; lifecycle/event readiness is not a version mismatch.", snapshot));
106
+ continue;
107
+ }
108
+ if (!snapshot.controlCapabilities?.includes("prepare-upgrade")
109
+ || !snapshot.controlCapabilities.includes("finish-upgrade")
110
+ || !snapshot.hostImplementation?.generation || !snapshot.endpointImplementation?.generation
111
+ || !snapshot.processInstanceId) {
112
+ results.push(result("unsupported", "Old Host has no graceful-upgrade capability; retained without sending unsupported commands.", snapshot));
113
+ continue;
114
+ }
115
+ if (existing && mailboxHasWork(existing)) {
116
+ results.push(result("waiting", "Another exact lifecycle operation owns this Role.", snapshot));
117
+ continue;
118
+ }
119
+ const session = this.session(owner, candidate.agentId);
120
+ if (!session || session.nativeSessionId !== nativeSessionId)
121
+ throw new Error("Session changed before upgrade admission.");
122
+ const body = {
123
+ nativeSessionId, processInstanceId: snapshot.processInstanceId,
124
+ hostGeneration: snapshot.hostImplementation.generation, targetGeneration: this.target.generation,
125
+ agentId: candidate.agentId, adapterId: candidate.adapterId, effectiveDigest: digest(session.effective)
126
+ };
127
+ const request = { requestId: `host-upgrade-${digest([owner, body])}`, ...body };
128
+ this.store.transaction(tx => {
129
+ const latest = tx.getWorkMailbox(runtimeLifecycleTarget(owner));
130
+ if (latest && mailboxHasWork(latest))
131
+ throw new Error("Runtime lane changed before upgrade admission.");
132
+ const current = this.session(owner, candidate.agentId);
133
+ if (!current || current.nativeSessionId !== nativeSessionId || digest(current.effective) !== request.effectiveDigest) {
134
+ throw new Error("Session changed before upgrade admission.");
135
+ }
136
+ tx.saveWorkMailbox(enqueueSignal(latest ?? createWorkMailbox(runtimeLifecycleTarget(owner)), {
137
+ reason: RUNTIME_HOST_UPGRADE_REASON, refs: [{ type: "session", id: nativeSessionId }],
138
+ source: SOURCE + JSON.stringify(request), dedupeKey: request.requestId, occurredAt: new Date().toISOString()
139
+ }));
140
+ });
141
+ results.push(await this.step(owner));
142
+ }
143
+ catch (error) {
144
+ results.push(result("unknown", message(error)));
145
+ }
146
+ }
147
+ return results;
148
+ });
149
+ }
150
+ reconcile() {
151
+ return this.serial(async () => {
152
+ const results = [];
153
+ for (const mailbox of this.store.listWorkMailboxes()) {
154
+ if (this.#closed)
155
+ break;
156
+ const target = mailbox.target;
157
+ if (target.kind !== "role-runtime" && target.kind !== "global-role-runtime")
158
+ continue;
159
+ const owner = target.kind === "role-runtime"
160
+ ? { scope: "task", taskId: target.taskId, roleName: target.roleName }
161
+ : { scope: "global", roleName: target.roleName };
162
+ try {
163
+ if (!readRequest(mailbox))
164
+ continue;
165
+ results.push(await this.step(owner));
166
+ }
167
+ catch (error) {
168
+ results.push({ owner, nativeSessionId: "", status: "unknown", detail: message(error) });
169
+ }
170
+ }
171
+ return results;
172
+ });
173
+ }
174
+ async step(owner) {
175
+ let mailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
176
+ const request = readRequest(mailbox);
177
+ const result = (status, detail, snapshot) => ({
178
+ owner, nativeSessionId: request.nativeSessionId, status, detail,
179
+ hostImplementation: snapshot?.hostImplementation, endpointImplementation: snapshot?.endpointImplementation
180
+ });
181
+ try {
182
+ if (runtimeCleanupDisposition(mailbox) !== "upgrade-host")
183
+ return result("waiting", "Explicit lifecycle cleanup supersedes upgrade.");
184
+ if (request.targetGeneration !== this.target.generation)
185
+ return result("waiting", "Controller generation differs from the pinned update target.");
186
+ if (!this.enabled(owner))
187
+ return result("paused", "Role/Task remains paused; original upgrade intent retained.");
188
+ const assertCurrent = () => {
189
+ const session = this.session(owner, request.agentId);
190
+ const currentMailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
191
+ if (this.#closed || !session || session.status !== "active" || session.nativeSessionId !== request.nativeSessionId
192
+ || digest(session.effective) !== request.effectiveDigest || !this.enabled(owner)
193
+ || runtimeCleanupDisposition(currentMailbox) !== "upgrade-host"
194
+ || !isDeepStrictEqual(readRequest(currentMailbox), request)) {
195
+ throw new Error("Upgrade no longer owns the original Session/configuration/lifecycle lane.");
196
+ }
197
+ return session;
198
+ };
199
+ const session = assertCurrent();
200
+ let live;
201
+ const hostState = await this.ports.hostState(owner);
202
+ if (hostState === "running") {
203
+ live = await this.ports.inspect(owner);
204
+ if (this.verified(live, request.nativeSessionId, request.adapterId)) {
205
+ assertCurrent();
206
+ this.complete(owner, request);
207
+ return result("upgraded", undefined, live);
208
+ }
209
+ if (reasons(mailbox).includes(FAILED))
210
+ return result("failed", "Prior upgrade failed; exact request and drain facts retained. Inspect before retrying.", live);
211
+ if (live.nativeSessionId !== request.nativeSessionId || live.processInstanceId !== request.processInstanceId
212
+ || live.hostImplementation?.generation !== request.hostGeneration) {
213
+ return result("unknown", "Host changed without the target handshake; no process was stopped.", live);
214
+ }
215
+ if (!reasons(mailbox).includes(DRAINED)) {
216
+ assertCurrent();
217
+ live = await this.ports.control(owner, "prepare-upgrade", request);
218
+ const drain = live.hostUpgrade;
219
+ if (drain?.requestId !== request.requestId)
220
+ return result("unknown", "Drain acknowledgement is missing or mismatched.", live);
221
+ if (drain.state === "failed") {
222
+ this.signal(owner, request, FAILED);
223
+ return result("failed", drain.detail, live);
224
+ }
225
+ if (drain.state !== "drained")
226
+ return result("waiting", "Admission fenced; waiting for native terminal, event persistence and owned-resource drain.", live);
227
+ assertCurrent();
228
+ this.signal(owner, request, DRAINED);
229
+ mailbox = this.store.getWorkMailbox(runtimeLifecycleTarget(owner));
230
+ }
231
+ if (live.hostUpgrade?.requestId !== request.requestId || live.hostUpgrade.state !== "drained") {
232
+ return result("unknown", "Persisted drain receipt does not match this live Host.", live);
233
+ }
234
+ assertCurrent();
235
+ await this.ports.control(owner, "finish-upgrade", request);
236
+ return result("waiting", "Old Host drained; awaiting its graceful process exit.", live);
237
+ }
238
+ if (reasons(mailbox).includes(FAILED))
239
+ return result("failed", "Prior launch is unconfirmed; retained request is not automatically replayed.");
240
+ if (hostState !== "stopped" || !reasons(mailbox).includes(DRAINED)) {
241
+ return result("unknown", "No proven old Host drain/exit; original inputs and Session retained.");
242
+ }
243
+ assertCurrent();
244
+ // A failed or unknown launch is NOT automatically replayed. The existing
245
+ // Host/pane and the exact mailbox request remain available for inspection.
246
+ try {
247
+ await this.ports.restore(owner, request, session.effective, assertCurrent);
248
+ }
249
+ catch (error) {
250
+ this.signal(owner, request, FAILED);
251
+ return result("failed", `Target Host launch/handshake failed: ${message(error)}`);
252
+ }
253
+ live = await this.ports.inspect(owner);
254
+ if (!this.verified(live, request.nativeSessionId, request.adapterId)) {
255
+ this.signal(owner, request, FAILED);
256
+ return result("failed", "Target Host did not prove native identity, implementations and control/event capabilities.", live);
257
+ }
258
+ assertCurrent();
259
+ this.complete(owner, request);
260
+ return result("upgraded", undefined, live);
261
+ }
262
+ catch (error) {
263
+ return result("unknown", message(error));
264
+ }
265
+ }
266
+ verified(snapshot, nativeSessionId, adapterId) {
267
+ return snapshot.nativeSessionId === nativeSessionId && snapshot.adapterId === adapterId
268
+ && this.sameImplementations(snapshot, adapterId)
269
+ && snapshot.controlCapabilities?.includes("reconcile-turn")
270
+ && snapshot.eventDelivery !== undefined && snapshot.eventDelivery.unpersisted === 0
271
+ && snapshot.eventDelivery.failure === undefined && ["idle", "ready", "busy"].includes(snapshot.state);
272
+ }
273
+ sameImplementations(snapshot, adapterId) {
274
+ return isDeepStrictEqual(snapshot.hostImplementation, this.target)
275
+ && isDeepStrictEqual(snapshot.endpointImplementation, this.ports.endpoint(adapterId));
276
+ }
277
+ session(owner, agentId) {
278
+ const set = owner.scope === "task" ? this.store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
279
+ : this.store.getGlobalRoleSessionSet(owner.roleName);
280
+ return set?.activeAgentId === agentId ? set.sessions[agentId] : undefined;
281
+ }
282
+ enabled(owner) {
283
+ const role = owner.scope === "task" ? this.store.getRole(owner.taskId, owner.roleName) : this.store.getGlobalRole(owner.roleName);
284
+ if (!role || !isEnabled(role))
285
+ return false;
286
+ if (owner.scope === "global")
287
+ return true;
288
+ const task = this.store.getTask(owner.taskId);
289
+ return !!task && ["active", "draft"].includes(task.status) && task.executionGate.state === "enabled";
290
+ }
291
+ signal(owner, request, reason) {
292
+ this.store.transaction(tx => {
293
+ const mailbox = tx.getWorkMailbox(runtimeLifecycleTarget(owner));
294
+ if (!mailbox || !isDeepStrictEqual(readRequest(mailbox), request))
295
+ throw new Error("Upgrade mailbox changed.");
296
+ tx.saveWorkMailbox(enqueueSignal(mailbox, { reason, refs: [], source: "host-upgrade",
297
+ dedupeKey: `${request.requestId}:${reason}`, occurredAt: new Date().toISOString() }));
298
+ });
299
+ }
300
+ complete(owner, request) {
301
+ this.store.transaction(tx => {
302
+ let mailbox = tx.getWorkMailbox(runtimeLifecycleTarget(owner));
303
+ if (!mailbox || !isDeepStrictEqual(readRequest(mailbox), request)
304
+ || runtimeCleanupDisposition(mailbox) !== "upgrade-host")
305
+ throw new Error("Upgrade mailbox changed.");
306
+ if (mailbox.processing)
307
+ mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
308
+ if (mailbox.pending) {
309
+ const batchId = request.requestId;
310
+ mailbox = completeProcessing(claimPending(mailbox, {
311
+ batchId, owner: "runtime-lifecycle", startedAt: new Date().toISOString()
312
+ }), batchId);
313
+ }
314
+ tx.saveWorkMailbox(mailbox); // retain bounded dedupe receipts for repeated update
315
+ });
316
+ }
317
+ serial(run) {
318
+ const next = this.#tail.then(run);
319
+ this.#tail = next.catch(() => undefined);
320
+ return next;
321
+ }
322
+ }
323
+ function reasons(mailbox) {
324
+ return [...mailbox?.pending?.reasons ?? [], ...mailbox?.processing?.batch.reasons ?? []];
325
+ }
326
+ function readRequest(mailbox) {
327
+ if (!reasons(mailbox).includes(RUNTIME_HOST_UPGRADE_REASON))
328
+ return undefined;
329
+ const sources = [...mailbox?.pending?.sources ?? [], ...mailbox?.processing?.batch.sources ?? []]
330
+ .filter(source => source.startsWith(SOURCE));
331
+ const unique = [...new Set(sources)];
332
+ if (unique.length !== 1)
333
+ throw new Error("Host upgrade request is missing or ambiguous.");
334
+ const request = JSON.parse(unique[0].slice(SOURCE.length));
335
+ for (const key of ["requestId", "nativeSessionId", "processInstanceId", "hostGeneration",
336
+ "targetGeneration", "agentId", "adapterId", "effectiveDigest"]) {
337
+ if (typeof request[key] !== "string" || !request[key] || request[key].includes("\0")) {
338
+ throw new Error("Host upgrade request is invalid.");
339
+ }
340
+ }
341
+ return request;
342
+ }
343
+ function digest(value) { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
344
+ function message(error) { return error instanceof Error ? error.message : String(error); }
@@ -91,13 +91,14 @@ function recoveryGuidance(o) {
91
91
  if (o.wakeId !== null && roleName === "leader") {
92
92
  const wakeTarget = `${arg(taskId)} ${arg(o.wakeId)}`;
93
93
  if (o.deliveryEvidence?.outcome === "rejected") {
94
- lines.push(`After correcting the rejection cause, and only if this exact wake is still unconsumed, its original claim matches and native acceptance/activity are proven absent: yui task wake retry ${wakeTarget} --reason '<verified correction and quiescence>'. Queued is not accepted. Do not retry accepted/unknown input. This candidate does not establish resolve support for rejected wakes; do not assume another recovery package is installed.`);
94
+ lines.push(`Choose alternative actions, not a sequence. After correcting the rejection cause, and only if this exact wake is still unconsumed, its original claim matches and native acceptance/activity are proven absent: yui task wake retry ${wakeTarget} --reason '<verified correction and quiescence>'. Queued is not accepted. Do not retry accepted/unknown input.`);
95
+ lines.push(`Alternatively, after exact shared-native/background/writer quiescence and persisted notification-attempt evidence: yui task wake resolve ${wakeTarget} --reason '<exact quiescence evidence>'. A rejection is not quiescence, including rejection after an unknown outcome. Resolve releases only scheduling without replay, ACK or acceptance; it preserves original claims and delivery evidence. If the operational mailbox is absent, the exact durable wake/attempt must still be proven; another current batch is never released. The rightful handler must inspect original effects and settle still-valid obligations separately.`);
95
96
  }
96
97
  else if (o.deliveryEvidence?.outcome === "unknown") {
97
- lines.push(`Do not replay unknown input. After exact shared-native/background/writer quiescence and persisted evidence, with the original unknown claim still current: yui task wake resolve ${wakeTarget} --reason '<exact quiescence evidence>'. This releases only scheduling without replay, ACK or acceptance. The rightful handler must inspect original effects and settle still-valid obligations separately.`);
98
+ lines.push(`Do not replay unknown input. After exact shared-native/background/writer quiescence and persisted evidence identifying the original wake/notification attempt: yui task wake resolve ${wakeTarget} --reason '<exact quiescence evidence>'. This releases only scheduling without replay, ACK or acceptance; an absent operational mailbox requires exact durable evidence, never release of another current batch. The rightful handler must inspect original effects and settle still-valid obligations separately.`);
98
99
  }
99
100
  else {
100
- lines.push("Deferred, accepted or unreported delivery does not meet this candidate's rejected-retry/unknown-resolve preconditions. Keep the original wake and inspect the runtime barrier instead of mechanically invoking either action.");
101
+ lines.push("Deferred, accepted or unreported delivery does not meet this candidate's rejected-retry/unknown-or-rejected-resolve preconditions. Keep the original wake and inspect the runtime barrier instead of mechanically invoking either action.");
101
102
  }
102
103
  }
103
104
  }
@@ -4,6 +4,7 @@ import { blockingProviderContinuations } from "../runtime/runtimeContinuationPro
4
4
  import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  import { isHandlingReminder } from "../message/inboxTurn.js";
7
+ import { roleSessionMayContinue } from "../executor/effectiveLaunch.js";
7
8
  export function ownedProviderInput(input) {
8
9
  if (input.runId !== undefined && input.taskId !== undefined) {
9
10
  return { kind: "run", taskId: input.taskId, runId: input.runId };
@@ -90,8 +91,21 @@ export function admitOwnedProviderInput(store, binding, input) {
90
91
  if (input.taskId !== undefined && input.runId !== undefined) {
91
92
  const run = store.getRun(input.taskId, input.runId);
92
93
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
94
+ const session = sessions?.sessions[input.agentId];
95
+ // Session provenance can describe an older ReviewRound. The exact failed
96
+ // Run, not that first launch, owns this retry's frozen launch configuration.
97
+ // Continuity still fences the native Session's physical workspace/authority;
98
+ // exact equality with the failed Run retains model, permission and candidate.
99
+ const previous = run?.purpose === "review" && retry.previousRunId !== undefined
100
+ ? store.getRun(input.taskId, retry.previousRunId) : null;
101
+ const matchesLaunch = run !== null && session !== undefined && (run.purpose === "review"
102
+ ? previous?.status === "failed" && previous.purpose === "review"
103
+ && previous.roleName === run.roleName && previous.reviewRoundId === run.reviewRoundId
104
+ && isDeepStrictEqual(run.effective, previous.effective)
105
+ && roleSessionMayContinue(session.effective, run.effective)
106
+ : isDeepStrictEqual(run.effective, session.effective));
93
107
  if (run === null || run.status !== "active"
94
- || !isDeepStrictEqual(run.effective, sessions?.sessions[input.agentId]?.effective)) {
108
+ || !matchesLaunch) {
95
109
  throw new Error("Retry execution no longer matches its fixed native Session.");
96
110
  }
97
111
  if (run.workspace !== undefined && !isDeepStrictEqual(store.getManagedWorkspace(run.workspace.owner), run.workspace)) {
@@ -171,9 +171,25 @@ export function createProviderRetryHooks(home, store, ports = {}) {
171
171
  }
172
172
  try {
173
173
  if (owner.scope === "task" && retry.previousRunId !== undefined) {
174
- if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
175
- continue;
176
174
  const previous = store.getRun(owner.taskId, retry.previousRunId);
175
+ if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined) {
176
+ const round = retry.successorReviewRoundId === undefined
177
+ ? null : store.getReviewRound(owner.taskId, retry.successorReviewRoundId);
178
+ const successorId = retry.successorRunId ?? (previous?.executionLaneId === undefined
179
+ ? round?.reviewerRunId
180
+ : round?.executionGroup?.lanes.find(lane => lane.id === previous.executionLaneId)?.currentRunId);
181
+ const successor = successorId === undefined ? null : store.getRun(owner.taskId, successorId);
182
+ // A scheduled Run can fail before Host admission, leaving the
183
+ // Provider binding on the original failed Turn. Stop waiting;
184
+ // this is not proof of native quiescence or permission to replay.
185
+ if (successor?.status === "failed" && successor.id !== previous?.id
186
+ && successor.roleName === owner.roleName
187
+ && (retry.successorReviewRoundId === undefined
188
+ || successor.reviewRoundId === retry.successorReviewRoundId)) {
189
+ stop(set, "successor-failed-before-admission", at);
190
+ }
191
+ continue;
192
+ }
177
193
  if (previous?.status === "active")
178
194
  continue; // ordinary rejected-delivery terminal fold
179
195
  if (previous?.status !== "failed") {
@@ -27,7 +27,7 @@ import { launchBrokerForHome } from "../runtime/launchBroker.js";
27
27
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
28
28
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
29
29
  import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
30
- import { providerRetryPrompt } from "../runtime/providerRetry.js";
30
+ import { providerRetryContext } from "../runtime/providerRetry.js";
31
31
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
32
32
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
33
33
  import { managedRuntimeRoot } from "../storage/homeLayout.js";
@@ -60,6 +60,13 @@ import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./dom
60
60
  import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
61
61
  import { AgentHostProviderSessionBusyError, AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
62
62
  import { deliverGlobalInputs } from "./globalInputDelivery.js";
63
+ import { HostUpgradeCoordinator } from "./hostUpgrade.js";
64
+ import { createHostLifecycleChecks } from "./hostLifecycleChecks.js";
65
+ import { createControllerHostLifecycle } from "./hostLifecycle.js";
66
+ import { createNonBlockingChecks } from "../runtime/lifecycleChecks.js";
67
+ import { AGENT_HOST_IMPLEMENTATION } from "../runtime/agentHostImplementation.js";
68
+ import { builtinAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
69
+ import { sendAgentHostUpgradeControl } from "../runtime/agentHost.js";
63
70
  import { authorizeJobStart } from "./jobControl.js";
64
71
  import { authorizeGlobalJobStart, createJobDefinitionControl } from "./jobDefinitionControl.js";
65
72
  import { reconcileTaskDelegation } from "./jobTaskDelegationControl.js";
@@ -89,6 +96,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
89
96
  const running = await createFileTaskControllerRuntime(home, {
90
97
  ...options,
91
98
  diagnosticMetrics: () => diagnostics.metrics(),
99
+ onRequestDiagnostic: record => diagnostics.request(record),
92
100
  onError: error => { diagnostics.error(error); options.onError?.(error); }
93
101
  });
94
102
  let expectedAt = monotonicMilliseconds() + 250;
@@ -197,7 +205,7 @@ async function createFileTaskControllerRuntime(home, options) {
197
205
  ?? new AgentConfigurationCatalogService(home, {
198
206
  environment: options.environment ?? process.env
199
207
  });
200
- const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
208
+ const rawSessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
201
209
  validateLaunch: async (request) => {
202
210
  const agent = store.getConfiguredAgent(request.agentId);
203
211
  if (agent === null)
@@ -221,7 +229,7 @@ async function createFileTaskControllerRuntime(home, options) {
221
229
  && typeof session.nativeSessionId === "string" && session.nativeSessionId.trim().length > 0) {
222
230
  return session.nativeSessionId;
223
231
  }
224
- for (const event of runtimeEventInbox.list()) {
232
+ for (const event of await runtimeEventInbox.listAsync()) {
225
233
  if (event.type === "runtime-observation" && event.observation.kind === "session.started"
226
234
  && event.observation.fence.taskId === owner.taskId
227
235
  && event.observation.fence.roleName === owner.roleName
@@ -266,13 +274,13 @@ async function createFileTaskControllerRuntime(home, options) {
266
274
  globalInstallPaths: [process.execPath]
267
275
  }
268
276
  });
269
- const lifecycleHost = {
270
- inspectOwner: (owner) => (sessionHost.inspectOwner(owner)),
271
- ...(sessionHost.inspectOwners === undefined
272
- ? {}
273
- : {
274
- inspectOwners: (owners) => sessionHost.inspectOwners(owners)
275
- }),
277
+ const hostStateChecks = createNonBlockingChecks([{
278
+ id: "host-version-hint",
279
+ run: (_transition, signal) => { if (!signal.aborted)
280
+ hostLifecycleChecks.notify("host-observed"); }
281
+ }], options.onError);
282
+ const controllerHosts = createControllerHostLifecycle(rawSessionHost, {
283
+ after: hostStateChecks,
276
284
  stopOwner: (owner) => {
277
285
  // Issue 03: the durable `stopped` transition is gated on physical
278
286
  // exit proof. A blocked result keeps the Session non-terminal and
@@ -286,7 +294,9 @@ async function createFileTaskControllerRuntime(home, options) {
286
294
  return result.outcome === "stop-confirmed";
287
295
  });
288
296
  },
289
- };
297
+ });
298
+ const sessionHost = controllerHosts.port;
299
+ const lifecycleHost = sessionHost;
290
300
  const resourceActivity = createRuntimeResourceActivityTracker();
291
301
  let runningRuntime;
292
302
  let runningController;
@@ -297,6 +307,32 @@ async function createFileTaskControllerRuntime(home, options) {
297
307
  },
298
308
  runtimeIsolation
299
309
  });
310
+ const hostUpgrades = new HostUpgradeCoordinator(store, AGENT_HOST_IMPLEMENTATION, {
311
+ inspect: owner => inspectAgentHost({ home, ...owner }),
312
+ control: async (owner, type, request) => {
313
+ const send = async () => {
314
+ const result = await sendAgentHostUpgradeControl({
315
+ home, ...owner, control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type, ...request }
316
+ });
317
+ if (result.outcome === "rejected")
318
+ throw new Error(result.failure?.detail ?? "Host upgrade control rejected.");
319
+ return result.snapshot;
320
+ };
321
+ return type === "finish-upgrade" ? controllerHosts.requestExit(owner, send) : send();
322
+ },
323
+ hostState: async (owner) => (await sessionHost.inspectOwner(owner)).state,
324
+ endpoint: builtinAgentEndpointImplementation,
325
+ restore: async (owner, request, effective, assertCurrent) => {
326
+ const workspace = owner.scope === "task" ? currentDesiredManagedWorkspace(store, owner.taskId, owner.roleName) : undefined;
327
+ await launchCoordinator.prepare({
328
+ owner, agentId: request.agentId, adapterId: request.adapterId, effective,
329
+ workspace: effective.workspace.root, mode: "resume", nativeSessionId: request.nativeSessionId,
330
+ ...(workspace === undefined ? {} : { managedWorkspace: workspace }),
331
+ ...(effective.executionAuthority === "planning" ? { workspaceFree: true } : {})
332
+ }, assertCurrent);
333
+ }
334
+ });
335
+ const hostLifecycleChecks = createHostLifecycleChecks(hostUpgrades, options.onError);
300
336
  // One inventory scan per scheduler pass. When the inventory worker is active
301
337
  // the blocking /proc scan runs there; otherwise it runs on the main thread.
302
338
  const scanInventory = (panes) => inventoryClient !== undefined
@@ -416,7 +452,7 @@ async function createFileTaskControllerRuntime(home, options) {
416
452
  store,
417
453
  environment: options.environment
418
454
  });
419
- const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, launchCoordinator, planner);
455
+ const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, launchCoordinator, planner, workerMaintenance ? asyncStoreClient : undefined);
420
456
  // Process-exit observations are persisted by the Agent Host before socket
421
457
  // delivery. Drain them while this Controller is still the only storage
422
458
  // writer and before it begins accepting new work after a handover.
@@ -587,10 +623,22 @@ async function createFileTaskControllerRuntime(home, options) {
587
623
  let messageHealthHosts = [];
588
624
  const running = await startFileTaskController(home, workerMaintenance
589
625
  ? workerSchedulerStore(schedulerStore, asyncStoreClient)
590
- : schedulerStore, delivery, (method, params) => method === "task.role-capabilities"
591
- ? readTaskAgentCapabilities(store, catalogs, params)
592
- : method === "web.start" || method === "web.stop" || method === "web.status"
593
- ? web.dispatch(method, params) : lifecycleDispatcher(method, params), {
626
+ : schedulerStore, delivery, (method, params) => method === "runtime.host-upgrade-status"
627
+ ? hostUpgrades.status().then(hosts => ({ hosts }))
628
+ : method === "runtime.upgrade-hosts"
629
+ ? (params.generation === AGENT_HOST_IMPLEMENTATION.generation
630
+ ? hostUpgrades.request().then(hosts => ({ hosts }))
631
+ : Promise.reject(new Error("Host upgrade target is not this Controller's verified loaded generation.")))
632
+ : method === "task.role-capabilities"
633
+ ? readTaskAgentCapabilities(store, catalogs, params)
634
+ : method === "web.start" || method === "web.stop" || method === "web.status"
635
+ ? web.dispatch(method, params) : Promise.resolve(lifecycleDispatcher(method, params)).then(result => {
636
+ if (["runtime.ensure-role-session", "runtime.process-exit-observe",
637
+ "runtime.host-observation-apply", "runtime.observation-apply"].includes(method)) {
638
+ hostLifecycleChecks.notify("host-observed");
639
+ }
640
+ return result;
641
+ }), {
594
642
  intervalMs: options.intervalMs
595
643
  ?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
596
644
  signalWindowMs: options.signalWindowMs,
@@ -607,7 +655,9 @@ async function createFileTaskControllerRuntime(home, options) {
607
655
  now: options.now,
608
656
  onError: options.onError,
609
657
  diagnosticMetrics: options.diagnosticMetrics,
658
+ onRequestDiagnostic: options.onRequestDiagnostic,
610
659
  lifecycleHost,
660
+ lifecycleChecks: hostLifecycleChecks,
611
661
  providerRetry: createProviderRetryHooks(home, store, {
612
662
  now: options.now,
613
663
  onError: options.onError,
@@ -702,7 +752,9 @@ async function createFileTaskControllerRuntime(home, options) {
702
752
  }
703
753
  let resourceClose;
704
754
  const closeResources = () => {
705
- resourceClose ??= closeTelemetry().finally(() => Promise.all([
755
+ hostStateChecks.close();
756
+ hostLifecycleChecks.close();
757
+ resourceClose ??= hostUpgrades.close().then(() => closeTelemetry()).finally(() => Promise.all([
706
758
  web.close(),
707
759
  kernel.close(),
708
760
  asyncStoreClient?.close() ?? Promise.resolve(),
@@ -753,7 +805,7 @@ async function createFileTaskControllerRuntime(home, options) {
753
805
  throw error;
754
806
  }
755
807
  }
756
- export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, sharedLaunchCoordinator, environmentRefresher) {
808
+ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, sharedLaunchCoordinator, environmentRefresher, observationWorker) {
757
809
  const launchCoordinator = sharedLaunchCoordinator
758
810
  ?? new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
759
811
  assertCurrent: (request) => {
@@ -774,12 +826,20 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
774
826
  if (event.type !== "runtime-observation" || event.host === undefined) {
775
827
  throw applicationError("INVALID_PARAMS", "Expected an Agent Host observation.");
776
828
  }
777
- return { outcome: schedulerStore.observeAgentHostObservation(event, new Date()) };
829
+ // Eager Host delivery and the normal Inbox drainer fold the same durable
830
+ // fact. Share their existing worker writer: a second synchronous writer
831
+ // here can wait on BEGIN IMMEDIATE and starve even status socket I/O.
832
+ // Await the original atomic fold; no eager ACK or cursor advancement.
833
+ return { outcome: observationWorker === undefined
834
+ ? schedulerStore.observeAgentHostObservation(event, new Date())
835
+ : await observationWorker.invokeObserver("observeAgentHostObservation", [event, new Date()]) };
778
836
  }
779
837
  if (method === "runtime.observation-apply") {
780
838
  try {
781
839
  return {
782
- outcome: schedulerStore.observeRuntimeObservation(createRuntimeObservation(params), new Date())
840
+ outcome: observationWorker === undefined
841
+ ? schedulerStore.observeRuntimeObservation(createRuntimeObservation(params), new Date())
842
+ : await observationWorker.invokeObserver("observeRuntimeObservation", [createRuntimeObservation(params), new Date()])
783
843
  };
784
844
  }
785
845
  catch (error) {
@@ -835,7 +895,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
835
895
  const retry = binding?.retry;
836
896
  return { recorded: true, ...(retry?.currentAttemptId === value.attemptId && value.boundedText !== undefined
837
897
  ? { retryInput: {
838
- text: providerRetryPrompt(retry, value.boundedText),
898
+ text: value.boundedText,
899
+ ...(retry.failedNativeTurnId === undefined ? {} : { context: providerRetryContext(retry) }),
839
900
  ...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
840
901
  } } : {}) };
841
902
  }