@scotthuang/agent-knock-knock 0.13.3 → 0.13.5

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 (262) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +13 -5
  3. package/dist/src/callback-outbox-policy.js +27 -6
  4. package/dist/src/callback-outbox-policy.js.map +1 -1
  5. package/dist/src/callback-outbox-service.d.ts +4 -30
  6. package/dist/src/callback-outbox-service.js +21 -154
  7. package/dist/src/callback-outbox-service.js.map +1 -1
  8. package/dist/src/callback-outbox-settlement.js +13 -8
  9. package/dist/src/callback-outbox-settlement.js.map +1 -1
  10. package/dist/src/callback-route-service.d.ts +46 -0
  11. package/dist/src/callback-route-service.js +155 -0
  12. package/dist/src/callback-route-service.js.map +1 -0
  13. package/dist/src/codex-process-incarnation.d.ts +8 -0
  14. package/dist/src/codex-process-incarnation.js +36 -0
  15. package/dist/src/codex-process-incarnation.js.map +1 -0
  16. package/dist/src/codex-terminal-agent-adapter.d.ts +1 -1
  17. package/dist/src/codex-terminal-agent-adapter.js +56 -4
  18. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  19. package/dist/src/deferred-foreground-conflict-policy.d.ts +22 -0
  20. package/dist/src/deferred-foreground-conflict-policy.js +52 -0
  21. package/dist/src/deferred-foreground-conflict-policy.js.map +1 -0
  22. package/dist/src/durable-notification-kernel.d.ts +94 -0
  23. package/dist/src/durable-notification-kernel.js +68 -0
  24. package/dist/src/durable-notification-kernel.js.map +1 -0
  25. package/dist/src/herdr-socket-transport.d.ts +43 -0
  26. package/dist/src/herdr-socket-transport.js +170 -0
  27. package/dist/src/herdr-socket-transport.js.map +1 -0
  28. package/dist/src/herdr-terminal-control-provider.d.ts +2 -45
  29. package/dist/src/herdr-terminal-control-provider.js +2 -172
  30. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  31. package/dist/src/host-adapter-capabilities.d.ts +35 -0
  32. package/dist/src/host-adapter-capabilities.js +146 -0
  33. package/dist/src/host-adapter-capabilities.js.map +1 -0
  34. package/dist/src/host-adapter.d.ts +9 -3
  35. package/dist/src/host-adapter.js +33 -9
  36. package/dist/src/host-adapter.js.map +1 -1
  37. package/dist/src/host-bridge-tools.d.ts +3 -4
  38. package/dist/src/host-bridge-tools.js +49 -92
  39. package/dist/src/host-bridge-tools.js.map +1 -1
  40. package/dist/src/host-bridge.js +5 -5
  41. package/dist/src/host-bridge.js.map +1 -1
  42. package/dist/src/host-monitor-reconciliation.d.ts +6 -0
  43. package/dist/src/host-monitor-reconciliation.js +96 -0
  44. package/dist/src/host-monitor-reconciliation.js.map +1 -0
  45. package/dist/src/native-thread-lifecycle-cli-adapter.js +116 -60
  46. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  47. package/dist/src/openclaw-list-projection.d.ts +2 -0
  48. package/dist/src/openclaw-list-projection.js +3 -0
  49. package/dist/src/openclaw-list-projection.js.map +1 -0
  50. package/dist/src/openclaw-model-facing-field-policy.d.ts +2 -0
  51. package/dist/src/openclaw-model-facing-field-policy.js +3 -0
  52. package/dist/src/openclaw-model-facing-field-policy.js.map +1 -0
  53. package/dist/src/openclaw-plugin-command-adapter.d.ts +6 -29
  54. package/dist/src/openclaw-plugin-command-adapter.js +7 -2660
  55. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  56. package/dist/src/openclaw-plugin-helpers.d.ts +2 -148
  57. package/dist/src/openclaw-plugin-helpers.js +2 -1844
  58. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  59. package/dist/src/openclaw-plugin-schemas.d.ts +2 -563
  60. package/dist/src/openclaw-plugin-schemas.js +2 -572
  61. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  62. package/dist/src/openclaw-plugin-supervisor.d.ts +4 -5
  63. package/dist/src/openclaw-plugin-supervisor.js +5 -94
  64. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  65. package/dist/src/openclaw-plugin-value-helpers.d.ts +2 -0
  66. package/dist/src/openclaw-plugin-value-helpers.js +3 -0
  67. package/dist/src/openclaw-plugin-value-helpers.js.map +1 -0
  68. package/dist/src/openclaw-private-authority-offers.d.ts +4 -39
  69. package/dist/src/openclaw-private-authority-offers.js +3 -258
  70. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  71. package/dist/src/semantic-private-authority-offers.d.ts +41 -0
  72. package/dist/src/semantic-private-authority-offers.js +261 -0
  73. package/dist/src/semantic-private-authority-offers.js.map +1 -0
  74. package/dist/src/semantic-tool-arguments.d.ts +9 -0
  75. package/dist/src/semantic-tool-arguments.js +64 -0
  76. package/dist/src/semantic-tool-arguments.js.map +1 -0
  77. package/dist/src/semantic-tool-catalog.d.ts +48 -0
  78. package/dist/src/semantic-tool-catalog.js +105 -0
  79. package/dist/src/semantic-tool-catalog.js.map +1 -0
  80. package/dist/src/semantic-tool-command-helpers.d.ts +145 -0
  81. package/dist/src/semantic-tool-command-helpers.js +1272 -0
  82. package/dist/src/semantic-tool-command-helpers.js.map +1 -0
  83. package/dist/src/semantic-tool-list-projection.d.ts +10 -0
  84. package/dist/src/semantic-tool-list-projection.js +497 -0
  85. package/dist/src/semantic-tool-list-projection.js.map +1 -0
  86. package/dist/src/semantic-tool-model-facing-field-policy.d.ts +3 -0
  87. package/dist/src/semantic-tool-model-facing-field-policy.js +66 -0
  88. package/dist/src/semantic-tool-model-facing-field-policy.js.map +1 -0
  89. package/dist/src/semantic-tool-presentation.d.ts +37 -0
  90. package/dist/src/semantic-tool-presentation.js +680 -0
  91. package/dist/src/semantic-tool-presentation.js.map +1 -0
  92. package/dist/src/semantic-tool-private-authority.d.ts +39 -0
  93. package/dist/src/semantic-tool-private-authority.js +602 -0
  94. package/dist/src/semantic-tool-private-authority.js.map +1 -0
  95. package/dist/src/semantic-tool-relay.d.ts +16 -0
  96. package/dist/src/semantic-tool-relay.js +175 -0
  97. package/dist/src/semantic-tool-relay.js.map +1 -0
  98. package/dist/src/semantic-tool-runtime.d.ts +10 -0
  99. package/dist/src/semantic-tool-runtime.js +1157 -0
  100. package/dist/src/semantic-tool-runtime.js.map +1 -0
  101. package/dist/src/semantic-tool-schemas.d.ts +563 -0
  102. package/dist/src/semantic-tool-schemas.js +573 -0
  103. package/dist/src/semantic-tool-schemas.js.map +1 -0
  104. package/dist/src/semantic-tool-value-helpers.d.ts +5 -0
  105. package/dist/src/semantic-tool-value-helpers.js +27 -0
  106. package/dist/src/semantic-tool-value-helpers.js.map +1 -0
  107. package/dist/src/store-event-presentation.d.ts +17 -0
  108. package/dist/src/store-event-presentation.js +33 -0
  109. package/dist/src/store-event-presentation.js.map +1 -0
  110. package/dist/src/store.d.ts +3 -16
  111. package/dist/src/store.js +2 -30
  112. package/dist/src/store.js.map +1 -1
  113. package/dist/src/terminal-action-projection.d.ts +2 -98
  114. package/dist/src/terminal-action-projection.js +13 -215
  115. package/dist/src/terminal-action-projection.js.map +1 -1
  116. package/dist/src/terminal-agent-bridge.d.ts +14 -340
  117. package/dist/src/terminal-agent-bridge.js +155 -3226
  118. package/dist/src/terminal-agent-bridge.js.map +1 -1
  119. package/dist/src/terminal-approval-cli-adapter.d.ts +17 -0
  120. package/dist/src/terminal-approval-cli-adapter.js +921 -0
  121. package/dist/src/terminal-approval-cli-adapter.js.map +1 -0
  122. package/dist/src/terminal-command-cli-adapter.d.ts +2 -429
  123. package/dist/src/terminal-command-cli-adapter.js +344 -6061
  124. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  125. package/dist/src/terminal-command-cli-ports.d.ts +443 -0
  126. package/dist/src/terminal-command-cli-ports.js +2 -0
  127. package/dist/src/terminal-command-cli-ports.js.map +1 -0
  128. package/dist/src/terminal-command-dispatch-preparation.d.ts +81 -0
  129. package/dist/src/terminal-command-dispatch-preparation.js +209 -0
  130. package/dist/src/terminal-command-dispatch-preparation.js.map +1 -0
  131. package/dist/src/terminal-command-dispatch-transport.d.ts +35 -0
  132. package/dist/src/terminal-command-dispatch-transport.js +859 -0
  133. package/dist/src/terminal-command-dispatch-transport.js.map +1 -0
  134. package/dist/src/terminal-command-foreground-proof.d.ts +32 -0
  135. package/dist/src/terminal-command-foreground-proof.js +72 -0
  136. package/dist/src/terminal-command-foreground-proof.js.map +1 -0
  137. package/dist/src/terminal-command-send-preflight.d.ts +41 -0
  138. package/dist/src/terminal-command-send-preflight.js +86 -0
  139. package/dist/src/terminal-command-send-preflight.js.map +1 -0
  140. package/dist/src/terminal-composer-classifier.d.ts +55 -0
  141. package/dist/src/terminal-composer-classifier.js +638 -0
  142. package/dist/src/terminal-composer-classifier.js.map +1 -0
  143. package/dist/src/terminal-dispatch-recovery-authority.d.ts +56 -0
  144. package/dist/src/terminal-dispatch-recovery-authority.js +251 -0
  145. package/dist/src/terminal-dispatch-recovery-authority.js.map +1 -0
  146. package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +1 -9
  147. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +5 -240
  148. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  149. package/dist/src/terminal-handoff-cli-adapter.d.ts +1 -11
  150. package/dist/src/terminal-handoff-cli-adapter.js +2 -39
  151. package/dist/src/terminal-handoff-cli-adapter.js.map +1 -1
  152. package/dist/src/terminal-human-explicit-send-cli-adapter.d.ts +25 -0
  153. package/dist/src/terminal-human-explicit-send-cli-adapter.js +903 -0
  154. package/dist/src/terminal-human-explicit-send-cli-adapter.js.map +1 -0
  155. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -6
  156. package/dist/src/terminal-identity-authority-cli-adapter.js +3 -33
  157. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  158. package/dist/src/terminal-interaction-cli-adapter.js +209 -210
  159. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -1
  160. package/dist/src/terminal-interaction-core.d.ts +0 -2
  161. package/dist/src/terminal-interaction-core.js +4 -3
  162. package/dist/src/terminal-interaction-core.js.map +1 -1
  163. package/dist/src/terminal-interaction-protocol.d.ts +21 -0
  164. package/dist/src/terminal-interaction-protocol.js +74 -0
  165. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  166. package/dist/src/terminal-interaction-response-bridge.d.ts +126 -0
  167. package/dist/src/terminal-interaction-response-bridge.js +579 -0
  168. package/dist/src/terminal-interaction-response-bridge.js.map +1 -0
  169. package/dist/src/terminal-interaction-response-transaction.d.ts +44 -0
  170. package/dist/src/terminal-interaction-response-transaction.js +56 -0
  171. package/dist/src/terminal-interaction-response-transaction.js.map +1 -0
  172. package/dist/src/terminal-list-action-policy.d.ts +43 -0
  173. package/dist/src/terminal-list-action-policy.js +165 -0
  174. package/dist/src/terminal-list-action-policy.js.map +1 -0
  175. package/dist/src/terminal-list-cli-adapter.d.ts +6 -36
  176. package/dist/src/terminal-list-cli-adapter.js +241 -1946
  177. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  178. package/dist/src/terminal-list-facts.d.ts +129 -0
  179. package/dist/src/terminal-list-facts.js +179 -0
  180. package/dist/src/terminal-list-facts.js.map +1 -0
  181. package/dist/src/terminal-list-inventory.d.ts +59 -0
  182. package/dist/src/terminal-list-inventory.js +194 -0
  183. package/dist/src/terminal-list-inventory.js.map +1 -0
  184. package/dist/src/terminal-list-ownership-service.d.ts +118 -0
  185. package/dist/src/terminal-list-ownership-service.js +1003 -0
  186. package/dist/src/terminal-list-ownership-service.js.map +1 -0
  187. package/dist/src/terminal-list-projection-service.d.ts +49 -0
  188. package/dist/src/terminal-list-projection-service.js +407 -0
  189. package/dist/src/terminal-list-projection-service.js.map +1 -0
  190. package/dist/src/terminal-list-renderer.d.ts +11 -11
  191. package/dist/src/terminal-list-renderer.js +168 -119
  192. package/dist/src/terminal-list-renderer.js.map +1 -1
  193. package/dist/src/terminal-managed-send-cli-adapter.d.ts +33 -0
  194. package/dist/src/terminal-managed-send-cli-adapter.js +994 -0
  195. package/dist/src/terminal-managed-send-cli-adapter.js.map +1 -0
  196. package/dist/src/terminal-managed-turn-list-action-policy.d.ts +47 -0
  197. package/dist/src/terminal-managed-turn-list-action-policy.js +64 -0
  198. package/dist/src/terminal-managed-turn-list-action-policy.js.map +1 -0
  199. package/dist/src/terminal-model-control-action-policy.d.ts +60 -0
  200. package/dist/src/terminal-model-control-action-policy.js +96 -0
  201. package/dist/src/terminal-model-control-action-policy.js.map +1 -0
  202. package/dist/src/terminal-model-control-availability.d.ts +68 -0
  203. package/dist/src/terminal-model-control-availability.js +157 -0
  204. package/dist/src/terminal-model-control-availability.js.map +1 -0
  205. package/dist/src/terminal-model-control-bridge.d.ts +48 -0
  206. package/dist/src/terminal-model-control-bridge.js +259 -0
  207. package/dist/src/terminal-model-control-bridge.js.map +1 -0
  208. package/dist/src/terminal-model-control-profile.d.ts +55 -0
  209. package/dist/src/terminal-model-control-profile.js +137 -0
  210. package/dist/src/terminal-model-control-profile.js.map +1 -0
  211. package/dist/src/terminal-model-control-subject.d.ts +34 -0
  212. package/dist/src/terminal-model-control-subject.js +58 -0
  213. package/dist/src/terminal-model-control-subject.js.map +1 -0
  214. package/dist/src/terminal-model-control-transaction.d.ts +19 -0
  215. package/dist/src/terminal-model-control-transaction.js +39 -0
  216. package/dist/src/terminal-model-control-transaction.js.map +1 -0
  217. package/dist/src/terminal-model-control.d.ts +37 -18
  218. package/dist/src/terminal-model-control.js +285 -194
  219. package/dist/src/terminal-model-control.js.map +1 -1
  220. package/dist/src/terminal-monitor-application-service.js +50 -17
  221. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  222. package/dist/src/terminal-native-inspection-bridge.d.ts +211 -0
  223. package/dist/src/terminal-native-inspection-bridge.js +1127 -0
  224. package/dist/src/terminal-native-inspection-bridge.js.map +1 -0
  225. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -2
  226. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  227. package/dist/src/terminal-submission-retry-application.d.ts +24 -0
  228. package/dist/src/terminal-submission-retry-application.js +402 -0
  229. package/dist/src/terminal-submission-retry-application.js.map +1 -0
  230. package/dist/src/terminal-submission-retry-ports.d.ts +30 -0
  231. package/dist/src/terminal-submission-retry-ports.js +2 -0
  232. package/dist/src/terminal-submission-retry-ports.js.map +1 -0
  233. package/dist/src/terminal-submission-retry-reconciliation.d.ts +236 -0
  234. package/dist/src/terminal-submission-retry-reconciliation.js +1130 -0
  235. package/dist/src/terminal-submission-retry-reconciliation.js.map +1 -0
  236. package/dist/src/terminal-submission-retry-transport.d.ts +58 -0
  237. package/dist/src/terminal-submission-retry-transport.js +675 -0
  238. package/dist/src/terminal-submission-retry-transport.js.map +1 -0
  239. package/dist/src/terminal-text-submission-bridge.d.ts +200 -0
  240. package/dist/src/terminal-text-submission-bridge.js +632 -0
  241. package/dist/src/terminal-text-submission-bridge.js.map +1 -0
  242. package/dist/src/terminal-user-explicit-send-policy.d.ts +29 -0
  243. package/dist/src/terminal-user-explicit-send-policy.js +46 -0
  244. package/dist/src/terminal-user-explicit-send-policy.js.map +1 -0
  245. package/dist/src/terminal-watch-cli-adapter.js +91 -143
  246. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  247. package/dist/src/terminal-watch-codec.d.ts +9 -0
  248. package/dist/src/terminal-watch-codec.js +1051 -0
  249. package/dist/src/terminal-watch-codec.js.map +1 -0
  250. package/dist/src/terminal-watch-interaction-response-store.d.ts +19 -0
  251. package/dist/src/terminal-watch-interaction-response-store.js +87 -0
  252. package/dist/src/terminal-watch-interaction-response-store.js.map +1 -0
  253. package/dist/src/terminal-watch-record.d.ts +319 -0
  254. package/dist/src/terminal-watch-record.js +742 -0
  255. package/dist/src/terminal-watch-record.js.map +1 -0
  256. package/dist/src/terminal-watch-service.js +48 -27
  257. package/dist/src/terminal-watch-service.js.map +1 -1
  258. package/dist/src/terminal-watch-store.d.ts +3 -279
  259. package/dist/src/terminal-watch-store.js +5 -1779
  260. package/dist/src/terminal-watch-store.js.map +1 -1
  261. package/package.json +7 -2
  262. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +1 -1
@@ -1,27 +1,26 @@
1
1
  // Infrastructure composition for terminal list discovery and selector projection.
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import fs from "node:fs";
4
3
  import path from "node:path";
5
4
  import { listDeferredForegroundTransfers } from "./deferred-foreground-transfer.js";
6
- import { isFinalDeferredForegroundTransferStatus } from "./deferred-foreground-transfer-policy.js";
7
- import { humanObservedHandoffBindingToken, isExactNativeThreadId, managedSessionBindingToken, managedSessionRevision, unmanagedTerminalBindingToken } from "./managed-session.js";
8
- import { executorForConversation, isSessionSendBlockingStatus, isTerminalDispatchOwnerReleasedStatus, resolveExecutor, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
5
+ import { isExactNativeThreadId, unmanagedTerminalBindingToken } from "./managed-session.js";
6
+ import { executorForConversation, resolveExecutor, sessionIdForConversation } from "./protocol.js";
9
7
  import { resolveSessionSelector, sessionShortRef } from "./session-selector.js";
10
8
  import { projectSessionSelectorCandidate } from "./terminal-selector-projection-service.js";
11
- import { listManagedSessions, loadNativeThreadTransition, nativeThreadTransitionsDir, tryLoadManagedSession } from "./session-store.js";
9
+ import { listManagedSessions, tryLoadManagedSession } from "./session-store.js";
12
10
  import { defaultStoreDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, storeManifestPath, STORE_SESSION_AUTHORITY_PROTOCOL } from "./store.js";
13
- import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
14
11
  import { exactCodexReadyStyledComposerCapture } from "./terminal-agent-bridge.js";
15
12
  import { captureCodexHumanStartedActiveTaskAnchor } from "./terminal-submission-acceptance.js";
16
- import { applySessionAuthorityToDispatch, authoritativeTerminalIdentity, compareManagedConversationRecency, decideManagedTerminalAssociation, decideLocalTerminalDispatchOwnership, decideTerminalSendAuthority, decideTerminalSessionAuthorityConflict, decideTerminalUserExplicitModelControlAuthority, decideTerminalUserExplicitModelControlResidualEntryAuthority, decideTerminalUserExplicitModelControlRepairAuthority, decideTerminalUserExplicitSendAuthority, managedTurnNeedsAttention as terminalManagedTurnNeedsAttention, nonOwnerTerminalActions, projectBlockingTurn, projectHandoffDecision, projectHandoffPresentation, projectPublicManagementConflict, projectReconcileBindingAction, projectTerminalDispatchConflict, projectTerminalManagement, selectManagedTerminalHistory, selectTerminalAvailableActions } from "./terminal-action-projection.js";
17
- import { childProcessIdsForRoot, decideManagedBindingConflict, deferredCodexForegroundBindingToken, exactBoundCodexSendSource, isCompleteNativeRollout, nativeAgentIdentityMatchesTurn, processIncarnationRelationship, selectRootTerminalProcesses, terminalControlAliasMatches, terminalControlsShareIncarnation, verifiedEmptyCodexHandoffToken } from "./terminal-authority-policy.js";
18
- import { decideTerminalBindingMatch, terminalObservationFromListEntry } from "./terminal-binding-authority.js";
19
- import { decideTerminalScopedCodexApprovalAuthority, terminalScopedCodexApprovalPromptSnapshot } from "./terminal-scoped-approval-authority.js";
20
- import { sameCanonicalStatePath } from "./terminal-dispatch-ledger-codec.js";
21
- import * as dispatch from "./terminal-dispatch-policy.js";
22
- import { actionsForManagedSessionBinding, currentTerminalActions, listActionContracts, readOnlyListActions, readOnlyManagedTurn, userReleaseListActions, userReleasableManagedTurn, withoutInspectionActionsDuringNativeTransition, renderAvailableListActions, renderCurrentManagedTurn, renderHistoricalManagedTurn, renderManagedTurnListEntry, safeUnavailableManagedTurnActions, sendActionForManagedSession, withoutGenericHandoffSourceClose } from "./terminal-list-renderer.js";
23
- import { hasCanonicalTerminalEndpoint, terminalControlEvidenceMatches } from "./terminal-control-ref.js";
24
- import { validTerminalMonitorTimestampMs as validTimestampMs } from "./terminal-monitor-decision-policy.js";
13
+ import { decideTerminalUserExplicitSendAuthority } from "./terminal-action-projection.js";
14
+ import { materializeModelControlAvailability } from "./terminal-model-control-availability.js";
15
+ import { terminalModelControlPlanConforms } from "./terminal-model-control.js";
16
+ import { childProcessIdsForRoot, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
17
+ import { terminalScopedCodexApprovalPromptSnapshot } from "./terminal-scoped-approval-authority.js";
18
+ import { listActionContracts, renderAvailableListActions, renderTerminalModelControlActions } from "./terminal-list-renderer.js";
19
+ import { collectTerminalListInventory } from "./terminal-list-inventory.js";
20
+ import { collectTerminalListTerminalFacts } from "./terminal-list-facts.js";
21
+ import { decideTerminalListActions, modelControlPolicyFacts } from "./terminal-list-action-policy.js";
22
+ import { createTerminalListOwnershipService } from "./terminal-list-ownership-service.js";
23
+ import { createTerminalListProjectionService } from "./terminal-list-projection-service.js";
25
24
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
26
25
  import { expandHome, writeCliJson as printJson } from "./cli-command-runtime.js";
27
26
  import { cliCwd, cliNowMs, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
@@ -33,16 +32,43 @@ function terminalListRuntime() {
33
32
  }
34
33
  return runtime;
35
34
  }
35
+ function terminalListOwnershipService() {
36
+ return terminalListRuntime().ownershipService;
37
+ }
38
+ function terminalListProjectionService() {
39
+ return terminalListRuntime().projectionService;
40
+ }
36
41
  function withTerminalListRuntime(runtime, operation) {
37
42
  return terminalListRuntimeContext.run(runtime, operation);
38
43
  }
39
44
  export function createTerminalListCliFacade(dependencies) {
45
+ const ownershipService = createTerminalListOwnershipService({
46
+ ...dependencies.store,
47
+ ...dependencies.authority,
48
+ codexProcessIncarnationForPid: dependencies.discovery.codexProcessIncarnationForPid,
49
+ currentWorkingDirectory: cliCwd,
50
+ runtimeLog
51
+ });
52
+ const projectionService = createTerminalListProjectionService({
53
+ approvalTtlMs: dependencies.policy.approvalTtlMs,
54
+ callbackRetryDisposition: dependencies.store.callbackRetryDisposition,
55
+ isVerifiedDeadTerminalAgentProcess: dependencies.store.isVerifiedDeadTerminalAgentProcess,
56
+ listDeferredForegroundTransfers,
57
+ nowMs: cliNowMs,
58
+ ownershipService,
59
+ summarizeConversation: dependencies.store.summarizeConversation,
60
+ terminalBridgeEnabled: dependencies.store.terminalBridgeEnabled,
61
+ terminalBridgeSubmission: dependencies.store.terminalBridgeSubmission,
62
+ terminalControlFromTakeover: dependencies.store.terminalControlFromTakeover
63
+ });
40
64
  const runtime = {
41
65
  ...dependencies.reconciliation,
42
66
  ...dependencies.discovery,
43
67
  ...dependencies.store,
44
68
  ...dependencies.authority,
45
- ...dependencies.policy
69
+ ...dependencies.policy,
70
+ ownershipService,
71
+ projectionService
46
72
  };
47
73
  const call = (operation) => withTerminalListRuntime(runtime, operation);
48
74
  return {
@@ -203,7 +229,7 @@ function projectTerminalListScan(input) {
203
229
  let projection;
204
230
  if (managementErrors.length === 0) {
205
231
  try {
206
- projection = terminalFirstListProjection({
232
+ projection = terminalListProjectionService().projectTerminalFirstList({
207
233
  storeDir,
208
234
  terminals: physicalTerminals,
209
235
  managedSessions,
@@ -350,298 +376,36 @@ async function reconcileStoreForList(storeDir, options) {
350
376
  idle_timeout_minutes: idle.idle_timeout_minutes
351
377
  };
352
378
  }
353
- async function buildTerminalListGroup({ options, agentFilter, statusFilter, terminalId }) {
354
- const empty = {
355
- terminalControlled: [],
356
- summary: {
357
- enabled: false,
358
- agents: [],
359
- error: undefined
360
- }
361
- };
362
- if (options.managedOnly) {
363
- return empty;
364
- }
365
- const registry = terminalListRuntime().createRuntimeTerminalAgentRegistry(options);
366
- const adapters = agentFilter
367
- ? [registry.get(agentFilter)].filter((adapter) => adapter !== undefined)
368
- : registry.list();
369
- if (agentFilter && adapters.length === 0) {
370
- return {
371
- ...empty,
372
- summary: {
373
- enabled: true,
374
- agents: [],
375
- skipped: `terminal agent adapter is not registered for ${agentFilter}`
376
- }
377
- };
378
- }
379
- const terminalProvider = terminalListRuntime().createTerminalControlProvider(options);
380
- const bridge = terminalListRuntime().createTerminalAgentBridge(options, terminalProvider, registry);
381
- const terminalDiagnostics = options.terminalDebug
382
- ? await terminalControlDiagnostics(terminalProvider)
383
- : undefined;
384
- const exactTarget = terminalId
385
- ? exactTerminalTargetDiagnostic(terminalId)
386
- : undefined;
387
- if (terminalId) {
388
- runtimeLog("info", "terminal_exact_scan", {
389
- stage: "started",
390
- terminal_id: terminalId,
391
- target: exactTarget
392
- });
393
- }
394
- const terminalControlled = [];
395
- let activeCount = 0;
396
- const errors = [];
397
- try {
398
- const processSource = terminalListRuntime().createTerminalProcessSource(options);
399
- const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined), { includeAncestors: true });
400
- if (terminalId) {
401
- const targetPid = exactTarget?.pid;
402
- runtimeLog("info", "terminal_exact_scan", {
403
- stage: "process_inventory",
404
- terminal_id: terminalId,
405
- target_pid: targetPid,
406
- snapshot_count: snapshots.length,
407
- target_process_found: targetPid === undefined
408
- ? undefined
409
- : snapshots.some((snapshot) => snapshot.pid === targetPid),
410
- target_process_snapshots: targetPid === undefined
411
- ? []
412
- : snapshots
413
- .filter((snapshot) => snapshot.pid === targetPid)
414
- .map((snapshot) => ({ pid: snapshot.pid, ppid: snapshot.ppid }))
415
- });
416
- }
417
- const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
418
- const rootSessions = selectRootTerminalProcesses(activeSessions, snapshots);
419
- const controlledSessions = rootSessions.filter((session) => session.terminalControl !== undefined);
420
- if (terminalId) {
421
- const targetPid = exactTarget?.pid;
422
- const targetSessions = targetPid === undefined
423
- ? []
424
- : activeSessions.filter((session) => session.pid === targetPid);
425
- runtimeLog("info", "terminal_exact_scan", {
426
- stage: "process_classification_and_association",
427
- terminal_id: terminalId,
428
- target_pid: targetPid,
429
- classified_process_count: activeSessions.length,
430
- root_process_count: rootSessions.length,
431
- controlled_process_count: controlledSessions.length,
432
- target_classified_count: targetSessions.length,
433
- target_controlled_count: targetSessions.filter((session) => session.terminalControl !== undefined).length,
434
- target_sessions: targetSessions.map((session) => ({
435
- agent: session.agent,
436
- pid: session.pid,
437
- ppid: session.ppid,
438
- kind: session.kind,
439
- confidence: session.confidence,
440
- selected_as_root: rootSessions.includes(session),
441
- terminal_conversation_id: session.terminalControl
442
- ? bridge.terminalConversationId(session)
443
- : undefined,
444
- terminal_control: terminalControlScanDiagnostic(session.terminalControl)
445
- }))
446
- });
447
- }
448
- activeCount = controlledSessions.length;
449
- const selectedSessions = terminalId
450
- ? activeSessions.filter((session) => session.terminalControl !== undefined &&
451
- bridge.terminalConversationId(session) === terminalId)
452
- : controlledSessions;
453
- for (const session of selectedSessions) {
454
- try {
455
- terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
456
- }
457
- catch (error) {
458
- errors.push(`terminal process ${session.pid}: ` +
459
- (error instanceof Error ? error.message : String(error)));
460
- }
461
- }
462
- }
463
- catch (error) {
464
- errors.push(error instanceof Error ? error.message : String(error));
465
- }
466
- if (terminalId) {
467
- runtimeLog("info", "terminal_exact_scan", {
468
- stage: "complete",
469
- terminal_id: terminalId,
470
- controlled_process_count: activeCount,
471
- terminal_row_count: terminalControlled.length,
472
- terminal_row_ids: terminalControlled.map((terminal) => terminal.id),
473
- errors
474
- });
475
- }
476
- return {
477
- terminalControlled,
478
- summary: {
479
- enabled: true,
480
- agents: adapters.map((adapter) => adapter.agent),
481
- active_count: activeCount,
482
- terminal_count: terminalControlled.length,
483
- approval_scan: options.noApprovalScan ? "disabled" : "enabled",
484
- diagnostics: terminalDiagnostics,
485
- error: errors.length > 0 ? errors.join("; ") : undefined
379
+ async function buildTerminalListGroup({ options, agentFilter, statusFilter: _statusFilter, terminalId }) {
380
+ const runtime = terminalListRuntime();
381
+ return collectTerminalListInventory({
382
+ options,
383
+ agentFilter,
384
+ terminalId,
385
+ ports: {
386
+ createRegistry: (currentOptions) => runtime.createRuntimeTerminalAgentRegistry(currentOptions),
387
+ createBridge: (currentOptions, provider, registry) => runtime.createTerminalAgentBridge(currentOptions, provider, registry),
388
+ createProvider: (currentOptions) => runtime.createTerminalControlProvider(currentOptions),
389
+ createProcessSource: (currentOptions) => runtime.createTerminalProcessSource(currentOptions),
390
+ projectTerminal: ({ session, activeSessions, options: currentOptions, bridge }) => terminalControlledListEntry(session, activeSessions, currentOptions, bridge),
391
+ log: runtimeLog
486
392
  }
487
- };
488
- }
489
- function exactTerminalTargetDiagnostic(terminalId) {
490
- try {
491
- const parsed = parseTerminalConversationId(terminalId);
492
- return parsed ? {
493
- provider: parsed.kind,
494
- agent: parsed.agent,
495
- route: parsed.target,
496
- pid: parsed.pid
497
- } : undefined;
498
- }
499
- catch (error) {
500
- return {
501
- parse_error: error instanceof Error ? error.message : String(error)
502
- };
503
- }
504
- }
505
- function terminalControlScanDiagnostic(terminalControl) {
506
- if (!terminalControl) {
507
- return undefined;
508
- }
509
- const control = terminalControl;
510
- return {
511
- kind: terminalControl.kind,
512
- target: terminalControl.target,
513
- pane_pid: control.panePid,
514
- process_anchor_pid: control.processAnchorPid,
515
- endpoint_key: control.endpointKey,
516
- resource_key: control.resourceKey,
517
- terminal_id: control.terminalId
518
- };
519
- }
520
- async function terminalControlDiagnostics(provider) {
521
- return provider.diagnostics();
522
- }
523
- function managedTurnListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
524
- return renderManagedTurnListEntry(task, {
525
- terminalBridge,
526
- approvalState,
527
- actionFacts: managedTurnListActionFacts(task, conversation)
528
393
  });
529
394
  }
530
- function managedTurnListActionFacts(task, conversation) {
531
- const nativeTakeover = isRecord(conversation?.native_session_takeover)
532
- ? conversation.native_session_takeover
533
- : undefined;
534
- const managedApprovalPending = isRecord(nativeTakeover?.terminal_bridge_approval);
535
- const terminalBridgeReady = Boolean(conversation &&
536
- terminalListRuntime().terminalBridgeEnabled(conversation) &&
537
- terminalListRuntime().terminalControlFromTakeover(nativeTakeover) !== undefined);
538
- const submission = terminalListRuntime().terminalBridgeSubmission(conversation);
539
- const renewEligible = Boolean(terminalBridgeReady &&
540
- task.status === "stalled" &&
541
- submission?.status !== "uncertain" &&
542
- !terminalListRuntime().isVerifiedDeadTerminalAgentProcess(conversation ?? {}));
543
- const callbackDelivery = isRecord(conversation?.callback_delivery)
544
- ? conversation.callback_delivery
545
- : undefined;
546
- const retryCallbackEligible = Boolean(conversation &&
547
- conversation.legacy_callback_status_error === undefined &&
548
- terminalListRuntime().callbackRetryDisposition(callbackDelivery).state ===
549
- "retryable");
550
- const retrySubmissionCandidate = Boolean(conversation &&
551
- executorForConversation(conversation).kind === "codex" &&
552
- terminalBridgeReady &&
553
- task.status === "stalled" &&
554
- submission?.status === "uncertain" &&
555
- stringValue(submission.last_proven_stage) === "text_injected");
556
- return {
557
- terminalBridgeReady,
558
- managedApprovalPending,
559
- renewEligible,
560
- retryCallbackEligible,
561
- retrySubmissionCandidate
562
- };
563
- }
564
395
  async function terminalControlledListEntry(session, activeSessions, options, bridge = terminalListRuntime().createTerminalAgentBridge(options)) {
565
396
  const terminalControl = session.terminalControl;
566
397
  if (!terminalControl) {
567
398
  throw new Error(`process ${session.pid} is not terminal-controlled`);
568
399
  }
569
- const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge, {
570
- pid: session.pid,
571
- cwd: session.cwd,
572
- // Raw Codex status and approval use the exact full terminal selector as
573
- // their runtime conversation identity. Use the same identity while
574
- // projecting list actions so a prompt-bound terminal token can be
575
- // revalidated by the later status/approve calls.
576
- ...(session.agent === "codex"
577
- ? { conversationId: bridge.terminalConversationId(session) }
578
- : { sessionId: session.sessionId }),
579
- terminalTarget: terminalControl.target
580
- });
581
- const { orphanedDispatch, nativeIdentityObservation, nativeAgentIdentity, authorityNativeIdentityObservation, authorityNativeAgentIdentity, codexOpenRootRolloutInventory, nativeProcessUuid, nativeProcessBirth, nativeProcessEvidence } = await observeTerminalNativeListIdentity(session, terminalControl, options, bridge);
582
- const effectiveTerminalState = effectiveTerminalListState({
583
- session,
584
- terminalState,
585
- nativeIdentityObservation,
586
- nativeAgentIdentity,
587
- nativeIdentityAuthorityObservation: authorityNativeIdentityObservation,
588
- codexOpenRootRolloutInventory,
589
- nativeProcessUuid,
590
- nativeProcessBirth
591
- });
592
- const terminalStatusSnapshot = effectiveTerminalState._terminal_status_snapshot
593
- ? terminalBridgeStatusWithProjection(effectiveTerminalState._terminal_status_snapshot, effectiveTerminalState)
594
- : undefined;
595
- const statusCardObservation = session.agent === "codex" &&
596
- typeof effectiveTerminalState.screen_excerpt === "string"
597
- ? bridge.registry.require("codex").observeThreadLifecycle?.({
598
- operation: { kind: "new_thread" },
599
- phase: "before",
600
- screen: effectiveTerminalState.screen_excerpt
601
- })
602
- : undefined;
603
- const statusCardNativeThreadId = statusCardObservation?.status === "observed" &&
604
- effectiveTerminalState.activity_state === "idle" &&
605
- effectiveTerminalState.approval_state.blocked !== true &&
606
- isExactNativeThreadId(statusCardObservation.nativeThreadId)
607
- ? statusCardObservation.nativeThreadId
608
- : undefined;
609
- const agentVersion = terminalListRuntime().agentVersionForRunningProcess(session.agent, session.pid, options);
610
- const lifecycleCapability = bridge.registry.require(session.agent)
611
- .probeThreadLifecycle?.(agentVersion) ?? {
612
- status: "unsupported",
613
- agentVersion,
614
- newThread: false,
615
- resumeExact: false,
616
- reason: "native thread lifecycle is unavailable"
617
- };
618
- const nativeInspectionCapability = bridge.registry.require(session.agent)
619
- .probeNativeInspection?.(agentVersion) ?? {
620
- status: "unsupported",
621
- agentVersion,
622
- statusInspection: false,
623
- reason: "native inspection is unavailable"
624
- };
625
- const modelControlCapability = bridge.registry.require(session.agent)
626
- .probeModelControl?.(agentVersion) ?? {
627
- status: "unsupported",
628
- agentVersion,
629
- modelSelection: false,
630
- reasoningEffortSelection: false,
631
- reason: "terminal model control is unavailable"
632
- };
633
- const compatibilityWarnings = [...new Set([
634
- lifecycleCapability.compatibilityWarning,
635
- nativeInspectionCapability.compatibilityWarning
636
- ].filter((warning) => typeof warning === "string" && warning.trim().length > 0))];
637
- const codexLatentClearResumeObservationValue = session.agent === "codex"
638
- ? terminalListRuntime().codexLatentClearResumeObservation({
639
- screen: effectiveTerminalState.screen_excerpt,
640
- agentVersion
641
- })
642
- : undefined;
400
+ const facts = await collectTerminalFactsForList(session, activeSessions, options, bridge, terminalControl);
401
+ const { orphanedDispatch, nativeIdentityObservation, nativeAgentIdentity, authorityNativeIdentityObservation, authorityNativeAgentIdentity, codexOpenRootRolloutInventory, nativeProcessUuid, nativeProcessBirth, nativeProcessEvidence } = facts.native;
402
+ const { observed: _observedTerminalState, effective: effectiveTerminalState, projected: projectedTerminalState, snapshot: terminalStatusSnapshot, statusCardNativeThreadId } = facts.status;
403
+ const { agentVersion, lifecycleCapability, nativeInspectionCapability, modelControlCapability, compatibilityWarnings } = facts.runtime;
404
+ const { automatedInputComposerReady, userExplicitComposerReady } = facts.composer;
405
+ const { terminalId, childPids, processIncarnation: physicalProcessIncarnation } = facts.physical;
406
+ const { latentClearResume: codexLatentClearResumeObservationValue } = facts.codex;
643
407
  const lifecycleBindingToken = unmanagedTerminalBindingToken({
644
- terminalId: bridge.terminalConversationId(session),
408
+ terminalId,
645
409
  terminalControl,
646
410
  agent: session.agent,
647
411
  pid: session.pid,
@@ -652,7 +416,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
652
416
  rollout: nativeAgentIdentity?.rollout
653
417
  });
654
418
  const authorityLifecycleBindingToken = unmanagedTerminalBindingToken({
655
- terminalId: bridge.terminalConversationId(session),
419
+ terminalId,
656
420
  terminalControl,
657
421
  agent: session.agent,
658
422
  pid: session.pid,
@@ -662,92 +426,26 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
662
426
  processBirth: nativeProcessBirth,
663
427
  rollout: authorityNativeAgentIdentity?.rollout
664
428
  });
665
- const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
666
- Boolean(nativeProcessUuid && nativeProcessBirth);
667
- let terminalHasBlockingTurn = true;
668
- try {
669
- terminalHasBlockingTurn = terminalIncarnationBlockingTurns(terminalListRuntime().storeDirFromOptions(options), terminalControl).length > 0;
670
- }
671
- catch (error) {
672
- runtimeLog("warn", "terminal_managed_turn_inventory_unavailable", {
673
- terminal_target: terminalControl.target,
674
- error: error instanceof Error ? error.message : String(error)
675
- });
676
- }
677
- const composerAuthority = await observeAutomatedInputComposerReady({
678
- session,
679
- terminalControl,
680
- terminalState: effectiveTerminalState,
681
- options
682
- });
683
- const automatedInputComposerReady = composerAuthority.automatedInputComposerReady;
684
- const userExplicitComposerReady = composerAuthority.userExplicitComposerReady;
685
- let physicalProcessIncarnation;
686
- try {
687
- physicalProcessIncarnation =
688
- terminalListRuntime().processIncarnationForPid(session.pid);
689
- }
690
- catch (error) {
691
- runtimeLog("warn", "terminal_physical_process_incarnation_unavailable", {
692
- agent: session.agent,
693
- terminal_target: terminalControl.target,
694
- pid: session.pid,
695
- error: error instanceof Error ? error.message : String(error)
696
- });
697
- }
698
- const zeroRolloutModelControlVerified = session.agent === "codex" &&
699
- nativeIdentityObservation.status === "verified_absent" &&
700
- authorityNativeIdentityObservation.status === "verified_absent" &&
701
- codexOpenRootRolloutInventory?.status === "verified_absent" &&
702
- codexOpenRootRolloutInventory.pid === session.pid &&
703
- codexOpenRootRolloutInventory.roots.length === 0 &&
704
- codexOpenRootRolloutInventory.processBirth ===
705
- physicalProcessIncarnation?.processBirth;
706
- const exactModelControlNativeIdentity = session.agent === "codex" &&
707
- isExactNativeThreadId(nativeAgentIdentity?.sessionId);
708
- const modelControlResidual = await observeModelControlResidualForList({
709
- bridge,
710
- session,
711
- terminalControl,
712
- agentVersion,
713
- modelControlCapability,
714
- nativeAgentIdentity,
715
- zeroRolloutVerified: zeroRolloutModelControlVerified,
716
- nativeIdentityEligible: exactModelControlNativeIdentity || zeroRolloutModelControlVerified,
717
- effectiveTerminalState,
718
- terminalHasInteraction: terminalStatusSnapshot?.interaction_state !== undefined,
719
- terminalHasBlockingTurn,
720
- hasOrphanedDispatch: orphanedDispatch !== undefined
721
- });
722
- const modelControlSurfaceOpen = modelControlResidual?.state === "recoverable" &&
723
- modelControlResidual.kind === "model_surface";
724
- const projectedTerminalState = modelControlSurfaceOpen
725
- ? terminalListStateWithOpenModelControlSurface(effectiveTerminalState)
726
- : effectiveTerminalState;
727
- const commands = terminalListCommands({
429
+ const actionSubject = {
430
+ exactTerminalRow: true,
431
+ processState: "active",
728
432
  agent: session.agent,
729
433
  terminalControl,
730
- terminalState: projectedTerminalState,
731
- lifecycleCapability,
732
- nativeInspectionCapability,
733
- modelControlCapability,
734
- nativeAgentIdentity: authorityNativeAgentIdentity,
735
- nativeProcessUuid,
736
- nativeProcessBirth,
737
- codexLifecycleIncarnationAvailable,
738
- automatedInputComposerReady,
739
- hasOrphanedDispatch: orphanedDispatch !== undefined,
740
- terminalHasBlockingTurn,
741
- terminalHasInteraction: terminalStatusSnapshot?.interaction_state !== undefined
434
+ pid: session.pid
435
+ };
436
+ const actionDecisions = decideTerminalListActions({
437
+ subject: actionSubject,
438
+ facts
742
439
  });
440
+ const commands = actionDecisions.commands;
743
441
  const entry = {
744
- id: bridge.terminalConversationId(session),
745
- short_ref: sessionShortRef(bridge.terminalConversationId(session)),
442
+ id: terminalId,
443
+ short_ref: sessionShortRef(terminalId),
746
444
  source: "terminal",
747
445
  agent: session.agent,
748
446
  process_state: "active",
749
447
  pid: session.pid,
750
- child_pids: childProcessIdsForRoot(session, activeSessions),
448
+ child_pids: childPids,
751
449
  command: session.command,
752
450
  cwd: session.cwd,
753
451
  workspace: session.cwd,
@@ -791,9 +489,9 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
791
489
  ...(terminalStatusSnapshot
792
490
  ? { _terminal_status_snapshot: terminalStatusSnapshot }
793
491
  : {}),
794
- // Internal action-projection evidence. terminalFirstListProjection strips
795
- // this field after gating every automated-input action that can follow a
796
- // human native-thread switch.
492
+ // Internal action-projection evidence. The terminal-first projection
493
+ // service strips this field after gating every automated-input action that
494
+ // can follow a human native-thread switch.
797
495
  _automated_input_composer_ready: automatedInputComposerReady,
798
496
  _user_explicit_composer_ready: userExplicitComposerReady,
799
497
  _native_identity_authority: {
@@ -822,7 +520,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
822
520
  owner_conversation_id: stringValue(orphanedDispatch.conversation_id),
823
521
  message_id: stringValue(orphanedDispatch.message_id),
824
522
  transition_id: stringValue(orphanedDispatch.transition_id),
825
- recovery: `/akk close ${bridge.terminalConversationId(session)} ` +
523
+ recovery: `/akk close ${terminalId} ` +
826
524
  (orphanedDispatch.kind === "lifecycle"
827
525
  ? `--expected-transition-id ${String(orphanedDispatch.transition_id)}`
828
526
  : `--expected-message-id ${String(orphanedDispatch.message_id)}`)
@@ -832,23 +530,25 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
832
530
  commands
833
531
  };
834
532
  const renderedActions = renderAvailableListActions(entry);
835
- const terminalUserExplicitSendAuthority = decideTerminalUserExplicitSendAuthority({
836
- exactTerminalRow: true,
837
- terminalId: entry.id,
838
- processState: entry.process_state,
839
- terminalControl,
840
- agent: session.agent,
841
- pid: session.pid,
842
- processUuid: physicalProcessIncarnation?.processUuid,
843
- processBirth: physicalProcessIncarnation?.processBirth,
844
- approvalScanned: projectedTerminalState.approval_state.scanned === true,
845
- approvalBlocked: projectedTerminalState.approval_state.blocked === true,
846
- // Codex user-explicit Send treats this observation as advisory: an
847
- // off-screen or truncated composer must not hide the physical action.
848
- // Claude Code still consumes the exact-composer result in the shared
849
- // authority decision below.
850
- userExplicitComposerReady
851
- });
533
+ const terminalUserExplicitSendAuthority = actionDecisions.terminalUserExplicitSend.eligible
534
+ ? decideTerminalUserExplicitSendAuthority({
535
+ exactTerminalRow: true,
536
+ terminalId: entry.id,
537
+ processState: entry.process_state,
538
+ terminalControl,
539
+ agent: session.agent,
540
+ pid: session.pid,
541
+ processUuid: physicalProcessIncarnation?.processUuid,
542
+ processBirth: physicalProcessIncarnation?.processBirth,
543
+ approvalScanned: projectedTerminalState.approval_state.scanned === true,
544
+ approvalBlocked: projectedTerminalState.approval_state.blocked === true,
545
+ // Codex user-explicit Send treats this observation as advisory: an
546
+ // off-screen or truncated composer must not hide the physical action.
547
+ // Claude Code still consumes the exact-composer result in the shared
548
+ // authority decision below.
549
+ userExplicitComposerReady
550
+ })
551
+ : { eligible: false };
852
552
  const terminalUserExplicitSendAction = terminalUserExplicitSendAuthority.eligible
853
553
  ? {
854
554
  tool: "agent_knock_knock_send",
@@ -865,25 +565,26 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
865
565
  : {})
866
566
  }
867
567
  : undefined;
868
- const modelControlActions = projectTerminalModelControlActions({
568
+ const modelControlFacts = modelControlPolicyFacts({
569
+ subject: actionSubject,
570
+ facts
571
+ });
572
+ const modelControlAvailability = materializeModelControlAvailability({
573
+ ...modelControlFacts,
574
+ nativeAuthority: modelControlFacts.nativeAuthority.kind ===
575
+ "exact_session" && lifecycleBindingToken
576
+ ? {
577
+ kind: "exact_session",
578
+ ordinaryBindingToken: lifecycleBindingToken
579
+ }
580
+ : modelControlFacts.nativeAuthority.kind === "verified_zero_rollout"
581
+ ? { kind: "verified_zero_rollout" }
582
+ : { kind: "unavailable" }
583
+ }, actionDecisions.modelControl);
584
+ const modelControlActions = renderTerminalModelControlActions({
869
585
  renderedActions,
870
- terminalId: entry.id,
871
- processState: entry.process_state,
872
- terminalControl,
873
- session,
874
- physicalProcessIncarnation,
875
- agentVersion,
876
- modelControlCapability,
877
- nativeIdentityObservation,
878
- authorityNativeIdentityObservation,
879
- nativeAgentIdentity,
880
- codexOpenRootRolloutInventory,
881
- modelControlResidual,
882
- effectiveTerminalState: projectedTerminalState,
883
- automatedInputComposerReady,
884
- terminalHasInteraction: terminalStatusSnapshot?.interaction_state !== undefined,
885
- terminalHasBlockingTurn,
886
- hasOrphanedDispatch: orphanedDispatch !== undefined
586
+ availability: modelControlAvailability,
587
+ mutationScope: modelControlCapability.scope
887
588
  });
888
589
  const foregroundIdentificationActions = terminalUserExplicitSendAuthority.eligible &&
889
590
  commands.identify_foreground === true
@@ -922,172 +623,112 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
922
623
  }
923
624
  };
924
625
  }
925
- function projectTerminalModelControlActions(input) {
926
- const actions = { ...input.renderedActions };
927
- const authority = decideTerminalUserExplicitModelControlAuthority({
928
- exactTerminalRow: true,
929
- terminalId: input.terminalId,
930
- processState: input.processState,
931
- terminalControl: input.terminalControl,
932
- agent: input.session.agent,
933
- pid: input.session.pid,
934
- processUuid: input.physicalProcessIncarnation?.processUuid,
935
- processBirth: input.physicalProcessIncarnation?.processBirth,
936
- agentVersion: input.agentVersion,
937
- behaviorProfile: input.modelControlCapability.behaviorProfile ===
938
- "codex-model-control-0.154.0"
939
- ? input.modelControlCapability.behaviorProfile
940
- : undefined,
941
- zeroRolloutVerified: input.session.agent === "codex" &&
942
- input.nativeIdentityObservation.status === "verified_absent" &&
943
- input.authorityNativeIdentityObservation.status === "verified_absent" &&
944
- input.codexOpenRootRolloutInventory?.status === "verified_absent" &&
945
- input.codexOpenRootRolloutInventory.pid === input.session.pid &&
946
- input.codexOpenRootRolloutInventory.roots.length === 0 &&
947
- input.codexOpenRootRolloutInventory.processBirth ===
948
- input.physicalProcessIncarnation?.processBirth,
949
- modelControlSupported: input.modelControlCapability.status === "supported" &&
950
- input.modelControlCapability.modelSelection === true &&
951
- input.modelControlCapability.reasoningEffortSelection === true,
952
- activityState: input.effectiveTerminalState.activity_state,
953
- screenState: input.effectiveTerminalState.screen_state,
954
- approvalScanned: input.effectiveTerminalState.approval_state.scanned === true,
955
- approvalBlocked: input.effectiveTerminalState.approval_state.blocked === true,
956
- automatedInputComposerReady: input.automatedInputComposerReady,
957
- terminalHasInteraction: input.terminalHasInteraction,
958
- terminalHasBlockingTurn: input.terminalHasBlockingTurn,
959
- hasOrphanedDispatch: input.hasOrphanedDispatch
960
- });
961
- const hasExactCodexIdentity = input.session.agent === "codex" &&
962
- isExactNativeThreadId(input.nativeAgentIdentity?.sessionId);
963
- if (input.session.agent === "codex" && !hasExactCodexIdentity) {
964
- delete actions.model_options;
965
- }
966
- if (authority.eligible) {
967
- actions.model_options = {
968
- tool: "agent_knock_knock_model_options",
969
- arguments: {
970
- terminal_id: authority.terminalId,
971
- expected_binding_token: authority.expectedBindingToken
972
- },
973
- authority_scope: "terminal_user_explicit_model_control",
974
- mutation_scope: input.modelControlCapability.scope,
975
- requires_user_intent: true
976
- };
977
- }
978
- const residual = input.modelControlResidual?.state === "recoverable"
979
- ? input.modelControlResidual
980
- : undefined;
981
- const repairAuthority = decideTerminalUserExplicitModelControlRepairAuthority({
982
- exactTerminalRow: true,
983
- terminalId: input.terminalId,
984
- processState: input.processState,
985
- terminalControl: input.terminalControl,
986
- agent: input.session.agent,
987
- pid: input.session.pid,
988
- processUuid: input.physicalProcessIncarnation?.processUuid,
989
- processBirth: input.physicalProcessIncarnation?.processBirth,
990
- agentVersion: input.agentVersion,
991
- behaviorProfile: input.modelControlCapability.behaviorProfile ===
992
- "codex-model-control-0.154.0"
993
- ? input.modelControlCapability.behaviorProfile
994
- : undefined,
995
- nativeIdentityEligible: hasExactCodexIdentity || authority.eligible ||
996
- (input.session.agent === "codex" &&
997
- input.nativeIdentityObservation.status === "verified_absent" &&
998
- input.authorityNativeIdentityObservation.status === "verified_absent" &&
999
- input.codexOpenRootRolloutInventory?.status === "verified_absent" &&
1000
- input.codexOpenRootRolloutInventory.pid === input.session.pid &&
1001
- input.codexOpenRootRolloutInventory.roots.length === 0 &&
1002
- input.codexOpenRootRolloutInventory.processBirth ===
1003
- input.physicalProcessIncarnation?.processBirth),
1004
- modelControlSupported: input.modelControlCapability.status === "supported" &&
1005
- input.modelControlCapability.modelSelection === true &&
1006
- input.modelControlCapability.reasoningEffortSelection === true,
1007
- activityState: input.effectiveTerminalState.activity_state,
1008
- approvalScanned: input.effectiveTerminalState.approval_state.scanned === true,
1009
- approvalBlocked: input.effectiveTerminalState.approval_state.blocked === true,
1010
- terminalHasInteraction: input.terminalHasInteraction,
1011
- terminalHasBlockingTurn: input.terminalHasBlockingTurn,
1012
- hasOrphanedDispatch: input.hasOrphanedDispatch,
1013
- residualKind: residual?.kind,
1014
- residualFingerprint: residual?.fingerprint
1015
- });
1016
- const residualEntryAuthority = decideTerminalUserExplicitModelControlResidualEntryAuthority({
1017
- exactTerminalRow: true,
1018
- terminalId: input.terminalId,
1019
- processState: input.processState,
1020
- terminalControl: input.terminalControl,
1021
- agent: input.session.agent,
1022
- pid: input.session.pid,
1023
- processUuid: input.physicalProcessIncarnation?.processUuid,
1024
- processBirth: input.physicalProcessIncarnation?.processBirth,
1025
- agentVersion: input.agentVersion,
1026
- behaviorProfile: input.modelControlCapability.behaviorProfile ===
1027
- "codex-model-control-0.154.0"
1028
- ? input.modelControlCapability.behaviorProfile
1029
- : undefined,
1030
- nativeIdentityEligible: hasExactCodexIdentity || authority.eligible ||
1031
- (input.session.agent === "codex" &&
1032
- input.nativeIdentityObservation.status === "verified_absent" &&
1033
- input.authorityNativeIdentityObservation.status === "verified_absent" &&
1034
- input.codexOpenRootRolloutInventory?.status === "verified_absent" &&
1035
- input.codexOpenRootRolloutInventory.pid === input.session.pid &&
1036
- input.codexOpenRootRolloutInventory.roots.length === 0 &&
1037
- input.codexOpenRootRolloutInventory.processBirth ===
1038
- input.physicalProcessIncarnation?.processBirth),
1039
- modelControlSupported: input.modelControlCapability.status === "supported" &&
1040
- input.modelControlCapability.modelSelection === true &&
1041
- input.modelControlCapability.reasoningEffortSelection === true,
1042
- activityState: input.effectiveTerminalState.activity_state,
1043
- approvalScanned: input.effectiveTerminalState.approval_state.scanned === true,
1044
- approvalBlocked: input.effectiveTerminalState.approval_state.blocked === true,
1045
- terminalHasInteraction: input.terminalHasInteraction,
1046
- terminalHasBlockingTurn: input.terminalHasBlockingTurn,
1047
- hasOrphanedDispatch: input.hasOrphanedDispatch,
1048
- residualKind: residual?.kind,
1049
- residualFingerprint: residual?.fingerprint
626
+ async function collectTerminalFactsForList(session, activeSessions, options, bridge, terminalControl) {
627
+ const terminalId = bridge.terminalConversationId(session);
628
+ const adapter = bridge.registry.require(session.agent);
629
+ return collectTerminalListTerminalFacts({
630
+ session,
631
+ terminalControl,
632
+ terminalId,
633
+ childPids: childProcessIdsForRoot(session, activeSessions),
634
+ adapter,
635
+ ports: terminalListFactPortsFor({
636
+ session,
637
+ terminalControl,
638
+ terminalId,
639
+ options,
640
+ bridge
641
+ })
1050
642
  });
1051
- if (residualEntryAuthority.eligible) {
1052
- actions.model_options = {
1053
- tool: "agent_knock_knock_model_options",
1054
- arguments: {
1055
- terminal_id: residualEntryAuthority.terminalId,
1056
- expected_binding_token: residualEntryAuthority.expectedBindingToken
1057
- },
1058
- authority_scope: "terminal_user_explicit_model_control_residual_entry",
1059
- mutation_scope: input.modelControlCapability.scope,
1060
- requires_user_intent: true
1061
- };
643
+ }
644
+ function terminalListFactPortsFor(input) {
645
+ const { session, terminalControl, terminalId, options, bridge } = input;
646
+ return {
647
+ observeStatus: (agentVersion) => listStateForTerminal(session.agent, terminalControl, options, bridge, {
648
+ pid: session.pid, agentVersion,
649
+ cwd: session.cwd,
650
+ // Status and approval share this exact full terminal identity.
651
+ ...(session.agent === "codex"
652
+ ? { conversationId: terminalId }
653
+ : { sessionId: session.sessionId }),
654
+ terminalTarget: terminalControl.target
655
+ }),
656
+ observeNativeIdentity: (observedTerminalId) => observeTerminalNativeListIdentity(session, terminalControl, options, bridge, observedTerminalId),
657
+ projectEffectiveState: ({ terminalState, native }) => effectiveTerminalListState({
658
+ session,
659
+ terminalState,
660
+ nativeIdentityObservation: native.nativeIdentityObservation,
661
+ nativeAgentIdentity: native.nativeAgentIdentity,
662
+ nativeIdentityAuthorityObservation: native.authorityNativeIdentityObservation,
663
+ codexOpenRootRolloutInventory: native.codexOpenRootRolloutInventory,
664
+ nativeProcessUuid: native.nativeProcessUuid,
665
+ nativeProcessBirth: native.nativeProcessBirth
666
+ }),
667
+ observeAgentVersion: () => terminalListRuntime().agentVersionForRunningProcess(session.agent, session.pid, options),
668
+ observeTerminalHasBlockingTurn: () => observeTerminalHasBlockingTurnForList(options, terminalControl),
669
+ observeComposer: (terminalState) => observeAutomatedInputComposerReady({
670
+ session,
671
+ terminalControl,
672
+ terminalState,
673
+ options
674
+ }),
675
+ observePhysicalProcessIncarnation: () => observePhysicalProcessIncarnationForList(session, terminalControl),
676
+ observeLatentClearResume: (request) => terminalListRuntime().codexLatentClearResumeObservation(request),
677
+ observeModelControlResidual: ({ agentVersion, modelControlCapability, modelControlProfile, native, zeroRolloutVerified, effectiveTerminalState, terminalHasInteraction, terminalHasBlockingTurn, hasOrphanedDispatch }) => observeModelControlResidualForList({
678
+ bridge,
679
+ session,
680
+ terminalControl,
681
+ agentVersion,
682
+ modelControlCapability,
683
+ modelControlProfile,
684
+ nativeAgentIdentity: native.nativeAgentIdentity,
685
+ zeroRolloutVerified,
686
+ nativeIdentityEligible: (session.agent === "codex" &&
687
+ isExactNativeThreadId(native.nativeAgentIdentity?.sessionId)) ||
688
+ zeroRolloutVerified,
689
+ effectiveTerminalState,
690
+ terminalHasInteraction,
691
+ terminalHasBlockingTurn,
692
+ hasOrphanedDispatch
693
+ }),
694
+ projectStatusSnapshot: terminalBridgeStatusWithProjection
695
+ };
696
+ }
697
+ function observeTerminalHasBlockingTurnForList(options, terminalControl) {
698
+ try {
699
+ return terminalIncarnationBlockingTurns(terminalListRuntime().storeDirFromOptions(options), terminalControl).length > 0;
1062
700
  }
1063
- else if (residual) {
1064
- delete actions.model_options;
701
+ catch (error) {
702
+ runtimeLog("warn", "terminal_managed_turn_inventory_unavailable", {
703
+ terminal_target: terminalControl.target,
704
+ error: error instanceof Error ? error.message : String(error)
705
+ });
706
+ return true;
1065
707
  }
1066
- if (repairAuthority.eligible) {
1067
- actions.repair_model_control = {
1068
- tool: "agent_knock_knock_repair_model_control",
1069
- arguments: {
1070
- terminal_id: repairAuthority.terminalId,
1071
- expected_binding_token: repairAuthority.expectedBindingToken
1072
- },
1073
- authority_scope: "terminal_user_explicit_model_control_repair",
1074
- mutation_scope: "exact_model_control_residual_only",
1075
- requires_user_intent: true
1076
- };
708
+ }
709
+ function observePhysicalProcessIncarnationForList(session, terminalControl) {
710
+ try {
711
+ return terminalListRuntime().processIncarnationForPid(session.pid);
1077
712
  }
1078
- else {
1079
- delete actions.repair_model_control;
713
+ catch (error) {
714
+ runtimeLog("warn", "terminal_physical_process_incarnation_unavailable", {
715
+ agent: session.agent,
716
+ terminal_target: terminalControl.target,
717
+ pid: session.pid,
718
+ error: error instanceof Error ? error.message : String(error)
719
+ });
720
+ return undefined;
1080
721
  }
1081
- return actions;
1082
722
  }
1083
723
  async function observeModelControlResidualForList(input) {
1084
724
  const inspect = input.bridge.inspectModelControlResidual;
725
+ const profile = input.modelControlProfile;
1085
726
  if (typeof inspect !== "function" ||
1086
727
  input.session.agent !== "codex" ||
1087
- input.agentVersion !== "0.154.0" ||
728
+ !profile?.supportsResidualRepair ||
1088
729
  input.modelControlCapability.status !== "supported" ||
1089
730
  input.modelControlCapability.behaviorProfile !==
1090
- "codex-model-control-0.154.0" ||
731
+ profile.behaviorProfile ||
1091
732
  !input.nativeIdentityEligible ||
1092
733
  input.effectiveTerminalState.activity_state === "working" ||
1093
734
  input.effectiveTerminalState.activity_state === "awaiting_approval" ||
@@ -1099,11 +740,14 @@ async function observeModelControlResidualForList(input) {
1099
740
  return undefined;
1100
741
  }
1101
742
  const adapter = input.bridge.registry.require("codex");
1102
- const capability = adapter.probeModelControl?.(input.agentVersion);
1103
- const plan = capability?.status === "supported"
1104
- ? adapter.planModelControl?.(capability)
743
+ const plan = input.modelControlCapability.status === "supported"
744
+ ? adapter.planModelControl?.(input.modelControlCapability)
1105
745
  : undefined;
1106
- if (!plan || plan.behaviorProfile !== "codex-model-control-0.154.0") {
746
+ if (!plan || !terminalModelControlPlanConforms({
747
+ agent: input.session.agent,
748
+ agentVersion: input.agentVersion,
749
+ plan
750
+ })) {
1107
751
  return undefined;
1108
752
  }
1109
753
  const identity = input.nativeAgentIdentity;
@@ -1135,7 +779,7 @@ async function observeModelControlResidualForList(input) {
1135
779
  : {})
1136
780
  };
1137
781
  try {
1138
- return await inspect.call(input.bridge, "codex", input.terminalControl, input.agentVersion, plan, { runtime, beforeInput: () => undefined });
782
+ return await inspect.call(input.bridge, "codex", input.terminalControl, profile.agentVersion, plan, { runtime, beforeInput: () => undefined });
1139
783
  }
1140
784
  catch (error) {
1141
785
  runtimeLog("warn", "terminal_model_control_residual_probe_unavailable", {
@@ -1145,81 +789,6 @@ async function observeModelControlResidualForList(input) {
1145
789
  return undefined;
1146
790
  }
1147
791
  }
1148
- function terminalListCommands(input) {
1149
- const { agent, terminalControl, terminalState, lifecycleCapability, nativeInspectionCapability, modelControlCapability, nativeAgentIdentity, nativeProcessUuid, nativeProcessBirth, codexLifecycleIncarnationAvailable, automatedInputComposerReady, hasOrphanedDispatch, terminalHasBlockingTurn, terminalHasInteraction } = input;
1150
- const foregroundIdentificationEligible = agent === "codex" &&
1151
- nativeInspectionCapability.status === "supported" &&
1152
- nativeInspectionCapability.statusInspection === true &&
1153
- terminalState.screen_state === "idle" &&
1154
- terminalState.native_identity_state !== "resolved" &&
1155
- terminalState.approval_state.scanned === true &&
1156
- terminalState.approval_state.blocked !== true &&
1157
- automatedInputComposerReady &&
1158
- codexLifecycleIncarnationAvailable &&
1159
- terminalControl.capabilities.includes("send_keys") &&
1160
- terminalControl.capabilities.includes("screen_status") &&
1161
- !terminalHasInteraction &&
1162
- !hasOrphanedDispatch &&
1163
- !terminalHasBlockingTurn;
1164
- return {
1165
- send: !terminalHasBlockingTurn,
1166
- approve: terminalControl.capabilities.includes("terminal_approval") &&
1167
- terminalState.approval_state.approvable === true,
1168
- status: true,
1169
- cancel: terminalControl.capabilities.includes("terminal_cancel"),
1170
- close: hasOrphanedDispatch,
1171
- new_thread: lifecycleCapability.status === "supported" &&
1172
- lifecycleCapability.newThread === true &&
1173
- codexLifecycleIncarnationAvailable &&
1174
- !terminalHasBlockingTurn,
1175
- list_resumable_threads: lifecycleCapability.status === "supported" &&
1176
- lifecycleCapability.resumeExact === true &&
1177
- codexLifecycleIncarnationAvailable,
1178
- native_inspect: nativeInspectionCapability.status === "supported" &&
1179
- nativeInspectionCapability.statusInspection === true &&
1180
- terminalState.activity_state === "idle" &&
1181
- automatedInputComposerReady &&
1182
- terminalControl.capabilities.includes("send_keys") &&
1183
- terminalControl.capabilities.includes("screen_status") &&
1184
- (agent === "codex"
1185
- ? codexLifecycleIncarnationAvailable
1186
- : Boolean(nativeAgentIdentity?.sessionId &&
1187
- nativeAgentIdentity.processUuid)) &&
1188
- !hasOrphanedDispatch &&
1189
- !terminalHasBlockingTurn,
1190
- model_options: modelControlCapability.status === "supported" &&
1191
- modelControlCapability.modelSelection === true &&
1192
- modelControlCapability.reasoningEffortSelection === true &&
1193
- terminalState.activity_state === "idle" &&
1194
- terminalState.approval_state.scanned === true &&
1195
- terminalState.approval_state.blocked !== true &&
1196
- automatedInputComposerReady &&
1197
- terminalControl.capabilities.includes("send_keys") &&
1198
- terminalControl.capabilities.includes("screen_status") &&
1199
- (agent === "codex"
1200
- ? codexLifecycleIncarnationAvailable
1201
- : Boolean(nativeAgentIdentity?.sessionId &&
1202
- nativeAgentIdentity.processUuid)) &&
1203
- !terminalHasInteraction &&
1204
- !hasOrphanedDispatch &&
1205
- !terminalHasBlockingTurn,
1206
- identify_foreground: foregroundIdentificationEligible,
1207
- identify_and_send: foregroundIdentificationEligible,
1208
- watch: terminalControl.capabilities.includes("screen_status") ||
1209
- Boolean(nativeAgentIdentity?.sessionId &&
1210
- (agent !== "codex" || nativeAgentIdentity.rollout))
1211
- };
1212
- }
1213
- function terminalListStateWithOpenModelControlSurface(state) {
1214
- const reason = "an exact native model-control surface is open";
1215
- return {
1216
- ...state,
1217
- activity_state: "unknown",
1218
- activity_reason: reason,
1219
- screen_state: "unknown",
1220
- screen_reason: reason
1221
- };
1222
- }
1223
792
  function effectiveTerminalListState(input) {
1224
793
  const { session, terminalState, nativeIdentityObservation, nativeAgentIdentity, nativeIdentityAuthorityObservation, codexOpenRootRolloutInventory, nativeProcessUuid, nativeProcessBirth } = input;
1225
794
  const nativeIdentityState = terminalNativeIdentityState({
@@ -1381,7 +950,7 @@ function terminalBridgeStatusWithProjection(status, projection) {
1381
950
  }
1382
951
  return Object.create(Object.getPrototypeOf(status), descriptors);
1383
952
  }
1384
- async function observeTerminalNativeListIdentity(session, terminalControl, options, bridge) {
953
+ async function observeTerminalNativeListIdentity(session, terminalControl, options, bridge, terminalId) {
1385
954
  let orphanedDispatch;
1386
955
  try {
1387
956
  orphanedDispatch =
@@ -1399,7 +968,7 @@ async function observeTerminalNativeListIdentity(session, terminalControl, optio
1399
968
  codexIdentityContext = terminalListRuntime().codexManagedIdentityResolutionContext({
1400
969
  storeDir: terminalListRuntime().storeDirFromOptions(options),
1401
970
  terminal: {
1402
- conversationId: bridge.terminalConversationId(session),
971
+ conversationId: terminalId,
1403
972
  agent: session.agent,
1404
973
  pid: session.pid,
1405
974
  terminalControl
@@ -1570,1197 +1139,41 @@ async function observeAutomatedInputComposerReady({ session, terminalControl, te
1570
1139
  userExplicitComposerReady: userExplicitReady
1571
1140
  };
1572
1141
  }
1573
- function observeTerminalListBindingAuthority(listedTerminal, context) {
1574
- const { storeDir, managedSessions, allConversations, displayedConversations, mutationsAllowed, nonterminalDeferredTransfers } = context;
1575
- const { _automated_input_composer_ready: automatedInputComposerReady, _user_explicit_composer_ready: _userExplicitComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, _codex_latent_clear_resume: codexLatentClearResumeValue, _terminal_user_explicit_send_action: terminalUserExplicitSendAction, _terminal_status_snapshot: _terminalStatusSnapshot, _native_identity_authority: nativeIdentityAuthorityValue, ...publicTerminal } = listedTerminal;
1576
- const terminal = terminalIdentityAuthorityView(listedTerminal, nativeIdentityAuthorityValue);
1577
- const codexOpenRootRolloutInventory = isRecord(codexOpenRootRolloutInventoryValue)
1578
- ? codexOpenRootRolloutInventoryValue
1579
- : undefined;
1580
- const terminalControl = isRecord(terminal.terminal_control)
1581
- ? terminal.terminal_control
1582
- : undefined;
1583
- const terminalHasNonterminalDeferredTransfer = Boolean(terminalControl && nonterminalDeferredTransfers.some((transfer) => transfer.terminal_id === String(terminal.id) &&
1584
- transfer.process_pid === Number(terminal.pid) &&
1585
- terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl)));
1586
- const allRelated = terminalControl
1587
- ? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
1588
- : [];
1589
- const displayedRelated = terminalControl
1590
- ? displayedConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
1591
- : [];
1592
- const relatedSessions = terminalControl
1593
- ? managedSessions.filter((session) => terminalControlsShareIncarnation(session.binding?.terminal_control, terminalControl))
1594
- : [];
1595
- const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir));
1596
- const conflictingBoundSessionClaims = relatedSessions.flatMap((session) => {
1597
- const kind = managedBindingConflictKindForLiveTerminalEntry({
1598
- storeDir,
1599
- session,
1600
- terminal
1601
- });
1602
- return kind && kind !== "stale_process_incarnation"
1603
- ? [{ session, kind }]
1604
- : [];
1605
- });
1606
- const unresolvedSessionClaims = relatedSessions.filter((session) => ["transitioning", "quarantined"].includes(session.status) &&
1607
- managedSessionClaimsLiveTerminalEntry(session, terminal));
1608
- const sessionAuthorityConflict = decideTerminalSessionAuthorityConflict({
1609
- unresolvedSessionClaims,
1610
- conflictingBoundSessionClaims,
1611
- matchingSessions
1612
- });
1613
- const authoritativeSession = matchingSessions[0];
1614
- const discoveredOwnership = terminalControl
1615
- ? terminalDispatchOwnership(terminalControl)
1616
- : { state: "none" };
1617
- const localOwnership = discoveredOwnership.state === "current"
1618
- ? localTerminalDispatchOwnership(discoveredOwnership.conversation, allRelated, terminal)
1619
- : discoveredOwnership;
1620
- const dispatchOwnerMismatch = !sessionAuthorityConflict &&
1621
- localOwnership.state === "current" &&
1622
- authoritativeSession &&
1623
- sessionIdForConversation(localOwnership.conversation) !==
1624
- authoritativeSession.session_id
1625
- ? {
1626
- ownerSessionId: sessionIdForConversation(discoveredOwnership.state === "current"
1627
- ? discoveredOwnership.conversation
1628
- : localOwnership.conversation)
1629
- }
1630
- : undefined;
1631
- const ownership = applySessionAuthorityToDispatch({
1632
- localOwnership,
1633
- sessionAuthorityConflict,
1634
- authoritativeSession,
1635
- dispatchOwnerMismatch
1636
- });
1637
- const discoveredRawActions = isRecord(terminal.available_actions)
1638
- ? terminal.available_actions
1639
- : {};
1640
- const rawActions = mutationsAllowed
1641
- ? discoveredRawActions
1642
- : readOnlyListActions(discoveredRawActions);
1643
- const rawSendAction = isRecord(rawActions.send)
1644
- ? rawActions.send
1645
- : {};
1646
- const bindingAwareRawActions = authoritativeSession
1647
- ? actionsForManagedSessionBinding(rawActions, authoritativeSession)
1648
- : rawActions;
1649
- const sessionAwareRawActionsBase = authoritativeSession &&
1650
- managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
1651
- ? withoutInspectionActionsDuringNativeTransition(bindingAwareRawActions)
1652
- : bindingAwareRawActions;
1653
- const sessionAwareRawActions = terminalHasNonterminalDeferredTransfer
1654
- ? readOnlyListActions(sessionAwareRawActionsBase)
1655
- : sessionAwareRawActionsBase;
1656
- const soleBindingConflict = conflictingBoundSessionClaims.length === 1
1657
- ? conflictingBoundSessionClaims[0]
1658
- : undefined;
1659
- const externalHandoffDetected = conflictingBoundSessionClaims.some(({ kind }) => kind === "live_external_thread_change");
1660
- const conflictingSessionRevision = Number(soleBindingConflict?.session.revision);
1661
- const conflictingSessionTurns = soleBindingConflict
1662
- ? terminalListRuntime().managedTurnsForSession(storeDir, soleBindingConflict.session.session_id)
1663
- : [];
1664
- const expectedTerminalToken = stringValue(terminal.lifecycle_binding_token);
1665
- const externalHandoffNativeThreadId = stringValue(terminal.native_agent_status_card_session_id) ?? stringValue(terminal.native_agent_session_id);
1666
- const resolvedNativeThreadId = stringValue(terminal.native_agent_session_id);
1667
- const externalHandoffTerminalToken = terminalControl &&
1668
- externalHandoffNativeThreadId &&
1669
- isExactNativeThreadId(externalHandoffNativeThreadId)
1670
- ? unmanagedTerminalBindingToken({
1671
- terminalId: stringValue(terminal.id),
1672
- terminalControl,
1673
- agent: terminal.agent,
1674
- pid: Number(terminal.pid),
1675
- workspace: terminal.workspace ?? terminal.cwd ?? cliCwd(),
1676
- nativeThreadId: externalHandoffNativeThreadId,
1677
- processUuid: stringValue(terminal.native_agent_process_uuid),
1678
- processBirth: stringValue(terminal.native_agent_process_birth),
1679
- rollout: resolvedNativeThreadId === externalHandoffNativeThreadId &&
1680
- isRecord(terminal.native_agent_rollout)
1681
- ? terminal.native_agent_rollout
1682
- : undefined
1683
- })
1684
- : undefined;
1685
- const externalHandoffTarget = soleBindingConflict?.kind === "live_external_thread_change" &&
1686
- externalHandoffNativeThreadId &&
1687
- isExactNativeThreadId(externalHandoffNativeThreadId)
1688
- ? terminalListRuntime().observedHandoffTargetResolution({
1689
- storeDir,
1690
- agent: terminal.agent,
1691
- workspace: terminal.workspace ?? terminal.cwd ?? cliCwd(),
1692
- nativeThreadId: externalHandoffNativeThreadId.toLowerCase(),
1693
- sourceSessionId: soleBindingConflict.session.session_id
1694
- })
1695
- : undefined;
1696
- const externalHandoffSnapshotToken = externalHandoffTerminalToken &&
1697
- soleBindingConflict?.kind === "live_external_thread_change" &&
1698
- externalHandoffTarget?.status === "eligible"
1699
- ? humanObservedHandoffBindingToken({
1700
- terminal_token: externalHandoffTerminalToken,
1701
- source_session_id: soleBindingConflict.session.session_id,
1702
- source_revision: managedSessionRevision(soleBindingConflict.session),
1703
- source_binding_token: managedSessionBindingToken(soleBindingConflict.session),
1704
- target: externalHandoffTarget.snapshot
1705
- })
1706
- : undefined;
1707
- const blockingHandoffTurns = conflictingSessionTurns.filter((turn) => isSessionSendBlockingStatus(turn.status));
1708
- const terminalBlockingTurns = terminalControl
1709
- ? terminalIncarnationBlockingTurns(storeDir, terminalControl, allRelated)
1710
- : [];
1711
- const externalHandoffSourceSessionIds = new Set(conflictingBoundSessionClaims
1712
- .filter(({ kind }) => kind === "live_external_thread_change")
1713
- .map(({ session }) => session.session_id));
1714
- const handoffSourceBlockingTurns = terminalBlockingTurns.filter((turn) => externalHandoffSourceSessionIds.has(sessionIdForConversation(turn)));
1715
- const nativeIdentityObservation = isRecord(terminal.native_agent_identity_observation)
1716
- ? terminal.native_agent_identity_observation
1717
- : undefined;
1718
- const codexProcessUuid = stringValue(terminal.native_agent_process_uuid);
1719
- const codexProcessBirth = stringValue(terminal.native_agent_process_birth);
1720
- const codexWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
1721
- const codexSendAuthorityContext = terminalControl
1722
- ? {
1723
- terminalId: String(terminal.id),
1724
- terminalControl,
1725
- pid: Number(terminal.pid),
1726
- workspace: codexWorkspace,
1727
- liveProcessUuid: codexProcessUuid,
1728
- liveProcessBirth: codexProcessBirth
1729
- }
1730
- : undefined;
1731
- return {
1732
- authorityTerminal: terminal,
1733
- automatedInputComposerReady,
1734
- codexOpenRootRolloutInventory,
1735
- codexLatentClearResumeValue,
1736
- terminalUserExplicitSendAction,
1737
- publicTerminal,
1738
- terminalControl,
1739
- terminalHasNonterminalDeferredTransfer,
1740
- allRelated,
1741
- displayedRelated,
1742
- relatedSessions,
1743
- matchingSessions,
1744
- conflictingBoundSessionClaims,
1745
- unresolvedSessionClaims,
1746
- sessionAuthorityConflict,
1747
- authoritativeSession,
1748
- discoveredOwnership,
1749
- ownership,
1750
- rawActions,
1751
- rawSendAction,
1752
- sessionAwareRawActions,
1753
- soleBindingConflict,
1754
- externalHandoffDetected,
1755
- conflictingSessionRevision,
1756
- conflictingSessionTurns,
1757
- expectedTerminalToken,
1758
- externalHandoffNativeThreadId,
1759
- externalHandoffTarget,
1760
- externalHandoffSnapshotToken,
1761
- blockingHandoffTurns,
1762
- terminalBlockingTurns,
1763
- handoffSourceBlockingTurns,
1764
- nativeIdentityObservation,
1765
- codexProcessUuid,
1766
- codexProcessBirth,
1767
- codexWorkspace,
1768
- codexSendAuthorityContext
1769
- };
1770
- }
1771
- function terminalIdentityAuthorityView(terminal, authorityValue) {
1772
- if (!isRecord(authorityValue)) {
1773
- return terminal;
1774
- }
1775
- const observation = isRecord(authorityValue.observation)
1776
- ? authorityValue.observation
1777
- : { status: "not_observed" };
1778
- const identity = isRecord(authorityValue.identity)
1779
- ? authorityValue.identity
1780
- : undefined;
1781
- return {
1782
- ...terminal,
1783
- native_agent_session_id: stringValue(identity?.sessionId),
1784
- native_agent_rollout: isRecord(identity?.rollout)
1785
- ? identity.rollout
1786
- : undefined,
1787
- native_agent_identity_observation: observation,
1788
- lifecycle_binding_token: stringValue(authorityValue.lifecycle_binding_token) ??
1789
- terminal.lifecycle_binding_token
1790
- };
1791
- }
1792
- function observeVerifiedEmptyTerminalAuthority(terminal, context, binding) {
1793
- const { storeDir, terminals, mutationsAllowed } = context;
1794
- const { automatedInputComposerReady, terminalControl, matchingSessions, unresolvedSessionClaims, discoveredOwnership, rawActions, soleBindingConflict, blockingHandoffTurns, terminalBlockingTurns, nativeIdentityObservation, codexProcessUuid, codexProcessBirth, codexWorkspace, codexSendAuthorityContext } = binding;
1795
- const verifiedEmptySourceNativeThreadId = stringValue(soleBindingConflict?.session.binding?.native_thread_id)?.toLowerCase();
1796
- const verifiedEmptySourceActiveElsewhere = Boolean(verifiedEmptySourceNativeThreadId &&
1797
- terminals.some((candidate) => stringValue(candidate.id) !== stringValue(terminal.id) &&
1798
- candidate.agent === "codex" &&
1799
- stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
1800
- verifiedEmptySourceNativeThreadId));
1801
- const verifiedEmptyRawSendAction = isRecord(rawActions.send)
1802
- ? rawActions.send
1803
- : {
1804
- tool: "agent_knock_knock_send",
1805
- arguments: { selector: stringValue(terminal.id) },
1806
- missing_required: ["request"]
1807
- };
1808
- const verifiedEmptyCodexHandoffEligible = Boolean(mutationsAllowed &&
1809
- terminal.agent === "codex" &&
1810
- terminalControl &&
1811
- discoveredOwnership.state === "none" &&
1812
- unresolvedSessionClaims.length === 0 &&
1813
- matchingSessions.length === 0 &&
1814
- soleBindingConflict?.kind === "unverifiable" &&
1815
- nativeIdentityObservation?.status === "verified_absent" &&
1816
- codexSendAuthorityContext &&
1817
- codexProcessUuid &&
1818
- codexProcessBirth &&
1819
- codexWorkspace &&
1820
- exactBoundCodexSendSource({
1821
- kind: "verified_empty",
1822
- sourceSession: soleBindingConflict.session,
1823
- context: codexSendAuthorityContext
1824
- }) &&
1825
- ["idle", "unknown"].includes(String(terminal.activity_state)) &&
1826
- automatedInputComposerReady === true &&
1827
- !(isRecord(terminal.approval_state) &&
1828
- terminal.approval_state.blocked === true) &&
1829
- blockingHandoffTurns.length === 0 &&
1830
- terminalBlockingTurns.length === 0 &&
1831
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
1832
- !verifiedEmptySourceActiveElsewhere &&
1833
- terminal.orphaned_terminal_dispatch === undefined &&
1834
- terminalControl?.capabilities.includes("send_keys") &&
1835
- terminalControl.capabilities.includes("screen_status"));
1836
- const verifiedEmptyCodexSnapshotToken = verifiedEmptyCodexHandoffEligible &&
1837
- codexSendAuthorityContext &&
1838
- soleBindingConflict &&
1839
- codexProcessUuid &&
1840
- codexProcessBirth &&
1841
- codexWorkspace
1842
- ? verifiedEmptyCodexHandoffToken({
1843
- terminalId: String(terminal.id),
1844
- terminalControl: codexSendAuthorityContext.terminalControl,
1845
- pid: Number(terminal.pid),
1846
- workspace: codexWorkspace,
1847
- processUuid: codexProcessUuid,
1848
- processBirth: codexProcessBirth,
1849
- sourceSession: soleBindingConflict.session
1850
- })
1851
- : undefined;
1852
- return {
1853
- ...binding,
1854
- verifiedEmptyRawSendAction,
1855
- verifiedEmptyCodexSnapshotToken
1856
- };
1857
- }
1858
- function observeDeferredSourceAuthority(terminal, context, observation) {
1859
- const { terminals } = context;
1860
- const { codexLatentClearResumeValue, codexOpenRootRolloutInventory, authoritativeSession, matchingSessions, conflictingBoundSessionClaims, unresolvedSessionClaims, soleBindingConflict, nativeIdentityObservation } = observation;
1861
- const deferredCodexCandidateInventory = codexOpenRootRolloutInventory &&
1862
- codexOpenRootRolloutInventory.roots.length > 0
1863
- ? codexOpenRootRolloutInventory
1864
- : undefined;
1865
- const abandonedConflictSource = !authoritativeSession &&
1866
- soleBindingConflict?.kind === "unverifiable" &&
1867
- matchingSessions.length === 0 &&
1868
- conflictingBoundSessionClaims.length === 1 &&
1869
- unresolvedSessionClaims.length === 0 &&
1870
- terminal.agent === "codex"
1871
- ? soleBindingConflict.session
1872
- : undefined;
1873
- const deferredCodexSource = (authoritativeSession ??
1874
- abandonedConflictSource) &&
1875
- terminal.agent === "codex" &&
1876
- (nativeIdentityObservation?.status === "verified_absent" ||
1877
- deferredCodexCandidateInventory !== undefined)
1878
- ? authoritativeSession ?? abandonedConflictSource
1879
- : undefined;
1880
- const deferredCodexSourceNativeThreadId = stringValue(deferredCodexSource?.binding?.native_thread_id)?.toLowerCase();
1881
- const deferredCodexLatentClearResumeFingerprint = isRecord(codexLatentClearResumeValue) &&
1882
- stringValue(codexLatentClearResumeValue.source_native_thread_id)?.toLowerCase() === deferredCodexSourceNativeThreadId
1883
- ? stringValue(codexLatentClearResumeValue.fingerprint)
1884
- : undefined;
1885
- if (deferredCodexLatentClearResumeFingerprint) {
1886
- // The resume hint is useful operational context, but it is not durable
1887
- // foreground authority: it can scroll away while the latent thread is
1888
- // still current. Candidate routing and its token rely on the complete
1889
- // rollout inventory and Store authority below instead.
1890
- runtimeLog("info", "terminal_codex_latent_clear_hint_observed", {
1891
- terminal_id: String(terminal.id),
1892
- source_session_id: deferredCodexSource?.session_id,
1893
- source_native_thread_id: deferredCodexSourceNativeThreadId
1894
- });
1895
- }
1896
- const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
1897
- terminals.some((candidate) => stringValue(candidate.id) !== stringValue(terminal.id) &&
1898
- candidate.agent === "codex" &&
1899
- stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
1900
- deferredCodexSourceNativeThreadId));
1901
- return {
1902
- ...observation,
1903
- deferredCodexCandidateInventory,
1904
- abandonedConflictSource,
1905
- deferredCodexSource,
1906
- deferredCodexSourceActiveElsewhere
1907
- };
1908
- }
1909
- function observeDeferredTerminalAuthority(terminal, context, observation) {
1910
- const { storeDir, mutationsAllowed } = context;
1911
- const { automatedInputComposerReady, terminalControl, terminalHasNonterminalDeferredTransfer, matchingSessions, conflictingBoundSessionClaims, unresolvedSessionClaims, discoveredOwnership, terminalBlockingTurns, codexProcessUuid, codexProcessBirth, codexWorkspace, codexSendAuthorityContext, deferredCodexCandidateInventory, abandonedConflictSource, deferredCodexSource, deferredCodexSourceActiveElsewhere } = observation;
1912
- const deferredCodexAuthority = codexSendAuthorityContext
1913
- ? terminalListRuntime().observeDeferredCodexAuthority({
1914
- mode: "list",
1915
- storeDir,
1916
- context: codexSendAuthorityContext,
1917
- sourceSession: deferredCodexSource,
1918
- candidateInventory: deferredCodexCandidateInventory,
1919
- abandonment: abandonedConflictSource
1920
- ? "missing_rollout"
1921
- : "never",
1922
- requireUnclaimedCandidate: true
1923
- })
1924
- : undefined;
1925
- const deferredCodexSourceRolloutAuthority = deferredCodexAuthority?.sourceRolloutAuthority ?? "present";
1926
- const deferredCodexDispatchSnapshot = deferredCodexAuthority?.dispatchSnapshot;
1927
- const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
1928
- !terminalHasNonterminalDeferredTransfer &&
1929
- deferredCodexSource &&
1930
- terminalControl &&
1931
- hasCanonicalTerminalEndpoint(terminalControl) &&
1932
- discoveredOwnership.state === "none" &&
1933
- unresolvedSessionClaims.length === 0 &&
1934
- ((matchingSessions.length === 1 &&
1935
- conflictingBoundSessionClaims.length === 0) ||
1936
- (deferredCodexSourceRolloutAuthority ===
1937
- "explicitly_abandoned_predecessor" &&
1938
- matchingSessions.length === 0 &&
1939
- conflictingBoundSessionClaims.length === 1)) &&
1940
- codexProcessUuid &&
1941
- codexProcessBirth &&
1942
- codexWorkspace &&
1943
- deferredCodexAuthority?.exactSource &&
1944
- terminalBlockingTurns.length === 0 &&
1945
- terminal.orphaned_terminal_dispatch === undefined &&
1946
- deferredCodexDispatchSnapshot &&
1947
- ["idle", "unknown"].includes(String(terminal.activity_state)) &&
1948
- automatedInputComposerReady === true &&
1949
- !(isRecord(terminal.approval_state) &&
1950
- terminal.approval_state.blocked === true) &&
1951
- !deferredCodexSourceActiveElsewhere &&
1952
- terminalControl.capabilities.includes("send_keys") &&
1953
- terminalControl.capabilities.includes("screen_status"));
1954
- const deferredCodexForegroundToken = deferredCodexForegroundEligible &&
1955
- deferredCodexSource &&
1956
- terminalControl &&
1957
- codexProcessUuid &&
1958
- codexProcessBirth &&
1959
- codexWorkspace &&
1960
- deferredCodexDispatchSnapshot
1961
- ? deferredCodexForegroundBindingToken({
1962
- terminalId: String(terminal.id),
1963
- terminalControl,
1964
- pid: Number(terminal.pid),
1965
- workspace: codexWorkspace,
1966
- processUuid: codexProcessUuid,
1967
- processBirth: codexProcessBirth,
1968
- sourceSession: deferredCodexSource,
1969
- dispatchSnapshot: deferredCodexDispatchSnapshot,
1970
- sourceTurnHistory: deferredCodexAuthority?.sourceTurnHistory,
1971
- sourceRolloutAuthority: deferredCodexSourceRolloutAuthority,
1972
- sourceAbandonmentFingerprint: deferredCodexAuthority?.sourceAbandonmentFingerprint,
1973
- ...(deferredCodexCandidateInventory
1974
- ? { candidateInventory: deferredCodexCandidateInventory }
1975
- : {})
1976
- })
1977
- : undefined;
1978
- return {
1979
- ...observation,
1980
- deferredCodexSourceRolloutAuthority,
1981
- deferredCodexForegroundToken
1982
- };
1983
- }
1984
- function observeTerminalHandoffAuthority(terminal, context, observation) {
1985
- const { storeDir, mutationsAllowed, conversationHasNonterminalDeferredTransfer } = context;
1986
- const { automatedInputComposerReady, terminalControl, matchingSessions, unresolvedSessionClaims, discoveredOwnership, rawActions, soleBindingConflict, conflictingSessionRevision, conflictingSessionTurns, expectedTerminalToken, externalHandoffNativeThreadId, externalHandoffTarget, externalHandoffSnapshotToken, blockingHandoffTurns, terminalBlockingTurns, handoffSourceBlockingTurns } = observation;
1987
- const reconcileBindingAction = mutationsAllowed &&
1988
- discoveredOwnership.state === "none" &&
1989
- unresolvedSessionClaims.length === 0 &&
1990
- matchingSessions.length === 0 &&
1991
- soleBindingConflict &&
1992
- soleBindingConflict.kind !== "unverifiable" &&
1993
- Number.isSafeInteger(conflictingSessionRevision) &&
1994
- conflictingSessionRevision > 0 &&
1995
- expectedTerminalToken &&
1996
- terminal.activity_state === "idle" &&
1997
- !(isRecord(terminal.approval_state) &&
1998
- terminal.approval_state.blocked === true) &&
1999
- !conflictingSessionTurns.some((turn) => isSessionSendBlockingStatus(turn.status)) &&
2000
- terminalBlockingTurns.length === 0 &&
2001
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
2002
- ? projectReconcileBindingAction({
2003
- terminalId: stringValue(terminal.id),
2004
- conflictingSession: soleBindingConflict.session,
2005
- conflictingSessionRevision,
2006
- expectedTerminalToken
2007
- })
2008
- : undefined;
2009
- const externalHandoffAdoptable = Boolean(mutationsAllowed &&
2010
- discoveredOwnership.state === "none" &&
2011
- unresolvedSessionClaims.length === 0 &&
2012
- soleBindingConflict?.kind === "live_external_thread_change" &&
2013
- terminal.activity_state === "idle" &&
2014
- automatedInputComposerReady === true &&
2015
- !(isRecord(terminal.approval_state) &&
2016
- terminal.approval_state.blocked === true) &&
2017
- !conflictingSessionTurns.some((turn) => isSessionSendBlockingStatus(turn.status)) &&
2018
- terminalBlockingTurns.length === 0 &&
2019
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
2020
- externalHandoffTarget?.status === "eligible" &&
2021
- isRecord(rawActions.send) &&
2022
- Boolean(externalHandoffSnapshotToken));
2023
- const handoffDecisionTurn = mutationsAllowed &&
2024
- soleBindingConflict?.kind === "live_external_thread_change" &&
2025
- externalHandoffTarget?.status === "eligible" &&
2026
- terminal.activity_state === "idle" &&
2027
- !(isRecord(terminal.approval_state) &&
2028
- terminal.approval_state.blocked === true) &&
2029
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
2030
- blockingHandoffTurns.length === 1 &&
2031
- terminalBlockingTurns.every((turn) => turn.conversation_id === blockingHandoffTurns[0].conversation_id)
2032
- ? blockingHandoffTurns[0]
2033
- : undefined;
2034
- const handoffDecisionToken = handoffDecisionTurn &&
2035
- externalHandoffSnapshotToken &&
2036
- terminalControl
2037
- ? terminalListRuntime().activeTurnHandoffDecisionToken({
2038
- handoffToken: externalHandoffSnapshotToken,
2039
- turn: handoffDecisionTurn,
2040
- ledger: terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl)
2041
- })
2042
- : undefined;
2043
- const handoffDecision = handoffDecisionTurn &&
2044
- handoffDecisionToken &&
2045
- externalHandoffNativeThreadId
2046
- ? projectHandoffDecision({
2047
- sourceSessionId: soleBindingConflict?.session.session_id,
2048
- sourceTurnId: turnIdForConversation(handoffDecisionTurn),
2049
- liveNativeThreadId: externalHandoffNativeThreadId,
2050
- handoffDecisionToken,
2051
- actionTurnId: turnIdForConversation(handoffDecisionTurn)
2052
- })
2053
- : undefined;
2054
- const blockingHandoffTurnIds = new Set(handoffSourceBlockingTurns.map((turn) => turn.conversation_id));
2055
- // A blocking managed Turn always remains explicitly closable. Close releases
2056
- // AKK management only; it does not send input or stop the coding agent.
2057
- const terminalRecoveryBlockingTurns = terminalBlockingTurns;
2058
- return {
2059
- ...observation,
2060
- reconcileBindingAction,
2061
- externalHandoffAdoptable,
2062
- handoffDecision,
2063
- blockingHandoffTurnIds,
2064
- terminalRecoveryBlockingTurns
2065
- };
2066
- }
2067
- function observeTerminalScopedApprovalAuthority(terminal, context, observation) {
2068
- const { storeDir, mutationsAllowed } = context;
2069
- const { terminalControl, terminalHasNonterminalDeferredTransfer, conflictingBoundSessionClaims, unresolvedSessionClaims, sessionAuthorityConflict, authoritativeSession, discoveredOwnership, ownership, rawActions } = observation;
2070
- let terminalScopedCodexApprovalAction;
2071
- const terminalScopedCodexApprovalPrompt = terminalScopedCodexApprovalPromptSnapshot(terminal.approval_state);
2072
- if (mutationsAllowed &&
2073
- terminal.agent === "codex" &&
2074
- terminalControl &&
2075
- hasCanonicalTerminalEndpoint(terminalControl) &&
2076
- authoritativeSession &&
2077
- !sessionAuthorityConflict &&
2078
- !terminalHasNonterminalDeferredTransfer &&
2079
- unresolvedSessionClaims.length === 0 &&
2080
- conflictingBoundSessionClaims.length === 0 &&
2081
- terminalScopedCodexApprovalPrompt &&
2082
- isRecord(rawActions.approve)) {
2083
- try {
2084
- const ledger = terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl);
2085
- const boundary = ownership.state === "conflict" &&
2086
- discoveredOwnership.state === "current" &&
2087
- ledger
2088
- ? terminalScopedCodexApprovalBoundary({
2089
- storeDir,
2090
- terminal,
2091
- owner: discoveredOwnership.conversation,
2092
- session: authoritativeSession,
2093
- ledger,
2094
- approval: terminalScopedCodexApprovalPrompt
2095
- })
2096
- : ownership.state === "none" &&
2097
- discoveredOwnership.state === "none"
2098
- ? terminalScopedCodexApprovalBoundary({
2099
- storeDir,
2100
- terminal,
2101
- session: authoritativeSession,
2102
- ledger,
2103
- approval: terminalScopedCodexApprovalPrompt
2104
- })
2105
- : undefined;
2106
- if (!boundary) {
2107
- throw new Error("terminal-scoped Codex approval has no eligible managed authority");
2108
- }
2109
- terminalScopedCodexApprovalAction = {
2110
- ...rawActions.approve,
2111
- arguments: {
2112
- conversation_id: String(terminal.id),
2113
- expected_terminal_token: boundary.token
2114
- },
2115
- scope: "terminal_current_prompt",
2116
- authority: boundary.authority.kind,
2117
- managed_state_unchanged: true,
2118
- automatic_approval_eligible: false,
2119
- durable_dispatch_receipt: false,
2120
- uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
2121
- };
2122
- }
2123
- catch (error) {
2124
- runtimeLog("info", "terminal_scoped_codex_approval_not_advertised", {
2125
- terminal_id: String(terminal.id),
2126
- terminal_target: terminalControl.target,
2127
- reason: error instanceof Error ? error.message : String(error)
2128
- });
2129
- }
2130
- }
2131
- const rolloutBackedCodexSession = Boolean(authoritativeSession?.agent === "codex" &&
2132
- isCompleteNativeRollout(authoritativeSession.binding?.native_process.rollout));
2133
- return {
2134
- ...observation,
2135
- terminalScopedCodexApprovalAction,
2136
- rolloutBackedCodexSession
2137
- };
2138
- }
2139
- function observeTerminalListActionAuthority(terminal, context, binding) {
2140
- return observeTerminalScopedApprovalAuthority(terminal, context, observeTerminalHandoffAuthority(terminal, context, observeDeferredTerminalAuthority(terminal, context, observeDeferredSourceAuthority(terminal, context, observeVerifiedEmptyTerminalAuthority(terminal, context, binding)))));
2141
- }
2142
- function renderTerminalFirstListEntry(terminal, context, observation) {
2143
- const { sessionAuthorityRequired, includeAll, mutationsAllowed, conversationHasNonterminalDeferredTransfer } = context;
2144
- const { automatedInputComposerReady, terminalUserExplicitSendAction, publicTerminal, terminalControl, allRelated, displayedRelated, relatedSessions, authoritativeSession, ownership, rawSendAction, sessionAwareRawActions, externalHandoffDetected, externalHandoffSnapshotToken, handoffSourceBlockingTurns, verifiedEmptyRawSendAction, verifiedEmptyCodexSnapshotToken, deferredCodexSourceRolloutAuthority, deferredCodexForegroundToken, reconcileBindingAction, externalHandoffAdoptable, handoffDecision, blockingHandoffTurnIds, terminalRecoveryBlockingTurns, terminalScopedCodexApprovalAction, rolloutBackedCodexSession } = observation;
2145
- const association = decideManagedTerminalAssociation({
2146
- allRelated,
2147
- displayedRelated,
2148
- authoritativeSession,
2149
- sessionAuthorityRequired,
2150
- currentOwner: ownership.state === "current"
2151
- ? ownership.conversation
2152
- : undefined
2153
- });
2154
- const { managedSessionId, sessionIds, sessionAllRelated, sessionDisplayedRelated } = association;
2155
- // History remains useful when a pane has restarted and therefore no
2156
- // first-class Session is authoritative for the new process incarnation.
2157
- // Keep that display-only association separate from the send target: under
2158
- // protocol 3, only authoritativeSession may populate managedSessionId.
2159
- const sessionBindingMatchesLiveTerminal = authoritativeSession
2160
- ? true
2161
- : Boolean(!sessionAuthorityRequired &&
2162
- sessionAllRelated.some((turn) => managedTurnMatchesLiveTerminal(turn, terminal)));
2163
- const currentTurnValue = ownership.state === "current"
2164
- ? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
2165
- : undefined;
2166
- const currentTurnProjection = currentTurnValue
2167
- ? !mutationsAllowed
2168
- ? readOnlyManagedTurn(currentTurnValue)
2169
- : ownership.state === "current" &&
2170
- conversationHasNonterminalDeferredTransfer(ownership.conversation)
2171
- ? userReleasableManagedTurn(currentTurnValue)
2172
- : currentTurnValue
2173
- : undefined;
2174
- const currentTurn = currentTurnProjection
2175
- ? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
2176
- : undefined;
2177
- const nonOwnerRawActions = nonOwnerTerminalActions(sessionAwareRawActions, {
2178
- hasAuthoritativeSession: Boolean(authoritativeSession),
2179
- rolloutBackedCodexSession
2180
- });
2181
- const { recentConversation, historyConversations } = selectManagedTerminalHistory({
2182
- displayedRelated: sessionDisplayedRelated,
2183
- currentConversationId: stringValue(currentTurn?.conversation_id),
2184
- hasCurrentTurn: Boolean(currentTurn),
2185
- includeAll
2186
- });
2187
- const recentTurnValue = recentConversation
2188
- ? historicalManagedTurnForTerminal(recentConversation)
2189
- : undefined;
2190
- const recentTurnProjection = recentTurnValue
2191
- ? !mutationsAllowed
2192
- ? readOnlyManagedTurn(recentTurnValue)
2193
- : recentConversation &&
2194
- conversationHasNonterminalDeferredTransfer(recentConversation)
2195
- ? userReleasableManagedTurn(recentTurnValue)
2196
- : recentTurnValue
2197
- : undefined;
2198
- const recentTurn = recentTurnProjection
2199
- ? withoutGenericHandoffSourceClose(recentTurnProjection, blockingHandoffTurnIds)
2200
- : undefined;
2201
- const history = historyConversations.map((conversation) => {
2202
- const turn = historicalManagedTurnForTerminal(conversation);
2203
- return withoutGenericHandoffSourceClose(!mutationsAllowed
2204
- ? readOnlyManagedTurn(turn)
2205
- : conversationHasNonterminalDeferredTransfer(conversation)
2206
- ? userReleasableManagedTurn(turn)
2207
- : turn, blockingHandoffTurnIds);
2208
- });
2209
- const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
2210
- .map((turn) => stringValue(turn?.conversation_id))
2211
- .filter((id) => id !== undefined));
2212
- const managedSessionShortReference = managedSessionId
2213
- ? sessionShortRef(managedSessionId)
2214
- : null;
2215
- const management = projectTerminalManagement({
2216
- managedSessionId,
2217
- managedSessionShortRef: managedSessionShortReference,
2218
- currentTurn,
2219
- recentTurn,
2220
- sessionAllRelatedCount: sessionAllRelated.length,
2221
- hiddenTurnCount: sessionAllRelated.filter((conversation) => !visibleTurnIds.has(conversation.conversation_id)).length,
2222
- sessionCount: new Set([
2223
- ...sessionIds,
2224
- ...relatedSessions.map((session) => session.session_id)
2225
- ]).size,
2226
- authoritativeSession,
2227
- history: includeAll ? history : undefined
2228
- });
2229
- const sendAuthority = decideTerminalSendAuthority({
2230
- ownership: ownership.state,
2231
- verifiedEmptyToken: verifiedEmptyCodexSnapshotToken,
2232
- externalToken: externalHandoffAdoptable
2233
- ? externalHandoffSnapshotToken
2234
- : undefined,
2235
- deferredToken: ownership.state !== "conflict" ||
2236
- deferredCodexSourceRolloutAuthority ===
2237
- "explicitly_abandoned_predecessor"
2238
- ? deferredCodexForegroundToken
2239
- : undefined,
2240
- managedSendSessionId: managedSessionId &&
2241
- !rolloutBackedCodexSession &&
2242
- sessionBindingMatchesLiveTerminal &&
2243
- isRecord(sessionAwareRawActions.send)
2244
- ? managedSessionId
2245
- : undefined
2246
- });
2247
- const tokenSendAction = sendAuthority.mode === "external_handoff"
2248
- ? rawSendAction
2249
- : sendAuthority.mode === "verified_empty" ||
2250
- sendAuthority.mode === "deferred"
2251
- ? verifiedEmptyRawSendAction
2252
- : undefined;
2253
- const authoritativeSendAction = sendAuthority.mode === "managed" && isRecord(sessionAwareRawActions.send)
2254
- ? sendActionForManagedSession(sessionAwareRawActions.send, sendAuthority.sessionId)
2255
- : tokenSendAction &&
2256
- "token" in sendAuthority &&
2257
- sendAuthority.token
2258
- ? {
2259
- ...tokenSendAction,
2260
- arguments: {
2261
- ...(isRecord(tokenSendAction.arguments)
2262
- ? tokenSendAction.arguments
2263
- : {}),
2264
- expected_terminal_token: sendAuthority.token
2265
- }
2266
- }
2267
- : undefined;
2268
- const managedFastPathToken = "token" in sendAuthority
2269
- ? sendAuthority.token
2270
- : undefined;
2271
- const prioritizedTerminalUserExplicitSendAction = terminalUserExplicitSendAction && managedFastPathToken
2272
- ? {
2273
- ...terminalUserExplicitSendAction,
2274
- arguments: {
2275
- ...(isRecord(terminalUserExplicitSendAction.arguments)
2276
- ? terminalUserExplicitSendAction.arguments
2277
- : {}),
2278
- expected_managed_terminal_token: managedFastPathToken
2279
- }
2280
- }
2281
- : terminalUserExplicitSendAction;
2282
- const availableActions = selectTerminalAvailableActions({
2283
- ownership: ownership.state,
2284
- currentActions: ownership.state === "current"
2285
- ? currentTerminalActions(currentTurn)
2286
- : {},
2287
- sessionAwareRawActions: sessionAwareRawActions,
2288
- nonOwnerRawActions,
2289
- authoritativeSendAction,
2290
- terminalUserExplicitSendAction: prioritizedTerminalUserExplicitSendAction,
2291
- reconcileBindingAction,
2292
- terminalScopedApprovalAction: terminalScopedCodexApprovalAction,
2293
- isAction: isRecord
2294
- });
2295
- // An explicit undefined rollout prevents a lingering resolver rollout from
2296
- // being presented as the authoritative status-card thread.
2297
- const authoritativeIdentity = authoritativeTerminalIdentity(authoritativeSession);
2298
- const publicManagementConflict = ownership.state === "conflict"
2299
- ? projectPublicManagementConflict({
2300
- conflict: ownership.conflict,
2301
- verifiedEmptyToken: verifiedEmptyCodexSnapshotToken,
2302
- deferredToken: deferredCodexForegroundToken,
2303
- explicitlyAbandonedPredecessor: deferredCodexSourceRolloutAuthority ===
2304
- "explicitly_abandoned_predecessor"
2305
- })
2306
- : undefined;
2307
- const handoffPresentation = projectHandoffPresentation({
2308
- externalHandoffDetected,
2309
- externalHandoffAdoptable,
2310
- recoveryBlockingTurnCount: terminalRecoveryBlockingTurns.length,
2311
- hasHandoffDecision: Boolean(handoffDecision),
2312
- sourceBlockingTurnCount: handoffSourceBlockingTurns.length,
2313
- automatedInputComposerReady: automatedInputComposerReady === true,
2314
- verifiedEmptyToken: verifiedEmptyCodexSnapshotToken
2315
- });
2316
- return {
2317
- ...publicTerminal,
2318
- ...authoritativeIdentity,
2319
- management_state: ownership.state === "conflict"
2320
- ? "conflict"
2321
- : ownership.state === "current" || Boolean(authoritativeSession)
2322
- ? "managed"
2323
- : "unmanaged",
2324
- ...(ownership.state === "conflict"
2325
- ? { management_conflict: publicManagementConflict }
2326
- : {}),
2327
- ...handoffPresentation,
2328
- ...(handoffDecision ? { handoff_decision: handoffDecision } : {}),
2329
- ...(terminalRecoveryBlockingTurns.length > 0
2330
- ? {
2331
- blocking_turns: terminalRecoveryBlockingTurns.map((turn) => projectBlockingTurn({
2332
- sessionId: sessionIdForConversation(turn),
2333
- turnId: turnIdForConversation(turn),
2334
- status: turn.status,
2335
- recoveryTurnId: turnIdForConversation(turn)
2336
- }))
2337
- }
2338
- : {}),
2339
- managed: management,
2340
- available_actions: availableActions
2341
- };
2342
- }
2343
- function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
2344
- const nonterminalDeferredTransfers = listDeferredForegroundTransfers(storeDir).filter((transfer) => !isFinalDeferredForegroundTransferStatus(transfer.status));
2345
- const nonterminalDeferredTransferIds = new Set(nonterminalDeferredTransfers.map((transfer) => transfer.transfer_id));
2346
- const nonterminalDeferredSourceTurnIds = new Set(nonterminalDeferredTransfers.flatMap((transfer) => transfer.version === 2 &&
2347
- transfer.source_kind === "candidate_rollout_quiescent"
2348
- ? (transfer.source_turn_history ?? []).map((turn) => turn.turn_id)
2349
- : []));
2350
- const conversationHasNonterminalDeferredTransfer = (conversation) => {
2351
- if (nonterminalDeferredSourceTurnIds.has(turnIdForConversation(conversation))) {
2352
- return true;
2353
- }
2354
- const takeover = isRecord(conversation.native_session_takeover)
2355
- ? conversation.native_session_takeover
2356
- : undefined;
2357
- const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
2358
- return Boolean(transferId && nonterminalDeferredTransferIds.has(transferId));
2359
- };
2360
- const discoveredTerminalControls = terminals.flatMap((terminal) => {
2361
- const control = isRecord(terminal.terminal_control)
2362
- ? terminal.terminal_control
2363
- : undefined;
2364
- return control ? [control] : [];
2365
- });
2366
- const projectionContext = {
2367
- storeDir,
2368
- terminals,
2369
- managedSessions,
2370
- sessionAuthorityRequired,
2371
- allConversations,
2372
- displayedConversations,
2373
- includeAll,
2374
- mutationsAllowed,
2375
- nonterminalDeferredTransfers,
2376
- conversationHasNonterminalDeferredTransfer
2377
- };
2378
- const projectedTerminals = terminals.map((terminal) => {
2379
- const binding = observeTerminalListBindingAuthority(terminal, projectionContext);
2380
- return renderTerminalFirstListEntry(binding.authorityTerminal, projectionContext, observeTerminalListActionAuthority(binding.authorityTerminal, projectionContext, binding));
2381
- });
2382
- const unavailableManagedTurns = displayedConversations
2383
- .filter((conversation) => {
2384
- const managedControl = terminalControlForManagedConversation(conversation);
2385
- if (discoveredTerminalControls.some((control) => terminalControlsShareIncarnation(managedControl, control))) {
2386
- return false;
2387
- }
2388
- return (includeAll ||
2389
- managedOnly ||
2390
- statusFilter !== undefined ||
2391
- managedTurnNeedsAttention(conversation));
2392
- })
2393
- .sort(compareManagedConversationRecency)
2394
- .map((conversation) => {
2395
- const managedTurn = managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
2396
- terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
2397
- approvalState: managedListApprovalState(conversation),
2398
- conversation
2399
- });
2400
- return {
2401
- ...managedTurn,
2402
- available_actions: !mutationsAllowed
2403
- ? readOnlyListActions(isRecord(managedTurn.available_actions)
2404
- ? managedTurn.available_actions
2405
- : {})
2406
- : conversationHasNonterminalDeferredTransfer(conversation)
2407
- ? userReleaseListActions(isRecord(managedTurn.available_actions)
2408
- ? managedTurn.available_actions
2409
- : {}, turnIdForConversation(conversation))
2410
- : safeUnavailableManagedTurnActions(isRecord(managedTurn.available_actions)
2411
- ? managedTurn.available_actions
2412
- : {}),
2413
- terminal_availability: {
2414
- available: false,
2415
- reason: managedOnly
2416
- ? "terminal discovery was disabled by --managed-only"
2417
- : "the referenced terminal pane is not currently available"
2418
- }
2419
- };
2420
- });
2421
- return {
2422
- terminals: projectedTerminals,
2423
- unavailableManagedTurns
2424
- };
2425
- }
2426
1142
  function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
2427
- const binding = session.binding;
2428
- const liveControl = isRecord(terminal.terminal_control)
2429
- ? terminal.terminal_control
2430
- : undefined;
2431
- if (session.status !== "bound" ||
2432
- !binding ||
2433
- session.agent !== terminal.agent ||
2434
- binding.native_process.pid !== Number(terminal.pid)) {
2435
- return false;
2436
- }
2437
- const terminalAliasMatches = terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, liveControl);
2438
- if (!terminalAliasMatches) {
2439
- return false;
2440
- }
2441
- const workspaceMatches = terminalListRuntime().matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd);
2442
- if (!workspaceMatches) {
2443
- return false;
2444
- }
2445
- const observation = terminalObservationFromListEntry(terminal, session.agent);
2446
- const evidence = {
2447
- terminalAliasMatches,
2448
- workspaceMatches
2449
- };
2450
- let decision = decideTerminalBindingMatch(session, observation, evidence);
2451
- if (decision.state === "not_exact" &&
2452
- decision.reason === "native_identity_mismatch" &&
2453
- session.agent === "codex" &&
2454
- observation.nativeIdentity.status === "resolved") {
2455
- decision = decideTerminalBindingMatch(session, observation, {
2456
- ...evidence,
2457
- codexLingeringBeforeMatches: terminalListRuntime().codexLingeringBeforeIdentityMatchesSession({
2458
- storeDir,
2459
- session,
2460
- identity: observation.nativeIdentity.identity
2461
- })
2462
- });
2463
- }
2464
- return decision.state === "exact";
2465
- }
2466
- function managedSessionClaimsLiveTerminalEntry(session, terminal) {
2467
- const binding = session.binding;
2468
- const liveControl = isRecord(terminal.terminal_control)
2469
- ? terminal.terminal_control
2470
- : undefined;
2471
- return Boolean(binding &&
2472
- session.agent === terminal.agent &&
2473
- binding.native_process.pid === Number(terminal.pid) &&
2474
- terminalControlsShareIncarnation(binding.terminal_control, liveControl));
2475
- }
2476
- function listedTerminalProcessIncarnation(terminal) {
2477
- const processUuid = stringValue(terminal.native_agent_process_uuid);
2478
- const processBirth = stringValue(terminal.native_agent_process_birth);
2479
- if (terminal.agent !== "codex" ||
2480
- (processUuid && processBirth)) {
2481
- return { processUuid, processBirth };
2482
- }
2483
- const pid = Number(terminal.pid);
2484
- if (!Number.isSafeInteger(pid) || pid <= 1) {
2485
- return { processUuid, processBirth };
2486
- }
2487
- try {
2488
- const incarnation = terminalListRuntime().codexProcessIncarnationForPid(pid);
2489
- return {
2490
- processUuid: processUuid ?? incarnation.processUuid,
2491
- processBirth: processBirth ?? incarnation.processBirth
2492
- };
2493
- }
2494
- catch {
2495
- return { processUuid, processBirth };
2496
- }
2497
- }
2498
- function managedBindingConflictKindForLiveTerminalEntry({ storeDir, session, terminal }) {
2499
- const binding = session.binding;
2500
- if (session.status !== "bound" ||
2501
- !binding ||
2502
- !managedSessionClaimsLiveTerminalEntry(session, terminal) ||
2503
- managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir)) {
2504
- return undefined;
2505
- }
2506
- const livePid = Number(terminal.pid);
2507
- const incarnation = listedTerminalProcessIncarnation(terminal);
2508
- const relationship = processIncarnationRelationship({
2509
- binding,
2510
- livePid,
2511
- liveProcessUuid: incarnation.processUuid,
2512
- liveProcessBirth: incarnation.processBirth
2513
- });
2514
- if (relationship === "different") {
2515
- return "stale_process_incarnation";
2516
- }
2517
- if (!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, isRecord(terminal.terminal_control)
2518
- ? terminal.terminal_control
2519
- : undefined) ||
2520
- !terminalListRuntime().matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
2521
- return "unverifiable";
2522
- }
2523
- return decideManagedBindingConflict({
2524
- session,
2525
- claimsTerminal: true,
2526
- exactBinding: false,
2527
- ownerConclusivelyInactive: false,
2528
- processRelationship: relationship,
2529
- liveNativeThreadId: stringValue(terminal.native_agent_session_id),
2530
- statusCardNativeThreadId: stringValue(terminal.native_agent_status_card_session_id),
2531
- managedTurnCount: provisionalManagedBindingTurnCount(storeDir, session)
2532
- });
1143
+ return terminalListOwnershipService().managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir);
2533
1144
  }
2534
1145
  function provisionalManagedBindingTurnCount(storeDir, session) {
2535
- const binding = session.binding;
2536
- return binding && session.lineage.created_by === "attach" &&
2537
- !session.last_transition_id &&
2538
- !binding.native_thread_id &&
2539
- !binding.native_process.rollout
2540
- ? terminalListRuntime().managedTurnsForSession(storeDir, session.session_id).length
2541
- : undefined;
1146
+ return terminalListOwnershipService().provisionalManagedBindingTurnCount(storeDir, session);
2542
1147
  }
2543
1148
  function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
2544
- const root = nativeThreadTransitionsDir(storeDir);
2545
- if (!fs.existsSync(root)) {
2546
- return false;
2547
- }
2548
- let entries;
2549
- try {
2550
- entries = fs.readdirSync(root, { withFileTypes: true });
2551
- }
2552
- catch {
2553
- return true;
2554
- }
2555
- for (const entry of entries) {
2556
- if (!entry.isDirectory()) {
2557
- continue;
2558
- }
2559
- let transition;
2560
- try {
2561
- transition = loadNativeThreadTransition(storeDir, entry.name);
2562
- }
2563
- catch {
2564
- return true;
2565
- }
2566
- if (transition.source_session_id !== session.session_id &&
2567
- transition.target_session_id !== session.session_id) {
2568
- continue;
2569
- }
2570
- if (!["committed", "aborted"].includes(transition.status)) {
2571
- return true;
2572
- }
2573
- }
2574
- return false;
1149
+ return terminalListOwnershipService().managedSessionHasUnresolvedNativeTransition(storeDir, session);
2575
1150
  }
2576
1151
  function managedSessionHasAnyNativeTransition(storeDir, session) {
2577
- const root = nativeThreadTransitionsDir(storeDir);
2578
- if (!fs.existsSync(root)) {
2579
- return false;
2580
- }
2581
- let entries;
2582
- try {
2583
- entries = fs.readdirSync(root, { withFileTypes: true });
2584
- }
2585
- catch {
2586
- return true;
2587
- }
2588
- for (const entry of entries) {
2589
- if (!entry.isDirectory()) {
2590
- continue;
2591
- }
2592
- let transition;
2593
- try {
2594
- transition = loadNativeThreadTransition(storeDir, entry.name);
2595
- }
2596
- catch {
2597
- return true;
2598
- }
2599
- if (transition.source_session_id === session.session_id ||
2600
- transition.target_session_id === session.session_id) {
2601
- return true;
2602
- }
2603
- }
2604
- return false;
1152
+ return terminalListOwnershipService().managedSessionHasAnyNativeTransition(storeDir, session);
2605
1153
  }
2606
1154
  function terminalControlForManagedConversation(conversation) {
2607
- return terminalListRuntime().terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
2608
- ? conversation.native_session_takeover
2609
- : undefined);
1155
+ return terminalListOwnershipService().terminalControlForManagedConversation(conversation);
2610
1156
  }
2611
1157
  function terminalIncarnationBlockingTurns(storeDir, terminalControl, conversations = listConversations(storeDir)) {
2612
- return conversations
2613
- .filter(terminalListRuntime().isDiscoverableTmuxConversation)
2614
- .filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
2615
- isSessionSendBlockingStatus(turn.status))
2616
- .sort(compareManagedConversationRecency);
1158
+ return terminalListOwnershipService().terminalIncarnationBlockingTurns(storeDir, terminalControl, conversations);
2617
1159
  }
2618
1160
  function managedTurnNeedsAttention(conversation) {
2619
- return terminalManagedTurnNeedsAttention({
2620
- status: conversation.status,
2621
- get callbackDeliveryStatus() {
2622
- const delivery = isRecord(conversation.callback_delivery)
2623
- ? conversation.callback_delivery
2624
- : undefined;
2625
- return String(delivery?.status ?? "");
2626
- }
2627
- });
1161
+ return terminalListOwnershipService().managedTurnNeedsAttention(conversation);
2628
1162
  }
2629
1163
  function assertTerminalIncarnationCanStartTurn(storeDir, terminalControl) {
2630
- const blocker = terminalIncarnationBlockingTurns(storeDir, terminalControl)[0];
2631
- if (!blocker) {
2632
- return;
2633
- }
2634
- throw new Error(`terminal ${terminalControl.target} still has unresolved Turn ` +
2635
- `${turnIdForConversation(blocker)} (${blocker.status})`);
1164
+ return terminalListOwnershipService().assertTerminalIncarnationCanStartTurn(storeDir, terminalControl);
2636
1165
  }
2637
1166
  function terminalDispatchOwnership(terminalControl) {
2638
- let ledger;
2639
- try {
2640
- ledger = terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl);
2641
- }
2642
- catch (error) {
2643
- const decision = dispatch.decideTerminalDispatchOwnership("unreadable");
2644
- return {
2645
- state: "conflict",
2646
- conflict: {
2647
- reason: decision.code === "ledger_unreadable"
2648
- ? error instanceof Error ? error.message : String(error)
2649
- : "terminal dispatch ledger is unreadable",
2650
- recovery: "inspect the shared terminal pane before performing a side effect"
2651
- }
2652
- };
2653
- }
2654
- const ledgerAuthority = !ledger
2655
- ? "absent"
2656
- : ledger.status === "resolved"
2657
- ? "resolved"
2658
- : terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, terminalControl, {
2659
- requireProcessAnchor: false
2660
- }) && !terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, terminalControl)
2661
- ? "stale_process_incarnation"
2662
- : dispatch.isActiveTerminalDispatchStatus(String(ledger.status))
2663
- ? "active"
2664
- : "inactive_status";
2665
- let decision = dispatch.decideTerminalDispatchOwnership(ledgerAuthority);
2666
- if (decision.state === "none" || !ledger)
2667
- return { state: "none" };
2668
- const owner = terminalListRuntime().loadTerminalDispatchLedgerOwner(ledger);
2669
- const ledgerMessageId = stringValue(ledger.message_id);
2670
- const ownerAuthority = !owner
2671
- ? "unavailable"
2672
- : isTerminalDispatchOwnerReleasedStatus(owner.status)
2673
- ? "released"
2674
- : !terminalControlsShareIncarnation(terminalControlForManagedConversation(owner), terminalControl)
2675
- ? "terminal_mismatch"
2676
- : ledgerMessageId && stringValue(isRecord(owner.native_session_takeover)
2677
- ? owner.native_session_takeover.terminal_bridge_message_id
2678
- : undefined) !== ledgerMessageId
2679
- ? "generation_mismatch"
2680
- : "current";
2681
- decision = dispatch.decideTerminalDispatchOwnership(ledgerAuthority, ownerAuthority);
2682
- if (decision.state === "none")
2683
- return { state: "none" };
2684
- if (decision.state === "current" && owner) {
2685
- return { state: "current", conversation: owner };
2686
- }
2687
- const reason = decision.state === "conflict" &&
2688
- decision.code === "owner_terminal_mismatch"
2689
- ? "dispatch owner does not reference this terminal pane incarnation"
2690
- : decision.state === "conflict" &&
2691
- decision.code === "owner_generation_mismatch"
2692
- ? "dispatch generation does not match the owner state"
2693
- : "dispatch owner state is unavailable";
2694
- return {
2695
- state: "conflict",
2696
- conflict: projectTerminalDispatchConflict({
2697
- reason,
2698
- dispatchStatus: stringValue(ledger.status),
2699
- ownerConversationId: stringValue(ledger.conversation_id),
2700
- messageId: stringValue(ledger.message_id)
2701
- })
2702
- };
2703
- }
2704
- function localTerminalDispatchOwnership(ledgerOwner, localConversations, terminal) {
2705
- const localOwner = localConversations.find((conversation) => conversation.conversation_id === ledgerOwner.conversation_id &&
2706
- sameCanonicalStatePath(conversation.state_path, ledgerOwner.state_path));
2707
- return decideLocalTerminalDispatchOwnership({
2708
- ledgerOwnerId: ledgerOwner.conversation_id,
2709
- localOwner,
2710
- localOwnerMatchesLiveTerminal: localOwner
2711
- ? managedTurnMatchesLiveTerminal(localOwner, terminal)
2712
- : false
2713
- });
1167
+ return terminalListOwnershipService().terminalDispatchOwnership(terminalControl);
2714
1168
  }
2715
1169
  function terminalScopedCodexApprovalBoundary({ storeDir, terminal, session, ledger, approval, owner }) {
2716
- const terminalRecord = isRecord(terminal) ? terminal : {};
2717
- const terminalControl = isRecord(terminalRecord.terminal_control)
2718
- ? terminalRecord.terminal_control
2719
- : undefined;
2720
- const control = terminalControl;
2721
- const terminalId = String(terminalRecord.id);
2722
- const relatedBoundSessionIds = () => listManagedSessions(storeDir)
2723
- .filter((candidate) => candidate.status === "bound" &&
2724
- candidate.agent === "codex" &&
2725
- candidate.binding?.native_process.pid === Number(terminalRecord.pid) &&
2726
- terminalControlsShareIncarnation(candidate.binding?.terminal_control, terminalControl))
2727
- .map((candidate) => candidate.session_id);
2728
- const blockingTurnIds = () => terminalIncarnationBlockingTurns(storeDir, control)
2729
- .map((turn) => turnIdForConversation(turn));
2730
- const hasDeferredRecovery = () => listDeferredForegroundTransfers(storeDir).some((transfer) => !isFinalDeferredForegroundTransferStatus(transfer.status) &&
2731
- (transfer.source_session_id === session.session_id ||
2732
- transfer.target_session_id === session.session_id ||
2733
- (transfer.terminal_id === terminalId &&
2734
- terminalControlEvidenceMatches(transfer.terminal_endpoint, control))));
2735
- const commonChecks = {
2736
- relatedBoundSessionIds, blockingTurnIds,
2737
- hasNativeTransition: () => managedSessionHasAnyNativeTransition(storeDir, session),
2738
- hasDeferredRecovery,
2739
- ledgerMatchesTerminal: () => terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, control)
2740
- };
2741
- if (owner && ledger) {
2742
- return decideTerminalScopedCodexApprovalAuthority({
2743
- kind: "current_dispatch_owner", storeDir, terminal, owner, session,
2744
- ledger, approval,
2745
- checks: {
2746
- ...commonChecks,
2747
- assertDispatchOwner: () => terminalListRuntime().assertManagedTerminalDispatchOwner({
2748
- storeDir, conversation: owner, terminalControl: control, action: "approve"
2749
- }),
2750
- ownerMatchesNativeIdentity: (identity) => nativeAgentIdentityMatchesTurn(owner, identity
2751
- ? { ...identity, evidence: "terminal_scoped_approval" }
2752
- : undefined)
2753
- }
2754
- });
2755
- }
2756
- return decideTerminalScopedCodexApprovalAuthority({
2757
- kind: "managed_session_no_dispatch_owner", storeDir, terminal, session,
2758
- ledger, approval,
2759
- checks: {
2760
- ...commonChecks,
2761
- dispatchOwnershipIsNone: () => terminalDispatchOwnership(control).state === "none",
2762
- hasOrphanedDispatch: () => Boolean(terminalListRuntime().orphanedTerminalDispatchForRecovery(control))
2763
- }
1170
+ return terminalListOwnershipService().terminalScopedCodexApprovalBoundary({
1171
+ storeDir,
1172
+ terminal,
1173
+ session,
1174
+ ledger,
1175
+ approval,
1176
+ owner
2764
1177
  });
2765
1178
  }
2766
1179
  async function resolveTerminalScopedCodexApproval({ options, terminal, approvalSnapshot }) {
@@ -2942,90 +1355,6 @@ async function resolveTerminalScopedCodexApproval({ options, terminal, approvalS
2942
1355
  };
2943
1356
  }
2944
1357
  }
2945
- function historicalManagedTurnForTerminal(conversation) {
2946
- return renderHistoricalManagedTurn(managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
2947
- terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
2948
- approvalState: managedListApprovalState(conversation),
2949
- conversation
2950
- }));
2951
- }
2952
- function managedTurnMatchesLiveTerminal(conversation, terminal) {
2953
- const takeover = isRecord(conversation.native_session_takeover)
2954
- ? conversation.native_session_takeover
2955
- : undefined;
2956
- const liveControl = isRecord(terminal.terminal_control)
2957
- ? terminal.terminal_control
2958
- : undefined;
2959
- const storedControl = terminalListRuntime().terminalControlFromTakeover(takeover);
2960
- const livePid = Number(terminal.pid);
2961
- const storedPid = Number(takeover?.terminal_agent_pid);
2962
- if (executorForConversation(conversation).kind !== terminal.agent ||
2963
- !Number.isSafeInteger(livePid) ||
2964
- livePid <= 1 ||
2965
- storedPid !== livePid ||
2966
- !terminalControlAliasMatches(stringValue(takeover?.native_session_id), storedControl, stringValue(terminal.id), liveControl) ||
2967
- !terminalControlsShareIncarnation(storedControl, liveControl)) {
2968
- return false;
2969
- }
2970
- const liveSessionId = stringValue(terminal.native_agent_session_id);
2971
- const liveProcessUuid = stringValue(terminal.native_agent_process_uuid);
2972
- const liveProcessBirth = stringValue(terminal.native_agent_process_birth);
2973
- const liveRollout = isRecord(terminal.native_agent_rollout)
2974
- ? terminal.native_agent_rollout
2975
- : undefined;
2976
- const liveNativeIdentity = liveSessionId
2977
- ? {
2978
- sessionId: liveSessionId,
2979
- ...(liveProcessUuid ? { processUuid: liveProcessUuid } : {}),
2980
- ...(liveProcessBirth ? { processBirth: liveProcessBirth } : {}),
2981
- ...(liveRollout
2982
- ? {
2983
- rollout: {
2984
- fd: String(liveRollout.fd ?? ""),
2985
- device: String(liveRollout.device ?? ""),
2986
- inode: String(liveRollout.inode ?? ""),
2987
- path: String(liveRollout.path ?? "")
2988
- }
2989
- }
2990
- : {}),
2991
- evidence: "live_terminal"
2992
- }
2993
- : undefined;
2994
- if (!nativeAgentIdentityMatchesTurn(conversation, liveNativeIdentity)) {
2995
- return false;
2996
- }
2997
- const liveWorkspace = terminal.workspace ?? terminal.cwd;
2998
- if (!terminalListRuntime().matchesConfiguredWorkspace(conversation.workspace, liveWorkspace)) {
2999
- return false;
3000
- }
3001
- const livePanePath = liveControl?.currentPath;
3002
- if (livePanePath !== undefined &&
3003
- !terminalListRuntime().matchesConfiguredWorkspace(conversation.workspace, livePanePath)) {
3004
- return false;
3005
- }
3006
- return true;
3007
- }
3008
- function currentManagedTurnForTerminal(conversation, terminal, rawTerminalActions) {
3009
- const managedTurn = managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
3010
- terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
3011
- approvalState: managedListApprovalState(conversation),
3012
- conversation
3013
- });
3014
- const rawApproval = isRecord(rawTerminalActions.approve)
3015
- ? rawTerminalActions.approve
3016
- : undefined;
3017
- if (!rawApproval || executorForConversation(conversation).kind !== "codex") {
3018
- return managedTurn;
3019
- }
3020
- return renderCurrentManagedTurn(managedTurn, {
3021
- isCodex: true,
3022
- ownerId: conversation.conversation_id,
3023
- rawApproval,
3024
- terminalApprovalState: () => isRecord(terminal.approval_state)
3025
- ? terminal.approval_state
3026
- : undefined
3027
- });
3028
- }
3029
1358
  async function listStateForTerminal(agent, terminalControl, options, bridge = terminalListRuntime().createTerminalAgentBridge(options), runtime) {
3030
1359
  if (options.noApprovalScan) {
3031
1360
  return {
@@ -3077,39 +1406,6 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = te
3077
1406
  };
3078
1407
  }
3079
1408
  }
3080
- function managedListApprovalState(conversation) {
3081
- if (!terminalListRuntime().terminalBridgeEnabled(conversation) ||
3082
- !["waiting_for_agent", "waiting_for_openclaw"].includes(String(conversation.status))) {
3083
- return undefined;
3084
- }
3085
- const nativeTakeover = isRecord(conversation.native_session_takeover)
3086
- ? conversation.native_session_takeover
3087
- : undefined;
3088
- if (!terminalListRuntime().terminalControlFromTakeover(nativeTakeover) ||
3089
- !stringValue(nativeTakeover?.terminal_bridge_message_id)) {
3090
- return undefined;
3091
- }
3092
- const approval = isRecord(nativeTakeover?.terminal_bridge_approval)
3093
- ? nativeTakeover.terminal_bridge_approval
3094
- : undefined;
3095
- const approvalState = isRecord(approval?.approval_state)
3096
- ? approval.approval_state
3097
- : undefined;
3098
- const fingerprint = stringValue(approval?.fingerprint);
3099
- const notifiedAt = stringValue(approval?.notified_at);
3100
- const notifiedAtMs = validTimestampMs(notifiedAt);
3101
- if (!approvalState ||
3102
- !fingerprint ||
3103
- notifiedAtMs === undefined ||
3104
- cliNowMs() - notifiedAtMs > terminalListRuntime().approvalTtlMs) {
3105
- return undefined;
3106
- }
3107
- return {
3108
- ...approvalState,
3109
- fingerprint,
3110
- notified_at: notifiedAt
3111
- };
3112
- }
3113
1409
  async function resolveConversationSelectorOption(commandName, options) {
3114
1410
  const sendOperation = commandName === "send";
3115
1411
  // Submission retry is already bound to one authoritative managed Turn.
@@ -3280,12 +1576,11 @@ async function sessionSelectorCandidates(commandName, options) {
3280
1576
  .filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace));
3281
1577
  const discoverableWorkspaceConversations = workspaceConversations
3282
1578
  .filter(terminalListRuntime().isDiscoverableTmuxConversation);
3283
- const managed = discoverableWorkspaceConversations.map((conversation) => managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
1579
+ const managed = discoverableWorkspaceConversations.map((conversation) => terminalListProjectionService().managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
3284
1580
  terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
3285
- approvalState: managedListApprovalState(conversation),
3286
1581
  conversation
3287
1582
  }));
3288
- const terminalProjection = terminalFirstListProjection({
1583
+ const terminalProjection = terminalListProjectionService().projectTerminalFirstList({
3289
1584
  storeDir,
3290
1585
  terminals: terminalScan.terminalControlled.filter((entry) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)),
3291
1586
  managedSessions: selectorStore.readable