@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.
Files changed (69) 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/executor/agentAdapter.js +7 -2
  24. package/dist/executor/agentExecutor.js +23 -0
  25. package/dist/executor/effectiveLaunch.js +24 -0
  26. package/dist/executor/executorRegistry.js +7 -1
  27. package/dist/executor/fileRoleLaunchPlanner.js +73 -27
  28. package/dist/lifecycle/exactRunTerminalization.js +2 -3
  29. package/dist/lifecycle/providerErrorClass.js +8 -3
  30. package/dist/observability/executionAudit.js +87 -2
  31. package/dist/repository/taskWorkspacePreparer.js +2 -2
  32. package/dist/run/agentRun.js +101 -16
  33. package/dist/run/providerRetry.js +167 -56
  34. package/dist/run/providerRetryConfig.js +5 -1
  35. package/dist/run/runControlRequest.js +50 -0
  36. package/dist/runtime/agentDriver.js +47 -0
  37. package/dist/runtime/agentHost.js +327 -0
  38. package/dist/runtime/builtinAgentDrivers.js +23 -1
  39. package/dist/runtime/builtinTranscriptObserver.js +4 -0
  40. package/dist/runtime/builtinTranscriptUsage.js +2 -0
  41. package/dist/runtime/exactControlPlane.js +2 -2
  42. package/dist/runtime/globalProcessExitStore.js +38 -0
  43. package/dist/runtime/launchBroker.js +95 -0
  44. package/dist/runtime/processExitObservation.js +60 -0
  45. package/dist/runtime/runtimeBinding.js +6 -0
  46. package/dist/runtime/runtimeObservation.js +27 -6
  47. package/dist/runtime/runtimeProjection.js +6 -3
  48. package/dist/runtime/runtimeStopReceipt.js +42 -0
  49. package/dist/runtime/sessionTerminationGuard.js +13 -0
  50. package/dist/runtime/tmuxAdapters.js +203 -220
  51. package/dist/scheduler/activeRoleRunDelivery.js +24 -3
  52. package/dist/scheduler/leaderWakeupProcessor.js +18 -60
  53. package/dist/scheduler/roleRunLiveness.js +61 -27
  54. package/dist/storage/migration/productionRegistry.js +264 -0
  55. package/dist/storage/sqliteSchema.js +23 -2
  56. package/dist/storage/sqliteStore.js +39 -2
  57. package/dist/storage/taskStore.js +54 -5
  58. package/dist/storage/upgrade/recordVersions.js +3 -1
  59. package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
  60. package/dist/task/taskRecordReference.js +1 -0
  61. package/dist/tmux/tmuxManager.js +15 -4
  62. package/dist/web/assets/client/components.js +1 -1
  63. package/package.json +1 -1
  64. package/skills/yui-leader/SKILL.md +10 -5
  65. package/skills/yui-operator/SKILL.md +4 -0
  66. package/skills/yui-reviewer/SKILL.md +4 -0
  67. package/skills/yui-runtime/SKILL.md +61 -0
  68. package/skills/yui-worker/SKILL.md +82 -218
  69. 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 { 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
+ }
@@ -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") })