@scotthuang/agent-knock-knock 0.13.2 → 0.13.4

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 (278) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +27 -6
  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/claude-terminal-agent-adapter.js +4 -0
  14. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  15. package/dist/src/cli-core.js +9 -1
  16. package/dist/src/cli-core.js.map +1 -1
  17. package/dist/src/codex-process-incarnation.d.ts +8 -0
  18. package/dist/src/codex-process-incarnation.js +36 -0
  19. package/dist/src/codex-process-incarnation.js.map +1 -0
  20. package/dist/src/codex-terminal-agent-adapter.js +4 -0
  21. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  22. package/dist/src/deferred-foreground-conflict-policy.d.ts +22 -0
  23. package/dist/src/deferred-foreground-conflict-policy.js +52 -0
  24. package/dist/src/deferred-foreground-conflict-policy.js.map +1 -0
  25. package/dist/src/durable-notification-kernel.d.ts +94 -0
  26. package/dist/src/durable-notification-kernel.js +68 -0
  27. package/dist/src/durable-notification-kernel.js.map +1 -0
  28. package/dist/src/herdr-socket-transport.d.ts +43 -0
  29. package/dist/src/herdr-socket-transport.js +170 -0
  30. package/dist/src/herdr-socket-transport.js.map +1 -0
  31. package/dist/src/herdr-terminal-control-provider.d.ts +2 -45
  32. package/dist/src/herdr-terminal-control-provider.js +8 -173
  33. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  34. package/dist/src/host-adapter-capabilities.d.ts +35 -0
  35. package/dist/src/host-adapter-capabilities.js +146 -0
  36. package/dist/src/host-adapter-capabilities.js.map +1 -0
  37. package/dist/src/host-adapter.d.ts +9 -3
  38. package/dist/src/host-adapter.js +33 -9
  39. package/dist/src/host-adapter.js.map +1 -1
  40. package/dist/src/host-bridge-tools.d.ts +3 -4
  41. package/dist/src/host-bridge-tools.js +49 -92
  42. package/dist/src/host-bridge-tools.js.map +1 -1
  43. package/dist/src/host-bridge.js +5 -5
  44. package/dist/src/host-bridge.js.map +1 -1
  45. package/dist/src/host-monitor-reconciliation.d.ts +6 -0
  46. package/dist/src/host-monitor-reconciliation.js +96 -0
  47. package/dist/src/host-monitor-reconciliation.js.map +1 -0
  48. package/dist/src/managed-session.js +2 -18
  49. package/dist/src/managed-session.js.map +1 -1
  50. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
  51. package/dist/src/native-thread-lifecycle-cli-adapter.js +635 -3
  52. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  53. package/dist/src/openclaw-list-projection.d.ts +2 -0
  54. package/dist/src/openclaw-list-projection.js +3 -0
  55. package/dist/src/openclaw-list-projection.js.map +1 -0
  56. package/dist/src/openclaw-model-facing-field-policy.d.ts +2 -0
  57. package/dist/src/openclaw-model-facing-field-policy.js +3 -0
  58. package/dist/src/openclaw-model-facing-field-policy.js.map +1 -0
  59. package/dist/src/openclaw-plugin-command-adapter.d.ts +6 -29
  60. package/dist/src/openclaw-plugin-command-adapter.js +7 -2370
  61. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  62. package/dist/src/openclaw-plugin-helpers.d.ts +2 -121
  63. package/dist/src/openclaw-plugin-helpers.js +2 -1164
  64. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  65. package/dist/src/openclaw-plugin-schemas.d.ts +2 -510
  66. package/dist/src/openclaw-plugin-schemas.js +2 -519
  67. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  68. package/dist/src/openclaw-plugin-supervisor.d.ts +4 -5
  69. package/dist/src/openclaw-plugin-supervisor.js +5 -94
  70. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  71. package/dist/src/openclaw-plugin-value-helpers.d.ts +2 -0
  72. package/dist/src/openclaw-plugin-value-helpers.js +3 -0
  73. package/dist/src/openclaw-plugin-value-helpers.js.map +1 -0
  74. package/dist/src/openclaw-private-authority-offers.d.ts +4 -39
  75. package/dist/src/openclaw-private-authority-offers.js +3 -258
  76. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  77. package/dist/src/semantic-private-authority-offers.d.ts +41 -0
  78. package/dist/src/semantic-private-authority-offers.js +261 -0
  79. package/dist/src/semantic-private-authority-offers.js.map +1 -0
  80. package/dist/src/semantic-tool-arguments.d.ts +9 -0
  81. package/dist/src/semantic-tool-arguments.js +64 -0
  82. package/dist/src/semantic-tool-arguments.js.map +1 -0
  83. package/dist/src/semantic-tool-catalog.d.ts +48 -0
  84. package/dist/src/semantic-tool-catalog.js +105 -0
  85. package/dist/src/semantic-tool-catalog.js.map +1 -0
  86. package/dist/src/semantic-tool-command-helpers.d.ts +145 -0
  87. package/dist/src/semantic-tool-command-helpers.js +1272 -0
  88. package/dist/src/semantic-tool-command-helpers.js.map +1 -0
  89. package/dist/src/semantic-tool-list-projection.d.ts +10 -0
  90. package/dist/src/semantic-tool-list-projection.js +497 -0
  91. package/dist/src/semantic-tool-list-projection.js.map +1 -0
  92. package/dist/src/semantic-tool-model-facing-field-policy.d.ts +3 -0
  93. package/dist/src/semantic-tool-model-facing-field-policy.js +66 -0
  94. package/dist/src/semantic-tool-model-facing-field-policy.js.map +1 -0
  95. package/dist/src/semantic-tool-presentation.d.ts +37 -0
  96. package/dist/src/semantic-tool-presentation.js +680 -0
  97. package/dist/src/semantic-tool-presentation.js.map +1 -0
  98. package/dist/src/semantic-tool-private-authority.d.ts +39 -0
  99. package/dist/src/semantic-tool-private-authority.js +602 -0
  100. package/dist/src/semantic-tool-private-authority.js.map +1 -0
  101. package/dist/src/semantic-tool-relay.d.ts +16 -0
  102. package/dist/src/semantic-tool-relay.js +175 -0
  103. package/dist/src/semantic-tool-relay.js.map +1 -0
  104. package/dist/src/semantic-tool-runtime.d.ts +10 -0
  105. package/dist/src/semantic-tool-runtime.js +1157 -0
  106. package/dist/src/semantic-tool-runtime.js.map +1 -0
  107. package/dist/src/semantic-tool-schemas.d.ts +563 -0
  108. package/dist/src/semantic-tool-schemas.js +573 -0
  109. package/dist/src/semantic-tool-schemas.js.map +1 -0
  110. package/dist/src/semantic-tool-value-helpers.d.ts +5 -0
  111. package/dist/src/semantic-tool-value-helpers.js +27 -0
  112. package/dist/src/semantic-tool-value-helpers.js.map +1 -0
  113. package/dist/src/store-event-presentation.d.ts +17 -0
  114. package/dist/src/store-event-presentation.js +33 -0
  115. package/dist/src/store-event-presentation.js.map +1 -0
  116. package/dist/src/store.d.ts +3 -16
  117. package/dist/src/store.js +2 -30
  118. package/dist/src/store.js.map +1 -1
  119. package/dist/src/terminal-action-projection.d.ts +3 -17
  120. package/dist/src/terminal-action-projection.js +13 -41
  121. package/dist/src/terminal-action-projection.js.map +1 -1
  122. package/dist/src/terminal-agent-adapter.d.ts +7 -0
  123. package/dist/src/terminal-agent-adapter.js +8 -0
  124. package/dist/src/terminal-agent-adapter.js.map +1 -1
  125. package/dist/src/terminal-agent-bridge.d.ts +35 -332
  126. package/dist/src/terminal-agent-bridge.js +293 -2798
  127. package/dist/src/terminal-agent-bridge.js.map +1 -1
  128. package/dist/src/terminal-approval-cli-adapter.d.ts +17 -0
  129. package/dist/src/terminal-approval-cli-adapter.js +921 -0
  130. package/dist/src/terminal-approval-cli-adapter.js.map +1 -0
  131. package/dist/src/terminal-command-cli-adapter.d.ts +2 -429
  132. package/dist/src/terminal-command-cli-adapter.js +344 -6061
  133. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  134. package/dist/src/terminal-command-cli-ports.d.ts +443 -0
  135. package/dist/src/terminal-command-cli-ports.js +2 -0
  136. package/dist/src/terminal-command-cli-ports.js.map +1 -0
  137. package/dist/src/terminal-command-dispatch-preparation.d.ts +81 -0
  138. package/dist/src/terminal-command-dispatch-preparation.js +209 -0
  139. package/dist/src/terminal-command-dispatch-preparation.js.map +1 -0
  140. package/dist/src/terminal-command-dispatch-transport.d.ts +35 -0
  141. package/dist/src/terminal-command-dispatch-transport.js +859 -0
  142. package/dist/src/terminal-command-dispatch-transport.js.map +1 -0
  143. package/dist/src/terminal-command-foreground-proof.d.ts +32 -0
  144. package/dist/src/terminal-command-foreground-proof.js +72 -0
  145. package/dist/src/terminal-command-foreground-proof.js.map +1 -0
  146. package/dist/src/terminal-command-send-preflight.d.ts +41 -0
  147. package/dist/src/terminal-command-send-preflight.js +86 -0
  148. package/dist/src/terminal-command-send-preflight.js.map +1 -0
  149. package/dist/src/terminal-composer-classifier.d.ts +55 -0
  150. package/dist/src/terminal-composer-classifier.js +638 -0
  151. package/dist/src/terminal-composer-classifier.js.map +1 -0
  152. package/dist/src/terminal-control-ref.d.ts +23 -0
  153. package/dist/src/terminal-control-ref.js +32 -0
  154. package/dist/src/terminal-control-ref.js.map +1 -1
  155. package/dist/src/terminal-dispatch-recovery-authority.d.ts +56 -0
  156. package/dist/src/terminal-dispatch-recovery-authority.js +251 -0
  157. package/dist/src/terminal-dispatch-recovery-authority.js.map +1 -0
  158. package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +1 -9
  159. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +5 -240
  160. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  161. package/dist/src/terminal-handoff-cli-adapter.d.ts +1 -11
  162. package/dist/src/terminal-handoff-cli-adapter.js +2 -39
  163. package/dist/src/terminal-handoff-cli-adapter.js.map +1 -1
  164. package/dist/src/terminal-human-explicit-send-cli-adapter.d.ts +25 -0
  165. package/dist/src/terminal-human-explicit-send-cli-adapter.js +903 -0
  166. package/dist/src/terminal-human-explicit-send-cli-adapter.js.map +1 -0
  167. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -6
  168. package/dist/src/terminal-identity-authority-cli-adapter.js +3 -33
  169. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  170. package/dist/src/terminal-interaction-cli-adapter.js +209 -210
  171. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -1
  172. package/dist/src/terminal-interaction-core.d.ts +0 -2
  173. package/dist/src/terminal-interaction-core.js +4 -3
  174. package/dist/src/terminal-interaction-core.js.map +1 -1
  175. package/dist/src/terminal-interaction-protocol.d.ts +21 -0
  176. package/dist/src/terminal-interaction-protocol.js +74 -0
  177. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  178. package/dist/src/terminal-interaction-response-bridge.d.ts +126 -0
  179. package/dist/src/terminal-interaction-response-bridge.js +579 -0
  180. package/dist/src/terminal-interaction-response-bridge.js.map +1 -0
  181. package/dist/src/terminal-interaction-response-transaction.d.ts +44 -0
  182. package/dist/src/terminal-interaction-response-transaction.js +56 -0
  183. package/dist/src/terminal-interaction-response-transaction.js.map +1 -0
  184. package/dist/src/terminal-list-action-policy.d.ts +43 -0
  185. package/dist/src/terminal-list-action-policy.js +164 -0
  186. package/dist/src/terminal-list-action-policy.js.map +1 -0
  187. package/dist/src/terminal-list-cli-adapter.d.ts +6 -36
  188. package/dist/src/terminal-list-cli-adapter.js +314 -1711
  189. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  190. package/dist/src/terminal-list-facts.d.ts +129 -0
  191. package/dist/src/terminal-list-facts.js +179 -0
  192. package/dist/src/terminal-list-facts.js.map +1 -0
  193. package/dist/src/terminal-list-inventory.d.ts +59 -0
  194. package/dist/src/terminal-list-inventory.js +194 -0
  195. package/dist/src/terminal-list-inventory.js.map +1 -0
  196. package/dist/src/terminal-list-ownership-service.d.ts +118 -0
  197. package/dist/src/terminal-list-ownership-service.js +1003 -0
  198. package/dist/src/terminal-list-ownership-service.js.map +1 -0
  199. package/dist/src/terminal-list-projection-service.d.ts +49 -0
  200. package/dist/src/terminal-list-projection-service.js +407 -0
  201. package/dist/src/terminal-list-projection-service.js.map +1 -0
  202. package/dist/src/terminal-list-renderer.d.ts +12 -11
  203. package/dist/src/terminal-list-renderer.js +237 -104
  204. package/dist/src/terminal-list-renderer.js.map +1 -1
  205. package/dist/src/terminal-managed-send-cli-adapter.d.ts +33 -0
  206. package/dist/src/terminal-managed-send-cli-adapter.js +994 -0
  207. package/dist/src/terminal-managed-send-cli-adapter.js.map +1 -0
  208. package/dist/src/terminal-managed-turn-list-action-policy.d.ts +47 -0
  209. package/dist/src/terminal-managed-turn-list-action-policy.js +64 -0
  210. package/dist/src/terminal-managed-turn-list-action-policy.js.map +1 -0
  211. package/dist/src/terminal-model-control-action-policy.d.ts +60 -0
  212. package/dist/src/terminal-model-control-action-policy.js +96 -0
  213. package/dist/src/terminal-model-control-action-policy.js.map +1 -0
  214. package/dist/src/terminal-model-control-availability.d.ts +68 -0
  215. package/dist/src/terminal-model-control-availability.js +157 -0
  216. package/dist/src/terminal-model-control-availability.js.map +1 -0
  217. package/dist/src/terminal-model-control-bridge.d.ts +48 -0
  218. package/dist/src/terminal-model-control-bridge.js +259 -0
  219. package/dist/src/terminal-model-control-bridge.js.map +1 -0
  220. package/dist/src/terminal-model-control-profile.d.ts +55 -0
  221. package/dist/src/terminal-model-control-profile.js +137 -0
  222. package/dist/src/terminal-model-control-profile.js.map +1 -0
  223. package/dist/src/terminal-model-control-subject.d.ts +34 -0
  224. package/dist/src/terminal-model-control-subject.js +58 -0
  225. package/dist/src/terminal-model-control-subject.js.map +1 -0
  226. package/dist/src/terminal-model-control-transaction.d.ts +19 -0
  227. package/dist/src/terminal-model-control-transaction.js +39 -0
  228. package/dist/src/terminal-model-control-transaction.js.map +1 -0
  229. package/dist/src/terminal-model-control.d.ts +331 -0
  230. package/dist/src/terminal-model-control.js +1686 -0
  231. package/dist/src/terminal-model-control.js.map +1 -0
  232. package/dist/src/terminal-monitor-application-service.js +50 -17
  233. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  234. package/dist/src/terminal-native-inspection-bridge.d.ts +211 -0
  235. package/dist/src/terminal-native-inspection-bridge.js +1127 -0
  236. package/dist/src/terminal-native-inspection-bridge.js.map +1 -0
  237. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
  238. package/dist/src/terminal-runtime-cli-adapter.js +59 -17
  239. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  240. package/dist/src/terminal-submission-retry-application.d.ts +24 -0
  241. package/dist/src/terminal-submission-retry-application.js +402 -0
  242. package/dist/src/terminal-submission-retry-application.js.map +1 -0
  243. package/dist/src/terminal-submission-retry-ports.d.ts +30 -0
  244. package/dist/src/terminal-submission-retry-ports.js +2 -0
  245. package/dist/src/terminal-submission-retry-ports.js.map +1 -0
  246. package/dist/src/terminal-submission-retry-reconciliation.d.ts +236 -0
  247. package/dist/src/terminal-submission-retry-reconciliation.js +1130 -0
  248. package/dist/src/terminal-submission-retry-reconciliation.js.map +1 -0
  249. package/dist/src/terminal-submission-retry-transport.d.ts +58 -0
  250. package/dist/src/terminal-submission-retry-transport.js +675 -0
  251. package/dist/src/terminal-submission-retry-transport.js.map +1 -0
  252. package/dist/src/terminal-text-submission-bridge.d.ts +200 -0
  253. package/dist/src/terminal-text-submission-bridge.js +632 -0
  254. package/dist/src/terminal-text-submission-bridge.js.map +1 -0
  255. package/dist/src/terminal-user-explicit-send-policy.d.ts +29 -0
  256. package/dist/src/terminal-user-explicit-send-policy.js +46 -0
  257. package/dist/src/terminal-user-explicit-send-policy.js.map +1 -0
  258. package/dist/src/terminal-watch-cli-adapter.js +91 -143
  259. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  260. package/dist/src/terminal-watch-codec.d.ts +9 -0
  261. package/dist/src/terminal-watch-codec.js +1051 -0
  262. package/dist/src/terminal-watch-codec.js.map +1 -0
  263. package/dist/src/terminal-watch-interaction-response-store.d.ts +19 -0
  264. package/dist/src/terminal-watch-interaction-response-store.js +87 -0
  265. package/dist/src/terminal-watch-interaction-response-store.js.map +1 -0
  266. package/dist/src/terminal-watch-record.d.ts +319 -0
  267. package/dist/src/terminal-watch-record.js +742 -0
  268. package/dist/src/terminal-watch-record.js.map +1 -0
  269. package/dist/src/terminal-watch-service.js +48 -27
  270. package/dist/src/terminal-watch-service.js.map +1 -1
  271. package/dist/src/terminal-watch-store.d.ts +3 -279
  272. package/dist/src/terminal-watch-store.js +5 -1779
  273. package/dist/src/terminal-watch-store.js.map +1 -1
  274. package/docs/quickstart-herdr.md +1 -1
  275. package/docs/quickstart-tmux.md +27 -2
  276. package/openclaw.plugin.json +13 -1
  277. package/package.json +7 -2
  278. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +54 -11
@@ -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, 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, 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,290 +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 compatibilityWarnings = [...new Set([
626
- lifecycleCapability.compatibilityWarning,
627
- nativeInspectionCapability.compatibilityWarning
628
- ].filter((warning) => typeof warning === "string" && warning.trim().length > 0))];
629
- const codexLatentClearResumeObservationValue = session.agent === "codex"
630
- ? terminalListRuntime().codexLatentClearResumeObservation({
631
- screen: effectiveTerminalState.screen_excerpt,
632
- agentVersion
633
- })
634
- : 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;
635
407
  const lifecycleBindingToken = unmanagedTerminalBindingToken({
636
- terminalId: bridge.terminalConversationId(session),
408
+ terminalId,
637
409
  terminalControl,
638
410
  agent: session.agent,
639
411
  pid: session.pid,
@@ -644,7 +416,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
644
416
  rollout: nativeAgentIdentity?.rollout
645
417
  });
646
418
  const authorityLifecycleBindingToken = unmanagedTerminalBindingToken({
647
- terminalId: bridge.terminalConversationId(session),
419
+ terminalId,
648
420
  terminalControl,
649
421
  agent: session.agent,
650
422
  pid: session.pid,
@@ -654,62 +426,26 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
654
426
  processBirth: nativeProcessBirth,
655
427
  rollout: authorityNativeAgentIdentity?.rollout
656
428
  });
657
- const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
658
- Boolean(nativeProcessUuid && nativeProcessBirth);
659
- let terminalHasBlockingTurn = true;
660
- try {
661
- terminalHasBlockingTurn = terminalIncarnationBlockingTurns(terminalListRuntime().storeDirFromOptions(options), terminalControl).length > 0;
662
- }
663
- catch (error) {
664
- runtimeLog("warn", "terminal_managed_turn_inventory_unavailable", {
665
- terminal_target: terminalControl.target,
666
- error: error instanceof Error ? error.message : String(error)
667
- });
668
- }
669
- const composerAuthority = await observeAutomatedInputComposerReady({
670
- session,
671
- terminalControl,
672
- terminalState: effectiveTerminalState,
673
- options
674
- });
675
- const automatedInputComposerReady = composerAuthority.automatedInputComposerReady;
676
- const userExplicitComposerReady = composerAuthority.userExplicitComposerReady;
677
- let physicalProcessIncarnation;
678
- try {
679
- physicalProcessIncarnation =
680
- terminalListRuntime().processIncarnationForPid(session.pid);
681
- }
682
- catch (error) {
683
- runtimeLog("warn", "terminal_physical_process_incarnation_unavailable", {
684
- agent: session.agent,
685
- terminal_target: terminalControl.target,
686
- pid: session.pid,
687
- error: error instanceof Error ? error.message : String(error)
688
- });
689
- }
690
- const commands = terminalListCommands({
429
+ const actionSubject = {
430
+ exactTerminalRow: true,
431
+ processState: "active",
691
432
  agent: session.agent,
692
433
  terminalControl,
693
- terminalState: effectiveTerminalState,
694
- lifecycleCapability,
695
- nativeInspectionCapability,
696
- nativeAgentIdentity: authorityNativeAgentIdentity,
697
- nativeProcessUuid,
698
- nativeProcessBirth,
699
- codexLifecycleIncarnationAvailable,
700
- automatedInputComposerReady,
701
- hasOrphanedDispatch: orphanedDispatch !== undefined,
702
- terminalHasBlockingTurn,
703
- terminalHasInteraction: terminalStatusSnapshot?.interaction_state !== undefined
434
+ pid: session.pid
435
+ };
436
+ const actionDecisions = decideTerminalListActions({
437
+ subject: actionSubject,
438
+ facts
704
439
  });
440
+ const commands = actionDecisions.commands;
705
441
  const entry = {
706
- id: bridge.terminalConversationId(session),
707
- short_ref: sessionShortRef(bridge.terminalConversationId(session)),
442
+ id: terminalId,
443
+ short_ref: sessionShortRef(terminalId),
708
444
  source: "terminal",
709
445
  agent: session.agent,
710
446
  process_state: "active",
711
447
  pid: session.pid,
712
- child_pids: childProcessIdsForRoot(session, activeSessions),
448
+ child_pids: childPids,
713
449
  command: session.command,
714
450
  cwd: session.cwd,
715
451
  workspace: session.cwd,
@@ -731,6 +467,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
731
467
  agent_version: agentVersion,
732
468
  native_thread_lifecycle: lifecycleCapability,
733
469
  native_inspection: nativeInspectionCapability,
470
+ model_control: modelControlCapability,
734
471
  ...(compatibilityWarnings.length > 0
735
472
  ? { compatibility_warnings: compatibilityWarnings }
736
473
  : {}),
@@ -738,23 +475,23 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
738
475
  confidence: session.confidence,
739
476
  reason: session.reason,
740
477
  terminal_control: terminalControl,
741
- approval_state: effectiveTerminalState.approval_state,
742
- activity_state: effectiveTerminalState.activity_state,
743
- activity_reason: effectiveTerminalState.activity_reason,
744
- screen_state: effectiveTerminalState.screen_state,
745
- screen_reason: effectiveTerminalState.screen_reason,
746
- native_identity_state: effectiveTerminalState.native_identity_state,
747
- durable_activity_state: effectiveTerminalState.durable_activity_state,
748
- durable_activity_reason: effectiveTerminalState.durable_activity_reason,
478
+ approval_state: projectedTerminalState.approval_state,
479
+ activity_state: projectedTerminalState.activity_state,
480
+ activity_reason: projectedTerminalState.activity_reason,
481
+ screen_state: projectedTerminalState.screen_state,
482
+ screen_reason: projectedTerminalState.screen_reason,
483
+ native_identity_state: projectedTerminalState.native_identity_state,
484
+ durable_activity_state: projectedTerminalState.durable_activity_state,
485
+ durable_activity_reason: projectedTerminalState.durable_activity_reason,
749
486
  // Internal exact-observation evidence. The public projection strips this
750
487
  // object; raw terminal status reuses it so screen, approval, and activity
751
488
  // all describe the same capture.
752
489
  ...(terminalStatusSnapshot
753
490
  ? { _terminal_status_snapshot: terminalStatusSnapshot }
754
491
  : {}),
755
- // Internal action-projection evidence. terminalFirstListProjection strips
756
- // this field after gating every automated-input action that can follow a
757
- // 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.
758
495
  _automated_input_composer_ready: automatedInputComposerReady,
759
496
  _user_explicit_composer_ready: userExplicitComposerReady,
760
497
  _native_identity_authority: {
@@ -783,7 +520,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
783
520
  owner_conversation_id: stringValue(orphanedDispatch.conversation_id),
784
521
  message_id: stringValue(orphanedDispatch.message_id),
785
522
  transition_id: stringValue(orphanedDispatch.transition_id),
786
- recovery: `/akk close ${bridge.terminalConversationId(session)} ` +
523
+ recovery: `/akk close ${terminalId} ` +
787
524
  (orphanedDispatch.kind === "lifecycle"
788
525
  ? `--expected-transition-id ${String(orphanedDispatch.transition_id)}`
789
526
  : `--expected-message-id ${String(orphanedDispatch.message_id)}`)
@@ -793,23 +530,25 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
793
530
  commands
794
531
  };
795
532
  const renderedActions = renderAvailableListActions(entry);
796
- const terminalUserExplicitSendAuthority = decideTerminalUserExplicitSendAuthority({
797
- exactTerminalRow: true,
798
- terminalId: entry.id,
799
- processState: entry.process_state,
800
- terminalControl,
801
- agent: session.agent,
802
- pid: session.pid,
803
- processUuid: physicalProcessIncarnation?.processUuid,
804
- processBirth: physicalProcessIncarnation?.processBirth,
805
- approvalScanned: effectiveTerminalState.approval_state.scanned === true,
806
- approvalBlocked: effectiveTerminalState.approval_state.blocked === true,
807
- // Codex user-explicit Send treats this observation as advisory: an
808
- // off-screen or truncated composer must not hide the physical action.
809
- // Claude Code still consumes the exact-composer result in the shared
810
- // authority decision below.
811
- userExplicitComposerReady
812
- });
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 };
813
552
  const terminalUserExplicitSendAction = terminalUserExplicitSendAuthority.eligible
814
553
  ? {
815
554
  tool: "agent_knock_knock_send",
@@ -826,6 +565,27 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
826
565
  : {})
827
566
  }
828
567
  : undefined;
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({
585
+ renderedActions,
586
+ availability: modelControlAvailability,
587
+ mutationScope: modelControlCapability.scope
588
+ });
829
589
  const foregroundIdentificationActions = terminalUserExplicitSendAuthority.eligible &&
830
590
  commands.identify_foreground === true
831
591
  ? {
@@ -858,59 +618,176 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
858
618
  ? { _terminal_user_explicit_send_action: terminalUserExplicitSendAction }
859
619
  : {}),
860
620
  available_actions: {
861
- ...renderedActions,
621
+ ...modelControlActions,
862
622
  ...foregroundIdentificationActions
863
623
  }
864
624
  };
865
625
  }
866
- function terminalListCommands(input) {
867
- const { agent, terminalControl, terminalState, lifecycleCapability, nativeInspectionCapability, nativeAgentIdentity, nativeProcessUuid, nativeProcessBirth, codexLifecycleIncarnationAvailable, automatedInputComposerReady, hasOrphanedDispatch, terminalHasBlockingTurn, terminalHasInteraction } = input;
868
- const foregroundIdentificationEligible = agent === "codex" &&
869
- nativeInspectionCapability.status === "supported" &&
870
- nativeInspectionCapability.statusInspection === true &&
871
- terminalState.screen_state === "idle" &&
872
- terminalState.native_identity_state !== "resolved" &&
873
- terminalState.approval_state.scanned === true &&
874
- terminalState.approval_state.blocked !== true &&
875
- automatedInputComposerReady &&
876
- codexLifecycleIncarnationAvailable &&
877
- terminalControl.capabilities.includes("send_keys") &&
878
- terminalControl.capabilities.includes("screen_status") &&
879
- !terminalHasInteraction &&
880
- !hasOrphanedDispatch &&
881
- !terminalHasBlockingTurn;
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
+ })
642
+ });
643
+ }
644
+ function terminalListFactPortsFor(input) {
645
+ const { session, terminalControl, terminalId, options, bridge } = input;
882
646
  return {
883
- send: !terminalHasBlockingTurn,
884
- approve: terminalControl.capabilities.includes("terminal_approval") &&
885
- terminalState.approval_state.approvable === true,
886
- status: true,
887
- cancel: terminalControl.capabilities.includes("terminal_cancel"),
888
- close: hasOrphanedDispatch,
889
- new_thread: lifecycleCapability.status === "supported" &&
890
- lifecycleCapability.newThread === true &&
891
- codexLifecycleIncarnationAvailable &&
892
- !terminalHasBlockingTurn,
893
- list_resumable_threads: lifecycleCapability.status === "supported" &&
894
- lifecycleCapability.resumeExact === true &&
895
- codexLifecycleIncarnationAvailable,
896
- native_inspect: nativeInspectionCapability.status === "supported" &&
897
- nativeInspectionCapability.statusInspection === true &&
898
- terminalState.activity_state === "idle" &&
899
- automatedInputComposerReady &&
900
- terminalControl.capabilities.includes("send_keys") &&
901
- terminalControl.capabilities.includes("screen_status") &&
902
- (agent === "codex"
903
- ? codexLifecycleIncarnationAvailable
904
- : Boolean(nativeAgentIdentity?.sessionId &&
905
- nativeAgentIdentity.processUuid)) &&
906
- !hasOrphanedDispatch &&
907
- !terminalHasBlockingTurn,
908
- identify_foreground: foregroundIdentificationEligible,
909
- identify_and_send: foregroundIdentificationEligible,
910
- watch: terminalControl.capabilities.includes("screen_status") ||
911
- Boolean(nativeAgentIdentity?.sessionId &&
912
- (agent !== "codex" || nativeAgentIdentity.rollout))
647
+ observeStatus: () => listStateForTerminal(session.agent, terminalControl, options, bridge, {
648
+ pid: session.pid,
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;
700
+ }
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;
707
+ }
708
+ }
709
+ function observePhysicalProcessIncarnationForList(session, terminalControl) {
710
+ try {
711
+ return terminalListRuntime().processIncarnationForPid(session.pid);
712
+ }
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;
721
+ }
722
+ }
723
+ async function observeModelControlResidualForList(input) {
724
+ const inspect = input.bridge.inspectModelControlResidual;
725
+ const profile = input.modelControlProfile;
726
+ if (typeof inspect !== "function" ||
727
+ input.session.agent !== "codex" ||
728
+ !profile?.supportsResidualRepair ||
729
+ input.modelControlCapability.status !== "supported" ||
730
+ input.modelControlCapability.behaviorProfile !==
731
+ profile.behaviorProfile ||
732
+ !input.nativeIdentityEligible ||
733
+ input.effectiveTerminalState.activity_state === "working" ||
734
+ input.effectiveTerminalState.activity_state === "awaiting_approval" ||
735
+ input.effectiveTerminalState.approval_state.scanned !== true ||
736
+ input.effectiveTerminalState.approval_state.blocked === true ||
737
+ input.terminalHasInteraction ||
738
+ input.terminalHasBlockingTurn ||
739
+ input.hasOrphanedDispatch) {
740
+ return undefined;
741
+ }
742
+ const adapter = input.bridge.registry.require("codex");
743
+ const plan = input.modelControlCapability.status === "supported"
744
+ ? adapter.planModelControl?.(input.modelControlCapability)
745
+ : undefined;
746
+ if (!plan || !terminalModelControlPlanConforms({
747
+ agent: input.session.agent,
748
+ agentVersion: input.agentVersion,
749
+ plan
750
+ })) {
751
+ return undefined;
752
+ }
753
+ const identity = input.nativeAgentIdentity;
754
+ const runtime = {
755
+ pid: input.session.pid,
756
+ agentVersion: input.agentVersion,
757
+ cwd: input.terminalControl.currentPath,
758
+ terminalTarget: input.terminalControl.target,
759
+ ...(input.zeroRolloutVerified
760
+ ? { expectedEmptyNativeSession: true }
761
+ : {}),
762
+ ...(identity?.sessionId
763
+ ? {
764
+ nativeSessionId: identity.sessionId,
765
+ expectedNativeSessionId: identity.sessionId
766
+ }
767
+ : {}),
768
+ ...(identity?.processUuid
769
+ ? { nativeProcessUuid: identity.processUuid }
770
+ : {}),
771
+ ...(identity?.processBirth
772
+ ? { nativeProcessBirth: identity.processBirth }
773
+ : {}),
774
+ ...(identity?.rollout
775
+ ? {
776
+ requireNativeRolloutIdentity: true,
777
+ nativeRollout: identity.rollout
778
+ }
779
+ : {})
913
780
  };
781
+ try {
782
+ return await inspect.call(input.bridge, "codex", input.terminalControl, profile.agentVersion, plan, { runtime, beforeInput: () => undefined });
783
+ }
784
+ catch (error) {
785
+ runtimeLog("warn", "terminal_model_control_residual_probe_unavailable", {
786
+ terminal_target: input.terminalControl.target,
787
+ error: error instanceof Error ? error.message : String(error)
788
+ });
789
+ return undefined;
790
+ }
914
791
  }
915
792
  function effectiveTerminalListState(input) {
916
793
  const { session, terminalState, nativeIdentityObservation, nativeAgentIdentity, nativeIdentityAuthorityObservation, codexOpenRootRolloutInventory, nativeProcessUuid, nativeProcessBirth } = input;
@@ -1073,7 +950,7 @@ function terminalBridgeStatusWithProjection(status, projection) {
1073
950
  }
1074
951
  return Object.create(Object.getPrototypeOf(status), descriptors);
1075
952
  }
1076
- async function observeTerminalNativeListIdentity(session, terminalControl, options, bridge) {
953
+ async function observeTerminalNativeListIdentity(session, terminalControl, options, bridge, terminalId) {
1077
954
  let orphanedDispatch;
1078
955
  try {
1079
956
  orphanedDispatch =
@@ -1091,7 +968,7 @@ async function observeTerminalNativeListIdentity(session, terminalControl, optio
1091
968
  codexIdentityContext = terminalListRuntime().codexManagedIdentityResolutionContext({
1092
969
  storeDir: terminalListRuntime().storeDirFromOptions(options),
1093
970
  terminal: {
1094
- conversationId: bridge.terminalConversationId(session),
971
+ conversationId: terminalId,
1095
972
  agent: session.agent,
1096
973
  pid: session.pid,
1097
974
  terminalControl
@@ -1262,1197 +1139,41 @@ async function observeAutomatedInputComposerReady({ session, terminalControl, te
1262
1139
  userExplicitComposerReady: userExplicitReady
1263
1140
  };
1264
1141
  }
1265
- function observeTerminalListBindingAuthority(listedTerminal, context) {
1266
- const { storeDir, managedSessions, allConversations, displayedConversations, mutationsAllowed, nonterminalDeferredTransfers } = context;
1267
- 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;
1268
- const terminal = terminalIdentityAuthorityView(listedTerminal, nativeIdentityAuthorityValue);
1269
- const codexOpenRootRolloutInventory = isRecord(codexOpenRootRolloutInventoryValue)
1270
- ? codexOpenRootRolloutInventoryValue
1271
- : undefined;
1272
- const terminalControl = isRecord(terminal.terminal_control)
1273
- ? terminal.terminal_control
1274
- : undefined;
1275
- const terminalHasNonterminalDeferredTransfer = Boolean(terminalControl && nonterminalDeferredTransfers.some((transfer) => transfer.terminal_id === String(terminal.id) &&
1276
- transfer.process_pid === Number(terminal.pid) &&
1277
- terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl)));
1278
- const allRelated = terminalControl
1279
- ? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
1280
- : [];
1281
- const displayedRelated = terminalControl
1282
- ? displayedConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
1283
- : [];
1284
- const relatedSessions = terminalControl
1285
- ? managedSessions.filter((session) => terminalControlsShareIncarnation(session.binding?.terminal_control, terminalControl))
1286
- : [];
1287
- const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir));
1288
- const conflictingBoundSessionClaims = relatedSessions.flatMap((session) => {
1289
- const kind = managedBindingConflictKindForLiveTerminalEntry({
1290
- storeDir,
1291
- session,
1292
- terminal
1293
- });
1294
- return kind && kind !== "stale_process_incarnation"
1295
- ? [{ session, kind }]
1296
- : [];
1297
- });
1298
- const unresolvedSessionClaims = relatedSessions.filter((session) => ["transitioning", "quarantined"].includes(session.status) &&
1299
- managedSessionClaimsLiveTerminalEntry(session, terminal));
1300
- const sessionAuthorityConflict = decideTerminalSessionAuthorityConflict({
1301
- unresolvedSessionClaims,
1302
- conflictingBoundSessionClaims,
1303
- matchingSessions
1304
- });
1305
- const authoritativeSession = matchingSessions[0];
1306
- const discoveredOwnership = terminalControl
1307
- ? terminalDispatchOwnership(terminalControl)
1308
- : { state: "none" };
1309
- const localOwnership = discoveredOwnership.state === "current"
1310
- ? localTerminalDispatchOwnership(discoveredOwnership.conversation, allRelated, terminal)
1311
- : discoveredOwnership;
1312
- const dispatchOwnerMismatch = !sessionAuthorityConflict &&
1313
- localOwnership.state === "current" &&
1314
- authoritativeSession &&
1315
- sessionIdForConversation(localOwnership.conversation) !==
1316
- authoritativeSession.session_id
1317
- ? {
1318
- ownerSessionId: sessionIdForConversation(discoveredOwnership.state === "current"
1319
- ? discoveredOwnership.conversation
1320
- : localOwnership.conversation)
1321
- }
1322
- : undefined;
1323
- const ownership = applySessionAuthorityToDispatch({
1324
- localOwnership,
1325
- sessionAuthorityConflict,
1326
- authoritativeSession,
1327
- dispatchOwnerMismatch
1328
- });
1329
- const discoveredRawActions = isRecord(terminal.available_actions)
1330
- ? terminal.available_actions
1331
- : {};
1332
- const rawActions = mutationsAllowed
1333
- ? discoveredRawActions
1334
- : readOnlyListActions(discoveredRawActions);
1335
- const rawSendAction = isRecord(rawActions.send)
1336
- ? rawActions.send
1337
- : {};
1338
- const bindingAwareRawActions = authoritativeSession
1339
- ? actionsForManagedSessionBinding(rawActions, authoritativeSession)
1340
- : rawActions;
1341
- const sessionAwareRawActionsBase = authoritativeSession &&
1342
- managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
1343
- ? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
1344
- : bindingAwareRawActions;
1345
- const sessionAwareRawActions = terminalHasNonterminalDeferredTransfer
1346
- ? readOnlyListActions(sessionAwareRawActionsBase)
1347
- : sessionAwareRawActionsBase;
1348
- const soleBindingConflict = conflictingBoundSessionClaims.length === 1
1349
- ? conflictingBoundSessionClaims[0]
1350
- : undefined;
1351
- const externalHandoffDetected = conflictingBoundSessionClaims.some(({ kind }) => kind === "live_external_thread_change");
1352
- const conflictingSessionRevision = Number(soleBindingConflict?.session.revision);
1353
- const conflictingSessionTurns = soleBindingConflict
1354
- ? terminalListRuntime().managedTurnsForSession(storeDir, soleBindingConflict.session.session_id)
1355
- : [];
1356
- const expectedTerminalToken = stringValue(terminal.lifecycle_binding_token);
1357
- const externalHandoffNativeThreadId = stringValue(terminal.native_agent_status_card_session_id) ?? stringValue(terminal.native_agent_session_id);
1358
- const resolvedNativeThreadId = stringValue(terminal.native_agent_session_id);
1359
- const externalHandoffTerminalToken = terminalControl &&
1360
- externalHandoffNativeThreadId &&
1361
- isExactNativeThreadId(externalHandoffNativeThreadId)
1362
- ? unmanagedTerminalBindingToken({
1363
- terminalId: stringValue(terminal.id),
1364
- terminalControl,
1365
- agent: terminal.agent,
1366
- pid: Number(terminal.pid),
1367
- workspace: terminal.workspace ?? terminal.cwd ?? cliCwd(),
1368
- nativeThreadId: externalHandoffNativeThreadId,
1369
- processUuid: stringValue(terminal.native_agent_process_uuid),
1370
- processBirth: stringValue(terminal.native_agent_process_birth),
1371
- rollout: resolvedNativeThreadId === externalHandoffNativeThreadId &&
1372
- isRecord(terminal.native_agent_rollout)
1373
- ? terminal.native_agent_rollout
1374
- : undefined
1375
- })
1376
- : undefined;
1377
- const externalHandoffTarget = soleBindingConflict?.kind === "live_external_thread_change" &&
1378
- externalHandoffNativeThreadId &&
1379
- isExactNativeThreadId(externalHandoffNativeThreadId)
1380
- ? terminalListRuntime().observedHandoffTargetResolution({
1381
- storeDir,
1382
- agent: terminal.agent,
1383
- workspace: terminal.workspace ?? terminal.cwd ?? cliCwd(),
1384
- nativeThreadId: externalHandoffNativeThreadId.toLowerCase(),
1385
- sourceSessionId: soleBindingConflict.session.session_id
1386
- })
1387
- : undefined;
1388
- const externalHandoffSnapshotToken = externalHandoffTerminalToken &&
1389
- soleBindingConflict?.kind === "live_external_thread_change" &&
1390
- externalHandoffTarget?.status === "eligible"
1391
- ? humanObservedHandoffBindingToken({
1392
- terminal_token: externalHandoffTerminalToken,
1393
- source_session_id: soleBindingConflict.session.session_id,
1394
- source_revision: managedSessionRevision(soleBindingConflict.session),
1395
- source_binding_token: managedSessionBindingToken(soleBindingConflict.session),
1396
- target: externalHandoffTarget.snapshot
1397
- })
1398
- : undefined;
1399
- const blockingHandoffTurns = conflictingSessionTurns.filter((turn) => isSessionSendBlockingStatus(turn.status));
1400
- const terminalBlockingTurns = terminalControl
1401
- ? terminalIncarnationBlockingTurns(storeDir, terminalControl, allRelated)
1402
- : [];
1403
- const externalHandoffSourceSessionIds = new Set(conflictingBoundSessionClaims
1404
- .filter(({ kind }) => kind === "live_external_thread_change")
1405
- .map(({ session }) => session.session_id));
1406
- const handoffSourceBlockingTurns = terminalBlockingTurns.filter((turn) => externalHandoffSourceSessionIds.has(sessionIdForConversation(turn)));
1407
- const nativeIdentityObservation = isRecord(terminal.native_agent_identity_observation)
1408
- ? terminal.native_agent_identity_observation
1409
- : undefined;
1410
- const codexProcessUuid = stringValue(terminal.native_agent_process_uuid);
1411
- const codexProcessBirth = stringValue(terminal.native_agent_process_birth);
1412
- const codexWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
1413
- const codexSendAuthorityContext = terminalControl
1414
- ? {
1415
- terminalId: String(terminal.id),
1416
- terminalControl,
1417
- pid: Number(terminal.pid),
1418
- workspace: codexWorkspace,
1419
- liveProcessUuid: codexProcessUuid,
1420
- liveProcessBirth: codexProcessBirth
1421
- }
1422
- : undefined;
1423
- return {
1424
- authorityTerminal: terminal,
1425
- automatedInputComposerReady,
1426
- codexOpenRootRolloutInventory,
1427
- codexLatentClearResumeValue,
1428
- terminalUserExplicitSendAction,
1429
- publicTerminal,
1430
- terminalControl,
1431
- terminalHasNonterminalDeferredTransfer,
1432
- allRelated,
1433
- displayedRelated,
1434
- relatedSessions,
1435
- matchingSessions,
1436
- conflictingBoundSessionClaims,
1437
- unresolvedSessionClaims,
1438
- sessionAuthorityConflict,
1439
- authoritativeSession,
1440
- discoveredOwnership,
1441
- ownership,
1442
- rawActions,
1443
- rawSendAction,
1444
- sessionAwareRawActions,
1445
- soleBindingConflict,
1446
- externalHandoffDetected,
1447
- conflictingSessionRevision,
1448
- conflictingSessionTurns,
1449
- expectedTerminalToken,
1450
- externalHandoffNativeThreadId,
1451
- externalHandoffTarget,
1452
- externalHandoffSnapshotToken,
1453
- blockingHandoffTurns,
1454
- terminalBlockingTurns,
1455
- handoffSourceBlockingTurns,
1456
- nativeIdentityObservation,
1457
- codexProcessUuid,
1458
- codexProcessBirth,
1459
- codexWorkspace,
1460
- codexSendAuthorityContext
1461
- };
1462
- }
1463
- function terminalIdentityAuthorityView(terminal, authorityValue) {
1464
- if (!isRecord(authorityValue)) {
1465
- return terminal;
1466
- }
1467
- const observation = isRecord(authorityValue.observation)
1468
- ? authorityValue.observation
1469
- : { status: "not_observed" };
1470
- const identity = isRecord(authorityValue.identity)
1471
- ? authorityValue.identity
1472
- : undefined;
1473
- return {
1474
- ...terminal,
1475
- native_agent_session_id: stringValue(identity?.sessionId),
1476
- native_agent_rollout: isRecord(identity?.rollout)
1477
- ? identity.rollout
1478
- : undefined,
1479
- native_agent_identity_observation: observation,
1480
- lifecycle_binding_token: stringValue(authorityValue.lifecycle_binding_token) ??
1481
- terminal.lifecycle_binding_token
1482
- };
1483
- }
1484
- function observeVerifiedEmptyTerminalAuthority(terminal, context, binding) {
1485
- const { storeDir, terminals, mutationsAllowed } = context;
1486
- const { automatedInputComposerReady, terminalControl, matchingSessions, unresolvedSessionClaims, discoveredOwnership, rawActions, soleBindingConflict, blockingHandoffTurns, terminalBlockingTurns, nativeIdentityObservation, codexProcessUuid, codexProcessBirth, codexWorkspace, codexSendAuthorityContext } = binding;
1487
- const verifiedEmptySourceNativeThreadId = stringValue(soleBindingConflict?.session.binding?.native_thread_id)?.toLowerCase();
1488
- const verifiedEmptySourceActiveElsewhere = Boolean(verifiedEmptySourceNativeThreadId &&
1489
- terminals.some((candidate) => stringValue(candidate.id) !== stringValue(terminal.id) &&
1490
- candidate.agent === "codex" &&
1491
- stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
1492
- verifiedEmptySourceNativeThreadId));
1493
- const verifiedEmptyRawSendAction = isRecord(rawActions.send)
1494
- ? rawActions.send
1495
- : {
1496
- tool: "agent_knock_knock_send",
1497
- arguments: { selector: stringValue(terminal.id) },
1498
- missing_required: ["request"]
1499
- };
1500
- const verifiedEmptyCodexHandoffEligible = Boolean(mutationsAllowed &&
1501
- terminal.agent === "codex" &&
1502
- terminalControl &&
1503
- discoveredOwnership.state === "none" &&
1504
- unresolvedSessionClaims.length === 0 &&
1505
- matchingSessions.length === 0 &&
1506
- soleBindingConflict?.kind === "unverifiable" &&
1507
- nativeIdentityObservation?.status === "verified_absent" &&
1508
- codexSendAuthorityContext &&
1509
- codexProcessUuid &&
1510
- codexProcessBirth &&
1511
- codexWorkspace &&
1512
- exactBoundCodexSendSource({
1513
- kind: "verified_empty",
1514
- sourceSession: soleBindingConflict.session,
1515
- context: codexSendAuthorityContext
1516
- }) &&
1517
- ["idle", "unknown"].includes(String(terminal.activity_state)) &&
1518
- automatedInputComposerReady === true &&
1519
- !(isRecord(terminal.approval_state) &&
1520
- terminal.approval_state.blocked === true) &&
1521
- blockingHandoffTurns.length === 0 &&
1522
- terminalBlockingTurns.length === 0 &&
1523
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
1524
- !verifiedEmptySourceActiveElsewhere &&
1525
- terminal.orphaned_terminal_dispatch === undefined &&
1526
- terminalControl?.capabilities.includes("send_keys") &&
1527
- terminalControl.capabilities.includes("screen_status"));
1528
- const verifiedEmptyCodexSnapshotToken = verifiedEmptyCodexHandoffEligible &&
1529
- codexSendAuthorityContext &&
1530
- soleBindingConflict &&
1531
- codexProcessUuid &&
1532
- codexProcessBirth &&
1533
- codexWorkspace
1534
- ? verifiedEmptyCodexHandoffToken({
1535
- terminalId: String(terminal.id),
1536
- terminalControl: codexSendAuthorityContext.terminalControl,
1537
- pid: Number(terminal.pid),
1538
- workspace: codexWorkspace,
1539
- processUuid: codexProcessUuid,
1540
- processBirth: codexProcessBirth,
1541
- sourceSession: soleBindingConflict.session
1542
- })
1543
- : undefined;
1544
- return {
1545
- ...binding,
1546
- verifiedEmptyRawSendAction,
1547
- verifiedEmptyCodexSnapshotToken
1548
- };
1549
- }
1550
- function observeDeferredSourceAuthority(terminal, context, observation) {
1551
- const { terminals } = context;
1552
- const { codexLatentClearResumeValue, codexOpenRootRolloutInventory, authoritativeSession, matchingSessions, conflictingBoundSessionClaims, unresolvedSessionClaims, soleBindingConflict, nativeIdentityObservation } = observation;
1553
- const deferredCodexCandidateInventory = codexOpenRootRolloutInventory &&
1554
- codexOpenRootRolloutInventory.roots.length > 0
1555
- ? codexOpenRootRolloutInventory
1556
- : undefined;
1557
- const abandonedConflictSource = !authoritativeSession &&
1558
- soleBindingConflict?.kind === "unverifiable" &&
1559
- matchingSessions.length === 0 &&
1560
- conflictingBoundSessionClaims.length === 1 &&
1561
- unresolvedSessionClaims.length === 0 &&
1562
- terminal.agent === "codex"
1563
- ? soleBindingConflict.session
1564
- : undefined;
1565
- const deferredCodexSource = (authoritativeSession ??
1566
- abandonedConflictSource) &&
1567
- terminal.agent === "codex" &&
1568
- (nativeIdentityObservation?.status === "verified_absent" ||
1569
- deferredCodexCandidateInventory !== undefined)
1570
- ? authoritativeSession ?? abandonedConflictSource
1571
- : undefined;
1572
- const deferredCodexSourceNativeThreadId = stringValue(deferredCodexSource?.binding?.native_thread_id)?.toLowerCase();
1573
- const deferredCodexLatentClearResumeFingerprint = isRecord(codexLatentClearResumeValue) &&
1574
- stringValue(codexLatentClearResumeValue.source_native_thread_id)?.toLowerCase() === deferredCodexSourceNativeThreadId
1575
- ? stringValue(codexLatentClearResumeValue.fingerprint)
1576
- : undefined;
1577
- if (deferredCodexLatentClearResumeFingerprint) {
1578
- // The resume hint is useful operational context, but it is not durable
1579
- // foreground authority: it can scroll away while the latent thread is
1580
- // still current. Candidate routing and its token rely on the complete
1581
- // rollout inventory and Store authority below instead.
1582
- runtimeLog("info", "terminal_codex_latent_clear_hint_observed", {
1583
- terminal_id: String(terminal.id),
1584
- source_session_id: deferredCodexSource?.session_id,
1585
- source_native_thread_id: deferredCodexSourceNativeThreadId
1586
- });
1587
- }
1588
- const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
1589
- terminals.some((candidate) => stringValue(candidate.id) !== stringValue(terminal.id) &&
1590
- candidate.agent === "codex" &&
1591
- stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
1592
- deferredCodexSourceNativeThreadId));
1593
- return {
1594
- ...observation,
1595
- deferredCodexCandidateInventory,
1596
- abandonedConflictSource,
1597
- deferredCodexSource,
1598
- deferredCodexSourceActiveElsewhere
1599
- };
1600
- }
1601
- function observeDeferredTerminalAuthority(terminal, context, observation) {
1602
- const { storeDir, mutationsAllowed } = context;
1603
- const { automatedInputComposerReady, terminalControl, terminalHasNonterminalDeferredTransfer, matchingSessions, conflictingBoundSessionClaims, unresolvedSessionClaims, discoveredOwnership, terminalBlockingTurns, codexProcessUuid, codexProcessBirth, codexWorkspace, codexSendAuthorityContext, deferredCodexCandidateInventory, abandonedConflictSource, deferredCodexSource, deferredCodexSourceActiveElsewhere } = observation;
1604
- const deferredCodexAuthority = codexSendAuthorityContext
1605
- ? terminalListRuntime().observeDeferredCodexAuthority({
1606
- mode: "list",
1607
- storeDir,
1608
- context: codexSendAuthorityContext,
1609
- sourceSession: deferredCodexSource,
1610
- candidateInventory: deferredCodexCandidateInventory,
1611
- abandonment: abandonedConflictSource
1612
- ? "missing_rollout"
1613
- : "never",
1614
- requireUnclaimedCandidate: true
1615
- })
1616
- : undefined;
1617
- const deferredCodexSourceRolloutAuthority = deferredCodexAuthority?.sourceRolloutAuthority ?? "present";
1618
- const deferredCodexDispatchSnapshot = deferredCodexAuthority?.dispatchSnapshot;
1619
- const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
1620
- !terminalHasNonterminalDeferredTransfer &&
1621
- deferredCodexSource &&
1622
- terminalControl &&
1623
- hasCanonicalTerminalEndpoint(terminalControl) &&
1624
- discoveredOwnership.state === "none" &&
1625
- unresolvedSessionClaims.length === 0 &&
1626
- ((matchingSessions.length === 1 &&
1627
- conflictingBoundSessionClaims.length === 0) ||
1628
- (deferredCodexSourceRolloutAuthority ===
1629
- "explicitly_abandoned_predecessor" &&
1630
- matchingSessions.length === 0 &&
1631
- conflictingBoundSessionClaims.length === 1)) &&
1632
- codexProcessUuid &&
1633
- codexProcessBirth &&
1634
- codexWorkspace &&
1635
- deferredCodexAuthority?.exactSource &&
1636
- terminalBlockingTurns.length === 0 &&
1637
- terminal.orphaned_terminal_dispatch === undefined &&
1638
- deferredCodexDispatchSnapshot &&
1639
- ["idle", "unknown"].includes(String(terminal.activity_state)) &&
1640
- automatedInputComposerReady === true &&
1641
- !(isRecord(terminal.approval_state) &&
1642
- terminal.approval_state.blocked === true) &&
1643
- !deferredCodexSourceActiveElsewhere &&
1644
- terminalControl.capabilities.includes("send_keys") &&
1645
- terminalControl.capabilities.includes("screen_status"));
1646
- const deferredCodexForegroundToken = deferredCodexForegroundEligible &&
1647
- deferredCodexSource &&
1648
- terminalControl &&
1649
- codexProcessUuid &&
1650
- codexProcessBirth &&
1651
- codexWorkspace &&
1652
- deferredCodexDispatchSnapshot
1653
- ? deferredCodexForegroundBindingToken({
1654
- terminalId: String(terminal.id),
1655
- terminalControl,
1656
- pid: Number(terminal.pid),
1657
- workspace: codexWorkspace,
1658
- processUuid: codexProcessUuid,
1659
- processBirth: codexProcessBirth,
1660
- sourceSession: deferredCodexSource,
1661
- dispatchSnapshot: deferredCodexDispatchSnapshot,
1662
- sourceTurnHistory: deferredCodexAuthority?.sourceTurnHistory,
1663
- sourceRolloutAuthority: deferredCodexSourceRolloutAuthority,
1664
- sourceAbandonmentFingerprint: deferredCodexAuthority?.sourceAbandonmentFingerprint,
1665
- ...(deferredCodexCandidateInventory
1666
- ? { candidateInventory: deferredCodexCandidateInventory }
1667
- : {})
1668
- })
1669
- : undefined;
1670
- return {
1671
- ...observation,
1672
- deferredCodexSourceRolloutAuthority,
1673
- deferredCodexForegroundToken
1674
- };
1675
- }
1676
- function observeTerminalHandoffAuthority(terminal, context, observation) {
1677
- const { storeDir, mutationsAllowed, conversationHasNonterminalDeferredTransfer } = context;
1678
- const { automatedInputComposerReady, terminalControl, matchingSessions, unresolvedSessionClaims, discoveredOwnership, rawActions, soleBindingConflict, conflictingSessionRevision, conflictingSessionTurns, expectedTerminalToken, externalHandoffNativeThreadId, externalHandoffTarget, externalHandoffSnapshotToken, blockingHandoffTurns, terminalBlockingTurns, handoffSourceBlockingTurns } = observation;
1679
- const reconcileBindingAction = mutationsAllowed &&
1680
- discoveredOwnership.state === "none" &&
1681
- unresolvedSessionClaims.length === 0 &&
1682
- matchingSessions.length === 0 &&
1683
- soleBindingConflict &&
1684
- soleBindingConflict.kind !== "unverifiable" &&
1685
- Number.isSafeInteger(conflictingSessionRevision) &&
1686
- conflictingSessionRevision > 0 &&
1687
- expectedTerminalToken &&
1688
- terminal.activity_state === "idle" &&
1689
- !(isRecord(terminal.approval_state) &&
1690
- terminal.approval_state.blocked === true) &&
1691
- !conflictingSessionTurns.some((turn) => isSessionSendBlockingStatus(turn.status)) &&
1692
- terminalBlockingTurns.length === 0 &&
1693
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
1694
- ? projectReconcileBindingAction({
1695
- terminalId: stringValue(terminal.id),
1696
- conflictingSession: soleBindingConflict.session,
1697
- conflictingSessionRevision,
1698
- expectedTerminalToken
1699
- })
1700
- : undefined;
1701
- const externalHandoffAdoptable = Boolean(mutationsAllowed &&
1702
- discoveredOwnership.state === "none" &&
1703
- unresolvedSessionClaims.length === 0 &&
1704
- soleBindingConflict?.kind === "live_external_thread_change" &&
1705
- terminal.activity_state === "idle" &&
1706
- automatedInputComposerReady === true &&
1707
- !(isRecord(terminal.approval_state) &&
1708
- terminal.approval_state.blocked === true) &&
1709
- !conflictingSessionTurns.some((turn) => isSessionSendBlockingStatus(turn.status)) &&
1710
- terminalBlockingTurns.length === 0 &&
1711
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
1712
- externalHandoffTarget?.status === "eligible" &&
1713
- isRecord(rawActions.send) &&
1714
- Boolean(externalHandoffSnapshotToken));
1715
- const handoffDecisionTurn = mutationsAllowed &&
1716
- soleBindingConflict?.kind === "live_external_thread_change" &&
1717
- externalHandoffTarget?.status === "eligible" &&
1718
- terminal.activity_state === "idle" &&
1719
- !(isRecord(terminal.approval_state) &&
1720
- terminal.approval_state.blocked === true) &&
1721
- !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
1722
- blockingHandoffTurns.length === 1 &&
1723
- terminalBlockingTurns.every((turn) => turn.conversation_id === blockingHandoffTurns[0].conversation_id)
1724
- ? blockingHandoffTurns[0]
1725
- : undefined;
1726
- const handoffDecisionToken = handoffDecisionTurn &&
1727
- externalHandoffSnapshotToken &&
1728
- terminalControl
1729
- ? terminalListRuntime().activeTurnHandoffDecisionToken({
1730
- handoffToken: externalHandoffSnapshotToken,
1731
- turn: handoffDecisionTurn,
1732
- ledger: terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl)
1733
- })
1734
- : undefined;
1735
- const handoffDecision = handoffDecisionTurn &&
1736
- handoffDecisionToken &&
1737
- externalHandoffNativeThreadId
1738
- ? projectHandoffDecision({
1739
- sourceSessionId: soleBindingConflict?.session.session_id,
1740
- sourceTurnId: turnIdForConversation(handoffDecisionTurn),
1741
- liveNativeThreadId: externalHandoffNativeThreadId,
1742
- handoffDecisionToken,
1743
- actionTurnId: turnIdForConversation(handoffDecisionTurn)
1744
- })
1745
- : undefined;
1746
- const blockingHandoffTurnIds = new Set(handoffSourceBlockingTurns.map((turn) => turn.conversation_id));
1747
- // A blocking managed Turn always remains explicitly closable. Close releases
1748
- // AKK management only; it does not send input or stop the coding agent.
1749
- const terminalRecoveryBlockingTurns = terminalBlockingTurns;
1750
- return {
1751
- ...observation,
1752
- reconcileBindingAction,
1753
- externalHandoffAdoptable,
1754
- handoffDecision,
1755
- blockingHandoffTurnIds,
1756
- terminalRecoveryBlockingTurns
1757
- };
1758
- }
1759
- function observeTerminalScopedApprovalAuthority(terminal, context, observation) {
1760
- const { storeDir, mutationsAllowed } = context;
1761
- const { terminalControl, terminalHasNonterminalDeferredTransfer, conflictingBoundSessionClaims, unresolvedSessionClaims, sessionAuthorityConflict, authoritativeSession, discoveredOwnership, ownership, rawActions } = observation;
1762
- let terminalScopedCodexApprovalAction;
1763
- const terminalScopedCodexApprovalPrompt = terminalScopedCodexApprovalPromptSnapshot(terminal.approval_state);
1764
- if (mutationsAllowed &&
1765
- terminal.agent === "codex" &&
1766
- terminalControl &&
1767
- hasCanonicalTerminalEndpoint(terminalControl) &&
1768
- authoritativeSession &&
1769
- !sessionAuthorityConflict &&
1770
- !terminalHasNonterminalDeferredTransfer &&
1771
- unresolvedSessionClaims.length === 0 &&
1772
- conflictingBoundSessionClaims.length === 0 &&
1773
- terminalScopedCodexApprovalPrompt &&
1774
- isRecord(rawActions.approve)) {
1775
- try {
1776
- const ledger = terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl);
1777
- const boundary = ownership.state === "conflict" &&
1778
- discoveredOwnership.state === "current" &&
1779
- ledger
1780
- ? terminalScopedCodexApprovalBoundary({
1781
- storeDir,
1782
- terminal,
1783
- owner: discoveredOwnership.conversation,
1784
- session: authoritativeSession,
1785
- ledger,
1786
- approval: terminalScopedCodexApprovalPrompt
1787
- })
1788
- : ownership.state === "none" &&
1789
- discoveredOwnership.state === "none"
1790
- ? terminalScopedCodexApprovalBoundary({
1791
- storeDir,
1792
- terminal,
1793
- session: authoritativeSession,
1794
- ledger,
1795
- approval: terminalScopedCodexApprovalPrompt
1796
- })
1797
- : undefined;
1798
- if (!boundary) {
1799
- throw new Error("terminal-scoped Codex approval has no eligible managed authority");
1800
- }
1801
- terminalScopedCodexApprovalAction = {
1802
- ...rawActions.approve,
1803
- arguments: {
1804
- conversation_id: String(terminal.id),
1805
- expected_terminal_token: boundary.token
1806
- },
1807
- scope: "terminal_current_prompt",
1808
- authority: boundary.authority.kind,
1809
- managed_state_unchanged: true,
1810
- automatic_approval_eligible: false,
1811
- durable_dispatch_receipt: false,
1812
- uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
1813
- };
1814
- }
1815
- catch (error) {
1816
- runtimeLog("info", "terminal_scoped_codex_approval_not_advertised", {
1817
- terminal_id: String(terminal.id),
1818
- terminal_target: terminalControl.target,
1819
- reason: error instanceof Error ? error.message : String(error)
1820
- });
1821
- }
1822
- }
1823
- const rolloutBackedCodexSession = Boolean(authoritativeSession?.agent === "codex" &&
1824
- isCompleteNativeRollout(authoritativeSession.binding?.native_process.rollout));
1825
- return {
1826
- ...observation,
1827
- terminalScopedCodexApprovalAction,
1828
- rolloutBackedCodexSession
1829
- };
1830
- }
1831
- function observeTerminalListActionAuthority(terminal, context, binding) {
1832
- return observeTerminalScopedApprovalAuthority(terminal, context, observeTerminalHandoffAuthority(terminal, context, observeDeferredTerminalAuthority(terminal, context, observeDeferredSourceAuthority(terminal, context, observeVerifiedEmptyTerminalAuthority(terminal, context, binding)))));
1833
- }
1834
- function renderTerminalFirstListEntry(terminal, context, observation) {
1835
- const { sessionAuthorityRequired, includeAll, mutationsAllowed, conversationHasNonterminalDeferredTransfer } = context;
1836
- 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;
1837
- const association = decideManagedTerminalAssociation({
1838
- allRelated,
1839
- displayedRelated,
1840
- authoritativeSession,
1841
- sessionAuthorityRequired,
1842
- currentOwner: ownership.state === "current"
1843
- ? ownership.conversation
1844
- : undefined
1845
- });
1846
- const { managedSessionId, sessionIds, sessionAllRelated, sessionDisplayedRelated } = association;
1847
- // History remains useful when a pane has restarted and therefore no
1848
- // first-class Session is authoritative for the new process incarnation.
1849
- // Keep that display-only association separate from the send target: under
1850
- // protocol 3, only authoritativeSession may populate managedSessionId.
1851
- const sessionBindingMatchesLiveTerminal = authoritativeSession
1852
- ? true
1853
- : Boolean(!sessionAuthorityRequired &&
1854
- sessionAllRelated.some((turn) => managedTurnMatchesLiveTerminal(turn, terminal)));
1855
- const currentTurnValue = ownership.state === "current"
1856
- ? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
1857
- : undefined;
1858
- const currentTurnProjection = currentTurnValue
1859
- ? !mutationsAllowed
1860
- ? readOnlyManagedTurn(currentTurnValue)
1861
- : ownership.state === "current" &&
1862
- conversationHasNonterminalDeferredTransfer(ownership.conversation)
1863
- ? userReleasableManagedTurn(currentTurnValue)
1864
- : currentTurnValue
1865
- : undefined;
1866
- const currentTurn = currentTurnProjection
1867
- ? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
1868
- : undefined;
1869
- const nonOwnerRawActions = nonOwnerTerminalActions(sessionAwareRawActions, {
1870
- hasAuthoritativeSession: Boolean(authoritativeSession),
1871
- rolloutBackedCodexSession
1872
- });
1873
- const { recentConversation, historyConversations } = selectManagedTerminalHistory({
1874
- displayedRelated: sessionDisplayedRelated,
1875
- currentConversationId: stringValue(currentTurn?.conversation_id),
1876
- hasCurrentTurn: Boolean(currentTurn),
1877
- includeAll
1878
- });
1879
- const recentTurnValue = recentConversation
1880
- ? historicalManagedTurnForTerminal(recentConversation)
1881
- : undefined;
1882
- const recentTurnProjection = recentTurnValue
1883
- ? !mutationsAllowed
1884
- ? readOnlyManagedTurn(recentTurnValue)
1885
- : recentConversation &&
1886
- conversationHasNonterminalDeferredTransfer(recentConversation)
1887
- ? userReleasableManagedTurn(recentTurnValue)
1888
- : recentTurnValue
1889
- : undefined;
1890
- const recentTurn = recentTurnProjection
1891
- ? withoutGenericHandoffSourceClose(recentTurnProjection, blockingHandoffTurnIds)
1892
- : undefined;
1893
- const history = historyConversations.map((conversation) => {
1894
- const turn = historicalManagedTurnForTerminal(conversation);
1895
- return withoutGenericHandoffSourceClose(!mutationsAllowed
1896
- ? readOnlyManagedTurn(turn)
1897
- : conversationHasNonterminalDeferredTransfer(conversation)
1898
- ? userReleasableManagedTurn(turn)
1899
- : turn, blockingHandoffTurnIds);
1900
- });
1901
- const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
1902
- .map((turn) => stringValue(turn?.conversation_id))
1903
- .filter((id) => id !== undefined));
1904
- const managedSessionShortReference = managedSessionId
1905
- ? sessionShortRef(managedSessionId)
1906
- : null;
1907
- const management = projectTerminalManagement({
1908
- managedSessionId,
1909
- managedSessionShortRef: managedSessionShortReference,
1910
- currentTurn,
1911
- recentTurn,
1912
- sessionAllRelatedCount: sessionAllRelated.length,
1913
- hiddenTurnCount: sessionAllRelated.filter((conversation) => !visibleTurnIds.has(conversation.conversation_id)).length,
1914
- sessionCount: new Set([
1915
- ...sessionIds,
1916
- ...relatedSessions.map((session) => session.session_id)
1917
- ]).size,
1918
- authoritativeSession,
1919
- history: includeAll ? history : undefined
1920
- });
1921
- const sendAuthority = decideTerminalSendAuthority({
1922
- ownership: ownership.state,
1923
- verifiedEmptyToken: verifiedEmptyCodexSnapshotToken,
1924
- externalToken: externalHandoffAdoptable
1925
- ? externalHandoffSnapshotToken
1926
- : undefined,
1927
- deferredToken: ownership.state !== "conflict" ||
1928
- deferredCodexSourceRolloutAuthority ===
1929
- "explicitly_abandoned_predecessor"
1930
- ? deferredCodexForegroundToken
1931
- : undefined,
1932
- managedSendSessionId: managedSessionId &&
1933
- !rolloutBackedCodexSession &&
1934
- sessionBindingMatchesLiveTerminal &&
1935
- isRecord(sessionAwareRawActions.send)
1936
- ? managedSessionId
1937
- : undefined
1938
- });
1939
- const tokenSendAction = sendAuthority.mode === "external_handoff"
1940
- ? rawSendAction
1941
- : sendAuthority.mode === "verified_empty" ||
1942
- sendAuthority.mode === "deferred"
1943
- ? verifiedEmptyRawSendAction
1944
- : undefined;
1945
- const authoritativeSendAction = sendAuthority.mode === "managed" && isRecord(sessionAwareRawActions.send)
1946
- ? sendActionForManagedSession(sessionAwareRawActions.send, sendAuthority.sessionId)
1947
- : tokenSendAction &&
1948
- "token" in sendAuthority &&
1949
- sendAuthority.token
1950
- ? {
1951
- ...tokenSendAction,
1952
- arguments: {
1953
- ...(isRecord(tokenSendAction.arguments)
1954
- ? tokenSendAction.arguments
1955
- : {}),
1956
- expected_terminal_token: sendAuthority.token
1957
- }
1958
- }
1959
- : undefined;
1960
- const managedFastPathToken = "token" in sendAuthority
1961
- ? sendAuthority.token
1962
- : undefined;
1963
- const prioritizedTerminalUserExplicitSendAction = terminalUserExplicitSendAction && managedFastPathToken
1964
- ? {
1965
- ...terminalUserExplicitSendAction,
1966
- arguments: {
1967
- ...(isRecord(terminalUserExplicitSendAction.arguments)
1968
- ? terminalUserExplicitSendAction.arguments
1969
- : {}),
1970
- expected_managed_terminal_token: managedFastPathToken
1971
- }
1972
- }
1973
- : terminalUserExplicitSendAction;
1974
- const availableActions = selectTerminalAvailableActions({
1975
- ownership: ownership.state,
1976
- currentActions: ownership.state === "current"
1977
- ? currentTerminalActions(currentTurn)
1978
- : {},
1979
- sessionAwareRawActions: sessionAwareRawActions,
1980
- nonOwnerRawActions,
1981
- authoritativeSendAction,
1982
- terminalUserExplicitSendAction: prioritizedTerminalUserExplicitSendAction,
1983
- reconcileBindingAction,
1984
- terminalScopedApprovalAction: terminalScopedCodexApprovalAction,
1985
- isAction: isRecord
1986
- });
1987
- // An explicit undefined rollout prevents a lingering resolver rollout from
1988
- // being presented as the authoritative status-card thread.
1989
- const authoritativeIdentity = authoritativeTerminalIdentity(authoritativeSession);
1990
- const publicManagementConflict = ownership.state === "conflict"
1991
- ? projectPublicManagementConflict({
1992
- conflict: ownership.conflict,
1993
- verifiedEmptyToken: verifiedEmptyCodexSnapshotToken,
1994
- deferredToken: deferredCodexForegroundToken,
1995
- explicitlyAbandonedPredecessor: deferredCodexSourceRolloutAuthority ===
1996
- "explicitly_abandoned_predecessor"
1997
- })
1998
- : undefined;
1999
- const handoffPresentation = projectHandoffPresentation({
2000
- externalHandoffDetected,
2001
- externalHandoffAdoptable,
2002
- recoveryBlockingTurnCount: terminalRecoveryBlockingTurns.length,
2003
- hasHandoffDecision: Boolean(handoffDecision),
2004
- sourceBlockingTurnCount: handoffSourceBlockingTurns.length,
2005
- automatedInputComposerReady: automatedInputComposerReady === true,
2006
- verifiedEmptyToken: verifiedEmptyCodexSnapshotToken
2007
- });
2008
- return {
2009
- ...publicTerminal,
2010
- ...authoritativeIdentity,
2011
- management_state: ownership.state === "conflict"
2012
- ? "conflict"
2013
- : ownership.state === "current" || Boolean(authoritativeSession)
2014
- ? "managed"
2015
- : "unmanaged",
2016
- ...(ownership.state === "conflict"
2017
- ? { management_conflict: publicManagementConflict }
2018
- : {}),
2019
- ...handoffPresentation,
2020
- ...(handoffDecision ? { handoff_decision: handoffDecision } : {}),
2021
- ...(terminalRecoveryBlockingTurns.length > 0
2022
- ? {
2023
- blocking_turns: terminalRecoveryBlockingTurns.map((turn) => projectBlockingTurn({
2024
- sessionId: sessionIdForConversation(turn),
2025
- turnId: turnIdForConversation(turn),
2026
- status: turn.status,
2027
- recoveryTurnId: turnIdForConversation(turn)
2028
- }))
2029
- }
2030
- : {}),
2031
- managed: management,
2032
- available_actions: availableActions
2033
- };
2034
- }
2035
- function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
2036
- const nonterminalDeferredTransfers = listDeferredForegroundTransfers(storeDir).filter((transfer) => !isFinalDeferredForegroundTransferStatus(transfer.status));
2037
- const nonterminalDeferredTransferIds = new Set(nonterminalDeferredTransfers.map((transfer) => transfer.transfer_id));
2038
- const nonterminalDeferredSourceTurnIds = new Set(nonterminalDeferredTransfers.flatMap((transfer) => transfer.version === 2 &&
2039
- transfer.source_kind === "candidate_rollout_quiescent"
2040
- ? (transfer.source_turn_history ?? []).map((turn) => turn.turn_id)
2041
- : []));
2042
- const conversationHasNonterminalDeferredTransfer = (conversation) => {
2043
- if (nonterminalDeferredSourceTurnIds.has(turnIdForConversation(conversation))) {
2044
- return true;
2045
- }
2046
- const takeover = isRecord(conversation.native_session_takeover)
2047
- ? conversation.native_session_takeover
2048
- : undefined;
2049
- const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
2050
- return Boolean(transferId && nonterminalDeferredTransferIds.has(transferId));
2051
- };
2052
- const discoveredTerminalControls = terminals.flatMap((terminal) => {
2053
- const control = isRecord(terminal.terminal_control)
2054
- ? terminal.terminal_control
2055
- : undefined;
2056
- return control ? [control] : [];
2057
- });
2058
- const projectionContext = {
2059
- storeDir,
2060
- terminals,
2061
- managedSessions,
2062
- sessionAuthorityRequired,
2063
- allConversations,
2064
- displayedConversations,
2065
- includeAll,
2066
- mutationsAllowed,
2067
- nonterminalDeferredTransfers,
2068
- conversationHasNonterminalDeferredTransfer
2069
- };
2070
- const projectedTerminals = terminals.map((terminal) => {
2071
- const binding = observeTerminalListBindingAuthority(terminal, projectionContext);
2072
- return renderTerminalFirstListEntry(binding.authorityTerminal, projectionContext, observeTerminalListActionAuthority(binding.authorityTerminal, projectionContext, binding));
2073
- });
2074
- const unavailableManagedTurns = displayedConversations
2075
- .filter((conversation) => {
2076
- const managedControl = terminalControlForManagedConversation(conversation);
2077
- if (discoveredTerminalControls.some((control) => terminalControlsShareIncarnation(managedControl, control))) {
2078
- return false;
2079
- }
2080
- return (includeAll ||
2081
- managedOnly ||
2082
- statusFilter !== undefined ||
2083
- managedTurnNeedsAttention(conversation));
2084
- })
2085
- .sort(compareManagedConversationRecency)
2086
- .map((conversation) => {
2087
- const managedTurn = managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
2088
- terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
2089
- approvalState: managedListApprovalState(conversation),
2090
- conversation
2091
- });
2092
- return {
2093
- ...managedTurn,
2094
- available_actions: !mutationsAllowed
2095
- ? readOnlyListActions(isRecord(managedTurn.available_actions)
2096
- ? managedTurn.available_actions
2097
- : {})
2098
- : conversationHasNonterminalDeferredTransfer(conversation)
2099
- ? userReleaseListActions(isRecord(managedTurn.available_actions)
2100
- ? managedTurn.available_actions
2101
- : {}, turnIdForConversation(conversation))
2102
- : safeUnavailableManagedTurnActions(isRecord(managedTurn.available_actions)
2103
- ? managedTurn.available_actions
2104
- : {}),
2105
- terminal_availability: {
2106
- available: false,
2107
- reason: managedOnly
2108
- ? "terminal discovery was disabled by --managed-only"
2109
- : "the referenced terminal pane is not currently available"
2110
- }
2111
- };
2112
- });
2113
- return {
2114
- terminals: projectedTerminals,
2115
- unavailableManagedTurns
2116
- };
2117
- }
2118
1142
  function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
2119
- const binding = session.binding;
2120
- const liveControl = isRecord(terminal.terminal_control)
2121
- ? terminal.terminal_control
2122
- : undefined;
2123
- if (session.status !== "bound" ||
2124
- !binding ||
2125
- session.agent !== terminal.agent ||
2126
- binding.native_process.pid !== Number(terminal.pid)) {
2127
- return false;
2128
- }
2129
- const terminalAliasMatches = terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, liveControl);
2130
- if (!terminalAliasMatches) {
2131
- return false;
2132
- }
2133
- const workspaceMatches = terminalListRuntime().matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd);
2134
- if (!workspaceMatches) {
2135
- return false;
2136
- }
2137
- const observation = terminalObservationFromListEntry(terminal, session.agent);
2138
- const evidence = {
2139
- terminalAliasMatches,
2140
- workspaceMatches
2141
- };
2142
- let decision = decideTerminalBindingMatch(session, observation, evidence);
2143
- if (decision.state === "not_exact" &&
2144
- decision.reason === "native_identity_mismatch" &&
2145
- session.agent === "codex" &&
2146
- observation.nativeIdentity.status === "resolved") {
2147
- decision = decideTerminalBindingMatch(session, observation, {
2148
- ...evidence,
2149
- codexLingeringBeforeMatches: terminalListRuntime().codexLingeringBeforeIdentityMatchesSession({
2150
- storeDir,
2151
- session,
2152
- identity: observation.nativeIdentity.identity
2153
- })
2154
- });
2155
- }
2156
- return decision.state === "exact";
2157
- }
2158
- function managedSessionClaimsLiveTerminalEntry(session, terminal) {
2159
- const binding = session.binding;
2160
- const liveControl = isRecord(terminal.terminal_control)
2161
- ? terminal.terminal_control
2162
- : undefined;
2163
- return Boolean(binding &&
2164
- session.agent === terminal.agent &&
2165
- binding.native_process.pid === Number(terminal.pid) &&
2166
- terminalControlsShareIncarnation(binding.terminal_control, liveControl));
2167
- }
2168
- function listedTerminalProcessIncarnation(terminal) {
2169
- const processUuid = stringValue(terminal.native_agent_process_uuid);
2170
- const processBirth = stringValue(terminal.native_agent_process_birth);
2171
- if (terminal.agent !== "codex" ||
2172
- (processUuid && processBirth)) {
2173
- return { processUuid, processBirth };
2174
- }
2175
- const pid = Number(terminal.pid);
2176
- if (!Number.isSafeInteger(pid) || pid <= 1) {
2177
- return { processUuid, processBirth };
2178
- }
2179
- try {
2180
- const incarnation = terminalListRuntime().codexProcessIncarnationForPid(pid);
2181
- return {
2182
- processUuid: processUuid ?? incarnation.processUuid,
2183
- processBirth: processBirth ?? incarnation.processBirth
2184
- };
2185
- }
2186
- catch {
2187
- return { processUuid, processBirth };
2188
- }
2189
- }
2190
- function managedBindingConflictKindForLiveTerminalEntry({ storeDir, session, terminal }) {
2191
- const binding = session.binding;
2192
- if (session.status !== "bound" ||
2193
- !binding ||
2194
- !managedSessionClaimsLiveTerminalEntry(session, terminal) ||
2195
- managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir)) {
2196
- return undefined;
2197
- }
2198
- const livePid = Number(terminal.pid);
2199
- const incarnation = listedTerminalProcessIncarnation(terminal);
2200
- const relationship = processIncarnationRelationship({
2201
- binding,
2202
- livePid,
2203
- liveProcessUuid: incarnation.processUuid,
2204
- liveProcessBirth: incarnation.processBirth
2205
- });
2206
- if (relationship === "different") {
2207
- return "stale_process_incarnation";
2208
- }
2209
- if (!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, isRecord(terminal.terminal_control)
2210
- ? terminal.terminal_control
2211
- : undefined) ||
2212
- !terminalListRuntime().matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
2213
- return "unverifiable";
2214
- }
2215
- return decideManagedBindingConflict({
2216
- session,
2217
- claimsTerminal: true,
2218
- exactBinding: false,
2219
- ownerConclusivelyInactive: false,
2220
- processRelationship: relationship,
2221
- liveNativeThreadId: stringValue(terminal.native_agent_session_id),
2222
- statusCardNativeThreadId: stringValue(terminal.native_agent_status_card_session_id),
2223
- managedTurnCount: provisionalManagedBindingTurnCount(storeDir, session)
2224
- });
1143
+ return terminalListOwnershipService().managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir);
2225
1144
  }
2226
1145
  function provisionalManagedBindingTurnCount(storeDir, session) {
2227
- const binding = session.binding;
2228
- return binding && session.lineage.created_by === "attach" &&
2229
- !session.last_transition_id &&
2230
- !binding.native_thread_id &&
2231
- !binding.native_process.rollout
2232
- ? terminalListRuntime().managedTurnsForSession(storeDir, session.session_id).length
2233
- : undefined;
1146
+ return terminalListOwnershipService().provisionalManagedBindingTurnCount(storeDir, session);
2234
1147
  }
2235
1148
  function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
2236
- const root = nativeThreadTransitionsDir(storeDir);
2237
- if (!fs.existsSync(root)) {
2238
- return false;
2239
- }
2240
- let entries;
2241
- try {
2242
- entries = fs.readdirSync(root, { withFileTypes: true });
2243
- }
2244
- catch {
2245
- return true;
2246
- }
2247
- for (const entry of entries) {
2248
- if (!entry.isDirectory()) {
2249
- continue;
2250
- }
2251
- let transition;
2252
- try {
2253
- transition = loadNativeThreadTransition(storeDir, entry.name);
2254
- }
2255
- catch {
2256
- return true;
2257
- }
2258
- if (transition.source_session_id !== session.session_id &&
2259
- transition.target_session_id !== session.session_id) {
2260
- continue;
2261
- }
2262
- if (!["committed", "aborted"].includes(transition.status)) {
2263
- return true;
2264
- }
2265
- }
2266
- return false;
1149
+ return terminalListOwnershipService().managedSessionHasUnresolvedNativeTransition(storeDir, session);
2267
1150
  }
2268
1151
  function managedSessionHasAnyNativeTransition(storeDir, session) {
2269
- const root = nativeThreadTransitionsDir(storeDir);
2270
- if (!fs.existsSync(root)) {
2271
- return false;
2272
- }
2273
- let entries;
2274
- try {
2275
- entries = fs.readdirSync(root, { withFileTypes: true });
2276
- }
2277
- catch {
2278
- return true;
2279
- }
2280
- for (const entry of entries) {
2281
- if (!entry.isDirectory()) {
2282
- continue;
2283
- }
2284
- let transition;
2285
- try {
2286
- transition = loadNativeThreadTransition(storeDir, entry.name);
2287
- }
2288
- catch {
2289
- return true;
2290
- }
2291
- if (transition.source_session_id === session.session_id ||
2292
- transition.target_session_id === session.session_id) {
2293
- return true;
2294
- }
2295
- }
2296
- return false;
1152
+ return terminalListOwnershipService().managedSessionHasAnyNativeTransition(storeDir, session);
2297
1153
  }
2298
1154
  function terminalControlForManagedConversation(conversation) {
2299
- return terminalListRuntime().terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
2300
- ? conversation.native_session_takeover
2301
- : undefined);
1155
+ return terminalListOwnershipService().terminalControlForManagedConversation(conversation);
2302
1156
  }
2303
1157
  function terminalIncarnationBlockingTurns(storeDir, terminalControl, conversations = listConversations(storeDir)) {
2304
- return conversations
2305
- .filter(terminalListRuntime().isDiscoverableTmuxConversation)
2306
- .filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
2307
- isSessionSendBlockingStatus(turn.status))
2308
- .sort(compareManagedConversationRecency);
1158
+ return terminalListOwnershipService().terminalIncarnationBlockingTurns(storeDir, terminalControl, conversations);
2309
1159
  }
2310
1160
  function managedTurnNeedsAttention(conversation) {
2311
- return terminalManagedTurnNeedsAttention({
2312
- status: conversation.status,
2313
- get callbackDeliveryStatus() {
2314
- const delivery = isRecord(conversation.callback_delivery)
2315
- ? conversation.callback_delivery
2316
- : undefined;
2317
- return String(delivery?.status ?? "");
2318
- }
2319
- });
1161
+ return terminalListOwnershipService().managedTurnNeedsAttention(conversation);
2320
1162
  }
2321
1163
  function assertTerminalIncarnationCanStartTurn(storeDir, terminalControl) {
2322
- const blocker = terminalIncarnationBlockingTurns(storeDir, terminalControl)[0];
2323
- if (!blocker) {
2324
- return;
2325
- }
2326
- throw new Error(`terminal ${terminalControl.target} still has unresolved Turn ` +
2327
- `${turnIdForConversation(blocker)} (${blocker.status})`);
1164
+ return terminalListOwnershipService().assertTerminalIncarnationCanStartTurn(storeDir, terminalControl);
2328
1165
  }
2329
1166
  function terminalDispatchOwnership(terminalControl) {
2330
- let ledger;
2331
- try {
2332
- ledger = terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl);
2333
- }
2334
- catch (error) {
2335
- const decision = dispatch.decideTerminalDispatchOwnership("unreadable");
2336
- return {
2337
- state: "conflict",
2338
- conflict: {
2339
- reason: decision.code === "ledger_unreadable"
2340
- ? error instanceof Error ? error.message : String(error)
2341
- : "terminal dispatch ledger is unreadable",
2342
- recovery: "inspect the shared terminal pane before performing a side effect"
2343
- }
2344
- };
2345
- }
2346
- const ledgerAuthority = !ledger
2347
- ? "absent"
2348
- : ledger.status === "resolved"
2349
- ? "resolved"
2350
- : terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, terminalControl, {
2351
- requireProcessAnchor: false
2352
- }) && !terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, terminalControl)
2353
- ? "stale_process_incarnation"
2354
- : dispatch.isActiveTerminalDispatchStatus(String(ledger.status))
2355
- ? "active"
2356
- : "inactive_status";
2357
- let decision = dispatch.decideTerminalDispatchOwnership(ledgerAuthority);
2358
- if (decision.state === "none" || !ledger)
2359
- return { state: "none" };
2360
- const owner = terminalListRuntime().loadTerminalDispatchLedgerOwner(ledger);
2361
- const ledgerMessageId = stringValue(ledger.message_id);
2362
- const ownerAuthority = !owner
2363
- ? "unavailable"
2364
- : isTerminalDispatchOwnerReleasedStatus(owner.status)
2365
- ? "released"
2366
- : !terminalControlsShareIncarnation(terminalControlForManagedConversation(owner), terminalControl)
2367
- ? "terminal_mismatch"
2368
- : ledgerMessageId && stringValue(isRecord(owner.native_session_takeover)
2369
- ? owner.native_session_takeover.terminal_bridge_message_id
2370
- : undefined) !== ledgerMessageId
2371
- ? "generation_mismatch"
2372
- : "current";
2373
- decision = dispatch.decideTerminalDispatchOwnership(ledgerAuthority, ownerAuthority);
2374
- if (decision.state === "none")
2375
- return { state: "none" };
2376
- if (decision.state === "current" && owner) {
2377
- return { state: "current", conversation: owner };
2378
- }
2379
- const reason = decision.state === "conflict" &&
2380
- decision.code === "owner_terminal_mismatch"
2381
- ? "dispatch owner does not reference this terminal pane incarnation"
2382
- : decision.state === "conflict" &&
2383
- decision.code === "owner_generation_mismatch"
2384
- ? "dispatch generation does not match the owner state"
2385
- : "dispatch owner state is unavailable";
2386
- return {
2387
- state: "conflict",
2388
- conflict: projectTerminalDispatchConflict({
2389
- reason,
2390
- dispatchStatus: stringValue(ledger.status),
2391
- ownerConversationId: stringValue(ledger.conversation_id),
2392
- messageId: stringValue(ledger.message_id)
2393
- })
2394
- };
2395
- }
2396
- function localTerminalDispatchOwnership(ledgerOwner, localConversations, terminal) {
2397
- const localOwner = localConversations.find((conversation) => conversation.conversation_id === ledgerOwner.conversation_id &&
2398
- sameCanonicalStatePath(conversation.state_path, ledgerOwner.state_path));
2399
- return decideLocalTerminalDispatchOwnership({
2400
- ledgerOwnerId: ledgerOwner.conversation_id,
2401
- localOwner,
2402
- localOwnerMatchesLiveTerminal: localOwner
2403
- ? managedTurnMatchesLiveTerminal(localOwner, terminal)
2404
- : false
2405
- });
1167
+ return terminalListOwnershipService().terminalDispatchOwnership(terminalControl);
2406
1168
  }
2407
1169
  function terminalScopedCodexApprovalBoundary({ storeDir, terminal, session, ledger, approval, owner }) {
2408
- const terminalRecord = isRecord(terminal) ? terminal : {};
2409
- const terminalControl = isRecord(terminalRecord.terminal_control)
2410
- ? terminalRecord.terminal_control
2411
- : undefined;
2412
- const control = terminalControl;
2413
- const terminalId = String(terminalRecord.id);
2414
- const relatedBoundSessionIds = () => listManagedSessions(storeDir)
2415
- .filter((candidate) => candidate.status === "bound" &&
2416
- candidate.agent === "codex" &&
2417
- candidate.binding?.native_process.pid === Number(terminalRecord.pid) &&
2418
- terminalControlsShareIncarnation(candidate.binding?.terminal_control, terminalControl))
2419
- .map((candidate) => candidate.session_id);
2420
- const blockingTurnIds = () => terminalIncarnationBlockingTurns(storeDir, control)
2421
- .map((turn) => turnIdForConversation(turn));
2422
- const hasDeferredRecovery = () => listDeferredForegroundTransfers(storeDir).some((transfer) => !isFinalDeferredForegroundTransferStatus(transfer.status) &&
2423
- (transfer.source_session_id === session.session_id ||
2424
- transfer.target_session_id === session.session_id ||
2425
- (transfer.terminal_id === terminalId &&
2426
- terminalControlEvidenceMatches(transfer.terminal_endpoint, control))));
2427
- const commonChecks = {
2428
- relatedBoundSessionIds, blockingTurnIds,
2429
- hasNativeTransition: () => managedSessionHasAnyNativeTransition(storeDir, session),
2430
- hasDeferredRecovery,
2431
- ledgerMatchesTerminal: () => terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, control)
2432
- };
2433
- if (owner && ledger) {
2434
- return decideTerminalScopedCodexApprovalAuthority({
2435
- kind: "current_dispatch_owner", storeDir, terminal, owner, session,
2436
- ledger, approval,
2437
- checks: {
2438
- ...commonChecks,
2439
- assertDispatchOwner: () => terminalListRuntime().assertManagedTerminalDispatchOwner({
2440
- storeDir, conversation: owner, terminalControl: control, action: "approve"
2441
- }),
2442
- ownerMatchesNativeIdentity: (identity) => nativeAgentIdentityMatchesTurn(owner, identity
2443
- ? { ...identity, evidence: "terminal_scoped_approval" }
2444
- : undefined)
2445
- }
2446
- });
2447
- }
2448
- return decideTerminalScopedCodexApprovalAuthority({
2449
- kind: "managed_session_no_dispatch_owner", storeDir, terminal, session,
2450
- ledger, approval,
2451
- checks: {
2452
- ...commonChecks,
2453
- dispatchOwnershipIsNone: () => terminalDispatchOwnership(control).state === "none",
2454
- hasOrphanedDispatch: () => Boolean(terminalListRuntime().orphanedTerminalDispatchForRecovery(control))
2455
- }
1170
+ return terminalListOwnershipService().terminalScopedCodexApprovalBoundary({
1171
+ storeDir,
1172
+ terminal,
1173
+ session,
1174
+ ledger,
1175
+ approval,
1176
+ owner
2456
1177
  });
2457
1178
  }
2458
1179
  async function resolveTerminalScopedCodexApproval({ options, terminal, approvalSnapshot }) {
@@ -2634,90 +1355,6 @@ async function resolveTerminalScopedCodexApproval({ options, terminal, approvalS
2634
1355
  };
2635
1356
  }
2636
1357
  }
2637
- function historicalManagedTurnForTerminal(conversation) {
2638
- return renderHistoricalManagedTurn(managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
2639
- terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
2640
- approvalState: managedListApprovalState(conversation),
2641
- conversation
2642
- }));
2643
- }
2644
- function managedTurnMatchesLiveTerminal(conversation, terminal) {
2645
- const takeover = isRecord(conversation.native_session_takeover)
2646
- ? conversation.native_session_takeover
2647
- : undefined;
2648
- const liveControl = isRecord(terminal.terminal_control)
2649
- ? terminal.terminal_control
2650
- : undefined;
2651
- const storedControl = terminalListRuntime().terminalControlFromTakeover(takeover);
2652
- const livePid = Number(terminal.pid);
2653
- const storedPid = Number(takeover?.terminal_agent_pid);
2654
- if (executorForConversation(conversation).kind !== terminal.agent ||
2655
- !Number.isSafeInteger(livePid) ||
2656
- livePid <= 1 ||
2657
- storedPid !== livePid ||
2658
- !terminalControlAliasMatches(stringValue(takeover?.native_session_id), storedControl, stringValue(terminal.id), liveControl) ||
2659
- !terminalControlsShareIncarnation(storedControl, liveControl)) {
2660
- return false;
2661
- }
2662
- const liveSessionId = stringValue(terminal.native_agent_session_id);
2663
- const liveProcessUuid = stringValue(terminal.native_agent_process_uuid);
2664
- const liveProcessBirth = stringValue(terminal.native_agent_process_birth);
2665
- const liveRollout = isRecord(terminal.native_agent_rollout)
2666
- ? terminal.native_agent_rollout
2667
- : undefined;
2668
- const liveNativeIdentity = liveSessionId
2669
- ? {
2670
- sessionId: liveSessionId,
2671
- ...(liveProcessUuid ? { processUuid: liveProcessUuid } : {}),
2672
- ...(liveProcessBirth ? { processBirth: liveProcessBirth } : {}),
2673
- ...(liveRollout
2674
- ? {
2675
- rollout: {
2676
- fd: String(liveRollout.fd ?? ""),
2677
- device: String(liveRollout.device ?? ""),
2678
- inode: String(liveRollout.inode ?? ""),
2679
- path: String(liveRollout.path ?? "")
2680
- }
2681
- }
2682
- : {}),
2683
- evidence: "live_terminal"
2684
- }
2685
- : undefined;
2686
- if (!nativeAgentIdentityMatchesTurn(conversation, liveNativeIdentity)) {
2687
- return false;
2688
- }
2689
- const liveWorkspace = terminal.workspace ?? terminal.cwd;
2690
- if (!terminalListRuntime().matchesConfiguredWorkspace(conversation.workspace, liveWorkspace)) {
2691
- return false;
2692
- }
2693
- const livePanePath = liveControl?.currentPath;
2694
- if (livePanePath !== undefined &&
2695
- !terminalListRuntime().matchesConfiguredWorkspace(conversation.workspace, livePanePath)) {
2696
- return false;
2697
- }
2698
- return true;
2699
- }
2700
- function currentManagedTurnForTerminal(conversation, terminal, rawTerminalActions) {
2701
- const managedTurn = managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
2702
- terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
2703
- approvalState: managedListApprovalState(conversation),
2704
- conversation
2705
- });
2706
- const rawApproval = isRecord(rawTerminalActions.approve)
2707
- ? rawTerminalActions.approve
2708
- : undefined;
2709
- if (!rawApproval || executorForConversation(conversation).kind !== "codex") {
2710
- return managedTurn;
2711
- }
2712
- return renderCurrentManagedTurn(managedTurn, {
2713
- isCodex: true,
2714
- ownerId: conversation.conversation_id,
2715
- rawApproval,
2716
- terminalApprovalState: () => isRecord(terminal.approval_state)
2717
- ? terminal.approval_state
2718
- : undefined
2719
- });
2720
- }
2721
1358
  async function listStateForTerminal(agent, terminalControl, options, bridge = terminalListRuntime().createTerminalAgentBridge(options), runtime) {
2722
1359
  if (options.noApprovalScan) {
2723
1360
  return {
@@ -2769,39 +1406,6 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = te
2769
1406
  };
2770
1407
  }
2771
1408
  }
2772
- function managedListApprovalState(conversation) {
2773
- if (!terminalListRuntime().terminalBridgeEnabled(conversation) ||
2774
- !["waiting_for_agent", "waiting_for_openclaw"].includes(String(conversation.status))) {
2775
- return undefined;
2776
- }
2777
- const nativeTakeover = isRecord(conversation.native_session_takeover)
2778
- ? conversation.native_session_takeover
2779
- : undefined;
2780
- if (!terminalListRuntime().terminalControlFromTakeover(nativeTakeover) ||
2781
- !stringValue(nativeTakeover?.terminal_bridge_message_id)) {
2782
- return undefined;
2783
- }
2784
- const approval = isRecord(nativeTakeover?.terminal_bridge_approval)
2785
- ? nativeTakeover.terminal_bridge_approval
2786
- : undefined;
2787
- const approvalState = isRecord(approval?.approval_state)
2788
- ? approval.approval_state
2789
- : undefined;
2790
- const fingerprint = stringValue(approval?.fingerprint);
2791
- const notifiedAt = stringValue(approval?.notified_at);
2792
- const notifiedAtMs = validTimestampMs(notifiedAt);
2793
- if (!approvalState ||
2794
- !fingerprint ||
2795
- notifiedAtMs === undefined ||
2796
- cliNowMs() - notifiedAtMs > terminalListRuntime().approvalTtlMs) {
2797
- return undefined;
2798
- }
2799
- return {
2800
- ...approvalState,
2801
- fingerprint,
2802
- notified_at: notifiedAt
2803
- };
2804
- }
2805
1409
  async function resolveConversationSelectorOption(commandName, options) {
2806
1410
  const sendOperation = commandName === "send";
2807
1411
  // Submission retry is already bound to one authoritative managed Turn.
@@ -2972,12 +1576,11 @@ async function sessionSelectorCandidates(commandName, options) {
2972
1576
  .filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace));
2973
1577
  const discoverableWorkspaceConversations = workspaceConversations
2974
1578
  .filter(terminalListRuntime().isDiscoverableTmuxConversation);
2975
- const managed = discoverableWorkspaceConversations.map((conversation) => managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
1579
+ const managed = discoverableWorkspaceConversations.map((conversation) => terminalListProjectionService().managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
2976
1580
  terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
2977
- approvalState: managedListApprovalState(conversation),
2978
1581
  conversation
2979
1582
  }));
2980
- const terminalProjection = terminalFirstListProjection({
1583
+ const terminalProjection = terminalListProjectionService().projectTerminalFirstList({
2981
1584
  storeDir,
2982
1585
  terminals: terminalScan.terminalControlled.filter((entry) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)),
2983
1586
  managedSessions: selectorStore.readable