@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.
@@ -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({
@@ -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.model === undefined ? {} : { model: input.model }),
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", { threadId: id });
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 { status: "not-accepted", reason: `active-turn:${snapshot.activeTurnId}` };
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-stdio")
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
- if ([initialTurnRunId, initialTurnDeliveryUnknownRunId, initialTurnRejectedRunId]
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
  ? {}