From 3791bc0c7cc3757e8d5d326ebb8f9e991ae3514e Mon Sep 17 00:00:00 2001 From: ayushsingh82 Date: Fri, 21 Aug 2026 17:44:44 +0530 Subject: [PATCH] fix(cli): session not found names the owning profile SESSION_NOT_FOUND said "Session not found: " even when the id existed under a different profile, and `session list` gave no visible signal that it was scoped to the selected profile. Agents omitting --profile on cleanup read this as the session never existed and either retried the same wrong command or inspected unrelated default-profile state. - SessionNotFoundError now looks up the owning profile and, when found, hints the exact `webcmd --profile session close ` retry. - `session list` includes a `profileId` column so scoping is visible in table output, not just implied by the selected profile. Fixes #388 --- docs/troubleshooting.mdx | 2 +- skill-src/webcmd-browser/SKILL.src.md | 6 ++++++ skills/webcmd-browser/SKILL.md | 1 + src/browser/sessions.test.ts | 9 ++++++++- src/browser/sessions.ts | 17 +++++++++++++---- src/cli.ts | 2 +- src/skills.test.ts | 1 + 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx index 2382c2ad..7fcb30dc 100644 --- a/docs/troubleshooting.mdx +++ b/docs/troubleshooting.mdx @@ -48,7 +48,7 @@ Common Session codes: | `SESSION_REQUIRED` | A raw browser command needs a root Session selector. | Run `webcmd session create -f json`, then retry as `webcmd --session browser ...`. | | `INVALID_SESSION_SELECTOR` | The selector is not an opaque Webcmd Session ID. | Use an ID returned by `webcmd session create` or `webcmd session list`. | | `SESSION_SELECTOR_POSITION` | `--session` was placed after the command name. | Move it before the command: `webcmd --session browser ...`. | -| `SESSION_NOT_FOUND` | The selected Session is missing for the current Profile. | Run `webcmd session list -f json`; create a new Session if needed. | +| `SESSION_NOT_FOUND` | The selected Session is missing for the current Profile. Sessions are per profile. | If the Session exists under another profile, the error names it; retry with `webcmd --profile session list` or `... session close `. Otherwise run `webcmd session list -f json` for the current profile; create a new Session if needed. | | `INVALID_SESSION_LIMIT` | `session list --limit` is outside 1-100. | Retry with a limit from 1 to 100. | | `SESSION_BUSY` | Another command is writing in the same Session or site scope. | Wait for the holder to finish; if it is dead, use `webcmd session close --force` as the last resort. | | `SESSION_PAUSED_FOR_HUMAN_HANDOFF` | The Session is waiting for user action such as sign-in. | Finish the action in the browser, then run the returned verifier before retrying. | diff --git a/skill-src/webcmd-browser/SKILL.src.md b/skill-src/webcmd-browser/SKILL.src.md index a1ee6d44..1ba1d934 100644 --- a/skill-src/webcmd-browser/SKILL.src.md +++ b/skill-src/webcmd-browser/SKILL.src.md @@ -272,6 +272,7 @@ Use `run` and inspect `page.frames()`; target the frame by URL/name and keep ifr | `run` times out before returning | Increase `--timeout` only after checking whether the wait condition is wrong. | | Write may have happened before timeout | Take a fresh snapshot before retrying. Avoid duplicate submissions. | | `SESSION_REQUIRED` | Create a Session, then retry with root `--session `. | +| `SESSION_NOT_FOUND` | Pass the same `--profile` used on `session create`. `session list` is per profile; the error names the owning profile when the id exists under another one. | | `SESSION_BUSY` | Wait for the listed holder; if it is dead, `webcmd session close --force` is the last resort. | | `SESSION_PAUSED_FOR_HUMAN_HANDOFF` | Finish the handoff and run the returned verifier before retrying. | | Login wall appears | Use the Authentication and human handoff recipe. | @@ -296,4 +297,9 @@ Author-only. Stripped by litprompt, so it costs the running agent nothing. Append one dated line whenever a correction lands, or whenever an approach is tried and rejected. Record what was tried and why it failed, not just what won. + +- 2026-08-21: `session close`/`list` without `--profile` reported `Session not + found` even when the id existed under another profile, and the list output + did not read as profile-scoped. Name the owning profile in the error hint + and show `profileId` in `session list` output (#388). --> diff --git a/skills/webcmd-browser/SKILL.md b/skills/webcmd-browser/SKILL.md index c9bcfa2c..9890b346 100644 --- a/skills/webcmd-browser/SKILL.md +++ b/skills/webcmd-browser/SKILL.md @@ -272,6 +272,7 @@ Use `run` and inspect `page.frames()`; target the frame by URL/name and keep ifr | `run` times out before returning | Increase `--timeout` only after checking whether the wait condition is wrong. | | Write may have happened before timeout | Take a fresh snapshot before retrying. Avoid duplicate submissions. | | `SESSION_REQUIRED` | Create a Session, then retry with root `--session `. | +| `SESSION_NOT_FOUND` | Pass the same `--profile` used on `session create`. `session list` is per profile; the error names the owning profile when the id exists under another one. | | `SESSION_BUSY` | Wait for the listed holder; if it is dead, `webcmd session close --force` is the last resort. | | `SESSION_PAUSED_FOR_HUMAN_HANDOFF` | Finish the handoff and run the returned verifier before retrying. | | Login wall appears | Use the Authentication and human handoff recipe. | diff --git a/src/browser/sessions.test.ts b/src/browser/sessions.test.ts index 676a677d..f4ea9d0b 100644 --- a/src/browser/sessions.test.ts +++ b/src/browser/sessions.test.ts @@ -43,7 +43,14 @@ describe('LocalBrowserSessionStore', () => { const store = new LocalBrowserSessionStore({ baseDir: tempDir(), idFactory: () => 'session_a' }); const created = store.create('profile_work'); - expect(() => store.require('profile_other', created.id)).toThrowError(expect.objectContaining({ code: 'SESSION_NOT_FOUND' })); + expect(() => store.require('profile_other', created.id)).toThrowError(expect.objectContaining({ + code: 'SESSION_NOT_FOUND', + hint: expect.stringContaining('webcmd --profile profile_work session close'), + })); + expect(() => store.require('profile_missing', 'session_zzzzzzzz-zzzz-4zzz-8zzz-zzzzzzzzzzzz')).toThrowError(expect.objectContaining({ + code: 'SESSION_NOT_FOUND', + hint: expect.stringContaining('Sessions are per profile'), + })); expect(() => store.find('profile_work', 'work')).toThrowError(expect.objectContaining({ code: 'INVALID_SESSION_SELECTOR' })); }); diff --git a/src/browser/sessions.ts b/src/browser/sessions.ts index d046999b..92e1f247 100644 --- a/src/browser/sessions.ts +++ b/src/browser/sessions.ts @@ -30,11 +30,14 @@ type StateFile = { version: 1; sessions: BrowserSessionRecord[] }; const SESSION_RETENTION_MS = 30 * 24 * 60 * 60 * 1000; export class SessionNotFoundError extends CliError { - constructor(sessionId: string, profileId: string) { + constructor(sessionId: string, profileId: string, ownerProfileId?: string) { + const hint = ownerProfileId && ownerProfileId !== profileId + ? `This Session belongs to profile ${ownerProfileId}. Retry with \`webcmd --profile ${ownerProfileId} session close ${sessionId}\`. List with \`webcmd --profile ${ownerProfileId} session list\`.` + : `Sessions are per profile. Run \`webcmd --profile ${profileId} session list\` or \`webcmd profile list\`, then pass the same \`--profile\` used on create.`; super( 'SESSION_NOT_FOUND', `Session not found: ${sessionId}`, - `Run \`webcmd --profile ${profileId} session list\` to choose an existing Session.`, + hint, EXIT_CODES.EMPTY_RESULT, ); } @@ -84,7 +87,10 @@ export class LocalBrowserSessionStore { requireSessionIdShape(id); const state = this.load(); const record = state.sessions.find((row) => row.id === id && row.profileId === profileId); - if (!record) throw new SessionNotFoundError(id, profileId); + if (!record) { + const owner = state.sessions.find((row) => row.id === id); + throw new SessionNotFoundError(id, profileId, owner?.profileId); + } this.touchRecord(state, record); return { ...record }; } @@ -176,7 +182,10 @@ export class LocalBrowserSessionStore { private requireMutable(state: StateFile, profileId: string, sessionId: string): BrowserSessionRecord { requireSessionIdShape(sessionId); const record = state.sessions.find((row) => row.id === sessionId && row.profileId === profileId); - if (!record) throw new SessionNotFoundError(sessionId, profileId); + if (!record) { + const owner = state.sessions.find((row) => row.id === sessionId); + throw new SessionNotFoundError(sessionId, profileId, owner?.profileId); + } return record; } diff --git a/src/cli.ts b/src/cli.ts index 440d76ad..951194f7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -892,7 +892,7 @@ export function createProgram(BUILTIN_CLIS: string, USER_CLIS: string, pluginsDi console.log(`No browser Sessions found for Profile ${profileId}.`); return; } - await renderOutput(output, { fmt, fmtExplicit: outputFormatIsExplicit(command), columns: ['id', 'kind', 'runtimeState', 'handoff'] }); + await renderOutput(output, { fmt, fmtExplicit: outputFormatIsExplicit(command), columns: ['id', 'kind', 'profileId', 'runtimeState', 'handoff'] }); }); const sessionCloseCmd = addOutputFormatOption(sessionCmd diff --git a/src/skills.test.ts b/src/skills.test.ts index b4f04d7e..7a50239e 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -292,6 +292,7 @@ describe('webcmd skills content', () => { expect(browser).toMatch(/Profiles are cookie jars[\s\S]{0,180}sessions are browser workspaces\/windows/i); expect(browser).toMatch(/Parallel agents use separate sessions/i); expect(browser).toContain('SESSION_BUSY'); + expect(browser).toContain('SESSION_NOT_FOUND'); expect(browser).toContain('SESSION_PAUSED_FOR_HUMAN_HANDOFF'); expect(browser).toContain('webcmd session close --force'); for (const skill of [usage, browser, autofix]) {