@zq-silk/yui 0.6.8 → 0.6.10

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 (58) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +63 -28
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +101 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +217 -6
  34. package/dist/runtime/runtimeProjection.js +172 -11
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +2 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/task/nextAction.js +1 -1
  49. package/dist/web/assets/client/app.js +1 -1
  50. package/dist/web/assets/client/components.js +233 -4
  51. package/dist/web/assets/client/i18n.js +166 -2
  52. package/dist/web/assets/client/view.js +30 -13
  53. package/dist/web/assets/styles/cards.js +62 -0
  54. package/dist/web/assets/styles/widgets.js +1 -0
  55. package/dist/web/webSnapshot.js +11 -2
  56. package/package.json +1 -1
  57. package/skills/yui-leader/SKILL.md +33 -24
  58. package/skills/yui-operator/SKILL.md +7 -5
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { activeLiveRoleAgentSession, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, recordObservedTaskRoleCompletion, recordRoleAgentSession, rememberRoleAgentCompletedTurn, settleTaskRoleCompletion, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
4
- import { createPendingTurnCompletion, hasRecentTurnId } from "../executor/turnCompletion.js";
3
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
+ import { createProviderRuntimeBinding, endProviderActivation, startProviderActivation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
5
+ import { hasRecentTurnId } from "../executor/turnCompletion.js";
5
6
  import { createTaskEvent } from "../event/taskEvent.js";
6
7
  import { answerInputRequest } from "../input/inputRequest.js";
7
8
  import { activeRoleAgentBinding, updateRoleStatus } from "../role/role.js";
@@ -18,15 +19,18 @@ import { createLeaderRecoveryNotification, createLeaderStallNotification } from
18
19
  import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
19
20
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
20
21
  import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
22
+ import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
23
+ import { providerContinuationKey } from "../runtime/providerContinuation.js";
21
24
  import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
22
25
  import { recordExecutionLaneResult } from "../execution/executionGroup.js";
23
26
  import { formatAgentRunReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
24
- import { bindExecution, claimPending, completeProcessing, releaseProcessing } from "../coordination/workMailbox.js";
27
+ import { bindExecution, claimPending, claimInputDelivery as claimMailboxInputDelivery, completeInputDelivery as completeMailboxInputDelivery, completeProcessing, mailboxHasPending, mailboxHasWork, pendingLane, markInputDeliveryPushed as markMailboxInputDeliveryPushed, markInputDeliveryUnknown as markMailboxInputDeliveryUnknown, releaseInputDelivery as releaseMailboxInputDelivery, resolveInputDeliveryNotAccepted as resolveMailboxInputDeliveryNotAccepted, releaseProcessing } from "../coordination/workMailbox.js";
25
28
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
26
29
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
27
30
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
28
31
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
29
- import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
32
+ import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
33
+ import { projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
30
34
  /** Maps the authoritative FileTaskStore records to the scheduler's narrow port. */
31
35
  export class FileSchedulerStoreAdapter {
32
36
  store;
@@ -56,6 +60,9 @@ export class FileSchedulerStoreAdapter {
56
60
  if (adapterId === null) {
57
61
  return this.recordObsoleteCanonicalObservation(input, "driver-or-run-mismatch", now);
58
62
  }
63
+ if (this.store.listEvents(taskId).some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
64
+ && event.payload.semanticKey === input.semanticKey)))
65
+ return "applied";
59
66
  let outcome;
60
67
  switch (input.kind) {
61
68
  case "session.started":
@@ -63,6 +70,7 @@ export class FileSchedulerStoreAdapter {
63
70
  outcome = this.observeRuntimeSession(input, adapterId, now);
64
71
  break;
65
72
  case "turn.accepted":
73
+ case "input.accepted":
66
74
  outcome = this.observeRuntimePromptAccepted(input, adapterId, now);
67
75
  break;
68
76
  case "turn.completed": {
@@ -80,12 +88,46 @@ export class FileSchedulerStoreAdapter {
80
88
  const classification = this.classifyRuntimeTurnCompleted(completed);
81
89
  if (classification !== "apply")
82
90
  return classification;
91
+ if (this.runtimeTurnHasActiveNativeSubagents(input)) {
92
+ // This is an intermediate provider Turn boundary, not the end of the
93
+ // durable Yui Run. The provider will deliver child completion
94
+ // notifications as later native Turns in the same Run generation.
95
+ outcome = this.validateCanonicalRunObservation(input, now);
96
+ break;
97
+ }
83
98
  const result = this.observeRuntimeTurnCompleted(completed, now);
84
99
  outcome = result.disposition === "obsolete" ? "obsolete" : "applied";
85
100
  break;
86
101
  }
87
102
  case "turn.failed": {
88
103
  const failureEvidence = input.payload.failure;
104
+ if (failureEvidence.runTerminal !== true) {
105
+ const classification = this.classifyRuntimeTurnCompleted({
106
+ taskId,
107
+ roleName: input.fence.roleName,
108
+ agentId: input.fence.agentId,
109
+ adapterId,
110
+ launchId: input.fence.launchId,
111
+ nativeSessionId: input.fence.nativeSessionId,
112
+ turnId: input.fence.nativeTurnId,
113
+ runId: input.fence.runId
114
+ });
115
+ if (classification !== "apply")
116
+ return classification;
117
+ const boundary = this.observeRuntimeTurnCompleted({
118
+ taskId,
119
+ roleName: input.fence.roleName,
120
+ agentId: input.fence.agentId,
121
+ adapterId,
122
+ launchId: input.fence.launchId,
123
+ nativeSessionId: input.fence.nativeSessionId,
124
+ turnId: input.fence.nativeTurnId,
125
+ runId: input.fence.runId,
126
+ summary: input.payload.summary ?? `Provider Turn failed: ${failureEvidence.code}.`
127
+ }, now);
128
+ outcome = boundary.disposition === "obsolete" ? "obsolete" : "applied";
129
+ break;
130
+ }
89
131
  const failure = {
90
132
  eventId: input.eventId,
91
133
  eventType: input.kind,
@@ -118,8 +160,19 @@ export class FileSchedulerStoreAdapter {
118
160
  case "turn.cancelled":
119
161
  case "activity.observed":
120
162
  case "observer.health":
163
+ case "native-work.snapshot":
164
+ case "continuation.started":
165
+ case "continuation.reported":
166
+ case "continuation.settled":
167
+ case "input.delivery-unknown":
121
168
  outcome = this.validateCanonicalRunObservation(input, now);
122
169
  break;
170
+ case "conversation.observed":
171
+ case "activation.started":
172
+ case "activation.ended":
173
+ case "activation.failed":
174
+ outcome = this.observeProviderRuntimeIdentity(input, now);
175
+ break;
123
176
  case "session.ended":
124
177
  case "session.failed":
125
178
  outcome = this.validateCanonicalSessionObservation(input, now);
@@ -156,22 +209,30 @@ export class FileSchedulerStoreAdapter {
156
209
  validateCanonicalRunObservation(input, now) {
157
210
  return this.store.transaction((store) => {
158
211
  if (store.listEvents(input.fence.taskId).some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
159
- && event.payload.eventId === input.eventId)))
212
+ && event.payload.semanticKey === input.semanticKey)))
160
213
  return "applied";
161
214
  const run = store.getAgentRun(input.fence.taskId, input.fence.runId);
162
215
  const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
163
216
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
164
217
  const session = sessions?.sessions[input.fence.agentId];
165
- const requiresActive = input.kind !== "turn.cancelled";
218
+ const knownContinuation = input.kind.startsWith("continuation.")
219
+ && projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.runId === input.fence.runId
220
+ && entry.identity.providerNamespace === input.fence.driverId
221
+ && entry.identity.accountScope === input.fence.agentId
222
+ && entry.identity.conversationId === input.fence.conversationId
223
+ && entry.identity.activationId === input.fence.activationId
224
+ && entry.identity.continuationId === input.fence.continuationId
225
+ && entry.identity.generation === input.fence.continuationGeneration));
226
+ const requiresCurrentRuntime = input.kind !== "turn.cancelled" && !knownContinuation;
166
227
  const valid = run !== null
167
- && (!requiresActive || (run.status === "active" && active?.id === run.id))
228
+ && (!requiresCurrentRuntime || (run.status === "active" && active?.id === run.id))
168
229
  && run.roleName === input.fence.roleName
169
230
  && run.effective.agentId === input.fence.agentId
170
231
  && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId
171
- && session?.launchId === input.fence.launchId
172
- && session.nativeSessionId === input.fence.nativeSessionId
173
- && input.fence.sessionGenerationId === input.fence.launchId
174
- && input.fence.receiptId === formatAgentRunReceiptId(input.fence.taskId, run.id)
232
+ && (knownContinuation || (session?.launchId === input.fence.launchId
233
+ && session.nativeSessionId === input.fence.nativeSessionId
234
+ && input.fence.sessionGenerationId === input.fence.launchId))
235
+ && runtimeReceiptBelongsToRun(store, input)
175
236
  && (run.deliveredAt !== undefined || input.kind === "turn.cancelled");
176
237
  if (!valid) {
177
238
  recordCanonicalObservationObsolete(store, input, "runtime-fence-not-current", now);
@@ -180,6 +241,14 @@ export class FileSchedulerStoreAdapter {
180
241
  return "applied";
181
242
  });
182
243
  }
244
+ runtimeTurnHasActiveNativeSubagents(input) {
245
+ const taskId = input.fence.taskId;
246
+ const run = this.store.getAgentRun(taskId, input.fence.runId);
247
+ if (run === null)
248
+ return false;
249
+ const projection = projectRuntimeTaskEvents(input.fence, run.createdAt, this.store.listEvents(taskId));
250
+ return Object.values(projection.operations).some(({ kind }) => kind === "subagent");
251
+ }
183
252
  validateCanonicalSessionObservation(input, now) {
184
253
  return this.store.transaction((store) => {
185
254
  const run = store.getAgentRun(input.fence.taskId, input.fence.runId);
@@ -189,7 +258,6 @@ export class FileSchedulerStoreAdapter {
189
258
  || run.roleName !== input.fence.roleName
190
259
  || run.effective.agentId !== input.fence.agentId
191
260
  || this.drivers.requireByAdapterId(run.effective.adapterId).id !== input.fence.driverId
192
- || input.fence.receiptId !== formatAgentRunReceiptId(input.fence.taskId, run.id)
193
261
  || session?.launchId !== input.fence.launchId
194
262
  || session.nativeSessionId !== input.fence.nativeSessionId
195
263
  || input.fence.sessionGenerationId !== input.fence.launchId) {
@@ -197,7 +265,101 @@ export class FileSchedulerStoreAdapter {
197
265
  return "obsolete";
198
266
  }
199
267
  const status = input.kind === "session.failed" ? "broken" : "stopped";
200
- store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, input.fence.agentId, status, now));
268
+ let updatedSessions = updateRoleAgentSessionStatus(sessions, input.fence.agentId, status, now);
269
+ if (updatedSessions.providerBinding !== null) {
270
+ const activationId = input.fence.activationId ?? input.fence.launchId;
271
+ updatedSessions = updateTaskRoleProviderRuntime(updatedSessions, endProviderActivation(updatedSessions.providerBinding, activationId, {
272
+ status: input.kind === "session.failed" ? "failed" : "ended",
273
+ endedAt: input.observedAt ?? input.receivedAt,
274
+ reason: input.kind
275
+ }), now);
276
+ }
277
+ store.saveTaskRoleSessionSet(updatedSessions);
278
+ for (const continuation of projectProviderContinuations(store.listEvents(input.fence.taskId))) {
279
+ if (continuation.runId !== input.fence.runId
280
+ || continuation.identity.conversationId
281
+ !== (input.fence.conversationId ?? input.fence.nativeSessionId)
282
+ || continuation.identity.activationId
283
+ !== (input.fence.activationId ?? input.fence.launchId)
284
+ || continuation.execution === "quiescent"
285
+ || continuation.attachment === "detached")
286
+ continue;
287
+ const key = providerContinuationKey(continuation.identity);
288
+ const identityDigest = createHash("sha256").update(key).digest("hex");
289
+ const detached = createRuntimeObservation({
290
+ schemaVersion: 2,
291
+ eventId: `derived-continuation-detached:${identityDigest}`,
292
+ semanticKey: `continuation-detached:${identityDigest}`,
293
+ kind: "continuation.started",
294
+ authority: "controller",
295
+ receivedAt: input.receivedAt,
296
+ observedAt: input.observedAt ?? input.receivedAt,
297
+ fence: {
298
+ ...input.fence,
299
+ conversationId: continuation.identity.conversationId,
300
+ activationId: continuation.identity.activationId,
301
+ continuationId: continuation.identity.continuationId,
302
+ continuationGeneration: continuation.identity.generation,
303
+ ...(continuation.parentContinuationId === undefined
304
+ ? {}
305
+ : { parentContinuationId: continuation.parentContinuationId })
306
+ },
307
+ payload: {
308
+ execution: continuation.execution,
309
+ outcome: continuation.outcome,
310
+ attachment: "detached",
311
+ observationQuality: continuation.observation,
312
+ mayWriteWorkspace: continuation.mayWriteWorkspace,
313
+ ...(continuation.resultRef === undefined
314
+ ? {}
315
+ : { resultRef: continuation.resultRef })
316
+ }
317
+ });
318
+ const eventId = store.nextEventId(input.fence.taskId);
319
+ store.saveEvent(input.fence.taskId, createTaskEvent(eventId, input.fence.taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(detached), now));
320
+ enqueueWork(store, {
321
+ kind: "role",
322
+ taskId: input.fence.taskId,
323
+ roleName: "leader"
324
+ }, "provider-continuation-detached", now, [
325
+ { type: "event", taskId: input.fence.taskId, id: eventId }
326
+ ], {
327
+ source: input.fence.driverId,
328
+ dedupeKey: detached.semanticKey,
329
+ deliveryMode: "followup",
330
+ lane: "normal"
331
+ });
332
+ }
333
+ const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
334
+ const role = store.getRole(input.fence.taskId, input.fence.roleName);
335
+ if ((input.kind === "session.ended" || input.kind === "session.failed")
336
+ && active !== null
337
+ && active.id === input.fence.runId
338
+ && active.status === "active"
339
+ && active.deliveredAt !== undefined
340
+ && role !== null) {
341
+ store.saveRole(input.fence.taskId, updateRoleStatus(role, "detached", now));
342
+ if (role.name === "leader") {
343
+ const message = [
344
+ `Leader native Session became unavailable while Run ${active.id} remains active.`,
345
+ "Yui preserved the Run because Session/host termination is not a workflow outcome.",
346
+ "Inspect native-child and mailbox facts, then explicitly continue, reset, or request user input."
347
+ ].join(" ");
348
+ store.saveOperatorNotification(createLeaderRecoveryNotification(input.fence.taskId, message, now, store.getOperatorNotification(input.fence.taskId)));
349
+ enqueueWork(store, { kind: "operator" }, "leader-runtime-detached", now, [
350
+ { type: "run", taskId: input.fence.taskId, id: active.id }
351
+ ]);
352
+ }
353
+ else {
354
+ enqueueWork(store, {
355
+ kind: "role",
356
+ taskId: input.fence.taskId,
357
+ roleName: "leader"
358
+ }, "role-runtime-detached", now, [
359
+ { type: "run", taskId: input.fence.taskId, id: active.id }
360
+ ]);
361
+ }
362
+ }
201
363
  return "applied";
202
364
  });
203
365
  }
@@ -206,7 +368,7 @@ export class FileSchedulerStoreAdapter {
206
368
  const taskId = input.fence.taskId;
207
369
  const events = store.listEvents(taskId);
208
370
  if (events.some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
209
- && event.payload.eventId === input.eventId)))
371
+ && event.payload.semanticKey === input.semanticKey)))
210
372
  return;
211
373
  if (usageSnapshotIsSuperseded(events, input))
212
374
  return;
@@ -219,7 +381,90 @@ export class FileSchedulerStoreAdapter {
219
381
  ];
220
382
  if (removable.length > 0)
221
383
  store.removeEvents(taskId, removable);
222
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(input), now));
384
+ const observationEventId = store.nextEventId(taskId);
385
+ store.saveEvent(taskId, createTaskEvent(observationEventId, taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(input), now));
386
+ if (input.kind === "native-work.snapshot"
387
+ && input.payload.snapshotComplete === true
388
+ && input.payload.observationQuality === "exact") {
389
+ for (const continuation of projectProviderContinuations(events)) {
390
+ if (continuation.runId !== input.fence.runId
391
+ || continuation.identity.conversationId !== input.fence.conversationId
392
+ || continuation.identity.activationId !== input.fence.activationId
393
+ || continuation.execution === "quiescent")
394
+ continue;
395
+ const identityKey = providerContinuationKey(continuation.identity);
396
+ const digest = createHash("sha256")
397
+ .update(`${input.semanticKey}\u0000${identityKey}`)
398
+ .digest("hex");
399
+ const settled = createRuntimeObservation({
400
+ schemaVersion: 2,
401
+ eventId: `native-snapshot-settled:${digest}`,
402
+ semanticKey: `native-snapshot-settled:${digest}`,
403
+ kind: "continuation.settled",
404
+ authority: "controller",
405
+ receivedAt: input.receivedAt,
406
+ observedAt: input.observedAt ?? input.receivedAt,
407
+ fence: {
408
+ ...input.fence,
409
+ continuationId: continuation.identity.continuationId,
410
+ continuationGeneration: continuation.identity.generation,
411
+ ...(continuation.parentContinuationId === undefined
412
+ ? {}
413
+ : { parentContinuationId: continuation.parentContinuationId })
414
+ },
415
+ payload: {
416
+ execution: "quiescent",
417
+ outcome: "unknown",
418
+ attachment: continuation.attachment,
419
+ observationQuality: "exact",
420
+ mayWriteWorkspace: false,
421
+ ...(continuation.resultRef === undefined
422
+ ? {}
423
+ : { resultRef: continuation.resultRef })
424
+ }
425
+ });
426
+ const settledEventId = store.nextEventId(taskId);
427
+ store.saveEvent(taskId, createTaskEvent(settledEventId, taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(settled), now));
428
+ enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, "provider-continuation-settled", now, [{ type: "event", taskId, id: settledEventId }], {
429
+ source: input.fence.driverId,
430
+ dedupeKey: settled.semanticKey,
431
+ deliveryMode: "followup",
432
+ lane: "normal"
433
+ });
434
+ }
435
+ }
436
+ if (input.kind === "continuation.reported"
437
+ || input.kind === "continuation.settled") {
438
+ enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, input.kind === "continuation.reported"
439
+ ? "provider-continuation-report"
440
+ : "provider-continuation-settled", now, [{ type: "event", taskId, id: observationEventId }], {
441
+ source: input.fence.driverId,
442
+ dedupeKey: input.semanticKey,
443
+ deliveryMode: "followup",
444
+ lane: "normal"
445
+ });
446
+ }
447
+ if (input.kind === "turn.failed" && input.payload.failure?.runTerminal !== true) {
448
+ enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, "provider-turn-failed", now, [{ type: "event", taskId, id: observationEventId }], {
449
+ source: input.fence.driverId,
450
+ dedupeKey: input.semanticKey,
451
+ deliveryMode: "followup",
452
+ lane: "normal"
453
+ });
454
+ }
455
+ if ((input.kind === "operation.completed" || input.kind === "operation.failed")
456
+ && input.payload.operation === "subagent") {
457
+ const role = store.getRole(taskId, input.fence.roleName);
458
+ const session = store.getRoleSession(taskId, input.fence.roleName);
459
+ // A live provider Session owns delivery of its native child result.
460
+ // Route a Yui wake only when that parent is gone and another observer
461
+ // supplied the child's terminal fact.
462
+ if (role !== null && (session?.status === "stopped" || session?.status === "broken")) {
463
+ enqueueWork(store, role.name === "leader"
464
+ ? { kind: "operator" }
465
+ : { kind: "role", taskId, roleName: "leader" }, "detached-native-subagent-terminal", now, [{ type: "run", taskId, id: input.fence.runId }]);
466
+ }
467
+ }
223
468
  if (confirmedActivity !== null) {
224
469
  store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(confirmedActivity), now));
225
470
  }
@@ -497,6 +742,42 @@ export class FileSchedulerStoreAdapter {
497
742
  const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
498
743
  return sessions !== null && sessions.inFlight !== null;
499
744
  }
745
+ getActiveProviderTurnFence(input) {
746
+ const observations = this.store.listEvents(input.taskId)
747
+ .map(runtimeObservationFromTaskEvent)
748
+ .filter((entry) => entry !== null
749
+ && entry.fence.runId === input.runId
750
+ && entry.fence.roleName === input.roleName
751
+ && entry.fence.agentId === input.agentId
752
+ && entry.fence.launchId === input.launchId
753
+ && entry.fence.nativeSessionId === input.nativeSessionId
754
+ && entry.fence.nativeTurnId !== undefined)
755
+ .sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
756
+ || (left.sequence ?? -1) - (right.sequence ?? -1)
757
+ || (left.ordinal ?? -1) - (right.ordinal ?? -1)
758
+ || left.semanticKey.localeCompare(right.semanticKey)));
759
+ const state = new Map();
760
+ const fences = new Map();
761
+ for (const observation of observations) {
762
+ const turnId = observation.fence.nativeTurnId;
763
+ if (observation.kind === "turn.accepted" || observation.kind === "input.accepted") {
764
+ state.set(turnId, "active");
765
+ fences.set(turnId, observation);
766
+ }
767
+ else if (["turn.completed", "turn.failed", "turn.cancelled"].includes(observation.kind)) {
768
+ state.set(turnId, "terminal");
769
+ }
770
+ }
771
+ const active = [...state.entries()].filter(([, status]) => status === "active");
772
+ if (active.length !== 1)
773
+ return null;
774
+ const observation = fences.get(active[0][0]);
775
+ return {
776
+ conversationId: observation.fence.conversationId ?? input.nativeSessionId,
777
+ activationId: observation.fence.activationId ?? input.launchId,
778
+ nativeTurnId: active[0][0]
779
+ };
780
+ }
500
781
  isRoleGenerationProviderReady(input) {
501
782
  // A session.ready observation for this exact generation is the durable
502
783
  // pre-input fence. This reads folded Driver truth
@@ -568,7 +849,7 @@ export class FileSchedulerStoreAdapter {
568
849
  claimWorkMailbox(input) {
569
850
  return this.store.transaction((store) => {
570
851
  const mailbox = store.getWorkMailbox(input.target);
571
- if (mailbox === null || (mailbox.processing === null && mailbox.pending === null)) {
852
+ if (mailbox === null || !mailboxHasWork(mailbox)) {
572
853
  return { status: "empty" };
573
854
  }
574
855
  if (mailbox.processing !== null) {
@@ -586,6 +867,104 @@ export class FileSchedulerStoreAdapter {
586
867
  return { status: "claimed", processing: claimed.processing };
587
868
  });
588
869
  }
870
+ claimInputDelivery(input) {
871
+ return this.store.transaction((store) => {
872
+ const mailbox = store.getWorkMailbox(input.target);
873
+ if (mailbox === null)
874
+ return { status: "empty" };
875
+ if (mailbox.inputDelivery !== null) {
876
+ return { status: "delivery", delivery: mailbox.inputDelivery };
877
+ }
878
+ const pending = pendingLane(mailbox, input.lane);
879
+ if (pending === null)
880
+ return { status: "empty" };
881
+ const claimed = claimMailboxInputDelivery(mailbox, {
882
+ attemptId: input.attemptId,
883
+ lane: input.lane,
884
+ mode: input.mode,
885
+ owner: input.owner,
886
+ startedAt: input.now.toISOString(),
887
+ executionRef: input.executionRef,
888
+ providerFence: input.providerFence
889
+ });
890
+ store.saveWorkMailbox(claimed);
891
+ return { status: "claimed", delivery: claimed.inputDelivery };
892
+ });
893
+ }
894
+ markInputDeliveryPushed(target, attemptId, now) {
895
+ return this.store.transaction((store) => {
896
+ const mailbox = store.getWorkMailbox(target);
897
+ if (mailbox?.inputDelivery?.attemptId !== attemptId)
898
+ return false;
899
+ store.saveWorkMailbox(markMailboxInputDeliveryPushed(mailbox, attemptId, now));
900
+ return true;
901
+ });
902
+ }
903
+ completeInputDelivery(target, attemptId, now) {
904
+ return this.store.transaction((store) => {
905
+ const mailbox = store.getWorkMailbox(target);
906
+ if (mailbox?.inputDelivery?.attemptId !== attemptId)
907
+ return false;
908
+ const delivery = mailbox.inputDelivery;
909
+ if (target.kind === "role"
910
+ && delivery.executionRef.type === "run"
911
+ && delivery.executionRef.taskId === target.taskId) {
912
+ store.saveEvent(target.taskId, createTaskEvent(store.nextEventId(target.taskId), target.taskId, "run.input-delivered", {
913
+ attemptId: delivery.attemptId,
914
+ runId: delivery.executionRef.id,
915
+ ...(delivery.providerFence === undefined ? {} : {
916
+ conversationId: delivery.providerFence.conversationId,
917
+ activationId: delivery.providerFence.activationId,
918
+ ...(delivery.providerFence.nativeTurnId === undefined ? {} : {
919
+ nativeTurnId: delivery.providerFence.nativeTurnId
920
+ })
921
+ })
922
+ }, now));
923
+ }
924
+ store.saveWorkMailbox(completeMailboxInputDelivery(mailbox, attemptId, now));
925
+ return true;
926
+ });
927
+ }
928
+ releaseInputDelivery(target, attemptId) {
929
+ return this.store.transaction((store) => {
930
+ const mailbox = store.getWorkMailbox(target);
931
+ if (mailbox?.inputDelivery?.attemptId !== attemptId)
932
+ return false;
933
+ store.saveWorkMailbox(releaseMailboxInputDelivery(mailbox, attemptId));
934
+ return true;
935
+ });
936
+ }
937
+ resolveInputDeliveryNotAccepted(target, attemptId) {
938
+ return this.store.transaction((store) => {
939
+ const mailbox = store.getWorkMailbox(target);
940
+ if (mailbox?.inputDelivery?.attemptId !== attemptId)
941
+ return false;
942
+ store.saveWorkMailbox(resolveMailboxInputDeliveryNotAccepted(mailbox, attemptId));
943
+ return true;
944
+ });
945
+ }
946
+ markInputDeliveryUnknown(target, attemptId, reason, now) {
947
+ return this.store.transaction((store) => {
948
+ const mailbox = store.getWorkMailbox(target);
949
+ if (mailbox?.inputDelivery?.attemptId !== attemptId)
950
+ return false;
951
+ const unknown = markMailboxInputDeliveryUnknown(mailbox, attemptId, reason, now);
952
+ store.saveWorkMailbox(unknown);
953
+ const taskId = target.kind === "role" ? target.taskId : undefined;
954
+ if (taskId !== undefined) {
955
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "run.input-delivery-unknown", { attemptId, reason }, now));
956
+ enqueueWork(store, { kind: "operator" }, "run-input-delivery-unknown", now, [
957
+ unknown.inputDelivery.executionRef
958
+ ], {
959
+ source: "input-router",
960
+ dedupeKey: `input-delivery-unknown:${attemptId}`,
961
+ deliveryMode: "followup",
962
+ lane: "normal"
963
+ });
964
+ }
965
+ return true;
966
+ });
967
+ }
589
968
  completeWorkMailbox(target, batchId) {
590
969
  return this.store.transaction((store) => {
591
970
  const mailbox = store.getWorkMailbox(target);
@@ -623,12 +1002,13 @@ export class FileSchedulerStoreAdapter {
623
1002
  }
624
1003
  mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
625
1004
  }
626
- if (mailbox.pending !== null) {
627
- if (mailbox.pending.reasons.length !== 1
628
- || mailbox.pending.reasons[0] !== RUNTIME_CLEANUP_REQUIRED_REASON) {
1005
+ const pending = pendingLane(mailbox, "normal");
1006
+ if (pending !== null) {
1007
+ if (pending.reasons.length !== 1
1008
+ || pending.reasons[0] !== RUNTIME_CLEANUP_REQUIRED_REASON) {
629
1009
  return false;
630
1010
  }
631
- const batchId = `runtime-cleanup-complete:${mailbox.pending.fromSequence}-${mailbox.pending.toSequence}`;
1011
+ const batchId = `runtime-cleanup-complete:${pending.fromSequence}-${pending.toSequence}`;
632
1012
  mailbox = completeProcessing(claimPending(mailbox, {
633
1013
  batchId,
634
1014
  owner: RUNTIME_LIFECYCLE_OWNER,
@@ -729,7 +1109,7 @@ export class FileSchedulerStoreAdapter {
729
1109
  }
730
1110
  const target = { kind: "role", taskId: input.task.id, roleName: input.role.name };
731
1111
  const mailbox = store.getWorkMailbox(target);
732
- if (mailbox === null || mailbox.pending === null)
1112
+ if (mailbox === null || !mailboxHasPending(mailbox))
733
1113
  return "state-changed";
734
1114
  if (mailbox.processing !== null)
735
1115
  return "busy";
@@ -1249,8 +1629,9 @@ export class FileSchedulerStoreAdapter {
1249
1629
  });
1250
1630
  }
1251
1631
  /**
1252
- * Fast hook path: durably records the native Turn boundary and a two-second
1253
- * closure deadline. It never performs tmux, workspace, or Controller I/O.
1632
+ * Fast hook path: validates the native Turn boundary before it is either
1633
+ * retained as an intermediate child wait or recorded as a ready boundary
1634
+ * for later mailbox input. It never performs tmux, workspace, or Controller I/O.
1254
1635
  */
1255
1636
  classifyRuntimeTurnCompleted(input) {
1256
1637
  const task = this.store.getTask(input.taskId);
@@ -1277,11 +1658,6 @@ export class FileSchedulerStoreAdapter {
1277
1658
  catch {
1278
1659
  return "obsolete";
1279
1660
  }
1280
- if (sessions?.pendingTurnCompletion !== null && sessions !== null) {
1281
- return sessions.pendingTurnCompletion.turnId === input.turnId
1282
- ? "apply"
1283
- : "deferred";
1284
- }
1285
1661
  // A normal explicit CLI yield may precede its native Hook; that Hook still
1286
1662
  // owns the turn fact. A forced cleanup boundary or stopped process instead
1287
1663
  // makes the old generation obsolete.
@@ -1333,19 +1709,6 @@ export class FileSchedulerStoreAdapter {
1333
1709
  && hasRecentTurnId(effectiveExisting.recentCompletedTurnIds, input.turnId)) {
1334
1710
  return { session: effectiveExisting, duplicate: true };
1335
1711
  }
1336
- const pending = sessions.pendingTurnCompletion;
1337
- if (effectiveExisting !== undefined
1338
- && pending !== null
1339
- && pending.agentId === input.agentId
1340
- && pending.nativeSessionId === input.nativeSessionId
1341
- && pending.turnId === input.turnId
1342
- && pending.runId === input.runId) {
1343
- return {
1344
- session: effectiveExisting,
1345
- duplicate: true,
1346
- pendingRunId: pending.runId
1347
- };
1348
- }
1349
1712
  if (sessions.inFlight === null
1350
1713
  && existing !== undefined
1351
1714
  && (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))
@@ -1386,205 +1749,23 @@ export class FileSchedulerStoreAdapter {
1386
1749
  completeRuntimeHookReservation(store, owner, input.launchId);
1387
1750
  return { session: sessions.sessions[input.agentId], duplicate: false };
1388
1751
  }
1389
- const fence = {
1390
- agentId: sessions.inFlight.agentId,
1391
- runId: sessions.inFlight.runId,
1392
- receiptId: sessions.inFlight.receiptId
1393
- };
1394
- const completion = createPendingTurnCompletion({
1395
- taskId: task.id,
1396
- roleName: role.name,
1752
+ // A provider Turn is an activation boundary, not a Yui Run outcome.
1753
+ // Preserve the Run fence and make the fixed native Session available for
1754
+ // a later mailbox batch. Provider-owned background subagents may finish
1755
+ // after this point and Yui must not invent a failure while waiting.
1756
+ sessions = recordTaskRoleTurnBoundary(sessions, {
1397
1757
  agentId: input.agentId,
1398
1758
  nativeSessionId: input.nativeSessionId,
1399
- turnId: input.turnId,
1400
- runId: fence.runId,
1401
- summary: input.summary,
1402
- observedAt: now,
1403
- dueAt: new Date(now.getTime() + 2_000)
1404
- });
1405
- // Runtime completion is not workflow progress. The exact Run remains
1406
- // open for the short outcome grace period so a concurrently committed
1407
- // yui yield/block/complete action can win; otherwise the contract fails.
1408
- sessions = recordObservedTaskRoleCompletion(sessions, completion);
1409
- const active = store.getActiveAgentRun(task.id, role.name);
1410
- if (active === null || active.id !== fence.runId || active.status !== "active") {
1411
- sessions = settleTaskRoleCompletion(sessions, {
1412
- agentId: input.agentId,
1413
- runId: fence.runId,
1414
- turnId: input.turnId
1415
- }, now);
1416
- const settledStatus = sessions.sessions[input.agentId]?.status;
1417
- if (settledStatus !== "stopped" && settledStatus !== "broken") {
1418
- sessions = updateRoleAgentSessionStatus(sessions, input.agentId, "ready", now);
1419
- }
1420
- store.saveTaskRoleSessionSet(sessions);
1421
- completeRuntimeHookReservation(store, owner, input.launchId);
1422
- return { session: sessions.sessions[input.agentId], duplicate: false };
1423
- }
1759
+ turnId: input.turnId
1760
+ }, now);
1424
1761
  store.saveTaskRoleSessionSet(sessions);
1425
1762
  completeRuntimeHookReservation(store, owner, input.launchId);
1426
1763
  return {
1427
1764
  session: sessions.sessions[input.agentId],
1428
- duplicate: false,
1429
- pendingRunId: fence.runId
1765
+ duplicate: false
1430
1766
  };
1431
1767
  });
1432
1768
  }
1433
- listPendingRuntimeTurnCompletions(taskIds) {
1434
- return this.store.listPendingRuntimeTurnCompletions(taskIds);
1435
- }
1436
- /** Resolves only completions whose grace deadline has elapsed. */
1437
- resolveDueRuntimeTurnCompletions(now, taskIds) {
1438
- const selectedTaskIds = taskIds === undefined
1439
- ? undefined
1440
- : [...taskIds].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
1441
- const due = this.listPendingRuntimeTurnCompletions(selectedTaskIds).filter((completion) => Date.parse(completion.dueAt) <= now.getTime());
1442
- const finalized = [];
1443
- for (const completion of due) {
1444
- this.store.transaction((store) => {
1445
- const current = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
1446
- if (current === null)
1447
- return;
1448
- const pending = current.pendingTurnCompletion;
1449
- if (pending === null
1450
- || pending.turnId !== completion.turnId
1451
- || pending.runId !== completion.runId)
1452
- return;
1453
- const active = store.getActiveAgentRun(completion.taskId, completion.roleName);
1454
- if (active?.id === completion.runId) {
1455
- const result = this.recordRuntimeTurnCompleted({
1456
- taskId: completion.taskId,
1457
- roleName: completion.roleName,
1458
- agentId: completion.agentId,
1459
- adapterId: current.sessions[completion.agentId].adapterId,
1460
- nativeSessionId: completion.nativeSessionId,
1461
- turnId: completion.turnId,
1462
- expectedRunId: completion.runId,
1463
- summary: completion.summary
1464
- }, now);
1465
- if (result.finalizedRunId !== undefined) {
1466
- finalized.push(formatTaskRecordReference(completion.taskId, result.finalizedRunId, "agentRun"));
1467
- }
1468
- }
1469
- let settled = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
1470
- if (settled?.pendingTurnCompletion?.turnId !== completion.turnId)
1471
- return;
1472
- settled = settleTaskRoleCompletion(settled, {
1473
- agentId: completion.agentId,
1474
- runId: completion.runId,
1475
- turnId: completion.turnId
1476
- }, now);
1477
- const settledStatus = settled.sessions[completion.agentId]?.status;
1478
- if (settledStatus !== "stopped" && settledStatus !== "broken") {
1479
- settled = updateRoleAgentSessionStatus(settled, completion.agentId, "ready", now);
1480
- }
1481
- store.saveTaskRoleSessionSet(settled);
1482
- });
1483
- }
1484
- return finalized;
1485
- }
1486
- /** A structured native Turn-completion event terminalizes its exact Run fence. */
1487
- recordRuntimeTurnCompleted(input, now = new Date()) {
1488
- return this.store.transaction((store) => {
1489
- const task = store.getTask(input.taskId);
1490
- if (task === null)
1491
- throw new Error(`Task not found: ${input.taskId}.`);
1492
- if (task.status !== "active") {
1493
- throw new Error(`Cannot complete a runtime turn for inactive Task: ${input.taskId}.`);
1494
- }
1495
- const role = requireRole(store, input.taskId, input.roleName);
1496
- const current = store.getRoleSessionSet(input.taskId, input.roleName)
1497
- ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
1498
- const existing = current.sessions[input.agentId];
1499
- const owner = {
1500
- scope: "task",
1501
- taskId: input.taskId,
1502
- roleName: input.roleName
1503
- };
1504
- if (existing === undefined
1505
- && !runtimeHookMatchesReservation(store, owner, input.launchId)) {
1506
- throw new Error("Runtime turn completion does not match the launch reservation.");
1507
- }
1508
- const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
1509
- const effective = taskSessionEffective(store, task.id, role.name, input.agentId, effectiveExisting);
1510
- if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
1511
- throw new Error("Runtime turn completion does not match the effective launch identity.");
1512
- }
1513
- const sessions = effectiveExisting?.status === "ready"
1514
- || effectiveExisting?.status === "stopped"
1515
- || effectiveExisting?.status === "broken"
1516
- ? current
1517
- : recordRoleAgentSession(current, {
1518
- agentId: input.agentId,
1519
- adapterId: input.adapterId,
1520
- nativeSessionId: input.nativeSessionId,
1521
- ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1522
- policy: "fixed",
1523
- status: "ready",
1524
- effective
1525
- }, now);
1526
- if (input.expectedRunId !== undefined
1527
- && store.getActiveAgentRun(task.id, role.name)?.id !== input.expectedRunId) {
1528
- return { session: sessions.sessions[input.agentId] };
1529
- }
1530
- if (sessions !== current)
1531
- store.saveRoleSessionSet(sessions);
1532
- completeRuntimeHookReservation(store, owner, input.launchId);
1533
- const session = sessions.sessions[input.agentId];
1534
- const active = store.getActiveAgentRun(task.id, role.name);
1535
- if (task.status !== "active"
1536
- || active === null
1537
- || active.pushedAt === undefined
1538
- || (input.expectedRunId !== undefined && active.id !== input.expectedRunId)) {
1539
- return { session };
1540
- }
1541
- const summary = `Agent turn completed without a Yui workflow outcome. Last assistant message: ${input.summary}`;
1542
- const result = terminalizeExactTaskRun(store, {
1543
- taskId: task.id,
1544
- roleName: role.name,
1545
- agentId: active.effective.agentId,
1546
- runId: active.id,
1547
- receiptId: formatAgentRunReceiptId(task.id, active.id),
1548
- nativeSessionId: input.nativeSessionId,
1549
- ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1550
- outcome: { status: "failed", summary }
1551
- }, now);
1552
- if (result.disposition !== "applied" || result.run === null) {
1553
- throw new Error(`Role Run changed during native Turn completion: ${active.id}/${result.reason}.`);
1554
- }
1555
- const terminal = result.run;
1556
- const leaderWorkflowRun = role.name === "leader" && active.workItemId === undefined;
1557
- if (active.purpose === "execution" && active.workItemId !== undefined) {
1558
- const item = store.getWorkItem(task.id, active.workItemId);
1559
- if (item !== null && !["completed", "failed", "retired"].includes(item.status)) {
1560
- store.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", now, summary));
1561
- }
1562
- }
1563
- store.saveRole(task.id, updateRoleStatus(role, leaderWorkflowRun ? "failed" : "idle", now));
1564
- clearMatchingLeaderStallAttention(store, task.id, terminal.id);
1565
- if (!leaderWorkflowRun) {
1566
- enqueueWork(store, { kind: "role", taskId: task.id, roleName: "leader" }, "role-run-failed", now, [
1567
- { type: "run", taskId: task.id, id: terminal.id },
1568
- ...(terminal.workItemId === undefined
1569
- ? []
1570
- : [{
1571
- type: "work-item",
1572
- taskId: task.id,
1573
- id: terminal.workItemId
1574
- }])
1575
- ]);
1576
- }
1577
- else {
1578
- store.saveLeaderFailure(recordLeaderFailure(task.id, input.nativeSessionId, summary, now, store.getLeaderFailure(task.id)));
1579
- store.saveOperatorNotification(createLeaderRecoveryNotification(task.id, summary, now, store.getOperatorNotification(task.id)));
1580
- enqueueWork(store, { kind: "operator" }, "leader-run-failed", now, [
1581
- { type: "task", id: task.id },
1582
- { type: "run", taskId: task.id, id: terminal.id }
1583
- ]);
1584
- }
1585
- return { session, finalizedRunId: terminal.id };
1586
- });
1587
- }
1588
1769
  classifyRuntimeTurnFailed(input) {
1589
1770
  const task = this.store.getTask(input.taskId);
1590
1771
  if (task?.status !== "active")
@@ -1719,24 +1900,6 @@ export class FileSchedulerStoreAdapter {
1719
1900
  }
1720
1901
  return null;
1721
1902
  }
1722
- // transport-uncertain: the Provider may have accepted the turn. If a
1723
- // native completion is already durable, the completion path owns the
1724
- // yield; do not resend.
1725
- if (classification.errorClass === "transport-uncertain") {
1726
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1727
- const pending = sessions?.pendingTurnCompletion;
1728
- if (pending !== null
1729
- && pending !== undefined
1730
- && pending.runId === input.runId
1731
- && pending.agentId === input.agentId) {
1732
- this.recordProviderRetryClassified(store, input, classification.errorClass, {
1733
- wouldRetry: "false",
1734
- shadow: "false",
1735
- note: "native-turn-completion-durable"
1736
- }, now);
1737
- return { disposition: "applied", runId: input.runId };
1738
- }
1739
- }
1740
1903
  const run = store.getAgentRun(input.taskId, input.runId);
1741
1904
  if (run === null || run.status !== "active")
1742
1905
  return null;
@@ -1902,6 +2065,51 @@ export class FileSchedulerStoreAdapter {
1902
2065
  * Applies already-normalized Session observations. Driver-specific startup
1903
2066
  * names and source variants have terminated before this boundary.
1904
2067
  */
2068
+ observeProviderRuntimeIdentity(input, now) {
2069
+ return this.store.transaction((store) => {
2070
+ const taskId = input.fence.taskId;
2071
+ const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2072
+ const run = store.getAgentRun(taskId, input.fence.runId);
2073
+ if (sessions === null || sessions.providerBinding === null || run === null
2074
+ || run.id !== sessions.inFlight?.runId
2075
+ || input.fence.conversationId === undefined) {
2076
+ recordCanonicalObservationObsolete(store, input, "provider-binding-missing", now);
2077
+ return "obsolete";
2078
+ }
2079
+ let binding = sessions.providerBinding;
2080
+ if (input.fence.conversationId !== binding.conversations.find((entry) => (entry.epoch === binding.currentConversationEpoch))?.conversationId) {
2081
+ recordCanonicalObservationObsolete(store, input, "provider-conversation-mismatch", now);
2082
+ return "obsolete";
2083
+ }
2084
+ try {
2085
+ if (input.kind === "conversation.observed") {
2086
+ binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
2087
+ }
2088
+ else if (input.kind === "activation.started") {
2089
+ const active = binding.activations.find((entry) => entry.status === "active");
2090
+ if (active?.activationId !== input.fence.activationId) {
2091
+ binding = startProviderActivation(binding, {
2092
+ activationId: input.fence.activationId,
2093
+ startedAt: input.observedAt ?? input.receivedAt
2094
+ });
2095
+ }
2096
+ }
2097
+ else {
2098
+ binding = endProviderActivation(binding, input.fence.activationId, {
2099
+ status: input.kind === "activation.failed" ? "failed" : "ended",
2100
+ endedAt: input.observedAt ?? input.receivedAt,
2101
+ reason: input.kind
2102
+ });
2103
+ }
2104
+ }
2105
+ catch {
2106
+ recordCanonicalObservationObsolete(store, input, "provider-identity-conflict", now);
2107
+ return "obsolete";
2108
+ }
2109
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, binding, now));
2110
+ return "applied";
2111
+ });
2112
+ }
1905
2113
  observeRuntimeSession(input, adapterId, now) {
1906
2114
  return this.store.transaction((store) => {
1907
2115
  const event = createCanonicalLifecycleEvent({
@@ -1951,7 +2159,17 @@ export class FileSchedulerStoreAdapter {
1951
2159
  status: "running",
1952
2160
  effective: run.effective
1953
2161
  }, now);
1954
- store.saveTaskRoleSessionSet(bound);
2162
+ const withProvider = bound.providerBinding !== null || bound.inFlight === null
2163
+ ? bound
2164
+ : bindTaskRoleProviderRuntime(bound, createProviderRuntimeBinding({
2165
+ providerNamespace: input.fence.driverId,
2166
+ accountScope: input.fence.agentId,
2167
+ runId: bound.inFlight.runId,
2168
+ conversationId: input.fence.conversationId ?? decision.outcome.nativeSessionId,
2169
+ activationId: input.fence.activationId ?? input.fence.launchId,
2170
+ startedAt: bound.inFlight.preparedAt
2171
+ }), now);
2172
+ store.saveTaskRoleSessionSet(withProvider);
1955
2173
  const driver = this.drivers.require(input.fence.driverId);
1956
2174
  if (driver.capabilities.observation.sessionBootstrap === "discovered"
1957
2175
  && run.pushedAt === undefined) {
@@ -1962,6 +2180,21 @@ export class FileSchedulerStoreAdapter {
1962
2180
  }
1963
2181
  completeRuntimeHookReservation(store, { scope: "task", taskId, roleName: input.fence.roleName }, input.fence.launchId);
1964
2182
  }
2183
+ const current = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2184
+ const currentSession = current?.sessions[input.fence.agentId];
2185
+ if (current !== null && current !== undefined
2186
+ && current.providerBinding === null
2187
+ && current.inFlight !== null
2188
+ && currentSession?.nativeSessionId === input.fence.nativeSessionId) {
2189
+ store.saveTaskRoleSessionSet(bindTaskRoleProviderRuntime(current, createProviderRuntimeBinding({
2190
+ providerNamespace: input.fence.driverId,
2191
+ accountScope: input.fence.agentId,
2192
+ runId: current.inFlight.runId,
2193
+ conversationId: input.fence.conversationId ?? input.fence.nativeSessionId,
2194
+ activationId: input.fence.activationId ?? input.fence.launchId,
2195
+ startedAt: current.inFlight.preparedAt
2196
+ }), now));
2197
+ }
1965
2198
  return "applied";
1966
2199
  }
1967
2200
  }
@@ -1970,6 +2203,52 @@ export class FileSchedulerStoreAdapter {
1970
2203
  /** Provider acceptance is canonical before storage and remains receipt-fenced. */
1971
2204
  observeRuntimePromptAccepted(input, adapterId, now) {
1972
2205
  return this.store.transaction((store) => {
2206
+ const roleTarget = {
2207
+ kind: "role",
2208
+ taskId: input.fence.taskId,
2209
+ roleName: input.fence.roleName
2210
+ };
2211
+ const mailbox = store.getWorkMailbox(roleTarget);
2212
+ const processing = mailbox?.processing;
2213
+ const inputDelivery = mailbox?.inputDelivery;
2214
+ const continuation = inputDelivery !== null
2215
+ && inputDelivery !== undefined
2216
+ && inputDelivery.attemptId === input.fence.receiptId
2217
+ && inputDelivery.executionRef.type === "run"
2218
+ && inputDelivery.executionRef.taskId === input.fence.taskId
2219
+ && inputDelivery.executionRef.id === input.fence.runId
2220
+ && inputDelivery.attemptId !== formatAgentRunReceiptId(input.fence.taskId, input.fence.runId);
2221
+ if (continuation) {
2222
+ const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
2223
+ const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2224
+ const session = sessions?.sessions[input.fence.agentId];
2225
+ if (active === null
2226
+ || active.id !== input.fence.runId
2227
+ || active.status !== "active"
2228
+ || active.pushedAt === undefined
2229
+ || sessions?.inFlight?.runId !== active.id
2230
+ || session?.launchId !== input.fence.launchId
2231
+ || session.nativeSessionId !== input.fence.nativeSessionId) {
2232
+ recordCanonicalObservationObsolete(store, input, "continuation-fence-mismatch", now);
2233
+ return "obsolete";
2234
+ }
2235
+ store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, input.fence.agentId, "running", now));
2236
+ const role = store.getRole(input.fence.taskId, input.fence.roleName);
2237
+ if (role !== null && role.status !== "running") {
2238
+ store.saveRole(input.fence.taskId, updateRoleStatus(role, "running", now));
2239
+ }
2240
+ store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "run.input-delivered", {
2241
+ attemptId: inputDelivery.attemptId,
2242
+ runId: active.id,
2243
+ conversationId: input.fence.conversationId ?? input.fence.nativeSessionId,
2244
+ activationId: input.fence.activationId ?? input.fence.launchId,
2245
+ ...(input.fence.nativeTurnId === undefined
2246
+ ? {}
2247
+ : { nativeTurnId: input.fence.nativeTurnId })
2248
+ }, now));
2249
+ store.saveWorkMailbox(completeMailboxInputDelivery(mailbox, inputDelivery.attemptId, now));
2250
+ return "applied";
2251
+ }
1973
2252
  const event = createCanonicalLifecycleEvent({
1974
2253
  phase: "provider-accepted",
1975
2254
  source: "provider-native",
@@ -2003,6 +2282,13 @@ export class FileSchedulerStoreAdapter {
2003
2282
  markTaskRoleRunDeliveredInFlight(store, role, delivered, now);
2004
2283
  store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "run.delivered", runLaunchEventPayload(delivered), now));
2005
2284
  }
2285
+ if (processing !== null && processing !== undefined
2286
+ && processing.batchId === input.fence.receiptId
2287
+ && processing.executionRef?.type === "run"
2288
+ && processing.executionRef.taskId === input.fence.taskId
2289
+ && processing.executionRef.id === input.fence.runId) {
2290
+ store.saveWorkMailbox(completeProcessing(mailbox, processing.batchId));
2291
+ }
2006
2292
  return "applied";
2007
2293
  });
2008
2294
  }
@@ -2197,12 +2483,33 @@ function runtimeObservationLifecycleFence(input, adapterId) {
2197
2483
  ...(input.fence.receiptId === undefined ? {} : { receiptId: input.fence.receiptId })
2198
2484
  };
2199
2485
  }
2486
+ /** Accepts the original Run receipt or a later mailbox activation receipt. */
2487
+ function runtimeReceiptBelongsToRun(store, input) {
2488
+ const taskId = input.fence.taskId;
2489
+ const runId = input.fence.runId;
2490
+ const receiptId = input.fence.receiptId;
2491
+ if (receiptId === formatAgentRunReceiptId(taskId, runId))
2492
+ return true;
2493
+ if (receiptId === undefined)
2494
+ return false;
2495
+ return store.listEvents(taskId).some((event) => {
2496
+ const accepted = runtimeObservationFromTaskEvent(event);
2497
+ return accepted?.kind === "turn.accepted"
2498
+ && accepted.fence.runId === runId
2499
+ && accepted.fence.roleName === input.fence.roleName
2500
+ && accepted.fence.agentId === input.fence.agentId
2501
+ && accepted.fence.launchId === input.fence.launchId
2502
+ && accepted.fence.nativeSessionId === input.fence.nativeSessionId
2503
+ && accepted.fence.receiptId === receiptId;
2504
+ });
2505
+ }
2200
2506
  function recordCanonicalObservationObsolete(store, input, reason, now) {
2201
2507
  const taskId = input.fence.taskId;
2202
2508
  if (taskId === undefined)
2203
2509
  return;
2204
2510
  recordObsoleteRuntimeEvent(store, {
2205
2511
  eventId: input.eventId,
2512
+ dedupeKey: input.semanticKey,
2206
2513
  eventType: input.kind,
2207
2514
  taskId,
2208
2515
  roleName: input.fence.roleName,
@@ -2214,10 +2521,11 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
2214
2521
  }
2215
2522
  function recordObsoleteRuntimeEvent(store, input, reason, now) {
2216
2523
  if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.event-obsolete"
2217
- && event.payload.eventId === input.eventId)))
2524
+ && (event.payload.dedupeKey ?? event.payload.eventId) === (input.dedupeKey ?? input.eventId))))
2218
2525
  return;
2219
2526
  store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.event-obsolete", {
2220
2527
  eventId: input.eventId,
2528
+ ...(input.dedupeKey === undefined ? {} : { dedupeKey: input.dedupeKey }),
2221
2529
  eventType: input.eventType ?? input.type ?? "unknown",
2222
2530
  roleName: input.roleName,
2223
2531
  agentId: input.agentId,
@@ -2324,7 +2632,7 @@ function completeRuntimeHookReservation(store, owner, launchId) {
2324
2632
  saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, launchId));
2325
2633
  }
2326
2634
  function saveRuntimeLifecycleMailbox(store, mailbox) {
2327
- if (mailbox.processing === null && mailbox.pending === null) {
2635
+ if (!mailboxHasWork(mailbox)) {
2328
2636
  store.removeWorkMailbox(mailbox.target);
2329
2637
  return;
2330
2638
  }
@@ -2422,8 +2730,7 @@ function preparedDeliveryFailureSessionFenceMatches(sessions, session, input) {
2422
2730
  return false;
2423
2731
  if (sessions.inFlight === null) {
2424
2732
  return input.nativeSessionId === undefined
2425
- && session === undefined
2426
- && sessions.pendingTurnCompletion === null;
2733
+ && session === undefined;
2427
2734
  }
2428
2735
  return sessions.inFlight.agentId === input.agentId
2429
2736
  && sessions.inFlight.runId === input.runId
@@ -2658,7 +2965,7 @@ function bindTaskRoleRunInFlight(store, role, run, now) {
2658
2965
  let current = store.getRoleSessionSet(role.taskId, role.name)
2659
2966
  ?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, agentId, now);
2660
2967
  if (current.activeAgentId !== agentId) {
2661
- if (current.inFlight !== null || current.pendingTurnCompletion !== null) {
2968
+ if (current.inFlight !== null || current.providerBinding !== null) {
2662
2969
  throw new Error("Task Role Session identity cannot change with an unsettled Run fence.");
2663
2970
  }
2664
2971
  const liveConflict = Object.values(current.sessions).find((session) => (session.agentId !== agentId
@@ -2808,8 +3115,13 @@ function runtimeObservationTelemetryEntry(input) {
2808
3115
  function compactedRuntimeObservationIds(events, incoming) {
2809
3116
  const existing = events.flatMap((event) => {
2810
3117
  const observation = runtimeObservationFromTaskEvent(event);
3118
+ const matches = incoming.kind.startsWith("operation.")
3119
+ ? observation !== null
3120
+ && runtimeObservationRunFenceMatches(observation.fence, incoming.fence)
3121
+ : observation !== null
3122
+ && runtimeObservationFenceMatches(observation.fence, incoming.fence);
2811
3123
  return observation !== null
2812
- && runtimeObservationFenceMatches(observation.fence, incoming.fence)
3124
+ && matches
2813
3125
  ? [{ event, observation }]
2814
3126
  : [];
2815
3127
  });
@@ -2838,7 +3150,8 @@ function compactedRuntimeObservationIds(events, incoming) {
2838
3150
  && observation.payload.sourceId === incoming.payload.sourceId;
2839
3151
  }
2840
3152
  if (["turn.completed", "turn.failed", "turn.cancelled"].includes(incoming.kind)) {
2841
- return observation.kind.startsWith("operation.")
3153
+ return (observation.kind.startsWith("operation.")
3154
+ && observation.payload.operation !== "subagent")
2842
3155
  || observation.kind === "turn.waiting"
2843
3156
  || observation.kind === "turn.completed"
2844
3157
  || observation.kind === "turn.failed"
@@ -2872,8 +3185,9 @@ function confirmedUsageActivityObservation(events, incoming) {
2872
3185
  .update(`confirmed-runtime-activity\0${incoming.eventId}`)
2873
3186
  .digest("hex");
2874
3187
  return createRuntimeObservation({
2875
- schemaVersion: 1,
3188
+ schemaVersion: 2,
2876
3189
  eventId: `runtime-activity-${digest}`,
3190
+ semanticKey: `runtime-activity-${digest}`,
2877
3191
  kind: "activity.observed",
2878
3192
  authority: "controller",
2879
3193
  receivedAt: incoming.receivedAt,