From 380fdbf1297dcf30f92f4a1b20e4cd5d74aa18a5 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Tue, 10 Mar 2026 06:23:50 +0530 Subject: [PATCH 1/5] Include available sessions in error when --resume gets invalid ID --- docs/bug-fix-tracking-issue.md | 135 +++++++++++++++++++++++ docs/issue-resume-invalid-id-fallback.md | 94 ++++++++++++++++ packages/cli/src/utils/sessionUtils.ts | 47 +++++++- 3 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 docs/bug-fix-tracking-issue.md create mode 100644 docs/issue-resume-invalid-id-fallback.md diff --git a/docs/bug-fix-tracking-issue.md b/docs/bug-fix-tracking-issue.md new file mode 100644 index 00000000000..9f39d2c439f --- /dev/null +++ b/docs/bug-fix-tracking-issue.md @@ -0,0 +1,135 @@ +# Bug fix tracking issue + +Use the content below to open a single GitHub issue (e.g. **"Fix known bugs: +skipped tests, settings merge, context compression, and error handling"**) or to +track bug-fix work locally. Each section can also be split into separate issues +if preferred. + +--- + +## Title (for GitHub) + +**Fix known bugs: skipped tests, settings merge, context compression, and error +handling** + +--- + +## Description + +This issue tracks a set of bugs identified in the gemini-cli codebase (from +TODOs, skipped tests, and documented workarounds). Fixing them will improve +reliability, test coverage, and maintainability. + +--- + +## 1. Integration / E2E test bugs (re-enable skipped tests) + +These tests are currently skipped due to flakiness or environment issues. Fixing +the underlying causes would allow re-enabling them. + +| Location | Issue / TODO | Description | +| -------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `integration-tests/run_shell_command.test.ts` | TODO(#11062) | **"should combine multiple --allowed-tools flags"** – Un-skip once reliable (e.g. hard-coded expectations). | +| `integration-tests/run_shell_command.test.ts` | TODO(#11966) | **"should reject chained commands when only the first segment is allowlisted in non-interactive mode"** – Deflake and re-enable once race is resolved. | +| `integration-tests/extensions-reload.test.ts` | TODO(#14527) | **Extension reload test** – Fails in Linux non-sandbox e2e and in sandbox mode (can't check local extension updates). Re-enable once fixed. | +| `integration-tests/context-compress-interactive.test.ts` | Inline TODO | **"should handle compression failure on token inflation"** – Context compression is broken: it doesn’t include system instructions or tool counts, so it thinks compression is beneficial when it isn’t. | +| `integration-tests/stdin-context.test.ts` | `describe.skip` | **stdin context** – Fails in sandbox mode (Docker/Podman). | +| `integration-tests/simple-mcp-server.test.ts` | `describe.skip` | **simple-mcp-server** – Entire describe is skipped. | +| `integration-tests/read_many_files.test.ts` | `it.skip` | **"should be able to read multiple files"**. | +| `integration-tests/file-system.test.ts` | `it.skip` | **"should replace multiple instances of a string"**. | +| `integration-tests/replace.test.ts` | Multiple `it.skip` | **"should handle $ literally when replacing..."**, **"should insert a multi-line block..."**, **"should delete a block of text"**. | +| `packages/core/src/core/client.test.ts` | `it.skip` | **"will not attempt to compress context after a failure"**. | +| `packages/cli/src/config/config.test.ts` | `it.skip` | **"should combine and resolve paths from settings and CLI arguments"**. | + +**Suggested scope:** Start with one or two tests (e.g. #11062 or #11966), make +them stable, then un-skip. Document environment assumptions. + +--- + +## 2. Settings / config merge bug + +**Location:** `packages/core/src/config/config.ts` (around lines 989–1008) + +**Problem:** Settings loading does not merge the default generation config with +the user’s settings. If the user provides any `generation` settings (e.g. only +`overrides`), default `aliases` are lost. A manual merge hack restores default +aliases/overrides when missing. + +**References:** + +- Comment: `HACK: The settings loading logic doesn't currently merge...` +- `TODO(12593): Fix the settings loading logic to properly merge defaults and remove this hack.` + +**Suggested fix:** Implement proper default merging in the settings loading +layer so this hack can be removed. + +--- + +## 3. Context compression logic bug + +**Location:** `integration-tests/context-compress-interactive.test.ts` (lines +53–56) and related compression logic in core. + +**Problem:** Context compression does not include system instructions or tool +counts when deciding if compression is beneficial, so it can incorrectly treat +compression as beneficial when it isn’t (token inflation case). + +**Suggested fix:** Include system instructions and tool counts in the +compression benefit calculation and re-enable the skipped test **"should handle +compression failure on token inflation"**. + +--- + +## 4. Error handling and edge cases + +| Location | Description | +| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `packages/cli/src/ui/commands/types.ts` | `TODO(abhipatel12): Ensure that config is never null` – Avoid null `config` in command context. | +| `packages/cli/src/config/extension-manager.ts` | `TODO: Gracefully handle this call failing, we should back up the old...` – Handle extension manager call failures and back up previous state. | +| `packages/core/src/ide/ide-client.ts` | `TODO(#3487): use the CLI version here` (two places) – Use CLI version for consistency. | +| `packages/core/src/agents/local-executor.ts` | `TODO(joshualitt): This try / catch is inconsistent with the routing...` – Align error handling with routing. | + +--- + +## 5. Platform-specific tests (optional) + +These are skipped or conditional on platform; fixing them would improve +cross-platform coverage: + +- **Windows:** `packages/cli/src/utils/skillUtils.test.ts` – TODO issue 19388: + enable linkSkill tests on Windows. +- **Windows:** `packages/cli/src/config/trustedFolders.test.ts` – TODO issue + 19387: enable symlink tests on Windows. + +--- + +## Acceptance criteria (example) + +- [ ] At least one of the skipped integration tests (sections 1 or 5) is fixed + and re-enabled, with a short comment linking to this issue or the specific + TODO. +- [ ] OR: Settings loading is fixed (section 2) and the config merge hack in + `config.ts` is removed, with TODO(12593) resolved. +- [ ] OR: Context compression (section 3) is fixed and the skipped test + **"should handle compression failure on token inflation"** is re-enabled. +- [ ] OR: One of the error-handling TODOs in section 4 is addressed (config null + safety, extension-manager failure handling, or IDE client version). +- [ ] All changes pass `npm run preflight` and any new/updated tests are stable + in CI where applicable. + +--- + +## How to use this + +1. **Single issue:** Copy the **Title** and **Description** plus the sections + you care about into a new + [GitHub issue](https://github.com/google-gemini/gemini-cli/issues/new) (e.g. + using the Bug Report template and pasting this into "What happened?" / + "Additional context" or as the main body). +2. **Multiple issues:** Create one issue per section (or per test) and link them + to a parent "Bug fix” epic or project board. +3. **Local tracking:** Use this file as a checklist while working; open a PR + that references the GitHub issue(s) you created. + +Ensure any PR is linked to the corresponding issue(s) per +[CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/docs/issue-resume-invalid-id-fallback.md b/docs/issue-resume-invalid-id-fallback.md new file mode 100644 index 00000000000..fe4ddb07d65 --- /dev/null +++ b/docs/issue-resume-invalid-id-fallback.md @@ -0,0 +1,94 @@ +# Issue: Include available sessions in error when `--resume` gets invalid ID + +Use this content to open a GitHub issue as a follow-up to +[PR #21429](https://github.com/google-gemini/gemini-cli/pull/21429). + +--- + +## Title + +**Include available sessions in error output when `--resume` is used with an +invalid session ID** + +--- + +## Description + +### Context + +[PR #21429](https://github.com/google-gemini/gemini-cli/pull/21429) fixed the +case where `gemini -r` / `--resume` is used in a directory with **no** previous +sessions: the CLI now starts a fresh session and shows a startup warning instead +of crashing. + +When the user passes an **invalid** session identifier (e.g. `--resume 99` when +only 5 sessions exist, or a typo'd UUID), the CLI correctly exits with an error +and tells the user to run `--list-sessions` to see available sessions. Exiting +is the right behavior here — the user asked to resume a specific session, so we +shouldn't start a fresh one instead. + +### Problem + +The error message says: + +```text +Invalid session identifier "99". + Use --list-sessions to see available sessions, then use --resume {number}, --resume {uuid}, or --resume latest. +``` + +The user then has to run a **second** command (`gemini --list-sessions`) to see +what's actually available, copy an index or UUID, and run `gemini --resume` +again. That's an extra round-trip. + +### Proposed improvement + +When `SessionError` with code `INVALID_SESSION_IDENTIFIER` is thrown, we already +have the list of available sessions in memory (we're in the branch where +sessions exist but the identifier didn't match). Use that to **include the +available sessions** (or a compact summary) directly in the error output before +exiting. + +For example, the output could look like: + +```text +Error resuming session: Invalid session identifier "99". + +Available sessions for this project: + 1. Fix bug in auth (2 days ago) + 2. Refactor database schema (5 hours ago) + 3. Update documentation (Just now) + +Use --resume 1, --resume 2, --resume 3, or --resume latest. +``` + +So the user can correct their command in one go, without running +`--list-sessions` separately. + +### Benefits + +- Same behavior: we still exit when the session ID is invalid (no "start fresh" + for invalid ID). +- Better UX: one command gives both the error and the list of valid options. +- Reuses data we already have when throwing the error. + +### Implementation notes + +- `SessionSelector.findSession()` / `resolveSession()` has access to the + sessions list when it throws + `SessionError.invalidSessionIdentifier(identifier)`. Options: + - Extend `SessionError.invalidSessionIdentifier(identifier, sessions?)` to + optionally accept the session list and format it into the message, or + - Add an optional `availableSessions` property to `SessionError` and format + the list in `gemini.tsx` when handling the error. +- Keep the output compact (e.g. index, display name, relative time) so it + doesn't flood the terminal for projects with many sessions; consider a limit + (e.g. show at most 10) with "Run --list-sessions for more" if needed. +- Ensure `--list-sessions` behavior and output format stay the single source of + truth for the full list; this is just a convenience snippet in the error. + +--- + +## Labels (suggested) + +`help-wanted`, `good first issue` (focused UX improvement, no behavior change to +exit vs continue). diff --git a/packages/cli/src/utils/sessionUtils.ts b/packages/cli/src/utils/sessionUtils.ts index 3aa0131ac20..1034f9ec7c9 100644 --- a/packages/cli/src/utils/sessionUtils.ts +++ b/packages/cli/src/utils/sessionUtils.ts @@ -56,8 +56,51 @@ export class SessionError extends Error { /** * Creates an error for when a session identifier is invalid. + * + * When `sessions` is provided, a compact summary of available sessions is + * included in the error message so the user can correct their command without + * needing to run --list-sessions separately. */ - static invalidSessionIdentifier(identifier: string): SessionError { + static invalidSessionIdentifier( + identifier: string, + sessions?: SessionInfo[], + ): SessionError { + const MAX_DISPLAY = 10; + + if (sessions && sessions.length > 0) { + // Sort oldest-first (consistent with --list-sessions numbering) + const sorted = [...sessions].sort( + (a, b) => + new Date(a.startTime).getTime() - new Date(b.startTime).getTime(), + ); + + const displaySessions = sorted.slice(0, MAX_DISPLAY); + const hasMore = sorted.length > MAX_DISPLAY; + + const sessionLines = displaySessions + .map((s, i) => { + const title = + s.displayName.length > 60 + ? s.displayName.slice(0, 57) + '...' + : s.displayName; + return ` ${i + 1}. ${title} (${formatRelativeTime(s.lastUpdated)})`; + }) + .join('\n'); + + const moreNote = hasMore + ? `\n Run --list-sessions for the full list.` + : ''; + + const indices = displaySessions + .map((_, i) => `--resume ${i + 1}`) + .join(', '); + + return new SessionError( + 'INVALID_SESSION_IDENTIFIER', + `Invalid session identifier "${identifier}".\n\nAvailable sessions for this project:\n${sessionLines}${moreNote}\n\nUse ${indices}, or --resume latest.`, + ); + } + return new SessionError( 'INVALID_SESSION_IDENTIFIER', `Invalid session identifier "${identifier}".\n Use --list-sessions to see available sessions, then use --resume {number}, --resume {uuid}, or --resume latest.`, @@ -447,7 +490,7 @@ export class SessionSelector { return sortedSessions[index - 1]; } - throw SessionError.invalidSessionIdentifier(identifier); + throw SessionError.invalidSessionIdentifier(identifier, sortedSessions); } /** From 323eb6141893ad36e0eb3e55850637adfa3b3765 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Tue, 10 Mar 2026 06:33:30 +0530 Subject: [PATCH 2/5] test(session): add tests for invalid session identifier error message --- packages/cli/src/utils/sessionUtils.test.ts | 218 ++++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/packages/cli/src/utils/sessionUtils.test.ts b/packages/cli/src/utils/sessionUtils.test.ts index bcf7c19dfe6..352590c4c82 100644 --- a/packages/cli/src/utils/sessionUtils.test.ts +++ b/packages/cli/src/utils/sessionUtils.test.ts @@ -765,3 +765,221 @@ describe('formatRelativeTime', () => { expect(formatRelativeTime(thirtySecondsAgo.toISOString())).toBe('Just now'); }); }); + +describe('SessionError.invalidSessionIdentifier', () => { + it('returns fallback message when no sessions are provided', () => { + const error = SessionError.invalidSessionIdentifier('bad-id'); + expect(error.code).toBe('INVALID_SESSION_IDENTIFIER'); + expect(error.message).toContain('"bad-id"'); + expect(error.message).toContain('--list-sessions'); + }); + + it('includes compact session list in message when sessions are provided', () => { + const sessions = [ + { + id: 'uuid-1', + displayName: 'Fix auth bug', + lastUpdated: '2024-01-01T10:00:00.000Z', + startTime: '2024-01-01T09:00:00.000Z', + index: 1, + }, + { + id: 'uuid-2', + displayName: 'Refactor database', + lastUpdated: '2024-01-02T10:00:00.000Z', + startTime: '2024-01-02T09:00:00.000Z', + index: 2, + }, + ] as Array; + + const error = SessionError.invalidSessionIdentifier('99', sessions); + expect(error.code).toBe('INVALID_SESSION_IDENTIFIER'); + expect(error.message).toContain('"99"'); + expect(error.message).toContain('Fix auth bug'); + expect(error.message).toContain('Refactor database'); + expect(error.message).toContain('--resume 1'); + expect(error.message).toContain('--resume 2'); + expect(error.message).toContain('--resume latest'); + // Should NOT include the generic --list-sessions redirect + expect(error.message).not.toContain( + 'Use --list-sessions to see available sessions', + ); + }); + + it('sorts sessions oldest-first regardless of input order', () => { + const sessions = [ + { + id: 'uuid-newer', + displayName: 'Newer session', + lastUpdated: '2024-01-02T10:00:00.000Z', + startTime: '2024-01-02T09:00:00.000Z', + index: 2, + }, + { + id: 'uuid-older', + displayName: 'Older session', + lastUpdated: '2024-01-01T10:00:00.000Z', + startTime: '2024-01-01T09:00:00.000Z', + index: 1, + }, + ] as Array; + + const error = SessionError.invalidSessionIdentifier('bad', sessions); + const olderPos = error.message.indexOf('Older session'); + const newerPos = error.message.indexOf('Newer session'); + expect(olderPos).toBeLessThan(newerPos); + // Older session should be index 1, newer should be index 2 + expect(error.message).toMatch(/1\. Older session/); + expect(error.message).toMatch(/2\. Newer session/); + }); + + it('truncates display names longer than 60 characters', () => { + const longName = 'A'.repeat(80); + const sessions = [ + { + id: 'uuid-1', + displayName: longName, + lastUpdated: '2024-01-01T10:00:00.000Z', + startTime: '2024-01-01T09:00:00.000Z', + index: 1, + }, + ] as Array; + + const error = SessionError.invalidSessionIdentifier('bad', sessions); + expect(error.message).toContain('A'.repeat(57) + '...'); + expect(error.message).not.toContain(longName); + }); + + it('appends "Run --list-sessions for the full list." when more than 10 sessions exist', () => { + const sessions = Array.from({ length: 11 }, (_, i) => ({ + id: `uuid-${i}`, + displayName: `Session ${i + 1}`, + lastUpdated: `2024-01-${String(i + 1).padStart(2, '0')}T10:00:00.000Z`, + startTime: `2024-01-${String(i + 1).padStart(2, '0')}T09:00:00.000Z`, + index: i + 1, + })) as Array; + + const error = SessionError.invalidSessionIdentifier('bad', sessions); + expect(error.message).toContain('Run --list-sessions for the full list.'); + // Only 10 sessions should appear in the list + expect(error.message).toContain('--resume 10'); + expect(error.message).not.toContain('--resume 11'); + }); + + it('does not append overflow note when sessions are exactly 10', () => { + const sessions = Array.from({ length: 10 }, (_, i) => ({ + id: `uuid-${i}`, + displayName: `Session ${i + 1}`, + lastUpdated: `2024-01-${String(i + 1).padStart(2, '0')}T10:00:00.000Z`, + startTime: `2024-01-${String(i + 1).padStart(2, '0')}T09:00:00.000Z`, + index: i + 1, + })) as Array; + + const error = SessionError.invalidSessionIdentifier('bad', sessions); + expect(error.message).not.toContain( + 'Run --list-sessions for the full list.', + ); + }); +}); + +describe('SessionSelector.findSession error message', () => { + let tmpDir: string; + let config: Config; + + beforeEach(async () => { + tmpDir = path.join(process.cwd(), '.tmp-test-find-session'); + await fs.mkdir(tmpDir, { recursive: true }); + + config = { + storage: { + getProjectTempDir: () => tmpDir, + }, + getSessionId: () => 'current-session-id', + } as Partial as Config; + }); + + afterEach(async () => { + try { + await fs.rm(tmpDir, { recursive: true, force: true }); + } catch (_error) { + // Ignore cleanup errors + } + }); + + it('includes available sessions in error message for invalid numeric index', async () => { + const sessionId = randomUUID(); + const chatsDir = path.join(tmpDir, 'chats'); + await fs.mkdir(chatsDir, { recursive: true }); + + await fs.writeFile( + path.join( + chatsDir, + `${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId.slice(0, 8)}.json`, + ), + JSON.stringify({ + sessionId, + projectHash: 'test-hash', + startTime: '2024-01-01T10:00:00.000Z', + lastUpdated: '2024-01-01T10:30:00.000Z', + messages: [ + { + type: 'user', + content: 'My only session', + id: 'msg1', + timestamp: '2024-01-01T10:00:00.000Z', + }, + ], + }), + ); + + const sessionSelector = new SessionSelector(config); + + const error = await sessionSelector + .resolveSession('99') + .catch((e: unknown) => e); + + expect(error).toBeInstanceOf(SessionError); + expect((error as SessionError).code).toBe('INVALID_SESSION_IDENTIFIER'); + expect((error as SessionError).message).toContain('"99"'); + expect((error as SessionError).message).toContain('My only session'); + expect((error as SessionError).message).toContain('--resume 1'); + expect((error as SessionError).message).toContain('--resume latest'); + }); + + it('includes available sessions in error message for invalid string identifier', async () => { + const sessionId = randomUUID(); + const chatsDir = path.join(tmpDir, 'chats'); + await fs.mkdir(chatsDir, { recursive: true }); + + await fs.writeFile( + path.join( + chatsDir, + `${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId.slice(0, 8)}.json`, + ), + JSON.stringify({ + sessionId, + projectHash: 'test-hash', + startTime: '2024-01-01T10:00:00.000Z', + lastUpdated: '2024-01-01T10:30:00.000Z', + messages: [ + { + type: 'user', + content: 'My only session', + id: 'msg1', + timestamp: '2024-01-01T10:00:00.000Z', + }, + ], + }), + ); + + const sessionSelector = new SessionSelector(config); + + const error = await sessionSelector + .resolveSession('not-a-valid-uuid') + .catch((e: unknown) => e); + + expect(error).toBeInstanceOf(SessionError); + expect((error as SessionError).message).toContain('"not-a-valid-uuid"'); + expect((error as SessionError).message).toContain('My only session'); + }); +}); From 594e493460353671c3cf735505d72cf4cfe518fd Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Tue, 10 Mar 2026 06:39:50 +0530 Subject: [PATCH 3/5] Omit issue description files --- docs/bug-fix-tracking-issue.md | 135 ----------------------- docs/issue-resume-invalid-id-fallback.md | 94 ---------------- 2 files changed, 229 deletions(-) delete mode 100644 docs/bug-fix-tracking-issue.md delete mode 100644 docs/issue-resume-invalid-id-fallback.md diff --git a/docs/bug-fix-tracking-issue.md b/docs/bug-fix-tracking-issue.md deleted file mode 100644 index 9f39d2c439f..00000000000 --- a/docs/bug-fix-tracking-issue.md +++ /dev/null @@ -1,135 +0,0 @@ -# Bug fix tracking issue - -Use the content below to open a single GitHub issue (e.g. **"Fix known bugs: -skipped tests, settings merge, context compression, and error handling"**) or to -track bug-fix work locally. Each section can also be split into separate issues -if preferred. - ---- - -## Title (for GitHub) - -**Fix known bugs: skipped tests, settings merge, context compression, and error -handling** - ---- - -## Description - -This issue tracks a set of bugs identified in the gemini-cli codebase (from -TODOs, skipped tests, and documented workarounds). Fixing them will improve -reliability, test coverage, and maintainability. - ---- - -## 1. Integration / E2E test bugs (re-enable skipped tests) - -These tests are currently skipped due to flakiness or environment issues. Fixing -the underlying causes would allow re-enabling them. - -| Location | Issue / TODO | Description | -| -------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `integration-tests/run_shell_command.test.ts` | TODO(#11062) | **"should combine multiple --allowed-tools flags"** – Un-skip once reliable (e.g. hard-coded expectations). | -| `integration-tests/run_shell_command.test.ts` | TODO(#11966) | **"should reject chained commands when only the first segment is allowlisted in non-interactive mode"** – Deflake and re-enable once race is resolved. | -| `integration-tests/extensions-reload.test.ts` | TODO(#14527) | **Extension reload test** – Fails in Linux non-sandbox e2e and in sandbox mode (can't check local extension updates). Re-enable once fixed. | -| `integration-tests/context-compress-interactive.test.ts` | Inline TODO | **"should handle compression failure on token inflation"** – Context compression is broken: it doesn’t include system instructions or tool counts, so it thinks compression is beneficial when it isn’t. | -| `integration-tests/stdin-context.test.ts` | `describe.skip` | **stdin context** – Fails in sandbox mode (Docker/Podman). | -| `integration-tests/simple-mcp-server.test.ts` | `describe.skip` | **simple-mcp-server** – Entire describe is skipped. | -| `integration-tests/read_many_files.test.ts` | `it.skip` | **"should be able to read multiple files"**. | -| `integration-tests/file-system.test.ts` | `it.skip` | **"should replace multiple instances of a string"**. | -| `integration-tests/replace.test.ts` | Multiple `it.skip` | **"should handle $ literally when replacing..."**, **"should insert a multi-line block..."**, **"should delete a block of text"**. | -| `packages/core/src/core/client.test.ts` | `it.skip` | **"will not attempt to compress context after a failure"**. | -| `packages/cli/src/config/config.test.ts` | `it.skip` | **"should combine and resolve paths from settings and CLI arguments"**. | - -**Suggested scope:** Start with one or two tests (e.g. #11062 or #11966), make -them stable, then un-skip. Document environment assumptions. - ---- - -## 2. Settings / config merge bug - -**Location:** `packages/core/src/config/config.ts` (around lines 989–1008) - -**Problem:** Settings loading does not merge the default generation config with -the user’s settings. If the user provides any `generation` settings (e.g. only -`overrides`), default `aliases` are lost. A manual merge hack restores default -aliases/overrides when missing. - -**References:** - -- Comment: `HACK: The settings loading logic doesn't currently merge...` -- `TODO(12593): Fix the settings loading logic to properly merge defaults and remove this hack.` - -**Suggested fix:** Implement proper default merging in the settings loading -layer so this hack can be removed. - ---- - -## 3. Context compression logic bug - -**Location:** `integration-tests/context-compress-interactive.test.ts` (lines -53–56) and related compression logic in core. - -**Problem:** Context compression does not include system instructions or tool -counts when deciding if compression is beneficial, so it can incorrectly treat -compression as beneficial when it isn’t (token inflation case). - -**Suggested fix:** Include system instructions and tool counts in the -compression benefit calculation and re-enable the skipped test **"should handle -compression failure on token inflation"**. - ---- - -## 4. Error handling and edge cases - -| Location | Description | -| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `packages/cli/src/ui/commands/types.ts` | `TODO(abhipatel12): Ensure that config is never null` – Avoid null `config` in command context. | -| `packages/cli/src/config/extension-manager.ts` | `TODO: Gracefully handle this call failing, we should back up the old...` – Handle extension manager call failures and back up previous state. | -| `packages/core/src/ide/ide-client.ts` | `TODO(#3487): use the CLI version here` (two places) – Use CLI version for consistency. | -| `packages/core/src/agents/local-executor.ts` | `TODO(joshualitt): This try / catch is inconsistent with the routing...` – Align error handling with routing. | - ---- - -## 5. Platform-specific tests (optional) - -These are skipped or conditional on platform; fixing them would improve -cross-platform coverage: - -- **Windows:** `packages/cli/src/utils/skillUtils.test.ts` – TODO issue 19388: - enable linkSkill tests on Windows. -- **Windows:** `packages/cli/src/config/trustedFolders.test.ts` – TODO issue - 19387: enable symlink tests on Windows. - ---- - -## Acceptance criteria (example) - -- [ ] At least one of the skipped integration tests (sections 1 or 5) is fixed - and re-enabled, with a short comment linking to this issue or the specific - TODO. -- [ ] OR: Settings loading is fixed (section 2) and the config merge hack in - `config.ts` is removed, with TODO(12593) resolved. -- [ ] OR: Context compression (section 3) is fixed and the skipped test - **"should handle compression failure on token inflation"** is re-enabled. -- [ ] OR: One of the error-handling TODOs in section 4 is addressed (config null - safety, extension-manager failure handling, or IDE client version). -- [ ] All changes pass `npm run preflight` and any new/updated tests are stable - in CI where applicable. - ---- - -## How to use this - -1. **Single issue:** Copy the **Title** and **Description** plus the sections - you care about into a new - [GitHub issue](https://github.com/google-gemini/gemini-cli/issues/new) (e.g. - using the Bug Report template and pasting this into "What happened?" / - "Additional context" or as the main body). -2. **Multiple issues:** Create one issue per section (or per test) and link them - to a parent "Bug fix” epic or project board. -3. **Local tracking:** Use this file as a checklist while working; open a PR - that references the GitHub issue(s) you created. - -Ensure any PR is linked to the corresponding issue(s) per -[CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/docs/issue-resume-invalid-id-fallback.md b/docs/issue-resume-invalid-id-fallback.md deleted file mode 100644 index fe4ddb07d65..00000000000 --- a/docs/issue-resume-invalid-id-fallback.md +++ /dev/null @@ -1,94 +0,0 @@ -# Issue: Include available sessions in error when `--resume` gets invalid ID - -Use this content to open a GitHub issue as a follow-up to -[PR #21429](https://github.com/google-gemini/gemini-cli/pull/21429). - ---- - -## Title - -**Include available sessions in error output when `--resume` is used with an -invalid session ID** - ---- - -## Description - -### Context - -[PR #21429](https://github.com/google-gemini/gemini-cli/pull/21429) fixed the -case where `gemini -r` / `--resume` is used in a directory with **no** previous -sessions: the CLI now starts a fresh session and shows a startup warning instead -of crashing. - -When the user passes an **invalid** session identifier (e.g. `--resume 99` when -only 5 sessions exist, or a typo'd UUID), the CLI correctly exits with an error -and tells the user to run `--list-sessions` to see available sessions. Exiting -is the right behavior here — the user asked to resume a specific session, so we -shouldn't start a fresh one instead. - -### Problem - -The error message says: - -```text -Invalid session identifier "99". - Use --list-sessions to see available sessions, then use --resume {number}, --resume {uuid}, or --resume latest. -``` - -The user then has to run a **second** command (`gemini --list-sessions`) to see -what's actually available, copy an index or UUID, and run `gemini --resume` -again. That's an extra round-trip. - -### Proposed improvement - -When `SessionError` with code `INVALID_SESSION_IDENTIFIER` is thrown, we already -have the list of available sessions in memory (we're in the branch where -sessions exist but the identifier didn't match). Use that to **include the -available sessions** (or a compact summary) directly in the error output before -exiting. - -For example, the output could look like: - -```text -Error resuming session: Invalid session identifier "99". - -Available sessions for this project: - 1. Fix bug in auth (2 days ago) - 2. Refactor database schema (5 hours ago) - 3. Update documentation (Just now) - -Use --resume 1, --resume 2, --resume 3, or --resume latest. -``` - -So the user can correct their command in one go, without running -`--list-sessions` separately. - -### Benefits - -- Same behavior: we still exit when the session ID is invalid (no "start fresh" - for invalid ID). -- Better UX: one command gives both the error and the list of valid options. -- Reuses data we already have when throwing the error. - -### Implementation notes - -- `SessionSelector.findSession()` / `resolveSession()` has access to the - sessions list when it throws - `SessionError.invalidSessionIdentifier(identifier)`. Options: - - Extend `SessionError.invalidSessionIdentifier(identifier, sessions?)` to - optionally accept the session list and format it into the message, or - - Add an optional `availableSessions` property to `SessionError` and format - the list in `gemini.tsx` when handling the error. -- Keep the output compact (e.g. index, display name, relative time) so it - doesn't flood the terminal for projects with many sessions; consider a limit - (e.g. show at most 10) with "Run --list-sessions for more" if needed. -- Ensure `--list-sessions` behavior and output format stay the single source of - truth for the full list; this is just a convenience snippet in the error. - ---- - -## Labels (suggested) - -`help-wanted`, `good first issue` (focused UX improvement, no behavior change to -exit vs continue). From db6c5d2f3b451d5b118a0dcbc87f42b502921208 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Tue, 10 Mar 2026 06:54:06 +0530 Subject: [PATCH 4/5] fix(session): use cpLen/cpSlice for Unicode-safe display name truncation Replace .length/.slice() with grapheme-aware cpLen/cpSlice from textUtils to avoid splitting multi-byte characters (e.g. emojis) when truncating session display names in the --resume error message. Adds a test case with emoji-only display names to cover this path. --- packages/cli/src/utils/sessionUtils.test.ts | 22 +++++++++++++++++++++ packages/cli/src/utils/sessionUtils.ts | 10 +++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/utils/sessionUtils.test.ts b/packages/cli/src/utils/sessionUtils.test.ts index 352590c4c82..6f1833f9aa8 100644 --- a/packages/cli/src/utils/sessionUtils.test.ts +++ b/packages/cli/src/utils/sessionUtils.test.ts @@ -850,6 +850,28 @@ describe('SessionError.invalidSessionIdentifier', () => { expect(error.message).not.toContain(longName); }); + it('truncates display names with multi-byte Unicode characters without splitting them', () => { + // Each emoji is 2 UTF-16 code units but 1 grapheme cluster. + // Naive .slice() would split at a surrogate pair boundary; cpSlice must not. + const emoji = '😀'; + const longName = emoji.repeat(80); // 80 grapheme clusters, 160 UTF-16 code units + const sessions = [ + { + id: 'uuid-1', + displayName: longName, + lastUpdated: '2024-01-01T10:00:00.000Z', + startTime: '2024-01-01T09:00:00.000Z', + index: 1, + }, + ] as Array; + + const error = SessionError.invalidSessionIdentifier('bad', sessions); + // Should end with exactly 57 emojis followed by '...' + expect(error.message).toContain(emoji.repeat(57) + '...'); + // Must not contain the full un-truncated name + expect(error.message).not.toContain(longName); + }); + it('appends "Run --list-sessions for the full list." when more than 10 sessions exist', () => { const sessions = Array.from({ length: 11 }, (_, i) => ({ id: `uuid-${i}`, diff --git a/packages/cli/src/utils/sessionUtils.ts b/packages/cli/src/utils/sessionUtils.ts index 1034f9ec7c9..a2204f4388c 100644 --- a/packages/cli/src/utils/sessionUtils.ts +++ b/packages/cli/src/utils/sessionUtils.ts @@ -15,7 +15,11 @@ import { } from '@google/gemini-cli-core'; import * as fs from 'node:fs/promises'; import path from 'node:path'; -import { stripUnsafeCharacters } from '../ui/utils/textUtils.js'; +import { + cpLen, + cpSlice, + stripUnsafeCharacters, +} from '../ui/utils/textUtils.js'; import { MessageType, type HistoryItemWithoutId } from '../ui/types.js'; /** @@ -80,8 +84,8 @@ export class SessionError extends Error { const sessionLines = displaySessions .map((s, i) => { const title = - s.displayName.length > 60 - ? s.displayName.slice(0, 57) + '...' + cpLen(s.displayName) > 60 + ? cpSlice(s.displayName, 0, 57) + '...' : s.displayName; return ` ${i + 1}. ${title} (${formatRelativeTime(s.lastUpdated)})`; }) From 18ea580b1b57fbf80718fea950faae1c0b15e3eb Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Tue, 10 Mar 2026 07:18:45 +0530 Subject: [PATCH 5/5] fix(session): show most-recent sessions in --resume error, clean up test imports --- packages/cli/src/utils/sessionUtils.test.ts | 19 ++++++++++--------- packages/cli/src/utils/sessionUtils.ts | 9 ++++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/utils/sessionUtils.test.ts b/packages/cli/src/utils/sessionUtils.test.ts index 6f1833f9aa8..90ee61ca71b 100644 --- a/packages/cli/src/utils/sessionUtils.test.ts +++ b/packages/cli/src/utils/sessionUtils.test.ts @@ -11,6 +11,7 @@ import { formatRelativeTime, hasUserOrAssistantMessage, SessionError, + type SessionInfo, } from './sessionUtils.js'; import type { Config, MessageRecord } from '@google/gemini-cli-core'; import { SESSION_FILE_PREFIX } from '@google/gemini-cli-core'; @@ -790,7 +791,7 @@ describe('SessionError.invalidSessionIdentifier', () => { startTime: '2024-01-02T09:00:00.000Z', index: 2, }, - ] as Array; + ] as SessionInfo[]; const error = SessionError.invalidSessionIdentifier('99', sessions); expect(error.code).toBe('INVALID_SESSION_IDENTIFIER'); @@ -822,7 +823,7 @@ describe('SessionError.invalidSessionIdentifier', () => { startTime: '2024-01-01T09:00:00.000Z', index: 1, }, - ] as Array; + ] as SessionInfo[]; const error = SessionError.invalidSessionIdentifier('bad', sessions); const olderPos = error.message.indexOf('Older session'); @@ -843,7 +844,7 @@ describe('SessionError.invalidSessionIdentifier', () => { startTime: '2024-01-01T09:00:00.000Z', index: 1, }, - ] as Array; + ] as SessionInfo[]; const error = SessionError.invalidSessionIdentifier('bad', sessions); expect(error.message).toContain('A'.repeat(57) + '...'); @@ -863,7 +864,7 @@ describe('SessionError.invalidSessionIdentifier', () => { startTime: '2024-01-01T09:00:00.000Z', index: 1, }, - ] as Array; + ] as SessionInfo[]; const error = SessionError.invalidSessionIdentifier('bad', sessions); // Should end with exactly 57 emojis followed by '...' @@ -879,13 +880,13 @@ describe('SessionError.invalidSessionIdentifier', () => { lastUpdated: `2024-01-${String(i + 1).padStart(2, '0')}T10:00:00.000Z`, startTime: `2024-01-${String(i + 1).padStart(2, '0')}T09:00:00.000Z`, index: i + 1, - })) as Array; + })) as SessionInfo[]; const error = SessionError.invalidSessionIdentifier('bad', sessions); expect(error.message).toContain('Run --list-sessions for the full list.'); - // Only 10 sessions should appear in the list - expect(error.message).toContain('--resume 10'); - expect(error.message).not.toContain('--resume 11'); + // Most recent 10 sessions (2–11) shown; oldest (1) is hidden behind the note + expect(error.message).toContain('--resume 11'); + expect(error.message).not.toContain('--resume 1,'); }); it('does not append overflow note when sessions are exactly 10', () => { @@ -895,7 +896,7 @@ describe('SessionError.invalidSessionIdentifier', () => { lastUpdated: `2024-01-${String(i + 1).padStart(2, '0')}T10:00:00.000Z`, startTime: `2024-01-${String(i + 1).padStart(2, '0')}T09:00:00.000Z`, index: i + 1, - })) as Array; + })) as SessionInfo[]; const error = SessionError.invalidSessionIdentifier('bad', sessions); expect(error.message).not.toContain( diff --git a/packages/cli/src/utils/sessionUtils.ts b/packages/cli/src/utils/sessionUtils.ts index a2204f4388c..82d723b9746 100644 --- a/packages/cli/src/utils/sessionUtils.ts +++ b/packages/cli/src/utils/sessionUtils.ts @@ -78,7 +78,10 @@ export class SessionError extends Error { new Date(a.startTime).getTime() - new Date(b.startTime).getTime(), ); - const displaySessions = sorted.slice(0, MAX_DISPLAY); + // Show the most recent sessions — users are more likely to want a recent one. + // Preserve absolute indices so they match what --list-sessions shows. + const startIndex = Math.max(0, sorted.length - MAX_DISPLAY); + const displaySessions = sorted.slice(startIndex); const hasMore = sorted.length > MAX_DISPLAY; const sessionLines = displaySessions @@ -87,7 +90,7 @@ export class SessionError extends Error { cpLen(s.displayName) > 60 ? cpSlice(s.displayName, 0, 57) + '...' : s.displayName; - return ` ${i + 1}. ${title} (${formatRelativeTime(s.lastUpdated)})`; + return ` ${startIndex + i + 1}. ${title} (${formatRelativeTime(s.lastUpdated)})`; }) .join('\n'); @@ -96,7 +99,7 @@ export class SessionError extends Error { : ''; const indices = displaySessions - .map((_, i) => `--resume ${i + 1}`) + .map((_, i) => `--resume ${startIndex + i + 1}`) .join(', '); return new SessionError(