Skip to content

fix(seed/automation): package seed can't wedge the platform — suppress flows on seed + coerce SQLite booleans + re-entrancy loop guard - #2661

Merged
os-zhuang merged 4 commits into
mainfrom
fix/seed-skip-flows-and-loop-guard
Jul 6, 2026
Merged

fix(seed/automation): package seed can't wedge the platform — suppress flows on seed + coerce SQLite booleans + re-entrancy loop guard#2661
os-zhuang merged 4 commits into
mainfrom
fix/seed-skip-flows-and-loop-guard

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Why

A new workspace that installed HotCRM was unopenable (2026-07-06): its per-env kernel first-boot hung forever (503 kernel_warming, sso-open retried to attempt=5). Root cause, confirmed by connecting to the live env's turso DB and watching the loop:

HotCRM's case_escalation flow fires on record-after-update and its action writes back to the same case (is_escalated=true, status='escalated', escalated_date=NOW()), which re-triggers record-after-update. The guard record.is_escalated != true was meant to stop the second fire — but a boolean field persists as integer 1 on SQLite/libsql and CEL 1 != true is true, 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)

  1. 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-loader SEED_OPTIONS sets it; buildSession threads it onto HookContext.session; the RecordChangeTrigger dispatch handler skips flow launch when set. Lifecycle hooks still run — only automation flows are suppressed.

  2. 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 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.

  3. 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-run MAX_NODE_REENTRIES can'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 coerceBooleanFields cases, 2 seed-skip dispatch cases, 3 loop-guard cases.

🤖 Generated with Claude Code

os-zhuangand others added 3 commits July 7, 2026 00:06
…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>
@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 6, 2026 4:39pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/metadata-protocol, @objectstack/objectql, packages/services, @objectstack/spec, packages/triggers.

100 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol, @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/cli.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx(via packages/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/profiles.mdx(via @objectstack/spec)
  • content/docs/permissions/roles.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx(via packages/services, @objectstack/spec)
  • content/docs/protocol/objectos/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Jul 6, 2026
@os-zhuang
os-zhuang merged commit 8b3d363 into mainJul 6, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the fix/seed-skip-flows-and-loop-guard branch July 6, 2026 16:48
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang