fix(automation): stop logging the degraded "child run is gone" warning on the healthy subflow up-bubble - #14827
fix(automation): stop logging the degraded "child run is gone" warning on the healthy subflow up-bubble#14827claude[bot] wants to merge 3 commits into
Conversation
…restart Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a27799b89384c7bdb78da66a9a3b2e4c6929e7d5 && git checkout a27799b89384c7bdb78da66a9a3b2e4c6929e7d5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6665c5c916caf6d95022734328ae82154e1a5f10 f6c6cb94abd21c949098571e4e37be9546e3b0e5 && git checkout -B drift-repro 6665c5c916caf6d95022734328ae82154e1a5f10 && git merge --no-ff f6c6cb94abd21c949098571e4e37be9546e3b0e5
node scripts/docs-audit/affected-docs.mjs --json 6665c5c916caf6d95022734328ae82154e1a5f10 |
|
CI red diagnosed as not this PR's — the fix is already on
|
| ref | packages/cli/bin/stderr-nonblocking.mjs |
|---|---|
origin/main |
present (with test/fixtures/stderr-nonblocking-probe.mjs and test/run-dev-stderr-nonblocking.e2e.test.ts) |
this PR's head af8d0e7eb |
absent — 0 hits |
Control for the absence read: packages/cli/bin/run-dev.js = 1 hit at the same ref, so the query resolves.
#14875 (bin/stderr-nonblocking.mjs re-asserts O_NONBLOCK on fd 2 per write, installed above run() in bin/run-dev.js) landed on main at 11:34:02Z. This PR's head was pushed at 04:43Z — about seven hours earlier. Its CI therefore ran the test in the exact window where the hang reproduces, against a base that did not yet carry the repair.
Action taken
⛔ No re-run, and ⛔ nothing skipped, disabled, or quarantined. I have updated this branch from main, which brings #14875 in and re-runs CI against a base that carries the fix. If the shard goes green, this PR is undrafted and armed; if it fails again on the updated base, the failure is this PR's and I root-cause it here.
Generated by Claude Code
Fixes #14392
What was wrong
Every successful subflow completion logged this, at
warn:Both halves are false on that path. The child had not gone anywhere — it had
completed, the normal outcome — and the parent was continuing with the
child's output, not without it: the very signal the engine held when it wrote
the line already carried it.
The mechanism (verified against
origin/main, not assumed)A parent parked at a
subflownode correlates to its child assubflow:CHILDID. On resume,resumeInternalcallsloadSuspendedRun(CHILDID)— which finds only SUSPENDED runs. A child thatfinished has no suspension to find, so the miss fell through to an
elsewritten for the genuinely degraded case.
The lookup answers "is the child still parked", which on the up-bubble is a
question about nothing: the child is supposed to be finished there. That is
why the fix is not a second run lookup — that would move the same confusion
one call over.
The fix
Branch on the fact the message is actually about: does the incoming signal
already carry the child's output.
debugline naming the carried output;sentence, existing level, untouched.
The engine-built marker is load-bearing, not decoration.
outputis acaller-writable field, and on this node a caller's signal is delegated down
to the child, so matching on shape alone would let a caller's own bag silence a
genuine degraded warning. Only the engine can mint that symbol, and there are
exactly two mint sites in the package (
buildSubflowResumeSignal, and themaphandoff — which parks under a distinct
map:correlation and never reaches thisbranch).
Before / after of the log call site
The
elseis byte-identical to before — the entire engine diff is additive.Every added non-comment line in
engine.ts:Removed lines in
engine.ts: zero. The degradedwarn, its text and itslevel are untouched, and both branches fall through to exactly the same
continuation code — no early return, no state change.
The level fence, measured
Every
this.logger.*call site inengine.ts, base vs head:warnerrorinfodebugDiffing the call-site text with line numbers stripped yields exactly one line:
> this.logger.debug(. No existing call site changed level — the onlymovement is one added
debug, which is the option the ruling offers for thehealthy path.
The pin can fail — ablation
Pinned on the branch, not on the sentence. The test uses a
recordingLogger(the pre-existing
silentLoggerdiscards, so absence cannot be asserted againstit) and asserts the degraded sentence is absent on the engine-built
up-bubble, plus a positive control that it still fires when no child run and no
carried output exist.
Ablating the predicate to always return
falserestores the old always-logbehaviour:
Test Files 1 passed (1)·Tests 10 passed (10)Test Files 1 failed (1)·Tests 1 failed | 9 passed (10)Test Files 1 passed (1)·Tests 10 passed (10)The ablated failure is the original bug, reproduced verbatim:
Mutation confirmed on disk (not by a
//comment — esbuild strips those):anchor line count
1 → 0, injectedglobalThismarker count0 → 1, and blobhash
9f7ef89f5… → 059f24835…. Restore proven by blob equality back to theHEADblob9f7ef89f5…, marker count back to0, an emptygit diff HEADanda clean
git status.No rebuild is in the ablation loop, and that is a measured claim rather than an
assumption: the test imports
../engine.js— a same-package relativeimport, which vitest resolves to
src/engine.ts, never throughexportstodist/. The red/green flip is itself the proof, since a mutation the runner didnot read would have stayed green.
Verification
All at
af8d0e7eb.pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2—Test Files 101 passed (101)·Tests 1203 passed (1203)tsc --noEmit -p tsconfig.jsonover the package reports 3 errors, none in either edited file — all three are TS2341 insrc/nested-region-parity.test.tsat 95/151/180, which is exactly the entry recorded incheck-type-check-coverage.mjs(errors: 3, "code-tier 3 (TS2341 x3), all in src/nested-region-parity.test.ts at 95/151/180"). The debt number moves by zero.--listFilesconfirms both edited files are genuinely in the checked set (443 files), so this is a measurement and not a vacuous pass.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat the final commit — 36 commands, identical to the pre-changeset derivation. 33 pass. The 3 remaining exit 3 and say so themselves —check-test-completeness("Nothing was measured… ⛔ It is NOT a finding"),check:dual-build-cjs-loads("PREREQUISITE NOT MET… ⛔ This is NOT a pass"),check:type-check-debt("⛔ This is NOT a pass and NOT a finding"). All three need a full-repo build, which CI performs. Exit codes captured by redirect-then-read, never across a pipe.A
patchchangeset is included: this is a bug fix in a released package(
@objectstack/service-automation@17.2.0).Scope
Log text and one branch. No new exported symbol, no payload key, no accept/reject
change, no behaviour change. Adjacent but deliberately not folded in: #14379
remains open and is not addressed here.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Generated by Claude Code