Repository navigation
Commit 748b240
feat(types,automation): a host's per-kernel scheduled-work OFF reports its own reason (#21270)
Fixes #21110
Clause-②: yes (widening)
**Cross-lane surfaces, named before the change list:**
- `packages/types/src/env.ts` (`@objectstack/types`, a `domain:cli`
file). `ScheduledWorkPolicy` gains one optional field and the package
gains one export. The claim (comment 5942544148) declares it as its
cross-lane surface, per triage's routing (5927226845).
- `packages/spec/src/contracts/automation-service.ts` (a `domain:spec`
file), **TSDoc only**: the `FlowRuntimeState.reason` docblock. No key,
type, export or `.describe()` changes. The claim revision (comment
5943184720) adds it in patch round 1, because this PR made that
published sentence false for a host-injected OFF.
## What was wrong, measured on `main` at `434c6c7ca`
This was a real boot: `@objectstack/verify` `bootStack` with a real
`AutomationServicePlugin` given `scheduledWorkPolicy: { enabled: false,
... }`, `OS_AUTOMATION_SCHEDULED_WORK_ENABLED='true'`, and one
`schedule` flow plus one `time_relative` flow in the app config. All
three surfaces named the variable as the cause, even though it was set:
- `GET /automation/_status` (HTTP 200): both rows had `bound: false` and
a `reason` equal to `SCHEDULED_WORK_DISABLED_REASON` ("...
(OS_AUTOMATION_SCHEDULED_WORK_ENABLED is unset or not truthy) ... set
OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true ...").
- `getTriggerBindingAudit()`: the same sentence for both flows.
- The bind log: `Flow 'swr_nightly' is not armed on trigger 'schedule' —
disabled by deployment policy — ... is unset or not truthy ...`. The
`time_relative` flow's line was the same.
So the premise holds.
## The change
Triage's direction B: one optional host reason and one answer, with no
second copy of the sentence.
- **`@objectstack/types`** (`env.ts`)
- `ScheduledWorkPolicy.hostDisabledReason?: string` is the host's own
sentence for why scheduled work is off on this kernel. It is read only
when `enabled` is `false`.
- `scheduledWorkDisabledReason(policy)` is the one answer. It returns
`policy.hostDisabledReason` when present, and otherwise
`SCHEDULED_WORK_DISABLED_REASON`, byte for byte.
- `resolveScheduledWorkPolicy()` never sets the field, so the
environment switch keeps exactly one sentence.
- The docblocks on the field, the function, the constant and the
resolver say so. They also say why the name carries "host": the field is
not a remedy for the deployment path and not a way to reword it.
- **`@objectstack/service-automation`** (`engine.ts`)
- `activateFlowTrigger` reads the policy once per bind, and still only
for a time-triggered kind.
- It records `scheduledWorkDisabledReason` of the reading that refused.
`policyDisabledFlows` changes from a set of flow names to a map from
flow name to sentence.
- The bind log prints that sentence.
- `describeUnboundReason` (the binding audit and the `/_status` row)
returns the recorded sentence. It is still read from the record and
never re-derived.
- The engine no longer imports the constant at all.
- The log line repeats only when the recorded sentence changes.
- **`@objectstack/trigger-schedule`**
- `refuseScheduledWorkDisabled` takes the policy its caller's gate
refused on and reports `scheduledWorkDisabledReason(policy)`. It is not
exported from the package index.
- Both callers pass the reading their gate refused on. They are
`schedule-trigger.ts` and `time-relative-trigger.ts`. The second file is
a one-line call-site edit inside the claim's cleared
`trigger-schedule/src/`. It is named here because the claim's file list
names only `schedule-trigger.ts`.
- The README's per-kernel paragraph no longer says a policy-unarmed flow
always gets the deployment sentence.
- **`@objectstack/spec`** (`contracts/automation-service.ts`, TSDoc
only): the `FlowRuntimeState.reason` docblock no longer says a
policy-disabled flow always carries `SCHEDULED_WORK_DISABLED_REASON`. It
now says such a flow carries the policy's reason,
`scheduledWorkDisabledReason(policy)` from `@objectstack/types`: the
host's `hostDisabledReason` when a host-injected per-kernel
`ScheduledWorkPolicy` carries one, else
`SCHEDULED_WORK_DISABLED_REASON`, which names the deployment switch and
its remedy. The rest of that docblock is unchanged, including the "never
reads as binding failed" rule, the "not a new vocabulary" and
recorded-refusal rule, and the #17396 / #18235 history. `pnpm --filter
@objectstack/spec check:generated` reports all 15 generated artifacts up
to date, so nothing is regenerated. The spec gets no changeset entry,
because nothing it publishes changes beyond a comment.
- **Changeset**: `minor` for all three packages (an optional field and a
new export on `@objectstack/types`, and the engine and triggers now
honour that key). The changeset carries `Clause-②: yes (widening)`.
## After the change, measured at `48f366522` through the same real boot
over HTTP
| case | `/_status` reason (both rows) | audit (both flows) | bind log |
|:--|:--|:--|:--|
| host policy `enabled: false` + `hostDisabledReason`, variable `'true'`
| the host sentence | the host sentence | `... is not armed on trigger
'schedule' — ` + the host sentence |
| host policy `enabled: false`, no reason, variable `'true'` |
`SCHEDULED_WORK_DISABLED_REASON` | same | same |
| control: no host policy, variable unset |
`SCHEDULED_WORK_DISABLED_REASON` | same | same |
That boot used a scratch dogfood file, which is not committed. See
Acceptance notes for why.
## Pins (committed)
- `packages/types/src/env.test.ts`:
- a host reason is answered verbatim;
- a host policy with no reason answers `SCHEDULED_WORK_DISABLED_REASON`;
- the resolver's reading never carries the key (checked by key
presence), so an unset switch reports the deployment sentence.
-
`packages/services/service-automation/src/per-kernel-scheduled-work-policy.test.ts`,
against the engine source:
- host OFF with a reason, under the deployment ON and OFF: the bind log,
the audit and the `getFlowRuntimeStates()` row (the row `/_status`
serves verbatim) all carry the host reason, and none contains the
variable name;
- host OFF with no reason: the deployment sentence on all three;
- the control (no policy, variable unset): the deployment sentence on
all three;
- a resolver that changes after the bind still reports the refusing
reading's sentence;
- booted through `AutomationServicePlugin`.
-
`packages/triggers/trigger-schedule/src/per-kernel-scheduled-work-policy.test.ts`:
- a LiteKernel acceptance case: a host OFF with a reason under the
deployment ON, using the real `AutomationServicePlugin`,
`ScheduleTriggerPlugin` and `TimeRelativeTriggerPlugin`. Nothing is
scheduled, and the audit and both `/_status` rows carry the host reason;
- `ScheduleTrigger` and `TimeRelativeTrigger`, driven directly: the
refusal's message and its `info` line carry the host reason and do not
name the variable;
- a host OFF with no reason keeps the deployment sentence.
- The pins assert which sentence is reported (identity with the host
string or with the exported constant, and absence of the variable name).
They do not assert the refusal's own framing words.
## Ablations
The fix was committed first. Each mutation went through
`scripts/ablation-replace.mjs`, which checks that the anchor hit and the
blob changed, and then that the blob equals HEAD and `git diff HEAD` is
empty after the restore. Each site was put back on the hard-coded
deployment sentence:
| leg | site | suite | result |
|:--|:--|:--|:--|
| A1 | engine bind log | service-automation pins | 2 failed / 10 passed:
both host-reason cases |
| A2 | engine `describeUnboundReason` (audit + `/_status`) |
service-automation pins | 4 failed / 8 passed: both host-reason cases,
the record pin, the plugin-boot pin |
| A3 | the trigger refusal | trigger-schedule pins | 2 failed / 8
passed: the `ScheduleTrigger` and `TimeRelativeTrigger` host-reason
cases |
| A4 | `scheduledWorkDisabledReason` returns `''` without a host reason
(ablates the default-kept control) | types / service-automation /
trigger-schedule | 2 failed / 53 passed; 6 failed / 6 passed; 6 failed /
4 passed |
No `dist/` was involved in A1–A3, because each of those suites reads the
mutated file from source. In A4, the trigger-schedule acceptance cases
went red too, because that package aliases `@objectstack/types` to
source.
## Verification
Gates were re-derived and re-run at `cbdd42efe`, the final commit. The
package suites and the typecheck ran at `f80ddaaf2`. Round 1 moved no
file in `types`, `service-automation` or `trigger-schedule`; it changed
one spec comment.
- Build: `pnpm turbo run build --filter=@objectstack/types
--filter=@objectstack/service-automation
--filter=@objectstack/trigger-schedule`. The new symbols were confirmed
in `types/dist` and `service-automation/dist`.
- Tests:
- `pnpm --filter @objectstack/types test`: 22 files, 688 passed.
- `pnpm --filter @objectstack/trigger-schedule test`: 8 files, 174
passed.
- `pnpm --filter @objectstack/service-automation exec vitest run`: 162
files, 2027 passed.
- Typecheck: `typecheck` for the three packages exits 0, including
service-automation's `check:test-typecheck`.
- Gates at `cbdd42efe`: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 85 commands. That is the
62 from `f80ddaaf2` plus 23 the spec path adds, among them
`check:api-surface`, `check:docs`, `check:authorable-surface`,
`check:spec-docblock-symbol-anchors` and `check:merge-driver`. All 85
were run against a full workspace build (`turbo run build
--filter=!@objectstack/docs`, 72 tasks), and all exited 0. `--ran`
reconciliation: "85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN". At
`f80ddaaf2` the 62 also all exited 0. There,
`check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit
3) for 8 packages with no `dist/`, and passed once those 8 were built.
- Spec, round 1: `src/contracts/automation-service.test.ts` passed 17 of
17. Its doc pin still finds `SCHEDULED_WORK_DISABLED_REASON`, "never
reads as binding failed", "RECORDED refusal" and
`getTriggerBindingAudit()` in the docblock.
- Also run, because the engine's recorded refusal changed shape:
`check:startup-registry-verdict` and `check:durability-log-level`, both
exit 0.
- Lint, narrowed: `eslint --no-inline-config --format json` over the 7
changed `.ts` files gave 7 files, 0 errors, 0 warnings, none ignored.
`--print-config` shows no `parserOptions.project` or `projectService` on
any of them. Lint is not type-aware, so this diff cannot move the
verdict on any untouched file. The repo-wide `pnpm lint` is left to CI.
- `origin/main` has moved to `ee42f00e3` since the base. Those commits
touch none of `packages/types`, `service-automation`,
`trigger-schedule`, the spec contract or the runtime `_status` handler
(empty diffstat), so this branch is not merged with it.
## Docs
- `packages/triggers/trigger-schedule/README.md`, per-kernel paragraph.
Old: "A flow the policy leaves unarmed is reported with the same policy
reason as a deployment-disabled one." New: it is reported the way a
deployment-disabled one is, and the reason is the policy's
`hostDisabledReason` when the host sets one, otherwise the deployment's
sentence.
- `content/docs/**` (outside `releases/`) and `skills/**`: zero hits for
`ScheduledWorkPolicy`, `scheduledWorkPolicy`,
`SCHEDULED_WORK_DISABLED_REASON` or `hostDisabledReason`. The positive
control is the same pattern's hit on the trigger-schedule README. Eight
lines name `OS_AUTOMATION_SCHEDULED_WORK_ENABLED` (in
`automation/flows.mdx`, `automation/jobs.mdx`,
`deployment/environment-variables.mdx`,
`deployment/production-readiness.mdx`, `deployment/tenancy-modes.mdx`
and `references/automation/schedule-organization.mdx`). Every one
describes the environment-switch path, which this PR leaves
byte-identical. None becomes false, so none is edited.
## Acceptance notes
- **The spec contract's `FlowRuntimeState.reason` TSDoc is fixed in this
PR (patch round 1).** It said a policy-disabled flow carries
`SCHEDULED_WORK_DISABLED_REASON`, and this PR made that published
sentence false for a host-injected OFF with a `hostDisabledReason`. It
now names the policy's reason, `scheduledWorkDisabledReason(policy)`.
`ScheduledWorkPolicy` has no zod or spec mirror (zero hits in
`packages/spec/src`), so no other spec text moves, and no generated
artifact changes.
- **Other readers of `SCHEDULED_WORK_DISABLED_REASON`, left as they
are.** `packages/runtime/src/app-plugin.ts` (the declarative `defineJob`
gate) quotes the constant, but it refuses only when the zero-argument
`resolveScheduledWorkEnabled()` is off. There the deployment sentence is
the true cause. Reach: no wrong sentence is reachable through it,
because a per-kernel host OFF never reaches that loop. That is the
separate open question 1 of the earlier per-kernel card (the
declarative-job gate ignores a host policy), and it is not this card's
scope. The other readers are tests on the environment path
(`engine.test.ts` and the dogfood `schedule-sweep-organization-scope`
suite), and both remain correct.
- **No door-level dogfood pin is committed.** `@objectstack/dogfood`
does not depend on `@objectstack/service-automation`, and `bootStack`'s
`automation` option takes no policy. A committed door pin would
therefore need a new dependency (`package.json` and the lockfile) or a
new `bootStack` option in `packages/verify`, and both are outside the
claimed file surface. The `/_status` handler
(`packages/runtime/src/domains/automation.ts`, the `_status` branch)
returns `getFlowRuntimeStates()` verbatim, and that is the row the
committed pins read. The HTTP door itself was measured once before and
once after, as above.
- `hostDisabledReason` is not validated as non-empty. The docblock asks
for a whole, non-empty sentence. Falling back to the deployment sentence
for a blank value would bring back the wrong-cause defect without anyone
noticing, and refusing it would turn a reporting slip into a bind
failure. Neither was added.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c7396f1 commit 748b240
10 files changed
Lines changed: 450 additions & 29 deletions
File tree
- .changeset
- packages
- services/service-automation/src
- spec/src/contracts
- triggers/trigger-schedule
- src
- types/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| |||
875 | 875 | | |
876 | 876 | | |
877 | 877 | | |
878 | | - | |
879 | | - | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
880 | 883 | | |
881 | 884 | | |
882 | 885 | | |
| |||
2451 | 2454 | | |
2452 | 2455 | | |
2453 | 2456 | | |
| 2457 | + | |
| 2458 | + | |
| 2459 | + | |
| 2460 | + | |
| 2461 | + | |
| 2462 | + | |
| 2463 | + | |
| 2464 | + | |
| 2465 | + | |
2454 | 2466 | | |
2455 | | - | |
| 2467 | + | |
2456 | 2468 | | |
2457 | 2469 | | |
2458 | 2470 | | |
| |||
3684 | 3696 | | |
3685 | 3697 | | |
3686 | 3698 | | |
3687 | | - | |
3688 | | - | |
3689 | | - | |
3690 | | - | |
3691 | | - | |
3692 | | - | |
3693 | | - | |
3694 | | - | |
3695 | | - | |
| 3699 | + | |
| 3700 | + | |
| 3701 | + | |
| 3702 | + | |
| 3703 | + | |
| 3704 | + | |
| 3705 | + | |
| 3706 | + | |
| 3707 | + | |
| 3708 | + | |
| 3709 | + | |
| 3710 | + | |
| 3711 | + | |
| 3712 | + | |
| 3713 | + | |
| 3714 | + | |
| 3715 | + | |
| 3716 | + | |
| 3717 | + | |
3696 | 3718 | | |
3697 | | - | |
| 3719 | + | |
3698 | 3720 | | |
3699 | 3721 | | |
3700 | 3722 | | |
| |||
4538 | 4560 | | |
4539 | 4561 | | |
4540 | 4562 | | |
4541 | | - | |
| 4563 | + | |
| 4564 | + | |
| 4565 | + | |
| 4566 | + | |
4542 | 4567 | | |
4543 | 4568 | | |
4544 | 4569 | | |
| |||
4550 | 4575 | | |
4551 | 4576 | | |
4552 | 4577 | | |
4553 | | - | |
| 4578 | + | |
| 4579 | + | |
4554 | 4580 | | |
4555 | 4581 | | |
4556 | 4582 | | |
| |||
Lines changed: 121 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
256 | 256 | | |
257 | 257 | | |
258 | 258 | | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 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 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
587 | 587 | | |
588 | 588 | | |
589 | 589 | | |
590 | | - | |
591 | | - | |
592 | | - | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
593 | 596 | | |
594 | 597 | | |
595 | 598 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
97 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
98 | 101 | | |
99 | 102 | | |
100 | 103 | | |
| |||
0 commit comments