@zq-silk/yui 0.13.6 → 0.13.7
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 +12 -13
- package/README.md +13 -11
- package/dist/executor/agentAdapter.js +6 -2
- package/dist/executor/fileRoleLaunchPlanner.js +3 -2
- package/dist/runtime/agentHost.js +2 -2
- package/dist/runtime/builtinAgentDrivers.js +2 -1
- package/dist/runtime/launchBroker.js +1 -1
- package/dist/runtime/structuredProviderHost.js +172 -33
- package/dist/runtime/tmuxAdapters.js +9 -1
- package/i18n/README.zh-CN.md +4 -4
- package/package.json +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -254,21 +254,20 @@ Yui's current attachment, not exclusive ownership of the Provider thread. One
|
|
|
254
254
|
Turn identifies one provider-native execution. Yui's authority epoch fences
|
|
255
255
|
only Yui's own submissions and retries.
|
|
256
256
|
|
|
257
|
-
Codex Task threads remain ordinary native sessions and can be
|
|
258
|
-
Desktop.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
thread. The process loads the selected native config profile; Role model,
|
|
266
|
-
effort, permission, workspace, and shell settings are passed at
|
|
257
|
+
Codex Task threads remain ordinary native sessions and can be opened and used
|
|
258
|
+
directly in Desktop. If a direct user Turn is active, Yui keeps its pending
|
|
259
|
+
Run/message until that Turn settles. Global interactive entry remains a native
|
|
260
|
+
session-lifecycle operation outside the Task delivery contract.
|
|
261
|
+
|
|
262
|
+
Codex establishes an App Server WebSocket through the byte-forwarding
|
|
263
|
+
`app-server proxy` to create or resume a normal thread on the shared daemon.
|
|
264
|
+
Role model, effort, permission, workspace, and shell settings are passed at
|
|
267
265
|
`thread/start`/`thread/resume`, while the ordinary Task message points to the
|
|
268
266
|
Session Manifest and matching Role Skill. Yui does not write either to global
|
|
269
|
-
Codex config.
|
|
270
|
-
|
|
271
|
-
the
|
|
267
|
+
Codex config. A native Codex profile is rejected because it cannot be isolated
|
|
268
|
+
to one shared-daemon thread. The Agent Host owns only its proxy and WebSocket:
|
|
269
|
+
Task execution stop discards the Yui attachment without waiting for or changing
|
|
270
|
+
the daemon or native thread, and start creates a new attachment.
|
|
272
271
|
Claude uses a persistent stream-json
|
|
273
272
|
transport with exact user-message replay acknowledgement. In both cases, Yui
|
|
274
273
|
records Turn intent before writing, accepts only exact Provider evidence, and
|
package/README.md
CHANGED
|
@@ -875,13 +875,14 @@ Provider conversations remain ordinary user conversations. Yui adds the Role
|
|
|
875
875
|
Skill and Session Manifest pointer, then sends Task work through provider-native
|
|
876
876
|
structured requests. Managed prompts are never delivered as terminal bytes.
|
|
877
877
|
|
|
878
|
-
Codex
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
disconnects, the Host may
|
|
884
|
-
exact owned Turn from native history.
|
|
878
|
+
Codex establishes the App Server WebSocket protocol through `app-server proxy`
|
|
879
|
+
to create or resume an ordinary thread on the shared native daemon. The thread
|
|
880
|
+
remains visible and directly usable in Desktop. Task execution stop terminates
|
|
881
|
+
Yui's Agent Host and proxy while leaving the daemon and thread untouched; start
|
|
882
|
+
creates a fresh proxy attachment.
|
|
883
|
+
If the proxy disconnects, the Host may attach a bounded replacement client and
|
|
884
|
+
reconcile the exact owned Turn from native history. A failed fresh attachment
|
|
885
|
+
is released instead of becoming a cleanup prerequisite for later Runs.
|
|
885
886
|
Claude Code keeps its independent stream-json process with exact
|
|
886
887
|
user-message replay acknowledgement. A timeout or uncertain write becomes
|
|
887
888
|
`delivery-unknown` and is never automatically retried.
|
|
@@ -894,9 +895,10 @@ yui task role takeover <task-id> <role>
|
|
|
894
895
|
yui task role release <task-id> <role>
|
|
895
896
|
```
|
|
896
897
|
|
|
897
|
-
For
|
|
898
|
-
human-control boundary.
|
|
899
|
-
|
|
898
|
+
For an independently hosted Provider such as Claude, these commands are the
|
|
899
|
+
supported human-control boundary. A Codex Role uses an ordinary shared thread
|
|
900
|
+
and may be operated directly in Desktop; an active Desktop Turn creates bounded
|
|
901
|
+
backpressure for Yui rather than a failed Run.
|
|
900
902
|
|
|
901
903
|
Global Operator and global Role sessions remain native interactive CLIs:
|
|
902
904
|
|
|
@@ -961,7 +963,7 @@ movement cannot conceal a workflow that is not advancing.
|
|
|
961
963
|
|
|
962
964
|
Stable Role context never creates a separate bootstrap Turn. Task execution Runs use the generic Leader or Worker Skill, while review Runs use the generic Reviewer Skill based on durable Run purpose rather than a configured Role name. The provider either carries the Skill through a safe additive native context channel or points to it from the ordinary Task delivery. These Yui-owned Role Skills define portable orchestration only. Project Skills remain ordinary versioned files in the Project and are discovered, selected, and loaded by the Agent through its native project mechanism; Yui does not scan, parse, copy, or inject them.
|
|
963
965
|
|
|
964
|
-
Managed Codex keeps the user's native developer instructions unchanged. The ordinary Task message includes a compact absolute Session Manifest pointer, and the manifest identifies the matching Yui-owned Role Skill for Codex to read on demand.
|
|
966
|
+
Managed Codex keeps the user's native developer instructions unchanged. The ordinary Task message includes a compact absolute Session Manifest pointer, and the manifest identifies the matching Yui-owned Role Skill for Codex to read on demand. Model, effort, permission, workspace, and shell settings are supplied to `thread/start` or `thread/resume` through the shared App Server daemon; a Codex native config profile is rejected because it cannot be isolated to one shared-daemon thread. The underlying Codex config file is never mutated. App Server notifications are the managed thread's lifecycle authority; Yui installs no managed Codex Hook and does not claim `notify`. Interactive Codex Sessions may still use Yui's structured `notify` callback, and Doctor reports any effective configuration conflict. `skills.config` is not misused because it only enables or disables already-discovered Skills. Claude receives the same Yui-owned Role Skill content from a private `0600` managed context file rather than a large or sensitive argv value; retries and resumes reuse the purpose-specific Role path. Non-Operator global Roles stay neutral and receive no Task orchestration Skill. Operator therefore opens at an empty native composer, so the user's text remains its first user message. Leader wakeups and Worker or Reviewer Run assignments remain real mailbox-delivered work messages.
|
|
965
967
|
|
|
966
968
|
## Controller and failure handling
|
|
967
969
|
|
|
@@ -150,6 +150,10 @@ class CodexAdapter extends BaseAdapter {
|
|
|
150
150
|
}
|
|
151
151
|
compileManagedControl(input, _mode, _nativeSessionId) {
|
|
152
152
|
const config = this.canonicalizeConfig(input.config);
|
|
153
|
+
if (config.profile !== undefined) {
|
|
154
|
+
throw new Error("Managed Codex does not accept a Codex config profile because it cannot be scoped to one "
|
|
155
|
+
+ "shared-daemon thread. Use a Yui Agent Profile for skills, model, and effort.");
|
|
156
|
+
}
|
|
153
157
|
const launch = this.compileNew(input);
|
|
154
158
|
// A managed Codex thread is an ordinary user thread. Its Yui guidance is
|
|
155
159
|
// part of the durable Task message, not a developer_instructions override
|
|
@@ -166,8 +170,8 @@ class CodexAdapter extends BaseAdapter {
|
|
|
166
170
|
}
|
|
167
171
|
return {
|
|
168
172
|
...launch,
|
|
169
|
-
argv: [...argv, "app-server"],
|
|
170
|
-
transport: "codex-app-server",
|
|
173
|
+
argv: [...argv, "app-server", "proxy"],
|
|
174
|
+
transport: "codex-app-server-proxy",
|
|
171
175
|
codexThread: codexThreadOptions(input, config)
|
|
172
176
|
};
|
|
173
177
|
}
|
|
@@ -420,8 +420,9 @@ export class FileRoleLaunchPlanner {
|
|
|
420
420
|
if (owner.scope !== "task" || input.runId === undefined) {
|
|
421
421
|
args = addCodexSessionNotify(args, launchMode, this.#cliPath);
|
|
422
422
|
}
|
|
423
|
-
// Managed Codex Runs use
|
|
424
|
-
// Manifest points at the Yui Skills;
|
|
423
|
+
// Managed Codex Runs use disposable proxy clients against the shared
|
|
424
|
+
// native App Server. Their Session Manifest points at the Yui Skills;
|
|
425
|
+
// no Yui Hook config is installed.
|
|
425
426
|
if (owner.scope === "task" && input.runId !== undefined) {
|
|
426
427
|
if (managedRun === null || managedRun.status !== "active") {
|
|
427
428
|
throw new Error(`Managed Codex Run is no longer active: ${input.runId}.`);
|
|
@@ -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([]),
|
|
@@ -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,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
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -510,9 +510,9 @@ Task 生命周期的交互选择只展示有效来源状态:activate 只展示
|
|
|
510
510
|
|
|
511
511
|
## Session 与 tmux
|
|
512
512
|
|
|
513
|
-
受管理的 Provider 会话仍然是普通用户会话。Yui 只添加对应的 Role Skill 与 Session Manifest 指针,并通过 Provider 原生结构化协议提交 Task 工作;Yui 不接管完整对话历史。受管理输入绝不会作为终端按键、粘贴文本或启动 argv 发送。Codex
|
|
513
|
+
受管理的 Provider 会话仍然是普通用户会话。Yui 只添加对应的 Role Skill 与 Session Manifest 指针,并通过 Provider 原生结构化协议提交 Task 工作;Yui 不接管完整对话历史。受管理输入绝不会作为终端按键、粘贴文本或启动 argv 发送。Codex 在只转发字节的 `app-server proxy` 上完成 App Server WebSocket 握手,接入与 Desktop 相同的共享 daemon;原生 thread 可在 Desktop 中直接查看和操作。Task execution stop 只终止 Yui 的 Agent Host、WebSocket 与 proxy,保留共享 daemon、原生 thread、Task、WorkItem、代码与持久消息;start 创建新的 attachment。Claude 继续使用独立的持久 stream-json 进程,并以精确回放的 user message 作为接收确认。
|
|
514
514
|
|
|
515
|
-
Run、Conversation、Activation 与 Turn 是四个独立身份。Conversation 可以跨多个 Run 和客户端连接;Activation 只代表 Yui 当前的连接,而不是对 Provider thread 的独占所有权;Turn 在写入前先持久化。写入超时或结果不明确会进入 `delivery-unknown
|
|
515
|
+
Run、Conversation、Activation 与 Turn 是四个独立身份。Conversation 可以跨多个 Run 和客户端连接;Activation 只代表 Yui 当前的连接,而不是对 Provider thread 的独占所有权;Turn 在写入前先持久化。写入超时或结果不明确会进入 `delivery-unknown`,不会自动重发。Codex 已存在的原生 active Turn 只会让 Yui 暂时等待,不会导致 Yui Run 失败;Claude 等独立进程 Provider 继续通过 Yui 的 view/takeover 边界进行人工控制。
|
|
516
516
|
|
|
517
517
|
Task Role 使用以下显式入口:
|
|
518
518
|
|
|
@@ -524,7 +524,7 @@ yui task role takeover <task-id> <role>
|
|
|
524
524
|
yui task role release <task-id> <role>
|
|
525
525
|
```
|
|
526
526
|
|
|
527
|
-
Codex Role thread
|
|
527
|
+
Codex Role thread 可在 Desktop 中直接查看和操作;Desktop 已有 active Turn 时,Yui 只保留待投递工作并等待,不会失败或重复投递。`view`、`takeover`、`release` 继续作为 Claude 等独立进程 Provider 的人工控制入口。Yui 不写入全局 Hook/config,也不启动、重启或停止共享 daemon;Codex CLI/daemon 故障由 Task 生命周期之外修复。Global Operator 与 global Role 继续使用原生交互式 CLI,不属于受管理 Task Provider 协议。
|
|
528
528
|
|
|
529
529
|
当新版本需要离线迁移 Home 时,应等待当前 Turn/Run 完成,然后从普通 shell
|
|
530
530
|
执行 `yui session stop --all`,再重新执行 `yui update`。停止命令会先整体预检:
|
|
@@ -560,7 +560,7 @@ state、receipt 与 pane fence。Yui 不会解析 prompt glyph、进度文本、
|
|
|
560
560
|
或其他 Agent 终端输出来推断 ready 或 success。`captureRole()` 只用于显式的人类
|
|
561
561
|
transcript 查看,不具备生命周期权威。
|
|
562
562
|
|
|
563
|
-
稳定的 Role 上下文不会创建额外的 bootstrap Turn。Task execution Run 按角色使用通用 Leader 或 Worker Skill,review Run 则按持久 Run purpose 使用通用 Reviewer Skill;Provider 可以通过安全的追加式原生上下文通道携带 Skill,也可以在普通 Task 投递中指向它。这些都只是 Yui 自己拥有的可移植编排规则。Project Skills 始终是 Project 中正常版本化的文件,由 Agent 通过自身项目机制发现、选择并按需加载;Yui 不扫描、不解析、不复制,也不注入 Project Skills。Managed Codex 保留用户原有的 developer instructions;普通 Task 消息会携带精简的 Session Manifest 绝对路径,Manifest 再指向对应的 Yui Role Skill,供 Codex
|
|
563
|
+
稳定的 Role 上下文不会创建额外的 bootstrap Turn。Task execution Run 按角色使用通用 Leader 或 Worker Skill,review Run 则按持久 Run purpose 使用通用 Reviewer Skill;Provider 可以通过安全的追加式原生上下文通道携带 Skill,也可以在普通 Task 投递中指向它。这些都只是 Yui 自己拥有的可移植编排规则。Project Skills 始终是 Project 中正常版本化的文件,由 Agent 通过自身项目机制发现、选择并按需加载;Yui 不扫描、不解析、不复制,也不注入 Project Skills。Managed Codex 保留用户原有的 developer instructions;普通 Task 消息会携带精简的 Session Manifest 绝对路径,Manifest 再指向对应的 Yui Role Skill,供 Codex 按需读取。model、effort、permission、workspace 与 shell 设置作为共享 daemon 上的线程级 `thread/start` 或 `thread/resume` 配置传入;Codex 原生 config profile 因无法隔离到单条共享 thread 而被拒绝,Yui 不修改底层 Codex 配置文件。App Server 原生通知是 Managed Codex 线程的生命周期权威;Yui 不为它安装 Hook,也不占用 `notify`。交互式 Codex Session 仍可使用 Yui 的结构化 `notify` callback,Doctor 会报告最终生效的配置冲突。`skills.config` 只负责启停已发现 Skill,Yui 不会误用它。Claude 从 Yui 管理的私有 `0600` context 文件读取同一份 Yui Role Skill 内容,不再把大段或敏感文本放进 argv;重试和 resume 会复用按 purpose 区分的稳定路径。非 Operator 的 global Role 保持中性,不会注入 Task 编排 Skill。因此 Operator 会停在空白的原生 composer,用户输入仍是第一条 user message;Leader wake、Worker 和 Reviewer Run assignment 仍是邮箱投递的真实工作消息。
|
|
564
564
|
|
|
565
565
|
## Controller 与失败处理
|
|
566
566
|
|