@zq-silk/yui 0.13.4 → 0.13.6

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.
Files changed (54) hide show
  1. package/ARCHITECTURE.md +13 -13
  2. package/README.md +19 -20
  3. package/dist/cli/commandCatalog.js +22 -21
  4. package/dist/cli/interactionPolicy.js +0 -14
  5. package/dist/cli.js +42 -0
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/taskCommands.js +60 -326
  8. package/dist/commands/taskContextCommand.js +3 -14
  9. package/dist/commands/taskExecutionCommands.js +254 -0
  10. package/dist/commands/taskNextActionCommand.js +1 -3
  11. package/dist/commands/taskOverviewCommand.js +9 -2
  12. package/dist/commands/taskRoleRuntimeStatus.js +2 -25
  13. package/dist/controller/agentRuntimeObserver.js +4 -2
  14. package/dist/controller/clientRuntime.js +45 -2
  15. package/dist/controller/controller.js +6 -3
  16. package/dist/controller/fileSchedulerStoreAdapter.js +59 -188
  17. package/dist/controller/jobControl.js +3 -2
  18. package/dist/controller/runtime.js +6 -33
  19. package/dist/controller/runtimeEventProcessor.js +8 -4
  20. package/dist/controller/runtimeHookRunFence.js +4 -10
  21. package/dist/execution/executionHealth.js +8 -16
  22. package/dist/executor/agentAdapter.js +3 -8
  23. package/dist/executor/agentExecutor.js +13 -14
  24. package/dist/executor/fileRoleLaunchPlanner.js +10 -26
  25. package/dist/lifecycle/exactRunTerminalization.js +24 -322
  26. package/dist/repository/taskWorkspaceCoordinator.js +0 -9
  27. package/dist/runtime/agentHost.js +22 -83
  28. package/dist/runtime/builtinAgentDrivers.js +1 -1
  29. package/dist/runtime/exactControlPlane.js +15 -9
  30. package/dist/runtime/launchBroker.js +1 -11
  31. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  32. package/dist/runtime/providerRecoveryDecision.js +1 -1
  33. package/dist/runtime/providerRuntimeIdentity.js +25 -15
  34. package/dist/runtime/structuredProviderHost.js +0 -57
  35. package/dist/scheduler/activeRoleRunDelivery.js +1 -19
  36. package/dist/scheduler/leaderWakeupProcessor.js +12 -63
  37. package/dist/scheduler/ports.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +4 -1
  39. package/dist/scheduler/roleRunStall.js +0 -2
  40. package/dist/scheduler/taskExecutionProjection.js +18 -1
  41. package/dist/scheduler/wakeupQueue.js +2 -1
  42. package/dist/storage/migration/productionRegistry.js +65 -0
  43. package/dist/storage/sqliteStore.js +10 -2
  44. package/dist/storage/taskStore.js +11 -3
  45. package/dist/task/completionReadiness.js +0 -67
  46. package/dist/task/nextAction.js +16 -32
  47. package/dist/task/task.js +38 -3
  48. package/dist/web/assets/client/i18n.js +0 -4
  49. package/dist/web/assets/client/view.js +0 -18
  50. package/dist/web/webSnapshot.js +7 -13
  51. package/i18n/README.zh-CN.md +4 -4
  52. package/package.json +1 -1
  53. package/dist/run/recoveryProjection.js +0 -252
  54. package/dist/runtime/conversationSwitch.js +0 -277
@@ -1,7 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { retireConfirmedAbsentInactiveTaskRolePlaceholders } from "../executor/agentExecutor.js";
3
3
  import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
- import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
5
4
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
6
5
  import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
7
6
  import { WorkspaceCleanupBlockedError } from "./taskWorkspacePreparer.js";
@@ -159,7 +158,6 @@ export class TaskWorkspaceCoordinator {
159
158
  if (task.status !== "completed" && task.status !== "retired") {
160
159
  throw new Error(`Task must be completed or retired before archive cleanup: ${task.id}.`);
161
160
  }
162
- this.#assertNoProviderContinuationWriters(task.id);
163
161
  const managedWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
164
162
  .sort((left, right) => managedWorkspaceKey(left.owner)
165
163
  .localeCompare(managedWorkspaceKey(right.owner)));
@@ -326,13 +324,6 @@ export class TaskWorkspaceCoordinator {
326
324
  if (current === null || !isDeepStrictEqual(current, expected)) {
327
325
  throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
328
326
  }
329
- this.#assertNoProviderContinuationWriters(expected.id);
330
- }
331
- #assertNoProviderContinuationWriters(taskId) {
332
- const blockers = blockingProviderContinuations(this.store.listEvents(taskId));
333
- if (blockers.length > 0) {
334
- throw new WorkspaceCleanupBlockedError("active-run", `task:${taskId}`, true, `Task has Provider continuations that may still write its Workspace: ${taskId}.`);
335
- }
336
327
  }
337
328
  #assertWorkItemRuntimeQuiescent(item) {
338
329
  const activeRun = this.store.listAgentRuns(item.taskId)
@@ -14,7 +14,7 @@ import { ProviderDeliveryUnknownError, ProviderConversationMissingError, Provide
14
14
  import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
15
15
  import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
16
16
  import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
17
- import { readRuntimeStopReceipt, removeRuntimeStopReceipt, writeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
17
+ import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
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;
@@ -34,7 +34,6 @@ export async function runAgentHost(input) {
34
34
  let activeNativeTurnId;
35
35
  let codexClientAttachedAt;
36
36
  let consecutiveCodexDisconnects = 0;
37
- const switchDetachedSessions = new WeakSet();
38
37
  let activationId;
39
38
  let conversationRecoverability = "unknown";
40
39
  let authority;
@@ -146,7 +145,7 @@ export async function runAgentHost(input) {
146
145
  providerControl: {
147
146
  schemaVersion: 1,
148
147
  adapterId: "codex",
149
- transport: "codex-app-server-proxy",
148
+ transport: "codex-app-server",
150
149
  kind: "ensure",
151
150
  mode: "resume",
152
151
  nativeSessionId: disconnectedSession.nativeSessionId,
@@ -154,7 +153,6 @@ export async function runAgentHost(input) {
154
153
  ? {}
155
154
  : { sessionTitle: previousControl.sessionTitle }),
156
155
  codexThread: previousControl.codexThread,
157
- codexDaemonStartArgs: previousControl.codexDaemonStartArgs,
158
156
  ...(ownedTurn === undefined ? {} : { ownedTurn }),
159
157
  authority
160
158
  }
@@ -228,8 +226,7 @@ export async function runAgentHost(input) {
228
226
  const reconnectableCodexClient = ownsCurrentSession
229
227
  && providerSession.adapterId === "codex"
230
228
  && !hostStopRequested
231
- && stopReceipt === null
232
- && !switchDetachedSessions.has(providerSession);
229
+ && stopReceipt === null;
233
230
  if (reconnectableCodexClient) {
234
231
  session = undefined;
235
232
  if (codexClientAttachedAt !== undefined
@@ -260,7 +257,7 @@ export async function runAgentHost(input) {
260
257
  ...(activeTurnAttemptId === undefined ? {} : { attemptId: activeTurnAttemptId }),
261
258
  ...(activeNativeTurnId === undefined ? {} : { nativeTurnId: activeNativeTurnId }),
262
259
  ...exitAuthority,
263
- detail: "Codex client disconnected; reattaching to the shared daemon."
260
+ detail: "Codex App Server disconnected; starting a replacement process."
264
261
  }));
265
262
  await reconnectCodexClient(providerSession, currentPayload);
266
263
  return;
@@ -274,21 +271,19 @@ export async function runAgentHost(input) {
274
271
  hostSequence += 1;
275
272
  const observedAt = new Date().toISOString();
276
273
  const failures = [];
277
- if (!switchDetachedSessions.has(providerSession)) {
278
- try {
279
- await publishStructuredProviderActivationTerminal({
280
- home: input.home,
281
- environment: currentPayload.environment,
282
- conversationId: providerSession.conversationId,
283
- nativeSessionId: providerSession.nativeSessionId,
284
- activationId: currentActivationId,
285
- status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
286
- observedAt
287
- });
288
- }
289
- catch (error) {
290
- failures.push(`activation terminal: ${errorText(error)}`);
291
- }
274
+ try {
275
+ await publishStructuredProviderActivationTerminal({
276
+ home: input.home,
277
+ environment: currentPayload.environment,
278
+ conversationId: providerSession.conversationId,
279
+ nativeSessionId: providerSession.nativeSessionId,
280
+ activationId: currentActivationId,
281
+ status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
282
+ observedAt
283
+ });
284
+ }
285
+ catch (error) {
286
+ failures.push(`activation terminal: ${errorText(error)}`);
292
287
  }
293
288
  let exitPersisted = false;
294
289
  try {
@@ -357,10 +352,12 @@ export async function runAgentHost(input) {
357
352
  }
358
353
  const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
359
354
  const replacesCurrentConversation = session !== undefined && providerControl.kind === "new";
360
- if (!replacesCurrentConversation && authority === undefined)
355
+ if (replacesCurrentConversation) {
356
+ throw new Error("Agent Host cannot replace a live Provider Session; stop it before starting a fresh Session.");
357
+ }
358
+ if (authority === undefined)
361
359
  authority = requestedAuthority;
362
- else if (!replacesCurrentConversation
363
- && authority !== undefined
360
+ else if (authority !== undefined
364
361
  && !sameProviderAuthorityFence(authority, requestedAuthority)) {
365
362
  throw new Error("Agent Host launch carries a stale Provider authority fence.");
366
363
  }
@@ -380,43 +377,6 @@ export async function runAgentHost(input) {
380
377
  let providerAcceptedAttemptId;
381
378
  let durableInitialTurn;
382
379
  try {
383
- if (replacesCurrentConversation) {
384
- const previousSession = session;
385
- const previousPayload = sessionPayload;
386
- const previousActivationId = activationId ?? previousPayload?.launchId;
387
- if (previousPayload === undefined || previousActivationId === undefined
388
- || authority?.owner !== "controller") {
389
- throw new Error("Agent Host cannot detach an inexact Provider Activation.");
390
- }
391
- await detachDurableProviderForConversationSwitch(input.home, {
392
- taskId: requiredEnvironment(next.environment.YUI_TASK_ID, "Task id"),
393
- roleName: requiredEnvironment(next.environment.YUI_ROLE, "Role name"),
394
- runId: requiredEnvironment(next.environment.YUI_RUN_ID, "Run id"),
395
- agentId: requiredEnvironment(next.environment.YUI_AGENT_ID, "Agent id"),
396
- launchId: next.launchId,
397
- previousConversationId: previousSession.conversationId,
398
- previousNativeSessionId: previousSession.nativeSessionId,
399
- previousActivationId,
400
- nextAuthorityEpoch: requestedAuthority.epoch,
401
- nextAuthorityHolderId: requestedAuthority.holderId,
402
- observedAt: new Date().toISOString()
403
- });
404
- switchDetachedSessions.add(previousSession);
405
- // The persistent Provider child belongs to the Activation that first
406
- // created it, not to the most recent resume Run payload. The exit
407
- // observer carries that Activation launch id, so the stop receipt must
408
- // use the same identity or the expected switch detach is misclassified
409
- // as an abnormal child exit and generic cleanup can kill the Host.
410
- writeRuntimeStopReceipt(input.home, previousActivationId, new Date());
411
- authority = undefined;
412
- await terminateProviderSessionForConversationSwitch(previousSession);
413
- if (session === previousSession)
414
- session = undefined;
415
- sessionPayload = undefined;
416
- activationId = undefined;
417
- conversationRecoverability = "unknown";
418
- authority = requestedAuthority;
419
- }
420
380
  if (session !== undefined) {
421
381
  if (session.adapterId !== providerControl.adapterId
422
382
  || providerControl.mode !== "resume"
@@ -1123,9 +1083,6 @@ async function beginDurableProviderTurn(home, durableTurn) {
1123
1083
  throw error;
1124
1084
  }
1125
1085
  }
1126
- async function detachDurableProviderForConversationSwitch(home, request) {
1127
- await callControllerIdempotently(home, "runtime.conversation-switch-detach", request);
1128
- }
1129
1086
  async function callControllerIdempotently(home, method, request) {
1130
1087
  try {
1131
1088
  await callAgentController(home, method, request);
@@ -1152,24 +1109,6 @@ async function callControllerIdempotently(home, method, request) {
1152
1109
  class ControllerAcknowledgementUnknownError extends Error {
1153
1110
  name = "ControllerAcknowledgementUnknownError";
1154
1111
  }
1155
- async function terminateProviderSessionForConversationSwitch(providerSession) {
1156
- providerSession.terminate("SIGTERM");
1157
- const forceKill = setTimeout(() => providerSession.terminate("SIGKILL"), 3_000);
1158
- forceKill.unref();
1159
- let hardTimeout;
1160
- const timeout = new Promise((_resolve, reject) => {
1161
- hardTimeout = setTimeout(() => reject(new Error("Old Provider Activation did not exit after its switch authority was revoked.")), 8_000);
1162
- hardTimeout.unref();
1163
- });
1164
- try {
1165
- await Promise.race([providerSession.waitForExit(), timeout]);
1166
- }
1167
- finally {
1168
- clearTimeout(forceKill);
1169
- if (hardTimeout !== undefined)
1170
- clearTimeout(hardTimeout);
1171
- }
1172
- }
1173
1112
  async function resolveProviderTurnSubmission(home, durableTurn, error) {
1174
1113
  const attemptId = durableTurn.attemptId;
1175
1114
  if (typeof attemptId !== "string") {
@@ -121,7 +121,7 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
121
121
  }),
122
122
  observation: Object.freeze({
123
123
  ...STRUCTURED_CLI_CAPABILITIES.observation,
124
- // Managed Codex uses the ordinary shared App Server event stream.
124
+ // Managed Codex uses its Yui-owned App Server event stream.
125
125
  // Turn lifecycle is exact; Yui does not install per-thread Hooks merely
126
126
  // to manufacture tool/wait/usage observations.
127
127
  operations: Object.freeze([]),
@@ -268,7 +268,9 @@ export function assertExactTaskRuntimeEnvironment(runtimeSource, environment, ex
268
268
  /** Fences a descriptor to the one currently active durable Task runtime. */
269
269
  export function assertExactTaskRuntimeState(runtime, store, options = {}) {
270
270
  const task = store.getTask(runtime.taskId);
271
- if (task === null || task.status !== "active") {
271
+ if (task === null
272
+ || task.status !== "active"
273
+ || task.executionGate.state !== "enabled") {
272
274
  throw new Error("Exact Task runtime Task is not current and active.");
273
275
  }
274
276
  const role = store.getRole(runtime.taskId, runtime.roleName);
@@ -304,17 +306,21 @@ export function assertExactTaskRuntimeState(runtime, store, options = {}) {
304
306
  && session?.launchId === runtime.launchId;
305
307
  const executionRef = lifecycleMailbox?.processing?.executionRef;
306
308
  const preallocated = options.preallocatedDriverSessionReservation;
307
- const exactPreallocatedReservation = preallocated !== undefined
308
- && runtime.adapterId === preallocated.adapterId
309
- && runtime.runId !== undefined
309
+ const exactRunLaunchReservation = runtime.runId !== undefined
310
310
  && runtime.launchId !== undefined
311
- && runtime.nativeSessionId !== undefined
312
- && session === undefined
313
311
  && reservation
314
312
  && !hasRuntimeCleanupObligation(lifecycleMailbox)
315
313
  && executionRef?.type === "run"
316
314
  && executionRef.taskId === runtime.taskId
317
- && executionRef.id === runtime.runId
315
+ && executionRef.id === runtime.runId;
316
+ const terminalSessionReplacementReservation = exactRunLaunchReservation
317
+ && session !== undefined
318
+ && (session.status === "stopped" || session.status === "broken");
319
+ const exactPreallocatedReservation = preallocated !== undefined
320
+ && runtime.adapterId === preallocated.adapterId
321
+ && exactRunLaunchReservation
322
+ && runtime.nativeSessionId !== undefined
323
+ && (session === undefined || terminalSessionReplacementReservation)
318
324
  && runtime.nativeSessionId === nativeSessionIdForLaunch(preallocated.yuiHome, runtime.launchId, runtime.agentId, runtime.adapterId);
319
325
  const preallocatedBeforeInFlightProjection = exactPreallocatedReservation
320
326
  && (sessions?.inFlight === null || sessions?.inFlight === undefined);
@@ -327,11 +333,11 @@ export function assertExactTaskRuntimeState(runtime, store, options = {}) {
327
333
  throw new Error("Exact Task runtime launch fence is not current.");
328
334
  }
329
335
  if (runtime.nativeSessionId === undefined) {
330
- if (session !== undefined) {
336
+ if (session !== undefined && !terminalSessionReplacementReservation) {
331
337
  throw new Error("Exact Task runtime native Session fence is missing.");
332
338
  }
333
339
  }
334
- else if (session === undefined) {
340
+ else if (session === undefined || terminalSessionReplacementReservation) {
335
341
  if (!exactPreallocatedReservation) {
336
342
  throw new Error("Exact Task runtime native Session fence is not current.");
337
343
  }
@@ -86,25 +86,15 @@ 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-proxy")
89
+ if ((control.adapterId === "codex" && control.transport !== "codex-app-server")
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
93
  if ((control.adapterId === "codex") !== (control.codexThread !== undefined)) {
94
94
  throw new Error("Agent Host Provider thread settings do not match its adapter.");
95
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
96
  if (control.codexThread !== undefined)
100
97
  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
- }
108
98
  if (control.mode !== "new" && control.mode !== "resume") {
109
99
  throw new Error("Agent Host Provider control mode is invalid.");
110
100
  }
@@ -20,7 +20,7 @@ export class ProviderContinuationReconciliationService {
20
20
  }
21
21
  async reconcile(now) {
22
22
  const changedTaskIds = new Set();
23
- for (const task of this.store.listTasks().filter((entry) => entry.status === "active")) {
23
+ for (const task of this.store.listTasks().filter((entry) => (entry.status === "active" && entry.executionGate.state === "enabled"))) {
24
24
  const events = this.store.listEvents(task.id);
25
25
  const groups = groupDetachedContinuations(projectProviderContinuations(events), events.map(runtimeObservationFromTaskEvent)
26
26
  .filter((entry) => entry !== null));
@@ -47,7 +47,7 @@ export function decideProviderRecovery(input) {
47
47
  reason: "Provider Conversation is missing but its Activation writer has not ended."
48
48
  };
49
49
  }
50
- return { action: "replace", conversationId: conversation.conversationId };
50
+ return { action: "restart-run", conversationId: conversation.conversationId };
51
51
  }
52
52
  function providerTurnIsUnsettled(binding) {
53
53
  return binding.turn !== null
@@ -294,21 +294,30 @@ export function updateProviderConversationRecoverability(raw, recoverability) {
294
294
  export function supersedeProviderConversation(raw, input) {
295
295
  const binding = validateProviderRuntimeBinding(raw);
296
296
  const current = currentProviderConversation(binding);
297
- const basis = input.basis ?? "exact-unrecoverable";
298
- if (basis !== "actor-request" && basis !== "exact-unrecoverable") {
299
- throw new Error("Provider Conversation switch basis is invalid.");
297
+ const basis = input.basis;
298
+ if (basis !== "terminal-session") {
299
+ throw new Error("Provider Conversation replacement basis is invalid.");
300
300
  }
301
- if (basis === "exact-unrecoverable" && current.recoverability !== "unrecoverable") {
302
- throw new Error("Current Provider Conversation is not exactly unrecoverable.");
303
- }
304
- if (!input.noUnsettledInputDelivery) {
305
- throw new Error("Cannot replace a Provider Conversation with unsettled input delivery.");
306
- }
307
- if (binding.authority.owner !== "none" || currentProviderActivation(binding) !== null) {
308
- throw new Error("Cannot replace a Provider Conversation while its writer umbrella is owned.");
309
- }
310
- const switchedAt = timestamp(input.switchedAt, "Provider Conversation switch timestamp");
301
+ const switchedAt = timestamp(input.switchedAt, "Provider Conversation replacement timestamp");
311
302
  const epoch = current.epoch + 1;
303
+ const terminalReason = "terminal-session-replaced";
304
+ const activations = binding.activations.map((entry) => entry.status === "active"
305
+ ? {
306
+ ...entry,
307
+ status: "failed",
308
+ endedAt: switchedAt,
309
+ terminalReason
310
+ }
311
+ : entry);
312
+ const turn = binding.turn !== null
313
+ && ["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn.status)
314
+ ? {
315
+ ...binding.turn,
316
+ status: binding.turn.turnId === undefined ? "rejected" : "failed",
317
+ updatedAt: switchedAt,
318
+ terminalReason
319
+ }
320
+ : binding.turn;
312
321
  return validateProviderRuntimeBinding({
313
322
  ...binding,
314
323
  currentConversationEpoch: epoch,
@@ -324,7 +333,7 @@ export function supersedeProviderConversation(raw, input) {
324
333
  createdAt: switchedAt
325
334
  }
326
335
  ],
327
- activations: [...binding.activations, {
336
+ activations: [...activations, {
328
337
  activationId: identity(input.activationId, "Provider Activation id"),
329
338
  conversationId: input.conversationId,
330
339
  generation: 1,
@@ -336,7 +345,8 @@ export function supersedeProviderConversation(raw, input) {
336
345
  owner: "controller",
337
346
  holderId: input.activationId,
338
347
  changedAt: switchedAt
339
- }
348
+ },
349
+ turn
340
350
  });
341
351
  }
342
352
  export function validateProviderRuntimeBinding(value) {
@@ -4,8 +4,6 @@ import { CodexAppServerRequestError, CodexAppServerRuntime, codexAppServerErrorI
4
4
  import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
5
5
  import { YUI_VERSION } from "../version.js";
6
6
  const PROVIDER_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
7
- const CODEX_DAEMON_START_TIMEOUT_MS = 10_000;
8
- const CODEX_DAEMON_OUTPUT_MAX_BYTES = 64 * 1024;
9
7
  export class ProviderDeliveryUnknownError extends Error {
10
8
  attemptId;
11
9
  name = "ProviderDeliveryUnknownError";
@@ -46,9 +44,6 @@ export async function startStructuredProviderSession(payload, input = {}) {
46
44
  if (control === undefined) {
47
45
  throw new Error("Managed Agent Host launch requires Provider control metadata.");
48
46
  }
49
- if (control.adapterId === "codex") {
50
- await ensureCodexAppServerDaemon(payload, control);
51
- }
52
47
  const child = spawn(payload.command, [...payload.args], {
53
48
  cwd: payload.cwd,
54
49
  env: { ...payload.environment },
@@ -78,58 +73,6 @@ export async function startStructuredProviderSession(payload, input = {}) {
78
73
  throw error;
79
74
  }
80
75
  }
81
- async function ensureCodexAppServerDaemon(payload, control) {
82
- if (payload.args.at(-2) !== "app-server" || payload.args.at(-1) !== "proxy")
83
- return;
84
- if (control.adapterId !== "codex")
85
- return;
86
- const environment = Object.fromEntries(Object.entries(payload.environment).filter(([key]) => (!key.startsWith("YUI_")
87
- && key !== "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
88
- && !["TMPDIR", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR"]
89
- .includes(key))));
90
- const child = spawn(payload.command, [...control.codexDaemonStartArgs], {
91
- cwd: payload.cwd,
92
- env: environment,
93
- stdio: ["ignore", "pipe", "pipe"]
94
- });
95
- await new Promise((resolvePromise, reject) => {
96
- let output = "";
97
- let settled = false;
98
- let timer;
99
- const settle = (error) => {
100
- if (settled)
101
- return;
102
- settled = true;
103
- if (timer !== undefined)
104
- clearTimeout(timer);
105
- if (error === undefined)
106
- resolvePromise();
107
- else
108
- reject(error);
109
- };
110
- const capture = (chunk) => {
111
- output += String(chunk);
112
- if (Buffer.byteLength(output, "utf8") <= CODEX_DAEMON_OUTPUT_MAX_BYTES)
113
- return;
114
- child.kill("SIGTERM");
115
- settle(new Error("Codex App Server daemon start output exceeded its bound."));
116
- };
117
- child.stdout.on("data", capture);
118
- child.stderr.on("data", capture);
119
- child.once("error", (error) => settle(error));
120
- child.once("close", (code, signal) => {
121
- if (code === 0)
122
- settle();
123
- else
124
- settle(new Error(`Codex App Server daemon start failed (${code ?? signal ?? "unknown"})${output.trim().length === 0 ? "" : `: ${output.trim()}`}`));
125
- });
126
- timer = setTimeout(() => {
127
- child.kill("SIGTERM");
128
- settle(new Error("Codex App Server daemon start timed out."));
129
- }, CODEX_DAEMON_START_TIMEOUT_MS);
130
- timer.unref();
131
- });
132
- }
133
76
  class JsonLineChannel {
134
77
  child;
135
78
  mirror;
@@ -8,7 +8,7 @@ import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
8
8
  import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
9
9
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
10
10
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
11
- import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
11
+ import { RuntimeLifecycleBusyError } from "../runtime/lifecycleReservation.js";
12
12
  /**
13
13
  * Delivers durable Work AgentRuns before liveness reconciliation. Task command
14
14
  * handlers only record intent; this Controller path is the sole automated
@@ -48,24 +48,6 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
48
48
  });
49
49
  continue;
50
50
  }
51
- // Single-flight: a Role runtime lifecycle lane that already holds a
52
- // launch reservation or cleanup obligation must not be entered by a
53
- // second delivery. The Run stays active-unpushed and is retried after
54
- // the lane settles; the contention is never terminalized as a failure.
55
- if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
56
- scope: "task",
57
- taskId: task.id,
58
- roleName: role.name
59
- })))) {
60
- results.push({
61
- taskId: task.id,
62
- roleName: role.name,
63
- runId: run.id,
64
- status: "skipped",
65
- reason: "runtime-unavailable"
66
- });
67
- continue;
68
- }
69
51
  const existingSession = store.getRoleSession(task.id, role.name, run.effective.agentId);
70
52
  const receiptId = agentRunDeliveryReceiptId(run);
71
53
  const target = { kind: "role", taskId: task.id, roleName: role.name };
@@ -2,11 +2,11 @@ import { createAgentRun } from "../run/agentRun.js";
2
2
  import { createRunAssignment } from "../context/runContextContract.js";
3
3
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
4
4
  import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession } from "../executor/effectiveLaunch.js";
5
- import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
5
+ import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
6
+ import { RuntimeLifecycleBusyError } from "../runtime/lifecycleReservation.js";
6
7
  import { recordLeaderFailure } from "./leaderFailure.js";
7
8
  import { isSchedulerTaskWorkspaceReady } from "./ports.js";
8
9
  import { RuntimeLaunchError } from "../runtime/ports.js";
9
- import { roleSessionDispatchModeWithConversationSwitch } from "../runtime/conversationSwitch.js";
10
10
  export async function processLeaderWakeups(store, delivery, now, selection) {
11
11
  const results = [];
12
12
  const wakeups = selection === undefined || selection.full
@@ -20,7 +20,10 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
20
20
  for (const wakeup of wakeups) {
21
21
  const task = store.getTask(wakeup.taskId);
22
22
  const role = store.getRole(wakeup.taskId, "leader");
23
- if (task === null || task.status !== "active" || role === null) {
23
+ if (task === null
24
+ || task.status !== "active"
25
+ || task.executionGate.state !== "enabled"
26
+ || role === null) {
24
27
  results.push({ taskId: wakeup.taskId, status: "skipped", reason: "unavailable" });
25
28
  continue;
26
29
  }
@@ -29,10 +32,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
29
32
  results.push({ taskId: task.id, status: "skipped", reason: "workspace-not-ready" });
30
33
  continue;
31
34
  }
32
- if (store.getLeaderFailure(task.id) !== null) {
33
- results.push({ taskId: task.id, status: "skipped", reason: "recovery-blocked" });
34
- continue;
35
- }
36
35
  if (store.hasOpenInputRequest(task.id)) {
37
36
  results.push({ taskId: task.id, status: "skipped", reason: "waiting-input" });
38
37
  continue;
@@ -48,20 +47,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
48
47
  results.push({ taskId: task.id, status: "skipped", reason: "busy" });
49
48
  continue;
50
49
  }
51
- // Single-flight: a Role runtime lifecycle lane that already holds a
52
- // launch reservation or cleanup obligation must not be entered by a
53
- // second wake. The wake stays durable (pendingWakeup is not consumed) and
54
- // is retried after the lane settles; the suppression is recorded for the
55
- // audit instead of manufacturing a failed Run.
56
- if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
57
- scope: "task",
58
- taskId: task.id,
59
- roleName: role.name
60
- })))) {
61
- store.recordWakeSuppression?.(task.id, "lifecycle-busy", now);
62
- results.push({ taskId: task.id, status: "skipped", reason: "recovery-blocked" });
63
- continue;
64
- }
65
50
  const reopening = wakeup.reasons.includes("task-reopened");
66
51
  let existingSession = store.getRoleSession(task.id, role.name, reopening ? undefined : role.effective.agentId);
67
52
  let effectiveSession = existingSession;
@@ -71,43 +56,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
71
56
  let prepared;
72
57
  let preStartFencePersisted = false;
73
58
  try {
74
- if (existingSession !== null && !hasNativeSession(existingSession)) {
75
- const sessionIsTerminal = existingSession.status === "stopped"
76
- || existingSession.status === "broken";
77
- if (!sessionIsTerminal) {
78
- // An opaque live Session has no provider identity that can be safely
79
- // rebound. A host absence observation is not a verified stop; keep
80
- // the wake durable until an explicit exact cleanup/reset settles it.
81
- if (existingSession.launchId !== undefined) {
82
- await delivery.inspectRole({
83
- taskId: task.id,
84
- roleName: role.name,
85
- agentId: existingSession.agentId,
86
- adapterId: existingSession.adapterId
87
- });
88
- }
89
- results.push({
90
- taskId: task.id,
91
- status: "skipped",
92
- reason: "recovery-blocked"
93
- });
94
- continue;
95
- }
96
- if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
97
- scope: "task",
98
- taskId: task.id,
99
- roleName: role.name
100
- })))) {
101
- // A stopped/broken Session is eligible for a fresh mode only after
102
- // its exact runtime cleanup/reservation lane has settled.
103
- results.push({
104
- taskId: task.id,
105
- status: "skipped",
106
- reason: "recovery-blocked"
107
- });
108
- continue;
109
- }
110
- }
111
59
  const compatibleSession = existingSession !== null
112
60
  && (reopening
113
61
  ? effectiveLaunchSnapshotsCompatible(existingSession.effective, role.effective)
@@ -123,15 +71,14 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
123
71
  const resumableSession = hasNativeSession(existingSession)
124
72
  && existingSession.status !== "stopped"
125
73
  && existingSession.status !== "broken";
126
- // A failed or quiet Run is not proof that its Provider Conversation is
127
- // unusable. Keep resuming the same identity unless an explicit switch
128
- // request (or exact terminal/missing evidence) authorizes a fresh one.
74
+ // Reuse a healthy Session. A terminal Session is disposable and the next
75
+ // Run starts fresh without consulting historical recovery records.
129
76
  const sessionSet = store.getTaskRoleSessionSet?.(task.id, role.name) ?? null;
130
77
  const mode = reopenIdentityDrift
131
78
  ? "new"
132
79
  : sessionSet === null
133
80
  ? resumableSession && compatibleSession ? "resume" : "new"
134
- : roleSessionDispatchModeWithConversationSwitch(sessionSet, store.listEvents?.(task.id) ?? [], store.getWorkMailbox({ kind: "role", taskId: task.id, roleName: role.name }), role.name, role.effective.agentId, role.effective);
81
+ : roleAgentSessionResumeMode(sessionSet, role.effective.agentId, role.effective);
135
82
  const runId = store.peekNextAgentRunId(task.id);
136
83
  const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
137
84
  const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);
@@ -226,7 +173,9 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
226
173
  || ready.prepared.turnAcceptedDuringLaunch === true
227
174
  || ready.prepared.turnDeliveryUnknownDuringLaunch === true;
228
175
  const latestTask = store.getTask(task.id);
229
- if (latestTask === null || latestTask.status !== "active") {
176
+ if (latestTask === null
177
+ || latestTask.status !== "active"
178
+ || latestTask.executionGate.state !== "enabled") {
230
179
  delivery.forgetPrepared?.({
231
180
  taskId: task.id,
232
181
  roleName: role.name,