Running an R file with source() (Positron's "run file" / r.sourceCurrentFile) intermittently produces a plot with no origin attribution, so Positron never shows the source-file link on the plot.
Found while chasing a flaky Positron e2e test (plot-file-attribution.test.ts › R - Plot origin shows source file after run file command). It fails as flaky (passes on retry), which is the signature of a timing race rather than a hard bug.
Flake rates
Last 14 days on main, 121 runs each (see metrics):
| Test |
Path |
Flaky |
| R - run file command |
source() |
21% |
| R - line execution |
line-by-line |
1% |
| Python - line execution |
line-by-line |
0% |
| Python - run file command |
python.execInConsole |
5% |
The R source() path is 21% flaky vs. 1% for R line-execution. The line-execution path attaches a code_location that the source() path lacks, so it isn't exposed to this race.
What the R Kernel log shows
The source() execute request arrives with positron: None (no code_location):
ExecuteRequest { code: "source(\"/tmp/.../plot-attribution-test.R\")", ... positron: None }
The line-by-line variant carries code_location: Some(...). Both plots then go through the same deferred path: hook_holdflush -> process_changes -> store_plot_context -> take_pending_origin.
Why the source() path is exposed
capture_plot_origin checks the source-context stack first, then falls back to ctx.code_location. Line execution always has code_location, so even if the stack timing is off, origin still resolves. The source() path has no such fallback: origin depends entirely on the source-context stack (pushed/popped by the source() hook in hooks_source.R via defer()) being populated at capture time.
When the eager pending_origin snapshot in hook_mode is skipped (it's gated on !old_has_changes && is_drawing), take_pending_origin re-derives origin at process_changes time. If defer() has already popped the stack by then, origin comes back None for good, with no signal to the frontend.
Expected vs. actual
- Expected: a plot from
source("file.R") is attributed to file.R every time.
- Actual: attribution is empty ~21% of the time on the
source() path.
Reproduction
No reliable one-shot repro - it's a timing race, reproducing intermittently under load.
Manual (occasional):
- Start a fresh R console in Positron.
- Open a
.R file containing just plot(1:10).
- Run the whole file ("Source" /
r.sourceCurrentFile).
- Check the Plots pane. Usually the origin link appears; occasionally it's missing.
Via the Positron e2e test: tests/plots/plot-file-attribution.test.ts › "R - Plot origin shows source file after run file command". Fails ~21% in CI (Linux x86_64 electron), which runs parallel workers under load.
Existing ark coverage: test_plot_source_context_stacking and test_plot_from_source_dynamic in crates/ark/tests/integration/plots.rs exercise source() + origin and pass deterministically. The DummyArkFrontend harness is single-threaded, so the defer() pop never races the deferred process_changes - which is why the existing suite doesn't catch this.
Fix direction (per Claude)
Make source-path capture independent of the pop timing, e.g. capture origin eagerly when the source context is pushed, hold the pop until pending plots' process_changes runs, or drop the !old_has_changes gate while a source context is active.
Notes
- I couldn't confirm the exact stack state from logs alone - the push/pop and origin-capture functions have no trace logging, so the empty-stack moment is inferred from the surrounding sequence. Trace logging there (or a repro under load) would pin down the precise sub-mechanism.
- The Python run-file case is also elevated (5% flaky vs. 0% for Python line-execution). Python doesn't use
source(), so this is likely a separate, milder origin-attribution gap on the Python side rather than the same bug - worth a look independently.
Running an R file with
source()(Positron's "run file" /r.sourceCurrentFile) intermittently produces a plot with no origin attribution, so Positron never shows the source-file link on the plot.Found while chasing a flaky Positron e2e test (
plot-file-attribution.test.ts › R - Plot origin shows source file after run file command). It fails as flaky (passes on retry), which is the signature of a timing race rather than a hard bug.Flake rates
Last 14 days on
main, 121 runs each (see metrics):source()python.execInConsoleThe R
source()path is 21% flaky vs. 1% for R line-execution. The line-execution path attaches acode_locationthat thesource()path lacks, so it isn't exposed to this race.What the R Kernel log shows
The
source()execute request arrives withpositron: None(nocode_location):The line-by-line variant carries
code_location: Some(...). Both plots then go through the same deferred path:hook_holdflush->process_changes->store_plot_context->take_pending_origin.Why the
source()path is exposedcapture_plot_originchecks the source-context stack first, then falls back toctx.code_location. Line execution always hascode_location, so even if the stack timing is off, origin still resolves. Thesource()path has no such fallback: origin depends entirely on the source-context stack (pushed/popped by thesource()hook inhooks_source.Rviadefer()) being populated at capture time.When the eager
pending_originsnapshot inhook_modeis skipped (it's gated on!old_has_changes && is_drawing),take_pending_originre-derives origin atprocess_changestime. Ifdefer()has already popped the stack by then, origin comes backNonefor good, with no signal to the frontend.Expected vs. actual
source("file.R")is attributed tofile.Revery time.source()path.Reproduction
No reliable one-shot repro - it's a timing race, reproducing intermittently under load.
Manual (occasional):
.Rfile containing justplot(1:10).r.sourceCurrentFile).Via the Positron e2e test:
tests/plots/plot-file-attribution.test.ts› "R - Plot origin shows source file after run file command". Fails ~21% in CI (Linux x86_64 electron), which runs parallel workers under load.Existing ark coverage:
test_plot_source_context_stackingandtest_plot_from_source_dynamicincrates/ark/tests/integration/plots.rsexercisesource()+ origin and pass deterministically. TheDummyArkFrontendharness is single-threaded, so thedefer()pop never races the deferredprocess_changes- which is why the existing suite doesn't catch this.Fix direction (per Claude)
Make source-path capture independent of the pop timing, e.g. capture origin eagerly when the source context is pushed, hold the pop until pending plots'
process_changesruns, or drop the!old_has_changesgate while a source context is active.Notes
source(), so this is likely a separate, milder origin-attribution gap on the Python side rather than the same bug - worth a look independently.