chainlesschain 0.165.2 → 0.165.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -29
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-lS1R0Cza.js → AIOps-C__nnraW.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BmlgBPsj.js → ActionButton-CsKWHEZ-.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CNKczv1S.js → Analytics-BhIjfjWO.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BuZGJARQ.js → AppLayout-BjuXFzWZ.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-Dm73Sl-O.js → Artifacts-k6whDkZJ.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BGMW7A5e.js → Audit-C4FBMYRh.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BQ6M54JR.js → BackgroundAgents-C_vRrSXk.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DBzGP0bR.js → Backup-CngFZeVp.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D1mLCoOV.js → BaseInput-B_H9EBRm.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BUfcN7p8.js → Chat-DLEC5kt7.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-xijcWkJe.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-Du0Yg5Pi.js → Checkbox-m3ClR4xh.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B2RXziHW.js → Codegen-CmLSFrgp.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DWhekOrR.js → Col-DFGQteHN.js} +1 -1
- package/src/assets/web-panel/assets/{Community-HQhrkon4.js → Community-EUUVORHE.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-izHNb4uz.js → Compact-2PSg0Yr7.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BDwLy41-.js → Compliance-CcKur7gB.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DFJhLa7K.js → Cowork-DK_Fezfa.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DYY9RG7-.js → Cron-Dbw89xl7.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-rr7tJTtv.js → Crosschain-CH66h_Ec.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D2sfHRLi.js → DID-D1igRb17.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-q_eGdhha.js → Dashboard-DVAsrobk.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Cufrh2Bz.js → Dropdown-Dl7W8AQs.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-B-kdv10f.js → EmailListRenderer-D5tpaEQC.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D21XjgfG.js → FamilyGuardDashboard-O_VQf8gq.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CAwRTLZH.js → Federation-BTVcgS9n.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-vx3daNDF.js → FormItemContext-BRnkJAIz.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CRWY2wU4.js → GenericCardRenderer-8OWSCkDj.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Q6SwauyD.js → Git-WXLYTQj2.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BJ1q4IQZ.js → Governance-BezaUhtc.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CSWjROgD.js → Inference-CuRay3Gu.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-fzuQsWuY.js → KnowledgeGraph-DfWLDW1m.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DVF8Mnns.js → Logs-C55y2pPY.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-hDpWdiBc.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BQP7LOA4.js → Marketplace-Dk08Uycg.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BNlJCmNc.js → McpTools-C11fSB5V.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-DLLg5WQ4.js → Memory-mr0eZc97.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CR2skNAF.js → MobileBridge-C0iCSRbb.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BxVavObl.js → MobileProjects-CY-xMG8Z.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-B17pgqMI.js → Mtc-Bplqc_Wc.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-4UmV5cIL.js → MtcAudit-BlzwsoSM.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DoMEXEcU.js → Multisig-BBLmTty6.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-h9DMXFX4.js → NLProgramming-DtT1uf1W.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Cihn-xX8.js → Notes-DcqQAxTK.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BE6ZGa3R.js → NotificationSettings-cb0kWoZe.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CnwojoSI.js → OrderTableRenderer-kw9HZQNN.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B2vpwmjb.js → Organization-DlTlbkUN.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C4z28R0-.js → Overflow-pp1qs-oY.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BVlFkwhW.js → P2P-BESLnyYz.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BXbEdL5B.js → PdhVaultBrowser-B-J1mGuf.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C28xs4lp.js → Permissions-B36PeT1_.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Dgl_rBjM.js → PersonalDataHub-CTK2ad-3.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-3QUIhrKI.js → Pipeline-zJNGXsac.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-8Gh33OZ5.js → Privacy-b9fz0Qf0.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BNmg4tZi.js → ProjectInit-DjZbZgiB.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-vgMfeCf9.js → ProjectSettings-C-hHJXlD.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BC9KSDM6.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CXoOa8eX.js → Providers-Dz-CAYYU.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BhfTF0oS.js → QrScannerModal-A0yoQ3-3.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-C746HEAj.js → QuickAsk-DrFgFJNC.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BSej8Eo0.js → Recommend-KC8hF1g4.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-f3gmnlG3.js → RemoteSession-DGTPIk6u.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-CyvybpkN.js → Reputation-QmbwM7Yk.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DwV2PeYn.js → Row-BxjJylia.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-11GrzjZy.js → RssFeed-B7Wda1J6.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BjKXkrAY.js → Search-_plQ_YSA.js} +1 -1
- package/src/assets/web-panel/assets/{Security-eWiFb9Yk.js → Security-CD2YJZfx.js} +2 -2
- package/src/assets/web-panel/assets/{Services-CdCsMAtt.js → Services-qQE7NmwE.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-4Q2UTKQ5.js → Skeleton-DgQqkFf7.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-v4rtYrRj.js → Skills-C4C0oFFt.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CRhsFvd_.js → Sla-Px2N2B5c.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DN6C3lQd.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D16zonl7.js → SyncSettings-jm5aH_m7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Xxozz0i_.js → Tasks-DlkJcv67.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CXJpPtCT.js → Templates-DyrTHUBs.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-XRV0L1ZF.js → Tenant-mC434PtL.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CYuO1i_w.js → Terminal-o0_s3NgK.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BXqitWsu.js → TimelineRenderer-DAGlAXOw.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BvPrDHIy.js → Tokens-BnulS_bs.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DhCrSTf5.js → Trigger-suQbVXYM.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DmsuZD6K.js → Trust-DbIJAPo5.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-D_yrts5C.js → UkeySign-CQNpbIHi.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B01ihPrm.js → VideoEditing-D8aWCERk.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DpUjKa-N.js → Wallet-CuLpYfZc.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-CL07yON6.js → WebAuthn-CsfG8ZMc.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-CLMflnRr.js → WorkflowEditor-BYsEAZbd.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DtUqe8h_.js → chat-FT4Um3-I.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DPKrgVfy.js → colors-CKbS_Wlt.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BTt2GtOm.js → compact-item-CFJtzuJM.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D_VeUc1J.js → createContext-B9kSRW6K.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DJZ0gAMu.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-1kndiylj.js → hasIn-DR-x0Xpi.js} +1 -1
- package/src/assets/web-panel/assets/{index-dVc-CXY4.js → index-7jHr_A_k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFIZ_1Sf.js → index-B3ZX81mK.js} +1 -1
- package/src/assets/web-panel/assets/{index-fk2Rjacw.js → index-B7osrXqy.js} +3 -3
- package/src/assets/web-panel/assets/{index-CHG-KcS4.js → index-BGbPnC5x.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dbbymxpj.js → index-BI5hP_qy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaZAbw9A.js → index-BK3TiKVq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzjkKUmR.js → index-BVPU43b1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAXfRKfG.js → index-BW-t9n-q.js} +1 -1
- package/src/assets/web-panel/assets/{index-8nz_fmlj.js → index-BfFCQHL3.js} +1 -1
- package/src/assets/web-panel/assets/index-Bizb4hkw.js +1 -0
- package/src/assets/web-panel/assets/{index-DdkplL9R.js → index-Bt4fS2eL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DG2MZQ3f.js → index-BwR7yc2h.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfKBRdSS.js → index-Bzz0C1BZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-GsoUQ2M7.js → index-C2JvmYRc.js} +1 -1
- package/src/assets/web-panel/assets/{index-l1BETrDb.js → index-CGaXUsIs.js} +1 -1
- package/src/assets/web-panel/assets/{index-4bna2_SR.js → index-CJmIQ9XR.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfGl8CqG.js → index-C_enk9I_.js} +1 -1
- package/src/assets/web-panel/assets/{index-D95OFht7.js → index-Cr2FLUsD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsJCDUJG.js → index-CvrCCqJt.js} +1 -1
- package/src/assets/web-panel/assets/{index-sDoMHTNA.js → index-D4LfPvWA.js} +1 -1
- package/src/assets/web-panel/assets/{index-By5n8up9.js → index-DMGmg497.js} +1 -1
- package/src/assets/web-panel/assets/{index-ocf06EVQ.js → index-DShSkww-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhudc_Ez.js → index-DTxNy0ZU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CX81qSM4.js → index-DW4BWjTk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlDNVgBt.js → index-DWdqx7zI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcO04eDn.js → index-Dap9c3eD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dwqmtk65.js → index-DcfxhKXR.js} +1 -1
- package/src/assets/web-panel/assets/{index-2eyAXE3d.js → index-DdkIhfi-.js} +1 -1
- package/src/assets/web-panel/assets/{index-brCInrzq.js → index-DeSgw-WV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJ8vXGOa.js → index-DiNUEpSQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHpBMWCh.js → index-DjEP7WkH.js} +1 -1
- package/src/assets/web-panel/assets/{index-m31Ka8hZ.js → index-DtkFqPVA.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtF7EN49.js → index-KqDaCApw.js} +1 -1
- package/src/assets/web-panel/assets/index-OHSl-xCh.js +1 -0
- package/src/assets/web-panel/assets/{index-B9KvcS7R.js → index-OSzjTzGM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CauoBQkH.js → index-QBSeNVO3.js} +1 -1
- package/src/assets/web-panel/assets/{index-FzdwcSy2.js → index-WE08gMJP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxdTdBix.js → index-r2VI1Ikk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BepLZwgB.js → index-tqSEHPRT.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CvyYJd7z.js → initDefaultProps-Dd7n7VgI.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DYZkhUdz.js → motion-BwxouRw1.js} +1 -1
- package/src/assets/web-panel/assets/{move-slnJRVT1.js → move-DObN0PG9.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DVc0CKmy.js → mtc-parser-B3nOUQc7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BCXAtHCq.js → omit-o9WU2Nbl.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DBsemz_K.js → pickAttrs-CL_vvfQ_.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BRtkJ-nv.js → placementArrow-CYrM2tyK.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cebq7EZn.js → responsiveObserve-k_06cQ96.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DT7qvId2.js → slide-CPKG0pxS.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-pQiITLkG.js → statusUtils-BDDEeLTw.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-c-19BVKQ.js → styleChecker-Cmb7RTh7.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DsS5-0bE.js → useFlexGapSupport-e2IedC4N.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CxGiMe5n.js → useFs-CE_gxVnm.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-D9btVID5.js → usePersonalDataHub-BkMIIKMz.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CUYFsFEN.js → vnode-DW886XV1.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CXmn_35j.js → zoom-Dwo_adaf.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/agent.js +105 -35
- package/src/commands/artifacts.js +363 -33
- package/src/commands/cowork.js +202 -36
- package/src/commands/session-budget.js +155 -0
- package/src/commands/session-location.js +948 -0
- package/src/commands/session.js +72 -46
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/artifact-protocol.js +77 -13
- package/src/gateways/ws/session-protocol.js +391 -24
- package/src/gateways/ws/ws-agent-handler.js +191 -14
- package/src/gateways/ws/ws-server.js +14 -3
- package/src/gateways/ws/ws-session-gateway.js +101 -5
- package/src/harness/jsonl-session-store.js +1210 -0
- package/src/lib/agent-team/team-runner.js +43 -13
- package/src/lib/artifact-access-ledger.js +461 -0
- package/src/lib/artifact-cleanup-ledger.js +861 -0
- package/src/lib/artifact-deletion-ledger.js +677 -0
- package/src/lib/artifact-store.js +256 -51
- package/src/lib/background-agent-keeper-protocol.js +75 -4
- package/src/lib/background-agent-launch-barrier.js +74 -0
- package/src/lib/background-agent-supervisor.js +149 -5
- package/src/lib/background-turn-bootstrap-protocol.js +4 -0
- package/src/lib/command-lifecycle-report.js +8 -4
- package/src/lib/cowork-adapter.js +9 -5
- package/src/lib/cowork-task-runner.js +20 -0
- package/src/lib/cowork-workflow.js +178 -20
- package/src/lib/direct-model-usage.js +62 -22
- package/src/lib/dynamic-workflow-runtime.js +3184 -111
- package/src/lib/execution-location-result-apply.js +339 -0
- package/src/lib/execution-location-result-artifact.js +271 -0
- package/src/lib/execution-location-result-review.js +253 -0
- package/src/lib/execution-location-result-store.js +331 -0
- package/src/lib/execution-location-result.js +617 -0
- package/src/lib/execution-location-target.js +236 -1
- package/src/lib/managed-tool-checkpoint.js +47 -4
- package/src/lib/process-execution-broker/workspace-transaction.js +3 -0
- package/src/lib/session-budget-production-root.js +376 -0
- package/src/lib/session-budget-runtime.js +109 -18
- package/src/lib/session-budget-usage.js +63 -0
- package/src/lib/session-host-snapshot.js +19 -2
- package/src/lib/session-resource-budget.js +766 -105
- package/src/lib/sub-agent-context.js +69 -22
- package/src/lib/web-ui-server.js +12 -1
- package/src/lib/with-file-lock.js +175 -26
- package/src/repl/agent-repl.js +237 -16
- package/src/runtime/agent-core.js +123 -0
- package/src/runtime/headless-runner.js +135 -46
- package/src/runtime/headless-stream.js +133 -25
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/workers/background-agent-keeper.js +112 -19
- package/src/workers/background-agent-worker.js +383 -134
- package/src/workers/background-turn-bootstrap.js +13 -8
- package/src/assets/web-panel/assets/ChatBubbleRenderer-CY5m8ZHg.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +0 -1
- package/src/assets/web-panel/assets/Projects-BhtAVilc.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +0 -1
- package/src/assets/web-panel/assets/index-CPcM-eqo.js +0 -1
- package/src/assets/web-panel/assets/index-xeSyVoRe.js +0 -1
|
@@ -24,6 +24,7 @@ import fs from "fs";
|
|
|
24
24
|
import os from "os";
|
|
25
25
|
import path from "path";
|
|
26
26
|
import crypto from "crypto";
|
|
27
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
27
28
|
|
|
28
29
|
export const ARTIFACT_KINDS = Object.freeze([
|
|
29
30
|
"report",
|
|
@@ -62,6 +63,7 @@ export function publicArtifactMetadata(entry) {
|
|
|
62
63
|
expiresAt,
|
|
63
64
|
immutable,
|
|
64
65
|
recordDigest,
|
|
66
|
+
lineage,
|
|
65
67
|
} = entry;
|
|
66
68
|
return {
|
|
67
69
|
id,
|
|
@@ -76,6 +78,7 @@ export function publicArtifactMetadata(entry) {
|
|
|
76
78
|
expiresAt,
|
|
77
79
|
immutable: immutable === true,
|
|
78
80
|
recordDigest: recordDigest || null,
|
|
81
|
+
...(lineage ? { lineage } : {}),
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -120,10 +123,36 @@ function normalizeKind(kind) {
|
|
|
120
123
|
return ARTIFACT_KINDS.includes(k) ? k : "other";
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
function normalizeLineage(lineage) {
|
|
127
|
+
if (lineage == null) return null;
|
|
128
|
+
if (typeof lineage !== "object" || Array.isArray(lineage)) {
|
|
129
|
+
throw new Error("artifact lineage must be an object");
|
|
130
|
+
}
|
|
131
|
+
let normalized;
|
|
132
|
+
try {
|
|
133
|
+
normalized = JSON.parse(JSON.stringify(lineage));
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error("artifact lineage must be JSON serializable");
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
!normalized ||
|
|
139
|
+
typeof normalized !== "object" ||
|
|
140
|
+
Array.isArray(normalized)
|
|
141
|
+
) {
|
|
142
|
+
throw new Error("artifact lineage must be a JSON object");
|
|
143
|
+
}
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
export class ArtifactStore {
|
|
124
|
-
constructor({
|
|
148
|
+
constructor({
|
|
149
|
+
dir = null,
|
|
150
|
+
now = () => Date.now(),
|
|
151
|
+
indexLock = withFileLock,
|
|
152
|
+
} = {}) {
|
|
125
153
|
this.dir = dir || artifactsDir();
|
|
126
154
|
this._now = typeof now === "function" ? now : () => now;
|
|
155
|
+
this._indexLock = indexLock;
|
|
127
156
|
}
|
|
128
157
|
|
|
129
158
|
_indexFile() {
|
|
@@ -138,6 +167,31 @@ export class ArtifactStore {
|
|
|
138
167
|
fs.mkdirSync(this._filesDir(), { recursive: true });
|
|
139
168
|
}
|
|
140
169
|
|
|
170
|
+
_withIndexLock(callback) {
|
|
171
|
+
this._ensureDirs();
|
|
172
|
+
return this._indexLock(this._indexFile(), callback, {
|
|
173
|
+
failIfUnavailable: true,
|
|
174
|
+
timeoutMs: 30_000,
|
|
175
|
+
retryMs: 1,
|
|
176
|
+
maxRetryMs: 8,
|
|
177
|
+
retryJitterMs: 4,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Run a read-only callback against one locked index generation. The callback
|
|
183
|
+
* may inspect stored bytes before it returns; all ArtifactStore mutations use
|
|
184
|
+
* this same lock and therefore cannot replace that generation mid-read.
|
|
185
|
+
*/
|
|
186
|
+
withIndexSnapshot(callback) {
|
|
187
|
+
if (typeof callback !== "function") {
|
|
188
|
+
throw new TypeError("artifact index snapshot requires a callback");
|
|
189
|
+
}
|
|
190
|
+
return this._withIndexLock(() =>
|
|
191
|
+
callback(Object.freeze(this._readEntries())),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
141
195
|
/**
|
|
142
196
|
* Publish a file as an artifact (copy + metadata row).
|
|
143
197
|
* @param {{ filePath: string, title?: string, kind?: string,
|
|
@@ -162,14 +216,16 @@ export class ArtifactStore {
|
|
|
162
216
|
);
|
|
163
217
|
}
|
|
164
218
|
const body = fs.readFileSync(abs);
|
|
165
|
-
return this.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
219
|
+
return this._withIndexLock(() =>
|
|
220
|
+
this._publishDataUnlocked(body, {
|
|
221
|
+
fileName: path.basename(abs),
|
|
222
|
+
title,
|
|
223
|
+
kind,
|
|
224
|
+
sessionId,
|
|
225
|
+
ttlDays,
|
|
226
|
+
sourcePath: abs,
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
173
229
|
}
|
|
174
230
|
|
|
175
231
|
/**
|
|
@@ -191,20 +247,78 @@ export class ArtifactStore {
|
|
|
191
247
|
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
|
|
192
248
|
throw new Error("publishData requires string or Buffer data");
|
|
193
249
|
}
|
|
194
|
-
return this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
250
|
+
return this._withIndexLock(() =>
|
|
251
|
+
this._publishDataUnlocked(Buffer.from(data), {
|
|
252
|
+
fileName,
|
|
253
|
+
title,
|
|
254
|
+
kind,
|
|
255
|
+
mime,
|
|
256
|
+
sessionId,
|
|
257
|
+
ttlDays,
|
|
258
|
+
immutable,
|
|
259
|
+
recordDigest,
|
|
260
|
+
sourcePath: null,
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Publish generated bytes once for a stable authority digest.
|
|
267
|
+
*
|
|
268
|
+
* The strict cross-process index lock makes response-loss retries return the
|
|
269
|
+
* same metadata row instead of creating duplicate managed artifacts. The
|
|
270
|
+
* caller still owns the semantic validation of the persisted lineage and
|
|
271
|
+
* must verifyIntegrity() before treating the returned entry as readable.
|
|
272
|
+
*/
|
|
273
|
+
publishDataOnce({
|
|
274
|
+
data,
|
|
275
|
+
fileName = "artifact.bin",
|
|
276
|
+
title,
|
|
277
|
+
kind,
|
|
278
|
+
mime,
|
|
279
|
+
sessionId = null,
|
|
280
|
+
ttlDays,
|
|
281
|
+
immutable = false,
|
|
282
|
+
recordDigest,
|
|
283
|
+
lineage,
|
|
284
|
+
} = {}) {
|
|
285
|
+
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
|
|
286
|
+
throw new Error("publishDataOnce requires string or Buffer data");
|
|
287
|
+
}
|
|
288
|
+
const stableDigest = String(recordDigest || "");
|
|
289
|
+
if (!/^sha256:[a-f0-9]{64}$/u.test(stableDigest)) {
|
|
290
|
+
throw new Error("publishDataOnce requires a canonical recordDigest");
|
|
291
|
+
}
|
|
292
|
+
const normalizedLineage = normalizeLineage(lineage);
|
|
293
|
+
return this._withIndexLock(() => {
|
|
294
|
+
const matches = this._readEntries().filter(
|
|
295
|
+
(entry) => entry.recordDigest === stableDigest,
|
|
296
|
+
);
|
|
297
|
+
if (matches.length > 1) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`artifact authority ${stableDigest} has duplicate index rows`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (matches.length === 1) {
|
|
303
|
+
return Object.freeze({ entry: matches[0], published: false });
|
|
304
|
+
}
|
|
305
|
+
const entry = this._publishDataUnlocked(Buffer.from(data), {
|
|
306
|
+
fileName,
|
|
307
|
+
title,
|
|
308
|
+
kind,
|
|
309
|
+
mime,
|
|
310
|
+
sessionId,
|
|
311
|
+
ttlDays,
|
|
312
|
+
immutable,
|
|
313
|
+
recordDigest: stableDigest,
|
|
314
|
+
lineage: normalizedLineage,
|
|
315
|
+
sourcePath: null,
|
|
316
|
+
});
|
|
317
|
+
return Object.freeze({ entry, published: true });
|
|
204
318
|
});
|
|
205
319
|
}
|
|
206
320
|
|
|
207
|
-
|
|
321
|
+
_publishDataUnlocked(
|
|
208
322
|
body,
|
|
209
323
|
{
|
|
210
324
|
fileName,
|
|
@@ -215,6 +329,7 @@ export class ArtifactStore {
|
|
|
215
329
|
ttlDays,
|
|
216
330
|
immutable = false,
|
|
217
331
|
recordDigest = null,
|
|
332
|
+
lineage = null,
|
|
218
333
|
sourcePath = null,
|
|
219
334
|
},
|
|
220
335
|
) {
|
|
@@ -247,10 +362,15 @@ export class ArtifactStore {
|
|
|
247
362
|
expiresAt: new Date(createdAt + days * 24 * 60 * 60 * 1000).toISOString(),
|
|
248
363
|
immutable: immutable === true,
|
|
249
364
|
recordDigest: recordDigest ? String(recordDigest) : null,
|
|
365
|
+
...(lineage ? { lineage: normalizeLineage(lineage) } : {}),
|
|
250
366
|
};
|
|
251
367
|
this._ensureDirs();
|
|
252
368
|
const storedPath = path.join(this._filesDir(), entry.file);
|
|
253
|
-
fs.writeFileSync(storedPath, body, {
|
|
369
|
+
fs.writeFileSync(storedPath, body, {
|
|
370
|
+
flag: "wx",
|
|
371
|
+
mode: 0o600,
|
|
372
|
+
flush: true,
|
|
373
|
+
});
|
|
254
374
|
if (entry.immutable) {
|
|
255
375
|
try {
|
|
256
376
|
fs.chmodSync(storedPath, 0o444);
|
|
@@ -259,12 +379,37 @@ export class ArtifactStore {
|
|
|
259
379
|
// support; immutable here means append-only/no update API, not WORM.
|
|
260
380
|
}
|
|
261
381
|
}
|
|
262
|
-
|
|
382
|
+
let descriptor = null;
|
|
383
|
+
try {
|
|
384
|
+
descriptor = fs.openSync(
|
|
385
|
+
this._indexFile(),
|
|
386
|
+
fs.constants.O_WRONLY |
|
|
387
|
+
fs.constants.O_APPEND |
|
|
388
|
+
fs.constants.O_CREAT |
|
|
389
|
+
Number(fs.constants.O_NOFOLLOW || 0),
|
|
390
|
+
0o600,
|
|
391
|
+
);
|
|
392
|
+
const indexStat = fs.fstatSync(descriptor);
|
|
393
|
+
if (!indexStat.isFile() || Number(indexStat.nlink) !== 1) {
|
|
394
|
+
throw new Error("artifact index identity is invalid");
|
|
395
|
+
}
|
|
396
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
397
|
+
fs.fsyncSync(descriptor);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
try {
|
|
400
|
+
this._deleteStoredFile(entry);
|
|
401
|
+
} catch {
|
|
402
|
+
/* best-effort rollback of an unpublished stored copy */
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
} finally {
|
|
406
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
407
|
+
}
|
|
263
408
|
return entry;
|
|
264
409
|
}
|
|
265
410
|
|
|
266
411
|
/** All (non-corrupt) metadata rows, oldest first. */
|
|
267
|
-
|
|
412
|
+
_readEntries({ sessionId } = {}) {
|
|
268
413
|
let raw;
|
|
269
414
|
try {
|
|
270
415
|
raw = fs.readFileSync(this._indexFile(), "utf-8");
|
|
@@ -285,6 +430,10 @@ export class ArtifactStore {
|
|
|
285
430
|
return sessionId ? out.filter((e) => e.sessionId === sessionId) : out;
|
|
286
431
|
}
|
|
287
432
|
|
|
433
|
+
list(options = {}) {
|
|
434
|
+
return this._withIndexLock(() => this._readEntries(options));
|
|
435
|
+
}
|
|
436
|
+
|
|
288
437
|
/** One entry by id (or null). */
|
|
289
438
|
get(id) {
|
|
290
439
|
return this.list().find((e) => e.id === id) || null;
|
|
@@ -348,47 +497,103 @@ export class ArtifactStore {
|
|
|
348
497
|
|
|
349
498
|
/** Remove one artifact (file + index row). @returns {boolean} found */
|
|
350
499
|
remove(id) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
500
|
+
return this._withIndexLock(() => {
|
|
501
|
+
const entries = this._readEntries();
|
|
502
|
+
const matches = entries.filter((entry) => entry.id === id);
|
|
503
|
+
if (matches.length === 0) return false;
|
|
504
|
+
if (matches.length > 1) {
|
|
505
|
+
throw new Error(`artifact id ${id} has duplicate index rows`);
|
|
506
|
+
}
|
|
507
|
+
const target = matches[0];
|
|
508
|
+
this._rewriteUnlocked(entries.filter((entry) => entry.id !== id));
|
|
509
|
+
try {
|
|
510
|
+
this._deleteStoredFile(target);
|
|
511
|
+
} catch {
|
|
512
|
+
/* best-effort — the locked index generation is the source of truth */
|
|
513
|
+
}
|
|
514
|
+
return true;
|
|
515
|
+
});
|
|
361
516
|
}
|
|
362
517
|
|
|
363
518
|
/**
|
|
364
|
-
*
|
|
519
|
+
* Low-level legacy primitive for removing expired artifacts.
|
|
520
|
+
* Official CLI and WebSocket callers must use artifact cleanup settlement so
|
|
521
|
+
* retries preserve a frozen scope and leave content-free audit evidence.
|
|
365
522
|
* @returns {{ removed: number }}
|
|
366
523
|
*/
|
|
367
524
|
cleanupExpired() {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
525
|
+
return this._withIndexLock(() => {
|
|
526
|
+
const entries = this._readEntries();
|
|
527
|
+
const now = this._now();
|
|
528
|
+
const keep = [];
|
|
529
|
+
const expired = [];
|
|
530
|
+
for (const entry of entries) {
|
|
531
|
+
const exp = Date.parse(entry.expiresAt || "");
|
|
532
|
+
if (Number.isFinite(exp) && exp <= now) {
|
|
533
|
+
expired.push(entry);
|
|
534
|
+
} else {
|
|
535
|
+
keep.push(entry);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (expired.length > 0) this._rewriteUnlocked(keep);
|
|
539
|
+
for (const entry of expired) {
|
|
375
540
|
try {
|
|
376
|
-
this._deleteStoredFile(
|
|
541
|
+
this._deleteStoredFile(entry);
|
|
377
542
|
} catch {
|
|
378
|
-
/* best-effort */
|
|
543
|
+
/* best-effort — the locked index generation is the source of truth */
|
|
379
544
|
}
|
|
380
|
-
removed += 1;
|
|
381
|
-
} else {
|
|
382
|
-
keep.push(e);
|
|
383
545
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return { removed };
|
|
546
|
+
return { removed: expired.length };
|
|
547
|
+
});
|
|
387
548
|
}
|
|
388
549
|
|
|
389
|
-
|
|
550
|
+
_rewriteUnlocked(entries) {
|
|
390
551
|
this._ensureDirs();
|
|
391
552
|
const text = entries.map((e) => JSON.stringify(e)).join("\n");
|
|
392
|
-
|
|
553
|
+
const indexFile = this._indexFile();
|
|
554
|
+
const temporary = `${indexFile}.${process.pid}.${crypto
|
|
555
|
+
.randomBytes(8)
|
|
556
|
+
.toString("hex")}.tmp`;
|
|
557
|
+
let descriptor = null;
|
|
558
|
+
try {
|
|
559
|
+
descriptor = fs.openSync(
|
|
560
|
+
temporary,
|
|
561
|
+
fs.constants.O_WRONLY |
|
|
562
|
+
fs.constants.O_CREAT |
|
|
563
|
+
fs.constants.O_EXCL |
|
|
564
|
+
Number(fs.constants.O_NOFOLLOW || 0),
|
|
565
|
+
0o600,
|
|
566
|
+
);
|
|
567
|
+
fs.writeFileSync(descriptor, text ? `${text}\n` : "", "utf-8");
|
|
568
|
+
fs.fsyncSync(descriptor);
|
|
569
|
+
fs.closeSync(descriptor);
|
|
570
|
+
descriptor = null;
|
|
571
|
+
fs.renameSync(temporary, indexFile);
|
|
572
|
+
this._syncIndexDirectory();
|
|
573
|
+
} finally {
|
|
574
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
575
|
+
try {
|
|
576
|
+
fs.rmSync(temporary, { force: true });
|
|
577
|
+
} catch {
|
|
578
|
+
/* best-effort cleanup of an unpublished index generation */
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
_syncIndexDirectory() {
|
|
584
|
+
let descriptor = null;
|
|
585
|
+
try {
|
|
586
|
+
descriptor = fs.openSync(this.dir, "r");
|
|
587
|
+
fs.fsyncSync(descriptor);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (
|
|
590
|
+
process.platform !== "win32" ||
|
|
591
|
+
!["EACCES", "EINVAL", "ENOTSUP", "EPERM"].includes(error?.code)
|
|
592
|
+
) {
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
} finally {
|
|
596
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
597
|
+
}
|
|
393
598
|
}
|
|
394
599
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstatSync, mkdirSync, rmdirSync } from "node:fs";
|
|
3
|
-
import { posix } from "node:path";
|
|
3
|
+
import { join, posix, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
export const BACKGROUND_AGENT_KEEPER_PROTOCOL_VERSION = 1;
|
|
6
6
|
export const BACKGROUND_AGENT_KEEPER_HELLO = "background-agent-keeper-hello";
|
|
@@ -12,8 +12,22 @@ export const BACKGROUND_AGENT_KEEPER_ARMED = "background-agent-keeper-armed";
|
|
|
12
12
|
export const BACKGROUND_AGENT_KEEPER_RETIRE = "background-agent-keeper-retire";
|
|
13
13
|
export const BACKGROUND_AGENT_KEEPER_RETIRED =
|
|
14
14
|
"background-agent-keeper-retired";
|
|
15
|
+
export const BACKGROUND_AGENT_KEEPER_LAUNCH_CLAIM =
|
|
16
|
+
"background-agent-keeper-launch-claim";
|
|
15
17
|
export const BACKGROUND_AGENT_KEEPER_HEARTBEAT_INTERVAL_MS = 1_000;
|
|
16
|
-
|
|
18
|
+
// Do not launch an OS process-identity probe while authenticated application
|
|
19
|
+
// heartbeats are fresh. On Windows the cached WMIC/CIM fallback can otherwise
|
|
20
|
+
// make 20 keepers synchronously spawn probe helpers together every ten seconds.
|
|
21
|
+
export const BACKGROUND_AGENT_KEEPER_IDENTITY_PROBE_DELAY_MS = 15_000;
|
|
22
|
+
// This application-level fence is a backstop for the rare case where Windows
|
|
23
|
+
// retains both the named-pipe handle and an exact process object after worker
|
|
24
|
+
// death. Normal death still closes the socket immediately; a stale channel
|
|
25
|
+
// gets the delayed PID/start-time probe before this final fence. A 20-worker
|
|
26
|
+
// hosted Windows launch has demonstrated
|
|
27
|
+
// 37-second scheduling tails, so 15 seconds could kill a live, healthy worker
|
|
28
|
+
// while it was merely descheduled. Keep the backstop bounded but above that
|
|
29
|
+
// measured contention window.
|
|
30
|
+
export const BACKGROUND_AGENT_KEEPER_HEARTBEAT_TIMEOUT_MS = 60_000;
|
|
17
31
|
|
|
18
32
|
// RETIRE is the only keeper request that performs destructive OS cleanup. Its
|
|
19
33
|
// deadline is deliberately independent from the short HELLO/ARM request
|
|
@@ -22,7 +36,7 @@ export const BACKGROUND_AGENT_KEEPER_HEARTBEAT_TIMEOUT_MS = 15_000;
|
|
|
22
36
|
//
|
|
23
37
|
// 2 targets * (strict identity + taskkill + cleanup confirmation identity) +
|
|
24
38
|
// 2 cleanup-critical persistence retry windows + cleanup confirmation +
|
|
25
|
-
// scheduling margin =
|
|
39
|
+
// scheduling margin = 128 seconds.
|
|
26
40
|
//
|
|
27
41
|
// Keeping these limits in the shared protocol contract prevents either side
|
|
28
42
|
// from silently reintroducing the old 10-second client / longer keeper race.
|
|
@@ -36,7 +50,12 @@ export const BACKGROUND_AGENT_KEEPER_IDENTITY_PROBE_TIMEOUT_MS =
|
|
|
36
50
|
export const BACKGROUND_AGENT_KEEPER_STATE_LOCK_TIMEOUT_MS = 5_000;
|
|
37
51
|
export const BACKGROUND_AGENT_KEEPER_PERSIST_RETRY_TIMEOUT_MS =
|
|
38
52
|
3 * BACKGROUND_AGENT_KEEPER_STATE_LOCK_TIMEOUT_MS;
|
|
39
|
-
|
|
53
|
+
// A 20-Agent macOS formal run can leave successfully SIGKILLed process groups
|
|
54
|
+
// observable for several seconds while launchd reaps their orphaned leaders.
|
|
55
|
+
// Two seconds made that bounded kernel/reaper delay look like an escaped tree.
|
|
56
|
+
// Ten seconds stays inside the formal observer's 150-second budget while
|
|
57
|
+
// retaining a finite fail-closed confirmation window.
|
|
58
|
+
export const BACKGROUND_AGENT_KEEPER_CLEANUP_CONFIRM_TIMEOUT_MS = 10_000;
|
|
40
59
|
export const BACKGROUND_AGENT_KEEPER_RETIRE_TIMEOUT_MARGIN_MS = 8_000;
|
|
41
60
|
export const BACKGROUND_AGENT_KEEPER_RETIRE_TIMEOUT_MS =
|
|
42
61
|
BACKGROUND_AGENT_KEEPER_CLEANUP_TARGET_LIMIT *
|
|
@@ -92,6 +111,58 @@ function requiredTimestamp(value, label) {
|
|
|
92
111
|
return normalized;
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
export function backgroundAgentKeeperLaunchClaimPath(
|
|
115
|
+
id,
|
|
116
|
+
keeperGeneration,
|
|
117
|
+
directory,
|
|
118
|
+
) {
|
|
119
|
+
const safeId = requiredSafeString(id, "id");
|
|
120
|
+
const safeGeneration = requiredSafeString(
|
|
121
|
+
keeperGeneration,
|
|
122
|
+
"keeper generation",
|
|
123
|
+
);
|
|
124
|
+
const requestedDirectory =
|
|
125
|
+
typeof directory === "string" ? directory.trim() : "";
|
|
126
|
+
if (!requestedDirectory || requestedDirectory.includes("\0")) {
|
|
127
|
+
throw new TypeError("invalid background agent keeper state directory");
|
|
128
|
+
}
|
|
129
|
+
const stateDirectory = resolve(requestedDirectory);
|
|
130
|
+
return join(
|
|
131
|
+
stateDirectory,
|
|
132
|
+
`.${safeId}.keeper-${safeGeneration}.launch-claim.json`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createBackgroundAgentKeeperLaunchClaim(binding = {}) {
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
type: BACKGROUND_AGENT_KEEPER_LAUNCH_CLAIM,
|
|
139
|
+
protocolVersion: BACKGROUND_AGENT_KEEPER_PROTOCOL_VERSION,
|
|
140
|
+
id: requiredSafeString(binding.id, "id"),
|
|
141
|
+
workerGeneration: requiredSafeString(
|
|
142
|
+
binding.workerGeneration,
|
|
143
|
+
"worker generation",
|
|
144
|
+
),
|
|
145
|
+
keeperGeneration: requiredSafeString(
|
|
146
|
+
binding.keeperGeneration,
|
|
147
|
+
"keeper generation",
|
|
148
|
+
),
|
|
149
|
+
keeperPid: requiredPositiveInteger(binding.keeperPid, "keeper pid"),
|
|
150
|
+
token: requiredSafeString(binding.token, "token", SAFE_TOKEN),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function matchesBackgroundAgentKeeperLaunchClaim(value, expected = {}) {
|
|
155
|
+
let actual;
|
|
156
|
+
let binding;
|
|
157
|
+
try {
|
|
158
|
+
actual = createBackgroundAgentKeeperLaunchClaim(value);
|
|
159
|
+
binding = createBackgroundAgentKeeperLaunchClaim(expected);
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return Object.keys(binding).every((key) => actual[key] === binding[key]);
|
|
164
|
+
}
|
|
165
|
+
|
|
95
166
|
function digestKeeperPath(value, length) {
|
|
96
167
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
97
168
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only startup barrier shared by the detached background worker and its
|
|
3
|
+
* keeper. The launcher publishes both exact child PIDs in one state
|
|
4
|
+
* transaction before clearing `launchFinalizationUncertain`. Until then the
|
|
5
|
+
* children must not contend for the same state lock.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const BACKGROUND_AGENT_LAUNCH_BARRIER_TIMEOUT_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
const DEFAULT_POLL_MS = 25;
|
|
11
|
+
const PID_FIELDS = new Set(["workerPid", "keeperPid"]);
|
|
12
|
+
|
|
13
|
+
function delay(milliseconds) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function waitForBackgroundAgentLaunchBarrier(options = {}) {
|
|
18
|
+
const id = String(options.id || "").trim();
|
|
19
|
+
const workerGeneration = String(options.workerGeneration || "").trim();
|
|
20
|
+
const expectedPid = Number(options.expectedPid);
|
|
21
|
+
const pidField = String(options.pidField || "");
|
|
22
|
+
const readState = options.readState;
|
|
23
|
+
const readyWhen = options.readyWhen;
|
|
24
|
+
const now = options.now || Date.now;
|
|
25
|
+
const sleep = options.sleep || delay;
|
|
26
|
+
const timeoutMs = Math.max(
|
|
27
|
+
1,
|
|
28
|
+
Number(options.timeoutMs) || BACKGROUND_AGENT_LAUNCH_BARRIER_TIMEOUT_MS,
|
|
29
|
+
);
|
|
30
|
+
const pollMs = Math.max(1, Number(options.pollMs) || DEFAULT_POLL_MS);
|
|
31
|
+
|
|
32
|
+
if (!id) {
|
|
33
|
+
throw new TypeError("background launch barrier id is required");
|
|
34
|
+
}
|
|
35
|
+
if (!PID_FIELDS.has(pidField)) {
|
|
36
|
+
throw new TypeError("background launch barrier pid field is invalid");
|
|
37
|
+
}
|
|
38
|
+
if (!Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
|
|
39
|
+
throw new TypeError("background launch barrier pid is invalid");
|
|
40
|
+
}
|
|
41
|
+
if (typeof readState !== "function") {
|
|
42
|
+
throw new TypeError("background launch barrier state reader is required");
|
|
43
|
+
}
|
|
44
|
+
if (readyWhen !== undefined && typeof readyWhen !== "function") {
|
|
45
|
+
throw new TypeError("background launch barrier readiness check is invalid");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deadline = now() + timeoutMs;
|
|
49
|
+
for (;;) {
|
|
50
|
+
const state = readState(id);
|
|
51
|
+
if (!state || state.status !== "running") {
|
|
52
|
+
return Object.freeze({ status: "terminal", state: state || null });
|
|
53
|
+
}
|
|
54
|
+
// Jobs written before generation fencing are allowed to observe an
|
|
55
|
+
// already-terminal record above, but can never enter a running record.
|
|
56
|
+
if (!workerGeneration || state.workerGeneration !== workerGeneration) {
|
|
57
|
+
return Object.freeze({ status: "terminal", state });
|
|
58
|
+
}
|
|
59
|
+
if (state.launchFinalizationUncertain !== true) {
|
|
60
|
+
if (Number(state[pidField]) !== expectedPid) {
|
|
61
|
+
return Object.freeze({ status: "identity-mismatch", state });
|
|
62
|
+
}
|
|
63
|
+
if (!readyWhen || readyWhen(state)) {
|
|
64
|
+
return Object.freeze({ status: "ready", state });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const remaining = deadline - now();
|
|
69
|
+
if (remaining <= 0) {
|
|
70
|
+
return Object.freeze({ status: "timeout", state });
|
|
71
|
+
}
|
|
72
|
+
await sleep(Math.min(pollMs, remaining));
|
|
73
|
+
}
|
|
74
|
+
}
|