@zq-silk/yui 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/cli/commandCatalog.js +3 -7
  2. package/dist/cli.js +12 -33
  3. package/dist/commands/executionAuditCommands.js +19 -0
  4. package/dist/commands/globalRoleCommands.js +70 -0
  5. package/dist/commands/taskActor.js +3 -2
  6. package/dist/commands/taskCommands.js +160 -41
  7. package/dist/commands/taskContextCommand.js +1 -1
  8. package/dist/commands/taskInputCommands.js +3 -2
  9. package/dist/commands/taskRoleRuntimeStatus.js +3 -3
  10. package/dist/context/contextSnapshot.js +228 -0
  11. package/dist/context/roleSessionContext.js +3 -1
  12. package/dist/context/runContextContract.js +162 -0
  13. package/dist/context/runContextPack.js +322 -0
  14. package/dist/context/sessionBootstrapManifest.js +81 -0
  15. package/dist/context/sessionProtocolIdentity.js +23 -0
  16. package/dist/controller/agentRuntimeObserver.js +6 -1
  17. package/dist/controller/controller.js +4 -3
  18. package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
  19. package/dist/controller/jobControl.js +2 -1
  20. package/dist/controller/runtime.js +83 -0
  21. package/dist/controller/runtimeHookRunFence.js +6 -2
  22. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  23. package/dist/coordination/workMailbox.js +25 -22
  24. package/dist/executor/agentAdapter.js +7 -2
  25. package/dist/executor/agentExecutor.js +23 -0
  26. package/dist/executor/effectiveLaunch.js +24 -0
  27. package/dist/executor/executorRegistry.js +7 -1
  28. package/dist/executor/fileRoleLaunchPlanner.js +73 -27
  29. package/dist/lifecycle/exactRunTerminalization.js +2 -3
  30. package/dist/lifecycle/providerErrorClass.js +8 -3
  31. package/dist/observability/executionAudit.js +87 -2
  32. package/dist/repository/taskWorkspacePreparer.js +2 -2
  33. package/dist/run/agentRun.js +101 -16
  34. package/dist/run/providerRetry.js +167 -56
  35. package/dist/run/providerRetryConfig.js +5 -1
  36. package/dist/run/runControlRequest.js +50 -0
  37. package/dist/runtime/agentDriver.js +47 -0
  38. package/dist/runtime/agentHost.js +327 -0
  39. package/dist/runtime/builtinAgentDrivers.js +23 -1
  40. package/dist/runtime/builtinTranscriptObserver.js +4 -0
  41. package/dist/runtime/builtinTranscriptUsage.js +2 -0
  42. package/dist/runtime/exactControlPlane.js +2 -2
  43. package/dist/runtime/globalProcessExitStore.js +38 -0
  44. package/dist/runtime/launchBroker.js +95 -0
  45. package/dist/runtime/processExitObservation.js +60 -0
  46. package/dist/runtime/runtimeBinding.js +6 -0
  47. package/dist/runtime/runtimeObservation.js +27 -6
  48. package/dist/runtime/runtimeProjection.js +6 -3
  49. package/dist/runtime/runtimeStopReceipt.js +42 -0
  50. package/dist/runtime/sessionTerminationGuard.js +13 -0
  51. package/dist/runtime/tmuxAdapters.js +203 -220
  52. package/dist/scheduler/activeRoleRunDelivery.js +24 -3
  53. package/dist/scheduler/leaderWakeupProcessor.js +18 -60
  54. package/dist/scheduler/roleRunLiveness.js +61 -27
  55. package/dist/storage/migration/productionRegistry.js +264 -0
  56. package/dist/storage/sqliteSchema.js +23 -2
  57. package/dist/storage/sqliteStore.js +48 -4
  58. package/dist/storage/taskStore.js +54 -5
  59. package/dist/storage/upgrade/recordVersions.js +3 -1
  60. package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
  61. package/dist/task/taskRecordReference.js +1 -0
  62. package/dist/tmux/tmuxManager.js +15 -4
  63. package/dist/web/assets/client/components.js +1 -1
  64. package/package.json +1 -1
  65. package/skills/yui-leader/SKILL.md +10 -5
  66. package/skills/yui-operator/SKILL.md +4 -0
  67. package/skills/yui-reviewer/SKILL.md +4 -0
  68. package/skills/yui-runtime/SKILL.md +61 -0
  69. package/skills/yui-worker/SKILL.md +82 -218
  70. package/dist/executor/managedClaudeRunner.js +0 -121
@@ -41,6 +41,36 @@ export function validateAgentDriverCapabilities(input) {
41
41
  throw new Error(`Agent Driver control capability ${name} must be boolean.`);
42
42
  }
43
43
  }
44
+ const lifecycle = input.lifecycle;
45
+ if (lifecycle === null || typeof lifecycle !== "object" || Array.isArray(lifecycle)) {
46
+ throw new Error("Agent Driver lifecycle capabilities must be an object.");
47
+ }
48
+ if (lifecycle.host !== "persistent") {
49
+ throw new Error("Managed Agent Host lifecycle must be persistent.");
50
+ }
51
+ if (lifecycle.providerProcess !== "persistent" && lifecycle.providerProcess !== "per-turn") {
52
+ throw new Error("Provider process lifecycle capability is invalid.");
53
+ }
54
+ if (lifecycle.nativeConversationResume !== "exact"
55
+ && lifecycle.nativeConversationResume !== "unsupported") {
56
+ throw new Error("Native conversation resume capability is invalid.");
57
+ }
58
+ if (!["automatic", "native-explicit", "unsupported", "unknown"].includes(lifecycle.compaction)) {
59
+ throw new Error("Native compaction capability is invalid.");
60
+ }
61
+ if (lifecycle.compactionEvents !== "exact" && lifecycle.compactionEvents !== "unavailable") {
62
+ throw new Error("Native compaction event capability is invalid.");
63
+ }
64
+ if (!["per-request", "remaining-context", "cumulative-only", "unavailable"].includes(lifecycle.contextUsage)) {
65
+ throw new Error("Context usage capability is invalid.");
66
+ }
67
+ if (typeof lifecycle.inSessionContinuation !== "boolean") {
68
+ throw new Error("In-Session continuation capability must be boolean.");
69
+ }
70
+ if (lifecycle.deliveryDeduplication !== "exact"
71
+ && lifecycle.deliveryDeduplication !== "unsupported") {
72
+ throw new Error("Delivery deduplication capability is invalid.");
73
+ }
44
74
  const observation = input.observation;
45
75
  if (observation === null || typeof observation !== "object" || Array.isArray(observation)) {
46
76
  throw new Error("Agent Driver observation capabilities must be an object.");
@@ -92,6 +122,16 @@ export function validateAgentDriverCapabilities(input) {
92
122
  }
93
123
  return Object.freeze({
94
124
  surfaces: Object.freeze(surfaces),
125
+ lifecycle: Object.freeze({
126
+ host: lifecycle.host,
127
+ providerProcess: lifecycle.providerProcess,
128
+ nativeConversationResume: lifecycle.nativeConversationResume,
129
+ compaction: lifecycle.compaction,
130
+ compactionEvents: lifecycle.compactionEvents,
131
+ contextUsage: lifecycle.contextUsage,
132
+ inSessionContinuation: lifecycle.inSessionContinuation,
133
+ deliveryDeduplication: lifecycle.deliveryDeduplication
134
+ }),
95
135
  control: Object.freeze({
96
136
  start: control.start,
97
137
  resume: control.resume,
@@ -149,6 +189,13 @@ export function managedRuntimeAdmission(capabilities) {
149
189
  if (actual.observation.promptAcceptance !== "exact"
150
190
  || actual.input.acceptance !== "exact")
151
191
  missing.push("exact-prompt-acceptance");
192
+ if (actual.lifecycle.host !== "persistent")
193
+ missing.push("persistent-agent-host");
194
+ if (actual.lifecycle.nativeConversationResume !== "exact") {
195
+ missing.push("exact-native-conversation-resume");
196
+ }
197
+ if (!actual.lifecycle.inSessionContinuation)
198
+ missing.push("in-session-continuation");
152
199
  if (actual.observation.turnLifecycle !== "exact")
153
200
  missing.push("exact-turn-lifecycle");
154
201
  return missing.length === 0
@@ -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 { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
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 !== formatAgentRunReceiptId(runtime.taskId, runtime.runId)) && !preallocatedBeforeInFlightProjection) {
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
+ }