@zq-silk/yui 2.21.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 (125) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +85 -25
  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/invocationRouter.js +5 -5
  7. package/dist/cli/roleWizard.js +5 -1
  8. package/dist/commands/globalRoleCommands.js +33 -8
  9. package/dist/commands/inboxCommands.js +39 -12
  10. package/dist/commands/operatorCommands.js +2 -5
  11. package/dist/commands/releaseCommands.js +16 -2
  12. package/dist/commands/roleConfiguration.js +25 -2
  13. package/dist/commands/sessionCommands.js +5 -2
  14. package/dist/commands/taskCommands.js +217 -64
  15. package/dist/commands/taskContextCommand.js +26 -4
  16. package/dist/commands/taskFactCommands.js +15 -2
  17. package/dist/commands/taskIntegrationCommands.js +3 -3
  18. package/dist/commands/taskRoleRuntimeStatus.js +15 -5
  19. package/dist/commands/textInput.js +2 -2
  20. package/dist/context/contextFamilies.js +10 -0
  21. package/dist/context/currentSkills.js +6 -4
  22. package/dist/context/runContextPack.js +44 -5
  23. package/dist/context/runInputContract.js +1 -1
  24. package/dist/context/sessionBootstrapManifest.js +7 -5
  25. package/dist/context/sessionSkillNotice.js +53 -0
  26. package/dist/context/taskContext.js +100 -16
  27. package/dist/controlPlaneCli.js +17 -7
  28. package/dist/controller/agentRuntimeObserver.js +57 -12
  29. package/dist/controller/clientRuntime.js +37 -8
  30. package/dist/controller/controller.js +3 -1
  31. package/dist/controller/diagnosticJournal.js +10 -2
  32. package/dist/controller/fileSchedulerStoreAdapter.js +72 -24
  33. package/dist/controller/messageHealth.js +4 -3
  34. package/dist/controller/providerRetryAdmission.js +15 -1
  35. package/dist/controller/providerRetryDelivery.js +18 -2
  36. package/dist/controller/runtime.js +15 -5
  37. package/dist/controller/runtimeEventInbox.js +107 -13
  38. package/dist/controller/runtimeEventProcessor.js +3 -3
  39. package/dist/controller/taskRunRecovery.js +153 -0
  40. package/dist/core/controllerClient.js +16 -2
  41. package/dist/core/controllerServer.js +14 -5
  42. package/dist/core/controllerTelemetry.js +51 -5
  43. package/dist/errors/cliFailure.js +14 -5
  44. package/dist/executor/agentAdapter.js +32 -4
  45. package/dist/executor/agentConfigurationFields.js +2 -0
  46. package/dist/integration/gitIntegrationService.js +3 -2
  47. package/dist/integration/integrationSourceApplication.js +12 -1
  48. package/dist/interaction/notificationPresentation.js +11 -16
  49. package/dist/interaction/operatorPresentation.js +1 -1
  50. package/dist/message/messageContinuation.js +3 -1
  51. package/dist/output/boundedRead.js +36 -7
  52. package/dist/output/rolePresentation.js +3 -1
  53. package/dist/runtime/agentHost.js +12 -3
  54. package/dist/runtime/codexAppServerRuntime.js +110 -24
  55. package/dist/runtime/codexInteractiveHost.js +46 -0
  56. package/dist/runtime/providerRetry.js +5 -2
  57. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  58. package/dist/runtime/structuredProviderHost.js +32 -34
  59. package/dist/scheduler/wakeupQueue.js +10 -4
  60. package/dist/storage/sqliteSchema.js +9 -0
  61. package/dist/storage/storageVersions.js +1 -1
  62. package/dist/tmux/tmuxManager.js +4 -2
  63. package/dist/web/webSettings.js +2 -0
  64. package/docs/authorization.md +18 -3
  65. package/docs/cli-information-contract.md +228 -6
  66. package/docs/cli-information-contract.zh-CN.md +95 -5
  67. package/docs/managed-turn-and-session-runtime.md +82 -21
  68. package/docs/managed-turn-and-session-runtime.zh-CN.md +56 -13
  69. package/docs/provider-retry.md +11 -0
  70. package/docs/release-workflow.md +37 -0
  71. package/docs/release-workflow.zh-CN.md +28 -0
  72. package/docs/roles-and-configuration.md +28 -0
  73. package/docs/roles-and-configuration.zh-CN.md +21 -0
  74. package/docs/storage-baseline.md +4 -1
  75. package/docs/storage-baseline.zh-CN.md +1 -1
  76. package/docs/task-delivery.md +7 -0
  77. package/docs/task-delivery.zh-CN.md +5 -0
  78. package/docs/testing/review-evaluation.md +7 -7
  79. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  80. package/docs/testing/verification-levels.md +45 -2
  81. package/docs/testing/verification-levels.zh-CN.md +21 -2
  82. package/i18n/README.zh-CN.md +6 -0
  83. package/package.json +1 -1
  84. package/skills/yui-leader/SKILL.md +98 -193
  85. package/skills/yui-leader/VERSION +1 -1
  86. package/skills/yui-leader/references/artifacts.md +41 -0
  87. package/skills/yui-leader/references/assignment-execution.md +140 -0
  88. package/skills/yui-leader/references/completion.md +79 -0
  89. package/skills/yui-leader/references/delivery-scope.md +26 -0
  90. package/skills/yui-leader/references/execution.md +16 -487
  91. package/skills/yui-leader/references/implementation.md +81 -0
  92. package/skills/yui-leader/references/planning.md +7 -45
  93. package/skills/yui-leader/references/review.md +90 -0
  94. package/skills/yui-leader/references/task-progress.md +23 -0
  95. package/skills/yui-leader/references/work-allocation.md +118 -0
  96. package/skills/yui-operator/SKILL.md +77 -333
  97. package/skills/yui-operator/VERSION +1 -1
  98. package/skills/yui-operator/references/body-transport.md +62 -0
  99. package/skills/yui-operator/references/configuration.md +47 -0
  100. package/skills/yui-operator/references/lifecycle.md +41 -0
  101. package/skills/yui-operator/references/progress.md +39 -0
  102. package/skills/yui-operator/references/routing.md +57 -0
  103. package/skills/yui-operator/references/submission.md +56 -0
  104. package/skills/yui-operator/references/task-delivery.md +1 -1
  105. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  106. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  107. package/skills/yui-runtime/SKILL.md +177 -531
  108. package/skills/yui-runtime/VERSION +1 -1
  109. package/skills/yui-runtime/references/archive.md +51 -0
  110. package/skills/yui-runtime/references/command-discovery.md +41 -0
  111. package/skills/yui-runtime/references/completed-followup.md +65 -0
  112. package/skills/yui-runtime/references/context-ordering.md +3 -34
  113. package/skills/yui-runtime/references/context-reads.md +61 -0
  114. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  115. package/skills/yui-runtime/references/execution-failure.md +105 -0
  116. package/skills/yui-runtime/references/global-context.md +18 -0
  117. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  118. package/skills/yui-runtime/references/host-custody.md +30 -0
  119. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  120. package/skills/yui-runtime/references/incremental-context.md +40 -0
  121. package/skills/yui-runtime/references/publication.md +4 -111
  122. package/skills/yui-runtime/references/recovery.md +32 -192
  123. package/skills/yui-runtime/references/session-identity.md +18 -0
  124. package/skills/yui-runtime/references/session-replacement.md +45 -0
  125. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
@@ -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);
@@ -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") {
@@ -89,6 +89,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
89
89
  const running = await createFileTaskControllerRuntime(home, {
90
90
  ...options,
91
91
  diagnosticMetrics: () => diagnostics.metrics(),
92
+ onRequestDiagnostic: record => diagnostics.request(record),
92
93
  onError: error => { diagnostics.error(error); options.onError?.(error); }
93
94
  });
94
95
  let expectedAt = monotonicMilliseconds() + 250;
@@ -221,7 +222,7 @@ async function createFileTaskControllerRuntime(home, options) {
221
222
  && typeof session.nativeSessionId === "string" && session.nativeSessionId.trim().length > 0) {
222
223
  return session.nativeSessionId;
223
224
  }
224
- for (const event of runtimeEventInbox.list()) {
225
+ for (const event of await runtimeEventInbox.listAsync()) {
225
226
  if (event.type === "runtime-observation" && event.observation.kind === "session.started"
226
227
  && event.observation.fence.taskId === owner.taskId
227
228
  && event.observation.fence.roleName === owner.roleName
@@ -416,7 +417,7 @@ async function createFileTaskControllerRuntime(home, options) {
416
417
  store,
417
418
  environment: options.environment
418
419
  });
419
- const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, launchCoordinator, planner);
420
+ const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, launchCoordinator, planner, workerMaintenance ? asyncStoreClient : undefined);
420
421
  // Process-exit observations are persisted by the Agent Host before socket
421
422
  // delivery. Drain them while this Controller is still the only storage
422
423
  // writer and before it begins accepting new work after a handover.
@@ -607,6 +608,7 @@ async function createFileTaskControllerRuntime(home, options) {
607
608
  now: options.now,
608
609
  onError: options.onError,
609
610
  diagnosticMetrics: options.diagnosticMetrics,
611
+ onRequestDiagnostic: options.onRequestDiagnostic,
610
612
  lifecycleHost,
611
613
  providerRetry: createProviderRetryHooks(home, store, {
612
614
  now: options.now,
@@ -753,7 +755,7 @@ async function createFileTaskControllerRuntime(home, options) {
753
755
  throw error;
754
756
  }
755
757
  }
756
- export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, sharedLaunchCoordinator, environmentRefresher) {
758
+ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, sharedLaunchCoordinator, environmentRefresher, observationWorker) {
757
759
  const launchCoordinator = sharedLaunchCoordinator
758
760
  ?? new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
759
761
  assertCurrent: (request) => {
@@ -774,12 +776,20 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
774
776
  if (event.type !== "runtime-observation" || event.host === undefined) {
775
777
  throw applicationError("INVALID_PARAMS", "Expected an Agent Host observation.");
776
778
  }
777
- return { outcome: schedulerStore.observeAgentHostObservation(event, new Date()) };
779
+ // Eager Host delivery and the normal Inbox drainer fold the same durable
780
+ // fact. Share their existing worker writer: a second synchronous writer
781
+ // here can wait on BEGIN IMMEDIATE and starve even status socket I/O.
782
+ // Await the original atomic fold; no eager ACK or cursor advancement.
783
+ return { outcome: observationWorker === undefined
784
+ ? schedulerStore.observeAgentHostObservation(event, new Date())
785
+ : await observationWorker.invokeObserver("observeAgentHostObservation", [event, new Date()]) };
778
786
  }
779
787
  if (method === "runtime.observation-apply") {
780
788
  try {
781
789
  return {
782
- outcome: schedulerStore.observeRuntimeObservation(createRuntimeObservation(params), new Date())
790
+ outcome: observationWorker === undefined
791
+ ? schedulerStore.observeRuntimeObservation(createRuntimeObservation(params), new Date())
792
+ : await observationWorker.invokeObserver("observeRuntimeObservation", [createRuntimeObservation(params), new Date()])
783
793
  };
784
794
  }
785
795
  catch (error) {