@zq-silk/yui 0.11.3 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +44 -22
  3. package/dist/cli/commandCatalog.js +13 -10
  4. package/dist/cli/interactionPolicy.js +5 -6
  5. package/dist/cli/updateCommand.js +3 -1
  6. package/dist/cli/updateOrchestrator.js +173 -28
  7. package/dist/cli/updatePorts.js +137 -8
  8. package/dist/cli/upgradeCommand.js +19 -9
  9. package/dist/cli.js +156 -57
  10. package/dist/commands/configCommands.js +13 -46
  11. package/dist/commands/executionAuditCommands.js +3 -2
  12. package/dist/commands/jobCommands.js +4 -10
  13. package/dist/commands/taskCommands.js +234 -200
  14. package/dist/commands/taskCompletionGate.js +29 -54
  15. package/dist/commands/taskContextCommand.js +61 -13
  16. package/dist/commands/taskInputCommands.js +8 -5
  17. package/dist/commands/taskNextActionCommand.js +56 -6
  18. package/dist/commands/taskOverviewCommand.js +17 -31
  19. package/dist/commands/taskRoleRuntimeStatus.js +55 -7
  20. package/dist/commands/taskWorkspaceCommands.js +5 -2
  21. package/dist/config/configCatalog.js +3 -3
  22. package/dist/config/yuiConfig.js +3 -7
  23. package/dist/context/wakeNotification.js +20 -5
  24. package/dist/controller/agentRuntimeObserver.js +247 -51
  25. package/dist/controller/clientRuntime.js +39 -3
  26. package/dist/controller/controller.js +31 -27
  27. package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
  28. package/dist/controller/runtime.js +58 -3
  29. package/dist/controller/runtimeEventInbox.js +17 -7
  30. package/dist/controller/runtimeEventProcessor.js +12 -19
  31. package/dist/controller/runtimeHookRunFence.js +19 -4
  32. package/dist/controller/runtimeObservationHook.js +45 -0
  33. package/dist/controller/structuredProviderObservation.js +20 -3
  34. package/dist/core/controllerClient.js +20 -2
  35. package/dist/core/controllerServer.js +1 -0
  36. package/dist/execution/resourceBroker.js +5 -4
  37. package/dist/executor/agentExecutor.js +52 -50
  38. package/dist/executor/effectiveLaunch.js +8 -48
  39. package/dist/executor/fileRoleLaunchPlanner.js +129 -45
  40. package/dist/executor/workspacePreflightClassification.js +23 -2
  41. package/dist/interaction/operatorPresentation.js +33 -89
  42. package/dist/lifecycle/exactRunTerminalization.js +69 -5
  43. package/dist/observability/executionAudit.js +5 -0
  44. package/dist/observability/orchestrationMetrics.js +8 -3
  45. package/dist/profile/agentProfile.js +1 -1
  46. package/dist/release/cliHomeReleaseFence.js +123 -0
  47. package/dist/release/runtimeRelease.js +20 -0
  48. package/dist/repository/taskWorkspacePreparer.js +176 -60
  49. package/dist/resources/sqliteResourceRegistry.js +1 -1
  50. package/dist/review/deltaRecheck.js +12 -51
  51. package/dist/review/reviewAcceptance.js +26 -0
  52. package/dist/review/reviewConfig.js +0 -31
  53. package/dist/review/reviewDecision.js +113 -0
  54. package/dist/review/reviewOutcomeClassifier.js +1 -1
  55. package/dist/review/reviewRound.js +1 -1
  56. package/dist/review/reviewerAvailability.js +69 -0
  57. package/dist/run/recoveryProjection.js +45 -6
  58. package/dist/runtime/agentDriverObservation.js +24 -10
  59. package/dist/runtime/agentHost.js +159 -85
  60. package/dist/runtime/builtinAgentDrivers.js +4 -3
  61. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  62. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  63. package/dist/runtime/conversationSwitch.js +277 -0
  64. package/dist/runtime/index.js +2 -1
  65. package/dist/runtime/launchBroker.js +12 -0
  66. package/dist/runtime/processExitOutbox.js +88 -0
  67. package/dist/runtime/providerRuntimeIdentity.js +29 -1
  68. package/dist/runtime/runtimeHealthPolicy.js +5 -5
  69. package/dist/runtime/runtimeObservation.js +28 -0
  70. package/dist/runtime/runtimeProjection.js +22 -32
  71. package/dist/runtime/sessionTokenMetrics.js +181 -0
  72. package/dist/runtime/structuredProviderHost.js +7 -1
  73. package/dist/runtime/tmuxAdapters.js +4 -1
  74. package/dist/scheduler/activeRoleRunDelivery.js +34 -199
  75. package/dist/scheduler/activeTaskProgress.js +7 -6
  76. package/dist/scheduler/leaderWakeupProcessor.js +42 -138
  77. package/dist/scheduler/operatorEvent.js +34 -0
  78. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  79. package/dist/scheduler/roleRunStall.js +53 -31
  80. package/dist/scheduler/taskExecutionProjection.js +97 -76
  81. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  82. package/dist/storage/migration/productionRegistry.js +379 -0
  83. package/dist/storage/sqliteSchema.js +66 -23
  84. package/dist/storage/sqliteStore.js +46 -23
  85. package/dist/storage/storeRpc.js +0 -1
  86. package/dist/storage/taskStore.js +15 -30
  87. package/dist/storage/upgrade/homeClassification.js +52 -0
  88. package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
  89. package/dist/storage/upgrade/recordVersions.js +3 -2
  90. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  91. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  92. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
  94. package/dist/task/completionReadiness.js +10 -6
  95. package/dist/task/nextAction.js +82 -58
  96. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  97. package/dist/web/assets/client/components.js +13 -3
  98. package/dist/web/assets/client/i18n.js +6 -2
  99. package/dist/web/webSnapshot.js +7 -1
  100. package/i18n/README.zh-CN.md +2 -2
  101. package/package.json +1 -1
  102. package/skills/yui-leader/SKILL.md +60 -20
  103. package/skills/yui-operator/SKILL.md +13 -4
  104. package/skills/yui-reviewer/SKILL.md +35 -11
  105. package/dist/context/sessionContextBudget.js +0 -71
  106. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  107. package/dist/lifecycle/taskRoleSessionReset.js +0 -126
  108. package/dist/scheduler/operatorNotification.js +0 -59
@@ -7,7 +7,7 @@ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_M
7
7
  import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
8
  import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
9
9
  import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
10
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
10
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
11
11
  import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "../executor/agentConfigurationCatalog.js";
12
12
  import { isTaskOwnedWorkspace, managedWorkspaceIdentity, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
13
13
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
@@ -36,6 +36,7 @@ import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
36
36
  import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
37
37
  import { launchBrokerForHome } from "../runtime/launchBroker.js";
38
38
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
39
+ import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
39
40
  import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
40
41
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
41
42
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
@@ -385,6 +386,12 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
385
386
  environment: options.environment
386
387
  });
387
388
  const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, signalRuntimeCleanup, launchCoordinator, planner);
389
+ // Process-exit observations are persisted by the Agent Host before socket
390
+ // delivery. Drain them while this Controller is still the only storage
391
+ // writer and before it begins accepting new work after a handover.
392
+ await replayRuntimeProcessExitOutbox(home, async (observation) => {
393
+ await lifecycleDispatcher("runtime.process-exit-observe", observation);
394
+ });
388
395
  // f7/rr5: This same inbox feeds the supervisor's terminal channel and the
389
396
  // runtime event processor. When a Job reaches a terminal state, the
390
397
  // supervisor enqueues a durable-job-terminal event; the processor drains it
@@ -577,6 +584,11 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
577
584
  }
578
585
  return { recorded: true };
579
586
  }
587
+ if (method === "runtime.conversation-switch-detach") {
588
+ const value = conversationSwitchDetachParams(params);
589
+ const outcome = schedulerStore.detachAgentHostProviderForConversationSwitch(value);
590
+ return { recorded: outcome === "detached", outcome };
591
+ }
580
592
  if (method === "runtime.provider-turn-submission-resolve") {
581
593
  const value = providerTurnControlParams(params);
582
594
  const status = params.status;
@@ -671,6 +683,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
671
683
  || !Number.isSafeInteger(hostPid) || hostPid <= 0) {
672
684
  throw applicationError("INVALID_PARAMS", "Launch redemption identity is invalid.");
673
685
  }
686
+ // The launch payload is validated at reservation time and every member
687
+ // of its discriminated Provider-control union is JSON serializable.
674
688
  return launchBrokerForHome(store.rootDirectory()).redeem(launchId, ticket);
675
689
  }
676
690
  if (method === "runtime.replace-agent-environment") {
@@ -735,6 +749,9 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
735
749
  const activeRun = request.scope === "task"
736
750
  ? store.getActiveAgentRun(request.taskId, request.roleName)
737
751
  : null;
752
+ if (request.scope === "task" && activeRun === null) {
753
+ throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Run; it cannot create an empty Provider Conversation.");
754
+ }
738
755
  const managedWorkspace = request.scope === "task"
739
756
  ? activeRun?.workspace
740
757
  ?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
@@ -754,8 +771,21 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
754
771
  throw applicationError("INVALID_PARAMS", `Configured Agent does not match Role: ${effective.agentId}.`);
755
772
  }
756
773
  validateLifecycleEnvironment(request.environment, agent);
757
- const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective, managedWorkspace);
758
774
  const session = sessions?.sessions[effective.agentId];
775
+ const managedTurnDispatch = activeRun !== null && (activeRun.pushedAt === undefined
776
+ || activeRun.providerRetry?.state === "dispatching"
777
+ || activeRun.controlRequest?.state === "dispatching");
778
+ // An ensure call may reattach the already-admitted Run to its existing
779
+ // Conversation, but it may not manufacture a fresh Conversation without a
780
+ // pending managed Turn. Fresh replacement remains owned by Run dispatch.
781
+ const mode = activeRun === null
782
+ ? roleAgentSessionResumeMode(sessions, effective.agentId, effective)
783
+ : managedTurnDispatch ? activeRun.mode : "resume";
784
+ if (request.scope === "task" && mode === "resume"
785
+ && (session?.nativeSessionId === undefined
786
+ || session.nativeSessionId.trim().length === 0)) {
787
+ throw applicationError("INVALID_PARAMS", "Task Role runtime attachment cannot resume because its Provider Conversation identity is missing.");
788
+ }
759
789
  const owner = request.scope === "task"
760
790
  ? { scope: "task", taskId: request.taskId, roleName: request.roleName }
761
791
  : { scope: "global", roleName: request.roleName };
@@ -878,7 +908,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
878
908
  throw new Error(`Native session changed: ${request.owner.roleName}.`);
879
909
  }
880
910
  const sessionEffectiveCompatible = request.owner.scope === "task"
881
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, request.effective, request.managedWorkspace)
911
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, request.effective)
882
912
  : effectiveLaunchSnapshotsCompatible(session.effective, request.effective);
883
913
  if (!sessionEffectiveCompatible) {
884
914
  throw new Error(`Native session effective launch changed: ${request.owner.roleName}.`);
@@ -1105,6 +1135,31 @@ function providerTurnControlParams(params) {
1105
1135
  now: new Date(observedAt)
1106
1136
  };
1107
1137
  }
1138
+ function conversationSwitchDetachParams(params) {
1139
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
1140
+ throw applicationError("INVALID_PARAMS", "Provider Conversation detachment params are invalid.");
1141
+ }
1142
+ const value = params;
1143
+ const nextAuthorityEpoch = value.nextAuthorityEpoch;
1144
+ const observedAt = requiredParam(value.observedAt);
1145
+ if (!Number.isSafeInteger(nextAuthorityEpoch) || nextAuthorityEpoch < 1
1146
+ || !Number.isFinite(Date.parse(observedAt))) {
1147
+ throw applicationError("INVALID_PARAMS", "Provider Conversation detachment fence is invalid.");
1148
+ }
1149
+ return {
1150
+ taskId: requiredParam(value.taskId),
1151
+ roleName: requiredParam(value.roleName),
1152
+ runId: requiredParam(value.runId),
1153
+ agentId: requiredParam(value.agentId),
1154
+ launchId: requiredParam(value.launchId),
1155
+ previousConversationId: requiredParam(value.previousConversationId),
1156
+ previousNativeSessionId: requiredParam(value.previousNativeSessionId),
1157
+ previousActivationId: requiredParam(value.previousActivationId),
1158
+ nextAuthorityEpoch: nextAuthorityEpoch,
1159
+ nextAuthorityHolderId: requiredParam(value.nextAuthorityHolderId),
1160
+ now: new Date(observedAt)
1161
+ };
1162
+ }
1108
1163
  function abortableDelay(milliseconds, signal) {
1109
1164
  return new Promise((resolve, reject) => {
1110
1165
  const timer = setTimeout(() => {
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { withUpgradeCoordinationLock } from "../storage/upgradeCoordination.js";
5
- import { createRuntimeObservation } from "../runtime/runtimeObservation.js";
5
+ import { createRuntimeObservation, isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
6
6
  export const MAX_RUNTIME_TURN_SUMMARY_BYTES = 32 * 1024;
7
7
  export const MAX_RUNTIME_EVENT_FILE_BYTES = 16 * 1024 * 1024;
8
8
  const RUNTIME_EVENT_DIRECTORY = join("runtime", "inbox");
@@ -41,7 +41,7 @@ export class FileRuntimeEventInbox {
41
41
  return this.publish(event);
42
42
  }
43
43
  enqueueTurnCompleted(input) {
44
- const normalized = normalizeCodexInput(input);
44
+ const normalized = normalizeNativeTurnCompletedInput(input);
45
45
  return this.publish(Object.freeze({
46
46
  schemaVersion: 1,
47
47
  id: runtimeEventId("native-turn-completed", normalized),
@@ -163,6 +163,15 @@ export class FileRuntimeEventInbox {
163
163
  // UpgradeFenceError after the cutover holder releases the lock.
164
164
  return withUpgradeCoordinationLock(this.home, () => {
165
165
  this.hooks.afterAdmission?.();
166
+ if (event.type === "runtime-observation"
167
+ && isRuntimeTokenEvidence(event.observation)) {
168
+ const existing = this.list().find((candidate) => (candidate.type === "runtime-observation"
169
+ && candidate.taskId === event.taskId
170
+ && candidate.observation.eventId === event.observation.eventId));
171
+ if (existing !== undefined) {
172
+ return { event: existing, created: false };
173
+ }
174
+ }
166
175
  return this.publishUnlocked(event);
167
176
  });
168
177
  }
@@ -281,7 +290,7 @@ function runtimeEventId(type, input) {
281
290
  ];
282
291
  return `turn-${createHash("sha256").update(JSON.stringify(common)).digest("hex")}`;
283
292
  }
284
- function normalizeCodexInput(input) {
293
+ function normalizeNativeTurnCompletedInput(input) {
285
294
  const scope = input.scope;
286
295
  if (scope !== "task" && scope !== "global")
287
296
  throw invalidEvent();
@@ -305,7 +314,8 @@ function normalizeCodexInput(input) {
305
314
  : { title: requireIdentityText(input.title, "Session title") }),
306
315
  summary: truncateUtf8(input.summary.trim(), MAX_RUNTIME_TURN_SUMMARY_BYTES)
307
316
  };
308
- if (common.adapterId !== "codex" || common.summary.length === 0)
317
+ if ((common.adapterId !== "codex" && common.adapterId !== "claude")
318
+ || common.summary.length === 0)
309
319
  throw invalidEvent();
310
320
  return scope === "task"
311
321
  ? { ...common, taskId: requireIdentityText(input.taskId, "Task id") }
@@ -333,7 +343,7 @@ function parseRuntimeEvent(value) {
333
343
  throw invalidEvent();
334
344
  switch (value.type) {
335
345
  case "runtime-observation": return parseRuntimeObservationEvent(value);
336
- case "native-turn-completed": return parseCodexEvent(value);
346
+ case "native-turn-completed": return parseNativeTurnCompletedEvent(value);
337
347
  case "durable-job-terminal": return parseDurableJobTerminalEvent(value);
338
348
  default: throw invalidEvent();
339
349
  }
@@ -378,7 +388,7 @@ function parseDurableJobTerminalEvent(value) {
378
388
  ...normalized
379
389
  });
380
390
  }
381
- function parseCodexEvent(value) {
391
+ function parseNativeTurnCompletedEvent(value) {
382
392
  const scope = value.scope;
383
393
  const expected = scope === "task"
384
394
  ? [
@@ -400,7 +410,7 @@ function parseCodexEvent(value) {
400
410
  || !hasExactKeys(value, expected))
401
411
  throw invalidEvent();
402
412
  const receivedAt = requireTimestamp(value.receivedAt);
403
- const normalized = normalizeCodexInput({
413
+ const normalized = normalizeNativeTurnCompletedInput({
404
414
  scope,
405
415
  ...(scope === "task" ? { taskId: value.taskId } : {}),
406
416
  roleName: value.roleName,
@@ -1,3 +1,4 @@
1
+ import { isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
1
2
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
2
3
  const DEFAULT_MAX_RUNTIME_EVENTS_PER_DRAIN = 64;
3
4
  /** Folds immutable Hook facts in one bounded transaction before acknowledging them. */
@@ -111,7 +112,7 @@ export class FileRuntimeEventProcessor {
111
112
  const event = candidate.event;
112
113
  let outcome = "applied";
113
114
  if (event.type === "native-turn-completed") {
114
- outcome = this.applyCodex(event, now);
115
+ outcome = this.applyNativeTurnCompleted(event, now);
115
116
  }
116
117
  else if (event.type === "runtime-observation") {
117
118
  outcome = this.applyRuntimeObservation(event, now);
@@ -199,7 +200,7 @@ export class FileRuntimeEventProcessor {
199
200
  // transaction as the terminal transition.
200
201
  void event;
201
202
  }
202
- applyCodex(event, now) {
203
+ applyNativeTurnCompleted(event, now) {
203
204
  if (event.scope === "task") {
204
205
  const task = this.observer.getTask(event.taskId);
205
206
  const input = {
@@ -315,17 +316,10 @@ export function coalesceRuntimeProgress(events, instrumentation = {}) {
315
316
  }
316
317
  const representedByIndex = new Map();
317
318
  for (const indices of indicesByStream.values()) {
318
- const hasUsage = segment[indices[0]].observation.payload.usage !== undefined;
319
- const retainedPositions = hasUsage ? [0] : [];
320
- if (hasUsage) {
321
- for (let position = 1; position < indices.length; position += 1) {
322
- const previous = segment[indices[position - 1]].observation.payload.usage;
323
- const current = segment[indices[position]].observation.payload.usage;
324
- if (runtimeUsageTotal(current) < runtimeUsageTotal(previous)) {
325
- retainedPositions.push(position);
326
- }
327
- }
328
- }
319
+ const hasUsage = isRuntimeTokenEvidence(segment[indices[0]].observation);
320
+ // Usage facts and incomplete boundaries are authoritative history for
321
+ // read-only Session token projection, so ingress never coalesces them.
322
+ const retainedPositions = hasUsage ? indices.map((_, position) => position) : [];
329
323
  const lastPosition = indices.length - 1;
330
324
  if (retainedPositions.at(-1) !== lastPosition)
331
325
  retainedPositions.push(lastPosition);
@@ -424,12 +418,9 @@ function progressStreamKey(event) {
424
418
  fence.nativeSessionId ?? null,
425
419
  fence.runId ?? null,
426
420
  payload.activity,
427
- payload.usage === undefined ? "signal" : "usage"
421
+ isRuntimeTokenEvidence(event.observation) ? "usage" : "signal"
428
422
  ]);
429
423
  }
430
- function runtimeUsageTotal(usage) {
431
- return usage.inputTokens + usage.outputTokens;
432
- }
433
424
  function isRuntimeActivityEvent(event) {
434
425
  return event.type === "runtime-observation"
435
426
  && event.observation.kind === "activity.observed";
@@ -536,6 +527,8 @@ export function createAsyncRuntimeObserver(invoke) {
536
527
  observeRuntimeObservation: (input, now) => withNow("observeRuntimeObservation", input, now),
537
528
  observeRuntimeTurnCompleted: (input, now) => withNow("observeRuntimeTurnCompleted", input, now),
538
529
  observeGlobalRuntimeTurnCompleted: (input, now) => withNow("observeGlobalRuntimeTurnCompleted", input, now),
530
+ classifyRuntimeTurnCompleted: (input) => call("classifyRuntimeTurnCompleted", [input]),
531
+ classifyGlobalRuntimeTurnCompleted: (input) => call("classifyGlobalRuntimeTurnCompleted", [input]),
539
532
  observeObsoleteRuntimeEvent: (input, now) => withNow("observeObsoleteRuntimeEvent", input, now)
540
533
  };
541
534
  }
@@ -581,7 +574,7 @@ export class AsyncRuntimeEventProcessor {
581
574
  try {
582
575
  let outcome = "applied";
583
576
  if (event.type === "native-turn-completed") {
584
- outcome = await this.applyCodex(event, now);
577
+ outcome = await this.applyNativeTurnCompleted(event, now);
585
578
  }
586
579
  else if (event.type === "runtime-observation") {
587
580
  outcome = await this.applyRuntimeObservation(event, now);
@@ -652,7 +645,7 @@ export class AsyncRuntimeEventProcessor {
652
645
  }
653
646
  return outcome;
654
647
  }
655
- async applyCodex(event, now) {
648
+ async applyNativeTurnCompleted(event, now) {
656
649
  if (event.scope === "task") {
657
650
  const task = await this.observer.getTask(event.taskId);
658
651
  const input = {
@@ -4,6 +4,7 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
4
4
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
5
5
  import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeEnvironment, exactControlPlaneDigest, parseExactControlPlaneDescriptor, refreshReusedTaskRuntimeDescriptorSource } from "../runtime/exactControlPlane.js";
6
6
  import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
7
+ import { conversationReplacementBasis } from "../runtime/conversationSwitch.js";
7
8
  /**
8
9
  * Resolves turn identity from the current durable in-flight fence. The one
9
10
  * exception is a Driver-declared startup Session Hook, which can arrive before
@@ -101,14 +102,27 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
101
102
  const startupRun = startupRunId === undefined
102
103
  ? null
103
104
  : store.getAgentRun(taskId, startupRunId);
105
+ const replacementStartup = options.startupSession !== undefined
106
+ && session !== undefined
107
+ && sessions !== null
108
+ && startupReservation
109
+ && startupRun?.mode === "new"
110
+ && conversationReplacementBasis({
111
+ sessions,
112
+ events: store.listEvents(taskId),
113
+ mailbox: roleMailbox,
114
+ roleName,
115
+ runId: startupRun.id,
116
+ runMode: startupRun.mode
117
+ }) !== null;
104
118
  const preallocatedStartup = options.startupSession === "preallocated"
105
119
  && expectedNativeSessionId !== undefined
106
- && session === undefined
120
+ && (session === undefined || replacementStartup)
107
121
  && startupReservation
108
122
  && nativeSessionId === nativeSessionIdForLaunch(home, launchId, agentId, adapterId);
109
123
  const discoveredStartup = options.startupSession === "discovered"
110
124
  && expectedNativeSessionId === undefined
111
- && session === undefined
125
+ && (session === undefined || replacementStartup)
112
126
  && startupReservation;
113
127
  const terminalRunId = options.terminal === true && acceptedBinding === null
114
128
  ? requireIdentity(environment.YUI_RUN_ID ?? runtime?.runId, "Run id")
@@ -154,6 +168,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
154
168
  if (acceptedBinding === null
155
169
  && runtime !== undefined
156
170
  && session !== undefined
171
+ && !replacementStartup
157
172
  && sessionLaunchId !== undefined
158
173
  && typeof runtimeSource === "string"
159
174
  && !runtimeSource.trimStart().startsWith("{")
@@ -187,7 +202,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
187
202
  if (run.effective.workspace.root !== workspace) {
188
203
  throw new Error("Runtime observation Hook workspace does not match the durable Run snapshot.");
189
204
  }
190
- if (session !== undefined && acceptedBinding === null) {
205
+ if (session !== undefined && acceptedBinding === null && !replacementStartup) {
191
206
  if (session.adapterId !== adapterId
192
207
  || session.launchId !== effectiveLaunchId
193
208
  || session.nativeSessionId !== nativeSessionId
@@ -195,7 +210,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
195
210
  throw new Error("Runtime observation Hook Session does not match its durable generation.");
196
211
  }
197
212
  }
198
- else if (session === undefined && acceptedBinding === null) {
213
+ else if (acceptedBinding === null && (session === undefined || replacementStartup)) {
199
214
  if (!discoveredStartup && !preallocatedStartup) {
200
215
  throw new Error("Runtime observation Hook launch is not durably reserved.");
201
216
  }
@@ -12,6 +12,10 @@ import { resolveRuntimeHookRunFence } from "./runtimeHookRunFence.js";
12
12
  * provider-independent RuntimeObservation with an exact generation/Run fence.
13
13
  */
14
14
  export async function runRuntimeObservationHookCommand(stdinJson, environment = process.env, call = callController, now = new Date(), dependencies = {}) {
15
+ if (environment.YUI_SESSION_SCOPE === "global") {
16
+ await runGlobalRuntimeTurnHook(stdinJson, environment, call, now, dependencies);
17
+ return;
18
+ }
15
19
  const parsed = parseRuntimeObservationHook(stdinJson, environment, now, dependencies);
16
20
  const inbox = new FileRuntimeEventInbox(parsed.home);
17
21
  for (const observation of parsed.observations)
@@ -26,6 +30,47 @@ export async function runRuntimeObservationHookCommand(stdinJson, environment =
26
30
  })
27
31
  }, { timeoutMs: 100 }).catch(() => { });
28
32
  }
33
+ async function runGlobalRuntimeTurnHook(stdinJson, environment, call, now, dependencies) {
34
+ const payload = parseObject(stdinJson);
35
+ const hookEventName = requireIdentity(payload.hook_event_name, "Agent Driver Hook event name");
36
+ if (hookEventName !== "Stop" && hookEventName !== "StopFailure")
37
+ return;
38
+ const driverId = requireIdentity(environment.YUI_DRIVER_ID, "Agent Driver id");
39
+ const driver = (dependencies.drivers ?? builtinAgentDriverRegistry()).require(driverId);
40
+ if (driver.adapterId !== "claude" || environment.YUI_ADAPTER_ID !== driver.adapterId) {
41
+ throw new Error("Global runtime Hook requires the Claude adapter.");
42
+ }
43
+ const occurrenceId = `${now.toISOString()}:${(dependencies.sequence ?? monotonicSequence)()}`;
44
+ const nativeHook = Object.freeze({ hookEventName, payload, occurrenceId });
45
+ const nativeSessionId = requireIdentity(driver.runtime.nativeSessionId(nativeHook), "Agent Driver native Session id");
46
+ if (nativeSessionId !== requireIdentity(environment.YUI_NATIVE_SESSION_ID, "YUI native session id")) {
47
+ throw new Error("Global runtime Hook native Session does not match its launch envelope.");
48
+ }
49
+ const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
50
+ const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
51
+ new FileRuntimeEventInbox(home, () => now).enqueueTurnCompleted({
52
+ scope: "global",
53
+ roleName,
54
+ agentId: requireIdentity(environment.YUI_AGENT_ID, "Agent id"),
55
+ adapterId: "claude",
56
+ launchId: requireIdentity(environment.YUI_LAUNCH_ID, "Launch id"),
57
+ nativeSessionId,
58
+ turnId: optionalIdentity(driver.runtime.nativeTurnId(nativeHook)) ?? occurrenceId,
59
+ ...(environment.YUI_SESSION_TITLE === undefined
60
+ ? {}
61
+ : { title: requireIdentity(environment.YUI_SESSION_TITLE, "Session title") }),
62
+ summary: globalHookSummary(payload, hookEventName)
63
+ });
64
+ await call(home, "scheduler.signal", { key: runtimeLifecycleSignalKey({ scope: "global", roleName }) }, { timeoutMs: 100 }).catch(() => { });
65
+ }
66
+ function globalHookSummary(payload, hookEventName) {
67
+ for (const field of ["last_assistant_message", "summary", "message", "error"]) {
68
+ const value = payload[field];
69
+ if (typeof value === "string" && value.trim().length > 0)
70
+ return value.trim();
71
+ }
72
+ return hookEventName === "Stop" ? "Native Turn completed." : "Native Turn failed.";
73
+ }
29
74
  export function parseRuntimeObservationHook(stdinJson, environment, now = new Date(), dependencies = {}) {
30
75
  const payload = parseObject(stdinJson);
31
76
  const drivers = dependencies.drivers ?? builtinAgentDriverRegistry();
@@ -3,6 +3,7 @@ import { callController } from "../core/controllerClient.js";
3
3
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
4
4
  import { createRuntimeObservation, runtimeObservationSemanticKey } from "../runtime/runtimeObservation.js";
5
5
  import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
6
+ import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
6
7
  import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
7
8
  import { resolveRuntimeHookRunFence } from "./runtimeHookRunFence.js";
8
9
  let structuredSequence = 0;
@@ -256,10 +257,26 @@ async function persistAndApply(home, observations, taskId, roleName) {
256
257
  const inbox = new FileRuntimeEventInbox(home);
257
258
  for (const entry of observations)
258
259
  inbox.enqueueObservation(entry);
260
+ // The immutable inbox is authoritative. During a release/update handover the
261
+ // old Controller is draining and the replacement is not ready yet; leave the
262
+ // entries for normal inbox replay instead of turning a healthy Provider Turn
263
+ // into a transport failure.
264
+ if (isForeignHandoverLockHeld(home))
265
+ return;
259
266
  for (const entry of observations) {
260
- const result = await callController(home, "runtime.observation-apply", entry, {
261
- timeoutMs: 10_000
262
- });
267
+ let result;
268
+ try {
269
+ result = await callController(home, "runtime.observation-apply", entry, {
270
+ timeoutMs: 10_000
271
+ });
272
+ }
273
+ catch (error) {
274
+ // Close the race where the handover begins after the first check but
275
+ // before this socket call. The durable entry remains pending for replay.
276
+ if (isForeignHandoverLockHeld(home))
277
+ return;
278
+ throw error;
279
+ }
263
280
  // A fast Provider can accept the initial Turn before the scheduler call
264
281
  // that launched this Host has returned and committed `run.pushed`. The
265
282
  // immutable inbox entry already makes that exact fenced fact durable;
@@ -14,6 +14,15 @@ export class ControllerClientError extends Error {
14
14
  this.name = "ControllerClientError";
15
15
  }
16
16
  }
17
+ /** The Controller may have committed the request before this client lost its acknowledgement. */
18
+ export function controllerCallMayHaveApplied(error) {
19
+ return error instanceof ControllerClientError
20
+ && [
21
+ "CONTROLLER_TIMEOUT",
22
+ "CONTROLLER_UNAVAILABLE",
23
+ "INVALID_RESPONSE"
24
+ ].includes(error.code);
25
+ }
17
26
  export async function readControllerDiscovery(home) {
18
27
  const discoveryPath = join(home, CONTROLLER_DISCOVERY_PATH);
19
28
  try {
@@ -210,6 +219,7 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
210
219
  const socket = createConnection(socketPath);
211
220
  let buffer = Buffer.alloc(0);
212
221
  let settled = false;
222
+ let deliveryStarted = false;
213
223
  const timer = setTimeout(() => {
214
224
  fail(new ControllerClientError("CONTROLLER_TIMEOUT", "Controller request timed out."));
215
225
  }, timeoutMs);
@@ -229,7 +239,13 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
229
239
  socket.destroy();
230
240
  reject(error);
231
241
  };
232
- socket.on("connect", () => socket.write(requestLine));
242
+ socket.on("connect", () => {
243
+ // Once write begins, a missing response cannot prove that the Controller
244
+ // did not commit the request. Callers must use their domain identity to
245
+ // decide whether an explicit retry is safe.
246
+ deliveryStarted = true;
247
+ socket.write(requestLine);
248
+ });
233
249
  socket.on("data", (chunk) => {
234
250
  if (settled)
235
251
  return;
@@ -260,7 +276,9 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
260
276
  fail(invalidResponse());
261
277
  });
262
278
  socket.on("error", () => {
263
- fail(new ControllerClientError("CONTROLLER_UNAVAILABLE", "Controller is unavailable."));
279
+ fail(deliveryStarted
280
+ ? new ControllerClientError("CONTROLLER_DELIVERY_UNKNOWN", "Controller request delivery is unknown.")
281
+ : new ControllerClientError("CONTROLLER_UNAVAILABLE", "Controller is unavailable."));
264
282
  });
265
283
  });
266
284
  }
@@ -833,6 +833,7 @@ function safeApplicationErrorCode(code) {
833
833
  case "METHOD_NOT_FOUND":
834
834
  case "NOT_FOUND":
835
835
  case "UNAUTHORIZED":
836
+ case "CONTROLLER_DRAINING":
836
837
  return code;
837
838
  default:
838
839
  return undefined;
@@ -3,9 +3,13 @@ import { requireIdentity, requireText, requireTimestamp } from "../domain/valida
3
3
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
4
4
  import { validateExecutionGroup } from "./executionGroup.js";
5
5
  export const RESOURCE_BROKER_POLICY_SCHEMA_VERSION = 1;
6
+ /** Compatibility/display default; observed tokens never close execution spend. */
7
+ export const DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE = 120_000;
6
8
  /** One shared hard-stop predicate for admission, recovery, and Leader routing. */
7
9
  export function executionStageSpendClosed(resources) {
8
- return resources.deadlineReached || resources.exhaustedBudgets.length > 0;
10
+ return resources.deadlineReached
11
+ || resources.exhaustedBudgets.includes("tool-calls")
12
+ || resources.exhaustedBudgets.includes("wall-clock");
9
13
  }
10
14
  const DEFAULT_ACTIVE_LANES = 4;
11
15
  const DEFAULT_ACTIVE_LANES_PER_TASK = 2;
@@ -147,9 +151,6 @@ export function projectExecutionStageResources(input) {
147
151
  const tokensObservable = input.usage.tokensObservable ?? true;
148
152
  const toolCallsObservable = input.usage.toolCallsObservable ?? true;
149
153
  const exhaustedBudgets = [];
150
- if (stage?.budget.maxTokens !== undefined
151
- && tokens >= stage.budget.maxTokens)
152
- exhaustedBudgets.push("tokens");
153
154
  if (toolCallsObservable && stage?.budget.maxToolCalls !== undefined
154
155
  && toolCalls >= stage.budget.maxToolCalls)
155
156
  exhaustedBudgets.push("tool-calls");