@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.
- package/README.md +5 -0
- package/dist/cli/commandCatalog.js +87 -27
- package/dist/cli/dynamicCompletion.js +4 -1
- package/dist/cli/helpRenderer.js +12 -1
- package/dist/cli/interactionPolicy.js +2 -0
- package/dist/cli/invocationRouter.js +5 -5
- package/dist/cli/roleWizard.js +5 -1
- package/dist/cli/updateCommand.js +20 -3
- package/dist/cli/updateOrchestrator.js +13 -0
- package/dist/cli/updatePorts.js +27 -0
- package/dist/commands/globalRoleCommands.js +33 -8
- package/dist/commands/inboxCommands.js +39 -12
- package/dist/commands/operatorCommands.js +2 -5
- package/dist/commands/releaseCommands.js +16 -2
- package/dist/commands/roleConfiguration.js +25 -2
- package/dist/commands/sessionCommands.js +5 -2
- package/dist/commands/taskCommands.js +217 -64
- package/dist/commands/taskContextCommand.js +26 -4
- package/dist/commands/taskFactCommands.js +15 -2
- package/dist/commands/taskIntegrationCommands.js +3 -3
- package/dist/commands/taskRoleRuntimeStatus.js +19 -5
- package/dist/commands/textInput.js +2 -2
- package/dist/context/contextFamilies.js +10 -0
- package/dist/context/currentSkills.js +37 -4
- package/dist/context/runContextPack.js +44 -5
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +21 -6
- package/dist/context/sessionSkillNotice.js +74 -0
- package/dist/context/taskContext.js +100 -16
- package/dist/controlPlaneCli.js +17 -7
- package/dist/controller/agentRuntimeObserver.js +57 -12
- package/dist/controller/clientRuntime.js +66 -18
- package/dist/controller/controller.js +19 -2
- package/dist/controller/diagnosticJournal.js +10 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
- package/dist/controller/hostLifecycle.js +153 -0
- package/dist/controller/hostLifecycleChecks.js +27 -0
- package/dist/controller/hostUpgrade.js +344 -0
- package/dist/controller/messageHealth.js +4 -3
- package/dist/controller/providerRetryAdmission.js +15 -1
- package/dist/controller/providerRetryDelivery.js +18 -2
- package/dist/controller/runtime.js +82 -21
- package/dist/controller/runtimeEventInbox.js +107 -13
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/controller/sessionNotify.js +5 -46
- package/dist/controller/taskRunRecovery.js +153 -0
- package/dist/core/controllerClient.js +16 -2
- package/dist/core/controllerServer.js +14 -5
- package/dist/core/controllerTelemetry.js +51 -5
- package/dist/errors/cliFailure.js +14 -5
- package/dist/execution/codexThreadNaming.js +54 -43
- package/dist/executor/agentAdapter.js +32 -4
- package/dist/executor/agentConfigurationFields.js +2 -0
- package/dist/executor/fileRoleLaunchPlanner.js +18 -3
- package/dist/integration/gitIntegrationService.js +3 -2
- package/dist/integration/integrationSourceApplication.js +12 -1
- package/dist/interaction/notificationPresentation.js +11 -16
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/message/messageContinuation.js +3 -1
- package/dist/output/boundedRead.js +36 -7
- package/dist/output/rolePresentation.js +3 -1
- package/dist/runtime/agentEndpoint.js +11 -1
- package/dist/runtime/agentEndpointIdentity.js +2 -0
- package/dist/runtime/agentHost.js +174 -40
- package/dist/runtime/agentHostImplementation.js +3 -1
- package/dist/runtime/codexAppServerRuntime.js +113 -24
- package/dist/runtime/codexInteractiveHost.js +102 -11
- package/dist/runtime/codexNativeAccess.js +1 -1
- package/dist/runtime/contextRecovery.js +15 -0
- package/dist/runtime/hostSessionLifecycle.js +83 -0
- package/dist/runtime/hostUpgradeGate.js +43 -0
- package/dist/runtime/lifecycleChecks.js +47 -0
- package/dist/runtime/lifecycleReservation.js +9 -1
- package/dist/runtime/nativeTitleReceipt.js +31 -0
- package/dist/runtime/nativeTitleSync.js +192 -0
- package/dist/runtime/providerRetry.js +9 -6
- package/dist/runtime/providerRuntimeIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +65 -35
- package/dist/scheduler/wakeupQueue.js +10 -4
- package/dist/storage/sqliteSchema.js +29 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/tmux/tmuxManager.js +4 -2
- package/dist/web/webSettings.js +2 -0
- package/docs/authorization.md +18 -3
- package/docs/cli-information-contract.md +228 -6
- package/docs/cli-information-contract.zh-CN.md +95 -5
- package/docs/managed-turn-and-session-runtime.md +177 -21
- package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +65 -0
- package/docs/release-workflow.zh-CN.md +48 -0
- package/docs/roles-and-configuration.md +28 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/storage-baseline.md +13 -1
- package/docs/storage-baseline.zh-CN.md +5 -1
- package/docs/task-delivery.md +7 -0
- package/docs/task-delivery.zh-CN.md +5 -0
- package/docs/testing/review-evaluation.md +7 -7
- package/docs/testing/review-evaluation.zh-CN.md +7 -7
- package/docs/testing/verification-levels.md +45 -2
- package/docs/testing/verification-levels.zh-CN.md +21 -2
- package/i18n/README.zh-CN.md +6 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +98 -193
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/artifacts.md +41 -0
- package/skills/yui-leader/references/assignment-execution.md +140 -0
- package/skills/yui-leader/references/completion.md +79 -0
- package/skills/yui-leader/references/delivery-scope.md +26 -0
- package/skills/yui-leader/references/execution.md +16 -487
- package/skills/yui-leader/references/implementation.md +81 -0
- package/skills/yui-leader/references/planning.md +7 -45
- package/skills/yui-leader/references/review.md +90 -0
- package/skills/yui-leader/references/task-progress.md +23 -0
- package/skills/yui-leader/references/work-allocation.md +118 -0
- package/skills/yui-operator/SKILL.md +77 -333
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-operator/references/body-transport.md +62 -0
- package/skills/yui-operator/references/configuration.md +47 -0
- package/skills/yui-operator/references/lifecycle.md +41 -0
- package/skills/yui-operator/references/progress.md +39 -0
- package/skills/yui-operator/references/routing.md +57 -0
- package/skills/yui-operator/references/submission.md +56 -0
- package/skills/yui-operator/references/task-delivery.md +1 -1
- package/skills/yui-operator/references/web-lifecycle.md +19 -0
- package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
- package/skills/yui-runtime/SKILL.md +182 -531
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/archive.md +51 -0
- package/skills/yui-runtime/references/command-discovery.md +41 -0
- package/skills/yui-runtime/references/completed-followup.md +65 -0
- package/skills/yui-runtime/references/context-ordering.md +3 -34
- package/skills/yui-runtime/references/context-reads.md +61 -0
- package/skills/yui-runtime/references/dispatched-run.md +61 -0
- package/skills/yui-runtime/references/execution-failure.md +105 -0
- package/skills/yui-runtime/references/global-context.md +18 -0
- package/skills/yui-runtime/references/global-input-controls.md +44 -0
- package/skills/yui-runtime/references/host-custody.md +30 -0
- package/skills/yui-runtime/references/inbox-recovery.md +77 -0
- package/skills/yui-runtime/references/incremental-context.md +40 -0
- package/skills/yui-runtime/references/publication.md +4 -111
- package/skills/yui-runtime/references/recovery.md +32 -192
- package/skills/yui-runtime/references/session-identity.md +18 -0
- package/skills/yui-runtime/references/session-replacement.md +45 -0
- 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
|
|
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
|
|
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
|
-
|| !
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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 === "
|
|
591
|
-
?
|
|
592
|
-
: method === "
|
|
593
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
898
|
+
text: value.boundedText,
|
|
899
|
+
...(retry.failedNativeTurnId === undefined ? {} : { context: providerRetryContext(retry) }),
|
|
839
900
|
...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
|
|
840
901
|
} } : {}) };
|
|
841
902
|
}
|