From 671789ef23d23c10bcd61d4b28f7ccf179e48658 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Tue, 15 Sep 2026 05:59:54 -0700 Subject: [PATCH 1/2] fix(deploy): surface actionable guidance on stale-workspace 404s Resolving a deleted/expired workspace (via the local ~/.agentworkforce/relay/workspaces.json active pointer, or an explicit --workspace) surfaced a raw 404 from @agent-relay/cloud with no hint that the fix is to re-pick a workspace, not retry. Detect the 404 and append guidance to run `agent-relay workspace list` / `agent-relay workspace switch `, or `agentworkforce deploy --mode cloud` to provision a new one. Co-Authored-By: Claude Sonnet 5 --- packages/deploy/src/login.test.ts | 21 +++++++++++++++++++++ packages/deploy/src/login.ts | 29 ++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/deploy/src/login.test.ts b/packages/deploy/src/login.test.ts index e55bcfb4..cb4ad010 100644 --- a/packages/deploy/src/login.test.ts +++ b/packages/deploy/src/login.test.ts @@ -141,6 +141,27 @@ test('resolveWorkspaceToken fails clearly when workspace resolve returns non-JSO }); }); +test('resolveWorkspaceToken surfaces stale-workspace guidance on 404', async () => { + await withCloudSessionEnv(async () => { + const restoreFetch = withTrappedFetch(async () => + new Response('Workspace not found', { status: 404 }) + ); + try { + await assert.rejects( + resolveWorkspaceToken({ + workspace: 'rw_stale', + cloudUrl: 'https://cloud.example.test', + io: createBufferedIO(), + noPrompt: true + }), + /agent-relay workspace list.*agent-relay workspace switch/s + ); + } finally { + restoreFetch(); + } + }); +}); + test('resolveWorkspaceToken without a cloud session fails with login guidance', () => { const home = mkdtempSync(path.join(os.tmpdir(), 'wf-no-cloud-session-')); const env: NodeJS.ProcessEnv = { diff --git a/packages/deploy/src/login.ts b/packages/deploy/src/login.ts index b7187678..aa058e87 100644 --- a/packages/deploy/src/login.ts +++ b/packages/deploy/src/login.ts @@ -158,6 +158,8 @@ async function resolveWorkspaceDescriptor(args: { return resolveActiveWorkspace({ apiUrl: args.apiUrl, interactive: false + }).catch((error) => { + throw workspaceNotFoundError(error, undefined); }); } @@ -171,12 +173,37 @@ async function resolveWorkspaceDescriptor(args: { ); if (!response.ok) { const text = await response.text().catch(() => ''); - throw new Error(`workspace resolve failed for ${workspace}: ${response.status} ${text}`.trim()); + throw workspaceNotFoundError( + new Error(`workspace resolve failed for ${workspace}: ${response.status} ${text}`.trim()), + workspace, + response.status + ); } const payload = await response.json().catch(() => null); return normalizeWorkspaceDescriptor(payload, session.auth.apiUrl || args.apiUrl); } +/** + * The active-workspace pointer stored locally (`agent-relay workspace switch`) + * can go stale if the workspace was deleted or expired server-side; the raw + * 404 from @agent-relay/cloud gives no hint that the *fix* is to re-pick a + * workspace rather than retry. Detect that case and append actionable guidance. + */ +function workspaceNotFoundError(error: unknown, workspace: string | undefined, status?: number): Error { + const message = error instanceof Error ? error.message : String(error); + const is404 = status === 404 || /:\s*404\b/.test(message) || /workspace not found/i.test(message); + if (!is404) { + return error instanceof Error ? error : new Error(message); + } + const target = workspace ? `workspace "${workspace}"` : 'the active workspace'; + return new Error( + `${message}\n\n` + + `${target} was not found server-side (deleted, expired, or never provisioned) — the local workspace pointer is stale. ` + + 'Run `agent-relay workspace list` to see valid workspaces, then `agent-relay workspace switch ` to pick one, ' + + 'or `agentworkforce deploy --mode cloud` to provision a new one.' + ); +} + function normalizeWorkspaceDescriptor(payload: unknown, apiUrl: string): ActiveWorkspaceDescriptor { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw new Error('workspace resolve returned an invalid descriptor'); From 046259e6df7760d54e638438a4e6a1aa0e8653b3 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Tue, 15 Sep 2026 06:07:03 -0700 Subject: [PATCH 2/2] fix(deploy): use source-neutral wording for explicit-workspace 404s CodeRabbit review on #340: the guidance message claimed "the local workspace pointer is stale" even when resolving an explicitly-named workspace (--workspace/WORKFORCE_WORKSPACE_ID), which never goes through the local active-workspace pointer. Only attribute staleness to the local pointer when no workspace name was given. Co-Authored-By: Claude Sonnet 5 --- packages/deploy/src/login.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/deploy/src/login.ts b/packages/deploy/src/login.ts index aa058e87..b0d4229b 100644 --- a/packages/deploy/src/login.ts +++ b/packages/deploy/src/login.ts @@ -195,10 +195,11 @@ function workspaceNotFoundError(error: unknown, workspace: string | undefined, s if (!is404) { return error instanceof Error ? error : new Error(message); } - const target = workspace ? `workspace "${workspace}"` : 'the active workspace'; + const diagnosis = workspace + ? `workspace "${workspace}" was not found server-side (deleted, expired, revoked, or never provisioned).` + : 'the active workspace was not found server-side (deleted, expired, or never provisioned) — the local workspace pointer is stale.'; return new Error( - `${message}\n\n` + - `${target} was not found server-side (deleted, expired, or never provisioned) — the local workspace pointer is stale. ` + + `${message}\n\n${diagnosis} ` + 'Run `agent-relay workspace list` to see valid workspaces, then `agent-relay workspace switch ` to pick one, ' + 'or `agentworkforce deploy --mode cloud` to provision a new one.' );