@scotthuang/agent-knock-knock 0.12.37 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +3 -0
  3. package/dist/src/agent-session-provider.d.ts +5 -0
  4. package/dist/src/agent-session-provider.js +10 -1
  5. package/dist/src/agent-session-provider.js.map +1 -1
  6. package/dist/src/callback-outbox-service.d.ts +2 -0
  7. package/dist/src/callback-outbox-service.js +1 -1
  8. package/dist/src/callback-outbox-service.js.map +1 -1
  9. package/dist/src/callback-transport.d.ts +7 -0
  10. package/dist/src/callback-transport.js +1 -1
  11. package/dist/src/callback-transport.js.map +1 -1
  12. package/dist/src/claude-lifecycle-compatibility.js +49 -2
  13. package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
  14. package/dist/src/claude-terminal-agent-adapter.d.ts +1 -1
  15. package/dist/src/claude-terminal-agent-adapter.js +2 -2
  16. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  17. package/dist/src/cli-core.js +35 -32
  18. package/dist/src/cli-core.js.map +1 -1
  19. package/dist/src/codex-store-adapter.d.ts +1 -1
  20. package/dist/src/codex-store-adapter.js +35 -8
  21. package/dist/src/codex-store-adapter.js.map +1 -1
  22. package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +20 -0
  23. package/dist/src/deferred-foreground-authority-cli-adapter.js +216 -8
  24. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  25. package/dist/src/host-adapter.d.ts +1 -1
  26. package/dist/src/host-adapter.js +1 -1
  27. package/dist/src/host-bridge-tools.js +2 -2
  28. package/dist/src/managed-turn-recovery-service.d.ts +8 -1
  29. package/dist/src/managed-turn-recovery-service.js +55 -13
  30. package/dist/src/managed-turn-recovery-service.js.map +1 -1
  31. package/dist/src/mutation-transaction.d.ts +1 -0
  32. package/dist/src/mutation-transaction.js +19 -15
  33. package/dist/src/mutation-transaction.js.map +1 -1
  34. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +39 -3
  35. package/dist/src/native-thread-lifecycle-cli-adapter.js +307 -7
  36. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  37. package/dist/src/openclaw-plugin-callback-adapter.js +17 -7
  38. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  39. package/dist/src/openclaw-plugin-command-adapter.js +297 -83
  40. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  41. package/dist/src/openclaw-plugin-schemas.d.ts +57 -0
  42. package/dist/src/openclaw-plugin-schemas.js +60 -2
  43. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  44. package/dist/src/openclaw-private-authority-offers.d.ts +17 -0
  45. package/dist/src/openclaw-private-authority-offers.js +75 -3
  46. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  47. package/dist/src/store.d.ts +1 -1
  48. package/dist/src/store.js +6 -4
  49. package/dist/src/store.js.map +1 -1
  50. package/dist/src/terminal-acceptance-application-service.js +2 -2
  51. package/dist/src/terminal-acceptance-application-service.js.map +1 -1
  52. package/dist/src/terminal-acceptance-cli-adapter.d.ts +11 -1
  53. package/dist/src/terminal-acceptance-cli-adapter.js +290 -16
  54. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  55. package/dist/src/terminal-action-projection.d.ts +1 -1
  56. package/dist/src/terminal-action-projection.js +6 -0
  57. package/dist/src/terminal-action-projection.js.map +1 -1
  58. package/dist/src/terminal-agent-adapter.d.ts +9 -0
  59. package/dist/src/terminal-agent-adapter.js.map +1 -1
  60. package/dist/src/terminal-agent-bridge.d.ts +52 -8
  61. package/dist/src/terminal-agent-bridge.js +131 -94
  62. package/dist/src/terminal-agent-bridge.js.map +1 -1
  63. package/dist/src/terminal-authority-policy.js +3 -0
  64. package/dist/src/terminal-authority-policy.js.map +1 -1
  65. package/dist/src/terminal-binding-authority.d.ts +2 -0
  66. package/dist/src/terminal-binding-authority.js +8 -0
  67. package/dist/src/terminal-binding-authority.js.map +1 -1
  68. package/dist/src/terminal-command-cli-adapter.d.ts +17 -1
  69. package/dist/src/terminal-command-cli-adapter.js +784 -286
  70. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  71. package/dist/src/terminal-delegate-cli-adapter.js +21 -7
  72. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
  73. package/dist/src/terminal-dispatch-application.d.ts +3 -1
  74. package/dist/src/terminal-dispatch-application.js +13 -5
  75. package/dist/src/terminal-dispatch-application.js.map +1 -1
  76. package/dist/src/terminal-dispatch-composition.d.ts +43 -0
  77. package/dist/src/terminal-dispatch-execution.d.ts +2 -2
  78. package/dist/src/terminal-dispatch-execution.js +53 -3
  79. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  80. package/dist/src/terminal-dispatch-policy.d.ts +20 -0
  81. package/dist/src/terminal-dispatch-policy.js +33 -1
  82. package/dist/src/terminal-dispatch-policy.js.map +1 -1
  83. package/dist/src/terminal-dispatch-presenter.d.ts +45 -0
  84. package/dist/src/terminal-dispatch-presenter.js +220 -18
  85. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  86. package/dist/src/terminal-dispatch-receipt.d.ts +5 -1
  87. package/dist/src/terminal-dispatch-receipt.js +26 -3
  88. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  89. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +63 -1
  90. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  91. package/dist/src/terminal-dispatch-recovery-service.d.ts +1 -0
  92. package/dist/src/terminal-dispatch-recovery-service.js +7 -0
  93. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
  94. package/dist/src/terminal-identity-authority-cli-adapter.js +103 -4
  95. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  96. package/dist/src/terminal-interaction-authority.d.ts +48 -1
  97. package/dist/src/terminal-interaction-authority.js +30 -0
  98. package/dist/src/terminal-interaction-authority.js.map +1 -1
  99. package/dist/src/terminal-interaction-cli-adapter.js +9 -0
  100. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -1
  101. package/dist/src/terminal-interaction-core.d.ts +104 -0
  102. package/dist/src/terminal-interaction-core.js +407 -0
  103. package/dist/src/terminal-interaction-core.js.map +1 -0
  104. package/dist/src/terminal-interaction-protocol.d.ts +89 -0
  105. package/dist/src/terminal-interaction-protocol.js +199 -0
  106. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  107. package/dist/src/terminal-list-cli-adapter.d.ts +5 -0
  108. package/dist/src/terminal-list-cli-adapter.js +190 -40
  109. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  110. package/dist/src/terminal-list-renderer.js +60 -7
  111. package/dist/src/terminal-list-renderer.js.map +1 -1
  112. package/dist/src/terminal-maintenance-cli-adapter.js +14 -3
  113. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  114. package/dist/src/terminal-monitor-application-service.d.ts +1 -0
  115. package/dist/src/terminal-monitor-application-service.js +72 -10
  116. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  117. package/dist/src/terminal-monitor-cli-adapter.d.ts +3 -0
  118. package/dist/src/terminal-monitor-cli-adapter.js +24 -8
  119. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  120. package/dist/src/terminal-monitor-state-cli-adapter.js +15 -2
  121. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  122. package/dist/src/terminal-monitor-state-reconciliation-service.js +15 -1
  123. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
  124. package/dist/src/terminal-mutation-cli-runtime.d.ts +11 -1
  125. package/dist/src/terminal-mutation-cli-runtime.js +9 -4
  126. package/dist/src/terminal-mutation-cli-runtime.js.map +1 -1
  127. package/dist/src/terminal-questionnaire-adapter.d.ts +3 -1
  128. package/dist/src/terminal-questionnaire-adapter.js +34 -21
  129. package/dist/src/terminal-questionnaire-adapter.js.map +1 -1
  130. package/dist/src/terminal-status-cli-adapter.d.ts +1 -7
  131. package/dist/src/terminal-status-cli-adapter.js +29 -3
  132. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  133. package/dist/src/terminal-status-facts.d.ts +17 -1
  134. package/dist/src/terminal-status-facts.js +59 -0
  135. package/dist/src/terminal-status-facts.js.map +1 -1
  136. package/dist/src/terminal-submission-acceptance.d.ts +37 -2
  137. package/dist/src/terminal-submission-acceptance.js +633 -30
  138. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  139. package/dist/src/terminal-submission-facts.d.ts +5 -0
  140. package/dist/src/terminal-submission-facts.js.map +1 -1
  141. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +2 -1
  142. package/dist/src/terminal-watch-callback-cli-adapter.js +2 -1
  143. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  144. package/dist/src/terminal-watch-cli-adapter.d.ts +6 -2
  145. package/dist/src/terminal-watch-cli-adapter.js +1051 -15
  146. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  147. package/dist/src/terminal-watch-service.d.ts +15 -1
  148. package/dist/src/terminal-watch-service.js +169 -13
  149. package/dist/src/terminal-watch-service.js.map +1 -1
  150. package/dist/src/terminal-watch-store.d.ts +72 -3
  151. package/dist/src/terminal-watch-store.js +541 -35
  152. package/dist/src/terminal-watch-store.js.map +1 -1
  153. package/docs/host-bridge-profiles.md +2 -2
  154. package/docs/quickstart-herdr.md +5 -3
  155. package/docs/quickstart-tmux.md +8 -6
  156. package/openclaw.plugin.json +8 -0
  157. package/package.json +1 -1
  158. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +16 -12
@@ -8,6 +8,14 @@ const CODEX_ACCEPTANCE_MAX_BYTES = 16 * 1024 * 1024;
8
8
  const CODEX_COMPLETION_MAX_BYTES = 256 * 1024 * 1024;
9
9
  const CODEX_COMPLETION_MAX_TEXT_LENGTH = 4000;
10
10
  const CODEX_ACTIVE_TASK_SCAN_CHUNK_BYTES = 64 * 1024;
11
+ const CODEX_QUESTIONNAIRE_TAIL_MAX_BYTES = 1024 * 1024;
12
+ const CODEX_QUESTIONNAIRE_MAX_TEXT_LENGTH = 16 * 1024;
13
+ const CODEX_QUESTIONNAIRE_MAX_OPTIONS = 8;
14
+ const CODEX_QUESTIONNAIRE_MAX_QUESTIONS = 3;
15
+ const CODEX_QUESTIONNAIRE_OTHER_LABEL = "None of the above";
16
+ const CODEX_QUESTIONNAIRE_OTHER_DESCRIPTION = "Optionally, add details in notes (tab).";
17
+ const CODEX_QUESTIONNAIRE_CUSTOM_LABEL = "Type something.";
18
+ const CODEX_QUESTIONNAIRE_CUSTOM_DESCRIPTION = "Enter a free-form answer through Codex Notes.";
11
19
  const CODEX_ACTIVE_TASK_MAX_RECORD_BYTES = 16 * 1024 * 1024;
12
20
  const CODEX_ROLLOUT_HEADER_MAX_BYTES = 1024 * 1024;
13
21
  const CODEX_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
@@ -151,6 +159,9 @@ export function observeCodexHumanStartedActiveTask(options) {
151
159
  safeResumeOffsetBytes
152
160
  };
153
161
  }
162
+ if (completion.status === "aborted") {
163
+ throw new Error("Codex active-task abort evidence escaped its exact lifecycle fence");
164
+ }
154
165
  return {
155
166
  status: "completed",
156
167
  completion: {
@@ -763,7 +774,7 @@ export function captureCodexCandidateSetRolloutAcceptanceAnchor({ inventory: inv
763
774
  anchor_fingerprint: fingerprint(base)
764
775
  };
765
776
  }
766
- export function detectCodexCandidateSetRolloutAcceptance({ anchor: anchorValue, currentInventory: inventoryValue, requestHash: requestHashValue }) {
777
+ export function detectCodexCandidateSetRolloutAcceptance({ anchor: anchorValue, currentInventory: inventoryValue, requestHash: requestHashValue, recoveryCandidate: recoveryCandidateValue }) {
767
778
  const anchor = validateCodexRolloutAcceptanceAnchor(anchorValue);
768
779
  if (anchor.version !== 3) {
769
780
  throw new Error("Codex candidate-set acceptance requires a version 3 anchor");
@@ -782,15 +793,41 @@ export function detectCodexCandidateSetRolloutAcceptance({ anchor: anchorValue,
782
793
  exact_matches: 0
783
794
  };
784
795
  }
796
+ let recoveryCandidate;
797
+ if (recoveryCandidateValue) {
798
+ try {
799
+ recoveryCandidate = validateCodexRecoveryCandidateForAcceptance(recoveryCandidateValue, anchor);
800
+ }
801
+ catch (error) {
802
+ return {
803
+ status: "uncertain",
804
+ code: "candidate_inventory_changed",
805
+ reason: error instanceof Error ? error.message : String(error),
806
+ inspected_candidates: 0,
807
+ exact_matches: 0
808
+ };
809
+ }
810
+ }
785
811
  const anchoredByThread = new Map(anchor.candidate_rollouts.map((candidate) => [
786
812
  candidate.native_thread_id,
787
813
  candidate
788
814
  ]));
815
+ const anchoredByFile = new Map(anchor.candidate_rollouts.map((candidate) => [
816
+ codexRolloutInodeKey(candidate.rollout),
817
+ candidate
818
+ ]));
789
819
  const currentByThread = new Map(inventory.roots.map((identity) => [identity.sessionId, identity]));
790
- for (const identity of inventory.roots) {
820
+ const currentByFile = new Map(inventory.roots.map((identity) => [
821
+ codexRolloutInodeKey(identity.rollout),
822
+ identity
823
+ ]));
824
+ for (const identity of [
825
+ ...inventory.roots,
826
+ ...(recoveryCandidate ? [recoveryCandidate] : [])
827
+ ]) {
791
828
  const anchored = anchoredByThread.get(identity.sessionId);
792
829
  if (anchored &&
793
- !sameRolloutIdentity(anchored.rollout, identity.rollout)) {
830
+ !sameRolloutFileIdentity(anchored.rollout, identity.rollout)) {
794
831
  return {
795
832
  status: "uncertain",
796
833
  code: "candidate_inventory_changed",
@@ -799,26 +836,72 @@ export function detectCodexCandidateSetRolloutAcceptance({ anchor: anchorValue,
799
836
  exact_matches: 0
800
837
  };
801
838
  }
839
+ const anchoredFile = anchoredByFile.get(codexRolloutInodeKey(identity.rollout));
840
+ if (anchoredFile &&
841
+ anchoredFile.native_thread_id !== identity.sessionId) {
842
+ return {
843
+ status: "uncertain",
844
+ code: "candidate_inventory_changed",
845
+ reason: `Codex candidate rollout changed native thread identity from ` +
846
+ `${anchoredFile.native_thread_id} to ${identity.sessionId} after capture`,
847
+ inspected_candidates: 0,
848
+ exact_matches: 0
849
+ };
850
+ }
802
851
  }
803
- for (const anchored of anchor.candidate_rollouts) {
804
- if (!currentByThread.has(anchored.native_thread_id)) {
852
+ if (recoveryCandidate) {
853
+ const currentThread = currentByThread.get(recoveryCandidate.sessionId);
854
+ const currentFile = currentByFile.get(codexRolloutInodeKey(recoveryCandidate.rollout));
855
+ if ((currentThread && !sameRolloutFileIdentity(currentThread.rollout, recoveryCandidate.rollout)) ||
856
+ (currentFile && currentFile.sessionId !== recoveryCandidate.sessionId)) {
805
857
  return {
806
858
  status: "uncertain",
807
859
  code: "candidate_inventory_changed",
808
- reason: `Codex candidate ${anchored.native_thread_id} is no longer open in the exact process inventory`,
860
+ reason: "persisted Codex recovery candidate conflicts with the current root inventory",
809
861
  inspected_candidates: 0,
810
862
  exact_matches: 0
811
863
  };
812
864
  }
813
865
  }
814
- const candidates = inventory.roots.map((identity) => {
815
- const anchored = anchoredByThread.get(identity.sessionId);
816
- return {
866
+ // A Codex thread may close (or reopen) its process FD while the acceptance
867
+ // poll is running. The captured path/device/inode/offset remains the durable
868
+ // read authority in that case, so inspect the union rather than treating a
869
+ // missing lsof row as evidence that the candidate changed identity.
870
+ const candidates = [
871
+ ...anchor.candidate_rollouts.map((anchored) => ({
872
+ identity: currentByThread.get(anchored.native_thread_id) ??
873
+ (recoveryCandidate?.sessionId === anchored.native_thread_id
874
+ ? recoveryCandidate
875
+ : {
876
+ sessionId: anchored.native_thread_id,
877
+ processUuid: anchor.process_uuid,
878
+ processBirth: anchor.process_birth,
879
+ rollout: anchored.rollout,
880
+ evidence: "codex_open_root_rollout"
881
+ }),
882
+ offsetBytes: anchored.offset_bytes,
883
+ requireFreshHeader: false,
884
+ currentlyOpen: currentByThread.has(anchored.native_thread_id)
885
+ })),
886
+ ...inventory.roots
887
+ .filter((identity) => !anchoredByThread.has(identity.sessionId))
888
+ .map((identity) => ({
817
889
  identity,
818
- offsetBytes: anchored?.offset_bytes ?? 0,
819
- requireFreshHeader: anchored === undefined
820
- };
821
- });
890
+ offsetBytes: 0,
891
+ requireFreshHeader: true,
892
+ currentlyOpen: true
893
+ })),
894
+ ...(recoveryCandidate &&
895
+ !anchoredByThread.has(recoveryCandidate.sessionId) &&
896
+ !currentByThread.has(recoveryCandidate.sessionId)
897
+ ? [{
898
+ identity: recoveryCandidate,
899
+ offsetBytes: 0,
900
+ requireFreshHeader: true,
901
+ currentlyOpen: false
902
+ }]
903
+ : [])
904
+ ];
822
905
  const matches = [];
823
906
  let incompleteCandidates = 0;
824
907
  for (const candidate of candidates) {
@@ -846,6 +929,16 @@ export function detectCodexCandidateSetRolloutAcceptance({ anchor: anchorValue,
846
929
  };
847
930
  }
848
931
  if (scan.status === "incomplete") {
932
+ if (!candidate.currentlyOpen) {
933
+ return {
934
+ status: "uncertain",
935
+ code: "candidate_scan_invalid",
936
+ reason: `Codex candidate ${candidate.identity.sessionId} closed with an ` +
937
+ "incomplete rollout record after capture",
938
+ inspected_candidates: candidates.indexOf(candidate) + 1,
939
+ exact_matches: matches.length
940
+ };
941
+ }
849
942
  incompleteCandidates += 1;
850
943
  }
851
944
  else if (scan.status === "accepted") {
@@ -880,6 +973,94 @@ export function detectCodexCandidateSetRolloutAcceptance({ anchor: anchorValue,
880
973
  evidence: matches[0].evidence
881
974
  };
882
975
  }
976
+ /**
977
+ * Attributes a rendered questionnaire to one exact accepted Codex turn while
978
+ * the process has multiple open root rollouts. No prompt or tool arguments are
979
+ * returned: callers receive only a bounded fingerprint or a typed failure.
980
+ */
981
+ export function detectCodexBoundQuestionnaireAttribution(options) {
982
+ let inventory;
983
+ let acceptedThreadId;
984
+ let acceptedRollout;
985
+ let acceptanceId;
986
+ try {
987
+ inventory = validateCodexOpenRootInventoryForAcceptance(options.currentInventory);
988
+ acceptedThreadId = exactNativeThreadId(options.acceptedIdentity.sessionId);
989
+ acceptanceId = exactNativeThreadId(options.acceptanceId);
990
+ if (!options.acceptedIdentity.rollout) {
991
+ throw new Error("accepted Codex rollout identity is unavailable");
992
+ }
993
+ acceptedRollout = normalizedRolloutIdentity(options.acceptedIdentity.rollout);
994
+ if (requiredString(options.acceptedIdentity.processUuid, "accepted Codex process UUID") !== inventory.processUuid ||
995
+ requiredString(options.acceptedIdentity.processBirth, "accepted Codex process birth") !== inventory.processBirth) {
996
+ return { status: "not_matched", code: "accepted_root_missing" };
997
+ }
998
+ }
999
+ catch {
1000
+ return { status: "unavailable", code: "invalid_inventory" };
1001
+ }
1002
+ let normalizedScreen;
1003
+ try {
1004
+ normalizedScreen = normalizedCodexQuestionnaireScreen(options.screen);
1005
+ }
1006
+ catch {
1007
+ return { status: "unavailable", code: "invalid_screen" };
1008
+ }
1009
+ const acceptedRoots = inventory.roots.filter((root) => root.sessionId.toLowerCase() === acceptedThreadId &&
1010
+ sameRolloutFileIdentity(root.rollout, acceptedRollout));
1011
+ if (acceptedRoots.length !== 1) {
1012
+ return { status: "not_matched", code: "accepted_root_missing" };
1013
+ }
1014
+ let maxBytes;
1015
+ try {
1016
+ maxBytes = positiveByteLimit(options.maxBytesPerRollout, CODEX_QUESTIONNAIRE_TAIL_MAX_BYTES, "Codex questionnaire rollout scan limit");
1017
+ }
1018
+ catch {
1019
+ return { status: "unavailable", code: "rollout_scan_unavailable" };
1020
+ }
1021
+ const pending = [];
1022
+ try {
1023
+ for (const root of inventory.roots) {
1024
+ pending.push(...readPendingCodexRequestUserInput({
1025
+ rollout: root.rollout,
1026
+ nativeThreadId: root.sessionId,
1027
+ maxBytes
1028
+ }));
1029
+ }
1030
+ }
1031
+ catch {
1032
+ return { status: "unavailable", code: "rollout_scan_unavailable" };
1033
+ }
1034
+ const acceptedCalls = pending.filter((call) => call.nativeThreadId === acceptedThreadId &&
1035
+ call.turnId === acceptanceId &&
1036
+ sameRolloutFileIdentity(call.rollout, acceptedRollout));
1037
+ if (acceptedCalls.length !== 1) {
1038
+ return { status: "not_matched", code: "accepted_call_not_unique" };
1039
+ }
1040
+ const matching = options.screen.exactShape &&
1041
+ normalizedScreen.responseKind !== "confirm" &&
1042
+ normalizedScreen.responseKind !== "multi_select"
1043
+ ? pending.filter((call) => codexPendingQuestionnaireMatchesScreen(call, normalizedScreen))
1044
+ : pending;
1045
+ if (matching.length !== 1 || matching[0] !== acceptedCalls[0]) {
1046
+ return { status: "not_matched", code: "screen_signature_not_unique" };
1047
+ }
1048
+ return {
1049
+ status: "matched",
1050
+ evidenceFingerprint: fingerprint({
1051
+ schema: "agent-knock-knock/codex-bound-questionnaire-attribution",
1052
+ version: 1,
1053
+ native_thread_id: acceptedThreadId,
1054
+ turn_id: acceptanceId,
1055
+ rollout: {
1056
+ device: acceptedRollout.device,
1057
+ inode: acceptedRollout.inode,
1058
+ path: acceptedRollout.path
1059
+ },
1060
+ screen: normalizedScreen
1061
+ })
1062
+ };
1063
+ }
883
1064
  export function detectCodexRolloutAcceptance(options) {
884
1065
  const anchor = validateCodexRolloutAcceptanceAnchor(options.anchor);
885
1066
  if (anchor.version === 3) {
@@ -1033,7 +1214,10 @@ export function detectCodexBoundRolloutCompletion(options) {
1033
1214
  const anchoredRollout = anchor.version === 3
1034
1215
  ? acceptedCandidate?.rollout
1035
1216
  : anchor.rollout;
1036
- if (anchoredRollout && !sameRolloutIdentity(anchoredRollout, rollout)) {
1217
+ const anchoredRolloutMatches = !anchoredRollout || (anchor.version === 3
1218
+ ? sameRolloutFileIdentity(anchoredRollout, rollout)
1219
+ : sameRolloutIdentity(anchoredRollout, rollout));
1220
+ if (!anchoredRolloutMatches) {
1037
1221
  return codexCompletionFailure("rollout_identity_mismatch", new Error("Codex rollout identity changed after terminal acceptance"), rolloutDiagnostics);
1038
1222
  }
1039
1223
  let opened;
@@ -1092,7 +1276,8 @@ export function detectCodexBoundRolloutCompletion(options) {
1092
1276
  const scanDiagnostics = {
1093
1277
  ...observedDiagnostics,
1094
1278
  scanned_records: scan.scannedRecords,
1095
- observed_task_complete_records: scan.observedTaskCompleteRecords
1279
+ observed_task_complete_records: scan.observedTaskCompleteRecords,
1280
+ observed_turn_aborted_records: scan.observedTurnAbortedRecords
1096
1281
  };
1097
1282
  if (scan.status === "failure") {
1098
1283
  return codexCompletionFailure(scan.code, scan.detail, scanDiagnostics);
@@ -1100,6 +1285,38 @@ export function detectCodexBoundRolloutCompletion(options) {
1100
1285
  if (scan.status === "pending") {
1101
1286
  return codexCompletionPending("exact_turn_not_complete", scanDiagnostics, "the accepted Codex native turn has no durable task_complete record yet");
1102
1287
  }
1288
+ if (scan.status === "aborted") {
1289
+ const reason = truncateCompletionText(redactString(scan.reason ?? "interrupted"));
1290
+ const completion = {
1291
+ source: "durable",
1292
+ outcome: "failure",
1293
+ text: truncateCompletionText(redactString(`Codex task stopped: ${reason}`)),
1294
+ ...(scan.timestamp ? { timestamp: scan.timestamp } : {}),
1295
+ id: acceptanceId,
1296
+ confidence: "high",
1297
+ metadata: {
1298
+ match: "bound_rollout_turn_aborted",
1299
+ turn_id: acceptanceId,
1300
+ native_thread_id: expectedNativeThreadId,
1301
+ anchor_fingerprint: anchor.anchor_fingerprint,
1302
+ rollout_identity_fingerprint: rolloutDiagnostics.rollout_identity_fingerprint,
1303
+ abort_reason: reason,
1304
+ scan_start_offset_bytes: scanStartOffset,
1305
+ observed_end_offset_bytes: before.size,
1306
+ scanned_records: scan.scannedRecords,
1307
+ observed_task_complete_records: scan.observedTaskCompleteRecords,
1308
+ observed_turn_aborted_records: scan.observedTurnAbortedRecords
1309
+ }
1310
+ };
1311
+ return {
1312
+ status: "completed",
1313
+ completion,
1314
+ diagnostics: {
1315
+ ...scanDiagnostics,
1316
+ code: "abort_found"
1317
+ }
1318
+ };
1319
+ }
1103
1320
  const completion = {
1104
1321
  source: "durable",
1105
1322
  outcome: "success",
@@ -1116,7 +1333,8 @@ export function detectCodexBoundRolloutCompletion(options) {
1116
1333
  scan_start_offset_bytes: scanStartOffset,
1117
1334
  observed_end_offset_bytes: before.size,
1118
1335
  scanned_records: scan.scannedRecords,
1119
- observed_task_complete_records: scan.observedTaskCompleteRecords
1336
+ observed_task_complete_records: scan.observedTaskCompleteRecords,
1337
+ observed_turn_aborted_records: scan.observedTurnAbortedRecords
1120
1338
  }
1121
1339
  };
1122
1340
  return {
@@ -1145,7 +1363,11 @@ export function detectCodexBoundRolloutCompletion(options) {
1145
1363
  function scanExactCodexTaskComplete(text, acceptanceId) {
1146
1364
  let scannedRecords = 0;
1147
1365
  let observedTaskCompleteRecords = 0;
1366
+ let observedTurnAbortedRecords = 0;
1367
+ let exactTaskStartedIndex;
1368
+ let laterTaskStartedIndex;
1148
1369
  const exactMatches = [];
1370
+ const exactAborts = [];
1149
1371
  for (const rawLine of text.split("\n")) {
1150
1372
  const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
1151
1373
  if (!line) {
@@ -1161,10 +1383,37 @@ function scanExactCodexTaskComplete(text, acceptanceId) {
1161
1383
  code: "invalid_rollout_jsonl",
1162
1384
  detail: `Codex bound completion suffix contains invalid JSONL at record ${scannedRecords + 1}`,
1163
1385
  scannedRecords,
1164
- observedTaskCompleteRecords
1386
+ observedTaskCompleteRecords,
1387
+ observedTurnAbortedRecords
1165
1388
  };
1166
1389
  }
1390
+ const recordIndex = scannedRecords;
1167
1391
  scannedRecords += 1;
1392
+ if (isRecord(value) &&
1393
+ value.type === "event_msg" &&
1394
+ isRecord(value.payload) &&
1395
+ value.payload.type === "task_started") {
1396
+ const turnId = optionalString(value.payload.turn_id)?.toLowerCase();
1397
+ if (turnId === acceptanceId) {
1398
+ exactTaskStartedIndex ??= recordIndex;
1399
+ }
1400
+ else if (exactTaskStartedIndex !== undefined &&
1401
+ laterTaskStartedIndex === undefined) {
1402
+ laterTaskStartedIndex = recordIndex;
1403
+ }
1404
+ continue;
1405
+ }
1406
+ if (isRecord(value) &&
1407
+ value.type === "event_msg" &&
1408
+ isRecord(value.payload) &&
1409
+ value.payload.type === "turn_aborted") {
1410
+ observedTurnAbortedRecords += 1;
1411
+ const turnId = optionalString(value.payload.turn_id)?.toLowerCase();
1412
+ if (turnId === acceptanceId) {
1413
+ exactAborts.push({ value, index: recordIndex });
1414
+ }
1415
+ continue;
1416
+ }
1168
1417
  if (!isRecord(value) ||
1169
1418
  value.type !== "event_msg" ||
1170
1419
  !isRecord(value.payload) ||
@@ -1174,26 +1423,85 @@ function scanExactCodexTaskComplete(text, acceptanceId) {
1174
1423
  observedTaskCompleteRecords += 1;
1175
1424
  const turnId = optionalString(value.payload.turn_id)?.toLowerCase();
1176
1425
  if (turnId === acceptanceId) {
1177
- exactMatches.push(value);
1426
+ exactMatches.push({ value, index: recordIndex });
1178
1427
  }
1179
1428
  }
1180
- if (exactMatches.length === 0) {
1429
+ if (exactMatches.length > 1) {
1181
1430
  return {
1182
- status: "pending",
1431
+ status: "failure",
1432
+ code: "duplicate_exact_completion",
1433
+ detail: "the exact accepted Codex turn has duplicate task_complete records",
1183
1434
  scannedRecords,
1184
- observedTaskCompleteRecords
1435
+ observedTaskCompleteRecords,
1436
+ observedTurnAbortedRecords
1185
1437
  };
1186
1438
  }
1187
- if (exactMatches.length > 1) {
1439
+ if (exactAborts.length > 1) {
1188
1440
  return {
1189
1441
  status: "failure",
1190
- code: "duplicate_exact_completion",
1191
- detail: "the exact accepted Codex turn has duplicate task_complete records",
1442
+ code: "duplicate_exact_abort",
1443
+ detail: "the exact accepted Codex turn has duplicate turn_aborted records",
1444
+ scannedRecords,
1445
+ observedTaskCompleteRecords,
1446
+ observedTurnAbortedRecords
1447
+ };
1448
+ }
1449
+ if (exactMatches.length > 0 && exactAborts.length > 0) {
1450
+ return {
1451
+ status: "failure",
1452
+ code: "conflicting_exact_settlement",
1453
+ detail: "the exact accepted Codex turn has conflicting completion and abort records",
1454
+ scannedRecords,
1455
+ observedTaskCompleteRecords,
1456
+ observedTurnAbortedRecords
1457
+ };
1458
+ }
1459
+ const exactSettlementIndex = exactMatches[0]?.index ?? exactAborts[0]?.index;
1460
+ if (laterTaskStartedIndex !== undefined &&
1461
+ (exactSettlementIndex === undefined ||
1462
+ laterTaskStartedIndex < exactSettlementIndex)) {
1463
+ return {
1464
+ status: "failure",
1465
+ code: "later_turn_started",
1466
+ detail: "a later Codex native turn started before the exact accepted turn settled",
1467
+ scannedRecords,
1468
+ observedTaskCompleteRecords,
1469
+ observedTurnAbortedRecords
1470
+ };
1471
+ }
1472
+ if (exactAborts.length === 1) {
1473
+ const match = exactAborts[0].value;
1474
+ if (match.timestamp !== undefined && !validTimestamp(match.timestamp)) {
1475
+ return {
1476
+ status: "failure",
1477
+ code: "invalid_exact_abort",
1478
+ detail: "the exact Codex turn_aborted record has an invalid timestamp",
1479
+ scannedRecords,
1480
+ observedTaskCompleteRecords,
1481
+ observedTurnAbortedRecords
1482
+ };
1483
+ }
1484
+ const payload = match.payload;
1485
+ return {
1486
+ status: "aborted",
1487
+ reason: optionalString(payload.reason),
1488
+ ...(match.timestamp !== undefined
1489
+ ? { timestamp: String(match.timestamp) }
1490
+ : {}),
1192
1491
  scannedRecords,
1193
- observedTaskCompleteRecords
1492
+ observedTaskCompleteRecords,
1493
+ observedTurnAbortedRecords
1194
1494
  };
1195
1495
  }
1196
- const match = exactMatches[0];
1496
+ if (exactMatches.length === 0) {
1497
+ return {
1498
+ status: "pending",
1499
+ scannedRecords,
1500
+ observedTaskCompleteRecords,
1501
+ observedTurnAbortedRecords
1502
+ };
1503
+ }
1504
+ const match = exactMatches[0].value;
1197
1505
  const payload = match.payload;
1198
1506
  const textValue = optionalString(payload.last_agent_message);
1199
1507
  if (!textValue) {
@@ -1202,7 +1510,8 @@ function scanExactCodexTaskComplete(text, acceptanceId) {
1202
1510
  code: "invalid_exact_completion",
1203
1511
  detail: "the exact Codex task_complete record has no final agent message",
1204
1512
  scannedRecords,
1205
- observedTaskCompleteRecords
1513
+ observedTaskCompleteRecords,
1514
+ observedTurnAbortedRecords
1206
1515
  };
1207
1516
  }
1208
1517
  if (match.timestamp !== undefined && !validTimestamp(match.timestamp)) {
@@ -1211,7 +1520,8 @@ function scanExactCodexTaskComplete(text, acceptanceId) {
1211
1520
  code: "invalid_exact_completion",
1212
1521
  detail: "the exact Codex task_complete record has an invalid timestamp",
1213
1522
  scannedRecords,
1214
- observedTaskCompleteRecords
1523
+ observedTaskCompleteRecords,
1524
+ observedTurnAbortedRecords
1215
1525
  };
1216
1526
  }
1217
1527
  return {
@@ -1221,7 +1531,8 @@ function scanExactCodexTaskComplete(text, acceptanceId) {
1221
1531
  ? { timestamp: String(match.timestamp) }
1222
1532
  : {}),
1223
1533
  scannedRecords,
1224
- observedTaskCompleteRecords
1534
+ observedTaskCompleteRecords,
1535
+ observedTurnAbortedRecords
1225
1536
  };
1226
1537
  }
1227
1538
  function codexCompletionPending(code, diagnostics, detail) {
@@ -1490,6 +1801,292 @@ function validateCodexOpenRootInventoryForAcceptance(value) {
1490
1801
  }
1491
1802
  return value;
1492
1803
  }
1804
+ function readPendingCodexRequestUserInput(input) {
1805
+ const rollout = normalizedRolloutIdentity(input.rollout);
1806
+ const nativeThreadId = exactNativeThreadId(input.nativeThreadId);
1807
+ const opened = openExactRollout(rollout);
1808
+ try {
1809
+ const before = opened.stat;
1810
+ assertPrivateRegularFile(before);
1811
+ const header = readCodexRolloutHeader(opened.fd, before.size);
1812
+ assertExistingCodexRolloutHeader(header, nativeThreadId);
1813
+ if (!fileEndsWithNewline(opened.fd, before.size)) {
1814
+ throw new Error("Codex questionnaire rollout has a partial JSONL record");
1815
+ }
1816
+ const readStart = Math.max(0, before.size - input.maxBytes);
1817
+ const length = before.size - readStart;
1818
+ const buffer = Buffer.allocUnsafe(length);
1819
+ if (fs.readSync(opened.fd, buffer, 0, length, readStart) !== length) {
1820
+ throw new Error("Codex questionnaire rollout changed while it was read");
1821
+ }
1822
+ const after = fs.fstatSync(opened.fd);
1823
+ if (!sameStableFile(before, after)) {
1824
+ throw new Error("Codex questionnaire rollout changed while it was scanned");
1825
+ }
1826
+ const atBoundary = readStart === 0 ||
1827
+ byteAtOffset(opened.fd, readStart - 1) === 0x0a;
1828
+ const firstComplete = atBoundary ? 0 : buffer.indexOf(0x0a) + 1;
1829
+ if (firstComplete <= 0 && !atBoundary) {
1830
+ throw new Error("Codex questionnaire rollout record exceeded its bounded tail scan");
1831
+ }
1832
+ const records = parseCodexJsonlRecords(buffer.subarray(firstComplete), "questionnaire tail");
1833
+ return pendingCodexRequestUserInputFromRecords({
1834
+ records,
1835
+ nativeThreadId,
1836
+ rollout,
1837
+ prefixTruncated: readStart > 0
1838
+ });
1839
+ }
1840
+ finally {
1841
+ fs.closeSync(opened.fd);
1842
+ }
1843
+ }
1844
+ function pendingCodexRequestUserInputFromRecords(input) {
1845
+ const calls = [];
1846
+ const outputs = new Map();
1847
+ const terminalTurns = new Map();
1848
+ const unscopedTerminalIndexes = [];
1849
+ const startedTurns = [];
1850
+ let sawLifecycle = false;
1851
+ input.records.forEach(({ value }, index) => {
1852
+ const payload = isRecord(value.payload) ? value.payload : undefined;
1853
+ if (!payload)
1854
+ return;
1855
+ if (value.type === "event_msg" &&
1856
+ payload.type === "task_started") {
1857
+ startedTurns.push({
1858
+ index,
1859
+ turnId: exactNativeThreadId(payload.turn_id)
1860
+ });
1861
+ sawLifecycle = true;
1862
+ return;
1863
+ }
1864
+ if (value.type === "event_msg" &&
1865
+ (payload.type === "task_complete" || payload.type === "turn_aborted")) {
1866
+ const rawTurnId = optionalString(payload.turn_id);
1867
+ if (!rawTurnId) {
1868
+ unscopedTerminalIndexes.push(index);
1869
+ sawLifecycle = true;
1870
+ return;
1871
+ }
1872
+ const turnId = exactNativeThreadId(rawTurnId);
1873
+ const indexes = terminalTurns.get(turnId) ?? [];
1874
+ indexes.push(index);
1875
+ terminalTurns.set(turnId, indexes);
1876
+ sawLifecycle = true;
1877
+ return;
1878
+ }
1879
+ if (value.type !== "response_item" ||
1880
+ payload.type !== "function_call" ||
1881
+ payload.name !== "request_user_input") {
1882
+ if (value.type === "response_item" &&
1883
+ payload.type === "function_call_output") {
1884
+ const callId = requiredString(payload.call_id, "Codex questionnaire output call id");
1885
+ const indexes = outputs.get(callId) ?? [];
1886
+ indexes.push(index);
1887
+ outputs.set(callId, indexes);
1888
+ }
1889
+ return;
1890
+ }
1891
+ const metadata = isRecord(payload.internal_chat_message_metadata_passthrough)
1892
+ ? payload.internal_chat_message_metadata_passthrough
1893
+ : undefined;
1894
+ const callId = requiredString(payload.call_id, "Codex questionnaire call id");
1895
+ if (calls.some((call) => call.callId === callId)) {
1896
+ throw new Error("Codex questionnaire call id is duplicated");
1897
+ }
1898
+ calls.push({
1899
+ index,
1900
+ callId,
1901
+ turnId: exactNativeThreadId(metadata?.turn_id),
1902
+ questions: parseCodexRequestUserInputArguments(payload.arguments)
1903
+ });
1904
+ });
1905
+ const pending = calls.filter((call) => {
1906
+ if (outputs.get(call.callId)?.some((index) => index > call.index)) {
1907
+ return false;
1908
+ }
1909
+ if (terminalTurns.get(call.turnId)?.some((index) => index > call.index)) {
1910
+ return false;
1911
+ }
1912
+ if (unscopedTerminalIndexes.some((index) => index > call.index)) {
1913
+ return false;
1914
+ }
1915
+ const precedingStart = latestCodexTaskStartBefore(startedTurns, call.index);
1916
+ if (precedingStart && precedingStart.turnId !== call.turnId) {
1917
+ return false;
1918
+ }
1919
+ return !startedTurns.some((started) => started.index > call.index);
1920
+ });
1921
+ if (input.prefixTruncated &&
1922
+ pending.length === 0 &&
1923
+ !sawLifecycle) {
1924
+ throw new Error("Codex questionnaire tail cannot exclude an omitted pending request");
1925
+ }
1926
+ return pending.map((call) => ({
1927
+ nativeThreadId: input.nativeThreadId,
1928
+ rollout: input.rollout,
1929
+ turnId: call.turnId,
1930
+ questions: call.questions
1931
+ }));
1932
+ }
1933
+ function latestCodexTaskStartBefore(starts, recordIndex) {
1934
+ let latest;
1935
+ for (const start of starts) {
1936
+ if (start.index >= recordIndex)
1937
+ break;
1938
+ latest = start;
1939
+ }
1940
+ return latest;
1941
+ }
1942
+ function parseCodexRequestUserInputArguments(value) {
1943
+ if (typeof value !== "string" ||
1944
+ value.length === 0 ||
1945
+ value.length > CODEX_QUESTIONNAIRE_TAIL_MAX_BYTES) {
1946
+ throw new Error("Codex questionnaire arguments are invalid");
1947
+ }
1948
+ let parsed;
1949
+ try {
1950
+ parsed = JSON.parse(value);
1951
+ }
1952
+ catch {
1953
+ throw new Error("Codex questionnaire arguments are not valid JSON");
1954
+ }
1955
+ const questions = isRecord(parsed) ? parsed.questions : undefined;
1956
+ if (!Array.isArray(questions) ||
1957
+ questions.length === 0 ||
1958
+ questions.length > CODEX_QUESTIONNAIRE_MAX_QUESTIONS) {
1959
+ throw new Error("Codex questionnaire question count is invalid");
1960
+ }
1961
+ return questions.map((question) => {
1962
+ if (!isRecord(question)) {
1963
+ throw new Error("Codex questionnaire question is invalid");
1964
+ }
1965
+ const rawOptions = question.options;
1966
+ if (rawOptions !== undefined &&
1967
+ (!Array.isArray(rawOptions) ||
1968
+ rawOptions.length > CODEX_QUESTIONNAIRE_MAX_OPTIONS)) {
1969
+ throw new Error("Codex questionnaire option count is invalid");
1970
+ }
1971
+ return {
1972
+ prompt: normalizedCodexQuestionnaireText(question.question, "Codex questionnaire prompt"),
1973
+ options: (rawOptions ?? []).map((option) => {
1974
+ if (!isRecord(option)) {
1975
+ throw new Error("Codex questionnaire option is invalid");
1976
+ }
1977
+ return {
1978
+ label: normalizedCodexQuestionnaireText(option.label, "Codex questionnaire option label"),
1979
+ ...(option.description === undefined
1980
+ ? {}
1981
+ : {
1982
+ description: normalizedCodexQuestionnaireText(option.description, "Codex questionnaire option description")
1983
+ })
1984
+ };
1985
+ })
1986
+ };
1987
+ });
1988
+ }
1989
+ function codexPendingQuestionnaireMatchesScreen(call, screen) {
1990
+ if (screen.currentStep < 1 ||
1991
+ screen.currentStep > screen.totalSteps ||
1992
+ call.questions.length !== screen.totalSteps) {
1993
+ return false;
1994
+ }
1995
+ const question = call.questions[screen.currentStep - 1];
1996
+ if (!question)
1997
+ return false;
1998
+ if (question.prompt !== screen.prompt)
1999
+ return false;
2000
+ if (screen.responseKind === "free_text")
2001
+ return true;
2002
+ if (screen.responseKind !== "single_select")
2003
+ return false;
2004
+ return codexRenderedOptionsMatch(question.options, screen.options ?? []);
2005
+ }
2006
+ function normalizedCodexQuestionnaireScreen(screen) {
2007
+ if (!Number.isSafeInteger(screen.currentStep) ||
2008
+ !Number.isSafeInteger(screen.totalSteps) ||
2009
+ screen.currentStep < 1 ||
2010
+ screen.totalSteps < 1 ||
2011
+ screen.currentStep > screen.totalSteps ||
2012
+ screen.totalSteps > CODEX_QUESTIONNAIRE_MAX_QUESTIONS ||
2013
+ !["single_select", "multi_select", "free_text", "confirm"].includes(screen.responseKind) ||
2014
+ (screen.options?.length ?? 0) > CODEX_QUESTIONNAIRE_MAX_OPTIONS) {
2015
+ throw new Error("Codex questionnaire screen evidence is invalid");
2016
+ }
2017
+ return {
2018
+ currentStep: screen.currentStep,
2019
+ totalSteps: screen.totalSteps,
2020
+ prompt: normalizedCodexQuestionnaireText(screen.prompt, "Codex questionnaire screen prompt"),
2021
+ responseKind: screen.responseKind,
2022
+ ...(screen.options === undefined
2023
+ ? {}
2024
+ : {
2025
+ options: screen.options.map((option) => ({
2026
+ label: normalizedCodexQuestionnaireText(option.label, "Codex questionnaire screen option label"),
2027
+ ...(option.description === undefined
2028
+ ? {}
2029
+ : {
2030
+ description: normalizedCodexQuestionnaireText(option.description, "Codex questionnaire screen option description")
2031
+ })
2032
+ }))
2033
+ })
2034
+ };
2035
+ }
2036
+ function normalizedCodexQuestionnaireText(value, label) {
2037
+ if (typeof value !== "string" ||
2038
+ value.length === 0 ||
2039
+ value.length > CODEX_QUESTIONNAIRE_MAX_TEXT_LENGTH ||
2040
+ value.includes("\0")) {
2041
+ throw new Error(`${label} is invalid`);
2042
+ }
2043
+ const normalized = value.replace(/\s+/gu, " ").trim();
2044
+ if (!normalized)
2045
+ throw new Error(`${label} is empty`);
2046
+ return normalized;
2047
+ }
2048
+ function codexRenderedOptionsMatch(durable, rendered) {
2049
+ if (sameCodexQuestionnaireOptions(durable, rendered))
2050
+ return true;
2051
+ const canonicalOther = {
2052
+ label: CODEX_QUESTIONNAIRE_OTHER_LABEL,
2053
+ description: CODEX_QUESTIONNAIRE_OTHER_DESCRIPTION
2054
+ };
2055
+ if (sameCodexQuestionnaireOptions([...durable, canonicalOther], rendered)) {
2056
+ return true;
2057
+ }
2058
+ return !durable.some((option) => option.label === CODEX_QUESTIONNAIRE_CUSTOM_LABEL) && sameCodexQuestionnaireOptions([
2059
+ ...durable,
2060
+ {
2061
+ label: CODEX_QUESTIONNAIRE_CUSTOM_LABEL,
2062
+ description: CODEX_QUESTIONNAIRE_CUSTOM_DESCRIPTION
2063
+ },
2064
+ canonicalOther
2065
+ ], rendered);
2066
+ }
2067
+ function sameCodexQuestionnaireOptions(left, right) {
2068
+ return left.length === right.length && left.every((option, index) => option.label === right[index]?.label &&
2069
+ option.description === right[index]?.description);
2070
+ }
2071
+ function validateCodexRecoveryCandidateForAcceptance(value, anchor) {
2072
+ if (!isRecord(value)) {
2073
+ throw new Error("persisted Codex recovery candidate is invalid");
2074
+ }
2075
+ const sessionId = exactNativeThreadId(value.sessionId);
2076
+ const rollout = normalizedRolloutIdentity(value.rollout);
2077
+ if (value.processUuid !== anchor.process_uuid ||
2078
+ value.processBirth !== anchor.process_birth ||
2079
+ value.evidence !== "codex_open_root_rollout") {
2080
+ throw new Error("persisted Codex recovery candidate has different process authority");
2081
+ }
2082
+ return {
2083
+ sessionId,
2084
+ processUuid: anchor.process_uuid,
2085
+ processBirth: anchor.process_birth,
2086
+ rollout,
2087
+ evidence: "codex_open_root_rollout"
2088
+ };
2089
+ }
1493
2090
  function openExactRollout(rollout) {
1494
2091
  if (!path.isAbsolute(rollout.path)) {
1495
2092
  throw new Error("Codex rollout path is not absolute");
@@ -1556,10 +2153,16 @@ function sameStableFile(left, right) {
1556
2153
  }
1557
2154
  function sameRolloutIdentity(left, right) {
1558
2155
  return left.fd === right.fd &&
1559
- left.device === right.device &&
2156
+ sameRolloutFileIdentity(left, right);
2157
+ }
2158
+ function sameRolloutFileIdentity(left, right) {
2159
+ return left.device === right.device &&
1560
2160
  left.inode === right.inode &&
1561
2161
  left.path === right.path;
1562
2162
  }
2163
+ function codexRolloutInodeKey(rollout) {
2164
+ return `${rollout.device}:${rollout.inode}`;
2165
+ }
1563
2166
  function optionalString(value) {
1564
2167
  return typeof value === "string" && value.trim().length > 0
1565
2168
  ? value.trim()