Skip to content

fix(pl-middle-layer): re-render production when a block's render failed outright - #1784

Open
vgpopov wants to merge 1 commit into
mainfrom
MILAB-XXXX/run-rerenders-failed-production
Open

fix(pl-middle-layer): re-render production when a block's render failed outright#1784
vgpopov wants to merge 1 commit into
mainfrom
MILAB-XXXX/run-rerenders-failed-production

Conversation

@vgpopov

@vgpopov vgpopov commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Problem

Press Run on a block whose production failed and nothing happens. The click is accepted — the desktop logs Task: run-block | Success: true in ~300ms — and the block never re-runs. No error, no feedback, no state change.

Reproduced against a real project: a block sitting at calculationStatus: "Done", stale: false, outputErrors: true, canRun: true, with 4 of 8 outputs failed. Seven consecutive Run dispatches, seven successes, zero render activity.

Cause

Two places decide whether a block's production "has errors", and they disagree.

The button-enable rulemiddle_layer/project_overview.ts:117-121 — is a 4-way disjunction:

result.error !== undefined || ctx.error !== undefined ||
result.value?.getError() !== undefined || ctx.value?.getError() !== undefined

The actually-render rulemutator/project.ts productionHasErrors — read only the field status, and that status is derived at hydration from getResourceData(f.value).error. So it covered the last two disjuncts and missed the first two.

The gap is in hydration. FieldData (pl-client/src/core/types.ts:126-135) carries value and error, but ProjectMutator.load read only value. A field whose own error slot is filled has a null value, so it was stored as a bare { modCount: 0 } — no ref, no status, no trace the failure ever happened.

Downstream: productionRendered is true (the field state object exists), productionStale is false (args unchanged), productionHasErrors is false — so requireProductionRendering is false, renderProduction selects nothing, and the transaction commits empty.

Fix

  • BlockFieldState gains fieldError, set from f.error during hydration — in both branches, since an errored field takes the branch that has no ref.
  • hasError() folds the two failure shapes into one predicate.
  • productionHasErrors applies it to prodCtx, prodOutput, prodUiCtx.

requireProductionRendering is the only consumer, and it is reached only from the explicit user action Project.runBlock, so the blast radius is one code path.

Verification

  • tsc --noEmit — exit 0
  • formatter:check — clean
  • linter:check — 0 warnings, 0 errors
  • Consumer audit — requireProductionRendering is the sole reader

Not runtime-tested. pl-middle-layer's suite is integration-only (TestHelpers.withTempRoot needs a live pl), and there is no fixture block pack that fails production, so a test needs both a backend and a new fixture. Happy to add it if wanted.

That the observed block takes the newly-covered shape follows by elimination rather than by reading its resource: the two disjuncts the mutator already saw map onto status === "Error", which must be false or Run would have re-rendered — so the true disjunct is one of the two that were dropped.

Follow-ups (not in this PR)

  • A force path for runBlock, so an explicit Run never depends on a staleness predicate being right.
  • get_project_overview in pl-mcp-server drops outputsError/exportsError, so output error text is unreadable headlessly.

Greptile Summary

This PR preserves field-level production errors while hydrating project mutation state, allowing an explicit Run action to rerender production that failed before creating a value resource.

  • Adds BlockFieldState.fieldError, which records whether the project field’s own error slot is populated.
  • Adds hasError(), which unifies field-level failures with errors carried by an existing value resource.
  • Updates productionHasErrors to use the unified predicate for production context, output, and UI context fields.
  • Adds a patch changeset for @milaboratories/pl-middle-layer.
  • Important touched terms:
    • BlockFieldState — the mutator’s hydrated state for a block project field; it now retains a fieldError flag alongside its reference, resource status, and value.
    • fieldError — indicates that a project field’s own error slot is populated, including outright render failures that leave no value resource; newly captured from FieldData.error.
    • FieldStatus — describes the state of an existing value resource, including "Error"; unchanged, but now complemented by fieldError.
    • hasError() — the new common predicate for the two production failure representations: a field-level error or an errored value resource.
    • productionHasErrors — determines whether failed production requires rendering again; changed to apply hasError() to prodCtx, prodOutput, and prodUiCtx.
    • Production rendering — the durable execution path for a block; explicit Run can now select production whose field failed before producing a value resource.

Confidence Score: 5/5

The PR appears safe to merge, with the newly retained field-error state consistently feeding the existing explicit production-rerender path.

The change preserves an error signal previously discarded during hydration and combines it with the existing value-resource error state without changing public interfaces or introducing a demonstrated incorrect execution path.

Important Files Changed

Filename Overview
lib/node/pl-middle-layer/src/mutator/project.ts Preserves field-level errors during hydration and includes them in the production rerender predicate; no actionable defect was identified.
.changeset/run-rerenders-failed-production.md Adds an accurate patch release note describing the newly handled production-failure representation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[ProjectMutator loads production fields] --> B{Field has its own error?}
  B -- Yes --> C[fieldError = true]
  B -- No --> D[fieldError = false]
  C --> E[hasError]
  D --> E
  F{Value resource status is Error?} --> E
  E --> G{productionHasErrors}
  G -- Yes --> H[requireProductionRendering]
  H --> I[Explicit Run rerenders production]
Loading

Reviews (1): Last reviewed commit: "[MILAB-XXXX]: fix: re-render production ..." | Re-trigger Greptile

Context used:

…outright

The Run button is enabled whenever a block's production carries an error,
but the mutator recognised only one of the two shapes such a failure takes.
`productionHasErrors` read the field's `status`, and that status is derived
from the resource the field points at — so it saw a value resource that
exists and carries an error, and missed a field whose own error slot is
filled, which has no value resource at all.

A block in that second shape reports `outputErrors` to the desktop, so its
Run button is live, yet `renderProduction` finds nothing to re-render for it
and commits an empty transaction: the click is accepted and the block never
re-runs. Nothing tells the user their retry was dropped.

`f.error` is now carried through `ProjectMutator.load` alongside the value
reference, and both shapes are folded into one `hasError` predicate that
`productionHasErrors` applies to all three production fields. The condition
the desktop enables the button on and the condition the mutator renders on
are the same condition again.

Carrying the flag in both branches of the hydration is the load-bearing
part: an errored field has a null value, so it takes the branch that has no
ref, and setting the flag only where a ref exists would have changed nothing.
@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cbe94c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@milaboratories/pl-middle-layer Patch
@platforma-sdk/pl-cli Patch
@platforma-sdk/test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
2274 1 2273 22
View the full list of 1 ❄️ flaky test(s)
src/exec/workdir/mkdir.test.ts > exec.builder.mkDir creates real nested directories (MILAB-6460)

Flake rate in main: 7.32% (Passed 38 times, Failed 3 times)

Stack Traces | 4.13s run time
ComputableError: Computable error: PlErrorReport: [I] "NG:0x4D7E/dirs": has input errors:
[I] "NG:0x4D7D/resource": has input errors:
[O] "NG:0x4D7B/outputs/dirs": has input errors:
[I] "NG:0x4D8C/resource": has input errors:
[I] "NG:0x4D8B/resource": has input errors:
[O] "NG:0x4D86/outputs/progress": has input errors:
[I] "NG:0x4DE4/resource": has input errors:
[I] "NG:0x4DDC/inputs": has input errors:
[I] "NG:0x4DDD/workdir": has input errors:
[I] "NG:0x4DD7/allocation": has input errors:
[I] "NG:0x4DD6/resource": has input errors:
[I] "NG:0x4DD4/inputs": has input errors:
failed to fill working directory "workdirs/0x4DFC": failed to create dir "a/b" in workdir "workdirs/0x4DFC": mkdir .../0x4DFC/a/b: no such file or directory
 ❯ throwComputableError ../...../src/computable/computable.ts:77:34
 ❯ Computable.getFullValue ../...../src/computable/computable.ts:339:34
 ❯ Computable.awaitStableFullValue ../...../src/computable/computable.ts:294:23
 ❯ Computable.awaitStableValue ../...../src/computable/computable.ts:315:13
 ❯ .../exec/workdir/mkdir.test.ts:15:21

Caused by: Caused by: Caused by: PlErrorReport: PlErrorReport: [I] "NG:0x4D7E/dirs": has input errors:
[I] "NG:0x4D7D/resource": has input errors:
[O] "NG:0x4D7B/outputs/dirs": has input errors:
[I] "NG:0x4D8C/resource": has input errors:
[I] "NG:0x4D8B/resource": has input errors:
[O] "NG:0x4D86/outputs/progress": has input errors:
[I] "NG:0x4DE4/resource": has input errors:
[I] "NG:0x4DDC/inputs": has input errors:
[I] "NG:0x4DDD/workdir": has input errors:
[I] "NG:0x4DD7/allocation": has input errors:
[I] "NG:0x4DD6/resource": has input errors:
[I] "NG:0x4DD4/inputs": has input errors:
failed to fill working directory "workdirs/0x4DFC": failed to create dir "a/b" in workdir "workdirs/0x4DFC": mkdir .../0x4DFC/a/b: no such file or directory
 ❯ parsePlError ../...../pl-errors/src/parsed_error.ts:230:10
 ❯ getResourceFromTree ../...../pl-tree/src/accessors.ts:118:13
 ❯ PlTreeEntryAccessor.node ../...../pl-tree/src/accessors.ts:152:12
 ❯ ../...../test/src/test-template.ts:50:10
 ❯ Object.___kernel___ ../...../src/computable/computable.ts:592:19
 ❯ renderSelfState ../...../src/computable/computable_state.ts:411:28
 ❯ updateCellStateWithoutValue ../...../src/computable/computable_state.ts:612:7
 ❯ updateCellState ../...../src/computable/computable_state.ts:633:17
 ❯ ../...../src/computable/computable.ts:398:23
 ❯ Computable.getValueOrError ../...../src/computable/computable.ts:420:7

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { rawBackendMessage: '{"errorType":"","message":"[I] \"NG:0x4D7E/dirs\": has input errors:\n[I] \"NG:0x4D7D/resource\": has input errors:\n[O] \"NG:0x4D7B/outputs/dirs\": has input errors:\n[I] \"NG:0x4D8C/resource\": has input errors:\n[I] \"NG:0x4D8B/resource\": has input errors:\n[O] \"NG:0x4D86/outputs/progress\": has input errors:\n[I] \"NG:0x4DE4/resource\": has input errors:\n[I] \"NG:0x4DDC/inputs\": has input errors:\n[I] \"NG:0x4DDD/workdir\": has input errors:\n[I] \"NG:0x4DD7/allocation\": has input errors:\n[I] \"NG:0x4DD6/resource\": has input errors:\n[I] \"NG:0x4DD4/inputs\": has input errors:\nfailed to fill working directory \"workdirs/0x4DFC\": failed to create dir \"a/b\" in workdir \"workdirs/0x4DFC\": mkdir .../0x4DFC/a/b: no such file or directory"}', plErrorType: '', plMessage: '[I] "NG:0x4D7E/dirs": has input errors:\n[I] "NG:0x4D7D/resource": has input errors:\n[O] "NG:0x4D7B/outputs/dirs": has input errors:\n[I] "NG:0x4D8C/resource": has input errors:\n[I] "NG:0x4D8B/resource": has input errors:\n[O] "NG:0x4D86/outputs/progress": has input errors:\n[I] "NG:0x4DE4/resource": has input errors:\n[I] "NG:0x4DDC/inputs": has input errors:\n[I] "NG:0x4DDD/workdir": has input errors:\n[I] "NG:0x4DD7/allocation": has input errors:\n[I] "NG:0x4DD6/resource": has input errors:\n[I] "NG:0x4DD4/inputs": has input errors:\nfailed to fill working directory "workdirs/0x4DFC": failed to create dir "a/b" in workdir "workdirs/0x4DFC": mkdir .../0x4DFC/a/b: no such file or directory', errors: [ { stack: 'PlInternalError: [I] "NG:0x4D7E/dirs": has input errors:\n[I] "NG:0x4D7D/resource": has input errors:\n[O] "NG:0x4D7B/outputs/dirs": has input errors:\n[I] "NG:0x4D8C/resource": has input errors:\n[I] "NG:0x4D8B/resource": has input errors:\n[O] "NG:0x4D86/outputs/progress": has input errors:\n[I] "NG:0x4DE4/resource": has input errors:\n[I] "NG:0x4DDC/inputs": has input errors:\n[I] "NG:0x4DDD/workdir": has input errors:\n[I] "NG:0x4DD7/allocation": has input errors:\n[I] "NG:0x4DD6/resource": has input errors:\n[I] "NG:0x4DD4/inputs": has input errors:\nfailed to fill working directory "workdirs/0x4DFC": failed to create dir "a/b" in workdir "workdirs/0x4DFC": mkdir .../0x4DFC/a/b: no such file or directory\n    at parseCoreError (.../pl-errors/src/parsed_error.ts:324:10)\n    at parseSubErrors (.../pl-errors/src/parsed_error.ts:275:21)\n    at parsePlError (.../pl-errors/src/parsed_error.ts:228:18)\n    at getResourceFromTree (/home/runner/_work/platforma/platforma.../pl-tree/src/accessors.ts:118:13)\n    at PlTreeEntryAccessor.node (/home/runner/_work/platforma/platforma.../pl-tree/src/accessors.ts:152:12)\n    at /home/runner/_work/platforma/platforma.../test/src/test-template.ts:50:10\n    at Object.___kernel___ (/home/runner/_work/platforma/platforma.../src/computable/computable.ts:592:19)\n    at renderSelfState (/home/runner/_work/platforma/platforma.../src/computable/computable_state.ts:411:28)\n    at updateCellStateWithoutValue (/home/runner/_work/platforma/platforma.../src/computable/computable_state.ts:612:7)\n    at updateCellState (/home/runner/_work/platforma/platforma.../src/computable/computable_state.ts:633:17)', message: '[I] "NG:0x4D7E/dirs": has input errors:\n[I] "NG:0x4D7D/resource": has input errors:\n[O] "NG:0x4D7B/outputs/dirs": has input errors:\n[I] "NG:0x4D8C/resource": has input errors:\n[I] "NG:0x4D8B/resource": has input errors:\n[O] "NG:0x4D86/outputs/progress": has input errors:\n[I] "NG:0x4DE4/resource": has input errors:\n[I] "NG:0x4DDC/inputs": has input errors:\n[I] "NG:0x4DDD/workdir": has input errors:\n[I] "NG:0x4DD7/allocation": has input errors:\n[I] "NG:0x4DD6/resource": has input errors:\n[I] "NG:0x4DD4/inputs": has input errors:\nfailed to fill working directory "workdirs/0x4DFC": failed to create dir "a/b" in workdir "workdirs/0x4DFC": mkdir .../0x4DFC/a/b: no such file or directory', fullMessage: 'PlInternalError: [I] "NG:0x4D7E/dirs": has input errors:\n[I] "NG:0x4D7D/resource": has input errors:\n[O] "NG:0x4D7B/outputs/dirs": has input errors:\n[I] "NG:0x4D8C/resource": has input errors:\n[I] "NG:0x4D8B/resource": has input errors:\n[O] "NG:0x4D86/outputs/progress": has input errors:\n[I] "NG:0x4DE4/resource": has input errors:\n[I] "NG:0x4DDC/inputs": has input errors:\n[I] "NG:0x4DDD/workdir": has input errors:\n[I] "NG:0x4DD7/allocation": has input errors:\n[I] "NG:0x4DD6/resource": has input errors:\n[I] "NG:0x4DD4/inputs": has input errors:\nfailed to fill working directory "workdirs/0x4DFC": failed to create dir "a/b" in workdir "workdirs/0x4DFC": mkdir .../0x4DFC/a/b: no such file or directory', name: 'PlInternalError', constructor: 'Function<PlInternalError>', toString: 'Function<toString>' } ], fieldName: undefined, resource: '19838|08fe9a0112102eddc65361c8e46bb57a36cec4bd9d831a0d088981808080808080800110012220e5743a0753fbae2d1c152e16ac76d27138e77355de7f724d254c2fc57f174730', resourceType: { name: 'StdMap', version: '1', constructor: 'Function<Object>', __defineGetter__: 'Function<__defineGetter__>', __defineSetter__: 'Function<__defineSetter__>', hasOwnProperty: 'Function<hasOwnProperty>', __lookupGetter__: 'Function<__lookupGetter__>', __lookupSetter__: 'Function<__lookupSetter__>', isPrototypeOf: 'Function<isPrototypeOf>', propertyIsEnumerable: 'Function<propertyIsEnumerable>', toString: 'Function<toString>', valueOf: 'Function<valueOf>', __proto__: { constructor: 'Function<Object>', __defineGetter__: 'Function<__defineGetter__>', __defineSetter__: 'Function<__defineSetter__>', hasOwnProperty: 'Function<hasOwnProperty>', __lookupGetter__: 'Function<__lookupGetter__>', __lookupSetter__: 'Function<__lookupSetter__>', isPrototypeOf: 'Function<isPrototypeOf>', propertyIsEnumerable: 'Function<propertyIsEnumerable>', toString: 'Function<toString>', valueOf: 'Function<valueOf>', __proto__: null, toLocaleString: 'Function<toLocaleString>' }, toLocaleString: 'Function<toLocaleString>' }, fullMessage: 'PlErrorReport: resource: StdMap:1, NG:0x4d7e\n\nPlInternalError: [I] "NG:0x4D7E/dirs": has input errors:\n[I] "NG:0x4D7D/resource": has input errors:\n[O] "NG:0x4D7B/outputs/dirs": has input errors:\n[I] "NG:0x4D8C/resource": has input errors:\n[I] "NG:0x4D8B/resource": has input errors:\n[O] "NG:0x4D86/outputs/progress": has input errors:\n[I] "NG:0x4DE4/resource": has input errors:\n[I] "NG:0x4DDC/inputs": has input errors:\n[I] "NG:0x4DDD/workdir": has input errors:\n[I] "NG:0x4DD7/allocation": has input errors:\n[I] "NG:0x4DD6/resource": has input errors:\n[I] "NG:0x4DD4/inputs": has input errors:\nfailed to fill working directory "workdirs/0x4DFC": failed to create dir "a/b" in workdir "workdirs/0x4DFC": mkdir .../0x4DFC/a/b: no such file or directory\n' }

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants