@zq-silk/yui 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +31 -25
- package/README.md +24 -20
- package/dist/cli/commandCatalog.js +3 -3
- package/dist/commands/taskCommands.js +32 -9
- package/dist/controller/controller.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/runtimeLaunchCoordinator.js +4 -0
- package/dist/executor/agentAdapter.js +62 -8
- package/dist/executor/executorRegistry.js +4 -0
- package/dist/executor/fileRoleLaunchPlanner.js +71 -27
- package/dist/runtime/agentHost.js +226 -14
- package/dist/runtime/builtinAgentDrivers.js +9 -0
- package/dist/runtime/codexAppServerRuntime.js +63 -12
- package/dist/runtime/launchBroker.js +51 -1
- package/dist/runtime/runtimeBinding.js +10 -1
- package/dist/runtime/structuredProviderHost.js +214 -54
- package/dist/runtime/tmuxAdapters.js +13 -2
- package/dist/scheduler/activeRoleRunDelivery.js +24 -3
- package/dist/scheduler/leaderWakeupProcessor.js +20 -0
- package/i18n/README.zh-CN.md +7 -7
- package/package.json +1 -1
|
@@ -22,21 +22,17 @@ export class CodexAppServerRuntime {
|
|
|
22
22
|
async openConversation(input) {
|
|
23
23
|
const result = await this.transport.request("thread/start", {
|
|
24
24
|
cwd: text(input.cwd, "Codex thread cwd"),
|
|
25
|
-
...(input
|
|
26
|
-
...(input.approvalPolicy === undefined ? {} : { approvalPolicy: input.approvalPolicy }),
|
|
27
|
-
...(input.sandbox === undefined ? {} : { sandbox: input.sandbox }),
|
|
28
|
-
...(input.developerInstructions === undefined
|
|
29
|
-
? {}
|
|
30
|
-
: { developerInstructions: input.developerInstructions }),
|
|
31
|
-
...(input.runtimeWorkspaceRoots === undefined
|
|
32
|
-
? {}
|
|
33
|
-
: { runtimeWorkspaceRoots: [...input.runtimeWorkspaceRoots] })
|
|
25
|
+
...threadOptions(input)
|
|
34
26
|
});
|
|
35
27
|
return { conversationId: threadId(result) };
|
|
36
28
|
}
|
|
37
|
-
async resumeConversation(conversationId) {
|
|
29
|
+
async resumeConversation(conversationId, options = {}) {
|
|
38
30
|
const id = text(conversationId, "Codex thread id");
|
|
39
|
-
const result = await this.transport.request("thread/resume", {
|
|
31
|
+
const result = await this.transport.request("thread/resume", {
|
|
32
|
+
threadId: id,
|
|
33
|
+
...(options.cwd === undefined ? {} : { cwd: text(options.cwd, "Codex thread cwd") }),
|
|
34
|
+
...threadOptions(options)
|
|
35
|
+
});
|
|
40
36
|
return parseThreadSnapshot(result, id, true);
|
|
41
37
|
}
|
|
42
38
|
async setConversationName(input) {
|
|
@@ -85,7 +81,11 @@ export class CodexAppServerRuntime {
|
|
|
85
81
|
const snapshot = await this.readConversation(requestedThreadId);
|
|
86
82
|
threadId = snapshot.threadId;
|
|
87
83
|
if (input.expectedNoActiveTurn && snapshot.activeTurnId !== undefined) {
|
|
88
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
status: "busy",
|
|
86
|
+
activeTurnId: snapshot.activeTurnId,
|
|
87
|
+
reason: `active-turn:${snapshot.activeTurnId}`
|
|
88
|
+
};
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
catch (error) {
|
|
@@ -249,12 +249,25 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
249
249
|
const turns = arrayMember(thread, "turns").map(object).filter((entry) => (entry !== null));
|
|
250
250
|
const active = [...turns].reverse().find((turn) => (["inProgress", "in_progress", "running", "active"].includes(String(turn.status))));
|
|
251
251
|
const latestStatus = optionalTurnStatus(turns.at(-1)?.status);
|
|
252
|
+
const turnSnapshots = turns.flatMap((turn) => {
|
|
253
|
+
const turnId = optionalId(turn.id);
|
|
254
|
+
if (turnId === undefined)
|
|
255
|
+
return [];
|
|
256
|
+
const status = optionalTurnStatus(turn.status);
|
|
257
|
+
const error = providerError(turn.error);
|
|
258
|
+
return [{
|
|
259
|
+
turnId,
|
|
260
|
+
...(status === undefined ? {} : { status }),
|
|
261
|
+
...(error === undefined ? {} : { error })
|
|
262
|
+
}];
|
|
263
|
+
});
|
|
252
264
|
return {
|
|
253
265
|
threadId: id,
|
|
254
266
|
loaded,
|
|
255
267
|
status: threadStatus(thread.status),
|
|
256
268
|
...(optionalId(active?.id) === undefined ? {} : { activeTurnId: optionalId(active?.id) }),
|
|
257
269
|
...(latestStatus === undefined ? {} : { latestTurnStatus: latestStatus }),
|
|
270
|
+
turns: Object.freeze(turnSnapshots),
|
|
258
271
|
...(optionalId(thread.parentThreadId) === undefined
|
|
259
272
|
? {}
|
|
260
273
|
: { parentThreadId: optionalId(thread.parentThreadId) }),
|
|
@@ -262,6 +275,17 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
262
275
|
raw: result
|
|
263
276
|
};
|
|
264
277
|
}
|
|
278
|
+
function providerError(value) {
|
|
279
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
280
|
+
return value.trim();
|
|
281
|
+
const record = object(value);
|
|
282
|
+
if (record === null)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (typeof record.message === "string" && record.message.trim().length > 0) {
|
|
285
|
+
return record.message.trim();
|
|
286
|
+
}
|
|
287
|
+
return JSON.stringify(record);
|
|
288
|
+
}
|
|
265
289
|
function codexContinuationState(snapshot) {
|
|
266
290
|
if (snapshot.status === "active" || snapshot.latestTurnStatus === "inProgress") {
|
|
267
291
|
return {
|
|
@@ -317,12 +341,19 @@ function optionalTurnStatus(value) {
|
|
|
317
341
|
}
|
|
318
342
|
function classifyMutationError(error) {
|
|
319
343
|
if (error instanceof CodexAppServerRequestError) {
|
|
344
|
+
if (codexAppServerErrorIsBusy(error)) {
|
|
345
|
+
return { status: "busy", reason: error.message };
|
|
346
|
+
}
|
|
320
347
|
if (["INVALID_PARAMS", "NOT_FOUND", "TURN_NOT_ACTIVE", -32602].includes(error.code)) {
|
|
321
348
|
return { status: "not-accepted", reason: error.message };
|
|
322
349
|
}
|
|
323
350
|
}
|
|
324
351
|
return { status: "unknown", reason: error instanceof Error ? error.message : String(error) };
|
|
325
352
|
}
|
|
353
|
+
function codexAppServerErrorIsBusy(error) {
|
|
354
|
+
return /\b(active turn|turn (?:is )?(?:already )?(?:active|in progress|running)|already has an active)\b/iu
|
|
355
|
+
.test(error.message);
|
|
356
|
+
}
|
|
326
357
|
function isNotLoaded(error) {
|
|
327
358
|
return error instanceof CodexAppServerRequestError
|
|
328
359
|
&& (String(error.code).toLowerCase().includes("not_loaded")
|
|
@@ -366,3 +397,23 @@ function text(value, label) {
|
|
|
366
397
|
}
|
|
367
398
|
return value.trim();
|
|
368
399
|
}
|
|
400
|
+
function threadOptions(input) {
|
|
401
|
+
return {
|
|
402
|
+
...(input.model === undefined ? {} : { model: text(input.model, "Codex model") }),
|
|
403
|
+
...(input.approvalPolicy === undefined
|
|
404
|
+
? {}
|
|
405
|
+
: { approvalPolicy: text(input.approvalPolicy, "Codex approval policy") }),
|
|
406
|
+
...(input.sandbox === undefined ? {} : { sandbox: text(input.sandbox, "Codex sandbox") }),
|
|
407
|
+
...(input.developerInstructions === undefined
|
|
408
|
+
? {}
|
|
409
|
+
: {
|
|
410
|
+
developerInstructions: text(input.developerInstructions, "Codex developer instructions")
|
|
411
|
+
}),
|
|
412
|
+
...(input.runtimeWorkspaceRoots === undefined
|
|
413
|
+
? {}
|
|
414
|
+
: {
|
|
415
|
+
runtimeWorkspaceRoots: input.runtimeWorkspaceRoots.map((root) => (text(root, "Codex runtime workspace root")))
|
|
416
|
+
}),
|
|
417
|
+
...(input.config === undefined ? {} : { config: { ...input.config } })
|
|
418
|
+
};
|
|
419
|
+
}
|
|
@@ -86,10 +86,25 @@ function validateProviderControl(control) {
|
|
|
86
86
|
if (control.adapterId !== "codex" && control.adapterId !== "claude") {
|
|
87
87
|
throw new Error("Agent Host Provider control adapter is invalid.");
|
|
88
88
|
}
|
|
89
|
-
if ((control.adapterId === "codex" && control.transport !== "codex-app-server-
|
|
89
|
+
if ((control.adapterId === "codex" && control.transport !== "codex-app-server-proxy")
|
|
90
90
|
|| (control.adapterId === "claude" && control.transport !== "claude-stream-json")) {
|
|
91
91
|
throw new Error("Agent Host Provider control transport does not match its adapter.");
|
|
92
92
|
}
|
|
93
|
+
if ((control.adapterId === "codex") !== (control.codexThread !== undefined)) {
|
|
94
|
+
throw new Error("Agent Host Provider thread settings do not match its adapter.");
|
|
95
|
+
}
|
|
96
|
+
if ((control.adapterId === "codex") !== (control.codexDaemonStartArgs !== undefined)) {
|
|
97
|
+
throw new Error("Agent Host Provider daemon bootstrap does not match its adapter.");
|
|
98
|
+
}
|
|
99
|
+
if (control.codexThread !== undefined)
|
|
100
|
+
validateCodexThreadOptions(control.codexThread);
|
|
101
|
+
if (control.codexDaemonStartArgs !== undefined) {
|
|
102
|
+
if (!Array.isArray(control.codexDaemonStartArgs)
|
|
103
|
+
|| control.codexDaemonStartArgs.length === 0) {
|
|
104
|
+
throw new Error("Agent Host Codex daemon bootstrap args are invalid.");
|
|
105
|
+
}
|
|
106
|
+
control.codexDaemonStartArgs.forEach((value) => text(value, "Codex daemon argument"));
|
|
107
|
+
}
|
|
93
108
|
if (control.mode !== "new" && control.mode !== "resume") {
|
|
94
109
|
throw new Error("Agent Host Provider control mode is invalid.");
|
|
95
110
|
}
|
|
@@ -105,6 +120,16 @@ function validateProviderControl(control) {
|
|
|
105
120
|
if (control.kind === "ensure" && control.initialTurn !== undefined) {
|
|
106
121
|
throw new Error("Managed Provider ensure launch cannot carry a new Turn.");
|
|
107
122
|
}
|
|
123
|
+
if (control.kind !== "ensure" && "ownedTurn" in control) {
|
|
124
|
+
throw new Error("Only a managed Provider ensure launch can recover an owned Turn.");
|
|
125
|
+
}
|
|
126
|
+
if (control.kind === "ensure" && control.ownedTurn !== undefined) {
|
|
127
|
+
if (control.adapterId !== "codex") {
|
|
128
|
+
throw new Error("Only Managed Codex can recover an owned Turn across client attachment.");
|
|
129
|
+
}
|
|
130
|
+
text(control.ownedTurn.attemptId, "owned Provider input attemptId");
|
|
131
|
+
text(control.ownedTurn.turnId, "owned Provider Turn id");
|
|
132
|
+
}
|
|
108
133
|
const requiresNativeSessionId = control.mode === "resume" || control.adapterId === "claude";
|
|
109
134
|
if (requiresNativeSessionId !== (control.nativeSessionId !== undefined)) {
|
|
110
135
|
throw new Error("Agent Host Provider resume identity is inconsistent.");
|
|
@@ -129,6 +154,31 @@ function validateProviderControl(control) {
|
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
}
|
|
157
|
+
function validateCodexThreadOptions(options) {
|
|
158
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
159
|
+
throw new Error("Agent Host Codex thread settings are invalid.");
|
|
160
|
+
}
|
|
161
|
+
for (const [value, label] of [
|
|
162
|
+
[options.model, "model"],
|
|
163
|
+
[options.approvalPolicy, "approval policy"],
|
|
164
|
+
[options.sandbox, "sandbox"],
|
|
165
|
+
[options.developerInstructions, "developer instructions"]
|
|
166
|
+
]) {
|
|
167
|
+
if (value !== undefined)
|
|
168
|
+
text(value, `Codex thread ${label}`);
|
|
169
|
+
}
|
|
170
|
+
if (options.runtimeWorkspaceRoots !== undefined) {
|
|
171
|
+
if (!Array.isArray(options.runtimeWorkspaceRoots)) {
|
|
172
|
+
throw new Error("Agent Host Codex runtime workspace roots are invalid.");
|
|
173
|
+
}
|
|
174
|
+
options.runtimeWorkspaceRoots.forEach((root) => text(root, "Codex runtime workspace root"));
|
|
175
|
+
}
|
|
176
|
+
if (options.config !== undefined
|
|
177
|
+
&& (options.config === null || typeof options.config !== "object"
|
|
178
|
+
|| Array.isArray(options.config))) {
|
|
179
|
+
throw new Error("Agent Host Codex thread config is invalid.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
132
182
|
function text(value, label) {
|
|
133
183
|
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
134
184
|
throw new Error(`Agent Host ${label} is invalid.`);
|
|
@@ -14,7 +14,15 @@ export function createRuntimeBinding(input) {
|
|
|
14
14
|
const initialTurnRejectedRunId = input.initialTurnRejectedRunId === undefined
|
|
15
15
|
? undefined
|
|
16
16
|
: requireSafeIdentity(input.initialTurnRejectedRunId, "Rejected initial Turn Run id");
|
|
17
|
-
|
|
17
|
+
const initialTurnBusyRunId = input.initialTurnBusyRunId === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: requireSafeIdentity(input.initialTurnBusyRunId, "Busy initial Turn Run id");
|
|
20
|
+
if ([
|
|
21
|
+
initialTurnRunId,
|
|
22
|
+
initialTurnDeliveryUnknownRunId,
|
|
23
|
+
initialTurnBusyRunId,
|
|
24
|
+
initialTurnRejectedRunId
|
|
25
|
+
]
|
|
18
26
|
.filter((value) => value !== undefined).length > 1) {
|
|
19
27
|
throw new TypeError("Runtime binding must report at most one initial Turn outcome.");
|
|
20
28
|
}
|
|
@@ -30,6 +38,7 @@ export function createRuntimeBinding(input) {
|
|
|
30
38
|
...(initialTurnDeliveryUnknownRunId === undefined
|
|
31
39
|
? {}
|
|
32
40
|
: { initialTurnDeliveryUnknownRunId }),
|
|
41
|
+
...(initialTurnBusyRunId === undefined ? {} : { initialTurnBusyRunId }),
|
|
33
42
|
...(initialTurnRejectedRunId === undefined ? {} : { initialTurnRejectedRunId }),
|
|
34
43
|
...(input.nativeSessionId === undefined
|
|
35
44
|
? {}
|
|
@@ -4,6 +4,8 @@ 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;
|
|
7
9
|
export class ProviderDeliveryUnknownError extends Error {
|
|
8
10
|
attemptId;
|
|
9
11
|
name = "ProviderDeliveryUnknownError";
|
|
@@ -20,6 +22,17 @@ export class ProviderTurnRejectedError extends Error {
|
|
|
20
22
|
this.attemptId = attemptId;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
25
|
+
/** Another ordinary client currently owns the thread's active Turn. */
|
|
26
|
+
export class ProviderTurnBusyError extends Error {
|
|
27
|
+
attemptId;
|
|
28
|
+
activeTurnId;
|
|
29
|
+
name = "ProviderTurnBusyError";
|
|
30
|
+
constructor(message, attemptId, activeTurnId) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.attemptId = attemptId;
|
|
33
|
+
this.activeTurnId = activeTurnId;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
23
36
|
export class ProviderConversationMissingError extends Error {
|
|
24
37
|
conversationId;
|
|
25
38
|
name = "ProviderConversationMissingError";
|
|
@@ -33,6 +46,9 @@ export async function startStructuredProviderSession(payload, input = {}) {
|
|
|
33
46
|
if (control === undefined) {
|
|
34
47
|
throw new Error("Managed Agent Host launch requires Provider control metadata.");
|
|
35
48
|
}
|
|
49
|
+
if (control.adapterId === "codex") {
|
|
50
|
+
await ensureCodexAppServerDaemon(payload, control);
|
|
51
|
+
}
|
|
36
52
|
const child = spawn(payload.command, [...payload.args], {
|
|
37
53
|
cwd: payload.cwd,
|
|
38
54
|
env: { ...payload.environment },
|
|
@@ -45,9 +61,16 @@ export async function startStructuredProviderSession(payload, input = {}) {
|
|
|
45
61
|
child.stderr.on("data", (chunk) => mirror("stderr", chunk));
|
|
46
62
|
const exit = childExit(child, processInstanceId);
|
|
47
63
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
if (control.adapterId === "codex") {
|
|
65
|
+
const opened = await CodexStructuredProviderSession.open(child, exit, processInstanceId, payload, control, input.onTerminal, mirror);
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
session: opened.session,
|
|
68
|
+
...(opened.recoveredTerminal === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { recoveredTerminal: opened.recoveredTerminal })
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const session = await ClaudeStructuredProviderSession.open(child, exit, processInstanceId, control, input.onTerminal, mirror);
|
|
51
74
|
return Object.freeze({ session });
|
|
52
75
|
}
|
|
53
76
|
catch (error) {
|
|
@@ -55,6 +78,58 @@ export async function startStructuredProviderSession(payload, input = {}) {
|
|
|
55
78
|
throw error;
|
|
56
79
|
}
|
|
57
80
|
}
|
|
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
|
+
}
|
|
58
133
|
class JsonLineChannel {
|
|
59
134
|
child;
|
|
60
135
|
mirror;
|
|
@@ -181,17 +256,24 @@ class CodexStructuredProviderSession {
|
|
|
181
256
|
processInstanceId;
|
|
182
257
|
conversationId;
|
|
183
258
|
runtime;
|
|
259
|
+
onTerminal;
|
|
184
260
|
adapterId = "codex";
|
|
185
261
|
#activeTurnId;
|
|
186
|
-
|
|
262
|
+
#clientOwnedTurnId;
|
|
263
|
+
#submissionPending = false;
|
|
264
|
+
#bufferedTerminals = [];
|
|
265
|
+
constructor(child, exit, processInstanceId, conversationId, runtime, onTerminal) {
|
|
187
266
|
this.child = child;
|
|
188
267
|
this.exit = exit;
|
|
189
268
|
this.processInstanceId = processInstanceId;
|
|
190
269
|
this.conversationId = conversationId;
|
|
191
270
|
this.runtime = runtime;
|
|
271
|
+
this.onTerminal = onTerminal;
|
|
192
272
|
}
|
|
193
273
|
static async open(child, exit, processInstanceId, payload, control, onTerminal, mirror) {
|
|
194
274
|
const channel = new JsonLineChannel(child, mirror);
|
|
275
|
+
const openingMessages = [];
|
|
276
|
+
const stopOpeningBuffer = channel.onMessage((message) => openingMessages.push(message));
|
|
195
277
|
await channel.request("initialize", {
|
|
196
278
|
clientInfo: { name: "yui", title: "Yui", version: YUI_VERSION },
|
|
197
279
|
capabilities: {
|
|
@@ -203,14 +285,22 @@ class CodexStructuredProviderSession {
|
|
|
203
285
|
const runtime = new CodexAppServerRuntime(channel);
|
|
204
286
|
let conversationId;
|
|
205
287
|
let resumedActiveTurnId;
|
|
288
|
+
let resumedTurns = [];
|
|
206
289
|
if (control.mode === "new") {
|
|
207
|
-
conversationId = (await runtime.openConversation({
|
|
290
|
+
conversationId = (await runtime.openConversation({
|
|
291
|
+
cwd: payload.cwd,
|
|
292
|
+
...control.codexThread
|
|
293
|
+
})).conversationId;
|
|
208
294
|
}
|
|
209
295
|
else {
|
|
210
296
|
try {
|
|
211
|
-
const resumed = await runtime.resumeConversation(control.nativeSessionId
|
|
297
|
+
const resumed = await runtime.resumeConversation(control.nativeSessionId, {
|
|
298
|
+
cwd: payload.cwd,
|
|
299
|
+
...control.codexThread
|
|
300
|
+
});
|
|
212
301
|
conversationId = resumed.threadId;
|
|
213
302
|
resumedActiveTurnId = resumed.activeTurnId;
|
|
303
|
+
resumedTurns = resumed.turns;
|
|
214
304
|
}
|
|
215
305
|
catch (error) {
|
|
216
306
|
if (codexAppServerErrorIsMissing(error)) {
|
|
@@ -225,39 +315,81 @@ class CodexStructuredProviderSession {
|
|
|
225
315
|
name: control.sessionTitle
|
|
226
316
|
});
|
|
227
317
|
}
|
|
228
|
-
const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime);
|
|
318
|
+
const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime, onTerminal);
|
|
229
319
|
session.#activeTurnId = resumedActiveTurnId;
|
|
320
|
+
const ownedTurn = control.kind === "ensure" ? control.ownedTurn : undefined;
|
|
321
|
+
session.#clientOwnedTurnId = ownedTurn?.turnId;
|
|
322
|
+
stopOpeningBuffer();
|
|
323
|
+
let recoveredTerminal;
|
|
324
|
+
for (const message of openingMessages) {
|
|
325
|
+
const terminal = session.#observeMessage(message, false);
|
|
326
|
+
if (terminal?.clientOwned === true)
|
|
327
|
+
recoveredTerminal = terminal;
|
|
328
|
+
}
|
|
230
329
|
channel.onMessage((message) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
330
|
+
session.#observeMessage(message, true);
|
|
331
|
+
});
|
|
332
|
+
if (ownedTurn !== undefined && recoveredTerminal === undefined
|
|
333
|
+
&& session.#activeTurnId !== ownedTurn.turnId) {
|
|
334
|
+
const recovered = resumedTurns.find((turn) => turn.turnId === ownedTurn.turnId);
|
|
335
|
+
if (recovered?.status === "completed"
|
|
336
|
+
|| recovered?.status === "interrupted"
|
|
337
|
+
|| recovered?.status === "failed") {
|
|
338
|
+
recoveredTerminal = {
|
|
339
|
+
conversationId,
|
|
340
|
+
nativeSessionId: conversationId,
|
|
341
|
+
nativeTurnId: ownedTurn.turnId,
|
|
342
|
+
clientOwned: true,
|
|
343
|
+
status: recovered.status === "failed"
|
|
344
|
+
? "failed"
|
|
345
|
+
: recovered.status === "interrupted" ? "cancelled" : "completed",
|
|
346
|
+
observedAt: new Date().toISOString(),
|
|
347
|
+
...(recovered.error === undefined ? {} : { error: recovered.error })
|
|
348
|
+
};
|
|
349
|
+
session.#clientOwnedTurnId = undefined;
|
|
240
350
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const status = turn.status === "failed"
|
|
249
|
-
? "failed"
|
|
250
|
-
: turn.status === "interrupted" ? "cancelled" : "completed";
|
|
251
|
-
onTerminal?.({
|
|
252
|
-
conversationId,
|
|
253
|
-
nativeSessionId: conversationId,
|
|
254
|
-
nativeTurnId,
|
|
255
|
-
status,
|
|
256
|
-
observedAt: new Date().toISOString(),
|
|
257
|
-
...(status !== "failed" ? {} : { error: providerErrorText(turn.error) })
|
|
258
|
-
});
|
|
351
|
+
else {
|
|
352
|
+
throw new ProviderDeliveryUnknownError(`Codex resume could not recover the persisted Yui Turn ${ownedTurn.turnId}.`, ownedTurn.attemptId);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return Object.freeze({
|
|
356
|
+
session,
|
|
357
|
+
...(recoveredTerminal === undefined ? {} : { recoveredTerminal })
|
|
259
358
|
});
|
|
260
|
-
|
|
359
|
+
}
|
|
360
|
+
#observeMessage(message, emit) {
|
|
361
|
+
const method = typeof message.method === "string" ? message.method : "";
|
|
362
|
+
const params = object(message.params) ?? {};
|
|
363
|
+
if (optionalId(params.threadId) !== this.conversationId)
|
|
364
|
+
return undefined;
|
|
365
|
+
if (method === "turn/started") {
|
|
366
|
+
const turnId = nestedId(params, "turn") ?? optionalId(params.turnId);
|
|
367
|
+
if (turnId !== undefined)
|
|
368
|
+
this.#activeTurnId = turnId;
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
if (method !== "turn/completed")
|
|
372
|
+
return undefined;
|
|
373
|
+
const turn = object(params.turn) ?? {};
|
|
374
|
+
const nativeTurnId = optionalId(turn.id) ?? optionalId(params.turnId);
|
|
375
|
+
if (nativeTurnId === undefined)
|
|
376
|
+
return undefined;
|
|
377
|
+
const status = turn.status === "failed"
|
|
378
|
+
? "failed"
|
|
379
|
+
: turn.status === "interrupted" ? "cancelled" : "completed";
|
|
380
|
+
const terminal = {
|
|
381
|
+
conversationId: this.conversationId,
|
|
382
|
+
nativeSessionId: this.conversationId,
|
|
383
|
+
nativeTurnId,
|
|
384
|
+
status,
|
|
385
|
+
observedAt: new Date().toISOString(),
|
|
386
|
+
...(status !== "failed" ? {} : { error: providerErrorText(turn.error) })
|
|
387
|
+
};
|
|
388
|
+
if (this.#submissionPending && emit) {
|
|
389
|
+
this.#bufferedTerminals.push(terminal);
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
return this.#completeTerminal(terminal, emit);
|
|
261
393
|
}
|
|
262
394
|
get nativeSessionId() {
|
|
263
395
|
return this.conversationId;
|
|
@@ -267,28 +399,55 @@ class CodexStructuredProviderSession {
|
|
|
267
399
|
}
|
|
268
400
|
async submitTurn(turn) {
|
|
269
401
|
if (this.#activeTurnId !== undefined) {
|
|
270
|
-
throw new
|
|
402
|
+
throw new ProviderTurnBusyError(`Provider Conversation already has active Turn ${this.#activeTurnId}.`, turn.attemptId, this.#activeTurnId);
|
|
271
403
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
404
|
+
this.#submissionPending = true;
|
|
405
|
+
try {
|
|
406
|
+
const acceptance = await this.runtime.submitTurn({
|
|
407
|
+
conversationId: this.conversationId,
|
|
408
|
+
attemptId: turn.attemptId,
|
|
409
|
+
text: turn.boundedText,
|
|
410
|
+
expectedNoActiveTurn: true
|
|
411
|
+
});
|
|
412
|
+
if (acceptance.status === "unknown") {
|
|
413
|
+
throw new ProviderDeliveryUnknownError(acceptance.reason, turn.attemptId);
|
|
414
|
+
}
|
|
415
|
+
if (acceptance.status === "busy") {
|
|
416
|
+
throw new ProviderTurnBusyError(acceptance.reason, turn.attemptId, acceptance.activeTurnId);
|
|
417
|
+
}
|
|
418
|
+
if (acceptance.status === "not-accepted") {
|
|
419
|
+
throw new ProviderTurnRejectedError(acceptance.reason, turn.attemptId);
|
|
420
|
+
}
|
|
421
|
+
this.#activeTurnId = acceptance.turnId;
|
|
422
|
+
this.#clientOwnedTurnId = acceptance.turnId;
|
|
423
|
+
return Object.freeze({
|
|
424
|
+
attemptId: turn.attemptId,
|
|
425
|
+
conversationId: this.conversationId,
|
|
426
|
+
nativeSessionId: this.conversationId,
|
|
427
|
+
nativeTurnId: acceptance.turnId,
|
|
428
|
+
acceptedAt: new Date().toISOString()
|
|
429
|
+
});
|
|
280
430
|
}
|
|
281
|
-
|
|
282
|
-
|
|
431
|
+
finally {
|
|
432
|
+
this.#submissionPending = false;
|
|
433
|
+
const buffered = this.#bufferedTerminals.splice(0);
|
|
434
|
+
for (const terminal of buffered)
|
|
435
|
+
this.#emitTerminal(terminal);
|
|
283
436
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
437
|
+
}
|
|
438
|
+
#emitTerminal(terminal) {
|
|
439
|
+
this.#completeTerminal(terminal, true);
|
|
440
|
+
}
|
|
441
|
+
#completeTerminal(terminal, emit) {
|
|
442
|
+
const clientOwned = terminal.nativeTurnId === this.#clientOwnedTurnId;
|
|
443
|
+
if (terminal.nativeTurnId === this.#activeTurnId)
|
|
444
|
+
this.#activeTurnId = undefined;
|
|
445
|
+
if (clientOwned)
|
|
446
|
+
this.#clientOwnedTurnId = undefined;
|
|
447
|
+
const completed = { ...terminal, clientOwned };
|
|
448
|
+
if (emit)
|
|
449
|
+
this.onTerminal?.(completed);
|
|
450
|
+
return completed;
|
|
292
451
|
}
|
|
293
452
|
waitForExit() {
|
|
294
453
|
return this.exit;
|
|
@@ -420,6 +579,7 @@ class ClaudeStructuredProviderSession {
|
|
|
420
579
|
conversationId: this.conversationId,
|
|
421
580
|
nativeSessionId: this.conversationId,
|
|
422
581
|
nativeTurnId,
|
|
582
|
+
clientOwned: true,
|
|
423
583
|
status: failed ? "failed" : "completed",
|
|
424
584
|
observedAt: new Date().toISOString(),
|
|
425
585
|
...(typeof message.result === "string" && message.result.length > 0
|
|
@@ -350,6 +350,7 @@ export class TmuxSessionHost {
|
|
|
350
350
|
let hostCreated;
|
|
351
351
|
let providerAcknowledged = false;
|
|
352
352
|
let providerDeliveryUnknown = false;
|
|
353
|
+
let providerBusy = false;
|
|
353
354
|
let providerRejected = false;
|
|
354
355
|
let providerDispatchObserved = false;
|
|
355
356
|
let providerSnapshot;
|
|
@@ -365,9 +366,11 @@ export class TmuxSessionHost {
|
|
|
365
366
|
requireTurnAck: planned.initialTurnRunId !== undefined
|
|
366
367
|
});
|
|
367
368
|
providerDeliveryUnknown = providerSnapshot.state === "delivery-unknown";
|
|
369
|
+
providerBusy = planned.initialTurnRunId !== undefined
|
|
370
|
+
&& providerSnapshot.state === "busy";
|
|
368
371
|
providerRejected = planned.initialTurnRunId !== undefined
|
|
369
372
|
&& providerSnapshot.state === "rejected";
|
|
370
|
-
providerAcknowledged = !providerDeliveryUnknown && !providerRejected;
|
|
373
|
+
providerAcknowledged = !providerDeliveryUnknown && !providerBusy && !providerRejected;
|
|
371
374
|
if (providerSnapshot.state === "rejected" && !providerRejected) {
|
|
372
375
|
throw new Error("Agent Host rejected a launch without an initial Provider Turn.");
|
|
373
376
|
}
|
|
@@ -398,9 +401,11 @@ export class TmuxSessionHost {
|
|
|
398
401
|
&& controlResult.snapshot.state === "idle");
|
|
399
402
|
const deliveryUnknownState = planned.initialTurnRunId !== undefined
|
|
400
403
|
&& controlResult.snapshot.state === "delivery-unknown";
|
|
404
|
+
const busyState = planned.initialTurnRunId !== undefined
|
|
405
|
+
&& controlResult.snapshot.state === "busy";
|
|
401
406
|
const rejectedState = planned.initialTurnRunId !== undefined
|
|
402
407
|
&& controlResult.snapshot.state === "rejected";
|
|
403
|
-
if ((!acceptableState && !deliveryUnknownState && !rejectedState)
|
|
408
|
+
if ((!acceptableState && !deliveryUnknownState && !busyState && !rejectedState)
|
|
404
409
|
|| controlResult.snapshot.launchId !== reservation.launchId) {
|
|
405
410
|
broker.revoke(request.launchId);
|
|
406
411
|
throw new Error(`Agent Host did not return an exact Provider acknowledgement for ${reservation.launchId}.`);
|
|
@@ -408,6 +413,7 @@ export class TmuxSessionHost {
|
|
|
408
413
|
providerSnapshot = controlResult.snapshot;
|
|
409
414
|
providerAcknowledged = acceptableState;
|
|
410
415
|
providerDeliveryUnknown = deliveryUnknownState;
|
|
416
|
+
providerBusy = busyState;
|
|
411
417
|
providerRejected = rejectedState;
|
|
412
418
|
providerDispatchObserved = true;
|
|
413
419
|
}
|
|
@@ -449,6 +455,9 @@ export class TmuxSessionHost {
|
|
|
449
455
|
...(providerDeliveryUnknown && planned.initialTurnRunId !== undefined
|
|
450
456
|
? { initialTurnDeliveryUnknownRunId: planned.initialTurnRunId }
|
|
451
457
|
: {}),
|
|
458
|
+
...(providerBusy && planned.initialTurnRunId !== undefined
|
|
459
|
+
? { initialTurnBusyRunId: planned.initialTurnRunId }
|
|
460
|
+
: {}),
|
|
452
461
|
...(providerRejected && planned.initialTurnRunId !== undefined
|
|
453
462
|
? { initialTurnRejectedRunId: planned.initialTurnRunId }
|
|
454
463
|
: {}),
|
|
@@ -567,6 +576,8 @@ export class AgentHostPromptPushAdapter {
|
|
|
567
576
|
});
|
|
568
577
|
if (result.snapshot.state === "delivery-unknown")
|
|
569
578
|
return "delivery-unknown";
|
|
579
|
+
if (result.snapshot.state === "busy")
|
|
580
|
+
return "busy";
|
|
570
581
|
if (result.outcome === "rejected")
|
|
571
582
|
return "rejected";
|
|
572
583
|
if (result.snapshot.attemptId !== request.envelope.id) {
|