Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions skill-src/webcmd-browser/SKILL.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,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-id>`. |
| `SESSION_NOT_FOUND` | Pass the same `--profile` used on `session create`. `session list` is per profile. |
| `SESSION_BUSY` | Wait for the listed holder; if it is dead, `webcmd session close <session-id> --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. |
Expand All @@ -294,4 +295,7 @@ 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-20: `session close` without `--profile` said Session not found and
listed the default profile. Name the owning profile when the id exists elsewhere.
-->
1 change: 1 addition & 0 deletions skills/webcmd-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,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-id>`. |
| `SESSION_NOT_FOUND` | Pass the same `--profile` used on `session create`. `session list` is per profile. |
| `SESSION_BUSY` | Wait for the listed holder; if it is dead, `webcmd session close <session-id> --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. |
Expand Down
9 changes: 8 additions & 1 deletion src/browser/sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }));
});

Expand Down
17 changes: 13 additions & 4 deletions src/browser/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,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: command.getOptionValueSource('format') === 'cli', columns: ['id', 'kind', 'runtimeState', 'handoff'] });
await renderOutput(output, { fmt, fmtExplicit: command.getOptionValueSource('format') === 'cli', columns: ['id', 'kind', 'profileId', 'runtimeState', 'handoff'] });
});

sessionCmd
Expand Down
1 change: 1 addition & 0 deletions src/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,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 <session-id> --force');
for (const skill of [usage, browser, autofix]) {
Expand Down
Loading