Repository navigation
Progress on worker-backed function calls - #1002
Conversation
|
What the gate saysjob log. Not a failing check — an unpredicted one, which the gate treats as "the check set cannot be confirmed complete". Why
The asymmetry is visible within this PR's own history: on Worth flagging beyond this PR: any stacked PR whose diff touches a release glob will hit this, and that is most of them. #991 does not, only because it is the bottom of its stack and based on Proposed patch (not applied here)Dropping The alternative fix is upstream in willfire, modelling whatever GitHub is doing with Either way it is a CI-config change with release-path implications, so it should not ride along on a progress-reporting PR. What is not blockedThe substantive checks are unaffected by this and were green locally before the push — 711 unit tests, all 48 integration/e2e suites, clippy, fmt, both binding e2e suites plus their attestations. They are still in flight on this head; if any of them comes back red, that one is mine and I will fix it. This should also resolve itself: when #1000 merges, GitHub retargets this PR to Generated by Claude Code |
7864bae to
b663404
Compare
728b4e5 to
7440497
Compare
b663404 to
4312081
Compare
7440497 to
61530bc
Compare
61530bc to
d27724f
Compare
a755086 to
fc53d7d
Compare
A query that calls a `[[dirsql.function]]` pays one worker round trip per
row, so `dirsql-plugin-embeddings` running `embed(content)` over a corpus
is tens of thousands of round trips inside one `query()` call -- in
silence. This captures what dirsql#957 asks for on that path, before any
implementation exists.
The third test is the one that decided the design (dirsql#1001): the
counter must be erased by the process that owns stdout, or the query
result is printed onto the leftover line. Measured with a fake worker
drawing its own bar:
fakeworker: embedded 13 values[{"v":1},{"v":2},…]
A worker cannot avoid that -- it is SIGKILLed, and it is killed after the
result is printed anyway -- so core counts the round trips instead.
Two tests fail on the empty stderr they capture today; the three guard
tests (no calls means no counter, a pipe stays silent, stdout stays
clean) pass now and hold through the change.
skip-changelog: red test commit, no observable change yet
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gWqG2prbmJg5Pz4caGjnn
Turns the red e2e green. A query calling a `[[dirsql.function]]` on every
row pays one worker round trip per row -- `dirsql-plugin-embeddings` runs
`embed(content)` over a whole corpus that way -- so a single `query()`
can run for minutes in silence.
Core counts the round trips and draws them with the same reporter the
scan uses:
dirsql: running 9204 worker calls (rewritten in place)
dirsql: ran 41231 worker calls in 2m41s (after the line is erased)
`Db::query` opens the phase with an RAII guard, so an error path still
ends it and the live line is always gone before the caller prints rows.
One reporter is shared by every declared function: what a user waiting on
a query wants to know is how many round trips it is paying for, not which
function made them.
Two supporting changes:
- `Progress` gains a `noun`, because worker round trips are not files and
a line calling them files would be lying about what it counted.
- `Progress::restart` reuses a reporter for a fresh phase instead of
constructing one per query. That keeps its sink -- reconstructing threw
away an injected one and silently reverted to stderr, which is how the
unit test for the phase guard first failed.
Fixes #1001.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gWqG2prbmJg5Pz4caGjnn
Extends the CLI reference's progress section with the query phase, points `[[dirsql.function]]`'s worker-lifecycle docs at it (that is where a reader learns round trips are per-row and will wonder what they cost), and notes it in the SDK reference. PARITY.md records it as at-parity, no drift: the reporter sits in `Db::query`, below every SDK's query surface. Worth naming there because it is how `dirsql-plugin-embeddings` gets progress for its `embed(content)`-per-row search without any plugin-side reporting. Migration fragment required -- runtime behavior changes without an API change, since a query that wrote nothing to stderr may now write to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gWqG2prbmJg5Pz4caGjnn
`unit lint` failed on the pushed head: the colocated tests constructed a `crate::progress::Progress` directly, and the isolation rule keeps a rust unit test inside its own module -- "inject a trait double; only `super::` is in-module". `CallReporter` now depends on a `CallProgress` trait that `Progress` implements, and the tests drive a recorder double defined in this module. The assertions get stronger for it: they pin the exact event sequence (`restart`, `update 1`, `update 2`, `finish 2`), which is the orchestration this module actually owns. What the reporter *draws* is progress.rs's business and stays unit-tested there. One test still runs a whole phase against the real `Progress` through the trait, so the production wiring is covered rather than only the double. It draws nothing -- the reporter is disabled unless stderr is a terminal. My miss, not a gate problem: `unit lint` is one of the gates CI declares and I pushed without running it. Run locally now, along with `colocated-test`, `integration lint`, clippy and fmt -- all clean, 711 unit tests and 48 integration suites green. skip-changelog: refactor of an unreleased change on this branch, no observable difference Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gWqG2prbmJg5Pz4caGjnn
call_reporter landed between conn()'s docs and conn() itself, so it took over both the prose and the #[doc(hidden)] and left conn() undocumented.
testing-conventions 0.0.111 now tests mutants on this diff (0.0.x before it reported none) and flagged the Progress delegation: stubbing restart to () left every assertion green, because the only test touching the real reporter went through CallReporter::new(), whose Progress is disabled off a terminal and so draws nothing to erase. Drive a Progress with an injected sink through the trait instead, and assert the live line is gone after restart.
The trait sat in functions.rs, so killing the mutant on its Progress
delegation meant a unit test importing crate::progress::{Mode,
SystemClock} -- which unit lint's no-out-of-module-import rule rejects,
and rightly: a unit test reaching into another first-party module is the
thing the rule exists to stop.
CallProgress describes Progress, so it belongs beside it. functions.rs
consumes it as an ordinary import and still injects its double; the
delegation is now unit-testable from progress.rs using only super::.
fc53d7d to
f6f5c05
Compare
Fixes #1001. Closes the second half of #957.
Stacked on #1000 (
claude/tackle-957-lrm0z6), which adds theProgressreporter this reuses. GitHub retargets this tomainwhen #1000 merges.Why this is in core and not in the plugin
#957 asks for progress "on the embedding path in the plugin". I probed that design before writing it and it does not work. A worker drawing its own
\rcounter on its inherited stderr leaves the line un-erased, and the query result lands on top of it — measured with a fake worker wired in as an ordinary[[dirsql.function]]:The worker cannot fix that itself, for two independent reasons:
ProcessTransport::dropSIGKILLs it (so noatexit, signal handler ortqdm.close()ever runs), and it is torn down afterprintln!prints the result anyway — so it could never know when to erase. An idle-timer thread inside the worker races the host's own stdout write and does not close the gap either. Full repro in #1001.The process that owns stdout has to own the line, and that process is dirsql. It also already has the number worth showing:
functions.rsperforms exactly one round trip per call. So core counts them — no protocol change, no plugin change, and it works for any worker-backed function rather than justembed(), which fits "dirsql knowing nothing about the domain" better than a domain-specific bar would.What it looks like
Same
DIRSQL_PROGRESSgate as #1000 (auto/always/never), same warmup, same silence on a pipe. A query that calls no worker prints nothing even underalways: the reporter speaks only when there is something to say.Notes for review
Db::queryopens the phase with an RAII guard, so an error path still ends it and the live line is always gone before the caller prints rows. The third e2e test is the regression test for exactly that.Progressgains anoun— worker round trips are not files, and a line calling them files would be lying about what it counted.Progress::restartreuses a reporter for a fresh phase instead of constructing one per query. That is not just an optimization: reconstructing discarded an injected sink and silently reverted to stderr, which is how the phase-guard unit test first failed.Db::queryis not reentrant here (create_path_tablerunsexecute_batch, not a nestedquery), so phases never nest.Scope
Core only, again — but the plugin gets it for free, and that is verified rather than assumed:
dirsql-plugin-embeddingsbuildsembed(content)-per-row SQL and runs it through the Python SDK'sdb.query, which is the sameDb::querythis brackets. Driving that path directly through the PyO3 binding (see the manual run below) reports identically to the CLI.The cache-hit split (
… (8811 cached)) stays out, filed as #1034. It is the plugin's private knowledge and surfacing it needs an optional metadata field in the worker response, i.e. a change to the response contract documented indocs/reference/config.md's "Worker protocol" section and restated infunctions.rs's module doc — worth doing only if it turns out to matter. It is also the only part of #1001 that lands inplugins/dirsql-plugin-embeddings/.Red/green
f8b77e7pushed the e2e first. CI went red onIntegration tests(run 32420619521):test result: FAILED. 3 passed; 2 failed, withthe summary carries the count and the elapsed time: ""— the two reporting assertions failing on the empty stderr they captured, the three guard assertions (no calls means no counter, a pipe stays silent, stdout stays clean) passing.863948cis the implementation, which flips those two green.Manual run
Through the CLI, 14 files and a worker sleeping 50 ms per call, under a real pty with no env var set (so this is the default
autopath, warmup and all):Through the Python SDK, the path the plugin actually takes:
E2E Verification
--testsruns all 48 suites underpackages/rust/tests/, including the newworker_call_progress_e2e)packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.jsonreceipt writtenpackages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.jsonreceipt writtencargo test --manifest-path packages/rust/Cargo.toml --features cli --lib→ 711 passedcargo test --manifest-path packages/rust/Cargo.toml --features cli --tests→ 48 suites, all okcd packages/python && uv run maturin develop --release && uvx testing-conventions e2e attest 'uv run python -m pytest tests/e2e/ -x -q'cd packages/ts && pnpm build && uvx testing-conventions e2e attest 'pnpm test:e2e'cargo clippy --workspace -- -D warnings,cargo fmt --all -- --check→ cleanpackages/rust/srcchange stales both bindings' attestations via[e2e].extra_scope.mainmerge:cargo test --features cli --lib→ 807 passed;--tests→ all suites pass exceptpersist_parsed_path_table(pre-existing, machine-speed-dependent, see above).just test-e2e→ 32 passed, 5 pre-existingrepl_interactive_test.pyfailures reproduced identically on cleanmain.uvx testing-conventions e2e verifypasses for both packages against the committed receipts.Rebase onto
main(2026-09-02)mainmoved (#1000 merged, so this retargeted; #1014 landed after). Merged in, and re-ran everything against the moving-tag toolchain, which had advanced totesting-conventions 0.0.111. Three things changed:rust / Unit mutation — changed linesnow actually runs. The earlier0 mutant(s) testednote is obsolete — 0.0.111 tests this diff and found one survivor:replace <impl CallProgress for Progress>::restart with (). Real gap, not an equivalent mutant: the only test touching a realProgresswent throughCallReporter::new(), whose reporter is disabled off a terminal and so has nothing to erase. Killed with an assertion.CallProgressmoved toprogress.rs. Killing that mutant fromfunctions.rsneededuse crate::progress::{Mode, SystemClock}in a unit test, whichunit lint'sno-out-of-module-importrule rejects. The trait describesProgress, so it now lives beside it;functions.rsimports it and still injects its double.conn()got its doc comment back.call_reporterhad landed betweenconn()'s docs andconn()itself, taking over both the prose and the#[doc(hidden)].Merge
main(2026-09-04)#1023 landed the fixes for the two
testing-conventions 0.0.111gates that were reding this PR, somain(612bcf1, all five workflows green) was merged in. The merge was clean — no conflicts. It brings only gate config andinternals/checks; no Rust source moved, so the implementation is untouched.one-function-per-fileneeded no work here: this PR's source diff is Rust-only, and the gate is Python/TypeScript-only ([python]/[typescript] one_function_per_file = { max_lines = 100 }). No exemption was added.Re-verified against the merged tree:
just preflight --gate one-function-per-file --gate colocated-test --gate unit-lint --gate integration-lint --gate e2e-verify --gate declared-deps→0 failing pair(s)across all 33 pairs.just preflight --gate unit-coverage→0 failing pair(s).just preflight --gate mutation→ python / typescript / internals / plugin arms allno mutatable changed lines; the rust arm cannot run on this machine — cargo-mutants aborts withcargo test failed in an unmutated treebecause persist_parsed_path_table asserts a wall-clock floor and fails on a fast machine #1028'spersist_parsed_path_tabletiming assertion fails before any mutant is built. CI's slower runner is the authority and it passes there (rust / Unit mutation — changed lines, 9m47s).dirsql-checks changelog-gate→ pass. Fragments unchanged and still correct:packages/rustis the only changed package, and it carries both a changelog and a migration fragment.PARITY.mdunchanged by the merge; still accurate, still no drift.E2E receipts were not rewritten. The gate is diff-based and does not compare a receipt's
commitagainstHEAD, both receipts are already in themain...HEADdiff, ande2e verifypasses for both packages against the merged tree. Re-attesting python would in fact fail —just e2e-attest-pythononly records a receipt when the command exits 0, andjust test-e2eis red on #1027's five pre-existing REPL failures, which are not this PR's to fix.Suites re-run in full on the merged tree:
cargo test --workspace --features cli --no-fail-fast→ every suite passes except persist_parsed_path_table asserts a wall-clock floor and fails on a fast machine #1028'sunchanged_second_run_reuses_the_cache_instead_of_reparsing(cold run measured 2.30s against a ≥3s assertion).cargo test --features cli --test worker_call_progress_e2e→ 5 passed, this PR's own e2e suite.uv run python -m pytest tests/e2e/ -q(packages/python) → 32 passed, 5 failed — the five are exactly REPL e2e tests still assert JSON output after table rendering landed #1027'srepl_interactive_test.pycases, all failing on table-vs-JSON output.pnpm test:e2e(packages/ts) → 16 passed, 5 files.Not from this PR
These failures are pre-existing on
mainand are handled elsewhere:Resolved — ci: adopt the testing-conventions one-function-per-file gate at max_lines = 100 #1017 / ci: Python integration lint is red -- no-first-party-patch now resolves the patch.object form #1024 are fixed by ci: adopt one-function-per-file at max_lines = 100 #1023, merged topython-sdk / Static checksandtypescript-sdk / Static checksfail on the newunit one-function-per-fileandintegration lint(no-first-party-patch) gates.mainand merged in here.packages/python/tests/e2e/repl_interactive_test.pyhas 5 failures on cleanmain: the REPL e2e tests (cb75f4e, 21:38 UTC) predate table rendering (060576f, 22:33 UTC), so they still assert JSON output while the REPL now draws a table. E2E is not in CI, so nothing caught it. persist_parsed_path_table asserts a wall-clock floor and fails on a fast machine #1028 —packages/rust/tests/persist_parsed_path_table.rsalso fails on a fast machine (asserts a cold run takes ≥3s; measured 2.3s).Changelog / Migrations
packages/rust/changelog.d/2026-08-20-worker-call-progress.mdpackages/rust/migrations.d/2026-08-20-worker-call-progress.md— required, because this changes runtime behavior without an API change: a query that used to write nothing to stderr may now write to it.DIRSQL_PROGRESS=neveris the documented restore.Parity
PARITY.mdupdated. No drift: the reporter sits inDb::query, below every SDK'squery/await db.query, so all three change at once and none carries a copy.