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-zhuang and 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

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 6, 2026 4:39pm

Request Review

@github-actions

github-actions Bot commented Jul 6, 2026

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-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Jul 6, 2026
@os-zhuang
os-zhuang merged commit 8b3d363 into main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant