chainlesschain 0.162.94 → 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.
- package/README.md +1 -1
- package/bin/chainlesschain.js +8 -18
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CiEoV_Hg.js → AIOps-Bo9Ux24z.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DrrjQMuK.js → ActionButton-CExgXSQT.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-ChlhPe9a.js → Analytics-BNnZhRTZ.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CIkI7d5o.js → AppLayout-D9xfqFCT.js} +4 -4
- package/src/assets/web-panel/assets/{Audit-_sY9bNSN.js → Audit-Cpgu8uCw.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DvzaThN8.js → Backup-CHdEJnNE.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DDZW_DU8.js → BaseInput-Cbw-PxBq.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DqErkog2.js → Chat-ChHTZK_6.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BcnNHS-r.js → ChatBubbleRenderer-Dl6z6ZH7.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-NWDuyRK6.js → Checkbox-Cj8d9bru.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B_jnSC8a.js → Codegen-C2YobW4j.js} +1 -1
- package/src/assets/web-panel/assets/{Col-B-uyUy0z.js → Col-CxfV5FGT.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Dmf1MpG7.js → Community-CiwOASMj.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-fGI68M5y.js → Compact-CTg_2iKW.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-PV9qT-iV.js → Compliance-HMbsTmtw.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BM6BvCgv.js → Cowork-DyNDqGR2.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BsSWwFFo.js → Cron-9-x5M7qy.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DUdPVYvS.js → Crosschain-4msgDaYJ.js} +1 -1
- package/src/assets/web-panel/assets/{DID-PPLLhrJs.js → DID--2Beb5Po.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Cn_Xjj80.js → Dashboard-T6nq0Lzs.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DkTXjy5D.js → Dropdown-D_Q6T10v.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BnA27cjB.js → EmailListRenderer-TnN21bJx.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-jqrv5VCp.js → FamilyGuardDashboard-kky_uIij.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DESwXwtR.js → Federation-C1GkNY4B.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-C9gbyY1r.js → FormItemContext-5-_Fc5YV.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CkZB9crH.js → GenericCardRenderer-CmHiB7sr.js} +1 -1
- package/src/assets/web-panel/assets/{Git-B6niu0W9.js → Git-Cuci_BPI.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BrF28dEi.js → Governance-BU0cO2LD.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CZDnV8SN.js → Inference-BDFNgEcx.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-eqZ0hP0l.js → KnowledgeGraph-DITXUzHz.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BlHS7DoS.js → Logs-Ea-07CRd.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-NOp9G83e.js → Marketplace-D6OBbRVG.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-B6wMx173.js → McpTools-eiKpIJMi.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-BKVeTAeT.js → Memory-M-X-T8UF.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Da2y6iYW.js → MobileBridge-xcZb9Ekv.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-DCfoTPRC.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-B7Hy5rAT.js → Mtc-BWfGDzD8.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BgBoX0XI.js → MtcAudit-imBuld-U.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Db7bwi5J.js → Multisig-C2kcsq7I.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-f9ca2vst.js → NLProgramming-DObvXSXP.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-meVqoOMz.js → Notes-C-6OGJMe.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BfEhBXNo.js → NotificationSettings-ub7Ozxci.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-e25Q6ALJ.js → OrderTableRenderer-CR2W89Vk.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BpgOG9lj.js → Organization-DNA_Wty7.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-fI3L05Rq.js → Overflow-rT4Wb4rQ.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-wD0wpJJY.js → P2P-CK-hARKv.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BWV9J4VZ.js → PdhVaultBrowser-C2pbtNbT.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BJH7W2iv.js → Permissions-B8EIEqGJ.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-CqQ3XpMz.js → PersonalDataHub-BxNVNU3s.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CHyXCU1i.js → Pipeline-CT9pbkyY.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CJtfNlc7.js → Privacy-DhCNd2CS.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CKOaHFGR.js → ProjectInit-Qngnx-vQ.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-QD8wp_EM.js → ProjectSettings-Dfn-nQc1.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BKdiSaPR.js +1 -0
- package/src/assets/web-panel/assets/{Providers-BPESqCEg.js → Providers-4o9B_n-X.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-C5AzvvRo.js → QuickAsk-bs1qvF30.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-cGpF3tsA.js → Recommend-CuEnKA2K.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-Duaxvbt9.js → Reputation-DbfvmRb2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-xsUwB5Ay.js → Row-DvMJGAI3.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DmUUsqaF.js → RssFeed-oJ0BZvmA.js} +3 -3
- package/src/assets/web-panel/assets/{Search-Do9YuDbX.js → Search-DnIVrY9O.js} +1 -1
- package/src/assets/web-panel/assets/{Security-06BgEn-d.js → Security-CIWhI_Tc.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DyaCes7r.js → Services-DLrnpjwm.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Cj_VbkFG.js → Skeleton-CYmqgtVb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DCOWMJOR.js → Skills-BUzCkoeu.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-C44_86bu.js → Sla-DYRE4bnS.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BXhBJSMB.js → SpeechSettings-_6MurdOg.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BpHsaNWg.js → SyncSettings-UIdnF5Li.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-COx2CjT8.js +1 -0
- package/src/assets/web-panel/assets/{Templates-aEWbCOdo.js → Templates-BcHAmkZo.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DkOLw1Yz.js → Tenant-CjYQS8yZ.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-5Z4paIoh.js → Terminal-BmfOPcWt.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CYhublOm.js → TimelineRenderer-D5HuarNQ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-h57dBT4T.js → Tokens-C_fbProA.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-abE21FpD.js → Trigger-JPulpRx8.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-urB3VZip.js → Trust-CnO-062O.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-D6uXV9pp.js → UkeySign-HpNovvau.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C9MxXAzw.js → VideoEditing-yMncxH7y.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DTn5rW4u.js → Wallet-Cug-t7Np.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-BNYEqeqr.js → WebAuthn--U1-NR-3.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-CsRoOJPT.js → WorkflowEditor-R-XvSJ3S.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B5GoUHDS.js → chat-CCI1i6Rp.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BF_Rqaab.js → colors-C-mqoaLt.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BNPfqT4P.js → compact-item-De_N2iRR.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C3qLKXX-.js → createContext-DbTM6sjZ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-OcLPHSl5.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BLpAeFXB.js → hasIn-D0Adylyc.js} +1 -1
- package/src/assets/web-panel/assets/index-7E_mHkDU.js +1 -0
- package/src/assets/web-panel/assets/{index-BOoTjHGU.js → index-AY937BxS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cv1WRH-j.js → index-B6hgy5VC.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2wJowV1.js → index-B7XnpLHk.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dj6rUk3f.js → index-B7gw-tDm.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dz7o1Zt6.js → index-BBgDgkwW.js} +1 -1
- package/src/assets/web-panel/assets/{index-CMRdeB2r.js → index-BMmeiw88.js} +1 -1
- package/src/assets/web-panel/assets/{index-2-7AD4Fq.js → index-BU6XtnSx.js} +1 -1
- package/src/assets/web-panel/assets/{index-7Tt41tHa.js → index-Bi7VUG0R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaROOJ5Z.js → index-BtE3cw9m.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmrPbYXh.js → index-C9ej-MF0.js} +3 -3
- package/src/assets/web-panel/assets/{index-CgIrRLBP.js → index-CIEiQl1j.js} +1 -1
- package/src/assets/web-panel/assets/{index-DudglWTC.js → index-CKgaW9E5.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8B3rHnr.js → index-CY0KKR5f.js} +1 -1
- package/src/assets/web-panel/assets/index-CbV3-WlB.js +1 -0
- package/src/assets/web-panel/assets/{index-ixN0R0We.js → index-Co1UkcFX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cqb8TXEp.js → index-CzP5d5LW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DH95hKUN.js → index-D0NJ5lRi.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6yRwT6e.js → index-D0gBsqTX.js} +1 -1
- package/src/assets/web-panel/assets/{index-o1doEeT3.js → index-D5F6shtn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxWEK9FS.js → index-DVtamtlm.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cxd2RsZR.js → index-DWT18hDG.js} +1 -1
- package/src/assets/web-panel/assets/{index-vXHPZOmO.js → index-DaXb4B9P.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJsVUj80.js → index-DjQA0Vez.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIjPwa7h.js → index-Ds8NJVEL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMKiwMJa.js → index-DuP-8HWu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIpGZeh2.js → index-DwLTiJ31.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn95Blhx.js → index-T3VhH8cO.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqjU6Jp9.js → index-TDY1o0w3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBFekqcY.js → index-TRy2Gc3V.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-PNuV4r.js → index-W1OMxCmI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlEDjdTc.js → index-ftFRZbuc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMKbLaTg.js → index-iGPGaORA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYWExa2Z.js → index-jhy5KQvg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BryugSCS.js → index-qUSZJcgw.js} +1 -1
- package/src/assets/web-panel/assets/{index-81hgV6U6.js → index-w3f6lqkD.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmBMP52w.js → index-xdMc71Kh.js} +1 -1
- package/src/assets/web-panel/assets/{index-KUdyAwqr.js → index-xhX5eg2Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGVf-eOG.js → index-zkTiW6d6.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Dhfx-jOJ.js → initDefaultProps-CIHqouWk.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DzZPc4Mq.js → motion-D1iUKftk.js} +1 -1
- package/src/assets/web-panel/assets/{move-D6R8EIjz.js → move-YOpWjmjL.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CPniUUR1.js → omit-C_1357UY.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DbzxHNNa.js → pickAttrs-CDb4dq6o.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B_MxmwXz.js → placementArrow-D-OGYyhd.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Em-jKJ7U.js → responsiveObserve-hS-j69A0.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B2RV1mQl.js → slide-tnjuvphU.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-C9Ps6HQe.js → statusUtils-BERfMMcU.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCoehh1S.js → styleChecker-DKpl2K9E.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DZOaLVto.js → useFlexGapSupport-C_sX4D3N.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Dk_JA91R.js → useFs-DBcxC1vK.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CJR6Dqtz.js → usePersonalDataHub-CeoDf2XI.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BaFNaDQL.js → vnode-DC9Pmj_u.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CxA9IYpM.js → zoom-CZA58J7M.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/activitypub.js +19 -3
- package/src/commands/agent.js +25 -0
- package/src/commands/ask.js +13 -0
- package/src/commands/config.js +17 -1
- package/src/commands/cost.js +6 -1
- package/src/commands/dao.js +16 -3
- package/src/commands/dlp.js +7 -1
- package/src/commands/hub.js +13 -0
- package/src/commands/mtc.js +37 -1
- package/src/commands/pdh.js +162 -0
- package/src/gateways/discord/discord-formatter.js +18 -1
- package/src/gateways/telegram/telegram-formatter.js +6 -3
- package/src/gateways/ws/ws-server.js +19 -1
- package/src/harness/mcp-client.js +19 -2
- package/src/harness/plugin-manager.js +20 -0
- package/src/harness/worktree-isolator.js +17 -1
- package/src/index.js +2 -0
- package/src/lib/__tests__/model-deprecation.test.js +194 -0
- package/src/lib/a2a-protocol.js +23 -9
- package/src/lib/agent-core.js +2 -5
- package/src/lib/audit-mtc.js +40 -2
- package/src/lib/chat-core.js +141 -18
- package/src/lib/claude-code-bridge.js +14 -2
- package/src/lib/cli-anything-bridge.js +5 -0
- package/src/lib/cli-numeric.js +38 -0
- package/src/lib/config-manager.js +37 -6
- package/src/lib/cowork/ab-comparator-cli.js +6 -2
- package/src/lib/cowork-cron.js +45 -34
- package/src/lib/cowork-learning.js +31 -1
- package/src/lib/cowork-share.js +39 -3
- package/src/lib/cowork-template-marketplace.js +54 -2
- package/src/lib/cowork-workflow.js +33 -6
- package/src/lib/cross-chain-mtc.js +41 -12
- package/src/lib/crypto-manager.js +12 -2
- package/src/lib/dbevo.js +19 -8
- package/src/lib/downloader.js +24 -9
- package/src/lib/evomap-governance.js +4 -4
- package/src/lib/evomap-manager.js +54 -7
- package/src/lib/fatal-handler.js +48 -0
- package/src/lib/file-checkpoint.js +56 -4
- package/src/lib/git-integration.js +28 -0
- package/src/lib/goal-store.js +36 -10
- package/src/lib/hierarchical-memory.js +6 -0
- package/src/lib/llm-pricing.js +52 -2
- package/src/lib/mcp-oauth.js +37 -7
- package/src/lib/memory-manager.js +23 -2
- package/src/lib/model-deprecation.js +249 -0
- package/src/lib/pdh-feedback-ledger.js +131 -0
- package/src/lib/permanent-memory.js +30 -2
- package/src/lib/permission-rules.cjs +11 -3
- package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
- package/src/lib/personal-data-hub-wiring.js +36 -13
- package/src/lib/project-instructions.js +10 -4
- package/src/lib/runnable-provider.js +62 -31
- package/src/lib/session-tail.js +6 -1
- package/src/lib/session-usage.js +34 -1
- package/src/lib/skill-loader.js +4 -1
- package/src/lib/skill-packs/generator.js +25 -2
- package/src/lib/stream-retry.js +56 -0
- package/src/lib/sync-credentials.js +53 -17
- package/src/lib/token-tracker.js +12 -2
- package/src/lib/with-file-lock.js +87 -0
- package/src/repl/agent-repl.js +52 -13
- package/src/repl/chat-repl.js +10 -0
- package/src/repl/session-cost.js +31 -3
- package/src/runtime/agent-core.js +474 -53
- package/src/runtime/coding-agent-contract-shared.cjs +58 -1
- package/src/runtime/coding-agent-policy.cjs +10 -0
- package/src/runtime/file-ref-expander.js +29 -1
- package/src/runtime/headless-runner.js +51 -6
- package/src/runtime/headless-stream.js +219 -2
- package/src/skills/video-editing/tools/commit.js +15 -1
- package/src/assets/web-panel/assets/MobileProjects-CTFPhn4A.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dj33CO-H.js +0 -1
- package/src/assets/web-panel/assets/Tasks-C83eyBn8.js +0 -1
- package/src/assets/web-panel/assets/devWarning-D5Rwbl_Y.js +0 -1
- package/src/assets/web-panel/assets/index-7ls1Z3dH.js +0 -1
- package/src/assets/web-panel/assets/index-DoldiBPq.js +0 -1
|
@@ -92,48 +92,79 @@ export function makeRunnableProviderFallback(
|
|
|
92
92
|
chatFn,
|
|
93
93
|
{ env = process.env, onFallback } = {},
|
|
94
94
|
) {
|
|
95
|
+
// Notify at most once per from→to pair: in an agent loop the same fallback
|
|
96
|
+
// would otherwise re-warn on every turn.
|
|
97
|
+
const notified = new Set();
|
|
95
98
|
const notify = (info) => {
|
|
96
|
-
if (typeof onFallback
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
if (typeof onFallback !== "function") return;
|
|
100
|
+
const key = `${info.from}→${info.to}`;
|
|
101
|
+
if (notified.has(key)) return;
|
|
102
|
+
notified.add(key);
|
|
103
|
+
try {
|
|
104
|
+
onFallback(info);
|
|
105
|
+
} catch {
|
|
106
|
+
/* notification is best-effort */
|
|
102
107
|
}
|
|
103
108
|
};
|
|
104
|
-
return async function runnableProviderFallback(messages, options = {}) {
|
|
105
|
-
try {
|
|
106
|
-
return await chatFn(messages, options);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
if (!isAuthError(err)) throw err;
|
|
109
|
-
const failed = options.provider;
|
|
110
109
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
// Compute the fallback for these options WITHOUT calling the model — the
|
|
111
|
+
// resolution is deterministic (baseUrl-inferred provider, then env-keyed
|
|
112
|
+
// provider). Returns { to, reason, override } or null. Pure so we can both
|
|
113
|
+
// react to an auth failure AND pre-empt the known-bad attempt on later turns.
|
|
114
|
+
const computeFallback = (options) => {
|
|
115
|
+
const failed = options.provider;
|
|
116
|
+
const inferred = inferProviderFromBaseUrl(options.baseUrl);
|
|
117
|
+
if (inferred && inferred !== failed) {
|
|
118
|
+
return {
|
|
119
|
+
to: inferred,
|
|
120
|
+
reason: "baseurl-mismatch",
|
|
121
|
+
override: {
|
|
117
122
|
provider: inferred,
|
|
118
123
|
model: defaultModelFor(inferred) || options.model,
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
|
|
128
|
+
if (name === failed) continue;
|
|
129
|
+
if (def.apiKeyEnv && nonEmpty(env[def.apiKeyEnv])) {
|
|
130
|
+
return {
|
|
131
|
+
to: name,
|
|
132
|
+
reason: "env-key",
|
|
133
|
+
override: {
|
|
129
134
|
provider: name,
|
|
130
135
|
baseUrl: def.baseUrl,
|
|
131
136
|
apiKey: envKey(name, env),
|
|
132
137
|
model: defaultModelFor(name) || options.model,
|
|
133
|
-
}
|
|
134
|
-
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
135
140
|
}
|
|
136
|
-
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Providers we have already proven need a fallback — later turns route
|
|
146
|
+
// straight to the working provider instead of re-attempting the broken one.
|
|
147
|
+
const stickyFrom = new Set();
|
|
148
|
+
|
|
149
|
+
return async function runnableProviderFallback(messages, options = {}) {
|
|
150
|
+
// Known-bad provider: skip the wasted failed attempt and route directly.
|
|
151
|
+
if (stickyFrom.has(options.provider)) {
|
|
152
|
+
const fb = computeFallback(options);
|
|
153
|
+
if (fb) {
|
|
154
|
+
notify({ from: options.provider, to: fb.to, reason: fb.reason });
|
|
155
|
+
return await chatFn(messages, { ...options, ...fb.override });
|
|
156
|
+
}
|
|
157
|
+
// Resolution no longer available — fall through to a normal attempt.
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
return await chatFn(messages, options);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (!isAuthError(err)) throw err;
|
|
163
|
+
const fb = computeFallback(options);
|
|
164
|
+
if (!fb) throw err; // nowhere runnable — surface the clear error
|
|
165
|
+
stickyFrom.add(options.provider);
|
|
166
|
+
notify({ from: options.provider, to: fb.to, reason: fb.reason });
|
|
167
|
+
return await chatFn(messages, { ...options, ...fb.override });
|
|
137
168
|
}
|
|
138
169
|
};
|
|
139
170
|
}
|
package/src/lib/session-tail.js
CHANGED
|
@@ -89,6 +89,10 @@ export async function* followSession(sessionId, options = {}) {
|
|
|
89
89
|
? fromOffset
|
|
90
90
|
: initialOffset(sessionId, { fromStart });
|
|
91
91
|
let buffer = "";
|
|
92
|
+
// Streaming decoder persisted across polls: when the file grows by a partial
|
|
93
|
+
// multi-byte UTF-8 character between reads (the rest arrives next poll), the
|
|
94
|
+
// held bytes are reassembled instead of corrupted into U+FFFD.
|
|
95
|
+
let decoder = new TextDecoder("utf-8");
|
|
92
96
|
|
|
93
97
|
// eslint-disable-next-line no-constant-condition
|
|
94
98
|
while (true) {
|
|
@@ -100,6 +104,7 @@ export async function* followSession(sessionId, options = {}) {
|
|
|
100
104
|
// File was truncated / rotated — restart from beginning
|
|
101
105
|
offset = 0;
|
|
102
106
|
buffer = "";
|
|
107
|
+
decoder = new TextDecoder("utf-8"); // drop any held partial-char bytes
|
|
103
108
|
}
|
|
104
109
|
if (stat.size > offset) {
|
|
105
110
|
const fd = await fsp.open(filePath, "r");
|
|
@@ -108,7 +113,7 @@ export async function* followSession(sessionId, options = {}) {
|
|
|
108
113
|
const buf = Buffer.alloc(length);
|
|
109
114
|
await fd.read(buf, 0, length, offset);
|
|
110
115
|
offset = stat.size;
|
|
111
|
-
buffer +=
|
|
116
|
+
buffer += decoder.decode(buf, { stream: true });
|
|
112
117
|
} finally {
|
|
113
118
|
await fd.close();
|
|
114
119
|
}
|
package/src/lib/session-usage.js
CHANGED
|
@@ -49,8 +49,23 @@ export function extractUsage(event) {
|
|
|
49
49
|
const totalTokens = toNumber(
|
|
50
50
|
raw.total_tokens ?? raw.totalTokens ?? inputTokens + outputTokens,
|
|
51
51
|
);
|
|
52
|
+
// Prompt-cache token counts (Anthropic caching): read tokens are billed at
|
|
53
|
+
// ~10% and creation/write at ~1.25× the input rate, so they are tracked
|
|
54
|
+
// separately for accurate cost. Absent on non-caching providers → 0.
|
|
55
|
+
const cacheReadTokens = toNumber(
|
|
56
|
+
raw.cache_read_input_tokens ?? raw.cacheReadTokens ?? 0,
|
|
57
|
+
);
|
|
58
|
+
const cacheCreationTokens = toNumber(
|
|
59
|
+
raw.cache_creation_input_tokens ?? raw.cacheCreationTokens ?? 0,
|
|
60
|
+
);
|
|
52
61
|
|
|
53
|
-
if (
|
|
62
|
+
if (
|
|
63
|
+
inputTokens === 0 &&
|
|
64
|
+
outputTokens === 0 &&
|
|
65
|
+
totalTokens === 0 &&
|
|
66
|
+
cacheReadTokens === 0 &&
|
|
67
|
+
cacheCreationTokens === 0
|
|
68
|
+
) {
|
|
54
69
|
return null;
|
|
55
70
|
}
|
|
56
71
|
|
|
@@ -60,6 +75,8 @@ export function extractUsage(event) {
|
|
|
60
75
|
inputTokens,
|
|
61
76
|
outputTokens,
|
|
62
77
|
totalTokens,
|
|
78
|
+
cacheReadTokens,
|
|
79
|
+
cacheCreationTokens,
|
|
63
80
|
timestamp: event.timestamp || null,
|
|
64
81
|
};
|
|
65
82
|
}
|
|
@@ -73,6 +90,8 @@ export function aggregateUsage(events) {
|
|
|
73
90
|
inputTokens: 0,
|
|
74
91
|
outputTokens: 0,
|
|
75
92
|
totalTokens: 0,
|
|
93
|
+
cacheReadTokens: 0,
|
|
94
|
+
cacheCreationTokens: 0,
|
|
76
95
|
calls: 0,
|
|
77
96
|
};
|
|
78
97
|
|
|
@@ -83,6 +102,8 @@ export function aggregateUsage(events) {
|
|
|
83
102
|
total.inputTokens += u.inputTokens;
|
|
84
103
|
total.outputTokens += u.outputTokens;
|
|
85
104
|
total.totalTokens += u.totalTokens;
|
|
105
|
+
total.cacheReadTokens += u.cacheReadTokens || 0;
|
|
106
|
+
total.cacheCreationTokens += u.cacheCreationTokens || 0;
|
|
86
107
|
total.calls += 1;
|
|
87
108
|
|
|
88
109
|
const key = `${u.provider || "?"}/${u.model || "?"}`;
|
|
@@ -92,11 +113,15 @@ export function aggregateUsage(events) {
|
|
|
92
113
|
inputTokens: 0,
|
|
93
114
|
outputTokens: 0,
|
|
94
115
|
totalTokens: 0,
|
|
116
|
+
cacheReadTokens: 0,
|
|
117
|
+
cacheCreationTokens: 0,
|
|
95
118
|
calls: 0,
|
|
96
119
|
};
|
|
97
120
|
entry.inputTokens += u.inputTokens;
|
|
98
121
|
entry.outputTokens += u.outputTokens;
|
|
99
122
|
entry.totalTokens += u.totalTokens;
|
|
123
|
+
entry.cacheReadTokens += u.cacheReadTokens || 0;
|
|
124
|
+
entry.cacheCreationTokens += u.cacheCreationTokens || 0;
|
|
100
125
|
entry.calls += 1;
|
|
101
126
|
byKey.set(key, entry);
|
|
102
127
|
}
|
|
@@ -129,6 +154,8 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
|
|
|
129
154
|
inputTokens: 0,
|
|
130
155
|
outputTokens: 0,
|
|
131
156
|
totalTokens: 0,
|
|
157
|
+
cacheReadTokens: 0,
|
|
158
|
+
cacheCreationTokens: 0,
|
|
132
159
|
calls: 0,
|
|
133
160
|
};
|
|
134
161
|
const byKey = new Map();
|
|
@@ -137,6 +164,8 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
|
|
|
137
164
|
total.inputTokens += s.total.inputTokens;
|
|
138
165
|
total.outputTokens += s.total.outputTokens;
|
|
139
166
|
total.totalTokens += s.total.totalTokens;
|
|
167
|
+
total.cacheReadTokens += s.total.cacheReadTokens || 0;
|
|
168
|
+
total.cacheCreationTokens += s.total.cacheCreationTokens || 0;
|
|
140
169
|
total.calls += s.total.calls;
|
|
141
170
|
for (const row of s.byModel) {
|
|
142
171
|
const key = `${row.provider || "?"}/${row.model || "?"}`;
|
|
@@ -146,11 +175,15 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
|
|
|
146
175
|
inputTokens: 0,
|
|
147
176
|
outputTokens: 0,
|
|
148
177
|
totalTokens: 0,
|
|
178
|
+
cacheReadTokens: 0,
|
|
179
|
+
cacheCreationTokens: 0,
|
|
149
180
|
calls: 0,
|
|
150
181
|
};
|
|
151
182
|
entry.inputTokens += row.inputTokens;
|
|
152
183
|
entry.outputTokens += row.outputTokens;
|
|
153
184
|
entry.totalTokens += row.totalTokens;
|
|
185
|
+
entry.cacheReadTokens += row.cacheReadTokens || 0;
|
|
186
|
+
entry.cacheCreationTokens += row.cacheCreationTokens || 0;
|
|
154
187
|
entry.calls += row.calls;
|
|
155
188
|
byKey.set(key, entry);
|
|
156
189
|
}
|
package/src/lib/skill-loader.js
CHANGED
|
@@ -199,7 +199,10 @@ export function substituteArguments(body, args) {
|
|
|
199
199
|
// Protect literal $$
|
|
200
200
|
const MARKER = "\u0000DOLLAR\u0000";
|
|
201
201
|
let out = body.replace(/\$\$/g, MARKER);
|
|
202
|
-
|
|
202
|
+
// Insert the args via a function so $-sequences in the user's arguments
|
|
203
|
+
// ($&, $`, $', $$) are placed literally instead of being interpreted as
|
|
204
|
+
// String.replace patterns (which would otherwise mangle the args).
|
|
205
|
+
out = out.replace(/\$ARGUMENTS\b/g, () => full);
|
|
203
206
|
out = out.replace(/\$(\d+)/g, (match, idx) => {
|
|
204
207
|
const i = parseInt(idx, 10) - 1;
|
|
205
208
|
if (i < 0 || i >= positional.length) return match;
|
|
@@ -158,7 +158,7 @@ ${commandDocs}
|
|
|
158
158
|
/**
|
|
159
159
|
* Generate handler.js content for a direct-execution pack
|
|
160
160
|
*/
|
|
161
|
-
function generateDirectHandler(domainKey, domainDef) {
|
|
161
|
+
export function generateDirectHandler(domainKey, domainDef) {
|
|
162
162
|
const commandList = Object.keys(domainDef.commands).join('", "');
|
|
163
163
|
const commandGuide = Object.entries(domainDef.commands)
|
|
164
164
|
.map(
|
|
@@ -241,6 +241,17 @@ ${commandGuide}
|
|
|
241
241
|
const useJson = JSON_SUPPORTED_COMMANDS.has(command) && !cliArgs.includes("--json");
|
|
242
242
|
if (useJson) cliArgs.push("--json");
|
|
243
243
|
|
|
244
|
+
// Security: spawnSync uses shell:true (needed to run the chainlesschain.cmd
|
|
245
|
+
// shim on Windows), which joins cliArgs into a shell command. Reject shell
|
|
246
|
+
// metacharacters so a crafted skill input can't inject commands.
|
|
247
|
+
const unsafeArg = cliArgs.find((a) => /[;&|\`$<>()\\n\\r]/.test(a));
|
|
248
|
+
if (unsafeArg) {
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
error: "参数包含不安全的 shell 字符,已拒绝执行: " + JSON.stringify(unsafeArg),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
244
255
|
// Execute via child process
|
|
245
256
|
const result = spawnSync("chainlesschain", cliArgs, {
|
|
246
257
|
encoding: "utf-8",
|
|
@@ -371,7 +382,7 @@ module.exports = handler;
|
|
|
371
382
|
/**
|
|
372
383
|
* Generate handler.js content for hybrid-mode pack
|
|
373
384
|
*/
|
|
374
|
-
function generateHybridHandler(domainKey, domainDef) {
|
|
385
|
+
export function generateHybridHandler(domainKey, domainDef) {
|
|
375
386
|
const agentCmds = Object.entries(domainDef.commands)
|
|
376
387
|
.filter(([cmd, info]) => info.isAgentMode || AGENT_MODE_COMMANDS.has(cmd))
|
|
377
388
|
.map(([cmd]) => `"${cmd}"`)
|
|
@@ -458,6 +469,18 @@ const handler = {
|
|
|
458
469
|
|
|
459
470
|
// Direct execution for other commands
|
|
460
471
|
const cliArgs = [...args, "--quiet"];
|
|
472
|
+
|
|
473
|
+
// Security: spawnSync uses shell:true (needed to run the chainlesschain.cmd
|
|
474
|
+
// shim on Windows), which joins cliArgs into a shell command. Reject shell
|
|
475
|
+
// metacharacters so a crafted skill input can't inject commands.
|
|
476
|
+
const unsafeArg = cliArgs.find((a) => /[;&|\`$<>()\\n\\r]/.test(a));
|
|
477
|
+
if (unsafeArg) {
|
|
478
|
+
return {
|
|
479
|
+
success: false,
|
|
480
|
+
error: "参数包含不安全的 shell 字符,已拒绝执行: " + JSON.stringify(unsafeArg),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
461
484
|
const result = spawnSync("chainlesschain", cliArgs, {
|
|
462
485
|
encoding: "utf-8",
|
|
463
486
|
shell: true,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stream-retry — shared classification of transient streaming-chat failures.
|
|
3
|
+
*
|
|
4
|
+
* A streaming LLM call can drop mid-flight (ECONNRESET / "terminated" / socket
|
|
5
|
+
* hangup / DNS). When that happens BEFORE any output reached the user it is safe
|
|
6
|
+
* to re-issue the request. This module is the single source of truth for "is
|
|
7
|
+
* this error a retryable connection drop?" so the agent path (agent-core) and
|
|
8
|
+
* the chat path (chat-core) classify identically.
|
|
9
|
+
*
|
|
10
|
+
* Pure + dependency-light (only the shared abort-utils) so it can be unit-tested
|
|
11
|
+
* and imported by either streaming implementation without pulling in the heavy
|
|
12
|
+
* agent runtime.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { isAbortError } from "./abort-utils.js";
|
|
16
|
+
|
|
17
|
+
/** Bounded auto-retry budget for streaming connection drops (Claude-Code 2.1.181). */
|
|
18
|
+
export const STREAM_RETRY_MAX = 2;
|
|
19
|
+
export const STREAM_RETRY_BASE_MS = 400;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Is this error from a streaming chat request a transient API CONNECTION drop
|
|
23
|
+
* that is safe to retry? True only for genuine network failures (reset /
|
|
24
|
+
* timeout / DNS / refused / socket hangup / undici "terminated" / "fetch
|
|
25
|
+
* failed"). False for user aborts and for HTTP/status errors (a 4xx/auth/5xx is
|
|
26
|
+
* the server's verdict carried in the message, not a dropped pipe — retrying a
|
|
27
|
+
* connection that never dropped won't help and could double-bill).
|
|
28
|
+
*
|
|
29
|
+
* @param {unknown} err
|
|
30
|
+
* @param {AbortSignal} [signal] the caller's signal — a user abort is never retryable
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isRetryableStreamError(err, signal) {
|
|
34
|
+
if (isAbortError(err)) return false;
|
|
35
|
+
if (signal && signal.aborted) return false;
|
|
36
|
+
if (!err) return false;
|
|
37
|
+
const code = String(err.code || err.cause?.code || "").toUpperCase();
|
|
38
|
+
if (
|
|
39
|
+
[
|
|
40
|
+
"ECONNRESET",
|
|
41
|
+
"ETIMEDOUT",
|
|
42
|
+
"ECONNREFUSED",
|
|
43
|
+
"EAI_AGAIN",
|
|
44
|
+
"EPIPE",
|
|
45
|
+
"ENETUNREACH",
|
|
46
|
+
"ENOTFOUND",
|
|
47
|
+
"UND_ERR_SOCKET",
|
|
48
|
+
].includes(code)
|
|
49
|
+
) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const msg = String(err.message || err).toLowerCase();
|
|
53
|
+
return /econnreset|etimedout|econnrefused|eai_again|socket hang ?up|terminated|fetch failed|network error|timed?\s*out|premature close/.test(
|
|
54
|
+
msg,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -23,6 +23,7 @@ import fs from "node:fs";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import os from "node:os";
|
|
25
25
|
import crypto from "node:crypto";
|
|
26
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
26
27
|
|
|
27
28
|
const SENSITIVE_FIELDS = ["sync.webdav.password", "sync.oss.secretAccessKey"];
|
|
28
29
|
const ALLOWED_PROVIDER_IDS = ["webdav", "oss"];
|
|
@@ -55,6 +56,37 @@ function _ensureDir() {
|
|
|
55
56
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Write a secret file atomically. A crash (or concurrent writer) mid-write must
|
|
61
|
+
* never corrupt the master key or the encrypted vault — a truncated key throws
|
|
62
|
+
* on every load, a truncated vault fails AEAD decrypt and forces a reconfigure.
|
|
63
|
+
* Temp sibling + rename (atomic within a filesystem); when `mode` is given the
|
|
64
|
+
* temp is created with it (0600) so the secret is never world-readable in the
|
|
65
|
+
* window before rename. chmod after rename is belt-and-suspenders (best-effort
|
|
66
|
+
* on NTFS / older fs).
|
|
67
|
+
*/
|
|
68
|
+
function _atomicWriteFileSync(filePath, data, mode) {
|
|
69
|
+
const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
70
|
+
try {
|
|
71
|
+
fs.writeFileSync(tmp, data, mode != null ? { mode } : undefined);
|
|
72
|
+
fs.renameSync(tmp, filePath);
|
|
73
|
+
if (mode != null) {
|
|
74
|
+
try {
|
|
75
|
+
fs.chmodSync(filePath, mode);
|
|
76
|
+
} catch (_e) {
|
|
77
|
+
/* non-fatal: NTFS / older fs */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
83
|
+
} catch {
|
|
84
|
+
/* best-effort temp cleanup */
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
function _loadOrCreateMasterKey() {
|
|
59
91
|
_ensureDir();
|
|
60
92
|
const kp = _keyPath();
|
|
@@ -69,12 +101,10 @@ function _loadOrCreateMasterKey() {
|
|
|
69
101
|
return buf;
|
|
70
102
|
}
|
|
71
103
|
const key = crypto.randomBytes(KEY_LEN);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/* non-fatal: NTFS / older fs */
|
|
77
|
-
}
|
|
104
|
+
// Atomic + 0600 from creation: a crash mid-write must not leave a truncated
|
|
105
|
+
// master key (a wrong-length key file then throws on every load), and the
|
|
106
|
+
// secret must never be briefly world-readable in the gap before chmod.
|
|
107
|
+
_atomicWriteFileSync(kp, key, 0o600);
|
|
78
108
|
return key;
|
|
79
109
|
}
|
|
80
110
|
|
|
@@ -123,7 +153,7 @@ function loadAll() {
|
|
|
123
153
|
|
|
124
154
|
function saveAll(all) {
|
|
125
155
|
_ensureDir();
|
|
126
|
-
|
|
156
|
+
_atomicWriteFileSync(_vaultPath(), _encryptBlob(JSON.stringify(all)));
|
|
127
157
|
return true;
|
|
128
158
|
}
|
|
129
159
|
|
|
@@ -184,20 +214,26 @@ function setCredentials(providerId, creds) {
|
|
|
184
214
|
if (!creds || typeof creds !== "object") {
|
|
185
215
|
throw new Error("sync-credentials: creds must be an object");
|
|
186
216
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
217
|
+
// Serialize the decrypt-modify-encrypt across processes so a concurrent write
|
|
218
|
+
// to a different provider doesn't drop this one (best-effort, never hangs).
|
|
219
|
+
return withFileLock(_vaultPath(), () => {
|
|
220
|
+
const all = loadAll();
|
|
221
|
+
if (!all.sync || typeof all.sync !== "object") all.sync = {};
|
|
222
|
+
all.sync[providerId] = { ...creds };
|
|
223
|
+
return saveAll(all);
|
|
224
|
+
});
|
|
191
225
|
}
|
|
192
226
|
|
|
193
227
|
function clearCredentials(providerId) {
|
|
194
228
|
assertProviderId(providerId);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
return withFileLock(_vaultPath(), () => {
|
|
230
|
+
const all = loadAll();
|
|
231
|
+
if (all?.sync?.[providerId]) {
|
|
232
|
+
delete all.sync[providerId];
|
|
233
|
+
return saveAll(all);
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
});
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
/** Test seam: override the resolved chainlesschain dir without env leak. */
|
package/src/lib/token-tracker.js
CHANGED
|
@@ -21,9 +21,19 @@ const PRICING = {
|
|
|
21
21
|
_default: { input: 2.5, output: 10 },
|
|
22
22
|
},
|
|
23
23
|
anthropic: {
|
|
24
|
+
// Current Anthropic list prices (USD/1M), verified 2026-06-21 vs the
|
|
25
|
+
// claude-api reference. Lookup is EXACT-key (calculateCost), so keep current
|
|
26
|
+
// model ids listed or they fall back to the Sonnet _default and mis-price —
|
|
27
|
+
// e.g. claude-opus-4-8 would otherwise be billed at $3/$15 not $5/$25. The
|
|
28
|
+
// Opus tier dropped to $5/$25 with Opus 4.5; Haiku 4.5 is $1/$5.
|
|
29
|
+
"claude-opus-4-8": { input: 5, output: 25 },
|
|
30
|
+
"claude-opus-4-7": { input: 5, output: 25 },
|
|
31
|
+
"claude-opus-4-6": { input: 5, output: 25 },
|
|
32
|
+
"claude-opus-4-5": { input: 5, output: 25 },
|
|
24
33
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
25
|
-
"claude-
|
|
26
|
-
"claude-haiku-4-5-20251001": { input:
|
|
34
|
+
"claude-haiku-4-5": { input: 1, output: 5 },
|
|
35
|
+
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
36
|
+
"claude-fable-5": { input: 10, output: 50 },
|
|
27
37
|
_default: { input: 3, output: 15 },
|
|
28
38
|
},
|
|
29
39
|
deepseek: {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort cross-process advisory lock around a state file.
|
|
3
|
+
*
|
|
4
|
+
* Multiple `cc` processes (and, for some dirs, the desktop app) can do
|
|
5
|
+
* read-modify-write on the same JSON state file. An atomic temp+rename makes
|
|
6
|
+
* each write crash-safe but does NOT prevent a lost update: A reads, B reads, A
|
|
7
|
+
* writes, B writes → A's change is gone. This serializes the whole
|
|
8
|
+
* read-mutate-write across processes using a lock DIRECTORY — `mkdir` is
|
|
9
|
+
* atomic-exclusive on POSIX and Windows, so exactly one process holds it.
|
|
10
|
+
*
|
|
11
|
+
* SAFETY — this is intentionally best-effort. If the lock can't be acquired
|
|
12
|
+
* within `timeoutMs`, it PROCEEDS WITHOUT THE LOCK rather than hanging or
|
|
13
|
+
* throwing: worst case degrades to the pre-lock behavior (a rare lost update),
|
|
14
|
+
* never a frozen CLI. A lock left by a crashed holder is reclaimed once its
|
|
15
|
+
* mtime is older than `staleMs`. The lock is always released in `finally`.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} targetPath the file being guarded (lock is `${targetPath}.lock`)
|
|
18
|
+
* @param {(ctx:{locked:boolean})=>T} fn critical section; receives whether the lock was held
|
|
19
|
+
* @returns {T} whatever `fn` returns
|
|
20
|
+
*/
|
|
21
|
+
export function withFileLock(targetPath, fn, opts = {}) {
|
|
22
|
+
const {
|
|
23
|
+
timeoutMs = 2000,
|
|
24
|
+
staleMs = 30000,
|
|
25
|
+
retryMs = 25,
|
|
26
|
+
_fs = defaultFs,
|
|
27
|
+
_now = () => Date.now(),
|
|
28
|
+
_sleep = sleepSync,
|
|
29
|
+
} = opts;
|
|
30
|
+
|
|
31
|
+
const lockDir = `${targetPath}.lock`;
|
|
32
|
+
let held = false;
|
|
33
|
+
const deadline = _now() + timeoutMs;
|
|
34
|
+
|
|
35
|
+
for (;;) {
|
|
36
|
+
try {
|
|
37
|
+
_fs.mkdirSync(lockDir);
|
|
38
|
+
held = true;
|
|
39
|
+
break;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (!err || err.code !== "EEXIST") break; // unexpected fs error → run unlocked
|
|
42
|
+
// Reclaim a stale lock left by a crashed holder.
|
|
43
|
+
try {
|
|
44
|
+
const age = _now() - _fs.statSync(lockDir).mtimeMs;
|
|
45
|
+
if (age > staleMs) {
|
|
46
|
+
_fs.rmSync(lockDir, { recursive: true, force: true });
|
|
47
|
+
continue; // retry acquisition immediately
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
/* stat lost a race with the holder releasing — fall through to wait */
|
|
51
|
+
}
|
|
52
|
+
if (_now() >= deadline) break; // timed out → run unlocked (best-effort)
|
|
53
|
+
_sleep(retryMs);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return fn({ locked: held });
|
|
59
|
+
} finally {
|
|
60
|
+
if (held) {
|
|
61
|
+
try {
|
|
62
|
+
_fs.rmSync(lockDir, { recursive: true, force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
/* best-effort release */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
import fs from "node:fs";
|
|
71
|
+
const defaultFs = {
|
|
72
|
+
mkdirSync: (p) => fs.mkdirSync(p),
|
|
73
|
+
statSync: (p) => fs.statSync(p),
|
|
74
|
+
rmSync: (p, o) => fs.rmSync(p, o),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Synchronous sleep that doesn't busy-spin the CPU when possible. */
|
|
78
|
+
function sleepSync(ms) {
|
|
79
|
+
try {
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
81
|
+
} catch {
|
|
82
|
+
const end = Date.now() + ms; // SharedArrayBuffer unavailable — bounded spin
|
|
83
|
+
while (Date.now() < end) {
|
|
84
|
+
/* spin */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|