@zq-silk/yui 0.6.2 → 0.6.3

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 (61) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +62 -24
  3. package/dist/agent/argumentPolicy.js +1 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +1 -0
  5. package/dist/cli/commandCatalog.js +19 -9
  6. package/dist/cli/interactionPolicy.js +4 -2
  7. package/dist/cli.js +77 -32
  8. package/dist/commands/taskCommands.js +46 -11
  9. package/dist/commands/taskContextCommand.js +1 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +170 -10
  11. package/dist/controller/agentRuntimeObserver.js +210 -0
  12. package/dist/controller/clientRuntime.js +3 -21
  13. package/dist/controller/controller.js +47 -7
  14. package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
  15. package/dist/controller/runtime.js +9 -3
  16. package/dist/controller/runtimeEventInbox.js +49 -295
  17. package/dist/controller/runtimeEventProcessor.js +184 -321
  18. package/dist/controller/runtimeHookRunFence.js +226 -0
  19. package/dist/controller/runtimeLaunchCoordinator.js +91 -26
  20. package/dist/controller/runtimeObservationHook.js +112 -0
  21. package/dist/core/controllerServer.js +5 -0
  22. package/dist/executor/agentAdapter.js +18 -3
  23. package/dist/executor/fileRoleLaunchPlanner.js +64 -15
  24. package/dist/executor/managedClaudeRunner.js +121 -0
  25. package/dist/observability/executionAudit.js +6 -3
  26. package/dist/repository/taskWorkspacePreparer.js +1 -4
  27. package/dist/run/providerRetryConfig.js +8 -3
  28. package/dist/runtime/agentDriver.js +229 -0
  29. package/dist/runtime/agentDriverObservation.js +57 -0
  30. package/dist/runtime/builtinAgentDrivers.js +235 -0
  31. package/dist/runtime/builtinTranscriptObserver.js +290 -0
  32. package/dist/runtime/builtinTranscriptUsage.js +97 -0
  33. package/dist/runtime/exactControlPlane.js +2 -2
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/ports.js +12 -1
  36. package/dist/runtime/runtimeObservation.js +297 -0
  37. package/dist/runtime/runtimeProjection.js +277 -0
  38. package/dist/runtime/sessionTerminationGuard.js +78 -22
  39. package/dist/runtime/tmuxAdapters.js +35 -0
  40. package/dist/scheduler/activeRoleRunDelivery.js +28 -13
  41. package/dist/scheduler/leaderWakeupProcessor.js +21 -2
  42. package/dist/scheduler/roleRunLiveness.js +2 -2
  43. package/dist/scheduler/roleRunStall.js +62 -114
  44. package/dist/storage/migration/productionRegistry.js +41 -0
  45. package/dist/storage/sqliteStore.js +3 -3
  46. package/dist/storage/storageVersions.js +1 -1
  47. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  48. package/dist/telemetry/telemetryCompaction.js +1 -0
  49. package/dist/telemetry/telemetryConfig.js +4 -5
  50. package/dist/tmux/tmuxManager.js +136 -22
  51. package/dist/web/assets/client/view.js +1 -1
  52. package/dist/web/tmuxWebTerminal.js +17 -12
  53. package/dist/web/webSnapshot.js +1 -1
  54. package/dist/worktree/managedWorkspace.js +14 -0
  55. package/i18n/README.zh-CN.md +7 -5
  56. package/package.json +1 -1
  57. package/dist/controller/claudeLifecycleHook.js +0 -203
  58. package/dist/controller/codexLifecycleHook.js +0 -108
  59. package/dist/controller/providerHookRunFence.js +0 -156
  60. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  61. package/dist/telemetry/telemetryRouter.js +0 -32
@@ -0,0 +1,226 @@
1
+ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
2
+ import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
+ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
4
+ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
5
+ import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeEnvironment, exactControlPlaneDigest, parseExactControlPlaneDescriptor, refreshReusedTaskRuntimeDescriptorSource } from "../runtime/exactControlPlane.js";
6
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
7
+ /**
8
+ * Resolves turn identity from the current durable in-flight fence. The one
9
+ * exception is a Driver-declared startup Session Hook, which can arrive before
10
+ * Session projection and is fenced by the exact Run-bound launch reservation.
11
+ * Preallocated identities are additionally checked against Yui's deterministic
12
+ * launch identity. The immutable event is revalidated by the inbox fold.
13
+ */
14
+ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNativeSessionId, options = {}) {
15
+ if (environment.YUI_SESSION_SCOPE !== "task") {
16
+ throw new Error("Runtime observation Hook requires a Task session scope.");
17
+ }
18
+ if (environment.YUI_ADAPTER_ID !== adapterId) {
19
+ throw new Error(`Runtime observation Hook requires the ${adapterId} adapter.`);
20
+ }
21
+ const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
22
+ const taskId = requireIdentity(environment.YUI_TASK_ID, "Task id");
23
+ const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
24
+ const agentId = requireIdentity(environment.YUI_AGENT_ID, "Agent id");
25
+ const workspace = requireIdentity(environment.YUI_WORKSPACE, "YUI workspace");
26
+ const runtimeSource = environment[YUI_TASK_RUNTIME_DESCRIPTOR];
27
+ const runtime = runtimeSource === undefined
28
+ ? undefined
29
+ : assertExactTaskRuntimeEnvironment(runtimeSource, environment, exactControlPlaneDigest(parseExactControlPlaneDescriptor(requireIdentity(environment[YUI_CONTROL_PLANE_DESCRIPTOR], "Exact control-plane descriptor"))), home);
30
+ const launchId = requireIdentity(runtime?.launchId ?? environment.YUI_LAUNCH_ID, "Launch id");
31
+ const nativeSessionId = requireIdentity(payloadNativeSessionId, "Provider session id");
32
+ const expectedNativeSessionId = runtime?.nativeSessionId ?? environment.YUI_NATIVE_SESSION_ID;
33
+ if (expectedNativeSessionId !== undefined
34
+ && nativeSessionId !== requireIdentity(expectedNativeSessionId, "YUI native session id")) {
35
+ throw new Error("Runtime observation Hook native Session does not match its launch envelope.");
36
+ }
37
+ const store = openCompatibleFileTaskStore(home);
38
+ const task = store.getTask(taskId);
39
+ if (task === null || task.status !== "active") {
40
+ throw new Error("Runtime observation Hook Task is not current and active.");
41
+ }
42
+ const role = store.getRole(taskId, roleName);
43
+ if (role === null || role.activeAgentId !== agentId) {
44
+ throw new Error("Runtime observation Hook Role or Agent is not current.");
45
+ }
46
+ const sessions = store.getTaskRoleSessionSet(taskId, roleName);
47
+ if (sessions !== null && sessions.activeAgentId !== agentId) {
48
+ throw new Error("Runtime observation Hook Session Agent is not current.");
49
+ }
50
+ const inFlight = sessions?.inFlight;
51
+ const session = sessions?.sessions[agentId];
52
+ const acceptedBinding = options.nativeTurnId === undefined
53
+ ? null
54
+ : acceptedTurnBinding(store.listEvents(taskId), {
55
+ taskId,
56
+ roleName,
57
+ agentId,
58
+ nativeSessionId,
59
+ nativeTurnId: options.nativeTurnId
60
+ });
61
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
62
+ scope: "task",
63
+ taskId,
64
+ roleName
65
+ }));
66
+ const exactReservation = isRuntimeLaunchReservation(mailbox?.processing, launchId)
67
+ && !hasRuntimeCleanupObligation(mailbox);
68
+ const executionRef = mailbox?.processing?.executionRef;
69
+ const startupRunId = options.startupSession === undefined
70
+ ? undefined
71
+ : requireIdentity(runtime?.runId
72
+ ?? environment.YUI_RUN_ID
73
+ ?? (executionRef?.type === "run" && executionRef.taskId === taskId
74
+ ? executionRef.id
75
+ : undefined), "Run id");
76
+ const startupReservation = startupRunId !== undefined
77
+ && exactReservation
78
+ && executionRef?.type === "run"
79
+ && executionRef.taskId === taskId
80
+ && executionRef.id === startupRunId;
81
+ const preallocatedStartup = options.startupSession === "preallocated"
82
+ && expectedNativeSessionId !== undefined
83
+ && session === undefined
84
+ && startupReservation
85
+ && nativeSessionId === nativeSessionIdForLaunch(home, launchId, agentId, adapterId);
86
+ const discoveredStartup = options.startupSession === "discovered"
87
+ && expectedNativeSessionId === undefined
88
+ && session === undefined
89
+ && startupReservation;
90
+ const terminalRunId = options.terminal === true && acceptedBinding === null
91
+ ? requireIdentity(environment.YUI_RUN_ID ?? runtime?.runId, "Run id")
92
+ : undefined;
93
+ const terminalRun = acceptedBinding !== null
94
+ ? store.getAgentRun(taskId, acceptedBinding.fence.runId)
95
+ : terminalRunId === undefined
96
+ ? null
97
+ : store.getAgentRun(taskId, terminalRunId);
98
+ const exactTerminal = terminalRun !== null
99
+ && terminalRun.status !== "active"
100
+ && terminalRun.roleName === roleName
101
+ && terminalRun.effective.agentId === agentId
102
+ && terminalRun.effective.adapterId === adapterId
103
+ && session !== undefined
104
+ && (acceptedBinding !== null || inFlight === null || inFlight === undefined);
105
+ if ((inFlight === null || inFlight === undefined)
106
+ && !preallocatedStartup
107
+ && !discoveredStartup
108
+ && !exactTerminal
109
+ && acceptedBinding === null) {
110
+ throw new Error("Runtime observation Hook has no matching durable in-flight Run.");
111
+ }
112
+ if (acceptedBinding === null
113
+ && inFlight !== null && inFlight !== undefined && inFlight.agentId !== agentId) {
114
+ throw new Error("Runtime observation Hook has no matching durable in-flight Run.");
115
+ }
116
+ if ((preallocatedStartup || discoveredStartup)
117
+ && inFlight !== null
118
+ && inFlight !== undefined
119
+ && (inFlight.runId !== startupRunId
120
+ || inFlight.receiptId !== formatAgentRunReceiptId(taskId, startupRunId))) {
121
+ throw new Error("Runtime observation Hook has no matching durable in-flight Run.");
122
+ }
123
+ const runId = acceptedBinding?.fence.runId
124
+ ?? inFlight?.runId
125
+ ?? startupRunId
126
+ ?? terminalRunId;
127
+ let effectiveRuntime = runtime;
128
+ let effectiveLaunchId = acceptedBinding?.fence.launchId ?? launchId;
129
+ const sessionLaunchId = session?.launchId;
130
+ if (acceptedBinding === null
131
+ && runtime !== undefined
132
+ && session !== undefined
133
+ && sessionLaunchId !== undefined
134
+ && typeof runtimeSource === "string"
135
+ && !runtimeSource.trimStart().startsWith("{")
136
+ && (runtime.runId !== runId
137
+ || runtime.launchId !== sessionLaunchId
138
+ || runtime.nativeSessionId !== session.nativeSessionId)) {
139
+ // A reused native pane keeps its original descriptor source. Advance only
140
+ // that Hook-owned source to the current durable generation before the
141
+ // volatile fence; the Controller no longer scans history to keep it fresh.
142
+ effectiveRuntime = refreshReusedTaskRuntimeDescriptorSource(runtimeSource, home, store, {
143
+ runId,
144
+ launchId: sessionLaunchId,
145
+ nativeSessionId: session.nativeSessionId
146
+ });
147
+ effectiveLaunchId = effectiveRuntime.launchId;
148
+ }
149
+ if (acceptedBinding === null
150
+ && effectiveRuntime?.runId !== undefined && effectiveRuntime.runId !== runId) {
151
+ throw new Error("Runtime observation Hook Run does not match its current descriptor.");
152
+ }
153
+ const run = acceptedBinding !== null || exactTerminal
154
+ ? terminalRun
155
+ : store.getActiveAgentRun(taskId, roleName);
156
+ if (run === null
157
+ || run.id !== runId
158
+ || (acceptedBinding === null && !exactTerminal && run.status !== "active")
159
+ || run.effective.agentId !== agentId
160
+ || run.effective.adapterId !== adapterId) {
161
+ throw new Error("Runtime observation Hook Run does not match durable active state.");
162
+ }
163
+ if (run.effective.workspace.root !== workspace) {
164
+ throw new Error("Runtime observation Hook workspace does not match the durable Run snapshot.");
165
+ }
166
+ if (session !== undefined) {
167
+ if (session.adapterId !== adapterId
168
+ || (acceptedBinding === null && session.launchId !== effectiveLaunchId)
169
+ || session.nativeSessionId !== nativeSessionId
170
+ || session.effective.workspace.root !== workspace) {
171
+ throw new Error("Runtime observation Hook Session does not match its durable generation.");
172
+ }
173
+ }
174
+ else {
175
+ if (!discoveredStartup && !preallocatedStartup) {
176
+ throw new Error("Runtime observation Hook launch is not durably reserved.");
177
+ }
178
+ }
179
+ return {
180
+ taskId,
181
+ roleName,
182
+ agentId,
183
+ launchId: effectiveLaunchId,
184
+ runId,
185
+ ...(acceptedBinding?.fence.receiptId === undefined
186
+ ? inFlight?.receiptId === undefined ? {} : { receiptId: inFlight.receiptId }
187
+ : { receiptId: acceptedBinding.fence.receiptId }),
188
+ nativeSessionId,
189
+ workspace
190
+ };
191
+ }
192
+ function acceptedTurnBinding(events, expected) {
193
+ const matches = events
194
+ .map(runtimeObservationFromTaskEvent)
195
+ .filter((observation) => observation !== null
196
+ && observation.kind === "turn.accepted"
197
+ && observation.fence.taskId === expected.taskId
198
+ && observation.fence.roleName === expected.roleName
199
+ && observation.fence.agentId === expected.agentId
200
+ && observation.fence.nativeSessionId === expected.nativeSessionId
201
+ && observation.fence.nativeTurnId === expected.nativeTurnId
202
+ && observation.fence.runId !== undefined)
203
+ .sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
204
+ || (left.sequence ?? -1) - (right.sequence ?? -1)
205
+ || (left.ordinal ?? -1) - (right.ordinal ?? -1)
206
+ || left.eventId.localeCompare(right.eventId)));
207
+ const binding = matches.at(-1) ?? null;
208
+ if (binding === null)
209
+ return null;
210
+ if (matches.some((candidate) => candidate.fence.runId !== binding.fence.runId
211
+ || candidate.fence.launchId !== binding.fence.launchId
212
+ || candidate.fence.receiptId !== binding.fence.receiptId)) {
213
+ throw new Error("Runtime observation Hook native Turn has conflicting durable Run bindings.");
214
+ }
215
+ return binding;
216
+ }
217
+ function requireIdentity(value, label) {
218
+ if (typeof value !== "string" || value.includes("\0")) {
219
+ throw new Error(`${label} is required.`);
220
+ }
221
+ const normalized = value.trim();
222
+ if (normalized.length === 0 || normalized.length > 1_024) {
223
+ throw new Error(`${label} is invalid.`);
224
+ }
225
+ return normalized;
226
+ }
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
- import { createRuntimeBinding, RuntimeLaunchError } from "../runtime/index.js";
3
+ import { createRuntimeBinding, RuntimeHostContentionError, RuntimeLaunchError } from "../runtime/index.js";
4
4
  import { effectiveLaunchSnapshotsCompatible, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
5
5
  class RuntimeBindingContractError extends Error {
6
6
  constructor(message, options) {
@@ -8,9 +8,22 @@ class RuntimeBindingContractError extends Error {
8
8
  this.name = "RuntimeBindingContractError";
9
9
  }
10
10
  }
11
+ class LaunchSubmittedHostBusyError extends RuntimeBindingContractError {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "LaunchSubmittedHostBusyError";
15
+ }
16
+ }
17
+ class RuntimeLaunchStateChangedError extends Error {
18
+ constructor(message, options) {
19
+ super(message, options);
20
+ this.name = "RuntimeLaunchStateChangedError";
21
+ }
22
+ }
11
23
  /**
12
24
  * One application service owns the reservation -> host -> persistence
13
- * protocol for both foreground enter and scheduler-driven Role launches.
25
+ * protocol for Controller/scheduler-driven Role launches. Foreground Task
26
+ * attach is deliberately outside this lifecycle boundary.
14
27
  */
15
28
  export class RuntimeLaunchCoordinator {
16
29
  reservations;
@@ -90,7 +103,12 @@ export class RuntimeLaunchCoordinator {
90
103
  ...(request.runId === undefined ? {} : { runId: request.runId })
91
104
  }, assertLaunchCurrent, this.#now());
92
105
  let reusedConfirmedRunningHost = false;
93
- let launchPromptAcknowledgementRequired = false;
106
+ const launchCarriesExactRunPrompt = carriesExactRunPrompt(request);
107
+ // Preserve the existing Codex recovery path; managed Claude is the one
108
+ // finite-process protocol that must never fall back to tmux key delivery
109
+ // when a newly-reserved Run encounters an older live pane.
110
+ let launchPromptAcknowledgementRequired = request.adapterId === "claude"
111
+ && launchCarriesExactRunPrompt;
94
112
  if (reservation.status === "existing") {
95
113
  if (!reservation.launchId.startsWith(generationPrefix)) {
96
114
  this.#requireCleanup(request.owner);
@@ -149,13 +167,12 @@ export class RuntimeLaunchCoordinator {
149
167
  throw new Error(`Runtime launch reservation belongs to another Run whose host is still running: ${reservation.runId}.`);
150
168
  }
151
169
  reusedConfirmedRunningHost = true;
152
- // Only a fresh Codex generation can carry the exact Run prompt in its
153
- // launch argv. A resume request targets an existing native Session and
154
- // must use the normal receipt-backed prompt push, even when the
155
- // Controller is recovering the same launch reservation.
170
+ // A fresh Codex generation carries the prompt in its launch argv. A
171
+ // managed Claude generation carries it through stream-json stdin for
172
+ // both new and native-resume modes. Only recovery of the exact same
173
+ // reserved Run may bridge a lost in-memory launch acknowledgement.
156
174
  launchPromptAcknowledgementRequired = sameRunReservation
157
- && request.mode === "new"
158
- && request.adapterId === "codex";
175
+ && launchCarriesExactRunPrompt;
159
176
  runtimeIsolation = this.#preflightRuntimeIsolation(request, reservation.launchId, reservation.launchId.slice(generationPrefix.length), true);
160
177
  assertLaunchCurrent();
161
178
  }
@@ -212,12 +229,53 @@ export class RuntimeLaunchCoordinator {
212
229
  : { environment: request.environment }),
213
230
  nativeSessionId: requireText(request.nativeSessionId, "Native session id")
214
231
  }, beforeHostStart === undefined ? undefined : observePreflight);
215
- if (!preflightObserved) {
232
+ binding = requireMatchingRuntimeBinding(rawBinding, request, launchId, launchPromptAcknowledgementRequired, reusedConfirmedRunningHost);
233
+ if (!preflightObserved
234
+ && !(binding.hostCreated === false
235
+ && request.adapterId === "claude"
236
+ && launchCarriesExactRunPrompt)) {
216
237
  throw new Error("Runtime session host did not expose a pre-host-start launch fence.");
217
238
  }
218
- binding = requireMatchingRuntimeBinding(rawBinding, request, launchId, launchPromptAcknowledgementRequired, reusedConfirmedRunningHost);
219
239
  }
220
240
  catch (error) {
241
+ if (error instanceof RuntimeHostContentionError && reusedConfirmedRunningHost) {
242
+ // The exact recovered generation remains authoritative. A late human
243
+ // writer is transient backpressure and must not settle, clean, or
244
+ // terminalize that reservation.
245
+ throw new RuntimeLaunchError(true, launchId, error.message, error.reason);
246
+ }
247
+ if ((error instanceof LaunchSubmittedHostBusyError
248
+ || error instanceof RuntimeHostContentionError)
249
+ && !reusedConfirmedRunningHost) {
250
+ let exactCleanupAttempted = false;
251
+ let completed = false;
252
+ try {
253
+ completed = this.reservations.completeRuntimeLaunchReservation(request.owner, launchId, undefined, runtimeIsolation === undefined
254
+ ? undefined
255
+ : () => {
256
+ exactCleanupAttempted = true;
257
+ this.#runtimeIsolation.cleanup(runtimeIsolation, "failure");
258
+ });
259
+ }
260
+ catch (cleanupError) {
261
+ if (exactCleanupAttempted)
262
+ this.#requireCleanup(request.owner);
263
+ throw cleanupError;
264
+ }
265
+ if (!completed) {
266
+ this.#requireCleanup(request.owner);
267
+ throw new Error("Busy managed runtime launch reservation changed during retry release.");
268
+ }
269
+ if (error instanceof LaunchSubmittedHostBusyError) {
270
+ // A terminal Run no longer owns its finite provider process. Move
271
+ // that exact Role owner through the durable cleanup lane before a
272
+ // later retry creates the successor generation.
273
+ this.#requireCleanup(request.owner);
274
+ }
275
+ throw new RuntimeLaunchError(true, launchId, error.message, error instanceof RuntimeHostContentionError
276
+ ? error.reason
277
+ : "previous-process");
278
+ }
221
279
  if (error instanceof RuntimeBindingContractError) {
222
280
  // Never pass an untrusted hostRef to stop(). Reconcile the requested
223
281
  // owner through the durable, owner-addressed cleanup lane instead.
@@ -259,17 +317,15 @@ export class RuntimeLaunchCoordinator {
259
317
  catch (error) {
260
318
  await this.#compensateStartedHost(request.owner, binding, launchId, runtimeIsolation, error);
261
319
  }
262
- if (request.owner.scope === "task"
263
- && request.adapterId === "codex"
264
- && request.runId !== undefined
320
+ if (launchCarriesExactRunPrompt
265
321
  && binding.hostCreated !== false
266
322
  && reservationConfirmation !== "provider-bound"
267
323
  && binding.initialPromptRunId !== request.runId) {
268
- // Fresh Codex has no pre-input Provider event. It may return before its
269
- // matching lifecycle Hook, but only an exact launch-carried Run marker
270
- // can bridge that asynchronous interval. This is transport evidence,
271
- // never Provider acceptance; the reservation remains fenced until the
272
- // matching Hook binds the native Session.
324
+ // A launch-carried prompt may return before its matching lifecycle Hook,
325
+ // but only an exact Run marker can bridge that asynchronous interval.
326
+ // This is transport evidence, never Provider acceptance; the
327
+ // reservation remains fenced until the matching Hook binds the native
328
+ // Session.
273
329
  this.#requireCleanup(request.owner);
274
330
  throw new RuntimeBindingContractError(`Session host cannot acknowledge the exact launch-carried prompt: ${request.owner.roleName}.`);
275
331
  }
@@ -342,7 +398,8 @@ export class RuntimeLaunchCoordinator {
342
398
  }, this.#now())) {
343
399
  this.#requireCleanup(owner);
344
400
  }
345
- throw cause;
401
+ const detail = cause instanceof Error ? cause.message : String(cause);
402
+ throw new RuntimeLaunchStateChangedError(`Runtime launch was compensated after state changed: ${detail}`, { cause });
346
403
  }
347
404
  #preflightRuntimeIsolation(request, launchId, generationId, allowExactActive) {
348
405
  if (this.#runtimeIsolation === undefined || request.owner.scope !== "task") {
@@ -426,11 +483,6 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
426
483
  && binding.initialPromptRunId !== request.runId) {
427
484
  throw new RuntimeBindingContractError(`Session host returned a launch-carried prompt for another Run: ${request.owner.roleName}.`);
428
485
  }
429
- if (launchPromptAcknowledgementRequired
430
- && binding.initialPromptRunId !== request.runId
431
- && !(launchPromptUncertaintyAllowed && binding.hostCreated === false)) {
432
- throw new RuntimeBindingContractError(`Session host cannot acknowledge the exact launch-carried prompt: ${request.owner.roleName}.`);
433
- }
434
486
  if (binding.launchId !== launchId
435
487
  || !ownerMatches
436
488
  || binding.agentId !== request.agentId
@@ -439,13 +491,20 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
439
491
  && binding.nativeSessionId !== request.nativeSessionId)) {
440
492
  throw new RuntimeBindingContractError(`Session host returned a binding that does not match the requested runtime: ${request.owner.roleName}.`);
441
493
  }
494
+ if (launchPromptAcknowledgementRequired
495
+ && binding.initialPromptRunId !== request.runId
496
+ && !(launchPromptUncertaintyAllowed && binding.hostCreated === false)) {
497
+ throw binding.hostCreated === false
498
+ ? new LaunchSubmittedHostBusyError(`An earlier managed runtime is still exiting: ${request.owner.roleName}.`)
499
+ : new RuntimeBindingContractError(`Session host cannot acknowledge the exact launch-carried prompt: ${request.owner.roleName}.`);
500
+ }
442
501
  if (launchPromptAcknowledgementRequired
443
502
  && launchPromptUncertaintyAllowed
444
503
  && binding.hostCreated === false
445
504
  && request.runId !== undefined
446
505
  && binding.initialPromptRunId !== request.runId) {
447
506
  // A Controller restart can lose only the in-memory fact that a still-
448
- // running Codex generation carried this Run in its launch argv. Keep the
507
+ // running generation carried this Run at process launch. Keep the
449
508
  // uncertainty explicitly tied to the exact reservation/Run; the matching
450
509
  // Provider Hook remains the sole acceptance authority.
451
510
  return {
@@ -455,6 +514,12 @@ function requireMatchingRuntimeBinding(raw, request, launchId, launchPromptAckno
455
514
  }
456
515
  return binding;
457
516
  }
517
+ function carriesExactRunPrompt(request) {
518
+ if (request.owner.scope !== "task" || request.runId === undefined)
519
+ return false;
520
+ return request.adapterId === "claude"
521
+ || (request.adapterId === "codex" && request.mode === "new");
522
+ }
458
523
  function defaultLaunchFingerprint(request) {
459
524
  return createHash("sha256").update(JSON.stringify([
460
525
  request.owner,
@@ -0,0 +1,112 @@
1
+ import { callController } from "../core/controllerClient.js";
2
+ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
3
+ import { normalizeAgentDriverHookClassification } from "../runtime/agentDriver.js";
4
+ import { mapAgentDriverHook } from "../runtime/agentDriverObservation.js";
5
+ import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
6
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
7
+ import { FileRuntimeEventInbox, MAX_RUNTIME_EVENT_FILE_BYTES } from "./runtimeEventInbox.js";
8
+ import { resolveRuntimeHookRunFence } from "./runtimeHookRunFence.js";
9
+ /**
10
+ * Single hidden ingress for every structured CLI Driver Hook. Native event
11
+ * names and payload shapes terminate here; the durable inbox contains only a
12
+ * provider-independent RuntimeObservation with an exact generation/Run fence.
13
+ */
14
+ export async function runRuntimeObservationHookCommand(stdinJson, environment = process.env, call = callController, now = new Date(), dependencies = {}) {
15
+ const parsed = parseRuntimeObservationHook(stdinJson, environment, now, dependencies);
16
+ const inbox = new FileRuntimeEventInbox(parsed.home);
17
+ for (const observation of parsed.observations)
18
+ inbox.enqueueObservation(observation);
19
+ // The immutable inbox write is authoritative. The socket call only reduces
20
+ // observation latency when the Controller is currently available.
21
+ await call(parsed.home, "scheduler.signal", {
22
+ key: runtimeLifecycleSignalKey({
23
+ scope: "task",
24
+ taskId: parsed.taskId,
25
+ roleName: parsed.roleName
26
+ })
27
+ }, { timeoutMs: 100 }).catch(() => { });
28
+ }
29
+ export function parseRuntimeObservationHook(stdinJson, environment, now = new Date(), dependencies = {}) {
30
+ const payload = parseObject(stdinJson);
31
+ const drivers = dependencies.drivers ?? builtinAgentDriverRegistry();
32
+ const driverId = requireIdentity(environment.YUI_DRIVER_ID, "Agent Driver id");
33
+ const driver = drivers.require(driverId);
34
+ const hookEventName = requireIdentity(payload.hook_event_name, "Agent Driver Hook event name");
35
+ const receivedAt = now.toISOString();
36
+ const sequence = (dependencies.sequence ?? monotonicSequence)();
37
+ const occurrenceId = `${receivedAt}:${sequence}`;
38
+ const nativeHook = Object.freeze({ hookEventName, payload, occurrenceId });
39
+ const nativeSessionId = requireIdentity(driver.runtime.nativeSessionId(nativeHook), "Agent Driver native Session id");
40
+ const nativeTurnId = optionalIdentity(driver.runtime.nativeTurnId(nativeHook));
41
+ const classification = normalizeAgentDriverHookClassification(driver.runtime.classifyHook(nativeHook));
42
+ const fence = (dependencies.resolveRunFence ?? resolveRuntimeHookRunFence)(environment, driver.adapterId, nativeSessionId, {
43
+ ...(classification.startupSession === undefined
44
+ ? {}
45
+ : { startupSession: classification.startupSession }),
46
+ terminal: classification.terminal,
47
+ ...(nativeTurnId === undefined ? {} : { nativeTurnId })
48
+ });
49
+ const driverInput = {
50
+ driver,
51
+ hookEventName,
52
+ receivedAt,
53
+ // Hook commands run in separate short-lived processes. CLOCK_MONOTONIC is
54
+ // shared by those processes on the host, so this preserves their order
55
+ // when wall-clock timestamps land in the same millisecond.
56
+ sequence,
57
+ occurrenceId,
58
+ fence: {
59
+ taskId: fence.taskId,
60
+ roleName: fence.roleName,
61
+ runId: fence.runId,
62
+ agentId: fence.agentId,
63
+ driverId,
64
+ launchId: fence.launchId,
65
+ sessionGenerationId: fence.launchId,
66
+ nativeSessionId: fence.nativeSessionId,
67
+ nativeTurnId: nativeTurnId ?? fence.runId,
68
+ receiptId: fence.receiptId ?? formatAgentRunReceiptId(fence.taskId, fence.runId)
69
+ },
70
+ payload
71
+ };
72
+ const observations = [mapAgentDriverHook({ ...driverInput, ordinal: 0 })];
73
+ return {
74
+ home: requireIdentity(environment.YUI_HOME, "YUI_HOME"),
75
+ taskId: fence.taskId,
76
+ roleName: fence.roleName,
77
+ observations: Object.freeze(observations)
78
+ };
79
+ }
80
+ function monotonicSequence() {
81
+ return Number(process.hrtime.bigint() / 1000n);
82
+ }
83
+ function parseObject(value) {
84
+ if (value === undefined
85
+ || Buffer.byteLength(value, "utf8") > MAX_RUNTIME_EVENT_FILE_BYTES) {
86
+ throw new Error("Agent Driver lifecycle Hook stdin JSON is invalid.");
87
+ }
88
+ try {
89
+ const parsed = JSON.parse(value);
90
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
91
+ throw new Error("shape");
92
+ return parsed;
93
+ }
94
+ catch {
95
+ throw new Error("Agent Driver lifecycle Hook stdin JSON is invalid.");
96
+ }
97
+ }
98
+ function optionalIdentity(value) {
99
+ if (value === undefined || value === null)
100
+ return undefined;
101
+ return requireIdentity(value, "Agent Driver native Turn id");
102
+ }
103
+ function requireIdentity(value, label) {
104
+ if (typeof value !== "string" || value.includes("\0")) {
105
+ throw new Error(`${label} is required.`);
106
+ }
107
+ const normalized = value.trim();
108
+ if (normalized.length === 0 || normalized.length > 1_024) {
109
+ throw new Error(`${label} is invalid.`);
110
+ }
111
+ return normalized;
112
+ }
@@ -732,6 +732,11 @@ function safeDispatcherError(error) {
732
732
  }
733
733
  return { code: "SERVICE_ERROR", message };
734
734
  }
735
+ case "RuntimeLaunchStateChangedError":
736
+ // The launch coordinator already stopped the exact host and bounded
737
+ // the detail to its currentness/reservation contract. Surface that
738
+ // actionable diagnosis instead of collapsing it to INTERNAL_ERROR.
739
+ return { code: "SERVICE_ERROR", message };
735
740
  default:
736
741
  return undefined;
737
742
  }
@@ -6,7 +6,7 @@ import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgent
6
6
  import { writeTextFileAtomically } from "../storage/durableFile.js";
7
7
  import { inspectCodexDeveloperInstructions } from "./codexConfigConflict.js";
8
8
  import { discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
9
- import { preInputReadinessCapability } from "../lifecycle/providerLifecycleMapping.js";
9
+ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
10
10
  const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
11
11
  const APPROVALS = ["untrusted", "on-request", "never"];
12
12
  const PROBE_TIMEOUT_MS = 2_000;
@@ -57,7 +57,7 @@ class CodexAdapter extends BaseAdapter {
57
57
  recover: true,
58
58
  interrupt: true,
59
59
  nativeSessionDiscovery: "runtime",
60
- preInputReadiness: preInputReadinessCapability("codex")
60
+ preInputReadiness: driverPreInputReadiness("codex")
61
61
  };
62
62
  discoverConfiguration(input) {
63
63
  return discoverCodexConfiguration(input);
@@ -166,7 +166,7 @@ class ClaudeAdapter extends BaseAdapter {
166
166
  recover: true,
167
167
  interrupt: true,
168
168
  nativeSessionDiscovery: "preallocated",
169
- preInputReadiness: preInputReadinessCapability("claude")
169
+ preInputReadiness: driverPreInputReadiness("claude")
170
170
  };
171
171
  discoverConfiguration(input) {
172
172
  return discoverClaudeConfiguration(input);
@@ -257,6 +257,21 @@ class ClaudeAdapter extends BaseAdapter {
257
257
  return { ...launch, argv: [...launch.argv, "--resume", nativeId(input.nativeSessionId)] };
258
258
  }
259
259
  }
260
+ function driverPreInputReadiness(adapterId) {
261
+ const capability = builtinAgentDriverRegistry().requireByAdapterId(adapterId)
262
+ .capabilities.observation.preInputReadiness;
263
+ return capability === "exact"
264
+ ? Object.freeze({
265
+ status: "supported",
266
+ nativeEvent: "Agent Driver session.ready",
267
+ note: "The registered Agent Driver supplies an exact pre-input readiness observation."
268
+ })
269
+ : Object.freeze({
270
+ status: "unsupported",
271
+ reason: "not-available",
272
+ note: "The registered Agent Driver does not expose exact pre-input readiness."
273
+ });
274
+ }
260
275
  const ADAPTERS = {
261
276
  codex: new CodexAdapter(), claude: new ClaudeAdapter()
262
277
  };