From fe466d764e840e92e98e07af1401c0cb5d6673d8 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 01:11:24 -0700 Subject: [PATCH 1/2] Add tests for agent create draft failure envelope (CL-6749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed Myra draft must answer the canonical envelope, keep that userMessage and refId on the client, and show both in the panel — never a generic 422 with no quotable id. --- apps/hub/src/draft-route-envelope.test.ts | 11 ++ apps/web/test/agents-api.test.ts | 86 +++++++++ apps/web/test/create-agent-panel.test.tsx | 31 ++++ .../agent-definition-draft-routes.test.ts | 174 ++++++++++++++++++ packages/api-query/src/envelope.test.ts | 8 + 5 files changed, 310 insertions(+) create mode 100644 apps/hub/src/draft-route-envelope.test.ts create mode 100644 packages/agent-directory/test/agent-definition-draft-routes.test.ts diff --git a/apps/hub/src/draft-route-envelope.test.ts b/apps/hub/src/draft-route-envelope.test.ts new file mode 100644 index 000000000..1f3061080 --- /dev/null +++ b/apps/hub/src/draft-route-envelope.test.ts @@ -0,0 +1,11 @@ +// The hub composition root must serve the package draft routes (CL-6749) +// rather than a local `{ code, message }` 422 the client cannot parse. + +import { readFileSync } from "node:fs"; +import { expect, test } from "bun:test"; + +test("hub mounts package draft routes rather than a local {code, message} 422", () => { + const source = readFileSync(new URL("./index.ts", import.meta.url), "utf8"); + expect(source).toContain("createAgentDefinitionDraftRoutes"); + expect(source).not.toMatch(/code:\s*"drafting_failed"/); +}); diff --git a/apps/web/test/agents-api.test.ts b/apps/web/test/agents-api.test.ts index 1046f8449..c69d5d2ee 100644 --- a/apps/web/test/agents-api.test.ts +++ b/apps/web/test/agents-api.test.ts @@ -3,8 +3,11 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { ApiQueryError } from "@corbits/api-query"; + import { clearAgentModel, + draftAgentDefinition, getAgentDefinitionBySlug, listCatalogModels, listRoutineRunFires, @@ -343,3 +346,86 @@ describe("clearAgentModel", () => { expect(calls[0]?.method).toBe("DELETE"); }); }); + +const DRAFT_FAILED_MESSAGE = + "Myra couldn't draft a starting prompt for that. Write one yourself, or try again."; + +describe("draftAgentDefinition errors", () => { + test("keeps the envelope userMessage and refId on a 422", async () => { + stubFetch(() => + json( + { + error: { + code: "drafting_failed", + userMessage: DRAFT_FAILED_MESSAGE, + refId: "ref_draft_1", + }, + }, + 422, + ), + ); + + try { + await draftAgentDefinition("tnt_1", { name: "Research Buddy" }); + throw new Error("expected draftAgentDefinition to reject"); + } catch (err) { + expect(err).toBeInstanceOf(ApiQueryError); + const error = err as ApiQueryError; + expect(error.message).toBe(DRAFT_FAILED_MESSAGE); + expect(error.refId).toBe("ref_draft_1"); + expect(error.status).toBe(422); + } + }); + + test("keeps the envelope userMessage and refId on a 500", async () => { + stubFetch(() => + json( + { + error: { + code: "internal_error", + userMessage: "Something went wrong. Please try again.", + refId: "ref_sink_1", + }, + }, + 500, + ), + ); + + try { + await draftAgentDefinition("tnt_1", { name: "Research Buddy" }); + throw new Error("expected draftAgentDefinition to reject"); + } catch (err) { + expect(err).toBeInstanceOf(ApiQueryError); + const error = err as ApiQueryError; + expect(error.message).toBe("Something went wrong. Please try again."); + expect(error.refId).toBe("ref_sink_1"); + expect(error.status).toBe(500); + } + }); + + test("does not treat a legacy {code, message} body as an envelope", async () => { + stubFetch(() => + json( + { + error: { + code: "drafting_failed", + message: + "MyraAgentDefinitionDraftingUnavailableError: no myra\n at draft", + }, + }, + 422, + ), + ); + + try { + await draftAgentDefinition("tnt_1", { name: "Research Buddy" }); + throw new Error("expected draftAgentDefinition to reject"); + } catch (err) { + expect(err).toBeInstanceOf(ApiQueryError); + const error = err as ApiQueryError; + expect(error.message).toBe("The server answered 422."); + expect(error.refId).toBeUndefined(); + expect(error.message).not.toContain("no myra"); + } + }); +}); diff --git a/apps/web/test/create-agent-panel.test.tsx b/apps/web/test/create-agent-panel.test.tsx index e9c7542ec..cd4e1c0ec 100644 --- a/apps/web/test/create-agent-panel.test.tsx +++ b/apps/web/test/create-agent-panel.test.tsx @@ -330,6 +330,7 @@ describe("CreateAgentPanel drafting failure — fails closed", () => { expect(document.body.textContent).toContain( "Myra couldn't draft a starting prompt for that.", ); + expect(document.body.textContent).toContain("Reference: ref_1"); expect(document.body.textContent).toContain( "Write a system prompt below to continue.", ); @@ -351,6 +352,36 @@ describe("CreateAgentPanel drafting failure — fails closed", () => { expect(createCalled).toBe(true); }); + + test("a 500 envelope shows the consumer sentence plus Reference, not a dead-end", async () => { + stubFetch({ + draft: () => + json( + { + error: { + code: "internal_error", + userMessage: "Something went wrong. Please try again.", + refId: "ref_sink_1", + }, + }, + 500, + ), + }); + + await mount(); + fillField("create-agent-name", "Research Buddy"); + await act(async () => { + findButton("Get started")?.click(); + }); + await settle(); + + expect(document.body.textContent).toContain( + "Something went wrong. Please try again.", + ); + expect(document.body.textContent).toContain("Reference: ref_sink_1"); + expect(document.body.textContent).not.toContain("The server answered 500."); + expect(document.body.textContent).not.toContain("at draft"); + }); }); describe("agent creation entry points", () => { diff --git a/packages/agent-directory/test/agent-definition-draft-routes.test.ts b/packages/agent-directory/test/agent-definition-draft-routes.test.ts new file mode 100644 index 000000000..a60eecf06 --- /dev/null +++ b/packages/agent-directory/test/agent-definition-draft-routes.test.ts @@ -0,0 +1,174 @@ +// Draft-route envelope (CL-6749): a fail-closed Myra draft answers the +// canonical `{ code, userMessage, refId }` envelope, reports through +// `reportError` so the person can quote the same id, and never puts a +// legacy `{ code, message }` body — or a stack — on the wire. + +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { configureSync, resetSync } from "@intx/log"; + +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import { makeErrorEnvelope, parseErrorEnvelope } from "@corbits/error-sink"; + +import { MyraAgentDefinitionDraftingUnavailableError } from "../src/agent-definition-drafting"; +import { createAgentDefinitionDraftRoutes } from "../src/agent-definition-draft-routes"; + +const TENANT = { + id: "tnt_1", + name: "Acme", + slug: "acme", + domain: "acme.example", + parentId: null, + config: null, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const PRINCIPAL = { + id: "prn_1", + tenantId: TENANT.id, + kind: "user" as const, + refId: "prn_1", + status: "active" as const, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const allowAll: RequireGrant = () => async (_c, next) => { + await next(); +}; + +const DRAFT_FAILED_MESSAGE = + "Myra couldn't draft a starting prompt for that. Write one yourself, or try again."; + +function buildApp( + draftAgentDefinition?: (input: { + readonly tenantId: string; + readonly principalId: string; + readonly name: string; + readonly purpose?: string; + }) => Promise<{ + readonly systemPrompt: string; + readonly toolPackagePins: readonly string[]; + readonly skills: readonly string[]; + }>, +): Hono { + const routes = createAgentDefinitionDraftRoutes({ + requireGrant: allowAll, + ...(draftAgentDefinition !== undefined ? { draftAgentDefinition } : {}), + }); + const asPrincipal: MiddlewareHandler = async (c, next) => { + c.set("tenant", TENANT); + c.set("principal", PRINCIPAL); + await next(); + }; + const app = new Hono(); + app.use("*", asPrincipal); + app.route("/", routes); + return app; +} + +async function postDraft( + app: Hono, + body: unknown, +): Promise { + return app.request("/agent-definitions/draft", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +let records: { properties: Record }[]; + +function installCapturingSink(): void { + records = []; + configureSync({ + reset: true, + sinks: { + capture: (record) => { + records.push(record as { properties: Record }); + }, + }, + loggers: [ + { category: ["errors"], sinks: ["capture"], lowestLevel: "debug" }, + { category: ["logtape", "meta"], sinks: [], lowestLevel: "warning" }, + ], + }); +} + +beforeEach(() => installCapturingSink()); +afterEach(() => resetSync()); + +describe("agent-definition draft route envelope", () => { + test("a drafting failure answers 422 makeErrorEnvelope and reports the same refId", async () => { + const app = buildApp(() => + Promise.reject( + new MyraAgentDefinitionDraftingUnavailableError("tnt_1", "no myra"), + ), + ); + + const res = await postDraft(app, { name: "Research Buddy" }); + + expect(res.status).toBe(422); + const body: unknown = await res.json(); + const envelope = parseErrorEnvelope(body); + expect(envelope).toBeDefined(); + expect(envelope?.error.code).toBe("drafting_failed"); + expect(envelope?.error.userMessage).toBe(DRAFT_FAILED_MESSAGE); + expect(typeof envelope?.error.refId).toBe("string"); + expect(envelope?.error.refId.length).toBeGreaterThan(0); + expect(JSON.stringify(body)).not.toContain("no myra"); + expect(JSON.stringify(body)).not.toContain("stack"); + expect( + (body as { error: { message?: unknown } }).error.message, + ).toBeUndefined(); + + expect(records).toHaveLength(1); + expect(records[0]?.properties.refId).toBe(envelope?.error.refId); + expect(records[0]?.properties.operation).toBe( + "agentDirectory.draftAgentDefinition", + ); + expect(records[0]?.properties.tenantId).toBe("tnt_1"); + }); + + test("a malformed body answers 400 makeErrorEnvelope, not {code, message}", async () => { + const app = buildApp(() => + Promise.resolve({ + systemPrompt: "unused", + toolPackagePins: [], + skills: [], + }), + ); + + const res = await postDraft(app, {}); + expect(res.status).toBe(400); + const body: unknown = await res.json(); + const envelope = parseErrorEnvelope(body); + expect(envelope).toBeDefined(); + expect(envelope?.error.code).toBe("bad_request"); + expect(envelope?.error.userMessage.length).toBeGreaterThan(0); + expect( + (body as { error: { message?: unknown } }).error.message, + ).toBeUndefined(); + }); + + test("an unexpected throw is not mapped to drafting_failed", async () => { + const app = buildApp(() => Promise.reject(new Error("disk full"))); + app.onError((_err, c) => + c.json( + makeErrorEnvelope({ + code: "internal_error", + userMessage: "Something went wrong. Please try again.", + }), + 500, + ), + ); + const res = await postDraft(app, { name: "Research Buddy" }); + expect(res.status).toBe(500); + const body: unknown = await res.json(); + const envelope = parseErrorEnvelope(body); + expect(envelope?.error.code).toBe("internal_error"); + }); +}); diff --git a/packages/api-query/src/envelope.test.ts b/packages/api-query/src/envelope.test.ts index f935062e8..674e2e930 100644 --- a/packages/api-query/src/envelope.test.ts +++ b/packages/api-query/src/envelope.test.ts @@ -17,6 +17,14 @@ describe("ApiQueryError", () => { const withoutStatus = new ApiQueryError("network down"); expect(withoutStatus.status).toBeUndefined(); }); + + test("carries an optional refId from a hub error envelope", () => { + const withRef = new ApiQueryError("boom", 500, "/api/x", "ref_sink_1"); + expect(withRef.refId).toBe("ref_sink_1"); + + const withoutRef = new ApiQueryError("boom", 500, "/api/x"); + expect(withoutRef.refId).toBeUndefined(); + }); }); describe("UnauthenticatedError", () => { From 0a0edfdcd7d5b15abc2d706a59747978a1368de8 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 01:11:46 -0700 Subject: [PATCH 2/2] Show draft failures with a consumer sentence and quotable refId (CL-6749) The hub was answering drafting_failed as {code, message}, so the client could not parse the envelope and the panel showed a generic 422 with no refId. Mount the package draft routes, report the failure through reportError, and thread that same refId through ApiQueryError into the panel. --- apps/hub/src/index.ts | 132 ++++-------------- apps/web/src/agents-api.ts | 18 ++- apps/web/src/pages/create-agent-panel.tsx | 37 +++-- .../src/agent-definition-draft-routes.ts | 15 +- packages/api-query/src/envelope.ts | 4 +- 5 files changed, 74 insertions(+), 132 deletions(-) diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index 8a467fd92..7f0dfd1af 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -54,6 +54,7 @@ import type { HarnessConfig } from "@intx/types/runtime"; // — the gate itself only stamps this hash on the `ok:true` arm. import { + createAgentDefinitionDraftRoutes, createAgentDefinitionRoutes, createDefinitionAssetHistory, createDrizzleDefinitionSkillsStore, @@ -61,11 +62,8 @@ import { createWorkflowCapabilityRoutes, createWorkflowSkillPinRoutes, type CapabilityInventoryProvider, - AgentDefinitionDraftReferenceOutOfInventoryError, - AgentDefinitionDraftReplyUnparseableError, createMyraAgentDefinitionDrafting, isPlannerCreatedDefinitionName, - MyraAgentDefinitionDraftingUnavailableError, resolveMyraDefinitionIdFromDb, type InventoryAgent, type InventoryModel, @@ -224,11 +222,7 @@ import { createDrizzleWorkflowDeploySourceStore, withDeploySourceRecording, } from "@corbits/workflows"; -import { - FoldedRunFailedError, - FoldedRunTimedOutError, - runOneShotFoldedPrompt, -} from "@corbits/folded-run-one-shot"; +import { runOneShotFoldedPrompt } from "@corbits/folded-run-one-shot"; import { createEventCollectorRegistry, @@ -3229,106 +3223,34 @@ export async function createHub(config: HubConfig) { // from a name + plain-language purpose, offering her the same // inventory `capabilityInventory` above reads through. Never deploys // on its own — the panel submits the validated draft through the - // ordinary create-agent-definition path once the person confirms. This - // route is hand-wired here rather than through a factory the package - // exports, since agent-directory's own route factories are each scoped - // to a narrower concern. - const CreateAgentDefinitionDraftBody = type({ - name: "string > 0", - "purpose?": "string > 0", - }); - const agentDefinitionDraftingInFlightPrincipals = new Set(); - function isAgentDefinitionDraftingFailure(err: unknown): boolean { - return ( - err instanceof MyraAgentDefinitionDraftingUnavailableError || - err instanceof FoldedRunTimedOutError || - err instanceof FoldedRunFailedError || - err instanceof AgentDefinitionDraftReplyUnparseableError || - err instanceof AgentDefinitionDraftReferenceOutOfInventoryError - ); - } - const plannerRoutes = new Hono(); - plannerRoutes.post( - "/agent-definitions/draft", - createRequireGrant({ + // ordinary create-agent-definition path once the person confirms. + // Failure copy is the package route's `makeErrorEnvelope` + + // `reportError` (CL-6749), not a local `{ code, message }` body. + const plannerRoutes = createAgentDefinitionDraftRoutes({ + requireGrant: createRequireGrant({ grantStore: chatGrantStore, conditionRegistry: chatConditionRegistry, - })("workflow-definition:*", "create"), - async (c) => { - const body = CreateAgentDefinitionDraftBody( - await c.req.json().catch(() => undefined), - ); - if (body instanceof type.errors) { - return c.json( - { - error: { - code: "bad_request", - message: `This couldn't be read: ${body.summary}`, - }, - }, - 400, - ); - } - - const tenant = c.get("tenant"); - const principal = c.get("principal"); - - if (agentDefinitionDraftingInFlightPrincipals.has(principal.id)) { - return c.json( - { - error: { - code: "dispatch_in_progress", - message: "Myra is already drafting your last agent.", - }, - }, - 409, - ); - } - agentDefinitionDraftingInFlightPrincipals.add(principal.id); - try { - const draft = await createMyraAgentDefinitionDrafting({ - resolveMyraDefinitionId: (tenantId) => - resolveMyraDefinitionIdFromDb(db, tenantId), - runner: { - run: (runnerInput) => - runOneShotFoldedPrompt( - { - foldedRuns: oneShotFoldedRunsDeps, - events: sidecarRouter.events, - cryptoProviders: agentDefinitionDraftingCryptoProviders, - undeploy: (address, reason) => - sidecarRouter.sendAgentUndeploy(address, reason), - }, - runnerInput, - ), - }, - inventorySources: plannerInventorySources, - }).propose({ - tenantId: tenant.id, - principalId: principal.id, - name: body.name, - ...(body.purpose !== undefined ? { purpose: body.purpose } : {}), - }); - return c.json({ draft }, 201); - } catch (err) { - if (isAgentDefinitionDraftingFailure(err)) { - return c.json( - { - error: { - code: "drafting_failed", - message: - "Myra couldn't draft a starting prompt for that. Write one yourself, or try again.", + }), + draftAgentDefinition: (input) => + createMyraAgentDefinitionDrafting({ + resolveMyraDefinitionId: (tenantId) => + resolveMyraDefinitionIdFromDb(db, tenantId), + runner: { + run: (runnerInput) => + runOneShotFoldedPrompt( + { + foldedRuns: oneShotFoldedRunsDeps, + events: sidecarRouter.events, + cryptoProviders: agentDefinitionDraftingCryptoProviders, + undeploy: (address, reason) => + sidecarRouter.sendAgentUndeploy(address, reason), }, - }, - 422, - ); - } - throw err; - } finally { - agentDefinitionDraftingInFlightPrincipals.delete(principal.id); - } - }, - ); + runnerInput, + ), + }, + inventorySources: plannerInventorySources, + }).propose(input), + }); app.route(`${TENANT_PREFIX}/planner`, plannerRoutes); // The sanctioned path for a workflow run to reach the memory plane diff --git a/apps/web/src/agents-api.ts b/apps/web/src/agents-api.ts index 5cc0d76e9..56409deb7 100644 --- a/apps/web/src/agents-api.ts +++ b/apps/web/src/agents-api.ts @@ -24,6 +24,7 @@ import { toAPIQuery, } from "@corbits/api-query"; import { isChatPickerModelName } from "@corbits/connections/model-capability"; +import { parseErrorEnvelope } from "@corbits/error-sink"; import { tenantKeys } from "./query-client"; export type AgentDefinition = typeof WorkflowDefinitionResponse.infer; @@ -83,10 +84,6 @@ async function getJSON(path: string, schema: Validator): Promise { return parsed; } -const ErrorEnvelope = type({ - error: { code: "string", userMessage: "string", refId: "string" }, -}); - async function postJSON( path: string, schema: Validator, @@ -109,12 +106,13 @@ async function postJSON( } const json: unknown = await response.json().catch(() => undefined); if (!response.ok) { - const envelope = ErrorEnvelope(json); - const message = - envelope instanceof type.errors - ? `The server answered ${response.status}.` - : envelope.error.userMessage; - throw new ApiQueryError(message, response.status, path); + const envelope = parseErrorEnvelope(json); + throw new ApiQueryError( + envelope?.error.userMessage ?? `The server answered ${response.status}.`, + response.status, + path, + envelope?.error.refId, + ); } const parsed = schema(json); if (parsed instanceof type.errors) { diff --git a/apps/web/src/pages/create-agent-panel.tsx b/apps/web/src/pages/create-agent-panel.tsx index 0c54160c6..00db734c8 100644 --- a/apps/web/src/pages/create-agent-panel.tsx +++ b/apps/web/src/pages/create-agent-panel.tsx @@ -71,6 +71,17 @@ import { AgentSkillsPicker } from "./agent-skills-picker"; // that true: one implementation, so a handle suggested or accepted here can // never be a shape the router refuses to resolve. +function submitErrorFromCause( + cause: unknown, + fallback: string, +): { readonly message: string; readonly refId?: string } { + if (!(cause instanceof ApiQueryError)) return { message: fallback }; + return { + message: cause.message, + ...(cause.refId !== undefined ? { refId: cause.refId } : {}), + }; +} + function initialsFromName(name: string): string { const [first, second] = name.trim().split(/\s+/).filter(Boolean); if (first === undefined) return "?"; @@ -238,7 +249,10 @@ export function CreateAgentPanel({ const [advancedOpen, setAdvancedOpen] = useState(false); const [draftFailed, setDraftFailed] = useState(false); const [submitting, setSubmitting] = useState(false); - const [submitError, setSubmitError] = useState(null); + const [submitError, setSubmitError] = useState<{ + readonly message: string; + readonly refId?: string; + } | null>(null); const [modelsState, setModelsState] = useState({ kind: "idle", }); @@ -354,9 +368,10 @@ export function CreateAgentPanel({ setDraftFailed(true); setAdvancedOpen(true); setSubmitError( - cause instanceof ApiQueryError - ? cause.message - : "Myra couldn't draft a starting prompt for this agent.", + submitErrorFromCause( + cause, + "Myra couldn't draft a starting prompt for this agent.", + ), ); setSubmitting(false); return; @@ -377,9 +392,7 @@ export function CreateAgentPanel({ onCreated(created); } catch (cause) { setSubmitError( - cause instanceof ApiQueryError - ? cause.message - : "Could not create the agent.", + submitErrorFromCause(cause, "Could not create the agent."), ); } finally { setSubmitting(false); @@ -403,7 +416,15 @@ export function CreateAgentPanel({ {submitError !== null && (

- {submitError} + {submitError.message} + {submitError.refId !== undefined ? ( + <> +
+ + Reference: {submitError.refId} + + + ) : null}

)} {modelsState.kind === "error" && ( diff --git a/packages/agent-directory/src/agent-definition-draft-routes.ts b/packages/agent-directory/src/agent-definition-draft-routes.ts index da14e64f0..ae26305ca 100644 --- a/packages/agent-directory/src/agent-definition-draft-routes.ts +++ b/packages/agent-directory/src/agent-definition-draft-routes.ts @@ -14,13 +14,11 @@ import { type } from "arktype"; import { Hono } from "hono"; import type { TenantEnv, RequireGrant } from "@intx/hub-api"; -import { getLogger } from "@intx/log"; - import { FoldedRunFailedError, FoldedRunTimedOutError, } from "@corbits/folded-run-one-shot"; -import { makeErrorEnvelope } from "@corbits/error-sink"; +import { makeErrorEnvelope, reportError } from "@corbits/error-sink"; import { AgentDefinitionDraftReferenceOutOfInventoryError, AgentDefinitionDraftReplyUnparseableError, @@ -28,8 +26,6 @@ import { type AgentDefinitionDraft, } from "./agent-definition-drafting"; -const log = getLogger(["agent-directory", "agent-definition-draft-routes"]); - const DRAFT_FAILED_MESSAGE = "Myra couldn't draft a starting prompt for that. Write one yourself, or try again."; @@ -138,14 +134,17 @@ export function createAgentDefinitionDraftRoutes( }); return c.json({ draft }, 201); } catch (err) { - log.error`agent definition drafting failed for tenant ${tenant.id}: ${ - err instanceof Error ? err.message : String(err) - }`; if (isDraftingFailure(err)) { + const refId = reportError(err, { + operation: "agentDirectory.draftAgentDefinition", + tenantId: tenant.id, + extra: { principalId: principal.id }, + }); return c.json( makeErrorEnvelope({ code: "drafting_failed", userMessage: DRAFT_FAILED_MESSAGE, + refId, }), 422, ); diff --git a/packages/api-query/src/envelope.ts b/packages/api-query/src/envelope.ts index a8424dd1e..2bef1fccc 100644 --- a/packages/api-query/src/envelope.ts +++ b/packages/api-query/src/envelope.ts @@ -32,12 +32,14 @@ export class UnauthenticatedError extends Error { /** The one HTTP-query error shape every hub request throws: a human message * plus the response status when one exists (absent for network failures), - * plus the request path for logs — never surfaced in user-facing copy. */ + * plus the request path for logs — never surfaced in user-facing copy — + * plus the envelope `refId` when the hub answered through the sink. */ export class ApiQueryError extends Error { constructor( message: string, readonly status?: number, readonly path?: string, + readonly refId?: string, ) { super(message); }