tailii-host 0.1.0 → 0.3.0

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 (348) hide show
  1. package/README.md +35 -14
  2. package/dist/{broker.d.ts → approval/broker.d.ts} +9 -5
  3. package/dist/{broker.js → approval/broker.js} +42 -6
  4. package/dist/approval/broker.js.map +1 -0
  5. package/dist/{hook.d.ts → approval/hook.d.ts} +48 -7
  6. package/dist/{hook.js → approval/hook.js} +359 -50
  7. package/dist/approval/hook.js.map +1 -0
  8. package/dist/backend/herdr.d.ts +226 -0
  9. package/dist/backend/herdr.js +749 -0
  10. package/dist/backend/herdr.js.map +1 -0
  11. package/dist/backend/sessionBackend.d.ts +81 -0
  12. package/dist/backend/sessionBackend.js +114 -0
  13. package/dist/backend/sessionBackend.js.map +1 -0
  14. package/dist/backend/tmux.d.ts +125 -0
  15. package/dist/backend/tmux.js +392 -0
  16. package/dist/backend/tmux.js.map +1 -0
  17. package/dist/{chatTailController.d.ts → chat/chatTailController.d.ts} +29 -6
  18. package/dist/{chatTailController.js → chat/chatTailController.js} +94 -18
  19. package/dist/chat/chatTailController.js.map +1 -0
  20. package/dist/{imageService.d.ts → chat/imageService.d.ts} +12 -1
  21. package/dist/{imageService.js → chat/imageService.js} +39 -11
  22. package/dist/chat/imageService.js.map +1 -0
  23. package/dist/chat/remoteQuestionMonitor.d.ts +27 -0
  24. package/dist/chat/remoteQuestionMonitor.js +128 -0
  25. package/dist/chat/remoteQuestionMonitor.js.map +1 -0
  26. package/dist/chat/subagentTailer.d.ts +30 -0
  27. package/dist/chat/subagentTailer.js +704 -0
  28. package/dist/chat/subagentTailer.js.map +1 -0
  29. package/dist/chat/subagentTranscript.d.ts +20 -0
  30. package/dist/chat/subagentTranscript.js +273 -0
  31. package/dist/chat/subagentTranscript.js.map +1 -0
  32. package/dist/{transcriptTailer.d.ts → chat/transcriptTailer.d.ts} +12 -1
  33. package/dist/{transcriptTailer.js → chat/transcriptTailer.js} +183 -10
  34. package/dist/chat/transcriptTailer.js.map +1 -0
  35. package/dist/cli.js +15 -10
  36. package/dist/cli.js.map +1 -1
  37. package/dist/codex/codexAccountUsage.d.ts +22 -0
  38. package/dist/codex/codexAccountUsage.js +128 -0
  39. package/dist/codex/codexAccountUsage.js.map +1 -0
  40. package/dist/codex/codexAppServer.d.ts +166 -0
  41. package/dist/codex/codexAppServer.js +1297 -0
  42. package/dist/codex/codexAppServer.js.map +1 -0
  43. package/dist/codex/codexNativeTurnController.d.ts +153 -0
  44. package/dist/codex/codexNativeTurnController.js +673 -0
  45. package/dist/codex/codexNativeTurnController.js.map +1 -0
  46. package/dist/codex/codexRolloutTailer.d.ts +100 -0
  47. package/dist/codex/codexRolloutTailer.js +568 -0
  48. package/dist/codex/codexRolloutTailer.js.map +1 -0
  49. package/dist/{codexSessionStore.d.ts → codex/codexSessionStore.d.ts} +10 -2
  50. package/dist/{codexSessionStore.js → codex/codexSessionStore.js} +184 -15
  51. package/dist/codex/codexSessionStore.js.map +1 -0
  52. package/dist/codex/codexSubagentTracker.d.ts +21 -0
  53. package/dist/codex/codexSubagentTracker.js +281 -0
  54. package/dist/codex/codexSubagentTracker.js.map +1 -0
  55. package/dist/codex/codexToolActivity.d.ts +69 -0
  56. package/dist/codex/codexToolActivity.js +425 -0
  57. package/dist/codex/codexToolActivity.js.map +1 -0
  58. package/dist/{codexUsage.d.ts → codex/codexUsage.d.ts} +1 -1
  59. package/dist/{codexUsage.js → codex/codexUsage.js} +19 -26
  60. package/dist/codex/codexUsage.js.map +1 -0
  61. package/dist/commands/doctor.d.ts +65 -0
  62. package/dist/commands/doctor.js +425 -0
  63. package/dist/commands/doctor.js.map +1 -0
  64. package/dist/{hookSettings.d.ts → commands/hookSettings.d.ts} +14 -1
  65. package/dist/{hookSettings.js → commands/hookSettings.js} +96 -12
  66. package/dist/commands/hookSettings.js.map +1 -0
  67. package/dist/{kick.js → commands/kick.js} +16 -1
  68. package/dist/commands/kick.js.map +1 -0
  69. package/dist/{launch.d.ts → commands/launch.d.ts} +67 -5
  70. package/dist/commands/launch.js +716 -0
  71. package/dist/commands/launch.js.map +1 -0
  72. package/dist/commands/legacyHomeMigration.js.map +1 -0
  73. package/dist/{pairingCode.d.ts → commands/pairingCode.d.ts} +29 -2
  74. package/dist/{pairingCode.js → commands/pairingCode.js} +120 -11
  75. package/dist/commands/pairingCode.js.map +1 -0
  76. package/dist/{pushTokenCommand.d.ts → commands/pushTokenCommand.d.ts} +1 -1
  77. package/dist/{pushTokenCommand.js → commands/pushTokenCommand.js} +2 -2
  78. package/dist/commands/pushTokenCommand.js.map +1 -0
  79. package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
  80. package/dist/{setup.js → commands/setup.js} +133 -75
  81. package/dist/commands/setup.js.map +1 -0
  82. package/dist/engine/context.d.ts +145 -0
  83. package/dist/engine/context.js +184 -0
  84. package/dist/engine/context.js.map +1 -0
  85. package/dist/engine/engine.d.ts +121 -0
  86. package/dist/engine/engine.js +799 -0
  87. package/dist/engine/engine.js.map +1 -0
  88. package/dist/engine/handlers/accountUsage.d.ts +53 -0
  89. package/dist/engine/handlers/accountUsage.js +203 -0
  90. package/dist/engine/handlers/accountUsage.js.map +1 -0
  91. package/dist/engine/handlers/claudeModels.d.ts +2 -0
  92. package/dist/engine/handlers/claudeModels.js +23 -0
  93. package/dist/engine/handlers/claudeModels.js.map +1 -0
  94. package/dist/engine/handlers/codex.d.ts +2 -0
  95. package/dist/engine/handlers/codex.js +97 -0
  96. package/dist/engine/handlers/codex.js.map +1 -0
  97. package/dist/engine/handlers/conversation.d.ts +2 -0
  98. package/dist/engine/handlers/conversation.js +259 -0
  99. package/dist/engine/handlers/conversation.js.map +1 -0
  100. package/dist/engine/handlers/core.d.ts +2 -0
  101. package/dist/engine/handlers/core.js +66 -0
  102. package/dist/engine/handlers/core.js.map +1 -0
  103. package/dist/engine/handlers/git.d.ts +2 -0
  104. package/dist/engine/handlers/git.js +127 -0
  105. package/dist/engine/handlers/git.js.map +1 -0
  106. package/dist/engine/handlers/index.d.ts +2 -0
  107. package/dist/engine/handlers/index.js +32 -0
  108. package/dist/engine/handlers/index.js.map +1 -0
  109. package/dist/engine/handlers/mode.d.ts +2 -0
  110. package/dist/engine/handlers/mode.js +194 -0
  111. package/dist/engine/handlers/mode.js.map +1 -0
  112. package/dist/engine/handlers/officialApps.d.ts +2 -0
  113. package/dist/engine/handlers/officialApps.js +79 -0
  114. package/dist/engine/handlers/officialApps.js.map +1 -0
  115. package/dist/engine/handlers/preview.d.ts +2 -0
  116. package/dist/engine/handlers/preview.js +59 -0
  117. package/dist/engine/handlers/preview.js.map +1 -0
  118. package/dist/engine/handlers/session.d.ts +2 -0
  119. package/dist/engine/handlers/session.js +385 -0
  120. package/dist/engine/handlers/session.js.map +1 -0
  121. package/dist/engine/handlers/usage.d.ts +2 -0
  122. package/dist/engine/handlers/usage.js +96 -0
  123. package/dist/engine/handlers/usage.js.map +1 -0
  124. package/dist/engine/handlers/workspace.d.ts +2 -0
  125. package/dist/engine/handlers/workspace.js +110 -0
  126. package/dist/engine/handlers/workspace.js.map +1 -0
  127. package/dist/engine/slashCommands.d.ts +3 -0
  128. package/dist/engine/slashCommands.js +211 -0
  129. package/dist/engine/slashCommands.js.map +1 -0
  130. package/dist/hub/engineRelaySocket.d.ts +58 -0
  131. package/dist/hub/engineRelaySocket.js +161 -0
  132. package/dist/hub/engineRelaySocket.js.map +1 -0
  133. package/dist/hub/hubClient.d.ts +28 -0
  134. package/dist/hub/hubClient.js +187 -0
  135. package/dist/hub/hubClient.js.map +1 -0
  136. package/dist/hub/hubDaemon.d.ts +69 -0
  137. package/dist/hub/hubDaemon.js +466 -0
  138. package/dist/hub/hubDaemon.js.map +1 -0
  139. package/dist/hub/hubProtocol.d.ts +161 -0
  140. package/dist/hub/hubProtocol.js +379 -0
  141. package/dist/hub/hubProtocol.js.map +1 -0
  142. package/dist/hub/panePreviewPump.d.ts +65 -0
  143. package/dist/hub/panePreviewPump.js +187 -0
  144. package/dist/hub/panePreviewPump.js.map +1 -0
  145. package/dist/hub/questionInjection.d.ts +19 -0
  146. package/dist/hub/questionInjection.js +126 -0
  147. package/dist/hub/questionInjection.js.map +1 -0
  148. package/dist/hub/reaper.d.ts +74 -0
  149. package/dist/hub/reaper.js +333 -0
  150. package/dist/hub/reaper.js.map +1 -0
  151. package/dist/hub/sessionHub.d.ts +203 -0
  152. package/dist/hub/sessionHub.js +1862 -0
  153. package/dist/hub/sessionHub.js.map +1 -0
  154. package/dist/protocol/common.d.ts +18 -0
  155. package/dist/protocol/common.js +78 -0
  156. package/dist/protocol/common.js.map +1 -0
  157. package/dist/protocol/decode.d.ts +3 -0
  158. package/dist/protocol/decode.js +1036 -0
  159. package/dist/protocol/decode.js.map +1 -0
  160. package/dist/protocol/encode.d.ts +3 -0
  161. package/dist/protocol/encode.js +84 -0
  162. package/dist/protocol/encode.js.map +1 -0
  163. package/dist/protocol/messages.d.ts +1002 -0
  164. package/dist/protocol/messages.js +8 -0
  165. package/dist/protocol/messages.js.map +1 -0
  166. package/dist/protocol.d.ts +4 -329
  167. package/dist/protocol.js +13 -531
  168. package/dist/protocol.js.map +1 -1
  169. package/dist/push/apnsConfigStore.js.map +1 -0
  170. package/dist/push/apnsJwtProvider.js.map +1 -0
  171. package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
  172. package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
  173. package/dist/push/approvalPushNotifier.js.map +1 -0
  174. package/dist/push/deviceTokenStore.js.map +1 -0
  175. package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
  176. package/dist/push/pushTypes.js.map +1 -0
  177. package/dist/push/sendLog.js.map +1 -0
  178. package/dist/services/accountIdentity.d.ts +44 -0
  179. package/dist/services/accountIdentity.js +184 -0
  180. package/dist/services/accountIdentity.js.map +1 -0
  181. package/dist/services/claudeCredentialRefresh.d.ts +30 -0
  182. package/dist/services/claudeCredentialRefresh.js +345 -0
  183. package/dist/services/claudeCredentialRefresh.js.map +1 -0
  184. package/dist/services/claudeModelCatalog.d.ts +21 -0
  185. package/dist/services/claudeModelCatalog.js +181 -0
  186. package/dist/services/claudeModelCatalog.js.map +1 -0
  187. package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
  188. package/dist/services/dirLister.js +169 -0
  189. package/dist/services/dirLister.js.map +1 -0
  190. package/dist/services/fileService.d.ts +10 -0
  191. package/dist/services/fileService.js +123 -0
  192. package/dist/services/fileService.js.map +1 -0
  193. package/dist/services/gitService.d.ts +79 -0
  194. package/dist/services/gitService.js +648 -0
  195. package/dist/services/gitService.js.map +1 -0
  196. package/dist/services/hostVersions.d.ts +38 -0
  197. package/dist/services/hostVersions.js +120 -0
  198. package/dist/services/hostVersions.js.map +1 -0
  199. package/dist/services/officialApps.d.ts +148 -0
  200. package/dist/services/officialApps.js +1079 -0
  201. package/dist/services/officialApps.js.map +1 -0
  202. package/dist/services/planUsageFetcher.d.ts +119 -0
  203. package/dist/services/planUsageFetcher.js +357 -0
  204. package/dist/services/planUsageFetcher.js.map +1 -0
  205. package/dist/services/previewServer.d.ts +31 -0
  206. package/dist/services/previewServer.js +233 -0
  207. package/dist/services/previewServer.js.map +1 -0
  208. package/dist/services/quicGateway.d.ts +143 -0
  209. package/dist/services/quicGateway.js +292 -0
  210. package/dist/services/quicGateway.js.map +1 -0
  211. package/dist/services/serveService.d.ts +50 -0
  212. package/dist/services/serveService.js +302 -0
  213. package/dist/services/serveService.js.map +1 -0
  214. package/dist/services/usageAggregator.js.map +1 -0
  215. package/dist/sessions/claudeSessionStore.d.ts +80 -0
  216. package/dist/sessions/claudeSessionStore.js +477 -0
  217. package/dist/sessions/claudeSessionStore.js.map +1 -0
  218. package/dist/sessions/heartbeat.d.ts +24 -0
  219. package/dist/sessions/heartbeat.js +86 -0
  220. package/dist/sessions/heartbeat.js.map +1 -0
  221. package/dist/sessions/liveSessionJoin.d.ts +20 -0
  222. package/dist/sessions/liveSessionJoin.js +80 -0
  223. package/dist/sessions/liveSessionJoin.js.map +1 -0
  224. package/dist/sessions/sessionActivityProvider.d.ts +14 -0
  225. package/dist/sessions/sessionActivityProvider.js +57 -0
  226. package/dist/sessions/sessionActivityProvider.js.map +1 -0
  227. package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
  228. package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
  229. package/dist/sessions/sessionListService.js.map +1 -0
  230. package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
  231. package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
  232. package/dist/sessions/sessionMetadataStore.js.map +1 -0
  233. package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
  234. package/dist/sessions/sessionSearch.js.map +1 -0
  235. package/dist/shared/hostDisplayName.d.ts +11 -0
  236. package/dist/shared/hostDisplayName.js +50 -0
  237. package/dist/shared/hostDisplayName.js.map +1 -0
  238. package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
  239. package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
  240. package/dist/shared/lineWriter.js.map +1 -0
  241. package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
  242. package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
  243. package/dist/{paths.js → shared/paths.js} +9 -0
  244. package/dist/shared/paths.js.map +1 -0
  245. package/dist/shared/permissionMode.d.ts +10 -0
  246. package/dist/shared/permissionMode.js +51 -0
  247. package/dist/shared/permissionMode.js.map +1 -0
  248. package/dist/shared/skillInjection.d.ts +9 -0
  249. package/dist/shared/skillInjection.js +16 -0
  250. package/dist/shared/skillInjection.js.map +1 -0
  251. package/dist/shared/sleep.js.map +1 -0
  252. package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
  253. package/dist/{socketPath.js → shared/socketPath.js} +22 -0
  254. package/dist/shared/socketPath.js.map +1 -0
  255. package/dist/shared/version.js.map +1 -0
  256. package/package.json +13 -7
  257. package/dist/apnsConfigStore.js.map +0 -1
  258. package/dist/apnsJwtProvider.js.map +0 -1
  259. package/dist/approvalPushNotifier.js.map +0 -1
  260. package/dist/broker.js.map +0 -1
  261. package/dist/chatTailController.js.map +0 -1
  262. package/dist/claudeSessionStore.d.ts +0 -15
  263. package/dist/claudeSessionStore.js +0 -201
  264. package/dist/claudeSessionStore.js.map +0 -1
  265. package/dist/codexRolloutTailer.d.ts +0 -56
  266. package/dist/codexRolloutTailer.js +0 -303
  267. package/dist/codexRolloutTailer.js.map +0 -1
  268. package/dist/codexSessionStore.js.map +0 -1
  269. package/dist/codexUsage.js.map +0 -1
  270. package/dist/deviceTokenStore.js.map +0 -1
  271. package/dist/dirLister.js +0 -98
  272. package/dist/dirLister.js.map +0 -1
  273. package/dist/doctor.d.ts +0 -36
  274. package/dist/doctor.js +0 -162
  275. package/dist/doctor.js.map +0 -1
  276. package/dist/engine.d.ts +0 -85
  277. package/dist/engine.js +0 -962
  278. package/dist/engine.js.map +0 -1
  279. package/dist/hook.js.map +0 -1
  280. package/dist/hookSettings.js.map +0 -1
  281. package/dist/idleReaper.d.ts +0 -13
  282. package/dist/idleReaper.js +0 -30
  283. package/dist/idleReaper.js.map +0 -1
  284. package/dist/imageService.js.map +0 -1
  285. package/dist/kick.js.map +0 -1
  286. package/dist/launch.js +0 -396
  287. package/dist/launch.js.map +0 -1
  288. package/dist/legacyHomeMigration.js.map +0 -1
  289. package/dist/lineWriter.js.map +0 -1
  290. package/dist/mdnsAdvertiser.d.ts +0 -46
  291. package/dist/mdnsAdvertiser.js +0 -268
  292. package/dist/mdnsAdvertiser.js.map +0 -1
  293. package/dist/pairingCode.js.map +0 -1
  294. package/dist/paths.js.map +0 -1
  295. package/dist/permissionMode.d.ts +0 -8
  296. package/dist/permissionMode.js +0 -32
  297. package/dist/permissionMode.js.map +0 -1
  298. package/dist/planUsageFetcher.d.ts +0 -35
  299. package/dist/planUsageFetcher.js +0 -172
  300. package/dist/planUsageFetcher.js.map +0 -1
  301. package/dist/pushTokenCommand.js.map +0 -1
  302. package/dist/pushTypes.js.map +0 -1
  303. package/dist/sendLog.js.map +0 -1
  304. package/dist/sessionActivityProvider.d.ts +0 -7
  305. package/dist/sessionActivityProvider.js +0 -44
  306. package/dist/sessionActivityProvider.js.map +0 -1
  307. package/dist/sessionIdleTracker.d.ts +0 -19
  308. package/dist/sessionIdleTracker.js +0 -42
  309. package/dist/sessionIdleTracker.js.map +0 -1
  310. package/dist/sessionListService.js.map +0 -1
  311. package/dist/sessionMetadataStore.js.map +0 -1
  312. package/dist/sessionSearch.js.map +0 -1
  313. package/dist/setup.js.map +0 -1
  314. package/dist/sleep.js.map +0 -1
  315. package/dist/socketPath.js.map +0 -1
  316. package/dist/subagentTailer.d.ts +0 -15
  317. package/dist/subagentTailer.js +0 -399
  318. package/dist/subagentTailer.js.map +0 -1
  319. package/dist/tmux.d.ts +0 -62
  320. package/dist/tmux.js +0 -163
  321. package/dist/tmux.js.map +0 -1
  322. package/dist/transcriptTailer.js.map +0 -1
  323. package/dist/usageAggregator.js.map +0 -1
  324. package/dist/version.js.map +0 -1
  325. /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
  326. /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
  327. /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
  328. /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
  329. /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
  330. /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
  331. /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
  332. /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
  333. /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
  334. /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
  335. /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
  336. /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
  337. /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
  338. /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
  339. /package/dist/{sendLog.js → push/sendLog.js} +0 -0
  340. /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
  341. /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
  342. /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
  343. /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
  344. /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
  345. /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
  346. /package/dist/{sleep.js → shared/sleep.js} +0 -0
  347. /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
  348. /package/dist/{version.js → shared/version.js} +0 -0
@@ -0,0 +1,673 @@
1
+ // Codex App Server の長寿命 thread 接続、native approval、turn/start を Tailii へ結線する。
2
+ import * as net from "node:net";
3
+ import { decodeControlMessage, encodeControlMessage, PROTOCOL_V1, PROTOCOL_V2, } from "../protocol.js";
4
+ import { resolveSocketPath } from "../shared/socketPath.js";
5
+ import { sleep } from "../shared/sleep.js";
6
+ import { codexItemToolActivities, codexPlanUpdateActivity, toolActivityContentKey, toolActivityMessage, } from "./codexToolActivity.js";
7
+ import { CodexSubagentTracker } from "./codexSubagentTracker.js";
8
+ const TITLE_GENERATION_MAX_ATTEMPTS = 3;
9
+ const TITLE_GENERATION_RETRY_BASE_MS = 250;
10
+ function isDefinitiveSteerRejection(error) {
11
+ if (!(error instanceof Error))
12
+ return false;
13
+ const message = error.message.toLowerCase();
14
+ return [
15
+ "no active turn to steer",
16
+ "expected active turn id",
17
+ "cannot steer a review turn",
18
+ "cannot steer a compact turn",
19
+ "active turn not steerable",
20
+ "turn already completed",
21
+ "turn has already completed",
22
+ ].some((marker) => message.includes(marker));
23
+ }
24
+ /** App Server の競合エラーから、その時点で実際に active だった turn ID を取り出す。 */
25
+ function activeTurnIdFromMismatch(error) {
26
+ if (!(error instanceof Error))
27
+ return null;
28
+ const match = error.message.match(/expected active turn id [A-Za-z0-9_-]+ but found ([A-Za-z0-9_-]+)/i);
29
+ return match?.[1] ?? null;
30
+ }
31
+ /** Codex thread ID はUUIDv7。先頭48bitのUnixミリ秒を履歴snapshotの安定時刻に使う。 */
32
+ function codexThreadIdTimestampMs(threadId) {
33
+ const compact = threadId.replaceAll("-", "");
34
+ if (!/^[0-9a-fA-F]{32}$/.test(compact) || compact[12]?.toLowerCase() !== "7")
35
+ return null;
36
+ const value = Number.parseInt(compact.slice(0, 12), 16);
37
+ return Number.isSafeInteger(value) && value > 0 ? value : null;
38
+ }
39
+ /**
40
+ * Tailii から開始した Codex turn を同じ App Server 接続で保持する。
41
+ * server-initiated approval は既存 per-session serve socket へ渡すため、Codex hook は不要。
42
+ */
43
+ export class CodexNativeTurnController {
44
+ appServer;
45
+ approvalBroker;
46
+ onProcessing;
47
+ onModel;
48
+ onTokenUsage;
49
+ onQuestion;
50
+ onQuestionDismiss;
51
+ onChatItem;
52
+ onDisconnect;
53
+ onThreadTitle;
54
+ open = new Map();
55
+ pendingUserInput = new Map();
56
+ constructor(options) {
57
+ this.appServer = options.appServer;
58
+ this.approvalBroker = options.approvalBroker ?? requestCodexApprovalViaBroker;
59
+ this.onProcessing = options.onProcessing ?? (() => { });
60
+ this.onModel = options.onModel ?? (() => { });
61
+ this.onTokenUsage = options.onTokenUsage ?? (() => { });
62
+ this.onQuestion = options.onQuestion ?? (() => { });
63
+ this.onQuestionDismiss = options.onQuestionDismiss ?? (() => { });
64
+ this.onChatItem = options.onChatItem ?? (() => { });
65
+ this.onDisconnect = options.onDisconnect ?? (() => { });
66
+ this.onThreadTitle = options.onThreadTitle ?? (() => { });
67
+ }
68
+ async subscribeSession(options) {
69
+ const opened = await this.threadFor(options.session, options.threadId, options.cwd);
70
+ const itemIds = new Set();
71
+ const contentCounts = new Map();
72
+ for (const item of opened.thread.initialItems ?? []) {
73
+ const id = item["id"];
74
+ if (typeof id === "string" && id.length > 0)
75
+ itemIds.add(id);
76
+ const payload = codexItemToChatOutput(item);
77
+ const key = payload === null ? null : chatContentKey(payload);
78
+ if (key !== null)
79
+ contentCounts.set(key, (contentCounts.get(key) ?? 0) + 1);
80
+ }
81
+ return {
82
+ itemIds,
83
+ contentCounts,
84
+ liveSubscribed: opened.thread.liveSubscriptionReady !== false,
85
+ };
86
+ }
87
+ async startTurn(options) {
88
+ const opened = await this.threadFor(options.session, options.threadId, options.cwd);
89
+ this.onProcessing(options.session, "active");
90
+ try {
91
+ // 未materialize fallback 接続は、別 client の実行中 turn を初期 snapshot で
92
+ // 復元できない。新規 turn/start の前に rollout を読み直し、既存 turn が
93
+ // materialize 済みなら steer へ戻す。undefined(まだ未生成)だけは従来どおり開始する。
94
+ if (opened.activeTurnId === null && opened.thread.liveSubscriptionReady === false) {
95
+ const refreshedTurnId = await opened.thread.readActiveTurnId?.();
96
+ if (refreshedTurnId !== undefined)
97
+ opened.activeTurnId = refreshedTurnId;
98
+ }
99
+ const activeTurnId = opened.activeTurnId;
100
+ if (activeTurnId !== null) {
101
+ try {
102
+ await opened.thread.steerTurn(activeTurnId, options.text, options.clientUserMessageId);
103
+ this.generateThreadTitle(opened, options.session, options.text);
104
+ return activeTurnId;
105
+ }
106
+ catch (error) {
107
+ // 接続切替などでローカル ID だけが古い場合、App Server は未受理を明示しつつ
108
+ // 実際の active ID を返す。同じ入力をその turn へ一度だけ steer し直せる。
109
+ const currentTurnId = activeTurnIdFromMismatch(error);
110
+ if (currentTurnId !== null && currentTurnId !== activeTurnId) {
111
+ opened.activeTurnId = currentTurnId;
112
+ await opened.thread.steerTurn(currentTurnId, options.text, options.clientUserMessageId);
113
+ this.generateThreadTitle(opened, options.session, options.text);
114
+ return currentTurnId;
115
+ }
116
+ // App Serverが明示的に「このturnへはsteerできない」と拒否した場合だけ、新規turnへ
117
+ // 切り替える。timeout/切断はsteer受理済みか不明なので、turn/startすると二重実行に
118
+ // なり得る。到達不明は上位へ返し、clientUserMessageId receiptで後から確定させる。
119
+ if (!isDefinitiveSteerRejection(error))
120
+ throw error;
121
+ }
122
+ }
123
+ const turnId = await opened.thread.startTurn(options.text, options.clientUserMessageId, options.effort, options.sandbox, options.approvalPolicy);
124
+ opened.activeTurnId = turnId;
125
+ this.generateThreadTitle(opened, options.session, options.text);
126
+ return turnId;
127
+ }
128
+ catch (error) {
129
+ this.onProcessing(options.session, "done");
130
+ throw error;
131
+ }
132
+ }
133
+ generateThreadTitle(opened, session, prompt) {
134
+ if (!opened.titleGenerationPending || this.appServer.generateThreadTitle === undefined)
135
+ return;
136
+ opened.titleGenerationPending = false;
137
+ void this.generateThreadTitleWithRetry({
138
+ threadId: opened.threadId,
139
+ cwd: opened.cwd,
140
+ prompt,
141
+ }).then(({ result, attempts }) => this.onThreadTitle({
142
+ session,
143
+ threadId: opened.threadId,
144
+ title: result.title,
145
+ source: result.source,
146
+ attempts,
147
+ error: null,
148
+ }), (error) => this.onThreadTitle({
149
+ session,
150
+ threadId: opened.threadId,
151
+ title: null,
152
+ source: null,
153
+ attempts: TITLE_GENERATION_MAX_ATTEMPTS,
154
+ error: String(error),
155
+ }));
156
+ }
157
+ async generateThreadTitleWithRetry(options) {
158
+ const generate = this.appServer.generateThreadTitle;
159
+ if (generate === undefined) {
160
+ return { result: { title: null, source: null }, attempts: 0 };
161
+ }
162
+ let lastError = new Error("Codex title generation did not run");
163
+ for (let attempt = 1; attempt <= TITLE_GENERATION_MAX_ATTEMPTS; attempt += 1) {
164
+ try {
165
+ return { result: await generate.call(this.appServer, options), attempts: attempt };
166
+ }
167
+ catch (error) {
168
+ lastError = error;
169
+ if (attempt < TITLE_GENERATION_MAX_ATTEMPTS) {
170
+ await sleep(TITLE_GENERATION_RETRY_BASE_MS * attempt);
171
+ }
172
+ }
173
+ }
174
+ throw new Error(`Codex title generation failed after ${TITLE_GENERATION_MAX_ATTEMPTS} attempts: ${String(lastError)}`);
175
+ }
176
+ async interruptTurn(session) {
177
+ const opened = this.open.get(session);
178
+ if (opened === undefined)
179
+ return;
180
+ if (opened.activeTurnId === null && opened.thread.liveSubscriptionReady === false) {
181
+ const refreshedTurnId = await opened.thread.readActiveTurnId?.();
182
+ if (refreshedTurnId !== undefined)
183
+ opened.activeTurnId = refreshedTurnId;
184
+ }
185
+ const activeTurnId = opened.activeTurnId;
186
+ if (activeTurnId === null)
187
+ return;
188
+ try {
189
+ await opened.thread.interruptTurn(activeTurnId);
190
+ }
191
+ catch (error) {
192
+ // 中断要求は「このセッションの現在の turn を止める」という利用者操作なので、
193
+ // App Server が返した実 ID へ同期して一度だけ再試行する。二度目の失敗は伝播する。
194
+ const currentTurnId = activeTurnIdFromMismatch(error);
195
+ if (currentTurnId === null || currentTurnId === activeTurnId)
196
+ throw error;
197
+ opened.activeTurnId = currentTurnId;
198
+ await opened.thread.interruptTurn(currentTurnId);
199
+ }
200
+ }
201
+ reconcileCompletedTurn(session, turnId) {
202
+ const opened = this.open.get(session);
203
+ if (opened === undefined || opened.activeTurnId !== turnId)
204
+ return false;
205
+ opened.activeTurnId = null;
206
+ this.onProcessing(session, "done");
207
+ this.resolvePendingQuestionsForSession(session);
208
+ return true;
209
+ }
210
+ closeSession(session) {
211
+ const opened = this.open.get(session);
212
+ if (opened === undefined)
213
+ return;
214
+ this.open.delete(session);
215
+ opened.thread.close();
216
+ this.resolvePendingQuestionsForSession(session);
217
+ this.onProcessing(session, "done");
218
+ }
219
+ close() {
220
+ for (const session of [...this.open.keys()])
221
+ this.closeSession(session);
222
+ }
223
+ /** iOS の既存 QuestionPromptSheet 回答を native requestUserInput response へ戻す。 */
224
+ answerQuestion(id, answers) {
225
+ const pending = this.pendingUserInput.get(id);
226
+ if (pending === undefined)
227
+ return false;
228
+ this.pendingUserInput.delete(id);
229
+ const wireAnswers = {};
230
+ for (const answer of answers) {
231
+ const question = pending.questions[answer.questionIndex];
232
+ if (question === undefined)
233
+ continue;
234
+ const questionId = question["id"];
235
+ if (typeof questionId !== "string")
236
+ continue;
237
+ const rawOptions = Array.isArray(question["options"])
238
+ ? question["options"].map(asRecord).filter((value) => value !== null)
239
+ : [];
240
+ const values = answer.selectedOptionIndexes.flatMap((index) => {
241
+ const label = rawOptions[index]?.["label"];
242
+ return typeof label === "string" ? [label] : [];
243
+ });
244
+ const other = answer.otherText?.trim();
245
+ if (other)
246
+ values.push(other);
247
+ wireAnswers[questionId] = { answers: values };
248
+ }
249
+ pending.resolve({ answers: wireAnswers });
250
+ this.onQuestionDismiss(pending.session, id);
251
+ return true;
252
+ }
253
+ async threadFor(session, threadId, cwd) {
254
+ const existing = this.open.get(session);
255
+ if (existing?.threadId === threadId)
256
+ return existing;
257
+ if (existing !== undefined)
258
+ this.closeSession(session);
259
+ const items = new Map();
260
+ const bufferedNotifications = [];
261
+ let notificationTargetReady = false;
262
+ const thread = await this.appServer.openThread({
263
+ threadId,
264
+ cwd,
265
+ onNotification: (notification) => {
266
+ if (!notificationTargetReady) {
267
+ bufferedNotifications.push(notification);
268
+ return;
269
+ }
270
+ this.handleNotification(session, threadId, items, notification);
271
+ },
272
+ onServerRequest: (request) => this.handleServerRequest(session, cwd, items, request),
273
+ onDisconnect: (error) => {
274
+ const current = this.open.get(session);
275
+ if (current?.threadId !== threadId)
276
+ return;
277
+ this.open.delete(session);
278
+ this.resolvePendingQuestionsForSession(session);
279
+ this.onProcessing(session, "done");
280
+ this.onDisconnect(session, error);
281
+ },
282
+ });
283
+ const opened = {
284
+ threadId,
285
+ cwd,
286
+ thread,
287
+ items,
288
+ activeTurnId: thread.initialActiveTurnId ?? null,
289
+ // 名前と最初の user prompt は生成直前の thread/read を権威にする。これにより
290
+ // TUI attach / resume の競合で initialItems が先に埋まっても、新規会話の命名を落とさない。
291
+ titleGenerationPending: true,
292
+ planSeq: 0,
293
+ subagents: new CodexSubagentTracker(threadId),
294
+ subagentSeq: 0,
295
+ };
296
+ this.open.set(session, opened);
297
+ if (opened.activeTurnId !== null)
298
+ this.onProcessing(session, "active");
299
+ const restoredSubagents = [];
300
+ for (const item of thread.initialItems ?? []) {
301
+ restoredSubagents.push(...opened.subagents.ingestItem(item, Date.now()));
302
+ }
303
+ // thread/resume の親履歴は subAgentActivity.started だけを返し、子の完了イベントを
304
+ // 省略する版がある。現在の親 turn が active でも過去の子は既に idle になり得るため、
305
+ // 子 thread 自身の status を権威にする。読めない子だけは親も idle の場合に終端補正する。
306
+ const restoredNodeIds = [...new Set(restoredSubagents.map((node) => node.nodeId))];
307
+ const unresolvedNodeIds = new Set(restoredNodeIds);
308
+ if (thread.readThreadStatus !== undefined) {
309
+ const statuses = await Promise.all(restoredNodeIds.map(async (nodeId) => {
310
+ try {
311
+ return { nodeId, status: await thread.readThreadStatus(nodeId) };
312
+ }
313
+ catch {
314
+ return { nodeId, status: undefined };
315
+ }
316
+ }));
317
+ // status 読み取り中に切断・別threadへの切替が起きた場合、古いsnapshotを配信しない。
318
+ if (this.open.get(session) !== opened)
319
+ return opened;
320
+ for (const { nodeId, status: snapshot } of statuses) {
321
+ if (snapshot === undefined)
322
+ continue;
323
+ unresolvedNodeIds.delete(nodeId);
324
+ restoredSubagents.push(...opened.subagents.ingestThreadSnapshot(nodeId, snapshot.status, snapshot.timestampMs ?? Date.now()));
325
+ }
326
+ }
327
+ if (opened.activeTurnId === null) {
328
+ for (const nodeId of unresolvedNodeIds) {
329
+ restoredSubagents.push(...opened.subagents.ingestThreadSnapshot(nodeId, { type: "idle" }, codexThreadIdTimestampMs(nodeId) ?? Date.now()));
330
+ }
331
+ }
332
+ const latestRestoredSubagents = new Map(restoredSubagents.map((node) => [node.nodeId, node]));
333
+ this.publishSubagentNodes(session, opened, [...latestRestoredSubagents.values()]);
334
+ // snapshot の構築中に届いたlive通知は、snapshot送出後に適用して必ず新しい状態を勝たせる。
335
+ notificationTargetReady = true;
336
+ for (const notification of bufferedNotifications) {
337
+ this.handleNotification(session, threadId, items, notification);
338
+ }
339
+ return opened;
340
+ }
341
+ handleNotification(session, threadId, items, notification) {
342
+ const params = asRecord(notification.params);
343
+ const notificationThreadId = params?.["threadId"];
344
+ // App Server が接続をまたいで通知する版でも、別 thread の lifecycle が
345
+ // このセッションの activeTurnId と processing 状態を上書きしないようにする。
346
+ // item は subagent thread 由来も表示対象になり得るため、ここでは一括破棄しない。
347
+ const lifecycleMatchesThread = typeof notificationThreadId !== "string" || notificationThreadId === threadId;
348
+ const current = this.open.get(session);
349
+ if (current?.threadId === threadId && notification.method === "thread/started") {
350
+ const startedThread = asRecord(params?.["thread"]);
351
+ if (startedThread !== null) {
352
+ this.publishSubagentNodes(session, current, current.subagents.ingestThreadStarted(startedThread, Date.now()));
353
+ }
354
+ }
355
+ if (current?.threadId === threadId && notification.method === "thread/status/changed") {
356
+ const changedThreadId = params?.["threadId"];
357
+ if (typeof changedThreadId === "string") {
358
+ this.publishSubagentNodes(session, current, current.subagents.ingestThreadStatus(changedThreadId, params?.["status"], Date.now()));
359
+ }
360
+ }
361
+ if (current?.threadId === threadId &&
362
+ (notification.method === "thread/closed" || notification.method === "thread/archived")) {
363
+ const closedThreadId = params?.["threadId"];
364
+ if (typeof closedThreadId === "string") {
365
+ this.publishSubagentNodes(session, current, current.subagents.ingestThreadClosed(closedThreadId, Date.now()));
366
+ }
367
+ }
368
+ if (notification.method === "item/started" || notification.method === "item/completed") {
369
+ const item = asRecord(params?.["item"]);
370
+ const id = item?.["id"];
371
+ if (item !== null && typeof id === "string")
372
+ items.set(id, item);
373
+ if (current?.threadId === threadId && item !== null) {
374
+ this.publishSubagentNodes(session, current, current.subagents.ingestItem(item, Date.now()));
375
+ }
376
+ if (notification.method === "item/completed" && item !== null && typeof id === "string") {
377
+ const payload = codexItemToChatOutput(item);
378
+ if (payload !== null)
379
+ this.onChatItem({ session, itemId: id, payload });
380
+ // コマンド実行 / ファイル変更は tool_activity カードとして別 itemId で流す
381
+ // (同一 item から chat 本文と tool カードの両方が出ることは無いが、dedup 集合を分ける)。
382
+ codexItemToolActivities(item).forEach((activity, index) => {
383
+ this.onChatItem({
384
+ session,
385
+ itemId: `${id}#tool-${index}`,
386
+ payload: toolActivityMessage(activity),
387
+ });
388
+ });
389
+ }
390
+ }
391
+ if (notification.method === "turn/plan/updated" && params !== null) {
392
+ const current = this.open.get(session);
393
+ if (current?.threadId === threadId) {
394
+ const turnId = typeof params["turnId"] === "string" ? params["turnId"] : "turn";
395
+ const itemId = `plan:${turnId}:${current.planSeq}`;
396
+ const activity = codexPlanUpdateActivity(itemId, params);
397
+ if (activity !== null) {
398
+ current.planSeq += 1;
399
+ this.onChatItem({ session, itemId, payload: toolActivityMessage(activity) });
400
+ }
401
+ }
402
+ }
403
+ if (notification.method === "turn/started" && lifecycleMatchesThread) {
404
+ const current = this.open.get(session);
405
+ const startedTurn = asRecord(params?.["turn"])?.["id"];
406
+ if (current?.threadId === threadId && typeof startedTurn === "string" && startedTurn.length > 0) {
407
+ current.activeTurnId = startedTurn;
408
+ this.onProcessing(session, "active");
409
+ }
410
+ }
411
+ if (notification.method === "turn/completed" && lifecycleMatchesThread) {
412
+ const current = this.open.get(session);
413
+ const completedTurn = asRecord(params?.["turn"])?.["id"];
414
+ if (current?.threadId === threadId &&
415
+ (typeof completedTurn !== "string" ||
416
+ current.activeTurnId === null || current.activeTurnId === completedTurn)) {
417
+ const turnStatus = asRecord(params?.["turn"])?.["status"];
418
+ this.publishSubagentNodes(session, current, current.subagents.settleRunning(turnStatus === "failed" ? "error" : "completed", Date.now()));
419
+ current.activeTurnId = null;
420
+ this.onProcessing(session, "done");
421
+ this.resolvePendingQuestionsForSession(session);
422
+ }
423
+ }
424
+ if (notification.method === "thread/settings/updated") {
425
+ const settings = asRecord(params?.["threadSettings"]);
426
+ const model = settings?.["model"];
427
+ if (typeof model === "string" && model.length > 0)
428
+ this.onModel(session, model);
429
+ }
430
+ if (notification.method === "model/rerouted" && params?.["threadId"] === threadId) {
431
+ const model = params["toModel"];
432
+ if (typeof model === "string" && model.length > 0)
433
+ this.onModel(session, model);
434
+ }
435
+ if (notification.method === "thread/tokenUsage/updated") {
436
+ const tokenUsage = asRecord(params?.["tokenUsage"]);
437
+ const last = asRecord(tokenUsage?.["last"]);
438
+ const contextTokens = nonNegativeInteger(last?.["totalTokens"]);
439
+ if (contextTokens !== null) {
440
+ this.onTokenUsage(session, contextTokens, positiveInteger(tokenUsage?.["modelContextWindow"]));
441
+ }
442
+ }
443
+ }
444
+ publishSubagentNodes(session, opened, nodes) {
445
+ for (const node of nodes) {
446
+ const itemId = `subagent:${node.nodeId}:${opened.subagentSeq}`;
447
+ opened.subagentSeq += 1;
448
+ this.onChatItem({
449
+ session,
450
+ itemId,
451
+ payload: { type: "subagent_node", v: PROTOCOL_V2, node },
452
+ });
453
+ }
454
+ }
455
+ async handleServerRequest(session, fallbackCwd, items, request) {
456
+ if (request.method === "item/commandExecution/requestApproval" ||
457
+ request.method === "item/fileChange/requestApproval") {
458
+ const params = asRecord(request.params) ?? {};
459
+ const itemId = typeof params["itemId"] === "string" ? params["itemId"] : "unknown";
460
+ const item = items.get(itemId);
461
+ const command = typeof params["command"] === "string" ? params["command"] : null;
462
+ const reason = typeof params["reason"] === "string" ? params["reason"] : null;
463
+ const cwd = typeof params["cwd"] === "string" ? params["cwd"] : fallbackCwd;
464
+ const isCommand = request.method === "item/commandExecution/requestApproval";
465
+ const summary = isCommand
466
+ ? (command ?? reason ?? "コマンドの実行を許可しますか?")
467
+ : fileChangeSummary(item, reason);
468
+ const decision = await this.approvalBroker({
469
+ id: `codex:${String(params["threadId"] ?? "thread")}:${String(request.id)}`,
470
+ session,
471
+ tool: isCommand ? "Bash" : "Edit",
472
+ summary,
473
+ cwd,
474
+ });
475
+ return { decision: decision === "allow" ? "accept" : "decline" };
476
+ }
477
+ if (request.method === "item/tool/requestUserInput") {
478
+ const params = asRecord(request.params) ?? {};
479
+ const threadId = typeof params["threadId"] === "string" ? params["threadId"] : "thread";
480
+ const rawQuestions = Array.isArray(params["questions"])
481
+ ? params["questions"].map(asRecord).filter((value) => value !== null)
482
+ : [];
483
+ const id = `codex-question:${threadId}:${String(request.id)}`;
484
+ const questions = rawQuestions.map((question) => ({
485
+ header: typeof question["header"] === "string" ? question["header"] : "質問",
486
+ question: typeof question["question"] === "string" ? question["question"] : "入力してください",
487
+ multiSelect: false,
488
+ options: Array.isArray(question["options"])
489
+ ? question["options"].flatMap((option) => {
490
+ const record = asRecord(option);
491
+ const label = record?.["label"];
492
+ if (typeof label !== "string")
493
+ return [];
494
+ return [{
495
+ label,
496
+ description: typeof record?.["description"] === "string" ? record["description"] : "",
497
+ }];
498
+ })
499
+ : [],
500
+ }));
501
+ this.onQuestion({ session, id, questions });
502
+ return new Promise((resolve) => {
503
+ this.pendingUserInput.set(id, {
504
+ session,
505
+ threadId,
506
+ questions: rawQuestions,
507
+ resolve,
508
+ });
509
+ });
510
+ }
511
+ if (request.method === "currentTime/read") {
512
+ return { currentTimeAt: Math.floor(Date.now() / 1000) };
513
+ }
514
+ throw new Error(`unsupported Codex App Server request: ${request.method}`);
515
+ }
516
+ resolvePendingQuestionsForSession(session) {
517
+ for (const [id, pending] of this.pendingUserInput) {
518
+ if (pending.session !== session)
519
+ continue;
520
+ this.pendingUserInput.delete(id);
521
+ pending.resolve({ answers: {} });
522
+ this.onQuestionDismiss(session, id);
523
+ }
524
+ }
525
+ }
526
+ /** rollout の event_msg(user_message / agent_message) と同じ表示範囲へ写像する。 */
527
+ export function codexItemToChatOutput(item) {
528
+ const id = item["id"];
529
+ const type = item["type"];
530
+ if (typeof id !== "string" || id.length === 0)
531
+ return null;
532
+ if (type === "userMessage") {
533
+ const content = item["content"];
534
+ if (!Array.isArray(content))
535
+ return null;
536
+ const text = content.flatMap((part) => {
537
+ const record = asRecord(part);
538
+ return record?.["type"] === "text" && typeof record["text"] === "string"
539
+ ? [record["text"]]
540
+ : [];
541
+ }).join("\n");
542
+ if (text.length === 0)
543
+ return null;
544
+ return { type: "chat_output", v: PROTOCOL_V1, streamId: `codex-item-${id}`,
545
+ role: "user", text, eof: true };
546
+ }
547
+ if (type === "agentMessage") {
548
+ const phase = item["phase"];
549
+ if (phase !== undefined && phase !== "commentary" && phase !== "final_answer")
550
+ return null;
551
+ const text = item["text"];
552
+ if (typeof text !== "string" || text.length === 0)
553
+ return null;
554
+ return { type: "chat_output", v: PROTOCOL_V1, streamId: `codex-item-${id}`,
555
+ role: "assistant", text, eof: true };
556
+ }
557
+ return null;
558
+ }
559
+ export function chatContentKey(payload) {
560
+ // tool_activity は live(App Server item)と rollout(response_item / patch_apply_end)の
561
+ // 両系統から同じ内容で生成される。id は系統間で一致しないため内容キーで照合する。
562
+ if (payload.type === "tool_activity") {
563
+ return toolActivityContentKey(payload.activity);
564
+ }
565
+ if (payload.type !== "chat_output" || (payload.role !== "user" && payload.role !== "assistant")) {
566
+ return null;
567
+ }
568
+ return `${payload.role}\u0000${payload.text}`;
569
+ }
570
+ /** App Server approval を既存 iPhone serve channel へ流し、同じ id の決定だけを待つ。 */
571
+ export async function requestCodexApprovalViaBroker(approval, options = {}) {
572
+ const socketPath = resolveSocketPath(approval.session);
573
+ const connectDeadline = Date.now() + (options.connectTimeoutMs ?? 10_000);
574
+ let socket = null;
575
+ do {
576
+ socket = await connectSocket(socketPath);
577
+ if (socket !== null)
578
+ break;
579
+ await sleep(200);
580
+ } while (Date.now() <= connectDeadline);
581
+ if (socket === null)
582
+ return "deny";
583
+ const request = encodeControlMessage({
584
+ type: "approval_request",
585
+ v: PROTOCOL_V1,
586
+ id: approval.id,
587
+ tool: approval.tool,
588
+ summary: approval.summary,
589
+ cwd: approval.cwd,
590
+ });
591
+ return new Promise((resolve) => {
592
+ let settled = false;
593
+ let buffer = "";
594
+ const finish = (decision) => {
595
+ if (settled)
596
+ return;
597
+ settled = true;
598
+ clearTimeout(timer);
599
+ socket.destroy();
600
+ resolve(decision);
601
+ };
602
+ const timer = setTimeout(() => finish("deny"), options.decisionTimeoutMs ?? 540_000);
603
+ socket.on("data", (chunk) => {
604
+ buffer += chunk.toString("utf8");
605
+ while (true) {
606
+ const index = buffer.indexOf("\n");
607
+ if (index < 0)
608
+ break;
609
+ const line = buffer.slice(0, index).replaceAll("\r", "");
610
+ buffer = buffer.slice(index + 1);
611
+ if (!line)
612
+ continue;
613
+ try {
614
+ const message = decodeControlMessage(line);
615
+ if (message.type === "approval_decision" && message.id === approval.id) {
616
+ finish(message.decision);
617
+ return;
618
+ }
619
+ }
620
+ catch {
621
+ // channel_hello 以外の壊れた行も無視し、正しい決定を待つ。
622
+ }
623
+ }
624
+ });
625
+ socket.once("error", () => finish("deny"));
626
+ socket.once("close", () => finish("deny"));
627
+ socket.write(request + "\n");
628
+ });
629
+ }
630
+ function connectSocket(socketPath) {
631
+ return new Promise((resolve) => {
632
+ const socket = net.createConnection(socketPath);
633
+ let settled = false;
634
+ socket.once("connect", () => {
635
+ if (settled)
636
+ return;
637
+ settled = true;
638
+ resolve(socket);
639
+ });
640
+ socket.once("error", () => {
641
+ if (settled)
642
+ return;
643
+ settled = true;
644
+ socket.destroy();
645
+ resolve(null);
646
+ });
647
+ });
648
+ }
649
+ function fileChangeSummary(item, reason) {
650
+ const changes = item?.["changes"];
651
+ if (Array.isArray(changes)) {
652
+ const paths = changes
653
+ .map((change) => asRecord(change)?.["path"])
654
+ .filter((value) => typeof value === "string")
655
+ .slice(0, 4);
656
+ if (paths.length > 0)
657
+ return `ファイル変更: ${paths.join(", ")}`;
658
+ }
659
+ return reason ?? "ファイルの変更を許可しますか?";
660
+ }
661
+ function asRecord(value) {
662
+ return typeof value === "object" && value !== null && !Array.isArray(value)
663
+ ? value
664
+ : null;
665
+ }
666
+ function nonNegativeInteger(value) {
667
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
668
+ }
669
+ function positiveInteger(value) {
670
+ const integer = nonNegativeInteger(value);
671
+ return integer !== null && integer > 0 ? integer : null;
672
+ }
673
+ //# sourceMappingURL=codexNativeTurnController.js.map