Repository navigation
feat: review inner joins that drop most grouped rows at answer_commit - #1551
anandgupta42 wants to merge 3 commits into
Conversation
- new `answer-commit-join-coverage.ts`: locates the `sql_execute` whose output shows every committed value (abstains on no evidence, truncation or differing matching SQL), extracts inner-join pairs from DuckDB's serialized AST, and measures per-side totals and EXISTS semi-join matches on the leased session scratch connection; the join itself is never executed - catalog gates before any data statement: scratch base tables only, <= 500,000 rows, key columns fixed-width or VARCHAR with known `Max String Length` <= 256 and no pending updates, rows x key width <= 16 M chars, built-in allowlisted wrappers only, effective query timeout >= 5 s - `answer_commit` reserves one review per user turn synchronously (keyed by the user message id) and returns a one-time `answer_commit review:` rejection when a grouped side keeps < 50% of >= 20 rows and `basis` does not state "<matched> of <total>"; the unchanged recommit is accepted - off with `ALTIMATE_SQL_AUTO_CHECK=off` Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
- abstain on constant-only WHERE conjuncts such as `1 = 0` - bound every side-filter column by the same catalog width gate as key columns - compare located queries by their exact SQL text, so queries differing inside a literal abstain Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
- skip when a side has more than 8 WHERE conjuncts or key and filter expressions together exceed 64 AST nodes, so probe work cannot grow with predicate count, `IN` lists or `COALESCE` arguments - abstain for `GROUP BY ALL`, `SELECT DISTINCT` and window-function queries instead of treating every table as a grouped side - pin the history-based review-slot guards; loosen wall-clock asserts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing after a live evaluation. On a sparse-join question the check fired on 15 of 16 trials, with no false positives across 20 trials on join-heavy questions where it should stay silent, and about 100 ms added per commit. It did not change the outcome: every agent acknowledged the measured coverage ("23 of 513") and recommitted the same answer, because the loss came from the choice of source rather than the join itself. Since it adds automatic queries on user data without a measured benefit, I'm not shipping it. The design (per-side counts plus EXISTS semi-joins, never re-running the join, catalog byte and node caps) and the evaluation are kept in the internal research notes in case we revisit it as a product feature. |
Issue for this PR
No linked issue (
feat). Draft: pending a live evaluation before it is marked ready.Type of change
What does this PR do?
Why. An analyst agent often materializes two tables into the session scratch database, inner-joins them on an ID whose format or coverage differs between the sides, and commits an aggregate over the few rows that survived (for example 23 of 513 patients), sometimes even writing "23 patients" in
basis. Nothing tells it that most of the grouped population vanished in the join. A check on everysql_executewas tried before and rejected: it ran probes on the shared scratch connection for every query and keyed on the wrong query. This PR checks once, atanswer_commit, on the query that produced the committed values.What changed (
packages/opencode/src/altimate/tools/answer-commit-join-coverage.ts, wired intoanswer-commit.ts):sql_executeparts, the one whose output shows every committed value (numbers above 10 or with decimals, exact or rounded/truncated at the committed places; non-numeric strings of 3+ chars). It abstains when there is no such evidence (e.g. a lone integer5), nothing matches, a matching output was truncated, or two matching queries differ in SQL text. It does not fall back to the latest query.json_serialize_sqlAST: one SELECT, no CTE/subquery/table function, 2 to 3 scratch base tables joined by INNER joins (ON,USINGor comma join with a WHERE equality), up to 2=key conjuncts per pair. Keys may be wrapped inupper/lower/trim/ltrim/rtrim/btrim/substr/substring/left/right,CAST,COALESCE. Every WHERE conjunct must belong to one side and becomes that side's filter. Anything else (outer joins,IS NOT DISTINCT FROM,lpad/concat/regexp_replace, cross-side filters, 16+ digit literals) means it skips.count(*)andcount(*) ... WHERE EXISTS (match in the other side). These are semi-joins built from single-table projections that the deserializer produces from the agent's own AST nodes, so a fan-out join (20k x 20k on one key, answer withLIMIT 1) costs milliseconds, not 400 M joined rows.scratch.main, no view of the same name, at most 500,000 rows (estimate); each key column fixed-width, or VARCHAR whosepragma_storage_infoMax String Lengthis known for segments covering every row, is at most 256 and has no pending in-place updates (those leave segment stats stale); rows x key width at most 16 M chars per side; every called function built in (a user macro namedtrimskips); every column a side filter reads passes the same width gate; at most 8 WHERE conjuncts per side and at most 64 AST expression nodes across all key and filter expressions (so work cannot grow with predicate count,INlists orCOALESCEarguments); effective scratch query timeout at least 5 s. Constant-only WHERE conjuncts (1 = 0) skip.basis/rows.notedo not already state " of " (or/,out of). A GROUP BY name that is both a select alias and an input column abstains, and so doGROUP BY ALL,SELECT DISTINCTand window-function queries, whose grouped side is not read from an explicit GROUP BY. Losses on the measure side alone do not fire.answer_commit review:rejection (status: "error", titleAnswer not committed) with the measured counts and a conditional instruction: look for a covering key/source, or state "M of N" in basis and recommit unchanged. The unchanged recommit is accepted. Metadata carriesevidence.joins,final_query_partandjoin_check_ms.answer_commitreserves the session's review slot for the current user message id synchronously, before any await. A concurrent second commit of the same turn sees the reservation and commits without review. The slot is released when nothing fired. A history without a user message id (the MCP adapter on main has none) is never reviewed.ALTIMATE_SQL_AUTO_CHECK=off(0,false).unresolvedcommits are not checked. Any error is swallowed and the commit proceeds.Known limitations (low, documented rather than fixed): the review evidence is kept only on the rejection's metadata (the accepted recommit does not copy it); the once-per-turn reservation assumes one committer per session, which holds on the native path.
Not included: the MCP adapter changes the check needs over MCP (recording
inputon tool parts, stable user-boundary ids). The adapter is not onmain; that change lives on the MCP branch.Cost side effects
This adds automatic queries on user data, only when all of these hold: the check is on, the commit is
fields/rows, the answer query is located unambiguously, and it ran on the session scratch DuckDB (local, ephemeral; never a remote warehouse, never a local DuckDB file, never SQLite). Per commit, at most once per user turn: ~6 catalog statements (json_serialize_sql,duckdb_views,duckdb_tables,duckdb_columns,duckdb_functions,pragma_storage_info), then for each of at most 2 pairs (key and filter expressions capped at 64 AST nodes, at most 8 filter conjuncts per side) onejson_deserialize_sqlper side and one count statement that is O(|L| + |R|) with a hash table bounded by the catalog gate (<= 500,000 rows per table, <= 16 M key chars per side). The join is never executed and no model is called. All statements run inside onewithScratchConnectionlease withreadOnly, are never aborted, and the check skips when the inherited query timeout is below 5 s. Measured locally: 500k x 500k wrapped VARCHAR keys < 1 s in the test (probe: 79 ms); the fan-out case is milliseconds. Added commit latency when it runs is those statements plus any operation already holding the scratch lease.How did you verify your code works?
Unit/integration, bun, real DuckDB through the session scratch connector and
withScratchConnection(test/altimate/tools/answer-commit-join-coverage.test.ts, 22 tests, pass atda9c781511):upper(trim())key with right-side filters, comma join,USING,scratch.-qualified names; the unchanged recommit is acceptedIS NOT DISTINCT FROM, subquery, CTE, cross-side filter,regexp_replace/lpad/LIKEkeys, 3 key conjuncts, wide integer literal,WHERE 1 = 0, user macro shadowingtrim, 1,024LIKEconjuncts, 9 conjuncts on one side (8 still measured), a 100-itemINlist, a 100-argumentCOALESCEkey,GROUP BY ALL,SELECT DISTINCT, window-only selectLIMIT 1: measured 20000/20000 (asserted < 5 s; measured in milliseconds); 500k x 500k: 250000/500000 (asserted < 5 s)test/altimate/tools/answer-commit.test.ts,test/agent/answer-commit.test.ts,test/cli/run/budget-finalization.test.ts: pass atda9c781511(one subprocess test hit its 5 s timeout once under load and passed on rerun).bun run typecheck: pass. Upstream marker check (--markers --base main --strict): pass. No tool schema changed.Not run: any live agent session or evaluation (the next step, and the reason this is a draft); the full CI suite (left to CI).
Reviews: local Codex (gpt-6.1-sol, read-only) of
c8923faf83found 3 P1s (constant-only WHERE dropped, side-filter columns unbounded, literal case folded in SQL comparison), fixed in743a356f96. A second Codex pass of743a356f96found 1 P1 (filter work unbounded by predicate count: 1,024LIKEconjuncts on 300k rows measured in 4.1 s against a 12.6 ms answer) and an independent review found a medium (GROUP BY ALL/DISTINCT/window queries treated every table as grouped); both fixed inda9c781511with regressions.da9c781511has not had another Codex pass. Open P2s: aCOALESCEconstant can widen a key past its column width (capped at 64 chars); the bullet can ask to acknowledge an already acknowledged side when the other side fired;reviewedTurnkeeps one entry per reviewed session.Screenshots / recordings
Not a UI change.
Checklist
🤖 Generated with Claude Code