From de8037636cdb037f0f15dcedbae464ef4495e4f6 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:10:29 -0700 Subject: [PATCH 1/5] fix(cli): a rejected runtime token says check it, not that it is revoked (TASK-301) After three 401s the poller said the token is likely revoked, so an agent with a good token but a wrong COMMONLY_TOKEN was told the wrong cause. Name the token to check, and only then the revoke-and-reattach path. Regression test fails on the old message. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: rivet Agent-Model: claude/claude-haiku-5-5/default --- cli/__tests__/lib.test.mjs | 21 +++++++++++++++++++++ cli/src/lib/poller.js | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/cli/__tests__/lib.test.mjs b/cli/__tests__/lib.test.mjs index 1c5466f513..86f824a019 100644 --- a/cli/__tests__/lib.test.mjs +++ b/cli/__tests__/lib.test.mjs @@ -524,6 +524,27 @@ describe('poller.js', () => { jest.clearAllMocks(); }); + test('a rejected token stops the poller with a message that says to check the token, not that it is revoked', async () => { + const authError = Object.assign(new Error('unauthorized'), { status: 401 }); + const mockGet = jest.fn().mockRejectedValue(authError); + createClient.mockReturnValue({ get: mockGet, post: jest.fn() }); + + const errors = []; + startPoller({ + instanceUrl: 'http://localhost:5000', + token: 'cm_test', + agentName: 'my-agent', + intervalMs: 1, + onEvent: async () => ({ outcome: 'acknowledged' }), + onError: (err) => errors.push(err), + }); + + await new Promise((r) => setTimeout(r, 50)); + const stopMsg = errors.map((e) => e.message).find((m) => /stopping poller/.test(m)); + expect(stopMsg).toMatch(/Check the token this agent runs with \(COMMONLY_TOKEN/); + expect(stopMsg).not.toMatch(/likely revoked/); + }); + const makeEvent = (id = 'evt-1') => ({ _id: id, type: 'heartbeat', diff --git a/cli/src/lib/poller.js b/cli/src/lib/poller.js index 9c6e2fd982..5c936ba160 100644 --- a/cli/src/lib/poller.js +++ b/cli/src/lib/poller.js @@ -139,7 +139,8 @@ export const startPoller = ({ if (consecutiveAuthErrors >= MAX_AUTH_ERRORS) { onError?.(new Error( `Runtime token rejected ${consecutiveAuthErrors} times in a row — stopping poller. ` - + `The token is likely revoked.`, + + `Check the token this agent runs with (COMMONLY_TOKEN, or the .commonly-env from agent init). ` + + `If it was rotated or revoked, detach and reattach the agent.`, )); running = false; return; From 4ce29fb9b6f27c30b45d02f774d3d6a09e6b6725 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:16:24 -0700 Subject: [PATCH 2/5] fix(cli): name the key this run reads for a rejected token (TASK-301) The poller runs under agent run, which reads the saved token file first and COMMONLY_AGENT_TOKEN only when no file exists. Name those, and the delete-then-rerun step for a rotated key. The test asserts the new names. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: rivet Agent-Model: claude/claude-haiku-5-5/default --- cli/__tests__/lib.test.mjs | 5 ++++- cli/src/lib/poller.js | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/__tests__/lib.test.mjs b/cli/__tests__/lib.test.mjs index 86f824a019..cc753c3a90 100644 --- a/cli/__tests__/lib.test.mjs +++ b/cli/__tests__/lib.test.mjs @@ -541,7 +541,10 @@ describe('poller.js', () => { await new Promise((r) => setTimeout(r, 50)); const stopMsg = errors.map((e) => e.message).find((m) => /stopping poller/.test(m)); - expect(stopMsg).toMatch(/Check the token this agent runs with \(COMMONLY_TOKEN/); + // The names this process reads: the saved token file, then COMMONLY_AGENT_TOKEN. + expect(stopMsg).toMatch(/~\/\.commonly\/tokens\/my-agent\.json/); + expect(stopMsg).toMatch(/COMMONLY_AGENT_TOKEN/); + expect(stopMsg).not.toMatch(/COMMONLY_TOKEN/); expect(stopMsg).not.toMatch(/likely revoked/); }); diff --git a/cli/src/lib/poller.js b/cli/src/lib/poller.js index 5c936ba160..d72e2ef0e2 100644 --- a/cli/src/lib/poller.js +++ b/cli/src/lib/poller.js @@ -139,8 +139,9 @@ export const startPoller = ({ if (consecutiveAuthErrors >= MAX_AUTH_ERRORS) { onError?.(new Error( `Runtime token rejected ${consecutiveAuthErrors} times in a row — stopping poller. ` - + `Check the token this agent runs with (COMMONLY_TOKEN, or the .commonly-env from agent init). ` - + `If it was rotated or revoked, detach and reattach the agent.`, + + `This run takes its key from ~/.commonly/tokens/${agentName}.json when that file exists, ` + + `and only from COMMONLY_AGENT_TOKEN when it does not. ` + + `If the key was rotated or revoked, delete that file and rerun with the new COMMONLY_AGENT_TOKEN.`, )); running = false; return; From fb6764ad1c4a1133e175b7078dcdf244abbb0266 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:50:21 -0700 Subject: [PATCH 3/5] fix(cli): a rotated key is reset with agent detach --force (TASK-301) Match the reset verb the sibling messages use, which also clears the session store that deleting the token file by hand would leave behind. The test pins the detach --force wording. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: rivet Agent-Model: claude/claude-haiku-5-5/default --- cli/__tests__/lib.test.mjs | 2 ++ cli/src/lib/poller.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/__tests__/lib.test.mjs b/cli/__tests__/lib.test.mjs index cc753c3a90..7965be992f 100644 --- a/cli/__tests__/lib.test.mjs +++ b/cli/__tests__/lib.test.mjs @@ -545,6 +545,8 @@ describe('poller.js', () => { expect(stopMsg).toMatch(/~\/\.commonly\/tokens\/my-agent\.json/); expect(stopMsg).toMatch(/COMMONLY_AGENT_TOKEN/); expect(stopMsg).not.toMatch(/COMMONLY_TOKEN/); + // The reset verb the sibling messages use, which also clears the session store. + expect(stopMsg).toMatch(/detach .* --force/); expect(stopMsg).not.toMatch(/likely revoked/); }); diff --git a/cli/src/lib/poller.js b/cli/src/lib/poller.js index d72e2ef0e2..eedbd72608 100644 --- a/cli/src/lib/poller.js +++ b/cli/src/lib/poller.js @@ -141,7 +141,8 @@ export const startPoller = ({ `Runtime token rejected ${consecutiveAuthErrors} times in a row — stopping poller. ` + `This run takes its key from ~/.commonly/tokens/${agentName}.json when that file exists, ` + `and only from COMMONLY_AGENT_TOKEN when it does not. ` - + `If the key was rotated or revoked, delete that file and rerun with the new COMMONLY_AGENT_TOKEN.`, + + `If the key was rotated or revoked, run commonly agent detach ${agentName} --force, ` + + `then re-run with the Connect-page env exports.`, )); running = false; return; From fb445606e2b247fc1425c3f956eb672bb5d9285e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:58:45 -0700 Subject: [PATCH 4/5] fix(cli): the rejected-token message names the sources agent connect reads (TASK-301) The poller's only caller is agent connect, which reads --token, then COMMONLY_TOKEN, then the saved login. Name those, and the rotate step for each. The test pins --token and COMMONLY_TOKEN and asserts that detach and tokens/ are absent. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: rivet Agent-Model: claude/claude-haiku-5-5/default --- cli/__tests__/lib.test.mjs | 12 ++++++------ cli/src/lib/poller.js | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cli/__tests__/lib.test.mjs b/cli/__tests__/lib.test.mjs index 7965be992f..69d7804ae2 100644 --- a/cli/__tests__/lib.test.mjs +++ b/cli/__tests__/lib.test.mjs @@ -541,12 +541,12 @@ describe('poller.js', () => { await new Promise((r) => setTimeout(r, 50)); const stopMsg = errors.map((e) => e.message).find((m) => /stopping poller/.test(m)); - // The names this process reads: the saved token file, then COMMONLY_AGENT_TOKEN. - expect(stopMsg).toMatch(/~\/\.commonly\/tokens\/my-agent\.json/); - expect(stopMsg).toMatch(/COMMONLY_AGENT_TOKEN/); - expect(stopMsg).not.toMatch(/COMMONLY_TOKEN/); - // The reset verb the sibling messages use, which also clears the session store. - expect(stopMsg).toMatch(/detach .* --force/); + // The three sources `agent connect` reads, in order: --token, COMMONLY_TOKEN, saved login. + expect(stopMsg).toMatch(/--token, then COMMONLY_TOKEN, then your saved login/); + expect(stopMsg).toMatch(/commonly login/); + // connect never reads the token file or detach: naming them would send the reader to the wrong place. + expect(stopMsg).not.toMatch(/detach/); + expect(stopMsg).not.toMatch(/tokens\//); expect(stopMsg).not.toMatch(/likely revoked/); }); diff --git a/cli/src/lib/poller.js b/cli/src/lib/poller.js index eedbd72608..edff167033 100644 --- a/cli/src/lib/poller.js +++ b/cli/src/lib/poller.js @@ -139,10 +139,9 @@ export const startPoller = ({ if (consecutiveAuthErrors >= MAX_AUTH_ERRORS) { onError?.(new Error( `Runtime token rejected ${consecutiveAuthErrors} times in a row — stopping poller. ` - + `This run takes its key from ~/.commonly/tokens/${agentName}.json when that file exists, ` - + `and only from COMMONLY_AGENT_TOKEN when it does not. ` - + `If the key was rotated or revoked, run commonly agent detach ${agentName} --force, ` - + `then re-run with the Connect-page env exports.`, + + `This connection takes its key from --token, then COMMONLY_TOKEN, then your saved login. ` + + `If the key was rotated or revoked, re-run with the new key in --token or COMMONLY_TOKEN; ` + + `with neither set, run commonly login.`, )); running = false; return; From d0262b882acb8fa52c011c0d5c69e72d7700a1ed Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:11:08 -0700 Subject: [PATCH 5/5] fix(cli): the login hint names the --instance connect ran with (TASK-301) Bare commonly login refreshes the active profile, so on a non-active instance the hint fixed the wrong login. Pass connect's --instance into the poller and name it in the hint; with none, keep the bare command. Both cases are tested. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: rivet Agent-Model: claude/claude-haiku-5-5/default --- cli/__tests__/lib.test.mjs | 28 ++++++++++++++++++++++++++++ cli/src/commands/agent.js | 1 + cli/src/lib/poller.js | 3 ++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cli/__tests__/lib.test.mjs b/cli/__tests__/lib.test.mjs index 69d7804ae2..cac8172d2f 100644 --- a/cli/__tests__/lib.test.mjs +++ b/cli/__tests__/lib.test.mjs @@ -550,6 +550,34 @@ describe('poller.js', () => { expect(stopMsg).not.toMatch(/likely revoked/); }); + const rejectedMessage = async (loginInstance) => { + const authError = Object.assign(new Error('unauthorized'), { status: 401 }); + createClient.mockReturnValue({ get: jest.fn().mockRejectedValue(authError), post: jest.fn() }); + const errors = []; + startPoller({ + instanceUrl: 'http://localhost:5000', + token: 'cm_test', + agentName: 'my-agent', + intervalMs: 1, + loginInstance, + onEvent: async () => ({ outcome: 'acknowledged' }), + onError: (err) => errors.push(err), + }); + await new Promise((r) => setTimeout(r, 50)); + return errors.map((e) => e.message).find((m) => /stopping poller/.test(m)); + }; + + test('the login hint names the --instance connect ran with, so it refreshes that profile', async () => { + const stopMsg = await rejectedMessage('dev'); + expect(stopMsg).toMatch(/run commonly login --instance dev\./); + }); + + test('with no --instance, the login hint is the bare commonly login', async () => { + const stopMsg = await rejectedMessage(undefined); + expect(stopMsg).toMatch(/run commonly login\./); + expect(stopMsg).not.toMatch(/--instance/); + }); + const makeEvent = (id = 'evt-1') => ({ _id: id, type: 'heartbeat', diff --git a/cli/src/commands/agent.js b/cli/src/commands/agent.js index 01c2ef9c96..d6325b1c65 100644 --- a/cli/src/commands/agent.js +++ b/cli/src/commands/agent.js @@ -3713,6 +3713,7 @@ Docs: agentName: opts.name, instanceId: opts.instanceId, intervalMs: parseInt(opts.interval, 10), + loginInstance: opts.instance, onEvent: async (event) => { const ts = new Date().toTimeString().slice(0, 8); process.stdout.write(`[${ts}] ${event.type.padEnd(18)}`); diff --git a/cli/src/lib/poller.js b/cli/src/lib/poller.js index edff167033..da56a9743d 100644 --- a/cli/src/lib/poller.js +++ b/cli/src/lib/poller.js @@ -36,6 +36,7 @@ export const startPoller = ({ agentName, instanceId = 'default', intervalMs = 5000, + loginInstance, // the --instance the connect ran with, so the login hint names it onEvent, // async (event) => { outcome, content? } onError, // (err) => void }) => { @@ -141,7 +142,7 @@ export const startPoller = ({ `Runtime token rejected ${consecutiveAuthErrors} times in a row — stopping poller. ` + `This connection takes its key from --token, then COMMONLY_TOKEN, then your saved login. ` + `If the key was rotated or revoked, re-run with the new key in --token or COMMONLY_TOKEN; ` - + `with neither set, run commonly login.`, + + `with neither set, run commonly login${loginInstance ? ` --instance ${loginInstance}` : ''}.`, )); running = false; return;