chainlesschain 0.163.4 → 0.163.5
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-DB5X7qqV.js → AIOps-Cv2EE1Zy.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DxHRtKLk.js → ActionButton-CcpxMLuW.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DIH5QrxT.js → Analytics-DE8mJoMI.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-uu0oxVz7.js → AppLayout-DKn9dk9L.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BNSMiW9Y.js → Artifacts-Cp3mG3Py.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DcAzB4_x.js → Audit-zvsQfF2C.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-ClSsvjIc.js → BackgroundAgents-Bi85SsjN.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BMciY0js.js → Backup-Cw47Dj6s.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Hn3b57Sh.js → BaseInput-C5Ip2afH.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CoSlC53V.js → Chat-C0iAFkJv.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CFevJ3So.js → ChatBubbleRenderer-BV9Wv7wO.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-B4L4Eu4w.js → Checkbox-3KZZCiAF.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-C77FgRxF.js → Codegen-C8OMbBAz.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BrLtJLH3.js → Col-B-MB84LJ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Be7pXXhA.js → Community-DpR7Dxao.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DuGPecjo.js → Compact-CyDtz0OS.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CzS66mjP.js → Compliance-5tiRa2An.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CEhjCoyK.js → Cowork-DnQaKZ5x.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-D7ir8Vja.js → Cron-BaWfoEDf.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BcXmuLxf.js → Crosschain-Bu0HoHhb.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cto1dtBy.js → DID-31zk8upX.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-B7KjgwOX.js → Dashboard-BmVne-e2.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-COMqxijg.js → Dropdown-B4PkoOWd.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BZrya-nf.js → EmailListRenderer-BZ6tyFtI.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-AmVeMq0J.js → FamilyGuardDashboard-Osozn4Ds.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-kZU8c-27.js → Federation-DaH8TOAP.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-sOV22v2-.js → FormItemContext-tbU0uwis.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BUuLtGCz.js → GenericCardRenderer-Df8LQBrL.js} +1 -1
- package/src/assets/web-panel/assets/{Git-K-ZxsLsH.js → Git-DsMB6-14.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-D3OEnawI.js → Governance-BnH0dZzd.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bll0gRmP.js → Inference-DhnN1N_P.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DBXwjr53.js → KnowledgeGraph-DnNshAgn.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CEzPwEfP.js → Logs-BmBFX3_N.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DCslfQU3.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BYyRyOPz.js → Marketplace-CPEgWDc3.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DVu8w_rM.js → McpTools-C36qjgF9.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-tenuZ_hO.js → Memory-tNHvrgqN.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Bxgy4ozS.js → MobileBridge-Xaf6cKjc.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Dgx1D6sc.js → MobileProjects-D2E5TTeO.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CRfJ8r8j.js → Mtc-D4AJ2dwV.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BBJq4Aiz.js → MtcAudit-Df-gtn2_.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-B3ii9lai.js → Multisig-DTCHLWld.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-FIi-Cubv.js → NLProgramming-DruZ_1Jb.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DIq8MPf1.js → Notes-Dx3pdGDs.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-BFOD9eGd.js → NotificationSettings-Bi3L69oL.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer--41u7vre.js → OrderTableRenderer-BHY33m7C.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B_0AWztW.js → Organization-BG2ZVEV-.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-B6I7TuXv.js → Overflow-Viwtg4Mn.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cdf6qens.js → P2P-Clm_yc1Z.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-B_0FcQJY.js → PdhVaultBrowser-NKmmDiOw.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-6nFBjsQp.js → Permissions-DIL8u9Nk.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-nQYV7xpn.js → PersonalDataHub-B-jM4wt-.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-CBVod5jm.js → Pipeline-627I__H5.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-D52jW1Nt.js → Privacy-2DN2qrkW.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Bdx3BYhf.js → ProjectInit-CN4wC0sF.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-b7xr4DUa.js → ProjectSettings-K35vsh5H.js} +2 -2
- package/src/assets/web-panel/assets/Projects-B_wQadhX.js +1 -0
- package/src/assets/web-panel/assets/Providers-MckaQUhQ.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-FQKSmc3e.js → QrScannerModal-BS3mtA_O.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CSTrhzcF.js → QuickAsk-BHp_nofL.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CxMABOUh.js → Recommend-DlmnPuZj.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Cv2T8yRa.js → RemoteSession-tsyFbvpy.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CE1k4Vvu.js → Reputation-Ba1iWv2D.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BNgNcAV_.js → Row-NnlGC--f.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-5ZiBZ3ph.js → RssFeed-CjThSIRv.js} +3 -3
- package/src/assets/web-panel/assets/{Search-mDOIF1UV.js → Search-CojUyC5x.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CtuCTC-e.js → Security-c-sxeJEc.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DxFO2Zg7.js → Services-CtcKEEmF.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CKmKkH9y.js → Skeleton-CIBBXflN.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-Bmzl9qkk.js → Skills-D1XOOK9n.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-9RqaWHOC.js → Sla-hRfEp5Sn.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CGdzFIYw.js → SpeechSettings-Dexb1f_6.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CvNuCeBT.js → SyncSettings-BJsSFGYy.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-B_sLdcnw.js → Tasks-CfSZ8A1L.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-D9_YijdK.js → Templates-idDf47W-.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Cuc7BUSP.js → Tenant-C-UsX6w6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-6VYRnJgd.js → Terminal-BgJtX5n1.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-GtK8xd8Y.js → TimelineRenderer-DP0BLKTl.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BZ94Lt5W.js → Tokens-LwCzqETp.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DEcZgDaX.js → Trigger-D_9tVch0.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CC3z9THe.js → Trust-BH2UlEWh.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Cvtw8cV3.js → UkeySign-CIpD0nYb.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CL4e6Bn3.js → VideoEditing-9ShhYxK_.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-ycEemP27.js → Wallet-C7afGNIG.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DgZ4X_aw.js → WebAuthn-bUKn6i5R.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-D0qMhSv7.js → WorkflowEditor-CxgQaGyp.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DsAg01Zn.js → chat-DMVkmJwx.js} +1 -1
- package/src/assets/web-panel/assets/{colors-B0oqAhrQ.js → colors-CDzQh5nS.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CmX0Rabz.js → compact-item-C51kEF4D.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Be4cn1Ub.js → createContext-BTk9bXBn.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CdwPwYj8.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D5v12kVq.js → hasIn-CMje33rb.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyyGTMKd.js → index-0Auljb10.js} +1 -1
- package/src/assets/web-panel/assets/{index-frSojpHj.js → index-B0-zMtKA.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3mUHuVc.js → index-BAfY7m7i.js} +1 -1
- package/src/assets/web-panel/assets/{index-BA4oulaP.js → index-BBz9LIMS.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1YGjOWC.js → index-BCRvG9lq.js} +1 -1
- package/src/assets/web-panel/assets/index-BEeOdfEe.js +1 -0
- package/src/assets/web-panel/assets/index-BTe0TOu6.js +1 -0
- package/src/assets/web-panel/assets/{index-BBvyHguy.js → index-BTlsE8Df.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGiIyTZw.js → index-BVePe_Rf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBREnn1V.js → index-BW4F5YDF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0BSkxYQ.js → index-BXQBPLyE.js} +1 -1
- package/src/assets/web-panel/assets/{index-0eWLkQKT.js → index-BeEaAEIp.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5YVopyr.js → index-BtlEqHLP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKmsS2jj.js → index-BzCkNylB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaE0Ui07.js → index-C1oZQmAA.js} +1 -1
- package/src/assets/web-panel/assets/{index-8WvYGnON.js → index-C1oZReaY.js} +1 -1
- package/src/assets/web-panel/assets/{index-_JjRLSD0.js → index-C4rQCS8T.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKUBj36_.js → index-C5IWTMVl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoAIAZ5C.js → index-CCLj9hcD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DikJ0Qqe.js → index-CDLkNkLj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Hfe_dEGx.js → index-Cd0hmPxD.js} +1 -1
- package/src/assets/web-panel/assets/{index-jK3ax6Z6.js → index-CfC-piWZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyUaFWEk.js → index-CtM8OT_7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqltWojQ.js → index-CvSeVzqe.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn2dSdXx.js → index-CwGPwjUu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRIqLy1m.js → index-CypsfcEq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXGSbelg.js → index-D9rC-fGW.js} +3 -3
- package/src/assets/web-panel/assets/{index-CZpfzcuy.js → index-DJfnCuMq.js} +1 -1
- package/src/assets/web-panel/assets/{index-5LGY0JbG.js → index-DRG-aqOs.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyWBir7N.js → index-DRLQyp2A.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQRsQen0.js → index-DgivOKHA.js} +1 -1
- package/src/assets/web-panel/assets/{index-7VKDn-t1.js → index-DpcrFxMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-q_fQ94SE.js → index-DqU9njjT.js} +1 -1
- package/src/assets/web-panel/assets/{index-301ogenv.js → index-Ld_4mFmC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZ2wUEgr.js → index-OS-vfbTA.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqoCZ5vR.js → index-h31MwrME.js} +1 -1
- package/src/assets/web-panel/assets/{index-DM724NgU.js → index-hg_emBsi.js} +1 -1
- package/src/assets/web-panel/assets/{index-qMuZ-NRR.js → index-togc_uYn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ckrk3pIV.js → index-x53Vf7la.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D4VdhJ9_.js → initDefaultProps-vrA8K-ac.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CZJA0MQW.js → motion-ByEzwIAl.js} +1 -1
- package/src/assets/web-panel/assets/{move-BqEs7Sb1.js → move-Ck4AWjw-.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BpHEtsu1.js → mtc-parser-6p0l9SrB.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BW94tY9W.js → omit-B1CUPQ6R.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BnBjPWP9.js → pickAttrs-B1STg1iW.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-uUkjQ_hu.js → placementArrow-DTTllkBV.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B-klUqXK.js → responsiveObserve-AIrrCfBb.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CvY1cNAs.js → slide-C_fPMjdR.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Ix5sEDT2.js → statusUtils-D5ow2yIP.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C8qcXOne.js → styleChecker-Dy3hEA--.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-KNF6Kj8f.js → useFlexGapSupport-WfzcfSh4.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DGJr345n.js → useFs-CKSejfas.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BboN99rI.js → usePersonalDataHub-od3zpPBN.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Bgt8Vgte.js → vnode-DRD_2DFA.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BhJtErgt.js → zoom-DmNrEtgf.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +52 -6
- package/src/commands/cowork.js +24 -12
- package/src/commands/routine.js +40 -10
- package/src/data/changelog.json +13 -0
- package/src/lib/agent-schedule-store.js +197 -4
- package/src/lib/cowork-cron.js +243 -25
- package/src/lib/micro-compact.js +11 -5
- package/src/lib/process-execution-broker/index.js +14 -3
- package/src/lib/routine-store.js +76 -3
- package/src/lib/scheduler-kernel/agenda-adapter.js +542 -0
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +774 -0
- package/src/lib/scheduler-kernel/routine-adapter.js +517 -0
- package/src/lib/scheduler-kernel/runtime.js +527 -0
- package/src/lib/scheduler-kernel/store.js +112 -2
- package/src/lib/session-message-provenance.js +4 -1
- package/src/repl/agent-repl.js +24 -5
- package/src/runtime/agent-core.js +36 -16
- package/src/assets/web-panel/assets/MarkdownRenderer-CLmCbHx6.js +0 -1
- package/src/assets/web-panel/assets/Projects-DhfifCKx.js +0 -1
- package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +0 -1
- package/src/assets/web-panel/assets/index-CKB0vbJU.js +0 -1
- package/src/assets/web-panel/assets/index-DgoXmwqB.js +0 -1
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
OCCURRENCE_STATUS,
|
|
4
|
+
SchedulerKernelError,
|
|
5
|
+
normalizeEpochMs,
|
|
6
|
+
normalizeIdentifier,
|
|
7
|
+
normalizeJson,
|
|
8
|
+
} from "./contract.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_RUNTIME_LEASE_MS = 60_000;
|
|
11
|
+
export const MIN_RUNTIME_LEASE_MS = 1_000;
|
|
12
|
+
export const DEFAULT_RUN_LIMIT = 100;
|
|
13
|
+
export const MAX_RUN_LIMIT = 10_000;
|
|
14
|
+
|
|
15
|
+
const TERMINAL_OCCURRENCE_STATUSES = new Set([
|
|
16
|
+
OCCURRENCE_STATUS.SUCCEEDED,
|
|
17
|
+
OCCURRENCE_STATUS.DEAD_LETTER,
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function runtimeError(code, message, details = undefined, cause = undefined) {
|
|
21
|
+
return new SchedulerKernelError(
|
|
22
|
+
code,
|
|
23
|
+
message,
|
|
24
|
+
details,
|
|
25
|
+
cause ? { cause } : undefined,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertStore(store) {
|
|
30
|
+
const required = [
|
|
31
|
+
"claimNext",
|
|
32
|
+
"claimOccurrence",
|
|
33
|
+
"getJob",
|
|
34
|
+
"getOccurrence",
|
|
35
|
+
"renew",
|
|
36
|
+
"settle",
|
|
37
|
+
];
|
|
38
|
+
if (
|
|
39
|
+
!store ||
|
|
40
|
+
required.some((method) => typeof store[method] !== "function")
|
|
41
|
+
) {
|
|
42
|
+
throw runtimeError(
|
|
43
|
+
"SCHEDULER_RUNTIME_INVALID_STORE",
|
|
44
|
+
"Scheduler runtime requires a compatible scheduler store",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return store;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeLeaseMs(value = DEFAULT_RUNTIME_LEASE_MS) {
|
|
51
|
+
if (!Number.isSafeInteger(value) || value < MIN_RUNTIME_LEASE_MS) {
|
|
52
|
+
throw runtimeError(
|
|
53
|
+
"SCHEDULER_RUNTIME_INVALID_LEASE",
|
|
54
|
+
`leaseMs must be an integer >= ${MIN_RUNTIME_LEASE_MS}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeRenewInterval(value, leaseMs) {
|
|
61
|
+
const interval =
|
|
62
|
+
value === undefined ? Math.max(250, Math.floor(leaseMs / 3)) : value;
|
|
63
|
+
if (!Number.isSafeInteger(interval) || interval < 1 || interval >= leaseMs) {
|
|
64
|
+
throw runtimeError(
|
|
65
|
+
"SCHEDULER_RUNTIME_INVALID_RENEW_INTERVAL",
|
|
66
|
+
"renewIntervalMs must be a positive integer smaller than leaseMs",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return interval;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeRunLimit(value = DEFAULT_RUN_LIMIT) {
|
|
73
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > MAX_RUN_LIMIT) {
|
|
74
|
+
throw runtimeError(
|
|
75
|
+
"SCHEDULER_RUNTIME_INVALID_LIMIT",
|
|
76
|
+
`limit must be an integer between 1 and ${MAX_RUN_LIMIT}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeAdapter(adapter) {
|
|
83
|
+
if (!adapter || typeof adapter !== "object" || Array.isArray(adapter)) {
|
|
84
|
+
throw runtimeError(
|
|
85
|
+
"SCHEDULER_RUNTIME_INVALID_ADAPTER",
|
|
86
|
+
"Scheduler adapter must be an object",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const kind = normalizeIdentifier(adapter.kind, "adapter.kind", {
|
|
90
|
+
maxLength: 128,
|
|
91
|
+
});
|
|
92
|
+
if (typeof adapter.execute !== "function") {
|
|
93
|
+
throw runtimeError(
|
|
94
|
+
"SCHEDULER_RUNTIME_INVALID_ADAPTER",
|
|
95
|
+
`Scheduler adapter must implement execute(): ${kind}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return { kind, adapter };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeAdapters(adapters = []) {
|
|
102
|
+
const map = new Map();
|
|
103
|
+
const values = adapters instanceof Map ? adapters.values() : adapters;
|
|
104
|
+
if (!values || typeof values[Symbol.iterator] !== "function") {
|
|
105
|
+
throw runtimeError(
|
|
106
|
+
"SCHEDULER_RUNTIME_INVALID_ADAPTER",
|
|
107
|
+
"adapters must be iterable",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
for (const value of values) {
|
|
111
|
+
const { kind, adapter } = normalizeAdapter(value);
|
|
112
|
+
if (map.has(kind)) {
|
|
113
|
+
throw runtimeError(
|
|
114
|
+
"SCHEDULER_RUNTIME_DUPLICATE_ADAPTER",
|
|
115
|
+
`Scheduler adapter is registered more than once: ${kind}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
map.set(kind, adapter);
|
|
119
|
+
}
|
|
120
|
+
return map;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function safeError(error, fallbackCode = "execution_failed") {
|
|
124
|
+
const code =
|
|
125
|
+
typeof error?.code === "string" && error.code.trim()
|
|
126
|
+
? error.code.trim().slice(0, 128)
|
|
127
|
+
: fallbackCode;
|
|
128
|
+
const message =
|
|
129
|
+
typeof error?.message === "string" && error.message
|
|
130
|
+
? error.message.slice(0, 2_000)
|
|
131
|
+
: String(error ?? "Execution failed").slice(0, 2_000);
|
|
132
|
+
let details;
|
|
133
|
+
try {
|
|
134
|
+
if (error?.details !== undefined) {
|
|
135
|
+
details = normalizeJson(error.details, "runtime.error.details");
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
details = { omitted: "invalid_error_details" };
|
|
139
|
+
}
|
|
140
|
+
const base = {
|
|
141
|
+
code,
|
|
142
|
+
message,
|
|
143
|
+
...(typeof error?.name === "string"
|
|
144
|
+
? { name: error.name.slice(0, 128) }
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
return normalizeJson(
|
|
149
|
+
{ ...base, ...(details === undefined ? {} : { details }) },
|
|
150
|
+
"runtime.error",
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
return normalizeJson(
|
|
154
|
+
{ ...base, details: { omitted: "error_details_too_large" } },
|
|
155
|
+
"runtime.error",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function executionPolicy(error, adapter, context) {
|
|
161
|
+
let policy = null;
|
|
162
|
+
if (typeof adapter?.classifyError === "function") {
|
|
163
|
+
policy = adapter.classifyError(error, context);
|
|
164
|
+
}
|
|
165
|
+
if (policy !== null && policy !== undefined) {
|
|
166
|
+
if (typeof policy !== "object" || Array.isArray(policy)) {
|
|
167
|
+
throw runtimeError(
|
|
168
|
+
"SCHEDULER_RUNTIME_INVALID_ERROR_POLICY",
|
|
169
|
+
"adapter.classifyError() must return an object",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
retryable: policy.retryable !== false,
|
|
174
|
+
...(policy.retryAt === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: { retryAt: normalizeEpochMs(policy.retryAt, "retryAt") }),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
retryable: error?.retryable !== false,
|
|
181
|
+
...(error?.retryAt === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { retryAt: normalizeEpochMs(error.retryAt, "retryAt") }),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createLinkedAbortController(signal) {
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const abort = () => controller.abort(signal?.reason);
|
|
190
|
+
if (signal?.aborted) abort();
|
|
191
|
+
else signal?.addEventListener?.("abort", abort, { once: true });
|
|
192
|
+
return {
|
|
193
|
+
controller,
|
|
194
|
+
dispose() {
|
|
195
|
+
signal?.removeEventListener?.("abort", abort);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function settledResult(occurrence, extra = {}) {
|
|
201
|
+
return {
|
|
202
|
+
status: occurrence.status,
|
|
203
|
+
occurrence,
|
|
204
|
+
result: occurrence.result,
|
|
205
|
+
error: occurrence.lastError,
|
|
206
|
+
...extra,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Host-owned execution service for scheduler occurrences.
|
|
212
|
+
*
|
|
213
|
+
* The runtime is deliberately stricter than the SQLite store: it resolves the
|
|
214
|
+
* exact job revision, requires an explicit authorization decision, selects a
|
|
215
|
+
* registered adapter, renews the durable lease while the adapter is running,
|
|
216
|
+
* and settles only under the same owner/fence token. Adapters receive the
|
|
217
|
+
* occurrence snapshots, never mutable host-side schedule definitions by
|
|
218
|
+
* implication.
|
|
219
|
+
*/
|
|
220
|
+
export class SchedulerRuntime {
|
|
221
|
+
constructor({
|
|
222
|
+
store,
|
|
223
|
+
adapters = [],
|
|
224
|
+
authorize,
|
|
225
|
+
ownerId = `scheduler-runtime:${process.pid}:${randomUUID()}`,
|
|
226
|
+
leaseMs = DEFAULT_RUNTIME_LEASE_MS,
|
|
227
|
+
renewIntervalMs,
|
|
228
|
+
setIntervalFn = setInterval,
|
|
229
|
+
clearIntervalFn = clearInterval,
|
|
230
|
+
} = {}) {
|
|
231
|
+
this.store = assertStore(store);
|
|
232
|
+
this.adapters = normalizeAdapters(adapters);
|
|
233
|
+
if (typeof authorize !== "function") {
|
|
234
|
+
throw runtimeError(
|
|
235
|
+
"SCHEDULER_RUNTIME_AUTHORIZER_REQUIRED",
|
|
236
|
+
"Scheduler runtime requires an explicit authorize() callback",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
this.authorize = authorize;
|
|
240
|
+
this.ownerId = normalizeIdentifier(ownerId, "ownerId");
|
|
241
|
+
this.leaseMs = normalizeLeaseMs(leaseMs);
|
|
242
|
+
this.renewIntervalMs = normalizeRenewInterval(
|
|
243
|
+
renewIntervalMs,
|
|
244
|
+
this.leaseMs,
|
|
245
|
+
);
|
|
246
|
+
if (
|
|
247
|
+
typeof setIntervalFn !== "function" ||
|
|
248
|
+
typeof clearIntervalFn !== "function"
|
|
249
|
+
) {
|
|
250
|
+
throw runtimeError(
|
|
251
|
+
"SCHEDULER_RUNTIME_INVALID_TIMER",
|
|
252
|
+
"Scheduler runtime requires timer functions",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
this.setIntervalFn = setIntervalFn;
|
|
256
|
+
this.clearIntervalFn = clearIntervalFn;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
registerAdapter(adapter) {
|
|
260
|
+
const normalized = normalizeAdapter(adapter);
|
|
261
|
+
if (this.adapters.has(normalized.kind)) {
|
|
262
|
+
throw runtimeError(
|
|
263
|
+
"SCHEDULER_RUNTIME_DUPLICATE_ADAPTER",
|
|
264
|
+
`Scheduler adapter is registered more than once: ${normalized.kind}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
this.adapters.set(normalized.kind, normalized.adapter);
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async _settleRejected(occurrence, error, retryable = false) {
|
|
272
|
+
const settled = this.store.settle({
|
|
273
|
+
occurrenceId: occurrence.id,
|
|
274
|
+
ownerId: this.ownerId,
|
|
275
|
+
fence: occurrence.fence,
|
|
276
|
+
outcome: "failed",
|
|
277
|
+
error: safeError(error, "scheduler_rejected"),
|
|
278
|
+
retryable,
|
|
279
|
+
});
|
|
280
|
+
return settledResult(settled);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async _executeClaim(occurrence, { signal } = {}) {
|
|
284
|
+
const job = this.store.getJob(occurrence.jobId);
|
|
285
|
+
if (!job) {
|
|
286
|
+
return this._settleRejected(
|
|
287
|
+
occurrence,
|
|
288
|
+
runtimeError(
|
|
289
|
+
"SCHEDULER_RUNTIME_JOB_MISSING",
|
|
290
|
+
`Scheduler job disappeared before execution: ${occurrence.jobId}`,
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (job.revision !== occurrence.jobRevision) {
|
|
295
|
+
return this._settleRejected(
|
|
296
|
+
occurrence,
|
|
297
|
+
runtimeError(
|
|
298
|
+
"SCHEDULER_RUNTIME_STALE_REVISION",
|
|
299
|
+
`Occurrence references stale job revision: ${occurrence.id}`,
|
|
300
|
+
{
|
|
301
|
+
occurrenceRevision: occurrence.jobRevision,
|
|
302
|
+
currentRevision: job.revision,
|
|
303
|
+
},
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const adapter = this.adapters.get(job.kind);
|
|
308
|
+
if (!adapter) {
|
|
309
|
+
return this._settleRejected(
|
|
310
|
+
occurrence,
|
|
311
|
+
runtimeError(
|
|
312
|
+
"SCHEDULER_RUNTIME_ADAPTER_MISSING",
|
|
313
|
+
`No scheduler adapter is registered for job kind: ${job.kind}`,
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let decision;
|
|
319
|
+
let allowed = false;
|
|
320
|
+
let deniedReason = "not_allowed";
|
|
321
|
+
try {
|
|
322
|
+
decision = await this.authorize({ job, occurrence, signal });
|
|
323
|
+
allowed =
|
|
324
|
+
decision !== null &&
|
|
325
|
+
typeof decision === "object" &&
|
|
326
|
+
decision.allowed === true;
|
|
327
|
+
if (typeof decision?.reason === "string") {
|
|
328
|
+
deniedReason = decision.reason.slice(0, 256);
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const authError = runtimeError(
|
|
332
|
+
"SCHEDULER_RUNTIME_AUTHORIZATION_UNAVAILABLE",
|
|
333
|
+
"Scheduler authorization could not be completed",
|
|
334
|
+
undefined,
|
|
335
|
+
error,
|
|
336
|
+
);
|
|
337
|
+
authError.retryable = true;
|
|
338
|
+
return this._settleRejected(occurrence, authError, true);
|
|
339
|
+
}
|
|
340
|
+
if (!allowed) {
|
|
341
|
+
return this._settleRejected(
|
|
342
|
+
occurrence,
|
|
343
|
+
runtimeError(
|
|
344
|
+
"SCHEDULER_RUNTIME_AUTHORIZATION_DENIED",
|
|
345
|
+
"Scheduler occurrence was denied by the authority resolver",
|
|
346
|
+
{ reason: deniedReason },
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Authorization may involve remote policy evaluation. Revalidate ownership
|
|
352
|
+
// immediately before any adapter side effect so an expired/reclaimed claim
|
|
353
|
+
// cannot execute under a stale fence.
|
|
354
|
+
occurrence = this.store.renew({
|
|
355
|
+
occurrenceId: occurrence.id,
|
|
356
|
+
ownerId: this.ownerId,
|
|
357
|
+
fence: occurrence.fence,
|
|
358
|
+
leaseMs: this.leaseMs,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const linked = createLinkedAbortController(signal);
|
|
362
|
+
if (linked.controller.signal.aborted) {
|
|
363
|
+
linked.dispose();
|
|
364
|
+
const aborted = runtimeError(
|
|
365
|
+
"SCHEDULER_RUNTIME_ABORTED",
|
|
366
|
+
"Scheduler occurrence was aborted before adapter execution",
|
|
367
|
+
);
|
|
368
|
+
aborted.retryable = true;
|
|
369
|
+
return this._settleRejected(occurrence, aborted, true);
|
|
370
|
+
}
|
|
371
|
+
let leaseError = null;
|
|
372
|
+
let renewing = false;
|
|
373
|
+
const renew = () => {
|
|
374
|
+
if (renewing || leaseError || linked.controller.signal.aborted) return;
|
|
375
|
+
renewing = true;
|
|
376
|
+
try {
|
|
377
|
+
this.store.renew({
|
|
378
|
+
occurrenceId: occurrence.id,
|
|
379
|
+
ownerId: this.ownerId,
|
|
380
|
+
fence: occurrence.fence,
|
|
381
|
+
leaseMs: this.leaseMs,
|
|
382
|
+
});
|
|
383
|
+
} catch (error) {
|
|
384
|
+
leaseError = error;
|
|
385
|
+
linked.controller.abort(error);
|
|
386
|
+
} finally {
|
|
387
|
+
renewing = false;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
let timer;
|
|
391
|
+
try {
|
|
392
|
+
timer = this.setIntervalFn(renew, this.renewIntervalMs);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
linked.dispose();
|
|
395
|
+
return this._settleRejected(
|
|
396
|
+
occurrence,
|
|
397
|
+
runtimeError(
|
|
398
|
+
"SCHEDULER_RUNTIME_TIMER_FAILED",
|
|
399
|
+
"Scheduler lease-renewal timer could not be started",
|
|
400
|
+
undefined,
|
|
401
|
+
error,
|
|
402
|
+
),
|
|
403
|
+
true,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
timer?.unref?.();
|
|
407
|
+
const context = {
|
|
408
|
+
job,
|
|
409
|
+
occurrence,
|
|
410
|
+
authority: occurrence.authority,
|
|
411
|
+
decision,
|
|
412
|
+
signal: linked.controller.signal,
|
|
413
|
+
renewLease: renew,
|
|
414
|
+
};
|
|
415
|
+
try {
|
|
416
|
+
const result = await adapter.execute(context);
|
|
417
|
+
if (leaseError) throw leaseError;
|
|
418
|
+
if (linked.controller.signal.aborted) {
|
|
419
|
+
const aborted = runtimeError(
|
|
420
|
+
"SCHEDULER_RUNTIME_ABORTED",
|
|
421
|
+
"Scheduler occurrence was aborted before settlement",
|
|
422
|
+
);
|
|
423
|
+
aborted.retryable = true;
|
|
424
|
+
throw aborted;
|
|
425
|
+
}
|
|
426
|
+
const settled = this.store.settle({
|
|
427
|
+
occurrenceId: occurrence.id,
|
|
428
|
+
ownerId: this.ownerId,
|
|
429
|
+
fence: occurrence.fence,
|
|
430
|
+
outcome: "succeeded",
|
|
431
|
+
result: result ?? null,
|
|
432
|
+
});
|
|
433
|
+
return settledResult(settled);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (leaseError) throw leaseError;
|
|
436
|
+
let policy;
|
|
437
|
+
let settlementError = error;
|
|
438
|
+
try {
|
|
439
|
+
policy = executionPolicy(error, adapter, context);
|
|
440
|
+
} catch (classificationError) {
|
|
441
|
+
settlementError = runtimeError(
|
|
442
|
+
"SCHEDULER_RUNTIME_ERROR_POLICY_FAILED",
|
|
443
|
+
"Scheduler adapter error policy failed",
|
|
444
|
+
undefined,
|
|
445
|
+
classificationError,
|
|
446
|
+
);
|
|
447
|
+
policy = { retryable: false };
|
|
448
|
+
}
|
|
449
|
+
const settled = this.store.settle({
|
|
450
|
+
occurrenceId: occurrence.id,
|
|
451
|
+
ownerId: this.ownerId,
|
|
452
|
+
fence: occurrence.fence,
|
|
453
|
+
outcome: "failed",
|
|
454
|
+
error: safeError(settlementError),
|
|
455
|
+
retryable: policy.retryable,
|
|
456
|
+
...(policy.retryAt === undefined ? {} : { retryAt: policy.retryAt }),
|
|
457
|
+
});
|
|
458
|
+
return settledResult(settled);
|
|
459
|
+
} finally {
|
|
460
|
+
this.clearIntervalFn(timer);
|
|
461
|
+
linked.dispose();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async runOccurrence(occurrenceId, options = {}) {
|
|
466
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
467
|
+
const current = this.store.getOccurrence(id);
|
|
468
|
+
if (!current) {
|
|
469
|
+
throw runtimeError(
|
|
470
|
+
"SCHEDULER_NOT_FOUND",
|
|
471
|
+
`Scheduler occurrence does not exist: ${id}`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (TERMINAL_OCCURRENCE_STATUSES.has(current.status)) {
|
|
475
|
+
return settledResult(current, { alreadySettled: true });
|
|
476
|
+
}
|
|
477
|
+
if (options.signal?.aborted) {
|
|
478
|
+
return { status: "aborted", occurrence: current };
|
|
479
|
+
}
|
|
480
|
+
const claimed = this.store.claimOccurrence({
|
|
481
|
+
occurrenceId: id,
|
|
482
|
+
ownerId: this.ownerId,
|
|
483
|
+
leaseMs: this.leaseMs,
|
|
484
|
+
});
|
|
485
|
+
if (!claimed) {
|
|
486
|
+
return {
|
|
487
|
+
status: "busy",
|
|
488
|
+
occurrence: this.store.getOccurrence(id),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return this._executeClaim(claimed, options);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async runNext(options = {}) {
|
|
495
|
+
if (options.signal?.aborted) return { status: "aborted" };
|
|
496
|
+
const claimed = this.store.claimNext({
|
|
497
|
+
ownerId: this.ownerId,
|
|
498
|
+
leaseMs: this.leaseMs,
|
|
499
|
+
...(options.jobKind === undefined ? {} : { jobKind: options.jobKind }),
|
|
500
|
+
...(options.workspaceId === undefined
|
|
501
|
+
? {}
|
|
502
|
+
: { workspaceId: options.workspaceId }),
|
|
503
|
+
});
|
|
504
|
+
if (!claimed) return { status: "idle" };
|
|
505
|
+
return this._executeClaim(claimed, options);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async runUntilIdle({ limit, signal, jobKind, workspaceId } = {}) {
|
|
509
|
+
const bounded = normalizeRunLimit(limit);
|
|
510
|
+
const results = [];
|
|
511
|
+
while (results.length < bounded && !signal?.aborted) {
|
|
512
|
+
const result = await this.runNext({ signal, jobKind, workspaceId });
|
|
513
|
+
if (result.status === "idle" || result.status === "aborted") {
|
|
514
|
+
return { status: result.status, results };
|
|
515
|
+
}
|
|
516
|
+
results.push(result);
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
status: signal?.aborted ? "aborted" : "limit_reached",
|
|
520
|
+
results,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function createSchedulerRuntime(options) {
|
|
526
|
+
return new SchedulerRuntime(options);
|
|
527
|
+
}
|
|
@@ -1003,9 +1003,17 @@ export class SchedulerStore {
|
|
|
1003
1003
|
}
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
|
-
claimNext({ ownerId, leaseMs } = {}) {
|
|
1006
|
+
claimNext({ ownerId, leaseMs, jobKind, workspaceId } = {}) {
|
|
1007
1007
|
const owner = normalizeIdentifier(ownerId, "ownerId");
|
|
1008
1008
|
const lease = normalizeLeaseMs(leaseMs);
|
|
1009
|
+
const kind =
|
|
1010
|
+
jobKind === undefined
|
|
1011
|
+
? null
|
|
1012
|
+
: normalizeIdentifier(jobKind, "jobKind", { maxLength: 128 });
|
|
1013
|
+
const workspace =
|
|
1014
|
+
workspaceId === undefined
|
|
1015
|
+
? null
|
|
1016
|
+
: normalizeIdentifier(workspaceId, "workspaceId", { maxLength: 256 });
|
|
1009
1017
|
const now = this._now();
|
|
1010
1018
|
const leaseExpiresAt = now + lease;
|
|
1011
1019
|
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
@@ -1020,6 +1028,11 @@ export class SchedulerStore {
|
|
|
1020
1028
|
FROM occurrences o
|
|
1021
1029
|
JOIN jobs j ON j.job_id = o.job_id
|
|
1022
1030
|
WHERE j.enabled = 1
|
|
1031
|
+
AND (@jobKind IS NULL OR j.kind = @jobKind)
|
|
1032
|
+
AND (
|
|
1033
|
+
@workspaceId IS NULL
|
|
1034
|
+
OR json_extract(j.authority_json, '$.workspaceId') = @workspaceId
|
|
1035
|
+
)
|
|
1023
1036
|
AND o.attempt < o.max_attempts
|
|
1024
1037
|
AND (
|
|
1025
1038
|
(o.status IN ('queued', 'retry_wait') AND o.available_at <= @now)
|
|
@@ -1033,7 +1046,7 @@ export class SchedulerStore {
|
|
|
1033
1046
|
LIMIT 1
|
|
1034
1047
|
`,
|
|
1035
1048
|
)
|
|
1036
|
-
.get({ now });
|
|
1049
|
+
.get({ now, jobKind: kind, workspaceId: workspace });
|
|
1037
1050
|
if (!candidate) return null;
|
|
1038
1051
|
const previousStatus = candidate.status;
|
|
1039
1052
|
const previousOwner = candidate.lease_owner;
|
|
@@ -1094,6 +1107,103 @@ export class SchedulerStore {
|
|
|
1094
1107
|
});
|
|
1095
1108
|
}
|
|
1096
1109
|
|
|
1110
|
+
/**
|
|
1111
|
+
* Claim one known occurrence without consuming unrelated scheduler work.
|
|
1112
|
+
*
|
|
1113
|
+
* Command adapters use this after durably enqueueing a user-selected job.
|
|
1114
|
+
* Returning `null` means the occurrence exists but is not currently
|
|
1115
|
+
* claimable (another live owner holds it, it is not due yet, or it is
|
|
1116
|
+
* terminal). A missing occurrence remains a hard error so callers cannot
|
|
1117
|
+
* confuse a lost durable enqueue with ordinary contention.
|
|
1118
|
+
*/
|
|
1119
|
+
claimOccurrence({ occurrenceId, ownerId, leaseMs } = {}) {
|
|
1120
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
1121
|
+
const owner = normalizeIdentifier(ownerId, "ownerId");
|
|
1122
|
+
const lease = normalizeLeaseMs(leaseMs);
|
|
1123
|
+
const now = this._now();
|
|
1124
|
+
const leaseExpiresAt = now + lease;
|
|
1125
|
+
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
1126
|
+
throw invalidArgument("lease expiry exceeds the safe integer range");
|
|
1127
|
+
}
|
|
1128
|
+
return this._write(() => {
|
|
1129
|
+
this._deadLetterExpiredLeases(now);
|
|
1130
|
+
const candidate = this.statements.getOccurrence.get(id);
|
|
1131
|
+
if (!candidate) {
|
|
1132
|
+
throw new SchedulerKernelError(
|
|
1133
|
+
"SCHEDULER_NOT_FOUND",
|
|
1134
|
+
`Scheduler occurrence does not exist: ${id}`,
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const job = this.statements.getJob.get(candidate.job_id);
|
|
1138
|
+
const eligible =
|
|
1139
|
+
job?.enabled === 1 &&
|
|
1140
|
+
candidate.attempt < candidate.max_attempts &&
|
|
1141
|
+
(([OCCURRENCE_STATUS.QUEUED, OCCURRENCE_STATUS.RETRY_WAIT].includes(
|
|
1142
|
+
candidate.status,
|
|
1143
|
+
) &&
|
|
1144
|
+
candidate.available_at <= now) ||
|
|
1145
|
+
(candidate.status === OCCURRENCE_STATUS.RUNNING &&
|
|
1146
|
+
candidate.lease_expires_at <= now));
|
|
1147
|
+
if (!eligible) return null;
|
|
1148
|
+
|
|
1149
|
+
const previousStatus = candidate.status;
|
|
1150
|
+
const previousOwner = candidate.lease_owner;
|
|
1151
|
+
const result = this.db
|
|
1152
|
+
.prepare(
|
|
1153
|
+
`
|
|
1154
|
+
UPDATE occurrences
|
|
1155
|
+
SET status = 'running',
|
|
1156
|
+
attempt = attempt + 1,
|
|
1157
|
+
fence = fence + 1,
|
|
1158
|
+
lease_owner = @ownerId,
|
|
1159
|
+
lease_expires_at = @leaseExpiresAt,
|
|
1160
|
+
updated_at = @now,
|
|
1161
|
+
settled_at = NULL
|
|
1162
|
+
WHERE occurrence_id = @occurrenceId
|
|
1163
|
+
AND fence = @expectedFence
|
|
1164
|
+
AND attempt < max_attempts
|
|
1165
|
+
AND (
|
|
1166
|
+
(status IN ('queued', 'retry_wait') AND available_at <= @now)
|
|
1167
|
+
OR
|
|
1168
|
+
(status = 'running' AND lease_expires_at <= @now)
|
|
1169
|
+
)
|
|
1170
|
+
`,
|
|
1171
|
+
)
|
|
1172
|
+
.run({
|
|
1173
|
+
occurrenceId: id,
|
|
1174
|
+
expectedFence: candidate.fence,
|
|
1175
|
+
ownerId: owner,
|
|
1176
|
+
leaseExpiresAt,
|
|
1177
|
+
now,
|
|
1178
|
+
});
|
|
1179
|
+
if (result.changes !== 1) {
|
|
1180
|
+
throw new SchedulerKernelError(
|
|
1181
|
+
"SCHEDULER_CLAIM_CONFLICT",
|
|
1182
|
+
"Occurrence eligibility changed during targeted claim",
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const claimed = this.statements.getOccurrence.get(id);
|
|
1186
|
+
this._appendEvent({
|
|
1187
|
+
jobId: candidate.job_id,
|
|
1188
|
+
occurrenceId: id,
|
|
1189
|
+
type:
|
|
1190
|
+
previousStatus === OCCURRENCE_STATUS.RUNNING
|
|
1191
|
+
? "occurrence_reclaimed"
|
|
1192
|
+
: "occurrence_claimed",
|
|
1193
|
+
occurredAt: now,
|
|
1194
|
+
ownerId: owner,
|
|
1195
|
+
fence: claimed.fence,
|
|
1196
|
+
data: {
|
|
1197
|
+
attempt: claimed.attempt,
|
|
1198
|
+
previousOwner,
|
|
1199
|
+
previousStatus,
|
|
1200
|
+
targeted: true,
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
return mapOccurrence(claimed);
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1097
1207
|
renew({ occurrenceId, ownerId, fence, leaseMs } = {}) {
|
|
1098
1208
|
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
1099
1209
|
const owner = normalizeIdentifier(ownerId, "ownerId");
|
|
@@ -14,6 +14,8 @@ export const SESSION_FORK_AUTHORITY_FIELD = "_cc_fork_authority";
|
|
|
14
14
|
export const SESSION_MESSAGE_PROVENANCE_SCHEMA =
|
|
15
15
|
"chainlesschain.session-message-provenance/v1";
|
|
16
16
|
|
|
17
|
+
const MICRO_COMPACTED_RUNTIME_FIELD = "_microCompacted";
|
|
18
|
+
|
|
17
19
|
export const DURABLE_SYSTEM_MESSAGE_KINDS = Object.freeze({
|
|
18
20
|
COMPACT_SUMMARY: "compact-summary",
|
|
19
21
|
COMPACT_TOOL_COLLAPSE: "compact-tool-collapse",
|
|
@@ -190,7 +192,8 @@ function cloneWithoutWireTag(snapshot) {
|
|
|
190
192
|
for (const key of snapshot.keys) {
|
|
191
193
|
if (
|
|
192
194
|
key === SESSION_MESSAGE_PROVENANCE_FIELD ||
|
|
193
|
-
key === SESSION_FORK_AUTHORITY_FIELD
|
|
195
|
+
key === SESSION_FORK_AUTHORITY_FIELD ||
|
|
196
|
+
key === MICRO_COMPACTED_RUNTIME_FIELD
|
|
194
197
|
) {
|
|
195
198
|
continue;
|
|
196
199
|
}
|