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,363 @@
|
|
|
1
|
+
// OpenAI Codex source: dormant sessions from rollouts
|
|
2
|
+
// (<codexHome>/sessions/YYYY/MM/DD/rollout-*.jsonl), live ones from held
|
|
3
|
+
// thread locks (codex-live.ts), names from session_index.jsonl. Codex writes no
|
|
4
|
+
// status registry: busy = a turn is open in the rollout, else idle. Prompts
|
|
5
|
+
// waiting for approval are not persisted, so they show as busy.
|
|
6
|
+
// Formats: docs/design.md "Codex data".
|
|
7
|
+
import { watch as fsWatch } from "node:fs";
|
|
8
|
+
import { readdir, realpath, stat } from "node:fs/promises";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { basename, join } from "node:path";
|
|
11
|
+
import { createProcessTable } from "../desktop/process-table.js";
|
|
12
|
+
import { assignLive, isDaemon, looksLikeCodex, scanCodexProcesses } from "./codex-live.js";
|
|
13
|
+
import { CODEX_ID, isUserThread, RolloutScanner, rolloutThreadId, SessionIndex, uuidV7Ms } from "./codex-rollout.js";
|
|
14
|
+
import { GitReader } from "./git.js";
|
|
15
|
+
import { onceLogger } from "./log.js";
|
|
16
|
+
export const codexResumeCommand = (id) => `codex resume ${id}`;
|
|
17
|
+
export function defaultCodexDir(env = process.env) {
|
|
18
|
+
return env.CODEX_HOME || join(homedir(), ".codex");
|
|
19
|
+
}
|
|
20
|
+
function defaultIsCodexSessionProcess(procRoot) {
|
|
21
|
+
const table = procRoot ? createProcessTable({ platform: "linux", procRoot }) : createProcessTable();
|
|
22
|
+
return async (pid) => {
|
|
23
|
+
const argv = await table.argv(pid);
|
|
24
|
+
return !!argv && looksLikeCodex(argv, await table.exe(pid)) && !isDaemon(argv);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
28
|
+
export { uuidV7Ms };
|
|
29
|
+
export function codexSession(id, r, opts) {
|
|
30
|
+
const live = opts.live;
|
|
31
|
+
const cwd = r?.cwd ?? live?.cwd ?? "";
|
|
32
|
+
const s = {
|
|
33
|
+
id,
|
|
34
|
+
source: "codex",
|
|
35
|
+
cwd,
|
|
36
|
+
repo: basename(cwd),
|
|
37
|
+
status: live ? (r?.turnOpen ? "busy" : "idle") : "dormant",
|
|
38
|
+
// The newest line, else the file's mtime, else (a fresh thread has no rollout yet) the id's creation time.
|
|
39
|
+
lastActivityAt: r?.lastTimestamp || Math.floor(opts.mtimeMs ?? 0) || (uuidV7Ms(id) ?? 0),
|
|
40
|
+
cacheTtlMs: 0,
|
|
41
|
+
cacheExpiresAt: 0,
|
|
42
|
+
live: !!live,
|
|
43
|
+
prLinks: [],
|
|
44
|
+
resumeCommand: codexResumeCommand(id),
|
|
45
|
+
};
|
|
46
|
+
if (live)
|
|
47
|
+
s.pid = live.pid;
|
|
48
|
+
// Stop only where the process is known for sure, never a directory guess.
|
|
49
|
+
if (live?.exact)
|
|
50
|
+
s.canEnd = true;
|
|
51
|
+
const title = opts.name ?? r?.firstUserMessage;
|
|
52
|
+
if (title)
|
|
53
|
+
s.title = title;
|
|
54
|
+
if (r?.branch)
|
|
55
|
+
s.branch = r.branch;
|
|
56
|
+
const git = opts.git;
|
|
57
|
+
if (git) {
|
|
58
|
+
s.repo = basename(git.root);
|
|
59
|
+
s.repoRoot = git.root;
|
|
60
|
+
// A live session is on the repo's current branch; a parked one keeps the one it recorded.
|
|
61
|
+
if (live || !s.branch)
|
|
62
|
+
s.branch = git.branch;
|
|
63
|
+
}
|
|
64
|
+
return s;
|
|
65
|
+
}
|
|
66
|
+
export function createCodexSource(opts = {}) {
|
|
67
|
+
const codexDir = opts.codexDir ?? defaultCodexDir();
|
|
68
|
+
const sessionsDir = join(codexDir, "sessions");
|
|
69
|
+
const lockDir = join(codexDir, "thread-writer-locks");
|
|
70
|
+
const scanProcs = opts.scan ?? ((dir) => scanCodexProcesses(dir, opts.procRoot));
|
|
71
|
+
const pollMs = opts.pollMs ?? 2000;
|
|
72
|
+
const cacheMs = opts.cacheMs ?? pollMs;
|
|
73
|
+
const debounceMs = opts.debounceMs ?? 200;
|
|
74
|
+
const maxDormant = opts.maxDormant ?? 200;
|
|
75
|
+
const warn = onceLogger(opts.log ?? console.warn);
|
|
76
|
+
const scanner = new RolloutScanner({ warn });
|
|
77
|
+
const git = new GitReader({ ttlMs: opts.gitTtlMs ?? 2000 });
|
|
78
|
+
const index = new SessionIndex(join(codexDir, "session_index.jsonl"));
|
|
79
|
+
const isCodexSessionProcess = opts.isCodexSessionProcess ?? defaultIsCodexSessionProcess(opts.procRoot);
|
|
80
|
+
const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
81
|
+
const pathById = new Map();
|
|
82
|
+
const realCwd = new Map();
|
|
83
|
+
async function real(p) {
|
|
84
|
+
let r = realCwd.get(p);
|
|
85
|
+
if (r === undefined) {
|
|
86
|
+
r = await realpath(p).catch(() => p);
|
|
87
|
+
realCwd.set(p, r);
|
|
88
|
+
}
|
|
89
|
+
return r;
|
|
90
|
+
}
|
|
91
|
+
async function subdirs(dir) {
|
|
92
|
+
try {
|
|
93
|
+
return (await readdir(dir, { withFileTypes: true }))
|
|
94
|
+
.filter((d) => d.isDirectory())
|
|
95
|
+
.map((d) => d.name)
|
|
96
|
+
.sort()
|
|
97
|
+
.reverse();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Rollout files, newest day first, stopping once `limit` files are found. */
|
|
104
|
+
async function rolloutFiles(limit = Infinity) {
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const y of await subdirs(sessionsDir))
|
|
107
|
+
for (const m of await subdirs(join(sessionsDir, y)))
|
|
108
|
+
for (const d of await subdirs(join(sessionsDir, y, m))) {
|
|
109
|
+
if (out.length >= limit)
|
|
110
|
+
return out;
|
|
111
|
+
const dir = join(sessionsDir, y, m, d);
|
|
112
|
+
const names = await readdir(dir).catch(() => []);
|
|
113
|
+
const day = [];
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
const id = rolloutThreadId(name);
|
|
116
|
+
if (!id)
|
|
117
|
+
continue;
|
|
118
|
+
const path = join(dir, name);
|
|
119
|
+
const st = await stat(path).catch(() => undefined);
|
|
120
|
+
if (!st?.isFile())
|
|
121
|
+
continue;
|
|
122
|
+
// A thread's plain file wins over a compressed twin.
|
|
123
|
+
const prev = day.find((f) => f.id === id);
|
|
124
|
+
if (prev && !prev.path.endsWith(".zst"))
|
|
125
|
+
continue;
|
|
126
|
+
if (prev)
|
|
127
|
+
day.splice(day.indexOf(prev), 1);
|
|
128
|
+
day.push({ id, path, mtimeMs: st.mtimeMs });
|
|
129
|
+
}
|
|
130
|
+
for (const f of day)
|
|
131
|
+
pathById.set(f.id, f.path);
|
|
132
|
+
out.push(...day);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/** id → when a full walk last failed to find it (fresh threads have no rollout yet). */
|
|
137
|
+
const missing = new Map();
|
|
138
|
+
async function findRollout(id) {
|
|
139
|
+
const cached = pathById.get(id);
|
|
140
|
+
if (cached && (await stat(cached).catch(() => undefined)))
|
|
141
|
+
return cached;
|
|
142
|
+
pathById.delete(id);
|
|
143
|
+
// A v7 thread id carries its creation time, and the file sits in that
|
|
144
|
+
// (local) day's directory: look there first, a day either side for safety.
|
|
145
|
+
const created = uuidV7Ms(id);
|
|
146
|
+
if (created !== undefined) {
|
|
147
|
+
for (const delta of [0, -1, 1]) {
|
|
148
|
+
const d = new Date(created + delta * 86_400_000);
|
|
149
|
+
const dir = join(sessionsDir, String(d.getFullYear()), pad(d.getMonth() + 1), pad(d.getDate()));
|
|
150
|
+
const names = await readdir(dir).catch(() => []);
|
|
151
|
+
const hits = names.filter((n) => rolloutThreadId(n) === id).sort((a, b) => Number(a.endsWith(".zst")) - Number(b.endsWith(".zst")));
|
|
152
|
+
if (hits[0]) {
|
|
153
|
+
pathById.set(id, join(dir, hits[0]));
|
|
154
|
+
return pathById.get(id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const failedAt = missing.get(id);
|
|
159
|
+
if (failedAt !== undefined && Date.now() - failedAt < 60_000)
|
|
160
|
+
return undefined;
|
|
161
|
+
await rolloutFiles();
|
|
162
|
+
if (!pathById.has(id))
|
|
163
|
+
missing.set(id, Date.now());
|
|
164
|
+
else
|
|
165
|
+
missing.delete(id);
|
|
166
|
+
return pathById.get(id);
|
|
167
|
+
}
|
|
168
|
+
async function liveThreads() {
|
|
169
|
+
const scan = await scanProcs(lockDir).catch((e) => {
|
|
170
|
+
warn(`excaliagent: reading Codex processes failed: ${e.message}`, "codex-scan");
|
|
171
|
+
return undefined;
|
|
172
|
+
});
|
|
173
|
+
if (!scan?.locks.length)
|
|
174
|
+
return [];
|
|
175
|
+
const infos = new Map();
|
|
176
|
+
for (const l of scan.locks) {
|
|
177
|
+
const path = await findRollout(l.threadId);
|
|
178
|
+
infos.set(l.threadId, path ? await scanner.scan(path) : undefined);
|
|
179
|
+
}
|
|
180
|
+
const cwds = new Map();
|
|
181
|
+
for (const [id, r] of infos)
|
|
182
|
+
cwds.set(id, r?.cwd ? await real(r.cwd) : undefined);
|
|
183
|
+
// A thread without a rollout yet is a fresh TUI's; a thread with one must be a person's.
|
|
184
|
+
return assignLive(scan.locks, scan.procs, (id) => cwds.get(id), (id) => {
|
|
185
|
+
const r = infos.get(id);
|
|
186
|
+
return !r || isUserThread(r);
|
|
187
|
+
}, (id) => infos.get(id)?.lastTimestamp);
|
|
188
|
+
}
|
|
189
|
+
/** One look at the live threads and their sessions, reused for cacheMs. */
|
|
190
|
+
let look;
|
|
191
|
+
let looking;
|
|
192
|
+
async function current(fresh = false) {
|
|
193
|
+
if (!fresh && look && Date.now() - look.at < cacheMs)
|
|
194
|
+
return look;
|
|
195
|
+
looking ??= (async () => {
|
|
196
|
+
const threads = await liveThreads();
|
|
197
|
+
const sessions = await build(threads);
|
|
198
|
+
look = { at: Date.now(), threads, sessions };
|
|
199
|
+
return look;
|
|
200
|
+
})().finally(() => (looking = undefined));
|
|
201
|
+
return looking;
|
|
202
|
+
}
|
|
203
|
+
async function list() {
|
|
204
|
+
return (await current()).sessions;
|
|
205
|
+
}
|
|
206
|
+
async function build(threads) {
|
|
207
|
+
const names = await index.read();
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const live of threads) {
|
|
210
|
+
if (!CODEX_ID.test(live.threadId))
|
|
211
|
+
continue;
|
|
212
|
+
const path = pathById.get(live.threadId);
|
|
213
|
+
const r = path ? await scanner.scan(path) : undefined;
|
|
214
|
+
const st = path && !r?.lastTimestamp ? await stat(path).catch(() => undefined) : undefined;
|
|
215
|
+
const cwd = r?.cwd ?? live.cwd;
|
|
216
|
+
const name = names.get(live.threadId);
|
|
217
|
+
out.push(codexSession(live.threadId, r, {
|
|
218
|
+
live,
|
|
219
|
+
...(st ? { mtimeMs: st.mtimeMs } : {}),
|
|
220
|
+
...(name ? { name } : {}),
|
|
221
|
+
...(cwd ? { git: await git.get(cwd) } : {}),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
225
|
+
}
|
|
226
|
+
async function dormant(f, r, names) {
|
|
227
|
+
const name = names.get(f.id);
|
|
228
|
+
return codexSession(f.id, r, {
|
|
229
|
+
mtimeMs: f.mtimeMs,
|
|
230
|
+
...(name ? { name } : {}),
|
|
231
|
+
...(r.cwd ? { git: await git.get(r.cwd) } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function listDormant() {
|
|
235
|
+
const live = new Set((await current()).threads.map((t) => t.threadId));
|
|
236
|
+
const names = await index.read();
|
|
237
|
+
const files = (await rolloutFiles()).filter((f) => !live.has(f.id)).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const f of files) {
|
|
240
|
+
if (out.length >= maxDormant)
|
|
241
|
+
break;
|
|
242
|
+
if (!CODEX_ID.test(f.id))
|
|
243
|
+
continue;
|
|
244
|
+
const r = await scanner.scan(f.path);
|
|
245
|
+
if (!r || !isUserThread(r))
|
|
246
|
+
continue;
|
|
247
|
+
out.push(await dormant(f, r, names));
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
async function get(id) {
|
|
252
|
+
if (!CODEX_ID.test(id))
|
|
253
|
+
return undefined;
|
|
254
|
+
id = id.toLowerCase();
|
|
255
|
+
const live = (await list()).find((s) => s.id === id);
|
|
256
|
+
if (live)
|
|
257
|
+
return live;
|
|
258
|
+
const path = await findRollout(id);
|
|
259
|
+
const st = path ? await stat(path).catch(() => undefined) : undefined;
|
|
260
|
+
const r = path ? await scanner.scan(path) : undefined;
|
|
261
|
+
if (!path || !st || !r)
|
|
262
|
+
return undefined;
|
|
263
|
+
return dormant({ id, path, mtimeMs: st.mtimeMs }, r, await index.read());
|
|
264
|
+
}
|
|
265
|
+
function watch(cb) {
|
|
266
|
+
let closed = false;
|
|
267
|
+
let running = false;
|
|
268
|
+
let again = false;
|
|
269
|
+
let last;
|
|
270
|
+
let debounce;
|
|
271
|
+
let watcher;
|
|
272
|
+
const refresh = async () => {
|
|
273
|
+
if (running) {
|
|
274
|
+
again = true;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
running = true;
|
|
278
|
+
try {
|
|
279
|
+
do {
|
|
280
|
+
again = false;
|
|
281
|
+
const sessions = (await current(true)).sessions;
|
|
282
|
+
if (closed)
|
|
283
|
+
return;
|
|
284
|
+
const key = JSON.stringify(sessions);
|
|
285
|
+
if (key !== last) {
|
|
286
|
+
last = key;
|
|
287
|
+
try {
|
|
288
|
+
cb(sessions);
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
warn(`excaliagent: session watcher callback threw: ${e.message}`, "codex-watch-callback");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} while (again && !closed);
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
warn(`excaliagent: refreshing Codex sessions failed: ${e.message}`, "codex-refresh");
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
running = false;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const kick = () => {
|
|
304
|
+
if (closed)
|
|
305
|
+
return;
|
|
306
|
+
clearTimeout(debounce);
|
|
307
|
+
debounce = setTimeout(() => void refresh(), debounceMs);
|
|
308
|
+
};
|
|
309
|
+
// Lock files come and go as threads load and unload: react at once.
|
|
310
|
+
const ensureWatcher = () => {
|
|
311
|
+
if (watcher || closed)
|
|
312
|
+
return;
|
|
313
|
+
try {
|
|
314
|
+
watcher = fsWatch(lockDir, kick);
|
|
315
|
+
watcher.on("error", () => {
|
|
316
|
+
watcher?.close();
|
|
317
|
+
watcher = undefined;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
watcher = undefined; // dir missing; the poll retries
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
ensureWatcher();
|
|
325
|
+
// Turn starts and ends are only in the rollouts: poll.
|
|
326
|
+
const poll = setInterval(() => {
|
|
327
|
+
ensureWatcher();
|
|
328
|
+
void refresh();
|
|
329
|
+
}, pollMs);
|
|
330
|
+
void refresh();
|
|
331
|
+
return () => {
|
|
332
|
+
closed = true;
|
|
333
|
+
clearInterval(poll);
|
|
334
|
+
clearTimeout(debounce);
|
|
335
|
+
watcher?.close();
|
|
336
|
+
watcher = undefined;
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* SIGTERM to the TUI (or `codex exec`), as when its terminal closes. The thread
|
|
341
|
+
* stays on disk and can be resumed. Only for exact matches: a thread matched
|
|
342
|
+
* by directory is "unsupported", so a guess never ends a process.
|
|
343
|
+
*/
|
|
344
|
+
async function end(id) {
|
|
345
|
+
if (!CODEX_ID.test(id))
|
|
346
|
+
return "not-live";
|
|
347
|
+
const t = (await current()).threads.find((x) => x.threadId === id.toLowerCase());
|
|
348
|
+
if (!t)
|
|
349
|
+
return "not-live";
|
|
350
|
+
if (!t.exact)
|
|
351
|
+
return "unsupported";
|
|
352
|
+
if (!(await isCodexSessionProcess(t.pid).catch(() => false)))
|
|
353
|
+
return "not-live";
|
|
354
|
+
try {
|
|
355
|
+
kill(t.pid, "SIGTERM");
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return "not-live";
|
|
359
|
+
}
|
|
360
|
+
return "ended";
|
|
361
|
+
}
|
|
362
|
+
return { list, listDormant, get, watch, end };
|
|
363
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Incremental JSONL folding, shared by the harness adapters: keeps a byte
|
|
2
|
+
// offset per file and parses only appended bytes (a partial trailing line is
|
|
3
|
+
// kept for the next scan; a replaced, shrunk or rewritten file is rescanned).
|
|
4
|
+
// Each adapter supplies a fold: what to keep from a line, never the content.
|
|
5
|
+
// `.zst` files (Codex compresses old rollouts) are read whole and decompressed
|
|
6
|
+
// when this Node has zstd (22.15+), else skipped with a warning.
|
|
7
|
+
import { open, readFile, stat } from "node:fs/promises";
|
|
8
|
+
import * as zlib from "node:zlib";
|
|
9
|
+
import { onceLogger } from "./log.js";
|
|
10
|
+
const CHUNK = 1 << 20;
|
|
11
|
+
const NL = 0x0a;
|
|
12
|
+
const zstdDecompress = zlib.zstdDecompress;
|
|
13
|
+
/** A decompressed rollout larger than this is skipped (Codex rollouts are a few MB). */
|
|
14
|
+
export const MAX_DECOMPRESSED = 256 * 1024 * 1024;
|
|
15
|
+
function unzstd(data) {
|
|
16
|
+
return new Promise((resolve, reject) => zstdDecompress(data, { maxOutputLength: MAX_DECOMPRESSED }, (err, out) => (err ? reject(err) : resolve(out))));
|
|
17
|
+
}
|
|
18
|
+
export class JsonlScanner {
|
|
19
|
+
files = new Map();
|
|
20
|
+
inflight = new Map();
|
|
21
|
+
warn;
|
|
22
|
+
noun;
|
|
23
|
+
fold;
|
|
24
|
+
constructor(fold, opts = {}) {
|
|
25
|
+
this.fold = fold;
|
|
26
|
+
this.warn = opts.warn ?? onceLogger(opts.log ?? console.warn);
|
|
27
|
+
this.noun = opts.noun ?? "file";
|
|
28
|
+
}
|
|
29
|
+
/** Scans new bytes of `path` and returns the folded info, or undefined if the file is gone. */
|
|
30
|
+
scan(path) {
|
|
31
|
+
const running = this.inflight.get(path);
|
|
32
|
+
if (running)
|
|
33
|
+
return running;
|
|
34
|
+
const p = this.doScan(path).finally(() => this.inflight.delete(path));
|
|
35
|
+
this.inflight.set(path, p);
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
/** Read bookkeeping, for tests. */
|
|
39
|
+
stats(path) {
|
|
40
|
+
const s = this.files.get(path);
|
|
41
|
+
return s && { offset: s.offset, bytesRead: s.bytesRead };
|
|
42
|
+
}
|
|
43
|
+
forget(path) {
|
|
44
|
+
this.files.delete(path);
|
|
45
|
+
}
|
|
46
|
+
async doScan(path) {
|
|
47
|
+
let st;
|
|
48
|
+
try {
|
|
49
|
+
st = await stat(path);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
this.files.delete(path);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
let s = this.files.get(path);
|
|
56
|
+
const replaced = !s || s.ino !== st.ino || s.dev !== st.dev;
|
|
57
|
+
const shrunk = !!s && st.size < s.offset;
|
|
58
|
+
// Same size but touched: rewritten in place (appends always grow the file).
|
|
59
|
+
const rewritten = !!s && st.size === s.offset && st.mtimeMs !== s.mtimeMs;
|
|
60
|
+
if (!s || replaced || shrunk || rewritten) {
|
|
61
|
+
s = { ino: st.ino, dev: st.dev, mtimeMs: st.mtimeMs, offset: 0, bytesRead: s?.bytesRead ?? 0, partial: Buffer.alloc(0), acc: this.fold.empty() };
|
|
62
|
+
this.files.set(path, s);
|
|
63
|
+
}
|
|
64
|
+
if (st.size > s.offset) {
|
|
65
|
+
try {
|
|
66
|
+
if (path.endsWith(".zst"))
|
|
67
|
+
await this.readCompressed(path, s, st.size);
|
|
68
|
+
else
|
|
69
|
+
await this.readFrom(path, s, st.size);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
this.warn(`excaliagent: cannot read ${this.noun} ${path}: ${e.message}`, path);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
s.mtimeMs = st.mtimeMs;
|
|
76
|
+
return this.fold.snapshot(s.acc);
|
|
77
|
+
}
|
|
78
|
+
async readFrom(path, s, end) {
|
|
79
|
+
const fh = await open(path, "r");
|
|
80
|
+
try {
|
|
81
|
+
const buf = Buffer.allocUnsafe(Math.min(CHUNK, end - s.offset));
|
|
82
|
+
while (s.offset < end) {
|
|
83
|
+
const { bytesRead } = await fh.read(buf, 0, Math.min(buf.length, end - s.offset), s.offset);
|
|
84
|
+
if (bytesRead === 0)
|
|
85
|
+
break;
|
|
86
|
+
s.offset += bytesRead;
|
|
87
|
+
s.bytesRead += bytesRead;
|
|
88
|
+
this.consume(path, s, buf.subarray(0, bytesRead));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await fh.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** A compressed file is immutable: decompress it once, whole. */
|
|
96
|
+
async readCompressed(path, s, size) {
|
|
97
|
+
s.offset = size;
|
|
98
|
+
if (!zstdDecompress) {
|
|
99
|
+
this.warn("excaliagent: this Node has no zstd (needs 22.15+); compressed .zst files are skipped", "no-zstd");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const data = await readFile(path);
|
|
103
|
+
s.bytesRead += data.length;
|
|
104
|
+
this.consume(path, s, await unzstd(data));
|
|
105
|
+
if (s.partial.length) {
|
|
106
|
+
this.parseLine(path, s.acc, s.partial);
|
|
107
|
+
s.partial = Buffer.alloc(0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
consume(path, s, chunk) {
|
|
111
|
+
let start = 0;
|
|
112
|
+
let nl = chunk.indexOf(NL, start);
|
|
113
|
+
while (nl !== -1) {
|
|
114
|
+
const piece = chunk.subarray(start, nl);
|
|
115
|
+
const line = s.partial.length ? Buffer.concat([s.partial, piece]) : piece;
|
|
116
|
+
s.partial = Buffer.alloc(0);
|
|
117
|
+
this.parseLine(path, s.acc, line);
|
|
118
|
+
start = nl + 1;
|
|
119
|
+
nl = chunk.indexOf(NL, start);
|
|
120
|
+
}
|
|
121
|
+
if (start < chunk.length)
|
|
122
|
+
s.partial = Buffer.concat([s.partial, chunk.subarray(start)]);
|
|
123
|
+
}
|
|
124
|
+
parseLine(path, acc, line) {
|
|
125
|
+
if (line.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
const text = line.toString("utf8");
|
|
128
|
+
this.fold.raw?.(acc, text);
|
|
129
|
+
if (!this.fold.interesting(text))
|
|
130
|
+
return;
|
|
131
|
+
let o;
|
|
132
|
+
try {
|
|
133
|
+
o = JSON.parse(text);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
this.warn(`excaliagent: skipping malformed line(s) in ${path}`, path);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (o && typeof o === "object" && !Array.isArray(o))
|
|
140
|
+
this.fold.apply(acc, o);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
2
|
+
/** Fail soft: one broken adapter must not take the others down. */
|
|
3
|
+
async function settle(work, warn, what) {
|
|
4
|
+
try {
|
|
5
|
+
return await work;
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
warn(`excaliagent: ${what} failed: ${e.message}`);
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function combineSources(sources, warn = console.warn) {
|
|
13
|
+
if (sources.length === 1)
|
|
14
|
+
return sources[0];
|
|
15
|
+
return {
|
|
16
|
+
async list() {
|
|
17
|
+
const lists = await Promise.all(sources.map((s) => settle(s.list(), warn, "listing sessions")));
|
|
18
|
+
return lists.flat().sort(byId);
|
|
19
|
+
},
|
|
20
|
+
watch(cb) {
|
|
21
|
+
const latest = sources.map(() => undefined);
|
|
22
|
+
const emit = () => cb(latest.flatMap((l) => l ?? []).sort(byId));
|
|
23
|
+
const offs = sources.map((s, i) => s.watch((sessions) => {
|
|
24
|
+
latest[i] = sessions;
|
|
25
|
+
emit();
|
|
26
|
+
}));
|
|
27
|
+
return () => {
|
|
28
|
+
for (const off of offs)
|
|
29
|
+
off();
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
async listDormant() {
|
|
33
|
+
const lists = await Promise.all(sources.map((s) => settle(s.listDormant(), warn, "listing dormant sessions")));
|
|
34
|
+
return lists.flat().sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
35
|
+
},
|
|
36
|
+
async get(id) {
|
|
37
|
+
for (const s of sources) {
|
|
38
|
+
const found = await s.get(id).catch(() => undefined);
|
|
39
|
+
if (found)
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
},
|
|
44
|
+
async end(id) {
|
|
45
|
+
for (const s of sources) {
|
|
46
|
+
const found = await s.get(id).catch(() => undefined);
|
|
47
|
+
if (found)
|
|
48
|
+
return s.end ? s.end(id) : "unsupported";
|
|
49
|
+
}
|
|
50
|
+
return "not-live";
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|