@zq-silk/yui 2.22.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/dist/cli/commandCatalog.js +2 -2
- package/dist/cli/updateCommand.js +20 -3
- package/dist/cli/updateOrchestrator.js +13 -0
- package/dist/cli/updatePorts.js +27 -0
- package/dist/commands/taskRoleRuntimeStatus.js +4 -0
- package/dist/context/currentSkills.js +32 -1
- package/dist/context/sessionBootstrapManifest.js +17 -4
- package/dist/context/sessionSkillNotice.js +33 -12
- package/dist/controller/clientRuntime.js +37 -18
- package/dist/controller/controller.js +16 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +2 -0
- package/dist/controller/hostLifecycle.js +153 -0
- package/dist/controller/hostLifecycleChecks.js +27 -0
- package/dist/controller/hostUpgrade.js +344 -0
- package/dist/controller/runtime.js +67 -16
- package/dist/controller/sessionNotify.js +5 -46
- package/dist/execution/codexThreadNaming.js +54 -43
- package/dist/executor/fileRoleLaunchPlanner.js +18 -3
- package/dist/runtime/agentEndpoint.js +11 -1
- package/dist/runtime/agentEndpointIdentity.js +2 -0
- package/dist/runtime/agentHost.js +167 -42
- package/dist/runtime/agentHostImplementation.js +3 -1
- package/dist/runtime/codexAppServerRuntime.js +3 -0
- package/dist/runtime/codexInteractiveHost.js +62 -17
- 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 +4 -4
- package/dist/runtime/structuredProviderHost.js +42 -10
- package/dist/storage/sqliteSchema.js +20 -0
- package/dist/storage/storageVersions.js +1 -1
- package/docs/managed-turn-and-session-runtime.md +96 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +70 -1
- package/docs/release-workflow.md +28 -0
- package/docs/release-workflow.zh-CN.md +20 -0
- package/docs/storage-baseline.md +10 -1
- package/docs/storage-baseline.zh-CN.md +5 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +7 -2
- package/skills/yui-runtime/VERSION +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createNonBlockingChecks } from "../runtime/lifecycleChecks.js";
|
|
2
|
+
/** Checks produce a recommendation; only the separately fenced coordinator
|
|
3
|
+
* can admit/execute it. Hints contain no target identity or restart authority. */
|
|
4
|
+
export function createHostLifecycleChecks(coordinator, onError = () => { }) {
|
|
5
|
+
const checks = createNonBlockingChecks([{
|
|
6
|
+
id: "host-version",
|
|
7
|
+
run: async (_hint, signal) => {
|
|
8
|
+
const observations = await coordinator.status();
|
|
9
|
+
if (signal.aborted)
|
|
10
|
+
return;
|
|
11
|
+
const recommended = coordinator.hasPending() || observations.some(entry => entry.status === "waiting" || entry.status === "unknown" || entry.status === "failed");
|
|
12
|
+
if (!recommended)
|
|
13
|
+
return;
|
|
14
|
+
// This await belongs exclusively to this optional check lane. Neither
|
|
15
|
+
// Host transitions nor Controller event folding wait for this operation.
|
|
16
|
+
const results = await coordinator.request();
|
|
17
|
+
for (const entry of results) {
|
|
18
|
+
if (entry.status === "failed" || entry.status === "unknown") {
|
|
19
|
+
onError(new Error(`Host upgrade ${entry.owner.roleName}/${entry.nativeSessionId}: ${entry.detail}`));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}], onError);
|
|
24
|
+
const close = checks.close;
|
|
25
|
+
checks.close = () => { close(); coordinator.stop(); };
|
|
26
|
+
return checks;
|
|
27
|
+
}
|
|
@@ -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); }
|
|
@@ -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";
|
|
@@ -198,7 +205,7 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
198
205
|
?? new AgentConfigurationCatalogService(home, {
|
|
199
206
|
environment: options.environment ?? process.env
|
|
200
207
|
});
|
|
201
|
-
const
|
|
208
|
+
const rawSessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
|
|
202
209
|
validateLaunch: async (request) => {
|
|
203
210
|
const agent = store.getConfiguredAgent(request.agentId);
|
|
204
211
|
if (agent === null)
|
|
@@ -267,13 +274,13 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
267
274
|
globalInstallPaths: [process.execPath]
|
|
268
275
|
}
|
|
269
276
|
});
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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,
|
|
277
284
|
stopOwner: (owner) => {
|
|
278
285
|
// Issue 03: the durable `stopped` transition is gated on physical
|
|
279
286
|
// exit proof. A blocked result keeps the Session non-terminal and
|
|
@@ -287,7 +294,9 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
287
294
|
return result.outcome === "stop-confirmed";
|
|
288
295
|
});
|
|
289
296
|
},
|
|
290
|
-
};
|
|
297
|
+
});
|
|
298
|
+
const sessionHost = controllerHosts.port;
|
|
299
|
+
const lifecycleHost = sessionHost;
|
|
291
300
|
const resourceActivity = createRuntimeResourceActivityTracker();
|
|
292
301
|
let runningRuntime;
|
|
293
302
|
let runningController;
|
|
@@ -298,6 +307,32 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
298
307
|
},
|
|
299
308
|
runtimeIsolation
|
|
300
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);
|
|
301
336
|
// One inventory scan per scheduler pass. When the inventory worker is active
|
|
302
337
|
// the blocking /proc scan runs there; otherwise it runs on the main thread.
|
|
303
338
|
const scanInventory = (panes) => inventoryClient !== undefined
|
|
@@ -588,10 +623,22 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
588
623
|
let messageHealthHosts = [];
|
|
589
624
|
const running = await startFileTaskController(home, workerMaintenance
|
|
590
625
|
? workerSchedulerStore(schedulerStore, asyncStoreClient)
|
|
591
|
-
: schedulerStore, delivery, (method, params) => method === "
|
|
592
|
-
?
|
|
593
|
-
: method === "
|
|
594
|
-
?
|
|
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
|
+
}), {
|
|
595
642
|
intervalMs: options.intervalMs
|
|
596
643
|
?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
|
|
597
644
|
signalWindowMs: options.signalWindowMs,
|
|
@@ -610,6 +657,7 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
610
657
|
diagnosticMetrics: options.diagnosticMetrics,
|
|
611
658
|
onRequestDiagnostic: options.onRequestDiagnostic,
|
|
612
659
|
lifecycleHost,
|
|
660
|
+
lifecycleChecks: hostLifecycleChecks,
|
|
613
661
|
providerRetry: createProviderRetryHooks(home, store, {
|
|
614
662
|
now: options.now,
|
|
615
663
|
onError: options.onError,
|
|
@@ -704,7 +752,9 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
704
752
|
}
|
|
705
753
|
let resourceClose;
|
|
706
754
|
const closeResources = () => {
|
|
707
|
-
|
|
755
|
+
hostStateChecks.close();
|
|
756
|
+
hostLifecycleChecks.close();
|
|
757
|
+
resourceClose ??= hostUpgrades.close().then(() => closeTelemetry()).finally(() => Promise.all([
|
|
708
758
|
web.close(),
|
|
709
759
|
kernel.close(),
|
|
710
760
|
asyncStoreClient?.close() ?? Promise.resolve(),
|
|
@@ -845,7 +895,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
845
895
|
const retry = binding?.retry;
|
|
846
896
|
return { recorded: true, ...(retry?.currentAttemptId === value.attemptId && value.boundedText !== undefined
|
|
847
897
|
? { retryInput: {
|
|
848
|
-
text:
|
|
898
|
+
text: value.boundedText,
|
|
899
|
+
...(retry.failedNativeTurnId === undefined ? {} : { context: providerRetryContext(retry) }),
|
|
849
900
|
...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
|
|
850
901
|
} } : {}) };
|
|
851
902
|
}
|
|
@@ -2,10 +2,9 @@ import { callController } from "../core/controllerClient.js";
|
|
|
2
2
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
3
3
|
import { transportAgentResult } from "../domain/agentResultTransport.js";
|
|
4
4
|
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
5
|
-
import { setCodexThreadName } from "../execution/codexThreadNaming.js";
|
|
6
5
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
7
6
|
/** Hidden CLI entrypoint used by Codex's structured notify hook. */
|
|
8
|
-
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call
|
|
7
|
+
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call) {
|
|
9
8
|
const params = parseCodexSessionNotification(payloadArgument, environment);
|
|
10
9
|
// Old global TUIs can still emit their invocation-local hook. It is not
|
|
11
10
|
// evidence of a shared-daemon Thread's identity or process lifecycle.
|
|
@@ -15,7 +14,7 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
|
|
|
15
14
|
const home = requireText(environment.YUI_HOME, "YUI_HOME");
|
|
16
15
|
// The caller reports native identity; durable state supplies the current AgentRun.
|
|
17
16
|
const current = currentNotifyRun(home, params);
|
|
18
|
-
|
|
17
|
+
new FileRuntimeEventInbox(home).enqueueRunTerminal({
|
|
19
18
|
scope: params.scope,
|
|
20
19
|
...(params.scope === "task" ? { taskId: params.taskId } : {}),
|
|
21
20
|
roleName: params.roleName,
|
|
@@ -42,11 +41,9 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
|
|
|
42
41
|
roleName: params.roleName
|
|
43
42
|
})
|
|
44
43
|
}, { timeoutMs: 100 }).catch(() => { });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await setThreadName(request).catch(() => { });
|
|
49
|
-
}
|
|
44
|
+
// Title synchronization belongs to the Host's separately fenced metadata
|
|
45
|
+
// lane. A terminal notification is neither a rename receipt nor permission
|
|
46
|
+
// to replay an unknown write or overwrite a native user's name.
|
|
50
47
|
}
|
|
51
48
|
/**
|
|
52
49
|
* Resolve the active AgentRun only when the notifying native Session matches.
|
|
@@ -130,44 +127,6 @@ function requireText(value, label) {
|
|
|
130
127
|
throw new Error(`${label} is invalid.`);
|
|
131
128
|
return text;
|
|
132
129
|
}
|
|
133
|
-
function shouldSetThreadName(home, params, runId) {
|
|
134
|
-
if (params.scope !== "task"
|
|
135
|
-
|| params.title === undefined
|
|
136
|
-
|| runId === undefined)
|
|
137
|
-
return false;
|
|
138
|
-
try {
|
|
139
|
-
const store = openCurrentTaskStore(home);
|
|
140
|
-
return store.getRun(params.taskId, runId)?.mode === "new";
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function threadNameRequest(params, environment) {
|
|
147
|
-
if (params.title === undefined)
|
|
148
|
-
return null;
|
|
149
|
-
const command = environment.YUI_AGENT_COMMAND;
|
|
150
|
-
const encodedBaseArgs = environment.YUI_AGENT_BASE_ARGS;
|
|
151
|
-
if (command === undefined || encodedBaseArgs === undefined)
|
|
152
|
-
return null;
|
|
153
|
-
try {
|
|
154
|
-
const parsed = JSON.parse(encodedBaseArgs);
|
|
155
|
-
if (!Array.isArray(parsed)
|
|
156
|
-
|| parsed.some((value) => typeof value !== "string" || value.includes("\0"))) {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
command: requireText(command, "YUI_AGENT_COMMAND"),
|
|
161
|
-
baseArgs: parsed,
|
|
162
|
-
environment,
|
|
163
|
-
threadId: params.nativeSessionId,
|
|
164
|
-
name: params.title
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
130
|
function isObject(value) {
|
|
172
131
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
132
|
}
|