feat(workspace): tell the model what the bound workspace serves - #1182
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughAdds shared capability projection, workspace-routing guidance, disabled-state handling, workspace-name sanitization, size limiting, and per-step insertion of non-empty awareness sections into session system prompts. Tests cover routing, permissions, fallbacks, truncation, sanitization, and prompt integration. ChangesWorkspace awareness
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: 🟡 Moderate · up to This PR changes bound sessions to call workspace execution tools directly. Merge should wait for confirmation that remote execution enforces equivalent write authorization and confirmation controls; concurrent test-state interference remains a bounded follow-up risk. Sequence Diagram(s)sequenceDiagram
participant SessionPrompt
participant Precedence
participant Awareness
SessionPrompt->>Precedence: read session precedence snapshot
SessionPrompt->>Awareness: call systemSection(precedence)
Awareness->>Precedence: read servedInventory(precedence)
Awareness-->>SessionPrompt: return bounded workspace guidance
SessionPrompt-->>SessionPrompt: insert non-empty section
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description follows the required template, identifies the issue, marks the change as a new feature, explains the implementation and safety considerations, documents verification results, notes that screenshots are not applicable, and completes the checklist.
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. |
2575d46 to
40c57a8
Compare
c5f0368 to
9e8e1fb
Compare
|
👋 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. |
8f47bcd to
eb798cb
Compare
|
Restacked this branch onto the current #1169 head ( |
3aca807 to
1ac24db
Compare
ralphstodomingo
left a comment
There was a problem hiding this comment.
Review — 1ac24dbd9 (restacked on #1169 eb798cb57)
Scope, corrected: the 48-file compare was stale from the base-branch rewrites. The net change is six files, +540/−46: awareness.ts (new), precedence.ts (servedInventory added; inventoryLine / warehouseListNote routed through it — behaviour-preserving, same iteration order and servedFor filter as before), session/prompt.ts (+13, the per-step render into the system array), awareness.test.ts (new), a shared precedence-fixture.ts, and precedence.test.ts. Nothing else in the stack's files is touched here. No scope creep.
What holds up: the section is a projection of the same Precedence snapshot check() reads, filtered through the same servedFor → reachable gate — so the analyst case renders nothing and a served row is exactly a row check() would redirect; "agrees with check() on the same snapshot" is the right property test for that. Capability-scoped rows match the engine's real asymmetry (BigQuery execute-only). The exhaustive Record tables did their job during the restack (they caught two new disabledReasons at compile time). The escape hatch speaking, and only it, is correct. Ordering inside a step is right: tool resolution (which runs refresh) precedes system assembly in the same loop iteration, so section and guard read one snapshot. Smoke on the compiled UAT bundle from this head: warehouse_list + the section produced the served/local split with the exact engine keys on the first turn, no probe call.
No blockers. Two minors and some nits inline. One recorded residual, inherited from the stack: per-step re-derivation under a concurrent re-link (#1168's R-P1) — the section flips to the new engine's keys while the turn's catalog stays pinned; fails closed (pinned wrapper → closed client), lands with the lease work. Worth recording on this PR's log rather than fixing here.
PR body: no Jira keys or internal paths (clean). Two asks: (a) state the safety case as numbered claims + residuals in the stack's style — it is already four claims in prose (byte-identical prompt when not routing; section ⊆ check() redirects; converse never dropped; cap) plus R1 = the residual above — so the codex rounds have something to review against; (b) refresh the stale lines after the refactors and restack: "118 insertions, 0 deletions" (now +540/−46), "purely additive" (inventoryLine / warehouseListNote were rewritten), and "rebased onto 40c57a890" (now eb798cb57, head 1ac24dbd9).
Ralph is running the UAT bundle built from this head; any behavioural findings will follow as a separate comment.
eb798cb to
3a873cf
Compare
|
Heads-up: this PR's base branch |
|
Restacked onto the current |
1ac24db to
12be6c8
Compare
9324653 to
d448ff3
Compare
|
Restacked again — #1169 was rebased onto |
12be6c8 to
4a1c7d5
Compare
returns a redirect naming the engine tool. It did not make the model *pick*
the engine first, so every session pays a wasted turn learning the rule.
The only model-visible steering today is a sentence `describeNativeTool`
appends to a description whose first line already matches user intent
("Execute SQL against a connected data warehouse."), and it never names the
engine key — so even an obedient model cannot comply without a probe call.
The routing table the model needs already exists as `inventoryLine`, and goes
only to a TUI toast. Nothing in the system prompt mentions the workspace.
`session/system.ts:129-142` records this repo's own benchmark finding: a
lazily-described capability fired in "<1% of tool calls", and guidance placed
at the END of a section was "treated as background reference rather than
binding directive" while the same content placed FIRST was applied. The
precedence suffix is exactly that shape.
So state it in the system prompt instead, per turn, naming the exact engine
keys, and say the converse explicitly so unserved types keep running locally.
Purely additive by construction — 118 insertions, 0 deletions:
- `awareness.ts` renders a string and nothing else. It does not touch
`check()`, `derive()`, `redirectFor()` or any tool body, so which calls are
shadowed and what a shadowed call returns are unchanged.
- It returns "" in every state except a bound, attributed workspace with
materialised engine tools. A session without a workspace assembles a
byte-identical system prompt to before this commit.
- `servedInventory()` is a projection over the snapshot the guard already
uses, filtered through the same `servedFor`/`reachable`, so the section can
never advertise a routing `check()` would not perform, nor one the caller's
agent is forbidden to follow.
- No tool descriptions change, so no existing description assertions move.
Deliberate details:
- Per capability, not per warehouse type. BigQuery serves execute only, so
its line says explain and inspect stay on the local tools — claiming the
type would steer the model off the only tools that work there.
- The converse paragraph is never dropped under the char cap; without it the
section reads as "prefer the workspace for everything", which is the
over-steering failure this most needs to avoid.
- The escape hatch speaks rather than falling silent: engine tools can still
materialise with `--integrations=local` on, so silence would leave the
model free to use tools it can see and should not.
- An agent denied the engine keys renders no section, matching what
precedence actually does for it.
Verification: `bun run typecheck` clean. 19 new tests (14 awareness, 5
precedence), all passing. Full `test/altimate/` sweep goes 4420 -> 4439 pass
with the same 3 pre-existing failures present on the untouched base commit
(cross-file pollution in `default-target.test.ts`, which passes 12/12 in
isolation on both).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cleanup pass over the tool-awareness change. No behaviour change: the full `test/altimate/` sweep is 4454 pass / 0 fail before and after. - `servedInventory()` returns a grouped `ServedType[]` instead of a flat list the caller immediately regrouped. `precedence.shadowed` is already grouped, so the flatten/regroup round trip was undoing work nothing asked for. This also drops two provably-dead branches (`shadowed.get(type)` after iterating `shadowed.keys()`; `byCapability.get(capability)` after `servedFor` already filtered on entry presence), and halves the per-type `PermissionNext.evaluate` work by deriving `local` from the same `servedFor` pass. - Deleted `localCapabilitiesFor()`. It was a third copy of `CAPABILITIES.filter((c) => !served.includes(c))`, already inline in `inventoryLine` and `warehouseListNote`; `local` now rides on the projection. - `ALL_LOCAL_TOOLS` is derived rather than hand-copied, and `CAPABILITY_COPY`'s `localTool` field is gone — every value equalled its key, because the `Capability` union IS the native tool id (`describeNativeTool` already relies on that identity). Output is byte-identical. - The disabled-state branch is a `Record` over `disabledReason`, not a ternary. The old test comment claimed adding a reason "fails to compile"; that was false — a `Union[]` annotation accepts a short list. A `Record` is genuinely exhaustiveness-checked: adding a sixth reason now raises TS2741 in both `awareness.ts` and its test, verified by doing it. - Corrected the module and call-site docs: `systemSection` runs once per STEP (inside the `while (true)` prompt loop), not once per turn. Noted why it is deliberately not memoised — a cached section outliving its snapshot would advertise routing that no longer holds. - Extracted `test/altimate/workspace/precedence-fixture.ts`. `bindTo`'s `attachOutcome` shape is coupled to the attach module's SERVING allowlist, so two hand-maintained copies break differently when it changes. Both suites now share the tool maps, warehouse configs and analyst ruleset; the awareness suite picks up the engine-less duckdb/redshift connections it had been omitting. - Dropped comments that restated an adjacent doc, and replaced rationale copied verbatim into tests with pointers to the source of truth. Not done, deliberately: `prompt.ts` and `precedence.test.ts` fail `prettier`, but they already fail on the untouched base, so reformatting them would add unrelated churn. The duplicated `short()` helper and the two `servedFor` sweeps in `describeNativeTool` are pre-existing and outside this diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… projection Closes the duplication the previous commit opened. `servedInventory` carried a third copy of `CAPABILITIES.filter((c) => !served.includes(c))`, alongside the two already inline in `inventoryLine` and `warehouseListNote`; both now consume the projection instead, leaving one copy. The twice-declared `short()` helper is hoisted to module scope beside `CAPABILITIES`. The payoff is not line count: the toast, the `warehouse_list` row note and the model-facing prompt section now derive "what is served" from one function, so they cannot disagree about which capabilities a workspace serves — previously three independent walks of the shadow table. Kept deliberately separate: `short()` (terse — `execute/explain/inspect` for a one-line toast) and the section's `CAPABILITY_LABEL` (prose — "table stats / schema inspection"). Two audiences, and the section's whole thesis is that vague phrasing is what failed to steer the model. Behaviour is unchanged and the tests prove it byte-for-byte: the suite asserts exact output including "snowflake: execute/explain/inspect via workspace analytics", "bigquery: execute via workspace analytics" and "explain/inspect stay local". Verification: typecheck clean. Full `test/altimate/` sweep 4434 -> 4453 pass with the same single pre-existing failure on both (`tracing-rename-race` M3-natural, which also fails in isolation on the untouched base 40c57a8). Not done: `test/altimate/precedence-guard-order.test.ts` keeps its own SNOWFLAKE_TOOLS. It is a genuinely different fixture — three keys rather than four, and a different binding — so folding it into the shared module would change what that suite covers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… disabled reasons render nothing `binding-unreadable` and `derive-failed` arrived below this PR. Both mean the routing decision is unknown and the tool result already states why, so, like `unattributed`, the system section says nothing for them.
4a1c7d5 to
531fffc
Compare
|
#1169 merged to |
Review —
|
| Finding | Why | |
|---|---|---|
| 🔴 Blocker (conditional) | M1 headline false for execute-only integrations | Only fires for BigQuery / PostgreSQL / Databricks. A Snowflake-only pilot is unaffected — Snowflake serves all three capabilities, so its line has no parenthetical and the headline is true for it. |
| 🟡 Cheap, take it with M1 | M3 unescaped workspace name | One line. In-tenant only, so the threat model is thin for a trusted pilot audience; the realistic failure is accidental — a name with a newline or # garbling the prompt. |
| 🟡 First after the pilot | M4 uncertain states leave the model unguided | Worst potential consequence (a datamate_* call against a different workspace), but not a regression — behaviour in those states is unchanged from today. Likely to be common early while engine attribution settles. |
| ⚪ Future | M2, M5, M6, all minors and nits | M2 needs a concurrent session replacing the engine mid-turn; M5 is unreachable by construction; M6 guards M5. |
Major
M1 — The headline instruction is false for every partial-coverage type · awareness.ts:124-126 · 🔴
The intro reads "For the connection types listed below the local tools will NOT execute — they return a redirect." But BigQuery, PostgreSQL and Databricks ship execute-only, so their sql_explain / schema_inspect do run locally — as the same line's own parenthetical says. Rendered against the real fixture shape:
- bigquery — execute: `datamate_bigquery_execute_database_query` (explain plan and table stats /
schema inspection for bigquery stay on the local `sql_explain` / `schema_inspect`)
The section contradicts itself, and in the least helpful arrangement: the false claim is the headline, the correction is a trailing parenthetical. The PR's own cited evidence is that content placed FIRST is applied while trailing content is "treated as background reference rather than binding directive" — so the part the model is most likely to obey is the false part. The effect is over-steering off working local tools, which is the precise failure the converse paragraph exists to prevent.
Worth calling out as a pilot blocker for a second reason: if the point of the pilot is to measure whether prompt steering removes the wasted discovery turn, shipping steering that contradicts itself on three of the four integrations makes a negative result uninterpretable.
Fix: scope the introduction to capabilities rather than to connection types.
M2 — Precedence refreshes per step from the fresh catalog; exposed engine tools are pinned to the turn's first · prompt.ts:1288 vs 2063-2064
resolveTools() calls MCP.tools() and Precedence.refresh() on the fresh map (B). Only afterwards does pinTurnTools() delete B's engine tools from the exposed map and restore the turn's first catalog (A). On step 2+, if another session replaced or reconnected the engine, the model is handed tools A while the system prompt, the tool descriptions and check() all speak B — and check() can redirect a local call to a B key that is not in the catalog.
To be fair to the change: turn pinning predates it, and so does the underlying inconsistency. The narrow charge against this PR is that it promotes that inconsistency into the system prompt while asserting "One snapshot, one truth: the section cannot advertise a routing the guard would not perform." Measured against the exposed catalog, it can.
Relatedly, awareness.ts:76-79 says the snapshot is "re-derived per turn". resolveTools runs every step, so it is re-derived per step, and the no-memo rationale ("a cached section outliving its snapshot") does not describe the actual call sequence — refresh precedes the render inside the same step.
Fix: pin the raw MCP tool map before deriving precedence, so both follow one map.
M3 — Workspace name interpolated unescaped into the system prompt · awareness.ts:124 · 🟡
workspaceName is binding.datamateName (precedence.ts:498), validated only as a string. A name containing quotes, newlines or Markdown headings breaks out of the sentence and becomes first-class system-role instruction. Same-tenant only, which bounds it, but the system prompt is the highest-trust surface in the product.
Fix: prefer the numeric workspace id, or emit the name as inert data (JSON.stringify) with control characters stripped and length bounded. Escaping quotes alone is not enough.
M4 — Uncertain routing states leave the model unguided while engine tools stay visible · awareness.ts:85-93 · 🟡
binding-unreadable, unattributed and derive-failed all render "". But check() fails open in those states (precedence.ts:767-780) and the engine's tools may still be in the catalog, so the model sees datamate_* tools with no instruction about them. unattributed is the sharpest case: those tools may belong to a different workspace, which is exactly why routing refused them.
The module already accepts this argument — the escape-hatch copy exists because "silence here would leave the model free to reach for tools it can see and should not use." The same reasoning applies to the uncertain states and is not applied there.
Fix: render a fail-open section for those states pointing at the local tools, without naming an unverified workspace.
M5 — Truncation emits a self-contradicting directive · awareness.ts:120-150 · ⚪ latent
When the cap drops type lines the converse paragraph is kept verbatim, so the section asserts both "…and N further connection types served by this workspace" and "Do not use datamate_* … for connection types not listed above". Measured by rendering systemSection against a synthetic snapshot:
| served types | length | truncated |
|---|---|---|
| 4 | 1095 | no |
| 9 | 1960 | no |
| 10 | 1847 | yes |
Unreachable in this build — INTEGRATION_TYPE has four entries — but it activates on the ninth, which is exactly the growth the cap was written to survive.
Fix: when omitted > 0, drop the "Do not use" sentence and say the list is partial.
M6 — The size-ceiling test proves neither thing its name claims · awareness.test.ts:138-158 · ⚪
stays under the cap and degrades by dropping whole types builds four integrations → 1095 chars → the truncation loop never runs. expect(out.length).toBeLessThanOrEqual(MAX_SECTION_CHARS) passes trivially, and expect(out).toContain("Every other connection type…") passes on the untruncated render. Neither assertion can fail unless the section breaks entirely — and the test will keep passing when the path activates and starts emitting M5.
Fix: drive systemSection with a synthetic snapshot carrying enough types to truncate, then assert the omission line appears and that the converse was adjusted.
Minor
MAX_SECTION_CHARSis not the hard ceiling the comment claims —awareness.ts:145-149. The loop stops once one line remains, so a single oversized line (or a long workspace name) still exceeds the cap. Compounds M3.- No awareness test for postgres — partial capability coverage is exercised only through bigquery, even though postgres is the other execute-only integration.
- The analyst test asserts
section() === ""without pinning why —awareness.test.ts:128-134. Addingexpect(servedInventory(p!)).toEqual([])would distinguish reachability filtering from a merely disabled snapshot. bindTo'sattachOutcomemock is untethered from the realSERVINGallowlist —precedence-fixture.ts:46-49. The fixture comment acknowledges the coupling, but nothing assertsattributableEnginestill accepts that shape, so a change toSERVINGwould leave the mock producing a false-positive attribution with every test still green.
Nit
- The
systemSectiondoc block floats —awareness.ts:67-80. It sits aboveDISABLED_COPYand is separated from the function it documents, so editor hover surfaces nothing forsystemSection. Looks like a leftover from a refactor. postgresqlrenders as canonicalpostgreswhile the tool key sayspostgresql. Consistent withcanonicalTypeand pre-existing, so no code change — but a one-line comment noting these are canonical driver types, not user-facing connection names, would save a reader the round trip.
Checked and found fine
Recording these so they don't get re-raised later:
- Ordering is deterministic.
shadowedis built fromObject.entries(INTEGRATION_TYPE), a module const, so the rendered section is byte-stable across turns for a given inventory — no prompt-cache churn from ordering. - The shared fixture is not circular. It supplies inputs, not expected output, and deliberately includes engine-less types (duckdb, redshift) as the over-steering control. A bug in the key-construction formula would fail
present.has(engineTool)and break the tests rather than passing in lockstep. - Not memoising is the right call, even though the stated reason doesn't match the call sequence (see M2): the render is a few joins over at most four lines.
What's done well
servedInventoryas a single projection genuinely removed two duplicate derivations; the toast, thewarehouse_listrow and the model-facing section can no longer disagree with one another.DISABLED_COPYkeyed onNonNullable<Precedence["disabledReason"]>andCAPABILITY_LABELkeyed onCapabilitymake a new variant a compile error rather than a silent omission, andALL_LOCAL_TOOLSis derived from the label map rather than hand-written.- Reachability filtering means the section never advertises a tool the active agent is denied — the invariant that matters most here, and it is tested.
- Returning
""in every non-routing state keeps the system prompt byte-identical for unbound sessions, which is what makes the change safe to land behind its own emptiness. - Driving the tests through the real
refreshrather than hand-built snapshots is the right call and is why the fixture holds up.
Missing tests
Genuine truncation and single-value overflow (M5/M6); a composed step-2 case where MCP.tools() changes, pinTurnTools() restores the first catalog, and the section / descriptions / check() / exposed tools must all agree (M2); adversarial workspace names — newlines, headings, backticks (M3); the uncertain states from the model's point of view with datamate_* still visible (M4); the escape hatch with engine tools actually present in the catalog; a postgres partial-coverage case.
… inert on names, and honest when truncated From sahrizvi's review of the head, taken on the author's behalf: - The headline claimed every local tool returns a redirect for a listed type, which is false for the execute-only integrations (BigQuery, PostgreSQL, Databricks) whose explain and inspect stay local — the parenthetical said so while the headline said otherwise. The intro is scoped to capabilities now. - The workspace name is customer-authored and lands in the system prompt: it is emitted as inert data (control characters stripped, whitespace collapsed, length bounded, JSON-quoted) with the numeric id named alongside. - The uncertain states (binding unreadable, engine unattributed, derivation failed) steer to the local tools the way the escape hatch does, without naming the unverified workspace. `check()` fails open there and the engine's tools stay visible, so silence left the model unguided. Silence is kept for the states with nothing to misuse, so an unbound session's prompt is still byte-identical. - When types are dropped for length the converse no longer forbids `datamate_*` for "types not listed" — the omitted types are served — and says the list is partial. The cap is a real ceiling: a single oversized line is dropped too. - Doc block moved onto `systemSection`; the refresh-per-step sequence and the pinned-catalog caveat are stated; the canonical driver type is noted. Tests: a real truncation case through the snapshot's own shadow table (ten types), a single oversized line, the execute-only headline wording, postgres partial coverage, an adversarial workspace name, the uncertain states from the model's side, the analyst case pinned to an empty inventory, and the fixture's attach outcome pinned to `attributableEngine`.
Re-review disposition —
|
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
|
Marked ready for review at |
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Previous Review Summaries (4 snapshots, latest commit b4b95fa)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit b4b95fa)Status: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Previous review (commit 4f6ae42)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous review (commit 4fb41bf)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (3 files)
Fix these issues in Kilo Cloud Previous review (commit a21f61e)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (6 files)
Reviewed by deepseek-v4-pro · Input: 37.8K · Output: 7.5K · Cached: 418.3K Review guidance: REVIEW.md from base branch |
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…pshot The system section made the customer-authored name inert, but the same name reached the model raw through the redirect notices, the tool descriptions and the `warehouse_list` note. Sanitise it once in `derive` — control characters stripped, one line, bounded — so every downstream interpolation is inert; the section keeps its JSON quoting on top. Test walks every model-visible surface with a hostile name.
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
Inline comments:
In `@packages/opencode/src/altimate/workspace/precedence.ts`:
- Around line 160-161: Extend the control-character replacement in
inertWorkspaceName to also remove Unicode C1 controls U+0080–U+009F, while
preserving the existing whitespace normalization. Add a regression test covering
\u0085 to verify it is sanitized.
In `@packages/opencode/test/altimate/workspace/awareness.test.ts`:
- Around line 242-243: Update the workspace-awareness fixture around bindTo and
refresh to prevent concurrent Bun tests from interfering: use a unique session
ID per test and serialize fixture setup, refresh, and assertions, or mark the
suite serial. Ensure process-wide seams, environment, registry state, and shared
session entries are isolated for each test.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Team
Run ID: 932cdbbb-5703-4930-8623-5ac51c6981b0
📒 Files selected for processing (3)
packages/opencode/src/altimate/workspace/awareness.tspackages/opencode/src/altimate/workspace/precedence.tspackages/opencode/test/altimate/workspace/awareness.test.ts
Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.
…ace name; bound it in code points
E2E — headless
|
| # | Prompt / mode | Before (08-28) | After (this PR) |
|---|---|---|---|
| 1 | "Using the warehouse tools, run select 42 as answer on the Snowflake connection named ac8759_snowflake…" |
sql_execute → redirect ("Routed to workspace Test Workspace") → datamate_snowflake_list_database_connections → warehouse_list → datamate_snowflake_execute_database_query → 42 |
datamate_snowflake_list_database_connections → warehouse_list → datamate_snowflake_execute_database_query → 42 — no redirect turn |
| 2 | "Call warehouse_list and show its raw output verbatim" |
— | ac8759_snowflake · snowflake · ANALYTICS · execute/explain/inspect via workspace Test Workspace; the DuckDB rows say local |
| 3 | Row 1 with --integrations=local (escape hatch) |
— | sql_execute runs locally, no datamate_* call in the whole run (it then hit the pre-existing local "(0 rows)" / driver-not-installed behaviour — unrelated to this PR, tracked separately) |
| 4 | Row 1 with the binding cache unreadable (chmod 000 in an isolated state dir) |
— | sql_execute runs locally with the notice "Not routed through the bound workspace: the workspace link could not be read this turn"; no datamate_* call |
Row 1 is the PR's claim, measured: the model goes to the workspace tool first instead of discovering the routing by being refused. Row 3 confirms the hatch's section steers back to the local tools. Row 4 is the binding-unreadable shape of M4 from the model's side.
One rig note for anyone reproducing: the binding cache is revalidated against the server and a positive row the server does not know is dropped (by design, from the install-offer PR). A binding written into the local cache by hand — which is how this rig was set up on 08-31 — therefore vanishes on the first turn of a current build; link through altimate link / the palette so the server has the row.
|
Addendum for |
…the copy it renders Four findings from the end-to-end review of #1182. The module's header claimed it returns "" in every state but a routing one. Four of the seven `disabledReason` values render text, so the sentence the whole "safe to land" argument rested on was false. It now states the scoped claim `DISABLED_COPY` actually implements, and says plainly which states speak and why. That claim was also not true of the code. `ALTIMATE_INTEGRATIONS` is process-wide and `derive` read it before the workspace link, so a pilot user who exports `--integrations=local` got a "## Workspace integrations" block in the system prompt — and a toast on screen — in every project, including ones with no link and no `datamate_*` tools at all. The hatch is now read after the link. Both orders disable routing identically, so this changes only which reason is reported; it still outranks `binding-unreadable`, because the flag is a fact about the session whatever the link says. `binding-unreadable` said "the bound workspace's engine could not be verified", but `currentBinding()` maps any throw from the strict reader to `unreadable`, which a project with no link can reach. The shared copy now claims only what holds in all three states that use it. Under truncation the section counted the omitted types twice, once on the list tail and again in the converse. The count stays on the list; the converse carries only the instruction. Tests: the existing hatch test bound a workspace in `beforeEach`, so unbound + hatch was never exercised — which is how the leak survived review. Added that case, its `unreadable` counterpart, and a guard that drives every disabling condition on an unbound project and requires silence from each. - 24 new tests total; `awareness` + `precedence` suites 137 pass / 0 fail - Full `test/altimate/` sweep 4573 pass / 638 skip / 0 fail - `bun run typecheck` clean; `bun run lint` adds no new errors
|
👋 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. |
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
The unbound guard's empty-catalog line asserted silence without asserting the reason, so it read as `nothing-materialised` coverage while `derive` had already returned `unbound` at the link read — `engineToolKeys` is never reached. Four assertions of the same branch, one of them mislabelled by appearance. The reason is now asserted, which is the actual point: the link read short-circuits ahead of the catalog. `nothing-materialised` silence is covered where it belongs — "a declared-but-absent integration renders nothing" and the exhaustive reason-to-copy table. Raised by cubic on b4b95fa; verified against `derive`'s ordering before taking it.
|
👋 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. |
1 similar 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. |
sahrizvi
left a comment
There was a problem hiding this comment.
Approving 9c74f4d0f.
Everything from the previous round is closed or deferred with a reason I accept. I verified each fix by reverting it on disk and re-running the suites rather than reading the commit messages — every one is pinned by a test that fails without it:
- reverting the capability-scoped intro fails
never claims a capability the integration does not serve - reverting the truncation loop to
> 1failsa single oversized line cannot breach the cap either - reverting the
deriveordering fails three, includingno reason that speaks survives a link read that settled as unbound
I also rendered the section directly: a workspace name carrying a quote, LF, NEL (U+0085) and LS (U+2028) comes out inert with no newline surviving, and a thirty-type snapshot renders 1995 characters against the 2000 cap, so the ceiling is real now. 137 pass / 0 fail on this head.
The escape-hatch leak is the best catch of the round and it was yours, not the review's — ALTIMATE_INTEGRATIONS being process-wide meant --integrations=local put a workspace section and a toast into every project, including unbound ones. Diagnosing why it survived (the hatch test bound a workspace in beforeEach, so unbound + hatch was never exercised) is the part that will keep paying off. 9c74f4d0f is the same instinct applied again: an assertion that read as nothing-materialised coverage while derive had already short-circuited at the link read.
Deferred, agreed: the per-step precedence refresh against the turn-pinned catalog. The in-code caveat is accurate — I checked it against the resolution flow — and pinning the raw tool map belongs with the lease work rather than here.
One claim to ignore rather than churn on: the suggestion that inertWorkspaceName's comment is wrong about \s not matching NEL. I checked — /\s/u does not match U+0085. The comment is correct as written. (\s does match LS/PS, NBSP and FEFF, so naming U+2028/U+2029 explicitly is belt-and-braces, not redundant.)
Non-blocking, for whenever:
workspaceLabelsanitises and JSON-quotes the name but interpolatesworkspaceIdraw (awareness.ts:161). No live risk — it is alwaysString(binding.datamateId)and validated as an integer — but the type isstring | undefined, so a future path setting a non-numeric id would bypass both guards.check()'s notice still says "Not routed through the bound workspace" forbinding-unreadable(precedence.ts:812-818), asserting a link that may not exist. That is the same copy-accuracy issueUNVERIFIED_SECTIONwas just corrected for; the two surfaces now disagree on the same fact.- The "inert on every model-visible surface" test does not cover
check()verdicts, which also interpolate the name. Transitively safe viaderive, but the describe block's scope overreaches its assertions. unattributedis driven throughderiveand asserted againstsystemSection();binding-unreadableis covered only in halves. An asymmetry rather than a hole.- Worth two boundary cases: a name of exactly 80 code points, and an all-whitespace name (sanitises to empty, renders
workspace "" (id 42)— harmless, the id carries identity).
Unrelated to the code: the github-actions comment claiming this PR was "automatically closed by our quality checks" looks spurious — there is no closed event in the timeline, only ready_for_review. Might be worth chasing so it does not bite a later PR.
|
Merging to test e2e on main. |
Issue for this PR
Closes the remaining integrations gap called out after #1167/#1168/#1169 landed: the harness being unaware of when to reach for workspace tools.
Type of change
What does this PR do?
Stacked on #1169 — review that first; this PR is the commit on top.
#1168 made the engine win: a shadowed warehouse call executes nothing and returns a redirect naming the engine tool. It did not make the model pick the engine first, so every session pays a wasted turn learning the rule, and re-learns it next session.
The only model-visible steering today is a sentence
describeNativeToolappends to a description whose first line already matches user intent ("Execute SQL against a connected data warehouse.",altimate/tools/sql-execute.ts:21), and it never names the engine key — so even an obedient model cannot comply without a probe call. The routing table the model actually needs already exists asinventoryLine(precedence.ts:707-737) and goes only to a TUI toast. The system prompt says nothing about the workspace.This states it in the system prompt instead, per turn, naming the exact engine keys, with the converse said explicitly so unserved types keep running locally.
Rendered example (illustrative — every type, capability and key is derived from what materialised this turn):
Why the system prompt and not a better tool description:
session/system.ts:129-142records, from this repo's own benchmark trace analysis, that a lazily-described capability fired in "<1% of tool calls", and that guidance placed at the END of a section was "treated as background reference rather than binding directive" while the same content placed FIRST was applied. #1168's suffix is exactly that shape. This is the documented cure, not a guess.Why this is safe to land
Additive in effect: no call changes where it executes or what it returns.
awareness.tsrenders a string and nothing else. It does not touchcheck(),redirectFor()or any tool body, so which calls get shadowed and what a shadowed call returns are unchanged. If the section is wrong, the model reads a misleading sentence — it does not run a query against the wrong credentials.derive()change is an ordering change, not an enforcement change. The escape hatch is now read after the workspace link instead of before it. Both orders disable routing identically — every path returns the same disabled snapshot andcheck()shadows nothing either way — so the order decides only whichdisabledReasonis reported. That matters because the reason is what every user-facing surface keys on, andescape-hatchis a claim about workspace routing:ALTIMATE_INTEGRATIONSis process-wide, so read before the link it fired on projects with no workspace at all, putting a toast on screen and a section in the prompt of a session that has none. It still outranksbinding-unreadable, because the flag is a fact about the session whatever the link says.pilot-off,unbound,nothing-materialisedand no-snapshot all render"", and after the ordering fix above no reason that does speak survives a link read that settled asunbound. (binding-unreadableis the read failing rather than saying no — the project may or may not be linked, which is exactly why its copy asserts neither.) Both halves are asserted directly, not argued (the regression guarddescribe block: the exhaustive reason→copy table, plus a test that drives every disabling condition on an unbound project and requires silence).binding-unreadable,unattributed,derive-failed) each render a short paragraph steering to the local tools, because in all four the engine's tools can still be in the catalog while routing refuses them — silence would leave the model free to call what it can see. (An earlier revision of this description claimed blanket silence and listedunattributedamong the silent states; that was wrong in both directions and is corrected here.)servedInventory()is a projection over the snapshot the guard already uses, filtered through the sameservedFor/reachable— so the section can never advertise a routingcheck()would not perform, nor one the caller's agent is forbidden to follow. There is a test that walks the inventory and assertscheck()redirects to each key it names.Deliberate details
--integrations=localon —deriverefuses before it looks at them, but MCP connects the configured entry regardless — so silence would leave the model free to use tools it can see and should not.UNIFIED_INJECTION_BUDGET— a routing directive must not compete with memory for space.How did you verify your code works?
bun run typecheckclean.awareness.test.ts, 7 inprecedence.test.ts. The two suites run 137 pass / 0 fail together.test/altimate/sweep: 4573 pass, 638 skip, 0 fail, repeated 7x to rule out order dependence. One run showed a single unrelated failure inskill-sync.test.ts(existsSync(skillFile(...))at :1227 — the skill file write, while the binding-cache assertion on the line above passed) during a sweep run concurrently with a typecheck; it did not recur in the other six, and the affected trio (precedence+awareness+skill-sync) ran 6x clean on both this branch and the base.bun run lint: no new errors. The one repo-wideoxlinterror is pre-existing inpackages/http-recorder; the warnings on these files are the sameno-unsafe-type-assertionfixture casts they carried before.089bb6223) the sweep went 4420 → 4439 pass with the same 3 failures present on the untouched base commit — pre-existing cross-file pollution indefault-target.test.ts, which passed 12/12 in isolation on both. Verified by running the sweep on a clean worktree of that base rather than inferred; those failures no longer occur after the restack.Not yet done: the before/after model-behaviour measurement (does the model call the engine tool first, and are redirects consumed → 0). Left out of this PR deliberately so the additive change can land on its own; the metrics come from trace JSON that already ships (
tracing.ts:907-930,metadata.redirected).Rebased onto the restacked #1169
Originally opened against
089bb6223. #1169 was force-pushed to40c57a890(the restack onto the newer #1167/#1168), so this has been rebased onto that. It applied with no conflicts — the changes are additive and touch none of the rewritten regions.Worth noting: the restacked base now carries
fix: undetermined outcomes always carry a stated reason in the result, theprecedence.tshardening (fail-opencheck/checkUnsafesplit, refusing attribution whenConfig.invalidate()fails) that the pre-restack #1169 was missing. Re-verified on the new base: typecheck clean, my two suites 111/111, and the fulltest/altimate/sweep is now 4454 pass / 0 fail — the 3 pre-existingdefault-target.test.tsfailures seen on the old base are gone.Review fixes applied
Four findings from the end-to-end review, all fixed in this branch:
awareness.tsclaimed the section returns""in every state but a routing one; four of the sevendisabledReasonvalues render text. Both the header and the "Why this is safe to land" section above now state the scoped claim, which is the oneDISABLED_COPYactually implements.ALTIMATE_INTEGRATIONSis process-wide andderiveread it before the workspace link, so a pilot user who exports--integrations=localgot a 256-char "## Workspace integrations" block — plus a TUI toast — in every project, including ones with no link and nodatamate_*tools. The hatch is now read after the link. Covered by a test on each side of the boundary (unbound→ silent,unreadable→ still the hatch).binding-unreadableasserted a link that may not exist.currentBinding()maps any throw from the strict reader tounreadable, which a project with no link can reach, so "the bound workspace's engine could not be verified" overstated. The shared copy now reads "Workspace routing could not be established for this session" — true in all three states that use it.Also added: a regression test that drives every disabling condition on an unbound project and requires silence from each. Finding 2 slipped through because the existing hatch test called
bindTo()inbeforeEach, so the unbound + hatch combination was never exercised.Follow-ups deliberately not in this PR
INTEGRATION_TYPE→ key-pattern discovery. Today that map is hardcoded to four ids (precedence.ts:90-97) andderive()iterates it, so a datamate that adds Redshift or Trino is silently ignored even when the engine advertisesredshift_execute_database_query— no shadowing, no warning. That fix rewrites feat(workspace): route warehouse tools through the bound workspace's engine #1168's enforcement core, so it belongs in its own PR with its own tests rather than sharing a diff (and a bisect) with a steering change. Note this section renders whateverPrecedencederives, so it becomes dynamic for new engines with no change to this PR's code.ask_knowledge_baseappears nowhere inpackages/opencode/src/, so the name is an assumption.Screenshots / recordings
n/a — system-prompt change, no UI.
Checklist
Summary by CodeRabbit
New Features
Bug Fixes
Tests