fix(triggers,spec,service-automation): a time-triggered flow declares its acting organization and runs as it (#16659) - #17126
Conversation
… its acting organization and runs as it A `type: 'schedule'` flow and a `time_relative` sweep launch their runs from a job tick, which carries no identity, so `AutomationContext.tenantId` was never set. On an install holding more than one `sys_organization` every tenant-scoped write beneath the run was then refused by the #8844 guard — the inbox rows a `notify` node emits and the `sys_automation_run` history row — while the tick still summarised itself healthy. - `@objectstack/spec` declares the start-node `config.organization` key, its value schema, the two kinds that owe it, the near-miss spellings an author reaches for, and the one refusal sentence every enforcement point says. - The engine lifts the declaration onto the schedule / time_relative binding. - Both triggers REFUSE to bind a flow that declares none, naming it at `error`, and thread the declared organization onto the run as `tenantId`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
📓 Docs Drift CheckThis PR changes 4 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 135 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 7a6978838c6982e685a84f4f8729025e439e5d32 && git checkout 7a6978838c6982e685a84f4f8729025e439e5d32
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3 b998f58f3832d3f8f952172bbd9b28f668f18ed9 && git checkout -B drift-repro fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3 && git merge --no-ff b998f58f3832d3f8f952172bbd9b28f668f18ed9
node scripts/docs-audit/affected-docs.mjs --json fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
… differential controls Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…reference total Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…gfood pin The pin's subject is the trigger's own run-context construction, so a dist merely behind would run it green against the old one — the exact shape this card is about. check:test-source-alias and check:type-source-resolution both name this as the required spelling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Contract review at
|
…ine records it `FlowTrigger.start()` is `void`, so logging the refusal and returning left the engine free to run `boundFlowTriggers.set(...)` and log "bound" one line later: `getFlowRuntimeStates()` answered `bound: true`, `getTriggerBindingAudit()` skipped the flow, and the CLI startup summary said every triggered flow was wired. Both time triggers now throw the same sentence they log, which is the engine's designed catch path — the flow is never marked bound and the audit lists it with `binding failed — see earlier warnings`. Also folds the trigger's inline near-miss scan back into `packages/spec` (`findScheduleOrganizationNearMissInConfig`, which takes the start-node config a trigger actually holds), drops the two consumer-less exports (`TIME_TRIGGERED_FLOW_KINDS`, `requiresScheduleOrganization`), makes the near-miss vocabulary module-local, renames the module to `schedule-organization.zod.ts` so its source ships and the docs generator gives it a page, and corrects the two comments that claimed `FlowSchema` emits the refusal sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
The fixture comment promised a refusal suite that did not exist. It exists now: the throw itself (the F1 contract), the `error` line carrying the same sentence the engine's audit points at, near-miss naming, the "hot re-publish removes the key" stop() limb, per-flow isolation, the no-`error`-channel fallback, and every limb of `resolveBindingOrganization`. The time-relative sweep gets the same two load-bearing pins, and `packages/spec` gets the unit test every sibling module in `src/automation` already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…l B honest Consequence (3)'s dogfood pin asserted only that the job service was never asked. That says nothing about what the ENGINE recorded, which is where the refusal was being lost: `getFlowRuntimeStates()` answered `bound: true` and `getTriggerBindingAudit()` skipped the flow entirely. Both are now pinned, each with the declaring flow as its paired control. Control B's memory limb asserted `status < 300` under a message claiming it pinned a 503 refusal — opposite polarity, so it certified nothing. It now states plainly that the HTTP control is unavailable on this driver and pins the reason at the seam that makes it so: a tenant-scoped read is refused with MEMORY_MULTI_TENANT_UNSUPPORTED, so the exemption expires by itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…ished surface The trigger-schedule README (shipped in files[]) still showed both worked examples without `organization` and said they auto-launch; after this change those exact flows are refused. Both now declare the key, and the README states the refusal and where it shows up. flows.mdx gains a dedicated "The acting organization" section and both worked schedule examples declare the key; hooks.mdx and capabilities.mdx point at it so "needs `triggers`" is no longer the whole story. The showcase digest's docstring stops promising that it fires: as a package-shipped flow it has no legal organization to name, no placeholder may be invented, and what such a flow should do instead is #17150's decision. Drops the unrelated esbuild@0.28.1/0.28.2 swap from pnpm-lock.yaml; the diff is now only the link: entry the new dev-dependency needs, and `pnpm install --frozen-lockfile` leaves it byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…t set `schedule-organization` now has its own reference page instead of landing in the "Misc (no single source file)" bucket, and api-surface / export-origins drop the three consumer-less exports. The changeset's export list matches what the barrel actually publishes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…w module Two gate findings the .zod.ts rename brought into scope, both real: check:spec-parsed-alias only reads *.zod.ts, so `ScheduleOrganization` was declared with z.infer where ADR-0122 reserves the bare name for the author state; and llms.txt counts *.zod.ts modules, so the automation domain and the total were one short. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
`ScheduleOrganizationSchema` is a bare `z.string().min(1)` — no coercion, no default, no transform — so input === infer and it takes a pin rather than a permanent `ScheduleOrganizationParsed` synonym. The note records that the module is not new, only its `.zod.ts` name is: this family of gates reads `*.zod.ts` only, so the violation sat green behind an extension. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Patch round complete — re-review requested at
|
| state | |
|---|---|
| F1 (blocking) | Both time triggers now throw the refusal sentence they log, so the engine's designed catch runs: the flow is never marked bound, getFlowRuntimeStates() reports bound:false, getTriggerBindingAudit() lists it with binding failed. Dogfood pin (3) extended with a (3, structured) case asserting exactly that, on both drivers, each half paired with the declaring flow as control. |
| F2 (blocking) | The published trigger-schedule README declares organization in both worked examples, names tenantId on the context, and states the refusal and the three surfaces that report it. |
| F5 | The trigger's inline near-miss scan now calls the spec helper (renamed findScheduleOrganizationNearMissInConfig, because it takes the start-node config a trigger actually holds — a flow-shaped signature would answer undefined for its only caller's only input). TIME_TRIGGERED_FLOW_KINDS and requiresScheduleOrganization dropped; SCHEDULE_ORGANIZATION_NEAR_MISSES made module-local; both comments claiming FlowSchema emits the refusal corrected. |
| F6 | The promised refusal suite exists — 7 + 8 cases on schedule, 2 on time-relative — plus a new 28-case spec unit test. |
| F7 | The memory control-B limb no longer asserts status < 300 under a message claiming a 503: it states the control is unavailable and pins the reason (MULTI_TENANT_UNSUPPORTED). |
| F8 | Renamed to schedule-organization.zod.ts with its own reference page; the Misc bucket is gone. |
| F9 | Measured, with two positive controls, and the lockfile churn reverted (pnpm install --frozen-lockfile leaves it byte-identical; the diff vs merge-base is now exactly the three link: lines). |
| F3 | flows.mdx gains an "The acting organization" section plus both examples; hooks.mdx and capabilities.mdx point at it. |
| F4 | ⛔ Not settled here. #17150 is named nine times in the body; showcase_scheduled_digest's docstring stops promising it fires. No example flow's behaviour changed, no symbolic organization invented, ⛔ no placeholder id written. |
⭐ The F1 ablation reproduces the review's own point
Mutation = swallow the throw at the schedule call site, i.e. the reviewed head's exact log-stop-return behaviour. Result: Tests 2 failed | 14 passed (16) — exactly the two new (3, structured) cases, on both drivers — while every other assertion, including the old job.has(...) === false, stayed green. That is the review's finding stated as a measurement: the previous pin could not see it. On-disk proof before the run (anchor 1, injected marker 1, deleted-shape 0, mutated blob ≠ HEAD blob); restore proven by blob equality and an empty git diff HEAD.
⚠️ Two things the reviewer should know that are NOT this PR's to fix
- F8's rename made two gates go red for real reasons —
check:spec-parsed-alias(the bare alias wasz.infer, which ADR-0122 does not permit) andcheck:llms-txt(inventory one short). Both fixed here, with an isomorphism pinIso871and count815 → 816. ⭐ The module had sat green through a full review cycle purely because it was a.tsand not a.zod.ts— that whole gate family reads*.zod.tsonly. Filed as [finding] a Zod schema authored in a plain.tsis invisible to the whole*.zod.ts-only gate family — one sat green through a full contract-review cycle and two gates redded the moment it was renamed #17192; ⛔ deliberately not fixed here, because the natural instrument has a measured false-positive population in the same directory. - [needs decision] #16659's ruled fix refuses the repo's own four example scheduled flows at bind, and today a package-shipped flow has no legal organization to name #17150 gained two consequences the delivery's F9 control turned up:
docs/qa/platform-checklist/areas/automation.jsonexpects "two ticks ⇒ twoshowcase_scheduled_digestrun rows" and the boot banner to countshowcase_task_due_reminderas bound, anddocs/adr/0057's pilot narrative stops being reproducible. Recorded on [needs decision] #16659's ruled fix refuses the repo's own four example scheduled flows at bind, and today a package-shipped flow has no legal organization to name #17150; they raise B's cost and leave A′ untouched.
The body corrects its own earlier claim
It previously stated that no platform-organization constant exists in packages/. Withdrawn — packages/runtime/src/http-dispatcher.ts carries PLATFORM_ORG_ID (a private dispatcher sentinel, ⛔ not usable as an organization, but it changes #17150's option-A baseline).
CI
b998f58f3: 30 success, 4 skipped, Lint & Repo Gates still in progress at the time of writing. Gates locally: 113 derived / 113 run / 0 NOT-MEASURED / 0 UNRUN; five needed a second pass and ⛔ none was reported as a pass — two were real (fixed), two were PREREQUISITE NOT MET on an unbuilt dist (green after pnpm build), and check:type-check-debt timed out at the standard 4096 MB prefix and came back green at 8192 MB, confirming #17151 a second time.
dispatch-gates reports this tree at least 16 commits behind origin/main with six gate-relevant scripts changed in that range. The local derivation is about this tree; CI on the merge is the authority for those six.
Generated by Claude Code
Fixes #16659
Ruling executed: 方向一, narrowed (director seat, comment 5577119379). Maintainer, verbatim:
The defect
A time-triggered flow launches its run from a job tick, and a job tick carries no identity, so
ScheduleTrigger(andTimeRelativeTrigger) built anAutomationContextwith notenantId. Two consumers already read that key and both resolved NULL:notify-node.tsthreads it onto the notification it emits ([finding] sys_inbox_message/sys_notification/sys_email 等平台表从不写 organization_id(存量与新增行 100% null)——请确认多组织语义是否设计如此 #11303) — sosys_notificationlanded withorganization_id = NULLand every tenant-scoped row beneath it (sys_inbox_message,sys_notification_delivery,sys_notification_receipt) was refused by the System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844 guard.AutomationEngine.recordLogcopies it onto thesys_automation_runhistory row (PromoteresolveRecordOrganizationFieldto the shared platform-row resolver (approvals + automation runs), per the ruled cloud#1395 Option A #10101) — so run history never persisted.Both refusals happen one layer below anything that summarises the run, so the tick reported
unmeasured=0and read healthy.Where the organization id lives, and why
A start-node
config.organizationkey, declared inpackages/spec.FlowSchemaalready refuses a top-levelschedulein as many words — "a schedule flow declares its cron/interval asconfig.scheduleon the START node, not at the flow top level". Every other trigger-binding fact lives on the start node, andresolveTriggerBindinghands the whole startconfigto the trigger. A flow-level key would split one binding across two layers.scheduledescriptor. Atime_relativesweep carries its cadence in that same slot but binds through a different descriptor, so a key insideschedulewould be invisible to exactly one of the two triggers that owes it.Enforcement is at bind, not at
FlowSchema.parse: the start node'sconfigis an open record (ADR-0018), and a parse-time requirement would make every package-shipped scheduled flow in this repo unparseable. Bind-time refusal loses nothing — there is no path by which an organization-less time-triggered run reaches the data layer once bind refuses.What landed
packages/spec/src/automation/schedule-organization.zod.ts—SCHEDULE_ORGANIZATION_KEY,ScheduleOrganizationSchema, theScheduleOrganizationalias,resolveScheduleOrganization,findScheduleOrganizationNearMissInConfig,describeMissingScheduleOrganization. One refusal sentence and one near-miss scan, so the engine's lift and both triggers cannot drift.AutomationEngine.resolveTriggerBindingliftsorganizationonto thescheduleandtime_relativebindings, besideschedule.record_changeandapibindings leave itundefinedby construction — both are fired by a caller who already carries an organization, and lifting a declared one onto them would let a flow overrule the tenant of the write that triggered it.stop()any prior binding first (a hot re-publish that REMOVES the key must not leave the previous job armed), log the reason aterrornaming the flow, then throw — see F1 below. The refusal names the near-miss spelling when the author wrote one (organizationId,tenantId, …); the start node'sconfigis an open record, so those spellings are accepted and then ignored, and the refusal is the only place that becomes visible.tenantIdon the run context unconditionally. ⛔ Not...(organization ? {…} : {})— the bind refused the flow otherwise, and a conditional spelling would re-open the org-less run as a silent state.⛔ No fan-out. ⛔ No fallback limb — not the install's only organization, not the platform organization, not the first row of
sys_organization. A wrongorganization_idis worse than a refusal: a refusal is visible at boot and names its flow, a wrong value is silently authoritative to every report, export and cleanup script that filters by organization.The patch round, finding by finding
trigger-scheduleREADME declares the key in both examples and states the refusalflows.mdxgains a dedicated section, both worked examples declare the key,hooks.mdxandcapabilities.mdxpoint at itschedule-organization.zod.ts; own reference page; two further real gate findings fell out of itF1 (blocking) — the refusal now reaches the engine
FlowTrigger.start()returnsvoid, so logging the refusal and returning was indistinguishable — to the engine — from arming successfully.activateFlowTriggerranboundFlowTriggers.set(flowName, …)and loggedFlow '…' bound to trigger 'schedule'one line after the trigger saidNOT BOUND, and all three machine-readable channels then said the opposite of the stderr line.refuseMissingOrganization(wasreportMissingOrganization) now returnsnever: it logs the sentence aterror, then throws the same sentence. That is the engine's designed catch path — thesetis inside thetry, after the call, so a throw skips it, andgetTriggerBindingAudit()lists the flow withbinding failed — see earlier warnings, which points at theerrorline already emitted. Loud channel and structured channel carry identical text and cannot drift.Pin (3) is extended, as the review requires, with a new case
(3, structured) the refused flow reads as NOT BOUND on every machine-readable surface:getFlowRuntimeStates()reportsbound: falsefor the refused flow — andbound: truefor the declaring one, as the paired control;getTriggerBindingAudit()contains the refused flow withtriggerType: 'schedule'and a reason containingbinding failed— and does not contain the declaring one.The old pin could not have caught this:
job.has(...) === falseis a statement about the job service, which was never asked either way.Ablation — the throw removed (swallowed at the schedule call site, so the limb reverts to log-
stop()-return, i.e. the reviewed head's exact behaviour):Exactly the two new cases went red, on both drivers, while everything else — including the old⚠️ No
job.has(...) === falseassertion — stayed green. That contrast is the review's point, reproduced. Restoration proved by blob equality and an emptygit diff HEAD, under atrap … EXIT INT TERMwith an absolute path; the mutation was proved to reach disk by two anchor greps (1/0) plus blob inequality before the run.distpre-flight leg here and that is not an omission:packages/qa/dogfood/vitest.config.tsaliases@objectstack/trigger-scheduleto source (its own registered entry, landed earlier in this PR), so the suite reads the mutated file directly and there is no artifact to fall behind.F2 (blocking) — the published README
packages/triggers/trigger-schedule/README.mdships infiles[], and both of its worked examples were made false by this PR. Both now carryorganization, the context line namestenantId, and a new section "The acting organization is required" states the refusal, where it surfaces (getTriggerBindingAudit(), the CLI startup summary,getFlowRuntimeStates()), that there is no fallback and no fan-out, and that a near-miss spelling parses and is then ignored. ThetimeRelativeexample carries the key too, with the stronger reason it owes one (it queriesisSystemon purpose, so org-less it selects across every tenant).F3 — hand-written docs
content/docs/automation/flows.mdx— both workedscheduleexamples declare the key, and a new "The acting organization" section carries the contract: what the run executes as, the bind-time refusal and the three surfaces that report it, no fallback, no fan-out, and a warning callout about near-miss spellings.content/docs/automation/hooks.mdx— the "reach for a Flow" row now links to that section.content/docs/permissions/capabilities.mdx— therequires: ['triggers']row now saystriggersis a capability, not the whole declaration.⛔ Nothing under
content/docs/releases/**was touched.F4 — NOT settled here: #17150
The four package-shipped example flows (
showcase_scheduled_digest,showcase_task_due_reminder, andexamples/app-todo'stask_reminderandoverdue_escalation) are refused at bind under this change, and there is no legal value an author could write for a flow that ships inside a package: organization ids are minted at runtime, per install.⇒ That question is #17150 (
needs-user-decision, options A/B/C with a director reading recommending A′), and it is named here so it is no longer implicit. This PR changes no example flow's behaviour, invents no symbolic organization and writes no placeholder id — a value matching no row is exactly the "wrongorganization_id, silently authoritative" outcome the ruling forbids, and strictly worse than the refusal.What this PR does do is stop one of them lying:
showcase_scheduled_digest's docstring no longer promises it is "observable end-to-end", and says instead that it does not fire as shipped, why, that a placeholder must not be invented, and that #17150 owns the answer.docs/qa/platform-checklist/areas/automation.jsonexpects "two ticks ⇒ twoshowcase_scheduled_digestrun rows", expects the boot banner to countshowcase_task_due_reminderas bound, and readsGET /api/v1/automation/showcase_task_due_reminder/runs;areas/platform-core.jsonuses the same digest as its notification-flood source. Those items go red under this change until #17150 is settled.docs/adr/0057's pilot narrative (that digest growingdev.dbto 260 MB) likewise stops being reproducible. ⛔ Nothing indocs/or.claude/was edited — governed surface, reported not written.F5 — the spec module's exports and the two false comments
reportMissingOrganization's inline near-miss scan is gone; it callsfindScheduleOrganizationNearMissInConfigfrompackages/spec.configrecord, not a flow, and the name says so. A trigger never holds the flow — the engine parses the start node and hands it a binding whoseconfigis that record — so a flow-shaped signature would answerundefinedfor the only caller's only input: a silent wrong answer, the exact class this module exists to end. The oldfindScheduleOrganizationNearMiss(flow)is therefore replaced rather than merely wired.TIME_TRIGGERED_FLOW_KINDSandrequiresScheduleOrganizationare dropped (zero consumers; their would-be consumer is the lint rule, which cannot learn this requirement until [needs decision] #16659's ruled fix refuses the repo's own four example scheduled flows at bind, and today a package-shipped flow has no legal organization to name #17150 is settled), andSCHEDULE_ORGANIZATION_NEAR_MISSESbecomes module-local (its only reader is the helper above). The repo's own rule, applied: an export whose consumers all live inside its own package does not belong on a published barrel.FlowSchema/ "validation" emits the refusal sentence are corrected — inautomation/index.tsand ondescribeMissingScheduleOrganization— and both now state why enforcement is at bind instead.F6 — the refusal suite the fixture comment promised
schedule-trigger.test.tsgainsScheduleTrigger — the acting-organization refusal (#16659)(7 cases) andresolveBindingOrganization (#16659)(8 cases): the throw, theerrorline carrying the same sentence as the thrown text, near-miss naming, that the refusal never echoes the author's value back, the "hot re-publish removes the key"stop()limb, per-flow isolation, the no-error-channelwarnfallback, and every limb ofresolveBindingOrganization(lifted field, raw-config fallback for an older engine, precedence, and the empty-string / number / object / null / absent cases).time-relative-trigger.test.tsgains the two load-bearing ones. The fixture comment now points at the suite by name.packages/spec/src/automation/schedule-organization.test.tsis new — 28 cases over the key, the schema,resolveScheduleOrganization(including "not on the START node" and the structural non-throwing contract), the near-miss helper, and the refusal sentence, one of which asserts the sentence offers no fallback and names no organization.F7 — control B on the memory driver
The old limb asserted
status < 300under a message claiming it pinned a 503: opposite polarity, no delivery check, certified nothing. The reason it certified nothing is structural — control B's whole discriminating power is the session being bound to an organization of its own, and this suite boots memory withorgContext: falseprecisely becausedriver-memoryrefuses any tenant-scoped call. A session with no organization is the unfixed state; a run triggered that way discriminates nothing.So the limb now says that plainly and pins the reason, at the seam that makes it so: a tenant-scoped read on this driver produces no answer, and the refusal carries
MULTI_TENANT_UNSUPPORTED. The row count is taken as a nullable value rather than with.toHaveLength, because.not.toHaveLengthpasses over a null target for the wrong reason. The day the driver gains isolation this goes red and whoever fixes it enables the real control. Control A (both drivers) and control B on sqlite still carry the proof.F8 —
.zod.ts, and the two gate findings that fell out of itschedule-organization.ts→schedule-organization.zod.ts. Regenerated: the module now ships viafiles[]: src/**/*.zod.tsand has its own reference page (content/docs/references/automation/schedule-organization.mdx); theMisc … (no single source file)bucket is deleted fromreferences/index.mdx,automation/index.mdxandautomation/meta.json.⭐ The rename brought the module into scope of gates that read
*.zod.tsonly, and two of them were red — both real, both green now:check:spec-parsed-alias—ScheduleOrganizationwasz.infer, where ADR-0122 reserves the bare name for the AUTHOR state. Nowz.input, with an isomorphism pin intype-alias-convention.pin.test.ts(Iso871, module slot M186) rather than a permanentScheduleOrganizationParsedsynonym: a barez.string().min(1)has no default, transform or coercion. The pin file's count moves 815 → 816, in all three places it is stated.check:llms-txt— the hand-kept inventory counts*.zod.tsmodules, soautomationand the total were each one short (13 → 14, 205 → 206), with the row's key-schema list updated rather than the number nudged.Both were invisible while the file was a plain
.ts. That is the substance of F8, beyond the docs page.F9 — the dispatch-required measurement, and the lockfile
Question: does any deployment or test depend on scheduled runs not landing in
sys_automation_run?Measured answer: no. Method and evidence:
sys_automation_run; 38 are tests. Scanning every one of them for an absence or zero-count shape around the reference (toHaveLength(0),toBe(0),toEqual([]),toBeUndefined,toBeNull,not.toContain, "never"/"no run"/"zero" in prose) turns up no assertion that a time-triggered flow contributes no run-history row. The hits are about the object not being registered or readable at startup, about field shapes, or about thepaused-visibility contract.flow-schedule:/flow-time-relative:/ScheduleTrigger/TimeRelativeTrigger), exactly one also readssys_automation_run: this PR's own pin.sys_automation_runalready declares two-sided retention — a per-flow cap on terminal rows at WRITE time (runHistoryMaxPerFlow, default 100) and declarative age retention (retention: { maxAge: '30d', onlyWhen: { status: { $in: ['completed','failed'] } } }, ADR-0057 / ADR-0057 data lifecycle follow-ups: retire per-plugin sweepers, dev telemetry datasource + db:clean, Studio surface, PG rotation (tracking) #2834,pausedrows retained regardless of age). A minute-cadence flow is bounded by the per-flow cap, not by its tick rate.⭐ Positive controls, because a zero from a probe that cannot answer "yes" is NOT MEASURED. The same method, unchanged, did find:
paused-run-visibility.test.ts(GET /automation/:name/runs → 200, zero rows); anddocs/qa/platform-checklist/areas/automation.json("two ticks ⇒ twoshowcase_scheduled_digestrun rows"), reported under F4 above.So the sweep can answer "yes", and here it answers "no".
Lockfile: the
esbuild@0.28.1↔0.28.2swap in the vitest resolution strings ofpackages/apps/setup/studiois reverted. The diff against the merge-base is now exactly the three lines the new dev-dependency needs, andpnpm install --frozen-lockfileexits 0 leaving the file byte-identical (diff -q→ unchanged).⛔ One correction to this body's own earlier measurement
The previous revision said "no platform-organization constant exists anywhere in
packages/(zero non-test hits)". That is false, as the PM seat measured:packages/runtime/src/http-dispatcher.tscarriesPLATFORM_ORG_ID(andSYSTEM_ENVIRONMENT_ID), used at:1412. The claim is withdrawn. The conclusion it supported survives on its own terms — that constant isprivate, unexported, not authorable, and a dispatcher sentinel is not a seededsys_organizationrow — but the option analysis in #17150 starts from something rather than from nothing, and that is where it matters.Verification
All figures below are from the tree at
b998f58f3unless a run is named otherwise.The pin, and that it can fail
packages/qa/dogfood/test/schedule-acting-organization.dogfood.test.ts— real automation + messaging + ObjectQL stack, both drivers, under the exact condition the card measured: twosys_organizationrows under the defaultsingleposture, which is theambiguous-organizationstatesystem-write-organization.tsrefuses on.Test Files 1 passed (1) · Tests 16 passed (16)The earlier round's ablation of the whole fix (context
tenantIdremoved) reddened 8 of 14 and is unchanged by this patch; the new one isolates F1 specifically, which is what the review asked to be provable.Predictions, written before the runs
sys_notificationlands withorganization_id = NULLandsys_inbox_messageis EMPTY. Fixed: both carry the declared organization. Observed, both drivers.sys_automation_runrow for the flow at all. Fixed: the row exists, carries the declared organization, and still namestrigger_type = 'schedule'(trigger.recordIdis never populated on record_change runs, and the persistedsys_automation_runrow carries no trigger block at all — runs cannot be correlated to their triggering record, and trigger kinds are lost across restart #7533). Observed.error, names the flow, names the key, saysNOT BOUND, mentions neither organization on the install — and the flow reads as not bound ongetFlowRuntimeStates()and is named bygetTriggerBindingAudit(). Observed, and the last clause is the one the ablation reddens.Suites
@objectstack/spec test@objectstack/service-automation testTest Files 127 passed (127) · Tests 1496 passed (1496)@objectstack/trigger-schedule testTest Files 5 passed (5) · Tests 99 passed (99)(was 82)@objectstack/dogfood(this pin)Tests 16 passed (16)(was 14)typecheck(spec, service-automation, trigger-schedule)pnpm lint(eslint . --no-inline-config, repo-wide)⛔ No test was skipped, disabled or quarantined. The one
skippedinpackages/specis pre-existing and untouched by this branch.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 113 families for this diff (112 last round, +1check:examples-live-imports, from the showcase docstring edit). All 113 were run and reconciled:Five were red or unmeasured on the first pass; every one was resolved, none reported as a pass:
check:spec-parsed-aliascheck:llms-txtcheck:skill-examplesclient-reactunbuilt)pnpm build, then green:✅ 258 prose examples type-checkcheck:dual-build-cjs-loadsdist/)pnpm build, then exit 0check:type-check-debtOK — 5 ledger entr(ies) re-measured … none above its recorded number(the gate pins its own 6144 MB tsc ceiling, #17151)dispatch-gatesreports this tree as at least 16 commits behindorigin/main, with 6 gate-relevant scripts changed across that range. The derivation is therefore about this tree; CI on the merge is the authority for anything those six moved.Bump
minoron all three packages, with a BREAKING banner and an ADR-0087 disposition (not-required (no-migration-prescription)) — the launch-window lockstep convention, the same shape #16645 landed under.check-adr-0087-registrationandcheck-changeset-no-majorboth green. Notmajor: the remedy is asys_organization.idonly the deployment holds, soobjectstack migrate metacannot mechanically supply it and inventing one is what the ruling forbids.Clause-② carrier: still
yespackages/spec/src/automation/index.tsre-exports the module and@objectstack/spec/automationis a published entry;gen:api-surface,export-originsanddeclaration-maprecord the delta. The patch round narrows that delta — three exports dropped, one renamed — but does not empty it, and the new optionalorganizationkey on both publishedFlowTriggerBindingtypes is untouched. Carrier stands;needs:contract-reviewstays on both carriers.Out of scope, deliberately
unmeasured=0makes a zero-delivery run indistinguishable from a successful one #17123 (a green run that delivered nothing is indistinguishable from a successful one) is NOT closed by this change and must not read as solved because delivery starts working.unmeasuredcounts exactly what it counted before.NotifyConfigSchema) and 方向三 (declaring the inbox objects platform-global) are ruled out. No object's tenancy declaration is touched;system-write-organization.tsstays as the guard — the producer it guards against now carries what it demands.packages/lint'svalidate-flow-trigger-readinesswas not taught the new requirement. It belongs in the same maintainer decision as F4: teaching it today would flag the four package-shipped flows with no value an author could write.维护者速读(草稿)
改了什么 — 定时流程(
schedule)和时间相对扫描(time_relative)现在必须在 start 节点上声明config.organization,声明的组织成为该次运行的执行组织。没有声明的流程在启动时被拒绝绑定:以error点名该流程,并且引擎把它记成"未绑定"—— Studio 的状态徽章显示bound: false,启动摘要的绑定审计会列出它。本轮补丁修的正是这后半句:上一版只打了日志,引擎仍然把它记成"已绑定"。为什么改 — 定时任务由时钟触发,没有会话可以继承租户。多组织安装上,它下面所有租户作用域的写入(通知收件箱、
sys_automation_run运行历史)都被 #8844 的守卫拒绝,而这一层之下没有任何东西汇总这些拒绝 —— 于是这一次 tick 报告自己健康,实际什么都没送达。维护者的裁决是:定时任务本来就只能在组织内运行,应该带组织 ID。风险与代价(含回滚) — 这是一次接受集收窄:今天能启动的、没有声明组织的定时流程,之后不再绑定。这是裁决要求的行为,不是副作用。回滚 = revert 本 PR,没有数据迁移、没有存储形状变化、没有已写入的行需要改写。
席位意见 — (待 at-tier 复核席位填写;实现席位低于
CONTRACT_REVIEW_TIER。)你要做的 — 一件事,在 #17150 上:本仓库自带的四个示例定时流程(showcase 两个、app-todo 两个)按本 PR 的规则会在启动时被拒绝,而包内置的流程没有任何合法的组织 id 可写。⛔ 本 PR 不发明占位 id,也不改这四个流程的行为。相关连带影响已量到:手工平台测试清单里依赖这两个 showcase 流程真的触发的条目会转红,直到 #17150 有结论。
Generated by Claude Code
Generated by Claude Code