chainlesschain 0.162.125 → 0.162.127
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/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-DcqNhZhA.js → AIOps-CsHJh6tT.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BigtmPoq.js → ActionButton-ClDr78Wl.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-1J_X_HF4.js → Analytics-CQQBX5mv.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DlcDG_a_.js → AppLayout-DIofBNeG.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-Do-y8_3w.js → Audit-d8pxGJLK.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-S2Df-50Y.js → Backup-ChZb31ek.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D5kgWJfk.js → BaseInput-CQsSXb5v.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-aUcp3kN1.js → Chat-C_mjILx8.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-B545kWla.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-DTg1U7Xs.js → Checkbox-CWFmVuo8.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-YO9ZZ4Ky.js → Codegen-DwicePOQ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BHonE9fU.js → Col-B36SnRdL.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DA2AlEFh.js → Community-C_sZ2HhK.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DULxLRNg.js → Compact-B2jzDcUl.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BZqfuuZL.js → Compliance-BD58_IHe.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CJe3vyMS.js → Cowork-VIBdGc4D.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CeV8AtRu.js → Cron-CDTVWUmV.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CAg66zS5.js → Crosschain-D5DkGNKU.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Dtup5JZm.js → DID-B23ae8PQ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DAR_8PNy.js → Dashboard-DTG5MsSx.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-K5bTaCYN.js → Dropdown-D35RRnMZ.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C890uErx.js → EmailListRenderer-DRvY2nPk.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-B-Tj_8yM.js → FamilyGuardDashboard-BT_eESkk.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C6WZ98ni.js → Federation-CxX9F7IL.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-D-LTfGWd.js → FormItemContext-C1xDz6D8.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C80DK4W7.js → GenericCardRenderer-CjBOAXcO.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Cjvvv3zW.js → Git-I9I9NkPO.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C0BND77H.js → Governance-BiigWxQW.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BL9kTtSu.js → Inference-C0KrSCgh.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CnVTBmJf.js → KnowledgeGraph-B1Nd54ow.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CbCbg7K6.js → Logs-CeOtvYIo.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CG5On-fH.js → Marketplace-CRhgvAVQ.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-AYYDZZK7.js → McpTools-BIJNah-H.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-tMWbMDQq.js → Memory-D6gsbo4e.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BAOsf4wB.js → MobileBridge-DDkIKA6U.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BT-2komQ.js → MobileProjects-BBkI02Lf.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BFwfC63n.js → Mtc-BziHK7HS.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-DYG3CWdH.js → MtcAudit-CWXjbkeF.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-KJwd0yWT.js → Multisig-9jNF_VWa.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BmL5dNWm.js → NLProgramming-BUdF7I2G.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DmkaVaMc.js → Notes-D5ls1r1T.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-tmVQszDZ.js → NotificationSettings-B2WX_TMX.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BtrR7anf.js → OrderTableRenderer-COSzdTcE.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-pppktQyW.js → Organization-tzWIgnbM.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D3lBoQDA.js → Overflow-Ca1gp_3S.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BgE2qGlZ.js → P2P-XnfDfRK9.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-D26Bz5gS.js → PdhVaultBrowser-23Susb25.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BR8no2Xe.js → Permissions-CnkhYPqt.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DabbyQEF.js → PersonalDataHub-CZ1UasJc.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-n4sNpEz8.js → Pipeline-DC1oC84K.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CGNp4n8M.js → Privacy-CdqayaCE.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CAVA5VsX.js → ProjectInit-DS7NbGQL.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-GCgkfM7A.js → ProjectSettings-fdzwup3n.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BYK17p52.js → Projects-FvodeZob.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DpUT5W-d.js → Providers-IxYyzeSI.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-ZJxTb7vi.js → QuickAsk-BAtFET-B.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CIzFRDk1.js → Recommend-DCNRV9Vr.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-lQ_WDNZn.js → Reputation-lg2JmIai.js} +1 -1
- package/src/assets/web-panel/assets/{Row-1MEtrgtF.js → Row-BAhyDuri.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-SSgcPPl4.js → RssFeed-BA_3issD.js} +2 -2
- package/src/assets/web-panel/assets/{Search-CcTMOGNY.js → Search-CJheUJDl.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BA1KMaDx.js → Security-D2VczQDD.js} +3 -3
- package/src/assets/web-panel/assets/{Services-_aj7czZn.js → Services-7ZFAzqVR.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BWgg_0Zr.js → Skeleton-CsBoTasD.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C2ZIziYM.js → Skills-DT_j_gj0.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CYxp4axY.js → Sla-C9D5geJx.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C7Csp1jV.js → SpeechSettings-CWCNVW42.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-svGeswiw.js → SyncSettings-TYCbf2kk.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-ClISj6oK.js → Tasks-CbRoXu1G.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CmO-Fp7r.js → Templates-CxBjIrkJ.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-a3Ac3lxz.js → Tenant-NKVyFsPM.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DyJIRh81.js → Terminal-D5aAL5_H.js} +2 -2
- package/src/assets/web-panel/assets/TimelineRenderer-CD1JE0Rb.js +1 -0
- package/src/assets/web-panel/assets/{Tokens-DJW0KqV4.js → Tokens-CdepBSYe.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-0HeTi4r6.js → Trigger-Br3n_PEh.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-aFym34eo.js → Trust-CSac0pMf.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CYhszHJv.js → UkeySign-BiWo43zc.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BbJxPF9X.js → VideoEditing-Dj50GuoF.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DDthEwiu.js → Wallet-CQK_g4_U.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DQ6McYPg.js → WebAuthn-D8mBl1NC.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-CnF8zRsi.js → WorkflowEditor-CHeS_vab.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Dzkx2gTP.js → chat-CbeOf_lH.js} +1 -1
- package/src/assets/web-panel/assets/{colors-35T51Oo5.js → colors-CTFiyfed.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-TtzNrlu1.js → compact-item-CZsh8FXE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Y1LkWrYb.js → createContext-UPhnXsap.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BFRqfgsk.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BOYw1BgS.js → hasIn-CCqQtSY-.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2od7Bgx.js → index-1d4pat4L.js} +1 -1
- package/src/assets/web-panel/assets/{index-NM9Oke2k.js → index-86dmMEZY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dof7lWA_.js → index-B-OcSTNA.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7dCBw_M.js → index-B1tylGKq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqhwG1ox.js → index-BAcem_Qs.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZ16GlOs.js → index-BKADY9Iw.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPGNc2tF.js → index-Be0uH7m1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAizH273.js → index-Bga0UuWA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFhlelzN.js → index-Blgzobgm.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6tG4B25.js → index-BwPULPuj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjeUZxE5.js → index-CIQVlE9u.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLqC2sRT.js → index-CMi91z74.js} +1 -1
- package/src/assets/web-panel/assets/{index-hyqEKkBT.js → index-CNteMctn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cs82BHAj.js → index-CQHQiYAO.js} +1 -1
- package/src/assets/web-panel/assets/{index-zF77jnI2.js → index-CVKFSFkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-G4ClSmJc.js → index-CctoAd9I.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhMSOd_2.js → index-CdiuRSqw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWgNn9As.js → index-Cy83zglX.js} +3 -3
- package/src/assets/web-panel/assets/{index-Bw1iOGhM.js → index-DBJjqjxL.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4BgCBa3.js → index-DFbyobqi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKRlWHUp.js → index-DcJxrM1F.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7aKAZzS.js → index-DgXn2sWa.js} +1 -1
- package/src/assets/web-panel/assets/{index-D47robkX.js → index-Dkn1r09O.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVrUs6rf.js → index-DmScDPXc.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bs69SI96.js → index-DpTaXoo4.js} +1 -1
- package/src/assets/web-panel/assets/index-DqADH38Q.js +1 -0
- package/src/assets/web-panel/assets/{index-BxWUEFKy.js → index-DvaDSd9k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtlJWaSP.js → index-LIFKNtNq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGPEaeB1.js → index-LyzbfmcN.js} +1 -1
- package/src/assets/web-panel/assets/index-Pbx8iJt_.js +1 -0
- package/src/assets/web-panel/assets/{index-KgXrlfNF.js → index-WGmri9tu.js} +1 -1
- package/src/assets/web-panel/assets/{index-XIuZV9by.js → index-Yd1x-xhs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dhfw-BXs.js → index-_OOipSkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx0hIfC-.js → index-dwBroN51.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Wt5feN.js → index-e6tj_vqt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdyFg4FI.js → index-lOxvi-gf.js} +1 -1
- package/src/assets/web-panel/assets/{index-8jffdb6b.js → index-qTrskpW-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmkG479D.js → index-uH-glcG6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSpFe6j5.js → index-z5pyzCXj.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DD5y5msp.js → initDefaultProps-K95PVa57.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BuoutMia.js → motion-DwTSyvvW.js} +1 -1
- package/src/assets/web-panel/assets/{move-DkpjqOXg.js → move-BuRSogkN.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BmGJD-v-.js → mtc-parser-twEnPl1V.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DftUE0Sz.js → omit-CdPhebkX.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDRb9FIu.js → pickAttrs-CGcIpdPi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DPgtxOOD.js → placementArrow-DixkX9VV.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BQFRzFeZ.js → responsiveObserve-C74t3rB5.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DQ4lmUSz.js → slide-TcNt6K6l.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-C1TR4ZiQ.js → statusUtils-mpTN9mrS.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B-Suj978.js → styleChecker-Bwg8zmIj.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Q7bDZ3EI.js → useFlexGapSupport-Cz4mN4sW.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DHjRLyQH.js → useFs-DF9zRfbI.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-lQOvCvCr.js → usePersonalDataHub-BcQ4O2UK.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Ie0g4-p3.js → vnode-juM2dgAV.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CHP0YEoH.js → zoom-c4aJj-vC.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +41 -2
- package/src/commands/compliance.js +3 -1
- package/src/commands/review.js +47 -17
- package/src/commands/session.js +11 -2
- package/src/gateways/ws/message-dispatcher.js +145 -165
- package/src/gateways/ws/ws-session-gateway.js +39 -5
- package/src/harness/jsonl-session-store.js +39 -12
- package/src/harness/worktree-isolator.js +5 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/audit-logger.js +3 -1
- package/src/lib/chat-core.js +5 -2
- package/src/lib/chat-intent-service.js +68 -21
- package/src/lib/config-manager.js +25 -1
- package/src/lib/credential-guard.js +43 -1
- package/src/lib/git-integration.js +5 -0
- package/src/lib/interaction-adapter.js +32 -11
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/llm-config-defaults.js +37 -0
- package/src/lib/mcp-oauth.js +76 -10
- package/src/lib/permission-engine.js +4 -1
- package/src/lib/personal-data-hub-wiring.js +14 -0
- package/src/lib/pipeline-orchestrator.js +20 -2
- package/src/lib/project-detector.js +44 -3
- package/src/lib/project-instructions.js +13 -0
- package/src/lib/repl-denials.js +137 -0
- package/src/lib/safe-json.js +22 -0
- package/src/lib/sandbox-v2.js +7 -6
- package/src/lib/search-command.js +65 -0
- package/src/lib/settings-hooks.cjs +133 -0
- package/src/lib/settings-loader.cjs +16 -7
- package/src/lib/social-graph.js +3 -1
- package/src/lib/stream-retry.js +27 -0
- package/src/lib/turn-context.js +15 -5
- package/src/repl/agent-repl.js +81 -8
- package/src/runtime/__tests__/message-roles.test.js +36 -3
- package/src/runtime/agent-core.js +175 -50
- package/src/runtime/coding-agent-contract-shared.cjs +9 -2
- package/src/runtime/coding-agent-shell-policy.cjs +23 -2
- package/src/runtime/file-ref-expander.js +51 -7
- package/src/runtime/headless-runner.js +83 -2
- package/src/runtime/headless-stream.js +69 -3
- package/src/runtime/mcp-config.js +42 -1
- package/src/runtime/message-roles.js +14 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +0 -1
- package/src/assets/web-panel/assets/TimelineRenderer-acXS5N5h.js +0 -1
- package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +0 -1
- package/src/assets/web-panel/assets/index-CEwuCM44.js +0 -1
- package/src/assets/web-panel/assets/index-DzzgU4IU.js +0 -1
|
@@ -22,6 +22,40 @@ import { firstBalancedJson } from "./json-schema-output.js";
|
|
|
22
22
|
|
|
23
23
|
const DEFAULT_INTENT_TIMEOUT_MS = 15000;
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the per-call intent-classification budget. `llmOptions.intentTimeoutMs`
|
|
27
|
+
* overrides the default; an explicit 0 disables the cap; NaN/negative/absent →
|
|
28
|
+
* default.
|
|
29
|
+
*/
|
|
30
|
+
function resolveIntentTimeout(llmOptions) {
|
|
31
|
+
const raw = llmOptions?.intentTimeoutMs;
|
|
32
|
+
if (raw === 0) return 0; // explicit disable
|
|
33
|
+
const n = Number(raw);
|
|
34
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_INTENT_TIMEOUT_MS;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bound an LLM call to the intent budget. chat-core has its OWN stall guard, but
|
|
39
|
+
* it only fires on SILENCE (no bytes for 180s) — a slow trickling stream never
|
|
40
|
+
* trips it, so without this the declared 15s intent budget was never enforced
|
|
41
|
+
* and classification could block for minutes. Racing the deadline makes it fail
|
|
42
|
+
* fast into the caller's graceful rule/verbatim fallback. `ms <= 0` disables.
|
|
43
|
+
* The losing background request is harmless (chat-core releases its own socket);
|
|
44
|
+
* the timer is unref'd so it never holds the process open.
|
|
45
|
+
*/
|
|
46
|
+
function withIntentTimeout(promise, ms) {
|
|
47
|
+
if (!(Number(ms) > 0)) return promise;
|
|
48
|
+
let timer;
|
|
49
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
50
|
+
timer = setTimeout(
|
|
51
|
+
() => reject(new Error(`intent LLM call timed out after ${ms}ms`)),
|
|
52
|
+
Number(ms),
|
|
53
|
+
);
|
|
54
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
55
|
+
});
|
|
56
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
/**
|
|
26
60
|
* Build the V5 understandIntent system + user prompt pair.
|
|
27
61
|
*
|
|
@@ -123,17 +157,20 @@ export async function understandIntent({
|
|
|
123
157
|
);
|
|
124
158
|
|
|
125
159
|
try {
|
|
126
|
-
const fullContent = await
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
const fullContent = await withIntentTimeout(
|
|
161
|
+
chatWithStreaming(
|
|
162
|
+
[
|
|
163
|
+
{ role: "system", content: systemPrompt },
|
|
164
|
+
{ role: "user", content: userPrompt },
|
|
165
|
+
],
|
|
166
|
+
{
|
|
167
|
+
...llmOptions,
|
|
168
|
+
// Lower temperature → more deterministic JSON output.
|
|
169
|
+
temperature: 0.3,
|
|
170
|
+
maxTokens: 500,
|
|
171
|
+
},
|
|
172
|
+
),
|
|
173
|
+
resolveIntentTimeout(llmOptions),
|
|
137
174
|
);
|
|
138
175
|
|
|
139
176
|
const jsonText = extractJson(fullContent);
|
|
@@ -204,6 +241,11 @@ export async function* understandIntentStream({
|
|
|
204
241
|
);
|
|
205
242
|
let buffer = "";
|
|
206
243
|
try {
|
|
244
|
+
// Incremental token yielding makes a per-call deadline awkward to apply
|
|
245
|
+
// here; this streaming variant is instead bounded by chat-core's own
|
|
246
|
+
// silence-based stall guard (CC_CHAT_STALL_MS, default 180s). The awaited
|
|
247
|
+
// understandIntent / classifyFollowupIntent paths enforce the tighter
|
|
248
|
+
// intent budget (resolveIntentTimeout) directly.
|
|
207
249
|
for await (const event of chatStream(
|
|
208
250
|
[
|
|
209
251
|
{ role: "system", content: systemPrompt },
|
|
@@ -452,16 +494,19 @@ ${
|
|
|
452
494
|
|
|
453
495
|
async function llmBasedClassify(userInput, context, llmOptions) {
|
|
454
496
|
const { systemPrompt, userPrompt } = buildFollowupPrompts(userInput, context);
|
|
455
|
-
const fullContent = await
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
497
|
+
const fullContent = await withIntentTimeout(
|
|
498
|
+
chatWithStreaming(
|
|
499
|
+
[
|
|
500
|
+
{ role: "system", content: systemPrompt },
|
|
501
|
+
{ role: "user", content: userPrompt },
|
|
502
|
+
],
|
|
503
|
+
{
|
|
504
|
+
...llmOptions,
|
|
505
|
+
temperature: 0.1,
|
|
506
|
+
maxTokens: 300,
|
|
507
|
+
},
|
|
508
|
+
),
|
|
509
|
+
resolveIntentTimeout(llmOptions),
|
|
465
510
|
);
|
|
466
511
|
const jsonText = extractJson(fullContent);
|
|
467
512
|
if (!jsonText) throw new Error("LLM response missing JSON");
|
|
@@ -540,5 +585,7 @@ export const _internal = {
|
|
|
540
585
|
buildFollowupPrompts,
|
|
541
586
|
extractJson,
|
|
542
587
|
ruleBasedClassify,
|
|
588
|
+
resolveIntentTimeout,
|
|
589
|
+
withIntentTimeout,
|
|
543
590
|
DEFAULT_INTENT_TIMEOUT_MS,
|
|
544
591
|
};
|
|
@@ -11,6 +11,16 @@ import { getConfigPath } from "./paths.js";
|
|
|
11
11
|
import { DEFAULT_CONFIG } from "../constants.js";
|
|
12
12
|
import { withFileLock } from "./with-file-lock.js";
|
|
13
13
|
|
|
14
|
+
// Warn at most once per config path per process. Injectable seam: the default
|
|
15
|
+
// stays silent under vitest so test output isn't polluted; tests override
|
|
16
|
+
// `_deps.warn` to assert it fired.
|
|
17
|
+
const _warnedConfigPaths = new Set();
|
|
18
|
+
export const _deps = {
|
|
19
|
+
warn: (msg) => {
|
|
20
|
+
if (!process.env.VITEST) process.stderr.write(msg);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
14
24
|
export function loadConfig() {
|
|
15
25
|
const configPath = getConfigPath();
|
|
16
26
|
if (!existsSync(configPath)) {
|
|
@@ -20,7 +30,21 @@ export function loadConfig() {
|
|
|
20
30
|
const raw = readFileSync(configPath, "utf-8");
|
|
21
31
|
const parsed = JSON.parse(raw);
|
|
22
32
|
return deepMerge(structuredClone(DEFAULT_CONFIG), parsed);
|
|
23
|
-
} catch {
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// The file EXISTS but couldn't be read/parsed (a typo, trailing comma, or
|
|
35
|
+
// truncated write). Silently returning defaults drops the user's entire
|
|
36
|
+
// config — provider, model, baseUrl, API key — and falls back to the
|
|
37
|
+
// default provider, producing a baffling "configured X but it runs Y / says
|
|
38
|
+
// no API key". Warn once so the failure is visible; still fall back so cc
|
|
39
|
+
// keeps working.
|
|
40
|
+
if (!_warnedConfigPaths.has(configPath)) {
|
|
41
|
+
_warnedConfigPaths.add(configPath);
|
|
42
|
+
_deps.warn(
|
|
43
|
+
`⚠️ Could not read config at ${configPath} (${err.message}). ` +
|
|
44
|
+
`Using defaults — your saved settings (provider / model / API key) are being IGNORED. ` +
|
|
45
|
+
`Fix the JSON to restore them.\n`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
24
48
|
return { ...structuredClone(DEFAULT_CONFIG) };
|
|
25
49
|
}
|
|
26
50
|
}
|
|
@@ -149,7 +149,10 @@ export function credentialFileReasonResolved(targetPath, opts = {}) {
|
|
|
149
149
|
// ── secret ENV-VAR + command detection ──────────────────────────────────────
|
|
150
150
|
|
|
151
151
|
// Content-reader commands: when one of these is aimed at a credential file, the
|
|
152
|
-
// file's bytes land in the agent's tool output.
|
|
152
|
+
// file's bytes land in the agent's tool output. Includes text processors
|
|
153
|
+
// (awk/sed), encoders (base64), and field/byte slicers — all of which dump a
|
|
154
|
+
// file's content just like `cat`, so an agent told to "read the .env" can't
|
|
155
|
+
// route around the guard with `base64 .env` / `awk '{print}' .env`.
|
|
153
156
|
const READ_COMMANDS = new Set([
|
|
154
157
|
"cat",
|
|
155
158
|
"tac",
|
|
@@ -165,8 +168,31 @@ const READ_COMMANDS = new Set([
|
|
|
165
168
|
"xxd",
|
|
166
169
|
"od",
|
|
167
170
|
"strings",
|
|
171
|
+
"awk",
|
|
172
|
+
"gawk",
|
|
173
|
+
"mawk",
|
|
174
|
+
"sed",
|
|
175
|
+
"base64",
|
|
176
|
+
"base32",
|
|
177
|
+
"cut",
|
|
178
|
+
"rev",
|
|
179
|
+
"sort",
|
|
180
|
+
"uniq",
|
|
181
|
+
"tr",
|
|
182
|
+
"fold",
|
|
183
|
+
"expand",
|
|
184
|
+
"unexpand",
|
|
185
|
+
"column",
|
|
186
|
+
"paste",
|
|
187
|
+
"pr",
|
|
168
188
|
]);
|
|
169
189
|
|
|
190
|
+
// grep-family is a content-reader too, but its FIRST non-flag arg is the search
|
|
191
|
+
// PATTERN (which may itself look credential-ish, e.g. `grep "id_rsa" notes.txt`)
|
|
192
|
+
// — so it is handled separately, checking only the FILE args after the pattern,
|
|
193
|
+
// to keep the guard precise (no false positive on the pattern).
|
|
194
|
+
const GREP_COMMANDS = new Set(["grep", "egrep", "fgrep", "rg", "ag"]);
|
|
195
|
+
|
|
170
196
|
// Commands whose job is to print a value into output — the exfil surface for a
|
|
171
197
|
// secret env var (a tool that merely CONSUMES `$API_KEY` does not echo it).
|
|
172
198
|
const PRINT_COMMANDS = new Set([
|
|
@@ -281,6 +307,22 @@ export function commandReadsCredentials(command) {
|
|
|
281
307
|
}
|
|
282
308
|
}
|
|
283
309
|
|
|
310
|
+
// (b2) grep-family: skip the search pattern (first non-flag arg) and check
|
|
311
|
+
// only the file args, so `grep KEY .env` is caught but `grep "id_rsa"
|
|
312
|
+
// notes.txt` (searching FOR that text) is not a false positive.
|
|
313
|
+
if (GREP_COMMANDS.has(first)) {
|
|
314
|
+
for (const tok of nonFlagArgs.slice(1)) {
|
|
315
|
+
const r = credentialFileReason(tok);
|
|
316
|
+
if (r)
|
|
317
|
+
return {
|
|
318
|
+
reason: `reads ${r}`,
|
|
319
|
+
kind: "file",
|
|
320
|
+
target: tok,
|
|
321
|
+
segment: seg,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
284
326
|
// (c) printing / reading a secret-looking env var (via $VAR / %VAR% / env:).
|
|
285
327
|
if (PRINT_COMMANDS.has(first) || READ_COMMANDS.has(first)) {
|
|
286
328
|
const vars = referencedSecretVars(seg);
|
|
@@ -105,6 +105,11 @@ export function gitExec(args, cwd) {
|
|
|
105
105
|
return execSync(`git ${args}`, {
|
|
106
106
|
cwd,
|
|
107
107
|
encoding: "utf-8",
|
|
108
|
+
// git diff/log/worktree-list output on a large repo easily exceeds
|
|
109
|
+
// execSync's 1 MB default → ENOBUFS. Match gitExecArgs' 64 MB ceiling so
|
|
110
|
+
// a big diff/log doesn't spuriously fail (e.g. worktree-diff's --numstat
|
|
111
|
+
// pass, which isn't wrapped in a degrade-gracefully try/catch).
|
|
112
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
108
113
|
stdio: ["pipe", "pipe", "pipe"],
|
|
109
114
|
}).trim();
|
|
110
115
|
} catch (err) {
|
|
@@ -159,7 +159,15 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
159
159
|
},
|
|
160
160
|
options.timeoutMs || 5 * 60 * 1000,
|
|
161
161
|
);
|
|
162
|
-
|
|
162
|
+
// `kind` tags what KIND of response settles this request ("question" vs
|
|
163
|
+
// "host-tool"). Checked in _resolvePending so a peer's session-answer can
|
|
164
|
+
// never resolve a host-tool call with a plain string (or vice versa).
|
|
165
|
+
this._pending.set(requestId, {
|
|
166
|
+
resolve,
|
|
167
|
+
reject,
|
|
168
|
+
timeoutId,
|
|
169
|
+
kind: options.kind || null,
|
|
170
|
+
});
|
|
163
171
|
|
|
164
172
|
this._sendWs({
|
|
165
173
|
...message,
|
|
@@ -170,12 +178,15 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
_ask(questionType, question, extra = {}) {
|
|
173
|
-
return this._request(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
return this._request(
|
|
182
|
+
{
|
|
183
|
+
type: "question",
|
|
184
|
+
questionType,
|
|
185
|
+
question,
|
|
186
|
+
...extra,
|
|
187
|
+
},
|
|
188
|
+
{ kind: "question" },
|
|
189
|
+
);
|
|
179
190
|
}
|
|
180
191
|
|
|
181
192
|
async askInput(question, options = {}) {
|
|
@@ -200,7 +211,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
200
211
|
* Resolves the corresponding pending promise.
|
|
201
212
|
*/
|
|
202
213
|
resolveAnswer(requestId, answer) {
|
|
203
|
-
this._resolvePending(requestId, answer);
|
|
214
|
+
this._resolvePending(requestId, answer, "question");
|
|
204
215
|
}
|
|
205
216
|
|
|
206
217
|
async requestHostTool(toolName, args = {}, extra = {}) {
|
|
@@ -211,12 +222,12 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
211
222
|
args,
|
|
212
223
|
...extra,
|
|
213
224
|
},
|
|
214
|
-
{ timeoutMs: extra.timeoutMs || 60 * 1000 },
|
|
225
|
+
{ timeoutMs: extra.timeoutMs || 60 * 1000, kind: "host-tool" },
|
|
215
226
|
);
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
resolveHostTool(requestId, payload) {
|
|
219
|
-
this._resolvePending(requestId, payload);
|
|
230
|
+
this._resolvePending(requestId, payload, "host-tool");
|
|
220
231
|
}
|
|
221
232
|
|
|
222
233
|
rejectAllPending(reason = createAbortError("Interaction interrupted")) {
|
|
@@ -310,11 +321,21 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
310
321
|
}
|
|
311
322
|
}
|
|
312
323
|
|
|
313
|
-
_resolvePending(requestId, payload) {
|
|
324
|
+
_resolvePending(requestId, payload, expectedKind = null) {
|
|
314
325
|
const pending = this._pending.get(requestId);
|
|
315
326
|
if (!pending) {
|
|
316
327
|
return;
|
|
317
328
|
}
|
|
329
|
+
// Kind guard: a question-answer must not settle a host-tool request (or
|
|
330
|
+
// vice versa). On a mismatch, IGNORE the message — leave the request pending
|
|
331
|
+
// so its correct resolver (or its timeout) can still settle it. Prevents a
|
|
332
|
+
// peer from resolving a host-tool call with a plain question string, or a
|
|
333
|
+
// question with a structured tool payload (type confusion of the awaited
|
|
334
|
+
// value). Backward-compatible: callers passing no expectedKind, and
|
|
335
|
+
// pre-kind pending entries, behave exactly as before.
|
|
336
|
+
if (expectedKind && pending.kind && pending.kind !== expectedKind) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
318
339
|
|
|
319
340
|
this._pending.delete(requestId);
|
|
320
341
|
if (pending.timeoutId) {
|
|
@@ -17,6 +17,43 @@
|
|
|
17
17
|
* lesson as the --settings "opus"→404 vision trap.
|
|
18
18
|
* - No configured provider → unchanged (runner defaults apply: ollama).
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Reconcile a MISLABELED llm config/options object. The `baseUrl` is the
|
|
22
|
+
* authoritative signal of which provider you actually reach, so when `provider`
|
|
23
|
+
* disagrees with the provider its `baseUrl` belongs to, the config is simply
|
|
24
|
+
* mislabeled — the recurring "provider:anthropic + volces baseUrl + model:haiku"
|
|
25
|
+
* corruption that makes runnable-provider relabel (and emit "provider 配置与
|
|
26
|
+
* baseUrl 不一致") on EVERY run because the on-disk config never gets fixed.
|
|
27
|
+
*
|
|
28
|
+
* Correct `provider` to match the endpoint, and if the model is foreign to the
|
|
29
|
+
* corrected provider, replace it with that provider's default. Pure: the input
|
|
30
|
+
* is never mutated — returns `{ llm, changed }` where `llm` is the original
|
|
31
|
+
* object (changed:false) or a corrected shallow copy (changed:true). Works for
|
|
32
|
+
* both a `config.llm` block and a resolved request-`options` (same shape).
|
|
33
|
+
*
|
|
34
|
+
* `inferProviderFromBaseUrl` returns null for unknown/custom hosts (proxies),
|
|
35
|
+
* so an intentionally-custom endpoint is left untouched.
|
|
36
|
+
*/
|
|
37
|
+
export async function reconcileConfigLlmProvider(cfgLlm = {}) {
|
|
38
|
+
if (!cfgLlm || !cfgLlm.provider || !cfgLlm.baseUrl) {
|
|
39
|
+
return { llm: cfgLlm, changed: false };
|
|
40
|
+
}
|
|
41
|
+
const { inferProviderFromBaseUrl, modelForeignToProvider } =
|
|
42
|
+
await import("./runnable-provider.js");
|
|
43
|
+
const inferred = inferProviderFromBaseUrl(cfgLlm.baseUrl);
|
|
44
|
+
if (!inferred || inferred === cfgLlm.provider) {
|
|
45
|
+
return { llm: cfgLlm, changed: false };
|
|
46
|
+
}
|
|
47
|
+
const corrected = { ...cfgLlm, provider: inferred };
|
|
48
|
+
if (modelForeignToProvider(inferred, cfgLlm.model)) {
|
|
49
|
+
const { selectModelForTask, TaskType } =
|
|
50
|
+
await import("./task-model-selector.js");
|
|
51
|
+
const def = selectModelForTask(inferred, TaskType.CHAT);
|
|
52
|
+
if (def) corrected.model = def;
|
|
53
|
+
}
|
|
54
|
+
return { llm: corrected, changed: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
20
57
|
export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
|
|
21
58
|
// The IDE chat panel pins --provider/--model (read from config) but DROPS
|
|
22
59
|
// --base-url/--api-key. With an explicit --provider the block below bails, so
|
package/src/lib/mcp-oauth.js
CHANGED
|
@@ -38,6 +38,10 @@ export const _deps = {
|
|
|
38
38
|
now: () => Date.now(),
|
|
39
39
|
// Backoff sleep seam (tests override with a no-op so the retry doesn't wait).
|
|
40
40
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
41
|
+
// Visible-warning seam (silent under vitest; tests override to assert).
|
|
42
|
+
warn: (msg) => {
|
|
43
|
+
if (!process.env.VITEST) process.stderr.write(msg);
|
|
44
|
+
},
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
/** Discovery/token requests retry ONCE after a transient error (CC 2.1.191). */
|
|
@@ -371,17 +375,56 @@ export function tokenStorePath() {
|
|
|
371
375
|
return pathDefault.join(_deps.homedir(), ".chainlesschain", "mcp-oauth.json");
|
|
372
376
|
}
|
|
373
377
|
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Read the token store, distinguishing "absent / empty" (ok) from "exists but
|
|
380
|
+
* unreadable / corrupt" (ok:false). The distinction matters on the WRITE path:
|
|
381
|
+
* a read-modify-write that treats an unreadable store as `{}` would overwrite
|
|
382
|
+
* and silently destroy every other server's token.
|
|
383
|
+
*/
|
|
384
|
+
function _readStore(file) {
|
|
385
|
+
if (!_deps.fs.existsSync(file)) return { ok: true, store: {} };
|
|
376
386
|
try {
|
|
377
|
-
if (!_deps.fs.existsSync(file)) return {};
|
|
378
387
|
const data = JSON.parse(_deps.fs.readFileSync(file, "utf-8"));
|
|
379
|
-
return data && typeof data === "object" ? data : {};
|
|
388
|
+
return { ok: true, store: data && typeof data === "object" ? data : {} };
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return { ok: false, store: {}, err };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const _warnedCorruptStore = new Set();
|
|
395
|
+
function _warnCorruptStoreOnce(file, err, archived) {
|
|
396
|
+
if (_warnedCorruptStore.has(file)) return;
|
|
397
|
+
_warnedCorruptStore.add(file);
|
|
398
|
+
_deps.warn(
|
|
399
|
+
`⚠️ MCP OAuth token store at ${file} is unreadable (${err?.message || "parse error"}). ` +
|
|
400
|
+
`Saved MCP logins are unavailable${archived ? `; the unreadable file was preserved as ${archived}` : ""} — ` +
|
|
401
|
+
"re-run `cc mcp login <url>` for the servers you use.\n",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Move an unreadable store aside so a fresh write doesn't destroy it. Returns
|
|
406
|
+
* the archive path, or "" when it couldn't be moved (best-effort). */
|
|
407
|
+
function _archiveCorruptStore(file) {
|
|
408
|
+
if (typeof _deps.fs.renameSync !== "function") return "";
|
|
409
|
+
const ts = typeof _deps.now === "function" ? _deps.now() : Date.now();
|
|
410
|
+
const dest = `${file}.corrupt-${ts}`;
|
|
411
|
+
try {
|
|
412
|
+
_deps.fs.renameSync(file, dest);
|
|
413
|
+
return dest;
|
|
380
414
|
} catch {
|
|
381
|
-
return
|
|
415
|
+
return "";
|
|
382
416
|
}
|
|
383
417
|
}
|
|
384
418
|
|
|
419
|
+
export function loadTokenStore() {
|
|
420
|
+
const file = tokenStorePath();
|
|
421
|
+
const r = _readStore(file);
|
|
422
|
+
// Read callers (getStoredToken) treat an unreadable store as "no token", but
|
|
423
|
+
// the user should still know their saved logins silently vanished.
|
|
424
|
+
if (!r.ok) _warnCorruptStoreOnce(file, r.err, "");
|
|
425
|
+
return r.store;
|
|
426
|
+
}
|
|
427
|
+
|
|
385
428
|
export function getStoredToken(serverUrl) {
|
|
386
429
|
return loadTokenStore()[serverKey(serverUrl)] || null;
|
|
387
430
|
}
|
|
@@ -527,7 +570,15 @@ function withStoreLock(file, fn) {
|
|
|
527
570
|
export function saveStoredToken(serverUrl, record) {
|
|
528
571
|
const file = tokenStorePath();
|
|
529
572
|
return withStoreLock(file, () => {
|
|
530
|
-
const
|
|
573
|
+
const r = _readStore(file); // re-read inside the lock (latest state)
|
|
574
|
+
if (!r.ok) {
|
|
575
|
+
// The existing store is unreadable. Writing a fresh single-entry store
|
|
576
|
+
// here would silently destroy every other server's token — move the
|
|
577
|
+
// unreadable file aside first so it can be recovered, and surface it.
|
|
578
|
+
const archived = _archiveCorruptStore(file);
|
|
579
|
+
_warnCorruptStoreOnce(file, r.err, archived);
|
|
580
|
+
}
|
|
581
|
+
const store = r.store;
|
|
531
582
|
store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
|
|
532
583
|
_writeStoreSecure(file, store);
|
|
533
584
|
return store[serverKey(serverUrl)];
|
|
@@ -560,13 +611,28 @@ export function isTokenExpired(record, { skewMs = 60_000 } = {}) {
|
|
|
560
611
|
/**
|
|
561
612
|
* Return a valid bearer token for a server, refreshing if expired. Returns the
|
|
562
613
|
* access_token string, or null if there's no stored token / refresh failed.
|
|
614
|
+
*
|
|
615
|
+
* `forceRefresh` bypasses the local-expiry check and exchanges the refresh
|
|
616
|
+
* token unconditionally. Use it when the server REJECTED a stored token that
|
|
617
|
+
* still looked unexpired locally (mid-session 401/403 from a rotated/revoked
|
|
618
|
+
* grant): the cached access_token is known-bad, so reusing it is pointless —
|
|
619
|
+
* we either mint a fresh one or return null to signal "re-login needed".
|
|
620
|
+
*
|
|
621
|
+
* @param {string} serverUrl
|
|
622
|
+
* @param {{ forceRefresh?: boolean }} [opts]
|
|
563
623
|
*/
|
|
564
|
-
export async function ensureValidToken(
|
|
624
|
+
export async function ensureValidToken(
|
|
625
|
+
serverUrl,
|
|
626
|
+
{ forceRefresh = false } = {},
|
|
627
|
+
) {
|
|
565
628
|
const record = getStoredToken(serverUrl);
|
|
566
629
|
if (!record) return null;
|
|
567
|
-
if (!isTokenExpired(record)) return record.access_token;
|
|
630
|
+
if (!forceRefresh && !isTokenExpired(record)) return record.access_token;
|
|
568
631
|
if (!record.refresh_token || !record.endpoints?.token_endpoint) {
|
|
569
|
-
|
|
632
|
+
// Can't refresh. On a forced refresh the cached token is known-rejected, so
|
|
633
|
+
// hand back null (caller should stop retrying and prompt re-login); on the
|
|
634
|
+
// proactive path keep the existing behaviour of using what we have.
|
|
635
|
+
return forceRefresh ? null : record.access_token || null;
|
|
570
636
|
}
|
|
571
637
|
try {
|
|
572
638
|
const tok = await refreshAccessToken(
|
|
@@ -576,7 +642,7 @@ export async function ensureValidToken(serverUrl) {
|
|
|
576
642
|
const updated = saveStoredToken(serverUrl, { ...record, ...tok });
|
|
577
643
|
return updated.access_token;
|
|
578
644
|
} catch {
|
|
579
|
-
return record.access_token || null;
|
|
645
|
+
return forceRefresh ? null : record.access_token || null;
|
|
580
646
|
}
|
|
581
647
|
}
|
|
582
648
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import crypto from "crypto";
|
|
7
|
+
import { safeJsonParse } from "./safe-json.js";
|
|
7
8
|
|
|
8
9
|
function generateId() {
|
|
9
10
|
return crypto.randomUUID();
|
|
@@ -160,7 +161,9 @@ export function getRoles(db) {
|
|
|
160
161
|
.all();
|
|
161
162
|
return rows.map((r) => ({
|
|
162
163
|
...r,
|
|
163
|
-
|
|
164
|
+
// `|| "[]"` only guarded null — a corrupt permissions cell still threw out
|
|
165
|
+
// of the whole role list (and could break permission checks). Skip it.
|
|
166
|
+
permissions: safeJsonParse(r.permissions, []),
|
|
164
167
|
isBuiltin: r.is_builtin === 1,
|
|
165
168
|
}));
|
|
166
169
|
}
|
|
@@ -62,6 +62,7 @@ let LocalVault,
|
|
|
62
62
|
SystemDataAndroidAdapter,
|
|
63
63
|
BrowserHistoryChromeAdapter,
|
|
64
64
|
BrowserHistoryEdgeAdapter,
|
|
65
|
+
BrowserHistoryAospAdapter,
|
|
65
66
|
VSCodeAdapter,
|
|
66
67
|
WinRecentAdapter,
|
|
67
68
|
GitActivityAdapter,
|
|
@@ -184,6 +185,7 @@ function ensurePdhLoaded() {
|
|
|
184
185
|
SystemDataAndroidAdapter,
|
|
185
186
|
BrowserHistoryChromeAdapter,
|
|
186
187
|
BrowserHistoryEdgeAdapter,
|
|
188
|
+
BrowserHistoryAospAdapter,
|
|
187
189
|
VSCodeAdapter,
|
|
188
190
|
WinRecentAdapter,
|
|
189
191
|
GitActivityAdapter,
|
|
@@ -626,6 +628,18 @@ async function initHub() {
|
|
|
626
628
|
// Continue boot
|
|
627
629
|
}
|
|
628
630
|
|
|
631
|
+
// AOSP / MIUI stock browser (com.android.browser → browser2.db). No host
|
|
632
|
+
// default — needs a device-pulled browser2.db via opts.dbPath at sync time
|
|
633
|
+
// (cc hub sync-adapter browser-history-aosp --db-path <pulled>), so bare
|
|
634
|
+
// registration is inert (healthCheck ok:false) until input is supplied,
|
|
635
|
+
// exactly like the snapshot adapters.
|
|
636
|
+
try {
|
|
637
|
+
const aospBrowser = new BrowserHistoryAospAdapter();
|
|
638
|
+
if (!registry.has(aospBrowser.name)) registry.register(aospBrowser);
|
|
639
|
+
} catch (_err) {
|
|
640
|
+
// Continue boot
|
|
641
|
+
}
|
|
642
|
+
|
|
629
643
|
// VS Code workspace history + global terminal history. Reads
|
|
630
644
|
// %APPDATA%\Code\User\workspaceStorage and \globalStorage\state.vscdb
|
|
631
645
|
// on Win; equivalents on macOS/Linux.
|
|
@@ -553,7 +553,25 @@ export function retryStage(db, pipelineId, stageIndex) {
|
|
|
553
553
|
const pipeline = _getPipelineRow(db, pipelineId);
|
|
554
554
|
if (!pipeline) throw new Error(`Pipeline not found: ${pipelineId}`);
|
|
555
555
|
|
|
556
|
-
|
|
556
|
+
// A retry may only re-open the CURRENT stage or an EARLIER one. A forward
|
|
557
|
+
// jump would move current_stage past the intervening stages — skipping their
|
|
558
|
+
// approval GATES (CODE_REVIEW / DEPLOY) — and let a caller (`cc pipeline retry
|
|
559
|
+
// --stage N`, user-supplied) reach a later stage without the gates that guard
|
|
560
|
+
// it. Going backward is safe: advancing from there re-runs through every gate
|
|
561
|
+
// again. Mirrors the state-machine rigor of completeStage / approveGate.
|
|
562
|
+
const idx = Number(stageIndex);
|
|
563
|
+
if (!Number.isInteger(idx) || idx < 0) {
|
|
564
|
+
throw new Error(`Invalid stage index: ${stageIndex}`);
|
|
565
|
+
}
|
|
566
|
+
if (idx > pipeline.current_stage) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Cannot retry stage ${idx}: it is ahead of the current stage ` +
|
|
569
|
+
`(${pipeline.current_stage}) — retrying forward would skip the ` +
|
|
570
|
+
`intervening stages and their approval gates`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const stage = _getStageRow(db, pipelineId, idx);
|
|
557
575
|
if (!stage) throw new Error(`Stage ${stageIndex} not found`);
|
|
558
576
|
|
|
559
577
|
const now = _now();
|
|
@@ -572,7 +590,7 @@ export function retryStage(db, pipelineId, stageIndex) {
|
|
|
572
590
|
});
|
|
573
591
|
_updatePipelineFields(db, pipelineId, {
|
|
574
592
|
status: PIPELINE_STATUS.RUNNING,
|
|
575
|
-
current_stage:
|
|
593
|
+
current_stage: idx,
|
|
576
594
|
error_message: null,
|
|
577
595
|
completed_at: null,
|
|
578
596
|
});
|
|
@@ -28,19 +28,60 @@ export function findProjectRoot(startDir) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// mtime-keyed parse cache. `config.json` is loaded on EVERY agent tool call
|
|
32
|
+
// (via _loadProjectPersona) and every buildSystemPrompt, so re-reading +
|
|
33
|
+
// re-parsing it each time is wasted syscalls + parse work on the agent hot
|
|
34
|
+
// path. We cache the parsed object keyed by absolute configPath and invalidate
|
|
35
|
+
// when the file's mtimeMs changes — so an external edit or a `persona set`
|
|
36
|
+
// write (which rewrites the file → new mtime) is always picked up on the next
|
|
37
|
+
// call. A cache HIT costs one statSync instead of readFileSync + JSON.parse.
|
|
38
|
+
//
|
|
39
|
+
// The cached object is SHARED across callers — treat the return value as
|
|
40
|
+
// READ-ONLY. Every current caller only reads it; `persona set` reads+writes the
|
|
41
|
+
// file directly (bypassing this loader), so it never aliases the cache.
|
|
42
|
+
const _configCache = new Map(); // configPath -> { mtimeMs, value }
|
|
43
|
+
|
|
31
44
|
/**
|
|
32
45
|
* Load project configuration from .chainlesschain/config.json
|
|
46
|
+
*
|
|
47
|
+
* The returned object is cached and shared — do not mutate it. To change the
|
|
48
|
+
* config, read+write the file directly (see `cc persona set`).
|
|
49
|
+
*
|
|
33
50
|
* @param {string} projectRoot - Project root directory
|
|
34
|
-
* @returns {object|null} Parsed config or null on error
|
|
51
|
+
* @returns {object|null} Parsed config (READ-ONLY) or null on error
|
|
35
52
|
*/
|
|
36
53
|
export function loadProjectConfig(projectRoot) {
|
|
37
54
|
const configPath = path.join(projectRoot, ".chainlesschain", "config.json");
|
|
55
|
+
let mtimeMs;
|
|
38
56
|
try {
|
|
39
|
-
|
|
40
|
-
return JSON.parse(content);
|
|
57
|
+
mtimeMs = fs.statSync(configPath).mtimeMs;
|
|
41
58
|
} catch {
|
|
59
|
+
// Missing / unreadable → no config. Drop any stale entry so a file that
|
|
60
|
+
// reappears later is re-read rather than served from a dead cache slot.
|
|
61
|
+
_configCache.delete(configPath);
|
|
42
62
|
return null;
|
|
43
63
|
}
|
|
64
|
+
const cached = _configCache.get(configPath);
|
|
65
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.value;
|
|
66
|
+
let value;
|
|
67
|
+
try {
|
|
68
|
+
value = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
69
|
+
} catch {
|
|
70
|
+
// Malformed JSON / read race → null (same as before). Cache it against the
|
|
71
|
+
// current mtime so a persistently-broken file isn't re-read every tool call;
|
|
72
|
+
// a later fix bumps mtime and invalidates this entry.
|
|
73
|
+
value = null;
|
|
74
|
+
}
|
|
75
|
+
_configCache.set(configPath, { mtimeMs, value });
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Test seam: clear the loadProjectConfig mtime cache. Not used in production —
|
|
81
|
+
* the cache is process-lifetime and self-invalidates on mtime change.
|
|
82
|
+
*/
|
|
83
|
+
export function _clearProjectConfigCache() {
|
|
84
|
+
_configCache.clear();
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
/**
|