@zq-silk/yui 0.13.6 → 0.13.8

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 (36) hide show
  1. package/ARCHITECTURE.md +52 -17
  2. package/README.md +32 -12
  3. package/dist/cli/updatePorts.js +4 -4
  4. package/dist/cli.js +1 -1
  5. package/dist/commands/taskCommands.js +1 -12
  6. package/dist/commands/taskContextCommand.js +1 -1
  7. package/dist/commands/taskExecutionCommands.js +0 -5
  8. package/dist/commands/taskOverviewCommand.js +5 -1
  9. package/dist/commands/taskRoleRuntimeStatus.js +0 -13
  10. package/dist/context/sessionBootstrapManifest.js +26 -23
  11. package/dist/controller/controller.js +0 -2
  12. package/dist/controller/fileSchedulerStoreAdapter.js +84 -78
  13. package/dist/executor/agentAdapter.js +6 -2
  14. package/dist/executor/agentExecutor.js +14 -28
  15. package/dist/executor/fileRoleLaunchPlanner.js +5 -5
  16. package/dist/lifecycle/exactRunTerminalization.js +1 -3
  17. package/dist/output/rolePresentation.js +0 -1
  18. package/dist/repository/taskWorkspacePreparer.js +0 -1
  19. package/dist/role/role.js +12 -20
  20. package/dist/runtime/agentHost.js +2 -2
  21. package/dist/runtime/builtinAgentDrivers.js +2 -1
  22. package/dist/runtime/index.js +1 -1
  23. package/dist/runtime/launchBroker.js +1 -1
  24. package/dist/runtime/providerRuntimeIdentity.js +8 -17
  25. package/dist/runtime/structuredProviderHost.js +172 -33
  26. package/dist/runtime/tmuxAdapters.js +9 -1
  27. package/dist/scheduler/activeRoleRunDelivery.js +2 -10
  28. package/dist/scheduler/activeTaskProgress.js +1 -4
  29. package/dist/scheduler/leaderWakeupProcessor.js +0 -5
  30. package/dist/scheduler/roleRunStall.js +3 -3
  31. package/dist/storage/migration/productionRegistry.js +143 -0
  32. package/dist/storage/taskStore.js +2 -2
  33. package/dist/web/webSnapshot.js +3 -0
  34. package/i18n/README.zh-CN.md +15 -7
  35. package/package.json +1 -1
  36. package/skills/yui-runtime/SKILL.md +5 -4
package/dist/role/role.js CHANGED
@@ -19,19 +19,22 @@ export function createRole(taskId, name, bindings, activeAgentId, workspace, now
19
19
  const owner = createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess);
20
20
  return validateTaskRole({
21
21
  ...owner,
22
- taskId: requireSafeIdentity(taskId, "Task id"),
23
- status: "idle"
22
+ schemaVersion: 4,
23
+ taskId: requireSafeIdentity(taskId, "Task id")
24
24
  });
25
25
  }
26
26
  export function createGlobalRole(name, bindings, activeAgentId, workspace, now, profile = {}, defaultAccess = "write") {
27
- return createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess);
27
+ return validateGlobalRole({
28
+ ...createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess),
29
+ schemaVersion: 3
30
+ });
28
31
  }
29
32
  export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalRole.name) {
30
33
  validateGlobalRole(globalRole);
31
34
  const timestamp = now.toISOString();
32
35
  return validateTaskRole({
33
36
  ...cloneProfile(globalRole),
34
- schemaVersion: 3,
37
+ schemaVersion: 4,
35
38
  launchRevision: 1,
36
39
  defaultAccess: globalRole.defaultAccess,
37
40
  taskId: requireSafeIdentity(taskId, "Task id"),
@@ -39,7 +42,6 @@ export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalR
39
42
  activeAgentId: globalRole.activeAgentId,
40
43
  agentBindings: cloneBindings(globalRole.agentBindings),
41
44
  workspace: globalRole.workspace,
42
- status: "idle",
43
45
  createdAt: timestamp,
44
46
  updatedAt: timestamp
45
47
  });
@@ -76,12 +78,6 @@ export function updateRole(role, patch, now) {
76
78
  : role.launchRevision + 1;
77
79
  return validateTaskRole(updated);
78
80
  }
79
- export function updateRoleStatus(role, status, now) {
80
- validateTaskRole(role);
81
- if (!isRoleStatus(status))
82
- throw new Error(`Role status is invalid: ${status}.`);
83
- return { ...role, status, updatedAt: now.toISOString() };
84
- }
85
81
  export function switchActiveRoleAgent(role, sessions, targetAgentId, runtime, now) {
86
82
  validateRoleOwner(role);
87
83
  const normalizedTarget = requireSafeIdentity(targetAgentId, "Target Agent id");
@@ -175,8 +171,6 @@ export function validateGlobalRole(role) {
175
171
  }
176
172
  export function validateTaskRole(role) {
177
173
  requireSafeIdentity(role.taskId, "Task id");
178
- if (!isRoleStatus(role.status))
179
- throw new Error(`Role status is invalid: ${role.status}.`);
180
174
  return validateRoleOwner(role);
181
175
  }
182
176
  function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess) {
@@ -189,9 +183,8 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
189
183
  mappedBindings[binding.agentId] = binding;
190
184
  }
191
185
  const timestamp = now.toISOString();
192
- return validateGlobalRole({
186
+ return {
193
187
  ...cloneProfile(profile),
194
- schemaVersion: 3,
195
188
  launchRevision: 1,
196
189
  defaultAccess,
197
190
  name: requireSafeIdentity(name, "Role name"),
@@ -200,11 +193,13 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
200
193
  workspace: requireText(workspace, "Role workspace"),
201
194
  createdAt: timestamp,
202
195
  updatedAt: timestamp
203
- });
196
+ };
204
197
  }
205
198
  function validateRoleOwner(role) {
206
- if (role.schemaVersion !== 3)
199
+ const expectedSchemaVersion = "taskId" in role ? 4 : 3;
200
+ if (role.schemaVersion !== expectedSchemaVersion) {
207
201
  throw new Error("Role schema version is invalid.");
202
+ }
208
203
  if (!Number.isSafeInteger(role.launchRevision) || role.launchRevision < 1) {
209
204
  throw new Error("Role launch revision must be a positive integer.");
210
205
  }
@@ -333,6 +328,3 @@ function requireText(value, label) {
333
328
  throw new Error(`${label} is required.`);
334
329
  return normalized;
335
330
  }
336
- function isRoleStatus(value) {
337
- return ["idle", "running", "detached", "exited", "failed"].includes(value);
338
- }
@@ -145,7 +145,7 @@ export async function runAgentHost(input) {
145
145
  providerControl: {
146
146
  schemaVersion: 1,
147
147
  adapterId: "codex",
148
- transport: "codex-app-server",
148
+ transport: "codex-app-server-proxy",
149
149
  kind: "ensure",
150
150
  mode: "resume",
151
151
  nativeSessionId: disconnectedSession.nativeSessionId,
@@ -257,7 +257,7 @@ export async function runAgentHost(input) {
257
257
  ...(activeTurnAttemptId === undefined ? {} : { attemptId: activeTurnAttemptId }),
258
258
  ...(activeNativeTurnId === undefined ? {} : { nativeTurnId: activeNativeTurnId }),
259
259
  ...exitAuthority,
260
- detail: "Codex App Server disconnected; starting a replacement process."
260
+ detail: "Codex App Server proxy disconnected; attaching a replacement client."
261
261
  }));
262
262
  await reconnectCodexClient(providerSession, currentPayload);
263
263
  return;
@@ -121,7 +121,8 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
121
121
  }),
122
122
  observation: Object.freeze({
123
123
  ...STRUCTURED_CLI_CAPABILITIES.observation,
124
- // Managed Codex uses its Yui-owned App Server event stream.
124
+ // Managed Codex uses its Yui-owned proxy subscription to the shared
125
+ // App Server event stream.
125
126
  // Turn lifecycle is exact; Yui does not install per-thread Hooks merely
126
127
  // to manufacture tool/wait/usage observations.
127
128
  operations: Object.freeze([]),
@@ -14,7 +14,7 @@ export { formatRuntimeLaunchDiagnostic, redactLaunchArgument, redactLaunchText,
14
14
  export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
15
15
  export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
16
16
  export { codexNotificationBoundary, codexAppServerErrorIsMissing, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
17
- export { createProviderRuntimeBinding, acceptProviderTurn, beginProviderTurn, currentProviderActivation, currentProviderAuthority, currentProviderConversation, endProviderActivation, markProviderTurnDeliveryUnknown, rebindProviderRuntimeRun, rejectProviderTurn, settleProviderTurnSubmission, settleProviderTurn, startProviderActivation, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
17
+ export { createProviderRuntimeBinding, acceptProviderTurn, beginProviderTurn, currentProviderActivation, currentProviderAuthority, currentProviderConversation, endProviderActivation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurnSubmission, settleProviderTurn, startProviderActivation, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
18
18
  export { FencedProviderControl } from "./providerControl.js";
19
19
  export { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
20
20
  export { decideProviderRecovery } from "./providerRecoveryDecision.js";
@@ -86,7 +86,7 @@ function validateProviderControl(control) {
86
86
  if (control.adapterId !== "codex" && control.adapterId !== "claude") {
87
87
  throw new Error("Agent Host Provider control adapter is invalid.");
88
88
  }
89
- if ((control.adapterId === "codex" && control.transport !== "codex-app-server")
89
+ if ((control.adapterId === "codex" && control.transport !== "codex-app-server-proxy")
90
90
  || (control.adapterId === "claude" && control.transport !== "claude-stream-json")) {
91
91
  throw new Error("Agent Host Provider control transport does not match its adapter.");
92
92
  }
@@ -1,10 +1,9 @@
1
1
  export function createProviderRuntimeBinding(input) {
2
2
  const startedAt = timestamp(input.startedAt, "Provider Activation startedAt");
3
3
  return validateProviderRuntimeBinding({
4
- schemaVersion: 2,
4
+ schemaVersion: 3,
5
5
  providerNamespace: identity(input.providerNamespace, "Provider namespace"),
6
6
  accountScope: identity(input.accountScope, "Provider account scope"),
7
- runId: identity(input.runId, "Run id"),
8
7
  currentConversationEpoch: 1,
9
8
  conversations: [{
10
9
  conversationId: identity(input.conversationId, "Provider Conversation id"),
@@ -40,17 +39,6 @@ export function currentProviderActivation(binding) {
40
39
  const conversation = currentProviderConversation(binding);
41
40
  return [...binding.activations].reverse().find((entry) => (entry.conversationId === conversation.conversationId && entry.status === "active")) ?? null;
42
41
  }
43
- /** Rebinds the live Conversation state to the next Yui Run without resetting authority. */
44
- export function rebindProviderRuntimeRun(raw, runId) {
45
- const binding = validateProviderRuntimeBinding(raw);
46
- if (providerTurnIsActive(binding.turn)) {
47
- throw new Error("Provider Runtime cannot bind another Run while a Turn is unsettled.");
48
- }
49
- return validateProviderRuntimeBinding({
50
- ...binding,
51
- runId: identity(runId, "Run id")
52
- });
53
- }
54
42
  export function startProviderActivation(raw, input) {
55
43
  const binding = validateProviderRuntimeBinding(raw);
56
44
  if (currentProviderActivation(binding) !== null) {
@@ -158,8 +146,10 @@ export function transferProviderAuthority(raw, input) {
158
146
  }
159
147
  export function beginProviderTurn(raw, input) {
160
148
  const binding = validateProviderRuntimeBinding(raw);
149
+ const runId = identity(input.runId, "Run id");
161
150
  const attemptId = identity(input.attemptId, "Provider input attempt id");
162
- if (binding.turn?.attemptId === attemptId
151
+ if (binding.turn?.runId === runId
152
+ && binding.turn.attemptId === attemptId
163
153
  && binding.turn.authorityEpoch === input.authorityEpoch
164
154
  && binding.turn.status === "submitting") {
165
155
  return binding;
@@ -176,6 +166,7 @@ export function beginProviderTurn(raw, input) {
176
166
  return validateProviderRuntimeBinding({
177
167
  ...binding,
178
168
  turn: {
169
+ runId,
179
170
  attemptId,
180
171
  authorityEpoch: input.authorityEpoch,
181
172
  status: "submitting",
@@ -350,11 +341,10 @@ export function supersedeProviderConversation(raw, input) {
350
341
  });
351
342
  }
352
343
  export function validateProviderRuntimeBinding(value) {
353
- if (value.schemaVersion !== 2)
354
- throw new Error("Provider Runtime Binding schemaVersion must be 2.");
344
+ if (value.schemaVersion !== 3)
345
+ throw new Error("Provider Runtime Binding schemaVersion must be 3.");
355
346
  identity(value.providerNamespace, "Provider namespace");
356
347
  identity(value.accountScope, "Provider account scope");
357
- identity(value.runId, "Run id");
358
348
  integer(value.currentConversationEpoch, 1, "Current Provider Conversation epoch");
359
349
  if (!Array.isArray(value.conversations) || value.conversations.length === 0) {
360
350
  throw new Error("Provider Runtime Binding requires a Conversation.");
@@ -459,6 +449,7 @@ export function validateProviderRuntimeBinding(value) {
459
449
  return value;
460
450
  }
461
451
  function validateProviderTurn(turn, currentAuthorityEpoch) {
452
+ identity(turn.runId, "Run id");
462
453
  identity(turn.attemptId, "Provider input attempt id");
463
454
  integer(turn.authorityEpoch, 1, "Provider Turn authority epoch");
464
455
  if (turn.authorityEpoch > currentAuthorityEpoch) {
@@ -1,9 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { Duplex } from "node:stream";
4
+ import WebSocket from "ws";
3
5
  import { CodexAppServerRequestError, CodexAppServerRuntime, codexAppServerErrorIsMissing } from "./codexAppServerRuntime.js";
4
6
  import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
5
7
  import { YUI_VERSION } from "../version.js";
6
8
  const PROVIDER_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
9
+ const CODEX_PROXY_HANDSHAKE_TIMEOUT_MS = 10_000;
7
10
  export class ProviderDeliveryUnknownError extends Error {
8
11
  attemptId;
9
12
  name = "ProviderDeliveryUnknownError";
@@ -73,27 +76,57 @@ export async function startStructuredProviderSession(payload, input = {}) {
73
76
  throw error;
74
77
  }
75
78
  }
76
- class JsonLineChannel {
79
+ class CodexProxyWebSocketChannel {
77
80
  child;
78
81
  mirror;
79
82
  #pending = new Map();
80
83
  #listeners = new Set();
81
- #buffer = "";
82
84
  #nextId = 1;
83
85
  #closedError;
86
+ #ready = false;
87
+ #readyPromise;
88
+ #resolveReady;
89
+ #rejectReady;
90
+ #webSocket;
84
91
  constructor(child, mirror) {
85
92
  this.child = child;
86
93
  this.mirror = mirror;
87
- child.stdout.setEncoding("utf8");
88
- child.stdout.on("data", (chunk) => this.#receive(chunk));
94
+ let resolveReady;
95
+ let rejectReady;
96
+ this.#readyPromise = new Promise((resolvePromise, reject) => {
97
+ resolveReady = resolvePromise;
98
+ rejectReady = reject;
99
+ });
100
+ this.#resolveReady = resolveReady;
101
+ this.#rejectReady = rejectReady;
102
+ const transport = new ChildProcessDuplex(child);
103
+ this.#webSocket = new WebSocket("ws://localhost/rpc", {
104
+ createConnection: () => transport,
105
+ handshakeTimeout: CODEX_PROXY_HANDSHAKE_TIMEOUT_MS,
106
+ maxPayload: PROVIDER_MESSAGE_MAX_BYTES,
107
+ perMessageDeflate: false
108
+ });
109
+ this.#webSocket.once("open", () => {
110
+ this.#ready = true;
111
+ this.#resolveReady();
112
+ });
113
+ this.#webSocket.on("message", (data, isBinary) => this.#receive(data, isBinary));
114
+ this.#webSocket.on("error", (error) => this.#close(error));
115
+ this.#webSocket.once("close", (code, reason) => this.#close(new Error(`Codex App Server proxy WebSocket closed (code=${code}, reason=${reason.toString() || "none"}).`)));
89
116
  child.once("error", (error) => this.#close(error));
90
117
  child.once("close", (code, signal) => this.#close(new Error(`Provider process exited before replying (code=${code ?? "none"}, signal=${signal ?? "none"}).`)));
91
118
  }
119
+ static async connect(child, mirror) {
120
+ const channel = new CodexProxyWebSocketChannel(child, mirror);
121
+ await channel.#readyPromise;
122
+ return channel;
123
+ }
92
124
  onMessage(listener) {
93
125
  this.#listeners.add(listener);
94
126
  return () => this.#listeners.delete(listener);
95
127
  }
96
128
  async request(method, params) {
129
+ await this.#readyPromise;
97
130
  if (this.#closedError !== undefined)
98
131
  throw this.#closedError;
99
132
  const id = String(this.#nextId++);
@@ -120,6 +153,136 @@ class JsonLineChannel {
120
153
  async notify(method, params) {
121
154
  await this.send({ method, ...(params === undefined ? {} : { params }) });
122
155
  }
156
+ async send(message) {
157
+ await this.#readyPromise;
158
+ if (this.#closedError !== undefined)
159
+ throw this.#closedError;
160
+ const encoded = JSON.stringify(message);
161
+ if (Buffer.byteLength(encoded, "utf8") > PROVIDER_MESSAGE_MAX_BYTES) {
162
+ throw new Error("Provider request exceeds its message bound.");
163
+ }
164
+ await new Promise((resolvePromise, reject) => {
165
+ this.#webSocket.send(encoded, (error) => {
166
+ if (error === undefined || error === null)
167
+ resolvePromise();
168
+ else
169
+ reject(error);
170
+ });
171
+ });
172
+ }
173
+ #receive(data, isBinary) {
174
+ const encoded = Buffer.isBuffer(data)
175
+ ? data
176
+ : data instanceof ArrayBuffer
177
+ ? Buffer.from(data)
178
+ : Buffer.concat(data);
179
+ if (isBinary || encoded.byteLength > PROVIDER_MESSAGE_MAX_BYTES) {
180
+ this.#close(new Error(isBinary
181
+ ? "Provider returned an unsupported binary WebSocket message."
182
+ : "Provider response message exceeds its bound."));
183
+ terminateProcessGroup(this.child, "SIGTERM");
184
+ return;
185
+ }
186
+ const text = encoded.toString("utf8");
187
+ this.mirror("stdout", `${text}\n`);
188
+ let message;
189
+ try {
190
+ const parsed = JSON.parse(text);
191
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
192
+ return;
193
+ message = parsed;
194
+ }
195
+ catch {
196
+ return;
197
+ }
198
+ const id = requestId(message.id);
199
+ const pending = id === undefined ? undefined : this.#pending.get(id);
200
+ if (pending !== undefined) {
201
+ clearTimeout(pending.timer);
202
+ this.#pending.delete(id);
203
+ const error = object(message.error);
204
+ if (error !== null) {
205
+ pending.reject(new CodexAppServerRequestError(typeof error.code === "number" || typeof error.code === "string"
206
+ ? error.code
207
+ : "UNKNOWN", typeof error.message === "string" ? error.message : "Provider request failed.", error.data));
208
+ }
209
+ else {
210
+ pending.resolve(object(message.result) ?? {});
211
+ }
212
+ return;
213
+ }
214
+ for (const listener of this.#listeners)
215
+ listener(message);
216
+ }
217
+ #close(error) {
218
+ if (this.#closedError !== undefined)
219
+ return;
220
+ this.#closedError = error;
221
+ if (!this.#ready)
222
+ this.#rejectReady(error);
223
+ for (const pending of this.#pending.values()) {
224
+ clearTimeout(pending.timer);
225
+ pending.reject(error);
226
+ }
227
+ this.#pending.clear();
228
+ if (this.child.exitCode === null && this.child.signalCode === null) {
229
+ terminateProcessGroup(this.child, "SIGTERM");
230
+ }
231
+ }
232
+ }
233
+ class ChildProcessDuplex extends Duplex {
234
+ child;
235
+ connecting = false;
236
+ constructor(child) {
237
+ super();
238
+ this.child = child;
239
+ child.stdout.on("data", (chunk) => {
240
+ if (!this.push(chunk))
241
+ child.stdout.pause();
242
+ });
243
+ child.stdout.once("end", () => this.push(null));
244
+ child.once("error", (error) => this.destroy(error));
245
+ child.once("close", () => this.destroy());
246
+ }
247
+ _read() {
248
+ this.child.stdout.resume();
249
+ }
250
+ _write(chunk, encoding, callback) {
251
+ this.child.stdin.write(chunk, encoding, callback);
252
+ }
253
+ _final(callback) {
254
+ this.child.stdin.end(callback);
255
+ }
256
+ setNoDelay() {
257
+ return this;
258
+ }
259
+ setKeepAlive() {
260
+ return this;
261
+ }
262
+ setTimeout(_timeout, callback) {
263
+ if (callback !== undefined)
264
+ this.once("timeout", callback);
265
+ return this;
266
+ }
267
+ }
268
+ class JsonLineChannel {
269
+ child;
270
+ mirror;
271
+ #listeners = new Set();
272
+ #buffer = "";
273
+ #closedError;
274
+ constructor(child, mirror) {
275
+ this.child = child;
276
+ this.mirror = mirror;
277
+ child.stdout.setEncoding("utf8");
278
+ child.stdout.on("data", (chunk) => this.#receive(chunk));
279
+ child.once("error", (error) => this.#close(error));
280
+ child.once("close", (code, signal) => this.#close(new Error(`Provider process exited (code=${code ?? "none"}, signal=${signal ?? "none"}).`)));
281
+ }
282
+ onMessage(listener) {
283
+ this.#listeners.add(listener);
284
+ return () => this.#listeners.delete(listener);
285
+ }
123
286
  async send(message) {
124
287
  if (this.#closedError !== undefined)
125
288
  throw this.#closedError;
@@ -152,45 +315,21 @@ class JsonLineChannel {
152
315
  this.#buffer = this.#buffer.slice(newline + 1);
153
316
  if (line.length === 0)
154
317
  continue;
155
- let message;
156
318
  try {
157
319
  const parsed = JSON.parse(line);
158
320
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
159
321
  continue;
160
- message = parsed;
322
+ for (const listener of this.#listeners)
323
+ listener(parsed);
161
324
  }
162
325
  catch {
163
326
  continue;
164
327
  }
165
- const id = requestId(message.id);
166
- const pending = id === undefined ? undefined : this.#pending.get(id);
167
- if (pending !== undefined) {
168
- clearTimeout(pending.timer);
169
- this.#pending.delete(id);
170
- const error = object(message.error);
171
- if (error !== null) {
172
- pending.reject(new CodexAppServerRequestError(typeof error.code === "number" || typeof error.code === "string"
173
- ? error.code
174
- : "UNKNOWN", typeof error.message === "string" ? error.message : "Provider request failed.", error.data));
175
- }
176
- else {
177
- pending.resolve(object(message.result) ?? {});
178
- }
179
- continue;
180
- }
181
- for (const listener of this.#listeners)
182
- listener(message);
183
328
  }
184
329
  }
185
330
  #close(error) {
186
- if (this.#closedError !== undefined)
187
- return;
188
- this.#closedError = error;
189
- for (const pending of this.#pending.values()) {
190
- clearTimeout(pending.timer);
191
- pending.reject(error);
192
- }
193
- this.#pending.clear();
331
+ if (this.#closedError === undefined)
332
+ this.#closedError = error;
194
333
  }
195
334
  }
196
335
  class CodexStructuredProviderSession {
@@ -214,7 +353,7 @@ class CodexStructuredProviderSession {
214
353
  this.onTerminal = onTerminal;
215
354
  }
216
355
  static async open(child, exit, processInstanceId, payload, control, onTerminal, mirror) {
217
- const channel = new JsonLineChannel(child, mirror);
356
+ const channel = await CodexProxyWebSocketChannel.connect(child, mirror);
218
357
  const openingMessages = [];
219
358
  const stopOpeningBuffer = channel.onMessage((message) => openingMessages.push(message));
220
359
  await channel.request("initialize", {
@@ -347,7 +347,7 @@ export class TmuxSessionHost {
347
347
  : { [YUI_TASK_RUNTIME_DESCRIPTOR]: frozenTaskRuntime })
348
348
  }
349
349
  };
350
- let hostCreated;
350
+ let hostCreated = false;
351
351
  let providerAcknowledged = false;
352
352
  let providerDeliveryUnknown = false;
353
353
  let providerBusy = false;
@@ -420,6 +420,14 @@ export class TmuxSessionHost {
420
420
  }
421
421
  catch (error) {
422
422
  broker.revoke(request.launchId);
423
+ if (hostCreated && !providerDispatchObserved) {
424
+ try {
425
+ await stopExactRole(this.tmux, hostId, request.owner.roleName);
426
+ }
427
+ catch (stopError) {
428
+ throw new Error(`Managed Provider launch failed and its disposable Agent Host could not be stopped: ${stopError instanceof Error ? stopError.message : String(stopError)}`, { cause: stopError });
429
+ }
430
+ }
423
431
  throw error;
424
432
  }
425
433
  if (providerDispatchObserved
@@ -497,17 +497,9 @@ async function processActiveRunContinuation(store, delivery, task, role, run, no
497
497
  };
498
498
  }
499
499
  // Managed input has one write path: a new structured Turn at a settled
500
- // boundary. Corrections retain their priority while the current Turn runs.
500
+ // boundary. AgentHost owns Provider serialization; a busy send releases the
501
+ // claim and leaves the durable correction pending for the next pass.
501
502
  const mode = "followup";
502
- if (session.status === "running") {
503
- return {
504
- taskId: task.id,
505
- roleName: role.name,
506
- runId: run.id,
507
- status: "skipped",
508
- reason: "not-ready"
509
- };
510
- }
511
503
  if (writer === null) {
512
504
  return {
513
505
  taskId: task.id,
@@ -25,8 +25,6 @@ export function repairOrphanedActiveTasks(store, now, selection) {
25
25
  const run = store.getActiveAgentRun(task.id, role.name);
26
26
  return run === null ? [] : [run];
27
27
  });
28
- const hasInFlightTurn = roles.some((role) => store.hasInFlightTurn(task.id, role.name));
29
- const hasLeaderInFlightTurn = store.hasInFlightTurn(task.id, "leader");
30
28
  const leaderTarget = { kind: "role", taskId: task.id, roleName: "leader" };
31
29
  const leaderMailbox = store.getWorkMailbox(leaderTarget);
32
30
  const projection = projectTaskExecution({
@@ -41,8 +39,7 @@ export function repairOrphanedActiveTasks(store, now, selection) {
41
39
  && !activeRuns.some(({ roleName }) => roleName === "leader")
42
40
  && activeRuns.some(({ roleName }) => roleName !== "leader")
43
41
  && projection.status === "waiting-on-agents";
44
- if ((queuedAlongsideActiveSibling ? hasLeaderInFlightTurn : hasInFlightTurn)
45
- || store.hasOpenInputRequest(task.id)
42
+ if (store.hasOpenInputRequest(task.id)
46
43
  || store.getLeaderFailure(task.id) !== null
47
44
  || (projection.status !== "needs-leader-action" && !queuedAlongsideActiveSibling)
48
45
  || hasUnclaimedLeaderWork(store, task.id, leaderMailbox)) {
@@ -42,11 +42,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
42
42
  results.push({ taskId: task.id, status: "skipped", reason: "busy" });
43
43
  continue;
44
44
  }
45
- if (typeof store.hasInFlightTurn === "function"
46
- && store.hasInFlightTurn(task.id, role.name)) {
47
- results.push({ taskId: task.id, status: "skipped", reason: "busy" });
48
- continue;
49
- }
50
45
  const reopening = wakeup.reasons.includes("task-reopened");
51
46
  let existingSession = store.getRoleSession(task.id, role.name, reopening ? undefined : role.effective.agentId);
52
47
  let effectiveSession = existingSession;
@@ -786,7 +786,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
786
786
  continue;
787
787
  const evidenceKey = [
788
788
  kind,
789
- stallEvidenceKey(candidate.role.status, candidate.session?.status),
789
+ stallEvidenceKey(candidate.session?.status),
790
790
  classification,
791
791
  ...(candidate.role.name === "leader"
792
792
  ? [leaderStallEvidence(store, candidate.task.id, observed, now, windowMs)]
@@ -1001,8 +1001,8 @@ function leaderStallEvidence(store, taskId, observed, now, windowMs) {
1001
1001
  : "recent";
1002
1002
  return `downstream=active:${active},healthy:${healthy},stalled:${stalled}:leader-mailbox=${pendingAge},leader-processing=${processingAge}`;
1003
1003
  }
1004
- function stallEvidenceKey(roleStatus, sessionStatus) {
1005
- return `live-pane-no-progress:role=${roleStatus}:session=${sessionStatus ?? "unknown"}`;
1004
+ function stallEvidenceKey(sessionStatus) {
1005
+ return `live-pane-no-progress:session=${sessionStatus ?? "unknown"}`;
1006
1006
  }
1007
1007
  function exactLiveStatuses(statuses, candidates) {
1008
1008
  const expected = new Set(candidates.map(({ task, role }) => `${task.id}\0${role.name}`));