Repository navigation
fix(release): the re-cut's failure carries its reason — a bound that faults, not one that substitutes - #5152
Conversation
…faults, not one that substitutes Issue #5057. ReleasePostCondition re-cuts the release a consumed request owes, and logs an ERROR when the re-cut does not land. In production that error read, in full: "...AND the release could not be re-cut. The node advertises a build no release names; instances will keep binding 'X' until a release is created for it." — 8 occurrences over 3 minutes across two memex pods and seven node types, naming the consequence, the stale path, the build and the cost, and not once the cause. The cause was discarded one frame below. TryCreateReleaseNode collapsed four unrelated failures into a bare IObservable<string?> emitting null: · no IMeshService on the hub → logged nothing · the create was refused → Warning + stack · the create threw while composing → Warning + stack · the 10 s bound expired → logged NOTHING AT ALL The last is the channel the incident names, and it was unobservable by construction: Timeout(bound, Observable.Return<string?>(null)) SUBSTITUTES the fallback rather than faulting, so the expiry never reached the Catch that logs. Its only trace in production is a gap between two adjacent lines — Hosting/InstanceRequest logged "Re-cutting..." at 22:16:14Z and "...could not be re-cut" at 22:16:24Z, exactly the bound. And the three channels that did log logged at Warning, which the log watcher does not ingest, so the loudest line in the log is the one that cannot say why. A remedy that reports its own failure without being able to report the failure is the defect it exists to catch, one level up: the output looks like a diagnosis and carries none. · ReleaseCreateOutcome carries the path OR the reason, with Attempted separating a failure from a create nobody asked for. Three states, because a string? holds two. · Timeout(CreateBound) FAULTS, routing the expiry into the same Catch as every other failure, where it becomes a reason naming the bound AND saying the create's fate is UNKNOWN rather than known not to have happened. · Every sentence the post-condition emits — the Error line and the compile _Activity diagnosis — now names the cause of BOTH attempts: the settle's own create (until now a Warning reaching no operator-facing surface) and the re-cut. · Because never returns the empty string: an attempted failure arriving with no reason SAYS so, so this defect cannot reappear wearing terseness. · The batch bake runs no post-condition at all and its stamp keeps the previous release path, so it logs the reason itself. It is unwatched by design, which is exactly why. The bound was NOT widened and must not be: a create that cannot land in ten seconds is not a slow create, and raising it is the band-aid that would have kept this unobservable for longer. Control on each side, measured: with the pre-fix wording restored on top of the fix, TheFailedDiagnosis_NamesBothCauses and ATimeout_NamesTheBoundItWaitedOut both go red — the assertions can fail. Green after restore: 29 tests, 0 failed, .trx written. The other side is TheRestoredDiagnosis_ReadsAsARepair — a re-cut that WORKED must read as a repair with no failure wording, because a fix that made every outcome sound like a failure would satisfy every other assertion here and be worse than the defect. What this does NOT establish: why the create does not land. That was the point — it was unobservable, and inventing a cause from a ten-second gap would be a guess dressed as a finding. The candidates the evidence admits (a boot-time storm — the Store/* requests were consumed a day and a half after they were made; a cross-hub write refused mid-flight; an owner that never answered) are distinguishable only by the next occurrence, which is now diagnosable. Doc: Architecture/ReleaseFailureClasses — same lesson as that page's own, one layer down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved moderate issues affect timeout correctness, operator visibility, localization, and production-path test coverage.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 2
Open (2)
What changed in this PR
This PR improves release re-cut failure diagnostics by preserving failure reasons and surfacing timeout outcomes.
Changes:
- Adds reason-carrying
ReleaseCreateOutcomestates. - Propagates timeout and creation failures through diagnostics and batch baking.
- Adds tests and architecture documentation.
| File | Summary | Review notes |
|---|---|---|
test/MeshWeaver.Compiler.Pipeline.Test/ReleaseRecutReportsWhyItFailedTest.cs |
Tests outcome and diagnosis behavior. | — |
src/MeshWeaver.Hosting/NodeTypeBatchBake.cs |
Logs failed batch release creation. | Moderate: use operator-visible severity and avoid asserting an unknown timeout did not create a release. Nit: update stale comment. |
src/MeshWeaver.Documentation/Data/Architecture/ReleaseFailureClasses.md |
Documents the failure model. | — |
src/MeshWeaver.Compiler.Pipeline/ReleasePostCondition.cs |
Composes recovery and failure diagnoses. | Moderate: accurately describe unknown re-cut outcomes and add localization keys. |
src/MeshWeaver.Compiler.Pipeline/NodeTypeCompilationHelpers.cs |
Integrates outcomes into compile settlement. | Nit: update stale outcome documentation. |
src/MeshWeaver.Compiler.Pipeline/NodeTypeBuildState.cs |
Carries release outcomes and timeout reasons. | Moderate: add an end-to-end timeout test covering the reactive chain. Nit: correct contradictory XML documentation. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| .Timeout(CreateBound) | ||
| .Catch<ReleaseCreateOutcome, Exception>(ex => |
There was a problem hiding this comment.
Accepted — and this is the finding that matters most, because it is the exact standard the change is about: a verification step that cannot fail is not a verification step, and my controls only proved the wording can fail.
I did measure a negative control, but on the wrong subject. Restoring the pre-fix sentence on top of the fix reddens TheFailedDiagnosis_NamesBothCauses and ATimeout_NamesTheBoundItWaitedOut — so the sentences are pinned. The Timeout(bound, Observable.Return(null)) → Timeout(bound) change, which is the load-bearing half, is pinned by nothing: you are right that reverting it leaves every new test green, because Describe is reached through a TimeoutException I construct by hand rather than one the operator produced.
Fixing it the way the house does this: extracting the bounded-create composition as Bounded(IObservable<Unit> create, string releasePath, IScheduler, ILogger) — pure over its source and its clock, the same seam FleetWatchHeartbeat.Track uses — and driving it with a HistoricalScheduler, no mesh and no sleep. Three cases through the real .Timeout(...).Catch(...) chain:
- a create that never emits, clock advanced past
CreateBound→ the outcome carries aFailurenaming the bound. This is the discriminator you asked for: a substituting fallback yields an outcome with no reason at all, soBecausereports "the attempt reported no reason" and the assertion on the bound fails. The revert is caught. - a create that emits inside the bound →
Landed, no failure wording. - a create that errors with
NodeAlreadyExists→ adopted at the same path, which also puts A release re-cut collides with its own first attempt, and the type is left advertising a build no release names #3407 under the production chain rather than only underAdoptOnOwnCollisionin isolation.
Pushing that now.
| internal static string RestoredDiagnosis(string violation, string firstAttemptClause, string restored) => | ||
| $"Release post-condition (#781): {violation}.{firstAttemptClause} Restored at {restored} " | ||
| + "from the bytes this compile produced — no recompile."; |
There was a problem hiding this comment.
Accepted. I checked the premise rather than taking it, and it holds on both halves:
src/MeshWeaver.Data.Contract/LogMessage.cssays it in its own words — an activity transcript is a RENDERED surface,Messagealone cannot be localized (ActivityLog messages are persisted English strings — a rendered surface that cannot be localized (53 sites, 0 localizable) #3236), a localizable entry carriesMessageKey+MessageArgsand is resolved at render time off the viewer locale, withMessageas the English fallback.- Two lines above the site you flagged, the sibling entry already does it:
new LogMessage($"Release created: {newReleasePath}", …).WithKey("activity.compile.releaseCreated", ("path", newReleasePath)). So the keyed pattern is established on this very transcript and the post-condition line is the one that skipped it.
It is pre-existing — the unkeyed new LogMessage(diagnosis, …) came in with #781, not with this diff — but that is not a defence: my diff makes that string longer and richer, which is precisely when the rule applies. Fixing it here.
What I am adding: activity.compile.releasePostCondition.restored and activity.compile.releasePostCondition.violated in both strings.en.json and strings.de.json, with the composed parts passed as named arguments, and the existing English sentence kept as the fallback so old persisted rows and any future catalog drift still render as they do today.
One thing I want to be explicit about rather than overclaim: the frame becomes localizable, the detail stays English. The arguments are a node path, a release path, a store version, and an exception type plus its message — none of which is translatable, and inventing German for an exception message would be worse than leaving it. A German viewer gets a German sentence around English identifiers, which is what the keyed pattern gives everywhere else on this transcript.
This adds catalog keys, so the PR body now carries the Mirror-sync: statement the i18n mirror gate wants — the mirror in MeshWeaver.Plugins/clients/react/src/i18n/ pins a core commit and reds nothing when a key is added here, so handing the sync over explicitly is the only thing that keeps it from going stale silently.
Test Results (shard 0) 1 files 1 suites 2m 42s ⏱️ Results for commit c8b0ee5. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)533 tests 533 ✅ 1m 2s ⏱️ Results for commit c8b0ee5. ♻️ This comment has been updated with latest results. |
Test Results (shard 4) 2 files 2 suites 6m 15s ⏱️ Results for commit c8b0ee5. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)1 528 tests 1 528 ✅ 7m 42s ⏱️ Results for commit c8b0ee5. ♻️ This comment has been updated with latest results. |
…cript entry Both findings from the automatic review on #5152, answered on their threads first. 1. The controls did not cover the load-bearing change. Reverting `Timeout(CreateBound)` to a SUBSTITUTING `Timeout(CreateBound, other)` left every new test green, because `Describe` was only ever reached through a TimeoutException built by hand. The bounded wait is now a seam — `Bounded(IObservable<Unit>, string, IScheduler?, ILogger?)`, pure over its source and its clock, the same shape the fleet-watch dead-man's switch uses — driven by a HistoricalScheduler with no mesh, no clock and no sleep. Four cases through the production `.Timeout(...).Catch(...)`: an elapsed create reports the bound; one that lands inside it succeeds; a NodeAlreadyExists refusal is adopted (#3407 through the chain, not only through AdoptOnOwnCollision in isolation); any other refusal fails carrying its reason. Measured: with the substituting bound restored, AnElapsedCreate_ReportsTheBound_ThroughTheRealChain fails on the absent reason. The revert is caught. 2. The _Activity transcript entry was unkeyed, so it rendered its English interpolation to every viewer forever. LogMessage says this in its own words — a transcript is a RENDERED surface, `Message` alone cannot be localized (#3236), a localizable entry carries MessageKey + MessageArgs and resolves at render time off AccessContext.Locale — and the sibling entry two lines above already does it (`activity.compile.releaseCreated`). Pre-existing (it came in with #781), but this diff makes that string longer and richer, which is exactly when the rule applies. `Restore` now returns the entry itself, keyed, carrying its own level: activity.compile.releasePostCondition.{restored,violated} in both catalogs, with the English text kept as the FALLBACK so an old persisted row and a key that later leaves the catalog still render as they do today. The ARGUMENTS stay English on purpose — a node path, a release path, a store version and an exception's own message are not translatable. BothActivityEntries_AreKeyed_WithTheEnglishTextAsFallback pins the keys, the fallbacks, the levels and the exact placeholder names the catalog values spell. Also recorded in the doc page: what the reportable expiry immediately revealed. The bound stops the WAIT, not the CREATE — the request is already on the bus, so the owning hub writes the node whether or not anyone is still listening. Measured on the control instance over Hosting/InstanceRequest/Release/* (200 nodes, a floor — the listing truncated), from each id's own second stamp to the node's creation: median 0.7 s, 8 of 200 (4%) BEYOND the bound, out to 17.8 s (20260920222136-3V8XoerZ, landed 22:21:53Z, minutes after the incident burst). So a refusal reading "could not be re-cut" was, 4% of the time, emitted over a release node that exists. And the retry compounds it: 20260917173651-dU1GWMZG and 20260917173701-dU1GWMZG share a content hash with ids exactly 10 s apart — the bound — and BOTH landed, because a retry one bound later mints a different second and so collides with nothing for AdoptOnOwnCollision to adopt. That root is named, not fixed here: the remedy is for the re-cut to reuse the abandoned attempt's path so the late landing becomes a NodeAlreadyExists the adoption already resolves. It is a behavioural change to release-id minting with a real in-flight race to design against, and widening the bound is not the alternative — a tail reaching 17.8 s would only make the same failure rarer and harder to catch. Verified: 34 release tests, 0 failed. 60 LocalizationTest cases, 0 failed. Doc gates 6, 0 failed. Release -warnaserror, 0 Error(s): MeshWeaver.Compiler.Pipeline, MeshWeaver.Hosting, MeshWeaver.Compiler.Pipeline.Test, MeshWeaver.Messaging.Hub.Test, MeshWeaver.Documentation.Test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Test Results (shard 3)429 tests 429 ✅ 58s ⏱️ Results for commit c8b0ee5. ♻️ This comment has been updated with latest results. |
…create latency beside the bound The Bounded extraction left the comment block and the catch lambda one level out. The added comment records what the expiry MEANS, beside the bound it belongs to: it bounds the WAIT, not the CREATE — the request is already on the bus. Measured over Hosting/InstanceRequest/Release/* (200 nodes, a floor), id second-stamp to creation: median 0.7 s, 8 of 200 beyond the bound, out to 17.8 s. No behaviour change. 34 release tests, 0 failed; Release -warnaserror, 0 Error(s). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Test Results (shard 5) 5 files 5 suites 13m 38s ⏱️ Results for commit c8b0ee5. |
Test Results 13 files 13 suites 32m 20s ⏱️ Results for commit c8b0ee5. |

Addresses issue 5057 — and deliberately does NOT close it. No closing keyword is used on purpose: this lands the diagnosability the issue asked for first, and the root it immediately revealed is named here but not fixed. Production verification and what is still owed are at the bottom, and on the issue itself: #5057 (comment)
What the defect actually is
ReleasePostConditionexists because a consumed release request can leavelatestReleasePathnaming an earlier build, and its answer is to re-cut the release from the bytes the compile just produced. That remedy can fail, and when it did it reported the failure in the one shape nobody can act on. The production line, in full:It names the consequence, the stale path, the build and the cost. It cannot name the cause — and the cause is the only thing that decides what to do. The reason was discarded one frame below:
NodeTypeBuildState.TryCreateReleaseNodecollapsed four unrelated failures into a bareIObservable<string?>emittingnull:IMeshServiceon the hubWarning+ stackWarning+ stackThe last row is the channel the incident names, and it was unobservable by construction:
Timeout(bound, Observable.Return<string?>(null))substitutes the fallback sequence rather than faulting, so the expiry never reached theCatchthat logs. Its only trace in production is a gap between two adjacent lines —Hosting/InstanceRequestlogged "Re-cutting…" at 22:16:14Z and "…could not be re-cut" at 22:16:24Z, exactly the bound. And the three channels that did log logged atWarning, which the log watcher does not ingest — so the loudest line in the log is the one that cannot say why.A remedy that reports its own failure without being able to report the failure is the defect it exists to catch, one level up: the output looks like a diagnosis and carries none.
Still live, measured today
get @Hosting/InstanceRequeston the control instance, 2026-09-21 ~19:50Z:compilationStatus: Ok,lastCompiledVersion: 4898(compiled 09-21 09:57Z),requestedReleaseAt == lastReleaseRequestHandledAt == 2026-09-20T23:23:19Z(consumed), andlatestReleasePathnaming…/Release/20260920223612-BEscMFNh— cut the previous day. The node still advertises a build no release names, and reads perfectly healthy from every field.What changed
ReleaseCreateOutcomecarries the path or the reason, withAttemptedseparating a failure from a create nobody asked for. Three states, because astring?holds two.Timeout(CreateBound)FAULTS, routing the expiry into the sameCatchas every other failure, where it becomes a reason naming the bound it waited out and saying the create's fate is UNKNOWN rather than known not to have happened — the difference between re-issuing it and going to look.Errorline and the compile_Activitydiagnosis — names the cause of both attempts: the settle's own create (until now aWarningreaching no operator-facing surface at all) and the re-cut.Becausenever returns the empty string: an attempted failure arriving with no reason SAYS the reason is missing, so this defect cannot reappear wearing terseness.FirstAttemptClause,RestoredDiagnosis,FailedDiagnosis,Describe) so the property is asserted rather than hoped for — the same shapeViolationalready had.🚨 The bound was NOT widened and must not be. A create that cannot land in ten seconds is not a slow create, and raising the number is precisely the band-aid that would have kept this unobservable for longer.
CreateBoundis named so the refusal can quote it and so no test writes the number again.Controls — one on each side, both measured
The failure is reported, and the assertion CAN fail. With the pre-fix wording restored on top of the fix, run against the same tests:
A healthy pass still reads healthy. After restoring the fix, the three release classes together:
TheRestoredDiagnosis_ReadsAsARepairis the load-bearing half of that side: a re-cut that WORKED must read as a repair, naming where the release landed, with no "could not" or "VIOLATED" in it — because a fix that made every outcome sound like a failure would satisfy every other assertion here and be worse than the defect.ALandedCreate_Succeeds_AndCarriesNoReasonandALandedFirstCreate_IsWordedAsALandingkeep the success wording total.Builds, one project per invocation,
-c Release -warnaserror, each0 Warning(s) / 0 Error(s):MeshWeaver.Compiler.Pipeline,MeshWeaver.Hosting,MeshWeaver.Graph,MeshWeaver.Compiler.Pipeline.Test,MeshWeaver.Documentation.Test. Doc gates:DocumentationLinkIntegrityTest+ArchitectureTopicMapTest+NoConflictMarkersGuard— 6 tests, 0 failed.scripts/check-record-signatures.py:✓ 5 changed C# file(s): no binary-breaking primary-constructor changes.Surface, and why no pair declaration
TryCreateReleaseNode,ReleaseCreateOutcome,CreateBound,DescribeandReleasePostCondition.*are allinternal(InternalsVisibleTo:MeshWeaver.Graph,MeshWeaver.Hosting, and the test projects). No public top-level type or public member is removed, and no interface member is added — soPairs-with:andImplementers:do not apply. Two i18n catalog keys ARE added, for review finding 2 below, so this PR carries theMirror-sync:declaration the mirror gate wants.🚨 But
internalis not the same as unreachable, and no gate checks it.MeshWeaver.Hosting.Monolith.Test— which lives in MeshWeaver.Plugins, not here — is on theInternalsVisibleTolist, and it is whereReleasePostConditionAtSettleTestpins this exact settle end-to-end. A changed internal signature is not a public-surface removal, soCross-repo pair (public surface)cannot fire on it: this is one of the break shapes the gates do not see. Checked by hand againstSystemorph/MeshWeaver.Pluginsorigin/main:ReleasePostConditionAtSettleTestreferences neitherRestorenorTryCreateReleaseNode— it drives the settle through the mesh, not through these symbols.TryCreateReleaseNodeat all aresrc/Memex.Database.Migration/Migrations/V19_DeleteLegacyReleaseNodes.cs:17andsrc/MeshWeaver.PluginCatalog.Test/RetiredNodePruneTest.cs:189, and both are comments — namingMeshDataSource.TryCreateReleaseNode, a spelling that no longer exists either way.So there is no compile-time caller outside this repo and no pairing is owed. Stated because the absence had to be measured, not assumed.
Review findings, both accepted and both answered on their threads
1 — the controls did not cover the load-bearing change. Correct, and it is the change's own standard turned back on it: reverting
Timeout(CreateBound)to a substitutingTimeout(CreateBound, other)left every new test green, becauseDescribewas only reached through aTimeoutExceptionI built by hand. The bounded wait is now a seam —Bounded(IObservable<Unit>, string, IScheduler?, ILogger?), pure over its source and its clock, the same shapeFleetWatchHeartbeat.Trackuses — driven by aHistoricalScheduler: no mesh, no clock, no sleep. Four cases through the production.Timeout(…).Catch(…).On the scheduler argument, pre-empting the obvious question:
Boundedpassesscheduler ?? DefaultScheduler.Instancewhere the code previously called the no-schedulerTimeout(dueTime)overload. That is behaviour-identical in production, not a change: Rx'sTimeout(source, dueTime)delegates toTimeout(source, dueTime, SchedulerDefaults.TimeBasedOperations), andSchedulerDefaults.TimeBasedOperationsisDefaultScheduler.Instance. Production passesnulland therefore gets exactly the scheduler it got before; only the tests pass anything else. Making it a parameter is what lets the expiry be driven rather than waited out — there is notimeoutbinary on this host and a sleep-based test of a ten-second bound would be both slow and a different measurement.Measured, with the substituting bound restored on top of the fix:
2 — the
_Activitytranscript entry was unkeyed, so it rendered its English interpolation to every viewer forever. Verified rather than taken:LogMessagesays it in its own words (a transcript is a RENDERED surface,Messagealone cannot be localized, #3236), and the sibling entry two lines above already keys itself asactivity.compile.releaseCreated. Pre-existing — it came in with #781 — but this diff makes that string longer and richer, which is exactly when the rule applies.Restorenow returns the keyed entry carrying its own level;activity.compile.releasePostCondition.{restored,violated}added to both catalogs with the English text kept as the FALLBACK, so an old persisted row and a key that later leaves the catalog still render as they do today. The arguments stay English on purpose: a node path, a release path, a store version and an exception's own message are not translatable.Mirror-sync: MeshWeaver.Plugins runs the i18n sync (npm run sync:i18n) against this merge sha after it lands — two keys added, activity.compile.releasePostCondition.restored and .violated, with no existing value re-worded. The mirror pins a core commit rather than tracking main, so adding a key reds nothing there and this handover is the only thing that keeps it from going stale silently.
🚨 What the reportable expiry immediately revealed — the ROOT, measured
Once the expiry had a name, one read settled what it means, and it is not what "timed out" reads as.
Timeoutdisposes this process's subscription to theCreateNoderesponse; the request is already on the bus, so the owning hub writes the node whether or not anyone is still listening. A release id is{yyyyMMddHHmmss}-{8 chars of SHA256(Collection/ContentPath)}— so the id records when the attempt STARTED andcreatedDaterecords when it LANDED, and the gap is measurable from the store with no instrumentation at all.Control instance,
Hosting/InstanceRequest/Release/*, 200 nodes (a floor — the listing truncated at the limit):20260920222136-3V8XoerZ, landed 2026-09-20T22:21:53Z, minutes after the incident burstSo a refusal reading "the release could not be re-cut" was, 4% of the time, emitted over a release node that exists, with the pointer never advanced to it.
🚨 And the retry compounds it, because the id encodes the SECOND:
Identical content hash — by the id's own construction, identical bytes — with ids exactly 10 seconds apart: the bound. The first wait expired, the re-cut minted a new id for the same build, and both landed.
AdoptOnOwnCollisioncannot rescue this: it adopts only a create REFUSED forNodeAlreadyExists, which needs the same id, and #3407's reasoning rests explicitly on the collision happening in the same second. A retry one bound later collides with nothing.The remedy this points at, deliberately not taken here: the re-cut should reuse the abandoned attempt's release path rather than mint a fresh one — the same id turns the late landing into a
NodeAlreadyExiststhe adoption mechanism already resolves correctly, and the duplicate stops being minted. That is a behavioural change to release-id minting with a genuine in-flight race to design against, needing a control that drives expiry → retry → adopt. Widening the bound is not the alternative: a tail reaching 17.8 s would only move the same failure further out while making it rarer and therefore harder to catch.Deploy: addresses to recycle
The change is in compiled
src/, so it ships with the platform image — no NodeType recompile is needed for the fix itself. But every per-node hub for a NodeType binds its configuration once, so after the roll the compile-settle path only runs the new code for activations created after it. Nothing needs recycling to get the fix; the types whose next compile will exercise it are the seven in the incident:Store/Catalog,Store/Purchase,Store/Provision,Store/Tier,Store/Publishing,Store/Subscription,Hosting/InstanceRequest.What this does NOT establish, and what production verification needs
🚨 This does not establish WHY the re-cut's create fails to land. That is the point: it was unobservable, the filed issue's own first task was "log why the re-cut fails", and inventing a cause from a ten-second gap would be a guess dressed as a finding. The candidates the evidence admits — a boot-time storm (the
Store/*requests were consumed a day and a half after they were made), a cross-hub write refused mid-flight (Ops/Logsfrom 09-12/13 showcross-hub write PARTIALLY refusedon this lane), an owner that never answered — are distinguishable only by the next occurrence.#5057 must not be closed on this merge. What would verify it:
memex(control instance) and the running tag confirmed on the Fleet Console.Errorline. It must now end with a reason: eitherdid not land within 00:00:10 — the owning hub did not answer, so whether the node exists is UNKNOWN from here, not no, or a named exception, or the settle's own create's refusal.release restored at …/ no post-condition line at all — no new noise on the healthy path.A second finding, measured and deliberately NOT fixed here
🚨 The post-condition's scope predicate never expires, so once a type has ever had a release request consumed it is held to "every build must have a release" forever.
Violationgates onLastReleaseRequestHandledAt >= RequestedReleaseAt, andRequestedReleaseAtis never cleared — a repo-wide grep finds six assignment sites and not one that nulls it. So every later compile of such a type re-enters the check, finds the build has moved past the standing release, and attempts a re-cut nobody asked for.Confirmed on live data, not inferred:
Hosting/InstanceRequestcarriesrequestedReleaseAt == lastReleaseRequestHandledAt == 2026-09-20T23:23:19ZandlastCompileSucceededAt: 2026-09-21T09:57:42Z— the 09-21 compile fired the post-condition off a request made the previous day. That is also why the incident shows the samerequestedReleaseAtrepeating.The consequence a reviewer should weigh: the release list for these types is sparse and irregular (
Hosting/InstanceRequest: 08-20, 08-21, 08-23, 09-14, 09-17, … 09-20) precisely because the re-cut keeps failing. Once the underlying create is fixed, the same predicate will cut a release per compile on every type that was ever released. Whether that is the intended reading of #781's invariant ("latestReleasePathmust never name a build older thanlastCompiledVersion" — under which it follows) or a latent release-node explosion is a real question. This change does not touch the predicate, the scope or when the re-cut is attempted — narrowing #781's guard is not something to do inside a diagnosability fix.A follow-up worth filing from the same evidence, deliberately NOT fixed here: the
Store/*release requests sat unconsumed from 2026-09-19 06:33 to 2026-09-20 22:19. Whether the release-request path stalls or loses requests before the post-condition is even evaluated is a separate question this change makes no claim about.🤖 Generated with Claude Code