Skip to content

[Bugfix #1333] afx send: surface Tower's descriptive error message instead of bare NOT_FOUND - #1334

Open
mohidmakhdoomi wants to merge 8 commits into
mainfrom
builder/bugfix-1333
Open

[Bugfix #1333] afx send: surface Tower's descriptive error message instead of bare NOT_FOUND#1334
mohidmakhdoomi wants to merge 8 commits into
mainfrom
builder/bugfix-1333

Conversation

@mohidmakhdoomi

Copy link
Copy Markdown
Collaborator

Summary

afx send architect:<name> from a builder to an architect it isn't allowed to address printed only the opaque code:

[error] NOT_FOUND

The agent/human couldn't tell why it failed — "no such architect" vs. "not authorized to address that architect" looked identical. Tower does generate a descriptive message for exactly these cases; it was being discarded client-side. This PR surfaces that message (with the code kept as a suffix) so the two cases are distinguishable and the reason is visible.

Now:

[error] builder bugfix-1333 may only address its own spawning architect (NOT_FOUND)

Fixes #1333.

Root Cause

The descriptive message is produced server-side and thrown away client-side:

  1. resolveArchitectByName (tower-messages.ts) returns a helpful message for the spoofing rejection (builder <id> may only address its own spawning architect) and a distinct one for a genuinely-missing architect (Architect '<name>' not found in workspace '<ws>'.).
  2. handleSend (tower-routes.ts) serializes both fields: { error: code, message }. ✓ The message reaches the wire.
  3. TowerClient.request() (packages/core/src/tower-client.ts) extracted json.error || json.message — preferring the machine code and discarding the human message. sendMessage then returned { error: 'NOT_FOUND' } and send.ts rendered fatal('NOT_FOUND').

The same extraction block existed verbatim in two methods (request() and pasteImage()), so both dropped the message identically.

Fix

packages/core/src/tower-client.ts — extracted a single module-private helper extractTowerError(text) used by both sites (consolidating the duplicated block rather than editing it twice):

  • When both a machine error code and a human message are present and distinct"<message> (<code>)".
  • Otherwise → message || code || rawText (unchanged behavior for code-only responses like STARTING_UP, message-only responses, and non-JSON bodies).
  • typeof === 'string' guards harden against non-string fields.

This keeps the machine code available for programmatic callers (it's still in the string) and is strictly more informative for every Tower CLI error, not just send.

Scope: addressing/spoofing semantics are unchanged — a builder may still only address its own spawning architect. This is purely the error-surfacing seam.

Blast radius (verified): no client-side code compares the extracted .error against code strings (the only === 'NOT_FOUND'/'AMBIGUOUS'/'NO_CONTEXT' checks are server-side on result.code); server tests assert on result.code and the HTTP response body (unaffected); send.test.ts mocks sendMessage directly (unaffected).

Test Plan

New regression test packages/codev/src/agent-farm/__tests__/bugfix-1333-error-surfacing.test.ts (6 cases), exercising TowerClient via the codev re-export → built core dist (the artifact the CLI consumes) against a stubbed fetch:

  1. The afx send: builder→non-spawning-architect send surfaces bare [error] NOT_FOUND, dropping the descriptive reason #1333 scenariosendMessage to a non-spawning architect surfaces builder … may only address its own spawning architect (NOT_FOUND), not a bare code.
  2. Distinguishability — the genuinely-missing-architect message differs from the spoofing message.
    3–6. Backward-compat guards — code-only (STARTING_UP), message-only, equal code==message (no "foo (foo)"), and non-JSON body.

Proven fails-without / passes-with: reverting only the core source and rebuilding makes cases 1 & 2 fail with Received: "NOT_FOUND" (the exact bug); the 4 guards still pass. With the fix, all 6 pass.

  • npm run build — passes
  • npm test (full codev suite) — passes

mohidmakhdoomi and others added 6 commits August 2, 2026 00:32
…re code

TowerClient.request() extracted `json.error || json.message`, preferring the
machine code and discarding the human-readable `message` Tower produces. So a
builder addressing a non-spawning architect saw a bare `[error] NOT_FOUND` with
no reason — "no such architect" and "not authorized to address that architect"
were indistinguishable.

Extract a single `extractTowerError(text)` helper (the same drop-the-message
block was duplicated verbatim in request() and pasteImage()) that surfaces the
descriptive message with the code as a suffix — "<message> (<code>)" — when both
are present and distinct, falling back to message || code || rawText otherwise.
Keeps the machine code available and is strictly more informative for every
Tower CLI error. Addressing/spoofing semantics are unchanged.

Fixes #1333.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exercises TowerClient via the codev re-export (built core dist — the artifact the
CLI consumes) against a stubbed fetch. Six cases: the #1333 spoofing scenario via
sendMessage, spoofing-vs-genuinely-missing distinguishability, and four
backward-compat guards (code-only, message-only, equal code==message, non-JSON).

Verified fails-without/passes-with: reverting only the core source makes the two
scenario cases fail with Received: "NOT_FOUND" (the exact bug); the guards pass
both ways.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-scope escalation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mohidmakhdoomi

Copy link
Copy Markdown
Collaborator Author

Architect Integration Review — approved, ready for maintainer merge

Human (architect) explicitly approved this change. Posting the review for the record — not self-merging; leaving the merge to a cluesmith/codev maintainer per repo policy.

Root causeTowerClient.request() did error = json.error || json.message, preferring the machine code and discarding Tower's descriptive message, so the CLI surfaced a bare NOT_FOUND.

FixextractTowerError() returns `${detail} (${code})` when both are present (e.g. "builder … may only address its own spawning architect (NOT_FOUND)"), falling back to code, then raw body text; applied at both request() sites.

Blast radius (independently verified) — no consumer compares result.error against a code string programmatically, so broadening code"message (code)" is safe; every caller just displays or throws it.

Testing — 93-line regression test, 6 cases (the #1333 spoofing scenario via sendMessage, spoofing-vs-genuinely-missing distinguishability, and backward-compat guards: code-only / message-only / code==code / non-JSON), verified fails-without/passes-with. Full build + full codev suite green (4112 passed, 0 failed). Prior CMAP: claude APPROVE; gemini/codex REQUEST_CHANGES were solely on an unrelated spec file, since removed by rebasing onto current main.

Diff — 4 files, MERGEABLE, base main: packages/core/src/tower-client.ts (27+/14-), the regression test, thread, porch status.yaml.

Risk: Low. Ready for a maintainer to merge — please use a regular merge commit (no squash), per repo convention.

— Architect integration review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

afx send: builder→non-spawning-architect send surfaces bare [error] NOT_FOUND, dropping the descriptive reason

1 participant