@zq-silk/yui 0.11.3 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +44 -22
  3. package/dist/cli/commandCatalog.js +13 -10
  4. package/dist/cli/interactionPolicy.js +5 -6
  5. package/dist/cli/updateCommand.js +3 -1
  6. package/dist/cli/updateOrchestrator.js +173 -28
  7. package/dist/cli/updatePorts.js +137 -8
  8. package/dist/cli/upgradeCommand.js +19 -9
  9. package/dist/cli.js +156 -57
  10. package/dist/commands/configCommands.js +13 -46
  11. package/dist/commands/executionAuditCommands.js +3 -2
  12. package/dist/commands/jobCommands.js +4 -10
  13. package/dist/commands/taskCommands.js +234 -200
  14. package/dist/commands/taskCompletionGate.js +29 -54
  15. package/dist/commands/taskContextCommand.js +61 -13
  16. package/dist/commands/taskInputCommands.js +8 -5
  17. package/dist/commands/taskNextActionCommand.js +56 -6
  18. package/dist/commands/taskOverviewCommand.js +17 -31
  19. package/dist/commands/taskRoleRuntimeStatus.js +55 -7
  20. package/dist/commands/taskWorkspaceCommands.js +5 -2
  21. package/dist/config/configCatalog.js +3 -3
  22. package/dist/config/yuiConfig.js +3 -7
  23. package/dist/context/wakeNotification.js +20 -5
  24. package/dist/controller/agentRuntimeObserver.js +247 -51
  25. package/dist/controller/clientRuntime.js +39 -3
  26. package/dist/controller/controller.js +31 -27
  27. package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
  28. package/dist/controller/runtime.js +58 -3
  29. package/dist/controller/runtimeEventInbox.js +17 -7
  30. package/dist/controller/runtimeEventProcessor.js +12 -19
  31. package/dist/controller/runtimeHookRunFence.js +19 -4
  32. package/dist/controller/runtimeObservationHook.js +45 -0
  33. package/dist/controller/structuredProviderObservation.js +20 -3
  34. package/dist/core/controllerClient.js +20 -2
  35. package/dist/core/controllerServer.js +1 -0
  36. package/dist/execution/resourceBroker.js +5 -4
  37. package/dist/executor/agentExecutor.js +52 -50
  38. package/dist/executor/effectiveLaunch.js +8 -48
  39. package/dist/executor/fileRoleLaunchPlanner.js +129 -45
  40. package/dist/executor/workspacePreflightClassification.js +23 -2
  41. package/dist/interaction/operatorPresentation.js +33 -89
  42. package/dist/lifecycle/exactRunTerminalization.js +69 -5
  43. package/dist/observability/executionAudit.js +5 -0
  44. package/dist/observability/orchestrationMetrics.js +8 -3
  45. package/dist/profile/agentProfile.js +1 -1
  46. package/dist/release/cliHomeReleaseFence.js +123 -0
  47. package/dist/release/runtimeRelease.js +20 -0
  48. package/dist/repository/taskWorkspacePreparer.js +176 -60
  49. package/dist/resources/sqliteResourceRegistry.js +1 -1
  50. package/dist/review/deltaRecheck.js +12 -51
  51. package/dist/review/reviewAcceptance.js +26 -0
  52. package/dist/review/reviewConfig.js +0 -31
  53. package/dist/review/reviewDecision.js +113 -0
  54. package/dist/review/reviewOutcomeClassifier.js +1 -1
  55. package/dist/review/reviewRound.js +1 -1
  56. package/dist/review/reviewerAvailability.js +69 -0
  57. package/dist/run/recoveryProjection.js +45 -6
  58. package/dist/runtime/agentDriverObservation.js +24 -10
  59. package/dist/runtime/agentHost.js +159 -85
  60. package/dist/runtime/builtinAgentDrivers.js +4 -3
  61. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  62. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  63. package/dist/runtime/conversationSwitch.js +277 -0
  64. package/dist/runtime/index.js +2 -1
  65. package/dist/runtime/launchBroker.js +12 -0
  66. package/dist/runtime/processExitOutbox.js +88 -0
  67. package/dist/runtime/providerRuntimeIdentity.js +29 -1
  68. package/dist/runtime/runtimeHealthPolicy.js +5 -5
  69. package/dist/runtime/runtimeObservation.js +28 -0
  70. package/dist/runtime/runtimeProjection.js +22 -32
  71. package/dist/runtime/sessionTokenMetrics.js +181 -0
  72. package/dist/runtime/structuredProviderHost.js +7 -1
  73. package/dist/runtime/tmuxAdapters.js +4 -1
  74. package/dist/scheduler/activeRoleRunDelivery.js +34 -199
  75. package/dist/scheduler/activeTaskProgress.js +7 -6
  76. package/dist/scheduler/leaderWakeupProcessor.js +42 -138
  77. package/dist/scheduler/operatorEvent.js +34 -0
  78. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  79. package/dist/scheduler/roleRunStall.js +53 -31
  80. package/dist/scheduler/taskExecutionProjection.js +97 -76
  81. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  82. package/dist/storage/migration/productionRegistry.js +379 -0
  83. package/dist/storage/sqliteSchema.js +66 -23
  84. package/dist/storage/sqliteStore.js +46 -23
  85. package/dist/storage/storeRpc.js +0 -1
  86. package/dist/storage/taskStore.js +15 -30
  87. package/dist/storage/upgrade/homeClassification.js +52 -0
  88. package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
  89. package/dist/storage/upgrade/recordVersions.js +3 -2
  90. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  91. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  92. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
  94. package/dist/task/completionReadiness.js +10 -6
  95. package/dist/task/nextAction.js +82 -58
  96. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  97. package/dist/web/assets/client/components.js +13 -3
  98. package/dist/web/assets/client/i18n.js +6 -2
  99. package/dist/web/webSnapshot.js +7 -1
  100. package/i18n/README.zh-CN.md +2 -2
  101. package/package.json +1 -1
  102. package/skills/yui-leader/SKILL.md +60 -20
  103. package/skills/yui-operator/SKILL.md +13 -4
  104. package/skills/yui-reviewer/SKILL.md +35 -11
  105. package/dist/context/sessionContextBudget.js +0 -71
  106. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  107. package/dist/lifecycle/taskRoleSessionReset.js +0 -126
  108. package/dist/scheduler/operatorNotification.js +0 -59
@@ -1,18 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleProviderRuntimeForCleanup, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
- import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
3
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, replaceTaskRoleAgentSessionForConversationSwitch, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, stopTaskRoleRuntimeAfterPhysicalExit, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
+ import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurnSubmission, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
5
+ import { CONVERSATION_SWITCH_DETACHED_EVENT, CONVERSATION_SWITCH_RESOLVED_EVENT, conversationDetachmentBasis, conversationReplacementBasis, pendingConversationSwitch, roleSessionDispatchModeWithConversationSwitch } from "../runtime/conversationSwitch.js";
5
6
  import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
6
7
  import { hasRecentTurnId } from "../executor/turnCompletion.js";
7
8
  import { createTaskEvent } from "../event/taskEvent.js";
8
9
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
9
10
  import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
10
11
  import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
11
- import { rolloverTaskRoleSessionForContextBudget } from "../lifecycle/contextBudgetRollover.js";
12
- import { resolveContextBudget } from "../config/yuiConfig.js";
13
12
  import { answerInputRequest } from "../input/inputRequest.js";
14
13
  import { activeRoleAgentBinding, updateRoleStatus } from "../role/role.js";
15
- import { effectiveLaunchWithTaskMainWorkspace, effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
14
+ import { effectiveLaunchWithTaskMainWorkspace, effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
16
15
  import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
17
16
  import { failAgentRun, markAgentRunDelivered, markAgentRunPushed, reopenRunForProviderRetry, clearProviderRetryOnProgress, agentRunDeliveryReceiptId, withProviderRetry } from "../run/agentRun.js";
18
17
  import { markWorkflowOutcomeRequestDispatched } from "../run/runControlRequest.js";
@@ -23,11 +22,11 @@ import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } fro
23
22
  import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
24
23
  import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
25
24
  import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
26
- import { createLeaderRecoveryNotification, createLeaderStallNotification } from "../scheduler/operatorNotification.js";
25
+ import { recordLeaderAttentionRequired, routeRoleEvent } from "../scheduler/operatorEvent.js";
27
26
  import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
28
27
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
29
28
  import { wakeReason } from "../scheduler/wakeReason.js";
30
- import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
29
+ import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_DIAGNOSTIC_FINISHED_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
31
30
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
32
31
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
33
32
  import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup, updateWorkItemStatus, workItemOwnsUnresolvedExecutionLane } from "../workItem/workItem.js";
@@ -38,7 +37,7 @@ import { enqueueWork } from "../coordination/workMailboxQueue.js";
38
37
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, RuntimeLifecycleBusyError, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
39
38
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
40
39
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
41
- import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
40
+ import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeTokenEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload, runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
42
41
  import { projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
43
42
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
44
43
  import { contextSnapshotDeltaRefIds, freezeRunContextSnapshot } from "../context/runContextPack.js";
@@ -90,8 +89,7 @@ export class FileSchedulerStoreAdapter {
90
89
  if (adapterId === null) {
91
90
  return this.recordObsoleteCanonicalObservation(input, "driver-or-run-mismatch", now);
92
91
  }
93
- if (this.store.listEvents(taskId).some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
94
- && event.payload.semanticKey === input.semanticKey)))
92
+ if (hasPersistedRuntimeObservation(this.store.listEvents(taskId), input))
95
93
  return "applied";
96
94
  let outcome;
97
95
  switch (input.kind) {
@@ -112,6 +110,7 @@ export class FileSchedulerStoreAdapter {
112
110
  launchId: input.fence.launchId,
113
111
  nativeSessionId: input.fence.nativeSessionId,
114
112
  turnId: input.fence.nativeTurnId,
113
+ attemptId: input.fence.receiptId,
115
114
  runId: input.fence.runId,
116
115
  summary: input.payload.summary ?? "Agent turn completed without a workflow outcome."
117
116
  };
@@ -152,6 +151,7 @@ export class FileSchedulerStoreAdapter {
152
151
  launchId: input.fence.launchId,
153
152
  nativeSessionId: input.fence.nativeSessionId,
154
153
  turnId: input.fence.nativeTurnId,
154
+ attemptId: input.fence.receiptId,
155
155
  runId: input.fence.runId,
156
156
  summary: input.payload.summary ?? `Provider Turn failed: ${failureEvidence.code}.`,
157
157
  providerStatus: "failed"
@@ -223,7 +223,8 @@ export class FileSchedulerStoreAdapter {
223
223
  }
224
224
  if (outcome === "applied"
225
225
  && input.kind === "activity.observed"
226
- && input.payload.activityId !== undefined) {
226
+ && input.payload.activityId !== undefined
227
+ && input.payload.usage === undefined) {
227
228
  this.clearProviderRetryForProgress(input, "correlated-activity", now);
228
229
  }
229
230
  if (outcome === "applied")
@@ -251,9 +252,9 @@ export class FileSchedulerStoreAdapter {
251
252
  }
252
253
  validateCanonicalRunObservation(input, now) {
253
254
  return this.store.transaction((store) => {
254
- if (store.listEvents(input.fence.taskId).some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
255
- && event.payload.semanticKey === input.semanticKey)))
255
+ if (hasPersistedRuntimeObservation(store.listEvents(input.fence.taskId), input)) {
256
256
  return "applied";
257
+ }
257
258
  const run = store.getAgentRun(input.fence.taskId, input.fence.runId);
258
259
  const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
259
260
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
@@ -386,12 +387,14 @@ export class FileSchedulerStoreAdapter {
386
387
  const message = [
387
388
  `Leader native Session became unavailable while Run ${active.id} remains active.`,
388
389
  "Yui preserved the Run because Session/host termination is not a workflow outcome.",
389
- "Inspect native-child and mailbox facts, then explicitly continue, reset, or request user input."
390
+ "Inspect native-child and mailbox facts, then explicitly diagnose, recover the Run, request a Conversation switch, or request user input."
390
391
  ].join(" ");
391
- store.saveOperatorNotification(createLeaderRecoveryNotification(input.fence.taskId, message, now, store.getOperatorNotification(input.fence.taskId)));
392
- enqueueWork(store, { kind: "operator" }, "leader-runtime-detached", now, [
393
- { type: "run", taskId: input.fence.taskId, id: active.id }
394
- ]);
392
+ recordLeaderAttentionRequired(store, {
393
+ taskId: input.fence.taskId,
394
+ reason: "leader-runtime-detached",
395
+ payload: { message, runId: active.id },
396
+ now
397
+ });
395
398
  }
396
399
  else {
397
400
  enqueueWork(store, {
@@ -410,22 +413,16 @@ export class FileSchedulerStoreAdapter {
410
413
  this.store.transaction((store) => {
411
414
  const taskId = input.fence.taskId;
412
415
  const events = store.listEvents(taskId);
413
- if (events.some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
414
- && event.payload.semanticKey === input.semanticKey)))
416
+ if (hasPersistedRuntimeObservation(events, input))
415
417
  return;
416
418
  if (usageSnapshotIsSuperseded(events, input))
417
419
  return;
418
- const confirmedActivity = confirmedUsageActivityObservation(events, input);
419
- const removable = [
420
- ...compactedRuntimeObservationIds(events, input),
421
- ...(confirmedActivity === null
422
- ? []
423
- : compactedRuntimeObservationIds(events, confirmedActivity))
424
- ];
420
+ const removable = compactedRuntimeObservationIds(events, input);
425
421
  if (removable.length > 0)
426
422
  store.removeEvents(taskId, removable);
427
423
  const observationEventId = store.nextEventId(taskId);
428
- store.saveEvent(taskId, createTaskEvent(observationEventId, taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(input), now));
424
+ const observationEvent = createTaskEvent(observationEventId, taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(input), now);
425
+ store.saveEvent(taskId, observationEvent);
429
426
  if (input.kind === "native-work.snapshot"
430
427
  && input.payload.snapshotComplete === true
431
428
  && input.payload.observationQuality === "exact") {
@@ -503,14 +500,9 @@ export class FileSchedulerStoreAdapter {
503
500
  // Route a Yui wake only when that parent is gone and another observer
504
501
  // supplied the child's terminal fact.
505
502
  if (role !== null && (session?.status === "stopped" || session?.status === "broken")) {
506
- enqueueWork(store, role.name === "leader"
507
- ? { kind: "operator" }
508
- : { kind: "role", taskId, roleName: "leader" }, "detached-native-subagent-terminal", now, [{ type: "run", taskId, id: input.fence.runId }]);
503
+ routeRoleEvent(store, observationEvent, role.name, "detached-native-subagent-terminal", now);
509
504
  }
510
505
  }
511
- if (confirmedActivity !== null) {
512
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(confirmedActivity), now));
513
- }
514
506
  });
515
507
  if (this.telemetry !== null && input.fence.runId !== undefined) {
516
508
  try {
@@ -573,9 +565,6 @@ export class FileSchedulerStoreAdapter {
573
565
  withRuntimeEventTransaction(execute) {
574
566
  return this.store.withRuntimeEventTransaction(execute);
575
567
  }
576
- getPresentationContext() {
577
- return { timeZone: this.store.getConfig().timeZone };
578
- }
579
568
  listTasks() { return this.store.listTasks(); }
580
569
  listActiveTaskIds() {
581
570
  return [...this.store.listActiveTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
@@ -608,12 +597,6 @@ export class FileSchedulerStoreAdapter {
608
597
  });
609
598
  });
610
599
  }
611
- rolloverTaskRoleSessionForContextBudget(input) {
612
- return this.store.transaction((reader) => rolloverTaskRoleSessionForContextBudget(reader, input.taskId, input.roleName, { peakTokens: input.peakTokens, hardTokens: input.hardTokens }, input.now));
613
- }
614
- getContextBudget() {
615
- return resolveContextBudget(this.store.getConfig().contextBudget);
616
- }
617
600
  listRoles(taskId) {
618
601
  return this.store.listRoles(taskId).map((role) => mapRole(this.store, role));
619
602
  }
@@ -646,6 +629,17 @@ export class FileSchedulerStoreAdapter {
646
629
  adapterId: effectiveSession.effective.adapterId
647
630
  };
648
631
  }
632
+ markOperatorTurnStarted(now) {
633
+ this.store.transaction((store) => {
634
+ const sessions = store.getGlobalRoleSessionSet(SYSTEM_OPERATOR_ROLE);
635
+ if (sessions === null)
636
+ return;
637
+ const active = sessions.sessions[sessions.activeAgentId];
638
+ if (active === undefined || active.status !== "ready")
639
+ return;
640
+ store.saveGlobalRoleSessionSet(updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "running", now));
641
+ });
642
+ }
649
643
  resolveExpiredInputRecommendations(now, taskIds) {
650
644
  return this.store.transaction((store) => {
651
645
  const selectedTaskIds = taskIds === undefined
@@ -725,6 +719,27 @@ export class FileSchedulerStoreAdapter {
725
719
  // the fold as present and skips the per-candidate fallback scans.
726
720
  return latestRunDurableProgressAt(view, taskId, roleName, runId, projected.runFacts.get(runId) ?? {});
727
721
  }
722
+ recordRoleRunDiagnostic(input) {
723
+ return this.store.transaction((store) => {
724
+ const task = store.getTask(input.taskId);
725
+ const run = store.getActiveAgentRun(input.taskId, input.roleName);
726
+ if (task === null || task.status !== "active"
727
+ || run === null || run.id !== input.runId || run.status !== "active") {
728
+ return "state-changed";
729
+ }
730
+ const latest = latestRunEventTime(store.listEvents(input.taskId), RUN_DIAGNOSTIC_FINISHED_EVENT, input.runId);
731
+ if (latest !== undefined && Date.parse(latest) >= Date.parse(input.startedAt)) {
732
+ return "already-recorded";
733
+ }
734
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, RUN_DIAGNOSTIC_FINISHED_EVENT, {
735
+ runId: input.runId,
736
+ roleName: input.roleName,
737
+ outcome: input.outcome,
738
+ startedAt: input.startedAt
739
+ }, input.now));
740
+ return "recorded";
741
+ });
742
+ }
728
743
  recordRoleRunStall(input) {
729
744
  return this.store.transaction((store) => {
730
745
  const task = store.getTask(input.taskId);
@@ -756,18 +771,9 @@ export class FileSchedulerStoreAdapter {
756
771
  progressAt: input.progressAt,
757
772
  idleMs: String(Math.max(0, Math.floor(input.idleMs))),
758
773
  evidenceKey: input.evidenceKey,
759
- status: "needs-attention"
774
+ status: "diagnostic-only"
760
775
  }, input.now);
761
776
  store.saveEvent(task.id, event);
762
- if (role.name === "leader") {
763
- store.saveOperatorNotification(createLeaderStallNotification(task.id, run.id, input.progressAt, input.evidenceKey, input.now, store.getOperatorNotification(task.id)));
764
- enqueueWork(store, { kind: "operator" }, "leader-run-stalled", input.now, [
765
- { type: "task", id: task.id }
766
- ]);
767
- }
768
- else {
769
- queueLeaderWakeup(store, task.id, wakeReason("role-run-stalled"), input.now);
770
- }
771
777
  return "raised";
772
778
  });
773
779
  }
@@ -795,10 +801,9 @@ export class FileSchedulerStoreAdapter {
795
801
  || (typeof event.payload.progressAt === "string"
796
802
  && Number.isFinite(Date.parse(event.payload.progressAt))
797
803
  && Date.parse(event.payload.progressAt) >= Date.parse(input.progressAt)))));
798
- const stalledAt = latestRunEventTime(events, RUN_STALLED_EVENT, run.id);
799
- const recoveredAt = latestRunEventTime(events, RUN_RECOVERED_EVENT, run.id);
800
- const recovered = stalledAt !== undefined
801
- && (recoveredAt === undefined || Date.parse(recoveredAt) <= Date.parse(stalledAt));
804
+ // Advisory 30-minute diagnostics are intentionally not lifecycle
805
+ // episodes and therefore must never synthesize run.recovered.
806
+ const recovered = isRoleRunStalled(events, run.id);
802
807
  if (!existing) {
803
808
  store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_PROGRESS_EVENT, {
804
809
  runId: run.id,
@@ -815,7 +820,6 @@ export class FileSchedulerStoreAdapter {
815
820
  progressAt: input.progressAt,
816
821
  kind: "durable-progress"
817
822
  }, input.now));
818
- clearMatchingLeaderStallAttention(store, task.id, run.id);
819
823
  }
820
824
  return existing && !recovered ? "already-recorded" : "recorded";
821
825
  });
@@ -824,11 +828,92 @@ export class FileSchedulerStoreAdapter {
824
828
  const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
825
829
  return sessions !== null && sessions.inFlight !== null;
826
830
  }
831
+ detachAgentHostProviderForConversationSwitch(input) {
832
+ return this.store.transaction((store) => {
833
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
834
+ const run = store.getAgentRun(input.taskId, input.runId);
835
+ const mailbox = store.getWorkMailbox({
836
+ kind: "role",
837
+ taskId: input.taskId,
838
+ roleName: input.roleName
839
+ });
840
+ const binding = sessions?.providerBinding;
841
+ const session = sessions?.sessions[input.agentId];
842
+ const task = store.getTask(input.taskId);
843
+ const role = store.getRole(input.taskId, input.roleName);
844
+ const activeRun = store.getActiveAgentRun(input.taskId, input.roleName);
845
+ if (task === null || task.status !== "active"
846
+ || role === null || role.activeAgentId !== input.agentId
847
+ || sessions === null || sessions === undefined
848
+ || run === null || run.status !== "active" || run.mode !== "new"
849
+ || activeRun?.id !== run.id
850
+ || run.effective.agentId !== input.agentId
851
+ || session?.nativeSessionId !== input.previousNativeSessionId
852
+ || binding === null || binding === undefined
853
+ || currentProviderConversation(binding).conversationId !== input.previousConversationId
854
+ || sessions.inFlight?.runId !== input.runId
855
+ || mailbox?.processing?.batchId !== sessions.inFlight.receiptId
856
+ || mailbox.processing.owner !== "controller"
857
+ || mailbox.processing.executionRef?.type !== "run"
858
+ || mailbox.processing.executionRef.taskId !== input.taskId
859
+ || mailbox.processing.executionRef.id !== input.runId
860
+ || mailbox.inputDelivery !== null
861
+ || pendingConversationSwitch(store.listEvents(input.taskId), input.roleName, sessions) === null
862
+ || !runtimeHookMatchesReservation(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, input.launchId)
863
+ || input.nextAuthorityHolderId !== input.launchId) {
864
+ throw new Error("Provider Conversation detachment no longer matches the exact switch Run.");
865
+ }
866
+ const target = binding.activations.find((entry) => (entry.activationId === input.previousActivationId
867
+ && entry.conversationId === input.previousConversationId));
868
+ if (target === undefined) {
869
+ throw new Error("Provider Conversation detachment targets an unknown Activation.");
870
+ }
871
+ if (target.status !== "active") {
872
+ if (binding.authority.owner !== "none"
873
+ || input.nextAuthorityEpoch !== binding.authority.epoch + 1) {
874
+ throw new Error("Provider Conversation detachment replay has a stale authority fence.");
875
+ }
876
+ return "already-detached";
877
+ }
878
+ if (conversationDetachmentBasis({
879
+ sessions,
880
+ events: store.listEvents(input.taskId),
881
+ mailbox,
882
+ roleName: input.roleName,
883
+ runId: input.runId,
884
+ runMode: run.mode
885
+ }) !== "actor-request"
886
+ || binding.authority.owner !== "controller"
887
+ || binding.authority.holderId !== target.activationId
888
+ || input.nextAuthorityEpoch !== binding.authority.epoch + 2) {
889
+ throw new Error("Provider Conversation detachment is not authorized at this boundary.");
890
+ }
891
+ const detached = endProviderActivation(binding, target.activationId, {
892
+ status: "ended",
893
+ endedAt: input.now.toISOString(),
894
+ reason: "conversation-switch"
895
+ });
896
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, detached, input.now));
897
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, CONVERSATION_SWITCH_DETACHED_EVENT, {
898
+ runId: input.runId,
899
+ roleName: input.roleName,
900
+ launchId: input.launchId,
901
+ conversationId: input.previousConversationId,
902
+ activationId: input.previousActivationId
903
+ }, input.now));
904
+ return "detached";
905
+ });
906
+ }
827
907
  beginAgentHostProviderTurn(input) {
828
908
  this.store.transaction((store) => {
829
909
  const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
830
910
  const session = sessions?.sessions[input.agentId];
831
911
  const binding = sessions?.providerBinding;
912
+ const mailbox = store.getWorkMailbox({
913
+ kind: "role",
914
+ taskId: input.taskId,
915
+ roleName: input.roleName
916
+ });
832
917
  if (sessions === null || sessions === undefined
833
918
  || binding === null || binding === undefined
834
919
  || sessions.inFlight?.runId !== input.runId
@@ -838,7 +923,8 @@ export class FileSchedulerStoreAdapter {
838
923
  || currentProviderConversation(binding).conversationId !== input.nativeSessionId
839
924
  || binding.authority.owner !== input.authorityOwner
840
925
  || binding.authority.epoch !== input.authorityEpoch
841
- || binding.authority.holderId !== input.holderId) {
926
+ || binding.authority.holderId !== input.holderId
927
+ || !processingOwnsExactInitialDelivery(mailbox, input.taskId, input.runId, input.attemptId)) {
842
928
  throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
843
929
  }
844
930
  const exactReplay = binding.turn?.attemptId === input.attemptId
@@ -863,21 +949,15 @@ export class FileSchedulerStoreAdapter {
863
949
  if (sessions === null || sessions === undefined
864
950
  || binding === null || binding === undefined
865
951
  || binding.runId !== input.runId
866
- || binding.turn?.attemptId !== input.attemptId
867
- || binding.turn.status !== "submitting") {
952
+ || binding.turn?.attemptId !== input.attemptId) {
868
953
  throw new Error("Agent Host Provider Turn submission is no longer current.");
869
954
  }
870
- const updated = input.status === "delivery-unknown"
871
- ? markProviderTurnDeliveryUnknown(binding, {
872
- attemptId: input.attemptId,
873
- observedAt: input.now.toISOString(),
874
- reason: input.reason
875
- })
876
- : rejectProviderTurn(binding, {
877
- attemptId: input.attemptId,
878
- rejectedAt: input.now.toISOString(),
879
- reason: input.reason
880
- });
955
+ const updated = settleProviderTurnSubmission(binding, {
956
+ attemptId: input.attemptId,
957
+ status: input.status,
958
+ reason: input.reason,
959
+ resolvedAt: input.now.toISOString()
960
+ });
881
961
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
882
962
  });
883
963
  }
@@ -900,68 +980,6 @@ export class FileSchedulerStoreAdapter {
900
980
  ...binding.authority
901
981
  };
902
982
  }
903
- beginRoleRunProviderTurn(input) {
904
- return this.store.transaction((store) => {
905
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
906
- const session = sessions?.sessions[input.agentId];
907
- const binding = sessions?.providerBinding;
908
- if (sessions === null || sessions === undefined
909
- || binding === null || binding === undefined
910
- || sessions.inFlight?.runId !== input.runId
911
- || binding.runId !== input.runId
912
- || session?.launchId !== input.launchId
913
- || session.nativeSessionId !== input.nativeSessionId
914
- || currentProviderConversation(binding).conversationId !== input.nativeSessionId
915
- || binding.authority.owner !== "controller")
916
- return false;
917
- store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
918
- attemptId: input.attemptId,
919
- authorityEpoch: binding.authority.epoch,
920
- submittedAt: input.now.toISOString()
921
- }), input.now));
922
- return true;
923
- });
924
- }
925
- resolveRoleRunProviderSubmission(input) {
926
- return this.store.transaction((store) => {
927
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
928
- const binding = sessions?.providerBinding;
929
- if (sessions === null || sessions === undefined
930
- || binding === null || binding === undefined
931
- || binding.runId !== input.runId
932
- || binding.turn?.attemptId !== input.attemptId
933
- || binding.turn.status !== "submitting")
934
- return false;
935
- const updated = input.status === "delivery-unknown"
936
- ? markProviderTurnDeliveryUnknown(binding, {
937
- attemptId: input.attemptId,
938
- observedAt: input.now.toISOString(),
939
- reason: input.reason
940
- })
941
- : rejectProviderTurn(binding, {
942
- attemptId: input.attemptId,
943
- rejectedAt: input.now.toISOString(),
944
- reason: input.reason
945
- });
946
- store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
947
- return true;
948
- });
949
- }
950
- isRoleGenerationProviderReady(input) {
951
- // A session.ready observation for this exact generation is the durable
952
- // pre-input fence. This reads folded Driver truth
953
- // only — no liveness, screen, or pane/PID inference. Served from the
954
- // revision projection: a stale miss only repeats an idempotent first push.
955
- return this.listEvents(input.taskId).some((event) => {
956
- const observation = runtimeObservationFromTaskEvent(event);
957
- return observation?.kind === "session.ready"
958
- && observation.fence.roleName === input.roleName
959
- && observation.fence.agentId === input.agentId
960
- && (input.launchId === undefined || observation.fence.launchId === input.launchId)
961
- && (input.nativeSessionId === undefined
962
- || observation.fence.nativeSessionId === input.nativeSessionId);
963
- });
964
- }
965
983
  peekNextAgentRunId(taskId) {
966
984
  return this.store.peekNextAgentRunId(taskId);
967
985
  }
@@ -1161,17 +1179,11 @@ export class FileSchedulerStoreAdapter {
1161
1179
  const unknown = markMailboxInputDeliveryUnknown(mailbox, attemptId, reason, now);
1162
1180
  markSubmittingProviderTurnUnknown(store, target, mailbox.inputDelivery, now, reason);
1163
1181
  store.saveWorkMailbox(unknown);
1164
- const taskId = target.kind === "role" ? target.taskId : undefined;
1165
- if (taskId !== undefined) {
1166
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "run.input-delivery-unknown", { attemptId, reason }, now));
1167
- enqueueWork(store, { kind: "operator" }, "run-input-delivery-unknown", now, [
1168
- unknown.inputDelivery.executionRef
1169
- ], {
1170
- source: "input-router",
1171
- dedupeKey: `input-delivery-unknown:${attemptId}`,
1172
- deliveryMode: "followup",
1173
- lane: "normal"
1174
- });
1182
+ if (target.kind === "role") {
1183
+ const taskId = target.taskId;
1184
+ const event = createTaskEvent(store.nextEventId(taskId), taskId, "run.input-delivery-unknown", { attemptId, reason }, now);
1185
+ store.saveEvent(taskId, event);
1186
+ routeRoleEvent(store, event, target.roleName, "run-input-delivery-unknown", now);
1175
1187
  }
1176
1188
  return true;
1177
1189
  });
@@ -1228,12 +1240,6 @@ export class FileSchedulerStoreAdapter {
1228
1240
  }
1229
1241
  const owner = runtimeOwnerFromTarget(target);
1230
1242
  markRuntimeOwnerSessionStopped(store, owner, now);
1231
- if (owner.scope === "task") {
1232
- const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
1233
- if (sessions !== null && sessions.providerBinding !== null) {
1234
- store.saveTaskRoleSessionSet(clearTaskRoleProviderRuntimeForCleanup(sessions, now));
1235
- }
1236
- }
1237
1243
  saveRuntimeLifecycleMailbox(store, mailbox);
1238
1244
  return true;
1239
1245
  });
@@ -1298,7 +1304,6 @@ export class FileSchedulerStoreAdapter {
1298
1304
  }
1299
1305
  clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
1300
1306
  getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
1301
- getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
1302
1307
  saveLeaderDispatch(input) {
1303
1308
  return this.store.transaction((store) => {
1304
1309
  const task = store.getTask(input.task.id);
@@ -1371,14 +1376,15 @@ export class FileSchedulerStoreAdapter {
1371
1376
  now: input.now
1372
1377
  }));
1373
1378
  }
1374
- if (input.session !== null && input.session.nativeSessionId !== undefined) {
1379
+ if (input.run.mode !== "new"
1380
+ && input.session !== null
1381
+ && input.session.nativeSessionId !== undefined) {
1375
1382
  saveTaskSession(store, role, {
1376
1383
  ...input.session,
1377
1384
  nativeSessionId: input.session.nativeSessionId
1378
1385
  }, "running", input.now);
1379
1386
  }
1380
1387
  store.clearLeaderFailure(input.task.id);
1381
- store.clearOperatorNotification(input.task.id);
1382
1388
  return "claimed";
1383
1389
  });
1384
1390
  }
@@ -1469,9 +1475,13 @@ export class FileSchedulerStoreAdapter {
1469
1475
  if (input.session !== null && input.session.nativeSessionId !== undefined) {
1470
1476
  const existing = store.getRoleSession(input.task.id, input.role.name);
1471
1477
  matchingPreparedRuntimeReservation(store, input);
1472
- if (existing?.nativeSessionId !== input.session.nativeSessionId
1473
- || (input.launchId !== undefined && existing.launchId !== input.launchId)
1474
- || existing.status !== "running") {
1478
+ const defersConversationReplacement = active.mode === "new"
1479
+ && existing !== null
1480
+ && existing.nativeSessionId !== input.session.nativeSessionId;
1481
+ if (!defersConversationReplacement
1482
+ && (existing?.nativeSessionId !== input.session.nativeSessionId
1483
+ || (input.launchId !== undefined && existing.launchId !== input.launchId)
1484
+ || existing.status !== "running")) {
1475
1485
  saveTaskSession(store, role, {
1476
1486
  ...input.session,
1477
1487
  nativeSessionId: input.session.nativeSessionId
@@ -1515,7 +1525,8 @@ export class FileSchedulerStoreAdapter {
1515
1525
  || !preparedReservationMatches(runtimeMailbox, input.taskId, input.runId, input.launchId)) {
1516
1526
  return "state-changed";
1517
1527
  }
1518
- const summary = `Role delivery retry limit exhausted before exact Run input delivery: ${input.runId}.`;
1528
+ const summary = input.summary
1529
+ ?? `Role delivery retry limit exhausted before exact Run input delivery: ${input.runId}.`;
1519
1530
  const result = terminalizeExactTaskRun(store, {
1520
1531
  taskId: input.taskId,
1521
1532
  roleName: input.roleName,
@@ -1531,17 +1542,20 @@ export class FileSchedulerStoreAdapter {
1531
1542
  if (result.disposition !== "applied" || result.run === null) {
1532
1543
  return "state-changed";
1533
1544
  }
1534
- enqueueWork(store, runtimeLifecycleTarget({
1535
- scope: "task",
1536
- taskId: input.taskId,
1537
- roleName: input.roleName
1538
- }), RUNTIME_CLEANUP_REQUIRED_REASON, input.now, [{ type: "task", id: input.taskId }]);
1545
+ if (input.cleanupRequired !== false) {
1546
+ enqueueWork(store, runtimeLifecycleTarget({
1547
+ scope: "task",
1548
+ taskId: input.taskId,
1549
+ roleName: input.roleName
1550
+ }), RUNTIME_CLEANUP_REQUIRED_REASON, input.now, [{ type: "task", id: input.taskId }]);
1551
+ }
1539
1552
  const terminal = result.run;
1540
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.role-delivery-failed", {
1553
+ const deliveryFailureEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.role-delivery-failed", {
1541
1554
  runId: terminal.id,
1542
1555
  roleName: input.roleName,
1543
1556
  outcome: terminal.status
1544
- }, input.now));
1557
+ }, input.now);
1558
+ store.saveEvent(input.taskId, deliveryFailureEvent);
1545
1559
  if (terminal.purpose === "execution" && terminal.workItemId !== undefined) {
1546
1560
  const item = store.getWorkItem(input.taskId, terminal.workItemId);
1547
1561
  if (item !== null && ![
@@ -1553,12 +1567,7 @@ export class FileSchedulerStoreAdapter {
1553
1567
  }
1554
1568
  }
1555
1569
  if (input.roleName !== "leader") {
1556
- enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, terminal.purpose === "review" ? "review-failed" : "role-run-failed", input.now, [
1557
- { type: "run", taskId: input.taskId, id: terminal.id },
1558
- ...(terminal.workItemId === undefined
1559
- ? []
1560
- : [{ type: "work-item", taskId: input.taskId, id: terminal.workItemId }])
1561
- ]);
1570
+ routeRoleEvent(store, deliveryFailureEvent, input.roleName, terminal.purpose === "review" ? "review-failed" : "role-run-failed", input.now);
1562
1571
  }
1563
1572
  else {
1564
1573
  const failedRole = store.getRole(input.taskId, input.roleName);
@@ -1566,11 +1575,7 @@ export class FileSchedulerStoreAdapter {
1566
1575
  store.saveRole(input.taskId, updateRoleStatus(failedRole, "failed", input.now));
1567
1576
  }
1568
1577
  store.saveLeaderFailure(recordLeaderFailure(input.taskId, session?.nativeSessionId ?? "(unregistered)", summary, input.now, store.getLeaderFailure(input.taskId)));
1569
- store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, summary, input.now, store.getOperatorNotification(input.taskId)));
1570
- enqueueWork(store, { kind: "operator" }, "leader-run-failed", input.now, [
1571
- { type: "task", id: input.taskId },
1572
- { type: "run", taskId: input.taskId, id: terminal.id }
1573
- ]);
1578
+ routeRoleEvent(store, deliveryFailureEvent, input.roleName, "leader-run-failed", input.now);
1574
1579
  }
1575
1580
  return "failed";
1576
1581
  });
@@ -1606,10 +1611,15 @@ export class FileSchedulerStoreAdapter {
1606
1611
  breakTaskSessionIfPresent(store, input.task.id, role.name, active.effective.agentId, input.now);
1607
1612
  const failure = recordLeaderFailure(input.task.id, input.failure.nativeSessionId, input.failure.message, input.now, store.getLeaderFailure(input.task.id));
1608
1613
  store.saveLeaderFailure(failure);
1609
- store.saveOperatorNotification(createLeaderRecoveryNotification(input.task.id, input.notification.message, input.now, store.getOperatorNotification(input.task.id)));
1610
- enqueueWork(store, { kind: "operator" }, "leader-recovery-failed", input.now, [
1611
- { type: "task", id: input.task.id }
1612
- ]);
1614
+ recordLeaderAttentionRequired(store, {
1615
+ taskId: input.task.id,
1616
+ reason: "leader-recovery-failed",
1617
+ payload: {
1618
+ message: input.failure.message,
1619
+ runId: active.id
1620
+ },
1621
+ now: input.now
1622
+ });
1613
1623
  return "failed";
1614
1624
  });
1615
1625
  }
@@ -1661,7 +1671,6 @@ export class FileSchedulerStoreAdapter {
1661
1671
  }, input.now);
1662
1672
  if (terminal.disposition !== "applied")
1663
1673
  return "state-changed";
1664
- clearMatchingLeaderStallAttention(store, task.id, currentRun.id);
1665
1674
  store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
1666
1675
  stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
1667
1676
  queueLeaderWakeup(store, task.id, wakeReason("review-failed"), input.now);
@@ -1679,7 +1688,6 @@ export class FileSchedulerStoreAdapter {
1679
1688
  }
1680
1689
  }
1681
1690
  store.saveAgentRun(failAgentRun(currentRun, input.summary, input.now));
1682
- clearMatchingLeaderStallAttention(store, task.id, currentRun.id);
1683
1691
  store.clearActiveAgentRun(task.id, role.name);
1684
1692
  clearTaskRoleRunInFlight(store, role, currentRun, input.now);
1685
1693
  if (mailbox !== null && processing !== null && processing !== undefined) {
@@ -1945,6 +1953,7 @@ export class FileSchedulerStoreAdapter {
1945
1953
  const role = requireRole(store, input.taskId, input.roleName);
1946
1954
  let sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName)
1947
1955
  ?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
1956
+ const canonicalTurnId = canonicalStructuredProviderTurnId(sessions, input.turnId, input.attemptId);
1948
1957
  const existing = sessions.sessions[input.agentId];
1949
1958
  const owner = {
1950
1959
  scope: "task",
@@ -1961,7 +1970,7 @@ export class FileSchedulerStoreAdapter {
1961
1970
  throw new Error("Runtime turn completion does not match the effective launch identity.");
1962
1971
  }
1963
1972
  if (effectiveExisting !== undefined
1964
- && hasRecentTurnId(effectiveExisting.recentCompletedTurnIds, input.turnId)) {
1973
+ && hasRecentTurnId(effectiveExisting.recentCompletedTurnIds, canonicalTurnId)) {
1965
1974
  return { session: effectiveExisting, duplicate: true };
1966
1975
  }
1967
1976
  if (sessions.inFlight === null
@@ -1998,9 +2007,9 @@ export class FileSchedulerStoreAdapter {
1998
2007
  : sessions.inFlight === null ? idleStatus : "running",
1999
2008
  effective
2000
2009
  }, now);
2001
- sessions = settleStructuredProviderTurn(sessions, input.turnId, input.providerStatus ?? "completed", now);
2010
+ sessions = settleStructuredProviderTurn(sessions, canonicalTurnId, input.providerStatus ?? "completed", now);
2002
2011
  if (sessions.inFlight === null) {
2003
- sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, input.turnId, now);
2012
+ sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, canonicalTurnId, now);
2004
2013
  store.saveTaskRoleSessionSet(sessions);
2005
2014
  completeRuntimeHookReservation(store, owner, input.launchId);
2006
2015
  // The in-flight fence was cleared by an in-place provider retry
@@ -2024,7 +2033,7 @@ export class FileSchedulerStoreAdapter {
2024
2033
  sessions = recordTaskRoleTurnBoundary(sessions, {
2025
2034
  agentId: input.agentId,
2026
2035
  nativeSessionId: input.nativeSessionId,
2027
- turnId: input.turnId
2036
+ turnId: canonicalTurnId
2028
2037
  }, now);
2029
2038
  store.saveTaskRoleSessionSet(sessions);
2030
2039
  completeRuntimeHookReservation(store, owner, input.launchId);
@@ -2109,27 +2118,16 @@ export class FileSchedulerStoreAdapter {
2109
2118
  if (role !== null) {
2110
2119
  store.saveRole(input.taskId, updateRoleStatus(role, input.roleName === "leader" ? "failed" : "idle", now));
2111
2120
  }
2112
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.turn-failed", {
2121
+ const failureEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.turn-failed", {
2113
2122
  eventId: input.eventId,
2114
2123
  runId: input.runId,
2115
2124
  roleName: input.roleName,
2116
2125
  outcome: terminal.status
2117
- }, now));
2118
- if (input.roleName !== "leader") {
2119
- enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, before.purpose === "review" ? "review-failed" : "role-run-failed", now, [
2120
- { type: "run", taskId: input.taskId, id: terminal.id },
2121
- ...(terminal.workItemId === undefined
2122
- ? []
2123
- : [{ type: "work-item", taskId: input.taskId, id: terminal.workItemId }])
2124
- ]);
2125
- }
2126
- else {
2127
- store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, summary, now, store.getOperatorNotification(input.taskId)));
2128
- enqueueWork(store, { kind: "operator" }, "leader-run-failed", now, [
2129
- { type: "task", id: input.taskId },
2130
- { type: "run", taskId: input.taskId, id: terminal.id }
2131
- ]);
2132
- }
2126
+ }, now);
2127
+ store.saveEvent(input.taskId, failureEvent);
2128
+ routeRoleEvent(store, failureEvent, input.roleName, input.roleName !== "leader" && before.purpose === "review"
2129
+ ? "review-failed"
2130
+ : input.roleName === "leader" ? "leader-run-failed" : "role-run-failed", now);
2133
2131
  return { disposition: "applied", runId: input.runId };
2134
2132
  });
2135
2133
  }
@@ -2222,14 +2220,12 @@ export class FileSchedulerStoreAdapter {
2222
2220
  if (blocked.outcome === "exhausted")
2223
2221
  return null;
2224
2222
  store.saveAgentRun(withProviderRetry(run, blocked.retry));
2225
- this.recordProviderRetryClassified(store, input, classification.errorClass, {
2223
+ const classifiedEvent = this.recordProviderRetryClassified(store, input, classification.errorClass, {
2226
2224
  wouldRetry: "false",
2227
2225
  shadow: "false",
2228
2226
  note: "delivery-deduplication-unproven"
2229
2227
  }, now);
2230
- enqueueWork(store, input.roleName === "leader"
2231
- ? { kind: "operator" }
2232
- : { kind: "role", taskId: input.taskId, roleName: "leader" }, "provider-delivery-uncertain", now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
2228
+ routeRoleEvent(store, classifiedEvent, input.roleName, "provider-delivery-uncertain", now);
2233
2229
  return { disposition: "applied", runId: input.runId };
2234
2230
  }
2235
2231
  }
@@ -2314,22 +2310,14 @@ export class FileSchedulerStoreAdapter {
2314
2310
  if (retryDecision.outcome === "exhausted")
2315
2311
  return null;
2316
2312
  store.saveAgentRun(withProviderRetry(run, retryDecision.retry));
2317
- this.recordProviderRetryClassified(store, input, "policy-denied", {
2313
+ const classifiedEvent = this.recordProviderRetryClassified(store, input, "policy-denied", {
2318
2314
  wouldRetry: "false",
2319
2315
  shadow: "false",
2320
2316
  note: "policy-denied-blocker"
2321
2317
  }, now);
2322
- if (input.roleName !== "leader") {
2323
- enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, "provider-policy-blocked", now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
2324
- }
2325
- else {
2326
- const message = `Leader Run ${input.runId} is blocked by Provider policy: ${summary}`;
2327
- store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, now, store.getOperatorNotification(input.taskId)));
2328
- enqueueWork(store, { kind: "operator" }, "leader-provider-policy-blocked", now, [
2329
- { type: "task", id: input.taskId },
2330
- { type: "run", taskId: input.taskId, id: input.runId }
2331
- ]);
2332
- }
2318
+ routeRoleEvent(store, classifiedEvent, input.roleName, input.roleName === "leader"
2319
+ ? "leader-provider-policy-blocked"
2320
+ : "provider-policy-blocked", now);
2333
2321
  return { disposition: "applied", runId: input.runId };
2334
2322
  }
2335
2323
  /**
@@ -2355,7 +2343,7 @@ export class FileSchedulerStoreAdapter {
2355
2343
  return null;
2356
2344
  store.saveAgentRun(withProviderRetry(run, decision.retry));
2357
2345
  const driver = this.drivers.requireByAdapterId(input.adapterId);
2358
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.context-capacity-failure", {
2346
+ const capacityEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.context-capacity-failure", {
2359
2347
  eventId: input.eventId,
2360
2348
  runId: input.runId,
2361
2349
  roleName: input.roleName,
@@ -2366,15 +2354,14 @@ export class FileSchedulerStoreAdapter {
2366
2354
  action: driver.capabilities.lifecycle.compaction === "native-explicit"
2367
2355
  ? "native-explicit-compaction-required"
2368
2356
  : "await-provider-native-recovery"
2369
- }, now));
2357
+ }, now);
2358
+ store.saveEvent(input.taskId, capacityEvent);
2370
2359
  this.recordProviderRetryClassified(store, input, "context-capacity", {
2371
2360
  wouldRetry: "false",
2372
2361
  shadow: "false",
2373
2362
  note: "provider-native-capacity-recovery-only"
2374
2363
  }, now);
2375
- enqueueWork(store, input.roleName === "leader"
2376
- ? { kind: "operator" }
2377
- : { kind: "role", taskId: input.taskId, roleName: "leader" }, "provider-context-capacity-recovery-required", now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
2364
+ routeRoleEvent(store, capacityEvent, input.roleName, "provider-context-capacity-recovery-required", now);
2378
2365
  return { disposition: "applied", runId: input.runId };
2379
2366
  }
2380
2367
  applyNativeSessionRecoverabilityBlock(store, input, summary, now) {
@@ -2393,27 +2380,28 @@ export class FileSchedulerStoreAdapter {
2393
2380
  if (decision.outcome === "exhausted")
2394
2381
  return null;
2395
2382
  store.saveAgentRun(withProviderRetry(run, decision.retry));
2396
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.native-session-recoverability-unproven", {
2383
+ const recoverabilityEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.native-session-recoverability-unproven", {
2397
2384
  eventId: input.eventId,
2398
2385
  runId: input.runId,
2399
2386
  roleName: input.roleName,
2400
2387
  launchId: input.launchId,
2401
2388
  nativeSessionId: input.nativeSessionId,
2402
2389
  requiredEvidence: "exact-native-resume-outcome"
2403
- }, now));
2404
- enqueueWork(store, input.roleName === "leader"
2405
- ? { kind: "operator" }
2406
- : { kind: "role", taskId: input.taskId, roleName: "leader" }, "native-session-recoverability-unproven", now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
2390
+ }, now);
2391
+ store.saveEvent(input.taskId, recoverabilityEvent);
2392
+ routeRoleEvent(store, recoverabilityEvent, input.roleName, "native-session-recoverability-unproven", now);
2407
2393
  return { disposition: "applied", runId: input.runId };
2408
2394
  }
2409
2395
  recordProviderRetryClassified(store, input, errorClass, extra, now) {
2410
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-retry-classified", {
2396
+ const event = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-retry-classified", {
2411
2397
  eventId: input.eventId,
2412
2398
  runId: input.runId,
2413
2399
  roleName: input.roleName,
2414
2400
  errorClass,
2415
2401
  ...extra
2416
- }, now));
2402
+ }, now);
2403
+ store.saveEvent(input.taskId, event);
2404
+ return event;
2417
2405
  }
2418
2406
  /**
2419
2407
  * Issue 04 durable retry timer: lists active Runs whose in-place retry is
@@ -2474,7 +2462,7 @@ export class FileSchedulerStoreAdapter {
2474
2462
  /**
2475
2463
  * Issue 04: reopens each due retry on its original Native Session. A Run
2476
2464
  * whose Session is proven dead terminalizes with an exact replacement
2477
- * blocker; a live Session is reset for the existing delivery path, which
2465
+ * blocker; a live Session is reopened for the existing delivery path, which
2478
2466
  * re-pushes the exact same input in the same pass.
2479
2467
  */
2480
2468
  resolveDueProviderRetries(now, taskIds) {
@@ -2507,9 +2495,10 @@ export class FileSchedulerStoreAdapter {
2507
2495
  || !providerRetryIsDue(run.providerRetry, now)) {
2508
2496
  return;
2509
2497
  }
2510
- let sessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
2498
+ const sessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
2511
2499
  const providerBinding = sessions?.providerBinding;
2512
2500
  let recoveryMode = "resume";
2501
+ let replacementRequired = false;
2513
2502
  if (providerBinding === null || providerBinding === undefined) {
2514
2503
  deferUnprovenProviderRecovery(store, run, entry.roleName, "Provider Conversation identity is missing; automatic recovery is fenced.", now);
2515
2504
  return;
@@ -2554,13 +2543,7 @@ export class FileSchedulerStoreAdapter {
2554
2543
  deferUnprovenProviderRecovery(store, run, entry.roleName, "Provider replacement lacks an exact missing-Conversation observation.", now);
2555
2544
  return;
2556
2545
  }
2557
- recoveryMode = "new";
2558
- const currentSession = sessions?.sessions[run.effective.agentId];
2559
- if (sessions !== null && currentSession !== undefined
2560
- && currentSession.status !== "broken") {
2561
- sessions = updateRoleAgentSessionStatus(sessions, run.effective.agentId, "broken", now);
2562
- store.saveTaskRoleSessionSet(sessions);
2563
- }
2546
+ replacementRequired = true;
2564
2547
  }
2565
2548
  const session = sessions?.sessions[run.effective.agentId];
2566
2549
  const identityMatches = session !== undefined
@@ -2580,21 +2563,25 @@ export class FileSchedulerStoreAdapter {
2580
2563
  return;
2581
2564
  }
2582
2565
  store.saveAgentRun(withProviderRetry(run, deferred));
2583
- if (!store.listEvents(entry.taskId).some((event) => (event.type === "runtime.provider-retry-native-resume-unproven"
2584
- && event.payload.runId === run.id))) {
2585
- store.saveEvent(entry.taskId, createTaskEvent(store.nextEventId(entry.taskId), entry.taskId, "runtime.provider-retry-native-resume-unproven", {
2566
+ let attentionEvent = store.listEvents(entry.taskId).find((event) => (event.type === "runtime.provider-retry-native-resume-unproven"
2567
+ && event.payload.runId === run.id));
2568
+ if (attentionEvent === undefined) {
2569
+ attentionEvent = createTaskEvent(store.nextEventId(entry.taskId), entry.taskId, "runtime.provider-retry-native-resume-unproven", {
2586
2570
  runId: run.id,
2587
2571
  consecutiveFailures: String(run.providerRetry.consecutiveFailures),
2588
2572
  dispatchedRetries: String(run.providerRetry.dispatchedRetries)
2589
- }, now));
2573
+ }, now);
2574
+ store.saveEvent(entry.taskId, attentionEvent);
2590
2575
  }
2591
- enqueueWork(store, { kind: "operator" }, "provider-retry-native-resume-unproven", now, [
2592
- { type: "task", id: entry.taskId },
2593
- { type: "run", taskId: entry.taskId, id: run.id }
2594
- ]);
2576
+ routeRoleEvent(store, attentionEvent, entry.roleName, "provider-retry-native-resume-unproven", now);
2595
2577
  return;
2596
2578
  }
2597
- if (hasUnsettledRuntimeOperations(store, entry.taskId, run.id)) {
2579
+ if (runHasActiveRuntimeOperations(store.listEvents(entry.taskId), {
2580
+ taskId: entry.taskId,
2581
+ roleName: entry.roleName,
2582
+ runId: run.id,
2583
+ agentId: run.effective.agentId
2584
+ })) {
2598
2585
  const deferred = deferProviderRetry(run.providerRetry, now);
2599
2586
  if (deferred === null) {
2600
2587
  finalizeProviderRetryDeadline(store, run, "Provider retry stopped because tool/subagent outcome remained uncertain through the bounded episode deadline.", "operation-outcome-uncertain", now);
@@ -2603,6 +2590,21 @@ export class FileSchedulerStoreAdapter {
2603
2590
  store.saveAgentRun(withProviderRetry(run, deferred));
2604
2591
  return;
2605
2592
  }
2593
+ if (replacementRequired) {
2594
+ try {
2595
+ recoveryMode = roleSessionDispatchModeWithConversationSwitch(sessions, store.listEvents(entry.taskId), roleMailbox, entry.roleName, run.effective.agentId, run.effective);
2596
+ }
2597
+ catch (error) {
2598
+ deferUnprovenProviderRecovery(store, run, entry.roleName, error instanceof Error
2599
+ ? error.message
2600
+ : "Independent Conversation-switch evaluation failed closed.", now);
2601
+ return;
2602
+ }
2603
+ if (recoveryMode !== "new") {
2604
+ deferUnprovenProviderRecovery(store, run, entry.roleName, "Independent Conversation-switch evaluation kept the current Conversation.", now);
2605
+ return;
2606
+ }
2607
+ }
2606
2608
  // Reopen the Run on its original Session: reset transport markers,
2607
2609
  // switch to resume, and mark the projection in-flight. The delivery
2608
2610
  // pass re-pushes the exact same input in this same pass.
@@ -2712,7 +2714,30 @@ export class FileSchedulerStoreAdapter {
2712
2714
  recordCanonicalObservationObsolete(store, input, "bind-state-missing", now);
2713
2715
  return "obsolete";
2714
2716
  }
2715
- const bound = recordRoleAgentSession(sessions, {
2717
+ const mailbox = store.getWorkMailbox({
2718
+ kind: "role",
2719
+ taskId,
2720
+ roleName: input.fence.roleName
2721
+ });
2722
+ const existingNativeSessionId = sessions.sessions[input.fence.agentId]
2723
+ ?.nativeSessionId;
2724
+ const replacingNativeSession = existingNativeSessionId !== undefined
2725
+ && existingNativeSessionId !== decision.outcome.nativeSessionId;
2726
+ const replacementBasis = replacingNativeSession
2727
+ ? conversationReplacementBasis({
2728
+ sessions,
2729
+ events: store.listEvents(taskId),
2730
+ mailbox,
2731
+ roleName: input.fence.roleName,
2732
+ runId: run.id,
2733
+ runMode: run.mode
2734
+ })
2735
+ : null;
2736
+ if (replacingNativeSession && replacementBasis === null) {
2737
+ recordCanonicalObservationObsolete(store, input, "conversation-switch-not-authorized", now);
2738
+ return "obsolete";
2739
+ }
2740
+ const sessionInput = {
2716
2741
  agentId: input.fence.agentId,
2717
2742
  adapterId,
2718
2743
  nativeSessionId: decision.outcome.nativeSessionId,
@@ -2720,9 +2745,20 @@ export class FileSchedulerStoreAdapter {
2720
2745
  policy: "fixed",
2721
2746
  status: "running",
2722
2747
  effective: run.effective
2723
- }, now);
2724
- const withProvider = bindOrSupersedeProviderRuntime(bound, input, now);
2748
+ };
2749
+ const bound = replacementBasis === null
2750
+ ? recordRoleAgentSession(sessions, sessionInput, now)
2751
+ : replaceTaskRoleAgentSessionForConversationSwitch(sessions, sessionInput, now);
2752
+ const withProvider = bindOrSupersedeProviderRuntime(bound, input, now, replacementBasis ?? undefined);
2725
2753
  store.saveTaskRoleSessionSet(withProvider);
2754
+ if (replacementBasis === "actor-request") {
2755
+ resolveConversationSwitchRequest(store, taskId, input.fence.roleName, sessions, "applied", now, {
2756
+ conversationId: input.fence.conversationId
2757
+ ?? decision.outcome.nativeSessionId,
2758
+ activationId: input.fence.activationId ?? input.fence.launchId,
2759
+ launchId: input.fence.launchId
2760
+ });
2761
+ }
2726
2762
  const driver = this.drivers.require(input.fence.driverId);
2727
2763
  if (driver.capabilities.observation.sessionBootstrap === "discovered"
2728
2764
  && run.pushedAt === undefined) {
@@ -2943,7 +2979,6 @@ export class FileSchedulerStoreAdapter {
2943
2979
  return null;
2944
2980
  const sessionSet = store.getTaskRoleSessionSet(fence.taskId, fence.roleName);
2945
2981
  const session = sessionSet?.sessions[fence.agentId] ?? null;
2946
- const boundNativeSessionId = session?.nativeSessionId;
2947
2982
  if (runId === undefined) {
2948
2983
  return {
2949
2984
  fence,
@@ -2952,7 +2987,9 @@ export class FileSchedulerStoreAdapter {
2952
2987
  pushed: false,
2953
2988
  accepted: false,
2954
2989
  terminal: false,
2955
- ...(boundNativeSessionId === undefined ? {} : { boundNativeSessionId })
2990
+ ...(session?.nativeSessionId === undefined
2991
+ ? {}
2992
+ : { boundNativeSessionId: session.nativeSessionId })
2956
2993
  };
2957
2994
  }
2958
2995
  const run = store.getAgentRun(fence.taskId, runId);
@@ -2964,8 +3001,15 @@ export class FileSchedulerStoreAdapter {
2964
3001
  || inFlight.agentId !== run.effective.agentId)
2965
3002
  return null;
2966
3003
  const owner = { scope: "task", taskId: fence.taskId, roleName: fence.roleName };
2967
- const launchId = session?.launchId
2968
- ?? (runtimeHookMatchesReservation(store, owner, fence.launchId) ? fence.launchId : undefined);
3004
+ const freshConversationLaunch = run.mode === "new"
3005
+ && runtimeHookMatchesReservation(store, owner, fence.launchId);
3006
+ const boundNativeSessionId = freshConversationLaunch
3007
+ ? undefined
3008
+ : session?.nativeSessionId;
3009
+ const launchId = freshConversationLaunch
3010
+ ? fence.launchId
3011
+ : session?.launchId
3012
+ ?? (runtimeHookMatchesReservation(store, owner, fence.launchId) ? fence.launchId : undefined);
2969
3013
  if (launchId === undefined)
2970
3014
  return null;
2971
3015
  const expectedFence = {
@@ -3027,7 +3071,8 @@ export class FileSchedulerStoreAdapter {
3027
3071
  && !runtimeHookMatchesReservation(store, owner, input.launchId)) {
3028
3072
  throw new Error("Runtime turn completion does not match the global launch reservation.");
3029
3073
  }
3030
- const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
3074
+ const nativeSessionId = globalCompletionNativeSessionId(existing, input);
3075
+ const effectiveExisting = nativeTransitionExisting(store, owner, existing, nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
3031
3076
  const effective = globalSessionEffective(role, effectiveExisting);
3032
3077
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
3033
3078
  throw new Error("Runtime turn completion does not match the effective global launch identity.");
@@ -3039,7 +3084,7 @@ export class FileSchedulerStoreAdapter {
3039
3084
  current = recordRoleAgentSession(current, {
3040
3085
  agentId: input.agentId,
3041
3086
  adapterId: input.adapterId,
3042
- nativeSessionId: input.nativeSessionId,
3087
+ nativeSessionId,
3043
3088
  ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
3044
3089
  title: effectiveExisting?.title ?? input.title,
3045
3090
  preview: effectiveExisting?.preview ?? (input.summary === undefined ? undefined : sessionPreview(input.summary)),
@@ -3047,7 +3092,7 @@ export class FileSchedulerStoreAdapter {
3047
3092
  status: completedStatus,
3048
3093
  effective
3049
3094
  }, now);
3050
- current = rememberRoleAgentCompletedTurn(current, input.agentId, input.nativeSessionId, input.turnId, now);
3095
+ current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.turnId, now);
3051
3096
  store.saveGlobalRoleSessionSet(current);
3052
3097
  completeRuntimeHookReservation(store, owner, input.launchId);
3053
3098
  return current.sessions[input.agentId];
@@ -3066,9 +3111,10 @@ export class FileSchedulerStoreAdapter {
3066
3111
  if (existing === undefined
3067
3112
  && !runtimeHookMatchesReservation(this.store, owner, input.launchId))
3068
3113
  return "obsolete";
3114
+ const nativeSessionId = globalCompletionNativeSessionId(existing, input);
3069
3115
  let effectiveExisting;
3070
3116
  try {
3071
- effectiveExisting = nativeTransitionExisting(this.store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
3117
+ effectiveExisting = nativeTransitionExisting(this.store, owner, existing, nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
3072
3118
  }
3073
3119
  catch {
3074
3120
  return "obsolete";
@@ -3080,14 +3126,6 @@ export class FileSchedulerStoreAdapter {
3080
3126
  return "apply";
3081
3127
  }
3082
3128
  }
3083
- function hasUnsettledRuntimeOperations(store, taskId, runId) {
3084
- return store.listEvents(taskId).some((event) => {
3085
- const observation = runtimeObservationFromTaskEvent(event);
3086
- return observation !== null
3087
- && observation.fence.runId === runId
3088
- && observation.kind === "operation.started";
3089
- });
3090
- }
3091
3129
  function runtimeTurnFailureSummary(input) {
3092
3130
  return [
3093
3131
  "Agent runtime turn failed.",
@@ -3313,9 +3351,12 @@ function markRuntimeOwnerSessionStopped(store, owner, now) {
3313
3351
  if (sessions === null)
3314
3352
  return false;
3315
3353
  const active = sessions.sessions[sessions.activeAgentId];
3316
- if (active === undefined || active.status === "stopped")
3354
+ if (active === undefined)
3317
3355
  return false;
3318
- store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now));
3356
+ const stopped = stopTaskRoleRuntimeAfterPhysicalExit(sessions, now);
3357
+ if (stopped === sessions)
3358
+ return false;
3359
+ store.saveTaskRoleSessionSet(stopped);
3319
3360
  return true;
3320
3361
  }
3321
3362
  const sessions = store.getGlobalRoleSessionSet(owner.roleName);
@@ -3369,7 +3410,7 @@ function finalizeProviderRetryDeadline(store, run, summary, reason, now) {
3369
3410
  if (terminalization.disposition !== "applied" || terminalization.run === null)
3370
3411
  return false;
3371
3412
  const terminal = terminalization.run;
3372
- store.saveEvent(run.taskId, createTaskEvent(store.nextEventId(run.taskId), run.taskId, "runtime.provider-retry-exhausted", {
3413
+ const exhaustionEvent = createTaskEvent(store.nextEventId(run.taskId), run.taskId, "runtime.provider-retry-exhausted", {
3373
3414
  runId: run.id,
3374
3415
  roleName: run.roleName,
3375
3416
  episodeId: retry.episodeId,
@@ -3377,7 +3418,8 @@ function finalizeProviderRetryDeadline(store, run, summary, reason, now) {
3377
3418
  consecutiveFailures: String(retry.consecutiveFailures),
3378
3419
  dispatchedRetries: String(retry.dispatchedRetries),
3379
3420
  elapsedMs: String(Math.max(0, now.getTime() - Date.parse(retry.firstFailureAt)))
3380
- }, now));
3421
+ }, now);
3422
+ store.saveEvent(run.taskId, exhaustionEvent);
3381
3423
  if (terminal.purpose === "execution" && terminal.workItemId !== undefined) {
3382
3424
  const item = store.getWorkItem(run.taskId, terminal.workItemId);
3383
3425
  if (item !== null
@@ -3392,15 +3434,8 @@ function finalizeProviderRetryDeadline(store, run, summary, reason, now) {
3392
3434
  }
3393
3435
  if (run.roleName === "leader") {
3394
3436
  store.saveLeaderFailure(recordLeaderFailure(run.taskId, retry.nativeSessionId ?? "(unproven)", summary, now, store.getLeaderFailure(run.taskId)));
3395
- store.saveOperatorNotification(createLeaderRecoveryNotification(run.taskId, summary, now, store.getOperatorNotification(run.taskId)));
3396
- enqueueWork(store, { kind: "operator" }, "provider-retry-exhausted", now, [
3397
- { type: "task", id: run.taskId },
3398
- { type: "run", taskId: run.taskId, id: run.id }
3399
- ]);
3400
- }
3401
- else {
3402
- enqueueWork(store, { kind: "role", taskId: run.taskId, roleName: "leader" }, "provider-retry-exhausted", now, [{ type: "run", taskId: run.taskId, id: run.id }]);
3403
3437
  }
3438
+ routeRoleEvent(store, exhaustionEvent, run.roleName, "provider-retry-exhausted", now);
3404
3439
  return true;
3405
3440
  }
3406
3441
  function preparedDeliveryFailureSessionFenceMatches(sessions, session, input, expectedReceiptId) {
@@ -3475,7 +3510,7 @@ function recordTaskRuntimeNativeSession(store, input, now) {
3475
3510
  const effective = input.effective === undefined
3476
3511
  ? resolvedEffective
3477
3512
  : validateEffectiveLaunchSnapshot(input.effective);
3478
- if (!effectiveLaunchSnapshotsCompatibleForTaskMain(resolvedEffective, effective, store.getTaskWorkspace(input.taskId))) {
3513
+ if (!effectiveLaunchSnapshotsCompatibleForTaskSession(resolvedEffective, effective)) {
3479
3514
  throw new Error("Reserved native Session effective launch changed before persistence.");
3480
3515
  }
3481
3516
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
@@ -3536,6 +3571,17 @@ function nativeTransitionExisting(store, owner, existing, nativeSessionId, launc
3536
3571
  }
3537
3572
  throw new Error(conflictMessage);
3538
3573
  }
3574
+ function globalCompletionNativeSessionId(existing, input) {
3575
+ // Codex app-server and its notify Hook expose different native IDs for the
3576
+ // same launch. The launch generation remains the authoritative fence.
3577
+ return input.adapterId === "codex"
3578
+ && input.launchId !== undefined
3579
+ && existing?.launchId === input.launchId
3580
+ && existing.agentId === input.agentId
3581
+ && existing.adapterId === input.adapterId
3582
+ ? existing.nativeSessionId
3583
+ : input.nativeSessionId;
3584
+ }
3539
3585
  function mapRole(store, role) {
3540
3586
  const binding = activeRoleAgentBinding(role);
3541
3587
  const item = store.listWorkItems(role.taskId).find((candidate) => (candidate.assignee === role.name
@@ -3581,8 +3627,7 @@ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
3581
3627
  throw new Error(`Native Session registration does not match the effective Run Agent: ${taskId}/${roleName}.`);
3582
3628
  }
3583
3629
  if (existing !== undefined) {
3584
- const workspace = active.workspace ?? store.getTaskWorkspace(taskId);
3585
- if (!effectiveLaunchSnapshotsCompatibleForTaskMain(existing.effective, active.effective, workspace)) {
3630
+ if (!effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, active.effective)) {
3586
3631
  throw new Error(`Native Session effective launch does not match the active Run: ${taskId}/${roleName}.`);
3587
3632
  }
3588
3633
  return existing.effective;
@@ -3727,6 +3772,19 @@ function schedulerRoleSessionsMatch(current, expected) {
3727
3772
  && current.launchId === expected.launchId
3728
3773
  && isDeepStrictEqual(current.effective, expected.effective);
3729
3774
  }
3775
+ /**
3776
+ * The durable Run/mailbox claim is the pre-Host delivery intent. The Host may
3777
+ * create a ProviderTurn only while that exact reservation still owns the Run;
3778
+ * this closes the gap where a Session could start without any user Turn.
3779
+ */
3780
+ function processingOwnsExactInitialDelivery(mailbox, taskId, runId, attemptId) {
3781
+ const processing = mailbox?.processing;
3782
+ return processing?.batchId === attemptId
3783
+ && processing.owner === "controller"
3784
+ && processing.executionRef?.type === "run"
3785
+ && processing.executionRef.taskId === taskId
3786
+ && processing.executionRef.id === runId;
3787
+ }
3730
3788
  function matchesStallSessionFence(current, expected) {
3731
3789
  if (current === null || expected === null)
3732
3790
  return current === expected;
@@ -3788,11 +3846,10 @@ function runtimeObservationTelemetryEntry(input) {
3788
3846
  };
3789
3847
  }
3790
3848
  /**
3791
- * Runtime observations are a current-state snapshot, not an append-only
3792
- * semantic history. Retain active operations, one latest usage baseline, and
3793
- * one latest confirmed activity boundary so a token stream remains
3794
- * O(active operations) per Run without losing the delta that was already
3795
- * proven before an unchanged later snapshot arrived.
3849
+ * Usage observations and incomplete request boundaries are authoritative
3850
+ * read-only history: retain each one so consecutive deltas remain projectable.
3851
+ * Other activity observations are a current explicit boundary and may replace
3852
+ * their predecessor. Token evidence never becomes lifecycle activity.
3796
3853
  */
3797
3854
  function compactedRuntimeObservationIds(events, incoming) {
3798
3855
  const existing = events.flatMap((event) => {
@@ -3809,9 +3866,10 @@ function compactedRuntimeObservationIds(events, incoming) {
3809
3866
  });
3810
3867
  const remove = ({ observation }) => {
3811
3868
  if (incoming.kind === "activity.observed") {
3869
+ if (isRuntimeTokenEvidence(incoming))
3870
+ return false;
3812
3871
  return observation.kind === "activity.observed"
3813
- && (observation.payload.usage === undefined)
3814
- === (incoming.payload.usage === undefined);
3872
+ && !isRuntimeTokenEvidence(observation);
3815
3873
  }
3816
3874
  if (incoming.kind === "operation.started") {
3817
3875
  return (observation.kind === "operation.started"
@@ -3854,7 +3912,6 @@ function recordStructuredProviderAcceptance(sessions, input, now) {
3854
3912
  if (current === null || attemptId === undefined || turnId === undefined)
3855
3913
  return sessions;
3856
3914
  if (current.turn?.attemptId === attemptId
3857
- && current.turn.turnId === turnId
3858
3915
  && ["accepted", "running", "completed", "failed", "cancelled"].includes(current.turn.status))
3859
3916
  return sessions;
3860
3917
  const binding = acceptProviderTurn(current, {
@@ -3864,7 +3921,21 @@ function recordStructuredProviderAcceptance(sessions, input, now) {
3864
3921
  });
3865
3922
  return updateTaskRoleProviderRuntime(sessions, binding, now);
3866
3923
  }
3867
- function bindOrSupersedeProviderRuntime(sessions, input, now) {
3924
+ /**
3925
+ * A Provider may expose the same accepted input through more than one exact
3926
+ * observation surface. Preserve the first native Turn identity bound to the
3927
+ * durable attempt; a later transport alias may settle that same attempt but
3928
+ * must neither replace nor strand the canonical identity.
3929
+ */
3930
+ function canonicalStructuredProviderTurnId(sessions, observedTurnId, attemptId) {
3931
+ const turn = sessions.providerBinding?.turn;
3932
+ return attemptId !== undefined
3933
+ && turn?.attemptId === attemptId
3934
+ && turn.turnId !== undefined
3935
+ ? turn.turnId
3936
+ : observedTurnId;
3937
+ }
3938
+ function bindOrSupersedeProviderRuntime(sessions, input, now, replacementBasis) {
3868
3939
  if (sessions.inFlight === null)
3869
3940
  return sessions;
3870
3941
  const conversationId = input.fence.conversationId ?? input.fence.nativeSessionId;
@@ -3899,9 +3970,22 @@ function bindOrSupersedeProviderRuntime(sessions, input, now) {
3899
3970
  conversationId,
3900
3971
  activationId,
3901
3972
  switchedAt: input.observedAt ?? input.receivedAt,
3902
- noUnsettledInputDelivery
3973
+ noUnsettledInputDelivery,
3974
+ ...(replacementBasis === undefined ? {} : { basis: replacementBasis })
3903
3975
  }), now);
3904
3976
  }
3977
+ function resolveConversationSwitchRequest(store, taskId, roleName, previousSessions, status, now, replacement) {
3978
+ const request = pendingConversationSwitch(store.listEvents(taskId), roleName, previousSessions);
3979
+ if (request === null)
3980
+ return;
3981
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, CONVERSATION_SWITCH_RESOLVED_EVENT, {
3982
+ requestId: request.requestId,
3983
+ roleName,
3984
+ generation: request.generation,
3985
+ status,
3986
+ ...(replacement === undefined ? {} : replacement)
3987
+ }, now));
3988
+ }
3905
3989
  function rejectSubmittingProviderTurn(store, target, delivery, now, reason) {
3906
3990
  if (target.kind !== "role"
3907
3991
  || delivery.executionRef.type !== "run"
@@ -3931,16 +4015,14 @@ function deferUnprovenProviderRecovery(store, run, roleName, reason, now) {
3931
4015
  return;
3932
4016
  }
3933
4017
  store.saveAgentRun(withProviderRetry(run, deferred));
3934
- store.saveEvent(run.taskId, createTaskEvent(store.nextEventId(run.taskId), run.taskId, "runtime.provider-recovery-attention", {
4018
+ const attentionEvent = createTaskEvent(store.nextEventId(run.taskId), run.taskId, "runtime.provider-recovery-attention", {
3935
4019
  runId: run.id,
3936
4020
  roleName,
3937
4021
  reason,
3938
4022
  nextAttemptAt: deferred.nextAttemptAt ?? ""
3939
- }, now));
3940
- enqueueWork(store, { kind: "operator" }, "provider-recovery-attention", now, [
3941
- { type: "task", id: run.taskId },
3942
- { type: "run", taskId: run.taskId, id: run.id }
3943
- ]);
4023
+ }, now);
4024
+ store.saveEvent(run.taskId, attentionEvent);
4025
+ routeRoleEvent(store, attentionEvent, roleName, "provider-recovery-attention", now);
3944
4026
  }
3945
4027
  function markSubmittingProviderTurnUnknown(store, target, delivery, now, reason) {
3946
4028
  if (target.kind !== "role"
@@ -3974,39 +4056,6 @@ function settleStructuredProviderTurn(sessions, turnId, status, now) {
3974
4056
  settledAt: now.toISOString()
3975
4057
  }), now);
3976
4058
  }
3977
- function confirmedUsageActivityObservation(events, incoming) {
3978
- const usage = incoming.payload.usage;
3979
- if (incoming.kind !== "activity.observed" || usage === undefined)
3980
- return null;
3981
- const previous = events
3982
- .map(runtimeObservationFromTaskEvent)
3983
- .filter((observation) => (observation !== null
3984
- && observation.kind === "activity.observed"
3985
- && observation.payload.usage !== undefined
3986
- && runtimeObservationFenceMatches(observation.fence, incoming.fence)))
3987
- .sort(compareCanonicalObservationOrder)
3988
- .at(-1)?.payload.usage;
3989
- if (previous === undefined
3990
- || usage.inputTokens + usage.outputTokens
3991
- <= previous.inputTokens + previous.outputTokens)
3992
- return null;
3993
- const digest = createHash("sha256")
3994
- .update(`confirmed-runtime-activity\0${incoming.eventId}`)
3995
- .digest("hex");
3996
- return createRuntimeObservation({
3997
- schemaVersion: 2,
3998
- eventId: `runtime-activity-${digest}`,
3999
- semanticKey: `runtime-activity-${digest}`,
4000
- kind: "activity.observed",
4001
- authority: "controller",
4002
- receivedAt: incoming.receivedAt,
4003
- ...(incoming.observedAt === undefined ? {} : { observedAt: incoming.observedAt }),
4004
- ...(incoming.sequence === undefined ? {} : { sequence: incoming.sequence }),
4005
- ...(incoming.ordinal === undefined ? {} : { ordinal: incoming.ordinal }),
4006
- fence: incoming.fence,
4007
- payload: { activity: incoming.payload.activity ?? "model" }
4008
- });
4009
- }
4010
4059
  function usageSnapshotIsSuperseded(events, incoming) {
4011
4060
  if (incoming.kind !== "activity.observed" || incoming.payload.usage === undefined) {
4012
4061
  return false;
@@ -4019,6 +4068,15 @@ function usageSnapshotIsSuperseded(events, incoming) {
4019
4068
  && runtimeObservationFenceMatches(observation.fence, incoming.fence)
4020
4069
  && observationIsStrictlyNewer(observation, incoming));
4021
4070
  }
4071
+ function hasPersistedRuntimeObservation(events, incoming) {
4072
+ const usageIdentity = isRuntimeTokenEvidence(incoming)
4073
+ ? incoming.eventId
4074
+ : undefined;
4075
+ return events.some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
4076
+ && (usageIdentity === undefined
4077
+ ? event.payload.semanticKey === incoming.semanticKey
4078
+ : event.payload.eventId === usageIdentity)));
4079
+ }
4022
4080
  function observationIsStrictlyNewer(left, right) {
4023
4081
  return compareCanonicalObservationOrder(left, right) > 0;
4024
4082
  }