@zq-silk/yui 0.8.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 (38) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +35 -46
  3. package/dist/cli/commandCatalog.js +49 -14
  4. package/dist/cli/interactionPolicy.js +4 -10
  5. package/dist/cli/invocationRouter.js +2 -1
  6. package/dist/cli.js +73 -21
  7. package/dist/commands/taskCommands.js +108 -53
  8. package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
  9. package/dist/controller/resourceInventory.js +9 -5
  10. package/dist/controller/runtime.js +80 -7
  11. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  12. package/dist/controller/structuredProviderObservation.js +273 -0
  13. package/dist/executor/agentAdapter.js +40 -0
  14. package/dist/executor/agentExecutor.js +31 -7
  15. package/dist/executor/executorRegistry.js +11 -49
  16. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  17. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  18. package/dist/run/agentRun.js +2 -2
  19. package/dist/runtime/agentHost.js +767 -158
  20. package/dist/runtime/builtinAgentDrivers.js +1 -5
  21. package/dist/runtime/codexAppServerRuntime.js +67 -60
  22. package/dist/runtime/exactControlPlane.js +7 -2
  23. package/dist/runtime/index.js +6 -2
  24. package/dist/runtime/launchBroker.js +30 -8
  25. package/dist/runtime/providerAuthorityFence.js +24 -0
  26. package/dist/runtime/providerControl.js +63 -0
  27. package/dist/runtime/providerRecoveryDecision.js +55 -0
  28. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  29. package/dist/runtime/runtimeBinding.js +20 -11
  30. package/dist/runtime/structuredProviderHost.js +476 -0
  31. package/dist/runtime/tmuxAdapters.js +143 -42
  32. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  33. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  34. package/dist/storage/migration/productionRegistry.js +111 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/tmux/tmuxManager.js +1 -1
  37. package/i18n/README.zh-CN.md +11 -8
  38. package/package.json +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;
@@ -812,42 +820,129 @@ export class FileSchedulerStoreAdapter {
812
820
  const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
813
821
  return sessions !== null && sessions.inFlight !== null;
814
822
  }
815
- getActiveProviderTurnFence(input) {
816
- const observations = this.store.listEvents(input.taskId)
817
- .map(runtimeObservationFromTaskEvent)
818
- .filter((entry) => entry !== null
819
- && entry.fence.runId === input.runId
820
- && entry.fence.roleName === input.roleName
821
- && entry.fence.agentId === input.agentId
822
- && entry.fence.launchId === input.launchId
823
- && entry.fence.nativeSessionId === input.nativeSessionId
824
- && entry.fence.nativeTurnId !== undefined)
825
- .sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
826
- || (left.sequence ?? -1) - (right.sequence ?? -1)
827
- || (left.ordinal ?? -1) - (right.ordinal ?? -1)
828
- || left.semanticKey.localeCompare(right.semanticKey)));
829
- const state = new Map();
830
- const fences = new Map();
831
- for (const observation of observations) {
832
- const turnId = observation.fence.nativeTurnId;
833
- if (observation.kind === "turn.accepted" || observation.kind === "input.accepted") {
834
- state.set(turnId, "active");
835
- fences.set(turnId, observation);
836
- }
837
- else if (["turn.completed", "turn.failed", "turn.cancelled"].includes(observation.kind)) {
838
- state.set(turnId, "terminal");
839
- }
840
- }
841
- const active = [...state.entries()].filter(([, status]) => status === "active");
842
- 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)
843
892
  return null;
844
- const observation = fences.get(active[0][0]);
845
893
  return {
846
- conversationId: observation.fence.conversationId ?? input.nativeSessionId,
847
- activationId: observation.fence.activationId ?? input.launchId,
848
- nativeTurnId: active[0][0]
894
+ conversationId: currentProviderConversation(binding).conversationId,
895
+ activationId: activation.activationId,
896
+ ...binding.authority
849
897
  };
850
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
+ }
851
946
  isRoleGenerationProviderReady(input) {
852
947
  // A session.ready observation for this exact generation is the durable
853
948
  // pre-input fence. This reads folded Driver truth
@@ -971,6 +1066,31 @@ export class FileSchedulerStoreAdapter {
971
1066
  executionRef: input.executionRef,
972
1067
  providerFence: input.providerFence
973
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
+ }
974
1094
  store.saveWorkMailbox(claimed);
975
1095
  return { status: "claimed", delivery: claimed.inputDelivery };
976
1096
  });
@@ -1009,20 +1129,22 @@ export class FileSchedulerStoreAdapter {
1009
1129
  return true;
1010
1130
  });
1011
1131
  }
1012
- releaseInputDelivery(target, attemptId) {
1132
+ releaseInputDelivery(target, attemptId, now) {
1013
1133
  return this.store.transaction((store) => {
1014
1134
  const mailbox = store.getWorkMailbox(target);
1015
1135
  if (mailbox?.inputDelivery?.attemptId !== attemptId)
1016
1136
  return false;
1137
+ rejectSubmittingProviderTurn(store, target, mailbox.inputDelivery, now, "Provider write was not attempted.");
1017
1138
  store.saveWorkMailbox(releaseMailboxInputDelivery(mailbox, attemptId));
1018
1139
  return true;
1019
1140
  });
1020
1141
  }
1021
- resolveInputDeliveryNotAccepted(target, attemptId) {
1142
+ resolveInputDeliveryNotAccepted(target, attemptId, now) {
1022
1143
  return this.store.transaction((store) => {
1023
1144
  const mailbox = store.getWorkMailbox(target);
1024
1145
  if (mailbox?.inputDelivery?.attemptId !== attemptId)
1025
1146
  return false;
1147
+ rejectSubmittingProviderTurn(store, target, mailbox.inputDelivery, now, "Provider returned an exact negative acknowledgement.");
1026
1148
  store.saveWorkMailbox(resolveMailboxInputDeliveryNotAccepted(mailbox, attemptId));
1027
1149
  return true;
1028
1150
  });
@@ -1033,6 +1155,7 @@ export class FileSchedulerStoreAdapter {
1033
1155
  if (mailbox?.inputDelivery?.attemptId !== attemptId)
1034
1156
  return false;
1035
1157
  const unknown = markMailboxInputDeliveryUnknown(mailbox, attemptId, reason, now);
1158
+ markSubmittingProviderTurnUnknown(store, target, mailbox.inputDelivery, now, reason);
1036
1159
  store.saveWorkMailbox(unknown);
1037
1160
  const taskId = target.kind === "role" ? target.taskId : undefined;
1038
1161
  if (taskId !== undefined) {
@@ -1872,6 +1995,7 @@ export class FileSchedulerStoreAdapter {
1872
1995
  : sessions.inFlight === null ? idleStatus : "running",
1873
1996
  effective
1874
1997
  }, now);
1998
+ sessions = settleStructuredProviderTurn(sessions, input.turnId, input.providerStatus ?? "completed", now);
1875
1999
  if (sessions.inFlight === null) {
1876
2000
  sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, input.turnId, now);
1877
2001
  store.saveTaskRoleSessionSet(sessions);
@@ -2137,16 +2261,9 @@ export class FileSchedulerStoreAdapter {
2137
2261
  }
2138
2262
  const retry = retryDecision.retry;
2139
2263
  store.saveAgentRun(withProviderRetry(run, retry));
2140
- // Clear the in-flight fence so the original Session can be re-bound and
2141
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
2142
- // re-pushed by the delivery path; the Session itself stays alive.
2143
- if (sessions !== null && sessions.inFlight !== null) {
2144
- store.saveTaskRoleSessionSet(clearTaskRoleRun(sessions, {
2145
- agentId: input.agentId,
2146
- runId: input.runId,
2147
- receiptId: agentRunDeliveryReceiptId(run)
2148
- }, now));
2149
- }
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.
2150
2267
  // Settle the delivery claim so the retry push can re-claim the mailbox.
2151
2268
  const target = runtimeLifecycleTarget({
2152
2269
  scope: "task",
@@ -2387,7 +2504,61 @@ export class FileSchedulerStoreAdapter {
2387
2504
  || !providerRetryIsDue(run.providerRetry, now)) {
2388
2505
  return;
2389
2506
  }
2390
- 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
+ }
2391
2562
  const session = sessions?.sessions[run.effective.agentId];
2392
2563
  const identityMatches = session !== undefined
2393
2564
  && session.adapterId === run.effective.adapterId
@@ -2432,7 +2603,15 @@ export class FileSchedulerStoreAdapter {
2432
2603
  // Reopen the Run on its original Session: reset transport markers,
2433
2604
  // switch to resume, and mark the projection in-flight. The delivery
2434
2605
  // pass re-pushes the exact same input in this same pass.
2435
- 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
+ }
2436
2615
  // The retry decision settled the original delivery claim; re-queue the
2437
2616
  // Role mailbox so the delivery pass can claim and re-push the Run.
2438
2617
  enqueueWork(store, { kind: "role", taskId: entry.taskId, roleName: entry.roleName }, "provider-retry-repush", now, [{ type: "run", taskId: entry.taskId, id: run.id }]);
@@ -2451,7 +2630,7 @@ export class FileSchedulerStoreAdapter {
2451
2630
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2452
2631
  const run = store.getAgentRun(taskId, input.fence.runId);
2453
2632
  if (sessions === null || sessions.providerBinding === null || run === null
2454
- || run.id !== sessions.inFlight?.runId
2633
+ || sessions.providerBinding.runId !== input.fence.runId
2455
2634
  || input.fence.conversationId === undefined) {
2456
2635
  recordCanonicalObservationObsolete(store, input, "provider-binding-missing", now);
2457
2636
  return "obsolete";
@@ -2539,16 +2718,7 @@ export class FileSchedulerStoreAdapter {
2539
2718
  status: "running",
2540
2719
  effective: run.effective
2541
2720
  }, now);
2542
- const withProvider = bound.providerBinding !== null || bound.inFlight === null
2543
- ? bound
2544
- : bindTaskRoleProviderRuntime(bound, createProviderRuntimeBinding({
2545
- providerNamespace: input.fence.driverId,
2546
- accountScope: input.fence.agentId,
2547
- runId: bound.inFlight.runId,
2548
- conversationId: input.fence.conversationId ?? decision.outcome.nativeSessionId,
2549
- activationId: input.fence.activationId ?? input.fence.launchId,
2550
- startedAt: bound.inFlight.preparedAt
2551
- }), now);
2721
+ const withProvider = bindOrSupersedeProviderRuntime(bound, input, now);
2552
2722
  store.saveTaskRoleSessionSet(withProvider);
2553
2723
  const driver = this.drivers.require(input.fence.driverId);
2554
2724
  if (driver.capabilities.observation.sessionBootstrap === "discovered"
@@ -2563,17 +2733,9 @@ export class FileSchedulerStoreAdapter {
2563
2733
  const current = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2564
2734
  const currentSession = current?.sessions[input.fence.agentId];
2565
2735
  if (current !== null && current !== undefined
2566
- && current.providerBinding === null
2567
2736
  && current.inFlight !== null
2568
2737
  && currentSession?.nativeSessionId === input.fence.nativeSessionId) {
2569
- store.saveTaskRoleSessionSet(bindTaskRoleProviderRuntime(current, createProviderRuntimeBinding({
2570
- providerNamespace: input.fence.driverId,
2571
- accountScope: input.fence.agentId,
2572
- runId: current.inFlight.runId,
2573
- conversationId: input.fence.conversationId ?? input.fence.nativeSessionId,
2574
- activationId: input.fence.activationId ?? input.fence.launchId,
2575
- startedAt: current.inFlight.preparedAt
2576
- }), now));
2738
+ store.saveTaskRoleSessionSet(bindOrSupersedeProviderRuntime(current, input, now));
2577
2739
  }
2578
2740
  return "applied";
2579
2741
  }
@@ -2598,6 +2760,32 @@ export class FileSchedulerStoreAdapter {
2598
2760
  && inputDelivery.executionRef.taskId === input.fence.taskId
2599
2761
  && inputDelivery.executionRef.id === input.fence.runId
2600
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
+ }
2601
2789
  if (continuation) {
2602
2790
  const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
2603
2791
  const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
@@ -2612,7 +2800,7 @@ export class FileSchedulerStoreAdapter {
2612
2800
  recordCanonicalObservationObsolete(store, input, "continuation-fence-mismatch", now);
2613
2801
  return "obsolete";
2614
2802
  }
2615
- store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, input.fence.agentId, "running", now));
2803
+ store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(updateRoleAgentSessionStatus(sessions, input.fence.agentId, "running", now), input, now));
2616
2804
  const role = store.getRole(input.fence.taskId, input.fence.roleName);
2617
2805
  if (role !== null && role.status !== "running") {
2618
2806
  store.saveRole(input.fence.taskId, updateRoleStatus(role, "running", now));
@@ -2659,6 +2847,10 @@ export class FileSchedulerStoreAdapter {
2659
2847
  recordCanonicalObservationObsolete(store, input, "role-missing", now);
2660
2848
  return "obsolete";
2661
2849
  }
2850
+ const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
2851
+ if (sessions !== null) {
2852
+ store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
2853
+ }
2662
2854
  const progressed = clearProviderRetryOnProgress(active);
2663
2855
  if (progressed !== active) {
2664
2856
  store.saveAgentRun(progressed);
@@ -3648,6 +3840,133 @@ function compactedRuntimeObservationIds(events, incoming) {
3648
3840
  };
3649
3841
  return existing.filter(remove).map(({ event }) => event.id);
3650
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
+ }
3651
3970
  function confirmedUsageActivityObservation(events, incoming) {
3652
3971
  const usage = incoming.payload.usage;
3653
3972
  if (incoming.kind !== "activity.observed" || usage === undefined)
@@ -113,16 +113,17 @@ export function buildControllerResourceInventory(facts) {
113
113
  for (const process of paneProcesses)
114
114
  claimed.add(process.pid);
115
115
  const role = findRole(homeFact.roles, pane);
116
- const archived = role?.taskStatus === "archived";
116
+ const terminalIsolation = role?.taskStatus === "retired"
117
+ || role?.taskStatus === "archived";
117
118
  resources.push(processResource({
118
119
  kind: "agent-session",
119
120
  state: pane.dead ? "dead" : role === undefined ? "orphaned" : "running",
120
121
  disposition: role === undefined
121
122
  ? pane.dead ? "safe" : "review"
122
- : archived ? "safe" : "protected",
123
+ : terminalIsolation ? "safe" : "protected",
123
124
  reasonCode: role === undefined
124
125
  ? pane.dead ? "dead-orphan-pane" : "orphan-pane"
125
- : archived ? "archived-task-pane" : "owned-role-pane",
126
+ : terminalIsolation ? `${role.taskStatus}-task-pane` : "owned-role-pane",
126
127
  yuiHome,
127
128
  owner: role === undefined ? { kind: "none" } : roleOwner(role),
128
129
  processes: paneProcesses,
@@ -359,7 +360,10 @@ function artifactResource(artifact, yuiHome, domain, homeId) {
359
360
  ...(domain === undefined ? {} : { domain })
360
361
  };
361
362
  }
362
- function domainDisposition(base, _reason, domain, target) {
363
+ function domainDisposition(base, reason, domain, target) {
364
+ if (reason === "retired-task-pane" || reason === "archived-task-pane") {
365
+ return "safe";
366
+ }
363
367
  if (domain === undefined)
364
368
  return base;
365
369
  if (domain.disposition === "safe") {
@@ -377,7 +381,7 @@ function domainDisposition(base, _reason, domain, target) {
377
381
  // A tmux server with panes is protected until the pane resources have
378
382
  // been removed and a later bounded pass can revalidate the empty
379
383
  // server. This avoids killing a server while a target race is in flight.
380
- if (base === "protected" && _reason === "owned-tmux-server")
384
+ if (base === "protected" && reason === "owned-tmux-server")
381
385
  return "protected";
382
386
  // `report-only` is deliberately reserved for an unrecognized process;
383
387
  // the YUI_HOME environment alone is not a cleanup ownership proof.