[Bugfix #1333] afx send: surface Tower's descriptive error message instead of bare NOT_FOUND - #1334
[Bugfix #1333] afx send: surface Tower's descriptive error message instead of bare NOT_FOUND#1334mohidmakhdoomi wants to merge 8 commits into
Conversation
…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>
e26ce59 to
2164b4b
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Architect Integration Review — approved, ready for maintainer mergeHuman (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 cause — Fix — Blast radius (independently verified) — no consumer compares Testing — 93-line regression test, 6 cases (the #1333 spoofing scenario via Diff — 4 files, Risk: Low. Ready for a maintainer to merge — please use a regular merge commit (no squash), per repo convention. — Architect integration review |
Summary
afx send architect:<name>from a builder to an architect it isn't allowed to address printed only the opaque code: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:
Fixes #1333.
Root Cause
The descriptive message is produced server-side and thrown away client-side:
resolveArchitectByName(tower-messages.ts) returns a helpfulmessagefor 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>'.).handleSend(tower-routes.ts) serializes both fields:{ error: code, message }. ✓ The message reaches the wire.TowerClient.request()(packages/core/src/tower-client.ts) extractedjson.error || json.message— preferring the machine code and discarding the human message.sendMessagethen returned{ error: 'NOT_FOUND' }andsend.tsrenderedfatal('NOT_FOUND').The same extraction block existed verbatim in two methods (
request()andpasteImage()), so both dropped the message identically.Fix
packages/core/src/tower-client.ts— extracted a single module-private helperextractTowerError(text)used by both sites (consolidating the duplicated block rather than editing it twice):errorcode and a humanmessageare present and distinct →"<message> (<code>)".message || code || rawText(unchanged behavior for code-only responses likeSTARTING_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
.erroragainst code strings (the only=== 'NOT_FOUND'/'AMBIGUOUS'/'NO_CONTEXT'checks are server-side onresult.code); server tests assert onresult.codeand the HTTP response body (unaffected);send.test.tsmockssendMessagedirectly (unaffected).Test Plan
New regression test
packages/codev/src/agent-farm/__tests__/bugfix-1333-error-surfacing.test.ts(6 cases), exercisingTowerClientvia the codev re-export → built core dist (the artifact the CLI consumes) against a stubbedfetch:sendMessageto a non-spawning architect surfacesbuilder … may only address its own spawning architect (NOT_FOUND), not a bare code.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— passesnpm test(full codev suite) — passes