@zq-silk/yui 0.13.2 → 0.13.4
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 +12 -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
|
@@ -292,7 +292,7 @@ export class FileRoleLaunchPlanner {
|
|
|
292
292
|
const adapter = resolveAgentAdapter(binding.adapterId);
|
|
293
293
|
const effectiveWorkspace = effective.workspace.root;
|
|
294
294
|
const agentWorkspace = nativeAgentWorkspace(effective.workspace);
|
|
295
|
-
if (adapter.id === "codex") {
|
|
295
|
+
if (adapter.id === "codex" && (owner.scope !== "task" || input.runId === undefined)) {
|
|
296
296
|
const codexConfig = inspectCodexLaunchConfig({
|
|
297
297
|
environment: launchEnvironment,
|
|
298
298
|
workspace: agentWorkspace,
|
|
@@ -301,9 +301,7 @@ export class FileRoleLaunchPlanner {
|
|
|
301
301
|
: undefined,
|
|
302
302
|
trustWorkspace: true
|
|
303
303
|
});
|
|
304
|
-
assertCodexLaunchOverridesAvailable(codexConfig,
|
|
305
|
-
? ["developerInstructions", "notify"]
|
|
306
|
-
: ["developerInstructions"]);
|
|
304
|
+
assertCodexLaunchOverridesAvailable(codexConfig, ["developerInstructions", "notify"]);
|
|
307
305
|
}
|
|
308
306
|
const runtimeIsolation = input.runtimeIsolation === undefined
|
|
309
307
|
? undefined
|
|
@@ -424,20 +422,17 @@ export class FileRoleLaunchPlanner {
|
|
|
424
422
|
}
|
|
425
423
|
if (binding.adapterId === "codex") {
|
|
426
424
|
// Global/interactive Codex sessions still use notify for presentation.
|
|
427
|
-
// Managed Runs
|
|
428
|
-
//
|
|
425
|
+
// Managed Runs receive lifecycle facts through their ordinary App Server
|
|
426
|
+
// subscription, avoiding a second Hook channel for the same Turn.
|
|
429
427
|
if (owner.scope !== "task" || input.runId === undefined) {
|
|
430
428
|
args = addCodexSessionNotify(args, launchMode, this.#cliPath);
|
|
431
429
|
}
|
|
432
|
-
// Managed Codex
|
|
433
|
-
//
|
|
430
|
+
// Managed Codex Runs are ordinary shared-daemon threads. Their Session
|
|
431
|
+
// Manifest points at the Yui Skills; no Yui Hook config is installed.
|
|
434
432
|
if (owner.scope === "task" && input.runId !== undefined) {
|
|
435
433
|
if (managedRun === null || managedRun.status !== "active") {
|
|
436
434
|
throw new Error(`Managed Codex Run is no longer active: ${input.runId}.`);
|
|
437
435
|
}
|
|
438
|
-
args = managedControl
|
|
439
|
-
? addCodexManagedLifecycleHooks(args, this.#cliPath)
|
|
440
|
-
: addCodexLifecycleHooks(args, launchMode, this.#cliPath);
|
|
441
436
|
}
|
|
442
437
|
session = launchMode === "resume"
|
|
443
438
|
? readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective)
|
|
@@ -502,13 +497,16 @@ export class FileRoleLaunchPlanner {
|
|
|
502
497
|
const providerAuthority = managedControl
|
|
503
498
|
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId, input.mode)
|
|
504
499
|
: undefined;
|
|
500
|
+
const providerOwnedTurn = managedControl && !carriesInitialTurn
|
|
501
|
+
? this.#providerOwnedTurnForLaunch(owner.taskId, role.name, input.runId)
|
|
502
|
+
: undefined;
|
|
505
503
|
const providerNativeSessionId = binding.adapterId === "claude"
|
|
506
504
|
? preallocatedNativeSessionId
|
|
507
505
|
: resumeNativeSessionId;
|
|
508
506
|
const initialTurn = carriesInitialTurn
|
|
509
507
|
? {
|
|
510
508
|
attemptId: agentRunDeliveryReceiptId(managedRun),
|
|
511
|
-
boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
|
|
509
|
+
boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionContext.sessionManifestPath, sessionTitle)
|
|
512
510
|
}
|
|
513
511
|
: undefined;
|
|
514
512
|
const providerControl = !managedControl
|
|
@@ -522,6 +520,13 @@ export class FileRoleLaunchPlanner {
|
|
|
522
520
|
mode: "resume",
|
|
523
521
|
nativeSessionId: requireText(providerNativeSessionId, "Managed Provider ensure native session id"),
|
|
524
522
|
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
523
|
+
...(managedCompiled.codexThread === undefined
|
|
524
|
+
? {}
|
|
525
|
+
: { codexThread: managedCompiled.codexThread }),
|
|
526
|
+
...(managedCompiled.codexDaemonStartArgs === undefined
|
|
527
|
+
? {}
|
|
528
|
+
: { codexDaemonStartArgs: managedCompiled.codexDaemonStartArgs }),
|
|
529
|
+
...(providerOwnedTurn === undefined ? {} : { ownedTurn: providerOwnedTurn }),
|
|
525
530
|
authority: providerAuthority
|
|
526
531
|
}
|
|
527
532
|
: resumeNativeSessionId === undefined
|
|
@@ -535,6 +540,12 @@ export class FileRoleLaunchPlanner {
|
|
|
535
540
|
? {}
|
|
536
541
|
: { nativeSessionId: providerNativeSessionId }),
|
|
537
542
|
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
543
|
+
...(managedCompiled.codexThread === undefined
|
|
544
|
+
? {}
|
|
545
|
+
: { codexThread: managedCompiled.codexThread }),
|
|
546
|
+
...(managedCompiled.codexDaemonStartArgs === undefined
|
|
547
|
+
? {}
|
|
548
|
+
: { codexDaemonStartArgs: managedCompiled.codexDaemonStartArgs }),
|
|
538
549
|
authority: providerAuthority,
|
|
539
550
|
initialTurn
|
|
540
551
|
}
|
|
@@ -546,6 +557,12 @@ export class FileRoleLaunchPlanner {
|
|
|
546
557
|
mode: "resume",
|
|
547
558
|
nativeSessionId: requireText(providerNativeSessionId, "Managed Provider resume native session id"),
|
|
548
559
|
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
560
|
+
...(managedCompiled.codexThread === undefined
|
|
561
|
+
? {}
|
|
562
|
+
: { codexThread: managedCompiled.codexThread }),
|
|
563
|
+
...(managedCompiled.codexDaemonStartArgs === undefined
|
|
564
|
+
? {}
|
|
565
|
+
: { codexDaemonStartArgs: managedCompiled.codexDaemonStartArgs }),
|
|
549
566
|
authority: providerAuthority,
|
|
550
567
|
initialTurn
|
|
551
568
|
};
|
|
@@ -601,6 +618,7 @@ export class FileRoleLaunchPlanner {
|
|
|
601
618
|
const scopedLaunch = owner.scope === "task"
|
|
602
619
|
? this.#applyWorkspaceScope(owner.taskId, role, launch, workspaceOverride)
|
|
603
620
|
: launch;
|
|
621
|
+
const ordinaryConversationLaunch = withCodexThreadEnvironment(scopedLaunch);
|
|
604
622
|
return {
|
|
605
623
|
role: {
|
|
606
624
|
name: role.name,
|
|
@@ -608,7 +626,7 @@ export class FileRoleLaunchPlanner {
|
|
|
608
626
|
...(agentWorkspace === effectiveWorkspace ? {} : { cwd: agentWorkspace }),
|
|
609
627
|
...(owner.scope === "task" ? { status: role.status } : {})
|
|
610
628
|
},
|
|
611
|
-
launch:
|
|
629
|
+
launch: ordinaryConversationLaunch,
|
|
612
630
|
session,
|
|
613
631
|
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
614
632
|
...(carriesInitialTurn && input.runId !== undefined
|
|
@@ -654,6 +672,18 @@ export class FileRoleLaunchPlanner {
|
|
|
654
672
|
}
|
|
655
673
|
throw new Error(`Provider writer authority is unknown: ${taskId}/${roleName}.`);
|
|
656
674
|
}
|
|
675
|
+
#providerOwnedTurnForLaunch(taskId, roleName, runId) {
|
|
676
|
+
const binding = this.store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding;
|
|
677
|
+
if (binding === null || binding === undefined || binding.runId !== runId)
|
|
678
|
+
return undefined;
|
|
679
|
+
const turn = binding.turn;
|
|
680
|
+
if (turn === null || !["accepted", "running"].includes(turn.status))
|
|
681
|
+
return undefined;
|
|
682
|
+
if (turn.turnId === undefined) {
|
|
683
|
+
throw new Error(`Active Provider Turn has no native identity: ${taskId}/${roleName}.`);
|
|
684
|
+
}
|
|
685
|
+
return { attemptId: turn.attemptId, turnId: turn.turnId };
|
|
686
|
+
}
|
|
657
687
|
#applyWorkspaceScope(taskId, role, launch, workspaceOverride) {
|
|
658
688
|
const workspace = workspaceOverride
|
|
659
689
|
?? (role.name === "leader"
|
|
@@ -691,7 +721,7 @@ export class FileRoleLaunchPlanner {
|
|
|
691
721
|
return selectEnvironment(source, names);
|
|
692
722
|
}
|
|
693
723
|
}
|
|
694
|
-
function managedRunLaunchEnvelope(run, mode, title) {
|
|
724
|
+
function managedRunLaunchEnvelope(run, mode, sessionManifestPath, title) {
|
|
695
725
|
const body = run.providerRetry?.state === "dispatching"
|
|
696
726
|
? serializeProviderRetryEnvelope({
|
|
697
727
|
taskId: run.taskId,
|
|
@@ -709,9 +739,14 @@ function managedRunLaunchEnvelope(run, mode, title) {
|
|
|
709
739
|
: mode === "resume" && run.pushedAt !== undefined
|
|
710
740
|
? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
|
|
711
741
|
: serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
|
|
742
|
+
const guided = [
|
|
743
|
+
`Yui Session Manifest: ${sessionManifestPath}`,
|
|
744
|
+
"Read the manifest and its matching Role Skill before using the yui CLI for this Task.",
|
|
745
|
+
body
|
|
746
|
+
].join("\n");
|
|
712
747
|
return title === undefined
|
|
713
|
-
?
|
|
714
|
-
: prefixYuiTitleInput(
|
|
748
|
+
? guided
|
|
749
|
+
: prefixYuiTitleInput(guided, title);
|
|
715
750
|
}
|
|
716
751
|
export function nativeAgentWorkspace(workspace) {
|
|
717
752
|
return workspace.entries.length === 1
|
|
@@ -916,17 +951,26 @@ function addCodexLifecycleHooks(args, mode, cliPath) {
|
|
|
916
951
|
}
|
|
917
952
|
return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
|
|
918
953
|
}
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
954
|
+
function withCodexThreadEnvironment(launch) {
|
|
955
|
+
const control = launch.providerControl;
|
|
956
|
+
if (control?.adapterId !== "codex" || control.codexThread === undefined)
|
|
957
|
+
return launch;
|
|
958
|
+
const set = Object.fromEntries(Object.entries(launch.env).filter(([key]) => (key.startsWith("YUI_")
|
|
959
|
+
|| ["TMPDIR", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR"]
|
|
960
|
+
.includes(key))));
|
|
961
|
+
return {
|
|
962
|
+
...launch,
|
|
963
|
+
providerControl: {
|
|
964
|
+
...control,
|
|
965
|
+
codexThread: {
|
|
966
|
+
...control.codexThread,
|
|
967
|
+
config: {
|
|
968
|
+
...(control.codexThread.config ?? {}),
|
|
969
|
+
shell_environment_policy: { set }
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
};
|
|
930
974
|
}
|
|
931
975
|
function readySession(agentId, adapterId, nativeSessionId, effective) {
|
|
932
976
|
return {
|
|
@@ -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({
|