@zq-silk/yui 0.6.16 → 0.7.0
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/dist/cli/commandCatalog.js +3 -7
- package/dist/cli.js +12 -33
- package/dist/commands/executionAuditCommands.js +19 -0
- package/dist/commands/globalRoleCommands.js +70 -0
- package/dist/commands/taskActor.js +3 -2
- package/dist/commands/taskCommands.js +160 -41
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskInputCommands.js +3 -2
- package/dist/commands/taskRoleRuntimeStatus.js +3 -3
- package/dist/context/contextSnapshot.js +228 -0
- package/dist/context/roleSessionContext.js +3 -1
- package/dist/context/runContextContract.js +162 -0
- package/dist/context/runContextPack.js +322 -0
- package/dist/context/sessionBootstrapManifest.js +81 -0
- package/dist/context/sessionProtocolIdentity.js +23 -0
- package/dist/controller/agentRuntimeObserver.js +6 -1
- package/dist/controller/controller.js +4 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
- package/dist/controller/jobControl.js +2 -1
- package/dist/controller/runtime.js +83 -0
- package/dist/controller/runtimeHookRunFence.js +6 -2
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/executor/agentAdapter.js +7 -2
- package/dist/executor/agentExecutor.js +23 -0
- package/dist/executor/effectiveLaunch.js +24 -0
- package/dist/executor/executorRegistry.js +7 -1
- package/dist/executor/fileRoleLaunchPlanner.js +73 -27
- package/dist/lifecycle/exactRunTerminalization.js +2 -3
- package/dist/lifecycle/providerErrorClass.js +8 -3
- package/dist/observability/executionAudit.js +87 -2
- package/dist/repository/taskWorkspacePreparer.js +2 -2
- package/dist/run/agentRun.js +101 -16
- package/dist/run/providerRetry.js +167 -56
- package/dist/run/providerRetryConfig.js +5 -1
- package/dist/run/runControlRequest.js +50 -0
- package/dist/runtime/agentDriver.js +47 -0
- package/dist/runtime/agentHost.js +327 -0
- package/dist/runtime/builtinAgentDrivers.js +23 -1
- package/dist/runtime/builtinTranscriptObserver.js +4 -0
- package/dist/runtime/builtinTranscriptUsage.js +2 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/globalProcessExitStore.js +38 -0
- package/dist/runtime/launchBroker.js +95 -0
- package/dist/runtime/processExitObservation.js +60 -0
- package/dist/runtime/runtimeBinding.js +6 -0
- package/dist/runtime/runtimeObservation.js +27 -6
- package/dist/runtime/runtimeProjection.js +6 -3
- package/dist/runtime/runtimeStopReceipt.js +42 -0
- package/dist/runtime/sessionTerminationGuard.js +13 -0
- package/dist/runtime/tmuxAdapters.js +203 -220
- package/dist/scheduler/activeRoleRunDelivery.js +24 -3
- package/dist/scheduler/leaderWakeupProcessor.js +18 -60
- package/dist/scheduler/roleRunLiveness.js +61 -27
- package/dist/storage/migration/productionRegistry.js +264 -0
- package/dist/storage/sqliteSchema.js +23 -2
- package/dist/storage/sqliteStore.js +39 -2
- package/dist/storage/taskStore.js +54 -5
- package/dist/storage/upgrade/recordVersions.js +3 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
- package/dist/task/taskRecordReference.js +1 -0
- package/dist/tmux/tmuxManager.js +15 -4
- package/dist/web/assets/client/components.js +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +10 -5
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +61 -0
- package/skills/yui-worker/SKILL.md +82 -218
- package/dist/executor/managedClaudeRunner.js +0 -121
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { appendFileSync, chmodSync, closeSync, fsyncSync, mkdirSync, openSync, rmSync } from "node:fs";
|
|
4
|
+
import { readdir, readFile, rename, unlink } from "node:fs/promises";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { createServer, createConnection } from "node:net";
|
|
7
|
+
import { callController } from "../core/controllerClient.js";
|
|
8
|
+
import { validateAgentHostLaunchPayload } from "./launchBroker.js";
|
|
9
|
+
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
10
|
+
import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
|
|
11
|
+
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v1";
|
|
12
|
+
const HOST_CONTROL_TIMEOUT_MS = 5_000;
|
|
13
|
+
const HOST_CONTROL_MAX_BYTES = 8 * 1024;
|
|
14
|
+
export function serializeAgentHostLaunchControl(control) {
|
|
15
|
+
return JSON.stringify(validateControl(control));
|
|
16
|
+
}
|
|
17
|
+
export async function runAgentHost(input) {
|
|
18
|
+
const hostInstanceId = randomUUID();
|
|
19
|
+
let hostSequence = 0;
|
|
20
|
+
let payload = await redeem(input.home, input.launchId, input.ticket);
|
|
21
|
+
await replayExitOutbox(input.home);
|
|
22
|
+
const control = await openHostControl(input.home, payload);
|
|
23
|
+
try {
|
|
24
|
+
for (;;) {
|
|
25
|
+
while (payload.startMode === "idle") {
|
|
26
|
+
const next = await control.next();
|
|
27
|
+
payload = await redeem(input.home, next.launchId, next.ticket);
|
|
28
|
+
}
|
|
29
|
+
control.setActive(true, payload.launchId);
|
|
30
|
+
const result = await runAgentHostProviderChild(payload);
|
|
31
|
+
control.setActive(false);
|
|
32
|
+
hostSequence += 1;
|
|
33
|
+
const stopReceipt = readRuntimeStopReceipt(input.home, payload.launchId);
|
|
34
|
+
await persistAndSubmitExit(input.home, validateRuntimeProcessExitObservation({
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
observationId: `${hostInstanceId}-${hostSequence}`,
|
|
37
|
+
hostSequence,
|
|
38
|
+
hostInstanceId,
|
|
39
|
+
providerProcessInstanceId: result.processInstanceId,
|
|
40
|
+
...(payload.environment.YUI_TASK_ID === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: { taskId: payload.environment.YUI_TASK_ID }),
|
|
43
|
+
roleName: payload.environment.YUI_ROLE ?? "unknown-role",
|
|
44
|
+
...(payload.environment.YUI_RUN_ID === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { runId: payload.environment.YUI_RUN_ID }),
|
|
47
|
+
launchId: payload.launchId,
|
|
48
|
+
...(payload.environment.YUI_NATIVE_SESSION_ID === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { nativeSessionId: payload.environment.YUI_NATIVE_SESSION_ID }),
|
|
51
|
+
processKind: "provider-child",
|
|
52
|
+
...(result.code === null ? {} : { exitCode: result.code }),
|
|
53
|
+
...(result.signal === null ? {} : { signal: result.signal }),
|
|
54
|
+
...(stopReceipt === null ? {} : { stopReceiptId: stopReceipt.receiptId }),
|
|
55
|
+
observedAt: new Date().toISOString()
|
|
56
|
+
}));
|
|
57
|
+
if (stopReceipt !== null)
|
|
58
|
+
removeRuntimeStopReceipt(input.home, payload.launchId);
|
|
59
|
+
if (result.hostStopRequested)
|
|
60
|
+
return 0;
|
|
61
|
+
// The Agent Host is the durable pane owner. Provider-child exit only
|
|
62
|
+
// moves it to idle; an exact resume launch may arrive over the private
|
|
63
|
+
// control socket without replacing the Host or native conversation.
|
|
64
|
+
const next = await control.next();
|
|
65
|
+
payload = await redeem(input.home, next.launchId, next.ticket);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await control.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function agentHostControlSocketPath(input) {
|
|
73
|
+
const owner = input.scope === "task" ? input.taskId ?? "missing-task" : "global";
|
|
74
|
+
const digest = Buffer.from(`${input.scope}\0${owner}\0${input.roleName}`, "utf8")
|
|
75
|
+
.toString("base64url")
|
|
76
|
+
.slice(0, 80);
|
|
77
|
+
return resolve(join(input.home, "runtime", "agent-host-control", `${digest}.sock`));
|
|
78
|
+
}
|
|
79
|
+
export async function sendAgentHostLaunchControl(input) {
|
|
80
|
+
const path = agentHostControlSocketPath(input);
|
|
81
|
+
return await new Promise((resolvePromise, reject) => {
|
|
82
|
+
const client = createConnection(path);
|
|
83
|
+
let response = "";
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
client.destroy();
|
|
86
|
+
reject(new Error("Agent Host control request timed out."));
|
|
87
|
+
}, HOST_CONTROL_TIMEOUT_MS);
|
|
88
|
+
const settle = (callback, value) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
callback(value);
|
|
91
|
+
};
|
|
92
|
+
client.setEncoding("utf8");
|
|
93
|
+
client.once("connect", () => client.end(`${serializeAgentHostLaunchControl(input.control)}\n`));
|
|
94
|
+
client.on("data", (chunk) => {
|
|
95
|
+
response += chunk;
|
|
96
|
+
if (Buffer.byteLength(response, "utf8") > HOST_CONTROL_MAX_BYTES) {
|
|
97
|
+
client.destroy(new Error("Agent Host control response exceeds its bound."));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
client.once("error", (error) => settle(reject, error));
|
|
101
|
+
client.once("close", () => {
|
|
102
|
+
const value = response.trim();
|
|
103
|
+
if (value === "accepted" || value === "active-same-launch"
|
|
104
|
+
|| value === "active-other-launch")
|
|
105
|
+
settle(resolvePromise, value);
|
|
106
|
+
else
|
|
107
|
+
settle(reject, new Error(`Agent Host control response is invalid: ${value || "empty"}.`));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function redeem(home, launchId, ticket) {
|
|
112
|
+
const result = await callController(home, "runtime.launch-redeem", {
|
|
113
|
+
launchId,
|
|
114
|
+
ticket,
|
|
115
|
+
hostPid: process.pid
|
|
116
|
+
});
|
|
117
|
+
return validateAgentHostLaunchPayload(result);
|
|
118
|
+
}
|
|
119
|
+
export async function runAgentHostProviderChild(payload) {
|
|
120
|
+
const processInstanceId = randomUUID();
|
|
121
|
+
const child = spawn(payload.command, [...payload.args], {
|
|
122
|
+
cwd: payload.cwd,
|
|
123
|
+
env: { ...payload.environment },
|
|
124
|
+
stdio: payload.providerInput === undefined ? "inherit" : ["pipe", "inherit", "inherit"],
|
|
125
|
+
detached: true
|
|
126
|
+
});
|
|
127
|
+
if (payload.providerInput !== undefined) {
|
|
128
|
+
if (child.stdin === null)
|
|
129
|
+
throw new Error("Provider input pipe is unavailable.");
|
|
130
|
+
const providerInput = `${JSON.stringify({
|
|
131
|
+
type: "user",
|
|
132
|
+
message: {
|
|
133
|
+
role: "user",
|
|
134
|
+
content: [{ type: "text", text: payload.providerInput.boundedText }]
|
|
135
|
+
}
|
|
136
|
+
})}\n`;
|
|
137
|
+
child.stdin.end(providerInput, "utf8");
|
|
138
|
+
}
|
|
139
|
+
const forward = (signal) => {
|
|
140
|
+
if (child.pid === undefined)
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
process.kill(-child.pid, signal);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code !== "ESRCH")
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
151
|
+
const handlers = new Map();
|
|
152
|
+
let hostStopRequested = false;
|
|
153
|
+
let forceKillTimer;
|
|
154
|
+
for (const signal of signals) {
|
|
155
|
+
const handler = () => {
|
|
156
|
+
hostStopRequested = true;
|
|
157
|
+
forward(signal);
|
|
158
|
+
forceKillTimer ??= setTimeout(() => forward("SIGKILL"), 10_000);
|
|
159
|
+
forceKillTimer.unref();
|
|
160
|
+
};
|
|
161
|
+
handlers.set(signal, handler);
|
|
162
|
+
process.on(signal, handler);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return await new Promise((resolve, reject) => {
|
|
166
|
+
child.once("error", reject);
|
|
167
|
+
child.once("close", (code, signal) => {
|
|
168
|
+
resolve({ code, signal, processInstanceId, hostStopRequested });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
for (const [signal, handler] of handlers)
|
|
174
|
+
process.removeListener(signal, handler);
|
|
175
|
+
if (forceKillTimer !== undefined)
|
|
176
|
+
clearTimeout(forceKillTimer);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function persistAndSubmitExit(home, observation) {
|
|
180
|
+
const directory = resolve(join(home, "runtime", "agent-host-outbox"));
|
|
181
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
182
|
+
const path = join(directory, `${observation.hostInstanceId}.jsonl`);
|
|
183
|
+
const descriptor = openSync(path, "a", 0o600);
|
|
184
|
+
try {
|
|
185
|
+
appendFileSync(descriptor, `${JSON.stringify(observation)}\n`, "utf8");
|
|
186
|
+
fsyncSync(descriptor);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
closeSync(descriptor);
|
|
190
|
+
}
|
|
191
|
+
chmodSync(path, 0o600);
|
|
192
|
+
await callController(home, "runtime.process-exit-observe", observation);
|
|
193
|
+
rmSync(path, { force: true });
|
|
194
|
+
}
|
|
195
|
+
async function replayExitOutbox(home) {
|
|
196
|
+
const directory = resolve(join(home, "runtime", "agent-host-outbox"));
|
|
197
|
+
let entries;
|
|
198
|
+
try {
|
|
199
|
+
entries = await readdir(directory);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error.code === "ENOENT")
|
|
203
|
+
return;
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
for (const name of entries.filter((value) => value.includes(".jsonl")).sort()) {
|
|
207
|
+
const path = join(directory, name);
|
|
208
|
+
const claimed = `${path}.claim-${process.pid}-${randomUUID()}`;
|
|
209
|
+
try {
|
|
210
|
+
await rename(path, claimed);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (error.code === "ENOENT")
|
|
214
|
+
continue;
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
const lines = (await readFile(claimed, "utf8")).split("\n").filter(Boolean);
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
const observation = validateRuntimeProcessExitObservation(JSON.parse(line));
|
|
220
|
+
await callController(home, "runtime.process-exit-observe", observation);
|
|
221
|
+
}
|
|
222
|
+
await unlink(claimed);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function openHostControl(home, payload) {
|
|
226
|
+
const directory = resolve(join(home, "runtime", "agent-host-control"));
|
|
227
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
228
|
+
const path = agentHostControlSocketPath({
|
|
229
|
+
home,
|
|
230
|
+
scope: payload.environment.YUI_SESSION_SCOPE ?? "task",
|
|
231
|
+
...(payload.environment.YUI_TASK_ID === undefined
|
|
232
|
+
? {}
|
|
233
|
+
: { taskId: payload.environment.YUI_TASK_ID }),
|
|
234
|
+
roleName: payload.environment.YUI_ROLE ?? "unknown-role"
|
|
235
|
+
});
|
|
236
|
+
if (await hostControlSocketIsLive(path)) {
|
|
237
|
+
throw new Error(`Agent Host control socket is already owned: ${path}.`);
|
|
238
|
+
}
|
|
239
|
+
rmSync(path, { force: true });
|
|
240
|
+
let active = false;
|
|
241
|
+
let activeLaunchId;
|
|
242
|
+
const queued = [];
|
|
243
|
+
const waiters = [];
|
|
244
|
+
const server = createServer((socket) => {
|
|
245
|
+
socket.setEncoding("utf8");
|
|
246
|
+
let body = "";
|
|
247
|
+
socket.on("data", (chunk) => {
|
|
248
|
+
body += chunk;
|
|
249
|
+
if (Buffer.byteLength(body, "utf8") > HOST_CONTROL_MAX_BYTES) {
|
|
250
|
+
socket.destroy(new Error("Agent Host control request exceeds its bound."));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
socket.once("end", () => {
|
|
254
|
+
try {
|
|
255
|
+
const control = validateControl(JSON.parse(body.trim()));
|
|
256
|
+
if (active || queued.length > 0) {
|
|
257
|
+
socket.end(activeLaunchId === control.launchId
|
|
258
|
+
? "active-same-launch\n"
|
|
259
|
+
: "active-other-launch\n");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const waiter = waiters.shift();
|
|
263
|
+
if (waiter === undefined)
|
|
264
|
+
queued.push(control);
|
|
265
|
+
else
|
|
266
|
+
waiter(control);
|
|
267
|
+
socket.end("accepted\n");
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
socket.destroy(error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
await new Promise((resolvePromise, reject) => {
|
|
275
|
+
server.once("error", reject);
|
|
276
|
+
server.listen(path, () => resolvePromise());
|
|
277
|
+
});
|
|
278
|
+
chmodSync(path, 0o600);
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
setActive(value, launchId) {
|
|
281
|
+
active = value;
|
|
282
|
+
activeLaunchId = value ? launchId : undefined;
|
|
283
|
+
},
|
|
284
|
+
next() {
|
|
285
|
+
const available = queued.shift();
|
|
286
|
+
if (available !== undefined)
|
|
287
|
+
return Promise.resolve(available);
|
|
288
|
+
return new Promise((resolvePromise) => waiters.push(resolvePromise));
|
|
289
|
+
},
|
|
290
|
+
close: async () => {
|
|
291
|
+
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
292
|
+
rmSync(path, { force: true });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async function hostControlSocketIsLive(path) {
|
|
297
|
+
return await new Promise((resolvePromise, reject) => {
|
|
298
|
+
const client = createConnection(path);
|
|
299
|
+
const timer = setTimeout(() => finish(false), 1_000);
|
|
300
|
+
const finish = (value) => {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
client.removeAllListeners();
|
|
303
|
+
client.destroy();
|
|
304
|
+
resolvePromise(value);
|
|
305
|
+
};
|
|
306
|
+
client.once("connect", () => finish(true));
|
|
307
|
+
client.once("error", (error) => {
|
|
308
|
+
if (error.code === "ENOENT" || error.code === "ECONNREFUSED")
|
|
309
|
+
finish(false);
|
|
310
|
+
else
|
|
311
|
+
reject(error);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function validateControl(control) {
|
|
316
|
+
if (control.protocol !== AGENT_HOST_CONTROL_PROTOCOL || control.type !== "launch") {
|
|
317
|
+
throw new Error("Agent Host control protocol is invalid.");
|
|
318
|
+
}
|
|
319
|
+
if (typeof control.launchId !== "string" || control.launchId.length === 0
|
|
320
|
+
|| control.launchId.length > 256 || control.launchId.includes("\0")) {
|
|
321
|
+
throw new Error("Agent Host launch control identity is invalid.");
|
|
322
|
+
}
|
|
323
|
+
if (typeof control.ticket !== "string" || !/^[a-f0-9]{64}$/u.test(control.ticket)) {
|
|
324
|
+
throw new Error("Agent Host launch control ticket is invalid.");
|
|
325
|
+
}
|
|
326
|
+
return Object.freeze({ ...control });
|
|
327
|
+
}
|
|
@@ -8,6 +8,18 @@ export function builtinDriverIdForAdapter(adapterId) {
|
|
|
8
8
|
}
|
|
9
9
|
const STRUCTURED_CLI_CAPABILITIES = Object.freeze({
|
|
10
10
|
surfaces: Object.freeze(["interactive-cli"]),
|
|
11
|
+
lifecycle: Object.freeze({
|
|
12
|
+
host: "persistent",
|
|
13
|
+
providerProcess: "persistent",
|
|
14
|
+
nativeConversationResume: "exact",
|
|
15
|
+
// Yui deliberately does not guess provider compaction behavior from token
|
|
16
|
+
// counters. A future Driver may upgrade these only with exact native facts.
|
|
17
|
+
compaction: "unknown",
|
|
18
|
+
compactionEvents: "unavailable",
|
|
19
|
+
contextUsage: "cumulative-only",
|
|
20
|
+
inSessionContinuation: true,
|
|
21
|
+
deliveryDeduplication: "unsupported"
|
|
22
|
+
}),
|
|
11
23
|
control: Object.freeze({
|
|
12
24
|
start: true,
|
|
13
25
|
resume: true,
|
|
@@ -55,6 +67,10 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
|
|
|
55
67
|
adapterId: "claude",
|
|
56
68
|
capabilities: Object.freeze({
|
|
57
69
|
...STRUCTURED_CLI_CAPABILITIES,
|
|
70
|
+
lifecycle: Object.freeze({
|
|
71
|
+
...STRUCTURED_CLI_CAPABILITIES.lifecycle,
|
|
72
|
+
providerProcess: "per-turn"
|
|
73
|
+
}),
|
|
58
74
|
observation: Object.freeze({
|
|
59
75
|
...STRUCTURED_CLI_CAPABILITIES.observation,
|
|
60
76
|
sessionBootstrap: "preallocated",
|
|
@@ -354,6 +370,11 @@ function claudeFailure(payload) {
|
|
|
354
370
|
const details = optionalText(payload.error_details);
|
|
355
371
|
const lastOutput = optionalText(payload.last_assistant_message);
|
|
356
372
|
const parsed = parseClaudeError(code, details);
|
|
373
|
+
const retryAfterMs = typeof payload.retry_after_ms === "number"
|
|
374
|
+
&& Number.isSafeInteger(payload.retry_after_ms)
|
|
375
|
+
&& payload.retry_after_ms > 0
|
|
376
|
+
? payload.retry_after_ms
|
|
377
|
+
: undefined;
|
|
357
378
|
return {
|
|
358
379
|
failure: {
|
|
359
380
|
...(parsed.code !== "unknown" ? { errorCode: parsed.code } : {}),
|
|
@@ -362,7 +383,8 @@ function claudeFailure(payload) {
|
|
|
362
383
|
...(lastOutput === undefined ? {} : { lastOutput }),
|
|
363
384
|
...(payload.run_terminal === true || payload.unrecoverable === true
|
|
364
385
|
? { runTerminal: true }
|
|
365
|
-
: {})
|
|
386
|
+
: {}),
|
|
387
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs })
|
|
366
388
|
},
|
|
367
389
|
summary: [
|
|
368
390
|
"Agent turn failed.",
|
|
@@ -199,6 +199,7 @@ function normalizedCodexUsage(usage) {
|
|
|
199
199
|
const cachedInputTokens = integer(usage?.cached_input_tokens);
|
|
200
200
|
const reasoningTokens = integer(usage?.reasoning_output_tokens);
|
|
201
201
|
return Object.freeze({
|
|
202
|
+
semantics: "cumulative-session",
|
|
202
203
|
inputTokens,
|
|
203
204
|
outputTokens,
|
|
204
205
|
...(cachedInputTokens === undefined ? {} : { cachedInputTokens }),
|
|
@@ -214,6 +215,7 @@ function normalizedClaudeUsage(usage) {
|
|
|
214
215
|
const cacheCreated = integer(usage?.cache_creation_input_tokens) ?? 0;
|
|
215
216
|
const reasoningTokens = integer(object(usage?.output_tokens_details)?.thinking_tokens);
|
|
216
217
|
return Object.freeze({
|
|
218
|
+
semantics: "cumulative-session",
|
|
217
219
|
inputTokens: directInput + cacheRead + cacheCreated,
|
|
218
220
|
outputTokens,
|
|
219
221
|
cachedInputTokens: cacheRead + cacheCreated,
|
|
@@ -239,6 +241,7 @@ function usageFrom(value) {
|
|
|
239
241
|
const cachedInputTokens = integer(raw?.cachedInputTokens);
|
|
240
242
|
const reasoningTokens = integer(raw?.reasoningTokens);
|
|
241
243
|
return Object.freeze({
|
|
244
|
+
semantics: "cumulative-session",
|
|
242
245
|
inputTokens,
|
|
243
246
|
outputTokens,
|
|
244
247
|
...(cachedInputTokens === undefined ? {} : { cachedInputTokens }),
|
|
@@ -259,6 +262,7 @@ function sumUsage(values) {
|
|
|
259
262
|
reasoningTokens += usage.reasoningTokens ?? 0;
|
|
260
263
|
}
|
|
261
264
|
return Object.freeze({
|
|
265
|
+
semantics: "cumulative-session",
|
|
262
266
|
inputTokens,
|
|
263
267
|
outputTokens,
|
|
264
268
|
cachedInputTokens,
|
|
@@ -3,6 +3,7 @@ export function codexTranscriptUsage(transcript) {
|
|
|
3
3
|
if (report === null)
|
|
4
4
|
return null;
|
|
5
5
|
return Object.freeze({
|
|
6
|
+
semantics: "cumulative-session",
|
|
6
7
|
inputTokens: report.uncachedInputTokens + report.cacheReadTokens + report.cacheCreatedTokens,
|
|
7
8
|
outputTokens: report.outputTokens,
|
|
8
9
|
...(report.cacheReadTokens + report.cacheCreatedTokens === 0
|
|
@@ -62,6 +63,7 @@ export function claudeTranscriptUsage(transcript) {
|
|
|
62
63
|
if (report === null)
|
|
63
64
|
return null;
|
|
64
65
|
return Object.freeze({
|
|
66
|
+
semantics: "cumulative-session",
|
|
65
67
|
inputTokens: report.uncachedInputTokens + report.cacheReadTokens + report.cacheCreatedTokens,
|
|
66
68
|
outputTokens: report.outputTokens,
|
|
67
69
|
...(report.cacheReadTokens + report.cacheCreatedTokens === 0
|
|
@@ -7,7 +7,7 @@ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
|
7
7
|
import { yuiVersionIdentity } from "../version.js";
|
|
8
8
|
import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "./lifecycleReservation.js";
|
|
9
9
|
import { nativeSessionIdForLaunch } from "./preallocatedNativeSession.js";
|
|
10
|
-
import {
|
|
10
|
+
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
11
11
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
12
12
|
import { readActiveReleasePointer } from "../release/runtimeRelease.js";
|
|
13
13
|
export const EXACT_CONTROL_ARGUMENT = "--yui-control";
|
|
@@ -297,7 +297,7 @@ export function assertExactTaskRuntimeState(runtime, store, options = {}) {
|
|
|
297
297
|
&& (sessions?.inFlight === null || sessions?.inFlight === undefined);
|
|
298
298
|
if (runtime.runId !== undefined && (sessions?.inFlight?.agentId !== runtime.agentId
|
|
299
299
|
|| sessions.inFlight.runId !== runtime.runId
|
|
300
|
-
|| sessions.inFlight.receiptId !==
|
|
300
|
+
|| sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)) && !preallocatedBeforeInFlightProjection) {
|
|
301
301
|
throw new Error("Exact Task runtime in-flight Run fence is not current.");
|
|
302
302
|
}
|
|
303
303
|
if (runtime.launchId === undefined || (!reservation && !sessionLaunch)) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
4
|
+
/** Immutable low-volume audit for GlobalRoles, which have no Task Event family. */
|
|
5
|
+
export function appendGlobalProcessExitObservation(home, observation, classification) {
|
|
6
|
+
validateRuntimeProcessExitObservation(observation);
|
|
7
|
+
if (observation.taskId !== undefined) {
|
|
8
|
+
throw new Error("Task process exits must use the Task Event store.");
|
|
9
|
+
}
|
|
10
|
+
const directory = resolve(join(home, "runtime"));
|
|
11
|
+
const path = join(directory, "global-process-exits.jsonl");
|
|
12
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
13
|
+
if (existsSync(path)) {
|
|
14
|
+
const duplicate = readFileSync(path, "utf8").split("\n").some((line) => {
|
|
15
|
+
if (line.length === 0)
|
|
16
|
+
return false;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(line).observationId
|
|
19
|
+
=== observation.observationId;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error("Global process-exit audit is malformed.");
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (duplicate)
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const descriptor = openSync(path, "a", 0o600);
|
|
29
|
+
try {
|
|
30
|
+
appendFileSync(descriptor, `${JSON.stringify({ ...observation, classification })}\n`, "utf8");
|
|
31
|
+
fsyncSync(descriptor);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
closeSync(descriptor);
|
|
35
|
+
}
|
|
36
|
+
chmodSync(path, 0o600);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const brokers = new Map();
|
|
4
|
+
const TICKET_TTL_MS = 60_000;
|
|
5
|
+
/** One Controller-process broker per canonical Home. Payloads never hit disk or tmux. */
|
|
6
|
+
export function launchBrokerForHome(home) {
|
|
7
|
+
const key = resolve(home);
|
|
8
|
+
const existing = brokers.get(key);
|
|
9
|
+
if (existing !== undefined)
|
|
10
|
+
return existing;
|
|
11
|
+
const broker = new LaunchBroker();
|
|
12
|
+
brokers.set(key, broker);
|
|
13
|
+
return broker;
|
|
14
|
+
}
|
|
15
|
+
export class LaunchBroker {
|
|
16
|
+
#reservations = new Map();
|
|
17
|
+
reserve(payload) {
|
|
18
|
+
validatePayload(payload);
|
|
19
|
+
if (this.#reservations.has(payload.launchId)) {
|
|
20
|
+
throw new Error(`Launch payload is already reserved: ${payload.launchId}.`);
|
|
21
|
+
}
|
|
22
|
+
const ticket = randomBytes(32).toString("hex");
|
|
23
|
+
this.#reservations.set(payload.launchId, Object.freeze({
|
|
24
|
+
ticket,
|
|
25
|
+
payload,
|
|
26
|
+
createdAt: Date.now()
|
|
27
|
+
}));
|
|
28
|
+
return Object.freeze({ launchId: payload.launchId, ticket });
|
|
29
|
+
}
|
|
30
|
+
redeem(launchId, ticket) {
|
|
31
|
+
const reservation = this.#reservations.get(launchId);
|
|
32
|
+
if (reservation === undefined || reservation.ticket !== ticket) {
|
|
33
|
+
throw new Error("Launch ticket is invalid or already consumed.");
|
|
34
|
+
}
|
|
35
|
+
this.#reservations.delete(launchId);
|
|
36
|
+
if (Date.now() - reservation.createdAt > TICKET_TTL_MS) {
|
|
37
|
+
throw new Error("Launch ticket expired before redemption.");
|
|
38
|
+
}
|
|
39
|
+
return reservation.payload;
|
|
40
|
+
}
|
|
41
|
+
revoke(launchId) {
|
|
42
|
+
this.#reservations.delete(launchId);
|
|
43
|
+
}
|
|
44
|
+
pendingCount() {
|
|
45
|
+
return this.#reservations.size;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function validateAgentHostLaunchPayload(value) {
|
|
49
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
50
|
+
throw new Error("Agent Host launch payload must be an object.");
|
|
51
|
+
}
|
|
52
|
+
return validatePayload(value);
|
|
53
|
+
}
|
|
54
|
+
function validatePayload(payload) {
|
|
55
|
+
if (payload.schemaVersion !== 1)
|
|
56
|
+
throw new Error("Agent Host launch payload version is invalid.");
|
|
57
|
+
text(payload.launchId, "launchId");
|
|
58
|
+
text(payload.command, "command");
|
|
59
|
+
text(payload.cwd, "cwd");
|
|
60
|
+
if (!Array.isArray(payload.args))
|
|
61
|
+
throw new Error("Agent Host launch args must be an array.");
|
|
62
|
+
payload.args.forEach((value) => text(value, "argument"));
|
|
63
|
+
if (payload.environment === null || typeof payload.environment !== "object") {
|
|
64
|
+
throw new Error("Agent Host launch environment must be an object.");
|
|
65
|
+
}
|
|
66
|
+
for (const [key, value] of Object.entries(payload.environment)) {
|
|
67
|
+
text(key, "environment key");
|
|
68
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
69
|
+
throw new Error("Agent Host launch environment value is invalid.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (payload.childLifecycle !== "persistent" && payload.childLifecycle !== "per-turn") {
|
|
73
|
+
throw new Error("Agent Host child lifecycle is invalid.");
|
|
74
|
+
}
|
|
75
|
+
if (payload.startMode !== "provider" && payload.startMode !== "idle") {
|
|
76
|
+
throw new Error("Agent Host start mode is invalid.");
|
|
77
|
+
}
|
|
78
|
+
if (payload.providerInput !== undefined) {
|
|
79
|
+
if (payload.providerInput.kind !== "stdin-json-user-message") {
|
|
80
|
+
throw new Error("Agent Host Provider input transport is invalid.");
|
|
81
|
+
}
|
|
82
|
+
if (typeof payload.providerInput.boundedText !== "string"
|
|
83
|
+
|| payload.providerInput.boundedText.includes("\0")
|
|
84
|
+
|| Buffer.byteLength(payload.providerInput.boundedText, "utf8") > 4 * 1024) {
|
|
85
|
+
throw new Error("Agent Host Provider input must be bounded bootstrap text.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return payload;
|
|
89
|
+
}
|
|
90
|
+
function text(value, label) {
|
|
91
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
92
|
+
throw new Error(`Agent Host ${label} is invalid.`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const RUNTIME_PROCESS_EXIT_SCHEMA_VERSION = 1;
|
|
2
|
+
export function validateRuntimeProcessExitObservation(observation) {
|
|
3
|
+
if (observation.schemaVersion !== RUNTIME_PROCESS_EXIT_SCHEMA_VERSION) {
|
|
4
|
+
throw new Error("Runtime process-exit observation version is invalid.");
|
|
5
|
+
}
|
|
6
|
+
identity(observation.observationId, "observationId");
|
|
7
|
+
identity(observation.hostInstanceId, "hostInstanceId");
|
|
8
|
+
optionalIdentity(observation.providerProcessInstanceId, "providerProcessInstanceId");
|
|
9
|
+
optionalIdentity(observation.taskId, "taskId");
|
|
10
|
+
identity(observation.roleName, "roleName");
|
|
11
|
+
optionalIdentity(observation.runId, "runId");
|
|
12
|
+
identity(observation.launchId, "launchId");
|
|
13
|
+
optionalIdentity(observation.nativeSessionId, "nativeSessionId");
|
|
14
|
+
if (observation.processKind !== "agent-host" && observation.processKind !== "provider-child") {
|
|
15
|
+
throw new Error("Runtime process kind is invalid.");
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isSafeInteger(observation.hostSequence) || observation.hostSequence < 1) {
|
|
18
|
+
throw new Error("Runtime host sequence is invalid.");
|
|
19
|
+
}
|
|
20
|
+
if (observation.exitCode !== undefined
|
|
21
|
+
&& (!Number.isSafeInteger(observation.exitCode) || observation.exitCode < 0)) {
|
|
22
|
+
throw new Error("Runtime process exit code is invalid.");
|
|
23
|
+
}
|
|
24
|
+
optionalIdentity(observation.signal, "signal");
|
|
25
|
+
if (!Number.isFinite(Date.parse(observation.observedAt))) {
|
|
26
|
+
throw new Error("Runtime process observedAt is invalid.");
|
|
27
|
+
}
|
|
28
|
+
optionalIdentity(observation.stopReceiptId, "stopReceiptId");
|
|
29
|
+
optionalIdentity(observation.lastProviderEventId, "lastProviderEventId");
|
|
30
|
+
optionalIdentity(observation.diagnosticTailRef, "diagnosticTailRef");
|
|
31
|
+
return Object.freeze({ ...observation });
|
|
32
|
+
}
|
|
33
|
+
export function classifyRuntimeProcessExit(observation, input) {
|
|
34
|
+
validateRuntimeProcessExitObservation(observation);
|
|
35
|
+
if (observation.stopReceiptId !== undefined)
|
|
36
|
+
return "yui-requested-stop";
|
|
37
|
+
if (observation.processKind === "provider-child" && input.turnFailureObserved === true) {
|
|
38
|
+
return "provider-turn-failed";
|
|
39
|
+
}
|
|
40
|
+
if (observation.processKind === "provider-child"
|
|
41
|
+
&& input.childLifecycle === "per-turn"
|
|
42
|
+
&& observation.exitCode === 0
|
|
43
|
+
&& input.turnTerminalObserved === true) {
|
|
44
|
+
return "expected-per-turn-exit";
|
|
45
|
+
}
|
|
46
|
+
if ((observation.exitCode !== undefined && observation.exitCode !== 0)
|
|
47
|
+
|| observation.signal !== undefined) {
|
|
48
|
+
return "host-abnormal";
|
|
49
|
+
}
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
function identity(value, label) {
|
|
53
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
54
|
+
throw new Error(`Runtime process ${label} is invalid.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function optionalIdentity(value, label) {
|
|
58
|
+
if (value !== undefined)
|
|
59
|
+
identity(value, label);
|
|
60
|
+
}
|
|
@@ -10,6 +10,9 @@ export function createRuntimeBinding(input) {
|
|
|
10
10
|
if (initialPromptRunId !== undefined && hostCreated !== true) {
|
|
11
11
|
throw new TypeError("An initial prompt Run id requires a newly-created runtime host.");
|
|
12
12
|
}
|
|
13
|
+
const launchPromptUncertainRunId = input.launchPromptUncertainRunId === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: requireSafeIdentity(input.launchPromptUncertainRunId, "Uncertain launch prompt Run id");
|
|
13
16
|
return {
|
|
14
17
|
id: requireSafeIdentity(input.id, "Runtime binding id"),
|
|
15
18
|
launchId: requireSafeIdentity(input.launchId, "Launch id"),
|
|
@@ -19,6 +22,9 @@ export function createRuntimeBinding(input) {
|
|
|
19
22
|
hostRef: requireText(input.hostRef, "Session host reference"),
|
|
20
23
|
...(hostCreated === undefined ? {} : { hostCreated }),
|
|
21
24
|
...(initialPromptRunId === undefined ? {} : { initialPromptRunId }),
|
|
25
|
+
...(launchPromptUncertainRunId === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { launchPromptUncertainRunId }),
|
|
22
28
|
...(input.nativeSessionId === undefined
|
|
23
29
|
? {}
|
|
24
30
|
: { nativeSessionId: requireText(input.nativeSessionId, "Native session id") })
|