chainlesschain 0.162.170 → 0.162.172
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/bin/chainlesschain.js +8 -0
- package/package.json +6 -1
- package/src/assets/web-panel/assets/{AIOps-BTXN24I3.js → AIOps-oKCWU-LK.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BBB1VF4j.js → ActionButton-4CBfc5E4.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BVCXq0k6.js → Analytics-BEmR-k_b.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-C3EOo2s5.js → AppLayout-D6a4ZnrC.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CsJTZSMv.js → Artifacts-BRWIHCPl.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CP0AllJ_.js → Audit-CzGKlkTc.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BCAjs08m.js → BackgroundAgents-CmMYN10o.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BxQI5XH4.js → Backup-CJ6e0ukD.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Bu-PNzC5.js → BaseInput-BpEvDrNt.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DjPQynup.js → Chat-wWuwwjKu.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CygkRopu.js → ChatBubbleRenderer-CJLyGo_u.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BxL6xm7Z.js → Checkbox-C6pW60Br.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CcyB7h_6.js → Codegen-DF-nP22Y.js} +1 -1
- package/src/assets/web-panel/assets/{Col-8NQiKJJx.js → Col-BuueEwZW.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B31qy5OS.js → Community-DuE0Yzwr.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-C-5fAsZ4.js → Compact-BrHim8EB.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BTSccvF0.js → Compliance-DgAet8f_.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BXctU9IK.js → Cowork-B9wY05lx.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-CxVFX9NG.js → Cron-CvNBxfJX.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DEF3dq8M.js → Crosschain-BnZRENa0.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BmKcE9he.js → DID-DiqYLHTI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CECBq_wG.js → Dashboard-t3SsRzZL.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-U-iuPB0n.js → Dropdown-BCjMLam9.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D8BIlSPL.js → EmailListRenderer-D2v9-IHJ.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BzQGfUH-.js → FamilyGuardDashboard-_iQu35qa.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-4_WlOqLZ.js → Federation-Bkty8VqB.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-gtrW-DdF.js → FormItemContext-DdPW81BY.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BLcvJJB6.js → GenericCardRenderer-DrNYM4Zj.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Bk0janGD.js → Git-BjF-B2h0.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CcfyMlV8.js → Governance-DPhGdJ1v.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CBLrAf4M.js → Inference--PPksOVj.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-ClguhteZ.js → KnowledgeGraph-y5WdV_JV.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Dp_oeKfJ.js → Logs-B6_x7zba.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Bg8zSyF8.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BHk50smM.js → Marketplace-CHpfMu1A.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BAY3CV6J.js → McpTools-qqjtX3Cf.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-w0pG2AKx.js → Memory-hASSgxbw.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C0VkDUE3.js → MobileBridge-Dwh7Bhcf.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BCvQ80I9.js → MobileProjects-D09voh7W.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DSIl41Tt.js → Mtc-9G1dXeId.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-D1mzUlIr.js → MtcAudit-CWtYXj6f.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CeJz4VSS.js → Multisig-bDI0EnDk.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-_O3aweu8.js → NLProgramming-CTanRaBP.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Dt7_pO86.js → Notes-BdQENJFv.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Dhg3Nktv.js → NotificationSettings-JvnQdStO.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-QsAPcTb2.js → OrderTableRenderer-BMGL9RPJ.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BuiaUPkv.js → Organization-sBZnIb2D.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C3jb6knl.js → Overflow-CzOhNGE6.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BOMyrLkb.js → P2P-D_D2yqda.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-UvLStCgS.js → PdhVaultBrowser-D5lKd_Kx.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-B9cN0dJH.js → Permissions-L5s-wENg.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DNjW5qro.js → PersonalDataHub-iI49x6cH.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-CFLyZ6Sh.js → Pipeline-DPHqyk09.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-D3w6vOxf.js → Privacy-B-ZL9360.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-cwcVsn8K.js → ProjectInit-f05YzsN2.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-aUoAroc-.js → ProjectSettings-DZLKS6ex.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CrzQv7Zv.js → Projects-ButF3DyM.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-jZaPizKR.js → Providers-CFDSXnkR.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DNzmwcfg.js → QrScannerModal-1N6Sdxdm.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-m-5Gwt70.js → QuickAsk-tEqRwRSR.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CrrAjNxP.js → Recommend-D4ZVHIwz.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BDCMjRFe.js → RemoteSession-Cqm32lhm.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CPCt1Jc8.js → Reputation-BnjVsU_a.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C-RorxHs.js → Row-DzpHyTuz.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CnQs4TuY.js → RssFeed-Du5-ouwx.js} +3 -3
- package/src/assets/web-panel/assets/{Search-B1jkeFZY.js → Search-CYztyf2o.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CGKI3vvd.js → Security-C1hVxy8Z.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DQjesYF8.js → Services-BscEbAZ2.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C9Ipj0A6.js → Skeleton-B2sMLqKQ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D2j54i9O.js → Skills-CoHKax06.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-xhrVjTeb.js → Sla-Bpie5KO6.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C6KMOUCa.js → SpeechSettings-BX63ijn5.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CmIPU5wR.js → SyncSettings-Bf72so5A.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-B-T1J42r.js → Tasks-CVB2PZk4.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Aq669sjw.js → Templates-q0qyclJx.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BKWUWNXB.js → Tenant-CW-W-i0k.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DEeAvA8L.js → Terminal-D7QoPiXQ.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C465ySdn.js → TimelineRenderer-BaRDAbyA.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BgVqF-bX.js → Tokens-D6U4PRW8.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BMNHmxwH.js → Trigger-ClBDWdLo.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B_adk89L.js → Trust-C_M6rWPq.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Biw38mog.js → UkeySign-bXHx8s4C.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-AC-eaw5k.js → VideoEditing-BcIt_ieI.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8lQ5_t0X.js → Wallet-Dd-4jcLs.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-JDmH_-DF.js → WebAuthn-B4F6yxQK.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DHQAdITG.js → WorkflowEditor-BEHkpfJq.js} +1 -1
- package/src/assets/web-panel/assets/{chat-C3qauUVv.js → chat-D7hhRpIO.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DLP70JGe.js → colors-AMRhrRzS.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DjsoqA5t.js → compact-item-BYpsOET5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CfEdl1in.js → createContext-CoUkR4LQ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BRylvxzY.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWwHHlBG.js → hasIn-CJKJMKki.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dfka-vzJ.js → index-8pmZHnQU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgQ92xdO.js → index-BBJFiZ9Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-VtCz3A64.js → index-BP69Ul3S.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8wrUFzY.js → index-BTFHYauX.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5VL0NYy.js → index-BUJt2ar4.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-rD2rhi.js → index-Bj30Aq6h.js} +1 -1
- package/src/assets/web-panel/assets/{index-gu8zBhtp.js → index-BlpuqGt5.js} +1 -1
- package/src/assets/web-panel/assets/index-Bzvxvzlk.js +1 -0
- package/src/assets/web-panel/assets/{index-CgruUvB3.js → index-C19xZKIP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTEcPJGf.js → index-C6bg22vc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4k6JGm_.js → index-CH6XEWdj.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0xMzvk8.js → index-CK1Kknpl.js} +1 -1
- package/src/assets/web-panel/assets/{index-UEbYl2pu.js → index-CNyLbBGj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dmt1HDWZ.js → index-COg__P6M.js} +19 -19
- package/src/assets/web-panel/assets/{index-ArZCi1rK.js → index-CRlJOWsf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqPuyN4p.js → index-CScG8TAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4gCFHtB.js → index-CdL9cQIE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C47EiQ7d.js → index-Ck35u_0v.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn2rNMmv.js → index-CkLZQtsh.js} +1 -1
- package/src/assets/web-panel/assets/{index-T5emAyFT.js → index-DFoujkWD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbFnfyKh.js → index-DFwh0R9h.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsEk9s04.js → index-DStN9E7o.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTDpF8MS.js → index-DcozYrXV.js} +1 -1
- package/src/assets/web-panel/assets/{index-_g5q2hF3.js → index-De5mYp86.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bp_WNm2_.js → index-DfEVOzNb.js} +1 -1
- package/src/assets/web-panel/assets/{index-OfMwkFIo.js → index-Dfna8sKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcDeMCUK.js → index-DjbMaBLf.js} +1 -1
- package/src/assets/web-panel/assets/index-Dl6H3dxS.js +1 -0
- package/src/assets/web-panel/assets/{index-D-te-S9f.js → index-Dpl_SJuk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKsWujXP.js → index-DyF9uYzB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydBh7oT.js → index-HF3GvATm.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAcBi6F-.js → index-OJmXGn8n.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNeLedtU.js → index-ZPdNPhUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ds1BfT_C.js → index-djikyGz3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbF27GqZ.js → index-gQWWnP3_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnhJbWsl.js → index-owVbLJnH.js} +1 -1
- package/src/assets/web-panel/assets/{index-Lh0e2afo.js → index-tWEDZHzj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbjUePgq.js → index-ttTl376q.js} +1 -1
- package/src/assets/web-panel/assets/{index-nuZaMsMy.js → index-z5rYvstM.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C_pMmi75.js → initDefaultProps-B_Uk-wzg.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BvKgS96o.js → motion-ez7H8y8b.js} +1 -1
- package/src/assets/web-panel/assets/{move-C-1U4R1I.js → move-CKWUhhhX.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-D6BC968-.js → mtc-parser-DkNl7ocj.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DxOZvcGQ.js → omit-C0uA2ytw.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-LpzmHdkw.js → pickAttrs-CySaWMPl.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Bm--qom1.js → placementArrow-BWL3-3xo.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CVZZlhJf.js → responsiveObserve-BRy_wJog.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DD1xINGx.js → slide-DmmvSaBD.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-LrjHAy63.js → statusUtils-C_7SYY0K.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BqknYxTe.js → styleChecker-DYgBJGDh.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Dljy3diT.js → useFlexGapSupport-nPkoZkT6.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-OedUtPLc.js → useFs-CzpsfP4_.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Cz-kxbP_.js → usePersonalDataHub-Bo2vvMWl.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C8ZuTk8W.js → vnode-D-hXAnHb.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CjbgJYXQ.js → zoom-DW7seTGG.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +39 -14
- package/src/commands/background-session.js +12 -1
- package/src/commands/permissions.js +24 -1
- package/src/gateways/ws/ws-agent-handler.js +26 -1
- package/src/gateways/ws/ws-approval-gate.js +131 -0
- package/src/index.js +63 -845
- package/src/lib/agent-ipc-bus.js +331 -0
- package/src/lib/agent-worktree.js +173 -5
- package/src/lib/background-agent-supervisor.js +193 -4
- package/src/lib/context-source-ledger.js +164 -0
- package/src/lib/execution-trace/index.js +10 -0
- package/src/lib/execution-trace/metrics-collector.js +218 -0
- package/src/lib/execution-trace/trace-context.js +182 -0
- package/src/lib/hook-manager.js +17 -0
- package/src/lib/hooks-v2-runtime.js +278 -0
- package/src/lib/interaction-adapter.js +26 -5
- package/src/lib/metrics-collector.js +127 -0
- package/src/lib/observability/index.js +33 -0
- package/src/lib/observability/otlp-exporter.js +260 -0
- package/src/lib/otlp-exporter.js +45 -0
- package/src/lib/process-execution-broker/SPAWN_AUDIT_REPORT.md +117 -0
- package/src/lib/process-execution-broker/index.js +450 -0
- package/src/lib/process-execution-broker/patch-child-process.js +50 -0
- package/src/lib/process-execution-broker/spawn-audit.json +3814 -0
- package/src/lib/repl-rewind.js +62 -14
- package/src/lib/runtime-provenance-ledger.js +190 -0
- package/src/lib/session-hooks.js +41 -0
- package/src/lib/turn-binding.js +134 -0
- package/src/program-base.js +4 -1
- package/src/repl/agent-repl.js +77 -3
- package/src/repl/repl-turn-binding.js +91 -0
- package/src/runtime/headless-runner.js +59 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-RQ1HbTY5.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Cwuuqc_f.js +0 -1
- package/src/assets/web-panel/assets/index-BkTk-zJK.js +0 -1
- package/src/assets/web-panel/assets/index-C-BSTEhe.js +0 -1
|
@@ -6,16 +6,21 @@ import {
|
|
|
6
6
|
openSync,
|
|
7
7
|
readFileSync,
|
|
8
8
|
readdirSync,
|
|
9
|
+
realpathSync,
|
|
9
10
|
renameSync,
|
|
10
11
|
rmSync,
|
|
11
12
|
statSync,
|
|
12
13
|
writeFileSync,
|
|
13
14
|
} from "node:fs";
|
|
14
15
|
import { randomBytes } from "node:crypto";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import { getHomeDir } from "./paths.js";
|
|
18
19
|
import { deriveSessionState } from "./session-lifecycle.js";
|
|
20
|
+
import {
|
|
21
|
+
finishAgentWorktree,
|
|
22
|
+
validateAgentWorktree,
|
|
23
|
+
} from "./agent-worktree.js";
|
|
19
24
|
|
|
20
25
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
|
|
21
26
|
export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
|
|
@@ -280,6 +285,20 @@ export function writeBackgroundAgentState(state) {
|
|
|
280
285
|
if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
|
|
281
286
|
next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
|
|
282
287
|
}
|
|
288
|
+
// A background worktree is immutable session identity, just like the
|
|
289
|
+
// conversation id. Heartbeat/finalize/rename writers may race with a stale
|
|
290
|
+
// snapshot, but no same-id transition is allowed to move the run back to
|
|
291
|
+
// the main checkout or drop the cleanup metadata.
|
|
292
|
+
if (current.worktreePath) {
|
|
293
|
+
next = {
|
|
294
|
+
...next,
|
|
295
|
+
cwd: current.worktreePath,
|
|
296
|
+
repoRoot: current.repoRoot,
|
|
297
|
+
worktreePath: current.worktreePath,
|
|
298
|
+
baseSha: current.baseSha,
|
|
299
|
+
branch: current.branch,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
283
302
|
}
|
|
284
303
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
285
304
|
writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
@@ -500,7 +519,7 @@ export function renameBackgroundAgent(id, title, options = {}) {
|
|
|
500
519
|
* conversation session is NOT touched — that's `cc session delete`.
|
|
501
520
|
*/
|
|
502
521
|
export function removeBackgroundAgent(id, options = {}) {
|
|
503
|
-
|
|
522
|
+
let state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
504
523
|
now: options.now,
|
|
505
524
|
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
506
525
|
});
|
|
@@ -513,15 +532,28 @@ export function removeBackgroundAgent(id, options = {}) {
|
|
|
513
532
|
}
|
|
514
533
|
try {
|
|
515
534
|
stopBackgroundAgent(id);
|
|
535
|
+
state =
|
|
536
|
+
effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
537
|
+
now: options.now,
|
|
538
|
+
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
539
|
+
}) || state;
|
|
516
540
|
} catch {
|
|
517
541
|
/* best-effort — the record removal below is the point of rm --force */
|
|
518
542
|
}
|
|
519
543
|
}
|
|
544
|
+
const worktree = cleanupBackgroundAgentWorktree(state, {
|
|
545
|
+
keepWorktree: options.keepWorktree === true,
|
|
546
|
+
});
|
|
520
547
|
rmSync(statePath(id), { force: true });
|
|
521
548
|
if (options.keepLog !== true) {
|
|
522
549
|
rmSync(logPath(id), { force: true });
|
|
523
550
|
}
|
|
524
|
-
return {
|
|
551
|
+
return {
|
|
552
|
+
id,
|
|
553
|
+
removed: true,
|
|
554
|
+
status: state.status,
|
|
555
|
+
...(worktree ? { worktree } : {}),
|
|
556
|
+
};
|
|
525
557
|
}
|
|
526
558
|
|
|
527
559
|
/**
|
|
@@ -598,6 +630,68 @@ export function buildFollowUpArgv(argv, opts = {}) {
|
|
|
598
630
|
return out;
|
|
599
631
|
}
|
|
600
632
|
|
|
633
|
+
function canonicalPath(value) {
|
|
634
|
+
const absolute = resolve(String(value || ""));
|
|
635
|
+
try {
|
|
636
|
+
const real = realpathSync.native
|
|
637
|
+
? realpathSync.native(absolute)
|
|
638
|
+
: realpathSync(absolute);
|
|
639
|
+
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
640
|
+
} catch {
|
|
641
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Normalize and verify worktree metadata before it enters a background job.
|
|
647
|
+
* The metadata is persisted as top-level state fields for compatibility with
|
|
648
|
+
* status/IDE consumers: repoRoot/worktreePath/baseSha/branch.
|
|
649
|
+
*/
|
|
650
|
+
export function normalizeBackgroundWorktree(worktree, cwd) {
|
|
651
|
+
if (!worktree) return null;
|
|
652
|
+
const normalized = {
|
|
653
|
+
repoRoot: resolve(String(worktree.repoRoot || "")),
|
|
654
|
+
worktreePath: resolve(String(worktree.worktreePath || worktree.path || "")),
|
|
655
|
+
baseSha: String(worktree.baseSha || ""),
|
|
656
|
+
branch: String(worktree.branch || ""),
|
|
657
|
+
};
|
|
658
|
+
if (
|
|
659
|
+
!normalized.repoRoot ||
|
|
660
|
+
!normalized.worktreePath ||
|
|
661
|
+
!normalized.baseSha ||
|
|
662
|
+
!normalized.branch
|
|
663
|
+
) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
"Cannot launch background agent: incomplete worktree metadata",
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (canonicalPath(cwd) !== canonicalPath(normalized.worktreePath)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
"Cannot launch background agent: cwd does not match the persisted worktree path",
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const validation = validateAgentWorktree({
|
|
674
|
+
...normalized,
|
|
675
|
+
path: normalized.worktreePath,
|
|
676
|
+
});
|
|
677
|
+
if (!validation.valid) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`Cannot launch background agent: worktree metadata is not usable (${validation.reason})`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return normalized;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function persistedWorktree(state) {
|
|
686
|
+
if (!state?.worktreePath) return null;
|
|
687
|
+
return {
|
|
688
|
+
repoRoot: state.repoRoot,
|
|
689
|
+
worktreePath: state.worktreePath,
|
|
690
|
+
baseSha: state.baseSha,
|
|
691
|
+
branch: state.branch,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
601
695
|
/**
|
|
602
696
|
* Continue a finished/crashed background session as a NEW background session
|
|
603
697
|
* on the same conversation (`cc daemon resume <id> <prompt>`). The original
|
|
@@ -622,14 +716,27 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
|
|
|
622
716
|
}
|
|
623
717
|
const text = String(prompt || "").trim();
|
|
624
718
|
if (!text) throw new Error("resume requires a prompt");
|
|
719
|
+
const worktree = persistedWorktree(state);
|
|
720
|
+
if (
|
|
721
|
+
worktree &&
|
|
722
|
+
options.cwd &&
|
|
723
|
+
canonicalPath(options.cwd) !== canonicalPath(worktree.worktreePath)
|
|
724
|
+
) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`Background agent ${id} is bound to worktree "${worktree.worktreePath}" and cannot resume in a different cwd`,
|
|
727
|
+
);
|
|
728
|
+
}
|
|
625
729
|
const argv = ["agent", "--session", state.sessionId, "-p", text];
|
|
626
730
|
return launchBackgroundAgent({
|
|
627
731
|
argv,
|
|
628
|
-
|
|
732
|
+
// Worktree sessions fail closed: never fall back to process.cwd(), which
|
|
733
|
+
// could silently resume a write task in the main checkout.
|
|
734
|
+
cwd: worktree?.worktreePath || options.cwd || state.cwd || process.cwd(),
|
|
629
735
|
sessionId: state.sessionId,
|
|
630
736
|
title: options.title || state.title || text.slice(0, 100),
|
|
631
737
|
cliEntry: options.cliEntry,
|
|
632
738
|
followUpArgv: ["agent", "--session", state.sessionId],
|
|
739
|
+
worktree,
|
|
633
740
|
});
|
|
634
741
|
}
|
|
635
742
|
|
|
@@ -667,8 +774,10 @@ export function launchBackgroundAgent({
|
|
|
667
774
|
title,
|
|
668
775
|
cliEntry,
|
|
669
776
|
followUpArgv,
|
|
777
|
+
worktree,
|
|
670
778
|
}) {
|
|
671
779
|
assertUsableCwd(cwd);
|
|
780
|
+
const worktreeState = normalizeBackgroundWorktree(worktree, cwd);
|
|
672
781
|
const id = createBackgroundAgentId();
|
|
673
782
|
const dir = backgroundAgentsDir();
|
|
674
783
|
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
@@ -683,6 +792,7 @@ export function launchBackgroundAgent({
|
|
|
683
792
|
title: title || "Background agent",
|
|
684
793
|
cliEntry: cliEntry || process.argv[1],
|
|
685
794
|
logFile: logPath(id),
|
|
795
|
+
...(worktreeState || {}),
|
|
686
796
|
// Present = interactive attach can start follow-up turns; absent = the
|
|
687
797
|
// transport rejects prompts (log-only session).
|
|
688
798
|
...(Array.isArray(followUpArgv) ? { followUpArgv } : {}),
|
|
@@ -706,6 +816,7 @@ export function launchBackgroundAgent({
|
|
|
706
816
|
endedAt: null,
|
|
707
817
|
exitCode: null,
|
|
708
818
|
logFile: job.logFile,
|
|
819
|
+
...(worktreeState || {}),
|
|
709
820
|
};
|
|
710
821
|
writeBackgroundAgentState(state);
|
|
711
822
|
let child;
|
|
@@ -866,6 +977,84 @@ export function stopBackgroundAgent(id) {
|
|
|
866
977
|
return { ...next, stopped: true };
|
|
867
978
|
}
|
|
868
979
|
|
|
980
|
+
function activeWorktreeProcesses(state) {
|
|
981
|
+
const active = [];
|
|
982
|
+
if (
|
|
983
|
+
Number.isInteger(Number(state?.pid)) &&
|
|
984
|
+
Number(state.pid) > 0 &&
|
|
985
|
+
isSameProcess(state.pid, state.startedAt)
|
|
986
|
+
) {
|
|
987
|
+
active.push("worker");
|
|
988
|
+
}
|
|
989
|
+
if (
|
|
990
|
+
Number.isInteger(Number(state?.agentPid)) &&
|
|
991
|
+
Number(state.agentPid) > 0 &&
|
|
992
|
+
isSameProcess(state.agentPid, state.agentStartedAt)
|
|
993
|
+
) {
|
|
994
|
+
active.push("agent");
|
|
995
|
+
}
|
|
996
|
+
return active;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Explicit background-record cleanup owns the worktree teardown. Normal
|
|
1001
|
+
* completion, kill, lost-worker reconciliation, attach and resume all retain
|
|
1002
|
+
* it so a later resume continues in the exact same isolated checkout.
|
|
1003
|
+
*/
|
|
1004
|
+
export function cleanupBackgroundAgentWorktree(state, options = {}) {
|
|
1005
|
+
const worktree = persistedWorktree(state);
|
|
1006
|
+
if (!worktree) return null;
|
|
1007
|
+
if (options.keepWorktree === true) {
|
|
1008
|
+
return {
|
|
1009
|
+
removed: false,
|
|
1010
|
+
kept: true,
|
|
1011
|
+
reason: "kept by explicit request",
|
|
1012
|
+
path: worktree.worktreePath,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const blockers = [];
|
|
1017
|
+
const active = activeWorktreeProcesses(state);
|
|
1018
|
+
if (active.length) blockers.push(`active ${active.join("+")} process`);
|
|
1019
|
+
if (Number(state?.pendingApprovals || 0) > 0) {
|
|
1020
|
+
blockers.push("pending approval");
|
|
1021
|
+
}
|
|
1022
|
+
if (state?.pendingQuestion) blockers.push("pending user input");
|
|
1023
|
+
if (
|
|
1024
|
+
Number(state?.uncertainSideEffects || 0) > 0 ||
|
|
1025
|
+
state?.phase === "uncertain_side_effect" ||
|
|
1026
|
+
state?.phase === "uncertain-side-effect"
|
|
1027
|
+
) {
|
|
1028
|
+
blockers.push("unfinished or uncertain side effect");
|
|
1029
|
+
}
|
|
1030
|
+
if (blockers.length) {
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`Refusing to clean background worktree "${worktree.worktreePath}": ${blockers.join(", ")}`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!existsSync(worktree.worktreePath)) {
|
|
1037
|
+
return {
|
|
1038
|
+
removed: false,
|
|
1039
|
+
kept: false,
|
|
1040
|
+
reason: "worktree path already missing",
|
|
1041
|
+
path: worktree.worktreePath,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const result = finishAgentWorktree({
|
|
1046
|
+
...worktree,
|
|
1047
|
+
path: worktree.worktreePath,
|
|
1048
|
+
});
|
|
1049
|
+
if (!result.removed) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`Refusing to remove background agent record while its worktree is kept (${result.reason}). ` +
|
|
1052
|
+
"Resolve or preserve the work first, or pass --keep-worktree to remove only the record.",
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return { ...result, path: worktree.worktreePath };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
869
1058
|
export function openBackgroundLogFile(id) {
|
|
870
1059
|
const fd = openSync(logPath(id), "a", 0o600);
|
|
871
1060
|
return { fd, close: () => closeSync(fd) };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Source Ledger — M4-1: 上下文来源归因系统
|
|
3
|
+
* 对应文档 §2.5
|
|
4
|
+
*
|
|
5
|
+
* 统一追踪每段上下文的来源、工具调用、权限模式、置信度,
|
|
6
|
+
* 让"AI 为什么此刻知道这件事"完全可追溯
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} ContextEntry
|
|
13
|
+
* @property {string} entryId
|
|
14
|
+
* @property {string} sessionId
|
|
15
|
+
* @property {string} turnId
|
|
16
|
+
* @property {string} sourceType - slash_command|mcp|tool|hook|skill|agent|file|memory|prompt|plugin
|
|
17
|
+
* @property {string} sourceId - tool name, mcp server name, hook id, etc.
|
|
18
|
+
* @property {'system'|'developer'|'plugin'|'user'|'agent'} permissionMode
|
|
19
|
+
* @property {number} confidence - 0-1
|
|
20
|
+
* @property {string} [summary]
|
|
21
|
+
* @property {any} [metadata]
|
|
22
|
+
* @property {number} tokenCount
|
|
23
|
+
* @property {Date} insertedAt
|
|
24
|
+
* @property {string[]} tags
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
class ContextSourceLedger {
|
|
28
|
+
constructor() {
|
|
29
|
+
/** @type {Map<string, ContextEntry>} */
|
|
30
|
+
this._entries = new Map();
|
|
31
|
+
/** @type {Map<string, ContextEntry[]>} turnIndex: turnId -> entries */
|
|
32
|
+
this._byTurn = new Map();
|
|
33
|
+
/** @type {Map<string, ContextEntry[]>} sessionIndex: sessionId -> entries */
|
|
34
|
+
this._bySession = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Record a context source entry
|
|
39
|
+
* @param {Partial<ContextEntry>} entry
|
|
40
|
+
* @returns {string} entryId
|
|
41
|
+
*/
|
|
42
|
+
record(entry) {
|
|
43
|
+
const entryId = entry.entryId || crypto.randomUUID();
|
|
44
|
+
const full = {
|
|
45
|
+
entryId,
|
|
46
|
+
sessionId: entry.sessionId || "unknown",
|
|
47
|
+
turnId: entry.turnId || "unknown",
|
|
48
|
+
sourceType: entry.sourceType || "unknown",
|
|
49
|
+
sourceId: entry.sourceId || "unknown",
|
|
50
|
+
permissionMode: entry.permissionMode || "agent",
|
|
51
|
+
confidence: entry.confidence ?? 0.8,
|
|
52
|
+
summary: entry.summary,
|
|
53
|
+
metadata: entry.metadata,
|
|
54
|
+
tokenCount: entry.tokenCount || 0,
|
|
55
|
+
insertedAt: new Date(),
|
|
56
|
+
tags: entry.tags || [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this._entries.set(entryId, full);
|
|
60
|
+
|
|
61
|
+
// Index by turn
|
|
62
|
+
if (!this._byTurn.has(full.turnId)) this._byTurn.set(full.turnId, []);
|
|
63
|
+
this._byTurn.get(full.turnId).push(full);
|
|
64
|
+
|
|
65
|
+
// Index by session
|
|
66
|
+
if (!this._bySession.has(full.sessionId)) this._bySession.set(full.sessionId, []);
|
|
67
|
+
this._bySession.get(full.sessionId).push(full);
|
|
68
|
+
|
|
69
|
+
return entryId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all context entries for a given turn
|
|
74
|
+
* @param {string} turnId
|
|
75
|
+
* @returns {ContextEntry[]}
|
|
76
|
+
*/
|
|
77
|
+
getTurnContext(turnId) {
|
|
78
|
+
return this._byTurn.get(turnId) || [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all context entries for a session
|
|
83
|
+
* @param {string} sessionId
|
|
84
|
+
* @returns {ContextEntry[]}
|
|
85
|
+
*/
|
|
86
|
+
getSessionContext(sessionId) {
|
|
87
|
+
return this._bySession.get(sessionId) || [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate context provenance summary for a turn
|
|
92
|
+
* Shows exactly where each piece of context came from
|
|
93
|
+
* @param {string} turnId
|
|
94
|
+
* @returns {Object} provenance report
|
|
95
|
+
*/
|
|
96
|
+
getTurnProvenance(turnId) {
|
|
97
|
+
const entries = this.getTurnContext(turnId);
|
|
98
|
+
const byType = {};
|
|
99
|
+
const byPermission = {};
|
|
100
|
+
const bySource = {};
|
|
101
|
+
let totalTokens = 0;
|
|
102
|
+
let avgConfidence = 0;
|
|
103
|
+
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
byType[e.sourceType] = (byType[e.sourceType] || 0) + 1;
|
|
106
|
+
byPermission[e.permissionMode] = (byPermission[e.permissionMode] || 0) + 1;
|
|
107
|
+
bySource[e.sourceId] = (bySource[e.sourceId] || 0) + 1;
|
|
108
|
+
totalTokens += e.tokenCount;
|
|
109
|
+
avgConfidence += e.confidence;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
avgConfidence = entries.length > 0 ? avgConfidence / entries.length : 0;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
turnId,
|
|
116
|
+
totalEntries: entries.length,
|
|
117
|
+
totalTokens,
|
|
118
|
+
avgConfidence: Math.round(avgConfidence * 100) / 100,
|
|
119
|
+
byType,
|
|
120
|
+
byPermission,
|
|
121
|
+
bySource,
|
|
122
|
+
entries: entries.map(e => ({
|
|
123
|
+
sourceType: e.sourceType,
|
|
124
|
+
sourceId: e.sourceId,
|
|
125
|
+
permissionMode: e.permissionMode,
|
|
126
|
+
confidence: e.confidence,
|
|
127
|
+
tokenCount: e.tokenCount,
|
|
128
|
+
summary: e.summary,
|
|
129
|
+
tags: e.tags,
|
|
130
|
+
})),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clear entries for a session
|
|
136
|
+
* @param {string} sessionId
|
|
137
|
+
*/
|
|
138
|
+
clearSession(sessionId) {
|
|
139
|
+
const entries = this._bySession.get(sessionId) || [];
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
this._entries.delete(e.entryId);
|
|
142
|
+
const turnEntries = this._byTurn.get(e.turnId);
|
|
143
|
+
if (turnEntries) {
|
|
144
|
+
const idx = turnEntries.findIndex(x => x.entryId === e.entryId);
|
|
145
|
+
if (idx >= 0) turnEntries.splice(idx, 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this._bySession.delete(sessionId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Get stats */
|
|
152
|
+
getStats() {
|
|
153
|
+
return {
|
|
154
|
+
totalEntries: this._entries.size,
|
|
155
|
+
sessionsTracked: this._bySession.size,
|
|
156
|
+
turnsTracked: this._byTurn.size,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Singleton
|
|
162
|
+
const ledger = new ContextSourceLedger();
|
|
163
|
+
export default ledger;
|
|
164
|
+
export { ContextSourceLedger };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetricsCollector - M3 轻量级指标收集器
|
|
3
|
+
*
|
|
4
|
+
* 收集CLI运行时的性能指标:
|
|
5
|
+
* - 执行计数器
|
|
6
|
+
* - 延迟直方图
|
|
7
|
+
* - 标签化指标
|
|
8
|
+
* - 自动与traceId关联
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
import { traceContext } from "./trace-context.js";
|
|
14
|
+
|
|
15
|
+
class MetricsCollector {
|
|
16
|
+
constructor() {
|
|
17
|
+
this._counters = new Map();
|
|
18
|
+
this._histograms = new Map();
|
|
19
|
+
this._gauges = new Map();
|
|
20
|
+
this._flushedAt = Date.now();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Increment a counter metric
|
|
25
|
+
* @param {string} name - Metric name
|
|
26
|
+
* @param {number} value - Value to increment by (default: 1)
|
|
27
|
+
* @param {Object} labels - Key-value labels
|
|
28
|
+
*/
|
|
29
|
+
increment(name, value = 1, labels = {}) {
|
|
30
|
+
const key = this._metricKey(name, labels);
|
|
31
|
+
const current = this._counters.get(key) || 0;
|
|
32
|
+
this._counters.set(key, current + value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Record a value in a histogram (latency, sizes, etc.)
|
|
37
|
+
* @param {string} name - Metric name
|
|
38
|
+
* @param {number} value - Value to record
|
|
39
|
+
* @param {Object} labels - Key-value labels
|
|
40
|
+
*/
|
|
41
|
+
observe(name, value, labels = {}) {
|
|
42
|
+
const key = this._metricKey(name, labels);
|
|
43
|
+
const data = this._histograms.get(key) || {
|
|
44
|
+
count: 0,
|
|
45
|
+
sum: 0,
|
|
46
|
+
min: Infinity,
|
|
47
|
+
max: -Infinity,
|
|
48
|
+
values: [],
|
|
49
|
+
};
|
|
50
|
+
data.count += 1;
|
|
51
|
+
data.sum += value;
|
|
52
|
+
data.min = Math.min(data.min, value);
|
|
53
|
+
data.max = Math.max(data.max, value);
|
|
54
|
+
// Keep last 1000 values for percentile calculation
|
|
55
|
+
if (data.values.length > 1000) {
|
|
56
|
+
data.values.shift();
|
|
57
|
+
}
|
|
58
|
+
data.values.push(value);
|
|
59
|
+
this._histograms.set(key, data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set a gauge value
|
|
64
|
+
* @param {string} name - Metric name
|
|
65
|
+
* @param {number} value - Gauge value
|
|
66
|
+
* @param {Object} labels - Key-value labels
|
|
67
|
+
*/
|
|
68
|
+
setGauge(name, value, labels = {}) {
|
|
69
|
+
const key = this._metricKey(name, labels);
|
|
70
|
+
this._gauges.set(key, value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Time an async function and record latency
|
|
75
|
+
* @param {string} name - Metric name
|
|
76
|
+
* @param {Function} fn - Async function to time
|
|
77
|
+
* @param {Object} labels - Key-value labels
|
|
78
|
+
* @returns {Promise<*>} Function result
|
|
79
|
+
*/
|
|
80
|
+
async time(name, fn, labels = {}) {
|
|
81
|
+
const start = performance.now();
|
|
82
|
+
try {
|
|
83
|
+
const result = await fn();
|
|
84
|
+
this.observe(name, performance.now() - start, {
|
|
85
|
+
...labels,
|
|
86
|
+
status: "success",
|
|
87
|
+
});
|
|
88
|
+
this.increment(`${name}_total`, 1, { ...labels, status: "success" });
|
|
89
|
+
return result;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
this.observe(name, performance.now() - start, {
|
|
92
|
+
...labels,
|
|
93
|
+
status: "error",
|
|
94
|
+
});
|
|
95
|
+
this.increment(`${name}_total`, 1, {
|
|
96
|
+
...labels,
|
|
97
|
+
status: "error",
|
|
98
|
+
errorType: err.name,
|
|
99
|
+
});
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all collected metrics as OTLP-compatible format
|
|
106
|
+
* @returns {Object} OTLP metrics data
|
|
107
|
+
*/
|
|
108
|
+
collect() {
|
|
109
|
+
const metrics = [];
|
|
110
|
+
|
|
111
|
+
// Convert counters
|
|
112
|
+
for (const [key, value] of this._counters) {
|
|
113
|
+
const { name, labels } = this._parseKey(key);
|
|
114
|
+
metrics.push({
|
|
115
|
+
name,
|
|
116
|
+
type: "counter",
|
|
117
|
+
value,
|
|
118
|
+
labels,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Convert histograms
|
|
124
|
+
for (const [key, data] of this._histograms) {
|
|
125
|
+
const { name, labels } = this._parseKey(key);
|
|
126
|
+
data.values.sort((a, b) => a - b);
|
|
127
|
+
const p50 = data.values[Math.floor(data.values.length * 0.5)];
|
|
128
|
+
const p95 = data.values[Math.floor(data.values.length * 0.95)];
|
|
129
|
+
const p99 = data.values[Math.floor(data.values.length * 0.99)];
|
|
130
|
+
metrics.push({
|
|
131
|
+
name,
|
|
132
|
+
type: "histogram",
|
|
133
|
+
count: data.count,
|
|
134
|
+
sum: data.sum,
|
|
135
|
+
min: data.min,
|
|
136
|
+
max: data.max,
|
|
137
|
+
avg: data.sum / data.count,
|
|
138
|
+
p50,
|
|
139
|
+
p95,
|
|
140
|
+
p99,
|
|
141
|
+
labels,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Convert gauges
|
|
147
|
+
for (const [key, value] of this._gauges) {
|
|
148
|
+
const { name, labels } = this._parseKey(key);
|
|
149
|
+
metrics.push({
|
|
150
|
+
name,
|
|
151
|
+
type: "gauge",
|
|
152
|
+
value,
|
|
153
|
+
labels,
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
resourceMetrics: [
|
|
160
|
+
{
|
|
161
|
+
resource: {
|
|
162
|
+
attributes: [
|
|
163
|
+
{
|
|
164
|
+
key: "service.name",
|
|
165
|
+
value: { stringValue: "chainlesschain-cli" },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: "service.version",
|
|
169
|
+
value: { stringValue: "0.10.0-alpha.74" },
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
scopeMetrics: [
|
|
174
|
+
{
|
|
175
|
+
scope: { name: "chainlesschain.cli" },
|
|
176
|
+
metrics,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Reset all metrics
|
|
186
|
+
*/
|
|
187
|
+
reset() {
|
|
188
|
+
this._counters.clear();
|
|
189
|
+
this._histograms.clear();
|
|
190
|
+
this._gauges.clear();
|
|
191
|
+
this._flushedAt = Date.now();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_metricKey(name, labels) {
|
|
195
|
+
const labelStr = Object.entries(labels)
|
|
196
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
197
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
198
|
+
.join(",");
|
|
199
|
+
return labelStr ? `${name}{${labelStr}}` : name;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_parseKey(key) {
|
|
203
|
+
const match = key.match(/^([^{]+)(?:\{([^}]+)\})?$/);
|
|
204
|
+
if (!match) return { name: key, labels: {} };
|
|
205
|
+
const [, name, labelStr] = match;
|
|
206
|
+
if (!labelStr) return { name, labels: {} };
|
|
207
|
+
const labels = {};
|
|
208
|
+
for (const pair of labelStr.split(",")) {
|
|
209
|
+
const [k, v] = pair.split("=");
|
|
210
|
+
if (k) labels[k] = v;
|
|
211
|
+
}
|
|
212
|
+
return { name, labels };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { MetricsCollector };
|
|
217
|
+
export const metricsCollector = new MetricsCollector();
|
|
218
|
+
export default metricsCollector;
|