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
|
@@ -43,6 +43,11 @@ import {
|
|
|
43
43
|
import { createToolContext } from "../tools/tool-context.js";
|
|
44
44
|
import { createToolTelemetryRecord } from "../tools/tool-telemetry.js";
|
|
45
45
|
import { isAbortError, throwIfAborted } from "../lib/abort-utils.js";
|
|
46
|
+
import {
|
|
47
|
+
isRetryableStreamError,
|
|
48
|
+
STREAM_RETRY_MAX,
|
|
49
|
+
STREAM_RETRY_BASE_MS,
|
|
50
|
+
} from "../lib/stream-retry.js";
|
|
46
51
|
import {
|
|
47
52
|
annotateLines,
|
|
48
53
|
replaceByHash,
|
|
@@ -154,9 +159,20 @@ function _killTask(task) {
|
|
|
154
159
|
try {
|
|
155
160
|
if (process.platform === "win32") {
|
|
156
161
|
if (child.pid) {
|
|
157
|
-
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
162
|
+
const tk = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
158
163
|
windowsHide: true,
|
|
159
164
|
});
|
|
165
|
+
// A spawn that fails to launch emits an async 'error' event — with no
|
|
166
|
+
// listener Node rethrows it as an UNCAUGHT exception (the try/catch
|
|
167
|
+
// here can't catch the async emit), crashing the CLI. Handle it and
|
|
168
|
+
// fall back to a direct kill.
|
|
169
|
+
tk.on("error", () => {
|
|
170
|
+
try {
|
|
171
|
+
child.kill();
|
|
172
|
+
} catch {
|
|
173
|
+
/* already dead */
|
|
174
|
+
}
|
|
175
|
+
});
|
|
160
176
|
} else {
|
|
161
177
|
child.kill();
|
|
162
178
|
}
|
|
@@ -684,7 +700,7 @@ export function computeProposedEdit(name, args = {}, cwd = process.cwd()) {
|
|
|
684
700
|
}
|
|
685
701
|
return {
|
|
686
702
|
filePath,
|
|
687
|
-
newContent: content.replace(args.old_string, args.new_string),
|
|
703
|
+
newContent: content.replace(args.old_string, () => args.new_string),
|
|
688
704
|
originalText: content,
|
|
689
705
|
};
|
|
690
706
|
}
|
|
@@ -953,7 +969,9 @@ export async function executeTool(name, args, context = {}) {
|
|
|
953
969
|
// An explicit settings `allow` rule is the only bypass (exact user
|
|
954
970
|
// pre-authorization); headless without a confirmer fails closed.
|
|
955
971
|
if (
|
|
956
|
-
(name === "write_file" ||
|
|
972
|
+
(name === "write_file" ||
|
|
973
|
+
name === "edit_file" ||
|
|
974
|
+
name === "notebook_edit") &&
|
|
957
975
|
settingsVerdict.decision !== "allow" &&
|
|
958
976
|
args?.path
|
|
959
977
|
) {
|
|
@@ -1212,7 +1230,8 @@ export async function executeTool(name, args, context = {}) {
|
|
|
1212
1230
|
if (
|
|
1213
1231
|
(name === "write_file" ||
|
|
1214
1232
|
name === "edit_file" ||
|
|
1215
|
-
name === "edit_file_hashed"
|
|
1233
|
+
name === "edit_file_hashed" ||
|
|
1234
|
+
name === "notebook_edit") &&
|
|
1216
1235
|
toolResult &&
|
|
1217
1236
|
typeof toolResult === "object" &&
|
|
1218
1237
|
!toolResult.error &&
|
|
@@ -1271,6 +1290,173 @@ export function writeFileVerified(filePath, content, fsImpl = fs) {
|
|
|
1271
1290
|
return { size: actual };
|
|
1272
1291
|
}
|
|
1273
1292
|
|
|
1293
|
+
// Short, stable-ish id for a freshly inserted notebook cell (nbformat 4.5+).
|
|
1294
|
+
function _newNotebookCellId() {
|
|
1295
|
+
return Math.random().toString(36).slice(2, 10);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Apply a single cell edit to a Jupyter notebook (.ipynb) given its raw text.
|
|
1300
|
+
* Pure (no fs) + exported for tests. Returns `{ text, summary, cellId }` on
|
|
1301
|
+
* success or `{ error }` on any validation failure — the caller writes `text`.
|
|
1302
|
+
*
|
|
1303
|
+
* edit_mode:
|
|
1304
|
+
* - "replace" (default): overwrite the target cell's source; a code cell also
|
|
1305
|
+
* gets outputs=[] + execution_count=null (stale output would be misleading).
|
|
1306
|
+
* - "insert": add a new cell of `cell_type` after the target (or at the top
|
|
1307
|
+
* when no target is given).
|
|
1308
|
+
* - "delete": remove the target cell.
|
|
1309
|
+
* Target = cell_id (preferred) or 0-based cell_index.
|
|
1310
|
+
*/
|
|
1311
|
+
export function editNotebookCell(notebookText, args = {}) {
|
|
1312
|
+
let nb;
|
|
1313
|
+
try {
|
|
1314
|
+
nb = JSON.parse(notebookText);
|
|
1315
|
+
} catch {
|
|
1316
|
+
return { error: "Notebook is not valid JSON (.ipynb)" };
|
|
1317
|
+
}
|
|
1318
|
+
if (!nb || !Array.isArray(nb.cells)) {
|
|
1319
|
+
return { error: "Not a valid notebook: missing cells[] (need nbformat 4)" };
|
|
1320
|
+
}
|
|
1321
|
+
const mode = args.edit_mode || "replace";
|
|
1322
|
+
if (!["replace", "insert", "delete"].includes(mode)) {
|
|
1323
|
+
return { error: `Unknown edit_mode "${mode}" (replace|insert|delete)` };
|
|
1324
|
+
}
|
|
1325
|
+
// .ipynb stores source as an array of lines that each KEEP their trailing \n.
|
|
1326
|
+
const toLines = (s) => {
|
|
1327
|
+
const t = String(s ?? "");
|
|
1328
|
+
return t === "" ? [] : t.split(/(?<=\n)/);
|
|
1329
|
+
};
|
|
1330
|
+
// Re-serialize with Jupyter's 1-space indent + trailing newline.
|
|
1331
|
+
const out = (summary, cellId) => ({
|
|
1332
|
+
text: JSON.stringify(nb, null, 1) + "\n",
|
|
1333
|
+
summary,
|
|
1334
|
+
cellId,
|
|
1335
|
+
});
|
|
1336
|
+
const locate = () => {
|
|
1337
|
+
if (args.cell_id != null) {
|
|
1338
|
+
return nb.cells.findIndex((c) => c && c.id === args.cell_id);
|
|
1339
|
+
}
|
|
1340
|
+
if (Number.isInteger(args.cell_index)) {
|
|
1341
|
+
return args.cell_index >= 0 && args.cell_index < nb.cells.length
|
|
1342
|
+
? args.cell_index
|
|
1343
|
+
: -1;
|
|
1344
|
+
}
|
|
1345
|
+
return -2; // no locator supplied
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
if (mode === "insert") {
|
|
1349
|
+
if (typeof args.new_source !== "string") {
|
|
1350
|
+
return { error: "new_source is required for edit_mode 'insert'" };
|
|
1351
|
+
}
|
|
1352
|
+
const ct = args.cell_type;
|
|
1353
|
+
if (ct !== "code" && ct !== "markdown") {
|
|
1354
|
+
return {
|
|
1355
|
+
error:
|
|
1356
|
+
"cell_type ('code'|'markdown') is required for edit_mode 'insert'",
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
const id = _newNotebookCellId();
|
|
1360
|
+
const cell =
|
|
1361
|
+
ct === "code"
|
|
1362
|
+
? {
|
|
1363
|
+
cell_type: "code",
|
|
1364
|
+
id,
|
|
1365
|
+
metadata: {},
|
|
1366
|
+
source: toLines(args.new_source),
|
|
1367
|
+
outputs: [],
|
|
1368
|
+
execution_count: null,
|
|
1369
|
+
}
|
|
1370
|
+
: {
|
|
1371
|
+
cell_type: "markdown",
|
|
1372
|
+
id,
|
|
1373
|
+
metadata: {},
|
|
1374
|
+
source: toLines(args.new_source),
|
|
1375
|
+
};
|
|
1376
|
+
let at = 0;
|
|
1377
|
+
if (args.cell_id != null || Number.isInteger(args.cell_index)) {
|
|
1378
|
+
const idx = locate();
|
|
1379
|
+
if (idx < 0) return { error: "Target cell not found for insert" };
|
|
1380
|
+
at = idx + 1; // insert AFTER the target
|
|
1381
|
+
}
|
|
1382
|
+
nb.cells.splice(at, 0, cell);
|
|
1383
|
+
return out(`inserted ${ct} cell at index ${at}`, id);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// replace / delete both need an existing target
|
|
1387
|
+
const idx = locate();
|
|
1388
|
+
if (idx === -2) {
|
|
1389
|
+
return {
|
|
1390
|
+
error: "Provide cell_id or cell_index to identify the target cell",
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (idx < 0) {
|
|
1394
|
+
return {
|
|
1395
|
+
error: `Target cell not found (${
|
|
1396
|
+
args.cell_id != null
|
|
1397
|
+
? "cell_id " + args.cell_id
|
|
1398
|
+
: "cell_index " + args.cell_index
|
|
1399
|
+
})`,
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (mode === "delete") {
|
|
1404
|
+
const [removed] = nb.cells.splice(idx, 1);
|
|
1405
|
+
return out(`deleted cell at index ${idx}`, removed?.id);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// replace
|
|
1409
|
+
if (typeof args.new_source !== "string") {
|
|
1410
|
+
return { error: "new_source is required for edit_mode 'replace'" };
|
|
1411
|
+
}
|
|
1412
|
+
const cell = nb.cells[idx];
|
|
1413
|
+
cell.source = toLines(args.new_source);
|
|
1414
|
+
if (cell.cell_type === "code") {
|
|
1415
|
+
cell.outputs = [];
|
|
1416
|
+
cell.execution_count = null;
|
|
1417
|
+
}
|
|
1418
|
+
return out(`replaced ${cell.cell_type} cell at index ${idx}`, cell.id);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Render a Jupyter notebook (.ipynb) as a compact, token-cheap cell listing so
|
|
1423
|
+
* the model can locate cells (index + id + type + source) to edit with
|
|
1424
|
+
* notebook_edit — instead of drowning in raw JSON / base64 output blobs. Cell
|
|
1425
|
+
* OUTPUTS are summarized, not dumped. Pure + exported for tests; returns null
|
|
1426
|
+
* when the text is not a parseable nbformat-4 notebook (caller falls back to raw).
|
|
1427
|
+
*/
|
|
1428
|
+
export function renderNotebook(text) {
|
|
1429
|
+
let nb;
|
|
1430
|
+
try {
|
|
1431
|
+
nb = JSON.parse(text);
|
|
1432
|
+
} catch {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
if (!nb || !Array.isArray(nb.cells)) return null;
|
|
1436
|
+
const srcOf = (c) =>
|
|
1437
|
+
Array.isArray(c.source) ? c.source.join("") : String(c.source ?? "");
|
|
1438
|
+
const lines = [
|
|
1439
|
+
`Jupyter notebook — ${nb.cells.length} cell(s), nbformat ${nb.nbformat ?? "?"}. Edit cells with the notebook_edit tool (target by the id shown below). Pass raw:true to read_file for the underlying JSON.`,
|
|
1440
|
+
"",
|
|
1441
|
+
];
|
|
1442
|
+
nb.cells.forEach((c, i) => {
|
|
1443
|
+
const id = c && c.id != null ? ` id=${c.id}` : "";
|
|
1444
|
+
lines.push(`── Cell ${i} [${(c && c.cell_type) || "?"}${id}] ──`);
|
|
1445
|
+
const src = srcOf(c || {});
|
|
1446
|
+
lines.push(src.length ? src.replace(/\n$/, "") : "(empty)");
|
|
1447
|
+
if (
|
|
1448
|
+
c &&
|
|
1449
|
+
c.cell_type === "code" &&
|
|
1450
|
+
Array.isArray(c.outputs) &&
|
|
1451
|
+
c.outputs.length
|
|
1452
|
+
) {
|
|
1453
|
+
lines.push(` ⟨${c.outputs.length} output(s) hidden⟩`);
|
|
1454
|
+
}
|
|
1455
|
+
lines.push("");
|
|
1456
|
+
});
|
|
1457
|
+
return lines.join("\n");
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1274
1460
|
/**
|
|
1275
1461
|
* Inner tool execution — no hooks, no plan-mode checks.
|
|
1276
1462
|
*/
|
|
@@ -1340,6 +1526,24 @@ async function executeToolInner(
|
|
|
1340
1526
|
return attachDescriptor({ error: `File not found: ${filePath}` });
|
|
1341
1527
|
}
|
|
1342
1528
|
const content = fs.readFileSync(filePath, "utf8");
|
|
1529
|
+
// Jupyter notebooks: render a compact cell listing (index/id/type/source,
|
|
1530
|
+
// outputs summarized) so the model can find cells for notebook_edit
|
|
1531
|
+
// without ingesting raw JSON / base64 output blobs. `raw:true` returns the
|
|
1532
|
+
// underlying JSON. Non-.ipynb reads are unchanged.
|
|
1533
|
+
if (args.raw !== true && /\.ipynb$/i.test(filePath)) {
|
|
1534
|
+
const nbView = renderNotebook(content);
|
|
1535
|
+
if (nbView) {
|
|
1536
|
+
return attachDescriptor(
|
|
1537
|
+
nbView.length > 50000
|
|
1538
|
+
? {
|
|
1539
|
+
content: nbView.substring(0, 50000) + "\n...(truncated)",
|
|
1540
|
+
size: nbView.length,
|
|
1541
|
+
notebook: true,
|
|
1542
|
+
}
|
|
1543
|
+
: { content: nbView, notebook: true },
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1343
1547
|
// Hashline mode: prefix each line with a 6-char content hash tag
|
|
1344
1548
|
// so downstream edit_file_hashed calls can anchor by hash.
|
|
1345
1549
|
const rendered = args.hashed === true ? annotateLines(content) : content;
|
|
@@ -1371,6 +1575,25 @@ async function executeToolInner(
|
|
|
1371
1575
|
});
|
|
1372
1576
|
}
|
|
1373
1577
|
|
|
1578
|
+
case "notebook_edit": {
|
|
1579
|
+
const filePath = path.resolve(cwd, args.path);
|
|
1580
|
+
if (!fs.existsSync(filePath)) {
|
|
1581
|
+
return attachDescriptor({ error: `Notebook not found: ${filePath}` });
|
|
1582
|
+
}
|
|
1583
|
+
const original = fs.readFileSync(filePath, "utf8");
|
|
1584
|
+
const res = editNotebookCell(original, args);
|
|
1585
|
+
if (res.error) return attachDescriptor({ error: res.error });
|
|
1586
|
+
const wrote = writeFileVerified(filePath, res.text);
|
|
1587
|
+
if (wrote.error) return attachDescriptor({ error: wrote.error });
|
|
1588
|
+
return attachDescriptor({
|
|
1589
|
+
success: true,
|
|
1590
|
+
path: filePath,
|
|
1591
|
+
size: wrote.size,
|
|
1592
|
+
summary: res.summary,
|
|
1593
|
+
cellId: res.cellId,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1374
1597
|
case "edit_file": {
|
|
1375
1598
|
const filePath = path.resolve(cwd, args.path);
|
|
1376
1599
|
if (!fs.existsSync(filePath)) {
|
|
@@ -1380,7 +1603,10 @@ async function executeToolInner(
|
|
|
1380
1603
|
if (!content.includes(args.old_string)) {
|
|
1381
1604
|
return attachDescriptor({ error: "old_string not found in file" });
|
|
1382
1605
|
}
|
|
1383
|
-
const newContent = content.replace(
|
|
1606
|
+
const newContent = content.replace(
|
|
1607
|
+
args.old_string,
|
|
1608
|
+
() => args.new_string,
|
|
1609
|
+
);
|
|
1384
1610
|
const wrote = writeFileVerified(filePath, newContent);
|
|
1385
1611
|
if (wrote.error) return attachDescriptor({ error: wrote.error });
|
|
1386
1612
|
return attachDescriptor({
|
|
@@ -2679,6 +2905,9 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2679
2905
|
{ model, messages: ollamaMessages, tools },
|
|
2680
2906
|
options.onToken,
|
|
2681
2907
|
signal,
|
|
2908
|
+
options.onStall,
|
|
2909
|
+
options.streamStallMs,
|
|
2910
|
+
options.streamStallTimeoutMs,
|
|
2682
2911
|
),
|
|
2683
2912
|
{ signal, onRetry: options.onStreamRetry },
|
|
2684
2913
|
);
|
|
@@ -2720,10 +2949,30 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2720
2949
|
input_schema: t.function.parameters,
|
|
2721
2950
|
}));
|
|
2722
2951
|
|
|
2952
|
+
// Prompt caching (Claude-Code parity, default-on): the system prompt + the
|
|
2953
|
+
// ~18 tool schemas are a large, stable prefix re-sent on every agent-loop
|
|
2954
|
+
// iteration. Marking the LAST tool and the system block as cache
|
|
2955
|
+
// breakpoints lets Anthropic serve that prefix from cache (~10% the input
|
|
2956
|
+
// cost, lower latency) across iterations and turns. Anthropic ignores a
|
|
2957
|
+
// breakpoint when the prefix is under the model's minimum cacheable size,
|
|
2958
|
+
// so it is always safe. Opt out (e.g. a custom gateway that rejects the
|
|
2959
|
+
// field) with CC_PROMPT_CACHE=0 or options.promptCaching:false.
|
|
2960
|
+
const cacheEnabled =
|
|
2961
|
+
options.promptCaching !== false && process.env.CC_PROMPT_CACHE !== "0";
|
|
2962
|
+
if (cacheEnabled && anthropicTools.length > 0) {
|
|
2963
|
+
const last = anthropicTools.length - 1;
|
|
2964
|
+
anthropicTools[last] = {
|
|
2965
|
+
...anthropicTools[last],
|
|
2966
|
+
cache_control: { type: "ephemeral" },
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2723
2970
|
// Model-aware max_tokens (Opus → 16384, Haiku → 4096, else 8192) via
|
|
2724
2971
|
// provider-options. We read ONLY maxTokens: the module's `temperature`
|
|
2725
2972
|
// default is never forwarded (400s on Opus 4.7/4.8).
|
|
2726
|
-
|
|
2973
|
+
// Fallback to the CURRENT Sonnet, not a retired snapshot:
|
|
2974
|
+
// claude-sonnet-4-20250514 was retired 2026-06-15 and would 404 here.
|
|
2975
|
+
const effModel = model || "claude-sonnet-4-6";
|
|
2727
2976
|
const { maxTokens: anthropicMaxTokens } = mergeProviderOptions(
|
|
2728
2977
|
"anthropic",
|
|
2729
2978
|
effModel,
|
|
@@ -2750,7 +2999,18 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2750
2999
|
);
|
|
2751
3000
|
if (thinkingParams) Object.assign(body, thinkingParams);
|
|
2752
3001
|
if (systemMsgs.length > 0) {
|
|
2753
|
-
|
|
3002
|
+
const systemText = systemMsgs.map((m) => m.content).join("\n");
|
|
3003
|
+
// Array-form system with a cache breakpoint when caching is on; plain
|
|
3004
|
+
// string otherwise (byte-identical to the prior request shape).
|
|
3005
|
+
body.system = cacheEnabled
|
|
3006
|
+
? [
|
|
3007
|
+
{
|
|
3008
|
+
type: "text",
|
|
3009
|
+
text: systemText,
|
|
3010
|
+
cache_control: { type: "ephemeral" },
|
|
3011
|
+
},
|
|
3012
|
+
]
|
|
3013
|
+
: systemText;
|
|
2754
3014
|
}
|
|
2755
3015
|
|
|
2756
3016
|
const url =
|
|
@@ -2771,6 +3031,9 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2771
3031
|
options.onToken,
|
|
2772
3032
|
signal,
|
|
2773
3033
|
options.onThinking,
|
|
3034
|
+
options.onStall,
|
|
3035
|
+
options.streamStallMs,
|
|
3036
|
+
options.streamStallTimeoutMs,
|
|
2774
3037
|
),
|
|
2775
3038
|
{ signal, onRetry: options.onStreamRetry },
|
|
2776
3039
|
);
|
|
@@ -2795,8 +3058,15 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2795
3058
|
const normalized = _normalizeAnthropicResponse(data);
|
|
2796
3059
|
if (data.usage) {
|
|
2797
3060
|
normalized.usage = {
|
|
3061
|
+
// With prompt caching, Anthropic splits input into uncached
|
|
3062
|
+
// `input_tokens` + cache read/write — capture all three so cost
|
|
3063
|
+
// accounting prices the cached prefix correctly (read ≈ 0.1×,
|
|
3064
|
+
// write ≈ 1.25× input). Absent (caching off) → 0, byte-identical.
|
|
2798
3065
|
input_tokens: data.usage.input_tokens || 0,
|
|
2799
3066
|
output_tokens: data.usage.output_tokens || 0,
|
|
3067
|
+
cache_read_input_tokens: data.usage.cache_read_input_tokens || 0,
|
|
3068
|
+
cache_creation_input_tokens:
|
|
3069
|
+
data.usage.cache_creation_input_tokens || 0,
|
|
2800
3070
|
};
|
|
2801
3071
|
}
|
|
2802
3072
|
return normalized;
|
|
@@ -2865,6 +3135,9 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2865
3135
|
options.onToken,
|
|
2866
3136
|
signal,
|
|
2867
3137
|
provider,
|
|
3138
|
+
options.onStall,
|
|
3139
|
+
options.streamStallMs,
|
|
3140
|
+
options.streamStallTimeoutMs,
|
|
2868
3141
|
),
|
|
2869
3142
|
{ signal, onRetry: options.onStreamRetry },
|
|
2870
3143
|
);
|
|
@@ -2895,9 +3168,14 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2895
3168
|
const choice = data.choices[0];
|
|
2896
3169
|
const out = { message: choice.message };
|
|
2897
3170
|
if (data.usage) {
|
|
3171
|
+
// OpenAI/DeepSeek/volcengine report cached prompt tokens AS PART of
|
|
3172
|
+
// prompt_tokens — split them out so cost prices the cached prefix at the
|
|
3173
|
+
// provider's cache rate, not full input. 0 (or absent) → input unchanged.
|
|
3174
|
+
const cached = _openaiCachedTokens(data.usage);
|
|
2898
3175
|
out.usage = {
|
|
2899
|
-
input_tokens: data.usage.prompt_tokens || 0,
|
|
3176
|
+
input_tokens: Math.max(0, (data.usage.prompt_tokens || 0) - cached),
|
|
2900
3177
|
output_tokens: data.usage.completion_tokens || 0,
|
|
3178
|
+
cache_read_input_tokens: cached,
|
|
2901
3179
|
};
|
|
2902
3180
|
}
|
|
2903
3181
|
return out;
|
|
@@ -3033,34 +3311,27 @@ export function formatProviderHttpError(provider, status) {
|
|
|
3033
3311
|
* streamed (partial-output drops are preserved internally and never throw) — so
|
|
3034
3312
|
* a retry can never duplicate visible text. Pure + exported for tests.
|
|
3035
3313
|
*/
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
msg,
|
|
3058
|
-
);
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
/** Bounded auto-retry for streaming connection drops (Claude-Code 2.1.181). */
|
|
3062
|
-
const STREAM_RETRY_MAX = 2;
|
|
3063
|
-
const STREAM_RETRY_BASE_MS = 400;
|
|
3314
|
+
// Re-exported from the shared classifier (src/lib/stream-retry.js) so agent-core
|
|
3315
|
+
// internals and existing test imports keep resolving it from this module, while
|
|
3316
|
+
// the agent and chat streaming paths share ONE definition (no drift). The retry
|
|
3317
|
+
// budget constants (STREAM_RETRY_MAX / STREAM_RETRY_BASE_MS) come from there too.
|
|
3318
|
+
export const _isRetryableStreamError = isRetryableStreamError;
|
|
3319
|
+
// 2.1.185 parity: when a streaming response goes silent mid-flight (the TCP
|
|
3320
|
+
// connection is alive but no bytes arrive — a slow/overloaded API), surface a
|
|
3321
|
+
// "waiting for API response" hint after this many ms instead of leaving the
|
|
3322
|
+
// user staring at a frozen spinner. Upstream raised this from 10s to 20s.
|
|
3323
|
+
const STREAM_STALL_MS = 20000;
|
|
3324
|
+
// Hard inactivity timeout: a stream silent for this long is treated as a dead
|
|
3325
|
+
// connection — the watchdog cancels the reader and throws a RETRYABLE error so
|
|
3326
|
+
// `_retryStreamingChat` re-issues the request instead of hanging forever.
|
|
3327
|
+
// Default 180s mirrors the long-standing `cc chat`/`cc ask` stall guard
|
|
3328
|
+
// (chat-core.js STREAM_STALL_MS / CC_CHAT_STALL_MS) — generous enough that even
|
|
3329
|
+
// a slow local model's first token arrives in time, while still recovering from
|
|
3330
|
+
// a genuinely dead connection by default. Set llm.streamStallTimeoutMs (or pass
|
|
3331
|
+
// streamStallTimeoutMs: 0) to tune or disable. The 20s hint above still fires
|
|
3332
|
+
// first. Must exceed STREAM_STALL_MS.
|
|
3333
|
+
const STREAM_STALL_TIMEOUT_MS = 180000;
|
|
3334
|
+
const _STREAM_STALL = Symbol("stream-stall");
|
|
3064
3335
|
|
|
3065
3336
|
/**
|
|
3066
3337
|
* Run a streaming chat attempt with bounded auto-retry on transient API
|
|
@@ -3114,7 +3385,99 @@ function _finalizeTruncatedStream(finalize, state) {
|
|
|
3114
3385
|
return out;
|
|
3115
3386
|
}
|
|
3116
3387
|
|
|
3117
|
-
|
|
3388
|
+
// Wrap a ReadableStream reader so the read loop yields chunk values while a
|
|
3389
|
+
// watchdog (a) fires `onStall(elapsedMs)` at most once per silent gap longer
|
|
3390
|
+
// than `stallMs` (2.1.185 stream-stall hint) and (b) — when `stallTimeoutMs` is
|
|
3391
|
+
// set — cancels the reader and throws a RETRYABLE ETIMEDOUT after that long a
|
|
3392
|
+
// silence, so a permanently dead-but-open connection recovers via the retry
|
|
3393
|
+
// layer instead of hanging forever. The SAME in-flight read() promise is
|
|
3394
|
+
// re-raced against a fresh timer each tick, so no chunk is ever dropped or
|
|
3395
|
+
// double-read; `done`, errors, and aborts propagate unchanged. With neither a
|
|
3396
|
+
// hook nor a timeout this degrades to a plain read loop (zero extra timers).
|
|
3397
|
+
export async function* _iterateStreamWithStall(reader, opts = {}) {
|
|
3398
|
+
const onStall = typeof opts.onStall === "function" ? opts.onStall : null;
|
|
3399
|
+
const stallMs = opts.stallMs ?? STREAM_STALL_MS;
|
|
3400
|
+
const timeoutMs =
|
|
3401
|
+
opts.stallTimeoutMs == null ? STREAM_STALL_TIMEOUT_MS : opts.stallTimeoutMs;
|
|
3402
|
+
for (;;) {
|
|
3403
|
+
const readP = reader.read();
|
|
3404
|
+
// Fast path: nothing to watch for — await the chunk directly.
|
|
3405
|
+
if (!onStall && !timeoutMs) {
|
|
3406
|
+
const { done, value } = await readP;
|
|
3407
|
+
if (done) return;
|
|
3408
|
+
yield value;
|
|
3409
|
+
continue;
|
|
3410
|
+
}
|
|
3411
|
+
const start = Date.now();
|
|
3412
|
+
let notified = false;
|
|
3413
|
+
let result;
|
|
3414
|
+
for (;;) {
|
|
3415
|
+
const elapsed = Date.now() - start;
|
|
3416
|
+
// Hard inactivity timeout reached: cancel the dead stream (release the
|
|
3417
|
+
// socket) and throw a retryable error the dispatch seam re-issues.
|
|
3418
|
+
if (timeoutMs && elapsed >= timeoutMs) {
|
|
3419
|
+
try {
|
|
3420
|
+
await reader.cancel();
|
|
3421
|
+
} catch {
|
|
3422
|
+
/* best-effort release — the throw below is what matters */
|
|
3423
|
+
}
|
|
3424
|
+
const err = new Error(
|
|
3425
|
+
`stream stalled: no data from API for ${Math.round(elapsed / 1000)}s`,
|
|
3426
|
+
);
|
|
3427
|
+
err.code = "ETIMEDOUT";
|
|
3428
|
+
throw err;
|
|
3429
|
+
}
|
|
3430
|
+
// Wake at whichever fires first: the one-shot stall hint or the timeout.
|
|
3431
|
+
const waits = [];
|
|
3432
|
+
if (onStall && !notified) waits.push(stallMs - elapsed);
|
|
3433
|
+
if (timeoutMs) waits.push(timeoutMs - elapsed);
|
|
3434
|
+
const nextWait = waits.length ? Math.max(0, Math.min(...waits)) : null;
|
|
3435
|
+
if (nextWait == null) {
|
|
3436
|
+
// Hint already fired and no timeout — just wait for the chunk.
|
|
3437
|
+
result = await readP;
|
|
3438
|
+
break;
|
|
3439
|
+
}
|
|
3440
|
+
let timer;
|
|
3441
|
+
const wakeP = new Promise((resolve) => {
|
|
3442
|
+
timer = setTimeout(() => resolve(_STREAM_STALL), nextWait);
|
|
3443
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
3444
|
+
});
|
|
3445
|
+
let r;
|
|
3446
|
+
try {
|
|
3447
|
+
r = await Promise.race([readP, wakeP]);
|
|
3448
|
+
} finally {
|
|
3449
|
+
clearTimeout(timer);
|
|
3450
|
+
}
|
|
3451
|
+
if (r === _STREAM_STALL) {
|
|
3452
|
+
// A timer woke us. Fire the one-shot hint if we've crossed stallMs; the
|
|
3453
|
+
// loop re-evaluates `elapsed` to decide hint-vs-hard-timeout next tick.
|
|
3454
|
+
if (onStall && !notified && Date.now() - start >= stallMs) {
|
|
3455
|
+
notified = true;
|
|
3456
|
+
try {
|
|
3457
|
+
onStall(Date.now() - start);
|
|
3458
|
+
} catch {
|
|
3459
|
+
/* stall hint is best-effort — never break the stream over it */
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
continue; // keep awaiting the SAME read() against a fresh timer
|
|
3463
|
+
}
|
|
3464
|
+
result = r;
|
|
3465
|
+
break;
|
|
3466
|
+
}
|
|
3467
|
+
if (result.done) return;
|
|
3468
|
+
yield result.value;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
async function _chatOllamaStreaming(
|
|
3473
|
+
apiUrl,
|
|
3474
|
+
body,
|
|
3475
|
+
onToken,
|
|
3476
|
+
signal,
|
|
3477
|
+
onStall,
|
|
3478
|
+
stallMs,
|
|
3479
|
+
stallTimeoutMs,
|
|
3480
|
+
) {
|
|
3118
3481
|
const response = await fetch(apiUrl, {
|
|
3119
3482
|
method: "POST",
|
|
3120
3483
|
headers: { "Content-Type": "application/json" },
|
|
@@ -3129,9 +3492,11 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
|
|
|
3129
3492
|
const decoder = new TextDecoder();
|
|
3130
3493
|
let buf = "";
|
|
3131
3494
|
try {
|
|
3132
|
-
for (
|
|
3133
|
-
|
|
3134
|
-
|
|
3495
|
+
for await (const value of _iterateStreamWithStall(reader, {
|
|
3496
|
+
onStall,
|
|
3497
|
+
stallMs,
|
|
3498
|
+
stallTimeoutMs,
|
|
3499
|
+
})) {
|
|
3135
3500
|
buf += decoder.decode(value, { stream: true });
|
|
3136
3501
|
let idx;
|
|
3137
3502
|
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
@@ -3157,7 +3522,14 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
|
|
|
3157
3522
|
// the same shape chatWithTools returns non-streamed.
|
|
3158
3523
|
|
|
3159
3524
|
function _anthropicInitState() {
|
|
3160
|
-
return {
|
|
3525
|
+
return {
|
|
3526
|
+
text: "",
|
|
3527
|
+
blocks: {},
|
|
3528
|
+
inputTokens: 0,
|
|
3529
|
+
outputTokens: 0,
|
|
3530
|
+
cacheReadTokens: 0,
|
|
3531
|
+
cacheCreationTokens: 0,
|
|
3532
|
+
};
|
|
3161
3533
|
}
|
|
3162
3534
|
|
|
3163
3535
|
function _anthropicReduceLine(state, raw, onToken, onThinking) {
|
|
@@ -3172,8 +3544,13 @@ function _anthropicReduceLine(state, raw, onToken, onThinking) {
|
|
|
3172
3544
|
return state;
|
|
3173
3545
|
}
|
|
3174
3546
|
if (obj.type === "message_start") {
|
|
3175
|
-
|
|
3176
|
-
|
|
3547
|
+
const u = obj.message?.usage || {};
|
|
3548
|
+
state.inputTokens = Number(u.input_tokens) || state.inputTokens;
|
|
3549
|
+
// Prompt-cache token counts ride message_start.usage (Anthropic caching).
|
|
3550
|
+
state.cacheReadTokens =
|
|
3551
|
+
Number(u.cache_read_input_tokens) || state.cacheReadTokens;
|
|
3552
|
+
state.cacheCreationTokens =
|
|
3553
|
+
Number(u.cache_creation_input_tokens) || state.cacheCreationTokens;
|
|
3177
3554
|
} else if (obj.type === "content_block_start") {
|
|
3178
3555
|
const cb = obj.content_block || {};
|
|
3179
3556
|
state.blocks[obj.index] =
|
|
@@ -3252,10 +3629,17 @@ function _anthropicFinalize(state) {
|
|
|
3252
3629
|
// tool turn — required by the API when extended thinking is on.
|
|
3253
3630
|
if (thinkingBlocks.length) message._thinkingBlocks = thinkingBlocks;
|
|
3254
3631
|
const out = { message };
|
|
3255
|
-
if (
|
|
3632
|
+
if (
|
|
3633
|
+
state.inputTokens ||
|
|
3634
|
+
state.outputTokens ||
|
|
3635
|
+
state.cacheReadTokens ||
|
|
3636
|
+
state.cacheCreationTokens
|
|
3637
|
+
) {
|
|
3256
3638
|
out.usage = {
|
|
3257
3639
|
input_tokens: state.inputTokens,
|
|
3258
3640
|
output_tokens: state.outputTokens,
|
|
3641
|
+
cache_read_input_tokens: state.cacheReadTokens,
|
|
3642
|
+
cache_creation_input_tokens: state.cacheCreationTokens,
|
|
3259
3643
|
};
|
|
3260
3644
|
}
|
|
3261
3645
|
return out;
|
|
@@ -3276,6 +3660,9 @@ async function _chatAnthropicStreaming(
|
|
|
3276
3660
|
onToken,
|
|
3277
3661
|
signal,
|
|
3278
3662
|
onThinking,
|
|
3663
|
+
onStall,
|
|
3664
|
+
stallMs,
|
|
3665
|
+
stallTimeoutMs,
|
|
3279
3666
|
) {
|
|
3280
3667
|
const response = await fetch(apiUrl, {
|
|
3281
3668
|
method: "POST",
|
|
@@ -3291,9 +3678,11 @@ async function _chatAnthropicStreaming(
|
|
|
3291
3678
|
const decoder = new TextDecoder();
|
|
3292
3679
|
let buf = "";
|
|
3293
3680
|
try {
|
|
3294
|
-
for (
|
|
3295
|
-
|
|
3296
|
-
|
|
3681
|
+
for await (const value of _iterateStreamWithStall(reader, {
|
|
3682
|
+
onStall,
|
|
3683
|
+
stallMs,
|
|
3684
|
+
stallTimeoutMs,
|
|
3685
|
+
})) {
|
|
3297
3686
|
buf += decoder.decode(value, { stream: true });
|
|
3298
3687
|
const lines = buf.split("\n");
|
|
3299
3688
|
buf = lines.pop() || "";
|
|
@@ -3316,8 +3705,30 @@ async function _chatAnthropicStreaming(
|
|
|
3316
3705
|
// concatenated across chunks). usage rides the terminal chunk when
|
|
3317
3706
|
// stream_options.include_usage was requested. Terminator: `data: [DONE]`.
|
|
3318
3707
|
|
|
3708
|
+
// OpenAI-compatible cached-prompt-token count. OpenAI / volcengine report it as
|
|
3709
|
+
// usage.prompt_tokens_details.cached_tokens; DeepSeek as
|
|
3710
|
+
// usage.prompt_cache_hit_tokens. In BOTH, prompt_tokens already INCLUDES the
|
|
3711
|
+
// cached count (unlike Anthropic, where input_tokens is the uncached remainder)
|
|
3712
|
+
// — so callers subtract it to recover the uncached input and avoid pricing the
|
|
3713
|
+
// cached prefix twice. Verified live against volcengine (field present, 0 when
|
|
3714
|
+
// the provider does not auto-cache).
|
|
3715
|
+
function _openaiCachedTokens(usage) {
|
|
3716
|
+
if (!usage || typeof usage !== "object") return 0;
|
|
3717
|
+
const detailed = Number(usage.prompt_tokens_details?.cached_tokens);
|
|
3718
|
+
if (Number.isFinite(detailed) && detailed > 0) return detailed;
|
|
3719
|
+
const deepseek = Number(usage.prompt_cache_hit_tokens);
|
|
3720
|
+
if (Number.isFinite(deepseek) && deepseek > 0) return deepseek;
|
|
3721
|
+
return 0;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3319
3724
|
function _openaiInitState() {
|
|
3320
|
-
return {
|
|
3725
|
+
return {
|
|
3726
|
+
text: "",
|
|
3727
|
+
tools: [],
|
|
3728
|
+
inputTokens: 0,
|
|
3729
|
+
outputTokens: 0,
|
|
3730
|
+
cacheReadTokens: 0,
|
|
3731
|
+
};
|
|
3321
3732
|
}
|
|
3322
3733
|
|
|
3323
3734
|
function _openaiReduceLine(state, raw, onToken) {
|
|
@@ -3354,7 +3765,11 @@ function _openaiReduceLine(state, raw, onToken) {
|
|
|
3354
3765
|
}
|
|
3355
3766
|
}
|
|
3356
3767
|
if (obj.usage) {
|
|
3357
|
-
|
|
3768
|
+
const cached = _openaiCachedTokens(obj.usage);
|
|
3769
|
+
const prompt = Number(obj.usage.prompt_tokens);
|
|
3770
|
+
if (Number.isFinite(prompt))
|
|
3771
|
+
state.inputTokens = Math.max(0, prompt - cached);
|
|
3772
|
+
if (cached) state.cacheReadTokens = cached;
|
|
3358
3773
|
state.outputTokens =
|
|
3359
3774
|
Number(obj.usage.completion_tokens) || state.outputTokens;
|
|
3360
3775
|
}
|
|
@@ -3370,10 +3785,11 @@ function _openaiFinalize(state) {
|
|
|
3370
3785
|
const message = { role: "assistant", content: state.text };
|
|
3371
3786
|
if (toolCalls.length) message.tool_calls = toolCalls;
|
|
3372
3787
|
const out = { message };
|
|
3373
|
-
if (state.inputTokens || state.outputTokens) {
|
|
3788
|
+
if (state.inputTokens || state.outputTokens || state.cacheReadTokens) {
|
|
3374
3789
|
out.usage = {
|
|
3375
3790
|
input_tokens: state.inputTokens,
|
|
3376
3791
|
output_tokens: state.outputTokens,
|
|
3792
|
+
cache_read_input_tokens: state.cacheReadTokens || 0,
|
|
3377
3793
|
};
|
|
3378
3794
|
}
|
|
3379
3795
|
return out;
|
|
@@ -3393,6 +3809,9 @@ async function _chatOpenAIStreaming(
|
|
|
3393
3809
|
onToken,
|
|
3394
3810
|
signal,
|
|
3395
3811
|
provider,
|
|
3812
|
+
onStall,
|
|
3813
|
+
stallMs,
|
|
3814
|
+
stallTimeoutMs,
|
|
3396
3815
|
) {
|
|
3397
3816
|
const response = await fetch(apiUrl, {
|
|
3398
3817
|
method: "POST",
|
|
@@ -3411,9 +3830,11 @@ async function _chatOpenAIStreaming(
|
|
|
3411
3830
|
const decoder = new TextDecoder();
|
|
3412
3831
|
let buf = "";
|
|
3413
3832
|
try {
|
|
3414
|
-
for (
|
|
3415
|
-
|
|
3416
|
-
|
|
3833
|
+
for await (const value of _iterateStreamWithStall(reader, {
|
|
3834
|
+
onStall,
|
|
3835
|
+
stallMs,
|
|
3836
|
+
stallTimeoutMs,
|
|
3837
|
+
})) {
|
|
3417
3838
|
buf += decoder.decode(value, { stream: true });
|
|
3418
3839
|
const lines = buf.split("\n");
|
|
3419
3840
|
buf = lines.pop() || "";
|