@zq-silk/yui 2.20.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +88 -28
  3. package/dist/cli/dynamicCompletion.js +4 -1
  4. package/dist/cli/helpRenderer.js +12 -1
  5. package/dist/cli/interactionPolicy.js +2 -0
  6. package/dist/cli/invocationAuthority.js +2 -6
  7. package/dist/cli/invocationRouter.js +5 -5
  8. package/dist/cli/managedDiagnostics.js +24 -16
  9. package/dist/cli/roleWizard.js +5 -1
  10. package/dist/commands/durableJobCommands.js +2 -3
  11. package/dist/commands/globalRoleCommands.js +33 -8
  12. package/dist/commands/inboxCommands.js +39 -12
  13. package/dist/commands/operatorCommands.js +2 -5
  14. package/dist/commands/releaseCommands.js +16 -2
  15. package/dist/commands/roleConfiguration.js +25 -2
  16. package/dist/commands/sessionCommands.js +5 -2
  17. package/dist/commands/taskCommands.js +225 -99
  18. package/dist/commands/taskContextCommand.js +26 -4
  19. package/dist/commands/taskFactCommands.js +20 -7
  20. package/dist/commands/taskIntegrationCommands.js +3 -3
  21. package/dist/commands/taskRoleRuntimeStatus.js +15 -5
  22. package/dist/commands/taskUsageCommand.js +2 -6
  23. package/dist/commands/textInput.js +2 -2
  24. package/dist/context/contextFamilies.js +10 -0
  25. package/dist/context/currentSkills.js +6 -4
  26. package/dist/context/runContextPack.js +44 -5
  27. package/dist/context/runInputContract.js +1 -1
  28. package/dist/context/sessionBootstrapManifest.js +7 -5
  29. package/dist/context/sessionSkillNotice.js +53 -0
  30. package/dist/context/taskCatalog.js +4 -13
  31. package/dist/context/taskContext.js +135 -103
  32. package/dist/context/taskSearch.js +3 -7
  33. package/dist/controlPlaneCli.js +23 -25
  34. package/dist/controller/agentRuntimeObserver.js +57 -12
  35. package/dist/controller/clientRuntime.js +37 -8
  36. package/dist/controller/controller.js +3 -1
  37. package/dist/controller/diagnosticJournal.js +10 -2
  38. package/dist/controller/fileSchedulerStoreAdapter.js +72 -24
  39. package/dist/controller/jobClient.js +2 -2
  40. package/dist/controller/jobControl.js +2 -2
  41. package/dist/controller/jobDefinitionControl.js +5 -1
  42. package/dist/controller/messageHealth.js +4 -3
  43. package/dist/controller/providerRetryAdmission.js +15 -1
  44. package/dist/controller/providerRetryDelivery.js +18 -2
  45. package/dist/controller/runtime.js +15 -5
  46. package/dist/controller/runtimeEventInbox.js +107 -13
  47. package/dist/controller/runtimeEventProcessor.js +3 -3
  48. package/dist/controller/taskRunRecovery.js +153 -0
  49. package/dist/core/controllerClient.js +16 -2
  50. package/dist/core/controllerServer.js +14 -5
  51. package/dist/core/controllerTelemetry.js +51 -5
  52. package/dist/errors/cliFailure.js +14 -5
  53. package/dist/executor/agentAdapter.js +32 -4
  54. package/dist/executor/agentConfigurationFields.js +2 -0
  55. package/dist/executor/taskAgentCapabilities.js +2 -2
  56. package/dist/integration/gitIntegrationService.js +3 -2
  57. package/dist/integration/integrationSourceApplication.js +12 -1
  58. package/dist/interaction/notificationPresentation.js +11 -16
  59. package/dist/interaction/operatorPresentation.js +1 -1
  60. package/dist/message/messageContinuation.js +3 -1
  61. package/dist/output/boundedRead.js +36 -7
  62. package/dist/output/rolePresentation.js +3 -1
  63. package/dist/runtime/agentHost.js +12 -3
  64. package/dist/runtime/codexAppServerRuntime.js +110 -24
  65. package/dist/runtime/codexInteractiveHost.js +46 -0
  66. package/dist/runtime/providerRetry.js +5 -2
  67. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  68. package/dist/runtime/structuredProviderHost.js +32 -34
  69. package/dist/scheduler/wakeupQueue.js +10 -4
  70. package/dist/storage/sqliteSchema.js +9 -0
  71. package/dist/storage/storageVersions.js +1 -1
  72. package/dist/task/taskAuthority.js +7 -3
  73. package/dist/tmux/tmuxManager.js +4 -2
  74. package/dist/web/webSettings.js +2 -0
  75. package/docs/authorization.md +18 -3
  76. package/docs/cli-information-contract.md +245 -6
  77. package/docs/cli-information-contract.zh-CN.md +107 -5
  78. package/docs/managed-turn-and-session-runtime.md +82 -21
  79. package/docs/managed-turn-and-session-runtime.zh-CN.md +56 -13
  80. package/docs/provider-retry.md +11 -0
  81. package/docs/release-workflow.md +62 -0
  82. package/docs/release-workflow.zh-CN.md +46 -0
  83. package/docs/roles-and-configuration.md +28 -0
  84. package/docs/roles-and-configuration.zh-CN.md +21 -0
  85. package/docs/storage-baseline.md +4 -1
  86. package/docs/storage-baseline.zh-CN.md +1 -1
  87. package/docs/task-delivery.md +7 -0
  88. package/docs/task-delivery.zh-CN.md +5 -0
  89. package/docs/task-discovery.md +5 -5
  90. package/docs/task-discovery.zh-CN.md +4 -4
  91. package/docs/testing/review-evaluation.md +7 -7
  92. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  93. package/docs/testing/verification-levels.md +45 -2
  94. package/docs/testing/verification-levels.zh-CN.md +21 -2
  95. package/i18n/README.zh-CN.md +6 -0
  96. package/package.json +1 -1
  97. package/skills/yui-leader/SKILL.md +98 -189
  98. package/skills/yui-leader/VERSION +1 -1
  99. package/skills/yui-leader/references/artifacts.md +41 -0
  100. package/skills/yui-leader/references/assignment-execution.md +140 -0
  101. package/skills/yui-leader/references/completion.md +79 -0
  102. package/skills/yui-leader/references/delivery-scope.md +26 -0
  103. package/skills/yui-leader/references/execution.md +16 -487
  104. package/skills/yui-leader/references/implementation.md +81 -0
  105. package/skills/yui-leader/references/planning.md +7 -45
  106. package/skills/yui-leader/references/review.md +90 -0
  107. package/skills/yui-leader/references/task-progress.md +23 -0
  108. package/skills/yui-leader/references/work-allocation.md +118 -0
  109. package/skills/yui-operator/SKILL.md +77 -331
  110. package/skills/yui-operator/VERSION +1 -1
  111. package/skills/yui-operator/references/body-transport.md +62 -0
  112. package/skills/yui-operator/references/configuration.md +47 -0
  113. package/skills/yui-operator/references/lifecycle.md +41 -0
  114. package/skills/yui-operator/references/progress.md +39 -0
  115. package/skills/yui-operator/references/routing.md +57 -0
  116. package/skills/yui-operator/references/submission.md +56 -0
  117. package/skills/yui-operator/references/task-delivery.md +5 -2
  118. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  119. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  120. package/skills/yui-reviewer/SKILL.md +4 -2
  121. package/skills/yui-reviewer/VERSION +1 -1
  122. package/skills/yui-runtime/SKILL.md +177 -505
  123. package/skills/yui-runtime/VERSION +1 -1
  124. package/skills/yui-runtime/references/archive.md +51 -0
  125. package/skills/yui-runtime/references/command-discovery.md +41 -0
  126. package/skills/yui-runtime/references/completed-followup.md +65 -0
  127. package/skills/yui-runtime/references/context-ordering.md +3 -34
  128. package/skills/yui-runtime/references/context-reads.md +61 -0
  129. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  130. package/skills/yui-runtime/references/execution-failure.md +105 -0
  131. package/skills/yui-runtime/references/global-context.md +18 -0
  132. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  133. package/skills/yui-runtime/references/host-custody.md +30 -0
  134. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  135. package/skills/yui-runtime/references/incremental-context.md +40 -0
  136. package/skills/yui-runtime/references/publication.md +4 -104
  137. package/skills/yui-runtime/references/recovery.md +32 -192
  138. package/skills/yui-runtime/references/session-identity.md +18 -0
  139. package/skills/yui-runtime/references/session-replacement.md +45 -0
  140. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
  141. package/skills/yui-worker/SKILL.md +7 -4
  142. package/skills/yui-worker/VERSION +1 -1
@@ -15,14 +15,45 @@ export class AgentRuntimeObserver {
15
15
  #states = new Map();
16
16
  #maxConcurrentSamples;
17
17
  #sequence = 0;
18
+ #samples = 0;
19
+ #sources = 0;
20
+ #published = 0;
21
+ #usagePublications = 0;
22
+ #failed = 0;
18
23
  constructor(store, inbox, drivers = builtinAgentDriverRegistry(), options = {}) {
19
24
  this.store = store;
20
25
  this.inbox = inbox;
21
26
  this.drivers = drivers;
22
27
  this.#maxConcurrentSamples = sampleConcurrency(options.maxConcurrentSamples);
23
28
  }
24
- async sample(now = new Date()) {
25
- const active = this.activeSources();
29
+ async sample(now = new Date(), phases) {
30
+ this.#samples++;
31
+ const finishDiscovery = phases?.start("runtime-sample-discovery", "home");
32
+ let active;
33
+ try {
34
+ active = this.activeSources();
35
+ finishDiscovery?.();
36
+ }
37
+ catch (error) {
38
+ finishDiscovery?.(true);
39
+ throw error;
40
+ }
41
+ this.#sources += active.length;
42
+ const publish = async (observation) => {
43
+ try {
44
+ const result = await (phases === undefined
45
+ ? this.inbox.enqueueObservationAsync(observation)
46
+ : phases.observe(observation.payload.usage === undefined ? "runtime-sample-publish" : "runtime-usage-publish", undefined, () => this.inbox.enqueueObservationAsync(observation)));
47
+ this.#published++;
48
+ if (observation.payload.usage !== undefined)
49
+ this.#usagePublications++;
50
+ return result;
51
+ }
52
+ catch (error) {
53
+ this.#failed++;
54
+ throw error;
55
+ }
56
+ };
26
57
  const activeKeys = new Set(active.map(({ key }) => key));
27
58
  for (const key of this.#states.keys()) {
28
59
  if (!activeKeys.has(key))
@@ -37,7 +68,10 @@ export class AgentRuntimeObserver {
37
68
  // usage/activity baseline is durable. Rehydrate it after Controller
38
69
  // restart so rereading the bounded transcript tail cannot manufacture a
39
70
  // fresh activity edge from tokens that were already observed.
40
- const state = existingState ?? { ...persistedState };
71
+ // Commit process-local state only when every fact from this sample is
72
+ // durable. A failed later publication must leave the old cursor usable;
73
+ // retry deduplicates any already-persisted prefix.
74
+ const state = { ...(existingState ?? persistedState) };
41
75
  const restoredUsageEventId = existingState === undefined
42
76
  ? persistedState.usageEventId
43
77
  : undefined;
@@ -63,7 +97,6 @@ export class AgentRuntimeObserver {
63
97
  detail: error instanceof Error ? error.message : String(error)
64
98
  });
65
99
  }
66
- state.cursor = sample.cursor;
67
100
  const at = now.toISOString();
68
101
  // Reserve sequence numbers in source-key order before sampling. Provider
69
102
  // latency can change completion order without changing observation
@@ -101,7 +134,7 @@ export class AgentRuntimeObserver {
101
134
  const identity = completeHistory
102
135
  ? tokenObservationIdentity("baseline", fence, source.sourceId, baselineKey)
103
136
  : usageObservationIdentity(fence, source.sourceId, latestOccurrence);
104
- this.inbox.enqueueObservation(createRuntimeObservation({
137
+ await publish(createRuntimeObservation({
105
138
  schemaVersion: 1,
106
139
  eventId: identity.eventId,
107
140
  semanticKey: identity.semanticKey,
@@ -136,7 +169,7 @@ export class AgentRuntimeObserver {
136
169
  }
137
170
  const health = JSON.stringify([sample.status, sample.detail ?? null]);
138
171
  if (state.health !== health) {
139
- this.inbox.enqueueObservation(createRuntimeObservation({
172
+ await publish(createRuntimeObservation({
140
173
  schemaVersion: 1,
141
174
  eventId: observationId("health", fence, source.sourceId, health),
142
175
  semanticKey: observationId("health", fence, source.sourceId, health),
@@ -180,9 +213,9 @@ export class AgentRuntimeObserver {
180
213
  : Object.freeze({ ...occurrence, observationQuality: "partial" })));
181
214
  }
182
215
  }
183
- usages.forEach((occurrence, usageIndex) => {
216
+ for (const [usageIndex, occurrence] of usages.entries()) {
184
217
  const identity = usageObservationIdentity(fence, source.sourceId, occurrence);
185
- this.inbox.enqueueObservation(createRuntimeObservation({
218
+ await publish(createRuntimeObservation({
186
219
  schemaVersion: 1,
187
220
  eventId: identity.eventId,
188
221
  semanticKey: identity.semanticKey,
@@ -210,9 +243,9 @@ export class AgentRuntimeObserver {
210
243
  state.usageOccurrenceId = occurrence.occurrenceId;
211
244
  state.usageOccurrenceCheckpoint = occurrence.resumeCheckpoint;
212
245
  }
213
- });
214
- if (activityChanged && state.cursor !== undefined) {
215
- this.inbox.enqueueObservation(createRuntimeObservation({
246
+ }
247
+ if (activityChanged && sample.cursor !== undefined) {
248
+ await publish(createRuntimeObservation({
216
249
  schemaVersion: 1,
217
250
  eventId: observationId("activity", fence, source.sourceId, sample.activityId),
218
251
  semanticKey: observationId("activity", fence, source.sourceId, sample.activityId),
@@ -232,10 +265,17 @@ export class AgentRuntimeObserver {
232
265
  }
233
266
  if (sample.activityId !== undefined)
234
267
  state.activityId = sample.activityId;
268
+ state.cursor = sample.cursor;
235
269
  this.#states.set(key, state);
236
270
  });
237
271
  return Object.freeze([...dirty].sort(numericCompare));
238
272
  }
273
+ /** Counts successful durable admissions (including duplicates), not tokens or
274
+ * provider content. Phase observations carry discovery/publication duration. */
275
+ metrics() {
276
+ return { samples: this.#samples, sources: this.#sources, published: this.#published,
277
+ usagePublications: this.#usagePublications, failed: this.#failed };
278
+ }
239
279
  activeSources() {
240
280
  const result = [];
241
281
  const activeTasks = [...new Set(this.store.listActiveTaskIds())]
@@ -354,13 +394,18 @@ export class AgentRuntimeObserver {
354
394
  async function forEachConcurrent(values, limit, visit) {
355
395
  let nextIndex = 0;
356
396
  const workers = Math.min(limit, values.length);
357
- await Promise.all(Array.from({ length: workers }, async () => {
397
+ const results = await Promise.allSettled(Array.from({ length: workers }, async () => {
358
398
  while (nextIndex < values.length) {
359
399
  const index = nextIndex;
360
400
  nextIndex += 1;
361
401
  await visit(values[index], index);
362
402
  }
363
403
  }));
404
+ // An admission failure must not release the Controller's in-flight sample
405
+ // guard while another source can still commit state from that sample.
406
+ const failure = results.find(result => result.status === "rejected");
407
+ if (failure?.status === "rejected")
408
+ throw failure.reason;
364
409
  }
365
410
  function sampleConcurrency(value) {
366
411
  const resolved = value ?? DEFAULT_MAX_CONCURRENT_SAMPLES;
@@ -2,7 +2,8 @@ import { spawn } from "node:child_process";
2
2
  import { jobCleanupBlocker } from "../job/jobCleanupInspection.js";
3
3
  import { describeCliFailure } from "../errors/cliFailure.js";
4
4
  import { fileURLToPath } from "node:url";
5
- import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "../core/controllerProcessIdentity.js";
5
+ import { findLiveControllerProcessForHome } from "../core/controllerProcessIdentity.js";
6
+ import { processGenerationIsLive } from "../core/fileLockOwner.js";
6
7
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
7
8
  import { parseControllerIdentity } from "../core/controllerIdentity.js";
8
9
  import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
@@ -12,6 +13,7 @@ import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
12
13
  import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
13
14
  import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
14
15
  import { assertControllerStatusIdentity } from "../runtime/runtimeCoherence.js";
16
+ import { readLinuxProcessIdentity } from "../runtime/sessionOwnerIdentity.js";
15
17
  import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
16
18
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
17
19
  import { yuiVersionIdentity } from "../version.js";
@@ -169,13 +171,27 @@ export async function stopFileTaskController(home, options = {}) {
169
171
  await waitForForeignHandover(home, options);
170
172
  const physical = options.call === undefined
171
173
  ? findLiveControllerProcessForHome(readHomeFilesystemId(home)) : undefined;
172
- const expectedPid = options.expectedPid ?? physical?.pid;
174
+ // A protected, Home-validated discovery can retain the generation fence
175
+ // even if the optional /proc ownership inventory is currently unreadable.
176
+ // It is not signal authority; physical recovery still revalidates ownership.
177
+ let captured = physical;
178
+ if (captured === undefined && options.call === undefined) {
179
+ try {
180
+ const discovery = await readControllerDiscovery(home);
181
+ captured = { pid: discovery.pid, processStartIdentity: discovery.processStartIdentity };
182
+ }
183
+ catch (error) {
184
+ if (!isDefinitelyNotRunning(error))
185
+ throw error;
186
+ }
187
+ }
188
+ const expectedPid = options.expectedPid ?? captured?.pid;
173
189
  const bounded = { ...options, requestTimeoutMs: options.requestTimeoutMs ?? 2000,
174
190
  ...(expectedPid === undefined ? {} : { expectedPid }) };
175
191
  try {
176
192
  const result = await stopControllerGracefully(home, bounded);
177
- if (result.stopped && physical !== undefined) {
178
- await waitForControllerProcessExit(home, physical, options);
193
+ if (result.stopped && captured !== undefined) {
194
+ await waitForControllerProcessExit(captured, options);
179
195
  }
180
196
  if (result.stopped || options.call !== undefined)
181
197
  return result;
@@ -185,15 +201,28 @@ export async function stopFileTaskController(home, options = {}) {
185
201
  throw error;
186
202
  }
187
203
  const stopped = await stopOrphanedFileTaskController(home, options.shutdownTimeoutMs ?? CONTROLLER_SHUTDOWN_TIMEOUT_MS, { force: true, ...(expectedPid === undefined ? {} : { expectedPid }),
188
- ...(physical === undefined ? {} : { expectedProcessStartIdentity: physical.processStartIdentity }) });
204
+ ...(captured === undefined ? {} : { expectedProcessStartIdentity: captured.processStartIdentity }) });
205
+ // No match is not exit proof: /proc ownership inspection may have become
206
+ // unreadable after we captured this exact generation. Physical recovery
207
+ // cannot authorize a successor until that original generation is gone.
208
+ if (captured !== undefined)
209
+ await waitForControllerProcessExit(captured, options);
189
210
  return stopped === undefined ? { stopped: false, alreadyStopped: true } : { stopped: true, pid: stopped.pid };
190
211
  }
191
- async function waitForControllerProcessExit(home, controller, options) {
212
+ async function waitForControllerProcessExit(controller, options) {
192
213
  const timeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
193
214
  const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
194
- const homeFilesystemId = readHomeFilesystemId(home);
195
215
  const deadline = Date.now() + timeoutMs;
196
- while (inspectLiveControllerProcess(controller.pid, homeFilesystemId, controller.processStartIdentity) !== undefined) {
216
+ // This check is tri-state: unreadable OS identity throws rather than
217
+ // collapsing an unknown/live process into the absence of a Controller match.
218
+ while (processGenerationIsLive(controller.pid, controller.processStartIdentity)) {
219
+ if (process.platform === "linux") {
220
+ const observed = readLinuxProcessIdentity(controller.pid);
221
+ // Preserve the existing physical-stop boundary: an exact zombie has
222
+ // exited, even if its parent has not reaped the process table entry.
223
+ if (observed?.startIdentity === controller.processStartIdentity && observed.state === "Z")
224
+ return;
225
+ }
197
226
  if (Date.now() >= deadline) {
198
227
  throw new ControllerClientError("CONTROLLER_TIMEOUT", `Controller process ${controller.pid} did not exit within ${timeoutMs} ms.`);
199
228
  }
@@ -792,6 +792,7 @@ export class FileTaskController {
792
792
  runtimeMetrics() {
793
793
  const metrics = this.#lastRuntimeDrain?.metrics ?? ZERO_DRAIN_METRICS;
794
794
  return {
795
+ sampler: this.#runtimeObserver?.metrics?.() ?? null,
795
796
  messageHealth: this.#lastMessageHealth,
796
797
  diagnostics: this.#diagnosticMetrics?.() ?? null,
797
798
  phases: this.#phases.snapshot(),
@@ -920,7 +921,7 @@ export class FileTaskController {
920
921
  if (this.#stopped || this.#runtimeObserver === undefined
921
922
  || this.#runtimeObserverCurrent !== undefined)
922
923
  return;
923
- const running = this.#runtimeObserver.sample(this.#now()).then((keys) => {
924
+ const running = this.#phases.observe("runtime-sample", "home", () => this.#runtimeObserver.sample(this.#now(), this.#phases)).then((keys) => {
924
925
  if (this.#stopped)
925
926
  return;
926
927
  for (const key of keys)
@@ -1779,6 +1780,7 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
1779
1780
  await runtime.shutdownAndDrain();
1780
1781
  }, {
1781
1782
  domainIdentity: options.domainIdentity,
1783
+ onRequestDiagnostic: options.onRequestDiagnostic,
1782
1784
  status: ({ commandObserver, eventLoopDelay }) => ({
1783
1785
  ...runtime.runtimeMetrics(),
1784
1786
  commands: {
@@ -13,13 +13,15 @@ const NUMERIC_KEYS = new Set([
13
13
  "completed", "failed", "received", "inFlight", "maximumMs", "maximumLagMs",
14
14
  "maximumObservedAtMs", "oldestInFlightAgeMs", "startedAtMs", "elapsedMs",
15
15
  "ageMs", "depth", "semanticDepth", "progressDepth", "pendingKeys", "queueDepth",
16
- "hits", "rebuilds", "invalidations", "passes", "listedEvents", "selectedEvents"
16
+ "hits", "rebuilds", "invalidations", "passes", "listedEvents", "selectedEvents",
17
+ "acceptedAtMs", "receivedAtMs", "routedAtMs", "handlerCompletedAtMs", "flushCompletedAtMs",
18
+ "handlerFailed", "flushFailed", "samples", "sources", "published", "usagePublications"
17
19
  ]);
18
20
  const CONTAINER_KEYS = new Set([
19
21
  "commands", "observations", "routes", "eventLoopDelay", "phases", "reads",
20
22
  "persistence", "main", "worker", "database", "roundTrips", "work", "body",
21
23
  "lock", "commit", "payloadReads", "inbox", "scheduler", "projectionCache",
22
- "active", "recent", "slowest", "latest"
24
+ "active", "recent", "slowest", "latest", "transport", "sampler"
23
25
  ]);
24
26
  const ERROR_CODES = new Set([
25
27
  "SQLITE_BUSY", "SQLITE_LOCKED", "SQLITE_IOERR", "SQLITE_FULL", "SQLITE_READONLY",
@@ -101,6 +103,12 @@ export class ControllerDiagnosticJournal {
101
103
  error(error) {
102
104
  this.#enqueue("error", { error: errorEvidence(error) });
103
105
  }
106
+ /** Completion-driven evidence bypasses the periodic recent-array/sample
107
+ * window, but uses the same allowlist, queue/record/file bounds and async I/O.
108
+ * Disk failure, queue pressure and force-kill remain explicit evidence gaps. */
109
+ request(record) {
110
+ this.#enqueue("request", { request: safeSnapshot(record) });
111
+ }
104
112
  sample(snapshot, lagMs) {
105
113
  const now = monotonicMilliseconds();
106
114
  if (now - this.#lastSampleAt < 1000) {
@@ -43,7 +43,7 @@ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
43
43
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
44
44
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
45
45
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
46
- import { acceptProviderTurn, beginProviderTurn, cancelQuiescentProviderInput, clearProviderGoal, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, settleProviderTurn, settleProviderTurnSubmission, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
46
+ import { acceptProviderTurn, beginProviderTurn, cancelQuiescentProviderInput, clearProviderGoal, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, providerTurnIsActive, settleProviderTurn, settleProviderTurnSubmission, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
47
47
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
48
48
  import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeUsageEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
49
49
  import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
@@ -106,11 +106,24 @@ export class FileSchedulerStoreAdapter {
106
106
  return errors;
107
107
  }
108
108
  observeAgentHostObservation(event, now = new Date()) {
109
+ let preparation;
110
+ if (isRunTerminalObservation(event.observation)) {
111
+ preparation = {};
112
+ try {
113
+ preparation = this.prepareRuntimeTerminalObservation(resolveAgentHostObservation(this.store, event), now);
114
+ }
115
+ catch (error) {
116
+ if (!(error instanceof AgentHostObservationDeferred || error instanceof RuntimeHookRunFenceError))
117
+ throw error;
118
+ // Re-resolve under the committing fence below; preparation grants no
119
+ // authority and must not settle a deferred/obsolete Host envelope.
120
+ }
121
+ }
109
122
  // Match canonical ingress: archive and Host fact settlement share the
110
123
  // same write boundary, including the original transport envelope.
111
- return this.store.transaction(() => this.foldAgentHostObservation(event, now));
124
+ return this.store.transaction(() => this.foldAgentHostObservation(event, now, preparation));
112
125
  }
113
- foldAgentHostObservation(event, now) {
126
+ foldAgentHostObservation(event, now, preparation) {
114
127
  const raw = createRuntimeObservation(event.observation);
115
128
  const task = raw.fence.taskId === undefined ? null : this.store.getTask(raw.fence.taskId);
116
129
  if (raw.fence.taskId !== undefined && task === null)
@@ -154,7 +167,7 @@ export class FileSchedulerStoreAdapter {
154
167
  // Rejection is durable diagnostic evidence, never business acceptance.
155
168
  return this.recordObsoleteCanonicalObservation(event.observation, error.message, now);
156
169
  }
157
- return this.observeRuntimeObservation(input, now);
170
+ return this.foldRuntimeObservation(input, now, preparation);
158
171
  }
159
172
  /**
160
173
  * Sole provider-independent ingress for structured runtime state. Driver
@@ -163,12 +176,30 @@ export class FileSchedulerStoreAdapter {
163
176
  * transitions, and retains the canonical observation for status projection.
164
177
  */
165
178
  observeRuntimeObservation(raw, now = new Date()) {
179
+ const preparation = this.prepareRuntimeTerminalObservation(raw, now);
166
180
  // Archive and observation settlement share the same write boundary.
167
181
  // An ingress read outside this transaction must never authorize clearing
168
182
  // a claim after another CLI has committed archive.
169
- return this.store.transaction(() => this.foldRuntimeObservation(raw, now));
183
+ return this.store.transaction(() => this.foldRuntimeObservation(raw, now, preparation));
170
184
  }
171
- foldRuntimeObservation(raw, now) {
185
+ prepareRuntimeTerminalObservation(raw, now) {
186
+ const input = createRuntimeObservation(raw);
187
+ if (!isRunTerminalObservation(input))
188
+ return undefined;
189
+ if (input.fence.taskId === undefined || Date.parse(input.receivedAt) > now.getTime())
190
+ return {};
191
+ const adapterId = this.adapterForRuntimeObservation(input);
192
+ if (adapterId === null)
193
+ return {};
194
+ return this.prepareRuntimeTerminal({
195
+ taskId: input.fence.taskId, roleName: input.fence.roleName,
196
+ agentId: input.fence.agentId, adapterId, conversationId: input.fence.conversationId,
197
+ nativeSessionId: input.fence.nativeSessionId, nativeTurnId: input.fence.nativeTurnId,
198
+ attemptId: input.fence.receiptId, runId: input.fence.runId
199
+ }, input.kind === "turn.completed" && input.payload.resultTransportDiagnostic === undefined
200
+ && transportAgentResult(input.payload.output).status === "completed");
201
+ }
202
+ foldRuntimeObservation(raw, now, preparation) {
172
203
  const input = createRuntimeObservation(raw);
173
204
  const taskId = input.fence.taskId;
174
205
  if (taskId === undefined)
@@ -263,13 +294,13 @@ export class FileSchedulerStoreAdapter {
263
294
  outcome = this.observeRuntimePromptAccepted(input, adapterId, now);
264
295
  break;
265
296
  case "turn.completed":
266
- outcome = this.foldProviderTurnBoundary(input, adapterId, "completed", now);
297
+ outcome = this.foldProviderTurnBoundary(input, adapterId, "completed", now, preparation);
267
298
  break;
268
299
  case "turn.failed": {
269
300
  // A Provider Turn is an activation boundary, not a Task outcome.
270
301
  // Keep the AgentRun and Session identity available for Agent-directed
271
302
  // restore + submit, even when a provider marks its AgentRun terminal.
272
- outcome = this.foldProviderTurnBoundary(input, adapterId, "failed", now);
303
+ outcome = this.foldProviderTurnBoundary(input, adapterId, "failed", now, preparation);
273
304
  break;
274
305
  }
275
306
  case "operation.started":
@@ -286,7 +317,7 @@ export class FileSchedulerStoreAdapter {
286
317
  outcome = this.validateCanonicalRunObservation(input, now);
287
318
  break;
288
319
  case "turn.cancelled": {
289
- outcome = this.foldProviderTurnBoundary(input, adapterId, "cancelled", now);
320
+ outcome = this.foldProviderTurnBoundary(input, adapterId, "cancelled", now, preparation);
290
321
  break;
291
322
  }
292
323
  case "conversation.observed":
@@ -617,7 +648,7 @@ export class FileSchedulerStoreAdapter {
617
648
  return "applied";
618
649
  });
619
650
  }
620
- foldProviderTurnBoundary(input, adapterId, providerStatus, now) {
651
+ foldProviderTurnBoundary(input, adapterId, providerStatus, now, preparation) {
621
652
  const outcome = providerStatus === "completed"
622
653
  ? input.payload.resultTransportDiagnostic === undefined
623
654
  ? transportAgentResult(input.payload.output)
@@ -656,7 +687,7 @@ export class FileSchedulerStoreAdapter {
656
687
  const classification = this.classifyRuntimeRunTerminal(completed);
657
688
  if (classification !== "apply")
658
689
  return classification;
659
- const result = this.observeRuntimeRunTerminal(completed, now);
690
+ const result = this.foldRuntimeRunTerminal(completed, now, preparation ?? {});
660
691
  return result.disposition === "obsolete" ? "obsolete" : "applied";
661
692
  }
662
693
  validateCanonicalSessionObservation(input, now) {
@@ -1774,8 +1805,7 @@ export class FileSchedulerStoreAdapter {
1774
1805
  listDormantRuntimeOwners() {
1775
1806
  return this.listRuntimeSessionCandidates().flatMap((candidate) => {
1776
1807
  if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(candidate.owner)))
1777
- || (candidate.owner.scope === "task"
1778
- && this.store.getActiveRun(candidate.owner.taskId, candidate.owner.roleName) !== null)) {
1808
+ || runtimeOwnerHasActiveWork(this.store, candidate.owner)) {
1779
1809
  return [];
1780
1810
  }
1781
1811
  return [{
@@ -2282,17 +2312,24 @@ export class FileSchedulerStoreAdapter {
2282
2312
  classifyRuntimeRunTerminal(input) {
2283
2313
  return resolveTerminalExecution(this.store, input) === null ? "obsolete" : "apply";
2284
2314
  }
2315
+ prepareRuntimeTerminal(input, completed) {
2316
+ const run = resolveTerminalExecution(this.store, input)?.run;
2317
+ const workspace = run?.status === "active"
2318
+ && run.executionGroupId !== undefined
2319
+ && run.executionLaneId !== undefined
2320
+ && run.workspace !== undefined
2321
+ && completed
2322
+ ? this.#observeRead("git-workspace-snapshot", run.taskId, () => this.snapshotExecutionLaneWorkspace(this.store, run.workspace))
2323
+ : undefined;
2324
+ return { run, workspace };
2325
+ }
2285
2326
  observeRuntimeRunTerminal(input, now = new Date()) {
2327
+ return this.foldRuntimeRunTerminal(input, now, this.prepareRuntimeTerminal(input, input.outcome.status === "completed"));
2328
+ }
2329
+ foldRuntimeRunTerminal(input, now, preparation) {
2286
2330
  // Resource inspection is bounded but external to SQLite. Revalidate the
2287
2331
  // immutable AgentRun workspace after acquiring the write transaction.
2288
- const preparedRun = resolveTerminalExecution(this.store, input)?.run;
2289
- const workspace = preparedRun?.status === "active"
2290
- && preparedRun.executionGroupId !== undefined
2291
- && preparedRun.executionLaneId !== undefined
2292
- && preparedRun.workspace !== undefined
2293
- && input.outcome.status === "completed"
2294
- ? this.#observeRead("git-workspace-snapshot", preparedRun.taskId, () => this.snapshotExecutionLaneWorkspace(this.store, preparedRun.workspace))
2295
- : undefined;
2332
+ const { run: preparedRun, workspace } = preparation;
2296
2333
  return this.store.transaction((store) => {
2297
2334
  const task = store.getTask(input.taskId);
2298
2335
  if (task === null)
@@ -2304,7 +2341,8 @@ export class FileSchedulerStoreAdapter {
2304
2341
  if (resolved === null)
2305
2342
  throw new Error("Runtime terminal has no exact execution binding.");
2306
2343
  const { run: observedRun, current: recordedProviderTurn, attemptId } = resolved;
2307
- if (!isDeepStrictEqual(preparedRun?.workspace, observedRun?.workspace)) {
2344
+ if (preparedRun?.id !== observedRun?.id
2345
+ || !isDeepStrictEqual(preparedRun?.workspace, observedRun?.workspace)) {
2308
2346
  throw new Error("Runtime terminal workspace changed during result preparation.");
2309
2347
  }
2310
2348
  if (workspace !== undefined && observedRun?.workspace !== undefined
@@ -3132,8 +3170,7 @@ function sameRuntimeOwner(left, right) {
3132
3170
  }
3133
3171
  function dormantRuntimeCandidateIsCurrent(store, candidate, allowActiveRun = false) {
3134
3172
  const { owner } = candidate;
3135
- if (!allowActiveRun && owner.scope === "task"
3136
- && store.getActiveRun(owner.taskId, owner.roleName) !== null) {
3173
+ if (!allowActiveRun && runtimeOwnerHasActiveWork(store, owner)) {
3137
3174
  return false;
3138
3175
  }
3139
3176
  const sessions = runtimeOwnerSessionSet(store, owner);
@@ -3143,6 +3180,17 @@ function dormantRuntimeCandidateIsCurrent(store, candidate, allowActiveRun = fal
3143
3180
  return active !== undefined && active.agentId === candidate.agentId && active.adapterId === candidate.adapterId
3144
3181
  && active.nativeSessionId === candidate.nativeSessionId && (allowActiveRun || active.updatedAt === candidate.sessionUpdatedAt);
3145
3182
  }
3183
+ function runtimeOwnerHasActiveWork(store, owner) {
3184
+ if (owner.scope === "task")
3185
+ return store.getActiveRun(owner.taskId, owner.roleName) !== null;
3186
+ // Global Turns do not have Task AgentRun records. Use their existing native
3187
+ // delivery binding, including submitting and uncertain acceptance.
3188
+ const sessions = store.getGlobalRoleSessionSet(owner.roleName);
3189
+ const binding = sessions?.providerBinding;
3190
+ return binding != null && (providerTurnIsActive(binding.run)
3191
+ || currentProviderConversation(binding).conversationId
3192
+ !== sessions?.sessions[sessions.activeAgentId]?.nativeSessionId);
3193
+ }
3146
3194
  function endRuntimeOwnerSession(store, owner, now) {
3147
3195
  if (owner.scope === "task") {
3148
3196
  const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
@@ -1,4 +1,4 @@
1
- import { resolveJobCaller } from "../task/taskAuthority.js";
1
+ import { resolveJobCaller, resolveJobReadCaller } from "../task/taskAuthority.js";
2
2
  import { callFileTaskController } from "./clientRuntime.js";
3
3
  export async function startDurableJob(home, params, clientOptions = {}) {
4
4
  // rr6/f2: Route through callFileTaskController so an Integration-issued
@@ -89,7 +89,7 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
89
89
  return job;
90
90
  },
91
91
  async getJob(taskId, jobId) {
92
- return getDurableJob(home, taskId, jobId, resolveJobCaller(clientOptions.environment, taskId), clientOptions);
92
+ return getDurableJob(home, taskId, jobId, resolveJobReadCaller(clientOptions.environment, taskId), clientOptions);
93
93
  },
94
94
  async cancelJob(taskId, jobId) {
95
95
  // rr8/rr12: Bind the cancel request to the caller's managed identity.
@@ -18,7 +18,7 @@ import { redactLaunchText } from "../runtime/launchDiagnostics.js";
18
18
  import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/managedCaller.js";
19
19
  import { CliError } from "../errors/cliError.js";
20
20
  import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
21
- import { assertContextRecordReadable } from "../context/taskContext.js";
21
+ import { assertTaskReadable } from "../context/taskContext.js";
22
22
  import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
23
23
  import { validateHttpPreview } from "../job/httpPreview.js";
24
24
  import { definitionFromTaskRun } from "../job/jobDefinition.js";
@@ -134,7 +134,7 @@ export function createDurableJobControl(store) {
134
134
  if (caller === undefined)
135
135
  throw jobControlError("UNAUTHORIZED", "Job reads require an explicit caller.");
136
136
  try {
137
- assertContextRecordReadable(store, taskId, "job", jobId, caller.scope === "user" ? {} : {
137
+ assertTaskReadable(store, taskId, caller.scope === "user" ? {} : {
138
138
  YUI_SESSION_SCOPE: caller.scope, YUI_TASK_ID: caller.taskId, YUI_ROLE: caller.role,
139
139
  YUI_AGENT_ID: caller.agentId, YUI_ADAPTER_ID: caller.adapterId, YUI_NATIVE_SESSION_ID: caller.nativeSessionId
140
140
  });
@@ -137,7 +137,7 @@ export function authorizeRunRead(store, run, caller) {
137
137
  throw new Error("Task delegation is not an Assignment grant.");
138
138
  return;
139
139
  }
140
- // A revised definition's Assignment does not grant access to earlier Runs.
140
+ // Ordinary process-Run evidence does not transfer its execution Assignment.
141
141
  if ("taskId" in run)
142
142
  createDurableJobControl(store).getJob(run.taskId, run.id, caller);
143
143
  }
@@ -168,6 +168,8 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
168
168
  const same = runs.find(run => run.trigger.key === trigger.key && run.trigger.source === trigger.source);
169
169
  if (same !== undefined) {
170
170
  authorizeRunRead(tx, same, caller);
171
+ if ("taskId" in same && !same.delegation)
172
+ requireJobAssignment(tx, same, caller);
171
173
  if (same.definitionRevision !== job.revision || same.retryOf !== retryOf) {
172
174
  throw new Error(`JobRun trigger conflicts with its original snapshot: ${same.id}.`);
173
175
  }
@@ -178,6 +180,8 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
178
180
  || run.status === "unknown-needs-attention");
179
181
  if (occupied !== undefined) {
180
182
  authorizeRunRead(tx, occupied, caller);
183
+ if ("taskId" in occupied && !occupied.delegation)
184
+ requireJobAssignment(tx, occupied, caller);
181
185
  return { run: occupied, created: false, reason: "overlap" };
182
186
  }
183
187
  if (retryOf !== undefined && !runs.some(run => run.id === retryOf
@@ -91,13 +91,14 @@ function recoveryGuidance(o) {
91
91
  if (o.wakeId !== null && roleName === "leader") {
92
92
  const wakeTarget = `${arg(taskId)} ${arg(o.wakeId)}`;
93
93
  if (o.deliveryEvidence?.outcome === "rejected") {
94
- lines.push(`After correcting the rejection cause, and only if this exact wake is still unconsumed, its original claim matches and native acceptance/activity are proven absent: yui task wake retry ${wakeTarget} --reason '<verified correction and quiescence>'. Queued is not accepted. Do not retry accepted/unknown input. This candidate does not establish resolve support for rejected wakes; do not assume another recovery package is installed.`);
94
+ lines.push(`Choose alternative actions, not a sequence. After correcting the rejection cause, and only if this exact wake is still unconsumed, its original claim matches and native acceptance/activity are proven absent: yui task wake retry ${wakeTarget} --reason '<verified correction and quiescence>'. Queued is not accepted. Do not retry accepted/unknown input.`);
95
+ lines.push(`Alternatively, after exact shared-native/background/writer quiescence and persisted notification-attempt evidence: yui task wake resolve ${wakeTarget} --reason '<exact quiescence evidence>'. A rejection is not quiescence, including rejection after an unknown outcome. Resolve releases only scheduling without replay, ACK or acceptance; it preserves original claims and delivery evidence. If the operational mailbox is absent, the exact durable wake/attempt must still be proven; another current batch is never released. The rightful handler must inspect original effects and settle still-valid obligations separately.`);
95
96
  }
96
97
  else if (o.deliveryEvidence?.outcome === "unknown") {
97
- lines.push(`Do not replay unknown input. After exact shared-native/background/writer quiescence and persisted evidence, with the original unknown claim still current: yui task wake resolve ${wakeTarget} --reason '<exact quiescence evidence>'. This releases only scheduling without replay, ACK or acceptance. The rightful handler must inspect original effects and settle still-valid obligations separately.`);
98
+ lines.push(`Do not replay unknown input. After exact shared-native/background/writer quiescence and persisted evidence identifying the original wake/notification attempt: yui task wake resolve ${wakeTarget} --reason '<exact quiescence evidence>'. This releases only scheduling without replay, ACK or acceptance; an absent operational mailbox requires exact durable evidence, never release of another current batch. The rightful handler must inspect original effects and settle still-valid obligations separately.`);
98
99
  }
99
100
  else {
100
- lines.push("Deferred, accepted or unreported delivery does not meet this candidate's rejected-retry/unknown-resolve preconditions. Keep the original wake and inspect the runtime barrier instead of mechanically invoking either action.");
101
+ lines.push("Deferred, accepted or unreported delivery does not meet this candidate's rejected-retry/unknown-or-rejected-resolve preconditions. Keep the original wake and inspect the runtime barrier instead of mechanically invoking either action.");
101
102
  }
102
103
  }
103
104
  }
@@ -4,6 +4,7 @@ import { blockingProviderContinuations } from "../runtime/runtimeContinuationPro
4
4
  import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  import { isHandlingReminder } from "../message/inboxTurn.js";
7
+ import { roleSessionMayContinue } from "../executor/effectiveLaunch.js";
7
8
  export function ownedProviderInput(input) {
8
9
  if (input.runId !== undefined && input.taskId !== undefined) {
9
10
  return { kind: "run", taskId: input.taskId, runId: input.runId };
@@ -90,8 +91,21 @@ export function admitOwnedProviderInput(store, binding, input) {
90
91
  if (input.taskId !== undefined && input.runId !== undefined) {
91
92
  const run = store.getRun(input.taskId, input.runId);
92
93
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
94
+ const session = sessions?.sessions[input.agentId];
95
+ // Session provenance can describe an older ReviewRound. The exact failed
96
+ // Run, not that first launch, owns this retry's frozen launch configuration.
97
+ // Continuity still fences the native Session's physical workspace/authority;
98
+ // exact equality with the failed Run retains model, permission and candidate.
99
+ const previous = run?.purpose === "review" && retry.previousRunId !== undefined
100
+ ? store.getRun(input.taskId, retry.previousRunId) : null;
101
+ const matchesLaunch = run !== null && session !== undefined && (run.purpose === "review"
102
+ ? previous?.status === "failed" && previous.purpose === "review"
103
+ && previous.roleName === run.roleName && previous.reviewRoundId === run.reviewRoundId
104
+ && isDeepStrictEqual(run.effective, previous.effective)
105
+ && roleSessionMayContinue(session.effective, run.effective)
106
+ : isDeepStrictEqual(run.effective, session.effective));
93
107
  if (run === null || run.status !== "active"
94
- || !isDeepStrictEqual(run.effective, sessions?.sessions[input.agentId]?.effective)) {
108
+ || !matchesLaunch) {
95
109
  throw new Error("Retry execution no longer matches its fixed native Session.");
96
110
  }
97
111
  if (run.workspace !== undefined && !isDeepStrictEqual(store.getManagedWorkspace(run.workspace.owner), run.workspace)) {
@@ -171,9 +171,25 @@ export function createProviderRetryHooks(home, store, ports = {}) {
171
171
  }
172
172
  try {
173
173
  if (owner.scope === "task" && retry.previousRunId !== undefined) {
174
- if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
175
- continue;
176
174
  const previous = store.getRun(owner.taskId, retry.previousRunId);
175
+ if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined) {
176
+ const round = retry.successorReviewRoundId === undefined
177
+ ? null : store.getReviewRound(owner.taskId, retry.successorReviewRoundId);
178
+ const successorId = retry.successorRunId ?? (previous?.executionLaneId === undefined
179
+ ? round?.reviewerRunId
180
+ : round?.executionGroup?.lanes.find(lane => lane.id === previous.executionLaneId)?.currentRunId);
181
+ const successor = successorId === undefined ? null : store.getRun(owner.taskId, successorId);
182
+ // A scheduled Run can fail before Host admission, leaving the
183
+ // Provider binding on the original failed Turn. Stop waiting;
184
+ // this is not proof of native quiescence or permission to replay.
185
+ if (successor?.status === "failed" && successor.id !== previous?.id
186
+ && successor.roleName === owner.roleName
187
+ && (retry.successorReviewRoundId === undefined
188
+ || successor.reviewRoundId === retry.successorReviewRoundId)) {
189
+ stop(set, "successor-failed-before-admission", at);
190
+ }
191
+ continue;
192
+ }
177
193
  if (previous?.status === "active")
178
194
  continue; // ordinary rejected-delivery terminal fold
179
195
  if (previous?.status !== "failed") {