@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.
- package/ARCHITECTURE.md +52 -17
- package/README.md +32 -12
- package/dist/cli/updatePorts.js +4 -4
- package/dist/cli.js +1 -1
- package/dist/commands/taskCommands.js +1 -12
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskExecutionCommands.js +0 -5
- package/dist/commands/taskOverviewCommand.js +5 -1
- package/dist/commands/taskRoleRuntimeStatus.js +0 -13
- package/dist/context/sessionBootstrapManifest.js +26 -23
- package/dist/controller/controller.js +0 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +84 -78
- package/dist/executor/agentAdapter.js +6 -2
- package/dist/executor/agentExecutor.js +14 -28
- package/dist/executor/fileRoleLaunchPlanner.js +5 -5
- package/dist/lifecycle/exactRunTerminalization.js +1 -3
- package/dist/output/rolePresentation.js +0 -1
- package/dist/repository/taskWorkspacePreparer.js +0 -1
- package/dist/role/role.js +12 -20
- package/dist/runtime/agentHost.js +2 -2
- package/dist/runtime/builtinAgentDrivers.js +2 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +1 -1
- package/dist/runtime/providerRuntimeIdentity.js +8 -17
- package/dist/runtime/structuredProviderHost.js +172 -33
- package/dist/runtime/tmuxAdapters.js +9 -1
- package/dist/scheduler/activeRoleRunDelivery.js +2 -10
- package/dist/scheduler/activeTaskProgress.js +1 -4
- package/dist/scheduler/leaderWakeupProcessor.js +0 -5
- package/dist/scheduler/roleRunStall.js +3 -3
- package/dist/storage/migration/productionRegistry.js +143 -0
- package/dist/storage/taskStore.js +2 -2
- package/dist/web/webSnapshot.js +3 -0
- package/i18n/README.zh-CN.md +15 -7
- package/package.json +1 -1
- 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
|
-
|
|
23
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
|
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([]),
|
package/dist/runtime/index.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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?.
|
|
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 !==
|
|
354
|
-
throw new Error("Provider Runtime Binding schemaVersion must be
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
187
|
-
|
|
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 =
|
|
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.
|
|
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 ((
|
|
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.
|
|
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(
|
|
1005
|
-
return `live-pane-no-progress:
|
|
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}`));
|