fix(seed/automation): package seed can't wedge the platform — suppress flows on seed + coerce SQLite booleans + re-entrancy loop guard#2661
Merged
Conversation
…rancy guard) A `record-after-update` flow whose action writes back to its OWN trigger record re-fires itself (update → afterUpdate → dispatch → execute → update → …). The start `condition` is meant to suppress the second fire, but a broken guard makes it INFINITE and nothing stops it: each re-fire is a NEW run, so the existing intra-run MAX_NODE_REENTRIES back-edge guard never sees it. 2026-07-06 incident: HotCRM `case_escalation` guards on `record.is_escalated != true`, but a `boolean` field persists as integer `1` on SQLite/libsql and CEL `1 != true` evaluates true, so the guard never trips. During a new env's first-boot metadata seed (which awaits automation to settle) this infinite cascade never settles → the per-env kernel build hangs forever → the runtime serves 503 kernel_warming → the workspace is unopenable (sso-open retried to attempt=5 for hours). Add `activeRecordFlows`: the SAME flow re-entering for the SAME record while an execution is still on the stack is broken (returns skipped, logs a hint about the boolean-stored-as-0/1 footgun). Different flows or different records are untouched, so cross-record fan-out and distinct-flow chains still run. Cleanup in `finally` runs before the returned promise settles, so an error-retry re-run is not falsely blocked. 252 tests pass (23 files) incl. 3 new: breaks the loop, allows cross-record fan-out, allows distinct-flow-same-record. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oerce SQLite booleans for flow conditions
Two root fixes for the class behind the 2026-07-06 first-boot wedge (a seeded
critical case whose escalation flow self-triggered into an infinite loop):
1. **Seed skips record-change automation.** A package's metadata seed is
pre-existing END-STATE reference/sample data, not a stream of user events —
firing on-create/on-update flows (notifications, escalations, assignments,
approvals) for it is semantically wrong and was the loop's vector. Add
`ExecutionContext.skipTriggers`; the seed-loader's SEED_OPTIONS sets it (with
the existing isSystem); `buildSession` threads it onto `HookContext.session`;
the RecordChangeTrigger dispatch handler skips flow launch when set. Lifecycle
HOOKS (derived/default fields, validation) still run — only automation flows
are suppressed.
2. **Coerce boolean fields for the flow/hook view.** SQLite/libsql have no native
boolean, so a driver returns integer `1` for a true column; a flow guard
`record.is_escalated != true` then evaluates `1 != true` = true and never
suppresses the re-fire. New `coerceBooleanFields(schema,row)` (record-validator)
converts 0/1 (and '0'/'1'/'true'/'false') → real booleans on a shallow copy;
the engine applies it to `hookContext.result` (and `.previous` on update)
before after-hooks fire, so flow conditions and `{record.<bool>}` see JS
booleans. The value RETURNED to the caller is untouched (no API shape change).
Null/undefined preserved (a nullable boolean stays null).
Complements the re-entrancy loop guard (prior commit): seed-skip prevents the
loop from ever starting during seed; boolean-coercion fixes the guard class at
runtime; the loop guard is the last-resort backstop for any other self-trigger.
Tests: objectql 797, service-automation 252, trigger-record-change 22,
record-validator 34 — all green. New: 6 coerceBooleanFields cases, 2 seed-skip
dispatch cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(re-entrancy guard)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 100 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Jul 21, 2026
The v16 marquee approval features (M-of-N quorum + finance∧legal 会签, server-computed progress, metadata-driven decision actions, ?request= deep links) could not be exercised in the showcase on a fresh boot: - the approval flows route to `finance`/`legal` positions that were never defined (only manager/exec/... existed) — a dangling reference that made the flows unroutable even when triggered manually; - no user held any approver position (assignments are runtime admin actions, users can't be seeded), so every request resolved to an empty slate; - the seed loader suppresses record-change flows (objectstack-ai#2661), so seeding a `sent` invoice never opened a request; and `sys_approval_request` is engine-owned (ADR-0103), so a request can't be inserted through the data API either. Fix, mirroring `bind-position-sets.ts` (imperative, on `kernel:bootstrapped`): - define the `finance` + `legal` approval-routing positions; - assign the dev-seeded admin to manager/finance/legal (`sys_user_position`) so they resolve as an approver and can act in the inbox — org resolved from `sys_member` (the admin's `sys_user.organization_id` is null), so the org-scoped approver resolution + `getRequest` both match; - provision a phone-based demo user so the "phone sign-in surfaces" show a real number in the All Users list + detail; - seed a high-value (`$8,900`) submitted `EXP-DEMO` report and launch the Invoice Dual Sign-off (会签) and High-Value Committee Quorum (2-of-3) flows through the real automation engine, so two genuine, resumable pending requests land in the inbox on first boot; - add a "Submit for Sign-off" record action on invoices so the flow can be re-triggered on demand from the UI. Idempotent throughout (persistent DB keeps the rows; `openNodeRequest` rejects duplicate pending requests, which we swallow). Verified: on a fresh `objectstack dev --seed-admin`, the Approval Center shows both requests (待我审批 = 2) with the server-computed progress ("Approvals — 0 of 1", the 2→1 quorum clamp) and the metadata-driven Approve/Reject/Reassign/Send-back/ Request-info bar. `os validate` + `tsc --noEmit` pass. Found during the objectstack-ai#3358 v16.0 verification sweep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A new workspace that installed HotCRM was unopenable (2026-07-06): its per-env kernel first-boot hung forever (
503 kernel_warming,sso-openretried toattempt=5). Root cause, confirmed by connecting to the live env's turso DB and watching the loop:HotCRM's
case_escalationflow fires onrecord-after-updateand its action writes back to the same case (is_escalated=true, status='escalated', escalated_date=NOW()), which re-triggers record-after-update. The guardrecord.is_escalated != truewas meant to stop the second fire — but abooleanfield persists as integer1on SQLite/libsql and CEL1 != trueistrue, so the guard never trips and the flow loops forever. The first-boot metadata seed awaits automation to settle → never settles → the whole kernel build hangs. (InMemoryDriver stores real booleans, which is why faithful local repro was green — it masked the bug.)What (three layers, so a package seed can never again wedge the platform)
Seed suppresses record-change automation. A package's metadata seed is pre-existing end-state reference data, not user events — firing on-create/on-update flows for it is semantically wrong and was the loop's vector. New
ExecutionContext.skipTriggers; seed-loaderSEED_OPTIONSsets it;buildSessionthreads it ontoHookContext.session; the RecordChangeTrigger dispatch handler skips flow launch when set. Lifecycle hooks still run — only automation flows are suppressed.Coerce boolean fields for the flow/hook view. New
coerceBooleanFields(schema,row)(record-validator) converts SQLite 0/1 (and'0'/'1'/'true'/'false') → real booleans on a shallow copy; the engine applies it tohookContext.result(and.previouson update) before after-hooks fire, so flow conditions and{record.<bool>}see JS booleans. The value returned to the caller is untouched (no API shape change); null/undefined preserved.Flow re-entrancy loop guard (backstop for any self-trigger).
activeRecordFlows: the SAME flow re-entering for the SAME record while an execution is still on the stack is broken (returns skipped). The existing intra-runMAX_NODE_REENTRIEScan't see this because each re-fire is a NEW run. Different flows / records untouched.Tests
objectql 797, service-automation 252, trigger-record-change 22, record-validator 34 — all green. New: 6
coerceBooleanFieldscases, 2 seed-skip dispatch cases, 3 loop-guard cases.🤖 Generated with Claude Code