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.
- package/README.md +129 -30
- package/dist/server/args.js +19 -3
- package/dist/server/characters.js +540 -0
- package/dist/server/cli.js +47 -6
- package/dist/server/http.js +122 -11
- package/dist/server/hub.js +17 -3
- package/dist/server/instance.js +52 -0
- package/dist/server/panes/herdr.js +29 -3
- package/dist/server/scene-store.js +58 -7
- package/dist/server/sources/claude.js +36 -1
- package/dist/server/sources/codex-live.js +279 -0
- package/dist/server/sources/codex-rollout.js +154 -0
- package/dist/server/sources/codex.js +363 -0
- package/dist/server/sources/jsonl.js +142 -0
- package/dist/server/sources/multi.js +53 -0
- package/dist/server/sources/opencode-live.js +144 -0
- package/dist/server/sources/opencode.js +265 -0
- package/dist/server/sources/proc.js +34 -0
- package/dist/server/sources/transcript.js +9 -103
- package/dist/server/token.js +23 -0
- package/dist/shared/packs.js +62 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/{abnfDiagram-VCTEODGH-tmjYj3VY.js → abnfDiagram-VCTEODGH-BxrKsDoB.js} +1 -1
- package/dist/web/assets/{ar-SA-G6X2FPQ2-8ZGGBDRT.js → ar-SA-G6X2FPQ2-Cc6gbg88.js} +1 -1
- package/dist/web/assets/{architecture-7GRP2DOG-CsvDkWt3.js → architecture-7GRP2DOG-CIqN5I9B.js} +1 -1
- package/dist/web/assets/{architectureDiagram-5GKGNRK7-Bq-1CDhU.js → architectureDiagram-5GKGNRK7-DsXC8AqK.js} +1 -1
- package/dist/web/assets/{az-AZ-76LH7QW2-DbdS_q-w.js → az-AZ-76LH7QW2-BH8VzNec.js} +1 -1
- package/dist/web/assets/{bg-BG-XCXSNQG7-CANqXhG1.js → bg-BG-XCXSNQG7-ClFUbics.js} +1 -1
- package/dist/web/assets/{blockDiagram-I7D4REHJ-DtYn-xOl.js → blockDiagram-I7D4REHJ-CltS-Zt2.js} +1 -1
- package/dist/web/assets/{bn-BD-2XOGV67Q-DJdq0B1R.js → bn-BD-2XOGV67Q-BVUiOO4B.js} +1 -1
- package/dist/web/assets/{c4Diagram-7LVT6UL2-DW6Pzj1e.js → c4Diagram-7LVT6UL2-C-C_SvWm.js} +1 -1
- package/dist/web/assets/{ca-ES-6MX7JW3Y-B7pUqerV.js → ca-ES-6MX7JW3Y-CEK8mB8J.js} +1 -1
- package/dist/web/assets/channel-Dom8D_0o.js +1 -0
- package/dist/web/assets/{chunk-4HAMMTFA-_AKNu1--.js → chunk-4HAMMTFA-Bfdmb0Hg.js} +1 -1
- package/dist/web/assets/{chunk-75Z2AOVW-BoMl8Mqw.js → chunk-75Z2AOVW-NYwkutzS.js} +1 -1
- package/dist/web/assets/{chunk-DU6HZSFF-DYHa8ENH.js → chunk-DU6HZSFF-SqE0xdu5.js} +1 -1
- package/dist/web/assets/{chunk-EIO257PC--adgQBo2.js → chunk-EIO257PC-DpOeKaLq.js} +1 -1
- package/dist/web/assets/{chunk-F27PBJKO-CvQi01qZ.js → chunk-F27PBJKO-g9bJrSwZ.js} +1 -1
- package/dist/web/assets/{chunk-GMAD6QVW-ojaxPuMG.js → chunk-GMAD6QVW-B1y2ol5w.js} +1 -1
- package/dist/web/assets/{chunk-GVQU2GXP-DYAeD1IL.js → chunk-GVQU2GXP-BUjRNRXo.js} +1 -1
- package/dist/web/assets/{chunk-IMKFNOWR-oVPWqbpV.js → chunk-IMKFNOWR-DyeQep8U.js} +1 -1
- package/dist/web/assets/{chunk-L3NEJ4N5-BelAXiIN.js → chunk-L3NEJ4N5-BcTVhsWp.js} +1 -1
- package/dist/web/assets/{chunk-OSK3NFVY-CATtjN8l.js → chunk-OSK3NFVY-Db7B8jPm.js} +1 -1
- package/dist/web/assets/{chunk-P2QGCYS3-CbKPCENJ.js → chunk-P2QGCYS3-Cx5iBC5z.js} +1 -1
- package/dist/web/assets/{chunk-POPQ4Y6H-BfOCSezn.js → chunk-POPQ4Y6H-CqVRZDA8.js} +1 -1
- package/dist/web/assets/{chunk-PWAF6VOD-C7O-JWJt.js → chunk-PWAF6VOD-Cf2j9yew.js} +1 -1
- package/dist/web/assets/{chunk-SHT3W25Y-BfEpPCTX.js → chunk-SHT3W25Y-0_N13xgE.js} +1 -1
- package/dist/web/assets/{chunk-SVP7TREG-CaeKcCSu.js → chunk-SVP7TREG-DkRJF9jr.js} +1 -1
- package/dist/web/assets/{chunk-TICWLB2K-Bx6AcLB_.js → chunk-TICWLB2K-D6OC50xH.js} +1 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-BMEkgkEJ.js +1 -0
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-BMEkgkEJ.js +1 -0
- package/dist/web/assets/{cs-CZ-2BRQDIVT-DF8hZ2mh.js → cs-CZ-2BRQDIVT-DWt1RUsP.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-Dol6Weuo.js → cynefin-OW5HDTMX-Dx0HzRCl.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-5FMLGOSQ-CbOJ2lks.js → cynefinDiagram-5FMLGOSQ-C8AMQNhe.js} +1 -1
- package/dist/web/assets/{da-DK-5WZEPLOC-CgPdNz6E.js → da-DK-5WZEPLOC-BPggh0j6.js} +1 -1
- package/dist/web/assets/{dagre-GXQ25YYZ-CcuaBID7.js → dagre-GXQ25YYZ-f9qU6BFq.js} +1 -1
- package/dist/web/assets/{de-DE-XR44H4JA-DaVeYWRc.js → de-DE-XR44H4JA-B1q1pJrQ.js} +1 -1
- package/dist/web/assets/{diagram-S7CK7UJ4-BR2gIsNx.js → diagram-S7CK7UJ4-Bcq77Nl3.js} +1 -1
- package/dist/web/assets/{diagram-UQ7AKVKN-DwSZsV-Z.js → diagram-UQ7AKVKN-DXzlgVTk.js} +1 -1
- package/dist/web/assets/{diagram-VSXAHHWV-BcJpQHbj.js → diagram-VSXAHHWV-C1vz2FvT.js} +1 -1
- package/dist/web/assets/{diagram-VX7I27RA-CvjRTkwu.js → diagram-VX7I27RA-_c4ucp3r.js} +1 -1
- package/dist/web/assets/{diagram-Z3DM3KII-vg0-r_P4.js → diagram-Z3DM3KII-cUg91K-F.js} +1 -1
- package/dist/web/assets/{dist-D-3cz42D.js → dist-DcwmvBdV.js} +4 -4
- package/dist/web/assets/{ebnfDiagram-PWID7BFC-DZCaMmMd.js → ebnfDiagram-PWID7BFC-9iWfohHg.js} +1 -1
- package/dist/web/assets/{el-GR-BZB4AONW-nYncjO-L.js → el-GR-BZB4AONW-CrhwvNDD.js} +1 -1
- package/dist/web/assets/{en-B4ZKOASM-DqIz0rJz.js → en-B4ZKOASM-DCjnkz5L.js} +1 -1
- package/dist/web/assets/{erDiagram-RLTQ6QDP-DgyrjoaZ.js → erDiagram-RLTQ6QDP-A72uVlS3.js} +1 -1
- package/dist/web/assets/{es-ES-U4NZUMDT-e3WeRDLl.js → es-ES-U4NZUMDT-D0sRCoDL.js} +1 -1
- package/dist/web/assets/{eu-ES-A7QVB2H4-CwXv1t13.js → eu-ES-A7QVB2H4-DLG-liPb.js} +1 -1
- package/dist/web/assets/{eventmodeling-NTZA5JFV-DCTWpAwP.js → eventmodeling-NTZA5JFV-CIKZvDAj.js} +1 -1
- package/dist/web/assets/{fa-IR-HGAKTJCU-D-9plwKu.js → fa-IR-HGAKTJCU-BW3pPOVT.js} +1 -1
- package/dist/web/assets/{fi-FI-Z5N7JZ37-BTK1rs8G.js → fi-FI-Z5N7JZ37-D2XaqwPp.js} +1 -1
- package/dist/web/assets/flowDiagram-HODETNUW-DhQSc3KP.js +1 -0
- package/dist/web/assets/{fr-FR-RHASNOE6-CAuUMPtN.js → fr-FR-RHASNOE6-B-BjRA7r.js} +1 -1
- package/dist/web/assets/{ganttDiagram-EL5Y4UJY-bFfu4BMj.js → ganttDiagram-EL5Y4UJY-cgwUfr0G.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-DIl4-oqK.js → gitGraph-4MIJSDKK-CeKopPn3.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-WWUBYQGX-ho3FT8XP.js → gitGraphDiagram-WWUBYQGX-BWjAy7ei.js} +1 -1
- package/dist/web/assets/{gl-ES-HMX3MZ6V-DlKPdjtk.js → gl-ES-HMX3MZ6V-DYUAqB0N.js} +1 -1
- package/dist/web/assets/{he-IL-6SHJWFNN-CLyP0zBo.js → he-IL-6SHJWFNN-CXkYqbuu.js} +1 -1
- package/dist/web/assets/{hi-IN-IWLTKZ5I-kvhBHNzH.js → hi-IN-IWLTKZ5I-BwVBsHWG.js} +1 -1
- package/dist/web/assets/{hu-HU-A5ZG7DT2-Dtd-o-RJ.js → hu-HU-A5ZG7DT2-DkmlE7Aa.js} +1 -1
- package/dist/web/assets/{id-ID-SAP4L64H-VZ3IV-8B.js → id-ID-SAP4L64H-CwkytHX_.js} +1 -1
- package/dist/web/assets/{image-GAAHSSAO-DWvjHHd3.js → image-GAAHSSAO-u-hBxkaK.js} +1 -1
- package/dist/web/assets/index-D43I17T9.js +212 -0
- package/dist/web/assets/{index-DtRVLazd.css → index-DxXUknKX.css} +1 -1
- package/dist/web/assets/{info-A6RAGUB7-DN8pwhKt.js → info-A6RAGUB7-DmadBmci.js} +1 -1
- package/dist/web/assets/{infoDiagram-27XIBGKW-i2JHBJKe.js → infoDiagram-27XIBGKW-D93CKwhy.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-5VMMS53U-DLLdu9-W.js → ishikawaDiagram-5VMMS53U-BQ-x5YXp.js} +1 -1
- package/dist/web/assets/{it-IT-JPQ66NNP-6SULWnRA.js → it-IT-JPQ66NNP-Bc-Qa6KI.js} +1 -1
- package/dist/web/assets/{ja-JP-DBVTYXUO-BbgutQYO.js → ja-JP-DBVTYXUO-BPmQCryQ.js} +1 -1
- package/dist/web/assets/{journeyDiagram-3NMN7TZE-DsysKJxX.js → journeyDiagram-3NMN7TZE-CO_LXx1E.js} +1 -1
- package/dist/web/assets/{kaa-6HZHGXH3-Bc6yhZWQ.js → kaa-6HZHGXH3-DuBFVAjv.js} +1 -1
- package/dist/web/assets/{kab-KAB-ZGHBKWFO-BaICxis-.js → kab-KAB-ZGHBKWFO-DhEFzSTh.js} +1 -1
- package/dist/web/assets/{kanban-definition-UXKFOSKX-w3k_6RhT.js → kanban-definition-UXKFOSKX-B72Jztau.js} +1 -1
- package/dist/web/assets/{kk-KZ-P5N5QNE5-BVsfmPoG.js → kk-KZ-P5N5QNE5-CW7PxU2h.js} +1 -1
- package/dist/web/assets/{km-KH-HSX4SM5Z-BS1M1bUx.js → km-KH-HSX4SM5Z-CYy7yySD.js} +1 -1
- package/dist/web/assets/{ko-KR-MTYHY66A-C0x16zQm.js → ko-KR-MTYHY66A-C5O645Qe.js} +1 -1
- package/dist/web/assets/{ku-TR-6OUDTVRD-BWI9z5E8.js → ku-TR-6OUDTVRD-CKcd_iWr.js} +1 -1
- package/dist/web/assets/{line-BRXlgUrO.js → line-Ckd3sAmP.js} +1 -1
- package/dist/web/assets/{lt-LT-XHIRWOB4-CTDq1zFR.js → lt-LT-XHIRWOB4-ya2_xWiy.js} +1 -1
- package/dist/web/assets/{lv-LV-5QDEKY6T-h8b-E15B.js → lv-LV-5QDEKY6T-cYYwyzmz.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-Da_XEVmR.js → mermaid-parser.core-Dbqkvrw-.js} +3 -3
- package/dist/web/assets/{mindmap-definition-YA3MSWOX-DUBdgu2g.js → mindmap-definition-YA3MSWOX-B4onxNgk.js} +1 -1
- package/dist/web/assets/{mr-IN-CRQNXWMA-dTdkz007.js → mr-IN-CRQNXWMA-CfaHpVWe.js} +1 -1
- package/dist/web/assets/{my-MM-5M5IBNSE-CmYwTRil.js → my-MM-5M5IBNSE-BfKiTATq.js} +1 -1
- package/dist/web/assets/{nb-NO-T6EIAALU-DLLYcK-s.js → nb-NO-T6EIAALU-Cz3yDFYg.js} +1 -1
- package/dist/web/assets/{nl-NL-IS3SIHDZ-mtv15Mmm.js → nl-NL-IS3SIHDZ-DDX026n5.js} +1 -1
- package/dist/web/assets/{nn-NO-6E72VCQL-CJDTT3kB.js → nn-NO-6E72VCQL-BcokL_SN.js} +1 -1
- package/dist/web/assets/{oc-FR-POXYY2M6-DdHJTqH4.js → oc-FR-POXYY2M6-bNFsGpEp.js} +1 -1
- package/dist/web/assets/{pa-IN-N4M65BXN-DxzzhbLu.js → pa-IN-N4M65BXN-iIHN9Z8W.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-CzX42SOE.js → packet-AYTQ26CC-BzBpP9hg.js} +1 -1
- package/dist/web/assets/{pegDiagram-XKGWAZYB-1R2Hd3Th.js → pegDiagram-XKGWAZYB-BPScRPVZ.js} +1 -1
- package/dist/web/assets/percentages-BXMCSKIN-EDfR-TFr.js +1 -0
- package/dist/web/assets/{pie-WAS4IAKB-DEI2uslz.js → pie-WAS4IAKB-N-9RHtKL.js} +1 -1
- package/dist/web/assets/{pieDiagram-E7YTZNPT-DOQnFSrs.js → pieDiagram-E7YTZNPT-Bpd_yCG-.js} +1 -1
- package/dist/web/assets/{pl-PL-T2D74RX3-Dpyab0lI.js → pl-PL-T2D74RX3-DeqOx-a4.js} +1 -1
- package/dist/web/assets/{pt-BR-5N22H2LF-DOGrUA6Y.js → pt-BR-5N22H2LF-DG1PCIkb.js} +1 -1
- package/dist/web/assets/{pt-PT-UZXXM6DQ-DAoNiiOg.js → pt-PT-UZXXM6DQ-BsUJk2fq.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AXDQQJYC-CQE1OxRJ.js → quadrantDiagram-AXDQQJYC-CSLCp0xS.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-B-er2ORT.js → radar-RG4KPBEZ-rKULJroO.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-D1Veuu81.js → railroad-74A4TZTK-hDn0WPg3.js} +1 -1
- package/dist/web/assets/{railroad-abnf-HS5TGJTU-DTxLo5az.js → railroad-abnf-HS5TGJTU-DfI0hiCR.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-LZEXJU2U-BmP2IZGe.js → railroad-ebnf-LZEXJU2U-paFQAnqR.js} +1 -1
- package/dist/web/assets/{railroad-peg-WCYAUIDC-Do60gMTC.js → railroad-peg-WCYAUIDC-B-2Um3QI.js} +1 -1
- package/dist/web/assets/{railroadDiagram-O6MQD6OU-5I8Qk1dI.js → railroadDiagram-O6MQD6OU-WkZPN5MA.js} +1 -1
- package/dist/web/assets/{requirementDiagram-BXWQKSXE-CIIM8YmC.js → requirementDiagram-BXWQKSXE-BJULFhuR.js} +1 -1
- package/dist/web/assets/{ro-RO-JPDTUUEW-DR9yu2MV.js → ro-RO-JPDTUUEW-yKjTMizq.js} +1 -1
- package/dist/web/assets/{ru-RU-B4JR7IUQ-B9uIhZUd.js → ru-RU-B4JR7IUQ-DITGdMQL.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-P5KCCOFB-BdGmiiCQ.js → sankeyDiagram-P5KCCOFB-b_vfju3s.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WJ2MYXX4-BeCmgCpG.js → sequenceDiagram-WJ2MYXX4-D5n2aEN6.js} +1 -1
- package/dist/web/assets/{si-LK-N5RQ5JYF-C7DYCsOB.js → si-LK-N5RQ5JYF-DaFciStR.js} +1 -1
- package/dist/web/assets/{sk-SK-C5VTKIMK-BbBBLLCs.js → sk-SK-C5VTKIMK-CpyCIFtD.js} +1 -1
- package/dist/web/assets/{sl-SI-NN7IZMDC-BIaW2Iuj.js → sl-SI-NN7IZMDC-cnwvs7FN.js} +1 -1
- package/dist/web/assets/{stateDiagram-D77RDMKH-CmENpSfH.js → stateDiagram-D77RDMKH-CbFjpKDm.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-MOkTt8Mv.js +1 -0
- package/dist/web/assets/subset-shared.chunk-CWAaXrHb.js +1 -0
- package/dist/web/assets/{subset-worker.chunk-Bi16Jh4x.js → subset-worker.chunk-Cebu2JDe.js} +1 -1
- package/dist/web/assets/{sv-SE-XGPEYMSR-D5A1U3nk.js → sv-SE-XGPEYMSR-dOntI1Fk.js} +1 -1
- package/dist/web/assets/{swimlanes-42K2YHIH-_ymwQg8o.js → swimlanes-42K2YHIH-DtmsuzRR.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-v2ue4raa.js +8 -0
- package/dist/web/assets/{ta-IN-2NMHFXQM-BDzXUwiE.js → ta-IN-2NMHFXQM-Comju6i2.js} +1 -1
- package/dist/web/assets/{th-TH-HPSO5L25-DdVnWuWW.js → th-TH-HPSO5L25-uPCBvbue.js} +1 -1
- package/dist/web/assets/{timeline-definition-24CTP7MA-CRcZde4k.js → timeline-definition-24CTP7MA-DKZOmZFD.js} +1 -1
- package/dist/web/assets/{tr-TR-DEFEU3FU-BQwfGN3o.js → tr-TR-DEFEU3FU-LaP2C4go.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-BHZZuSXr.js → treeView-Q6P3EWNA-DJii9IFr.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-DAzotEQZ.js → treemap-WGGIJYW6-U42pUP2Q.js} +1 -1
- package/dist/web/assets/{uk-UA-QMV73CPH-CFYnWqHD.js → uk-UA-QMV73CPH-u-WBulor.js} +1 -1
- package/dist/web/assets/{vennDiagram-4TSXK5OY-BsNz9Qnn.js → vennDiagram-4TSXK5OY-C8_zJPdp.js} +1 -1
- package/dist/web/assets/{vi-VN-M7AON7JQ-DO3BySmm.js → vi-VN-M7AON7JQ-DOSv3aq5.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-Cc2r8Vp8.js → wardley-WFR3VGLG-DU89dbbh.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VM6X3IG4-47UvJO4N.js → wardleyDiagram-VM6X3IG4-BGnASelu.js} +1 -1
- package/dist/web/assets/{xychartDiagram-S5SC5T6Z-CVl63zr-.js → xychartDiagram-S5SC5T6Z-js8-abv7.js} +1 -1
- package/dist/web/assets/{zh-CN-LNUGB5OW-DKJ2CF-O.js → zh-CN-LNUGB5OW-B8soJHf3.js} +1 -1
- package/dist/web/assets/{zh-HK-E62DVLB3-DHW8OH5j.js → zh-HK-E62DVLB3-DKByBmSJ.js} +1 -1
- package/dist/web/assets/{zh-TW-RAJ6MFWO-B9cMDifZ.js → zh-TW-RAJ6MFWO-CYBStGL6.js} +1 -1
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.svg +12 -0
- package/dist/web/index.html +6 -3
- package/package.json +5 -3
- package/dist/web/assets/channel-gFr9jEgP.js +0 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-9J2CNLBI.js +0 -1
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-9J2CNLBI.js +0 -1
- package/dist/web/assets/flowDiagram-HODETNUW--2QUGuZ7.js +0 -1
- package/dist/web/assets/index-B2hGCC2V.js +0 -212
- package/dist/web/assets/percentages-BXMCSKIN-Br7824d3.js +0 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-BWgn52cG.js +0 -1
- package/dist/web/assets/subset-shared.chunk-d2GNuFHV.js +0 -1
- 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
|
+
}
|