@zq-silk/yui 0.8.1 → 0.8.2

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 (38) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +35 -46
  3. package/dist/cli/commandCatalog.js +49 -14
  4. package/dist/cli/interactionPolicy.js +4 -10
  5. package/dist/cli/invocationRouter.js +2 -1
  6. package/dist/cli.js +73 -21
  7. package/dist/commands/taskCommands.js +108 -53
  8. package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
  9. package/dist/controller/resourceInventory.js +9 -5
  10. package/dist/controller/runtime.js +80 -7
  11. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  12. package/dist/controller/structuredProviderObservation.js +273 -0
  13. package/dist/executor/agentAdapter.js +40 -0
  14. package/dist/executor/agentExecutor.js +31 -7
  15. package/dist/executor/executorRegistry.js +11 -49
  16. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  17. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  18. package/dist/run/agentRun.js +2 -2
  19. package/dist/runtime/agentHost.js +767 -158
  20. package/dist/runtime/builtinAgentDrivers.js +1 -5
  21. package/dist/runtime/codexAppServerRuntime.js +67 -60
  22. package/dist/runtime/exactControlPlane.js +7 -2
  23. package/dist/runtime/index.js +6 -2
  24. package/dist/runtime/launchBroker.js +30 -8
  25. package/dist/runtime/providerAuthorityFence.js +24 -0
  26. package/dist/runtime/providerControl.js +63 -0
  27. package/dist/runtime/providerRecoveryDecision.js +55 -0
  28. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  29. package/dist/runtime/runtimeBinding.js +20 -11
  30. package/dist/runtime/structuredProviderHost.js +476 -0
  31. package/dist/runtime/tmuxAdapters.js +143 -42
  32. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  33. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  34. package/dist/storage/migration/productionRegistry.js +111 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/tmux/tmuxManager.js +1 -1
  37. package/i18n/README.zh-CN.md +11 -8
  38. package/package.json +1 -1
@@ -1,16 +1,22 @@
1
- import { spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
3
2
  import { appendFileSync, chmodSync, closeSync, fsyncSync, mkdirSync, openSync, rmSync } from "node:fs";
4
3
  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";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { createConnection, createServer } from "node:net";
7
+ import { createInterface } from "node:readline";
8
+ import { callController, ControllerClientError } from "../core/controllerClient.js";
9
+ import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
10
+ import { publishStructuredProviderAccepted, publishStructuredProviderActivationTerminal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderTerminal } from "../controller/structuredProviderObservation.js";
8
11
  import { validateAgentHostLaunchPayload } from "./launchBroker.js";
12
+ import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnRejectedError, startStructuredProviderSession } from "./structuredProviderHost.js";
13
+ import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
9
14
  import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
10
15
  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;
16
+ export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
17
+ const HOST_CONTROL_TIMEOUT_MS = 35_000;
18
+ const HOST_READY_TIMEOUT_MS = 35_000;
19
+ const HOST_CONTROL_MAX_BYTES = 32 * 1024;
14
20
  export function serializeAgentHostLaunchControl(control) {
15
21
  return JSON.stringify(validateControl(control));
16
22
  }
@@ -18,65 +24,609 @@ export async function runAgentHost(input) {
18
24
  const hostInstanceId = randomUUID();
19
25
  let hostSequence = 0;
20
26
  let payload = await redeem(input.home, input.launchId, input.ticket);
27
+ let session;
28
+ let sessionPayload;
29
+ let activeTurnPayload;
30
+ let activationId;
31
+ let conversationRecoverability = "unknown";
32
+ let authority;
33
+ let hostStopRequested = false;
34
+ let snapshot = hostSnapshot("idle");
35
+ let dispatchTail = Promise.resolve();
36
+ let promptHuman = () => { };
21
37
  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);
38
+ const updateSnapshot = (next) => {
39
+ snapshot = validateSnapshot(next);
40
+ };
41
+ const authorityFields = () => authority === undefined ? {} : {
42
+ authorityEpoch: authority.epoch,
43
+ authorityOwner: authority.owner,
44
+ authorityHolderId: authority.holderId
45
+ };
46
+ const handleTerminal = (terminal) => {
47
+ const terminalPayload = activeTurnPayload;
48
+ if (terminalPayload === undefined)
49
+ return;
50
+ const terminalActivationId = activationId ?? terminalPayload.launchId;
51
+ void enqueueSerialized(async () => {
52
+ if (activeTurnPayload !== terminalPayload)
53
+ return;
54
+ if (session !== undefined) {
55
+ updateSnapshot(hostSnapshot("settling", {
56
+ launchId: terminalPayload.launchId,
57
+ adapterId: terminalPayload.providerControl.adapterId,
58
+ processInstanceId: session.processInstanceId,
59
+ nativeSessionId: terminal.nativeSessionId,
60
+ conversationId: terminal.conversationId,
61
+ nativeTurnId: terminal.nativeTurnId,
62
+ ...authorityFields()
63
+ }));
64
+ }
65
+ try {
66
+ await publishStructuredProviderTerminal({
67
+ home: input.home,
68
+ environment: terminalPayload.environment,
69
+ activationId: terminalActivationId,
70
+ terminal
71
+ });
72
+ if (activeTurnPayload !== terminalPayload)
73
+ return;
74
+ activeTurnPayload = undefined;
75
+ if (session === undefined)
76
+ return;
77
+ const currentPayload = sessionPayload ?? terminalPayload;
78
+ updateSnapshot(hostSnapshot("idle", {
79
+ launchId: currentPayload.launchId,
80
+ adapterId: currentPayload.providerControl.adapterId,
81
+ processInstanceId: session.processInstanceId,
82
+ nativeSessionId: terminal.nativeSessionId,
83
+ conversationId: terminal.conversationId,
84
+ ...authorityFields()
85
+ }));
86
+ promptHuman();
87
+ }
88
+ catch (error) {
89
+ updateSnapshot(hostSnapshot("failed", {
90
+ launchId: terminalPayload.launchId,
91
+ adapterId: terminalPayload.providerControl.adapterId,
92
+ processInstanceId: session?.processInstanceId,
93
+ nativeSessionId: terminal.nativeSessionId,
94
+ conversationId: terminal.conversationId,
95
+ nativeTurnId: terminal.nativeTurnId,
96
+ ...authorityFields(),
97
+ detail: errorText(error)
98
+ }));
99
+ }
100
+ }).catch(() => { });
101
+ };
102
+ const observeExit = (providerSession, launched) => {
103
+ void providerSession.waitForExit().then((result) => enqueueSerialized(async () => {
104
+ const ownsCurrentSession = session === providerSession;
105
+ const currentPayload = ownsCurrentSession ? sessionPayload ?? launched : launched;
106
+ const currentActivationId = ownsCurrentSession
107
+ ? activationId ?? launched.launchId
108
+ : launched.launchId;
109
+ const exitAuthority = authorityFields();
110
+ if (ownsCurrentSession) {
111
+ session = undefined;
112
+ activationId = undefined;
113
+ conversationRecoverability = "unknown";
114
+ authority = undefined;
28
115
  }
29
- control.setActive(true, payload.launchId);
30
- const result = await runAgentHostProviderChild(payload);
31
- control.setActive(false);
32
116
  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 }),
117
+ const stopReceipt = readRuntimeStopReceipt(input.home, currentPayload.launchId);
118
+ const observedAt = new Date().toISOString();
119
+ const failures = [];
120
+ try {
121
+ await publishStructuredProviderActivationTerminal({
122
+ home: input.home,
123
+ environment: currentPayload.environment,
124
+ conversationId: providerSession.conversationId,
125
+ nativeSessionId: providerSession.nativeSessionId,
126
+ activationId: currentActivationId,
127
+ status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
128
+ observedAt
129
+ });
130
+ }
131
+ catch (error) {
132
+ failures.push(`activation terminal: ${errorText(error)}`);
133
+ }
134
+ let exitPersisted = false;
135
+ try {
136
+ await persistAndSubmitExit(input.home, validateRuntimeProcessExitObservation({
137
+ schemaVersion: 1,
138
+ observationId: `${hostInstanceId}-${hostSequence}`,
139
+ hostSequence,
140
+ hostInstanceId,
141
+ providerProcessInstanceId: result.processInstanceId,
142
+ ...(currentPayload.environment.YUI_TASK_ID === undefined
143
+ ? {}
144
+ : { taskId: currentPayload.environment.YUI_TASK_ID }),
145
+ roleName: currentPayload.environment.YUI_ROLE ?? "unknown-role",
146
+ ...(currentPayload.environment.YUI_RUN_ID === undefined
147
+ ? {}
148
+ : { runId: currentPayload.environment.YUI_RUN_ID }),
149
+ launchId: currentPayload.launchId,
150
+ ...(providerSession.nativeSessionId.length === 0
151
+ ? {}
152
+ : { nativeSessionId: providerSession.nativeSessionId }),
153
+ processKind: "provider-child",
154
+ ...(result.code === null ? {} : { exitCode: result.code }),
155
+ ...(result.signal === null ? {} : { signal: result.signal }),
156
+ ...(stopReceipt === null ? {} : { stopReceiptId: stopReceipt.receiptId }),
157
+ observedAt
158
+ }));
159
+ exitPersisted = true;
160
+ }
161
+ catch (error) {
162
+ failures.push(`process exit: ${errorText(error)}`);
163
+ }
164
+ if (stopReceipt !== null && exitPersisted) {
165
+ removeRuntimeStopReceipt(input.home, currentPayload.launchId);
166
+ }
167
+ if (hostStopRequested)
168
+ return;
169
+ updateSnapshot(hostSnapshot(failures.length === 0 ? "exited" : "failed", {
170
+ launchId: currentPayload.launchId,
171
+ adapterId: currentPayload.providerControl?.adapterId,
172
+ processInstanceId: result.processInstanceId,
173
+ nativeSessionId: providerSession.nativeSessionId,
174
+ conversationId: providerSession.conversationId,
175
+ ...exitAuthority,
176
+ ...(failures.length !== 0
177
+ ? { detail: failures.join("; ") }
178
+ : activeTurnPayload === undefined
179
+ ? {}
180
+ : { detail: "Provider process exited before the active Turn reached a terminal boundary." })
181
+ }));
182
+ })).catch((error) => updateSnapshot(hostSnapshot("failed", {
183
+ launchId: launched.launchId,
184
+ adapterId: launched.providerControl?.adapterId,
185
+ processInstanceId: providerSession.processInstanceId,
186
+ ...authorityFields(),
187
+ detail: errorText(error)
188
+ })));
189
+ };
190
+ const dispatch = async (next) => {
191
+ const providerControl = next.providerControl;
192
+ if (providerControl === undefined) {
193
+ throw new Error("Agent Host accepts only managed Provider control launches.");
194
+ }
195
+ if (activeTurnPayload !== undefined
196
+ || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
197
+ throw new Error("Agent Host still owns an unsettled Provider Turn.");
198
+ }
199
+ const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
200
+ if (authority === undefined)
201
+ authority = requestedAuthority;
202
+ else if (!sameProviderAuthorityFence(authority, requestedAuthority)) {
203
+ throw new Error("Agent Host launch carries a stale Provider authority fence.");
204
+ }
205
+ updateSnapshot(hostSnapshot("starting", {
206
+ launchId: next.launchId,
207
+ adapterId: providerControl.adapterId,
208
+ ...(providerControl.initialTurn === undefined
209
+ ? {}
210
+ : { attemptId: providerControl.initialTurn.attemptId }),
211
+ ...(session === undefined ? {} : {
212
+ processInstanceId: session.processInstanceId,
213
+ nativeSessionId: session.nativeSessionId,
214
+ conversationId: session.conversationId
215
+ }),
216
+ ...authorityFields()
217
+ }));
218
+ let providerAcceptedAttemptId;
219
+ let durableInitialTurn;
220
+ try {
221
+ if (session !== undefined) {
222
+ if (session.adapterId !== providerControl.adapterId
223
+ || providerControl.mode !== "resume"
224
+ || providerControl.nativeSessionId !== session.nativeSessionId) {
225
+ throw new Error("Agent Host launch does not match its live Provider Conversation.");
226
+ }
227
+ sessionPayload = next;
228
+ }
229
+ else {
230
+ activationId = next.launchId;
231
+ conversationRecoverability = providerControl.adapterId === "codex"
232
+ ? "recoverable"
233
+ : "unknown";
234
+ const started = await startStructuredProviderSession(next, { onTerminal: handleTerminal });
235
+ session = started.session;
236
+ sessionPayload = next;
237
+ observeExit(started.session, next);
238
+ }
239
+ await publishStructuredProviderOpened({
240
+ home: input.home,
241
+ environment: next.environment,
242
+ conversationId: session.conversationId,
243
+ nativeSessionId: session.nativeSessionId,
244
+ activationId: activationId ?? next.launchId,
245
+ recoverability: conversationRecoverability,
55
246
  observedAt: new Date().toISOString()
247
+ });
248
+ let receipt;
249
+ if (providerControl.initialTurn !== undefined) {
250
+ durableInitialTurn = hostTurnControlParams(next, session.nativeSessionId, requestedAuthority, providerControl.initialTurn.attemptId);
251
+ await beginDurableProviderTurn(input.home, durableInitialTurn);
252
+ activeTurnPayload = next;
253
+ try {
254
+ receipt = await session.submitTurn(providerControl.initialTurn);
255
+ providerAcceptedAttemptId = receipt.attemptId;
256
+ }
257
+ catch (error) {
258
+ await resolveProviderTurnSubmission(input.home, durableInitialTurn, error);
259
+ throw error;
260
+ }
261
+ }
262
+ if (receipt !== undefined) {
263
+ conversationRecoverability = "recoverable";
264
+ try {
265
+ await publishStructuredProviderAccepted({
266
+ home: input.home,
267
+ environment: next.environment,
268
+ activationId: activationId ?? next.launchId,
269
+ receipt
270
+ });
271
+ }
272
+ catch (error) {
273
+ await resolveProviderTurnSubmission(input.home, durableInitialTurn, new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, receipt.attemptId));
274
+ throw error;
275
+ }
276
+ }
277
+ updateSnapshot(hostSnapshot(receipt === undefined ? "idle" : "ready", {
278
+ launchId: next.launchId,
279
+ adapterId: providerControl.adapterId,
280
+ processInstanceId: session.processInstanceId,
281
+ nativeSessionId: receipt?.nativeSessionId ?? session.nativeSessionId,
282
+ conversationId: receipt?.conversationId ?? session.conversationId,
283
+ ...(receipt === undefined ? {} : {
284
+ attemptId: receipt.attemptId,
285
+ nativeTurnId: receipt.nativeTurnId
286
+ }),
287
+ ...authorityFields()
288
+ }));
289
+ return snapshot;
290
+ }
291
+ catch (error) {
292
+ if (error instanceof ProviderConversationMissingError) {
293
+ await publishStructuredConversationRecoverability({
294
+ home: input.home,
295
+ environment: next.environment,
296
+ conversationId: error.conversationId,
297
+ activationId: activationId ?? next.launchId,
298
+ recoverability: "unrecoverable",
299
+ observedAt: new Date().toISOString()
300
+ }).catch(() => { });
301
+ }
302
+ if (session === undefined) {
303
+ activationId = undefined;
304
+ conversationRecoverability = "unknown";
305
+ authority = undefined;
306
+ }
307
+ const deliveryUnknown = error instanceof ProviderDeliveryUnknownError
308
+ || providerAcceptedAttemptId !== undefined;
309
+ const state = deliveryUnknown
310
+ ? "delivery-unknown"
311
+ : error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
312
+ updateSnapshot(hostSnapshot(state, {
313
+ launchId: next.launchId,
314
+ adapterId: providerControl.adapterId,
315
+ processInstanceId: session?.processInstanceId,
316
+ nativeSessionId: session?.nativeSessionId ?? providerControl.nativeSessionId,
317
+ conversationId: session?.conversationId ?? providerControl.nativeSessionId,
318
+ ...(providerControl.initialTurn === undefined
319
+ ? {}
320
+ : { attemptId: providerControl.initialTurn.attemptId }),
321
+ ...authorityFields(),
322
+ detail: errorText(error)
323
+ }));
324
+ if (state !== "delivery-unknown")
325
+ activeTurnPayload = undefined;
326
+ if (deliveryUnknown && !(error instanceof ProviderDeliveryUnknownError)) {
327
+ throw new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, providerAcceptedAttemptId);
328
+ }
329
+ throw error;
330
+ }
331
+ };
332
+ const enqueueSerialized = (action) => {
333
+ const operation = dispatchTail.then(action);
334
+ dispatchTail = operation.then(() => undefined, () => undefined);
335
+ return operation;
336
+ };
337
+ const enqueueDispatch = (next) => (enqueueSerialized(() => dispatch(next)));
338
+ const submitTurn = async (request) => {
339
+ if (session === undefined || sessionPayload === undefined) {
340
+ throw new Error("Agent Host has no live Provider Conversation.");
341
+ }
342
+ if (request.nativeSessionId !== session.nativeSessionId) {
343
+ throw new Error("Agent Host Turn targets a different Provider Conversation.");
344
+ }
345
+ if (authority === undefined
346
+ || !sameProviderAuthorityFence(authority, request.authority)) {
347
+ throw new Error("Agent Host rejected a stale Provider writer fence.");
348
+ }
349
+ if (activeTurnPayload !== undefined || snapshot.state === "settling") {
350
+ throw new Error("Agent Host still owns an unsettled Provider Turn.");
351
+ }
352
+ activeTurnPayload = sessionPayload;
353
+ updateSnapshot(hostSnapshot("starting", {
354
+ launchId: request.launchId,
355
+ adapterId: session.adapterId,
356
+ processInstanceId: session.processInstanceId,
357
+ nativeSessionId: session.nativeSessionId,
358
+ conversationId: session.conversationId,
359
+ attemptId: request.turn.attemptId,
360
+ ...authorityFields()
361
+ }));
362
+ let providerAccepted = false;
363
+ try {
364
+ const receipt = await session.submitTurn(request.turn);
365
+ providerAccepted = true;
366
+ await publishStructuredProviderAccepted({
367
+ home: input.home,
368
+ environment: sessionPayload.environment,
369
+ activationId: activationId ?? sessionPayload.launchId,
370
+ receipt
371
+ });
372
+ updateSnapshot(hostSnapshot("ready", {
373
+ launchId: request.launchId,
374
+ adapterId: session.adapterId,
375
+ processInstanceId: session.processInstanceId,
376
+ nativeSessionId: receipt.nativeSessionId,
377
+ conversationId: receipt.conversationId,
378
+ attemptId: receipt.attemptId,
379
+ nativeTurnId: receipt.nativeTurnId,
380
+ ...authorityFields()
56
381
  }));
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);
382
+ return snapshot;
383
+ }
384
+ catch (error) {
385
+ const deliveryUnknown = error instanceof ProviderDeliveryUnknownError || providerAccepted;
386
+ const state = deliveryUnknown
387
+ ? "delivery-unknown"
388
+ : error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
389
+ updateSnapshot(hostSnapshot(state, {
390
+ launchId: request.launchId,
391
+ adapterId: session.adapterId,
392
+ processInstanceId: session.processInstanceId,
393
+ nativeSessionId: session.nativeSessionId,
394
+ conversationId: session.conversationId,
395
+ attemptId: request.turn.attemptId,
396
+ ...authorityFields(),
397
+ detail: errorText(error)
398
+ }));
399
+ if (state !== "delivery-unknown")
400
+ activeTurnPayload = undefined;
401
+ if (deliveryUnknown && !(error instanceof ProviderDeliveryUnknownError)) {
402
+ throw new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, request.turn.attemptId);
403
+ }
404
+ throw error;
405
+ }
406
+ };
407
+ const setAuthority = (request) => {
408
+ if (session === undefined || request.nativeSessionId !== session.nativeSessionId) {
409
+ throw new Error("Agent Host authority targets a different Provider Conversation.");
410
+ }
411
+ if (activeTurnPayload !== undefined
412
+ || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
413
+ throw new Error("Agent Host authority cannot transfer while a Turn is unsettled.");
414
+ }
415
+ const next = validateProviderAuthorityFence(request.authority);
416
+ if (authority !== undefined) {
417
+ if (sameProviderAuthorityFence(authority, next))
418
+ return snapshot;
419
+ if (next.epoch <= authority.epoch) {
420
+ throw new Error("Agent Host authority epoch did not advance monotonically.");
421
+ }
422
+ }
423
+ authority = next;
424
+ updateSnapshot(hostSnapshot("idle", {
425
+ launchId: sessionPayload?.launchId ?? snapshot.launchId,
426
+ adapterId: session.adapterId,
427
+ processInstanceId: session.processInstanceId,
428
+ nativeSessionId: session.nativeSessionId,
429
+ conversationId: session.conversationId,
430
+ ...authorityFields()
431
+ }));
432
+ promptHuman();
433
+ return snapshot;
434
+ };
435
+ const control = await openAgentHostControl(input.home, payload, () => snapshot, async (request) => {
436
+ if (request.type === "status") {
437
+ return controlResult("status", snapshot);
438
+ }
439
+ if (request.type === "submit-turn") {
440
+ const accepted = await enqueueSerialized(() => submitTurn(request));
441
+ return controlResult("accepted", accepted);
66
442
  }
443
+ if (request.type === "set-authority") {
444
+ const accepted = await enqueueSerialized(async () => setAuthority(request));
445
+ return controlResult("accepted", accepted);
446
+ }
447
+ if (snapshot.launchId === request.launchId
448
+ && ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
449
+ return controlResult("active-same-launch", snapshot);
450
+ }
451
+ const redeemed = await redeem(input.home, request.launchId, request.ticket);
452
+ if (activeTurnPayload !== undefined
453
+ || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
454
+ return controlResult("active-other-launch", snapshot);
455
+ }
456
+ const accepted = await enqueueDispatch(redeemed);
457
+ return controlResult("accepted", accepted);
458
+ });
459
+ const humanConsole = process.stdin.isTTY
460
+ ? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
461
+ : undefined;
462
+ promptHuman = () => {
463
+ if (humanConsole !== undefined && authority?.owner === "human"
464
+ && activeTurnPayload === undefined
465
+ && ["idle", "rejected", "failed"].includes(snapshot.state)) {
466
+ humanConsole.setPrompt("yui(provider)> ");
467
+ humanConsole.prompt();
468
+ }
469
+ };
470
+ humanConsole?.on("line", (line) => {
471
+ void enqueueSerialized(async () => {
472
+ const currentAuthority = authority;
473
+ const currentSession = session;
474
+ const currentPayload = sessionPayload;
475
+ if (currentAuthority?.owner !== "human"
476
+ || currentSession === undefined
477
+ || currentPayload === undefined) {
478
+ process.stderr.write("Provider input rejected: human authority is not active.\n");
479
+ return;
480
+ }
481
+ const boundedText = line.trim();
482
+ if (boundedText.length === 0)
483
+ return;
484
+ const attemptId = `human:${currentAuthority.holderId}:${randomUUID()}`;
485
+ const turnControl = {
486
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
487
+ type: "submit-turn",
488
+ launchId: currentPayload.launchId,
489
+ nativeSessionId: currentSession.nativeSessionId,
490
+ authority: currentAuthority,
491
+ turn: {
492
+ attemptId,
493
+ boundedText
494
+ }
495
+ };
496
+ const durableTurn = hostTurnControlParams(currentPayload, currentSession.nativeSessionId, currentAuthority, attemptId);
497
+ try {
498
+ await beginDurableProviderTurn(input.home, durableTurn);
499
+ }
500
+ catch (error) {
501
+ await callController(input.home, "runtime.provider-turn-submission-resolve", {
502
+ ...durableTurn,
503
+ status: "rejected",
504
+ reason: `Human Turn intent acknowledgement failed before Provider write: ${errorText(error)}`,
505
+ observedAt: new Date().toISOString()
506
+ }).catch(() => { });
507
+ throw error;
508
+ }
509
+ try {
510
+ await submitTurn(turnControl);
511
+ }
512
+ catch (error) {
513
+ await callController(input.home, "runtime.provider-turn-submission-resolve", {
514
+ ...durableTurn,
515
+ status: error instanceof ProviderDeliveryUnknownError
516
+ ? "delivery-unknown"
517
+ : "rejected",
518
+ reason: errorText(error),
519
+ observedAt: new Date().toISOString()
520
+ }).catch(() => { });
521
+ throw error;
522
+ }
523
+ process.stdout.write("Provider accepted the human Turn; waiting for its terminal boundary.\n");
524
+ }).catch((error) => {
525
+ process.stderr.write(`Provider input failed: ${errorText(error)}\n`);
526
+ promptHuman();
527
+ });
528
+ });
529
+ promptHuman();
530
+ let stopResolve;
531
+ const stopped = new Promise((resolvePromise) => {
532
+ stopResolve = resolvePromise;
533
+ });
534
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
535
+ const handlers = new Map();
536
+ let forceKillTimer;
537
+ for (const signal of signals) {
538
+ const handler = () => {
539
+ if (hostStopRequested)
540
+ return;
541
+ hostStopRequested = true;
542
+ session?.terminate(signal);
543
+ forceKillTimer = setTimeout(() => session?.terminate("SIGKILL"), 10_000);
544
+ forceKillTimer.unref();
545
+ stopResolve();
546
+ };
547
+ handlers.set(signal, handler);
548
+ process.on(signal, handler);
549
+ }
550
+ try {
551
+ if (payload.startMode === "provider") {
552
+ void enqueueDispatch(payload).catch(() => { });
553
+ }
554
+ await stopped;
555
+ return 0;
67
556
  }
68
557
  finally {
558
+ for (const [signal, handler] of handlers)
559
+ process.removeListener(signal, handler);
560
+ if (forceKillTimer !== undefined)
561
+ clearTimeout(forceKillTimer);
562
+ humanConsole?.close();
563
+ session?.terminate("SIGTERM");
69
564
  await control.close();
565
+ void sessionPayload;
70
566
  }
71
567
  }
72
568
  export function agentHostControlSocketPath(input) {
73
569
  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`));
570
+ const homeDigest = createHash("sha256")
571
+ .update(readHomeFilesystemId(resolve(input.home)))
572
+ .digest("hex")
573
+ .slice(0, 16);
574
+ const ownerDigest = createHash("sha256")
575
+ .update(`${input.scope}\0${owner}\0${input.roleName}`)
576
+ .digest("hex")
577
+ .slice(0, 16);
578
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
579
+ // Linux sockaddr_un paths have a small fixed budget. Keep the endpoint
580
+ // independent of a potentially deep YUI_HOME while fencing aliases and
581
+ // copied Homes by their physical filesystem identity.
582
+ const root = process.platform === "linux" ? "/tmp" : tmpdir();
583
+ return join(root, `yui-${uid}`, "agent-host", `${homeDigest}-${ownerDigest}.sock`);
78
584
  }
79
585
  export async function sendAgentHostLaunchControl(input) {
586
+ return await sendAgentHostControl(input);
587
+ }
588
+ export async function sendAgentHostTurnControl(input) {
589
+ return await sendAgentHostControl(input);
590
+ }
591
+ export async function sendAgentHostAuthorityControl(input) {
592
+ return await sendAgentHostControl(input);
593
+ }
594
+ export async function inspectAgentHost(input) {
595
+ const result = await sendAgentHostControl({
596
+ ...input,
597
+ control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "status" }
598
+ });
599
+ return result.snapshot;
600
+ }
601
+ export async function waitForAgentHostLaunchAck(input) {
602
+ const deadline = Date.now() + (input.timeoutMs ?? HOST_READY_TIMEOUT_MS);
603
+ let lastError;
604
+ while (Date.now() < deadline) {
605
+ try {
606
+ const snapshot = await inspectAgentHost(input);
607
+ if (snapshot.launchId === input.launchId) {
608
+ if (snapshot.state === "ready"
609
+ || (input.requireTurnAck !== true && snapshot.state === "idle"))
610
+ return snapshot;
611
+ if (snapshot.state === "delivery-unknown" || snapshot.state === "rejected") {
612
+ return snapshot;
613
+ }
614
+ if (snapshot.state === "failed" || snapshot.state === "exited") {
615
+ throw new Error(`Agent Host Provider launch ${snapshot.state}: ${snapshot.detail ?? "no detail"}.`);
616
+ }
617
+ }
618
+ }
619
+ catch (error) {
620
+ lastError = error;
621
+ const code = error.code;
622
+ if (code !== "ENOENT" && code !== "ECONNREFUSED")
623
+ throw error;
624
+ }
625
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
626
+ }
627
+ throw new Error(`Agent Host did not acknowledge Provider launch ${input.launchId}: ${errorText(lastError)}.`);
628
+ }
629
+ async function sendAgentHostControl(input) {
80
630
  const path = agentHostControlSocketPath(input);
81
631
  return await new Promise((resolvePromise, reject) => {
82
632
  const client = createConnection(path);
@@ -85,12 +635,16 @@ export async function sendAgentHostLaunchControl(input) {
85
635
  client.destroy();
86
636
  reject(new Error("Agent Host control request timed out."));
87
637
  }, HOST_CONTROL_TIMEOUT_MS);
638
+ let settled = false;
88
639
  const settle = (callback, value) => {
640
+ if (settled)
641
+ return;
642
+ settled = true;
89
643
  clearTimeout(timer);
90
644
  callback(value);
91
645
  };
92
646
  client.setEncoding("utf8");
93
- client.once("connect", () => client.end(`${serializeAgentHostLaunchControl(input.control)}\n`));
647
+ client.once("connect", () => client.end(`${JSON.stringify(validateControl(input.control))}\n`));
94
648
  client.on("data", (chunk) => {
95
649
  response += chunk;
96
650
  if (Buffer.byteLength(response, "utf8") > HOST_CONTROL_MAX_BYTES) {
@@ -99,12 +653,12 @@ export async function sendAgentHostLaunchControl(input) {
99
653
  });
100
654
  client.once("error", (error) => settle(reject, error));
101
655
  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"}.`));
656
+ try {
657
+ settle(resolvePromise, validateControlResult(JSON.parse(response.trim())));
658
+ }
659
+ catch (error) {
660
+ settle(reject, error);
661
+ }
108
662
  });
109
663
  });
110
664
  }
@@ -116,66 +670,6 @@ async function redeem(home, launchId, ticket) {
116
670
  });
117
671
  return validateAgentHostLaunchPayload(result);
118
672
  }
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
673
  async function persistAndSubmitExit(home, observation) {
180
674
  const directory = resolve(join(home, "runtime", "agent-host-outbox"));
181
675
  mkdirSync(directory, { recursive: true, mode: 0o700 });
@@ -222,9 +716,8 @@ async function replayExitOutbox(home) {
222
716
  await unlink(claimed);
223
717
  }
224
718
  }
225
- async function openHostControl(home, payload) {
226
- const directory = resolve(join(home, "runtime", "agent-host-control"));
227
- mkdirSync(directory, { recursive: true, mode: 0o700 });
719
+ /** Internal socket boundary exported for transport-level verification. */
720
+ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
228
721
  const path = agentHostControlSocketPath({
229
722
  home,
230
723
  scope: payload.environment.YUI_SESSION_SCOPE ?? "task",
@@ -233,16 +726,17 @@ async function openHostControl(home, payload) {
233
726
  : { taskId: payload.environment.YUI_TASK_ID }),
234
727
  roleName: payload.environment.YUI_ROLE ?? "unknown-role"
235
728
  });
729
+ const directory = dirname(path);
730
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
236
731
  if (await hostControlSocketIsLive(path)) {
237
732
  throw new Error(`Agent Host control socket is already owned: ${path}.`);
238
733
  }
239
734
  rmSync(path, { force: true });
240
- let active = false;
241
- let activeLaunchId;
242
- const queued = [];
243
- const waiters = [];
244
- const server = createServer((socket) => {
735
+ const server = createServer({ allowHalfOpen: true }, (socket) => {
245
736
  socket.setEncoding("utf8");
737
+ // A control client can disappear after sending its bounded request. Keep
738
+ // that connection-local failure from terminating the persistent Host.
739
+ socket.on("error", () => { });
246
740
  let body = "";
247
741
  socket.on("data", (chunk) => {
248
742
  body += chunk;
@@ -251,24 +745,20 @@ async function openHostControl(home, payload) {
251
745
  }
252
746
  });
253
747
  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;
748
+ void (async () => {
749
+ try {
750
+ const request = validateControl(JSON.parse(body.trim()));
751
+ socket.end(`${JSON.stringify(await dispatch(request))}\n`);
261
752
  }
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
- }
753
+ catch (error) {
754
+ const current = snapshot();
755
+ socket.end(`${JSON.stringify(controlResult("rejected", validateSnapshot({
756
+ ...current,
757
+ detail: errorText(error),
758
+ updatedAt: new Date().toISOString()
759
+ })))}\n`);
760
+ }
761
+ })();
272
762
  });
273
763
  });
274
764
  await new Promise((resolvePromise, reject) => {
@@ -277,16 +767,6 @@ async function openHostControl(home, payload) {
277
767
  });
278
768
  chmodSync(path, 0o600);
279
769
  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
770
  close: async () => {
291
771
  await new Promise((resolvePromise) => server.close(() => resolvePromise()));
292
772
  rmSync(path, { force: true });
@@ -313,15 +793,144 @@ async function hostControlSocketIsLive(path) {
313
793
  });
314
794
  }
315
795
  function validateControl(control) {
316
- if (control.protocol !== AGENT_HOST_CONTROL_PROTOCOL || control.type !== "launch") {
796
+ if (control.protocol !== AGENT_HOST_CONTROL_PROTOCOL) {
317
797
  throw new Error("Agent Host control protocol is invalid.");
318
798
  }
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.");
799
+ if (control.type === "status")
800
+ return Object.freeze({ ...control });
801
+ if (control.type === "submit-turn") {
802
+ validateLaunchId(control.launchId);
803
+ validateIdentity(control.nativeSessionId, "native Session id");
804
+ validateProviderAuthorityFence(control.authority);
805
+ validateIdentity(control.turn.attemptId, "Provider input attempt id");
806
+ if (typeof control.turn.boundedText !== "string"
807
+ || control.turn.boundedText.includes("\0")
808
+ || Buffer.byteLength(control.turn.boundedText, "utf8") > 32 * 1024) {
809
+ throw new Error("Agent Host Provider input is invalid.");
810
+ }
811
+ return Object.freeze({
812
+ ...control,
813
+ turn: Object.freeze({ ...control.turn })
814
+ });
815
+ }
816
+ if (control.type === "set-authority") {
817
+ validateIdentity(control.nativeSessionId, "native Session id");
818
+ return Object.freeze({
819
+ ...control,
820
+ authority: validateProviderAuthorityFence(control.authority)
821
+ });
322
822
  }
823
+ if (control.type !== "launch")
824
+ throw new Error("Agent Host control type is invalid.");
825
+ validateLaunchId(control.launchId);
323
826
  if (typeof control.ticket !== "string" || !/^[a-f0-9]{64}$/u.test(control.ticket)) {
324
827
  throw new Error("Agent Host launch control ticket is invalid.");
325
828
  }
326
829
  return Object.freeze({ ...control });
327
830
  }
831
+ function validateLaunchId(value) {
832
+ if (typeof value !== "string" || value.length === 0
833
+ || value.length > 256 || value.includes("\0")) {
834
+ throw new Error("Agent Host launch control identity is invalid.");
835
+ }
836
+ }
837
+ function validateIdentity(value, label) {
838
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
839
+ throw new Error(`Agent Host ${label} is invalid.`);
840
+ }
841
+ }
842
+ function validateControlResult(result) {
843
+ if (result.protocol !== AGENT_HOST_CONTROL_PROTOCOL
844
+ || !["status", "accepted", "rejected", "active-same-launch", "active-other-launch"].includes(result.outcome)) {
845
+ throw new Error("Agent Host control response is invalid.");
846
+ }
847
+ return Object.freeze({ ...result, snapshot: validateSnapshot(result.snapshot) });
848
+ }
849
+ function validateSnapshot(snapshot) {
850
+ if (snapshot.schemaVersion !== 1
851
+ || !["idle", "starting", "ready", "settling", "delivery-unknown", "rejected", "failed", "exited"]
852
+ .includes(snapshot.state)) {
853
+ throw new Error("Agent Host snapshot is invalid.");
854
+ }
855
+ if (!Number.isFinite(Date.parse(snapshot.updatedAt))) {
856
+ throw new Error("Agent Host snapshot timestamp is invalid.");
857
+ }
858
+ const authorityFields = [
859
+ snapshot.authorityEpoch,
860
+ snapshot.authorityOwner,
861
+ snapshot.authorityHolderId
862
+ ];
863
+ if (authorityFields.some((value) => value !== undefined)) {
864
+ if (authorityFields.some((value) => value === undefined)) {
865
+ throw new Error("Agent Host snapshot authority is incomplete.");
866
+ }
867
+ validateProviderAuthorityFence({
868
+ epoch: snapshot.authorityEpoch,
869
+ owner: snapshot.authorityOwner,
870
+ holderId: snapshot.authorityHolderId
871
+ });
872
+ }
873
+ return Object.freeze({ ...snapshot });
874
+ }
875
+ function hostSnapshot(state, fields = {}) {
876
+ return validateSnapshot({
877
+ schemaVersion: 1,
878
+ state,
879
+ ...definedFields(fields),
880
+ updatedAt: new Date().toISOString()
881
+ });
882
+ }
883
+ function controlResult(outcome, snapshot) {
884
+ return Object.freeze({ protocol: AGENT_HOST_CONTROL_PROTOCOL, outcome, snapshot });
885
+ }
886
+ function definedFields(value) {
887
+ return Object.fromEntries(Object.entries(value).filter(([, member]) => member !== undefined));
888
+ }
889
+ function errorText(error) {
890
+ return error instanceof Error ? error.message : String(error ?? "unknown error");
891
+ }
892
+ function hostTurnControlParams(payload, nativeSessionId, authority, attemptId) {
893
+ const environment = payload.environment;
894
+ return Object.freeze({
895
+ taskId: requiredEnvironment(environment.YUI_TASK_ID, "Task id"),
896
+ roleName: requiredEnvironment(environment.YUI_ROLE, "Role name"),
897
+ runId: requiredEnvironment(environment.YUI_RUN_ID, "Run id"),
898
+ agentId: requiredEnvironment(environment.YUI_AGENT_ID, "Agent id"),
899
+ launchId: payload.launchId,
900
+ nativeSessionId: requiredEnvironment(nativeSessionId, "native Session id"),
901
+ attemptId,
902
+ authorityEpoch: authority.epoch,
903
+ authorityOwner: authority.owner,
904
+ holderId: authority.holderId,
905
+ observedAt: new Date().toISOString()
906
+ });
907
+ }
908
+ async function beginDurableProviderTurn(home, durableTurn) {
909
+ try {
910
+ await callController(home, "runtime.provider-turn-begin", durableTurn);
911
+ }
912
+ catch (error) {
913
+ if (!(error instanceof ControllerClientError) || error.code !== "INTERNAL_ERROR") {
914
+ throw error;
915
+ }
916
+ // The exact attempt is idempotent, so one lost acknowledgement can be
917
+ // retried without creating a second Provider write.
918
+ await callController(home, "runtime.provider-turn-begin", durableTurn);
919
+ }
920
+ }
921
+ async function resolveProviderTurnSubmission(home, durableTurn, error) {
922
+ await callController(home, "runtime.provider-turn-submission-resolve", {
923
+ ...durableTurn,
924
+ status: error instanceof ProviderDeliveryUnknownError
925
+ ? "delivery-unknown"
926
+ : "rejected",
927
+ reason: errorText(error),
928
+ observedAt: new Date().toISOString()
929
+ }).catch(() => { });
930
+ }
931
+ function requiredEnvironment(value, label) {
932
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
933
+ throw new Error(`Agent Host ${label} is unavailable.`);
934
+ }
935
+ return value.trim();
936
+ }