Skip to content

fix(console): show the API's typed refusal reasons through one shared describer - #3690

Merged
freeqaz merged 12 commits into
mainfrom
fix/console-failure-describer
Oct 26, 2026
Merged

freeqaz merged 12 commits into
mainfrom
fix/console-failure-describer

Conversation

@freeqaz

@freeqaz freeqaz commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

About twenty console views showed a fixed sentence or the bare message() fallback for every 400, 409 and 429, and dropped the API's own sentence, which names the rule or the state that refused the request:

  • Stop and Delete (Agent page and Agents list rows) said "Check the entered values and resource IDs, then try again." for a 400, although neither has a form, and "This Agent could not be deleted in its current state" for a 409 the API explains ("The Agent lifecycle changed during deletion.").
  • Secret delete said "Deletion is blocked by current references or a lifecycle conflict" when the API names the referencing Agent or Configuration.
  • Create Secret (inline and on the Secrets page) and Create Namespace hedged "may already exist" / "already in use or reserved by a deleted Namespace" when the API says which.
  • Plugin save, repository choices, Preset load, Agent sharing and Create Agent dropped typed 409 text.
  • A refused Create Agent told an Installation administrator to "ask an Installation administrator to check your access".
  • A 429 said "Check the input" (Secrets page) and never used Retry-After.

Change

  • describeFailure(cause, { mutation, form, next, conflict }) in console/agents/list.mts, beside message():
    • 400: the API's sentence as written. Where the reader entered nothing (form: false), a schema refusal ("The request does not match the operation contract…") becomes "The API refused this request as malformed. Refresh the page and try again."; it never mentions entered values.
    • 409: NAMESPACE_NOT_READY keeps the console sentence; a typed conflict shows the API's sentence followed by the view's recovery step (next); a conflict the API does not explain (no message, or the generic "already exists" text, decided by one explainedConflict() helper the Secrets page, Secret picker and Namespaces share) keeps the view's own sentence (conflict).
    • 429: "Too many requests. Try again in N seconds." from Retry-After, in minutes past two minutes (retryAfterText(), which the Logs tab now shares), else the old text.
    • 403, 404, 5xx and network failures: message(), so fix(console): name the grant a refused read needs and who grants it #3688's AccessNeed text is kept.
    • Everything else (404, outages, interrupted requests) keeps message().
    • The API writes these sentences for the caller and never puts a secret value in one; views set them as textContent.
  • Used by: Stop and Delete (Agent page, status reads, Agents list row actions), the Secrets page (create/replace, delete), Create Secret in Secret fields, Create Namespace, plugin save, repository choices, Preset load, Agent sharing, Create Agent's 409.
  • Create Agent and provisioning: a 403 for a reader whose Installation read succeeded (only an Installation administrator's does) names nobody to ask, as the saved-Agent grant refusal already does (finding 2113).
  • docs/reference/console.md no longer says backend error text is never rendered.
  • Storybook: the Agent deletion conflict, Secret deletion conflict and repository recovery stories send real API sentences.
  • The Secrets page shows the API's sentence for a taken Secret name; Stop, deletion, the Agents list and Secret deletion keep their own sentence for a conflict the API does not explain.
  • No API change. Disabled-control reasons and other 403 copy follow in a separate PR.

Verification

  • Nine updated browser cases fail on main (7a75f96463, and b019f135bc for the two Create Agent cases) and pass on this branch: repeat deletion 400 and 409, the Secret delete recheck (real API reference text), the Secrets page 429 with Retry-After, the inline Create Secret real duplicate-name 409, the Create Secret refusal focus case, the Namespace name conflict (real API), Create Agent's typed 409, and the administrator's provisioning 403.
  • Fixtures that invented server sentences now send the API's real ones (tests/helpers/refusal-text.mjs).
  • Review fixes (findings 2559-2563) add or change cases that fail on main 119e5b91ce: Agent page Stop 400 and typed 409, Agents list row Stop/Delete 400 and typed 409, the Secrets page taken name (real API), and a 10-minute Retry-After. The unexplained-conflict subcases pass on main by design: they keep the view's old sentence.
  • Pinned 20/20 at 2 CPUs in a private network namespace on a remote test VM; the thirteen affected browser test files pass (357 cases).
  • Console and tests typecheck (tsc -b), eslint and prettier clean on touched files.
  • Storybook: Components/Deletion/Conflict (simulated UI proof).

Before / after (Secret delete when a new reference appeared after the confirmation opened; Agent delete answered with a typed 409):

Before After
secret-delete-referenced-before secret-delete-referenced-after
agent-delete-conflict-before agent-delete-conflict-after

The screenshots use the Console test fixture (real API, in-memory storage).

In this desktop Storybook walkthrough (721c8169, simulated data), the Console explains why deletion was refused. Refresh deletion status lets the operator return to the confirmation dialog.

deletion-conflict.webm

Risks

  • The administrator check reuses Create Agent's Installation read, as finding 2113's grant refusal does; a member whose Installation read fails keeps the sentence that names an Installation administrator.
  • A typed 409 or 400 sentence is shown verbatim; the HTTP error mapping already caps it at 256 characters and the client drops control characters. Sentences the mapping replaces (ResourceConflictError, ScopeViolationError) still arrive generic and keep the console's own text.

…ilure describer

Stop, Delete, the Agents list row actions, the Secrets page, Create Secret,
Namespace create, plugin save, repository choices, Preset load and Agent
sharing now show the API's own 400/409 sentence, a 429's wait from
Retry-After, and never ask to check entered values where there is no form.
…ret refusals

Fixtures that invented server sentences now send the API's real ones
(contract mismatch, lifecycle conflict, generic conflict, Secret name
conflict); the Secret delete recheck reads the real referenced-by text, and
a 429 with Retry-After names the wait.
@freeqaz
freeqaz requested a review from a team as a code owner October 25, 2026 23:57
@clawsweeper

clawsweeper Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

…s through the describer

Create Agent's 409 shows the API's typed reason; a denial for a viewer who
read the Installation (an Installation administrator) names nobody to ask.
describeFailure passes an AccessNeed through. console.md no longer says
backend error text is never rendered.
@clawsweeper clawsweeper Bot added clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-environment Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-environment Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. labels Oct 26, 2026
@clawsweeper

clawsweeper Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes

This PR shares Console refusal formatting so views show typed API reasons and Retry-After delays.

Example: Deleting an Agent returns HTTP 409 with “The Agent lifecycle changed during deletion.”

  • Before: The Console shows a generic current-state refusal.
  • After: The Console shows the API’s lifecycle reason followed by “Refresh deletion status before trying again.”

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Useful, focused implementation with credible screenshots and browser coverage; incomplete declared scope and the unmet UI walkthrough requirement limit readiness.
Proof confidence 🦐 gold shrimp (3/6) Real behavior proof is not required for maintainer- or bot-authored pull requests.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Product

Kind: Bug fix · Worth it: Yes · Fix scope: Partial
User problem: Users receive generic or misleading explanations when the API already names the rule or state that refused their action.
Reason: The collaborator-authored direction has clear recovery value; finish the Secrets-page duplicate-name case or narrow the stated scope to acknowledge that remainder.

Merge readiness

✅ Ready for maintainer review

Keep open for maintainer triage: this PR provides useful Console error explanations, and no introduced correctness or security defect was found.

Priority: P2
Reviewed head: 53c05aee107f39cd04db20b08302fc7429cc239e

Before merge

None.

Findings

None.

Tests

  • Missing end-to-end proof: Supply the repository-required short desktop interaction video from the final UI, including the rebuilt Components/Deletion/Conflict Storybook scenario.
Agent review details

How this fits together

The Console receives sanitized HTTP error metadata from its API client and turns it into text beside resource controls; authorization and resource mutations remain server-owned.

flowchart TB
  A[Console action] --> B[Controller API]
  B --> C[HTTP error mapping]
  C --> D[Console API client]
  D --> E[Shared refusal describer]
  E --> F[Visible status text]
Loading

Technical review

Best possible solution:

Complete the declared refusal-copy scope through the existing shared formatter while preserving retry guards, value clearing, and server authorization.

Do we have a high-confidence way to reproduce the issue?

Current-main handlers visibly discard typed API reasons; the supplied before/after screenshots corroborate that path, and the PR reports failing-base/passing-head browser cases.

Is this the best way to solve the issue?

Extending the existing shared message owner is a focused approach; the unchanged Secrets-page name-conflict override makes delivery partial.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 45aaad657728.

Provenance checked

  • Stop and deletion refusal handling changes intended behavior with a stated reason (eef3442: The TypeScript refactor preserved identity checks, uncertain-outcome handling, and manual recovery; it does not establish the original introduction of the carried refusal wording.)
  • Namespace Secrets mutation recovery keeps the original intent (2c9e7f6: Secret management clears submitted values, prevents duplicate writes, and requires inspection after uncertain outcomes; these protections remain intact.)
  • Repository and Preset feedback keeps the original intent (dd8706f: The migration retained browser URLs, access defaults, and keyboard interactions; this PR changes explanations without replacing those contracts.)
  • Shared expected refusal text keeps the original intent (21eb9fc: Independent expected strings prevent stale wording assertions; the patch retains that separation and replaces invented fixture sentences with actual API wording.)
  • Member and administrator denial guidance keeps the original intent (b019f13: Denied reads name needed grants and who can grant them, while known administrators receive a plain denial; the new Create Agent handling follows that distinction.)

Testing

Proof path: shipped entry point.

Security

None.

Evidence

What I checked:

Likely related people:

  • Free Wortley: Raw commit eef3442 adds apps/controller/src/console/agents/deletion.mts:321 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: eef3442ff948; files: apps/controller/src/console/agents/deletion.mts)
  • Derek Carr: Raw commit 2c9e7f6 adds apps/controller/src/console/secrets.mjs:440 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 2c9e7f6c489f; files: apps/controller/src/console/secrets.mjs)

Labels

Label changes:

  • add P2: Misleading refusal text causes avoidable recovery confusion, but the change does not repair an authorization or resource-lifecycle failure.
  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.

Label justifications:

  • P2: Misleading refusal text causes avoidable recovery confusion, but the change does not repair an authorization or resource-lifecycle failure.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Finish or explicitly narrow the Secrets-page duplicate-name scope.
  • Supply the required final-UI interaction recording.

Rating scale

6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior.

Workflow

ClawSweeper edits this one comment on every review. Comment @clawsweeper re-review for a fresh review only. Repair and merge execution lanes are retired.

Reviewed October 25, 2026, 8:20 PM ET / October 26, 2026, 00:20 UTC.

Findings 2559-2564 from review-3690.

- Secrets page: a taken Secret name shows the API's sentence; only the
  generic conflict text keeps the "may already be in use" hedge (2559).
- One explainedConflict() helper decides whether a 409 explains itself,
  shared by the describer, the Secrets page, the Secret picker and
  Namespaces, so a missing message is treated the same everywhere (2562).
- Stop, deletion, the Agents list and Secret deletion pass their own
  sentence for a conflict the API does not explain (2561).
- describeFailure drops its unused need/administrator options;
  tooManyRequests is module-private, and the shared retryAfterText()
  also serves the Logs tab, showing a long Retry-After in minutes (2562,
  2564).
- The malformed-request text no longer asks for a request ID that some
  views do not show; a Secret 400 without an API sentence keeps "Check
  the input and enter it again" (2564).
- Storybook: secretsDeleteConflict and createRepositoriesRecovery send
  real API sentences instead of the placeholder (2560). The console flow
  doc says which backend text failure views show (2564).
- Tests: Agent page Stop and Agents list row Stop/Delete 400 and 409,
  including a conflict the API does not explain (2563); the Secrets name
  conflict and a 10-minute Retry-After.

@freeqaz-openai freeqaz-openai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two cases still give the wrong recovery advice, or would let it pass the test:

  1. In the inline Secret picker, a 409 with no message satisfies undefined !== GENERIC_CONFLICT. It takes the branch for a specific API reason and replaces the dialog’s name/Namespace guidance with “Refresh, then try again.” Treat a missing reason like the generic conflict. Please exercise all three responses: no message, the generic message, and a real duplicate-name message.
  2. The credential-retry assertion accepts contradictory advice such as “This write may have succeeded. Repeat the grant now” after a read failed. Assert the expected read-refusal sentence, and keep the existing checks that Retry remains available and no grant is posted.

Claw’s review separately identifies the Secrets page’s duplicate-name override. The existing real duplicate POST is a good place to assert that the dialog preserves that specific API reason. These changes can stay within the current PR.

…a 429 in the Secrets refusals case

The new Agent page Stop case waited for idle fetches without tracking
them, and the Secrets refusal case's POST count and the reload case's
settle text still assumed the old name-conflict hedge and one 429.
…the credential retry's read refusal

Review of #3690: the inline Create Secret dialog is exercised with a 409
that carries no message, the API's generic conflict text and the real
duplicate-name sentence; the first two keep the dialog's name and
Namespace guidance. The credential-access Retry case asserts the read's
own refusal sentence instead of any text between the fixed ends.
@freeqaz

freeqaz commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Free's clanker here. Thanks for the review. Both points are fixed at 721c816:

  1. Inline Secret picker. A 409 now counts as explained only when it carries a message other than the generic conflict text. One shared explainedConflict() makes that call for the describer, the Secrets page, the picker and Namespaces. tests/browser/console-form-fields.test.ts covers all three responses:

    • no message (a body without the API envelope);
    • the generic "already exists" text;
    • the real duplicate-name sentence.

    The first two keep the dialog's name and Namespace guidance and never say "Refresh, then try again". The new case fails on the previous head, 53c05ae.

  2. Credential retry. console-agent-detail.test.mjs now asserts the read's own refusal sentence exactly, request ID included. The checks that Retry stays available and that no grant is posted are unchanged.

The Secrets page now shows the API's sentence for a taken name. The real duplicate POST in console-secrets.test.ts asserts that sentence and that the old hedge is gone.

Also in this push: fixes for the rest of our internal review.

  • Stop, deletion, the Agents list and Secret deletion keep their own sentence when a conflict comes with no explanation from the API.
  • New 400/409 cases for Agent page Stop and the Agents list row Stop/Delete.
  • Storybook fixtures send real API sentences.
  • A long Retry-After reads in minutes.
  • The flow doc is corrected.

Tests on a remote test VM: the touched browser files pass 445/445. The new cases pass 20/20 at 2 CPUs.

@freeqaz-openai
freeqaz-openai dismissed their stale review October 26, 2026 01:05

Verified on 721c816: the Secret picker gives the right guidance for absent, generic and specific 409 messages, and the credential-read test now rejects instructions to repeat a write. Both requests from my earlier review are addressed.

@freeqaz-openai

Copy link
Copy Markdown
Collaborator

@clawsweeper re-review

Please review the revised Secret conflict handling and credential-read regression test on 721c8169. The current desktop deletion walkthrough is in Verification.

@clawsweeper

clawsweeper Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
Exact review queued.

Re-review progress:

@freeqaz
freeqaz merged commit d28c024 into main Oct 26, 2026
42 checks passed
@freeqaz
freeqaz deleted the fix/console-failure-describer branch October 26, 2026 01:18
freeqaz added a commit that referenced this pull request Oct 26, 2026
The squash's tree equals #3690's final head, already merged here, so
the conflicted files keep this branch's side; the merged tree is
unchanged.
freeqaz added a commit that referenced this pull request Oct 26, 2026
…ess retry

#3690's retry test sends the real API denial, which now names the Agent
read grant the retry's reread lacks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants