chainlesschain 0.162.147 → 0.162.149

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 (257) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DywJ7xTv.js → AIOps-BJif14yR.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BTlqcY-q.js → ActionButton-CXek6gJY.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-FdG1Mvwy.js → Analytics-BgQhdAJi.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CVsnbvN1.js → AppLayout-DTYl5NtR.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-ITdpJ7vR.js → Audit-BzATQAeR.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-BfXqnXo1.js → Backup-BzdPEQhL.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-C09ip3_E.js → BaseInput-BM0KVh8E.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-qdkhB42l.js → Chat-D7fHXHkz.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-D2KfETZD.js → ChatBubbleRenderer-Wm34RR-b.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-j6WZCuff.js → Checkbox-CAiSQ9vO.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CGZ-K3uK.js → Codegen-Df40CrEe.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CIlPOAu6.js → Col-DIT2fNFU.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-sY-i-hic.js → Community-CSTIbW2k.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-DcoEfyL8.js → Compact-DtqXZZUi.js} +1 -1
  18. package/src/assets/web-panel/assets/Compliance-CUtzw6o7.js +1 -0
  19. package/src/assets/web-panel/assets/{Cowork-DLY_fkLv.js → Cowork-CezmlnmU.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-D049pZBC.js → Cron-P4BITarg.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DCXdE8M9.js → Crosschain-BRRiYwvq.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-JCvPZtjW.js → DID-L5ijB9i4.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Cg2xu3iE.js → Dashboard-CFxro4ob.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DjJLjbLh.js → Dropdown-BmAFCQ71.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DugNcSLR.js → EmailListRenderer-DJBCEuon.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-J6Nb-E30.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CQ3Ttpbl.js → Federation-DsVCpHMF.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cz0NKag4.js → FormItemContext-DP3bP5th.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-NYcMP6iz.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-B4hYN18N.js → Git-I3g_bAw9.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-DOQPCcC0.js → Governance-jHS5-ioS.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-HtLF746W.js → Inference-CPL7sfx9.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BXrk8rMm.js → KnowledgeGraph-Jf236h4C.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DS0JnD3-.js → Logs-Cm2tcSKg.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Cqnjihrz.js → Marketplace-CZK82rhi.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-Cx3Psk-U.js → McpTools-CZCeji7a.js} +5 -5
  37. package/src/assets/web-panel/assets/{Memory-BjyysPtj.js → Memory-3l2KVS9k.js} +2 -2
  38. package/src/assets/web-panel/assets/MobileBridge-D3Arxfeg.css +1 -0
  39. package/src/assets/web-panel/assets/MobileBridge-VYdpoLh6.js +1 -0
  40. package/src/assets/web-panel/assets/{MobileProjects-Xwf-fdOQ.js → MobileProjects-CiB9cmm1.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-Djkql5m5.js → Mtc-wCFZbBuL.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-D2BAZlu0.js → MtcAudit-1a3THIyG.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-ByqP1ZzH.js → Multisig-DMzLB2hG.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-C_kn0nE2.js → NLProgramming-D9wZbf6l.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-7l7eXHPq.js → Notes-hrFe2ZM-.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BrCDoitw.js → NotificationSettings-CHGX5Jwm.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-DdoNAxsM.js → Organization-By7FkhtY.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-Bw7R6dE3.js → Overflow--khGSzJn.js} +1 -1
  50. package/src/assets/web-panel/assets/{OverrideContext-C2r4vyBb.js → OverrideContext-BlgL9440.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-DJNtBugA.js → P2P-CAG9dH35.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dwhd5_ue.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-BYoSfhPR.js → Permissions-D35ec6JA.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-aYCYJbnH.js → PersonalDataHub-CxBF7hW_.js} +2 -2
  55. package/src/assets/web-panel/assets/{Pipeline-DJp6CprF.js → Pipeline-s6EtOO1s.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-Cq9HZweR.js → Privacy-BbJYmWmO.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-CmF0HsSt.js → ProjectInit-VGQq1jMT.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-P8V5Aea2.js → ProjectSettings-B1ew3Teh.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-D589v0q2.js → Providers-mrO47FIK.js} +1 -1
  61. package/src/assets/web-panel/assets/QrScannerModal-IJF2mHyw.js +1 -0
  62. package/src/assets/web-panel/assets/{MobileBridge-zJpPuf4E.css → QrScannerModal-NwAHNfiJ.css} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-lvRV2osB.js → QuickAsk-CU0wN_Z0.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-sb84MTpY.js → Recommend-DhN37R6G.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-D_xOX_hu.js +4 -0
  66. package/src/assets/web-panel/assets/{Reputation-d9Y3vy-W.js → Reputation-D_Ssv7uH.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-DMdhLUTp.js → Row-Kx5dKxX7.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-OL-SMDkz.js → RssFeed-BlGwqZkY.js} +2 -2
  69. package/src/assets/web-panel/assets/{Search-BjStLKq1.js → Search-hLFQzxpb.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-MeI411qr.js → Security-DMlvsVt4.js} +2 -2
  71. package/src/assets/web-panel/assets/{Services-UajosuhT.js → Services-Bd0Qsj2m.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-CGpG-QJ1.js → Skeleton-DFclq9X3.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-C6P2RPY-.js → Skills-BNtln2qY.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-B_fPprgw.js → Sla-Dch5H19G.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-CqxLjSlQ.js → SpeechSettings-2UfQcU1g.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-DixSfFOQ.js → SyncSettings-CD9YQr4i.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-PavzPBxc.js → Tasks-BNu-7MzI.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-BYdGRnfX.js → Templates-D1n9CaIR.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-BQLgnarD.js → Tenant-DenL3iBW.js} +1 -1
  80. package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
  81. package/src/assets/web-panel/assets/{TimelineRenderer-Bv7uZRM7.js → TimelineRenderer-D87IfukO.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-DxhgszRJ.js → Tokens-BkiZc8E3.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-nQYHayuc.js → Trigger-BNRSytGN.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-CvqZDuZ4.js → Trust-Bwbd4X2m.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DaR8GV2I.js → UkeySign-B8O2bs0o.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-C6Mpsokw.js → VideoEditing-C2fL8zmH.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-B57VRrWc.js → Wallet-BORYDdE1.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-BbtEEtpq.js → WebAuthn-C0V5S2FM.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-C5LwvKmH.js → WorkflowEditor-2dR0fcHL.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-MzuPR5jh.js → chat-BmuAFAn8.js} +1 -1
  91. package/src/assets/web-panel/assets/{collapseMotion-mxzzcDsg.js → collapseMotion-DVfhbsdP.js} +1 -1
  92. package/src/assets/web-panel/assets/{colors-HyW8mi_y.js → colors-CN3smMcK.js} +1 -1
  93. package/src/assets/web-panel/assets/{compact-item-DvUSzWAp.js → compact-item-DCnxb4kW.js} +1 -1
  94. package/src/assets/web-panel/assets/{createContext-4MppZl7X.js → createContext-vlR43pHn.js} +1 -1
  95. package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
  96. package/src/assets/web-panel/assets/{echarts-jxgIOFUZ.js → echarts-CL0F2SnX.js} +1 -1
  97. package/src/assets/web-panel/assets/{hasIn-D9q3CJ-u.js → hasIn-D04tHYFd.js} +1 -1
  98. package/src/assets/web-panel/assets/{icons-DP3uiYxy.js → icons-BsDmGpcT.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-C9I5MuJ0.js → index-0DnaZGkC.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Caqtu6Et.js → index-430S68MN.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-eOQO6KnV.js → index-B78xL3s2.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Ci6adKh8.js → index-BTPEZTk1.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CifKUNtV.js → index-BXRg8GFQ.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-D7TDMMfu.js → index-BasvsWDM.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DNYqw0MO.js → index-BeGDEXFs.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-m0b6Fj9q.js → index-BqhASEL4.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BF2JsbiX.js → index-BudvKQfG.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DFqvWeGc.js → index-C-6NO5il.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-SDblbKj6.js → index-C5Sxb1sE.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-UUMCA2Sq.js → index-CP9m9Ggr.js} +4 -4
  111. package/src/assets/web-panel/assets/{index--Bm6OqmU.js → index-CTOCnIQ7.js} +1 -1
  112. package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
  113. package/src/assets/web-panel/assets/{index-Dv7ffI2g.js → index-ClhAHDK3.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BcunsBIx.js → index-CnXebZLL.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CvAqCAo9.js → index-CngCC8hC.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DcMkjb92.js → index-Cr-A0FYJ.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BFGfC5RS.js → index-CsAsCPJ6.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DDZ6fP5Z.js → index-D28DeAAB.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CgIfXOD9.js → index-D5LZVZ0L.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DmZ6-suL.js → index-D65_XseV.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Cs-RKRUM.js → index-D8vwp4qp.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B994UQfE.js → index-DDwLgQY9.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Btz8pU68.js → index-DIpY0qM5.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-B5E4DhVc.js → index-DTWg-18U.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-D2jrnaq3.js → index-DTtRscUa.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BmCo2Xdp.js → index-DYAtmqiv.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-Zvtru2oE.js → index-D_n8_vfI.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CLAZs1Vd.js → index-DbADHwhr.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CucRo4Vz.js → index-DbVsQBOH.js} +1 -1
  130. package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
  131. package/src/assets/web-panel/assets/{index-C0DiL97c.js → index-HhNjnCfe.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-Bl-E4W4q.js → index-ToUh2b1-.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-ryFLxB1p.js → index-TslMTwNy.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BGQtFso0.js → index-_FIkN3jd.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-zLwYpvX0.js → index-hpvvtAZ3.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-Dvg5xYuP.js → index-jzR7Ujuw.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-xvUVuFnA.js → index-ljuBiQW9.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-CC--PMsC.js → initDefaultProps-C3wUb4je.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-DdrBjRF1.js → motion-D-jYFUNA.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-D712Gwl2.js → move-tqb8nwJ2.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-39y-keKO.js → mtc-parser-DSOjMJMz.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-6Y51gDB9.js → omit-CJ7VimIW.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-Cy6EHbq9.js → pickAttrs-qXiG1iT3.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-BYxdAm0p.js → placementArrow-CkGBcy1Z.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-C9R6q_cr.js → responsiveObserve-D3WvTlLO.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-CXUJlilB.js → slide-BEE2ftge.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-DOtrOeql.js → statusUtils-CsfBpxiG.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-D5r39o92.js → styleChecker-xPrIiJPI.js} +1 -1
  149. package/src/assets/web-panel/assets/useFlexGapSupport-C5Yd-SCR.js +1 -0
  150. package/src/assets/web-panel/assets/{useFs-BkrT-Zbc.js → useFs-D_MDS8HI.js} +1 -1
  151. package/src/assets/web-panel/assets/{useMergedState-Cl2q1fes.js → useMergedState-Dc9sO2d7.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-BwnnnZjU.js → usePersonalDataHub-BqJh-usA.js} +1 -1
  153. package/src/assets/web-panel/assets/{useRefs-DD9q4mOT.js → useRefs-CAeFRzPf.js} +1 -1
  154. package/src/assets/web-panel/assets/{useState-BK4hy8Ma.js → useState-LwExUYDO.js} +1 -1
  155. package/src/assets/web-panel/assets/{vendor-BvqAck49.js → vendor-BMxUs6UX.js} +16 -16
  156. package/src/assets/web-panel/assets/{vnode-SU-N4pum.js → vnode-chm_p-gz.js} +1 -1
  157. package/src/assets/web-panel/assets/{zoom-DHScfpTP.js → zoom-3-YDNz0Z.js} +1 -1
  158. package/src/assets/web-panel/index.html +3 -3
  159. package/src/assets/web-panel/remote-session-sw.js +43 -0
  160. package/src/command-manifest.json +22 -1
  161. package/src/commands/agent.js +44 -5
  162. package/src/commands/codeintel.js +252 -0
  163. package/src/commands/eval.js +248 -0
  164. package/src/commands/plugin.js +354 -0
  165. package/src/commands/serve.js +10 -0
  166. package/src/commands/team.js +456 -0
  167. package/src/gateways/remote-session-relay.js +160 -0
  168. package/src/gateways/ws/message-dispatcher.js +77 -2
  169. package/src/gateways/ws/remote-session-protocol.js +466 -0
  170. package/src/gateways/ws/ws-server.js +299 -0
  171. package/src/harness/golden-transcript.js +65 -0
  172. package/src/harness/mcp-client.js +6 -2
  173. package/src/harness/prompt-compressor.js +27 -1
  174. package/src/harness/remote-command-ledger.js +158 -0
  175. package/src/harness/remote-session-audit-sink.js +132 -0
  176. package/src/harness/remote-session-audit.js +110 -0
  177. package/src/harness/remote-session-crypto.js +217 -0
  178. package/src/harness/remote-session-push-apns.js +246 -0
  179. package/src/harness/remote-session-push-errors.js +15 -0
  180. package/src/harness/remote-session-push-fcm.js +238 -0
  181. package/src/harness/remote-session-push-huawei.js +174 -0
  182. package/src/harness/remote-session-push-oppo.js +171 -0
  183. package/src/harness/remote-session-push-senders.js +42 -0
  184. package/src/harness/remote-session-push-vivo.js +179 -0
  185. package/src/harness/remote-session-push-web.js +299 -0
  186. package/src/harness/remote-session-push-xiaomi.js +99 -0
  187. package/src/harness/remote-session-push.js +104 -0
  188. package/src/harness/remote-session-registry.js +420 -0
  189. package/src/index.js +6 -0
  190. package/src/lib/agent-core.js +2 -0
  191. package/src/lib/agent-sandbox.js +161 -5
  192. package/src/lib/agent-team/task-lease.js +434 -0
  193. package/src/lib/agent-team/team-budget.js +165 -0
  194. package/src/lib/agent-team/team-mailbox.js +106 -0
  195. package/src/lib/agent-team/team-runner.js +266 -0
  196. package/src/lib/agent-team/team-worktree.js +204 -0
  197. package/src/lib/agents.js +18 -0
  198. package/src/lib/async-hook-supervisor.cjs +305 -0
  199. package/src/lib/did-manager.js +6 -4
  200. package/src/lib/eval/runner.js +163 -0
  201. package/src/lib/eval/tasks.js +456 -0
  202. package/src/lib/eval/trend.js +185 -0
  203. package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
  204. package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
  205. package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
  206. package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
  207. package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
  208. package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
  209. package/src/lib/lsp/code-intelligence.js +356 -0
  210. package/src/lib/lsp/jsonrpc-stream.js +139 -0
  211. package/src/lib/lsp/lsp-client.js +318 -0
  212. package/src/lib/lsp/lsp-manager.js +323 -0
  213. package/src/lib/lsp/lsp-server-registry.js +196 -0
  214. package/src/lib/mcp-oauth.js +23 -6
  215. package/src/lib/plugin-monitor-supervisor.js +238 -0
  216. package/src/lib/plugin-runtime/agents.js +51 -0
  217. package/src/lib/plugin-runtime/bin.js +100 -0
  218. package/src/lib/plugin-runtime/hooks.js +100 -0
  219. package/src/lib/plugin-runtime/install.js +388 -0
  220. package/src/lib/plugin-runtime/lsp.js +75 -0
  221. package/src/lib/plugin-runtime/manifest.js +458 -0
  222. package/src/lib/plugin-runtime/mcp.js +89 -0
  223. package/src/lib/plugin-runtime/monitors.js +100 -0
  224. package/src/lib/plugin-runtime/policy.js +144 -0
  225. package/src/lib/plugin-runtime/reload.js +79 -0
  226. package/src/lib/plugin-runtime/scopes.js +197 -0
  227. package/src/lib/plugin-runtime/settings.js +119 -0
  228. package/src/lib/plugin-runtime/signature.js +107 -0
  229. package/src/lib/plugin-runtime/skills.js +43 -0
  230. package/src/lib/plugin-runtime/trust.js +140 -0
  231. package/src/lib/sandbox-egress-proxy.js +162 -0
  232. package/src/lib/sandbox-network-policy.js +134 -0
  233. package/src/lib/sandbox-v2.js +10 -4
  234. package/src/lib/session-manager.js +7 -3
  235. package/src/lib/settings-hook-events.cjs +78 -6
  236. package/src/lib/settings-hooks.cjs +29 -3
  237. package/src/lib/settings-loader.cjs +35 -1
  238. package/src/lib/skill-loader.js +18 -0
  239. package/src/lib/telemetry/span-recorder.js +237 -0
  240. package/src/repl/agent-repl.js +400 -4
  241. package/src/runtime/agent-core.js +546 -52
  242. package/src/runtime/agent-runtime.js +8 -13
  243. package/src/runtime/coding-agent-contract-shared.cjs +82 -8
  244. package/src/runtime/coding-agent-policy.cjs +32 -7
  245. package/src/runtime/fallback-model.js +134 -7
  246. package/src/runtime/headless-runner.js +353 -108
  247. package/src/runtime/mcp-config.js +46 -0
  248. package/src/runtime/policies/agent-policy.js +2 -0
  249. package/src/assets/web-panel/assets/Compliance-T8EtTecW.js +0 -1
  250. package/src/assets/web-panel/assets/MobileBridge-CYaITZ7w.js +0 -1
  251. package/src/assets/web-panel/assets/OrderTableRenderer-CmtqIdwr.js +0 -1
  252. package/src/assets/web-panel/assets/Projects-DPd5-lNS.js +0 -1
  253. package/src/assets/web-panel/assets/Terminal-BzOHpq-B.js +0 -3
  254. package/src/assets/web-panel/assets/devWarning-6YJJMjSH.js +0 -1
  255. package/src/assets/web-panel/assets/index-D_oeGLr6.js +0 -1
  256. package/src/assets/web-panel/assets/index-Qa57YG19.js +0 -1
  257. package/src/assets/web-panel/assets/useFlexGapSupport-DfAD5U4Z.js +0 -1
@@ -27,6 +27,19 @@ import {
27
27
  createRuntimeEvent,
28
28
  } from "../../runtime/runtime-events.js";
29
29
  import { createWsMessageDispatcher } from "./message-dispatcher.js";
30
+ import {
31
+ RemoteSessionRegistry,
32
+ RemoteSessionPolicy,
33
+ } from "../../harness/remote-session-registry.js";
34
+ import { RemoteSessionAuditLog } from "../../harness/remote-session-audit.js";
35
+ import { RemoteSessionAuditFileSink } from "../../harness/remote-session-audit-sink.js";
36
+ import {
37
+ RemoteSessionPushDispatcher,
38
+ isApprovalRequestEvent,
39
+ } from "../../harness/remote-session-push.js";
40
+ import { createRemoteSessionPushSender } from "../../harness/remote-session-push-senders.js";
41
+ import { RemoteSessionRelay } from "../remote-session-relay.js";
42
+ import { handleRemoteSessionPublish } from "./remote-session-protocol.js";
30
43
  import { handleTaskDetail, handleTaskHistory } from "./task-protocol.js";
31
44
  import {
32
45
  handleSessionCreate,
@@ -226,6 +239,63 @@ export class ChainlessChainWSServer extends EventEmitter {
226
239
 
227
240
  /** Session handlers: sessionId → WSAgentHandler | WSChatHandler */
228
241
  this.sessionHandlers = new Map();
242
+ /** Org-policy constraints for Remote Sessions (scopes, device cap, TTL). */
243
+ this.remoteSessionPolicy =
244
+ options.remoteSessionPolicy || RemoteSessionPolicy.fromEnv(process.env);
245
+ /** Local authorization state for multi-device Remote Sessions. */
246
+ this.remoteSessions =
247
+ options.remoteSessionRegistry ||
248
+ new RemoteSessionRegistry({ policy: this.remoteSessionPolicy });
249
+ /**
250
+ * Optional durable sink (JSONL) for the audit trail. Enabled only when a
251
+ * sink is injected or CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_FILE is set; the
252
+ * in-memory ring stays the primary store and query surface.
253
+ */
254
+ this.remoteSessionAuditSink =
255
+ options.remoteSessionAuditSink ||
256
+ RemoteSessionAuditFileSink.fromEnv(process.env);
257
+ /** Bounded in-memory audit trail for Remote Session lifecycle + control. */
258
+ this.remoteSessionAudit =
259
+ options.remoteSessionAudit ||
260
+ new RemoteSessionAuditLog(
261
+ this.remoteSessionAuditSink
262
+ ? {
263
+ sink: this.remoteSessionAuditSink.handler,
264
+ // Hydrate so audit queries survive a restart; cap to the ring size.
265
+ initialEntries: this.remoteSessionAuditSink.readAll({
266
+ limit: 1000,
267
+ }),
268
+ }
269
+ : {},
270
+ );
271
+ /**
272
+ * Vendor-push dispatcher — wakes backgrounded paired devices for approval
273
+ * requests. Credential-free by default (a no-op until a `sender` is
274
+ * injected); FCM/APNs delivery is host-supplied.
275
+ */
276
+ this.remoteSessionPush =
277
+ options.remoteSessionPush ||
278
+ RemoteSessionPushDispatcher.fromEnv(process.env, {
279
+ // Prefer an explicitly injected sender (tests / custom transport); else
280
+ // build a real FCM sender from env when service-account creds are
281
+ // configured. Stays a no-op (null) when neither is present.
282
+ sender:
283
+ options.remoteSessionPushSender ||
284
+ createRemoteSessionPushSender(process.env),
285
+ });
286
+ this.remoteSessionRelayUrl = options.remoteSessionRelayUrl || null;
287
+ this.remoteSessionPeerId = options.remoteSessionPeerId || null;
288
+ this.remoteSessionCrypto = new Map();
289
+ this.remoteSessionPairingSecrets = new Map();
290
+ this.remoteSessionRelay =
291
+ options.remoteSessionRelay ||
292
+ (this.remoteSessionRelayUrl && this.remoteSessionPeerId
293
+ ? new RemoteSessionRelay({
294
+ relayUrl: this.remoteSessionRelayUrl,
295
+ peerId: this.remoteSessionPeerId,
296
+ })
297
+ : null);
298
+ this._attachRemoteSessionRelay();
229
299
  this._dispatcher = createWsMessageDispatcher(this);
230
300
 
231
301
  this._heartbeatTimer = null;
@@ -251,6 +321,12 @@ export class ChainlessChainWSServer extends EventEmitter {
251
321
  this.port = addr.port;
252
322
  }
253
323
  this._startHeartbeat();
324
+ this.remoteSessionRelay?.connect().catch((error) => {
325
+ this.emit("client-error", {
326
+ clientId: "remote-session-relay",
327
+ error: error.message,
328
+ });
329
+ });
254
330
  this.emit("listening", { port: this.port, host: this.host });
255
331
  resolve();
256
332
  });
@@ -285,6 +361,9 @@ export class ChainlessChainWSServer extends EventEmitter {
285
361
  }
286
362
  }
287
363
  this.sessionHandlers.clear();
364
+ this.remoteSessionCrypto.clear();
365
+ this.remoteSessionPairingSecrets.clear();
366
+ this.remoteSessionRelay?.close();
288
367
 
289
368
  // Kill all running child processes
290
369
  for (const [id, child] of this.processes) {
@@ -391,6 +470,18 @@ export class ChainlessChainWSServer extends EventEmitter {
391
470
  client.authTimer = null;
392
471
  }
393
472
  this.clients.delete(clientId);
473
+ for (const affected of this.remoteSessions.removeClient(clientId)) {
474
+ if (affected.closed)
475
+ this.remoteSessionCrypto.delete(affected.sessionId);
476
+ if (affected.closed)
477
+ this.remoteSessionPairingSecrets.delete(affected.sessionId);
478
+ this.remoteSessionAudit?.record({
479
+ sessionId: affected.sessionId,
480
+ actor: clientId,
481
+ action: affected.closed ? "session.closed" : "device.disconnected",
482
+ detail: { reason: "disconnect" },
483
+ });
484
+ }
394
485
  this.emit("disconnection", { clientId });
395
486
  });
396
487
 
@@ -975,12 +1066,220 @@ export class ChainlessChainWSServer extends EventEmitter {
975
1066
  if (ws.readyState === ws.OPEN) {
976
1067
  try {
977
1068
  ws.send(JSON.stringify(data));
1069
+ this._mirrorRemoteSessionEvent(ws, data);
978
1070
  } catch (_err) {
979
1071
  // Connection may have just closed
980
1072
  }
981
1073
  }
982
1074
  }
983
1075
 
1076
+ /** Mirror canonical Agent Session output to paired Remote Session clients. */
1077
+ _mirrorRemoteSessionEvent(hostWs, data) {
1078
+ if (
1079
+ !data ||
1080
+ typeof data !== "object" ||
1081
+ !data.sessionId ||
1082
+ String(data.type || "").startsWith("remote-session-")
1083
+ ) {
1084
+ return;
1085
+ }
1086
+ let hostClientId = null;
1087
+ for (const [clientId, client] of this.clients) {
1088
+ if (client.ws === hostWs) {
1089
+ hostClientId = clientId;
1090
+ break;
1091
+ }
1092
+ }
1093
+ if (!hostClientId) return;
1094
+
1095
+ for (const remoteSession of this.remoteSessions.findHosted(
1096
+ data.sessionId,
1097
+ hostClientId,
1098
+ )) {
1099
+ for (const member of remoteSession.members.values()) {
1100
+ if (
1101
+ member.clientId === hostClientId ||
1102
+ !member.scopes.includes("observe")
1103
+ ) {
1104
+ continue;
1105
+ }
1106
+ // Wake a backgrounded device via vendor push for approval requests.
1107
+ // Independent of the WS mirror below — a relay-paired mobile app may
1108
+ // report a live socket while its process is suspended and will never
1109
+ // read the mirrored event until the user taps the push.
1110
+ if (member.pushToken && isApprovalRequestEvent(data.type)) {
1111
+ this._dispatchApprovalPush(remoteSession, member, data);
1112
+ }
1113
+ const target = this.clients.get(member.clientId);
1114
+ if (!target && this.remoteSessionRelay) {
1115
+ const crypto = this.remoteSessionCrypto.get(remoteSession.sessionId);
1116
+ if (crypto) {
1117
+ this.remoteSessionRelay.sendEncrypted(
1118
+ member.clientId,
1119
+ crypto.encrypt(member.clientId, data),
1120
+ );
1121
+ }
1122
+ continue;
1123
+ }
1124
+ if (!target) continue;
1125
+ this._send(target.ws, {
1126
+ type: "remote-session-event",
1127
+ remoteSessionId: remoteSession.sessionId,
1128
+ agentSessionId: remoteSession.agentSessionId,
1129
+ event: data,
1130
+ });
1131
+ }
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * Fire-and-forget a vendor push to one member for an approval request, then
1137
+ * record the outcome in the audit trail. Never throws (the dispatcher is
1138
+ * best-effort); the WS mirror + in-app notification remain primary.
1139
+ */
1140
+ _dispatchApprovalPush(remoteSession, member, data) {
1141
+ const dispatcher = this.remoteSessionPush;
1142
+ if (!dispatcher || !dispatcher.enabled) return;
1143
+ const requestId =
1144
+ data.requestId || data.approvalId || data.id || `approval:${data.type}`;
1145
+ dispatcher
1146
+ .dispatch({
1147
+ token: member.pushToken,
1148
+ provider: member.pushProvider,
1149
+ sessionId: remoteSession.sessionId,
1150
+ clientId: member.clientId,
1151
+ dedupeKey: requestId,
1152
+ notification: {
1153
+ title: "Approval requested",
1154
+ body: "A coding session needs your approval",
1155
+ },
1156
+ })
1157
+ .then((outcome) => {
1158
+ // A vendor that reports the token as retired means the app was
1159
+ // uninstalled / the token rotated — prune it so it is never retried.
1160
+ if (outcome.code === "PUSH_TOKEN_UNREGISTERED") {
1161
+ try {
1162
+ this.remoteSessions.registerPush(
1163
+ remoteSession.sessionId,
1164
+ member.clientId,
1165
+ { token: null },
1166
+ );
1167
+ } catch {
1168
+ // Member may have been revoked meanwhile — nothing to prune.
1169
+ }
1170
+ this.remoteSessionAudit?.record({
1171
+ sessionId: remoteSession.sessionId,
1172
+ actor: member.clientId,
1173
+ action: "push.unregistered",
1174
+ detail: { provider: member.pushProvider || null },
1175
+ });
1176
+ return;
1177
+ }
1178
+ this.remoteSessionAudit?.record({
1179
+ sessionId: remoteSession.sessionId,
1180
+ actor: member.clientId,
1181
+ action:
1182
+ outcome.status === "sent"
1183
+ ? "push.sent"
1184
+ : outcome.status === "failed"
1185
+ ? "push.failed"
1186
+ : "push.skipped",
1187
+ detail: {
1188
+ provider: member.pushProvider || null,
1189
+ reason: outcome.reason || null,
1190
+ error: outcome.error || null,
1191
+ },
1192
+ });
1193
+ })
1194
+ .catch(() => {
1195
+ // Dispatcher never rejects, but guard anyway — a failed courtesy push
1196
+ // must not surface as an unhandled rejection.
1197
+ });
1198
+ }
1199
+
1200
+ _attachRemoteSessionRelay() {
1201
+ if (!this.remoteSessionRelay) return;
1202
+ this.remoteSessionRelay.on("relay-message", (message) => {
1203
+ this._handleRemoteRelayMessage(message).catch((error) => {
1204
+ this.emit("client-error", {
1205
+ clientId: "remote-session-relay",
1206
+ error: error.message,
1207
+ });
1208
+ });
1209
+ });
1210
+ this.remoteSessionRelay.on("encrypted-message", ({ from, envelope }) => {
1211
+ this._handleRemoteEncryptedControl(from, envelope).catch((error) => {
1212
+ this.emit("client-error", { clientId: from, error: error.message });
1213
+ });
1214
+ });
1215
+ }
1216
+
1217
+ async _handleRemoteRelayMessage(message) {
1218
+ if (
1219
+ message?.type !== "message" ||
1220
+ message.payload?.type !== "remote-session.pair"
1221
+ )
1222
+ return;
1223
+ const payload = message.payload;
1224
+ const sessionId = payload.envelope?.sessionId;
1225
+ const crypto = this.remoteSessionCrypto.get(sessionId);
1226
+ const token = this.remoteSessionPairingSecrets.get(sessionId);
1227
+ if (!crypto || !token)
1228
+ throw new Error("Remote Session pairing is unavailable or expired");
1229
+ crypto.pair(payload.mobilePeerId, payload.mobilePublicKey, token);
1230
+ const join = crypto.decrypt(payload.envelope);
1231
+ if (join.type !== "pair.join" || join.token !== token) {
1232
+ throw new Error("Invalid encrypted Remote Session pairing request");
1233
+ }
1234
+ const relayMember = this.remoteSessions.join({
1235
+ sessionId,
1236
+ clientId: payload.mobilePeerId,
1237
+ token: join.token,
1238
+ via: "relay",
1239
+ pushToken: join.pushToken,
1240
+ pushProvider: join.pushProvider,
1241
+ });
1242
+ this.remoteSessionAudit?.record({
1243
+ sessionId,
1244
+ actor: payload.mobilePeerId,
1245
+ action: "device.joined",
1246
+ detail: {
1247
+ via: "relay",
1248
+ hasPush: relayMember?.member?.pushToken ? true : false,
1249
+ },
1250
+ });
1251
+ this.remoteSessionPairingSecrets.delete(sessionId);
1252
+ this.remoteSessionRelay.sendEncrypted(
1253
+ payload.mobilePeerId,
1254
+ crypto.encrypt(payload.mobilePeerId, {
1255
+ type: "pair.accepted",
1256
+ remoteSessionId: sessionId,
1257
+ }),
1258
+ );
1259
+ }
1260
+
1261
+ async _handleRemoteEncryptedControl(from, envelope) {
1262
+ const crypto = this.remoteSessionCrypto.get(envelope?.sessionId);
1263
+ if (!crypto) throw new Error("Remote Session crypto context not found");
1264
+ const event = crypto.decrypt(envelope);
1265
+ const virtualWs = {
1266
+ OPEN: 1,
1267
+ readyState: 1,
1268
+ send: (raw) => {
1269
+ const response = JSON.parse(raw);
1270
+ this.remoteSessionRelay.sendEncrypted(
1271
+ from,
1272
+ crypto.encrypt(from, response),
1273
+ );
1274
+ },
1275
+ };
1276
+ await handleRemoteSessionPublish(this, from, virtualWs, {
1277
+ id: `remote-${Date.now()}`,
1278
+ remoteSessionId: envelope.sessionId,
1279
+ event,
1280
+ });
1281
+ }
1282
+
984
1283
  /** @private — broadcast a message to all connected, authenticated clients */
985
1284
  _broadcast(data) {
986
1285
  for (const [, client] of this.clients) {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Normalize runtime events before comparing them with a golden transcript.
3
+ *
4
+ * Runtime envelopes intentionally contain volatile identity and timing fields.
5
+ * Golden files should describe observable behavior, not a particular run, so
6
+ * those fields are removed recursively and platform-specific workspace paths
7
+ * are replaced with a stable token.
8
+ */
9
+
10
+ const VOLATILE_KEYS = new Set([
11
+ "eventId",
12
+ "id",
13
+ "requestId",
14
+ "sessionId",
15
+ "sequence",
16
+ "timestamp",
17
+ "createdAt",
18
+ "updatedAt",
19
+ "duration",
20
+ "durationMs",
21
+ "toolTelemetryRecord",
22
+ ]);
23
+
24
+ function normalizeString(value, workspace) {
25
+ if (!workspace) return value;
26
+ const variants = new Set([
27
+ workspace,
28
+ workspace.replaceAll("\\", "/"),
29
+ workspace.replaceAll("/", "\\"),
30
+ ]);
31
+ let normalized = value;
32
+ for (const variant of variants) {
33
+ normalized = normalized.split(variant).join("<WORKSPACE>");
34
+ }
35
+ return normalized.replaceAll("<WORKSPACE>\\", "<WORKSPACE>/");
36
+ }
37
+
38
+ function normalizeValue(value, workspace) {
39
+ if (typeof value === "string") return normalizeString(value, workspace);
40
+ if (Array.isArray(value)) {
41
+ return value.map((item) => normalizeValue(item, workspace));
42
+ }
43
+ if (!value || typeof value !== "object") return value;
44
+
45
+ return Object.fromEntries(
46
+ Object.entries(value)
47
+ .filter(([key]) => !VOLATILE_KEYS.has(key))
48
+ .sort(([left], [right]) => left.localeCompare(right))
49
+ .map(([key, item]) => [key, normalizeValue(item, workspace)]),
50
+ );
51
+ }
52
+
53
+ /**
54
+ * @param {Array<object>} events
55
+ * @param {{workspace?: string, ignoredTypes?: string[]}} [options]
56
+ */
57
+ export function normalizeGoldenTranscript(events, options = {}) {
58
+ if (!Array.isArray(events)) {
59
+ throw new TypeError("Golden transcript input must be an array");
60
+ }
61
+ const ignored = new Set(options.ignoredTypes || ["run-started", "run-ended"]);
62
+ return events
63
+ .filter((event) => !ignored.has(event?.type))
64
+ .map((event) => normalizeValue(event, options.workspace));
65
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { spawn } from "child_process";
12
12
  import { EventEmitter } from "events";
13
+ import { safeJsonParse } from "../lib/safe-json.js";
13
14
 
14
15
  /**
15
16
  * Injectable dependencies — overridable from tests.
@@ -1156,8 +1157,11 @@ export class MCPServerConfig {
1156
1157
  return {
1157
1158
  name: row.name,
1158
1159
  command: row.command || null,
1159
- args: JSON.parse(row.args || "[]"),
1160
- env: JSON.parse(row.env || "{}"),
1160
+ // safeJsonParse, not bare JSON.parse: list()/getAutoConnect() map every
1161
+ // row through here, so one corrupt cell must not take down the whole
1162
+ // MCP server list (`|| "[]"` only guards NULL, not a corrupt non-empty string).
1163
+ args: safeJsonParse(row.args, []),
1164
+ env: safeJsonParse(row.env, {}),
1161
1165
  autoConnect: row.auto_connect === 1,
1162
1166
  url: row.url || null,
1163
1167
  transport:
@@ -251,8 +251,25 @@ export class PromptCompressor {
251
251
  };
252
252
  }
253
253
 
254
+ // Pinned facts — a DETERMINISTIC fact-retention guarantee. A message marked
255
+ // `pinned:true` (or matched by an `options.isPinned` predicate) MUST survive
256
+ // compaction verbatim regardless of any strategy or the non-deterministic LLM
257
+ // summary. Every other retention here is positional (system / recent-N / last
258
+ // user) or summary-dependent, so an important fact that scrolls out of the
259
+ // recent window would otherwise survive only if the summary happens to keep
260
+ // it. We pull pins out BEFORE any strategy runs (so nothing can drop them),
261
+ // then re-insert them as a sticky block right after the system messages.
262
+ const isPinned =
263
+ typeof options.isPinned === "function"
264
+ ? options.isPinned
265
+ : (m) => m && (m.pinned === true || m._pin === true);
266
+ const pinned = messages.filter((m) => isPinned(m));
267
+ const working = pinned.length
268
+ ? messages.filter((m) => !isPinned(m))
269
+ : messages;
270
+
254
271
  const originalTokens = estimateMessagesTokens(messages);
255
- let result = [...messages];
272
+ let result = [...working];
256
273
  const applied = [];
257
274
 
258
275
  if (feature("CONTEXT_SNIP")) {
@@ -288,6 +305,15 @@ export class PromptCompressor {
288
305
  }
289
306
  }
290
307
 
308
+ // Re-insert pinned facts verbatim (order preserved) right after the leading
309
+ // system messages, so they end up as a sticky facts block at the top.
310
+ if (pinned.length) {
311
+ const sys = result.filter((m) => m.role === "system");
312
+ const nonSys = result.filter((m) => m.role !== "system");
313
+ result = [...sys, ...pinned, ...nonSys];
314
+ applied.push("pinned");
315
+ }
316
+
291
317
  // Tool-pair repair (opt-in) — callers compacting tool-laden histories
292
318
  // (e.g. the headless agent loop) pass this so truncation/snip never leaves
293
319
  // an orphaned tool result or unanswered tool_call for a strict API.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * RemoteCommandLedger (Phase 5, Remote Control) — application-level idempotency
3
+ * + total ordering + device revocation for commands arriving from remote
4
+ * endpoints (terminal / web / mobile).
5
+ *
6
+ * The transport layer (remote-session-crypto) already rejects a replayed or
7
+ * out-of-order ENVELOPE within one connection — but a reconnect starts a fresh
8
+ * crypto session with a reset sequence baseline, so a command the client
9
+ * re-sends after a dropped ACK would sail through and EXECUTE A SECOND TIME. That
10
+ * is exactly the Phase 5 acceptance this guards: "断网恢复后不会重复执行工具调用".
11
+ *
12
+ * Guarantees:
13
+ * - Idempotency: a command carries a stable `commandId`; the side effect runs
14
+ * AT MOST ONCE. A re-delivered command returns its cached result flagged
15
+ * `replayed` — it never re-executes.
16
+ * - Total order: every applied command gets a monotonic `applyIndex`, so all
17
+ * three endpoints agree on one order; `appliedSince(cursor)` lets a
18
+ * reconnecting endpoint catch up deterministically.
19
+ * - Revocation: a revoked device's commands are rejected (can't read/control).
20
+ *
21
+ * Pure + injectable clock → fully unit-testable without any transport/hardware.
22
+ */
23
+
24
+ export class RemoteCommandLedger {
25
+ constructor({ now = () => Date.now(), revokedDevices = [] } = {}) {
26
+ this._now = typeof now === "function" ? now : () => now;
27
+ this._applied = new Map(); // commandId → { applyIndex, result, deviceId, ts }
28
+ this._order = []; // applyIndex → commandId (total order)
29
+ this._revoked = new Set(revokedDevices);
30
+ this._perDeviceSeq = new Map(); // deviceId → highest accepted seq
31
+ }
32
+
33
+ isRevoked(deviceId) {
34
+ return this._revoked.has(deviceId);
35
+ }
36
+
37
+ revokeDevice(deviceId) {
38
+ this._revoked.add(deviceId);
39
+ }
40
+
41
+ /**
42
+ * Apply a command idempotently. `execute` (async, side-effecting) runs AT MOST
43
+ * ONCE per commandId; a re-delivery returns the cached result without calling
44
+ * it again.
45
+ *
46
+ * @param {{commandId:string, deviceId:string, seq?:number, payload?:any}} command
47
+ * @param {(command)=>Promise<any>|any} execute
48
+ * @returns {Promise<{status:"applied"|"replayed"|"rejected", result?, reason?, applyIndex?}>}
49
+ */
50
+ async apply(command, execute) {
51
+ if (
52
+ !command ||
53
+ typeof command.commandId !== "string" ||
54
+ !command.commandId
55
+ ) {
56
+ return { status: "rejected", reason: "missing commandId" };
57
+ }
58
+ const { commandId, deviceId } = command;
59
+
60
+ // A revoked device can neither read nor control the session.
61
+ if (deviceId != null && this._revoked.has(deviceId)) {
62
+ return { status: "rejected", reason: "device revoked" };
63
+ }
64
+
65
+ // Idempotent replay: already applied → return the cached result, DO NOT
66
+ // re-run the side effect. This is the reconnect-safety guarantee.
67
+ const prior = this._applied.get(commandId);
68
+ if (prior) {
69
+ return {
70
+ status: "replayed",
71
+ result: prior.result,
72
+ applyIndex: prior.applyIndex,
73
+ };
74
+ }
75
+
76
+ // Optional per-device monotonic sequence check: a STALE seq (below what we've
77
+ // already accepted from this device) with a NEW commandId is a protocol
78
+ // violation (a genuine reconnect resends the SAME commandId, caught above).
79
+ if (
80
+ deviceId != null &&
81
+ Number.isFinite(command.seq) &&
82
+ this._perDeviceSeq.has(deviceId) &&
83
+ command.seq <= this._perDeviceSeq.get(deviceId)
84
+ ) {
85
+ return { status: "rejected", reason: "stale sequence" };
86
+ }
87
+
88
+ let result;
89
+ try {
90
+ result = await execute(command);
91
+ } catch (err) {
92
+ // A failed execution is NOT recorded as applied — the command may be
93
+ // retried (with the same commandId) and will run again. Surfacing the
94
+ // error lets the caller decide; idempotency only protects SUCCESSES.
95
+ return { status: "rejected", reason: `execute failed: ${err.message}` };
96
+ }
97
+
98
+ const applyIndex = this._order.length;
99
+ this._applied.set(commandId, {
100
+ applyIndex,
101
+ result,
102
+ deviceId,
103
+ ts: this._now(),
104
+ });
105
+ this._order.push(commandId);
106
+ if (deviceId != null && Number.isFinite(command.seq)) {
107
+ this._perDeviceSeq.set(deviceId, command.seq);
108
+ }
109
+ return { status: "applied", result, applyIndex };
110
+ }
111
+
112
+ /**
113
+ * The ordered tail of applied commands after `cursor` (an applyIndex), so a
114
+ * reconnecting endpoint can replay missed state in the one agreed order.
115
+ * @returns {Array<{applyIndex, commandId, result, deviceId}>}
116
+ */
117
+ appliedSince(cursor = -1) {
118
+ const out = [];
119
+ for (let i = cursor + 1; i < this._order.length; i++) {
120
+ const commandId = this._order[i];
121
+ const rec = this._applied.get(commandId);
122
+ out.push({
123
+ applyIndex: i,
124
+ commandId,
125
+ result: rec.result,
126
+ deviceId: rec.deviceId,
127
+ });
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /** Highest assigned applyIndex (−1 when nothing applied). */
133
+ cursor() {
134
+ return this._order.length - 1;
135
+ }
136
+
137
+ appliedCount() {
138
+ return this._order.length;
139
+ }
140
+
141
+ snapshot() {
142
+ return {
143
+ order: [...this._order],
144
+ applied: Array.from(this._applied.entries()),
145
+ revoked: Array.from(this._revoked),
146
+ perDeviceSeq: Array.from(this._perDeviceSeq.entries()),
147
+ };
148
+ }
149
+
150
+ static restore(snap, { now = () => Date.now() } = {}) {
151
+ const l = new RemoteCommandLedger({ now });
152
+ l._order = [...(snap?.order || [])];
153
+ l._applied = new Map(snap?.applied || []);
154
+ l._revoked = new Set(snap?.revoked || []);
155
+ l._perDeviceSeq = new Map(snap?.perDeviceSeq || []);
156
+ return l;
157
+ }
158
+ }