chainlesschain 0.162.158 → 0.162.159
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 +1 -1
- package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-ByIiF5bT.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-NlgSFZq5.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-D17cOUcb.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-B4mEZcbl.js} +4 -4
- package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
- package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DYAia2e2.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-C1lvYUSu.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-B6agK6bA.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DM43R2vy.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-Dtdp3eXp.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BAV9V-t-.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-D2xf7uZt.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-D3j45jJL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-voPJ9fUZ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DEsyTESF.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-OHV_QXLD.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-eDTJWC-J.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BBvy-6d1.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-Dw2eoJxu.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DQHms_Bh.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-Cyu7Id1c.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-sIDzkG92.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-DUW3T2ey.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-CECELXW8.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-BEZIQX84.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-BcdUMznL.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DxxEdn2r.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
- package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-63LhjBx2.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-CYtQtIcH.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-BwR6UUox.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-C4q5h7Ut.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-DexLlXfN.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-Plksliuw.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-DZUFfeuG.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-NoDYt29V.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-CEXGTkdh.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CVtTqgtg.js → MobileProjects-DnbJkbZo.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-DTAa0cwy.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-CVV7LnNS.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-BqqGrjt_.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-C4WP8DRp.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Zo4DJUVI.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-DBcjRaz2.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
- package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-BT9WPBOX.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-D6b4bZ3q.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-Cw3OsYx-.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C0nyiK2b.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-zVBZEB_C.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-BOspsFew.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-CQWfZ4XH.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-C2oGJ_WL.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-BZMR4qY1.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-DAujoP63.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-COATmHxF.js → Projects-DvH_P2Uq.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-D_RGHM-v.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Mf6KfL2J.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-5D8URoI_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-Df4kC8Gy.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-DGi3f3vO.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-jmkGFNWb.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-DP6Pcu3m.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-D60hSuPO.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-0mf1BQUK.js} +1 -1
- package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-CfZtLFYV.js} +4 -4
- package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D9zpZLWz.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-D1_44eMD.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-cVFa8zkz.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-Cinpzc3L.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-bqdiJE_P.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-Dgl_SLgj.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-DawWAEhy.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-Ca0nY651.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-CgaGEhLm.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-D-9EnpRx.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-BpwLZpL3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DgowwBK5.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-BZutapjE.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-QZPt8_Sp.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-DlQx5UDJ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-ZfdTgtWA.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-CAp0y_Cr.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-BVITZEmr.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-CC2tPaNg.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-31brc9CA.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-DLMkC9C-.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-BMtveI6F.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DbD6WHW2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B30NvR21.js → index-3oBsXZmF.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-ALvoNTRF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-AaIKG60c.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-BFRLmCR3.js} +1 -1
- package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-BKAB6-6V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BOM-VFlv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-BPpS-lGb.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-Bbc2JeQG.js} +1 -1
- package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
- package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-BiRQUoyE.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-BnFLzejT.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-BoO4WEJB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-C-cGlCDW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C1PfeI88.js} +1 -1
- package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-C21pdBqs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-C7LHNw_N.js} +1 -1
- package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-CEXa-4zd.js} +1 -1
- package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CEwjBA9q.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-CFXKbOek.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-CJhREOlG.js} +1 -1
- package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-COHHQ9cU.js} +1 -1
- package/src/assets/web-panel/assets/{index-D23L1V76.js → index-CUmLMFsy.js} +1 -1
- package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-CWgfipcC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-C_eUKGYP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-CcVszn2b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-Cod97Urs.js} +3 -3
- package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-Cwbqh0KZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-D67kIagl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-D7cV0tu0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-DJvxFyIz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-DZl-UXB6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-Dm0byjm1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-DndNnYc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-WQUO2vvK.js} +1 -1
- package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
- package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-bdaJwTXJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-ep6JchNG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCyP071m.js → index-jG6h22Qg.js} +1 -1
- package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-ntVQqSML.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-D5UoBRhN.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-DltOw0CR.js} +1 -1
- package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-_oPkk5zj.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-EES8ROCS.js} +1 -1
- package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-B-_39x_M.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-l6D0yJHz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-DIeUK31L.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-dG4EzUQo.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-DWFzYGdH.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-CECdTA5H.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-D6kP-mMK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-BWfna_rd.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-Bb-j6rzo.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-DNpwzFTM.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-vNLpFQBW.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +17 -3
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +143 -3
- package/src/commands/browse.js +86 -0
- package/src/commands/cloud.js +232 -0
- package/src/commands/doctor.js +61 -6
- package/src/commands/mcp.js +61 -0
- package/src/commands/plugin.js +63 -1
- package/src/commands/routine.js +367 -0
- package/src/commands/session.js +326 -0
- package/src/data/changelog.json +7 -2
- package/src/harness/jsonl-session-store.js +82 -2
- package/src/harness/session-index.js +295 -0
- package/src/harness/session-mirror.js +142 -0
- package/src/harness/transcript-integrity.js +182 -0
- package/src/index.js +8 -0
- package/src/lib/background-agent-supervisor.js +126 -2
- package/src/lib/channels/channel-manager.js +126 -0
- package/src/lib/channels/telegram-channel.js +112 -0
- package/src/lib/channels/webhook-channel.js +140 -0
- package/src/lib/chrome-connector.js +386 -0
- package/src/lib/cloud/bundle.js +110 -0
- package/src/lib/cloud/cloud-client.js +152 -0
- package/src/lib/computer-use/authorization.js +173 -0
- package/src/lib/computer-use/computer-use-server.js +276 -0
- package/src/lib/computer-use/control-backend.js +150 -0
- package/src/lib/doctor-checkup.js +652 -0
- package/src/lib/jsonl-session-store.js +4 -0
- package/src/lib/lsp/lsp-server-registry.js +48 -21
- package/src/lib/plugin-runtime/governance.js +174 -0
- package/src/lib/pr-link-ledger.js +221 -0
- package/src/lib/routine-store.js +352 -0
- package/src/lib/session-store-guard.js +40 -0
- package/src/lib/session-usage.js +318 -2
- package/src/lib/sub-agent-context.js +23 -0
- package/src/repl/agent-repl.js +361 -3
- package/src/repl/bg-dashboard.js +355 -0
- package/src/repl/fast-mode.js +103 -0
- package/src/repl/fullscreen-tui.js +197 -0
- package/src/repl/slash-command-registry.js +11 -3
- package/src/repl/voice-dictation.js +132 -0
- package/src/runtime/agent-core.js +242 -1
- package/src/runtime/coding-agent-contract-shared.cjs +88 -0
- package/src/runtime/coding-agent-policy.cjs +17 -0
- package/src/runtime/headless-runner.js +71 -7
- package/src/runtime/headless-stream.js +14 -8
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +0 -1
- package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
- package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
- package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
readdirSync,
|
|
9
9
|
renameSync,
|
|
10
10
|
rmSync,
|
|
11
|
+
statSync,
|
|
11
12
|
writeFileSync,
|
|
12
13
|
} from "node:fs";
|
|
13
14
|
import { randomBytes } from "node:crypto";
|
|
@@ -61,12 +62,57 @@ export function logPath(id) {
|
|
|
61
62
|
return join(backgroundAgentsDir(), `${safeId(id)}.log`);
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
|
|
66
|
+
|
|
64
67
|
export function writeBackgroundAgentState(state) {
|
|
65
68
|
const target = statePath(state.id);
|
|
69
|
+
// Field-aware merge against the freshest on-disk state. The state file has
|
|
70
|
+
// multiple concurrent read-modify-write writers (launcher, worker heartbeat/
|
|
71
|
+
// turn/finalize, rename/pin/stop from other processes) with last-writer-wins
|
|
72
|
+
// semantics — a writer holding a stale snapshot used to clobber a terminal
|
|
73
|
+
// status back to "running" (phantom session, real exit code lost) or roll a
|
|
74
|
+
// fresh rename back to the old title. Two invariants restore convergence:
|
|
75
|
+
// 1. a terminal status wins over a racing "running" snapshot — there is no
|
|
76
|
+
// legitimate same-id terminal→running transition (resume mints a NEW
|
|
77
|
+
// id, and the worker's own writeHeartbeat already refuses to resurrect);
|
|
78
|
+
// 2. the newest rename/pin (by renamedAt/pinnedAt) wins regardless of
|
|
79
|
+
// which writer's snapshot carries it.
|
|
80
|
+
// The read happens immediately before the atomic rename, shrinking the
|
|
81
|
+
// clobber window from "any caller's RMW span" to microseconds; rename/pin
|
|
82
|
+
// additionally verify-and-retry on top of this.
|
|
83
|
+
let next = state;
|
|
84
|
+
const current = readBackgroundAgentState(state.id);
|
|
85
|
+
if (current) {
|
|
86
|
+
if (TERMINAL_STATUSES.has(current.status) && next.status === "running") {
|
|
87
|
+
next = {
|
|
88
|
+
...next,
|
|
89
|
+
status: current.status,
|
|
90
|
+
endedAt: current.endedAt ?? next.endedAt ?? null,
|
|
91
|
+
exitCode: current.exitCode ?? next.exitCode ?? null,
|
|
92
|
+
...(current.signal !== undefined ? { signal: current.signal } : {}),
|
|
93
|
+
...(current.error !== undefined ? { error: current.error } : {}),
|
|
94
|
+
...(current.lostReason !== undefined
|
|
95
|
+
? { lostReason: current.lostReason }
|
|
96
|
+
: {}),
|
|
97
|
+
...(current.stoppedByUser !== undefined
|
|
98
|
+
? { stoppedByUser: current.stoppedByUser }
|
|
99
|
+
: {}),
|
|
100
|
+
// terminal sessions have no live phase or transport endpoint
|
|
101
|
+
phase: null,
|
|
102
|
+
transport: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (Number(current.renamedAt || 0) > Number(next.renamedAt || 0)) {
|
|
106
|
+
next = { ...next, title: current.title, renamedAt: current.renamedAt };
|
|
107
|
+
}
|
|
108
|
+
if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
|
|
109
|
+
next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
66
112
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
67
|
-
writeFileSync(tmp, JSON.stringify(
|
|
113
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
68
114
|
renameSync(tmp, target);
|
|
69
|
-
return
|
|
115
|
+
return next;
|
|
70
116
|
}
|
|
71
117
|
|
|
72
118
|
export function readBackgroundAgentState(id) {
|
|
@@ -177,6 +223,34 @@ export function renameBackgroundAgent(id, title, options = {}) {
|
|
|
177
223
|
return next;
|
|
178
224
|
}
|
|
179
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Pin/unpin a session for the dashboard (`cc daemon view`) — pinned sessions
|
|
228
|
+
* sort first inside their group. Same read-modify-write + verify-retry dance
|
|
229
|
+
* as rename: the worker's periodic state merges can clobber a write that
|
|
230
|
+
* lands inside its read→write window.
|
|
231
|
+
*/
|
|
232
|
+
export function setBackgroundAgentPinned(id, pinned, options = {}) {
|
|
233
|
+
const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
234
|
+
now: options.now,
|
|
235
|
+
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
236
|
+
});
|
|
237
|
+
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
238
|
+
const value = pinned === true;
|
|
239
|
+
const pinnedAt = typeof options.now === "number" ? options.now : Date.now();
|
|
240
|
+
let next = { ...state, pinned: value, pinnedAt };
|
|
241
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
242
|
+
writeBackgroundAgentState(next);
|
|
243
|
+
sleepSyncMs(15);
|
|
244
|
+
const current = readBackgroundAgentState(id) || next;
|
|
245
|
+
if (current.pinned === value) {
|
|
246
|
+
return { ...current, pinned: value, pinnedAt };
|
|
247
|
+
}
|
|
248
|
+
next = { ...current, pinned: value, pinnedAt };
|
|
249
|
+
}
|
|
250
|
+
writeBackgroundAgentState(next);
|
|
251
|
+
return next;
|
|
252
|
+
}
|
|
253
|
+
|
|
180
254
|
/**
|
|
181
255
|
* Build the follow-up argv template for interactive attach turns: the launch
|
|
182
256
|
* argv minus every token that carried the FIRST turn's prompt (positional
|
|
@@ -258,6 +332,33 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
|
|
|
258
332
|
});
|
|
259
333
|
}
|
|
260
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Fail fast on an unusable cwd. Without this, spawn() surfaces a bad cwd as
|
|
337
|
+
* an ASYNC 'error' event on the detached child — which nothing listened to
|
|
338
|
+
* (uncaught exception with no context) — and the pre-written state/job files
|
|
339
|
+
* stayed behind as a phantom "running" session. Deleted, file-replaced, and
|
|
340
|
+
* unmounted paths all land here with one clear message.
|
|
341
|
+
*/
|
|
342
|
+
function assertUsableCwd(cwd) {
|
|
343
|
+
let reason = null;
|
|
344
|
+
if (!cwd) {
|
|
345
|
+
reason = "no working directory given";
|
|
346
|
+
} else if (!existsSync(cwd)) {
|
|
347
|
+
reason = "directory does not exist (deleted or unmounted?)";
|
|
348
|
+
} else {
|
|
349
|
+
try {
|
|
350
|
+
if (!statSync(cwd).isDirectory()) reason = "path is not a directory";
|
|
351
|
+
} catch (err) {
|
|
352
|
+
reason = `directory is not accessible (${err.code || err.message})`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (reason) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Cannot launch background agent: cwd "${cwd ?? ""}" — ${reason}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
261
362
|
export function launchBackgroundAgent({
|
|
262
363
|
argv,
|
|
263
364
|
cwd,
|
|
@@ -266,6 +367,7 @@ export function launchBackgroundAgent({
|
|
|
266
367
|
cliEntry,
|
|
267
368
|
followUpArgv,
|
|
268
369
|
}) {
|
|
370
|
+
assertUsableCwd(cwd);
|
|
269
371
|
const id = createBackgroundAgentId();
|
|
270
372
|
const dir = backgroundAgentsDir();
|
|
271
373
|
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
@@ -317,6 +419,28 @@ export function launchBackgroundAgent({
|
|
|
317
419
|
rmSync(statePath(id), { force: true });
|
|
318
420
|
throw error;
|
|
319
421
|
}
|
|
422
|
+
// Async spawn failures (EPERM, cwd raced away between the check and the
|
|
423
|
+
// spawn, …) arrive as an 'error' event on the detached child. Reap them
|
|
424
|
+
// into the state file instead of leaving a phantom "running" session and
|
|
425
|
+
// an uncaught exception.
|
|
426
|
+
if (typeof child.on === "function") {
|
|
427
|
+
child.on("error", (error) => {
|
|
428
|
+
try {
|
|
429
|
+
rmSync(jobFile, { force: true });
|
|
430
|
+
const current = readBackgroundAgentState(id);
|
|
431
|
+
if (current && current.status === "running") {
|
|
432
|
+
writeBackgroundAgentState({
|
|
433
|
+
...current,
|
|
434
|
+
status: "failed",
|
|
435
|
+
endedAt: Date.now(),
|
|
436
|
+
lostReason: `spawn-error: ${error.code || error.message}`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
/* best-effort */
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
320
444
|
child.unref();
|
|
321
445
|
const current = readBackgroundAgentState(id) || state;
|
|
322
446
|
const withPid = {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound channels — external events entering a running agent session
|
|
3
|
+
* (gap-2026-07-11 P0#5; Claude-Code "channels" parity, self-hosted).
|
|
4
|
+
*
|
|
5
|
+
* `cc agent --channels webhook,telegram` starts channel listeners whose
|
|
6
|
+
* events become user turns in the interactive session (queued behind any
|
|
7
|
+
* in-flight turn by the REPL's pending-line queue). Every channel enforces a
|
|
8
|
+
* trust boundary: webhook = bearer token + loopback-only by default;
|
|
9
|
+
* telegram = explicit chat-id allowlist (deny-all when unset).
|
|
10
|
+
*
|
|
11
|
+
* Outbound replies reuse the existing notifier stack (`notify` agent tool /
|
|
12
|
+
* NotificationManager) — the inbound event's metadata tells the model which
|
|
13
|
+
* channel and sender to address.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { logger } from "../logger.js";
|
|
17
|
+
|
|
18
|
+
export const CHANNEL_KINDS = ["webhook", "telegram"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse `--channels` input: "webhook,telegram", "webhook:18901", or an array.
|
|
22
|
+
* @returns {Array<{kind:string, arg:string|null}>}
|
|
23
|
+
*/
|
|
24
|
+
export function parseChannelSpecs(input) {
|
|
25
|
+
const items = Array.isArray(input)
|
|
26
|
+
? input
|
|
27
|
+
: String(input || "")
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((s) => s.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
return items.map((item) => {
|
|
32
|
+
const idx = item.indexOf(":");
|
|
33
|
+
const kind = (idx === -1 ? item : item.slice(0, idx)).toLowerCase();
|
|
34
|
+
const arg = idx === -1 ? null : item.slice(idx + 1);
|
|
35
|
+
if (!CHANNEL_KINDS.includes(kind)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Unknown channel "${kind}" — supported: ${CHANNEL_KINDS.join(", ")}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return { kind, arg };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render a channel event as the user-turn text. The channel/sender prefix
|
|
46
|
+
* keeps provenance visible to the model AND guarantees the line can never be
|
|
47
|
+
* mistaken for a REPL slash command or `!` shell escape, no matter what the
|
|
48
|
+
* external sender wrote.
|
|
49
|
+
*/
|
|
50
|
+
export function formatChannelEvent(event) {
|
|
51
|
+
const channel = event?.channel || "channel";
|
|
52
|
+
const sender = event?.sender ? ` from ${event.sender}` : "";
|
|
53
|
+
const text = String(event?.text ?? "").trim();
|
|
54
|
+
return `[channel:${channel}${sender}] ${text}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start every requested channel. Returns a handle with the started channel
|
|
59
|
+
* descriptions and a single stop().
|
|
60
|
+
*
|
|
61
|
+
* @param {string|string[]} specsInput --channels value
|
|
62
|
+
* @param {object} opts
|
|
63
|
+
* @param {(event) => void} opts.onEvent called for each inbound event
|
|
64
|
+
* @param {object} [opts.config] loadConfig().channels subtree
|
|
65
|
+
* @param {object} [opts.deps] channel starters (tests inject)
|
|
66
|
+
*/
|
|
67
|
+
export async function startChannels(specsInput, opts = {}) {
|
|
68
|
+
const specs = parseChannelSpecs(specsInput);
|
|
69
|
+
if (specs.length === 0) {
|
|
70
|
+
throw new Error("--channels given but no channel names parsed");
|
|
71
|
+
}
|
|
72
|
+
const config = opts.config || {};
|
|
73
|
+
const log = opts.log || ((msg) => logger.info(msg));
|
|
74
|
+
const deps = opts.deps || {};
|
|
75
|
+
|
|
76
|
+
const started = [];
|
|
77
|
+
const stopAll = () => {
|
|
78
|
+
for (const c of started) {
|
|
79
|
+
try {
|
|
80
|
+
c.stop?.();
|
|
81
|
+
} catch {
|
|
82
|
+
/* best-effort */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
for (const spec of specs) {
|
|
89
|
+
if (spec.kind === "webhook") {
|
|
90
|
+
const { startWebhookChannel } =
|
|
91
|
+
deps.webhook || (await import("./webhook-channel.js"));
|
|
92
|
+
const cfg = config.webhook || {};
|
|
93
|
+
const handle = await (
|
|
94
|
+
deps.webhook?.startWebhookChannel || startWebhookChannel
|
|
95
|
+
)({
|
|
96
|
+
...cfg,
|
|
97
|
+
port: spec.arg ? Number(spec.arg) : cfg.port,
|
|
98
|
+
onEvent: opts.onEvent,
|
|
99
|
+
log,
|
|
100
|
+
});
|
|
101
|
+
started.push({ kind: "webhook", describe: handle.describe, ...handle });
|
|
102
|
+
} else if (spec.kind === "telegram") {
|
|
103
|
+
const { startTelegramChannel } =
|
|
104
|
+
deps.telegram || (await import("./telegram-channel.js"));
|
|
105
|
+
const cfg = config.telegram || {};
|
|
106
|
+
const handle = await (
|
|
107
|
+
deps.telegram?.startTelegramChannel || startTelegramChannel
|
|
108
|
+
)({
|
|
109
|
+
...cfg,
|
|
110
|
+
onEvent: opts.onEvent,
|
|
111
|
+
log,
|
|
112
|
+
});
|
|
113
|
+
started.push({
|
|
114
|
+
kind: "telegram",
|
|
115
|
+
describe: handle.describe,
|
|
116
|
+
...handle,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
stopAll(); // never leave half the channels listening after a failure
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { channels: started, stop: stopAll };
|
|
126
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram inbound channel — long-polls the Bot API (`getUpdates`) so no
|
|
3
|
+
* public endpoint is needed, and turns allowed chats' messages into session
|
|
4
|
+
* events.
|
|
5
|
+
*
|
|
6
|
+
* Trust boundary: `allowFrom` (array of chat ids) is REQUIRED — an inbound
|
|
7
|
+
* channel with no allowlist would let anyone who finds the bot drive the
|
|
8
|
+
* agent, so an empty/missing allowlist denies every update (fail closed).
|
|
9
|
+
*
|
|
10
|
+
* Replies: `handle.reply(event, text)` posts a sendMessage back to the
|
|
11
|
+
* event's chat. fetch is injectable for tests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const POLL_TIMEOUT_S = 25;
|
|
15
|
+
const ERROR_BACKOFF_MS = 5000;
|
|
16
|
+
|
|
17
|
+
export async function startTelegramChannel(options = {}) {
|
|
18
|
+
const {
|
|
19
|
+
botToken = process.env.TELEGRAM_BOT_TOKEN,
|
|
20
|
+
allowFrom,
|
|
21
|
+
onEvent,
|
|
22
|
+
log = () => {},
|
|
23
|
+
fetchImpl = globalThis.fetch,
|
|
24
|
+
pollTimeoutS = POLL_TIMEOUT_S,
|
|
25
|
+
// Real macrotask pause between polls. The server-side long-poll dominates
|
|
26
|
+
// in production; this exists so a fast-resolving fetch (tests, HTTP-level
|
|
27
|
+
// failures) can never turn the loop into a microtask chain that starves
|
|
28
|
+
// the event loop (timers, stop()) entirely.
|
|
29
|
+
pollPauseMs = 50,
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
if (!botToken) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"telegram channel needs a bot token (channels.telegram.botToken or TELEGRAM_BOT_TOKEN)",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const allowed = new Set((allowFrom || []).map((id) => String(id)));
|
|
38
|
+
if (allowed.size === 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"telegram channel needs channels.telegram.allowFrom (chat-id allowlist) — refusing to listen to everyone",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const api = (method) => `https://api.telegram.org/bot${botToken}/${method}`;
|
|
45
|
+
let stopped = false;
|
|
46
|
+
let offset = 0;
|
|
47
|
+
|
|
48
|
+
async function poll() {
|
|
49
|
+
while (!stopped) {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetchImpl(api("getUpdates"), {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "Content-Type": "application/json" },
|
|
54
|
+
body: JSON.stringify({ timeout: pollTimeoutS, offset }),
|
|
55
|
+
});
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
if (!data?.ok || !Array.isArray(data.result)) {
|
|
58
|
+
throw new Error(data?.description || `getUpdates HTTP ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
for (const update of data.result) {
|
|
61
|
+
offset = Math.max(offset, Number(update.update_id) + 1);
|
|
62
|
+
const msg = update.message || update.edited_message;
|
|
63
|
+
const chatId = msg?.chat?.id;
|
|
64
|
+
const text = typeof msg?.text === "string" ? msg.text.trim() : "";
|
|
65
|
+
if (!chatId || !text) continue;
|
|
66
|
+
if (!allowed.has(String(chatId))) continue; // fail closed
|
|
67
|
+
try {
|
|
68
|
+
onEvent?.({
|
|
69
|
+
channel: "telegram",
|
|
70
|
+
sender: String(chatId),
|
|
71
|
+
text,
|
|
72
|
+
meta: { updateId: update.update_id },
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
/* downstream owns its failures */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (stopped) return;
|
|
80
|
+
log(`channel telegram poll error: ${err.message} — backing off`);
|
|
81
|
+
await new Promise((r) => setTimeout(r, ERROR_BACKOFF_MS));
|
|
82
|
+
}
|
|
83
|
+
if (!stopped) {
|
|
84
|
+
await new Promise((r) => setTimeout(r, pollPauseMs));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const pollPromise = poll();
|
|
89
|
+
|
|
90
|
+
log(`channel telegram polling (allowlist: ${[...allowed].join(", ")})`);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
kind: "telegram",
|
|
94
|
+
describe: `telegram (${allowed.size} allowed chat${allowed.size === 1 ? "" : "s"})`,
|
|
95
|
+
async reply(event, text) {
|
|
96
|
+
const res = await fetchImpl(api("sendMessage"), {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify({ chat_id: event.sender, text }),
|
|
100
|
+
});
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
if (!data?.ok) {
|
|
103
|
+
throw new Error(data?.description || `sendMessage HTTP ${res.status}`);
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
stop: () => {
|
|
108
|
+
stopped = true;
|
|
109
|
+
return pollPromise.catch(() => {});
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook inbound channel — a tiny loopback HTTP listener that turns
|
|
3
|
+
* authenticated POSTs into session events.
|
|
4
|
+
*
|
|
5
|
+
* POST /v1/event
|
|
6
|
+
* Authorization: Bearer <token>
|
|
7
|
+
* { "text": "deploy finished", "sender": "ci", "meta": {...} }
|
|
8
|
+
*
|
|
9
|
+
* Trust boundary:
|
|
10
|
+
* - bearer token REQUIRED on every request; without a configured token a
|
|
11
|
+
* random one is generated and printed once at startup (pairing secret).
|
|
12
|
+
* - binds 127.0.0.1 unless `allowExternal: true` is explicitly configured
|
|
13
|
+
* (same LAN/loopback posture as remote-control direct pairing).
|
|
14
|
+
* - optional `allowlist` of sender ids — configured ⇒ only those senders.
|
|
15
|
+
* - body capped at 64 KB; JSON only.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createServer } from "node:http";
|
|
19
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
20
|
+
|
|
21
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
22
|
+
|
|
23
|
+
function tokenMatches(expected, got) {
|
|
24
|
+
const a = Buffer.from(String(expected));
|
|
25
|
+
const b = Buffer.from(String(got || ""));
|
|
26
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function startWebhookChannel(options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
port = 18901,
|
|
32
|
+
host = "127.0.0.1",
|
|
33
|
+
allowExternal = false,
|
|
34
|
+
allowlist,
|
|
35
|
+
onEvent,
|
|
36
|
+
log = () => {},
|
|
37
|
+
} = options;
|
|
38
|
+
|
|
39
|
+
const isLoopback =
|
|
40
|
+
host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
41
|
+
if (!isLoopback && allowExternal !== true) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`webhook channel refuses to bind non-loopback host "${host}" without channels.webhook.allowExternal=true`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const token = options.token || randomBytes(24).toString("base64url");
|
|
48
|
+
const generated = !options.token;
|
|
49
|
+
|
|
50
|
+
const server = createServer((req, res) => {
|
|
51
|
+
const respond = (status, body) => {
|
|
52
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
53
|
+
res.end(JSON.stringify(body));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (req.method !== "POST" || req.url !== "/v1/event") {
|
|
57
|
+
return respond(404, { error: "not found" });
|
|
58
|
+
}
|
|
59
|
+
const auth = String(req.headers.authorization || "");
|
|
60
|
+
const got = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
|
61
|
+
if (!tokenMatches(token, got)) {
|
|
62
|
+
return respond(401, { error: "unauthorized" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let size = 0;
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let aborted = false;
|
|
68
|
+
req.on("data", (chunk) => {
|
|
69
|
+
size += chunk.length;
|
|
70
|
+
if (size > MAX_BODY_BYTES) {
|
|
71
|
+
aborted = true;
|
|
72
|
+
respond(413, { error: "body too large" });
|
|
73
|
+
req.destroy();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
chunks.push(chunk);
|
|
77
|
+
});
|
|
78
|
+
req.on("end", () => {
|
|
79
|
+
if (aborted) return;
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
payload = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
83
|
+
} catch {
|
|
84
|
+
return respond(400, { error: "invalid JSON" });
|
|
85
|
+
}
|
|
86
|
+
const text = typeof payload?.text === "string" ? payload.text.trim() : "";
|
|
87
|
+
if (!text) return respond(400, { error: "missing text" });
|
|
88
|
+
const sender =
|
|
89
|
+
typeof payload.sender === "string" ? payload.sender : "webhook";
|
|
90
|
+
if (Array.isArray(allowlist) && allowlist.length > 0) {
|
|
91
|
+
if (!allowlist.includes(sender)) {
|
|
92
|
+
return respond(403, { error: "sender not in allowlist" });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
onEvent?.({
|
|
97
|
+
channel: "webhook",
|
|
98
|
+
sender,
|
|
99
|
+
text,
|
|
100
|
+
meta:
|
|
101
|
+
payload.meta && typeof payload.meta === "object"
|
|
102
|
+
? payload.meta
|
|
103
|
+
: undefined,
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
/* the session queue owns downstream failures */
|
|
107
|
+
}
|
|
108
|
+
respond(202, { ok: true });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await new Promise((resolve, reject) => {
|
|
113
|
+
server.once("error", reject);
|
|
114
|
+
server.listen(port, host, resolve);
|
|
115
|
+
});
|
|
116
|
+
server.unref?.();
|
|
117
|
+
|
|
118
|
+
const actualPort = server.address()?.port ?? port;
|
|
119
|
+
const url = `http://${host}:${actualPort}/v1/event`;
|
|
120
|
+
log(
|
|
121
|
+
`channel webhook listening on ${url}` +
|
|
122
|
+
(generated ? ` (generated token: ${token})` : ""),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
kind: "webhook",
|
|
127
|
+
url,
|
|
128
|
+
port: actualPort,
|
|
129
|
+
token,
|
|
130
|
+
describe: `webhook ${url}`,
|
|
131
|
+
stop: () =>
|
|
132
|
+
new Promise((resolve) => {
|
|
133
|
+
try {
|
|
134
|
+
server.close(() => resolve());
|
|
135
|
+
} catch {
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
}
|