@zq-silk/yui 0.15.3 → 0.15.6

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 (41) hide show
  1. package/dist/agent/managedRuntimeEnvironment.js +3 -0
  2. package/dist/cli.js +35 -127
  3. package/dist/commands/executionAuditCommands.js +6 -0
  4. package/dist/commands/taskContextCommand.js +4 -2
  5. package/dist/context/sessionBootstrapManifest.js +12 -21
  6. package/dist/controller/clientRuntime.js +1 -1
  7. package/dist/controller/fileSchedulerStoreAdapter.js +270 -162
  8. package/dist/controller/runtimeEventInbox.js +8 -0
  9. package/dist/controller/runtimeEventProcessor.js +31 -4
  10. package/dist/controller/runtimeHookTurnFence.js +101 -62
  11. package/dist/controller/runtimeLaunchCoordinator.js +38 -12
  12. package/dist/controller/runtimeObservationHook.js +17 -1
  13. package/dist/controller/structuredProviderObservation.js +39 -27
  14. package/dist/core/controllerClient.js +5 -0
  15. package/dist/core/controllerServer.js +7 -4
  16. package/dist/domain/agentResultTransport.js +2 -2
  17. package/dist/executor/agentExecutor.js +22 -38
  18. package/dist/executor/executorRegistry.js +16 -5
  19. package/dist/executor/fileRoleLaunchPlanner.js +22 -28
  20. package/dist/lifecycle/exactTurnTerminalization.js +3 -3
  21. package/dist/observability/executionAudit.js +12 -0
  22. package/dist/repository/executionLaneGitSnapshot.js +4 -3
  23. package/dist/repository/taskWorkspacePreparer.js +12 -4
  24. package/dist/review/taskFinalReviewContract.js +13 -32
  25. package/dist/runtime/agentError.js +299 -12
  26. package/dist/runtime/agentHost.js +419 -41
  27. package/dist/runtime/builtinAgentDrivers.js +5 -0
  28. package/dist/runtime/index.js +1 -1
  29. package/dist/runtime/ports.js +16 -2
  30. package/dist/runtime/providerRuntimeIdentity.js +34 -28
  31. package/dist/runtime/runtimeCoherence.js +91 -0
  32. package/dist/runtime/runtimeObservation.js +8 -5
  33. package/dist/runtime/structuredProviderHost.js +53 -44
  34. package/dist/runtime/tmuxAdapters.js +72 -43
  35. package/dist/scheduler/activeRoleTurnDelivery.js +59 -11
  36. package/dist/scheduler/leaderWakeupProcessor.js +61 -7
  37. package/dist/storage/sqliteSchema.js +9 -0
  38. package/dist/storage/storageVersions.js +1 -1
  39. package/dist/turn/turn.js +7 -1
  40. package/package.json +1 -1
  41. package/dist/runtime/exactControlPlane.js +0 -232
@@ -16,7 +16,7 @@ import { validateRuntimeProcessExitObservation } from "./processExitObservation.
16
16
  import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
17
17
  import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
18
18
  import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
19
- import { serializeAgentErrorRaw } from "./agentError.js";
19
+ import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
20
20
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
21
21
  export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v4";
22
22
  const HOST_CONTROL_MAX_BYTES = 32 * 1024;
@@ -48,7 +48,7 @@ export async function runAgentHost(input) {
48
48
  let snapshot = hostSnapshot("idle");
49
49
  let dispatchTail = Promise.resolve();
50
50
  let promptHuman = () => { };
51
- const recentSteerAttempts = new Set();
51
+ const recentSteerAttempts = new Map();
52
52
  await replayExitOutbox(input.home);
53
53
  const updateSnapshot = (next) => {
54
54
  snapshot = validateSnapshot(next);
@@ -114,11 +114,34 @@ export async function runAgentHost(input) {
114
114
  return;
115
115
  }
116
116
  const terminalPayload = activeTurnPayload;
117
- if (terminalPayload === undefined)
117
+ if (terminal.attemptId !== undefined && terminal.attemptId !== activeTurnAttemptId
118
+ && sessionPayload !== undefined && terminal.nativeSessionId === session?.nativeSessionId) {
119
+ // A duplicate/late terminal retains the Driver's exact old attempt.
120
+ // The durable observer validates that binding; it must not alter the
121
+ // current Host occupancy or inherit the successor's managed Turn.
122
+ const observedPayload = sessionPayload;
123
+ void enqueueSerialized(async () => {
124
+ await publishStructuredProviderTerminal({
125
+ home: input.home,
126
+ environment: observedPayload.environment,
127
+ activationId: activationId ?? observedPayload.runtimeGenerationId,
128
+ terminal
129
+ });
130
+ signalRoleMailbox(input.home, observedPayload);
131
+ }).catch(() => { });
118
132
  return;
133
+ }
134
+ if (terminalPayload === undefined
135
+ || terminal.attemptId !== activeTurnAttemptId
136
+ || terminal.nativeSessionId !== session?.nativeSessionId
137
+ || (terminal.nativeTurnId !== undefined && activeNativeTurnId !== undefined
138
+ && terminal.nativeTurnId !== activeNativeTurnId))
139
+ return;
140
+ const terminalAttemptId = terminal.attemptId;
119
141
  const terminalActivationId = activationId ?? terminalPayload.runtimeGenerationId;
120
142
  void enqueueSerialized(async () => {
121
- if (activeTurnPayload !== terminalPayload)
143
+ if (activeTurnPayload !== terminalPayload
144
+ || activeTurnAttemptId !== terminalAttemptId)
122
145
  return;
123
146
  if (session !== undefined) {
124
147
  updateSnapshot(hostSnapshot("settling", {
@@ -127,6 +150,7 @@ export async function runAgentHost(input) {
127
150
  processInstanceId: session.processInstanceId,
128
151
  nativeSessionId: terminal.nativeSessionId,
129
152
  conversationId: terminal.conversationId,
153
+ attemptId: terminalAttemptId,
130
154
  nativeTurnId: terminal.nativeTurnId,
131
155
  ...authorityFields()
132
156
  }));
@@ -569,21 +593,60 @@ export async function runAgentHost(input) {
569
593
  return operation;
570
594
  };
571
595
  const enqueueDispatch = (next) => (enqueueSerialized(() => dispatch(next)));
572
- const submitTurn = async (request) => {
596
+ const submitTurn = async (request, operation) => {
573
597
  if (session === undefined || sessionPayload === undefined) {
574
598
  throw new Error("Agent Host has no live Provider Conversation.");
575
599
  }
576
600
  if (request.nativeSessionId !== session.nativeSessionId) {
577
601
  throw new Error("Agent Host Turn targets a different Provider Conversation.");
578
602
  }
603
+ // A superseded generation is a real identity conflict, and the writer
604
+ // fence does not catch it: a Turn can carry an old generation under a
605
+ // fence that is still current. Without this the Host registered the stale
606
+ // Turn, wrote it to the Provider, and adopted the old generation into its
607
+ // own snapshot. Same generation is contention, which is decided below.
608
+ if (request.runtimeGenerationId !== sessionPayload.runtimeGenerationId) {
609
+ operation.failure = providerDeliveryFailure({
610
+ detail: "Agent Host Turn targets a superseded runtime generation.",
611
+ errorName: "ProviderGenerationConflictError",
612
+ phase: "turn-submit",
613
+ hostState: snapshot.state,
614
+ expectedRuntimeGenerationId: sessionPayload.runtimeGenerationId,
615
+ observedRuntimeGenerationId: request.runtimeGenerationId,
616
+ attemptId: request.turn.attemptId,
617
+ inputDisposition: "not-accepted",
618
+ registrationDisposition: "not-committed",
619
+ // A stale writer does not make the live Session unusable.
620
+ sessionDisposition: "recoverable"
621
+ });
622
+ throw new Error("Agent Host Turn targets a superseded runtime generation: expected "
623
+ + `${sessionPayload.runtimeGenerationId}, observed ${request.runtimeGenerationId}.`);
624
+ }
579
625
  if (authority === undefined
580
626
  || !sameProviderAuthorityFence(authority, request.authority)) {
581
627
  throw new Error("Agent Host rejected a stale Provider writer fence.");
582
628
  }
583
629
  if (activeTurnPayload !== undefined || snapshot.state === "settling") {
630
+ operation.failure = providerDeliveryFailure({
631
+ detail: "Agent Host still owns an unsettled Provider Turn.",
632
+ errorName: "ProviderTurnBusyError",
633
+ phase: "turn-submit",
634
+ hostState: snapshot.state === "settling" ? "settling" : snapshot.state,
635
+ expectedRuntimeGenerationId: sessionPayload.runtimeGenerationId,
636
+ observedRuntimeGenerationId: request.runtimeGenerationId,
637
+ attemptId: request.turn.attemptId,
638
+ inputDisposition: "not-accepted",
639
+ registrationDisposition: "not-committed",
640
+ // Busy is a healthy Session doing work, never a reason to stop it.
641
+ sessionDisposition: "recoverable"
642
+ });
584
643
  throw new ProviderTurnBusyError("Agent Host still owns an unsettled Provider Turn.", request.turn.attemptId, activeNativeTurnId);
585
644
  }
586
645
  const { YUI_TURN_ID: _launchTurnId, ...baseEnvironment } = sessionPayload.environment;
646
+ // Steer attempts belong to the active native Turn. Never evict an unknown
647
+ // attempt while that Turn remains active, but don't retain another Turn's
648
+ // completed steering history after a new exact submission begins.
649
+ recentSteerAttempts.clear();
587
650
  activeTurnPayload = {
588
651
  ...sessionPayload,
589
652
  environment: {
@@ -603,11 +666,70 @@ export async function runAgentHost(input) {
603
666
  ...authorityFields()
604
667
  }));
605
668
  const durableTurn = hostTurnControlParams(sessionPayload, session.nativeSessionId, request.authority, request.turn.attemptId, request.turnId);
606
- await beginDurableProviderTurn(input.home, durableTurn);
669
+ // Registration precedes the Provider write, so its failure modes are not
670
+ // the Provider's. A definite failure means the Provider never saw this
671
+ // input and the occupancy must be released; anything unconfirmed leaves
672
+ // durable state ambiguous and keeps it held.
673
+ try {
674
+ await beginDurableProviderTurn(input.home, durableTurn);
675
+ }
676
+ catch (error) {
677
+ // Registration ends in one of three states and they are not
678
+ // interchangeable. A definite refusal committed nothing. A lost
679
+ // acknowledgement whose compensating resolve also failed is genuinely
680
+ // unknown, and releasing that occupancy would discard a durable record
681
+ // that may exist. A lost acknowledgement whose resolve succeeded is
682
+ // neither: the resolve is fenced on this exact attempt id and could not
683
+ // have succeeded unless the registration committed, so that attempt is
684
+ // known committed and already settled.
685
+ const registrationSettled = error instanceof ProviderTurnRegistrationSettledError;
686
+ const registrationUnknown = !registrationSettled
687
+ && (error instanceof ControllerAcknowledgementUnknownError
688
+ || error instanceof ProviderDeliveryUnknownError);
689
+ updateSnapshot(hostSnapshot(registrationUnknown ? "delivery-unknown" : "failed", {
690
+ runtimeGenerationId: request.runtimeGenerationId,
691
+ adapterId: session.adapterId,
692
+ processInstanceId: session.processInstanceId,
693
+ nativeSessionId: session.nativeSessionId,
694
+ conversationId: session.conversationId,
695
+ attemptId: request.turn.attemptId,
696
+ ...authorityFields(),
697
+ detail: registrationUnknown
698
+ ? `Provider Turn registration acknowledgement is unconfirmed; the Provider was not sent this input: ${errorText(error)}`
699
+ : registrationSettled
700
+ ? `Provider Turn registration was settled before any Provider write: ${errorText(error)}`
701
+ : `Provider Turn registration failed before any Provider write: ${errorText(error)}`
702
+ }));
703
+ // The Provider never saw the input in any branch. Only the durable
704
+ // registration is in question, so that is the fact that differs.
705
+ operation.failure = providerDeliveryFailureFrom(error, {
706
+ phase: "turn-submit",
707
+ hostState: snapshot.state,
708
+ expectedRuntimeGenerationId: sessionPayload.runtimeGenerationId,
709
+ observedRuntimeGenerationId: request.runtimeGenerationId,
710
+ attemptId: request.turn.attemptId,
711
+ inputDisposition: "not-accepted",
712
+ registrationDisposition: registrationUnknown
713
+ ? "unknown"
714
+ : registrationSettled ? "committed" : "not-committed",
715
+ sessionDisposition: "recoverable"
716
+ });
717
+ if (!registrationUnknown) {
718
+ // The Provider provably holds nothing, and the durable record either
719
+ // never existed or is settled. Release the Session for the next
720
+ // attempt instead of stranding it in a false `starting`.
721
+ activeTurnPayload = undefined;
722
+ activeTurnAttemptId = undefined;
723
+ activeNativeTurnId = undefined;
724
+ }
725
+ throw error;
726
+ }
607
727
  let providerAccepted = false;
728
+ let transportAccepted = false;
608
729
  try {
609
730
  const receipt = await session.submitTurn(request.turn);
610
- providerAccepted = true;
731
+ transportAccepted = true;
732
+ providerAccepted = receipt.acceptance === "provider";
611
733
  activeNativeTurnId = receipt.nativeTurnId;
612
734
  try {
613
735
  await publishStructuredProviderAccepted({
@@ -618,7 +740,10 @@ export async function runAgentHost(input) {
618
740
  });
619
741
  }
620
742
  catch (error) {
621
- const unknown = new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, request.turn.attemptId);
743
+ const unknown = new ProviderDeliveryUnknownError("Input submission returned a receipt but its durable acknowledgement could not be confirmed.", request.turn.attemptId, { cause: error });
744
+ // A resolve failure here must not escape before the snapshot is
745
+ // written: the Provider has already accepted, and losing that fact
746
+ // would report a definite non-acceptance for a live Turn.
622
747
  await resolveProviderTurnSubmission(input.home, durableTurn, unknown);
623
748
  throw unknown;
624
749
  }
@@ -635,10 +760,19 @@ export async function runAgentHost(input) {
635
760
  return snapshot;
636
761
  }
637
762
  catch (error) {
638
- if (!providerAccepted) {
639
- await resolveProviderTurnSubmission(input.home, durableTurn, error);
763
+ let failureError = error;
764
+ let settlementUnknown = false;
765
+ if (!transportAccepted) {
766
+ try {
767
+ await resolveProviderTurnSubmission(input.home, durableTurn, error);
768
+ }
769
+ catch (resolutionError) {
770
+ settlementUnknown = true;
771
+ failureError = resolutionError;
772
+ }
640
773
  }
641
- const deliveryUnknown = error instanceof ProviderDeliveryUnknownError || providerAccepted;
774
+ const inputUnknown = error instanceof ProviderDeliveryUnknownError;
775
+ const deliveryUnknown = inputUnknown || transportAccepted || settlementUnknown;
642
776
  const state = deliveryUnknown
643
777
  ? "delivery-unknown"
644
778
  : error instanceof ProviderTurnBusyError
@@ -652,39 +786,113 @@ export async function runAgentHost(input) {
652
786
  conversationId: session.conversationId,
653
787
  attemptId: request.turn.attemptId,
654
788
  ...authorityFields(),
655
- detail: errorText(error)
789
+ detail: errorText(failureError)
656
790
  }));
791
+ operation.failure = providerDeliveryFailureFrom(failureError, {
792
+ phase: "turn-submit",
793
+ hostState: state,
794
+ expectedRuntimeGenerationId: sessionPayload.runtimeGenerationId,
795
+ observedRuntimeGenerationId: request.runtimeGenerationId,
796
+ attemptId: request.turn.attemptId,
797
+ // Acceptance is observed, never inferred from the error class.
798
+ inputDisposition: providerAccepted
799
+ ? "accepted"
800
+ : inputUnknown || transportAccepted ? "unknown" : "not-accepted",
801
+ registrationDisposition: "committed",
802
+ sessionDisposition: state === "failed" ? "unknown" : "recoverable"
803
+ });
657
804
  if (state !== "delivery-unknown") {
658
805
  activeTurnPayload = undefined;
659
806
  activeTurnAttemptId = undefined;
660
807
  activeNativeTurnId = undefined;
661
808
  }
662
- if (deliveryUnknown && !(error instanceof ProviderDeliveryUnknownError)) {
663
- throw new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, request.turn.attemptId);
809
+ if (deliveryUnknown && !(failureError instanceof ProviderDeliveryUnknownError)) {
810
+ throw new ProviderDeliveryUnknownError("Provider accepted input but its durable acknowledgement could not be confirmed.", request.turn.attemptId, { cause: failureError });
664
811
  }
665
- throw error;
812
+ throw failureError;
666
813
  }
667
814
  };
668
- const steerTurn = async (request) => {
815
+ const steerTurn = async (request, operation) => {
669
816
  if (session === undefined || sessionPayload === undefined || activeTurnPayload === undefined) {
817
+ operation.failure = providerDeliveryFailure({
818
+ detail: "Agent Host has no active Provider Turn to steer.",
819
+ errorName: "ProviderTurnRejectedError",
820
+ phase: "turn-submit",
821
+ hostState: snapshot.state,
822
+ expectedRuntimeGenerationId: sessionPayload?.runtimeGenerationId
823
+ ?? snapshot.runtimeGenerationId ?? "none",
824
+ observedRuntimeGenerationId: request.runtimeGenerationId,
825
+ attemptId: request.turn.attemptId,
826
+ inputDisposition: "not-accepted",
827
+ sessionDisposition: "recoverable"
828
+ });
670
829
  throw new ProviderTurnRejectedError("Agent Host has no active Provider Turn to steer.", request.turn.attemptId);
671
830
  }
831
+ // Steer carried no generation check, so a steer issued against a
832
+ // superseded activation was written into the current Provider Turn. The
833
+ // native Turn id alone does not catch it: a late steer can arrive while
834
+ // the id it names is still the active one under a newer generation.
835
+ if (request.runtimeGenerationId !== sessionPayload.runtimeGenerationId) {
836
+ operation.failure = providerDeliveryFailure({
837
+ detail: "Agent Host steer targets a superseded runtime generation.",
838
+ errorName: "ProviderGenerationConflictError",
839
+ phase: "turn-submit",
840
+ hostState: snapshot.state,
841
+ expectedRuntimeGenerationId: sessionPayload.runtimeGenerationId,
842
+ observedRuntimeGenerationId: request.runtimeGenerationId,
843
+ attemptId: request.turn.attemptId,
844
+ inputDisposition: "not-accepted",
845
+ sessionDisposition: "recoverable"
846
+ });
847
+ throw new ProviderTurnRejectedError("Agent Host steer targets a superseded runtime generation: expected "
848
+ + `${sessionPayload.runtimeGenerationId}, observed ${request.runtimeGenerationId}.`, request.turn.attemptId);
849
+ }
672
850
  if (request.nativeSessionId !== session.nativeSessionId
673
851
  || request.nativeTurnId !== activeNativeTurnId) {
852
+ operation.failure = providerDeliveryFailure({
853
+ detail: "Agent Host steer targets a different Provider Turn.",
854
+ errorName: "ProviderTurnRejectedError",
855
+ phase: "turn-submit",
856
+ hostState: snapshot.state,
857
+ expectedRuntimeGenerationId: sessionPayload.runtimeGenerationId,
858
+ observedRuntimeGenerationId: request.runtimeGenerationId,
859
+ attemptId: request.turn.attemptId,
860
+ inputDisposition: "not-accepted",
861
+ sessionDisposition: "recoverable"
862
+ });
674
863
  throw new ProviderTurnRejectedError("Agent Host steer targets a different Provider Turn.", request.turn.attemptId);
675
864
  }
676
865
  if (authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
677
866
  throw new Error("Agent Host rejected a stale Provider writer fence.");
678
867
  }
679
- if (recentSteerAttempts.has(request.turn.attemptId))
680
- return snapshot;
681
- const receipt = await session.steerTurn(request.turn);
682
- if (receipt.nativeTurnId !== request.nativeTurnId) {
683
- throw new ProviderDeliveryUnknownError("Provider accepted steer against an unexpected native Turn.", request.turn.attemptId);
868
+ if (recentSteerAttempts.has(request.turn.attemptId)) {
869
+ const previous = recentSteerAttempts.get(request.turn.attemptId);
870
+ if (previous === undefined)
871
+ return snapshot;
872
+ operation.failure = previous;
873
+ throw new ProviderDeliveryUnknownError(previous.detail, request.turn.attemptId);
874
+ }
875
+ try {
876
+ const receipt = await session.steerTurn(request.turn);
877
+ if (receipt.nativeTurnId !== request.nativeTurnId) {
878
+ throw new ProviderDeliveryUnknownError("Provider accepted steer against an unexpected native Turn.", request.turn.attemptId);
879
+ }
880
+ recentSteerAttempts.set(request.turn.attemptId, undefined);
684
881
  }
685
- recentSteerAttempts.add(request.turn.attemptId);
686
- if (recentSteerAttempts.size > 256) {
687
- recentSteerAttempts.delete(recentSteerAttempts.values().next().value);
882
+ catch (error) {
883
+ const unknown = error instanceof ProviderDeliveryUnknownError;
884
+ operation.failure = providerDeliveryFailureFrom(error, {
885
+ phase: "turn-submit",
886
+ hostState: snapshot.state,
887
+ expectedRuntimeGenerationId: sessionPayload.runtimeGenerationId,
888
+ observedRuntimeGenerationId: request.runtimeGenerationId,
889
+ attemptId: request.turn.attemptId,
890
+ inputDisposition: unknown ? "unknown" : "not-accepted",
891
+ sessionDisposition: "unknown"
892
+ });
893
+ if (unknown)
894
+ recentSteerAttempts.set(request.turn.attemptId, operation.failure);
895
+ throw error;
688
896
  }
689
897
  return snapshot;
690
898
  };
@@ -721,17 +929,37 @@ export async function runAgentHost(input) {
721
929
  return controlResult("status", snapshot);
722
930
  }
723
931
  if (request.type === "submit-turn") {
724
- const accepted = await enqueueSerialized(() => submitTurn(request));
932
+ const accepted = await enqueueSerialized(async () => {
933
+ const operation = {};
934
+ try {
935
+ return await submitTurn(request, operation);
936
+ }
937
+ catch (error) {
938
+ throw new AgentHostOperationError(error, snapshot, operation.failure);
939
+ }
940
+ });
725
941
  return controlResult("accepted", accepted);
726
942
  }
727
943
  if (request.type === "steer-turn") {
728
- const accepted = await enqueueSerialized(() => steerTurn(request));
944
+ const accepted = await enqueueSerialized(async () => {
945
+ const operation = {};
946
+ try {
947
+ return await steerTurn(request, operation);
948
+ }
949
+ catch (error) {
950
+ throw new AgentHostOperationError(error, snapshot, operation.failure);
951
+ }
952
+ });
729
953
  return controlResult("accepted", accepted);
730
954
  }
731
955
  if (request.type === "set-authority") {
732
956
  const accepted = await enqueueSerialized(async () => setAuthority(request));
733
957
  return controlResult("accepted", accepted);
734
958
  }
959
+ // Same generation means this exact activation is already live here: the
960
+ // control request was received and the identity matches. Whether the
961
+ // Conversation is usable yet is a separate fact the caller reads from
962
+ // `snapshot.state`; it is never an identity conflict.
735
963
  if (snapshot.runtimeGenerationId === request.runtimeGenerationId
736
964
  && ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
737
965
  return controlResult("active-same-generation", snapshot);
@@ -781,7 +1009,7 @@ export async function runAgentHost(input) {
781
1009
  boundedText
782
1010
  }
783
1011
  };
784
- await submitTurn(turnControl);
1012
+ await submitTurn(turnControl, {});
785
1013
  process.stdout.write("Provider accepted the human Turn; waiting for its terminal boundary.\n");
786
1014
  }).catch((error) => {
787
1015
  process.stderr.write(`Provider input failed: ${errorText(error)}\n`);
@@ -954,6 +1182,15 @@ async function replayExitOutbox(home) {
954
1182
  await replayRuntimeProcessExitOutbox(home, (observation) => callController(home, "runtime.process-exit-observe", observation).then(() => undefined));
955
1183
  }
956
1184
  /** Internal socket boundary exported for transport-level verification. */
1185
+ class AgentHostOperationError extends Error {
1186
+ snapshot;
1187
+ failure;
1188
+ constructor(cause, snapshot, failure) {
1189
+ super(errorText(cause), { cause });
1190
+ this.snapshot = snapshot;
1191
+ this.failure = failure;
1192
+ }
1193
+ }
957
1194
  export async function openAgentHostControl(home, payload, snapshot, dispatch) {
958
1195
  const path = agentHostControlSocketPath({
959
1196
  home,
@@ -985,17 +1222,49 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
985
1222
  void (async () => {
986
1223
  try {
987
1224
  const request = validateControl(JSON.parse(body.trim()));
988
- socket.end(`${JSON.stringify(await dispatch(request))}\n`);
1225
+ socket.end(`${JSON.stringify(boundControlResponse(await dispatch(request)))}\n`);
989
1226
  }
990
- catch (error) {
991
- const current = snapshot();
1227
+ catch (caught) {
1228
+ const error = caught instanceof AgentHostOperationError ? caught.cause : caught;
1229
+ const current = caught instanceof AgentHostOperationError ? caught.snapshot : snapshot();
992
1230
  const busy = error instanceof ProviderTurnBusyError;
993
- socket.end(`${JSON.stringify(controlResult(busy ? "accepted" : "rejected", validateSnapshot({
1231
+ // Two different facts can be unknown: whether the Provider accepted
1232
+ // the input, and whether its durable registration was committed.
1233
+ // Both leave the outcome ambiguous, so neither may be reported as a
1234
+ // definite non-acceptance.
1235
+ const unknown = error instanceof ProviderDeliveryUnknownError
1236
+ || error instanceof ControllerAcknowledgementUnknownError;
1237
+ // The failing operation already recorded the exact facts. Prefer its
1238
+ // record; only synthesize one when the failure came from outside a
1239
+ // Turn operation (a malformed control, say), where those facts do
1240
+ // not exist.
1241
+ const recorded = caught instanceof AgentHostOperationError ? caught.failure : undefined;
1242
+ const failure = recorded ?? providerDeliveryFailureFrom(error, {
1243
+ phase: controlRequestPhase(body),
1244
+ hostState: busy ? "busy" : unknown ? "delivery-unknown" : current.state,
1245
+ ...(current.runtimeGenerationId === undefined
1246
+ ? {}
1247
+ : { observedRuntimeGenerationId: current.runtimeGenerationId }),
1248
+ ...(current.attemptId === undefined
1249
+ ? {}
1250
+ : { attemptId: current.attemptId }),
1251
+ // The Host rejected this request before writing to the
1252
+ // Provider, except when it explicitly reported an ambiguous
1253
+ // delivery. Never silently upgrade that to not-accepted.
1254
+ inputDisposition: unknown ? "unknown" : "not-accepted"
1255
+ });
1256
+ // A bare `rejected` here is indistinguishable from the Provider
1257
+ // refusing the input. Carry the real cause and an explicit input
1258
+ // disposition so no consumer has to guess from the message text.
1259
+ socket.end(`${JSON.stringify(boundControlResponse(controlResult(busy ? "accepted" : "rejected", validateSnapshot({
994
1260
  ...current,
995
1261
  ...(busy ? { state: "busy" } : {}),
996
- detail: errorText(error),
1262
+ ...(unknown ? { state: "delivery-unknown" } : {}),
1263
+ // snapshot.detail reaches the same public read chain as the
1264
+ // failure record, so it passes the same redaction boundary.
1265
+ detail: redactAgentErrorText(errorText(error)),
997
1266
  updatedAt: new Date().toISOString()
998
- })))}\n`);
1267
+ }), failure)))}\n`);
999
1268
  }
1000
1269
  })();
1001
1270
  });
@@ -1012,6 +1281,49 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1012
1281
  }
1013
1282
  });
1014
1283
  }
1284
+ /**
1285
+ * Keeps a control response inside the socket bound the client enforces.
1286
+ *
1287
+ * A large `raw` chain could push the response past `HOST_CONTROL_MAX_BYTES`,
1288
+ * and the client destroys anything over it — turning a precise structured
1289
+ * failure into a bare transport error and losing the cause entirely. Bound
1290
+ * display projections first; if the raw payload still exceeds the UTF-8 byte
1291
+ * budget, retain its head and tail with an explicit truncation marker.
1292
+ */
1293
+ function boundControlResponse(result) {
1294
+ // Every public response, including status and successful controls, crosses
1295
+ // the same redaction/size boundary. An earlier failed operation may remain
1296
+ // visible through an otherwise successful status request.
1297
+ result = controlResult(result.outcome, validateSnapshot(result.snapshot), result.failure === undefined ? undefined : providerDeliveryFailure(result.failure));
1298
+ if (withinControlBound(result))
1299
+ return result;
1300
+ const clip = (text, chars) => {
1301
+ if (text.length <= chars)
1302
+ return text;
1303
+ const head = Math.floor(chars * 0.75);
1304
+ return `${text.slice(0, head)}…[truncated ${text.length - chars} chars: control byte bound]${text.slice(text.length - (chars - head))}`;
1305
+ };
1306
+ let bounded = controlResult(result.outcome, {
1307
+ ...result.snapshot,
1308
+ ...(result.snapshot.detail === undefined ? {} : { detail: clip(result.snapshot.detail, 1200) })
1309
+ }, result.failure === undefined ? undefined : { ...result.failure, detail: clip(result.failure.detail, 1200) });
1310
+ const raw = bounded.failure?.raw;
1311
+ if (withinControlBound(bounded) || raw === undefined)
1312
+ return bounded;
1313
+ let chars = raw.length;
1314
+ while (!withinControlBound(bounded) && chars > 0) {
1315
+ chars = Math.floor(chars / 2);
1316
+ bounded = controlResult(bounded.outcome, bounded.snapshot, {
1317
+ ...bounded.failure,
1318
+ raw: clip(raw, chars)
1319
+ });
1320
+ }
1321
+ return bounded;
1322
+ }
1323
+ function withinControlBound(result) {
1324
+ // The newline the socket appends counts against the same bound.
1325
+ return Buffer.byteLength(`${JSON.stringify(result)}\n`, "utf8") <= HOST_CONTROL_MAX_BYTES;
1326
+ }
1015
1327
  async function hostControlSocketIsLive(path) {
1016
1328
  return await new Promise((resolvePromise, reject) => {
1017
1329
  const client = createConnection(path);
@@ -1088,7 +1400,27 @@ function validateControlResult(result) {
1088
1400
  || !["status", "accepted", "rejected", "active-same-generation", "active-other-generation"].includes(result.outcome)) {
1089
1401
  throw new Error("Agent Host control response is invalid.");
1090
1402
  }
1091
- return Object.freeze({ ...result, snapshot: validateSnapshot(result.snapshot) });
1403
+ return Object.freeze({
1404
+ ...result,
1405
+ snapshot: validateSnapshot(result.snapshot),
1406
+ // A malformed failure record must never mask the outcome it describes.
1407
+ // Drop it and let the consumer fall back to snapshot.detail.
1408
+ ...(isProviderDeliveryFailure(result.failure)
1409
+ ? { failure: Object.freeze({ ...result.failure }) }
1410
+ : {})
1411
+ });
1412
+ }
1413
+ function isProviderDeliveryFailure(value) {
1414
+ if (value === null || typeof value !== "object")
1415
+ return false;
1416
+ const failure = value;
1417
+ return typeof failure.detail === "string"
1418
+ && failure.detail.trim().length > 0
1419
+ && typeof failure.phase === "string"
1420
+ && (failure.raw === undefined || typeof failure.raw === "string")
1421
+ && (failure.registrationDisposition === undefined
1422
+ || ["committed", "not-committed", "unknown"].includes(failure.registrationDisposition))
1423
+ && ["accepted", "not-accepted", "unknown"].includes(failure.inputDisposition ?? "");
1092
1424
  }
1093
1425
  function validateSnapshot(snapshot) {
1094
1426
  if (snapshot.schemaVersion !== 2
@@ -1114,7 +1446,13 @@ function validateSnapshot(snapshot) {
1114
1446
  holderId: snapshot.authorityHolderId
1115
1447
  });
1116
1448
  }
1117
- return Object.freeze({ ...snapshot });
1449
+ // Store only the redacted diagnostic in the live snapshot. Sanitizing the
1450
+ // immediate error response alone leaves status/launch acknowledgements able
1451
+ // to expose the original exception on their normal success paths.
1452
+ return Object.freeze({
1453
+ ...snapshot,
1454
+ ...(snapshot.detail === undefined ? {} : { detail: redactAgentErrorText(snapshot.detail) })
1455
+ });
1118
1456
  }
1119
1457
  function hostSnapshot(state, fields = {}) {
1120
1458
  return validateSnapshot({
@@ -1124,8 +1462,31 @@ function hostSnapshot(state, fields = {}) {
1124
1462
  updatedAt: new Date().toISOString()
1125
1463
  });
1126
1464
  }
1127
- function controlResult(outcome, snapshot) {
1128
- return Object.freeze({ protocol: AGENT_HOST_CONTROL_PROTOCOL, outcome, snapshot });
1465
+ function controlResult(outcome, snapshot, failure) {
1466
+ return Object.freeze({
1467
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1468
+ outcome,
1469
+ snapshot,
1470
+ ...(failure === undefined ? {} : { failure })
1471
+ });
1472
+ }
1473
+ /**
1474
+ * Best-effort failure phase for a request that failed before or during
1475
+ * parsing. An unparsable body cannot be attributed to a specific control, so
1476
+ * it stays `turn-submit` only when the body actually claims that type.
1477
+ */
1478
+ function controlRequestPhase(body) {
1479
+ try {
1480
+ const type = JSON.parse(body.trim()).type;
1481
+ if (type === "submit-turn" || type === "steer-turn")
1482
+ return "turn-submit";
1483
+ if (type === "launch")
1484
+ return "session-restore";
1485
+ }
1486
+ catch {
1487
+ // An unparsable control never reached the Provider.
1488
+ }
1489
+ return "host-start";
1129
1490
  }
1130
1491
  function definedFields(value) {
1131
1492
  return Object.fromEntries(Object.entries(value).filter(([, member]) => member !== undefined));
@@ -1173,8 +1534,13 @@ async function beginDurableProviderTurn(home, durableTurn) {
1173
1534
  catch (error) {
1174
1535
  if (!(error instanceof ControllerAcknowledgementUnknownError))
1175
1536
  throw error;
1176
- await resolveProviderTurnSubmission(home, durableTurn, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`));
1177
- throw error;
1537
+ await resolveProviderTurnSubmission(home, durableTurn, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`, { cause: error }));
1538
+ // The resolve is fenced on this exact attempt id, so its success proves
1539
+ // the registration did commit and is now settled with nothing written to
1540
+ // the Provider. That is a known outcome; reporting it as unknown would
1541
+ // strand a Session whose durable record is in fact resolved.
1542
+ throw new ProviderTurnRegistrationSettledError("Provider Turn registration acknowledgement was lost and its durable record has been "
1543
+ + `settled before any Provider write: ${errorText(error)}`, { cause: error });
1178
1544
  }
1179
1545
  }
1180
1546
  async function callControllerIdempotently(home, method, request) {
@@ -1194,7 +1560,7 @@ async function callControllerIdempotently(home, method, request) {
1194
1560
  }
1195
1561
  catch (replayError) {
1196
1562
  if (firstCallMayHaveApplied || controllerCallMayHaveApplied(replayError)) {
1197
- throw new ControllerAcknowledgementUnknownError(`${method} may have been committed, but its acknowledgement could not be confirmed: ${errorText(replayError)}`);
1563
+ throw new ControllerAcknowledgementUnknownError(`${method} may have been committed, but its acknowledgement could not be confirmed: ${errorText(replayError)}`, { cause: new AggregateError([error, replayError], "Controller acknowledgement could not be confirmed by its exact idempotent replay.", { cause: replayError }) });
1198
1564
  }
1199
1565
  throw replayError;
1200
1566
  }
@@ -1203,6 +1569,14 @@ async function callControllerIdempotently(home, method, request) {
1203
1569
  class ControllerAcknowledgementUnknownError extends Error {
1204
1570
  name = "ControllerAcknowledgementUnknownError";
1205
1571
  }
1572
+ /**
1573
+ * The registration acknowledgement was lost, but the compensating resolve
1574
+ * then succeeded against the same attempt fence. Both facts are therefore
1575
+ * known: the durable record is settled and the Provider was never written to.
1576
+ */
1577
+ class ProviderTurnRegistrationSettledError extends Error {
1578
+ name = "ProviderTurnRegistrationSettledError";
1579
+ }
1206
1580
  async function resolveProviderTurnSubmission(home, durableTurn, error) {
1207
1581
  const attemptId = durableTurn.attemptId;
1208
1582
  if (typeof attemptId !== "string") {
@@ -1221,7 +1595,11 @@ async function resolveProviderTurnSubmission(home, durableTurn, error) {
1221
1595
  await callControllerIdempotently(home, "runtime.provider-turn-submission-resolve", request);
1222
1596
  }
1223
1597
  catch (resolutionError) {
1224
- throw new ProviderDeliveryUnknownError(`Provider submission outcome could not be durably resolved: ${errorText(resolutionError)}. Original outcome: ${errorText(error)}`, attemptId);
1598
+ throw new ProviderDeliveryUnknownError(`Provider submission outcome could not be durably resolved: ${errorText(resolutionError)}. Original outcome: ${errorText(error)}`, attemptId,
1599
+ // Without this the causal chain ends at the wrapper, and the reason the
1600
+ // resolution failed — the fact that decides whether a retry is safe —
1601
+ // survives only as prose inside the message.
1602
+ { cause: new AggregateError([error, resolutionError], "Provider submission outcome and its failed durable settlement.", { cause: resolutionError }) });
1225
1603
  }
1226
1604
  }
1227
1605
  /**