@zq-silk/yui 0.13.2 → 0.13.3
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/ARCHITECTURE.md +31 -25
- package/README.md +24 -20
- package/dist/cli/commandCatalog.js +3 -3
- package/dist/commands/taskCommands.js +32 -9
- package/dist/controller/controller.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +1 -1
- package/dist/controller/runtimeLaunchCoordinator.js +4 -0
- package/dist/executor/agentAdapter.js +62 -8
- package/dist/executor/executorRegistry.js +4 -0
- package/dist/executor/fileRoleLaunchPlanner.js +71 -27
- package/dist/runtime/agentHost.js +226 -14
- package/dist/runtime/builtinAgentDrivers.js +9 -0
- package/dist/runtime/codexAppServerRuntime.js +63 -12
- package/dist/runtime/launchBroker.js +51 -1
- package/dist/runtime/runtimeBinding.js +10 -1
- package/dist/runtime/structuredProviderHost.js +214 -54
- package/dist/runtime/tmuxAdapters.js +13 -2
- package/dist/scheduler/activeRoleRunDelivery.js +24 -3
- package/dist/scheduler/leaderWakeupProcessor.js +20 -0
- package/i18n/README.zh-CN.md +7 -7
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { callFileTaskController } from "../controller/clientRuntime.js";
|
|
|
10
10
|
import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
11
11
|
import { publishStructuredProviderAccepted, publishStructuredProviderActivationTerminal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderTerminal } from "../controller/structuredProviderObservation.js";
|
|
12
12
|
import { validateAgentHostLaunchPayload } from "./launchBroker.js";
|
|
13
|
-
import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnRejectedError, startStructuredProviderSession } from "./structuredProviderHost.js";
|
|
13
|
+
import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnBusyError, ProviderTurnRejectedError, startStructuredProviderSession } from "./structuredProviderHost.js";
|
|
14
14
|
import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
15
15
|
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
16
16
|
import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
|
|
@@ -18,6 +18,8 @@ import { readRuntimeStopReceipt, removeRuntimeStopReceipt, writeRuntimeStopRecei
|
|
|
18
18
|
import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
19
19
|
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
|
|
20
20
|
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
21
|
+
const CODEX_CLIENT_STABLE_MS = 5_000;
|
|
22
|
+
const MAX_CONSECUTIVE_CODEX_DISCONNECTS = 3;
|
|
21
23
|
export function serializeAgentHostLaunchControl(control) {
|
|
22
24
|
return JSON.stringify(validateControl(control));
|
|
23
25
|
}
|
|
@@ -28,6 +30,10 @@ export async function runAgentHost(input) {
|
|
|
28
30
|
let session;
|
|
29
31
|
let sessionPayload;
|
|
30
32
|
let activeTurnPayload;
|
|
33
|
+
let activeTurnAttemptId;
|
|
34
|
+
let activeNativeTurnId;
|
|
35
|
+
let codexClientAttachedAt;
|
|
36
|
+
let consecutiveCodexDisconnects = 0;
|
|
31
37
|
const switchDetachedSessions = new WeakSet();
|
|
32
38
|
let activationId;
|
|
33
39
|
let conversationRecoverability = "unknown";
|
|
@@ -46,6 +52,27 @@ export async function runAgentHost(input) {
|
|
|
46
52
|
authorityHolderId: authority.holderId
|
|
47
53
|
};
|
|
48
54
|
const handleTerminal = (terminal) => {
|
|
55
|
+
if (!terminal.clientOwned) {
|
|
56
|
+
const busyPayload = sessionPayload;
|
|
57
|
+
if (busyPayload === undefined
|
|
58
|
+
|| session === undefined
|
|
59
|
+
|| terminal.conversationId !== session.conversationId)
|
|
60
|
+
return;
|
|
61
|
+
// Another ordinary client completed a Turn. It never enters Yui's Run
|
|
62
|
+
// observation path; it only removes backpressure from retained work.
|
|
63
|
+
if (snapshot.state === "busy") {
|
|
64
|
+
updateSnapshot(hostSnapshot("idle", {
|
|
65
|
+
launchId: busyPayload.launchId,
|
|
66
|
+
adapterId: session.adapterId,
|
|
67
|
+
processInstanceId: session.processInstanceId,
|
|
68
|
+
nativeSessionId: session.nativeSessionId,
|
|
69
|
+
conversationId: session.conversationId,
|
|
70
|
+
...authorityFields()
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
signalRoleMailbox(input.home, busyPayload);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
49
76
|
const terminalPayload = activeTurnPayload;
|
|
50
77
|
if (terminalPayload === undefined)
|
|
51
78
|
return;
|
|
@@ -74,10 +101,12 @@ export async function runAgentHost(input) {
|
|
|
74
101
|
if (activeTurnPayload !== terminalPayload)
|
|
75
102
|
return;
|
|
76
103
|
activeTurnPayload = undefined;
|
|
104
|
+
activeTurnAttemptId = undefined;
|
|
105
|
+
activeNativeTurnId = undefined;
|
|
77
106
|
if (session === undefined)
|
|
78
107
|
return;
|
|
79
108
|
const currentPayload = sessionPayload ?? terminalPayload;
|
|
80
|
-
updateSnapshot(hostSnapshot("idle", {
|
|
109
|
+
updateSnapshot(hostSnapshot(session.activeTurnId === undefined ? "idle" : "busy", {
|
|
81
110
|
launchId: currentPayload.launchId,
|
|
82
111
|
adapterId: currentPayload.providerControl.adapterId,
|
|
83
112
|
processInstanceId: session.processInstanceId,
|
|
@@ -101,6 +130,92 @@ export async function runAgentHost(input) {
|
|
|
101
130
|
}
|
|
102
131
|
}).catch(() => { });
|
|
103
132
|
};
|
|
133
|
+
const reconnectCodexClient = async (disconnectedSession, currentPayload) => {
|
|
134
|
+
const previousControl = currentPayload.providerControl;
|
|
135
|
+
if (previousControl?.adapterId !== "codex" || authority === undefined) {
|
|
136
|
+
throw new Error("Codex client reconnect lost its Provider control identity.");
|
|
137
|
+
}
|
|
138
|
+
const ownedTurn = activeTurnPayload === undefined
|
|
139
|
+
|| activeTurnAttemptId === undefined
|
|
140
|
+
|| activeNativeTurnId === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: { attemptId: activeTurnAttemptId, turnId: activeNativeTurnId };
|
|
143
|
+
const reconnectPayload = {
|
|
144
|
+
...currentPayload,
|
|
145
|
+
environment: activeTurnPayload?.environment ?? currentPayload.environment,
|
|
146
|
+
providerControl: {
|
|
147
|
+
schemaVersion: 1,
|
|
148
|
+
adapterId: "codex",
|
|
149
|
+
transport: "codex-app-server-proxy",
|
|
150
|
+
kind: "ensure",
|
|
151
|
+
mode: "resume",
|
|
152
|
+
nativeSessionId: disconnectedSession.nativeSessionId,
|
|
153
|
+
...(previousControl.sessionTitle === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { sessionTitle: previousControl.sessionTitle }),
|
|
156
|
+
codexThread: previousControl.codexThread,
|
|
157
|
+
codexDaemonStartArgs: previousControl.codexDaemonStartArgs,
|
|
158
|
+
...(ownedTurn === undefined ? {} : { ownedTurn }),
|
|
159
|
+
authority
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const delays = [0, 250, 1_000];
|
|
163
|
+
let lastError;
|
|
164
|
+
for (const delayMs of delays) {
|
|
165
|
+
if (hostStopRequested)
|
|
166
|
+
return;
|
|
167
|
+
if (delayMs !== 0)
|
|
168
|
+
await delay(delayMs);
|
|
169
|
+
if (hostStopRequested)
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
const started = await startStructuredProviderSession(reconnectPayload, {
|
|
173
|
+
onTerminal: handleTerminal
|
|
174
|
+
});
|
|
175
|
+
session = started.session;
|
|
176
|
+
sessionPayload = currentPayload;
|
|
177
|
+
conversationRecoverability = "recoverable";
|
|
178
|
+
codexClientAttachedAt = Date.now();
|
|
179
|
+
observeExit(started.session, currentPayload);
|
|
180
|
+
const reconnectState = activeTurnPayload !== undefined
|
|
181
|
+
? ownedTurn === undefined ? "delivery-unknown" : "ready"
|
|
182
|
+
: started.session.activeTurnId === undefined ? "idle" : "busy";
|
|
183
|
+
updateSnapshot(hostSnapshot(reconnectState, {
|
|
184
|
+
launchId: currentPayload.launchId,
|
|
185
|
+
adapterId: "codex",
|
|
186
|
+
processInstanceId: started.session.processInstanceId,
|
|
187
|
+
nativeSessionId: started.session.nativeSessionId,
|
|
188
|
+
conversationId: started.session.conversationId,
|
|
189
|
+
...(activeTurnAttemptId === undefined ? {} : { attemptId: activeTurnAttemptId }),
|
|
190
|
+
...(activeNativeTurnId === undefined ? {} : { nativeTurnId: activeNativeTurnId }),
|
|
191
|
+
...authorityFields(),
|
|
192
|
+
...(ownedTurn !== undefined || activeTurnPayload === undefined
|
|
193
|
+
? {}
|
|
194
|
+
: {
|
|
195
|
+
detail: "Codex client reattached, but the in-flight Turn has no exact native identity."
|
|
196
|
+
})
|
|
197
|
+
}));
|
|
198
|
+
if (started.recoveredTerminal !== undefined) {
|
|
199
|
+
handleTerminal(started.recoveredTerminal);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
lastError = error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
updateSnapshot(hostSnapshot("failed", {
|
|
208
|
+
launchId: currentPayload.launchId,
|
|
209
|
+
adapterId: "codex",
|
|
210
|
+
processInstanceId: disconnectedSession.processInstanceId,
|
|
211
|
+
nativeSessionId: disconnectedSession.nativeSessionId,
|
|
212
|
+
conversationId: disconnectedSession.conversationId,
|
|
213
|
+
...(activeTurnAttemptId === undefined ? {} : { attemptId: activeTurnAttemptId }),
|
|
214
|
+
...(activeNativeTurnId === undefined ? {} : { nativeTurnId: activeNativeTurnId }),
|
|
215
|
+
...authorityFields(),
|
|
216
|
+
detail: `Codex client could not reattach after bounded retries: ${errorText(lastError)}`
|
|
217
|
+
}));
|
|
218
|
+
};
|
|
104
219
|
const observeExit = (providerSession, launched) => {
|
|
105
220
|
void providerSession.waitForExit().then((result) => enqueueSerialized(async () => {
|
|
106
221
|
const ownsCurrentSession = session === providerSession;
|
|
@@ -109,6 +224,47 @@ export async function runAgentHost(input) {
|
|
|
109
224
|
? activationId ?? launched.launchId
|
|
110
225
|
: launched.launchId;
|
|
111
226
|
const exitAuthority = authorityFields();
|
|
227
|
+
const stopReceipt = readRuntimeStopReceipt(input.home, currentPayload.launchId);
|
|
228
|
+
const reconnectableCodexClient = ownsCurrentSession
|
|
229
|
+
&& providerSession.adapterId === "codex"
|
|
230
|
+
&& !hostStopRequested
|
|
231
|
+
&& stopReceipt === null
|
|
232
|
+
&& !switchDetachedSessions.has(providerSession);
|
|
233
|
+
if (reconnectableCodexClient) {
|
|
234
|
+
session = undefined;
|
|
235
|
+
if (codexClientAttachedAt !== undefined
|
|
236
|
+
&& Date.now() - codexClientAttachedAt >= CODEX_CLIENT_STABLE_MS) {
|
|
237
|
+
consecutiveCodexDisconnects = 0;
|
|
238
|
+
}
|
|
239
|
+
consecutiveCodexDisconnects += 1;
|
|
240
|
+
if (consecutiveCodexDisconnects > MAX_CONSECUTIVE_CODEX_DISCONNECTS) {
|
|
241
|
+
updateSnapshot(hostSnapshot("failed", {
|
|
242
|
+
launchId: currentPayload.launchId,
|
|
243
|
+
adapterId: "codex",
|
|
244
|
+
processInstanceId: result.processInstanceId,
|
|
245
|
+
nativeSessionId: providerSession.nativeSessionId,
|
|
246
|
+
conversationId: providerSession.conversationId,
|
|
247
|
+
...(activeTurnAttemptId === undefined ? {} : { attemptId: activeTurnAttemptId }),
|
|
248
|
+
...(activeNativeTurnId === undefined ? {} : { nativeTurnId: activeNativeTurnId }),
|
|
249
|
+
...exitAuthority,
|
|
250
|
+
detail: "Codex client repeatedly disconnected before reaching a stable attachment."
|
|
251
|
+
}));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
updateSnapshot(hostSnapshot("starting", {
|
|
255
|
+
launchId: currentPayload.launchId,
|
|
256
|
+
adapterId: "codex",
|
|
257
|
+
processInstanceId: result.processInstanceId,
|
|
258
|
+
nativeSessionId: providerSession.nativeSessionId,
|
|
259
|
+
conversationId: providerSession.conversationId,
|
|
260
|
+
...(activeTurnAttemptId === undefined ? {} : { attemptId: activeTurnAttemptId }),
|
|
261
|
+
...(activeNativeTurnId === undefined ? {} : { nativeTurnId: activeNativeTurnId }),
|
|
262
|
+
...exitAuthority,
|
|
263
|
+
detail: "Codex client disconnected; reattaching to the shared daemon."
|
|
264
|
+
}));
|
|
265
|
+
await reconnectCodexClient(providerSession, currentPayload);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
112
268
|
if (ownsCurrentSession) {
|
|
113
269
|
session = undefined;
|
|
114
270
|
activationId = undefined;
|
|
@@ -116,7 +272,6 @@ export async function runAgentHost(input) {
|
|
|
116
272
|
authority = undefined;
|
|
117
273
|
}
|
|
118
274
|
hostSequence += 1;
|
|
119
|
-
const stopReceipt = readRuntimeStopReceipt(input.home, currentPayload.launchId);
|
|
120
275
|
const observedAt = new Date().toISOString();
|
|
121
276
|
const failures = [];
|
|
122
277
|
if (!switchDetachedSessions.has(providerSession)) {
|
|
@@ -275,10 +430,22 @@ export async function runAgentHost(input) {
|
|
|
275
430
|
conversationRecoverability = providerControl.adapterId === "codex"
|
|
276
431
|
? "recoverable"
|
|
277
432
|
: "unknown";
|
|
433
|
+
if (providerControl.kind === "ensure" && providerControl.ownedTurn !== undefined) {
|
|
434
|
+
activeTurnPayload = next;
|
|
435
|
+
activeTurnAttemptId = providerControl.ownedTurn.attemptId;
|
|
436
|
+
activeNativeTurnId = providerControl.ownedTurn.turnId;
|
|
437
|
+
}
|
|
278
438
|
const started = await startStructuredProviderSession(next, { onTerminal: handleTerminal });
|
|
279
439
|
session = started.session;
|
|
280
440
|
sessionPayload = next;
|
|
441
|
+
if (started.session.adapterId === "codex") {
|
|
442
|
+
codexClientAttachedAt = Date.now();
|
|
443
|
+
consecutiveCodexDisconnects = 0;
|
|
444
|
+
}
|
|
281
445
|
observeExit(started.session, next);
|
|
446
|
+
if (started.recoveredTerminal !== undefined) {
|
|
447
|
+
handleTerminal(started.recoveredTerminal);
|
|
448
|
+
}
|
|
282
449
|
}
|
|
283
450
|
await publishStructuredProviderOpened({
|
|
284
451
|
home: input.home,
|
|
@@ -294,9 +461,12 @@ export async function runAgentHost(input) {
|
|
|
294
461
|
durableInitialTurn = hostTurnControlParams(next, session.nativeSessionId, requestedAuthority, providerControl.initialTurn.attemptId);
|
|
295
462
|
await beginDurableProviderTurn(input.home, durableInitialTurn);
|
|
296
463
|
activeTurnPayload = next;
|
|
464
|
+
activeTurnAttemptId = providerControl.initialTurn.attemptId;
|
|
465
|
+
activeNativeTurnId = undefined;
|
|
297
466
|
try {
|
|
298
467
|
receipt = await session.submitTurn(providerControl.initialTurn);
|
|
299
468
|
providerAcceptedAttemptId = receipt.attemptId;
|
|
469
|
+
activeNativeTurnId = receipt.nativeTurnId;
|
|
300
470
|
}
|
|
301
471
|
catch (error) {
|
|
302
472
|
await resolveProviderTurnSubmission(input.home, durableInitialTurn, error);
|
|
@@ -318,16 +488,26 @@ export async function runAgentHost(input) {
|
|
|
318
488
|
throw error;
|
|
319
489
|
}
|
|
320
490
|
}
|
|
321
|
-
|
|
491
|
+
const ensuredOwnedTurn = providerControl.kind === "ensure"
|
|
492
|
+
? providerControl.ownedTurn
|
|
493
|
+
: undefined;
|
|
494
|
+
const providerState = receipt !== undefined || ensuredOwnedTurn !== undefined
|
|
495
|
+
? "ready"
|
|
496
|
+
: session.activeTurnId === undefined ? "idle" : "busy";
|
|
497
|
+
updateSnapshot(hostSnapshot(providerState, {
|
|
322
498
|
launchId: next.launchId,
|
|
323
499
|
adapterId: providerControl.adapterId,
|
|
324
500
|
processInstanceId: session.processInstanceId,
|
|
325
501
|
nativeSessionId: receipt?.nativeSessionId ?? session.nativeSessionId,
|
|
326
502
|
conversationId: receipt?.conversationId ?? session.conversationId,
|
|
327
|
-
...(receipt
|
|
328
|
-
attemptId: receipt.attemptId,
|
|
329
|
-
|
|
330
|
-
|
|
503
|
+
...(receipt !== undefined
|
|
504
|
+
? { attemptId: receipt.attemptId, nativeTurnId: receipt.nativeTurnId }
|
|
505
|
+
: ensuredOwnedTurn === undefined
|
|
506
|
+
? {}
|
|
507
|
+
: {
|
|
508
|
+
attemptId: ensuredOwnedTurn.attemptId,
|
|
509
|
+
nativeTurnId: ensuredOwnedTurn.turnId
|
|
510
|
+
}),
|
|
331
511
|
...authorityFields()
|
|
332
512
|
}));
|
|
333
513
|
return snapshot;
|
|
@@ -352,7 +532,9 @@ export async function runAgentHost(input) {
|
|
|
352
532
|
|| providerAcceptedAttemptId !== undefined;
|
|
353
533
|
const state = deliveryUnknown
|
|
354
534
|
? "delivery-unknown"
|
|
355
|
-
: error instanceof
|
|
535
|
+
: error instanceof ProviderTurnBusyError
|
|
536
|
+
? "busy"
|
|
537
|
+
: error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
|
|
356
538
|
updateSnapshot(hostSnapshot(state, {
|
|
357
539
|
launchId: next.launchId,
|
|
358
540
|
adapterId: providerControl.adapterId,
|
|
@@ -365,8 +547,11 @@ export async function runAgentHost(input) {
|
|
|
365
547
|
...authorityFields(),
|
|
366
548
|
detail: errorText(error)
|
|
367
549
|
}));
|
|
368
|
-
if (state !== "delivery-unknown")
|
|
550
|
+
if (state !== "delivery-unknown") {
|
|
369
551
|
activeTurnPayload = undefined;
|
|
552
|
+
activeTurnAttemptId = undefined;
|
|
553
|
+
activeNativeTurnId = undefined;
|
|
554
|
+
}
|
|
370
555
|
if (deliveryUnknown && !(error instanceof ProviderDeliveryUnknownError)) {
|
|
371
556
|
throw new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, providerAcceptedAttemptId);
|
|
372
557
|
}
|
|
@@ -394,6 +579,8 @@ export async function runAgentHost(input) {
|
|
|
394
579
|
throw new Error("Agent Host still owns an unsettled Provider Turn.");
|
|
395
580
|
}
|
|
396
581
|
activeTurnPayload = sessionPayload;
|
|
582
|
+
activeTurnAttemptId = request.turn.attemptId;
|
|
583
|
+
activeNativeTurnId = undefined;
|
|
397
584
|
updateSnapshot(hostSnapshot("starting", {
|
|
398
585
|
launchId: request.launchId,
|
|
399
586
|
adapterId: session.adapterId,
|
|
@@ -407,6 +594,7 @@ export async function runAgentHost(input) {
|
|
|
407
594
|
try {
|
|
408
595
|
const receipt = await session.submitTurn(request.turn);
|
|
409
596
|
providerAccepted = true;
|
|
597
|
+
activeNativeTurnId = receipt.nativeTurnId;
|
|
410
598
|
await publishStructuredProviderAccepted({
|
|
411
599
|
home: input.home,
|
|
412
600
|
environment: sessionPayload.environment,
|
|
@@ -429,7 +617,9 @@ export async function runAgentHost(input) {
|
|
|
429
617
|
const deliveryUnknown = error instanceof ProviderDeliveryUnknownError || providerAccepted;
|
|
430
618
|
const state = deliveryUnknown
|
|
431
619
|
? "delivery-unknown"
|
|
432
|
-
: error instanceof
|
|
620
|
+
: error instanceof ProviderTurnBusyError
|
|
621
|
+
? "busy"
|
|
622
|
+
: error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
|
|
433
623
|
updateSnapshot(hostSnapshot(state, {
|
|
434
624
|
launchId: request.launchId,
|
|
435
625
|
adapterId: session.adapterId,
|
|
@@ -440,8 +630,11 @@ export async function runAgentHost(input) {
|
|
|
440
630
|
...authorityFields(),
|
|
441
631
|
detail: errorText(error)
|
|
442
632
|
}));
|
|
443
|
-
if (state !== "delivery-unknown")
|
|
633
|
+
if (state !== "delivery-unknown") {
|
|
444
634
|
activeTurnPayload = undefined;
|
|
635
|
+
activeTurnAttemptId = undefined;
|
|
636
|
+
activeNativeTurnId = undefined;
|
|
637
|
+
}
|
|
445
638
|
if (deliveryUnknown && !(error instanceof ProviderDeliveryUnknownError)) {
|
|
446
639
|
throw new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, request.turn.attemptId);
|
|
447
640
|
}
|
|
@@ -634,7 +827,9 @@ export async function waitForAgentHostLaunchAck(input) {
|
|
|
634
827
|
if (snapshot.state === "ready"
|
|
635
828
|
|| (input.requireTurnAck !== true && snapshot.state === "idle"))
|
|
636
829
|
return snapshot;
|
|
637
|
-
if (snapshot.state === "delivery-unknown"
|
|
830
|
+
if (snapshot.state === "delivery-unknown"
|
|
831
|
+
|| snapshot.state === "busy"
|
|
832
|
+
|| snapshot.state === "rejected") {
|
|
638
833
|
return snapshot;
|
|
639
834
|
}
|
|
640
835
|
if (snapshot.state === "failed" || snapshot.state === "exited") {
|
|
@@ -843,7 +1038,7 @@ function validateControlResult(result) {
|
|
|
843
1038
|
}
|
|
844
1039
|
function validateSnapshot(snapshot) {
|
|
845
1040
|
if (snapshot.schemaVersion !== 1
|
|
846
|
-
|| !["idle", "starting", "ready", "settling", "delivery-unknown", "rejected", "failed", "exited"]
|
|
1041
|
+
|| !["idle", "starting", "ready", "settling", "delivery-unknown", "busy", "rejected", "failed", "exited"]
|
|
847
1042
|
.includes(snapshot.state)) {
|
|
848
1043
|
throw new Error("Agent Host snapshot is invalid.");
|
|
849
1044
|
}
|
|
@@ -884,6 +1079,23 @@ function definedFields(value) {
|
|
|
884
1079
|
function errorText(error) {
|
|
885
1080
|
return error instanceof Error ? error.message : String(error ?? "unknown error");
|
|
886
1081
|
}
|
|
1082
|
+
function signalRoleMailbox(home, payload) {
|
|
1083
|
+
const taskId = payload.environment.YUI_TASK_ID;
|
|
1084
|
+
const roleName = payload.environment.YUI_ROLE;
|
|
1085
|
+
if (taskId === undefined || roleName === undefined)
|
|
1086
|
+
return;
|
|
1087
|
+
const key = `role:${encodeURIComponent(taskId)}/${encodeURIComponent(roleName)}`;
|
|
1088
|
+
void callController(home, "scheduler.signal", { key }).catch(() => {
|
|
1089
|
+
// This is a low-latency hint. Durable mailbox state and periodic
|
|
1090
|
+
// reconciliation remain the recovery path across a Controller handover.
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
async function delay(milliseconds) {
|
|
1094
|
+
await new Promise((resolvePromise) => {
|
|
1095
|
+
const timer = setTimeout(resolvePromise, milliseconds);
|
|
1096
|
+
timer.unref();
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
887
1099
|
function hostTurnControlParams(payload, nativeSessionId, authority, attemptId) {
|
|
888
1100
|
const environment = payload.environment;
|
|
889
1101
|
return Object.freeze({
|
|
@@ -118,6 +118,15 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
|
|
|
118
118
|
lineage: "partial",
|
|
119
119
|
detachedQuery: "partial",
|
|
120
120
|
resultRouting: "partial"
|
|
121
|
+
}),
|
|
122
|
+
observation: Object.freeze({
|
|
123
|
+
...STRUCTURED_CLI_CAPABILITIES.observation,
|
|
124
|
+
// Managed Codex uses the ordinary shared App Server event stream.
|
|
125
|
+
// Turn lifecycle is exact; Yui does not install per-thread Hooks merely
|
|
126
|
+
// to manufacture tool/wait/usage observations.
|
|
127
|
+
operations: Object.freeze([]),
|
|
128
|
+
waiting: Object.freeze([]),
|
|
129
|
+
usage: "unavailable"
|
|
121
130
|
})
|
|
122
131
|
}),
|
|
123
132
|
runtime: Object.freeze({
|
|
@@ -22,21 +22,17 @@ export class CodexAppServerRuntime {
|
|
|
22
22
|
async openConversation(input) {
|
|
23
23
|
const result = await this.transport.request("thread/start", {
|
|
24
24
|
cwd: text(input.cwd, "Codex thread cwd"),
|
|
25
|
-
...(input
|
|
26
|
-
...(input.approvalPolicy === undefined ? {} : { approvalPolicy: input.approvalPolicy }),
|
|
27
|
-
...(input.sandbox === undefined ? {} : { sandbox: input.sandbox }),
|
|
28
|
-
...(input.developerInstructions === undefined
|
|
29
|
-
? {}
|
|
30
|
-
: { developerInstructions: input.developerInstructions }),
|
|
31
|
-
...(input.runtimeWorkspaceRoots === undefined
|
|
32
|
-
? {}
|
|
33
|
-
: { runtimeWorkspaceRoots: [...input.runtimeWorkspaceRoots] })
|
|
25
|
+
...threadOptions(input)
|
|
34
26
|
});
|
|
35
27
|
return { conversationId: threadId(result) };
|
|
36
28
|
}
|
|
37
|
-
async resumeConversation(conversationId) {
|
|
29
|
+
async resumeConversation(conversationId, options = {}) {
|
|
38
30
|
const id = text(conversationId, "Codex thread id");
|
|
39
|
-
const result = await this.transport.request("thread/resume", {
|
|
31
|
+
const result = await this.transport.request("thread/resume", {
|
|
32
|
+
threadId: id,
|
|
33
|
+
...(options.cwd === undefined ? {} : { cwd: text(options.cwd, "Codex thread cwd") }),
|
|
34
|
+
...threadOptions(options)
|
|
35
|
+
});
|
|
40
36
|
return parseThreadSnapshot(result, id, true);
|
|
41
37
|
}
|
|
42
38
|
async setConversationName(input) {
|
|
@@ -85,7 +81,11 @@ export class CodexAppServerRuntime {
|
|
|
85
81
|
const snapshot = await this.readConversation(requestedThreadId);
|
|
86
82
|
threadId = snapshot.threadId;
|
|
87
83
|
if (input.expectedNoActiveTurn && snapshot.activeTurnId !== undefined) {
|
|
88
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
status: "busy",
|
|
86
|
+
activeTurnId: snapshot.activeTurnId,
|
|
87
|
+
reason: `active-turn:${snapshot.activeTurnId}`
|
|
88
|
+
};
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
catch (error) {
|
|
@@ -249,12 +249,25 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
249
249
|
const turns = arrayMember(thread, "turns").map(object).filter((entry) => (entry !== null));
|
|
250
250
|
const active = [...turns].reverse().find((turn) => (["inProgress", "in_progress", "running", "active"].includes(String(turn.status))));
|
|
251
251
|
const latestStatus = optionalTurnStatus(turns.at(-1)?.status);
|
|
252
|
+
const turnSnapshots = turns.flatMap((turn) => {
|
|
253
|
+
const turnId = optionalId(turn.id);
|
|
254
|
+
if (turnId === undefined)
|
|
255
|
+
return [];
|
|
256
|
+
const status = optionalTurnStatus(turn.status);
|
|
257
|
+
const error = providerError(turn.error);
|
|
258
|
+
return [{
|
|
259
|
+
turnId,
|
|
260
|
+
...(status === undefined ? {} : { status }),
|
|
261
|
+
...(error === undefined ? {} : { error })
|
|
262
|
+
}];
|
|
263
|
+
});
|
|
252
264
|
return {
|
|
253
265
|
threadId: id,
|
|
254
266
|
loaded,
|
|
255
267
|
status: threadStatus(thread.status),
|
|
256
268
|
...(optionalId(active?.id) === undefined ? {} : { activeTurnId: optionalId(active?.id) }),
|
|
257
269
|
...(latestStatus === undefined ? {} : { latestTurnStatus: latestStatus }),
|
|
270
|
+
turns: Object.freeze(turnSnapshots),
|
|
258
271
|
...(optionalId(thread.parentThreadId) === undefined
|
|
259
272
|
? {}
|
|
260
273
|
: { parentThreadId: optionalId(thread.parentThreadId) }),
|
|
@@ -262,6 +275,17 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
262
275
|
raw: result
|
|
263
276
|
};
|
|
264
277
|
}
|
|
278
|
+
function providerError(value) {
|
|
279
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
280
|
+
return value.trim();
|
|
281
|
+
const record = object(value);
|
|
282
|
+
if (record === null)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (typeof record.message === "string" && record.message.trim().length > 0) {
|
|
285
|
+
return record.message.trim();
|
|
286
|
+
}
|
|
287
|
+
return JSON.stringify(record);
|
|
288
|
+
}
|
|
265
289
|
function codexContinuationState(snapshot) {
|
|
266
290
|
if (snapshot.status === "active" || snapshot.latestTurnStatus === "inProgress") {
|
|
267
291
|
return {
|
|
@@ -317,12 +341,19 @@ function optionalTurnStatus(value) {
|
|
|
317
341
|
}
|
|
318
342
|
function classifyMutationError(error) {
|
|
319
343
|
if (error instanceof CodexAppServerRequestError) {
|
|
344
|
+
if (codexAppServerErrorIsBusy(error)) {
|
|
345
|
+
return { status: "busy", reason: error.message };
|
|
346
|
+
}
|
|
320
347
|
if (["INVALID_PARAMS", "NOT_FOUND", "TURN_NOT_ACTIVE", -32602].includes(error.code)) {
|
|
321
348
|
return { status: "not-accepted", reason: error.message };
|
|
322
349
|
}
|
|
323
350
|
}
|
|
324
351
|
return { status: "unknown", reason: error instanceof Error ? error.message : String(error) };
|
|
325
352
|
}
|
|
353
|
+
function codexAppServerErrorIsBusy(error) {
|
|
354
|
+
return /\b(active turn|turn (?:is )?(?:already )?(?:active|in progress|running)|already has an active)\b/iu
|
|
355
|
+
.test(error.message);
|
|
356
|
+
}
|
|
326
357
|
function isNotLoaded(error) {
|
|
327
358
|
return error instanceof CodexAppServerRequestError
|
|
328
359
|
&& (String(error.code).toLowerCase().includes("not_loaded")
|
|
@@ -366,3 +397,23 @@ function text(value, label) {
|
|
|
366
397
|
}
|
|
367
398
|
return value.trim();
|
|
368
399
|
}
|
|
400
|
+
function threadOptions(input) {
|
|
401
|
+
return {
|
|
402
|
+
...(input.model === undefined ? {} : { model: text(input.model, "Codex model") }),
|
|
403
|
+
...(input.approvalPolicy === undefined
|
|
404
|
+
? {}
|
|
405
|
+
: { approvalPolicy: text(input.approvalPolicy, "Codex approval policy") }),
|
|
406
|
+
...(input.sandbox === undefined ? {} : { sandbox: text(input.sandbox, "Codex sandbox") }),
|
|
407
|
+
...(input.developerInstructions === undefined
|
|
408
|
+
? {}
|
|
409
|
+
: {
|
|
410
|
+
developerInstructions: text(input.developerInstructions, "Codex developer instructions")
|
|
411
|
+
}),
|
|
412
|
+
...(input.runtimeWorkspaceRoots === undefined
|
|
413
|
+
? {}
|
|
414
|
+
: {
|
|
415
|
+
runtimeWorkspaceRoots: input.runtimeWorkspaceRoots.map((root) => (text(root, "Codex runtime workspace root")))
|
|
416
|
+
}),
|
|
417
|
+
...(input.config === undefined ? {} : { config: { ...input.config } })
|
|
418
|
+
};
|
|
419
|
+
}
|
|
@@ -86,10 +86,25 @@ function validateProviderControl(control) {
|
|
|
86
86
|
if (control.adapterId !== "codex" && control.adapterId !== "claude") {
|
|
87
87
|
throw new Error("Agent Host Provider control adapter is invalid.");
|
|
88
88
|
}
|
|
89
|
-
if ((control.adapterId === "codex" && control.transport !== "codex-app-server-
|
|
89
|
+
if ((control.adapterId === "codex" && control.transport !== "codex-app-server-proxy")
|
|
90
90
|
|| (control.adapterId === "claude" && control.transport !== "claude-stream-json")) {
|
|
91
91
|
throw new Error("Agent Host Provider control transport does not match its adapter.");
|
|
92
92
|
}
|
|
93
|
+
if ((control.adapterId === "codex") !== (control.codexThread !== undefined)) {
|
|
94
|
+
throw new Error("Agent Host Provider thread settings do not match its adapter.");
|
|
95
|
+
}
|
|
96
|
+
if ((control.adapterId === "codex") !== (control.codexDaemonStartArgs !== undefined)) {
|
|
97
|
+
throw new Error("Agent Host Provider daemon bootstrap does not match its adapter.");
|
|
98
|
+
}
|
|
99
|
+
if (control.codexThread !== undefined)
|
|
100
|
+
validateCodexThreadOptions(control.codexThread);
|
|
101
|
+
if (control.codexDaemonStartArgs !== undefined) {
|
|
102
|
+
if (!Array.isArray(control.codexDaemonStartArgs)
|
|
103
|
+
|| control.codexDaemonStartArgs.length === 0) {
|
|
104
|
+
throw new Error("Agent Host Codex daemon bootstrap args are invalid.");
|
|
105
|
+
}
|
|
106
|
+
control.codexDaemonStartArgs.forEach((value) => text(value, "Codex daemon argument"));
|
|
107
|
+
}
|
|
93
108
|
if (control.mode !== "new" && control.mode !== "resume") {
|
|
94
109
|
throw new Error("Agent Host Provider control mode is invalid.");
|
|
95
110
|
}
|
|
@@ -105,6 +120,16 @@ function validateProviderControl(control) {
|
|
|
105
120
|
if (control.kind === "ensure" && control.initialTurn !== undefined) {
|
|
106
121
|
throw new Error("Managed Provider ensure launch cannot carry a new Turn.");
|
|
107
122
|
}
|
|
123
|
+
if (control.kind !== "ensure" && "ownedTurn" in control) {
|
|
124
|
+
throw new Error("Only a managed Provider ensure launch can recover an owned Turn.");
|
|
125
|
+
}
|
|
126
|
+
if (control.kind === "ensure" && control.ownedTurn !== undefined) {
|
|
127
|
+
if (control.adapterId !== "codex") {
|
|
128
|
+
throw new Error("Only Managed Codex can recover an owned Turn across client attachment.");
|
|
129
|
+
}
|
|
130
|
+
text(control.ownedTurn.attemptId, "owned Provider input attemptId");
|
|
131
|
+
text(control.ownedTurn.turnId, "owned Provider Turn id");
|
|
132
|
+
}
|
|
108
133
|
const requiresNativeSessionId = control.mode === "resume" || control.adapterId === "claude";
|
|
109
134
|
if (requiresNativeSessionId !== (control.nativeSessionId !== undefined)) {
|
|
110
135
|
throw new Error("Agent Host Provider resume identity is inconsistent.");
|
|
@@ -129,6 +154,31 @@ function validateProviderControl(control) {
|
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
}
|
|
157
|
+
function validateCodexThreadOptions(options) {
|
|
158
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
159
|
+
throw new Error("Agent Host Codex thread settings are invalid.");
|
|
160
|
+
}
|
|
161
|
+
for (const [value, label] of [
|
|
162
|
+
[options.model, "model"],
|
|
163
|
+
[options.approvalPolicy, "approval policy"],
|
|
164
|
+
[options.sandbox, "sandbox"],
|
|
165
|
+
[options.developerInstructions, "developer instructions"]
|
|
166
|
+
]) {
|
|
167
|
+
if (value !== undefined)
|
|
168
|
+
text(value, `Codex thread ${label}`);
|
|
169
|
+
}
|
|
170
|
+
if (options.runtimeWorkspaceRoots !== undefined) {
|
|
171
|
+
if (!Array.isArray(options.runtimeWorkspaceRoots)) {
|
|
172
|
+
throw new Error("Agent Host Codex runtime workspace roots are invalid.");
|
|
173
|
+
}
|
|
174
|
+
options.runtimeWorkspaceRoots.forEach((root) => text(root, "Codex runtime workspace root"));
|
|
175
|
+
}
|
|
176
|
+
if (options.config !== undefined
|
|
177
|
+
&& (options.config === null || typeof options.config !== "object"
|
|
178
|
+
|| Array.isArray(options.config))) {
|
|
179
|
+
throw new Error("Agent Host Codex thread config is invalid.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
132
182
|
function text(value, label) {
|
|
133
183
|
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
134
184
|
throw new Error(`Agent Host ${label} is invalid.`);
|
|
@@ -14,7 +14,15 @@ export function createRuntimeBinding(input) {
|
|
|
14
14
|
const initialTurnRejectedRunId = input.initialTurnRejectedRunId === undefined
|
|
15
15
|
? undefined
|
|
16
16
|
: requireSafeIdentity(input.initialTurnRejectedRunId, "Rejected initial Turn Run id");
|
|
17
|
-
|
|
17
|
+
const initialTurnBusyRunId = input.initialTurnBusyRunId === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: requireSafeIdentity(input.initialTurnBusyRunId, "Busy initial Turn Run id");
|
|
20
|
+
if ([
|
|
21
|
+
initialTurnRunId,
|
|
22
|
+
initialTurnDeliveryUnknownRunId,
|
|
23
|
+
initialTurnBusyRunId,
|
|
24
|
+
initialTurnRejectedRunId
|
|
25
|
+
]
|
|
18
26
|
.filter((value) => value !== undefined).length > 1) {
|
|
19
27
|
throw new TypeError("Runtime binding must report at most one initial Turn outcome.");
|
|
20
28
|
}
|
|
@@ -30,6 +38,7 @@ export function createRuntimeBinding(input) {
|
|
|
30
38
|
...(initialTurnDeliveryUnknownRunId === undefined
|
|
31
39
|
? {}
|
|
32
40
|
: { initialTurnDeliveryUnknownRunId }),
|
|
41
|
+
...(initialTurnBusyRunId === undefined ? {} : { initialTurnBusyRunId }),
|
|
33
42
|
...(initialTurnRejectedRunId === undefined ? {} : { initialTurnRejectedRunId }),
|
|
34
43
|
...(input.nativeSessionId === undefined
|
|
35
44
|
? {}
|