@zq-silk/yui 0.7.1 → 0.8.2

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 (73) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +79 -71
  3. package/dist/cli/commandCatalog.js +283 -136
  4. package/dist/cli/completion.js +3 -3
  5. package/dist/cli/helpRenderer.js +3 -0
  6. package/dist/cli/interactionPolicy.js +48 -33
  7. package/dist/cli/interactiveSelection.js +1 -1
  8. package/dist/cli/invocationRouter.js +3 -2
  9. package/dist/cli/roleWizard.js +8 -8
  10. package/dist/cli.js +189 -93
  11. package/dist/commands/agentCommands.js +5 -5
  12. package/dist/commands/configCommands.js +351 -104
  13. package/dist/commands/configOverview.js +60 -0
  14. package/dist/commands/deliveryGuardPreflight.js +2 -2
  15. package/dist/commands/globalRoleCommands.js +9 -9
  16. package/dist/commands/profileCommands.js +8 -8
  17. package/dist/commands/resourcesCommands.js +6 -5
  18. package/dist/commands/taskCommands.js +111 -59
  19. package/dist/commands/taskRoleRuntimeStatus.js +3 -1
  20. package/dist/commands/telemetryCommands.js +11 -6
  21. package/dist/config/configCatalog.js +42 -0
  22. package/dist/config/yuiConfig.js +80 -35
  23. package/dist/context/sessionBootstrapManifest.js +1 -1
  24. package/dist/controller/clientRuntime.js +0 -2
  25. package/dist/controller/controller.js +21 -9
  26. package/dist/controller/fileSchedulerStoreAdapter.js +409 -79
  27. package/dist/controller/resourceInventory.js +9 -5
  28. package/dist/controller/runtime.js +112 -25
  29. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  30. package/dist/controller/structuredProviderObservation.js +273 -0
  31. package/dist/doctor/doctor.js +2 -2
  32. package/dist/executor/agentAdapter.js +40 -0
  33. package/dist/executor/agentExecutor.js +31 -7
  34. package/dist/executor/executorRegistry.js +11 -49
  35. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  36. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  37. package/dist/resources/autoResourceGc.js +3 -1
  38. package/dist/review/reviewConfig.js +0 -2
  39. package/dist/run/agentRun.js +2 -2
  40. package/dist/run/providerRetry.js +29 -16
  41. package/dist/run/providerRetryConfig.js +5 -3
  42. package/dist/runtime/agentHost.js +767 -158
  43. package/dist/runtime/builtinAgentDrivers.js +1 -5
  44. package/dist/runtime/codexAppServerRuntime.js +67 -60
  45. package/dist/runtime/exactControlPlane.js +7 -2
  46. package/dist/runtime/index.js +6 -2
  47. package/dist/runtime/launchBroker.js +30 -8
  48. package/dist/runtime/launchDiagnostics.js +1 -1
  49. package/dist/runtime/providerAuthorityFence.js +24 -0
  50. package/dist/runtime/providerControl.js +63 -0
  51. package/dist/runtime/providerRecoveryDecision.js +55 -0
  52. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  53. package/dist/runtime/runtimeBinding.js +20 -11
  54. package/dist/runtime/structuredProviderHost.js +476 -0
  55. package/dist/runtime/tmuxAdapters.js +143 -42
  56. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  57. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  58. package/dist/scheduler/roleRunStall.js +12 -9
  59. package/dist/setup/setupCommand.js +153 -492
  60. package/dist/storage/compatibleTaskStore.js +9 -5
  61. package/dist/storage/migration/productionRegistry.js +169 -0
  62. package/dist/storage/taskStore.js +22 -3
  63. package/dist/telemetry/sqliteTelemetryStore.js +9 -1
  64. package/dist/telemetry/telemetryConfig.js +1 -18
  65. package/dist/telemetry/telemetryStore.js +2 -2
  66. package/dist/telemetry/telemetryWiring.js +6 -5
  67. package/dist/tmux/tmuxManager.js +1 -1
  68. package/dist/web/webSnapshot.js +5 -3
  69. package/i18n/README.zh-CN.md +48 -40
  70. package/package.json +1 -1
  71. package/skills/yui-leader/SKILL.md +12 -5
  72. package/skills/yui-operator/SKILL.md +44 -6
  73. package/skills/yui-runtime/SKILL.md +1 -1
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
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";
3
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, 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";
5
+ import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
5
6
  import { hasRecentTurnId } from "../executor/turnCompletion.js";
6
7
  import { createTaskEvent } from "../event/taskEvent.js";
7
8
  import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
@@ -40,6 +41,12 @@ import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, runtimeObserv
40
41
  import { projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
41
42
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
42
43
  import { contextSnapshotDeltaRefIds, freezeRunContextSnapshot } from "../context/runContextPack.js";
44
+ export class AgentHostProviderTurnFenceError extends Error {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = "AgentHostProviderTurnFenceError";
48
+ }
49
+ }
43
50
  /** Maps the authoritative FileTaskStore records to the scheduler's narrow port. */
44
51
  export class FileSchedulerStoreAdapter {
45
52
  store;
@@ -145,7 +152,8 @@ export class FileSchedulerStoreAdapter {
145
152
  nativeSessionId: input.fence.nativeSessionId,
146
153
  turnId: input.fence.nativeTurnId,
147
154
  runId: input.fence.runId,
148
- summary: input.payload.summary ?? `Provider Turn failed: ${failureEvidence.code}.`
155
+ summary: input.payload.summary ?? `Provider Turn failed: ${failureEvidence.code}.`,
156
+ providerStatus: "failed"
149
157
  }, now);
150
158
  outcome = boundary.disposition === "obsolete" ? "obsolete" : "applied";
151
159
  break;
@@ -505,7 +513,14 @@ export class FileSchedulerStoreAdapter {
505
513
  });
506
514
  if (this.telemetry !== null && input.fence.runId !== undefined) {
507
515
  try {
508
- this.telemetry.sink.observe(runtimeObservationTelemetryEntry(input));
516
+ const entry = runtimeObservationTelemetryEntry(input);
517
+ this.telemetry.sink.observe(entry);
518
+ const run = this.store.getAgentRun(entry.taskId, input.fence.runId);
519
+ if (run !== null && run.status !== "active") {
520
+ void this.telemetry.retention.flush().then(() => {
521
+ this.telemetry?.retention.pruneGeneration(entry.taskId, entry.roleName, entry.runId, entry.generation);
522
+ }).catch(() => undefined);
523
+ }
509
524
  }
510
525
  catch {
511
526
  // Runtime telemetry is diagnostic; the compact durable state snapshot
@@ -805,42 +820,129 @@ export class FileSchedulerStoreAdapter {
805
820
  const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
806
821
  return sessions !== null && sessions.inFlight !== null;
807
822
  }
808
- getActiveProviderTurnFence(input) {
809
- const observations = this.store.listEvents(input.taskId)
810
- .map(runtimeObservationFromTaskEvent)
811
- .filter((entry) => entry !== null
812
- && entry.fence.runId === input.runId
813
- && entry.fence.roleName === input.roleName
814
- && entry.fence.agentId === input.agentId
815
- && entry.fence.launchId === input.launchId
816
- && entry.fence.nativeSessionId === input.nativeSessionId
817
- && entry.fence.nativeTurnId !== undefined)
818
- .sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
819
- || (left.sequence ?? -1) - (right.sequence ?? -1)
820
- || (left.ordinal ?? -1) - (right.ordinal ?? -1)
821
- || left.semanticKey.localeCompare(right.semanticKey)));
822
- const state = new Map();
823
- const fences = new Map();
824
- for (const observation of observations) {
825
- const turnId = observation.fence.nativeTurnId;
826
- if (observation.kind === "turn.accepted" || observation.kind === "input.accepted") {
827
- state.set(turnId, "active");
828
- fences.set(turnId, observation);
829
- }
830
- else if (["turn.completed", "turn.failed", "turn.cancelled"].includes(observation.kind)) {
831
- state.set(turnId, "terminal");
832
- }
833
- }
834
- const active = [...state.entries()].filter(([, status]) => status === "active");
835
- if (active.length !== 1)
823
+ beginAgentHostProviderTurn(input) {
824
+ this.store.transaction((store) => {
825
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
826
+ const session = sessions?.sessions[input.agentId];
827
+ const binding = sessions?.providerBinding;
828
+ if (sessions === null || sessions === undefined
829
+ || binding === null || binding === undefined
830
+ || sessions.inFlight?.runId !== input.runId
831
+ || binding.runId !== input.runId
832
+ || session?.launchId !== input.launchId
833
+ || session.nativeSessionId !== input.nativeSessionId
834
+ || currentProviderConversation(binding).conversationId !== input.nativeSessionId
835
+ || binding.authority.owner !== input.authorityOwner
836
+ || binding.authority.epoch !== input.authorityEpoch
837
+ || binding.authority.holderId !== input.holderId) {
838
+ throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
839
+ }
840
+ const exactReplay = binding.turn?.attemptId === input.attemptId
841
+ && binding.turn.authorityEpoch === input.authorityEpoch
842
+ && binding.turn.status === "submitting";
843
+ if (!exactReplay && binding.turn !== null
844
+ && ["submitting", "accepted", "running", "delivery-unknown"]
845
+ .includes(binding.turn.status)) {
846
+ throw new AgentHostProviderTurnFenceError("Provider Conversation already has an unsettled Turn.");
847
+ }
848
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
849
+ attemptId: input.attemptId,
850
+ authorityEpoch: input.authorityEpoch,
851
+ submittedAt: input.now.toISOString()
852
+ }), input.now));
853
+ });
854
+ }
855
+ resolveAgentHostProviderTurnSubmission(input) {
856
+ this.store.transaction((store) => {
857
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
858
+ const binding = sessions?.providerBinding;
859
+ if (sessions === null || sessions === undefined
860
+ || binding === null || binding === undefined
861
+ || binding.runId !== input.runId
862
+ || binding.turn?.attemptId !== input.attemptId
863
+ || binding.turn.status !== "submitting") {
864
+ throw new Error("Agent Host Provider Turn submission is no longer current.");
865
+ }
866
+ const updated = input.status === "delivery-unknown"
867
+ ? markProviderTurnDeliveryUnknown(binding, {
868
+ attemptId: input.attemptId,
869
+ observedAt: input.now.toISOString(),
870
+ reason: input.reason
871
+ })
872
+ : rejectProviderTurn(binding, {
873
+ attemptId: input.attemptId,
874
+ rejectedAt: input.now.toISOString(),
875
+ reason: input.reason
876
+ });
877
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
878
+ });
879
+ }
880
+ getProviderAuthorityFence(input) {
881
+ const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
882
+ const session = sessions?.sessions[input.agentId];
883
+ const binding = sessions?.providerBinding;
884
+ if (binding === null || binding === undefined
885
+ || binding.runId !== input.runId
886
+ || session?.launchId !== input.launchId
887
+ || session.nativeSessionId !== input.nativeSessionId
888
+ || currentProviderConversation(binding).conversationId !== input.nativeSessionId)
889
+ return null;
890
+ const activation = currentProviderActivation(binding);
891
+ if (activation === null)
836
892
  return null;
837
- const observation = fences.get(active[0][0]);
838
893
  return {
839
- conversationId: observation.fence.conversationId ?? input.nativeSessionId,
840
- activationId: observation.fence.activationId ?? input.launchId,
841
- nativeTurnId: active[0][0]
894
+ conversationId: currentProviderConversation(binding).conversationId,
895
+ activationId: activation.activationId,
896
+ ...binding.authority
842
897
  };
843
898
  }
899
+ beginRoleRunProviderTurn(input) {
900
+ return this.store.transaction((store) => {
901
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
902
+ const session = sessions?.sessions[input.agentId];
903
+ const binding = sessions?.providerBinding;
904
+ if (sessions === null || sessions === undefined
905
+ || binding === null || binding === undefined
906
+ || sessions.inFlight?.runId !== input.runId
907
+ || binding.runId !== input.runId
908
+ || session?.launchId !== input.launchId
909
+ || session.nativeSessionId !== input.nativeSessionId
910
+ || currentProviderConversation(binding).conversationId !== input.nativeSessionId
911
+ || binding.authority.owner !== "controller")
912
+ return false;
913
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
914
+ attemptId: input.attemptId,
915
+ authorityEpoch: binding.authority.epoch,
916
+ submittedAt: input.now.toISOString()
917
+ }), input.now));
918
+ return true;
919
+ });
920
+ }
921
+ resolveRoleRunProviderSubmission(input) {
922
+ return this.store.transaction((store) => {
923
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
924
+ const binding = sessions?.providerBinding;
925
+ if (sessions === null || sessions === undefined
926
+ || binding === null || binding === undefined
927
+ || binding.runId !== input.runId
928
+ || binding.turn?.attemptId !== input.attemptId
929
+ || binding.turn.status !== "submitting")
930
+ return false;
931
+ const updated = input.status === "delivery-unknown"
932
+ ? markProviderTurnDeliveryUnknown(binding, {
933
+ attemptId: input.attemptId,
934
+ observedAt: input.now.toISOString(),
935
+ reason: input.reason
936
+ })
937
+ : rejectProviderTurn(binding, {
938
+ attemptId: input.attemptId,
939
+ rejectedAt: input.now.toISOString(),
940
+ reason: input.reason
941
+ });
942
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
943
+ return true;
944
+ });
945
+ }
844
946
  isRoleGenerationProviderReady(input) {
845
947
  // A session.ready observation for this exact generation is the durable
846
948
  // pre-input fence. This reads folded Driver truth
@@ -964,6 +1066,31 @@ export class FileSchedulerStoreAdapter {
964
1066
  executionRef: input.executionRef,
965
1067
  providerFence: input.providerFence
966
1068
  });
1069
+ if (input.target.kind === "role"
1070
+ && input.executionRef.type === "run"
1071
+ && input.executionRef.taskId === input.target.taskId) {
1072
+ const sessions = store.getTaskRoleSessionSet(input.target.taskId, input.target.roleName);
1073
+ const binding = sessions?.providerBinding;
1074
+ if (sessions === null || sessions === undefined
1075
+ || binding === null || binding === undefined
1076
+ || binding.runId !== input.executionRef.id
1077
+ || binding.authority.owner !== "controller") {
1078
+ throw new Error("Controller does not own the Provider writer authority.");
1079
+ }
1080
+ const conversation = currentProviderConversation(binding);
1081
+ const activation = currentProviderActivation(binding);
1082
+ if (input.providerFence === undefined
1083
+ || conversation.conversationId !== input.providerFence.conversationId
1084
+ || activation?.activationId !== input.providerFence.activationId) {
1085
+ throw new Error("Provider input claim carries a stale Conversation/Activation fence.");
1086
+ }
1087
+ const next = beginProviderTurn(binding, {
1088
+ attemptId: input.attemptId,
1089
+ authorityEpoch: binding.authority.epoch,
1090
+ submittedAt: input.now.toISOString()
1091
+ });
1092
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, next, input.now));
1093
+ }
967
1094
  store.saveWorkMailbox(claimed);
968
1095
  return { status: "claimed", delivery: claimed.inputDelivery };
969
1096
  });
@@ -1002,20 +1129,22 @@ export class FileSchedulerStoreAdapter {
1002
1129
  return true;
1003
1130
  });
1004
1131
  }
1005
- releaseInputDelivery(target, attemptId) {
1132
+ releaseInputDelivery(target, attemptId, now) {
1006
1133
  return this.store.transaction((store) => {
1007
1134
  const mailbox = store.getWorkMailbox(target);
1008
1135
  if (mailbox?.inputDelivery?.attemptId !== attemptId)
1009
1136
  return false;
1137
+ rejectSubmittingProviderTurn(store, target, mailbox.inputDelivery, now, "Provider write was not attempted.");
1010
1138
  store.saveWorkMailbox(releaseMailboxInputDelivery(mailbox, attemptId));
1011
1139
  return true;
1012
1140
  });
1013
1141
  }
1014
- resolveInputDeliveryNotAccepted(target, attemptId) {
1142
+ resolveInputDeliveryNotAccepted(target, attemptId, now) {
1015
1143
  return this.store.transaction((store) => {
1016
1144
  const mailbox = store.getWorkMailbox(target);
1017
1145
  if (mailbox?.inputDelivery?.attemptId !== attemptId)
1018
1146
  return false;
1147
+ rejectSubmittingProviderTurn(store, target, mailbox.inputDelivery, now, "Provider returned an exact negative acknowledgement.");
1019
1148
  store.saveWorkMailbox(resolveMailboxInputDeliveryNotAccepted(mailbox, attemptId));
1020
1149
  return true;
1021
1150
  });
@@ -1026,6 +1155,7 @@ export class FileSchedulerStoreAdapter {
1026
1155
  if (mailbox?.inputDelivery?.attemptId !== attemptId)
1027
1156
  return false;
1028
1157
  const unknown = markMailboxInputDeliveryUnknown(mailbox, attemptId, reason, now);
1158
+ markSubmittingProviderTurnUnknown(store, target, mailbox.inputDelivery, now, reason);
1029
1159
  store.saveWorkMailbox(unknown);
1030
1160
  const taskId = target.kind === "role" ? target.taskId : undefined;
1031
1161
  if (taskId !== undefined) {
@@ -1865,6 +1995,7 @@ export class FileSchedulerStoreAdapter {
1865
1995
  : sessions.inFlight === null ? idleStatus : "running",
1866
1996
  effective
1867
1997
  }, now);
1998
+ sessions = settleStructuredProviderTurn(sessions, input.turnId, input.providerStatus ?? "completed", now);
1868
1999
  if (sessions.inFlight === null) {
1869
2000
  sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, input.turnId, now);
1870
2001
  store.saveTaskRoleSessionSet(sessions);
@@ -2084,7 +2215,7 @@ export class FileSchedulerStoreAdapter {
2084
2215
  failedNativeTurnId: input.nativeTurnId,
2085
2216
  lastErrorSummary: summary,
2086
2217
  scheduleNextAttempt: false
2087
- }, now);
2218
+ }, now, configuredProviderRetryPolicy(store));
2088
2219
  if (blocked.outcome === "exhausted")
2089
2220
  return null;
2090
2221
  store.saveAgentRun(withProviderRetry(run, blocked.retry));
@@ -2110,7 +2241,7 @@ export class FileSchedulerStoreAdapter {
2110
2241
  failedNativeTurnId: input.nativeTurnId,
2111
2242
  lastErrorSummary: summary,
2112
2243
  ...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
2113
- }, now);
2244
+ }, now, configuredProviderRetryPolicy(store));
2114
2245
  if (retryDecision.outcome === "exhausted") {
2115
2246
  this.recordProviderRetryClassified(store, input, classification.errorClass, {
2116
2247
  wouldRetry: "false",
@@ -2122,24 +2253,17 @@ export class FileSchedulerStoreAdapter {
2122
2253
  if (run.providerRetry === undefined)
2123
2254
  return null;
2124
2255
  const finalized = finalizeProviderRetryDeadline(store, run, retryDecision.reason === "attempts"
2125
- ? "Provider retry failed after all three in-Session continuation attempts."
2256
+ ? `Provider retry failed after all ${config.delaysMs.length} in-Session continuation attempts.`
2126
2257
  : retryDecision.reason === "retry-after-window"
2127
- ? "Provider Retry-After exceeded the bounded 600-second episode window."
2128
- : "Provider retry did not recover within the bounded 600-second episode window.", retryDecision.reason === "attempts" ? "attempts-exhausted" : "episode-window-exhausted", now);
2258
+ ? `Provider Retry-After exceeded the bounded ${config.maxWindowMs / 1_000}-second episode window.`
2259
+ : `Provider retry did not recover within the bounded ${config.maxWindowMs / 1_000}-second episode window.`, retryDecision.reason === "attempts" ? "attempts-exhausted" : "episode-window-exhausted", now);
2129
2260
  return finalized ? { disposition: "applied", runId: input.runId } : null;
2130
2261
  }
2131
2262
  const retry = retryDecision.retry;
2132
2263
  store.saveAgentRun(withProviderRetry(run, retry));
2133
- // Clear the in-flight fence so the original Session can be re-bound and
2134
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
2135
- // re-pushed by the delivery path; the Session itself stays alive.
2136
- if (sessions !== null && sessions.inFlight !== null) {
2137
- store.saveTaskRoleSessionSet(clearTaskRoleRun(sessions, {
2138
- agentId: input.agentId,
2139
- runId: input.runId,
2140
- receiptId: agentRunDeliveryReceiptId(run)
2141
- }, now));
2142
- }
2264
+ // The in-flight fence and Provider Conversation stay bound while the
2265
+ // retry waits. resolveDueProviderRetries advances only the delivery
2266
+ // receipt immediately before the exact redispatch.
2143
2267
  // Settle the delivery claim so the retry push can re-claim the mailbox.
2144
2268
  const target = runtimeLifecycleTarget({
2145
2269
  scope: "task",
@@ -2183,7 +2307,7 @@ export class FileSchedulerStoreAdapter {
2183
2307
  failedNativeTurnId: input.nativeTurnId,
2184
2308
  lastErrorSummary: summary,
2185
2309
  scheduleNextAttempt: false
2186
- }, now);
2310
+ }, now, configuredProviderRetryPolicy(store));
2187
2311
  if (retryDecision.outcome === "exhausted")
2188
2312
  return null;
2189
2313
  store.saveAgentRun(withProviderRetry(run, retryDecision.retry));
@@ -2223,7 +2347,7 @@ export class FileSchedulerStoreAdapter {
2223
2347
  failedNativeTurnId: input.nativeTurnId,
2224
2348
  lastErrorSummary: summary,
2225
2349
  scheduleNextAttempt: false
2226
- }, now);
2350
+ }, now, configuredProviderRetryPolicy(store));
2227
2351
  if (decision.outcome === "exhausted")
2228
2352
  return null;
2229
2353
  store.saveAgentRun(withProviderRetry(run, decision.retry));
@@ -2262,7 +2386,7 @@ export class FileSchedulerStoreAdapter {
2262
2386
  failedNativeTurnId: input.nativeTurnId,
2263
2387
  lastErrorSummary: summary,
2264
2388
  scheduleNextAttempt: false
2265
- }, now);
2389
+ }, now, configuredProviderRetryPolicy(store));
2266
2390
  if (decision.outcome === "exhausted")
2267
2391
  return null;
2268
2392
  store.saveAgentRun(withProviderRetry(run, decision.retry));
@@ -2380,7 +2504,61 @@ export class FileSchedulerStoreAdapter {
2380
2504
  || !providerRetryIsDue(run.providerRetry, now)) {
2381
2505
  return;
2382
2506
  }
2383
- const sessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
2507
+ let sessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
2508
+ const providerBinding = sessions?.providerBinding;
2509
+ let recoveryMode = "resume";
2510
+ if (providerBinding === null || providerBinding === undefined) {
2511
+ deferUnprovenProviderRecovery(store, run, entry.roleName, "Provider Conversation identity is missing; automatic recovery is fenced.", now);
2512
+ return;
2513
+ }
2514
+ const conversation = currentProviderConversation(providerBinding);
2515
+ const roleMailbox = store.getWorkMailbox({
2516
+ kind: "role",
2517
+ taskId: entry.taskId,
2518
+ roleName: entry.roleName
2519
+ });
2520
+ const activeTurnId = providerBinding.turn !== null
2521
+ && ["accepted", "running"].includes(providerBinding.turn.status)
2522
+ ? providerBinding.turn.turnId
2523
+ : undefined;
2524
+ const recovery = decideProviderRecovery({
2525
+ binding: providerBinding,
2526
+ probe: {
2527
+ state: conversation.recoverability === "recoverable"
2528
+ ? "exists"
2529
+ : conversation.recoverability === "unrecoverable" ? "missing" : "unknown",
2530
+ conversationId: conversation.conversationId,
2531
+ ...(activeTurnId === undefined ? {} : { activeTurnId })
2532
+ },
2533
+ unsettledInputDelivery: roleMailbox?.inputDelivery != null
2534
+ });
2535
+ if (recovery.action === "attention") {
2536
+ deferUnprovenProviderRecovery(store, run, entry.roleName, recovery.reason, now);
2537
+ return;
2538
+ }
2539
+ if (recovery.action === "observe-active-turn") {
2540
+ const deferred = deferProviderRetry(run.providerRetry, now);
2541
+ if (deferred === null) {
2542
+ finalizeProviderRetryDeadline(store, run, `Provider Turn ${recovery.turnId} remained active through the bounded recovery deadline.`, "provider-progress-timeout", now);
2543
+ }
2544
+ else {
2545
+ store.saveAgentRun(withProviderRetry(run, deferred));
2546
+ }
2547
+ return;
2548
+ }
2549
+ if (recovery.action === "replace") {
2550
+ if (conversation.recoverability !== "unrecoverable") {
2551
+ deferUnprovenProviderRecovery(store, run, entry.roleName, "Provider replacement lacks an exact missing-Conversation observation.", now);
2552
+ return;
2553
+ }
2554
+ recoveryMode = "new";
2555
+ const currentSession = sessions?.sessions[run.effective.agentId];
2556
+ if (sessions !== null && currentSession !== undefined
2557
+ && currentSession.status !== "broken") {
2558
+ sessions = updateRoleAgentSessionStatus(sessions, run.effective.agentId, "broken", now);
2559
+ store.saveTaskRoleSessionSet(sessions);
2560
+ }
2561
+ }
2384
2562
  const session = sessions?.sessions[run.effective.agentId];
2385
2563
  const identityMatches = session !== undefined
2386
2564
  && session.adapterId === run.effective.adapterId
@@ -2425,7 +2603,15 @@ export class FileSchedulerStoreAdapter {
2425
2603
  // Reopen the Run on its original Session: reset transport markers,
2426
2604
  // switch to resume, and mark the projection in-flight. The delivery
2427
2605
  // pass re-pushes the exact same input in this same pass.
2428
- store.saveAgentRun(reopenRunForProviderRetry(run, `provider-retry-${run.providerRetry.episodeId}-${run.providerRetry.dispatchedRetries + 1}`, now));
2606
+ const reopenedRun = reopenRunForProviderRetry(run, `provider-retry-${run.providerRetry.episodeId}-${run.providerRetry.dispatchedRetries + 1}`, now, recoveryMode);
2607
+ store.saveAgentRun(reopenedRun);
2608
+ if (sessions !== null && sessions.inFlight !== null) {
2609
+ store.saveTaskRoleSessionSet(prepareTaskRoleRunRedispatch(sessions, {
2610
+ agentId: run.effective.agentId,
2611
+ runId: run.id,
2612
+ receiptId: agentRunDeliveryReceiptId(reopenedRun)
2613
+ }, now));
2614
+ }
2429
2615
  // The retry decision settled the original delivery claim; re-queue the
2430
2616
  // Role mailbox so the delivery pass can claim and re-push the Run.
2431
2617
  enqueueWork(store, { kind: "role", taskId: entry.taskId, roleName: entry.roleName }, "provider-retry-repush", now, [{ type: "run", taskId: entry.taskId, id: run.id }]);
@@ -2444,7 +2630,7 @@ export class FileSchedulerStoreAdapter {
2444
2630
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2445
2631
  const run = store.getAgentRun(taskId, input.fence.runId);
2446
2632
  if (sessions === null || sessions.providerBinding === null || run === null
2447
- || run.id !== sessions.inFlight?.runId
2633
+ || sessions.providerBinding.runId !== input.fence.runId
2448
2634
  || input.fence.conversationId === undefined) {
2449
2635
  recordCanonicalObservationObsolete(store, input, "provider-binding-missing", now);
2450
2636
  return "obsolete";
@@ -2532,16 +2718,7 @@ export class FileSchedulerStoreAdapter {
2532
2718
  status: "running",
2533
2719
  effective: run.effective
2534
2720
  }, now);
2535
- const withProvider = bound.providerBinding !== null || bound.inFlight === null
2536
- ? bound
2537
- : bindTaskRoleProviderRuntime(bound, createProviderRuntimeBinding({
2538
- providerNamespace: input.fence.driverId,
2539
- accountScope: input.fence.agentId,
2540
- runId: bound.inFlight.runId,
2541
- conversationId: input.fence.conversationId ?? decision.outcome.nativeSessionId,
2542
- activationId: input.fence.activationId ?? input.fence.launchId,
2543
- startedAt: bound.inFlight.preparedAt
2544
- }), now);
2721
+ const withProvider = bindOrSupersedeProviderRuntime(bound, input, now);
2545
2722
  store.saveTaskRoleSessionSet(withProvider);
2546
2723
  const driver = this.drivers.require(input.fence.driverId);
2547
2724
  if (driver.capabilities.observation.sessionBootstrap === "discovered"
@@ -2556,17 +2733,9 @@ export class FileSchedulerStoreAdapter {
2556
2733
  const current = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2557
2734
  const currentSession = current?.sessions[input.fence.agentId];
2558
2735
  if (current !== null && current !== undefined
2559
- && current.providerBinding === null
2560
2736
  && current.inFlight !== null
2561
2737
  && currentSession?.nativeSessionId === input.fence.nativeSessionId) {
2562
- store.saveTaskRoleSessionSet(bindTaskRoleProviderRuntime(current, createProviderRuntimeBinding({
2563
- providerNamespace: input.fence.driverId,
2564
- accountScope: input.fence.agentId,
2565
- runId: current.inFlight.runId,
2566
- conversationId: input.fence.conversationId ?? input.fence.nativeSessionId,
2567
- activationId: input.fence.activationId ?? input.fence.launchId,
2568
- startedAt: current.inFlight.preparedAt
2569
- }), now));
2738
+ store.saveTaskRoleSessionSet(bindOrSupersedeProviderRuntime(current, input, now));
2570
2739
  }
2571
2740
  return "applied";
2572
2741
  }
@@ -2591,6 +2760,32 @@ export class FileSchedulerStoreAdapter {
2591
2760
  && inputDelivery.executionRef.taskId === input.fence.taskId
2592
2761
  && inputDelivery.executionRef.id === input.fence.runId
2593
2762
  && inputDelivery.attemptId !== formatAgentRunReceiptId(input.fence.taskId, input.fence.runId);
2763
+ const humanAttemptId = input.fence.receiptId;
2764
+ if (humanAttemptId !== undefined && humanAttemptId.startsWith("human:")) {
2765
+ const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
2766
+ const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2767
+ const session = sessions?.sessions[input.fence.agentId];
2768
+ const binding = sessions?.providerBinding;
2769
+ if (active === null || active.id !== input.fence.runId
2770
+ || sessions === null || sessions === undefined
2771
+ || binding === null || binding === undefined
2772
+ || binding.authority.owner !== "human"
2773
+ || !humanAttemptId.startsWith(`human:${binding.authority.holderId}:`)
2774
+ || binding.turn?.attemptId !== humanAttemptId
2775
+ || session?.launchId !== input.fence.launchId
2776
+ || session.nativeSessionId !== input.fence.nativeSessionId) {
2777
+ recordCanonicalObservationObsolete(store, input, "human-turn-fence-mismatch", now);
2778
+ return "obsolete";
2779
+ }
2780
+ store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(updateRoleAgentSessionStatus(sessions, input.fence.agentId, "running", now), input, now));
2781
+ store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "run.human-input-delivered", {
2782
+ attemptId: humanAttemptId,
2783
+ runId: active.id,
2784
+ authorityEpoch: String(binding.authority.epoch),
2785
+ nativeTurnId: input.fence.nativeTurnId
2786
+ }, now));
2787
+ return "applied";
2788
+ }
2594
2789
  if (continuation) {
2595
2790
  const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
2596
2791
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
@@ -2605,7 +2800,7 @@ export class FileSchedulerStoreAdapter {
2605
2800
  recordCanonicalObservationObsolete(store, input, "continuation-fence-mismatch", now);
2606
2801
  return "obsolete";
2607
2802
  }
2608
- store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, input.fence.agentId, "running", now));
2803
+ store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(updateRoleAgentSessionStatus(sessions, input.fence.agentId, "running", now), input, now));
2609
2804
  const role = store.getRole(input.fence.taskId, input.fence.roleName);
2610
2805
  if (role !== null && role.status !== "running") {
2611
2806
  store.saveRole(input.fence.taskId, updateRoleStatus(role, "running", now));
@@ -2652,6 +2847,10 @@ export class FileSchedulerStoreAdapter {
2652
2847
  recordCanonicalObservationObsolete(store, input, "role-missing", now);
2653
2848
  return "obsolete";
2654
2849
  }
2850
+ const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2851
+ if (sessions !== null) {
2852
+ store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
2853
+ }
2655
2854
  const progressed = clearProviderRetryOnProgress(active);
2656
2855
  if (progressed !== active) {
2657
2856
  store.saveAgentRun(progressed);
@@ -3641,6 +3840,133 @@ function compactedRuntimeObservationIds(events, incoming) {
3641
3840
  };
3642
3841
  return existing.filter(remove).map(({ event }) => event.id);
3643
3842
  }
3843
+ function recordStructuredProviderAcceptance(sessions, input, now) {
3844
+ const current = sessions.providerBinding;
3845
+ const attemptId = input.fence.receiptId;
3846
+ const turnId = input.fence.nativeTurnId;
3847
+ if (current === null || attemptId === undefined || turnId === undefined)
3848
+ return sessions;
3849
+ if (current.turn?.attemptId === attemptId
3850
+ && current.turn.turnId === turnId
3851
+ && ["accepted", "running", "completed", "failed", "cancelled"].includes(current.turn.status))
3852
+ return sessions;
3853
+ const binding = acceptProviderTurn(current, {
3854
+ attemptId,
3855
+ turnId,
3856
+ acceptedAt: input.observedAt ?? input.receivedAt
3857
+ });
3858
+ return updateTaskRoleProviderRuntime(sessions, binding, now);
3859
+ }
3860
+ function bindOrSupersedeProviderRuntime(sessions, input, now) {
3861
+ if (sessions.inFlight === null)
3862
+ return sessions;
3863
+ const conversationId = input.fence.conversationId ?? input.fence.nativeSessionId;
3864
+ const activationId = input.fence.activationId ?? input.fence.launchId;
3865
+ if (sessions.providerBinding === null) {
3866
+ return bindTaskRoleProviderRuntime(sessions, createProviderRuntimeBinding({
3867
+ providerNamespace: input.fence.driverId,
3868
+ accountScope: input.fence.agentId,
3869
+ runId: sessions.inFlight.runId,
3870
+ conversationId,
3871
+ activationId,
3872
+ startedAt: sessions.inFlight.preparedAt
3873
+ }), now);
3874
+ }
3875
+ const current = currentProviderConversation(sessions.providerBinding);
3876
+ if (current.conversationId === conversationId) {
3877
+ const active = currentProviderActivation(sessions.providerBinding);
3878
+ if (active?.activationId === activationId)
3879
+ return sessions;
3880
+ if (active !== null) {
3881
+ throw new Error("Provider Conversation cannot start a second Activation while the current one is active.");
3882
+ }
3883
+ return updateTaskRoleProviderRuntime(sessions, startProviderActivation(sessions.providerBinding, {
3884
+ activationId,
3885
+ startedAt: input.observedAt ?? input.receivedAt
3886
+ }), now);
3887
+ }
3888
+ const turn = sessions.providerBinding.turn;
3889
+ const noUnsettledInputDelivery = turn === null
3890
+ || ["completed", "failed", "cancelled", "rejected"].includes(turn.status);
3891
+ return updateTaskRoleProviderRuntime(sessions, supersedeProviderConversation(sessions.providerBinding, {
3892
+ conversationId,
3893
+ activationId,
3894
+ switchedAt: input.observedAt ?? input.receivedAt,
3895
+ noUnsettledInputDelivery
3896
+ }), now);
3897
+ }
3898
+ function rejectSubmittingProviderTurn(store, target, delivery, now, reason) {
3899
+ if (target.kind !== "role"
3900
+ || delivery.executionRef.type !== "run"
3901
+ || delivery.executionRef.taskId !== target.taskId)
3902
+ return;
3903
+ const sessions = store.getTaskRoleSessionSet(target.taskId, target.roleName);
3904
+ const binding = sessions?.providerBinding;
3905
+ if (sessions === null || sessions === undefined
3906
+ || binding === null || binding === undefined
3907
+ || binding.runId !== delivery.executionRef.id
3908
+ || binding.turn?.attemptId !== delivery.attemptId
3909
+ || (binding.turn.status !== "submitting"
3910
+ && binding.turn.status !== "delivery-unknown"))
3911
+ return;
3912
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, rejectProviderTurn(binding, {
3913
+ attemptId: delivery.attemptId,
3914
+ rejectedAt: now.toISOString(),
3915
+ reason
3916
+ }), now));
3917
+ }
3918
+ function deferUnprovenProviderRecovery(store, run, roleName, reason, now) {
3919
+ if (run.providerRetry === undefined)
3920
+ return;
3921
+ const deferred = deferProviderRetry(run.providerRetry, now);
3922
+ if (deferred === null) {
3923
+ finalizeProviderRetryDeadline(store, run, reason, "native-resume-unproven", now);
3924
+ return;
3925
+ }
3926
+ store.saveAgentRun(withProviderRetry(run, deferred));
3927
+ store.saveEvent(run.taskId, createTaskEvent(store.nextEventId(run.taskId), run.taskId, "runtime.provider-recovery-attention", {
3928
+ runId: run.id,
3929
+ roleName,
3930
+ reason,
3931
+ nextAttemptAt: deferred.nextAttemptAt ?? ""
3932
+ }, now));
3933
+ enqueueWork(store, { kind: "operator" }, "provider-recovery-attention", now, [
3934
+ { type: "task", id: run.taskId },
3935
+ { type: "run", taskId: run.taskId, id: run.id }
3936
+ ]);
3937
+ }
3938
+ function markSubmittingProviderTurnUnknown(store, target, delivery, now, reason) {
3939
+ if (target.kind !== "role"
3940
+ || delivery.executionRef.type !== "run"
3941
+ || delivery.executionRef.taskId !== target.taskId)
3942
+ return;
3943
+ const sessions = store.getTaskRoleSessionSet(target.taskId, target.roleName);
3944
+ const binding = sessions?.providerBinding;
3945
+ if (sessions === null || sessions === undefined
3946
+ || binding === null || binding === undefined
3947
+ || binding.runId !== delivery.executionRef.id
3948
+ || binding.turn?.attemptId !== delivery.attemptId
3949
+ || binding.turn.status !== "submitting")
3950
+ return;
3951
+ store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, markProviderTurnDeliveryUnknown(binding, {
3952
+ attemptId: delivery.attemptId,
3953
+ observedAt: now.toISOString(),
3954
+ reason
3955
+ }), now));
3956
+ }
3957
+ function settleStructuredProviderTurn(sessions, turnId, status, now) {
3958
+ const binding = sessions.providerBinding;
3959
+ if (binding === null || binding.turn === null || binding.turn.turnId !== turnId) {
3960
+ return sessions;
3961
+ }
3962
+ if (["completed", "failed", "cancelled"].includes(binding.turn.status))
3963
+ return sessions;
3964
+ return updateTaskRoleProviderRuntime(sessions, settleProviderTurn(binding, {
3965
+ turnId,
3966
+ status,
3967
+ settledAt: now.toISOString()
3968
+ }), now);
3969
+ }
3644
3970
  function confirmedUsageActivityObservation(events, incoming) {
3645
3971
  const usage = incoming.payload.usage;
3646
3972
  if (incoming.kind !== "activity.observed" || usage === undefined)
@@ -3695,3 +4021,7 @@ function compareCanonicalObservationOrder(left, right) {
3695
4021
  || (left.ordinal ?? -1) - (right.ordinal ?? -1)
3696
4022
  || left.eventId.localeCompare(right.eventId);
3697
4023
  }
4024
+ function configuredProviderRetryPolicy(store) {
4025
+ const config = providerRetryConfig(store.getConfig());
4026
+ return { delaysMs: config.delaysMs, maxWindowMs: config.maxWindowMs };
4027
+ }