@zq-silk/yui 0.6.9 → 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 (56) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +62 -27
  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 +546 -255
  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 +198 -9
  34. package/dist/runtime/runtimeProjection.js +162 -7
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +1 -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/web/assets/client/app.js +1 -1
  49. package/dist/web/assets/client/components.js +233 -4
  50. package/dist/web/assets/client/i18n.js +166 -2
  51. package/dist/web/assets/client/view.js +30 -13
  52. package/dist/web/assets/styles/cards.js +62 -0
  53. package/dist/web/assets/styles/widgets.js +1 -0
  54. package/dist/web/webSnapshot.js +11 -2
  55. package/package.json +1 -1
  56. package/skills/yui-leader/SKILL.md +14 -9
@@ -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,10 +19,12 @@ 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";
@@ -57,6 +60,9 @@ export class FileSchedulerStoreAdapter {
57
60
  if (adapterId === null) {
58
61
  return this.recordObsoleteCanonicalObservation(input, "driver-or-run-mismatch", now);
59
62
  }
63
+ if (this.store.listEvents(taskId).some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
64
+ && event.payload.semanticKey === input.semanticKey)))
65
+ return "applied";
60
66
  let outcome;
61
67
  switch (input.kind) {
62
68
  case "session.started":
@@ -64,6 +70,7 @@ export class FileSchedulerStoreAdapter {
64
70
  outcome = this.observeRuntimeSession(input, adapterId, now);
65
71
  break;
66
72
  case "turn.accepted":
73
+ case "input.accepted":
67
74
  outcome = this.observeRuntimePromptAccepted(input, adapterId, now);
68
75
  break;
69
76
  case "turn.completed": {
@@ -94,6 +101,33 @@ export class FileSchedulerStoreAdapter {
94
101
  }
95
102
  case "turn.failed": {
96
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
+ }
97
131
  const failure = {
98
132
  eventId: input.eventId,
99
133
  eventType: input.kind,
@@ -126,8 +160,19 @@ export class FileSchedulerStoreAdapter {
126
160
  case "turn.cancelled":
127
161
  case "activity.observed":
128
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":
129
168
  outcome = this.validateCanonicalRunObservation(input, now);
130
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;
131
176
  case "session.ended":
132
177
  case "session.failed":
133
178
  outcome = this.validateCanonicalSessionObservation(input, now);
@@ -164,22 +209,30 @@ export class FileSchedulerStoreAdapter {
164
209
  validateCanonicalRunObservation(input, now) {
165
210
  return this.store.transaction((store) => {
166
211
  if (store.listEvents(input.fence.taskId).some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
167
- && event.payload.eventId === input.eventId)))
212
+ && event.payload.semanticKey === input.semanticKey)))
168
213
  return "applied";
169
214
  const run = store.getAgentRun(input.fence.taskId, input.fence.runId);
170
215
  const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
171
216
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
172
217
  const session = sessions?.sessions[input.fence.agentId];
173
- 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;
174
227
  const valid = run !== null
175
- && (!requiresActive || (run.status === "active" && active?.id === run.id))
228
+ && (!requiresCurrentRuntime || (run.status === "active" && active?.id === run.id))
176
229
  && run.roleName === input.fence.roleName
177
230
  && run.effective.agentId === input.fence.agentId
178
231
  && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId
179
- && session?.launchId === input.fence.launchId
180
- && session.nativeSessionId === input.fence.nativeSessionId
181
- && input.fence.sessionGenerationId === input.fence.launchId
182
- && 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)
183
236
  && (run.deliveredAt !== undefined || input.kind === "turn.cancelled");
184
237
  if (!valid) {
185
238
  recordCanonicalObservationObsolete(store, input, "runtime-fence-not-current", now);
@@ -205,7 +258,6 @@ export class FileSchedulerStoreAdapter {
205
258
  || run.roleName !== input.fence.roleName
206
259
  || run.effective.agentId !== input.fence.agentId
207
260
  || this.drivers.requireByAdapterId(run.effective.adapterId).id !== input.fence.driverId
208
- || input.fence.receiptId !== formatAgentRunReceiptId(input.fence.taskId, run.id)
209
261
  || session?.launchId !== input.fence.launchId
210
262
  || session.nativeSessionId !== input.fence.nativeSessionId
211
263
  || input.fence.sessionGenerationId !== input.fence.launchId) {
@@ -213,7 +265,101 @@ export class FileSchedulerStoreAdapter {
213
265
  return "obsolete";
214
266
  }
215
267
  const status = input.kind === "session.failed" ? "broken" : "stopped";
216
- 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
+ }
217
363
  return "applied";
218
364
  });
219
365
  }
@@ -222,7 +368,7 @@ export class FileSchedulerStoreAdapter {
222
368
  const taskId = input.fence.taskId;
223
369
  const events = store.listEvents(taskId);
224
370
  if (events.some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
225
- && event.payload.eventId === input.eventId)))
371
+ && event.payload.semanticKey === input.semanticKey)))
226
372
  return;
227
373
  if (usageSnapshotIsSuperseded(events, input))
228
374
  return;
@@ -235,7 +381,90 @@ export class FileSchedulerStoreAdapter {
235
381
  ];
236
382
  if (removable.length > 0)
237
383
  store.removeEvents(taskId, removable);
238
- 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
+ }
239
468
  if (confirmedActivity !== null) {
240
469
  store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(confirmedActivity), now));
241
470
  }
@@ -513,6 +742,42 @@ export class FileSchedulerStoreAdapter {
513
742
  const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
514
743
  return sessions !== null && sessions.inFlight !== null;
515
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
+ }
516
781
  isRoleGenerationProviderReady(input) {
517
782
  // A session.ready observation for this exact generation is the durable
518
783
  // pre-input fence. This reads folded Driver truth
@@ -584,7 +849,7 @@ export class FileSchedulerStoreAdapter {
584
849
  claimWorkMailbox(input) {
585
850
  return this.store.transaction((store) => {
586
851
  const mailbox = store.getWorkMailbox(input.target);
587
- if (mailbox === null || (mailbox.processing === null && mailbox.pending === null)) {
852
+ if (mailbox === null || !mailboxHasWork(mailbox)) {
588
853
  return { status: "empty" };
589
854
  }
590
855
  if (mailbox.processing !== null) {
@@ -602,6 +867,104 @@ export class FileSchedulerStoreAdapter {
602
867
  return { status: "claimed", processing: claimed.processing };
603
868
  });
604
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
+ }
605
968
  completeWorkMailbox(target, batchId) {
606
969
  return this.store.transaction((store) => {
607
970
  const mailbox = store.getWorkMailbox(target);
@@ -639,12 +1002,13 @@ export class FileSchedulerStoreAdapter {
639
1002
  }
640
1003
  mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
641
1004
  }
642
- if (mailbox.pending !== null) {
643
- if (mailbox.pending.reasons.length !== 1
644
- || 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) {
645
1009
  return false;
646
1010
  }
647
- const batchId = `runtime-cleanup-complete:${mailbox.pending.fromSequence}-${mailbox.pending.toSequence}`;
1011
+ const batchId = `runtime-cleanup-complete:${pending.fromSequence}-${pending.toSequence}`;
648
1012
  mailbox = completeProcessing(claimPending(mailbox, {
649
1013
  batchId,
650
1014
  owner: RUNTIME_LIFECYCLE_OWNER,
@@ -745,7 +1109,7 @@ export class FileSchedulerStoreAdapter {
745
1109
  }
746
1110
  const target = { kind: "role", taskId: input.task.id, roleName: input.role.name };
747
1111
  const mailbox = store.getWorkMailbox(target);
748
- if (mailbox === null || mailbox.pending === null)
1112
+ if (mailbox === null || !mailboxHasPending(mailbox))
749
1113
  return "state-changed";
750
1114
  if (mailbox.processing !== null)
751
1115
  return "busy";
@@ -1266,8 +1630,8 @@ export class FileSchedulerStoreAdapter {
1266
1630
  }
1267
1631
  /**
1268
1632
  * Fast hook path: validates the native Turn boundary before it is either
1269
- * retained as an intermediate child wait or given a two-second workflow
1270
- * closure deadline. It never performs tmux, workspace, or Controller I/O.
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.
1271
1635
  */
1272
1636
  classifyRuntimeTurnCompleted(input) {
1273
1637
  const task = this.store.getTask(input.taskId);
@@ -1294,11 +1658,6 @@ export class FileSchedulerStoreAdapter {
1294
1658
  catch {
1295
1659
  return "obsolete";
1296
1660
  }
1297
- if (sessions?.pendingTurnCompletion !== null && sessions !== null) {
1298
- return sessions.pendingTurnCompletion.turnId === input.turnId
1299
- ? "apply"
1300
- : "deferred";
1301
- }
1302
1661
  // A normal explicit CLI yield may precede its native Hook; that Hook still
1303
1662
  // owns the turn fact. A forced cleanup boundary or stopped process instead
1304
1663
  // makes the old generation obsolete.
@@ -1350,19 +1709,6 @@ export class FileSchedulerStoreAdapter {
1350
1709
  && hasRecentTurnId(effectiveExisting.recentCompletedTurnIds, input.turnId)) {
1351
1710
  return { session: effectiveExisting, duplicate: true };
1352
1711
  }
1353
- const pending = sessions.pendingTurnCompletion;
1354
- if (effectiveExisting !== undefined
1355
- && pending !== null
1356
- && pending.agentId === input.agentId
1357
- && pending.nativeSessionId === input.nativeSessionId
1358
- && pending.turnId === input.turnId
1359
- && pending.runId === input.runId) {
1360
- return {
1361
- session: effectiveExisting,
1362
- duplicate: true,
1363
- pendingRunId: pending.runId
1364
- };
1365
- }
1366
1712
  if (sessions.inFlight === null
1367
1713
  && existing !== undefined
1368
1714
  && (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))
@@ -1403,205 +1749,23 @@ export class FileSchedulerStoreAdapter {
1403
1749
  completeRuntimeHookReservation(store, owner, input.launchId);
1404
1750
  return { session: sessions.sessions[input.agentId], duplicate: false };
1405
1751
  }
1406
- const fence = {
1407
- agentId: sessions.inFlight.agentId,
1408
- runId: sessions.inFlight.runId,
1409
- receiptId: sessions.inFlight.receiptId
1410
- };
1411
- const completion = createPendingTurnCompletion({
1412
- taskId: task.id,
1413
- 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, {
1414
1757
  agentId: input.agentId,
1415
1758
  nativeSessionId: input.nativeSessionId,
1416
- turnId: input.turnId,
1417
- runId: fence.runId,
1418
- summary: input.summary,
1419
- observedAt: now,
1420
- dueAt: new Date(now.getTime() + 2_000)
1421
- });
1422
- // Runtime completion is not workflow progress. The exact Run remains
1423
- // open for the short outcome grace period so a concurrently committed
1424
- // yui yield/block/complete action can win; otherwise the contract fails.
1425
- sessions = recordObservedTaskRoleCompletion(sessions, completion);
1426
- const active = store.getActiveAgentRun(task.id, role.name);
1427
- if (active === null || active.id !== fence.runId || active.status !== "active") {
1428
- sessions = settleTaskRoleCompletion(sessions, {
1429
- agentId: input.agentId,
1430
- runId: fence.runId,
1431
- turnId: input.turnId
1432
- }, now);
1433
- const settledStatus = sessions.sessions[input.agentId]?.status;
1434
- if (settledStatus !== "stopped" && settledStatus !== "broken") {
1435
- sessions = updateRoleAgentSessionStatus(sessions, input.agentId, "ready", now);
1436
- }
1437
- store.saveTaskRoleSessionSet(sessions);
1438
- completeRuntimeHookReservation(store, owner, input.launchId);
1439
- return { session: sessions.sessions[input.agentId], duplicate: false };
1440
- }
1759
+ turnId: input.turnId
1760
+ }, now);
1441
1761
  store.saveTaskRoleSessionSet(sessions);
1442
1762
  completeRuntimeHookReservation(store, owner, input.launchId);
1443
1763
  return {
1444
1764
  session: sessions.sessions[input.agentId],
1445
- duplicate: false,
1446
- pendingRunId: fence.runId
1765
+ duplicate: false
1447
1766
  };
1448
1767
  });
1449
1768
  }
1450
- listPendingRuntimeTurnCompletions(taskIds) {
1451
- return this.store.listPendingRuntimeTurnCompletions(taskIds);
1452
- }
1453
- /** Resolves only completions whose grace deadline has elapsed. */
1454
- resolveDueRuntimeTurnCompletions(now, taskIds) {
1455
- const selectedTaskIds = taskIds === undefined
1456
- ? undefined
1457
- : [...taskIds].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
1458
- const due = this.listPendingRuntimeTurnCompletions(selectedTaskIds).filter((completion) => Date.parse(completion.dueAt) <= now.getTime());
1459
- const finalized = [];
1460
- for (const completion of due) {
1461
- this.store.transaction((store) => {
1462
- const current = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
1463
- if (current === null)
1464
- return;
1465
- const pending = current.pendingTurnCompletion;
1466
- if (pending === null
1467
- || pending.turnId !== completion.turnId
1468
- || pending.runId !== completion.runId)
1469
- return;
1470
- const active = store.getActiveAgentRun(completion.taskId, completion.roleName);
1471
- if (active?.id === completion.runId) {
1472
- const result = this.recordRuntimeTurnCompleted({
1473
- taskId: completion.taskId,
1474
- roleName: completion.roleName,
1475
- agentId: completion.agentId,
1476
- adapterId: current.sessions[completion.agentId].adapterId,
1477
- nativeSessionId: completion.nativeSessionId,
1478
- turnId: completion.turnId,
1479
- expectedRunId: completion.runId,
1480
- summary: completion.summary
1481
- }, now);
1482
- if (result.finalizedRunId !== undefined) {
1483
- finalized.push(formatTaskRecordReference(completion.taskId, result.finalizedRunId, "agentRun"));
1484
- }
1485
- }
1486
- let settled = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
1487
- if (settled?.pendingTurnCompletion?.turnId !== completion.turnId)
1488
- return;
1489
- settled = settleTaskRoleCompletion(settled, {
1490
- agentId: completion.agentId,
1491
- runId: completion.runId,
1492
- turnId: completion.turnId
1493
- }, now);
1494
- const settledStatus = settled.sessions[completion.agentId]?.status;
1495
- if (settledStatus !== "stopped" && settledStatus !== "broken") {
1496
- settled = updateRoleAgentSessionStatus(settled, completion.agentId, "ready", now);
1497
- }
1498
- store.saveTaskRoleSessionSet(settled);
1499
- });
1500
- }
1501
- return finalized;
1502
- }
1503
- /** A structured native Turn-completion event terminalizes its exact Run fence. */
1504
- recordRuntimeTurnCompleted(input, now = new Date()) {
1505
- return this.store.transaction((store) => {
1506
- const task = store.getTask(input.taskId);
1507
- if (task === null)
1508
- throw new Error(`Task not found: ${input.taskId}.`);
1509
- if (task.status !== "active") {
1510
- throw new Error(`Cannot complete a runtime turn for inactive Task: ${input.taskId}.`);
1511
- }
1512
- const role = requireRole(store, input.taskId, input.roleName);
1513
- const current = store.getRoleSessionSet(input.taskId, input.roleName)
1514
- ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
1515
- const existing = current.sessions[input.agentId];
1516
- const owner = {
1517
- scope: "task",
1518
- taskId: input.taskId,
1519
- roleName: input.roleName
1520
- };
1521
- if (existing === undefined
1522
- && !runtimeHookMatchesReservation(store, owner, input.launchId)) {
1523
- throw new Error("Runtime turn completion does not match the launch reservation.");
1524
- }
1525
- const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
1526
- const effective = taskSessionEffective(store, task.id, role.name, input.agentId, effectiveExisting);
1527
- if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
1528
- throw new Error("Runtime turn completion does not match the effective launch identity.");
1529
- }
1530
- const sessions = effectiveExisting?.status === "ready"
1531
- || effectiveExisting?.status === "stopped"
1532
- || effectiveExisting?.status === "broken"
1533
- ? current
1534
- : recordRoleAgentSession(current, {
1535
- agentId: input.agentId,
1536
- adapterId: input.adapterId,
1537
- nativeSessionId: input.nativeSessionId,
1538
- ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1539
- policy: "fixed",
1540
- status: "ready",
1541
- effective
1542
- }, now);
1543
- if (input.expectedRunId !== undefined
1544
- && store.getActiveAgentRun(task.id, role.name)?.id !== input.expectedRunId) {
1545
- return { session: sessions.sessions[input.agentId] };
1546
- }
1547
- if (sessions !== current)
1548
- store.saveRoleSessionSet(sessions);
1549
- completeRuntimeHookReservation(store, owner, input.launchId);
1550
- const session = sessions.sessions[input.agentId];
1551
- const active = store.getActiveAgentRun(task.id, role.name);
1552
- if (task.status !== "active"
1553
- || active === null
1554
- || active.pushedAt === undefined
1555
- || (input.expectedRunId !== undefined && active.id !== input.expectedRunId)) {
1556
- return { session };
1557
- }
1558
- const summary = `Agent turn completed without a Yui workflow outcome. Last assistant message: ${input.summary}`;
1559
- const result = terminalizeExactTaskRun(store, {
1560
- taskId: task.id,
1561
- roleName: role.name,
1562
- agentId: active.effective.agentId,
1563
- runId: active.id,
1564
- receiptId: formatAgentRunReceiptId(task.id, active.id),
1565
- nativeSessionId: input.nativeSessionId,
1566
- ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
1567
- outcome: { status: "failed", summary }
1568
- }, now);
1569
- if (result.disposition !== "applied" || result.run === null) {
1570
- throw new Error(`Role Run changed during native Turn completion: ${active.id}/${result.reason}.`);
1571
- }
1572
- const terminal = result.run;
1573
- const leaderWorkflowRun = role.name === "leader" && active.workItemId === undefined;
1574
- if (active.purpose === "execution" && active.workItemId !== undefined) {
1575
- const item = store.getWorkItem(task.id, active.workItemId);
1576
- if (item !== null && !["completed", "failed", "retired"].includes(item.status)) {
1577
- store.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", now, summary));
1578
- }
1579
- }
1580
- store.saveRole(task.id, updateRoleStatus(role, leaderWorkflowRun ? "failed" : "idle", now));
1581
- clearMatchingLeaderStallAttention(store, task.id, terminal.id);
1582
- if (!leaderWorkflowRun) {
1583
- enqueueWork(store, { kind: "role", taskId: task.id, roleName: "leader" }, "role-run-failed", now, [
1584
- { type: "run", taskId: task.id, id: terminal.id },
1585
- ...(terminal.workItemId === undefined
1586
- ? []
1587
- : [{
1588
- type: "work-item",
1589
- taskId: task.id,
1590
- id: terminal.workItemId
1591
- }])
1592
- ]);
1593
- }
1594
- else {
1595
- store.saveLeaderFailure(recordLeaderFailure(task.id, input.nativeSessionId, summary, now, store.getLeaderFailure(task.id)));
1596
- store.saveOperatorNotification(createLeaderRecoveryNotification(task.id, summary, now, store.getOperatorNotification(task.id)));
1597
- enqueueWork(store, { kind: "operator" }, "leader-run-failed", now, [
1598
- { type: "task", id: task.id },
1599
- { type: "run", taskId: task.id, id: terminal.id }
1600
- ]);
1601
- }
1602
- return { session, finalizedRunId: terminal.id };
1603
- });
1604
- }
1605
1769
  classifyRuntimeTurnFailed(input) {
1606
1770
  const task = this.store.getTask(input.taskId);
1607
1771
  if (task?.status !== "active")
@@ -1736,24 +1900,6 @@ export class FileSchedulerStoreAdapter {
1736
1900
  }
1737
1901
  return null;
1738
1902
  }
1739
- // transport-uncertain: the Provider may have accepted the turn. If a
1740
- // native completion is already durable, the completion path owns the
1741
- // yield; do not resend.
1742
- if (classification.errorClass === "transport-uncertain") {
1743
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1744
- const pending = sessions?.pendingTurnCompletion;
1745
- if (pending !== null
1746
- && pending !== undefined
1747
- && pending.runId === input.runId
1748
- && pending.agentId === input.agentId) {
1749
- this.recordProviderRetryClassified(store, input, classification.errorClass, {
1750
- wouldRetry: "false",
1751
- shadow: "false",
1752
- note: "native-turn-completion-durable"
1753
- }, now);
1754
- return { disposition: "applied", runId: input.runId };
1755
- }
1756
- }
1757
1903
  const run = store.getAgentRun(input.taskId, input.runId);
1758
1904
  if (run === null || run.status !== "active")
1759
1905
  return null;
@@ -1919,6 +2065,51 @@ export class FileSchedulerStoreAdapter {
1919
2065
  * Applies already-normalized Session observations. Driver-specific startup
1920
2066
  * names and source variants have terminated before this boundary.
1921
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
+ }
1922
2113
  observeRuntimeSession(input, adapterId, now) {
1923
2114
  return this.store.transaction((store) => {
1924
2115
  const event = createCanonicalLifecycleEvent({
@@ -1968,7 +2159,17 @@ export class FileSchedulerStoreAdapter {
1968
2159
  status: "running",
1969
2160
  effective: run.effective
1970
2161
  }, now);
1971
- 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);
1972
2173
  const driver = this.drivers.require(input.fence.driverId);
1973
2174
  if (driver.capabilities.observation.sessionBootstrap === "discovered"
1974
2175
  && run.pushedAt === undefined) {
@@ -1979,6 +2180,21 @@ export class FileSchedulerStoreAdapter {
1979
2180
  }
1980
2181
  completeRuntimeHookReservation(store, { scope: "task", taskId, roleName: input.fence.roleName }, input.fence.launchId);
1981
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
+ }
1982
2198
  return "applied";
1983
2199
  }
1984
2200
  }
@@ -1987,6 +2203,52 @@ export class FileSchedulerStoreAdapter {
1987
2203
  /** Provider acceptance is canonical before storage and remains receipt-fenced. */
1988
2204
  observeRuntimePromptAccepted(input, adapterId, now) {
1989
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
+ }
1990
2252
  const event = createCanonicalLifecycleEvent({
1991
2253
  phase: "provider-accepted",
1992
2254
  source: "provider-native",
@@ -2020,6 +2282,13 @@ export class FileSchedulerStoreAdapter {
2020
2282
  markTaskRoleRunDeliveredInFlight(store, role, delivered, now);
2021
2283
  store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "run.delivered", runLaunchEventPayload(delivered), now));
2022
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
+ }
2023
2292
  return "applied";
2024
2293
  });
2025
2294
  }
@@ -2214,12 +2483,33 @@ function runtimeObservationLifecycleFence(input, adapterId) {
2214
2483
  ...(input.fence.receiptId === undefined ? {} : { receiptId: input.fence.receiptId })
2215
2484
  };
2216
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
+ }
2217
2506
  function recordCanonicalObservationObsolete(store, input, reason, now) {
2218
2507
  const taskId = input.fence.taskId;
2219
2508
  if (taskId === undefined)
2220
2509
  return;
2221
2510
  recordObsoleteRuntimeEvent(store, {
2222
2511
  eventId: input.eventId,
2512
+ dedupeKey: input.semanticKey,
2223
2513
  eventType: input.kind,
2224
2514
  taskId,
2225
2515
  roleName: input.fence.roleName,
@@ -2231,10 +2521,11 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
2231
2521
  }
2232
2522
  function recordObsoleteRuntimeEvent(store, input, reason, now) {
2233
2523
  if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.event-obsolete"
2234
- && event.payload.eventId === input.eventId)))
2524
+ && (event.payload.dedupeKey ?? event.payload.eventId) === (input.dedupeKey ?? input.eventId))))
2235
2525
  return;
2236
2526
  store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.event-obsolete", {
2237
2527
  eventId: input.eventId,
2528
+ ...(input.dedupeKey === undefined ? {} : { dedupeKey: input.dedupeKey }),
2238
2529
  eventType: input.eventType ?? input.type ?? "unknown",
2239
2530
  roleName: input.roleName,
2240
2531
  agentId: input.agentId,
@@ -2341,7 +2632,7 @@ function completeRuntimeHookReservation(store, owner, launchId) {
2341
2632
  saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, launchId));
2342
2633
  }
2343
2634
  function saveRuntimeLifecycleMailbox(store, mailbox) {
2344
- if (mailbox.processing === null && mailbox.pending === null) {
2635
+ if (!mailboxHasWork(mailbox)) {
2345
2636
  store.removeWorkMailbox(mailbox.target);
2346
2637
  return;
2347
2638
  }
@@ -2439,8 +2730,7 @@ function preparedDeliveryFailureSessionFenceMatches(sessions, session, input) {
2439
2730
  return false;
2440
2731
  if (sessions.inFlight === null) {
2441
2732
  return input.nativeSessionId === undefined
2442
- && session === undefined
2443
- && sessions.pendingTurnCompletion === null;
2733
+ && session === undefined;
2444
2734
  }
2445
2735
  return sessions.inFlight.agentId === input.agentId
2446
2736
  && sessions.inFlight.runId === input.runId
@@ -2675,7 +2965,7 @@ function bindTaskRoleRunInFlight(store, role, run, now) {
2675
2965
  let current = store.getRoleSessionSet(role.taskId, role.name)
2676
2966
  ?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, agentId, now);
2677
2967
  if (current.activeAgentId !== agentId) {
2678
- if (current.inFlight !== null || current.pendingTurnCompletion !== null) {
2968
+ if (current.inFlight !== null || current.providerBinding !== null) {
2679
2969
  throw new Error("Task Role Session identity cannot change with an unsettled Run fence.");
2680
2970
  }
2681
2971
  const liveConflict = Object.values(current.sessions).find((session) => (session.agentId !== agentId
@@ -2895,8 +3185,9 @@ function confirmedUsageActivityObservation(events, incoming) {
2895
3185
  .update(`confirmed-runtime-activity\0${incoming.eventId}`)
2896
3186
  .digest("hex");
2897
3187
  return createRuntimeObservation({
2898
- schemaVersion: 1,
3188
+ schemaVersion: 2,
2899
3189
  eventId: `runtime-activity-${digest}`,
3190
+ semanticKey: `runtime-activity-${digest}`,
2900
3191
  kind: "activity.observed",
2901
3192
  authority: "controller",
2902
3193
  receivedAt: incoming.receivedAt,