excaliagent 0.2.0 → 0.4.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 (168) hide show
  1. package/README.md +129 -30
  2. package/dist/server/args.js +19 -3
  3. package/dist/server/characters.js +540 -0
  4. package/dist/server/cli.js +47 -6
  5. package/dist/server/http.js +122 -11
  6. package/dist/server/hub.js +17 -3
  7. package/dist/server/instance.js +52 -0
  8. package/dist/server/panes/herdr.js +29 -3
  9. package/dist/server/scene-store.js +58 -7
  10. package/dist/server/sources/claude.js +36 -1
  11. package/dist/server/sources/codex-live.js +279 -0
  12. package/dist/server/sources/codex-rollout.js +154 -0
  13. package/dist/server/sources/codex.js +363 -0
  14. package/dist/server/sources/jsonl.js +142 -0
  15. package/dist/server/sources/multi.js +53 -0
  16. package/dist/server/sources/opencode-live.js +144 -0
  17. package/dist/server/sources/opencode.js +265 -0
  18. package/dist/server/sources/proc.js +34 -0
  19. package/dist/server/sources/transcript.js +9 -103
  20. package/dist/server/token.js +23 -0
  21. package/dist/shared/packs.js +62 -0
  22. package/dist/web/apple-touch-icon.png +0 -0
  23. package/dist/web/assets/{abnfDiagram-VCTEODGH-tmjYj3VY.js → abnfDiagram-VCTEODGH-BxrKsDoB.js} +1 -1
  24. package/dist/web/assets/{ar-SA-G6X2FPQ2-8ZGGBDRT.js → ar-SA-G6X2FPQ2-Cc6gbg88.js} +1 -1
  25. package/dist/web/assets/{architecture-7GRP2DOG-CsvDkWt3.js → architecture-7GRP2DOG-CIqN5I9B.js} +1 -1
  26. package/dist/web/assets/{architectureDiagram-5GKGNRK7-Bq-1CDhU.js → architectureDiagram-5GKGNRK7-DsXC8AqK.js} +1 -1
  27. package/dist/web/assets/{az-AZ-76LH7QW2-DbdS_q-w.js → az-AZ-76LH7QW2-BH8VzNec.js} +1 -1
  28. package/dist/web/assets/{bg-BG-XCXSNQG7-CANqXhG1.js → bg-BG-XCXSNQG7-ClFUbics.js} +1 -1
  29. package/dist/web/assets/{blockDiagram-I7D4REHJ-DtYn-xOl.js → blockDiagram-I7D4REHJ-CltS-Zt2.js} +1 -1
  30. package/dist/web/assets/{bn-BD-2XOGV67Q-DJdq0B1R.js → bn-BD-2XOGV67Q-BVUiOO4B.js} +1 -1
  31. package/dist/web/assets/{c4Diagram-7LVT6UL2-DW6Pzj1e.js → c4Diagram-7LVT6UL2-C-C_SvWm.js} +1 -1
  32. package/dist/web/assets/{ca-ES-6MX7JW3Y-B7pUqerV.js → ca-ES-6MX7JW3Y-CEK8mB8J.js} +1 -1
  33. package/dist/web/assets/channel-Dom8D_0o.js +1 -0
  34. package/dist/web/assets/{chunk-4HAMMTFA-_AKNu1--.js → chunk-4HAMMTFA-Bfdmb0Hg.js} +1 -1
  35. package/dist/web/assets/{chunk-75Z2AOVW-BoMl8Mqw.js → chunk-75Z2AOVW-NYwkutzS.js} +1 -1
  36. package/dist/web/assets/{chunk-DU6HZSFF-DYHa8ENH.js → chunk-DU6HZSFF-SqE0xdu5.js} +1 -1
  37. package/dist/web/assets/{chunk-EIO257PC--adgQBo2.js → chunk-EIO257PC-DpOeKaLq.js} +1 -1
  38. package/dist/web/assets/{chunk-F27PBJKO-CvQi01qZ.js → chunk-F27PBJKO-g9bJrSwZ.js} +1 -1
  39. package/dist/web/assets/{chunk-GMAD6QVW-ojaxPuMG.js → chunk-GMAD6QVW-B1y2ol5w.js} +1 -1
  40. package/dist/web/assets/{chunk-GVQU2GXP-DYAeD1IL.js → chunk-GVQU2GXP-BUjRNRXo.js} +1 -1
  41. package/dist/web/assets/{chunk-IMKFNOWR-oVPWqbpV.js → chunk-IMKFNOWR-DyeQep8U.js} +1 -1
  42. package/dist/web/assets/{chunk-L3NEJ4N5-BelAXiIN.js → chunk-L3NEJ4N5-BcTVhsWp.js} +1 -1
  43. package/dist/web/assets/{chunk-OSK3NFVY-CATtjN8l.js → chunk-OSK3NFVY-Db7B8jPm.js} +1 -1
  44. package/dist/web/assets/{chunk-P2QGCYS3-CbKPCENJ.js → chunk-P2QGCYS3-Cx5iBC5z.js} +1 -1
  45. package/dist/web/assets/{chunk-POPQ4Y6H-BfOCSezn.js → chunk-POPQ4Y6H-CqVRZDA8.js} +1 -1
  46. package/dist/web/assets/{chunk-PWAF6VOD-C7O-JWJt.js → chunk-PWAF6VOD-Cf2j9yew.js} +1 -1
  47. package/dist/web/assets/{chunk-SHT3W25Y-BfEpPCTX.js → chunk-SHT3W25Y-0_N13xgE.js} +1 -1
  48. package/dist/web/assets/{chunk-SVP7TREG-CaeKcCSu.js → chunk-SVP7TREG-DkRJF9jr.js} +1 -1
  49. package/dist/web/assets/{chunk-TICWLB2K-Bx6AcLB_.js → chunk-TICWLB2K-D6OC50xH.js} +1 -1
  50. package/dist/web/assets/classDiagram-ZZMXUADV-BMEkgkEJ.js +1 -0
  51. package/dist/web/assets/classDiagram-v2-VYDZK3BY-BMEkgkEJ.js +1 -0
  52. package/dist/web/assets/{cs-CZ-2BRQDIVT-DF8hZ2mh.js → cs-CZ-2BRQDIVT-DWt1RUsP.js} +1 -1
  53. package/dist/web/assets/{cynefin-OW5HDTMX-Dol6Weuo.js → cynefin-OW5HDTMX-Dx0HzRCl.js} +1 -1
  54. package/dist/web/assets/{cynefinDiagram-5FMLGOSQ-CbOJ2lks.js → cynefinDiagram-5FMLGOSQ-C8AMQNhe.js} +1 -1
  55. package/dist/web/assets/{da-DK-5WZEPLOC-CgPdNz6E.js → da-DK-5WZEPLOC-BPggh0j6.js} +1 -1
  56. package/dist/web/assets/{dagre-GXQ25YYZ-CcuaBID7.js → dagre-GXQ25YYZ-f9qU6BFq.js} +1 -1
  57. package/dist/web/assets/{de-DE-XR44H4JA-DaVeYWRc.js → de-DE-XR44H4JA-B1q1pJrQ.js} +1 -1
  58. package/dist/web/assets/{diagram-S7CK7UJ4-BR2gIsNx.js → diagram-S7CK7UJ4-Bcq77Nl3.js} +1 -1
  59. package/dist/web/assets/{diagram-UQ7AKVKN-DwSZsV-Z.js → diagram-UQ7AKVKN-DXzlgVTk.js} +1 -1
  60. package/dist/web/assets/{diagram-VSXAHHWV-BcJpQHbj.js → diagram-VSXAHHWV-C1vz2FvT.js} +1 -1
  61. package/dist/web/assets/{diagram-VX7I27RA-CvjRTkwu.js → diagram-VX7I27RA-_c4ucp3r.js} +1 -1
  62. package/dist/web/assets/{diagram-Z3DM3KII-vg0-r_P4.js → diagram-Z3DM3KII-cUg91K-F.js} +1 -1
  63. package/dist/web/assets/{dist-D-3cz42D.js → dist-DcwmvBdV.js} +4 -4
  64. package/dist/web/assets/{ebnfDiagram-PWID7BFC-DZCaMmMd.js → ebnfDiagram-PWID7BFC-9iWfohHg.js} +1 -1
  65. package/dist/web/assets/{el-GR-BZB4AONW-nYncjO-L.js → el-GR-BZB4AONW-CrhwvNDD.js} +1 -1
  66. package/dist/web/assets/{en-B4ZKOASM-DqIz0rJz.js → en-B4ZKOASM-DCjnkz5L.js} +1 -1
  67. package/dist/web/assets/{erDiagram-RLTQ6QDP-DgyrjoaZ.js → erDiagram-RLTQ6QDP-A72uVlS3.js} +1 -1
  68. package/dist/web/assets/{es-ES-U4NZUMDT-e3WeRDLl.js → es-ES-U4NZUMDT-D0sRCoDL.js} +1 -1
  69. package/dist/web/assets/{eu-ES-A7QVB2H4-CwXv1t13.js → eu-ES-A7QVB2H4-DLG-liPb.js} +1 -1
  70. package/dist/web/assets/{eventmodeling-NTZA5JFV-DCTWpAwP.js → eventmodeling-NTZA5JFV-CIKZvDAj.js} +1 -1
  71. package/dist/web/assets/{fa-IR-HGAKTJCU-D-9plwKu.js → fa-IR-HGAKTJCU-BW3pPOVT.js} +1 -1
  72. package/dist/web/assets/{fi-FI-Z5N7JZ37-BTK1rs8G.js → fi-FI-Z5N7JZ37-D2XaqwPp.js} +1 -1
  73. package/dist/web/assets/flowDiagram-HODETNUW-DhQSc3KP.js +1 -0
  74. package/dist/web/assets/{fr-FR-RHASNOE6-CAuUMPtN.js → fr-FR-RHASNOE6-B-BjRA7r.js} +1 -1
  75. package/dist/web/assets/{ganttDiagram-EL5Y4UJY-bFfu4BMj.js → ganttDiagram-EL5Y4UJY-cgwUfr0G.js} +1 -1
  76. package/dist/web/assets/{gitGraph-4MIJSDKK-DIl4-oqK.js → gitGraph-4MIJSDKK-CeKopPn3.js} +1 -1
  77. package/dist/web/assets/{gitGraphDiagram-WWUBYQGX-ho3FT8XP.js → gitGraphDiagram-WWUBYQGX-BWjAy7ei.js} +1 -1
  78. package/dist/web/assets/{gl-ES-HMX3MZ6V-DlKPdjtk.js → gl-ES-HMX3MZ6V-DYUAqB0N.js} +1 -1
  79. package/dist/web/assets/{he-IL-6SHJWFNN-CLyP0zBo.js → he-IL-6SHJWFNN-CXkYqbuu.js} +1 -1
  80. package/dist/web/assets/{hi-IN-IWLTKZ5I-kvhBHNzH.js → hi-IN-IWLTKZ5I-BwVBsHWG.js} +1 -1
  81. package/dist/web/assets/{hu-HU-A5ZG7DT2-Dtd-o-RJ.js → hu-HU-A5ZG7DT2-DkmlE7Aa.js} +1 -1
  82. package/dist/web/assets/{id-ID-SAP4L64H-VZ3IV-8B.js → id-ID-SAP4L64H-CwkytHX_.js} +1 -1
  83. package/dist/web/assets/{image-GAAHSSAO-DWvjHHd3.js → image-GAAHSSAO-u-hBxkaK.js} +1 -1
  84. package/dist/web/assets/index-D43I17T9.js +212 -0
  85. package/dist/web/assets/{index-DtRVLazd.css → index-DxXUknKX.css} +1 -1
  86. package/dist/web/assets/{info-A6RAGUB7-DN8pwhKt.js → info-A6RAGUB7-DmadBmci.js} +1 -1
  87. package/dist/web/assets/{infoDiagram-27XIBGKW-i2JHBJKe.js → infoDiagram-27XIBGKW-D93CKwhy.js} +1 -1
  88. package/dist/web/assets/{ishikawaDiagram-5VMMS53U-DLLdu9-W.js → ishikawaDiagram-5VMMS53U-BQ-x5YXp.js} +1 -1
  89. package/dist/web/assets/{it-IT-JPQ66NNP-6SULWnRA.js → it-IT-JPQ66NNP-Bc-Qa6KI.js} +1 -1
  90. package/dist/web/assets/{ja-JP-DBVTYXUO-BbgutQYO.js → ja-JP-DBVTYXUO-BPmQCryQ.js} +1 -1
  91. package/dist/web/assets/{journeyDiagram-3NMN7TZE-DsysKJxX.js → journeyDiagram-3NMN7TZE-CO_LXx1E.js} +1 -1
  92. package/dist/web/assets/{kaa-6HZHGXH3-Bc6yhZWQ.js → kaa-6HZHGXH3-DuBFVAjv.js} +1 -1
  93. package/dist/web/assets/{kab-KAB-ZGHBKWFO-BaICxis-.js → kab-KAB-ZGHBKWFO-DhEFzSTh.js} +1 -1
  94. package/dist/web/assets/{kanban-definition-UXKFOSKX-w3k_6RhT.js → kanban-definition-UXKFOSKX-B72Jztau.js} +1 -1
  95. package/dist/web/assets/{kk-KZ-P5N5QNE5-BVsfmPoG.js → kk-KZ-P5N5QNE5-CW7PxU2h.js} +1 -1
  96. package/dist/web/assets/{km-KH-HSX4SM5Z-BS1M1bUx.js → km-KH-HSX4SM5Z-CYy7yySD.js} +1 -1
  97. package/dist/web/assets/{ko-KR-MTYHY66A-C0x16zQm.js → ko-KR-MTYHY66A-C5O645Qe.js} +1 -1
  98. package/dist/web/assets/{ku-TR-6OUDTVRD-BWI9z5E8.js → ku-TR-6OUDTVRD-CKcd_iWr.js} +1 -1
  99. package/dist/web/assets/{line-BRXlgUrO.js → line-Ckd3sAmP.js} +1 -1
  100. package/dist/web/assets/{lt-LT-XHIRWOB4-CTDq1zFR.js → lt-LT-XHIRWOB4-ya2_xWiy.js} +1 -1
  101. package/dist/web/assets/{lv-LV-5QDEKY6T-h8b-E15B.js → lv-LV-5QDEKY6T-cYYwyzmz.js} +1 -1
  102. package/dist/web/assets/{mermaid-parser.core-Da_XEVmR.js → mermaid-parser.core-Dbqkvrw-.js} +3 -3
  103. package/dist/web/assets/{mindmap-definition-YA3MSWOX-DUBdgu2g.js → mindmap-definition-YA3MSWOX-B4onxNgk.js} +1 -1
  104. package/dist/web/assets/{mr-IN-CRQNXWMA-dTdkz007.js → mr-IN-CRQNXWMA-CfaHpVWe.js} +1 -1
  105. package/dist/web/assets/{my-MM-5M5IBNSE-CmYwTRil.js → my-MM-5M5IBNSE-BfKiTATq.js} +1 -1
  106. package/dist/web/assets/{nb-NO-T6EIAALU-DLLYcK-s.js → nb-NO-T6EIAALU-Cz3yDFYg.js} +1 -1
  107. package/dist/web/assets/{nl-NL-IS3SIHDZ-mtv15Mmm.js → nl-NL-IS3SIHDZ-DDX026n5.js} +1 -1
  108. package/dist/web/assets/{nn-NO-6E72VCQL-CJDTT3kB.js → nn-NO-6E72VCQL-BcokL_SN.js} +1 -1
  109. package/dist/web/assets/{oc-FR-POXYY2M6-DdHJTqH4.js → oc-FR-POXYY2M6-bNFsGpEp.js} +1 -1
  110. package/dist/web/assets/{pa-IN-N4M65BXN-DxzzhbLu.js → pa-IN-N4M65BXN-iIHN9Z8W.js} +1 -1
  111. package/dist/web/assets/{packet-AYTQ26CC-CzX42SOE.js → packet-AYTQ26CC-BzBpP9hg.js} +1 -1
  112. package/dist/web/assets/{pegDiagram-XKGWAZYB-1R2Hd3Th.js → pegDiagram-XKGWAZYB-BPScRPVZ.js} +1 -1
  113. package/dist/web/assets/percentages-BXMCSKIN-EDfR-TFr.js +1 -0
  114. package/dist/web/assets/{pie-WAS4IAKB-DEI2uslz.js → pie-WAS4IAKB-N-9RHtKL.js} +1 -1
  115. package/dist/web/assets/{pieDiagram-E7YTZNPT-DOQnFSrs.js → pieDiagram-E7YTZNPT-Bpd_yCG-.js} +1 -1
  116. package/dist/web/assets/{pl-PL-T2D74RX3-Dpyab0lI.js → pl-PL-T2D74RX3-DeqOx-a4.js} +1 -1
  117. package/dist/web/assets/{pt-BR-5N22H2LF-DOGrUA6Y.js → pt-BR-5N22H2LF-DG1PCIkb.js} +1 -1
  118. package/dist/web/assets/{pt-PT-UZXXM6DQ-DAoNiiOg.js → pt-PT-UZXXM6DQ-BsUJk2fq.js} +1 -1
  119. package/dist/web/assets/{quadrantDiagram-AXDQQJYC-CQE1OxRJ.js → quadrantDiagram-AXDQQJYC-CSLCp0xS.js} +1 -1
  120. package/dist/web/assets/{radar-RG4KPBEZ-B-er2ORT.js → radar-RG4KPBEZ-rKULJroO.js} +1 -1
  121. package/dist/web/assets/{railroad-74A4TZTK-D1Veuu81.js → railroad-74A4TZTK-hDn0WPg3.js} +1 -1
  122. package/dist/web/assets/{railroad-abnf-HS5TGJTU-DTxLo5az.js → railroad-abnf-HS5TGJTU-DfI0hiCR.js} +1 -1
  123. package/dist/web/assets/{railroad-ebnf-LZEXJU2U-BmP2IZGe.js → railroad-ebnf-LZEXJU2U-paFQAnqR.js} +1 -1
  124. package/dist/web/assets/{railroad-peg-WCYAUIDC-Do60gMTC.js → railroad-peg-WCYAUIDC-B-2Um3QI.js} +1 -1
  125. package/dist/web/assets/{railroadDiagram-O6MQD6OU-5I8Qk1dI.js → railroadDiagram-O6MQD6OU-WkZPN5MA.js} +1 -1
  126. package/dist/web/assets/{requirementDiagram-BXWQKSXE-CIIM8YmC.js → requirementDiagram-BXWQKSXE-BJULFhuR.js} +1 -1
  127. package/dist/web/assets/{ro-RO-JPDTUUEW-DR9yu2MV.js → ro-RO-JPDTUUEW-yKjTMizq.js} +1 -1
  128. package/dist/web/assets/{ru-RU-B4JR7IUQ-B9uIhZUd.js → ru-RU-B4JR7IUQ-DITGdMQL.js} +1 -1
  129. package/dist/web/assets/{sankeyDiagram-P5KCCOFB-BdGmiiCQ.js → sankeyDiagram-P5KCCOFB-b_vfju3s.js} +1 -1
  130. package/dist/web/assets/{sequenceDiagram-WJ2MYXX4-BeCmgCpG.js → sequenceDiagram-WJ2MYXX4-D5n2aEN6.js} +1 -1
  131. package/dist/web/assets/{si-LK-N5RQ5JYF-C7DYCsOB.js → si-LK-N5RQ5JYF-DaFciStR.js} +1 -1
  132. package/dist/web/assets/{sk-SK-C5VTKIMK-BbBBLLCs.js → sk-SK-C5VTKIMK-CpyCIFtD.js} +1 -1
  133. package/dist/web/assets/{sl-SI-NN7IZMDC-BIaW2Iuj.js → sl-SI-NN7IZMDC-cnwvs7FN.js} +1 -1
  134. package/dist/web/assets/{stateDiagram-D77RDMKH-CmENpSfH.js → stateDiagram-D77RDMKH-CbFjpKDm.js} +1 -1
  135. package/dist/web/assets/stateDiagram-v2-MP3YSRHH-MOkTt8Mv.js +1 -0
  136. package/dist/web/assets/subset-shared.chunk-CWAaXrHb.js +1 -0
  137. package/dist/web/assets/{subset-worker.chunk-Bi16Jh4x.js → subset-worker.chunk-Cebu2JDe.js} +1 -1
  138. package/dist/web/assets/{sv-SE-XGPEYMSR-D5A1U3nk.js → sv-SE-XGPEYMSR-dOntI1Fk.js} +1 -1
  139. package/dist/web/assets/{swimlanes-42K2YHIH-_ymwQg8o.js → swimlanes-42K2YHIH-DtmsuzRR.js} +1 -1
  140. package/dist/web/assets/swimlanesDiagram-VR7AAH4N-v2ue4raa.js +8 -0
  141. package/dist/web/assets/{ta-IN-2NMHFXQM-BDzXUwiE.js → ta-IN-2NMHFXQM-Comju6i2.js} +1 -1
  142. package/dist/web/assets/{th-TH-HPSO5L25-DdVnWuWW.js → th-TH-HPSO5L25-uPCBvbue.js} +1 -1
  143. package/dist/web/assets/{timeline-definition-24CTP7MA-CRcZde4k.js → timeline-definition-24CTP7MA-DKZOmZFD.js} +1 -1
  144. package/dist/web/assets/{tr-TR-DEFEU3FU-BQwfGN3o.js → tr-TR-DEFEU3FU-LaP2C4go.js} +1 -1
  145. package/dist/web/assets/{treeView-Q6P3EWNA-BHZZuSXr.js → treeView-Q6P3EWNA-DJii9IFr.js} +1 -1
  146. package/dist/web/assets/{treemap-WGGIJYW6-DAzotEQZ.js → treemap-WGGIJYW6-U42pUP2Q.js} +1 -1
  147. package/dist/web/assets/{uk-UA-QMV73CPH-CFYnWqHD.js → uk-UA-QMV73CPH-u-WBulor.js} +1 -1
  148. package/dist/web/assets/{vennDiagram-4TSXK5OY-BsNz9Qnn.js → vennDiagram-4TSXK5OY-C8_zJPdp.js} +1 -1
  149. package/dist/web/assets/{vi-VN-M7AON7JQ-DO3BySmm.js → vi-VN-M7AON7JQ-DOSv3aq5.js} +1 -1
  150. package/dist/web/assets/{wardley-WFR3VGLG-Cc2r8Vp8.js → wardley-WFR3VGLG-DU89dbbh.js} +1 -1
  151. package/dist/web/assets/{wardleyDiagram-VM6X3IG4-47UvJO4N.js → wardleyDiagram-VM6X3IG4-BGnASelu.js} +1 -1
  152. package/dist/web/assets/{xychartDiagram-S5SC5T6Z-CVl63zr-.js → xychartDiagram-S5SC5T6Z-js8-abv7.js} +1 -1
  153. package/dist/web/assets/{zh-CN-LNUGB5OW-DKJ2CF-O.js → zh-CN-LNUGB5OW-B8soJHf3.js} +1 -1
  154. package/dist/web/assets/{zh-HK-E62DVLB3-DHW8OH5j.js → zh-HK-E62DVLB3-DKByBmSJ.js} +1 -1
  155. package/dist/web/assets/{zh-TW-RAJ6MFWO-B9cMDifZ.js → zh-TW-RAJ6MFWO-CYBStGL6.js} +1 -1
  156. package/dist/web/favicon-32.png +0 -0
  157. package/dist/web/favicon.svg +12 -0
  158. package/dist/web/index.html +6 -3
  159. package/package.json +5 -3
  160. package/dist/web/assets/channel-gFr9jEgP.js +0 -1
  161. package/dist/web/assets/classDiagram-ZZMXUADV-9J2CNLBI.js +0 -1
  162. package/dist/web/assets/classDiagram-v2-VYDZK3BY-9J2CNLBI.js +0 -1
  163. package/dist/web/assets/flowDiagram-HODETNUW--2QUGuZ7.js +0 -1
  164. package/dist/web/assets/index-B2hGCC2V.js +0 -212
  165. package/dist/web/assets/percentages-BXMCSKIN-Br7824d3.js +0 -1
  166. package/dist/web/assets/stateDiagram-v2-MP3YSRHH-BWgn52cG.js +0 -1
  167. package/dist/web/assets/subset-shared.chunk-d2GNuFHV.js +0 -1
  168. package/dist/web/assets/swimlanesDiagram-VR7AAH4N-fzrjtpfS.js +0 -8
@@ -0,0 +1,279 @@
1
+ // Which Codex threads are running, and in which process. Codex has no session
2
+ // registry like Claude's; what it has (verified on 0.163.0, Linux):
3
+ //
4
+ // - `<codexHome>/thread-writer-locks/<thread id>.lock`: created and flock()ed
5
+ // by whoever has the thread loaded, unlocked and deleted when it is unloaded.
6
+ // Crashes leave the file but release the lock, so only a *held* lock counts:
7
+ // the holder is found in /proc/locks by inode and confirmed through
8
+ // /proc/<pid>/fd.
9
+ // - The interactive TUI (`codex`, `codex resume <id>`) does not hold it: it
10
+ // talks to a shared app-server daemon (`codex app-server ... --managed-daemon`,
11
+ // one per Codex home) that loads the thread. The daemon keeps a thread loaded
12
+ // for `thread_unload_delay_secs` (default 30 min) after its last client left,
13
+ // so a held lock alone does not mean someone is looking at it.
14
+ // - `codex exec` loads its thread in-process and holds the lock itself.
15
+ //
16
+ // So a thread held by a non-daemon codex process is live in that process. A
17
+ // thread held by the daemon is live only when a TUI process claims it: by its
18
+ // argv (`resume <id>`), else by working directory, among threads loaded after
19
+ // that TUI started (a thread loaded earlier belongs to another, maybe closed,
20
+ // TUI; a fresh TUI's own thread appears ~1.4 s after it): one with a rollout
21
+ // if any, else its earliest new thread (with auto-review on, the daemon loads
22
+ // a second, reviewer thread right after the conversation, which never gets a
23
+ // rollout unless it reviews something).
24
+ // Daemon threads no TUI claims are lingering, i.e. dormant. Only the first two
25
+ // (in-process holder, `resume <id>`) are exact; a working-directory claim is a
26
+ // guess, so such a session cannot be stopped from the board. Pure matching
27
+ // lives in `assignLive`; the /proc reading around it is Linux-only and takes a
28
+ // proc root, so tests read a synthetic tree.
29
+ import { readdir, readFile, readlink, realpath, stat } from "node:fs/promises";
30
+ import { basename, join } from "node:path";
31
+ import { createProcessTable } from "../desktop/process-table.js";
32
+ import { CODEX_ID, uuidV7Ms } from "./codex-rollout.js";
33
+ import { bootTime, startedAt } from "./proc.js";
34
+ /** A Codex executable: argv[0] or exe named `codex`, or the npm shim (`node …/codex.js`). */
35
+ export function looksLikeCodex(argv, exe) {
36
+ const names = [argv?.[0], exe].filter((n) => !!n).map((n) => basename(n));
37
+ if (names.some((n) => n === "codex" || n === "codex.exe"))
38
+ return true;
39
+ const script = argv?.[1] ? basename(argv[1]) : "";
40
+ return names.some((n) => /^node(\.exe)?$/.test(n)) && (script === "codex" || script === "codex.js");
41
+ }
42
+ // Options that take a value, so the next word is not the subcommand.
43
+ const VALUE_FLAGS = new Set([
44
+ "-c", "--config", "-m", "--model", "-p", "--profile", "-s", "--sandbox", "-a", "--ask-for-approval",
45
+ "-C", "--cd", "-i", "--image", "--add-dir", "--enable", "--disable", "--local-provider", "--remote",
46
+ "--remote-auth-token-env",
47
+ ]);
48
+ // Subcommands that are not an interactive session (the TUI is `codex [PROMPT]`, `resume`, `fork`).
49
+ const NOT_TUI = new Set([
50
+ "agents", "exec", "e", "review", "login", "logout", "mcp", "plugin", "app-server", "remote-control",
51
+ "completion", "update", "doctor", "sandbox", "debug", "apply", "a", "queue", "archive", "delete",
52
+ "migrate-rollouts", "unarchive", "cloud", "exec-server", "features", "help", "mcp-server",
53
+ ]);
54
+ /** The words after the program (and after the script, for the npm shim). */
55
+ function args(argv) {
56
+ const shim = /^node(\.exe)?$/.test(basename(argv[0] ?? ""));
57
+ return argv.slice(shim ? 2 : 1);
58
+ }
59
+ /** First positional word: the subcommand, a prompt, or undefined. */
60
+ export function codexSubcommand(argv) {
61
+ const a = args(argv);
62
+ for (let i = 0; i < a.length; i++) {
63
+ const w = a[i];
64
+ if (w === "--")
65
+ return a[i + 1];
66
+ if (w.startsWith("-")) {
67
+ if (VALUE_FLAGS.has(w))
68
+ i++;
69
+ continue;
70
+ }
71
+ return w;
72
+ }
73
+ return undefined;
74
+ }
75
+ export const isDaemon = (argv) => codexSubcommand(argv) === "app-server";
76
+ /**
77
+ * An interactive Codex TUI process. Codex also runs its own binary as helpers
78
+ * (verified on 0.163.0: `codex-linux-sandbox ...` around every sandboxed
79
+ * command, once under bwrap, with the repo as cwd); those have another
80
+ * argv[0] (`codex-linux-sandbox`, `apply_patch`) or a `--codex-run-as-*`
81
+ * argv[1], and are not TUIs.
82
+ */
83
+ export const isTui = (argv) => {
84
+ const shim = /^node(\.exe)?$/.test(basename(argv[0] ?? ""));
85
+ const name = basename((shim ? argv[1] : argv[0]) ?? "");
86
+ if (!(shim ? name === "codex" || name === "codex.js" : name === "codex" || name === "codex.exe"))
87
+ return false;
88
+ if (args(argv)[0]?.startsWith("--codex-run-as-"))
89
+ return false;
90
+ const sub = codexSubcommand(argv);
91
+ return sub === undefined || !NOT_TUI.has(sub);
92
+ };
93
+ /** `codex resume <id>` / `codex fork <id>`: the thread named on the command line. */
94
+ export function argvThreadId(argv) {
95
+ const a = args(argv);
96
+ const sub = codexSubcommand(argv);
97
+ if (sub !== "resume")
98
+ return undefined; // a fork gets a new id
99
+ const rest = a.slice(a.indexOf(sub) + 1);
100
+ return rest.find((w) => CODEX_ID.test(w))?.toLowerCase();
101
+ }
102
+ /**
103
+ * Pure: which held threads are live, and where. `threadCwd` gives a thread's
104
+ * start cwd (from its rollout; undefined before the first message is written).
105
+ * `isUser` says whether a thread may be claimed by a TUI (not a sub-agent etc.).
106
+ * `threadActivity` gives the epoch ms of a thread's last rollout activity, and
107
+ * is undefined exactly when it has no rollout (default: when it has no cwd).
108
+ * Deterministic: the same locks and processes give the same answer in any order.
109
+ */
110
+ export function assignLive(locks, procs, threadCwd, isUser = () => true, threadActivity = (id) => (threadCwd(id) === undefined ? undefined : 0)) {
111
+ const byPid = new Map(procs.map((p) => [p.pid, p]));
112
+ const out = [];
113
+ const daemonHeld = [];
114
+ for (const l of locks) {
115
+ const holder = byPid.get(l.pid);
116
+ if (!holder)
117
+ continue;
118
+ if (isDaemon(holder.argv))
119
+ daemonHeld.push(l);
120
+ else
121
+ out.push({ threadId: l.threadId, pid: l.pid, exact: true });
122
+ }
123
+ // TUIs; the npm shim and the native binary below it count once (the outer one).
124
+ const codexPids = new Set(procs.map((p) => p.pid));
125
+ const tuis = procs
126
+ .filter((p) => !isDaemon(p.argv) && isTui(p.argv) && !codexPids.has(p.ppid))
127
+ .filter((p) => !out.some((t) => t.pid === p.pid))
128
+ // Newest first: a later TUI is more likely on the newest thread of a directory,
129
+ // and what it leaves (its reviewer, say) is reserved before older TUIs choose.
130
+ .sort((a, b) => b.startedAt - a.startedAt || b.pid - a.pid);
131
+ const free = new Set(daemonHeld.filter((l) => isUser(l.threadId)).map((l) => l.threadId));
132
+ const loaded = new Map(daemonHeld.map((l) => [l.threadId, l.loadedAt]));
133
+ const created = (id) => uuidV7Ms(id) ?? loaded.get(id);
134
+ // Earliest created first; a tie goes to the smaller id, never to scan order.
135
+ const earlier = (a, b) => created(a) - created(b) || (a < b ? -1 : a > b ? 1 : 0);
136
+ /** Free threads `t` can have opened: from its directory (or no rollout yet), loaded after it started. */
137
+ const candidates = (t) => [...free]
138
+ .filter((id) => {
139
+ const cwd = threadCwd(id);
140
+ if (cwd !== undefined && cwd !== t.cwd)
141
+ return false;
142
+ const rollout = threadActivity(id) !== undefined;
143
+ // `codex resume` (its picker) only ever opens a thread with a rollout.
144
+ if (!rollout && codexSubcommand(t.argv) === "resume")
145
+ return false;
146
+ // Loaded after it started (else another TUI's, lingering), and, with no
147
+ // rollout yet (nobody can resume such a thread), created after it
148
+ // started. startedAt is never late (btime is whole seconds, rounded
149
+ // down), so its own thread always passes.
150
+ if (t.startedAt > 0 && (loaded.get(id) < t.startedAt || (!rollout && created(id) < t.startedAt)))
151
+ return false;
152
+ return true;
153
+ })
154
+ .sort(earlier);
155
+ // Threads loaded after a TUI started that it did not take (its reviewer, say):
156
+ // no other TUI may take them either, unless one is that TUI's own earliest.
157
+ const reserved = new Set();
158
+ const claimed = new Set();
159
+ for (const t of tuis) {
160
+ const id = argvThreadId(t.argv);
161
+ // A TUI that names its thread never guesses another one, even if that one is taken.
162
+ if (id)
163
+ claimed.add(t.pid);
164
+ if (id && free.has(id)) {
165
+ free.delete(id);
166
+ out.push({ threadId: id, pid: t.pid, exact: true, ...(t.cwd ? { cwd: t.cwd } : {}) });
167
+ }
168
+ }
169
+ for (const t of tuis)
170
+ if (claimed.has(t.pid) && t.cwd)
171
+ for (const id of candidates(t))
172
+ reserved.add(id);
173
+ for (const t of tuis) {
174
+ if (claimed.has(t.pid) || !t.cwd)
175
+ continue;
176
+ const mine = candidates(t);
177
+ const allowed = mine.filter((id, i) => i === 0 || !reserved.has(id));
178
+ for (const id of mine)
179
+ reserved.add(id);
180
+ // A thread with a rollout wins (the newest activity, then the newest load,
181
+ // then the larger id); else the earliest new thread: the conversation, not
182
+ // the auto-review thread the daemon loads just after it.
183
+ const withRollout = allowed.filter((id) => threadActivity(id) !== undefined);
184
+ const best = withRollout.length
185
+ ? withRollout.reduce((a, b) => {
186
+ const d = threadActivity(b) - threadActivity(a) || loaded.get(b) - loaded.get(a);
187
+ return d > 0 || (d === 0 && b > a) ? b : a;
188
+ })
189
+ : allowed[0];
190
+ if (best === undefined)
191
+ continue;
192
+ free.delete(best);
193
+ out.push({ threadId: best, pid: t.pid, cwd: t.cwd, exact: false });
194
+ }
195
+ return out;
196
+ }
197
+ /** `/proc/locks` → inode → pids holding (not waiting for) a lock on it. */
198
+ export function parseProcLocks(text) {
199
+ const out = new Map();
200
+ for (const line of text.split("\n")) {
201
+ const f = line.trim().split(/\s+/);
202
+ if (f.length < 6 || f[1] === "->")
203
+ continue;
204
+ const pid = Number(f[4]);
205
+ const inode = Number(f[5].split(":")[2]);
206
+ if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(inode))
207
+ continue;
208
+ out.set(inode, [...(out.get(inode) ?? []), pid]);
209
+ }
210
+ return out;
211
+ }
212
+ /**
213
+ * Reads the held thread locks under `lockDir`, their holders and, only when a
214
+ * daemon holds one, the Codex TUI processes that may be showing it. No lock
215
+ * file → nothing else is read. Undefined when /proc/locks is not readable (not
216
+ * Linux): liveness unknown.
217
+ */
218
+ export async function scanCodexProcesses(lockDir, procRoot = "/proc") {
219
+ const locksText = await readFile(join(procRoot, "locks"), "utf8").catch(() => undefined);
220
+ if (locksText === undefined)
221
+ return undefined;
222
+ lockDir = await realpath(lockDir).catch(() => lockDir);
223
+ const files = (await readdir(lockDir).catch(() => [])).filter((f) => f.endsWith(".lock") && CODEX_ID.test(f.slice(0, -5)));
224
+ if (!files.length)
225
+ return { locks: [], procs: [] };
226
+ const table = createProcessTable({ platform: "linux", procRoot });
227
+ const btime = await bootTime(procRoot);
228
+ const procs = new Map();
229
+ const proc = async (pid, ppid) => {
230
+ const known = procs.get(pid);
231
+ if (known !== undefined)
232
+ return known ?? undefined;
233
+ const [argv, exe] = await Promise.all([table.argv(pid), table.exe(pid)]);
234
+ if (!argv || !looksLikeCodex(argv, exe)) {
235
+ procs.set(pid, null);
236
+ return undefined;
237
+ }
238
+ const [cwd, parent, started] = await Promise.all([
239
+ readlink(join(procRoot, String(pid), "cwd")).catch(() => undefined),
240
+ ppid ?? table.parent(pid),
241
+ startedAt(procRoot, pid, btime),
242
+ ]);
243
+ const p = { pid, ppid: parent ?? 0, argv, startedAt: started, ...(cwd ? { cwd } : {}) };
244
+ procs.set(pid, p);
245
+ return p;
246
+ };
247
+ const fds = async (pid) => {
248
+ const dir = join(procRoot, String(pid), "fd");
249
+ const names = await readdir(dir).catch(() => []);
250
+ return new Set((await Promise.all(names.map((n) => readlink(join(dir, n)).catch(() => "")))).filter(Boolean));
251
+ };
252
+ const holders = parseProcLocks(locksText);
253
+ const fdCache = new Map();
254
+ const locks = [];
255
+ for (const f of files) {
256
+ const path = join(lockDir, f);
257
+ const st = await stat(path).catch(() => undefined);
258
+ if (!st)
259
+ continue;
260
+ for (const pid of holders.get(st.ino) ?? []) {
261
+ if (!(await proc(pid)))
262
+ continue;
263
+ if (!fdCache.has(pid))
264
+ fdCache.set(pid, await fds(pid));
265
+ if (!fdCache.get(pid).has(path))
266
+ continue;
267
+ locks.push({ threadId: f.slice(0, -5).toLowerCase(), pid, loadedAt: st.birthtimeMs || st.ctimeMs });
268
+ break;
269
+ }
270
+ }
271
+ if (locks.some((l) => isDaemon(procs.get(l.pid).argv))) {
272
+ for (const p of await table.list()) {
273
+ // Cheap filter before reading argv: the native binary, or a node shim.
274
+ if (/codex|node|MainThread/i.test(p.comm))
275
+ await proc(p.pid, p.ppid);
276
+ }
277
+ }
278
+ return { locks, procs: [...procs.values()].filter((p) => p !== null) };
279
+ }
@@ -0,0 +1,154 @@
1
+ // Codex rollouts: <codexHome>/sessions/YYYY/MM/DD/rollout-<local time>-<thread id>.jsonl
2
+ // (old ones may be compressed to `.jsonl.zst`). Folded incrementally; only
3
+ // metadata is kept, plus the first user message (cut short) as the fallback
4
+ // title, which is what Codex itself shows in its resume picker.
5
+ // Formats: docs/design.md "Codex data".
6
+ import { readFile, stat } from "node:fs/promises";
7
+ import { JsonlScanner } from "./jsonl.js";
8
+ export const TITLE_MAX = 120;
9
+ const USER_MESSAGE_BEGIN = "## My request for Codex:";
10
+ const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
11
+ const TS = /^\{"timestamp":"([^"]+)"/;
12
+ // Cheap prefilter on the line; the parsed type is still checked.
13
+ const INTERESTING = /"type":"(?:session_meta|task_started|turn_started|task_complete|turn_complete|turn_aborted|user_message|UserMessage)"/;
14
+ function short(text) {
15
+ const i = text.indexOf(USER_MESSAGE_BEGIN);
16
+ const t = (i >= 0 ? text.slice(i + USER_MESSAGE_BEGIN.length) : text).replace(/\s+/g, " ").trim();
17
+ if (!t)
18
+ return undefined;
19
+ const chars = [...t];
20
+ return chars.length <= TITLE_MAX ? t : chars.slice(0, TITLE_MAX - 1).join("") + "…";
21
+ }
22
+ /** Text of an item_completed UserMessage (paginated history) or a legacy user_message event. */
23
+ function userText(p) {
24
+ if (p.type === "user_message")
25
+ return str(p.message);
26
+ const item = p.item;
27
+ if (p.type !== "item_completed" || item?.type !== "UserMessage" || !Array.isArray(item.content))
28
+ return undefined;
29
+ const parts = item.content
30
+ .map((c) => (c && typeof c === "object" && c.type === "text" ? str(c.text) : undefined))
31
+ .filter((t) => t !== undefined);
32
+ return parts.length ? parts.join(" ") : undefined;
33
+ }
34
+ export function applyRolloutLine(acc, o) {
35
+ const p = o.payload;
36
+ if (!p || typeof p !== "object" || Array.isArray(p))
37
+ return;
38
+ const payload = p;
39
+ if (o.type === "session_meta") {
40
+ // A forked thread's file starts with its own meta; later metas (if any) do not override it.
41
+ if (acc.id)
42
+ return;
43
+ acc.id = str(payload.id);
44
+ acc.cwd = str(payload.cwd);
45
+ acc.branch = str(payload.git?.branch);
46
+ acc.source = payload.source;
47
+ acc.threadSource = str(payload.thread_source);
48
+ acc.parentThreadId = str(payload.parent_thread_id);
49
+ return;
50
+ }
51
+ if (o.type !== "event_msg")
52
+ return;
53
+ switch (payload.type) {
54
+ case "task_started":
55
+ case "turn_started":
56
+ acc.turnOpen = true;
57
+ return;
58
+ case "task_complete":
59
+ case "turn_complete":
60
+ case "turn_aborted":
61
+ acc.turnOpen = false;
62
+ return;
63
+ }
64
+ if (acc.firstUserMessage === undefined) {
65
+ const text = userText(payload);
66
+ if (text !== undefined)
67
+ acc.firstUserMessage = short(text);
68
+ }
69
+ }
70
+ const fold = {
71
+ empty: () => ({ turnOpen: false, lastTimestamp: 0 }),
72
+ raw(acc, line) {
73
+ const m = TS.exec(line);
74
+ const t = m ? Date.parse(m[1]) : NaN;
75
+ if (Number.isFinite(t) && t > acc.lastTimestamp)
76
+ acc.lastTimestamp = t;
77
+ },
78
+ interesting: (line) => INTERESTING.test(line),
79
+ apply: applyRolloutLine,
80
+ snapshot: (acc) => ({ ...acc }),
81
+ };
82
+ export class RolloutScanner extends JsonlScanner {
83
+ constructor(opts = {}) {
84
+ super(fold, { ...opts, noun: "Codex rollout" });
85
+ }
86
+ }
87
+ /** Thread ids are UUIDs (v7 in practice). */
88
+ export const CODEX_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
89
+ /** Creation time of a UUIDv7 (its first 48 bits, epoch ms), else undefined. */
90
+ export function uuidV7Ms(id) {
91
+ if (id[14] !== "7")
92
+ return undefined;
93
+ const ms = parseInt(id.slice(0, 8) + id.slice(9, 13), 16);
94
+ return Number.isFinite(ms) ? ms : undefined;
95
+ }
96
+ /**
97
+ * `rollout-2026-10-20T14-07-46-<uuid>.jsonl[.zst]`; a reverted thread's file
98
+ * appends `_<rollout id>` to the thread id. Returns the thread id.
99
+ */
100
+ export function rolloutThreadId(name) {
101
+ const m = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f-]{36})(?:_[0-9a-f-]{36})?\.jsonl(?:\.zst)?$/i.exec(name);
102
+ return m && CODEX_ID.test(m[1]) ? m[1].toLowerCase() : undefined;
103
+ }
104
+ /** Only threads a person started (not sub-agents, reviews, `codex exec` or MCP runs) go into the picker. */
105
+ export function isUserThread(r) {
106
+ if (r.parentThreadId)
107
+ return false;
108
+ if (r.threadSource !== undefined && r.threadSource !== "user")
109
+ return false;
110
+ if (r.source !== undefined && typeof r.source !== "string")
111
+ return false;
112
+ return r.source !== "exec" && r.source !== "mcp";
113
+ }
114
+ /**
115
+ * `<codexHome>/session_index.jsonl`: `{"id","thread_name","updated_at"}` per
116
+ * line, appended on every rename; the last entry for an id wins. Re-read only
117
+ * when its size or mtime changes.
118
+ */
119
+ export class SessionIndex {
120
+ key = "";
121
+ names = new Map();
122
+ path;
123
+ constructor(path) {
124
+ this.path = path;
125
+ }
126
+ async read() {
127
+ const st = await stat(this.path).catch(() => undefined);
128
+ const key = st ? `${st.size}:${st.mtimeMs}` : "";
129
+ if (key === this.key)
130
+ return this.names;
131
+ const names = new Map();
132
+ const text = st ? await readFile(this.path, "utf8").catch(() => "") : "";
133
+ for (const line of text.split("\n")) {
134
+ if (!line.trim())
135
+ continue;
136
+ try {
137
+ const o = JSON.parse(line);
138
+ if (typeof o.id !== "string")
139
+ continue;
140
+ const name = typeof o.thread_name === "string" ? o.thread_name.trim() : "";
141
+ if (name)
142
+ names.set(o.id.toLowerCase(), name);
143
+ else
144
+ names.delete(o.id.toLowerCase());
145
+ }
146
+ catch {
147
+ // a torn last line while Codex appends; the next read gets it
148
+ }
149
+ }
150
+ this.key = key;
151
+ this.names = names;
152
+ return names;
153
+ }
154
+ }