Repository navigation
[17.0-rc][疑似平台] 记录变更流会因自己的写入重入自身 —— 阻止死循环的是引擎的 loop-breaker,而不是流作者写的 start condition #701
Description
Activity
- addedbugSomething isn't workingSomething isn't workingupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstreamBlocked on / caused by the ObjectStack platform — tracked upstream
on Aug 5, 2026 - added a commit that references this issue
on Aug 5, 2026 补充一条来自 #684 实现过程的交互事实(不另开 issue,本条归属这里)。
#684 给全部 7 个 record-change flow 加上了
runAs: 'system',其中就包括本 issue 点名的case_escalation和opportunity_approval。这改变了重入分派时的第二道防线:- 修改前:这两个 flow 的自身写入(
case_escalation写status='escalated'、opportunity_approval写approval_status)是用户上下文下发出的,重入的那一次分派同样带着触发用户,所以 loop-breaker 之后的路径本来就能跑通——loop-breaker 确实是唯一拦住它的东西,与本 issue 的描述一致。 - 修改后:这两个 flow 的自身写入变成 system 写入(
runAs: 'system'的 run,其数据节点以平台身份执行),于是它触发的重入分派不带触发用户。在 Record-change flows never got therunAs: 'system'treatment the scheduled ones did — every system-driven write refuses their data ops (12 failed runs on one boot) #684 之前,这种无用户的重入 run 会在第一个数据节点被[runAs] refusing a data operation拒绝——也就是说,过去在 seed / 系统写入路径上,runAs 拒绝碰巧充当了重入的第二道防线(代价是这些 flow 在该路径上根本跑不起来)。Record-change flows never got therunAs: 'system'treatment the scheduled ones did — every system-driven write refuses their data ops (12 failed runs on one boot) #684 把这个"防线"正当地拿掉了。
结论:修完 #684 之后,loop-breaker 对这两个 flow 就是唯一的防线了,本 issue 描述的"作者写的 start condition 不是真正起作用的机制"这一风险不降反升。这不是 #684 引入的缺陷(那条拒绝本来就是 bug 的副产品,不是设计),但它把本 issue 的优先级论据坐实了:请在平台侧修 start condition 在重入分派上的求值时,把
runAs: 'system'的 record-change flow 一并纳入回归。实测环境同本 issue:
@objectstack/*17.0.0-rc.2。引擎的重入护栏activeRecordFlows以flowName::recordId为键,与runAs无关,所以护栏本身的行为没有变化。Refs #684
Generated by Claude Code
- 修改前:这两个 flow 的自身写入(
GA reading (17.0.0) — REPRODUCES on both named flows. The loop-breaker is still the only thing stopping the loop.
From #1153 (GA close-out B2), session
session_01XAK3brMLjd4ykF4QAhFnuo. Live, user-driven,objectstack dev -p 4002 --seed-admin --fresh,@objectstack/*17.0.0 GA, hotcrmd4ddee0.Why the probe was built this way
"Does the flow terminate" was never the question — it always terminated. The question is which mechanism halts it, and a flow that merely finishes cannot tell the two apart. So the probe asserts three things, and prints all of them:
- the flow actually fired (otherwise a missing WARN means nothing — that is the vacuous-pass shape),
- whether an
[automation]loop-breaker WARN carrying this record's id appears (the log is filtered by the probe's own record id, so seed-time WARNs cannot be mistaken for the repro), - whether the author's guard terms were in fact false at the moment of the re-fire.
case_escalation— the card's step 1/2 reprocreated crm_case v5_h-3H_CbMj6jag (HTTP 201) priority=medium PATCH priority=critical -> HTTP 200 record after the flow settled: status=escalated is_escalated=true escalated_date=2026-08-14T13:14:38.628Z => case_escalation actually FIRED: true loop-breaker WARNs naming v5_h-3H_CbMj6jag: 1 2026-08-14T13:14:39.075Z WARN [automation] flow 'case_escalation' re-entered for the same record while still running — breaking self-trigger loop; the triggering record's id is in this record's meta. Its start condition did not suppress the re-fire; if it guards on a boolean field (e.g. `is_escalated != true`), note booleans persist as 0/1 on SQLite/libsql and CEL `1 != true` is true. start-condition terms on the flow's OWN write: record.status != "escalated" -> false (status=escalated) record.escalated_date == null -> false (escalated_date=2026-08-14T13:14:38.628Z)The flow ran, its own write set both guard terms false, and the engine's loop-breaker still had to catch the re-entry. The engine's own message says it: "Its start condition did not suppress the re-fire."
opportunity_approval— the card's step 3 reprocreated crm_opportunity RnrCSh-5GvgPkzSM (HTTP 201) amount=1000 PATCH amount=250000 (>= LARGE_DEAL_AMOUNT 100000) -> HTTP 200 approval_status=pending loop-breaker WARNs naming RnrCSh-5GvgPkzSM: 1 2026-08-14T13:14:45.822Z WARN [automation] flow 'opportunity_approval' re-entered for the same record while still running — breaking self-trigger loop; ...Same shape. Its gate excludes deals already
pending, the flow's own write setsapproval_status = pending, and the breaker still fires.Not only user-driven
Three more of the same WARN appeared unprompted during the fresh-boot
demo_bootstrapseed sweep (13:10:06–07, record idsfoDLsXSrb6CKOKbe,hIICKFaqJ1q_DuZD,hN6omYKfEla8E9EV), which matches this card's note that an earlier boot showed it during seed-driven writes. Not user-context specific on GA either.What changed since rc.2, and what did not
Only the WARN's wording — GA adds the pointer to the record meta and the SQLite boolean hint. The behaviour is identical. The 2026-08-05 follow-up on this card (
runAs: 'system'from #684 removing the accidental second line of defence) therefore still stands as written: for these two flows the loop-breaker is the sole guard, and the risk this card raised is unmitigated on GA.Disposition
Stays open, labels untouched. Mirror and nomination posted (linked below). No
target:v17applied.
Generated by Claude Code
Generated by Claude Code
Mirror and nomination, per #1153:
- Mirror: A record-change flow's start condition is not evaluated on the re-entrant dispatch its own write causes — the loop-breaker is the only thing stopping the loop, and it says so itself (17.0.0 GA) objectstack#8689 — "A record-change flow's start condition is not evaluated on the re-entrant dispatch its own write causes — the loop-breaker is the only thing stopping the loop, and it says so itself (17.0.0 GA)". Searched objectstack#4862 (predicate bulk writes — closed, different path), #3427, #3481, #6637 first; none cover the ordinary single-record
record-after-updatere-entry. The mirror carries this card'srunAs: 'system'follow-up as the reason the risk is higher now, not lower. - Nomination: [Epic] v17 bug focus — Seat A: platform bugs (objectstack) objectstack#8667#issuecomment-5293907644. No
target:v17applied by this seat.
Generated by Claude Code
Generated by Claude Code
- Mirror: A record-change flow's start condition is not evaluated on the re-entrant dispatch its own write causes — the loop-breaker is the only thing stopping the loop, and it says so itself (17.0.0 GA) objectstack#8689 — "A record-change flow's start condition is not evaluated on the re-entrant dispatch its own write causes — the loop-breaker is the only thing stopping the loop, and it says so itself (17.0.0 GA)". Searched objectstack#4862 (predicate bulk writes — closed, different path), #3427, #3481, #6637 first; none cover the ordinary single-record
Closed — moved upstream to objectstack-ai/objectstack#8689
Dispositioned under the transfer sweep #1156 (part 2). Maintainer ruling, 2026-08-14, quoted verbatim and untranslated:
hotcrm 席位的原则是用平台的能力做元数据应用的开发,平台的需求应该转给平台
and for the hotcrm side of a transferred card, the same ruling: 「关掉,只留指向镜像的指针」. Authority: #1156.
The mirror — verified before this close, not assumed
objectstack-ai/objectstack#8689 — "A record-change flow's start condition is not evaluated on the re-entrant dispatch its own write causes — the loop-breaker is the only thing stopping the loop, and it says so itself (17.0.0 GA)". Confirmed open, labelled
bug, and carrying this card's defect on both named flows, including this card'srunAs: 'system'follow-up as the reason the risk is now higher rather than lower.The reading this close rests on
The GA reading already on this card — comment of 2026-08-14, live and user-driven on
@objectstack/*17.0.0 GA.The probe was built so a green could not be vacuous: it asserts that the flow actually fired, that a loop-breaker WARN carrying that record's id appeared (the log filtered by the probe's own id, so seed-time WARNs cannot be miscounted), and what the author's guard terms evaluated to at the re-fire. On
case_escalation, both guard terms were false on the flow's own write and the breaker still had to catch the re-entry;opportunity_approvalreproduced the same shape; three more occurred unprompted during the fresh-boot seed sweep.The engine's own message is the report: "Its start condition did not suppress the re-fire." And the SQLite boolean hint that message offers does not apply here — this app's condition gates on
statusstrings and a null check, not on the boolean.Why it closes here
The observable contract that fails — "a record-change flow fires only when its start condition is true" — is the automation engine's. The two candidate readings (the re-entrant dispatch skips condition evaluation, or evaluation aborts and the abort counts as a fire) need different platform fixes and neither is reachable from app metadata. This card's own 2026-08-05 follow-up also asks that
runAs: 'system'record-change flows be included in the upstream regression, which only the platform can honour.Nothing is lost by this close: #8689 is open, carries both flows, the probe design, the unprompted seed-time occurrences and the repro, and is readable without hotcrm context.
Generated by Claude Code
Generated by Claude Code
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 9, 2026
Found during the 17.0 GA acceptance sweep on
@objectstack/*17.0.0-rc.2 (currentmain), flow phase.Symptom
Two separate flows, same engine WARN, during ordinary user-driven runs:
Why this is a defect and not just noise
The conditions as authored should have evaluated false on the flow's own write:
case_escalationsetsstatus='escalated'/is_escalated=true— its start condition excludes already-escalated cases.opportunity_approvalsetsapproval_status='pending'— its gate excludes deals already pending.So on the re-entrant dispatch, either the start condition is skipped / failing open, or its evaluation aborts and is counted as a fire. Either way, the guard flow authors write to prevent self-triggering is demonstrably not the mechanism preventing the loop — the engine's last-resort loop-breaker is. That safety net holds today (one WARN per occurrence, no runaway, no data corruption observed), but every flow author who relies on a status-based guard is being silently saved by a mechanism they don't know they depend on, and any future change to the loop-breaker's scope (e.g. cross-record chains, delayed re-fires past the "while still running" window) has no second line of defence.
An earlier boot of the same build showed the same WARN on
case_escalationduring seed-driven writes, so it is not specific to user context.Reproduction
crm_casewithpriority='medium', then PATCHpriority='critical'(user session).opportunity_approval: PATCH an opportunity's amount across the large-deal threshold; the flow's ownapproval_statuswrite triggers the re-entry WARN.Attribution
Platform (automation engine, record-change trigger provider — trigger-context/condition evaluation on re-entrant dispatch). Related in family to #684 (both are trigger-context evaluation gaps on the record-change path); a fix for one should be tested against the other.
Refs #684 #507