@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.
@@ -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, owner.scope !== "task" || input.runId === undefined
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 use the structured Driver Hook as their sole lifecycle
428
- // authority, avoiding two terminal channels for the same Turn.
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 runs use App Server lifecycle hooks and structured Turn
433
- // submission. No Run prompt is placed in argv or written as terminal input.
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: scopedLaunch,
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
- ? body
714
- : prefixYuiTitleInput(body, title);
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 addCodexManagedLifecycleHooks(args, cliPath) {
920
- if (args.length < 2 || args.at(-2) !== "app-server" || args.at(-1) !== "--stdio") {
921
- throw new Error("Managed Codex App Server launch shape is invalid.");
922
- }
923
- return [
924
- ...args.slice(0, -2),
925
- "--enable", "hooks",
926
- "--config", codexLifecycleHooksConfig(cliPath),
927
- "--dangerously-bypass-hook-trust",
928
- ...args.slice(-2)
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
- updateSnapshot(hostSnapshot(receipt === undefined ? "idle" : "ready", {
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 === undefined ? {} : {
328
- attemptId: receipt.attemptId,
329
- nativeTurnId: receipt.nativeTurnId
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 ProviderTurnRejectedError ? "rejected" : "failed";
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 ProviderTurnRejectedError ? "rejected" : "failed";
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" || snapshot.state === "rejected") {
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({