pneuma-skills 2.9.2 → 2.10.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 (170) hide show
  1. package/backends/codex/cli-launcher.ts +37 -66
  2. package/backends/codex/codex-adapter.ts +298 -54
  3. package/bin/pneuma-cli-helpers.ts +10 -0
  4. package/bin/pneuma.ts +341 -141
  5. package/core/types/shared-history.ts +36 -0
  6. package/core/types/viewer-contract.ts +3 -0
  7. package/dist/assets/{AgentBubble-CMZ6nRGR.js → AgentBubble-__Eijm5H.js} +1 -1
  8. package/dist/assets/{EditorPanel-BKjcAySx.js → EditorPanel-B68JETZf.js} +1 -1
  9. package/dist/assets/Launcher-DS0ACG8v.js +181 -0
  10. package/dist/assets/{ScaffoldConfirm-x0-O4HSf.js → ScaffoldConfirm-Bi_bMQAz.js} +1 -1
  11. package/dist/assets/{TerminalPanel-CbZ-jW0T.js → TerminalPanel-BuA7Rf9o.js} +1 -1
  12. package/dist/assets/{ar-SA-G6X2FPQ2-DVe6YYLO.js → ar-SA-G6X2FPQ2-B31Xfc_E.js} +1 -1
  13. package/dist/assets/{arc-B1rNScez.js → arc-CyuNsreF.js} +1 -1
  14. package/dist/assets/{az-AZ-76LH7QW2-hjS2RsZB.js → az-AZ-76LH7QW2-CuRnl-Sw.js} +1 -1
  15. package/dist/assets/{bg-BG-XCXSNQG7-CFR_M_N2.js → bg-BG-XCXSNQG7-BvVwWw75.js} +1 -1
  16. package/dist/assets/{blockDiagram-38ab4fdb-BHHquyco.js → blockDiagram-38ab4fdb-DSws9v7M.js} +1 -1
  17. package/dist/assets/{bn-BD-2XOGV67Q-kqviDHJf.js → bn-BD-2XOGV67Q-g12DVKj5.js} +1 -1
  18. package/dist/assets/{c4Diagram-3d4e48cf-BjYeH1AJ.js → c4Diagram-3d4e48cf-ElmVreVH.js} +1 -1
  19. package/dist/assets/{ca-ES-6MX7JW3Y-DZrWV-vj.js → ca-ES-6MX7JW3Y-C7qPODeC.js} +1 -1
  20. package/dist/assets/channel-DiGiDB_L.js +1 -0
  21. package/dist/assets/{classDiagram-70f12bd4-Ce-UuQMw.js → classDiagram-70f12bd4-D8io7OAO.js} +1 -1
  22. package/dist/assets/{classDiagram-v2-f2320105-pke9s22r.js → classDiagram-v2-f2320105-Dx0FZcpH.js} +1 -1
  23. package/dist/assets/clone-XIS6rxyv.js +1 -0
  24. package/dist/assets/{createText-2e5e7dd3-ComqHWqI.js → createText-2e5e7dd3-plk4eMFK.js} +1 -1
  25. package/dist/assets/{cs-CZ-2BRQDIVT-BPSet93s.js → cs-CZ-2BRQDIVT-gZnhjLjt.js} +1 -1
  26. package/dist/assets/{da-DK-5WZEPLOC-kBC4S0XX.js → da-DK-5WZEPLOC-DH3OzKw5.js} +1 -1
  27. package/dist/assets/{de-DE-XR44H4JA-DXZWcb7v.js → de-DE-XR44H4JA-Cupdzsmc.js} +1 -1
  28. package/dist/assets/{download-D43IpEvW.js → download-DW7Va2rq.js} +1 -1
  29. package/dist/assets/{edges-e0da2a9e-j1tunLqf.js → edges-e0da2a9e-B3wCpawR.js} +1 -1
  30. package/dist/assets/{el-GR-BZB4AONW-DzEWI2pA.js → el-GR-BZB4AONW-Cx2YGl1B.js} +1 -1
  31. package/dist/assets/{erDiagram-9861fffd-C_T5c7_d.js → erDiagram-9861fffd-DZb53j60.js} +1 -1
  32. package/dist/assets/{es-ES-U4NZUMDT-C_C9YIWR.js → es-ES-U4NZUMDT-BhLuQ3hX.js} +1 -1
  33. package/dist/assets/{eu-ES-A7QVB2H4-DhHf-elD.js → eu-ES-A7QVB2H4-CCqNLi6t.js} +1 -1
  34. package/dist/assets/{fa-IR-HGAKTJCU-BaYY2OWl.js → fa-IR-HGAKTJCU-DLoACiBg.js} +1 -1
  35. package/dist/assets/{fi-FI-Z5N7JZ37-C8LCaGJm.js → fi-FI-Z5N7JZ37-CMACT2Jo.js} +1 -1
  36. package/dist/assets/{flowDb-956e92f1-D4hCuAgC.js → flowDb-956e92f1-DFYkVFcK.js} +1 -1
  37. package/dist/assets/{flowDiagram-66a62f08-BXSaIGhX.js → flowDiagram-66a62f08-DY9RC_-P.js} +1 -1
  38. package/dist/assets/flowDiagram-v2-96b9c2cf-6HAI7biC.js +1 -0
  39. package/dist/assets/{flowchart-elk-definition-4a651766-Dtvii5ag.js → flowchart-elk-definition-4a651766-DABg42Jp.js} +1 -1
  40. package/dist/assets/{fr-FR-RHASNOE6-D5lnyYip.js → fr-FR-RHASNOE6-DFyLSpAT.js} +1 -1
  41. package/dist/assets/{ganttDiagram-c361ad54-CZW5pQtt.js → ganttDiagram-c361ad54-BPqRq4-e.js} +1 -1
  42. package/dist/assets/{gitGraphDiagram-72cf32ee-BPpX5Zhn.js → gitGraphDiagram-72cf32ee-BqFi25_h.js} +1 -1
  43. package/dist/assets/{gl-ES-HMX3MZ6V-BGFbJfwN.js → gl-ES-HMX3MZ6V-PWHfuqfi.js} +1 -1
  44. package/dist/assets/{graph-DCVnljON.js → graph-COOWca2o.js} +1 -1
  45. package/dist/assets/{he-IL-6SHJWFNN-BUrDpH0Z.js → he-IL-6SHJWFNN-B0RoZp-b.js} +1 -1
  46. package/dist/assets/{hi-IN-IWLTKZ5I-DGA5P67V.js → hi-IN-IWLTKZ5I-DYxveKtS.js} +1 -1
  47. package/dist/assets/{hu-HU-A5ZG7DT2-B_ZZ0L6t.js → hu-HU-A5ZG7DT2-C483obeo.js} +1 -1
  48. package/dist/assets/{id-ID-SAP4L64H-CoPBEkck.js → id-ID-SAP4L64H-DIskCvbs.js} +1 -1
  49. package/dist/assets/{index-3862675e-B7FzOmHC.js → index-3862675e-MJppC4_3.js} +1 -1
  50. package/dist/assets/index-CW9op-ib.css +1 -0
  51. package/dist/assets/index-CdojEznK.js +1 -0
  52. package/dist/assets/{index-BjMHnCI7.js → index-CsCQmMXN.js} +1 -1
  53. package/dist/assets/{index-CCw_JpPQ.js → index-DOqJ5Cop.js} +1 -1
  54. package/dist/assets/index-DvB1CG8n.js +83 -0
  55. package/dist/assets/{index-h13dvvWW.js → index-Ogq0Y-Kp.js} +4 -4
  56. package/dist/assets/{infoDiagram-f8f76790-Czkw44zX.js → infoDiagram-f8f76790-1mwP_oYq.js} +1 -1
  57. package/dist/assets/{it-IT-JPQ66NNP-BMI1jVUV.js → it-IT-JPQ66NNP-CWrOj79b.js} +1 -1
  58. package/dist/assets/{ja-JP-DBVTYXUO-DSn8KL95.js → ja-JP-DBVTYXUO-BjB5xP6P.js} +1 -1
  59. package/dist/assets/{journeyDiagram-49397b02-D6l2GcOV.js → journeyDiagram-49397b02-s2FFQM88.js} +1 -1
  60. package/dist/assets/{kaa-6HZHGXH3-DkgEVo7W.js → kaa-6HZHGXH3-DRQ02w-P.js} +1 -1
  61. package/dist/assets/{kab-KAB-ZGHBKWFO-Dg75DA4e.js → kab-KAB-ZGHBKWFO-BIqWHIO7.js} +1 -1
  62. package/dist/assets/{kk-KZ-P5N5QNE5-DhHfX2yZ.js → kk-KZ-P5N5QNE5-BeB8Lkyi.js} +1 -1
  63. package/dist/assets/{km-KH-HSX4SM5Z-DhL7IOb4.js → km-KH-HSX4SM5Z-BPBAMdUj.js} +1 -1
  64. package/dist/assets/{ko-KR-MTYHY66A-B8LamGPN.js → ko-KR-MTYHY66A-DCDKpapW.js} +1 -1
  65. package/dist/assets/{ku-TR-6OUDTVRD-C-Ue4acf.js → ku-TR-6OUDTVRD-CVp6QVXq.js} +1 -1
  66. package/dist/assets/{layout-DBk3JhP2.js → layout-Ch_it_de.js} +1 -1
  67. package/dist/assets/{line-C2fwjjZs.js → line-BWivg3b0.js} +1 -1
  68. package/dist/assets/{lt-LT-XHIRWOB4-Cx_9I2Jp.js → lt-LT-XHIRWOB4-BENmkKFE.js} +1 -1
  69. package/dist/assets/{lv-LV-5QDEKY6T-DA1eV67H.js → lv-LV-5QDEKY6T-Blzv1Vol.js} +1 -1
  70. package/dist/assets/{mindmap-definition-fc14e90a-BJO6W_jl.js → mindmap-definition-fc14e90a-D1peR19j.js} +1 -1
  71. package/dist/assets/{mr-IN-CRQNXWMA-DD5P0SdJ.js → mr-IN-CRQNXWMA-B0N3Ayye.js} +1 -1
  72. package/dist/assets/{my-MM-5M5IBNSE-BbRyaobI.js → my-MM-5M5IBNSE-CrkOwPg6.js} +1 -1
  73. package/dist/assets/{nb-NO-T6EIAALU-BPUPMfp7.js → nb-NO-T6EIAALU-DD2piqGe.js} +1 -1
  74. package/dist/assets/{nl-NL-IS3SIHDZ-BLhlmQlY.js → nl-NL-IS3SIHDZ-evJQTIqF.js} +1 -1
  75. package/dist/assets/{nn-NO-6E72VCQL-CEvmCYDT.js → nn-NO-6E72VCQL-BIP8cljI.js} +1 -1
  76. package/dist/assets/{oc-FR-POXYY2M6-CzSpprL8.js → oc-FR-POXYY2M6-BN1d5Rtx.js} +1 -1
  77. package/dist/assets/{pa-IN-N4M65BXN-DEq6_MT9.js → pa-IN-N4M65BXN-BIe2bTHi.js} +1 -1
  78. package/dist/assets/{percentages-BXMCSKIN-D4QRkYSh.js → percentages-BXMCSKIN-Bc9S0tLg.js} +7 -7
  79. package/dist/assets/{pica-CJ7DRNvr.js → pica-Zwtvbd6o.js} +1 -1
  80. package/dist/assets/{pieDiagram-8a3498a8-CbcnHnE0.js → pieDiagram-8a3498a8-BFj8vX7l.js} +1 -1
  81. package/dist/assets/{pl-PL-T2D74RX3-DaxlnfpV.js → pl-PL-T2D74RX3-WT-OH4Vx.js} +1 -1
  82. package/dist/assets/{pneuma-mode-DgITUhq7.js → pneuma-mode-5X4Ybp9D.js} +1 -1
  83. package/dist/assets/{pneuma-mode-C9MJWf55.js → pneuma-mode-BKKOs-Nz.js} +1 -1
  84. package/dist/assets/pneuma-mode-Bw4FCf31.js +206 -0
  85. package/dist/assets/pneuma-mode-Ca9v8rvf.js +121 -0
  86. package/dist/assets/pneuma-mode-Dtx56YCa.js +17 -0
  87. package/dist/assets/pneuma-mode-RXAmMHRv.js +55 -0
  88. package/dist/assets/pneuma-mode-mVbaQ1QQ.js +10 -0
  89. package/dist/assets/{pt-BR-5N22H2LF-COlvU6eY.js → pt-BR-5N22H2LF-BUj6MuNp.js} +1 -1
  90. package/dist/assets/{pt-PT-UZXXM6DQ-Bu-UzvMu.js → pt-PT-UZXXM6DQ-BGv79upc.js} +1 -1
  91. package/dist/assets/{quadrantDiagram-120e2f19-DqZozxoa.js → quadrantDiagram-120e2f19-BeMo99TY.js} +1 -1
  92. package/dist/assets/{rasterize-34PCWURX-CfC5MoKT.js → rasterize-34PCWURX-BaZQqXEK.js} +1 -1
  93. package/dist/assets/{requirementDiagram-deff3bca-zdeZ5Zog.js → requirementDiagram-deff3bca-bu6gWiTD.js} +1 -1
  94. package/dist/assets/{ro-RO-JPDTUUEW-DNitNPKU.js → ro-RO-JPDTUUEW-CkhJhPA3.js} +1 -1
  95. package/dist/assets/{ru-RU-B4JR7IUQ-Dl-hvBe3.js → ru-RU-B4JR7IUQ-CDYvP590.js} +1 -1
  96. package/dist/assets/{sankeyDiagram-04a897e0-C0DId110.js → sankeyDiagram-04a897e0-BUANQW8A.js} +1 -1
  97. package/dist/assets/{sequenceDiagram-704730f1-CVL3EuWN.js → sequenceDiagram-704730f1-fRgSXUYf.js} +1 -1
  98. package/dist/assets/{si-LK-N5RQ5JYF-PrDOff3R.js → si-LK-N5RQ5JYF-DE4sIjlD.js} +1 -1
  99. package/dist/assets/{sk-SK-C5VTKIMK-D5cbWUua.js → sk-SK-C5VTKIMK-BLSxc3FY.js} +1 -1
  100. package/dist/assets/{sl-SI-NN7IZMDC-BadZqU8N.js → sl-SI-NN7IZMDC-Bfoj6QKN.js} +1 -1
  101. package/dist/assets/{stateDiagram-587899a1-BKiEXEIi.js → stateDiagram-587899a1-Cl58Yh8S.js} +1 -1
  102. package/dist/assets/{stateDiagram-v2-d93cdb3a-DTRzXuJv.js → stateDiagram-v2-d93cdb3a-BIt4qTgB.js} +1 -1
  103. package/dist/assets/{styles-6aaf32cf-6a0OV-Y-.js → styles-6aaf32cf-DRKwI1HK.js} +1 -1
  104. package/dist/assets/{styles-9a916d00-By4qztKe.js → styles-9a916d00-BoYeNSQs.js} +1 -1
  105. package/dist/assets/{styles-c10674c1-ntQTVRhG.js → styles-c10674c1-CZwzG4ch.js} +1 -1
  106. package/dist/assets/{subset-shared.chunk-BNYnoFpQ.js → subset-shared.chunk-DQlwBXHF.js} +1 -1
  107. package/dist/assets/{subset-worker.chunk-42iRwKgB.js → subset-worker.chunk-BVdWhVxw.js} +1 -1
  108. package/dist/assets/{sv-SE-XGPEYMSR-YOWLd5HQ.js → sv-SE-XGPEYMSR-CW5g8xRj.js} +1 -1
  109. package/dist/assets/{svgDrawCommon-08f97a94-BNShuOxL.js → svgDrawCommon-08f97a94-r7JHvos9.js} +1 -1
  110. package/dist/assets/{ta-IN-2NMHFXQM-DZ33CX4J.js → ta-IN-2NMHFXQM-rcxe7Aeh.js} +1 -1
  111. package/dist/assets/{th-TH-HPSO5L25-C__ogmor.js → th-TH-HPSO5L25-DoxgcHtt.js} +1 -1
  112. package/dist/assets/{timeline-definition-85554ec2-CnzsfCW9.js → timeline-definition-85554ec2-B1DmjfE2.js} +1 -1
  113. package/dist/assets/{toBlob-D6gas9M8.js → toBlob-JVl8QmBd.js} +1 -1
  114. package/dist/assets/{toCanvas-cdXr-2CR.js → toCanvas-Cytf3jkU.js} +1 -1
  115. package/dist/assets/{toImg-sFpicrMi.js → toImg-DMYlIQ1b.js} +1 -1
  116. package/dist/assets/{tr-TR-DEFEU3FU-6R7C_oA2.js → tr-TR-DEFEU3FU-lmn4Z-wR.js} +1 -1
  117. package/dist/assets/{uk-UA-QMV73CPH-CvolgTsC.js → uk-UA-QMV73CPH-rEOe_8mf.js} +1 -1
  118. package/dist/assets/{use-resilient-parse-EL9obkgJ.js → use-resilient-parse-cRas0DPn.js} +1 -1
  119. package/dist/assets/{vi-VN-M7AON7JQ-CTg63Crb.js → vi-VN-M7AON7JQ-mcIk0V-g.js} +1 -1
  120. package/dist/assets/{with-selector-DSJqZOiL.js → with-selector-CRZMlSge.js} +1 -1
  121. package/dist/assets/{xychartDiagram-e933f94c-C09MuqVP.js → xychartDiagram-e933f94c-DPshm1e0.js} +1 -1
  122. package/dist/assets/{zh-CN-LNUGB5OW-C0KLOptS.js → zh-CN-LNUGB5OW-BdZAXT9j.js} +1 -1
  123. package/dist/assets/{zh-HK-E62DVLB3-DTMwO0Uu.js → zh-HK-E62DVLB3-DVXSOPNN.js} +1 -1
  124. package/dist/assets/{zh-TW-RAJ6MFWO-DQvA3jgh.js → zh-TW-RAJ6MFWO-C2cX2bOn.js} +1 -1
  125. package/dist/index.html +2 -2
  126. package/modes/doc/viewer/DocPreview.tsx +12 -5
  127. package/modes/draw/viewer/DrawPreview.tsx +14 -5
  128. package/modes/illustrate/viewer/IllustratePreview.tsx +15 -8
  129. package/modes/slide/viewer/SlidePreview.tsx +16 -8
  130. package/modes/webcraft/viewer/WebPreview.tsx +17 -9
  131. package/package.json +1 -1
  132. package/server/__tests__/history-export.test.ts +76 -0
  133. package/server/__tests__/history-import.test.ts +77 -0
  134. package/server/__tests__/history-summary.test.ts +42 -0
  135. package/server/__tests__/replay-continue.test.ts +74 -0
  136. package/server/__tests__/shadow-git.test.ts +130 -0
  137. package/server/history-export.ts +173 -0
  138. package/server/history-import.ts +123 -0
  139. package/server/history-summary.ts +58 -0
  140. package/server/index.ts +428 -10
  141. package/server/replay-continue.ts +95 -0
  142. package/server/shadow-git.ts +154 -0
  143. package/server/share.ts +222 -0
  144. package/server/skill-installer.ts +42 -0
  145. package/server/ws-bridge-codex.ts +7 -0
  146. package/server/ws-bridge.ts +16 -3
  147. package/snapshot/history-share.ts +61 -0
  148. package/src/App.tsx +43 -25
  149. package/src/components/ChatPanel.tsx +15 -10
  150. package/src/components/Launcher.tsx +515 -9
  151. package/src/components/ReplayPlayer.tsx +162 -0
  152. package/src/components/TopBar.tsx +213 -6
  153. package/src/index.css +10 -0
  154. package/src/replay-engine.ts +281 -0
  155. package/src/store/index.ts +2 -0
  156. package/src/store/replay-slice.ts +72 -0
  157. package/src/store/types.ts +3 -1
  158. package/src/ws.ts +2 -0
  159. package/dist/assets/Launcher-Cjx0jp6x.js +0 -181
  160. package/dist/assets/channel-UEITcOsU.js +0 -1
  161. package/dist/assets/clone-BQRaJdPT.js +0 -1
  162. package/dist/assets/flowDiagram-v2-96b9c2cf-DUyY1urJ.js +0 -1
  163. package/dist/assets/index-C_KSF44-.js +0 -82
  164. package/dist/assets/index-CbMCOwb0.js +0 -1
  165. package/dist/assets/index-D9JIQN4c.css +0 -1
  166. package/dist/assets/pneuma-mode-4PHb7CVE.js +0 -55
  167. package/dist/assets/pneuma-mode-Br-ZcgvO.js +0 -206
  168. package/dist/assets/pneuma-mode-CvVPZhJH.js +0 -121
  169. package/dist/assets/pneuma-mode-D01AH-3X.js +0 -10
  170. package/dist/assets/pneuma-mode-LOMw9sgF.js +0 -17
@@ -8,7 +8,7 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { resolve, join, delimiter } from "node:path";
10
10
  import { existsSync, realpathSync, readFileSync } from "node:fs";
11
- import type { Subprocess } from "bun";
11
+ import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
12
12
  import { resolveBinary, getEnrichedPath } from "../../server/path-resolver.js";
13
13
  import { CodexAdapter, StdioTransport } from "./codex-adapter.js";
14
14
  import type { CodexAdapterOptions } from "./codex-adapter.js";
@@ -64,7 +64,7 @@ function isTextScript(filePath: string): boolean {
64
64
  */
65
65
  export class CodexCliLauncher {
66
66
  private sessions = new Map<string, CodexSessionInfo>();
67
- private processes = new Map<string, Subprocess>();
67
+ private nodeProcesses = new Map<string, ChildProcess>();
68
68
  private adapters = new Map<string, CodexAdapter>();
69
69
  private exitHandlers: ((sessionId: string, exitCode: number | null) => void)[] = [];
70
70
  private adapterCreatedHandlers: ((sessionId: string, adapter: CodexAdapter) => void)[] = [];
@@ -151,21 +151,26 @@ export class CodexCliLauncher {
151
151
 
152
152
  console.log(`[codex-launcher] Spawning session ${sessionId}: ${spawnCmd.join(" ")}`);
153
153
 
154
- const proc = Bun.spawn(spawnCmd, {
154
+ // Use node:child_process instead of Bun.spawn to avoid Bun's ReadableStream
155
+ // prematurely closing stdout while the process is still alive.
156
+ const cleanEnv: Record<string, string> = {};
157
+ for (const [k, v] of Object.entries(spawnEnv)) {
158
+ if (v !== undefined) cleanEnv[k] = v;
159
+ }
160
+
161
+ const nodeProc = nodeSpawn(spawnCmd[0], spawnCmd.slice(1), {
155
162
  cwd: info.cwd,
156
- env: spawnEnv,
157
- stdin: "pipe",
158
- stdout: "pipe",
159
- stderr: "pipe",
163
+ env: cleanEnv,
164
+ stdio: ["pipe", "pipe", "inherit"],
160
165
  });
161
166
 
162
- info.pid = proc.pid;
163
- this.processes.set(sessionId, proc);
167
+ info.pid = nodeProc.pid;
168
+ this.nodeProcesses.set(sessionId, nodeProc);
164
169
 
165
- // Pipe stderr for debugging
166
- this.pipeStderr(sessionId, proc);
170
+ // Create a StdioTransport from Node.js streams
171
+ const transport = StdioTransport.fromNodeStreams(nodeProc.stdin!, nodeProc.stdout!);
167
172
 
168
- // Create adapter this handles init, thread start, and all JSON-RPC communication
173
+ // Create adapter with the transport directly
169
174
  const adapterOptions: CodexAdapterOptions = {
170
175
  model: options.model,
171
176
  cwd: info.cwd,
@@ -174,16 +179,16 @@ export class CodexCliLauncher {
174
179
  threadId: options.resumeThreadId,
175
180
  killProcess: async () => {
176
181
  try {
177
- proc.kill("SIGTERM");
178
- await Promise.race([
179
- proc.exited,
180
- new Promise((r) => setTimeout(r, 5000)),
181
- ]);
182
+ nodeProc.kill("SIGTERM");
183
+ await new Promise<void>((resolve) => {
184
+ const timer = setTimeout(() => { nodeProc.kill("SIGKILL"); resolve(); }, 5000);
185
+ nodeProc.once("exit", () => { clearTimeout(timer); resolve(); });
186
+ });
182
187
  } catch {}
183
188
  },
184
189
  };
185
190
 
186
- const adapter = new CodexAdapter(proc, sessionId, adapterOptions);
191
+ const adapter = new CodexAdapter(transport, sessionId, adapterOptions);
187
192
  this.adapters.set(sessionId, adapter);
188
193
 
189
194
  // Wire adapter callbacks
@@ -211,14 +216,15 @@ export class CodexCliLauncher {
211
216
  }
212
217
 
213
218
  // Monitor process exit
214
- proc.exited.then((exitCode) => {
215
- console.log(`[codex-launcher] Session ${sessionId} exited (code=${exitCode})`);
219
+ nodeProc.once("exit", (exitCode) => {
216
220
  const session = this.sessions.get(sessionId);
221
+ const uptime = session ? Math.round((Date.now() - session.createdAt) / 1000) : 0;
222
+ console.error(`[codex-launcher] Session ${sessionId} process exited (code=${exitCode}, uptime=${uptime}s, state=${session?.state})`);
217
223
  if (session) {
218
224
  session.state = "exited";
219
225
  session.exitCode = exitCode;
220
226
  }
221
- this.processes.delete(sessionId);
227
+ this.nodeProcesses.delete(sessionId);
222
228
  this.adapters.delete(sessionId);
223
229
  for (const handler of this.exitHandlers) {
224
230
  try { handler(sessionId, exitCode); } catch {}
@@ -260,58 +266,23 @@ export class CodexCliLauncher {
260
266
  this.adapters.delete(sessionId);
261
267
  }
262
268
 
263
- const proc = this.processes.get(sessionId);
264
- if (!proc) return false;
265
-
266
- if (process.platform === "win32") {
267
- proc.kill();
268
- } else {
269
- proc.kill("SIGTERM");
270
- }
271
-
272
- const exited = await Promise.race([
273
- proc.exited.then(() => true),
274
- new Promise<false>((resolve) => setTimeout(() => resolve(false), 5_000)),
275
- ]);
269
+ const nodeProc = this.nodeProcesses.get(sessionId);
270
+ if (!nodeProc) return false;
276
271
 
277
- if (!exited) {
278
- if (process.platform === "win32") {
279
- proc.kill();
280
- } else {
281
- proc.kill("SIGKILL");
282
- }
283
- }
272
+ nodeProc.kill("SIGTERM");
273
+ await new Promise<void>((resolve) => {
274
+ const timer = setTimeout(() => { nodeProc.kill("SIGKILL"); resolve(); }, 5000);
275
+ nodeProc.once("exit", () => { clearTimeout(timer); resolve(); });
276
+ });
284
277
 
285
278
  const session = this.sessions.get(sessionId);
286
- if (session) {
287
- session.state = "exited";
288
- session.exitCode = -1;
289
- }
290
- this.processes.delete(sessionId);
279
+ if (session) { session.state = "exited"; session.exitCode = -1; }
280
+ this.nodeProcesses.delete(sessionId);
291
281
  return true;
292
282
  }
293
283
 
294
284
  async killAll(): Promise<void> {
295
- const ids = [...this.processes.keys()];
285
+ const ids = [...this.nodeProcesses.keys()];
296
286
  await Promise.all(ids.map((id) => this.kill(id)));
297
287
  }
298
-
299
- private async pipeStderr(sessionId: string, proc: Subprocess): Promise<void> {
300
- const stderr = proc.stderr;
301
- if (!stderr || typeof stderr === "number") return;
302
- const reader = (stderr as ReadableStream<Uint8Array>).getReader();
303
- const decoder = new TextDecoder();
304
- try {
305
- while (true) {
306
- const { done, value } = await reader.read();
307
- if (done) break;
308
- const text = decoder.decode(value);
309
- if (text.trim()) {
310
- console.error(`[codex:${sessionId}:stderr] ${text.trimEnd()}`);
311
- }
312
- }
313
- } catch {
314
- // stream closed
315
- }
316
- }
317
288
  }
@@ -88,28 +88,90 @@ export class StdioTransport implements ICodexTransport {
88
88
  private pendingTimers = new Map<number, ReturnType<typeof setTimeout>>();
89
89
  private notificationHandler: ((method: string, params: Record<string, unknown>) => void) | null = null;
90
90
  private requestHandler: ((method: string, id: number, params: Record<string, unknown>) => void) | null = null;
91
- private writer: WritableStreamDefaultWriter<Uint8Array>;
91
+ /** Node.js Writable stream for stdin. */
92
+ private nodeStdin: import("node:stream").Writable | null = null;
93
+ /** Fallback WritableStream writer for non-Node stdin (tests). */
94
+ private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
92
95
  private connected = true;
93
96
  private buffer = "";
94
97
 
98
+ /**
99
+ * Constructor for test usage with WritableStream/ReadableStream.
100
+ * Production code should use `StdioTransport.fromNodeStreams()`.
101
+ */
95
102
  constructor(
96
103
  stdin: WritableStream<Uint8Array> | { write(data: Uint8Array): number },
97
104
  stdout: ReadableStream<Uint8Array>,
98
105
  ) {
99
- let writable: WritableStream<Uint8Array>;
100
106
  if ("write" in stdin && typeof stdin.write === "function") {
101
- writable = new WritableStream({
107
+ const writable = new WritableStream({
102
108
  write(chunk) {
103
109
  (stdin as { write(data: Uint8Array): number }).write(chunk);
104
110
  },
105
111
  });
112
+ this.writer = writable.getWriter();
106
113
  } else {
107
- writable = stdin as WritableStream<Uint8Array>;
114
+ this.writer = (stdin as WritableStream<Uint8Array>).getWriter();
108
115
  }
109
- this.writer = writable.getWriter();
110
116
  this.readStdout(stdout);
111
117
  }
112
118
 
119
+ /**
120
+ * Create a StdioTransport from Node.js child_process streams.
121
+ * Avoids Bun's ReadableStream bug where proc.stdout prematurely closes.
122
+ */
123
+ static fromNodeStreams(
124
+ stdin: import("node:stream").Writable,
125
+ stdout: import("node:stream").Readable,
126
+ ): StdioTransport {
127
+ const transport = Object.create(StdioTransport.prototype) as StdioTransport;
128
+ transport.nextId = 1;
129
+ transport.pending = new Map();
130
+ transport.pendingTimers = new Map();
131
+ transport.notificationHandler = null;
132
+ transport.requestHandler = null;
133
+ transport.stdinSink = null;
134
+ transport.writer = null;
135
+ transport.nodeStdin = stdin;
136
+ transport.connected = true;
137
+ transport.buffer = "";
138
+ transport.readNodeStdout(stdout);
139
+ return transport;
140
+ }
141
+
142
+ private readNodeStdout(stdout: import("node:stream").Readable): void {
143
+ stdout.on("data", (chunk: Buffer) => {
144
+ this.buffer += chunk.toString("utf-8");
145
+ this.processBuffer();
146
+ });
147
+ stdout.on("end", () => {
148
+ console.error(`[codex-adapter] Node stdout stream ended`);
149
+ this.closeTransport();
150
+ });
151
+ stdout.on("error", (err) => {
152
+ console.error(`[codex-adapter] Node stdout error:`, err);
153
+ this.closeTransport();
154
+ });
155
+ }
156
+
157
+ private closeTransport(): void {
158
+ if (!this.connected) return;
159
+ const pendingCount = this.pending.size;
160
+ if (pendingCount > 0) {
161
+ console.error(`[codex-adapter] Transport closed with ${pendingCount} pending RPC call(s)`);
162
+ }
163
+ this.connected = false;
164
+ for (const [, timer] of this.pendingTimers) {
165
+ clearTimeout(timer);
166
+ }
167
+ this.pendingTimers.clear();
168
+ for (const [, { reject }] of this.pending) {
169
+ reject(new Error("Transport closed"));
170
+ }
171
+ this.pending.clear();
172
+ }
173
+
174
+ /** Read from a web ReadableStream (used by tests; production uses readNodeStdout). */
113
175
  private async readStdout(stdout: ReadableStream<Uint8Array>): Promise<void> {
114
176
  const reader = stdout.getReader();
115
177
  const decoder = new TextDecoder();
@@ -123,15 +185,7 @@ export class StdioTransport implements ICodexTransport {
123
185
  } catch (err) {
124
186
  console.error("[codex-adapter] stdout reader error:", err);
125
187
  } finally {
126
- this.connected = false;
127
- for (const [, timer] of this.pendingTimers) {
128
- clearTimeout(timer);
129
- }
130
- this.pendingTimers.clear();
131
- for (const [, { reject }] of this.pending) {
132
- reject(new Error("Transport closed"));
133
- }
134
- this.pending.clear();
188
+ this.closeTransport();
135
189
  }
136
190
  }
137
191
 
@@ -232,7 +286,18 @@ export class StdioTransport implements ICodexTransport {
232
286
  if (!this.connected) {
233
287
  throw new Error("Transport closed");
234
288
  }
235
- await this.writer.write(new TextEncoder().encode(data));
289
+ if (this.nodeStdin) {
290
+ return new Promise<void>((resolve, reject) => {
291
+ this.nodeStdin!.write(data, "utf-8", (err) => {
292
+ if (err) reject(err); else resolve();
293
+ });
294
+ });
295
+ }
296
+ if (this.writer) {
297
+ await this.writer.write(new TextEncoder().encode(data));
298
+ } else {
299
+ throw new Error("No stdin writer available");
300
+ }
236
301
  }
237
302
  }
238
303
 
@@ -460,9 +525,15 @@ export class CodexAdapter {
460
525
  capabilities: {
461
526
  experimentalApi: true,
462
527
  },
463
- }) as { serverInfo?: { name?: string; version?: string } } | undefined;
464
-
465
- const serverVersion = initResult?.serverInfo?.version || "";
528
+ }) as { serverInfo?: { name?: string; version?: string }; userAgent?: string } | undefined;
529
+
530
+ // v0.114+: response has `userAgent` instead of `serverInfo`
531
+ let serverVersion = initResult?.serverInfo?.version || "";
532
+ if (!serverVersion && initResult?.userAgent) {
533
+ // Parse version from userAgent string like "pneuma-skills/0.114.0 (...)"
534
+ const match = initResult.userAgent.match(/\/([\d.]+)/);
535
+ if (match) serverVersion = match[1];
536
+ }
466
537
 
467
538
  // Step 2: Send initialized notification
468
539
  await this.transport.notify("initialized", {});
@@ -482,14 +553,27 @@ export class CodexAdapter {
482
553
 
483
554
  try {
484
555
  if (this.options.threadId) {
485
- threadResult = await this.transport.call("thread/resume", {
486
- threadId: this.options.threadId,
487
- model: this.options.model,
488
- cwd: this.options.cwd || "",
489
- approvalPolicy: this.mapApprovalPolicy(this.currentPermissionMode),
490
- sandbox: this.mapSandboxPolicy(this.currentPermissionMode),
491
- }) as { thread: { id: string }; model?: string; model_provider?: string };
492
- this.threadId = threadResult.thread.id;
556
+ try {
557
+ threadResult = await this.transport.call("thread/resume", {
558
+ threadId: this.options.threadId,
559
+ model: this.options.model,
560
+ cwd: this.options.cwd || "",
561
+ approvalPolicy: this.mapApprovalPolicy(this.currentPermissionMode),
562
+ sandbox: this.mapSandboxPolicy(this.currentPermissionMode),
563
+ }) as { thread: { id: string }; model?: string; model_provider?: string };
564
+ this.threadId = threadResult.thread.id;
565
+ } catch (resumeErr) {
566
+ // Thread not found (e.g. rollout file cleaned up, version upgrade) — fall back to new thread
567
+ console.warn(`[codex-adapter] thread/resume failed: ${resumeErr}, falling back to thread/start`);
568
+ this.options.threadId = undefined;
569
+ threadResult = await this.transport.call("thread/start", {
570
+ model: this.options.model,
571
+ cwd: this.options.cwd || "",
572
+ approvalPolicy: this.mapApprovalPolicy(this.currentPermissionMode),
573
+ sandbox: this.mapSandboxPolicy(this.currentPermissionMode),
574
+ }) as { thread: { id: string }; model?: string; model_provider?: string };
575
+ this.threadId = threadResult.thread.id;
576
+ }
493
577
  } else {
494
578
  threadResult = await this.transport.call("thread/start", {
495
579
  model: this.options.model,
@@ -727,10 +811,19 @@ export class CodexAdapter {
727
811
  break; // We already got the thread ID from the RPC response
728
812
 
729
813
  case "thread/status/changed": {
730
- const status = params.status as string;
731
- if (status === "running") {
814
+ // v0.114+: status is an object { type: "active"|"idle"|"systemError"|"notLoaded", activeFlags?: [] }
815
+ // Legacy: status was a plain string
816
+ const rawStatus = params.status;
817
+ const statusType = typeof rawStatus === "object" && rawStatus !== null
818
+ ? (rawStatus as Record<string, unknown>).type as string
819
+ : rawStatus as string;
820
+
821
+ if (statusType === "active" || statusType === "running") {
732
822
  this.emit({ type: "status_change", status: "running" });
733
- } else if (status === "idle" || status === "completed") {
823
+ } else if (statusType === "idle" || statusType === "completed" || statusType === "notLoaded") {
824
+ this.emit({ type: "status_change", status: "idle" });
825
+ } else if (statusType === "systemError") {
826
+ this.emit({ type: "error", message: "Codex reported a system error" } as BrowserIncomingMessage);
734
827
  this.emit({ type: "status_change", status: "idle" });
735
828
  }
736
829
  // Extract model if reported in status
@@ -755,14 +848,21 @@ export class CodexAdapter {
755
848
  // Update turn count
756
849
  this.turnCount++;
757
850
 
758
- // Extract usage from turn/completed
759
- const status = params.status as string;
851
+ // v0.114+: status is in params.turn.status; legacy: params.status
852
+ const turn = params.turn as { status?: string; error?: { message?: string } } | undefined;
853
+ const status = turn?.status ?? params.status as string ?? "completed";
854
+ // Legacy: params.usage; v0.114+: usage arrives via thread/tokenUsage/updated
760
855
  const usage = params.usage as Record<string, number> | undefined;
761
856
  if (usage) {
762
857
  this.cumulativeInputTokens += usage.inputTokens ?? 0;
763
858
  this.cumulativeOutputTokens += usage.outputTokens ?? 0;
764
859
  }
765
860
 
861
+ // Surface turn errors
862
+ if (turn?.error?.message) {
863
+ this.emit({ type: "error", message: turn.error.message } as BrowserIncomingMessage);
864
+ }
865
+
766
866
  // Build a synthetic result message
767
867
  const result: CLIResultMessage = {
768
868
  type: "result",
@@ -821,6 +921,7 @@ export class CodexAdapter {
821
921
  // ── Reasoning / thinking ──
822
922
  case "item/reasoning/textDelta":
823
923
  case "item/reasoning/textSummaryDelta":
924
+ case "item/reasoning/summaryTextDelta":
824
925
  this.handleReasoningDelta(params);
825
926
  break;
826
927
 
@@ -858,7 +959,8 @@ export class CodexAdapter {
858
959
  }
859
960
 
860
961
  case "codex/event/mcp_startup_complete":
861
- // MCP servers finished loading — could fetch server list in the future
962
+ case "codex/event/mcp_startup_update":
963
+ // MCP servers loading / finished loading
862
964
  break;
863
965
 
864
966
  case "codex/event/user_message":
@@ -872,12 +974,50 @@ export class CodexAdapter {
872
974
  break;
873
975
  }
874
976
 
977
+ // v0.114+: model rerouted — update active model
978
+ case "model/rerouted": {
979
+ const toModel = params.toModel as string | undefined;
980
+ if (toModel) {
981
+ this.activeModel = toModel;
982
+ this.emitSessionUpdate({ model: toModel });
983
+ }
984
+ break;
985
+ }
986
+
987
+ // v0.114+: context compacted via notification (not just item)
988
+ case "thread/compacted":
989
+ this.emitSessionUpdate({ is_compacting: false });
990
+ break;
991
+
992
+ // v0.114+: hooks, plans, diffs, server request resolved — informational
993
+ case "hook/started":
994
+ case "hook/completed":
995
+ case "turn/diff/updated":
996
+ case "turn/plan/updated":
997
+ case "item/plan/delta":
998
+ case "serverRequest/resolved":
999
+ case "deprecationNotice":
1000
+ case "configWarning":
1001
+ case "thread/started":
1002
+ case "thread/closed":
1003
+ case "thread/archived":
1004
+ case "thread/unarchived":
1005
+ case "thread/name/updated":
1006
+ case "skills/changed":
1007
+ case "item/mcpToolCall/progress":
1008
+ // Known notifications — no action needed
1009
+ break;
1010
+
875
1011
  default:
876
1012
  // Silently ignore known event prefixes, log truly unknown ones
877
1013
  if (!method.startsWith("account/")
878
1014
  && !method.startsWith("codex/event/")
879
1015
  && !method.startsWith("rawResponseItem/")
880
- && method !== "turn/diff/updated") {
1016
+ && !method.startsWith("fuzzyFileSearch/")
1017
+ && !method.startsWith("thread/realtime/")
1018
+ && !method.startsWith("app/")
1019
+ && !method.startsWith("mcpServer/")
1020
+ && !method.startsWith("windows")) {
881
1021
  console.log(`[codex-adapter] Unhandled notification: ${method}`);
882
1022
  }
883
1023
  break;
@@ -918,12 +1058,14 @@ export class CodexAdapter {
918
1058
  const requestId = randomUUID();
919
1059
  this.pendingApprovals.set(requestId, id);
920
1060
 
921
- const serverName = params.serverName as string || "";
922
- const toolName = params.toolName as string || "";
1061
+ // v0.114+: `server`/`tool`/`arguments`; legacy: `serverName`/`toolName`/`args`
1062
+ const serverName = (params.server ?? params.serverName) as string || "";
1063
+ const toolName = (params.tool ?? params.toolName) as string || "";
1064
+ const toolArgs = (params.arguments ?? params.args) as Record<string, unknown> || {};
923
1065
  const perm: PermissionRequest = {
924
1066
  request_id: requestId,
925
1067
  tool_name: `mcp:${serverName}:${toolName}`,
926
- input: (params.args as Record<string, unknown>) || {},
1068
+ input: toolArgs,
927
1069
  description: `MCP tool: ${serverName}/${toolName}`,
928
1070
  tool_use_id: (params.itemId as string) || randomUUID(),
929
1071
  timestamp: Date.now(),
@@ -955,10 +1097,77 @@ export class CodexAdapter {
955
1097
  break;
956
1098
  }
957
1099
 
1100
+ // v0.114+: permissions approval request
1101
+ case "item/permissions/requestApproval": {
1102
+ const requestId = randomUUID();
1103
+ this.pendingApprovals.set(requestId, id);
1104
+ const reason = params.reason as string || "Permission request";
1105
+ const permissions = params.permissions as Record<string, unknown> || {};
1106
+ const perm: PermissionRequest = {
1107
+ request_id: requestId,
1108
+ tool_name: "Permissions",
1109
+ input: permissions,
1110
+ description: reason,
1111
+ tool_use_id: (params.itemId as string) || randomUUID(),
1112
+ timestamp: Date.now(),
1113
+ };
1114
+ this.emit({ type: "permission_request", request: perm });
1115
+ break;
1116
+ }
1117
+
1118
+ // v0.114+: tool requests user input — treat as permission request
1119
+ case "item/tool/requestUserInput": {
1120
+ const requestId = randomUUID();
1121
+ this.pendingApprovals.set(requestId, id);
1122
+ const questions = params.questions as Array<{ text?: string }> | undefined;
1123
+ const desc = questions?.map((q) => q.text).filter(Boolean).join("; ") || "Tool requests input";
1124
+ const perm: PermissionRequest = {
1125
+ request_id: requestId,
1126
+ tool_name: "UserInput",
1127
+ input: { questions: questions || [] },
1128
+ description: desc,
1129
+ tool_use_id: (params.itemId as string) || randomUUID(),
1130
+ timestamp: Date.now(),
1131
+ };
1132
+ this.emit({ type: "permission_request", request: perm });
1133
+ break;
1134
+ }
1135
+
1136
+ // v0.114+: MCP server elicitation
1137
+ case "mcpServer/elicitation/request": {
1138
+ const requestId = randomUUID();
1139
+ this.pendingApprovals.set(requestId, id);
1140
+ const serverName = params.serverName as string || "";
1141
+ const message = params.message as string || "MCP server elicitation";
1142
+ const perm: PermissionRequest = {
1143
+ request_id: requestId,
1144
+ tool_name: `mcp:${serverName}:elicitation`,
1145
+ input: params,
1146
+ description: message,
1147
+ tool_use_id: randomUUID(),
1148
+ timestamp: Date.now(),
1149
+ };
1150
+ this.emit({ type: "permission_request", request: perm });
1151
+ break;
1152
+ }
1153
+
1154
+ // v0.114+: dynamic tool call — execute client-side
1155
+ case "item/tool/call": {
1156
+ // We don't support client-side dynamic tools — decline gracefully
1157
+ console.log(`[codex-adapter] Dynamic tool call not supported: ${params.tool}`);
1158
+ this.transport.respond(id, { error: "Dynamic tools not supported by this client" }).catch(() => {});
1159
+ break;
1160
+ }
1161
+
1162
+ // Account token refresh — respond silently
1163
+ case "account/chatgptAuthTokens/refresh":
1164
+ this.transport.respond(id, {}).catch(() => {});
1165
+ break;
1166
+
958
1167
  default:
959
- // Unknown request — auto-accept to avoid blocking
960
- console.warn(`[codex-adapter] Unknown request method: ${method}, auto-accepting`);
961
- this.transport.respond(id, { decision: "accept" }).catch(() => {});
1168
+ // Unknown request — log and reject to avoid silently approving dangerous operations
1169
+ console.warn(`[codex-adapter] Unknown request method: ${method}, rejecting`);
1170
+ this.transport.respond(id, { decision: "decline" }).catch(() => {});
962
1171
  break;
963
1172
  }
964
1173
  }
@@ -1018,11 +1227,11 @@ export class CodexAdapter {
1018
1227
  const toolUseId = item.id;
1019
1228
  if (!this.emittedToolUseIds.has(toolUseId)) {
1020
1229
  this.emittedToolUseIds.add(toolUseId);
1021
- const serverName = item.serverName as string || "";
1022
- const toolName = item.toolName as string || "";
1023
- this.emitToolUse(toolUseId, `mcp:${serverName}:${toolName}`, {
1024
- ...(item.args as Record<string, unknown> || {}),
1025
- });
1230
+ // v0.114+: `server`/`tool`/`arguments`; legacy: `serverName`/`toolName`/`args`
1231
+ const serverName = (item.server ?? item.serverName) as string || "";
1232
+ const toolName = (item.tool ?? item.toolName) as string || "";
1233
+ const toolArgs = (item.arguments ?? item.args) as Record<string, unknown> || {};
1234
+ this.emitToolUse(toolUseId, `mcp:${serverName}:${toolName}`, { ...toolArgs });
1026
1235
  }
1027
1236
  break;
1028
1237
  }
@@ -1069,7 +1278,8 @@ export class CodexAdapter {
1069
1278
 
1070
1279
  // Emit tool_result with output
1071
1280
  const exitCode = item.exitCode as number | undefined;
1072
- const output = item.output as string | undefined;
1281
+ // v0.114+: `aggregatedOutput`; legacy: `output`
1282
+ const output = (item.aggregatedOutput ?? item.output) as string | undefined;
1073
1283
  const isError = item.status === "failed" || (exitCode !== undefined && exitCode !== 0);
1074
1284
  const resultText = output
1075
1285
  ? output.substring(0, 2000) + (output.length > 2000 ? "\n…truncated" : "")
@@ -1128,12 +1338,17 @@ export class CodexAdapter {
1128
1338
  const toolUseId = item.id;
1129
1339
  if (!this.emittedToolUseIds.has(toolUseId)) {
1130
1340
  this.emittedToolUseIds.add(toolUseId);
1131
- const serverName = item.serverName as string || "";
1132
- const toolName = item.toolName as string || "";
1341
+ // v0.114+: `server`/`tool`; legacy: `serverName`/`toolName`
1342
+ const serverName = (item.server ?? item.serverName) as string || "";
1343
+ const toolName = (item.tool ?? item.toolName) as string || "";
1133
1344
  this.emitToolUse(toolUseId, `mcp:${serverName}:${toolName}`, {});
1134
1345
  }
1135
1346
  const isError = item.status === "failed";
1136
- const output = item.output as string || item.error as string || "MCP call completed";
1347
+ // v0.114+: error is { message: string }; result is { content: [...] }
1348
+ const errorObj = item.error as { message?: string } | string | undefined;
1349
+ const errorStr = typeof errorObj === "string" ? errorObj : errorObj?.message;
1350
+ const resultObj = item.result as { content?: unknown[] } | undefined;
1351
+ const output = item.output as string || errorStr || (resultObj?.content ? JSON.stringify(resultObj.content) : undefined) || "MCP call completed";
1137
1352
  this.emitToolResult(toolUseId, typeof output === "string" ? output : JSON.stringify(output), isError);
1138
1353
  break;
1139
1354
  }
@@ -1226,11 +1441,39 @@ export class CodexAdapter {
1226
1441
  }
1227
1442
 
1228
1443
  private handleTokenUsageUpdated(params: Record<string, unknown>): void {
1229
- const inputTokens = (params.inputTokens as number) || 0;
1230
- const outputTokens = (params.outputTokens as number) || 0;
1231
- const modelContextWindow = (params.modelContextWindow as number) || 128_000;
1232
- const costUsd = (params.costUsd as number) || 0;
1233
- const model = params.model as string | undefined;
1444
+ // v0.114+: params.tokenUsage = { total: { totalTokens, inputTokens, ... }, last: {...}, modelContextWindow }
1445
+ // Legacy: flat params.inputTokens, params.outputTokens, etc.
1446
+ const tokenUsage = params.tokenUsage as {
1447
+ total?: { totalTokens?: number; inputTokens?: number; outputTokens?: number; cachedInputTokens?: number; reasoningOutputTokens?: number };
1448
+ last?: { totalTokens?: number; inputTokens?: number; outputTokens?: number };
1449
+ modelContextWindow?: number | null;
1450
+ } | undefined;
1451
+
1452
+ let inputTokens: number;
1453
+ let outputTokens: number;
1454
+ let modelContextWindow: number;
1455
+ let costUsd: number;
1456
+ let model: string | undefined;
1457
+
1458
+ if (tokenUsage?.total) {
1459
+ // v0.114+ format
1460
+ inputTokens = tokenUsage.total.inputTokens ?? 0;
1461
+ outputTokens = tokenUsage.total.outputTokens ?? 0;
1462
+ modelContextWindow = tokenUsage.modelContextWindow ?? 128_000;
1463
+ costUsd = (params.costUsd as number) || 0;
1464
+ model = params.model as string | undefined;
1465
+
1466
+ // Update cumulative counters from total
1467
+ this.cumulativeInputTokens = inputTokens;
1468
+ this.cumulativeOutputTokens = outputTokens;
1469
+ } else {
1470
+ // Legacy flat format
1471
+ inputTokens = (params.inputTokens as number) || 0;
1472
+ outputTokens = (params.outputTokens as number) || 0;
1473
+ modelContextWindow = (params.modelContextWindow as number) || 128_000;
1474
+ costUsd = (params.costUsd as number) || 0;
1475
+ model = params.model as string | undefined;
1476
+ }
1234
1477
 
1235
1478
  // Update cumulative cost if provided
1236
1479
  if (costUsd > 0) {
@@ -1254,7 +1497,8 @@ export class CodexAdapter {
1254
1497
  // ── Helper methods ──────────────────────────────────────────────────────
1255
1498
 
1256
1499
  private emit(msg: BrowserIncomingMessage): void {
1257
- this.browserMessageCb?.(msg);
1500
+ if (!this.browserMessageCb) return;
1501
+ this.browserMessageCb(msg);
1258
1502
  }
1259
1503
 
1260
1504
  private emitSessionUpdate(fields: Partial<SessionState>): void {