fix(showcase): unstrand the Invoice Dual Sign-off approval demo, and the second instance of the same hop (#7381) - #7395
Merged
Conversation
`notify_cleared` addressed `{record.account.owner}` with no `expand` on the
start node, so the hop read a scalar FK and resolved to nothing. The notify
node refuses a run with zero recipients, so approving the demo recorded the
decision and stranded the flow run at its payoff moment.
The hop was unfixable as written: `showcase_account` has no `owner` field.
The notice now addresses `showcase_invoice.owner`, while the start node
declares `expand: ['account']` and the body reads `{record.account.name}`, so
the demo still teaches relation hydration with a field the account has.
Sweep: `showcase_task_done_notify_owner` carried the same resume-time pattern
(`{record.project.owner}` into a subflow notify, no expand) and is fixed too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fkdTyGmMD5s8ZtEifvuGy
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-help
marked this pull request as ready for review
August 10, 2026 09:43
Contributor
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31376007719 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
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.
Fixes #7381
Approving Invoice Dual Sign-off in the showcase recorded the decision and then stranded the flow run, so the "Notify: Cleared" inbox message the demo promises never arrived.
notify_clearedaddressed{record.account.owner}while the start node declared noconfig.expand, the hop read a scalar foreign key, and the notify node refuses a run with zero recipients.The triage-suggested repair does not work, and the measurement says why
The card preferred option 1 — keep the hop, add
expand: ['account']. Measured against the schema, that leaves the demo just as broken:showcase_accounthas noownerfield. Its declared fields arename,industry,annual_revenue,website,hq,status,sales_region,signed_on,tax_id,support_config,churn_reason,billing_email; the platform's injected record-owner column isowner_id, notowner(OWNER_COLUMN,packages/spec/src/data/injected-system-columns.ts). Soaccount.ownerisundefinedhowever thoroughly the relation is hydrated, and the run strands identically.That is asserted rather than argued — one test reads the persisted request snapshot and pins that the hydrated account has no
ownerkey and does havebilling_email.What landed instead
Both halves of option 1's intent, with a field that exists:
{record.owner}, the invoice's own owner. It is the seeded rep, and the same anchor theshowcase_contributorpermission set scopes invoices by — which is who "your invoice cleared sign-off" is addressed to anyway.expand: ['account']and the message body reads{record.account.name}. The flagship demo still teaches theconfig.expandpath; the declaration is not inert config.Why the pause matters:
expandDeclaredLookupsruns once at flow START, but the failing read happens after an approval that can sit for days. It works because the expander mutates the run'srecordin place, so the hydrated relation is part of the state persisted at suspend and restored verbatim byresumeInternal.Sweep — the card's completion requirement
Every
{a.b.c}relation-hop read across all showcase flow files (comment lines excluded), against whether its start node declares a matchingexpand. 29 flows swept; 27 make no relation hop at all and need none.expandbeforeexpandaftershowcase_invoice_signoff{record.account.owner}, now{record.account.name}['account']account.owner); recipient re-pointed at{record.owner}, hop kept for the account name.showcase_task_done_notify_owner{record.project.owner}into the subflow's{ownerId}thennotify['project']showcase_project.owneris a real seeded field — so declaring the relation is the whole fix.The second instance was not theoretical: on
origin/mainevery completion of a task ranshowcase_task_done_notify_ownerto a failure, and the reverse check below reproduces it independently. It is fixed here per the card's "same class, same file face" clause.One comment-only clarification on
showcase_task_completed, which deliberately uses{record.assignee}: its note said the hop "would resolve to an empty string" without naming the remedy, which now reads as though hops are impossible.Tests
New real-kernel suite,
examples/app-showcase/test/approval-resume-relation-expand.test.ts— a realObjectKernelwith the real ObjectQL / automation / approvals / messaging plugins over real sqlite, the app's real objects and real flow definitions. Requests are opened the wayregisterShowcaseApprovalDemodoes and decided throughApprovalService.decidewith theposition:actor ids the flow's own docblock documents. This is the full level the card asked for, not the reduced fallback.Four cases: the end-to-end approve to resume to delivered inbox message; the persisted-snapshot hydration pin; an in-test rollback to the pre-fix shape; and the sweep's second instance.
Reverse verification — direction predicted, then measured
The fix was taken out at source (
git checkout origin/main -- …flows/index.ts, nevergit stash) and the same suite re-run. Predicted direction: red on resume, with the run stranded while the decision still lands. Measured, 3 of 4 red:The first reproduces #7381's reported toast verbatim. The second matches the issue's quoted payload (
"account": "h8AhbJyB-…", a bare id) — post-fix that column stores the hydrated object. The fourth case stays green by design: it authors the pre-fix shape itself, so the source revert cannot change it.Rejection-class note: the pre-fix case asserts the diagnostic's content (
notify_cleared, the empty template,expand), not a bare throw — a throw-only assertion would stay green on any unrelated failure.Gates
pnpm --filter @objectstack/example-showcase test— 168 passedpnpm --filter @objectstack/example-showcase typecheck— cleanpnpm --filter @objectstack/example-showcase validate— no errors (warnings pre-existing;expandraises nounrecognized_keys)eslinton both touched files — cleannode scripts/check-nul-bytes.mjs— OKThe harness needed four devDependencies the showcase did not carry (
@objectstack/core,service-automation,service-messaging,plugin-approvals) — dev-only, same pattern asexamples/app-todo.Changeset: patch for
@objectstack/example-showcase.Generated by Claude Code