chainlesschain 0.162.186 → 0.162.188
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-DTXiO-TB.js → AIOps-Brr1LVcE.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BbmDW-Pz.js → ActionButton-DQU74DKG.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CSfgXJuD.js → Analytics-CYp9178y.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BS0FFLb7.js → AppLayout-DW4SCYsW.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-B19WFPK2.js → Artifacts-CYlD_ed1.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CQhZBi7n.js → Audit-CWl1rZOi.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOLZCpGH.js → BackgroundAgents-BOR-LPsa.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Dz3R_ePD.js → Backup-DLpG7j1X.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CYfgMpax.js → BaseInput-B35K9wdr.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-5CymMr1j.js → Chat-CwUJb9gB.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-B2A_TJtl.js → ChatBubbleRenderer-1v6aMS1m.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CDXHVYcu.js → Checkbox-pi3-tPNW.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-uq9Z9oiS.js → Codegen-FyNF35eL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BuW1K_65.js → Col-tGt_JDEr.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Bbg-PvRY.js → Community-CC7LonQX.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-co0pPPTw.js → Compact-BGXrHHjE.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-B-pFAv7z.js → Compliance-DSW0TBZ8.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-54ZQS4gC.js → Cowork-Bhk7rbvE.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-B1BF2Seq.js → Cron-DxyiQ2NZ.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Dey6qQTj.js → Crosschain-B2ZkxHok.js} +1 -1
- package/src/assets/web-panel/assets/{DID-ltERvfAj.js → DID-yOJV99eJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DuWxOcqN.js → Dashboard-CKsttz7D.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrZ8ekTo.js → Dropdown-CVFldXSL.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-fIyHXhUS.js → EmailListRenderer-Bufv638J.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BtRyrRPm.js → FamilyGuardDashboard-4ncjoIjL.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-EUC6Wydj.js → Federation-DsknEDoX.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BOpka2G-.js → FormItemContext-Cqe5jWXA.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BXtrrYHn.js → GenericCardRenderer-NAJaFKxc.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8WMb7PiR.js → Git-CszKQpnl.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DNWOeDTV.js → Governance-BXGodXi6.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bne9b_Lb.js → Inference-CkoYuG2c.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cp-6Us9b.js → KnowledgeGraph-DFlEr_T0.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Ck-uSPAx.js → Logs-CVaENddA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Vdq7brrJ.js → Marketplace-CDlLT0fw.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-ggLnw4hY.js → McpTools-DdwOyszN.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BKIjNVCU.js → Memory-DKQnkKuG.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BElCY25a.js → MobileBridge-DGseqlzJ.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DpLgovdH.js → MobileProjects-COBbEOlj.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-bNdrF_Zh.js → Mtc-O61Vwyli.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dw8w1Cqk.js → MtcAudit-Dw2SyasS.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CfGKvlqc.js → Multisig-hSu_l8kf.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D6OJf7YN.js → NLProgramming-CX1Epurr.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B3n_LzIS.js → Notes-R9xX8o8_.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Uu1V_ICM.js → NotificationSettings-jtSNLCuK.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-aGyWwWcp.js → OrderTableRenderer-BU2Yqq5H.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-qjFFmuwK.js → Organization-Pu2WO-U3.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-J4ZgnwZz.js → Overflow-BjnqUyFp.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-YEZF-uVH.js → P2P-jjkvz44c.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-yqx7fwqk.js → PdhVaultBrowser-69ca_WeR.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-DdEjlZ02.js → Permissions-D4yDpivd.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DbAPYvns.js → PersonalDataHub-0qsL6iH1.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Bxlk_ZJU.js → Pipeline-Cx7Ybrk2.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CpE-05tz.js → Privacy-PbfR_M_V.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DWlhcGHT.js → ProjectInit-CyV6Gsub.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BnH6BiTQ.js → ProjectSettings-C7KZ2Wdk.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DQDjO-yK.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Bdz_sTyb.js → Providers-DyFVBBMX.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BHh2b3pH.js → QrScannerModal-CzB1BC9_.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D84M09pe.js → QuickAsk-Bk636mC1.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Czc30K6M.js → Recommend-DoWxHIT3.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-q_TU5qxt.js → RemoteSession-oOVIv9wq.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CnTce4WM.js → Reputation-BucoReGG.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C60UFLay.js → Row-DzmOdJU7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOUFEu9_.js → RssFeed-C1tK7KGU.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D4__blE-.js → Search-CSnInOqk.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BoOm7_JK.js → Security-dlejr15l.js} +3 -3
- package/src/assets/web-panel/assets/{Services-AvzrW6HA.js → Services-BioPZuNh.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BZlrHozi.js → Skeleton-DWMibwpC.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CKaMLuk0.js → Skills-BAAnx1B2.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DYCNJF8h.js → Sla-jWNp4hNO.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-y5kqKha-.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-B6tW8dVO.js → SyncSettings-B9GNsc3e.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-oNC_2NVz.js → Tasks-BDBYKRen.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DvLPvvjL.js → Templates-DRiE5eaN.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BDt9UBn_.js → Tenant-BGlAvExF.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ddCUnH-X.js → Terminal-B8uzrbNY.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CA4sDL47.js → TimelineRenderer-Df48iveY.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-By03hnVs.js → Tokens-CcmHndN6.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dz2mH-ZU.js → Trigger-DPkWxmQ2.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DShSWHeJ.js → Trust-DTb5a5AV.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-JN8lP3gt.js → UkeySign-CBmg5vch.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BgPy4gV3.js → VideoEditing-B0mZrz4O.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CQGEsXw_.js → Wallet-Bxs7_mpd.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-D8xR-ds_.js → WebAuthn-C8x3cMxU.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Df61_p1p.js → WorkflowEditor-DPrNznrP.js} +1 -1
- package/src/assets/web-panel/assets/{chat-fdPby427.js → chat-D3bYWu7S.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Dimqhufo.js → colors-DY-PwMu4.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DHayKcqZ.js → compact-item-LDk10Kw6.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-hZUhypRH.js → createContext-C-2MZnFq.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Dw7zC_dV.js → hasIn-BQIBjcnB.js} +1 -1
- package/src/assets/web-panel/assets/{index-y5LHQ5Ck.js → index-7xKu7Fns.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBQK-LEF.js → index-8JC9BJa1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuMSdwno.js → index-B5pgtZnY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM5BNqp9.js → index-BI4KgcYC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwnUluxu.js → index-BN-xdSWZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7jQEwB6.js → index-BRI3ngNg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIxxvaGN.js → index-BX2b9Orc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1yMYKi-.js → index-BdXnYMCZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuaVa8tX.js → index-BrnRO9yE.js} +1 -1
- package/src/assets/web-panel/assets/index-C-C7DaG6.js +1 -0
- package/src/assets/web-panel/assets/{index-CIOE8tt6.js → index-C3GNvExG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8XXPz2n.js → index-C7FC8cP2.js} +1 -1
- package/src/assets/web-panel/assets/{index-cEnkyHtX.js → index-COG61sRv.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJP7TjWm.js → index-CQVb2dWp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvKHpaao.js → index-CY4KnaO9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxoPzPHy.js → index-CaslYch6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSlda4-6.js → index-CbR6ROOa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPZ3U5iH.js → index-CiTvBAr-.js} +1 -1
- package/src/assets/web-panel/assets/{index-j0pxzOu7.js → index-Cx6D_dx2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzJXkoZs.js → index-Cz4b6Z_7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVHdkz8F.js → index-D3Oq-DLL.js} +17 -17
- package/src/assets/web-panel/assets/{index-ulvBurtw.js → index-D4K1weKc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyEhrvpm.js → index-DSvo_bp0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC2R4zvM.js → index-DTIZpPtG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsCo_A0I.js → index-DY2FIrmz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTiVrnpR.js → index-DYyLJPlF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHJUptSx.js → index-Dd2Km97U.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcpDboUd.js → index-Dh-tkpKQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnwDYBnG.js → index-DhRt9cYJ.js} +1 -1
- package/src/assets/web-panel/assets/index-DkfWzWtd.js +1 -0
- package/src/assets/web-panel/assets/{index--DVpUpz3.js → index-DlRYqt3S.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVM_KgzF.js → index-HasPt4-h.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX1DwDFI.js → index-KFZO_lcx.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPjBQJCy.js → index-L3smcoQX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Urt2R5aE.js → index-L6qixyLc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYdNwhkv.js → index-VCh5zNJo.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhmpp3cm.js → index-_HRO_cIu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAjvkQ4K.js → index-k7C_oDCO.js} +1 -1
- package/src/assets/web-panel/assets/{index-JE6Udz2r.js → index-lmm9p-jm.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-B4pR3px5.js → initDefaultProps-zQPSHf2C.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CE7dWPPs.js → motion-CKCStGVk.js} +1 -1
- package/src/assets/web-panel/assets/{move-Hw8cUObc.js → move-BufVDGYF.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CvRjE4Fa.js → mtc-parser-DcaFxy5D.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CkFeLVre.js → omit-EScpkGdo.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-5ZMqmbbb.js → pickAttrs-72hG_qfT.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BZ4oeZ0q.js → placementArrow-CMFWiH20.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-lwoiX_Us.js → responsiveObserve-DKQHS41p.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D_as37l8.js → slide-QVAFHTIQ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BsA-4Bs7.js → statusUtils-DIz38e2G.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BVar4sLQ.js → styleChecker-zK5cyLby.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-j1IwVpGd.js → useFlexGapSupport-DhZngk62.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-7Q9bEnUp.js → useFs-BvV2cvkg.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BBwTIRMd.js → usePersonalDataHub-CRmxb9l3.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C7Dk-M8w.js → vnode-CgQIbobP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D3UHaTFB.js → zoom-D4XrrO0R.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/auto-mode.js +47 -0
- package/src/commands/team.js +1805 -473
- package/src/data/auto-mode-safety-eval-v1.json +3294 -0
- package/src/data/changelog.json +26 -0
- package/src/harness/worktree-isolator.js +671 -79
- package/src/lib/agent-team/task-lease.js +378 -78
- package/src/lib/agent-team/team-agent-stream.js +488 -0
- package/src/lib/agent-team/team-budget.js +216 -20
- package/src/lib/agent-team/team-mailbox.js +389 -15
- package/src/lib/agent-team/team-run-state-lock.js +209 -0
- package/src/lib/agent-team/team-runner.js +547 -63
- package/src/lib/agent-team/team-scope-lock.js +409 -0
- package/src/lib/agent-team/team-task-contract.js +277 -0
- package/src/lib/agent-team/team-worktree.js +1171 -62
- package/src/lib/agent-worktree.js +19 -20
- package/src/lib/auto-mode-safety-classifier.js +2034 -0
- package/src/lib/auto-mode-safety-eval.js +1022 -0
- package/src/lib/collaboration-run-store.js +983 -68
- package/src/lib/with-file-lock.js +190 -24
- package/src/lib/worktree-isolator.js +3 -0
- package/src/lib/worktree-sparse.js +23 -0
- package/src/runtime/headless-stream.js +6 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-DxjsdAXw.js +0 -1
- package/src/assets/web-panel/assets/Projects-CHw2fXDB.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-BvCSU6d2.js +0 -1
- package/src/assets/web-panel/assets/devWarning-ePxoBp92.js +0 -1
- package/src/assets/web-panel/assets/index-BTBDdCoo.js +0 -1
- package/src/assets/web-panel/assets/index-CfOGx8Ns.js +0 -1
|
@@ -9,21 +9,39 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Prompts, argv, tool arguments, model output, credentials, and side-effect
|
|
11
11
|
* metadata never enter this store.
|
|
12
|
+
*
|
|
13
|
+
* Each run has one immutable `<id>.json` base manifest. Mutations are bounded,
|
|
14
|
+
* append-only `<id>.journal.jsonl` events replayed under the same fail-closed
|
|
15
|
+
* file lock. History is never compacted or rewritten: a resume anchor can
|
|
16
|
+
* therefore prove that its exact seq+digest remains a prefix of the journal.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
import {
|
|
20
|
+
appendFileSync,
|
|
15
21
|
existsSync,
|
|
16
22
|
mkdirSync,
|
|
17
23
|
readFileSync,
|
|
18
24
|
readdirSync,
|
|
19
25
|
renameSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
statSync,
|
|
20
28
|
writeFileSync,
|
|
21
29
|
} from "node:fs";
|
|
22
30
|
import { join } from "node:path";
|
|
23
31
|
import { createHash, randomBytes } from "node:crypto";
|
|
32
|
+
import { isDeepStrictEqual, TextDecoder } from "node:util";
|
|
24
33
|
import { getHomeDir } from "./paths.js";
|
|
34
|
+
import { tightenPermissionMode } from "./subagent-contract.js";
|
|
35
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
25
36
|
|
|
26
37
|
export const COLLABORATION_RUN_VERSION = 1;
|
|
38
|
+
export const MAX_COLLABORATION_UNITS = 10000;
|
|
39
|
+
export const COLLABORATION_JOURNAL_VERSION = 1;
|
|
40
|
+
export const MAX_COLLABORATION_JOURNAL_EVENTS = 50000;
|
|
41
|
+
export const MAX_COLLABORATION_JOURNAL_BYTES = 64 * 1024 * 1024;
|
|
42
|
+
export const MAX_COLLABORATION_JOURNAL_EVENT_BYTES = 32 * 1024;
|
|
43
|
+
const MAX_COLLABORATION_CACHE_ENTRIES = 64;
|
|
44
|
+
const MAX_COLLABORATION_CACHED_UNITS = 50000;
|
|
27
45
|
const RUN_KINDS = new Set(["team", "batch"]);
|
|
28
46
|
const PERMISSION_MODES = new Set([
|
|
29
47
|
"manual",
|
|
@@ -44,12 +62,45 @@ const UNIT_STATUSES = new Set([
|
|
|
44
62
|
"no-changes",
|
|
45
63
|
"cancelled",
|
|
46
64
|
]);
|
|
65
|
+
const TERMINAL_UNIT_STATUSES = new Set([
|
|
66
|
+
"completed",
|
|
67
|
+
"failed",
|
|
68
|
+
"test-failed",
|
|
69
|
+
"no-changes",
|
|
70
|
+
"cancelled",
|
|
71
|
+
]);
|
|
47
72
|
|
|
48
73
|
export const _deps = {
|
|
49
74
|
now: () => Date.now(),
|
|
50
75
|
randomHex: () => randomBytes(3).toString("hex"),
|
|
76
|
+
withFileLock,
|
|
77
|
+
maxJournalEvents: MAX_COLLABORATION_JOURNAL_EVENTS,
|
|
78
|
+
maxJournalBytes: MAX_COLLABORATION_JOURNAL_BYTES,
|
|
51
79
|
};
|
|
52
80
|
|
|
81
|
+
// The cache owns normalized, secret-free state plus an O(1) unit-key index.
|
|
82
|
+
// Public APIs always return clones so callers cannot mutate cached state.
|
|
83
|
+
const runCache = new Map();
|
|
84
|
+
let cachedUnitCount = 0;
|
|
85
|
+
|
|
86
|
+
function advanceJournalDigest(previous, line) {
|
|
87
|
+
return createHash("sha256")
|
|
88
|
+
.update(previous || "", "utf8")
|
|
89
|
+
.update("\0", "utf8")
|
|
90
|
+
.update(line, "utf8")
|
|
91
|
+
.digest("hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function collaborationStoreError(code, message, details = {}) {
|
|
95
|
+
const error = new Error(message);
|
|
96
|
+
error.name = "CollaborationRunStoreError";
|
|
97
|
+
error.code = code;
|
|
98
|
+
for (const [key, value] of Object.entries(details)) {
|
|
99
|
+
if (value !== undefined && value !== null) error[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return error;
|
|
102
|
+
}
|
|
103
|
+
|
|
53
104
|
function boundedString(value, max) {
|
|
54
105
|
if (value === null || value === undefined) return null;
|
|
55
106
|
const clean = String(value)
|
|
@@ -112,6 +163,87 @@ export function collaborationRunPath(id) {
|
|
|
112
163
|
return join(collaborationRunsDir(), `${safeRunId(id)}.json`);
|
|
113
164
|
}
|
|
114
165
|
|
|
166
|
+
export function collaborationRunJournalPath(id) {
|
|
167
|
+
return join(collaborationRunsDir(), `${safeRunId(id)}.journal.jsonl`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function withCollaborationRunLock(target, fn) {
|
|
171
|
+
return _deps.withFileLock(target, fn, { failIfUnavailable: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function journalEventLimit() {
|
|
175
|
+
const value = Number(_deps.maxJournalEvents);
|
|
176
|
+
return Number.isSafeInteger(value) && value > 0
|
|
177
|
+
? value
|
|
178
|
+
: MAX_COLLABORATION_JOURNAL_EVENTS;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function journalByteLimit() {
|
|
182
|
+
const value = Number(_deps.maxJournalBytes);
|
|
183
|
+
return Number.isSafeInteger(value) && value > 0
|
|
184
|
+
? value
|
|
185
|
+
: MAX_COLLABORATION_JOURNAL_BYTES;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function cloneRun(run) {
|
|
189
|
+
return JSON.parse(JSON.stringify(run));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cloneUnit(unit) {
|
|
193
|
+
return JSON.parse(JSON.stringify(unit));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildUnitIndex(run) {
|
|
197
|
+
return new Map(run.units.map((unit, index) => [unit.key, index]));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function setCachedState(target, state) {
|
|
201
|
+
const previous = runCache.get(target);
|
|
202
|
+
if (previous) {
|
|
203
|
+
cachedUnitCount -= previous.cacheWeight ?? previous.run.units.length;
|
|
204
|
+
}
|
|
205
|
+
runCache.delete(target);
|
|
206
|
+
state.cacheWeight = state.run.units.length;
|
|
207
|
+
runCache.set(target, state);
|
|
208
|
+
cachedUnitCount += state.cacheWeight;
|
|
209
|
+
while (
|
|
210
|
+
runCache.size > MAX_COLLABORATION_CACHE_ENTRIES ||
|
|
211
|
+
cachedUnitCount > MAX_COLLABORATION_CACHED_UNITS
|
|
212
|
+
) {
|
|
213
|
+
const oldestKey = runCache.keys().next().value;
|
|
214
|
+
const oldest = runCache.get(oldestKey);
|
|
215
|
+
runCache.delete(oldestKey);
|
|
216
|
+
cachedUnitCount -= oldest?.cacheWeight ?? oldest?.run?.units?.length ?? 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function dropCachedState(target) {
|
|
221
|
+
const previous = runCache.get(target);
|
|
222
|
+
if (previous) {
|
|
223
|
+
cachedUnitCount -= previous.cacheWeight ?? previous.run.units.length;
|
|
224
|
+
}
|
|
225
|
+
runCache.delete(target);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function fileSignature(filePath) {
|
|
229
|
+
if (!existsSync(filePath)) return null;
|
|
230
|
+
const stat = statSync(filePath);
|
|
231
|
+
return {
|
|
232
|
+
size: stat.size,
|
|
233
|
+
mtimeMs: stat.mtimeMs,
|
|
234
|
+
ctimeMs: stat.ctimeMs,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sameSignature(left, right) {
|
|
239
|
+
if (left === null || right === null) return left === right;
|
|
240
|
+
return (
|
|
241
|
+
left.size === right.size &&
|
|
242
|
+
left.mtimeMs === right.mtimeMs &&
|
|
243
|
+
left.ctimeMs === right.ctimeMs
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
115
247
|
function normalizeBudget(resourceBudget) {
|
|
116
248
|
const source =
|
|
117
249
|
resourceBudget && typeof resourceBudget === "object" ? resourceBudget : {};
|
|
@@ -124,6 +256,36 @@ function normalizeBudget(resourceBudget) {
|
|
|
124
256
|
};
|
|
125
257
|
}
|
|
126
258
|
|
|
259
|
+
function effectiveBudget(resourceBudget, parentBudget) {
|
|
260
|
+
const child = normalizeBudget(resourceBudget);
|
|
261
|
+
const parent = normalizeBudget(parentBudget);
|
|
262
|
+
const cap = (childValue, parentValue) => {
|
|
263
|
+
if (childValue == null) return parentValue;
|
|
264
|
+
if (parentValue == null) return childValue;
|
|
265
|
+
return Math.min(childValue, parentValue);
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
maxTurns: cap(child.maxTurns, parent.maxTurns),
|
|
269
|
+
maxCostUsd: cap(child.maxCostUsd, parent.maxCostUsd),
|
|
270
|
+
maxTasks: cap(child.maxTasks, parent.maxTasks),
|
|
271
|
+
maxTokens: cap(child.maxTokens, parent.maxTokens),
|
|
272
|
+
maxWallMs: cap(child.maxWallMs, parent.maxWallMs),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function normalizeScopePaths(value) {
|
|
277
|
+
if (!Array.isArray(value)) return [];
|
|
278
|
+
const out = [];
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
for (const item of value.slice(0, 128)) {
|
|
281
|
+
const clean = boundedString(item, 1024);
|
|
282
|
+
if (!clean || seen.has(clean)) continue;
|
|
283
|
+
seen.add(clean);
|
|
284
|
+
out.push(clean);
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
127
289
|
function summarizeSideEffects(source) {
|
|
128
290
|
const ops =
|
|
129
291
|
source && typeof source.list === "function"
|
|
@@ -164,6 +326,18 @@ function normalizeUnit(unit, run) {
|
|
|
164
326
|
sessionId,
|
|
165
327
|
branch: boundedString(unit?.branch, 512),
|
|
166
328
|
worktreePath: boundedString(unit?.worktreePath, 4096),
|
|
329
|
+
permissionMode: tightenPermissionMode(
|
|
330
|
+
run.permissionMode,
|
|
331
|
+
PERMISSION_MODES.has(unit?.permissionMode)
|
|
332
|
+
? unit.permissionMode
|
|
333
|
+
: run.permissionMode,
|
|
334
|
+
),
|
|
335
|
+
resourceBudget: effectiveBudget(unit?.resourceBudget, run.resourceBudget),
|
|
336
|
+
checkpointRequired:
|
|
337
|
+
run.checkpointRequired === true || unit?.checkpointRequired === true,
|
|
338
|
+
worktreeRequired:
|
|
339
|
+
run.worktreeRequired === true || unit?.worktreeRequired === true,
|
|
340
|
+
scopePaths: normalizeScopePaths(unit?.scopePaths),
|
|
167
341
|
status,
|
|
168
342
|
startedAt: positiveNumber(unit?.startedAt),
|
|
169
343
|
endedAt: positiveNumber(unit?.endedAt),
|
|
@@ -190,6 +364,7 @@ function normalizeRun(input) {
|
|
|
190
364
|
const startedAt = positiveNumber(input?.startedAt) || _deps.now();
|
|
191
365
|
const run = {
|
|
192
366
|
version: COLLABORATION_RUN_VERSION,
|
|
367
|
+
journalRequired: input?.journalRequired === true,
|
|
193
368
|
id,
|
|
194
369
|
kind,
|
|
195
370
|
owner: `${kind}:${id}`,
|
|
@@ -197,6 +372,8 @@ function normalizeRun(input) {
|
|
|
197
372
|
repoRoot: boundedString(input?.repoRoot, 4096),
|
|
198
373
|
permissionMode,
|
|
199
374
|
resourceBudget: normalizeBudget(input?.resourceBudget),
|
|
375
|
+
checkpointRequired: input?.checkpointRequired === true,
|
|
376
|
+
worktreeRequired: input?.worktreeRequired === true,
|
|
200
377
|
startedAt,
|
|
201
378
|
updatedAt: positiveNumber(input?.updatedAt) || startedAt,
|
|
202
379
|
endedAt: TERMINAL_RUN_STATUSES.has(status)
|
|
@@ -205,9 +382,13 @@ function normalizeRun(input) {
|
|
|
205
382
|
units: [],
|
|
206
383
|
};
|
|
207
384
|
const seen = new Set();
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
385
|
+
const units = Array.isArray(input?.units) ? input.units : [];
|
|
386
|
+
if (units.length > MAX_COLLABORATION_UNITS) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Collaboration run has ${units.length} units; maximum is ${MAX_COLLABORATION_UNITS}`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
for (const unit of units) {
|
|
211
392
|
const normalized = normalizeUnit(unit, run);
|
|
212
393
|
if (seen.has(normalized.key)) {
|
|
213
394
|
throw new Error(`Duplicate collaboration unit key: ${normalized.key}`);
|
|
@@ -218,21 +399,580 @@ function normalizeRun(input) {
|
|
|
218
399
|
return run;
|
|
219
400
|
}
|
|
220
401
|
|
|
402
|
+
const UNIT_PATCH_KEYS = new Set([
|
|
403
|
+
"branch",
|
|
404
|
+
"worktreePath",
|
|
405
|
+
"status",
|
|
406
|
+
"startedAt",
|
|
407
|
+
"endedAt",
|
|
408
|
+
"sideEffects",
|
|
409
|
+
]);
|
|
410
|
+
const SIDE_EFFECT_KEYS = new Set([
|
|
411
|
+
"total",
|
|
412
|
+
"unsettled",
|
|
413
|
+
"unknown",
|
|
414
|
+
"committed",
|
|
415
|
+
"failed",
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
function journalCorrupt(id, line, reason) {
|
|
419
|
+
return collaborationStoreError(
|
|
420
|
+
"COLLABORATION_JOURNAL_CORRUPT",
|
|
421
|
+
`Collaboration journal is corrupt for ${id} at line ${line}: ${reason}`,
|
|
422
|
+
{ runId: id, line },
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function assertExactKeys(value, allowed, required, id, line, label) {
|
|
427
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
428
|
+
throw journalCorrupt(id, line, `invalid ${label}`);
|
|
429
|
+
}
|
|
430
|
+
for (const key of Object.keys(value)) {
|
|
431
|
+
if (!allowed.has(key)) {
|
|
432
|
+
throw journalCorrupt(id, line, `unexpected ${label} field`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const key of required) {
|
|
436
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
437
|
+
throw journalCorrupt(id, line, `missing ${label} field`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function decodeJsonObject(raw, id, label, line = 0) {
|
|
443
|
+
let text;
|
|
444
|
+
try {
|
|
445
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
446
|
+
} catch {
|
|
447
|
+
throw collaborationStoreError(
|
|
448
|
+
label === "journal"
|
|
449
|
+
? "COLLABORATION_JOURNAL_CORRUPT"
|
|
450
|
+
: "COLLABORATION_RUN_CORRUPT",
|
|
451
|
+
`Invalid UTF-8 in collaboration ${label} for ${id}`,
|
|
452
|
+
{ runId: id, ...(line > 0 ? { line } : {}) },
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const value = JSON.parse(text);
|
|
457
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
458
|
+
throw new TypeError("not an object");
|
|
459
|
+
}
|
|
460
|
+
return value;
|
|
461
|
+
} catch {
|
|
462
|
+
throw collaborationStoreError(
|
|
463
|
+
label === "journal"
|
|
464
|
+
? "COLLABORATION_JOURNAL_CORRUPT"
|
|
465
|
+
: "COLLABORATION_RUN_CORRUPT",
|
|
466
|
+
`Invalid JSON in collaboration ${label} for ${id}${
|
|
467
|
+
line > 0 ? ` at line ${line}` : ""
|
|
468
|
+
}`,
|
|
469
|
+
{ runId: id, ...(line > 0 ? { line } : {}) },
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function atomicWrite(target, contents) {
|
|
475
|
+
const temp = `${target}.${process.pid}.${_deps.randomHex()}.tmp`;
|
|
476
|
+
try {
|
|
477
|
+
writeFileSync(temp, contents, { mode: 0o600 });
|
|
478
|
+
renameSync(temp, target);
|
|
479
|
+
} finally {
|
|
480
|
+
try {
|
|
481
|
+
if (existsSync(temp)) rmSync(temp, { force: true });
|
|
482
|
+
} catch {
|
|
483
|
+
// A failed cleanup must not hide the original write/rename failure.
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function buildUnitPatch(patch) {
|
|
489
|
+
const source = patch && typeof patch === "object" ? patch : {};
|
|
490
|
+
const result = {};
|
|
491
|
+
if (source.branch !== undefined) {
|
|
492
|
+
result.branch = boundedString(source.branch, 512);
|
|
493
|
+
}
|
|
494
|
+
if (source.worktreePath !== undefined) {
|
|
495
|
+
result.worktreePath = boundedString(source.worktreePath, 4096);
|
|
496
|
+
}
|
|
497
|
+
if (UNIT_STATUSES.has(source.status)) result.status = source.status;
|
|
498
|
+
if (source.startedAt !== undefined) {
|
|
499
|
+
result.startedAt = positiveNumber(source.startedAt);
|
|
500
|
+
}
|
|
501
|
+
if (source.endedAt !== undefined) {
|
|
502
|
+
result.endedAt = positiveNumber(source.endedAt);
|
|
503
|
+
}
|
|
504
|
+
if (source.sideEffects !== undefined) {
|
|
505
|
+
result.sideEffects = summarizeSideEffects(source.sideEffects);
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function validateJournalUnitPatch(raw, id, line) {
|
|
511
|
+
assertExactKeys(raw, UNIT_PATCH_KEYS, [], id, line, "unit patch");
|
|
512
|
+
const patch = {};
|
|
513
|
+
for (const key of Object.keys(raw)) {
|
|
514
|
+
if (key === "branch" || key === "worktreePath") {
|
|
515
|
+
const max = key === "branch" ? 512 : 4096;
|
|
516
|
+
const normalized = boundedString(raw[key], max);
|
|
517
|
+
if (normalized !== raw[key]) {
|
|
518
|
+
throw journalCorrupt(id, line, `invalid ${key}`);
|
|
519
|
+
}
|
|
520
|
+
patch[key] = normalized;
|
|
521
|
+
} else if (key === "status") {
|
|
522
|
+
if (!UNIT_STATUSES.has(raw.status)) {
|
|
523
|
+
throw journalCorrupt(id, line, "invalid unit status");
|
|
524
|
+
}
|
|
525
|
+
patch.status = raw.status;
|
|
526
|
+
} else if (key === "startedAt" || key === "endedAt") {
|
|
527
|
+
if (raw[key] !== null && positiveNumber(raw[key]) !== raw[key]) {
|
|
528
|
+
throw journalCorrupt(id, line, `invalid ${key}`);
|
|
529
|
+
}
|
|
530
|
+
patch[key] = raw[key];
|
|
531
|
+
} else if (key === "sideEffects") {
|
|
532
|
+
assertExactKeys(
|
|
533
|
+
raw.sideEffects,
|
|
534
|
+
SIDE_EFFECT_KEYS,
|
|
535
|
+
SIDE_EFFECT_KEYS,
|
|
536
|
+
id,
|
|
537
|
+
line,
|
|
538
|
+
"sideEffects",
|
|
539
|
+
);
|
|
540
|
+
for (const field of SIDE_EFFECT_KEYS) {
|
|
541
|
+
if (
|
|
542
|
+
!Number.isSafeInteger(raw.sideEffects[field]) ||
|
|
543
|
+
raw.sideEffects[field] < 0
|
|
544
|
+
) {
|
|
545
|
+
throw journalCorrupt(id, line, "invalid sideEffects count");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
patch.sideEffects = { ...raw.sideEffects };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return patch;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function applyTrustedEvent(state, event) {
|
|
555
|
+
if (event.type === "unit.update") {
|
|
556
|
+
const index = state.unitIndex.get(event.key);
|
|
557
|
+
state.run.units[index] = {
|
|
558
|
+
...state.run.units[index],
|
|
559
|
+
...event.patch,
|
|
560
|
+
};
|
|
561
|
+
state.run.updatedAt = event.at;
|
|
562
|
+
} else if (event.type === "run.finalize") {
|
|
563
|
+
state.run.status = event.status;
|
|
564
|
+
state.run.updatedAt = event.at;
|
|
565
|
+
state.run.endedAt = event.at;
|
|
566
|
+
} else if (event.type === "run.snapshot") {
|
|
567
|
+
state.run = event.run;
|
|
568
|
+
state.unitIndex = buildUnitIndex(event.run);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function validateAndApplyJournalEvent(state, raw, id, line) {
|
|
573
|
+
const commonKeys = ["version", "runId", "seq", "type", "at"];
|
|
574
|
+
const allowedByType = {
|
|
575
|
+
"unit.update": new Set([...commonKeys, "key", "patch"]),
|
|
576
|
+
"run.finalize": new Set([...commonKeys, "status"]),
|
|
577
|
+
"run.snapshot": new Set([...commonKeys, "run"]),
|
|
578
|
+
};
|
|
579
|
+
const allowed = allowedByType[raw?.type];
|
|
580
|
+
if (!allowed) throw journalCorrupt(id, line, "unsupported event type");
|
|
581
|
+
assertExactKeys(raw, allowed, allowed, id, line, "event");
|
|
582
|
+
if (
|
|
583
|
+
raw.version !== COLLABORATION_JOURNAL_VERSION ||
|
|
584
|
+
raw.runId !== id ||
|
|
585
|
+
!Number.isSafeInteger(raw.seq) ||
|
|
586
|
+
raw.seq <= 0 ||
|
|
587
|
+
!Number.isFinite(raw.at) ||
|
|
588
|
+
raw.at < 0
|
|
589
|
+
) {
|
|
590
|
+
throw journalCorrupt(id, line, "invalid event envelope");
|
|
591
|
+
}
|
|
592
|
+
if (state.lastSeq === 0) {
|
|
593
|
+
if (raw.type !== "run.snapshot" || raw.seq !== 1) {
|
|
594
|
+
throw journalCorrupt(
|
|
595
|
+
id,
|
|
596
|
+
line,
|
|
597
|
+
"journal must start with the genesis snapshot at sequence 1",
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
} else if (raw.seq !== state.lastSeq + 1) {
|
|
601
|
+
throw journalCorrupt(id, line, "journal sequence gap");
|
|
602
|
+
}
|
|
603
|
+
if (state.lastSeq > 0 && TERMINAL_RUN_STATUSES.has(state.run.status)) {
|
|
604
|
+
throw journalCorrupt(id, line, "event appears after terminal run state");
|
|
605
|
+
}
|
|
606
|
+
if (state.lastSeq > 0 && raw.type === "run.snapshot") {
|
|
607
|
+
throw journalCorrupt(id, line, "unexpected non-genesis run snapshot");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
let event;
|
|
611
|
+
if (raw.type === "unit.update") {
|
|
612
|
+
if (
|
|
613
|
+
typeof raw.key !== "string" ||
|
|
614
|
+
boundedString(raw.key, 256) !== raw.key ||
|
|
615
|
+
!state.unitIndex.has(raw.key)
|
|
616
|
+
) {
|
|
617
|
+
throw journalCorrupt(id, line, "unknown collaboration unit");
|
|
618
|
+
}
|
|
619
|
+
const patch = validateJournalUnitPatch(raw.patch, id, line);
|
|
620
|
+
const currentUnit = state.run.units[state.unitIndex.get(raw.key)];
|
|
621
|
+
const currentStatus = currentUnit.status;
|
|
622
|
+
if (
|
|
623
|
+
TERMINAL_UNIT_STATUSES.has(currentStatus) &&
|
|
624
|
+
Object.entries(patch).some(
|
|
625
|
+
([field, value]) => !isDeepStrictEqual(currentUnit[field], value),
|
|
626
|
+
)
|
|
627
|
+
) {
|
|
628
|
+
throw journalCorrupt(id, line, "terminal unit mutation");
|
|
629
|
+
}
|
|
630
|
+
event = {
|
|
631
|
+
...raw,
|
|
632
|
+
patch,
|
|
633
|
+
};
|
|
634
|
+
} else if (raw.type === "run.finalize") {
|
|
635
|
+
if (!TERMINAL_RUN_STATUSES.has(raw.status)) {
|
|
636
|
+
throw journalCorrupt(id, line, "invalid terminal run status");
|
|
637
|
+
}
|
|
638
|
+
if (
|
|
639
|
+
raw.status === "completed" &&
|
|
640
|
+
state.run.units.some((unit) => unit.status !== "completed")
|
|
641
|
+
) {
|
|
642
|
+
throw journalCorrupt(id, line, "completed run has incomplete units");
|
|
643
|
+
}
|
|
644
|
+
event = raw;
|
|
645
|
+
} else {
|
|
646
|
+
let normalized;
|
|
647
|
+
try {
|
|
648
|
+
normalized = normalizeRun(raw.run);
|
|
649
|
+
} catch {
|
|
650
|
+
throw journalCorrupt(id, line, "invalid run snapshot");
|
|
651
|
+
}
|
|
652
|
+
if (
|
|
653
|
+
normalized.id !== id ||
|
|
654
|
+
normalized.kind !== state.run.kind ||
|
|
655
|
+
!isDeepStrictEqual(normalized, raw.run)
|
|
656
|
+
) {
|
|
657
|
+
throw journalCorrupt(id, line, "non-canonical run snapshot");
|
|
658
|
+
}
|
|
659
|
+
event = { ...raw, run: normalized };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
applyTrustedEvent(state, event);
|
|
663
|
+
state.lastSeq = raw.seq;
|
|
664
|
+
state.eventCount += 1;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function readBaseManifest(target, id) {
|
|
668
|
+
let raw;
|
|
669
|
+
try {
|
|
670
|
+
raw = decodeJsonObject(readFileSync(target), id, "manifest");
|
|
671
|
+
const run = normalizeRun(raw);
|
|
672
|
+
if (run.id !== id) {
|
|
673
|
+
throw new Error("run id mismatch");
|
|
674
|
+
}
|
|
675
|
+
return run;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (error?.code === "COLLABORATION_RUN_CORRUPT") throw error;
|
|
678
|
+
throw collaborationStoreError(
|
|
679
|
+
"COLLABORATION_RUN_CORRUPT",
|
|
680
|
+
`Invalid collaboration manifest for ${id}`,
|
|
681
|
+
{ runId: id },
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function replayJournal(state, journalPath, id, signature) {
|
|
687
|
+
if (signature === null) {
|
|
688
|
+
if (state.run.journalRequired === true) {
|
|
689
|
+
throw collaborationStoreError(
|
|
690
|
+
"COLLABORATION_JOURNAL_MISSING",
|
|
691
|
+
`Collaboration journal is missing for ${id}`,
|
|
692
|
+
{ runId: id },
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const byteLimit = journalByteLimit();
|
|
698
|
+
if (signature.size > byteLimit) {
|
|
699
|
+
throw collaborationStoreError(
|
|
700
|
+
"COLLABORATION_JOURNAL_BYTE_LIMIT",
|
|
701
|
+
`Collaboration journal for ${id} exceeds ${byteLimit} bytes`,
|
|
702
|
+
{ runId: id, limit: byteLimit, actual: signature.size },
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const bytes = readFileSync(journalPath);
|
|
707
|
+
// This store never creates an empty journal: the file appears with the first
|
|
708
|
+
// complete append. An existing zero-byte file therefore means a torn first
|
|
709
|
+
// write or external truncation and must not silently reset state to base.
|
|
710
|
+
if (bytes.length === 0) {
|
|
711
|
+
throw journalCorrupt(id, 1, "empty journal");
|
|
712
|
+
}
|
|
713
|
+
if (bytes[bytes.length - 1] !== 0x0a) {
|
|
714
|
+
throw journalCorrupt(id, 1, "truncated final event");
|
|
715
|
+
}
|
|
716
|
+
let text;
|
|
717
|
+
try {
|
|
718
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
719
|
+
} catch {
|
|
720
|
+
throw journalCorrupt(id, 1, "invalid UTF-8");
|
|
721
|
+
}
|
|
722
|
+
const lines = text.slice(0, -1).split("\n");
|
|
723
|
+
const eventLimit = journalEventLimit();
|
|
724
|
+
if (lines.length > eventLimit) {
|
|
725
|
+
throw collaborationStoreError(
|
|
726
|
+
"COLLABORATION_JOURNAL_EVENT_LIMIT",
|
|
727
|
+
`Collaboration journal for ${id} exceeds ${eventLimit} events`,
|
|
728
|
+
{ runId: id, limit: eventLimit, actual: lines.length },
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
732
|
+
const lineNumber = i + 1;
|
|
733
|
+
if (lines[i] === "") {
|
|
734
|
+
throw journalCorrupt(id, lineNumber, "empty event");
|
|
735
|
+
}
|
|
736
|
+
const lineBytes = Buffer.byteLength(lines[i], "utf8");
|
|
737
|
+
let raw;
|
|
738
|
+
try {
|
|
739
|
+
raw = JSON.parse(lines[i]);
|
|
740
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
741
|
+
throw new TypeError("not an object");
|
|
742
|
+
}
|
|
743
|
+
} catch {
|
|
744
|
+
throw journalCorrupt(id, lineNumber, "invalid JSON");
|
|
745
|
+
}
|
|
746
|
+
if (
|
|
747
|
+
raw.type !== "run.snapshot" &&
|
|
748
|
+
lineBytes > MAX_COLLABORATION_JOURNAL_EVENT_BYTES
|
|
749
|
+
) {
|
|
750
|
+
throw collaborationStoreError(
|
|
751
|
+
"COLLABORATION_JOURNAL_EVENT_BYTES",
|
|
752
|
+
`Collaboration journal event for ${id} exceeds ${MAX_COLLABORATION_JOURNAL_EVENT_BYTES} bytes`,
|
|
753
|
+
{
|
|
754
|
+
runId: id,
|
|
755
|
+
line: lineNumber,
|
|
756
|
+
limit: MAX_COLLABORATION_JOURNAL_EVENT_BYTES,
|
|
757
|
+
actual: lineBytes,
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
validateAndApplyJournalEvent(state, raw, id, lineNumber);
|
|
762
|
+
state.journalDigest = advanceJournalDigest(
|
|
763
|
+
state.journalDigest,
|
|
764
|
+
`${lines[i]}\n`,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function loadStateLocked(target, id) {
|
|
770
|
+
const journalPath = collaborationRunJournalPath(id);
|
|
771
|
+
const baseSignature = fileSignature(target);
|
|
772
|
+
if (baseSignature === null) {
|
|
773
|
+
dropCachedState(target);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const journalSignature = fileSignature(journalPath);
|
|
777
|
+
const cached = runCache.get(target);
|
|
778
|
+
if (
|
|
779
|
+
cached &&
|
|
780
|
+
sameSignature(cached.baseSignature, baseSignature) &&
|
|
781
|
+
sameSignature(cached.journalSignature, journalSignature)
|
|
782
|
+
) {
|
|
783
|
+
if (cached.eventCount > journalEventLimit()) {
|
|
784
|
+
throw collaborationStoreError(
|
|
785
|
+
"COLLABORATION_JOURNAL_EVENT_LIMIT",
|
|
786
|
+
`Collaboration journal for ${id} exceeds the configured event limit`,
|
|
787
|
+
{ runId: id, limit: journalEventLimit(), actual: cached.eventCount },
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if ((journalSignature?.size || 0) > journalByteLimit()) {
|
|
791
|
+
throw collaborationStoreError(
|
|
792
|
+
"COLLABORATION_JOURNAL_BYTE_LIMIT",
|
|
793
|
+
`Collaboration journal for ${id} exceeds the configured byte limit`,
|
|
794
|
+
{
|
|
795
|
+
runId: id,
|
|
796
|
+
limit: journalByteLimit(),
|
|
797
|
+
actual: journalSignature?.size || 0,
|
|
798
|
+
},
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return cached;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const run = readBaseManifest(target, id);
|
|
805
|
+
const state = {
|
|
806
|
+
run,
|
|
807
|
+
unitIndex: buildUnitIndex(run),
|
|
808
|
+
eventCount: 0,
|
|
809
|
+
lastSeq: 0,
|
|
810
|
+
journalDigest: null,
|
|
811
|
+
baseSignature,
|
|
812
|
+
journalSignature,
|
|
813
|
+
};
|
|
814
|
+
replayJournal(state, journalPath, id, journalSignature);
|
|
815
|
+
setCachedState(target, state);
|
|
816
|
+
return state;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function serializeJournalEvent(event) {
|
|
820
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
821
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
822
|
+
if (
|
|
823
|
+
event.type !== "run.snapshot" &&
|
|
824
|
+
bytes > MAX_COLLABORATION_JOURNAL_EVENT_BYTES
|
|
825
|
+
) {
|
|
826
|
+
throw collaborationStoreError(
|
|
827
|
+
"COLLABORATION_JOURNAL_EVENT_BYTES",
|
|
828
|
+
`Collaboration journal event exceeds ${MAX_COLLABORATION_JOURNAL_EVENT_BYTES} bytes`,
|
|
829
|
+
{
|
|
830
|
+
runId: event.runId,
|
|
831
|
+
limit: MAX_COLLABORATION_JOURNAL_EVENT_BYTES,
|
|
832
|
+
actual: bytes,
|
|
833
|
+
},
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return { line, bytes };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function eventEnvelope(state, data, seq = state.lastSeq + 1) {
|
|
840
|
+
return {
|
|
841
|
+
version: COLLABORATION_JOURNAL_VERSION,
|
|
842
|
+
runId: state.run.id,
|
|
843
|
+
seq,
|
|
844
|
+
type: data.type,
|
|
845
|
+
at: data.at,
|
|
846
|
+
...(data.type === "unit.update"
|
|
847
|
+
? { key: data.key, patch: data.patch }
|
|
848
|
+
: data.type === "run.finalize"
|
|
849
|
+
? { status: data.status }
|
|
850
|
+
: { run: data.run }),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function eventLimitError(state, limit, actual) {
|
|
855
|
+
return collaborationStoreError(
|
|
856
|
+
"COLLABORATION_JOURNAL_EVENT_LIMIT",
|
|
857
|
+
`Collaboration journal for ${state.run.id} cannot stay within ${limit} events`,
|
|
858
|
+
{ runId: state.run.id, limit, actual },
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function appendJournalEventLocked(
|
|
863
|
+
state,
|
|
864
|
+
target,
|
|
865
|
+
data,
|
|
866
|
+
{ returnUnit = false } = {},
|
|
867
|
+
) {
|
|
868
|
+
const journalPath = collaborationRunJournalPath(state.run.id);
|
|
869
|
+
const eventLimit = journalEventLimit();
|
|
870
|
+
const byteLimit = journalByteLimit();
|
|
871
|
+
const event = eventEnvelope(state, data);
|
|
872
|
+
const serialized = serializeJournalEvent(event);
|
|
873
|
+
const currentBytes = state.journalSignature?.size || 0;
|
|
874
|
+
if (state.eventCount + 1 > eventLimit) {
|
|
875
|
+
throw eventLimitError(state, eventLimit, state.eventCount + 1);
|
|
876
|
+
}
|
|
877
|
+
if (currentBytes + serialized.bytes > byteLimit) {
|
|
878
|
+
throw collaborationStoreError(
|
|
879
|
+
"COLLABORATION_JOURNAL_BYTE_LIMIT",
|
|
880
|
+
`Collaboration journal for ${state.run.id} cannot exceed ${byteLimit} bytes`,
|
|
881
|
+
{
|
|
882
|
+
runId: state.run.id,
|
|
883
|
+
limit: byteLimit,
|
|
884
|
+
actual: currentBytes + serialized.bytes,
|
|
885
|
+
},
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
appendFileSync(journalPath, serialized.line, {
|
|
891
|
+
encoding: "utf8",
|
|
892
|
+
mode: 0o600,
|
|
893
|
+
});
|
|
894
|
+
} catch (error) {
|
|
895
|
+
dropCachedState(target);
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
applyTrustedEvent(state, event);
|
|
899
|
+
state.lastSeq = event.seq;
|
|
900
|
+
state.eventCount += 1;
|
|
901
|
+
state.journalDigest = advanceJournalDigest(
|
|
902
|
+
state.journalDigest,
|
|
903
|
+
serialized.line,
|
|
904
|
+
);
|
|
905
|
+
state.journalSignature = fileSignature(journalPath);
|
|
906
|
+
setCachedState(target, state);
|
|
907
|
+
if (returnUnit && event.type === "unit.update") {
|
|
908
|
+
return cloneUnit(state.run.units[state.unitIndex.get(event.key)]);
|
|
909
|
+
}
|
|
910
|
+
return cloneRun(state.run);
|
|
911
|
+
}
|
|
912
|
+
|
|
221
913
|
export function writeCollaborationRun(input) {
|
|
222
|
-
const run = normalizeRun(input);
|
|
914
|
+
const run = normalizeRun({ ...input, journalRequired: true });
|
|
223
915
|
const target = collaborationRunPath(run.id);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
916
|
+
const journalPath = collaborationRunJournalPath(run.id);
|
|
917
|
+
return withCollaborationRunLock(target, () => {
|
|
918
|
+
if (existsSync(target) || existsSync(journalPath)) {
|
|
919
|
+
throw new Error(`Collaboration run already exists: ${run.id}`);
|
|
920
|
+
}
|
|
921
|
+
const state = {
|
|
922
|
+
run,
|
|
923
|
+
unitIndex: buildUnitIndex(run),
|
|
924
|
+
eventCount: 0,
|
|
925
|
+
lastSeq: 0,
|
|
926
|
+
journalDigest: null,
|
|
927
|
+
baseSignature: fileSignature(target),
|
|
928
|
+
journalSignature: null,
|
|
929
|
+
};
|
|
930
|
+
const genesis = eventEnvelope(state, {
|
|
931
|
+
type: "run.snapshot",
|
|
932
|
+
at: run.updatedAt,
|
|
933
|
+
run,
|
|
934
|
+
});
|
|
935
|
+
const serializedGenesis = serializeJournalEvent(genesis);
|
|
936
|
+
if (serializedGenesis.bytes > journalByteLimit()) {
|
|
937
|
+
throw collaborationStoreError(
|
|
938
|
+
"COLLABORATION_JOURNAL_BYTE_LIMIT",
|
|
939
|
+
`Initial collaboration journal for ${run.id} exceeds ${journalByteLimit()} bytes`,
|
|
940
|
+
{
|
|
941
|
+
runId: run.id,
|
|
942
|
+
limit: journalByteLimit(),
|
|
943
|
+
actual: serializedGenesis.bytes,
|
|
944
|
+
},
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
// The manifest is the visibility/commit point used by list/read APIs.
|
|
948
|
+
// Materialize the validated journal first, then publish the manifest last.
|
|
949
|
+
// A normal write failure removes the not-yet-visible journal.
|
|
950
|
+
try {
|
|
951
|
+
atomicWrite(journalPath, serializedGenesis.line);
|
|
952
|
+
atomicWrite(target, JSON.stringify(run, null, 2));
|
|
953
|
+
} catch (error) {
|
|
954
|
+
try {
|
|
955
|
+
if (!existsSync(target) && existsSync(journalPath)) {
|
|
956
|
+
rmSync(journalPath, { force: true });
|
|
957
|
+
}
|
|
958
|
+
} catch {
|
|
959
|
+
/* preserve the authoritative creation failure */
|
|
960
|
+
}
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
state.baseSignature = fileSignature(target);
|
|
964
|
+
state.eventCount = 1;
|
|
965
|
+
state.lastSeq = genesis.seq;
|
|
966
|
+
state.journalDigest = advanceJournalDigest(null, serializedGenesis.line);
|
|
967
|
+
state.journalSignature = fileSignature(journalPath);
|
|
968
|
+
setCachedState(target, state);
|
|
969
|
+
return cloneRun(run);
|
|
970
|
+
});
|
|
228
971
|
}
|
|
229
972
|
|
|
230
973
|
export function createCollaborationRun(input) {
|
|
231
974
|
const kind = safeKind(input?.kind);
|
|
232
975
|
const id = input?.id ? safeRunId(input.id) : createCollaborationRunId(kind);
|
|
233
|
-
if (existsSync(collaborationRunPath(id))) {
|
|
234
|
-
throw new Error(`Collaboration run already exists: ${id}`);
|
|
235
|
-
}
|
|
236
976
|
return writeCollaborationRun({
|
|
237
977
|
...input,
|
|
238
978
|
id,
|
|
@@ -243,65 +983,208 @@ export function createCollaborationRun(input) {
|
|
|
243
983
|
}
|
|
244
984
|
|
|
245
985
|
export function readCollaborationRun(id) {
|
|
246
|
-
const
|
|
247
|
-
|
|
986
|
+
const safeId = safeRunId(id);
|
|
987
|
+
const target = collaborationRunPath(safeId);
|
|
988
|
+
return withCollaborationRunLock(target, () => {
|
|
989
|
+
const state = loadStateLocked(target, safeId);
|
|
990
|
+
return state ? cloneRun(state.run) : null;
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function cursorFromState(state) {
|
|
995
|
+
return {
|
|
996
|
+
runId: state.run.id,
|
|
997
|
+
lastSeq: state.lastSeq,
|
|
998
|
+
eventCount: state.eventCount,
|
|
999
|
+
journalBytes: state.journalSignature?.size || 0,
|
|
1000
|
+
journalDigest: state.journalDigest,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function validateRecoveryAnchorLocked(state, anchor) {
|
|
1005
|
+
if (
|
|
1006
|
+
!anchor ||
|
|
1007
|
+
anchor.runId !== state.run.id ||
|
|
1008
|
+
!Number.isSafeInteger(anchor.lastSeq) ||
|
|
1009
|
+
anchor.lastSeq <= 0 ||
|
|
1010
|
+
!/^[a-f0-9]{64}$/.test(anchor.journalDigest || "")
|
|
1011
|
+
) {
|
|
1012
|
+
throw collaborationStoreError(
|
|
1013
|
+
"COLLABORATION_JOURNAL_ANCHOR_INVALID",
|
|
1014
|
+
`Invalid collaboration recovery anchor for ${state.run.id}`,
|
|
1015
|
+
{ runId: state.run.id },
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
if (anchor.lastSeq > state.lastSeq) {
|
|
1019
|
+
throw collaborationStoreError(
|
|
1020
|
+
"COLLABORATION_JOURNAL_DIVERGED",
|
|
1021
|
+
`Collaboration journal diverges from the recovery anchor for ${state.run.id}`,
|
|
1022
|
+
{ runId: state.run.id },
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const journalPath = collaborationRunJournalPath(state.run.id);
|
|
1027
|
+
const text = readFileSync(journalPath, "utf8");
|
|
1028
|
+
let digest = null;
|
|
1029
|
+
let matched = false;
|
|
1030
|
+
let eventCount = 0;
|
|
1031
|
+
let lastSeq = 0;
|
|
248
1032
|
try {
|
|
249
|
-
|
|
1033
|
+
if (!text.endsWith("\n")) {
|
|
1034
|
+
throw new Error("journal has no terminal newline");
|
|
1035
|
+
}
|
|
1036
|
+
for (const line of text.slice(0, -1).split("\n")) {
|
|
1037
|
+
digest = advanceJournalDigest(digest, `${line}\n`);
|
|
1038
|
+
const event = JSON.parse(line);
|
|
1039
|
+
eventCount += 1;
|
|
1040
|
+
lastSeq = event.seq;
|
|
1041
|
+
if (event.seq === anchor.lastSeq) {
|
|
1042
|
+
matched = digest === anchor.journalDigest;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
250
1045
|
} catch {
|
|
251
|
-
|
|
1046
|
+
throw collaborationStoreError(
|
|
1047
|
+
"COLLABORATION_JOURNAL_DIVERGED",
|
|
1048
|
+
`Collaboration journal diverges from the recovery anchor for ${state.run.id}`,
|
|
1049
|
+
{ runId: state.run.id },
|
|
1050
|
+
);
|
|
252
1051
|
}
|
|
1052
|
+
if (
|
|
1053
|
+
!matched ||
|
|
1054
|
+
eventCount !== state.eventCount ||
|
|
1055
|
+
lastSeq !== state.lastSeq ||
|
|
1056
|
+
digest !== state.journalDigest ||
|
|
1057
|
+
!sameSignature(fileSignature(journalPath), state.journalSignature)
|
|
1058
|
+
) {
|
|
1059
|
+
throw collaborationStoreError(
|
|
1060
|
+
"COLLABORATION_JOURNAL_DIVERGED",
|
|
1061
|
+
`Collaboration journal diverges from the recovery anchor for ${state.run.id}`,
|
|
1062
|
+
{ runId: state.run.id },
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/** Monotonic recovery anchor for an external team-state snapshot. */
|
|
1068
|
+
export function readCollaborationRunCursor(id) {
|
|
1069
|
+
const safeId = safeRunId(id);
|
|
1070
|
+
const target = collaborationRunPath(safeId);
|
|
1071
|
+
return withCollaborationRunLock(target, () => {
|
|
1072
|
+
const state = loadStateLocked(target, safeId);
|
|
1073
|
+
if (!state) return null;
|
|
1074
|
+
return cursorFromState(state);
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Atomically read governance state and prove that an external resume cursor is
|
|
1080
|
+
* an exact prefix of the current append-only journal.
|
|
1081
|
+
*/
|
|
1082
|
+
export function readCollaborationRunRecovery(id, { anchor } = {}) {
|
|
1083
|
+
const safeId = safeRunId(id);
|
|
1084
|
+
const target = collaborationRunPath(safeId);
|
|
1085
|
+
return withCollaborationRunLock(target, () => {
|
|
1086
|
+
const state = loadStateLocked(target, safeId);
|
|
1087
|
+
if (!state) return null;
|
|
1088
|
+
validateRecoveryAnchorLocked(state, anchor);
|
|
1089
|
+
return {
|
|
1090
|
+
run: cloneRun(state.run),
|
|
1091
|
+
cursor: cursorFromState(state),
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
253
1094
|
}
|
|
254
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* Compatibility path retained for callers that probe/update without changing
|
|
1098
|
+
* anything. Generic snapshots cannot safely distinguish governance authority
|
|
1099
|
+
* from mutable progress, so any actual mutation is rejected. Use the
|
|
1100
|
+
* capability-scoped unit/finalize APIs below for state transitions.
|
|
1101
|
+
*/
|
|
255
1102
|
export function updateCollaborationRun(id, updater) {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
1103
|
+
const safeId = safeRunId(id);
|
|
1104
|
+
const target = collaborationRunPath(safeId);
|
|
1105
|
+
return withCollaborationRunLock(target, () => {
|
|
1106
|
+
const state = loadStateLocked(target, safeId);
|
|
1107
|
+
if (!state) throw new Error(`Collaboration run not found: ${safeId}`);
|
|
1108
|
+
if (TERMINAL_RUN_STATUSES.has(state.run.status)) {
|
|
1109
|
+
throw collaborationStoreError(
|
|
1110
|
+
"COLLABORATION_RUN_TERMINAL",
|
|
1111
|
+
`Collaboration run is already terminal: ${safeId}`,
|
|
1112
|
+
{ runId: safeId },
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const draft = cloneRun(state.run);
|
|
1116
|
+
const changed =
|
|
1117
|
+
typeof updater === "function"
|
|
1118
|
+
? updater(draft) || draft
|
|
1119
|
+
: { ...draft, ...updater };
|
|
1120
|
+
if (!isDeepStrictEqual(changed, state.run)) {
|
|
1121
|
+
throw collaborationStoreError(
|
|
1122
|
+
"COLLABORATION_RUN_UPDATE_FORBIDDEN",
|
|
1123
|
+
`Generic updates cannot mutate collaboration run ${safeId}`,
|
|
1124
|
+
{ runId: safeId },
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
return cloneRun(state.run);
|
|
267
1128
|
});
|
|
268
1129
|
}
|
|
269
1130
|
|
|
270
|
-
|
|
1131
|
+
/**
|
|
1132
|
+
* Append a secret-free unit update. The legacy/default return value is the
|
|
1133
|
+
* complete run. Large schedulers can pass `{returnUnit:true}` to clone only the
|
|
1134
|
+
* updated unit and avoid O(unit-count) serialization after every append.
|
|
1135
|
+
*/
|
|
1136
|
+
export function updateCollaborationUnit(
|
|
1137
|
+
runId,
|
|
1138
|
+
key,
|
|
1139
|
+
patch = {},
|
|
1140
|
+
{ returnUnit = false } = {},
|
|
1141
|
+
) {
|
|
1142
|
+
const safeId = safeRunId(runId);
|
|
271
1143
|
const targetKey = boundedString(key, 256);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
: {}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
1144
|
+
const target = collaborationRunPath(safeId);
|
|
1145
|
+
return withCollaborationRunLock(target, () => {
|
|
1146
|
+
const state = loadStateLocked(target, safeId);
|
|
1147
|
+
if (!state) throw new Error(`Collaboration run not found: ${safeId}`);
|
|
1148
|
+
if (TERMINAL_RUN_STATUSES.has(state.run.status)) {
|
|
1149
|
+
throw collaborationStoreError(
|
|
1150
|
+
"COLLABORATION_RUN_TERMINAL",
|
|
1151
|
+
`Collaboration run is already terminal: ${safeId}`,
|
|
1152
|
+
{ runId: safeId },
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
if (!targetKey || !state.unitIndex.has(targetKey)) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`Collaboration unit not found: ${targetKey || "(empty)"}`,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
const unit = state.run.units[state.unitIndex.get(targetKey)];
|
|
1161
|
+
const normalizedPatch = buildUnitPatch(patch);
|
|
1162
|
+
if (TERMINAL_UNIT_STATUSES.has(unit.status)) {
|
|
1163
|
+
const changed = Object.entries(normalizedPatch).some(
|
|
1164
|
+
([field, value]) => !isDeepStrictEqual(unit[field], value),
|
|
1165
|
+
);
|
|
1166
|
+
if (changed) {
|
|
1167
|
+
throw collaborationStoreError(
|
|
1168
|
+
"COLLABORATION_UNIT_TERMINAL",
|
|
1169
|
+
`Collaboration unit ${targetKey} is already ${unit.status}`,
|
|
1170
|
+
{ runId: safeId, unitKey: targetKey, status: unit.status },
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return returnUnit === true ? cloneUnit(unit) : cloneRun(state.run);
|
|
1174
|
+
}
|
|
1175
|
+
return appendJournalEventLocked(
|
|
1176
|
+
state,
|
|
1177
|
+
target,
|
|
1178
|
+
{
|
|
1179
|
+
type: "unit.update",
|
|
1180
|
+
at: _deps.now(),
|
|
1181
|
+
key: targetKey,
|
|
1182
|
+
patch: normalizedPatch,
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
returnUnit: returnUnit === true,
|
|
1186
|
+
},
|
|
1187
|
+
);
|
|
305
1188
|
});
|
|
306
1189
|
}
|
|
307
1190
|
|
|
@@ -309,11 +1192,34 @@ export function finalizeCollaborationRun(id, status = "completed") {
|
|
|
309
1192
|
if (!TERMINAL_RUN_STATUSES.has(status)) {
|
|
310
1193
|
throw new Error(`Invalid terminal collaboration status: ${status}`);
|
|
311
1194
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
1195
|
+
const safeId = safeRunId(id);
|
|
1196
|
+
const target = collaborationRunPath(safeId);
|
|
1197
|
+
return withCollaborationRunLock(target, () => {
|
|
1198
|
+
const state = loadStateLocked(target, safeId);
|
|
1199
|
+
if (!state) throw new Error(`Collaboration run not found: ${safeId}`);
|
|
1200
|
+
if (TERMINAL_RUN_STATUSES.has(state.run.status)) {
|
|
1201
|
+
if (state.run.status === status) return cloneRun(state.run);
|
|
1202
|
+
throw collaborationStoreError(
|
|
1203
|
+
"COLLABORATION_RUN_TERMINAL",
|
|
1204
|
+
`Collaboration run ${safeId} is already ${state.run.status}`,
|
|
1205
|
+
{ runId: safeId },
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
if (
|
|
1209
|
+
status === "completed" &&
|
|
1210
|
+
state.run.units.some((unit) => unit.status !== "completed")
|
|
1211
|
+
) {
|
|
1212
|
+
throw collaborationStoreError(
|
|
1213
|
+
"COLLABORATION_RUN_INCOMPLETE",
|
|
1214
|
+
`Collaboration run ${safeId} has units that are not completed`,
|
|
1215
|
+
{ runId: safeId },
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
return appendJournalEventLocked(state, target, {
|
|
1219
|
+
type: "run.finalize",
|
|
1220
|
+
at: _deps.now(),
|
|
1221
|
+
status,
|
|
1222
|
+
});
|
|
317
1223
|
});
|
|
318
1224
|
}
|
|
319
1225
|
|
|
@@ -321,7 +1227,13 @@ export function listCollaborationRuns({ limit = 200 } = {}) {
|
|
|
321
1227
|
const capped = Math.max(1, Math.min(1000, Number(limit) || 200));
|
|
322
1228
|
return readdirSync(collaborationRunsDir())
|
|
323
1229
|
.filter((name) => /^(team|batch)-[a-zA-Z0-9._-]+\.json$/.test(name))
|
|
324
|
-
.map((name) =>
|
|
1230
|
+
.map((name) => {
|
|
1231
|
+
try {
|
|
1232
|
+
return readCollaborationRun(name.slice(0, -5));
|
|
1233
|
+
} catch {
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
})
|
|
325
1237
|
.filter(Boolean)
|
|
326
1238
|
.sort((a, b) => b.startedAt - a.startedAt)
|
|
327
1239
|
.slice(0, capped);
|
|
@@ -350,8 +1262,11 @@ export function projectCollaborationTasks(runs) {
|
|
|
350
1262
|
version: 1,
|
|
351
1263
|
owner: unit.owner,
|
|
352
1264
|
sessionId: unit.sessionId,
|
|
353
|
-
permissionMode:
|
|
354
|
-
resourceBudget:
|
|
1265
|
+
permissionMode: unit.permissionMode,
|
|
1266
|
+
resourceBudget: unit.resourceBudget,
|
|
1267
|
+
checkpointRequired: unit.checkpointRequired,
|
|
1268
|
+
worktreeRequired: unit.worktreeRequired,
|
|
1269
|
+
scopePaths: unit.scopePaths,
|
|
355
1270
|
},
|
|
356
1271
|
sideEffects: summarizeSideEffects(unit.sideEffects),
|
|
357
1272
|
});
|