Repository navigation
feat(automation): accept a per-kernel scheduledWorkPolicy in the engine and both schedule triggers - #19858
Conversation
…ne and both schedule triggers Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
…d-work policies Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
…uled-work policy Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d4890403e9d6b80bdc34038774091ff55b83c289 && git checkout d4890403e9d6b80bdc34038774091ff55b83c289
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8cbc3c0084a3c3c8ef5e59763ab3be96aa1a6f07 817d4d38f2a17ab58d65413fa35e90914677f9e5 && git checkout -B drift-repro 8cbc3c0084a3c3c8ef5e59763ab3be96aa1a6f07 && git merge --no-ff 817d4d38f2a17ab58d65413fa35e90914677f9e5
node scripts/docs-audit/affected-docs.mjs --json 8cbc3c0084a3c3c8ef5e59763ab3be96aa1a6f07
|
Contract review (retroactive — PR landed as 863a775 before this record was written)Served-tier:
① Derived judgmentsThe changeset prose was checked claim by claim at
New public surface reachable through the
② Semver level
③ Boundary flagsZero The dev's three open questions are escalated as follow-ups: the Implemented-by: VERDICT: PASS Non-blocking defects:
Generated by Claude Code |
Closes #19834
Clause-②: no
This PR follows the ruled shape on #19834 (ruling batch #216 item 1, letter ①′, on objectstack-ai/cloud#2337). A kernel can now carry its own
ScheduledWorkPolicy. The deployment default and its spelling (OS_AUTOMATION_SCHEDULED_WORK_ENABLED) are unchanged, and so isobjectstack serve.What changed
AutomationEngineOptions(@objectstack/service-automation)scheduledWorkPolicy, which takes aScheduledWorkPolicyvalue or a resolver. The bind gate inactivateFlowTriggerreads it through a privatereadScheduledWorkPolicy(). When the option is absent, that method calls the zero-argumentresolveScheduledWorkPolicy()exactly as before. A resolver is called at each bind.AutomationServicePluginOptionsscheduledWorkPolicy, passed to the engine unchanged.maxLogSizeandrunSummaryLogare forwarded the same way.ScheduleTriggerPlugin,TimeRelativeTriggerPlugin(@objectstack/trigger-schedule)optionswith an optionalscheduledWorkPolicy, passed to the trigger they register.new XPlugin()behaves as before.ScheduleTrigger,TimeRelativeTriggeroptionswithscheduledWorkPolicy. Each trigger's own gate reads it, and falls back to the deployment resolver when it is absent.ScheduleTriggerPluginOptions,TimeRelativeTriggerPluginOptions,ScheduledWorkTriggerOptions,ScheduledWorkPolicySource.The refusal is still structured. A time-triggered flow that the per-kernel policy leaves unarmed gets
SCHEDULED_WORK_DISABLED_REASONongetTriggerBindingAudit()and on itsgetFlowRuntimeStates()row. The engine checks the policy before it looks up the trigger, as before, so the diagnostic names the policy and never says "addrequires: ['triggers']".Every bare call site, re-checked on this tree (
afc3b64928)service-automation/src/engine.tsactivateFlowTriggertrigger-schedule/src/schedule-trigger.tsScheduleTrigger.start(viareadScheduledWorkPolicy)trigger-schedule/src/time-relative-trigger.tsTimeRelativeTrigger.startsweepclosure reuses the result forrunOwnershipanddescribeScheduleRunOwnership. The card counted two sites at cloud's pin.runtime/src/app-plugin.ts(the gate fordefineStack({ jobs }))cli/src/commands/doctor.tsdoctorinspects the deployment's configuration and has no kernel options.Tests (all at
817d4d38f2)packages/triggers/trigger-schedule/src/per-kernel-scheduled-work-policy.test.ts. It boots two realLiteKernels in one process, each with the realAutomationServicePlugin,ScheduleTriggerPluginandTimeRelativeTriggerPlugin. The kernel built withenabled: falseschedules nothing: no plainscheduleflow and notime_relativesweep reach the job service, and its audit givesSCHEDULED_WORK_DISABLED_REASONfor both flows. Its sibling built withenabled: trueschedules both jobs. The test runs under the deployment switch OFF and again under it ON, which shows the per-kernel answer wins over the environment in both directions. Two pins check that the zero-argument path is unchanged: a kernel with no policy follows the deployment in each state. The file also drives both triggers directly, without an engine, with a value and with a resolver.packages/services/service-automation/src/per-kernel-scheduled-work-policy.test.ts. They cover two engines under both deployment states, the policy reason with no trigger registered, a resolver that is read at each bind, and the zero-argument path (no option,{},{ scheduledWorkPolicy: undefined }). They also cover the plugin forwarding, with two kernels plus a control kernel under a deployment that is ON.pnpm --filter @objectstack/trigger-schedule test: 6 files, 144 tests passed.pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2: 142 files, 1696 tests passed.pnpm --filter @objectstack/trigger-schedule --filter @objectstack/service-automation typecheck: both passed. The new trigger-schedule test file appears intsc --listFiles, and service-automation'scheck:test-typecheckis green.eslint --no-inline-config --format json) on the 10 changed TS files: 10 files, 0 errors, 0 warnings. None of them was ignored, because an ignored file would show up as a warning.eslint.config.mjsnever enables type-aware linting: there is noparserOptions.projectorprojectService, which--print-configconfirms. So this diff cannot change the lint result of any file it does not touch.Ablations (committed first, mutated with
scripts/ablation-replace.mjs, restored to the HEAD blob with a cleangit diff HEAD)engine.ts,readScheduledWorkPolicywas changed to ignore the option. The engine suite went from 6/6 passing to 5 failing and 1 passing. The zero-argument pin stayed green, as it should.schedule-trigger.ts,readScheduledWorkPolicywas changed to ignoresource. The trigger-schedule suite went from 6/6 passing to 3 failing and 3 passing. The failures were the two direct-trigger cases and the acceptance case under deployment OFF, where the ON kernel's triggers then refuse what its engine allowed.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 61 families, and all 61 were run. Reconciled with--ranincluding exit codes: 59 passed. 2 are NOT MEASURED because the prerequisite is not met (exit 3, both need a whole-workspace build that CI does):pnpm check:dual-build-cjs-loadsandpnpm check:type-check-debt. Neither package is in the debt ledger. The CI jobs and type-check lanes that the derivation lists separately are left to CI.Acceptance notes
AutomationEngineOptions. Hosts build the engine throughAutomationServicePlugin, so the option is useless per kernel unless the plugin forwards it. The plugin forwards it the same way it already forwardsmaxLogSizeandrunSummaryLog.requiresActingOrganization === (enabled && runOwnership === 'declared'). If a host spreads the deployment reading and overrides onlyenabled, the result is wrong under anisolatedposture: a kernel turned ON would bind org-less flows. The option docblocks and the changeset say so. No builder is added, because that would be a surface outside the ruled shape.SCHEDULED_WORK_DISABLED_REASONword for word, as ruled. That sentence tells the reader to setOS_AUTOMATION_SCHEDULED_WORK_ENABLED=true, which is the wrong fix for a kernel whose host turned it off. This is noted, not changed here.defineStack({ jobs }). Declarative jobs gated inruntime/src/app-plugin.tsstill follow the deployment switch only. In a mixed-plan host, a kernel whose flows are turned off would still schedule itsdefineJobs while the deployment default is ON. The ruled shape does not coverAppPlugin, so this is left for a decision.Changeset:
minorfor@objectstack/service-automationand@objectstack/trigger-schedule, withClause-②: noin its body. No edits underpackages/specorcontent/docs/releases/**.Generated by Claude Code