chainlesschain 0.162.95 → 0.162.96

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 (217) hide show
  1. package/README.md +1 -1
  2. package/bin/chainlesschain.js +8 -18
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DLvvkb9x.js → AIOps-Bo9Ux24z.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-CExgXSQT.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-BNnZhRTZ.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-D9xfqFCT.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-Cpgu8uCw.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CHdEJnNE.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-Cbw-PxBq.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-ChHTZK_6.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-Dl6z6ZH7.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Cj8d9bru.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-C2YobW4j.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-CxfV5FGT.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-CiwOASMj.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CTg_2iKW.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-HMbsTmtw.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-DyNDqGR2.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-9-x5M7qy.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-4msgDaYJ.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID--2Beb5Po.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-T6nq0Lzs.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-D_Q6T10v.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-TnN21bJx.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-kky_uIij.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-C1GkNY4B.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-5-_Fc5YV.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-CmHiB7sr.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-Cuci_BPI.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-BU0cO2LD.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-BDFNgEcx.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-DITXUzHz.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Ea-07CRd.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-D6OBbRVG.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-eiKpIJMi.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-M-X-T8UF.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-xcZb9Ekv.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-DCfoTPRC.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-BWfGDzD8.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-imBuld-U.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C2kcsq7I.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-DObvXSXP.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-C-6OGJMe.js} +4 -4
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-ub7Ozxci.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-CR2W89Vk.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-DNA_Wty7.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-rT4Wb4rQ.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CK-hARKv.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-C2pbtNbT.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-B8EIEqGJ.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-BxNVNU3s.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-CT9pbkyY.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-DhCNd2CS.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-Qngnx-vQ.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Dfn-nQc1.js} +2 -2
  57. package/src/assets/web-panel/assets/Projects-BKdiSaPR.js +1 -0
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-4o9B_n-X.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-bs1qvF30.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-CuEnKA2K.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-DbfvmRb2.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-DvMJGAI3.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-oJ0BZvmA.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DnIVrY9O.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-CIWhI_Tc.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-DLrnpjwm.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CYmqgtVb.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-BUzCkoeu.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DYRE4bnS.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-_6MurdOg.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-UIdnF5Li.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-COx2CjT8.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-BcHAmkZo.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-CjYQS8yZ.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-BmfOPcWt.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-D5HuarNQ.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-C_fbProA.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-JPulpRx8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-CnO-062O.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-HpNovvau.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-yMncxH7y.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-Cug-t7Np.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn--U1-NR-3.js} +2 -2
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-R-XvSJ3S.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CCI1i6Rp.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-C-mqoaLt.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-De_N2iRR.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-DbTM6sjZ.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-OcLPHSl5.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-D0Adylyc.js} +1 -1
  91. package/src/assets/web-panel/assets/index-7E_mHkDU.js +1 -0
  92. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-AY937BxS.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B6hgy5VC.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-B7XnpLHk.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-B7gw-tDm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-BBgDgkwW.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BMmeiw88.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BU6XtnSx.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-Bi7VUG0R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-BtE3cw9m.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-C9ej-MF0.js} +3 -3
  102. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-CIEiQl1j.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CKgaW9E5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-CY0KKR5f.js} +1 -1
  105. package/src/assets/web-panel/assets/index-CbV3-WlB.js +1 -0
  106. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-Co1UkcFX.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CzP5d5LW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-D0NJ5lRi.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-D0gBsqTX.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-D5F6shtn.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-DVtamtlm.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-DWT18hDG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DaXb4B9P.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-DjQA0Vez.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-Ds8NJVEL.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-DuP-8HWu.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-DwLTiJ31.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-T3VhH8cO.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-TDY1o0w3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-TRy2Gc3V.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-W1OMxCmI.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-ftFRZbuc.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-iGPGaORA.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-jhy5KQvg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-qUSZJcgw.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-w3f6lqkD.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-xdMc71Kh.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-xhX5eg2Z.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-zkTiW6d6.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-CIHqouWk.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-D1iUKftk.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-YOpWjmjL.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-C_1357UY.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-CDb4dq6o.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-D-OGYyhd.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve-hS-j69A0.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-tnjuvphU.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-BERfMMcU.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-DKpl2K9E.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-C_sX4D3N.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-DBcxC1vK.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-CeoDf2XI.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-DC9Pmj_u.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-CZA58J7M.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +19 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/ask.js +13 -0
  149. package/src/commands/config.js +17 -1
  150. package/src/commands/cost.js +6 -1
  151. package/src/commands/dao.js +16 -3
  152. package/src/commands/dlp.js +7 -1
  153. package/src/commands/hub.js +13 -0
  154. package/src/commands/mtc.js +37 -1
  155. package/src/gateways/discord/discord-formatter.js +18 -1
  156. package/src/gateways/telegram/telegram-formatter.js +6 -3
  157. package/src/gateways/ws/ws-server.js +19 -1
  158. package/src/harness/plugin-manager.js +20 -0
  159. package/src/harness/worktree-isolator.js +17 -1
  160. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  161. package/src/lib/a2a-protocol.js +23 -9
  162. package/src/lib/agent-core.js +2 -5
  163. package/src/lib/audit-mtc.js +40 -2
  164. package/src/lib/chat-core.js +141 -18
  165. package/src/lib/cli-anything-bridge.js +5 -0
  166. package/src/lib/cli-numeric.js +38 -0
  167. package/src/lib/config-manager.js +37 -6
  168. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  169. package/src/lib/cowork-cron.js +45 -34
  170. package/src/lib/cowork-learning.js +31 -1
  171. package/src/lib/cowork-share.js +39 -3
  172. package/src/lib/cowork-template-marketplace.js +54 -2
  173. package/src/lib/cowork-workflow.js +33 -6
  174. package/src/lib/cross-chain-mtc.js +41 -12
  175. package/src/lib/crypto-manager.js +12 -2
  176. package/src/lib/dbevo.js +19 -8
  177. package/src/lib/downloader.js +24 -9
  178. package/src/lib/evomap-governance.js +4 -4
  179. package/src/lib/evomap-manager.js +54 -7
  180. package/src/lib/fatal-handler.js +48 -0
  181. package/src/lib/file-checkpoint.js +26 -0
  182. package/src/lib/git-integration.js +28 -0
  183. package/src/lib/goal-store.js +36 -10
  184. package/src/lib/hierarchical-memory.js +6 -0
  185. package/src/lib/llm-pricing.js +52 -2
  186. package/src/lib/mcp-oauth.js +37 -7
  187. package/src/lib/memory-manager.js +23 -2
  188. package/src/lib/model-deprecation.js +249 -0
  189. package/src/lib/pdh-feedback-ledger.js +131 -0
  190. package/src/lib/permanent-memory.js +30 -2
  191. package/src/lib/permission-rules.cjs +11 -3
  192. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  193. package/src/lib/personal-data-hub-wiring.js +36 -13
  194. package/src/lib/project-instructions.js +10 -4
  195. package/src/lib/runnable-provider.js +62 -31
  196. package/src/lib/session-usage.js +34 -1
  197. package/src/lib/skill-loader.js +4 -1
  198. package/src/lib/skill-packs/generator.js +25 -2
  199. package/src/lib/stream-retry.js +56 -0
  200. package/src/lib/sync-credentials.js +53 -17
  201. package/src/lib/token-tracker.js +12 -2
  202. package/src/lib/with-file-lock.js +87 -0
  203. package/src/repl/agent-repl.js +39 -7
  204. package/src/repl/chat-repl.js +10 -0
  205. package/src/repl/session-cost.js +31 -3
  206. package/src/runtime/agent-core.js +474 -53
  207. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  208. package/src/runtime/coding-agent-policy.cjs +10 -0
  209. package/src/runtime/file-ref-expander.js +29 -1
  210. package/src/runtime/headless-runner.js +12 -1
  211. package/src/runtime/headless-stream.js +131 -5
  212. package/src/skills/video-editing/tools/commit.js +15 -1
  213. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  214. package/src/assets/web-panel/assets/Projects-CXydUXp8.js +0 -1
  215. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  216. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  217. package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
@@ -11,6 +11,11 @@
11
11
 
12
12
  import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
13
13
  import { appendTokenUsage } from "../harness/jsonl-session-store.js";
14
+ import {
15
+ isRetryableStreamError,
16
+ STREAM_RETRY_MAX,
17
+ STREAM_RETRY_BASE_MS,
18
+ } from "./stream-retry.js";
14
19
 
15
20
  // A streaming chat call must not hang forever if the API accepts the connection
16
21
  // but then goes silent (TCP up, no bytes). Abort the request if no data arrives
@@ -19,18 +24,43 @@ import { appendTokenUsage } from "../harness/jsonl-session-store.js";
19
24
  // generous (and env-overridable) so a slow local model's first token isn't cut
20
25
  // off; raise CC_CHAT_STALL_MS if you run very large local models.
21
26
  export const STREAM_STALL_MS = Number(process.env.CC_CHAT_STALL_MS) || 180000;
27
+ // Before the hard abort above, surface a "waiting for API response" hint once
28
+ // per silent gap (cc agent parity — agent-core STREAM_STALL_MS). Purely
29
+ // informational; never aborts. Env-overridable; disabled if it would exceed the
30
+ // abort threshold.
31
+ export const STREAM_STALL_HINT_MS =
32
+ Number(process.env.CC_CHAT_STALL_HINT_MS) || 20000;
22
33
 
23
- function makeStallGuard(stallMs = STREAM_STALL_MS) {
34
+ function makeStallGuard(
35
+ stallMs = STREAM_STALL_MS,
36
+ { onHint, hintMs = STREAM_STALL_HINT_MS } = {},
37
+ ) {
24
38
  const controller = new AbortController();
25
39
  let timer = null;
40
+ let hintTimer = null;
41
+ const hintArmed =
42
+ typeof onHint === "function" && hintMs > 0 && hintMs < stallMs;
26
43
  const bump = () => {
27
44
  if (timer) clearTimeout(timer);
28
45
  timer = setTimeout(() => controller.abort(), stallMs);
29
46
  if (timer && typeof timer.unref === "function") timer.unref();
47
+ if (hintArmed) {
48
+ if (hintTimer) clearTimeout(hintTimer);
49
+ hintTimer = setTimeout(() => {
50
+ try {
51
+ onHint(hintMs);
52
+ } catch {
53
+ /* hint is best-effort — never affect the stream */
54
+ }
55
+ }, hintMs);
56
+ if (hintTimer && typeof hintTimer.unref === "function") hintTimer.unref();
57
+ }
30
58
  };
31
59
  const stop = () => {
32
60
  if (timer) clearTimeout(timer);
61
+ if (hintTimer) clearTimeout(hintTimer);
33
62
  timer = null;
63
+ hintTimer = null;
34
64
  };
35
65
  return {
36
66
  signal: controller.signal,
@@ -40,13 +70,35 @@ function makeStallGuard(stallMs = STREAM_STALL_MS) {
40
70
  };
41
71
  }
42
72
 
73
+ // OpenAI-compatible cached-prompt-token count (mirrors agent-core's
74
+ // _openaiCachedTokens). OpenAI/volcengine report it as
75
+ // usage.prompt_tokens_details.cached_tokens; DeepSeek as
76
+ // usage.prompt_cache_hit_tokens. prompt_tokens INCLUDES the cached count, so
77
+ // callers subtract it to recover uncached input (avoids over-pricing in
78
+ // `cc cost` for cc chat sessions).
79
+ function _cachedPromptTokens(usage) {
80
+ if (!usage || typeof usage !== "object") return 0;
81
+ const detailed = Number(usage.prompt_tokens_details?.cached_tokens);
82
+ if (Number.isFinite(detailed) && detailed > 0) return detailed;
83
+ const deepseek = Number(usage.prompt_cache_hit_tokens);
84
+ if (Number.isFinite(deepseek) && deepseek > 0) return deepseek;
85
+ return 0;
86
+ }
87
+
43
88
  /**
44
89
  * Stream a response from Ollama.
45
90
  * If `onUsage` is provided, it's called with `{inputTokens, outputTokens}`
46
91
  * derived from Ollama's terminal `prompt_eval_count` / `eval_count` fields.
47
92
  */
48
- export async function streamOllama(messages, model, baseUrl, onToken, onUsage) {
49
- const guard = makeStallGuard();
93
+ export async function streamOllama(
94
+ messages,
95
+ model,
96
+ baseUrl,
97
+ onToken,
98
+ onUsage,
99
+ onStall,
100
+ ) {
101
+ const guard = makeStallGuard(STREAM_STALL_MS, { onHint: onStall });
50
102
  guard.bump();
51
103
  let response;
52
104
  try {
@@ -129,8 +181,9 @@ export async function streamOpenAI(
129
181
  apiKey,
130
182
  onToken,
131
183
  onUsage,
184
+ onStall,
132
185
  ) {
133
- const guard = makeStallGuard();
186
+ const guard = makeStallGuard(STREAM_STALL_MS, { onHint: onStall });
134
187
  guard.bump();
135
188
  let response;
136
189
  try {
@@ -189,10 +242,14 @@ export async function streamOpenAI(
189
242
  onToken(content);
190
243
  }
191
244
  if (json.usage && onUsage) {
192
- const inputTokens = Number(json.usage.prompt_tokens) || 0;
245
+ // Split cached prompt tokens out of prompt_tokens so cc cost
246
+ // prices the cached prefix correctly (matches agent-core).
247
+ const cacheReadTokens = _cachedPromptTokens(json.usage);
248
+ const prompt = Number(json.usage.prompt_tokens) || 0;
249
+ const inputTokens = Math.max(0, prompt - cacheReadTokens);
193
250
  const outputTokens = Number(json.usage.completion_tokens) || 0;
194
- if (inputTokens || outputTokens) {
195
- onUsage({ inputTokens, outputTokens });
251
+ if (inputTokens || outputTokens || cacheReadTokens) {
252
+ onUsage({ inputTokens, outputTokens, cacheReadTokens });
196
253
  }
197
254
  }
198
255
  } catch {
@@ -224,6 +281,7 @@ export async function streamAnthropic(
224
281
  apiKey,
225
282
  onToken,
226
283
  onUsage,
284
+ onStall,
227
285
  ) {
228
286
  // Split out a leading system prompt (Anthropic requires it as top-level
229
287
  // `system`, not an OpenAI-style role=system message).
@@ -237,7 +295,7 @@ export async function streamAnthropic(
237
295
  }
238
296
  }
239
297
 
240
- const guard = makeStallGuard();
298
+ const guard = makeStallGuard(STREAM_STALL_MS, { onHint: onStall });
241
299
  guard.bump();
242
300
  let response;
243
301
  try {
@@ -279,6 +337,8 @@ export async function streamAnthropic(
279
337
  let buf = "";
280
338
  let inputTokens = 0;
281
339
  let outputTokens = 0;
340
+ let cacheReadTokens = 0;
341
+ let cacheCreationTokens = 0;
282
342
 
283
343
  try {
284
344
  while (true) {
@@ -302,10 +362,13 @@ export async function streamAnthropic(
302
362
  onToken(delta);
303
363
  }
304
364
  } else if (obj.type === "message_start") {
305
- inputTokens =
306
- Number(obj.message?.usage?.input_tokens) || inputTokens;
307
- outputTokens =
308
- Number(obj.message?.usage?.output_tokens) || outputTokens;
365
+ const u = obj.message?.usage || {};
366
+ inputTokens = Number(u.input_tokens) || inputTokens;
367
+ outputTokens = Number(u.output_tokens) || outputTokens;
368
+ cacheReadTokens =
369
+ Number(u.cache_read_input_tokens) || cacheReadTokens;
370
+ cacheCreationTokens =
371
+ Number(u.cache_creation_input_tokens) || cacheCreationTokens;
309
372
  } else if (obj.type === "message_delta") {
310
373
  outputTokens = Number(obj.usage?.output_tokens) || outputTokens;
311
374
  }
@@ -324,8 +387,16 @@ export async function streamAnthropic(
324
387
  guard.stop();
325
388
  }
326
389
 
327
- if (onUsage && (inputTokens || outputTokens)) {
328
- onUsage({ inputTokens, outputTokens });
390
+ if (
391
+ onUsage &&
392
+ (inputTokens || outputTokens || cacheReadTokens || cacheCreationTokens)
393
+ ) {
394
+ onUsage({
395
+ inputTokens,
396
+ outputTokens,
397
+ cacheReadTokens,
398
+ cacheCreationTokens,
399
+ });
329
400
  }
330
401
 
331
402
  return fullResponse;
@@ -362,7 +433,12 @@ export async function* chatStream(messages, options) {
362
433
  waiter = null;
363
434
  w();
364
435
  };
436
+ // Count of tokens the provider has actually emitted this turn. A connection
437
+ // drop is only safe to retry while this is 0 — once any token has been
438
+ // streamed, re-issuing would duplicate visible output.
439
+ let tokensEmitted = 0;
365
440
  const onToken = (token) => {
441
+ tokensEmitted++;
366
442
  queue.push(token);
367
443
  wake();
368
444
  };
@@ -376,7 +452,7 @@ export async function* chatStream(messages, options) {
376
452
  let providerCall;
377
453
  if (provider === "ollama") {
378
454
  providerCall = () =>
379
- streamOllama(messages, model, baseUrl, onToken, onUsage);
455
+ streamOllama(messages, model, baseUrl, onToken, onUsage, options.onStall);
380
456
  } else if (provider === "anthropic") {
381
457
  const providerDef = BUILT_IN_PROVIDERS.anthropic;
382
458
  const url =
@@ -392,7 +468,15 @@ export async function* chatStream(messages, options) {
392
468
  );
393
469
  }
394
470
  providerCall = () =>
395
- streamAnthropic(messages, model, url, key, onToken, onUsage);
471
+ streamAnthropic(
472
+ messages,
473
+ model,
474
+ url,
475
+ key,
476
+ onToken,
477
+ onUsage,
478
+ options.onStall,
479
+ );
396
480
  } else {
397
481
  const providerDef = BUILT_IN_PROVIDERS[provider];
398
482
  const url =
@@ -408,7 +492,15 @@ export async function* chatStream(messages, options) {
408
492
  );
409
493
  }
410
494
  providerCall = () =>
411
- streamOpenAI(messages, model, url, key, onToken, onUsage);
495
+ streamOpenAI(
496
+ messages,
497
+ model,
498
+ url,
499
+ key,
500
+ onToken,
501
+ onUsage,
502
+ options.onStall,
503
+ );
412
504
  }
413
505
 
414
506
  // Run the provider stream concurrently with the queue-drain loop. finally
@@ -416,7 +508,36 @@ export async function* chatStream(messages, options) {
416
508
  // any token still terminates the generator cleanly.
417
509
  const streamPromise = (async () => {
418
510
  try {
419
- return await providerCall();
511
+ let attempt = 0;
512
+ for (;;) {
513
+ try {
514
+ return await providerCall();
515
+ } catch (err) {
516
+ // Retry ONLY a transient connection drop that hit before any token
517
+ // reached the user (tokensEmitted === 0) — otherwise the re-issue
518
+ // would duplicate the visible answer. Mirrors agent-core's
519
+ // zero-output retry safety (cc agent parity). Stall aborts (180s) and
520
+ // HTTP/auth errors are not retryable and surface immediately.
521
+ if (
522
+ attempt >= STREAM_RETRY_MAX ||
523
+ tokensEmitted > 0 ||
524
+ !isRetryableStreamError(err)
525
+ ) {
526
+ throw err;
527
+ }
528
+ attempt++;
529
+ if (typeof options.onStreamRetry === "function") {
530
+ try {
531
+ options.onStreamRetry(attempt, err);
532
+ } catch {
533
+ /* retry notice is best-effort */
534
+ }
535
+ }
536
+ await new Promise((r) =>
537
+ setTimeout(r, STREAM_RETRY_BASE_MS * 2 ** (attempt - 1)),
538
+ );
539
+ }
540
+ }
420
541
  } finally {
421
542
  done = true;
422
543
  wake();
@@ -451,6 +572,8 @@ export async function* chatStream(messages, options) {
451
572
  usage: {
452
573
  input_tokens: capturedUsage.inputTokens,
453
574
  output_tokens: capturedUsage.outputTokens,
575
+ cache_read_input_tokens: capturedUsage.cacheReadTokens || 0,
576
+ cache_creation_input_tokens: capturedUsage.cacheCreationTokens || 0,
454
577
  },
455
578
  });
456
579
  } catch {
@@ -258,6 +258,11 @@ module.exports = {
258
258
  async execute(task, context) {
259
259
  const input = (task?.params?.input || task?.action || "").trim();
260
260
  if (!input) return { success: false, error: "No input provided" };
261
+ // Security: input is interpolated into a shell command, so reject shell
262
+ // metacharacters to prevent command injection from untrusted skill input.
263
+ if (/[;&|\`$<>()\\n\\r]/.test(input)) {
264
+ return { success: false, error: "Input contains unsafe shell characters; refused." };
265
+ }
261
266
  try {
262
267
  const output = execSync(\`${command} \${input}\`, {
263
268
  encoding: "utf-8",
@@ -0,0 +1,38 @@
1
+ /**
2
+ * cli-numeric — validated parsing for user-supplied numeric CLI options.
3
+ *
4
+ * `parseInt`/`Number` on a bad arg yields NaN that silently corrupts whatever
5
+ * consumes it (a `LIMIT NaN`, a `Date.now()+NaN` deadline, a stored NaN config).
6
+ * This centralizes parse → validate → fall-back-or-throw so a command either
7
+ * fails LOUDLY with a clear message or falls back to a sane default — never
8
+ * threads NaN downstream.
9
+ *
10
+ * @param {*} raw the option value (string from commander, or undefined)
11
+ * @param {object} [opts]
12
+ * @param {string} [opts.name="value"] label for error messages (e.g. "--limit")
13
+ * @param {boolean} [opts.integer=false] parse as integer (parseInt) vs float (Number)
14
+ * @param {number} [opts.min] inclusive lower bound
15
+ * @param {number} [opts.max] inclusive upper bound
16
+ * @param {number} [opts.fallback] returned for missing/invalid input; when
17
+ * omitted, invalid input THROWS instead
18
+ * @returns {number}
19
+ */
20
+ export function numericOption(raw, opts = {}) {
21
+ const { name = "value", integer = false, min, max } = opts;
22
+ const hasFallback = Object.prototype.hasOwnProperty.call(opts, "fallback");
23
+ const fail = (msg) => {
24
+ if (hasFallback) return opts.fallback;
25
+ throw new Error(msg);
26
+ };
27
+
28
+ if (raw == null || raw === "") return fail(`${name} is required`);
29
+ const n = integer ? parseInt(raw, 10) : Number(raw);
30
+ if (!Number.isFinite(n)) {
31
+ return fail(
32
+ `${name} must be a ${integer ? "whole " : ""}number (got ${JSON.stringify(String(raw))})`,
33
+ );
34
+ }
35
+ if (min != null && n < min) return fail(`${name} must be >= ${min}`);
36
+ if (max != null && n > max) return fail(`${name} must be <= ${max}`);
37
+ return n;
38
+ }
@@ -1,7 +1,15 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ renameSync,
7
+ unlinkSync,
8
+ } from "node:fs";
2
9
  import { dirname } from "node:path";
3
10
  import { getConfigPath } from "./paths.js";
4
11
  import { DEFAULT_CONFIG } from "../constants.js";
12
+ import { withFileLock } from "./with-file-lock.js";
5
13
 
6
14
  export function loadConfig() {
7
15
  const configPath = getConfigPath();
@@ -20,7 +28,24 @@ export function loadConfig() {
20
28
  export function saveConfig(config) {
21
29
  const configPath = getConfigPath();
22
30
  mkdirSync(dirname(configPath), { recursive: true });
23
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
31
+ // Atomic write: config.json holds API keys + LLM settings, so a crash (or two
32
+ // concurrent `cc config set`) mid-write must never leave a truncated/corrupt
33
+ // file — that would break every cc command and could lose credentials. Write a
34
+ // temp sibling, then rename over the target (rename is atomic within a
35
+ // filesystem), so a reader/crash sees either the old file or the complete new
36
+ // one, never a half-written one.
37
+ const tmp = `${configPath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
38
+ try {
39
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
40
+ renameSync(tmp, configPath);
41
+ } catch (err) {
42
+ try {
43
+ if (existsSync(tmp)) unlinkSync(tmp);
44
+ } catch {
45
+ /* best-effort temp cleanup */
46
+ }
47
+ throw err;
48
+ }
24
49
  }
25
50
 
26
51
  export function getConfigValue(key) {
@@ -29,10 +54,16 @@ export function getConfigValue(key) {
29
54
  }
30
55
 
31
56
  export function setConfigValue(key, value) {
32
- const config = loadConfig();
33
- setNestedValue(config, key, parseValue(value));
34
- saveConfig(config);
35
- return config;
57
+ // Serialize the read-modify-write across processes: two concurrent
58
+ // `cc config set` invocations would otherwise each load, mutate, and write
59
+ // back, silently losing one update. Best-effort lock (see with-file-lock):
60
+ // on contention timeout it proceeds anyway, so the CLI never hangs.
61
+ return withFileLock(getConfigPath(), () => {
62
+ const config = loadConfig();
63
+ setNestedValue(config, key, parseValue(value));
64
+ saveConfig(config);
65
+ return config;
66
+ });
36
67
  }
37
68
 
38
69
  export function resetConfig() {
@@ -160,13 +160,17 @@ REASON: (1-2 sentence justification)`;
160
160
  };
161
161
  }
162
162
 
163
- function parseScores(text, variants, criteria) {
163
+ export function parseScores(text, variants, criteria) {
164
164
  const scores = [];
165
165
  for (let i = 0; i < variants.length; i++) {
166
166
  const variantScores = {};
167
167
  for (const c of criteria) {
168
+ // The criterion is matched LITERALLY — escape regex metacharacters so a
169
+ // name like "latency(ms)" or "f.score" doesn't silently mis-parse (and an
170
+ // unbalanced one like "a)b(" doesn't throw and crash the whole compare).
171
+ const cEsc = String(c).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
168
172
  const pattern = new RegExp(
169
- `variant\\s*${i + 1}[^\\n]*${c}\\s*=\\s*(\\d+)`,
173
+ `variant\\s*${i + 1}[^\\n]*${cEsc}\\s*=\\s*(\\d+)`,
170
174
  "i",
171
175
  );
172
176
  const match = text.match(pattern);
@@ -32,6 +32,7 @@
32
32
  import crypto from "node:crypto";
33
33
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
34
34
  import { join } from "node:path";
35
+ import { withFileLock } from "./with-file-lock.js";
35
36
 
36
37
  export const _deps = {
37
38
  existsSync,
@@ -260,49 +261,59 @@ export function addSchedule(cwd, input) {
260
261
  const err = validateCron(cron);
261
262
  if (err) throw new Error(`invalid cron: ${err}`);
262
263
 
263
- const schedules = loadSchedules(cwd);
264
- const entry = {
265
- id: `sch-${crypto.randomUUID().slice(0, 12)}`,
266
- cron: cron.trim(),
267
- templateId,
268
- userMessage,
269
- files: Array.isArray(files) ? files : [],
270
- enabled: true,
271
- createdAt: _deps.now().toISOString(),
272
- lastRunAt: null,
273
- lastStatus: null,
274
- };
275
- schedules.push(entry);
276
- saveSchedules(cwd, schedules);
277
- return entry;
264
+ // Serialize the read-modify-write across processes (cc+cc / cc+desktop share
265
+ // the cowork dir) so concurrently-added schedules don't clobber each other.
266
+ return withFileLock(_scheduleFile(cwd), () => {
267
+ const schedules = loadSchedules(cwd);
268
+ const entry = {
269
+ id: `sch-${crypto.randomUUID().slice(0, 12)}`,
270
+ cron: cron.trim(),
271
+ templateId,
272
+ userMessage,
273
+ files: Array.isArray(files) ? files : [],
274
+ enabled: true,
275
+ createdAt: _deps.now().toISOString(),
276
+ lastRunAt: null,
277
+ lastStatus: null,
278
+ };
279
+ schedules.push(entry);
280
+ saveSchedules(cwd, schedules);
281
+ return entry;
282
+ });
278
283
  }
279
284
 
280
285
  export function removeSchedule(cwd, id) {
281
- const schedules = loadSchedules(cwd);
282
- const idx = schedules.findIndex((s) => s.id === id);
283
- if (idx === -1) return false;
284
- schedules.splice(idx, 1);
285
- saveSchedules(cwd, schedules);
286
- return true;
286
+ return withFileLock(_scheduleFile(cwd), () => {
287
+ const schedules = loadSchedules(cwd);
288
+ const idx = schedules.findIndex((s) => s.id === id);
289
+ if (idx === -1) return false;
290
+ schedules.splice(idx, 1);
291
+ saveSchedules(cwd, schedules);
292
+ return true;
293
+ });
287
294
  }
288
295
 
289
296
  export function setScheduleEnabled(cwd, id, enabled) {
290
- const schedules = loadSchedules(cwd);
291
- const s = schedules.find((x) => x.id === id);
292
- if (!s) return false;
293
- s.enabled = !!enabled;
294
- saveSchedules(cwd, schedules);
295
- return true;
297
+ return withFileLock(_scheduleFile(cwd), () => {
298
+ const schedules = loadSchedules(cwd);
299
+ const s = schedules.find((x) => x.id === id);
300
+ if (!s) return false;
301
+ s.enabled = !!enabled;
302
+ saveSchedules(cwd, schedules);
303
+ return true;
304
+ });
296
305
  }
297
306
 
298
307
  export function updateScheduleRunState(cwd, id, { lastRunAt, lastStatus }) {
299
- const schedules = loadSchedules(cwd);
300
- const s = schedules.find((x) => x.id === id);
301
- if (!s) return false;
302
- if (lastRunAt) s.lastRunAt = lastRunAt;
303
- if (lastStatus) s.lastStatus = lastStatus;
304
- saveSchedules(cwd, schedules);
305
- return true;
308
+ return withFileLock(_scheduleFile(cwd), () => {
309
+ const schedules = loadSchedules(cwd);
310
+ const s = schedules.find((x) => x.id === id);
311
+ if (!s) return false;
312
+ if (lastRunAt) s.lastRunAt = lastRunAt;
313
+ if (lastStatus) s.lastStatus = lastStatus;
314
+ saveSchedules(cwd, schedules);
315
+ return true;
316
+ });
306
317
  }
307
318
 
308
319
  // ─── Scheduler ───────────────────────────────────────────────────────────────
@@ -20,6 +20,8 @@ import {
20
20
  writeFileSync,
21
21
  mkdirSync,
22
22
  appendFileSync,
23
+ renameSync,
24
+ unlinkSync,
23
25
  } from "node:fs";
24
26
  import { join } from "node:path";
25
27
 
@@ -29,9 +31,37 @@ export const _deps = {
29
31
  writeFileSync,
30
32
  mkdirSync,
31
33
  appendFileSync,
34
+ renameSync,
35
+ unlinkSync,
32
36
  now: () => new Date(),
33
37
  };
34
38
 
39
+ /**
40
+ * Atomically write a learned-template JSON via _deps. A crash mid-write would
41
+ * truncate `<templateId>.json`; loadUserTemplate's catch then drops it, losing
42
+ * the learned prompt extension. Temp sibling + rename (atomic within a
43
+ * filesystem). Graceful-degrade to a direct write when the injected fs lacks
44
+ * renameSync — the real fs always has it, so production is always atomic.
45
+ */
46
+ function _atomicWriteJson(file, data) {
47
+ if (typeof _deps.renameSync !== "function") {
48
+ _deps.writeFileSync(file, data, "utf-8");
49
+ return;
50
+ }
51
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
52
+ try {
53
+ _deps.writeFileSync(tmp, data, "utf-8");
54
+ _deps.renameSync(tmp, file);
55
+ } catch (err) {
56
+ try {
57
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
58
+ } catch {
59
+ /* best-effort temp cleanup */
60
+ }
61
+ throw err;
62
+ }
63
+ }
64
+
35
65
  /** Minimum historical runs before a template qualifies for a patch suggestion. */
36
66
  export const MIN_RUNS_FOR_PATCH = 10;
37
67
  /** Minimum distinct failures needed to trigger a suggestion. */
@@ -414,7 +444,7 @@ export function applyPromptPatch(cwd, patch) {
414
444
  updatedAt: _deps.now().toISOString(),
415
445
  };
416
446
  const file = join(dir, `${patch.templateId}.json`);
417
- _deps.writeFileSync(file, JSON.stringify(doc, null, 2), "utf-8");
447
+ _atomicWriteJson(file, JSON.stringify(doc, null, 2));
418
448
 
419
449
  // Audit trail — never pruned automatically
420
450
  _deps.appendFileSync(
@@ -15,7 +15,14 @@
15
15
  * @module cowork-share
16
16
  */
17
17
 
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import {
19
+ existsSync,
20
+ mkdirSync,
21
+ readFileSync,
22
+ writeFileSync,
23
+ renameSync,
24
+ unlinkSync,
25
+ } from "node:fs";
19
26
  import { join } from "node:path";
20
27
  import crypto, { createHash } from "node:crypto";
21
28
  import {
@@ -29,9 +36,38 @@ export const _deps = {
29
36
  mkdirSync,
30
37
  readFileSync,
31
38
  writeFileSync,
39
+ renameSync,
40
+ unlinkSync,
32
41
  now: () => new Date().toISOString(),
33
42
  };
34
43
 
44
+ /**
45
+ * Atomically write a JSON file via _deps. Imported result packets are persistent
46
+ * local state (shared-results/<taskId>.json) and exported packets are read by
47
+ * other tools — a crash mid-write must not leave a truncated/invalid packet
48
+ * (verifyPacket/readPacket would reject it). Temp sibling + rename (atomic
49
+ * within a filesystem). Graceful-degrade to a direct write when the injected fs
50
+ * lacks renameSync — the real fs always has it, so production is always atomic.
51
+ */
52
+ function _atomicWriteJson(file, data) {
53
+ if (typeof _deps.renameSync !== "function") {
54
+ _deps.writeFileSync(file, data, "utf-8");
55
+ return;
56
+ }
57
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
58
+ try {
59
+ _deps.writeFileSync(tmp, data, "utf-8");
60
+ _deps.renameSync(tmp, file);
61
+ } catch (err) {
62
+ try {
63
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
64
+ } catch {
65
+ /* best-effort temp cleanup */
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+
35
71
  const SUPPORTED_SIG_ALG = "Ed25519";
36
72
 
37
73
  const PACKET_VERSION = 1;
@@ -256,7 +292,7 @@ export function findHistoryRecord(cwd, taskId) {
256
292
  * Write a packet to disk as pretty-printed JSON.
257
293
  */
258
294
  export function writePacket(filePath, packet) {
259
- _deps.writeFileSync(filePath, JSON.stringify(packet, null, 2), "utf-8");
295
+ _atomicWriteJson(filePath, JSON.stringify(packet, null, 2));
260
296
  return filePath;
261
297
  }
262
298
 
@@ -317,7 +353,7 @@ export function importResultPacket(cwd, packet) {
317
353
  const dir = join(cwd, ".chainlesschain", "cowork", "shared-results");
318
354
  _deps.mkdirSync(dir, { recursive: true });
319
355
  const file = join(dir, `${packet.payload.taskId}.json`);
320
- _deps.writeFileSync(file, JSON.stringify(packet, null, 2), "utf-8");
356
+ _atomicWriteJson(file, JSON.stringify(packet, null, 2));
321
357
  return { file, taskId: packet.payload.taskId };
322
358
  }
323
359