feat(studio): grounded + AI Issue explainer - #215
Conversation
Fold Cloudflare's platform error codes (520-527, 1101, 1102, …) into the central error catalog as curated solutions. `findCloudflarePlatformSolution` recognizes Cloudflare's `Error <code>` phrasing (and a bare code when `cloudflare` is also mentioned, guarded against substring false-matches), and `findSolutionByMessage` falls back to it while Lunora message-solutions still win. Each solution carries a likely-cause, a fix, and the family docs link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the `__lunora_admin__:explainIssue` admin op to ShardDO. It re-derives the
grounded catalog fix server-side from the sample message (never trusting a
client-supplied hint), then asks the app's `AI` binding to explain the error
strictly from those facts. With no `AI` binding wired, an empty response, or a
model error it degrades to `{ degraded: true, reason }` carrying the grounded
hint — the AI layer is additive, never the only help. Registered in the DO-side
admin registry and dispatched ahead of the Issue-triage fall-through.
Also escapes a pre-existing raw NUL separator in the aggregate-cache key as
`\u0000` (byte-identical at runtime) to satisfy the no-nul-bytes hook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each Issue row now expands to two layers of help for the error. First, a grounded catalog fix derived entirely client-side from `@lunora/errors` (`findSolutionByMessage` over the sample message) — offline, instant, and always shown when the catalog recognizes the error. Second, an opt-in plain-language explanation behind a button that invokes the `explainIssue` action, grounded in that same fix and degrading to a note pointing back at the grounded fix when no `AI` binding is configured. Each row owns its own expand + explanation state, so opening or explaining one Issue never touches another. The row is extracted into an `IssueRow` component that carries both the existing triage controls (status, severity, assignee, resolve/ignore/reopen) and the new expandable detail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Deploy Preview for lunorash ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Thank you for confirming the Contributor License Agreement! 🙏 |
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdds Cloudflare platform-error hints, a reserved ChangesIssue explanation flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant IssuesPanel
participant ShardDO
participant ErrorsCatalog
participant WorkersAI
IssuesPanel->>ShardDO: Request issue explanation
ShardDO->>ErrorsCatalog: Match canonical solution
ErrorsCatalog-->>ShardDO: Grounded facts
ShardDO->>WorkersAI: Generate explanation
WorkersAI-->>ShardDO: Explanation or failure
ShardDO-->>IssuesPanel: Return explanation result
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
|
Thank you for following the naming conventions! 🙏 |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
packages/do/src/shard-do.ts (2)
1511-1517: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winBound the inference call with a timeout.
binding.runis awaited with no deadline on a single-threaded DO's admin dispatch.handleExplainIssuealready treats any throw asai-error, so racing a timer degrades gracefully instead of letting a slow model hold the dispatch open.♻️ Proposed guard
+/** Deadline for one explainer inference; a slow model degrades to the grounded hint. */ +const EXPLAIN_ISSUE_TIMEOUT_MS = 10_000; + ... - const result = await binding.run(model, { - max_tokens: 400, - messages: [ - { content: system, role: "system" }, - { content: facts.join("\n"), role: "user" }, - ], - }); + const result = await Promise.race([ + binding.run(model, { + max_tokens: 400, + messages: [ + { content: system, role: "system" }, + { content: facts.join("\n"), role: "user" }, + ], + }), + new Promise<never>((_, reject) => { + setTimeout(() => reject(new Error("explainIssue: inference timed out")), EXPLAIN_ISSUE_TIMEOUT_MS); + }), + ]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/do/src/shard-do.ts` around lines 1511 - 1517, Update the inference call in handleExplainIssue to enforce a timeout when awaiting binding.run, racing it against a timer that rejects once the deadline is reached. Preserve the existing error propagation so timeout failures are handled as ai-error, and ensure the timer is cleaned up when inference completes.
1438-1451: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
reasonis typedstringon both mirroredExplainIssueResultdeclarations. The shared root cause is one over-wide field: the DO derivesdegraded(...)'s parameter from it, and the studio branches on a literal — neither is checked, so a typo'd sentinel compiles and silently falls into the generic error copy.
packages/do/src/shard-do.ts#L1438-L1451: narrow toreason?: "ai-error" | "empty-response" | "no-ai-binding"sodegraded(reason)at Line 6801 rejects unknown values.packages/studio/src/lib/admin.ts#L1155-L1188: apply the identical union so thereason === "no-ai-binding"branch inissues-panel.tsxis exhaustively checkable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/do/src/shard-do.ts` around lines 1438 - 1451, In packages/do/src/shard-do.ts lines 1438-1451, narrow ExplainIssueResult.reason from string to the union "ai-error" | "empty-response" | "no-ai-binding" so degraded(reason) is type-checked. Apply the identical union to the mirrored ExplainIssueResult declaration in packages/studio/src/lib/admin.ts lines 1155-1188 so the issues-panel.tsx literal branch remains exhaustively checked.packages/studio/src/lib/admin.ts (1)
1155-1188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated wire contract — keep the two definitions in lockstep.
ExplainIssueArgs/ExplainIssueResultmirror@lunora/doby hand (intentionally, to avoid the dep edge), so nothing enforces agreement. Worth a note here pointing at the DO declaration, or a shared-types module if one already spans the two packages.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/studio/src/lib/admin.ts` around lines 1155 - 1188, The hand-maintained ExplainIssueArgs and ExplainIssueResult contracts in admin.ts can drift from their `@lunora/do` counterparts. Add a concise maintenance note referencing the corresponding Durable Object declarations, or reuse an existing shared-types module if one already spans both packages, while preserving the dependency boundary and keeping both definitions aligned.
🤖 Prompt for all review comments with AI agents
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/do/src/shard-do.ts`:
- Line 1403: Update DEFAULT_EXPLAIN_ISSUE_MODEL to a currently supported Workers
AI text-generation model, replacing the deprecated
`@cf/meta/llama-3.1-8b-instruct` value while preserving the existing
issue-explanation flow.
- Around line 1458-1474: Update parseExplainIssueArgs so title and culprit are
truncated to the same prompt-budget limit as sampleMessage before being
returned. Preserve their existing trimming and undefined behavior, and ensure
runExplainIssueModel receives capped values for all user-provided prompt fields.
In `@packages/errors/src/catalog.ts`:
- Around line 509-552: Update findCloudflarePlatformSolution to prioritize
explicit “error <code>” and “error: <code>” matches before applying the weaker
cloudflare-plus-number heuristic. Use a strong-match pass that returns the
corresponding CLOUDFLARE_PLATFORM_ERRORS entry first, then retain the existing
standalone-number matching for cases without an explicit error code.
In `@packages/studio/src/features/issues/issues-panel.tsx`:
- Around line 113-137: Track the sample message associated with each explanation
and invalidate both the explanation and its association whenever
issue.sampleMessage changes, alongside groundedHint. In onExplain, capture the
current sampleMessage and ignore late query results if it no longer matches the
issue’s current message; render the AI explanation only when explainedFor equals
issue.sampleMessage.
---
Nitpick comments:
In `@packages/do/src/shard-do.ts`:
- Around line 1511-1517: Update the inference call in handleExplainIssue to
enforce a timeout when awaiting binding.run, racing it against a timer that
rejects once the deadline is reached. Preserve the existing error propagation so
timeout failures are handled as ai-error, and ensure the timer is cleaned up
when inference completes.
- Around line 1438-1451: In packages/do/src/shard-do.ts lines 1438-1451, narrow
ExplainIssueResult.reason from string to the union "ai-error" | "empty-response"
| "no-ai-binding" so degraded(reason) is type-checked. Apply the identical union
to the mirrored ExplainIssueResult declaration in
packages/studio/src/lib/admin.ts lines 1155-1188 so the issues-panel.tsx literal
branch remains exhaustively checked.
In `@packages/studio/src/lib/admin.ts`:
- Around line 1155-1188: The hand-maintained ExplainIssueArgs and
ExplainIssueResult contracts in admin.ts can drift from their `@lunora/do`
counterparts. Add a concise maintenance note referencing the corresponding
Durable Object declarations, or reuse an existing shared-types module if one
already spans both packages, while preserving the dependency boundary and
keeping both definitions aligned.
🪄 Autofix (Beta)
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f76678e3-cd2d-4633-b980-bbb922415505
⛔ Files ignored due to path filters (3)
packages/do/__tests__/shard-do.admin.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/errors/__tests__/errors.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/issues/issues-panel.test.tsxis excluded by!**/__tests__/**and included bypackages/**
📒 Files selected for processing (7)
packages/do/src/introspect.tspackages/do/src/shard-do.tspackages/errors/src/catalog.tspackages/errors/src/index.tspackages/studio/src/features/issues/issues-panel.tsxpackages/studio/src/lib/admin.tspackages/studio/src/locales/en.ts
Merging this PR will improve performance by 15.74%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
Resolves every review comment on PR #215. @lunora/errors - findCloudflarePlatformSolution now matches in two passes, explicit `Error <code>` phrasing before the weaker cloudflare-plus-number heuristic, so a weak match on an earlier table entry can no longer beat an explicit match on a later one (`"Cloudflare Error 1102: exceeded after 524 ms"` resolved to 524, grounding the explainer in the wrong fix). - Add a static regex pre-filter so a message mentioning neither "error" nor "cloudflare" skips both the lowercasing and the per-entry scans. This restores the CodSpeed `unmatched message (worst case)` bench to its alpha baseline (normalized ratio .42 vs alpha .42, up from .28 at PR head). @lunora/do - Replace the deprecated `@cf/meta/llama-3.1-8b-instruct` default with `@cf/meta/llama-3.3-70b-instruct-fp8-fast` (retired May 2026; a dead model-id would degrade every explain to `ai-error`). - Cap `title`/`culprit` at 200 chars — they rode the same prompt as the message but bypassed the budget cap. - Bound the inference call with a 10s deadline so a hung model can't hold the DO's admin dispatch open. - Narrow `ExplainIssueResult["reason"]` to a closed union. @lunora/studio - Drop the redundant `useMemo` (React Compiler already caches it). - Track the sample message an explanation was grounded in, so a live re-fold under an unchanged hash can't leave stale AI text on screen. - Mirror the narrowed `reason` union and note the hand-mirrored contract. Also regenerates the API snapshots, which the branch had not updated after exporting `CLOUDFLARE_PLATFORM_ERRORS` / `CloudflarePlatformError`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bug/security pass
- `findCloudflarePlatformSolution` pass 1 matched a code as a prefix of a
longer number: `"Error 10061: connect ECONNREFUSED"` (WSAECONNREFUSED)
resolved to CF 1006, "your IP has been banned" — and that wrong hint
became the explainer prompt's grounding facts. Pass 1 now applies the
same digit-boundary check pass 2 already had.
- The platform table was folded into `findSolutionByMessage`, which sits on
`resolveHint` and therefore `toErrorBody`. Most catalog codes carry no
`hint`, so an ordinary `BAD_REQUEST` whose message merely mentioned
"cloudflare" near a number shipped operator-only zone guidance ("review
the zone's Firewall/WAF and IP Access Rules") to unauthenticated
browsers. It also put the table on the CLI renderer and Vite overlay.
Split out `findIssueSolution`, which the Studio and the explainer opt
into; the wire path stays Lunora-only. This also removes the CodSpeed
regression at its root (normalized ratio .423 vs alpha .421).
- Fence the untrusted error report in the model prompt and tell the system
prompt it is data, not instructions. `sampleMessage` is any text a
request can make a function throw, and it was interpolated with the same
`Label: value` shape as the grounded section.
- An unmatched message still called the model with no grounding, and the
result rendered identically to a catalog-backed one. The client now
flags an explanation with no `groundedId`.
- Audit every invocation that reached the binding, not only successes —
`ai-error`/`empty-response` are the billed calls worth recording.
- Cap the `model` override, the one caller-supplied field that was not.
Code-quality pass
- Extract `packages/do/src/issue-explainer.ts` (191 lines out of the
9.6k-line `shard-do.ts`), following the `sql-console.ts` precedent;
`handleExplainIssue` is now a thin adapter supplying `env.AI`.
- Extract `IssueDetail` from `IssueRow`, keyed on the sample message.
This replaces the `explainedFor` bookkeeping and also fixes what that
missed: a stale explain error and an in-flight busy flag survived a
re-fold. The component stays mounted while collapsed so collapsing does
not discard an inference already paid for.
- `ExplainIssueResult` is a discriminated union with named arms, so a
degraded result can no longer type-check without a `reason` (which the
client silently rendered as the generic AI-error copy).
- Drop the `hint` body from the wire — nothing read it; the client derives
it from the same catalog offline. `groundedId` stays and now drives the
ungrounded caveat.
- The DO test imports the real result type instead of a third hand-mirror
that had widened `reason` back to `string`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
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/do/src/issue-explainer.ts`:
- Line 171: Update the issue explanation flow around sampleMessage and
findIssueSolution so catalog lookup receives the full canonical issue message,
while EXPLAIN_ISSUE_MESSAGE_CAP is applied only to the prompt/model input
payload. Preserve the existing truncated field for model context without
truncating the value used for local solution grounding.
- Line 207: Escape or otherwise neutralize occurrences of UNTRUSTED_FENCE in
caller-controlled report content before constructing facts in issue explanation
generation. Update the report-building logic associated with title, culprit, and
sampleMessage so embedded delimiters cannot close the fence or create prompt
instructions, while preserving the existing report content and fixed fence
format.
🪄 Autofix (Beta)
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ea00918c-6ea1-4468-899b-b84996e8144f
⛔ Files ignored due to path filters (6)
api-snapshots/do.api.mdis excluded by none and included by noneapi-snapshots/errors.api.mdis excluded by none and included by noneapi-snapshots/lunora.api.mdis excluded by none and included by nonepackages/do/__tests__/shard-do.admin.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/errors/__tests__/errors.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/issues/issues-panel.test.tsxis excluded by!**/__tests__/**and included bypackages/**
📒 Files selected for processing (8)
packages/do/src/index.tspackages/do/src/issue-explainer.tspackages/do/src/shard-do.tspackages/errors/src/catalog.tspackages/errors/src/index.tspackages/studio/src/features/issues/issues-panel.tsxpackages/studio/src/lib/admin.tspackages/studio/src/locales/en.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/errors/src/index.ts
- packages/studio/src/locales/en.ts
- packages/studio/src/lib/admin.ts
- packages/studio/src/features/issues/issues-panel.tsx
| culprit: context(args["culprit"]), | ||
| model, | ||
| // Cap defensively so a runaway message can't inflate the model prompt. | ||
| sampleMessage: sampleMessage.slice(0, EXPLAIN_ISSUE_MESSAGE_CAP), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep catalog grounding independent of the prompt cap.
Studio resolves findIssueSolution(issue.sampleMessage) on the full message, but this path resolves after truncation. A Cloudflare code beyond 2,000 characters produces an ungrounded server response despite the UI showing a known fix. Preserve the canonical message for local solution lookup; apply the cap only when constructing model input.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/do/src/issue-explainer.ts` at line 171, Update the issue explanation
flow around sampleMessage and findIssueSolution so catalog lookup receives the
full canonical issue message, while EXPLAIN_ISSUE_MESSAGE_CAP is applied only to
the prompt/model input payload. Preserve the existing truncated field for model
context without truncating the value used for local solution grounding.
|
|
||
| report.push(`Error message: ${issue.sampleMessage}`); | ||
|
|
||
| const facts = [UNTRUSTED_FENCE, report.join("\n"), UNTRUSTED_FENCE]; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Escape caller-controlled fence delimiters.
The fixed delimiter can be embedded in title, culprit, or sampleMessage; the cap does not prevent that. This lets untrusted text forge the closing boundary and appear as prompt instructions outside the fenced report.
Proposed fix
- const facts = [UNTRUSTED_FENCE, report.join("\n"), UNTRUSTED_FENCE];
+ const fencedReport = report.join("\n").replaceAll(UNTRUSTED_FENCE, "[untrusted fence omitted]");
+ const facts = [UNTRUSTED_FENCE, fencedReport, UNTRUSTED_FENCE];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const facts = [UNTRUSTED_FENCE, report.join("\n"), UNTRUSTED_FENCE]; | |
| const fencedReport = report.join("\n").replaceAll(UNTRUSTED_FENCE, "[untrusted fence omitted]"); | |
| const facts = [UNTRUSTED_FENCE, fencedReport, UNTRUSTED_FENCE]; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/do/src/issue-explainer.ts` at line 207, Escape or otherwise
neutralize occurrences of UNTRUSTED_FENCE in caller-controlled report content
before constructing facts in issue explanation generation. Update the
report-building logic associated with title, culprit, and sampleMessage so
embedded delimiters cannot close the fence or create prompt instructions, while
preserving the existing report content and fixed fence format.
What
Each Studio Issue row now expands into two layers of help for the underlying error:
@lunora/errors(findSolutionByMessageover the Issue's sample message). Offline, instant, zero network, and always shown when the catalog recognizes the error. Now also covers Cloudflare platform error codes (520–527, 1101, 1102, …).explainIssueadmin action. The AI is grounded strictly in the same catalog fix, and degrades gracefully to a note pointing back at the grounded fix when noAIbinding is configured.Each row owns its own expand + explanation state, so opening or explaining one Issue never touches another.
Changes by package
@lunora/errors— Cloudflare platform error catalog (CLOUDFLARE_PLATFORM_ERRORS,findCloudflarePlatformSolution);findSolutionByMessagefalls back to it.@lunora/do—explainIssueadmin RPC onShardDO, grounded in the catalog hint and callingenv.AI.rundirectly (no@lunora/aidependency edge); degrades to the grounded hint when noAIbinding exists.@lunora/studio—IssueRowcomponent merging the existing triage controls with the new expandable grounded-fix + explain detail.Testing
@lunora/errors— 34 ✓ (incl. every catalog code resolves, CF-platform lookups)@lunora/do—shard-do.admin107 ✓ (explainIssue grounded + degraded paths)@lunora/studio—issues-panel12 ✓ (expand → grounded fix, explain → AI text, no-ai-binding degrade, no-known-fix note)build:packages,lint:types, andlint:eslintgreen on all three packages.🤖 Generated with Claude Code
Summary by CodeRabbit