Compare commits

...

11 Commits

Author SHA1 Message Date
Abhijit Balaji 11eadac9af test(core): update scheduler policy tests for new argsPattern format 2026-05-05 14:41:40 -07:00
Abhijit Balaji 5290c51451 fix(core): resolve policy engine bugs affecting tool approvals
Fixes #24772, #16970.

- Fixes regex null-byte mismatch in `buildParamArgsPattern` so "always allow" works.
- Removes sandbox requirement from `shouldDowngradeForRedirection` so YOLO/AUTO_EDIT modes behave correctly.
- Enhances `stripShellWrapper` to recognize generic shell scripts (e.g., `sh script.sh`).
2026-05-05 14:41:01 -07:00
Gal Zahavi 3627f4777f fix(core): allow redirection in YOLO and AUTO_EDIT modes without sandboxing (#26542) 2026-05-05 21:26:16 +00:00
Himanshu Kumar d8f2a89865 fix(core): remove unsafe type assertion suppressions in error utils (#19881)
Co-authored-by: David Pierce <davidapierce@google.com>
2026-05-05 19:52:29 +00:00
Abhijit Balaji f29eb9a569 fix(core): reject numeric project IDs in GOOGLE_CLOUD_PROJECT (#24695) (#26532) 2026-05-05 19:50:36 +00:00
Sandy Tao f17cfb2a71 docs: clarify Auto Memory proposes memory updates and skills (#26527) 2026-05-05 19:39:32 +00:00
Aishanee Shah 0218817fe3 feat(core): steer model to use edit tool for surgical edits, fix a typo (#26480) 2026-05-05 19:35:04 +00:00
joshualitt 0803007c8f fix(core): Minor fixes for generalist profile. (#26357) 2026-05-05 19:32:13 +00:00
Coco Sheng f5c0977e96 fix(core): retry on ERR_STREAM_PREMATURE_CLOSE errors (#26519) 2026-05-05 19:19:50 +00:00
Coco Sheng e80d7cc083 feat: allow queuing messages during compression (#24071) (#26506) 2026-05-05 17:52:08 +00:00
Jack Wotherspoon 7cc19c2a1b fix(cli): prevent settings dialog border clipping using maxHeight (#26507) 2026-05-05 16:22:58 +00:00
34 changed files with 576 additions and 152 deletions
+59 -37
View File
@@ -1,9 +1,10 @@
# Auto Memory
Auto Memory is an experimental feature that mines your past Gemini CLI sessions
in the background and turns recurring workflows into reusable
[Agent Skills](./skills.md). You review, accept, or discard each extracted skill
before it becomes available to future sessions.
in the background and proposes durable memory updates and reusable
[Agent Skills](./skills.md). You review each candidate before it becomes
available to future sessions: apply memory updates, promote skills, or discard
anything you do not want.
<!-- prettier-ignore -->
> [!NOTE]
@@ -12,28 +13,33 @@ before it becomes available to future sessions.
## Overview
Every session you run with Gemini CLI is recorded locally as a transcript. Auto
Memory scans those transcripts for procedural patterns that recur across
sessions, then drafts each pattern as a `SKILL.md` file in a project-local
inbox. You inspect the draft, decide whether it captures real expertise, and
promote it to your global or workspace skills directory if you want it.
Memory scans those transcripts for durable facts, preferences, workflow
constraints, and procedural patterns that recur across sessions. It can draft
memory updates as unified diff `.patch` files and draft reusable procedures as
`SKILL.md` files. All candidates are held in a project-local inbox until you
approve or discard them.
You'll use Auto Memory when you want to:
- **Capture team workflows** that you find yourself walking the agent through
more than once.
- **Preserve durable project context** such as repeated verification commands,
local constraints, or personal project notes.
- **Codify hard-won fixes** for project-specific landmines so future sessions
avoid them.
- **Bootstrap a skills library** without writing every `SKILL.md` by hand.
Auto Memory complements—but does not replace—the
[`save_memory` tool](../tools/memory.md), which captures single facts into
`GEMINI.md`. Auto Memory captures multi-step procedures into skills.
`GEMINI.md` when the agent explicitly calls it. Auto Memory infers candidates
from past sessions, writes reviewable patches or skill drafts, and never applies
them without your approval.
## Prerequisites
- Gemini CLI installed and authenticated.
- At least 10 user messages across recent, idle sessions in the project. Auto
Memory ignores active or trivial sessions.
- At least one idle project session with 10 or more user messages. Auto Memory
ignores active, trivial, and sub-agent sessions.
## How to enable Auto Memory
@@ -66,36 +72,45 @@ UI, consume your interactive turns, or surface tool prompts.
been idle for at least three hours and contain at least 10 user messages.
2. **Lock acquisition.** A lock file in the project's memory directory
coordinates across multiple CLI instances so extraction runs at most once at
a time.
3. **Sub-agent extraction.** A specialized sub-agent (named `confucius`)
reviews the session index, reads any sessions that look like they contain
repeated procedural workflows, and drafts new `SKILL.md` files. Its
instructions tell it to default to creating zero skills unless the evidence
is strong, so most runs produce no inbox items.
4. **Patch validation.** If the sub-agent proposes edits to skills outside the
inbox (for example, an existing global skill), it writes a unified diff
`.patch` file. Auto Memory dry-runs each patch and discards any that do not
apply cleanly.
5. **Notification.** When a run produces new skills or patches, Gemini CLI
surfaces an inline message telling you how many items are waiting.
a time. A state file records processed session versions, and extraction is
throttled so short back-to-back CLI launches do not repeatedly scan history.
3. **Candidate extraction.** A background extraction agent reviews the session
index, reads any sessions that look like they contain durable memory or
repeated procedural workflows, and drafts candidates. It defaults to
creating no artifacts unless the evidence is strong, so many runs produce no
inbox items.
4. **Safety boundaries.** Auto Memory writes candidates to a review inbox. It
cannot directly edit active memory files, settings, credentials, or project
`GEMINI.md` files.
5. **Patch validation.** Skill update patches are parsed and dry-run before
they are surfaced. Memory patches are parsed, target-allowlisted, and
applied atomically only when you approve them from the inbox.
6. **Notification.** When a run produces new candidates, Gemini CLI surfaces an
inline message telling you how many items are waiting.
## How to review extracted skills
## How to review extracted items
Use the `/memory inbox` slash command to open the inbox dialog at any time:
**Command:** `/memory inbox`
The dialog lists each draft skill with its name, description, and source
sessions. From there you can:
The dialog groups pending items into new skills, skill updates, and memory
updates. From there you can:
- **Read** the full `SKILL.md` body before deciding.
- **Promote** a skill to your user (`~/.gemini/skills/`) or workspace
(`.gemini/skills/`) directory.
- **Discard** a skill you do not want.
- **Apply** or reject a `.patch` proposal against an existing skill.
- **Review** memory diffs before they touch active files.
- **Apply** or dismiss private and global memory patches. Private patches target
the project memory directory; global patches target only your personal
`~/.gemini/GEMINI.md` file.
Promoted skills become discoverable in the next session and follow the standard
[skill discovery precedence](./skills.md#skill-discovery-tiers).
[skill discovery precedence](./skills.md#skill-discovery-tiers). Applied memory
patches update the underlying memory files and reload memory for the current
session.
## How to disable Auto Memory
@@ -117,19 +132,26 @@ start. Existing inbox items remain on disk; you can either drain them with
## Data and privacy
- Auto Memory only reads session files that already exist locally on your
machine. Nothing is uploaded to Gemini outside the normal API calls the
extraction sub-agent makes during its run.
- The sub-agent is instructed to redact secrets, tokens, and credentials it
encounters and to never copy large tool outputs verbatim.
- Drafted skills live in your project's memory directory until you promote or
discard them. They are not automatically loaded into any session.
machine.
- Auto Memory uses model calls to analyze selected local transcript content
during extraction. No candidates are applied automatically, but transcript
excerpts may be sent to the configured model as part of those calls.
- The extraction agent is instructed to redact secrets, tokens, and credentials
it encounters and to never copy large tool outputs verbatim.
- Drafted skills and memory patches live in your project's memory directory
until you promote, apply, dismiss, or discard them. They are not automatically
loaded into any session.
## Limitations
- The sub-agent runs on a preview Gemini Flash model. Extraction quality depends
on the model's ability to recognize durable patterns versus one-off incidents.
- Auto Memory does not extract skills from the current session. It only
considers sessions that have been idle for three hours or more.
- The extraction agent runs on a preview Gemini Flash model. Extraction quality
depends on the model's ability to recognize durable patterns versus one-off
incidents.
- Auto Memory does not extract memory or skills from the current session. It
only considers sessions that have been idle for three hours or more.
- Project or workspace shared instructions in project `GEMINI.md` files are not
auto-extractable. Auto Memory can propose private project memory, global
personal memory, and skills.
- Inbox items are stored per project. Skills extracted in one workspace are not
visible from another until you promote them to the user-scope skills
directory.
@@ -138,6 +160,6 @@ start. Existing inbox items remain on disk; you can either drain them with
- Learn how skills are discovered and activated in [Agent Skills](./skills.md).
- Explore the [memory management tutorial](./tutorials/memory-management.md) for
the complementary `save_memory` and `GEMINI.md` workflows.
the complementary explicit-memory and `GEMINI.md` workflows.
- Review the experimental settings catalog in
[Settings](./settings.md#experimental).
+1 -1
View File
@@ -125,4 +125,4 @@ immediately. Force a reload with:
`/memory` options.
- Read the technical spec for [Project context](../../cli/gemini-md.md).
- Try the experimental [Auto Memory](../auto-memory.md) feature to extract
reusable skills from your past sessions automatically.
memory updates and reusable skills from your past sessions automatically.
+62 -1
View File
@@ -100,7 +100,7 @@ import { type LoadedSettings } from '../config/settings.js';
import { createMockSettings } from '../test-utils/settings.js';
import type { InitializationResult } from '../core/initializer.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
import { StreamingState } from './types.js';
import { StreamingState, MessageType } from './types.js';
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
import {
UIActionsContext,
@@ -3576,4 +3576,65 @@ describe('AppContainer State Management', () => {
unmount();
});
});
describe('Compression Queuing', () => {
beforeEach(async () => {
const { checkPermissions } = await import(
'./hooks/atCommandProcessor.js'
);
vi.mocked(checkPermissions).mockResolvedValue([]);
vi.spyOn(mockConfig, 'isModelSteeringEnabled').mockReturnValue(true);
const actual = await vi.importActual('./hooks/useMessageQueue.js');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { useMessageQueue: realUseMessageQueue } = actual as any;
mockedUseMessageQueue.mockImplementation(realUseMessageQueue);
// Start compression by mocking pendingHistoryItems to include a pending compression
mockedUseGeminiStream.mockImplementation(() => ({
...DEFAULT_GEMINI_STREAM_MOCK,
pendingHistoryItems: [
{
type: MessageType.COMPRESSION,
compression: {
isPending: true,
originalTokenCount: null,
newTokenCount: null,
compressionStatus: null,
},
},
],
}));
});
it('queues messages during compression instead of handling as steering hints', async () => {
const { unmount } = await act(async () => renderAppContainer());
// Verify state isolation
expect(capturedUIState.streamingState).toBe(StreamingState.Idle);
// Submit a message
await act(async () =>
capturedUIActions.handleFinalSubmit('follow up message'),
);
// Verify it was queued, not submitted as steering hint
expect(capturedUIState.messageQueue).toContain('follow up message');
unmount();
});
it('executes slash commands immediately during compression', async () => {
const { unmount } = await act(async () => renderAppContainer());
// Submit a slash command
await act(async () => capturedUIActions.handleFinalSubmit('/help'));
// Verify it was NOT queued
expect(capturedUIState.messageQueue).not.toContain('/help');
unmount();
});
});
});
+21 -2
View File
@@ -1310,6 +1310,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
const { isMcpReady } = useMcpStatus(config);
const isCompressing = useMemo(
() =>
pendingHistoryItems.some(
(item) =>
item.type === MessageType.COMPRESSION && item.compression.isPending,
),
[pendingHistoryItems],
);
const {
messageQueue,
addMessage,
@@ -1321,6 +1330,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
streamingState,
submitQuery,
isMcpReady,
isCompressing,
});
cancelHandlerRef.current = useCallback(
@@ -1415,7 +1425,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
}
const isMcpOrConfigReady = isConfigInitialized && isMcpReady;
if ((isSlash && isConfigInitialized) || (isIdle && isMcpOrConfigReady)) {
if (
(isSlash && isConfigInitialized) ||
(!isCompressing && isIdle && isMcpOrConfigReady)
) {
if (!isSlash) {
const permissions = await checkPermissions(submittedValue, config);
if (permissions.length > 0) {
@@ -1438,7 +1451,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
void submitQuery(submittedValue);
} else {
// Check messageQueue.length === 0 to only notify on the first queued item
if (isIdle && !isMcpOrConfigReady && messageQueue.length === 0) {
if (
isIdle &&
!isCompressing &&
!isMcpOrConfigReady &&
messageQueue.length === 0
) {
coreEvents.emitFeedback(
'info',
!isConfigInitialized
@@ -1458,6 +1476,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
slashCommands,
isMcpReady,
streamingState,
isCompressing,
messageQueue.length,
pendingHistoryItems,
config,
@@ -42,6 +42,7 @@ describe('compressCommand', () => {
},
};
await compressCommand.action!(context, '');
await new Promise((r) => setTimeout(r, 0));
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
@@ -62,6 +63,7 @@ describe('compressCommand', () => {
mockTryCompressChat.mockResolvedValue(compressedResult);
await compressCommand.action!(context, '');
await new Promise((r) => setTimeout(r, 0));
expect(context.ui.setPendingItem).toHaveBeenNthCalledWith(1, {
type: MessageType.COMPRESSION,
@@ -98,6 +100,7 @@ describe('compressCommand', () => {
mockTryCompressChat.mockResolvedValue(null);
await compressCommand.action!(context, '');
await new Promise((r) => setTimeout(r, 0));
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
@@ -114,6 +117,7 @@ describe('compressCommand', () => {
mockTryCompressChat.mockRejectedValue(error);
await compressCommand.action!(context, '');
await new Promise((r) => setTimeout(r, 0));
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
@@ -128,6 +132,7 @@ describe('compressCommand', () => {
it('should clear the pending item in a finally block', async () => {
mockTryCompressChat.mockRejectedValue(new Error('some error'));
await compressCommand.action!(context, '');
await new Promise((r) => setTimeout(r, 0));
expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
});
+38 -35
View File
@@ -36,48 +36,51 @@ export const compressCommand: SlashCommand = {
},
};
try {
ui.setPendingItem(pendingMessage);
const promptId = `compress-${Date.now()}`;
const compressed =
await context.services.agentContext?.geminiClient?.tryCompressChat(
promptId,
true,
);
if (compressed) {
ui.addItem(
{
type: MessageType.COMPRESSION,
compression: {
isPending: false,
originalTokenCount: compressed.originalTokenCount,
newTokenCount: compressed.newTokenCount,
compressionStatus: compressed.compressionStatus,
ui.setPendingItem(pendingMessage);
void (async () => {
try {
const promptId = `compress-${Date.now()}`;
const compressed =
await context.services.agentContext?.geminiClient?.tryCompressChat(
promptId,
true,
);
if (compressed) {
ui.addItem(
{
type: MessageType.COMPRESSION,
compression: {
isPending: false,
originalTokenCount: compressed.originalTokenCount,
newTokenCount: compressed.newTokenCount,
compressionStatus: compressed.compressionStatus,
},
} as HistoryItemCompression,
Date.now(),
);
} else {
ui.addItem(
{
type: MessageType.ERROR,
text: 'Failed to compress chat history.',
},
} as HistoryItemCompression,
Date.now(),
);
} else {
Date.now(),
);
}
} catch (e) {
ui.addItem(
{
type: MessageType.ERROR,
text: 'Failed to compress chat history.',
text: `Failed to compress chat history: ${
e instanceof Error ? e.message : String(e)
}`,
},
Date.now(),
);
} finally {
ui.setPendingItem(null);
}
} catch (e) {
ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to compress chat history: ${
e instanceof Error ? e.message : String(e)
}`,
},
Date.now(),
);
} finally {
ui.setPendingItem(null);
}
})();
},
};
@@ -326,6 +326,36 @@ describe('SettingsDialog', () => {
});
unmount();
});
it('should render the bottom border correctly when height is constrained', async () => {
const settings = createMockSettings();
const onSelect = vi.fn();
const constrainedHeight = 15;
const renderResult = await renderDialog(settings, onSelect, {
availableTerminalHeight: constrainedHeight,
});
await renderResult.waitUntilReady();
await waitFor(() => {
const output = renderResult.lastFrame();
const lines = output.trim().split('\n');
// Verify height constraint
expect(lines.length).toBeLessThanOrEqual(constrainedHeight);
// Verify bottom border existence in the last line of the output
const lastLine = lines[lines.length - 1];
// 'round' border characters: ─, ╰, ╯
expect(lastLine).toMatch(/[─╰╯]/);
});
// SVG snapshot ensures visual layout and border rendering are preserved
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
});
describe('Setting Descriptions', () => {
@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="275" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#878787" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="19" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">&gt; Settings </text>
<text x="891" y="36" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="53" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#d7ffd7" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="891" y="70" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="87" fill="#d7ffd7" textLength="18" lengthAdjust="spacingAndGlyphs">╰─</text>
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
<text x="45" y="87" fill="#afafaf" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
<text x="180" y="87" fill="#d7ffd7" textLength="702" lengthAdjust="spacingAndGlyphs">─────────────────────────────────────────────────────────────────────────────╯</text>
<text x="891" y="87" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="104" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="104" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="119" width="9" height="17" fill="#005f00" />
<text x="27" y="121" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="36" y="119" width="9" height="17" fill="#005f00" />
<rect x="45" y="119" width="72" height="17" fill="#005f00" />
<text x="45" y="121" fill="#d7ffd7" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<rect x="117" y="119" width="711" height="17" fill="#005f00" />
<rect x="828" y="119" width="45" height="17" fill="#005f00" />
<text x="828" y="121" fill="#d7ffd7" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="121" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="136" width="198" height="17" fill="#005f00" />
<text x="45" y="138" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<text x="891" y="138" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="155" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="172" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
<text x="891" y="172" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="187" width="9" height="17" fill="#005f00" />
<text x="27" y="189" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="36" y="187" width="9" height="17" fill="#005f00" />
<rect x="45" y="187" width="117" height="17" fill="#005f00" />
<text x="45" y="189" fill="#d7ffd7" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<rect x="162" y="187" width="711" height="17" fill="#005f00" />
<text x="891" y="189" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="206" fill="#afafaf" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="206" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="223" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#878787" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -46,6 +46,24 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Initial Rendering > should render the bottom border correctly when height is constrained 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ > Settings │
│ │
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ ╰─Search to filter─────────────────────────────────────────────────────────────────────────────╯ │
│ ▲ │
│ ● Vim Mode false │
│ ▼ Enable Vim keybindings │
│ │
│ Apply To │
│ ● User Settings │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
@@ -425,7 +425,7 @@ export function BaseSettingsDialog({
flexDirection="row"
padding={1}
width="100%"
height="100%"
maxHeight={availableHeight}
>
<Box flexDirection="column" flexGrow={1}>
{/* Title */}
@@ -29,6 +29,7 @@ describe('useMessageQueue', () => {
streamingState: StreamingState;
submitQuery: (query: string) => void;
isMcpReady: boolean;
isCompressing?: boolean;
}) => {
let hookResult: ReturnType<typeof useMessageQueue>;
function TestComponent(props: typeof initialProps) {
@@ -402,4 +403,52 @@ describe('useMessageQueue', () => {
expect(result.current.messageQueue).toEqual([]);
});
});
describe('isCompressing logic', () => {
it('should not auto-submit when isCompressing is true, even if streamingState is Idle', async () => {
const { result } = await renderMessageQueueHook({
isConfigInitialized: true,
streamingState: StreamingState.Idle,
submitQuery: mockSubmitQuery,
isMcpReady: true,
isCompressing: true,
});
// Add messages
act(() => {
result.current.addMessage('Compression message');
});
expect(mockSubmitQuery).not.toHaveBeenCalled();
expect(result.current.messageQueue).toEqual(['Compression message']);
});
it('should auto-submit queued messages when isCompressing becomes false', async () => {
const { result, rerender } = await renderMessageQueueHook({
isConfigInitialized: true,
streamingState: StreamingState.Idle,
submitQuery: mockSubmitQuery,
isMcpReady: true,
isCompressing: true,
});
// Add messages
act(() => {
result.current.addMessage('Pending compression message 1');
result.current.addMessage('Pending compression message 2');
});
expect(mockSubmitQuery).not.toHaveBeenCalled();
// Transition isCompressing to false
rerender({ isCompressing: false });
await waitFor(() => {
expect(mockSubmitQuery).toHaveBeenCalledWith(
'Pending compression message 1\n\nPending compression message 2',
);
expect(result.current.messageQueue).toEqual([]);
});
});
});
});
@@ -12,6 +12,7 @@ export interface UseMessageQueueOptions {
streamingState: StreamingState;
submitQuery: (query: string) => void;
isMcpReady: boolean;
isCompressing?: boolean;
}
export interface UseMessageQueueReturn {
@@ -32,6 +33,7 @@ export function useMessageQueue({
streamingState,
submitQuery,
isMcpReady,
isCompressing = false,
}: UseMessageQueueOptions): UseMessageQueueReturn {
const [messageQueue, setMessageQueue] = useState<string[]>([]);
@@ -69,6 +71,7 @@ export function useMessageQueue({
if (
isConfigInitialized &&
streamingState === StreamingState.Idle &&
!isCompressing &&
isMcpReady &&
messageQueue.length > 0
) {
@@ -84,6 +87,7 @@ export function useMessageQueue({
isMcpReady,
messageQueue,
submitQuery,
isCompressing,
]);
return {
@@ -8,6 +8,7 @@ import {
ProjectIdRequiredError,
setupUser,
ValidationCancelledError,
InvalidNumericProjectIdError,
resetUserDataCacheForTesting,
} from './setup.js';
import { ValidationRequiredError } from '../utils/googleQuotaErrors.js';
@@ -218,6 +219,20 @@ describe('setupUser', () => {
ProjectIdRequiredError,
);
});
it('should throw InvalidNumericProjectIdError when GOOGLE_CLOUD_PROJECT is numeric', async () => {
vi.stubEnv('GOOGLE_CLOUD_PROJECT', '1234567890');
await expect(setupUser({} as OAuth2Client, mockConfig)).rejects.toThrow(
InvalidNumericProjectIdError,
);
});
it('should throw InvalidNumericProjectIdError when GOOGLE_CLOUD_PROJECT_ID is numeric', async () => {
vi.stubEnv('GOOGLE_CLOUD_PROJECT_ID', '1234567890');
await expect(setupUser({} as OAuth2Client, mockConfig)).rejects.toThrow(
InvalidNumericProjectIdError,
);
});
});
describe('new user', () => {
+13
View File
@@ -36,6 +36,15 @@ export class ProjectIdRequiredError extends Error {
}
}
export class InvalidNumericProjectIdError extends Error {
constructor(projectId: string) {
super(
`Invalid Google Cloud Project ID: "${projectId}". The GOOGLE_CLOUD_PROJECT (or GOOGLE_CLOUD_PROJECT_ID) environment variable must be set to your string-based Project ID (e.g., "my-project-123"), not your numeric Project Number. Please update your environment variables.`,
);
this.name = 'InvalidNumericProjectIdError';
}
}
/**
* Error thrown when user cancels the validation process.
* This is a non-recoverable error that should result in auth failure.
@@ -122,6 +131,10 @@ export async function setupUser(
process.env['GOOGLE_CLOUD_PROJECT_ID'] ||
undefined;
if (projectId && /^\d+$/.test(projectId)) {
throw new InvalidNumericProjectIdError(projectId);
}
const projectCache = userDataCache.getOrCreate(client, () =>
createCache<string | undefined, Promise<UserData>>({
storage: 'map',
+3 -2
View File
@@ -78,6 +78,7 @@ export const generalistProfile: ContextProfile = {
budget: {
retainedTokens: 65000,
maxTokens: 150000,
coalescingThresholdTokens: 5000,
},
},
@@ -117,14 +118,14 @@ export const generalistProfile: ContextProfile = {
'NodeDistillation',
env,
resolveProcessorOptions(config, 'NodeDistillation', {
nodeThresholdTokens: 1000,
nodeThresholdTokens: 3000,
}),
),
createNodeTruncationProcessor(
'NodeTruncation',
env,
resolveProcessorOptions(config, 'NodeTruncation', {
maxTokensPerNode: 1200,
maxTokensPerNode: 4000,
}),
),
],
@@ -42,6 +42,11 @@ export function getContextManagementConfigSchema(
description:
'The absolute maximum token count allowed before synchronous truncation kicks in.',
},
coalescingThresholdTokens: {
type: 'number',
description:
'Only trigger background consolidation (snapshots) when at least this many tokens have aged out. Prevents "turn-by-turn" utility model churn.',
},
},
},
processorOptions: {
@@ -29,6 +29,11 @@ export interface AsyncPipelineDef {
export interface ContextBudget {
retainedTokens: number;
maxTokens: number;
/**
* Only trigger background consolidation (snapshots) when at least this many
* tokens have aged out. Prevents "turn-by-turn" utility model churn.
*/
coalescingThresholdTokens?: number;
}
/**
+17 -9
View File
@@ -141,15 +141,23 @@ export class ContextManager {
}
if (agedOutNodes.size > 0) {
this.env.tokenCalculator.garbageCollectCache(
new Set(this.buffer.nodes.map((n) => n.id)),
);
this.eventBus.emitConsolidationNeeded({
nodes: this.buffer.nodes,
targetDeficit:
currentTokens - this.sidecar.config.budget.retainedTokens,
targetNodeIds: agedOutNodes,
});
const targetDeficit =
currentTokens - this.sidecar.config.budget.retainedTokens;
// Respect coalescing threshold for background work
const threshold =
this.sidecar.config.budget.coalescingThresholdTokens || 0;
if (targetDeficit >= threshold) {
this.env.tokenCalculator.garbageCollectCache(
new Set(this.buffer.nodes.map((n) => n.id)),
);
this.eventBus.emitConsolidationNeeded({
nodes: this.buffer.nodes,
targetDeficit,
targetNodeIds: agedOutNodes,
});
}
}
}
}
@@ -17,9 +17,9 @@ export class SnapshotGenerator {
const systemPrompt =
systemInstruction ??
`You are an expert Context Memory Manager. You will be provided with a raw transcript of older conversation turns between a user and an AI assistant.
Your task is to synthesize these turns into a single, dense, factual snapshot that preserves all critical context, preferences, active tasks, and factual knowledge, but discards conversational filler, pleasantries, and redundant back-and-forth iterations.
Your task is to synthesize these turns into a single, dense, factual snapshot that preserves all critical context, preferences, active tasks, and factual knowledge.
Output ONLY the raw factual snapshot, formatted compactly. Do not include markdown wrappers, prefixes like "Here is the snapshot", or conversational elements.`;
Discard conversational filler, pleasantries, and redundant back-and-forth iterations. Output ONLY the raw factual snapshot, formatted compactly. Do not include markdown wrappers, prefixes like "Here is the snapshot", or conversational elements.`;
let userPromptText = 'TRANSCRIPT TO SNAPSHOT:\n\n';
for (const node of nodes) {
@@ -26,7 +26,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -206,7 +206,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -507,7 +507,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -687,7 +687,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -868,7 +868,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -1001,7 +1001,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -1616,7 +1616,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -1793,7 +1793,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -1961,7 +1961,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -2129,7 +2129,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -2293,7 +2293,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -2457,7 +2457,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -2615,7 +2615,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -2747,7 +2747,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -3039,7 +3039,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -3461,7 +3461,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -3625,7 +3625,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -3903,7 +3903,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
@@ -4067,7 +4067,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like grep_search to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like read_file and grep_search.
- read_file fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- replace fails if old_string is ambiguous, causing extra turns. Take care to read enough with read_file and grep_search to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
+1
View File
@@ -289,6 +289,7 @@ describe('Gemini Client (client.ts)', () => {
resetTurn: vi.fn(),
isAutoDistillationEnabled: vi.fn().mockReturnValue(false),
isContextManagementEnabled: vi.fn().mockReturnValue(false),
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
getModelAvailabilityService: vi
.fn()
+4 -1
View File
@@ -827,7 +827,10 @@ export class GeminiChat {
const history = curated
? extractCuratedHistory([...this.agentHistory.get()])
: this.agentHistory.get();
return [...history];
return this.context.config.isContextManagementEnabled()
? scrubHistory([...history])
: [...history];
}
/**
@@ -587,4 +587,66 @@ describe('GeminiChat Network Retries', () => {
}),
);
});
it('should retry on premature stream closure (ERR_STREAM_PREMATURE_CLOSE)', async () => {
mockConfig.getRetryFetchErrors = vi.fn().mockReturnValue(true);
const prematureCloseError = new Error('Premature close');
Object.defineProperty(prematureCloseError, 'code', {
value: 'ERR_STREAM_PREMATURE_CLOSE',
});
vi.mocked(mockContentGenerator.generateContentStream)
.mockResolvedValueOnce(
(async function* () {
yield {
candidates: [{ content: { parts: [{ text: 'Incomplete part' }] } }],
} as unknown as GenerateContentResponse;
throw prematureCloseError;
})(),
)
.mockResolvedValueOnce(
(async function* () {
yield {
candidates: [
{
content: { parts: [{ text: 'Complete response after retry' }] },
finishReason: 'STOP',
},
],
} as unknown as GenerateContentResponse;
})(),
);
const stream = await chat.sendMessageStream(
{ model: 'test-model' },
'test message',
'prompt-id-premature-close',
new AbortController().signal,
LlmRole.MAIN,
);
const events: StreamEvent[] = [];
for await (const event of stream) {
events.push(event);
}
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
expect(retryEvent).toBeDefined();
const successChunk = events.find(
(e) =>
e.type === StreamEventType.CHUNK &&
e.value.candidates?.[0]?.content?.parts?.[0]?.text ===
'Complete response after retry',
);
expect(successChunk).toBeDefined();
expect(mockLogNetworkRetryAttempt).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
error_type: 'ERR_STREAM_PREMATURE_CLOSE',
}),
);
});
});
@@ -1898,6 +1898,30 @@ describe('PolicyEngine', () => {
expect(result.decision).toBe(PolicyDecision.ALLOW);
});
it('should NOT downgrade to ASK_USER for redirected commands in YOLO mode even without sandbox', async () => {
const rules: PolicyRule[] = [
{
toolName: 'run_shell_command',
decision: PolicyDecision.ALLOW,
priority: 10,
},
];
engine = new PolicyEngine({
rules,
approvalMode: ApprovalMode.YOLO,
sandboxManager: new NoopSandboxManager(),
});
const command = 'npm test 2>&1 | tail -80';
const { decision } = await engine.check(
{ name: 'run_shell_command', args: { command } },
undefined,
);
expect(decision).toBe(PolicyDecision.ALLOW);
});
it('should return ALLOW in YOLO mode even if shell command parsing fails', async () => {
const { splitCommands } = await import('../utils/shell-utils.js');
const rules: PolicyRule[] = [
+7 -6
View File
@@ -288,13 +288,14 @@ export class PolicyEngine {
if (allowRedirection) return false;
if (!hasRedirection(command)) return false;
// Do not downgrade (do not ask user) if sandboxing is enabled and in AUTO_EDIT or YOLO
// In YOLO mode, never downgrade.
if (this.approvalMode === ApprovalMode.YOLO) {
return false;
}
// In AUTO_EDIT mode, only bypass downgrade if sandboxing is enabled.
const sandboxEnabled = !(this.sandboxManager instanceof NoopSandboxManager);
if (
sandboxEnabled &&
(this.approvalMode === ApprovalMode.AUTO_EDIT ||
this.approvalMode === ApprovalMode.YOLO)
) {
if (this.approvalMode === ApprovalMode.AUTO_EDIT && sandboxEnabled) {
return false;
}
+2 -2
View File
@@ -102,10 +102,10 @@ export function buildParamArgsPattern(
value: unknown,
): string {
const encodedValue = JSON.stringify(value);
// We wrap the JSON string in escapeRegex and prepend/append \\0 to explicitly
// We wrap the JSON string in escapeRegex and prepend/append \\x00 to explicitly
// match top-level JSON properties generated by stableStringify, preventing
// argument injection bypass attacks.
return `\\\\0${escapeRegex(`"${paramName}":${encodedValue}`)}\\\\0`;
return `\\x00${escapeRegex(`"${paramName}":${encodedValue}`)}\\x00`;
}
/**
+1 -1
View File
@@ -242,7 +242,7 @@ Use the following guidelines to optimize your search and read patterns.
- Prefer using tools like ${GREP_TOOL_NAME} to identify points of interest instead of reading lots of files individually.
- If you need to read multiple ranges in a file, do so parallel, in as few turns as possible.
- It is more important to reduce extra turns, but please also try to minimize unnecessarily large file reads and search results, when doing so doesn't result in extra turns. Do this by always providing conservative limits and scopes to tools like ${READ_FILE_TOOL_NAME} and ${GREP_TOOL_NAME}.
- ${READ_FILE_TOOL_NAME} fails if ${EDIT_PARAM_OLD_STRING} is ambiguous, causing extra turns. Take care to read enough with ${READ_FILE_TOOL_NAME} and ${GREP_TOOL_NAME} to make the edit unambiguous.
- ${EDIT_TOOL_NAME} fails if ${EDIT_PARAM_OLD_STRING} is ambiguous, causing extra turns. Take care to read enough with ${READ_FILE_TOOL_NAME} and ${GREP_TOOL_NAME} to make the edit unambiguous.
- You can compensate for the risk of missing results with scoped or limited searches by doing multiple searches in parallel.
- Your primary goal is still to do your best quality work. Efficiency is an important, but secondary concern.
</guidelines>
+1 -1
View File
@@ -770,7 +770,7 @@ describe('policy.ts', () => {
expect.objectContaining({
toolName: 'write_file',
argsPattern:
'\\\\0' + escapeRegex('"file_path":"src/foo.ts"') + '\\\\0',
'\\x00' + escapeRegex('"file_path":"src/foo.ts"') + '\\x00',
}),
);
});
@@ -1333,7 +1333,7 @@ Use this tool when the user's query implies needing the content of several files
exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > snapshot for tool: replace 1`] = `
{
"description": "Replaces text within a file. By default, the tool expects to find and replace exactly ONE occurrence of \`old_string\`. If you want to replace multiple occurrences of the exact same string, set \`allow_multiple\` to true. This tool requires providing significant context around the change to ensure precise targeting.
"description": "Replaces text within a file. By default, the tool expects to find and replace exactly ONE occurrence of \`old_string\`. If you want to replace multiple occurrences of the exact same string, set \`allow_multiple\` to true. This tool is preferred for surgical edits to existing files as it minimizes token usage, simplifies code reviews, and avoids accidental deletions. This tool requires providing significant context around the change to ensure precise targeting.
The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response.",
"name": "replace",
"parametersJsonSchema": {
@@ -1496,7 +1496,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > snapshot for tool: write_file 1`] = `
{
"description": "Writes the complete content to a file, automatically creating missing parent directories. Overwrites existing files. The user has the ability to modify 'content' before it is saved. Best for new or small files; use 'replace' for targeted edits to large files.",
"description": "Writes the complete content to a file, automatically creating missing parent directories. Overwrites existing files. The user has the ability to modify 'content' before it is saved. Best for new or small files; use 'replace' for targeted edits to large files to minimize token usage and simplify reviews.",
"name": "write_file",
"parametersJsonSchema": {
"properties": {
@@ -120,7 +120,7 @@ export const GEMINI_3_SET: CoreToolSet = {
write_file: {
name: WRITE_FILE_TOOL_NAME,
description: `Writes the complete content to a file, automatically creating missing parent directories. Overwrites existing files. The user has the ability to modify 'content' before it is saved. Best for new or small files; use '${EDIT_TOOL_NAME}' for targeted edits to large files.`,
description: `Writes the complete content to a file, automatically creating missing parent directories. Overwrites existing files. The user has the ability to modify 'content' before it is saved. Best for new or small files; use '${EDIT_TOOL_NAME}' for targeted edits to large files to minimize token usage and simplify reviews.`,
parametersJsonSchema: {
type: 'object',
properties: {
@@ -355,7 +355,7 @@ export const GEMINI_3_SET: CoreToolSet = {
replace: {
name: EDIT_TOOL_NAME,
description: `Replaces text within a file. By default, the tool expects to find and replace exactly ONE occurrence of \`old_string\`. If you want to replace multiple occurrences of the exact same string, set \`allow_multiple\` to true. This tool requires providing significant context around the change to ensure precise targeting.
description: `Replaces text within a file. By default, the tool expects to find and replace exactly ONE occurrence of \`old_string\`. If you want to replace multiple occurrences of the exact same string, set \`allow_multiple\` to true. This tool is preferred for surgical edits to existing files as it minimizes token usage, simplifies code reviews, and avoids accidental deletions. This tool requires providing significant context around the change to ensure precise targeting.
The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response.`,
parametersJsonSchema: {
type: 'object',
+3 -10
View File
@@ -280,16 +280,9 @@ function parseResponseData(error: GaxiosError): ResponseData | undefined {
export function isAuthenticationError(error: unknown): boolean {
// Check for MCP SDK errors with code property
// (SseError and StreamableHTTPError both have numeric 'code' property)
if (
error &&
typeof error === 'object' &&
'code' in error &&
typeof (error as { code: unknown }).code === 'number'
) {
// Safe access after check
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const errorCode = (error as { code: number }).code;
if (errorCode === 401) {
if (error && typeof error === 'object' && 'code' in error) {
const errorCode: unknown = (error as Record<string, unknown>)['code'];
if (typeof errorCode === 'number' && errorCode === 401) {
return true;
}
}
+24 -14
View File
@@ -16,23 +16,33 @@ export interface ApiError {
}
export function isApiError(error: unknown): error is ApiError {
if (typeof error !== 'object' || error === null || !('error' in error)) {
return false;
}
const errorProp = (error as { error: unknown }).error;
if (typeof errorProp !== 'object' || errorProp === null) {
return false;
}
return (
typeof error === 'object' &&
error !== null &&
'error' in error &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
typeof (error as ApiError).error === 'object' &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
'message' in (error as ApiError).error
'code' in errorProp &&
typeof errorProp.code === 'number' &&
'message' in errorProp &&
typeof errorProp.message === 'string' &&
'status' in errorProp &&
typeof errorProp.status === 'string'
);
}
export function isStructuredError(error: unknown): error is StructuredError {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
typeof (error as StructuredError).message === 'string'
);
if (typeof error !== 'object' || error === null || !('message' in error)) {
return false;
}
if (typeof error.message !== 'string') {
return false;
}
if ('status' in error && typeof error.status !== 'number') {
return false;
}
return true;
}
+1
View File
@@ -58,6 +58,7 @@ const RETRYABLE_NETWORK_CODES = [
'UND_ERR_HEADERS_TIMEOUT',
'UND_ERR_BODY_TIMEOUT',
'UND_ERR_CONNECT_TIMEOUT',
'ERR_STREAM_PREMATURE_CLOSE',
];
// Node.js builds SSL error codes by prepending ERR_SSL_ to the uppercased
+12 -4
View File
@@ -809,11 +809,11 @@ export function getCommandRoots(command: string): string[] {
}
export function stripShellWrapper(command: string): string {
const pattern =
const cFlagPattern =
/^\s*(?:(?:(?:\S+\/)?(?:sh|bash|zsh))\s+-c|cmd\.exe\s+\/c|powershell(?:\.exe)?\s+(?:-NoProfile\s+)?-Command|pwsh(?:\.exe)?\s+(?:-NoProfile\s+)?-Command)\s+/i;
const match = command.match(pattern);
if (match) {
let newCommand = command.substring(match[0].length).trim();
const cFlagMatch = command.match(cFlagPattern);
if (cFlagMatch) {
let newCommand = command.substring(cFlagMatch[0].length).trim();
if (
(newCommand.startsWith('"') && newCommand.endsWith('"')) ||
(newCommand.startsWith("'") && newCommand.endsWith("'"))
@@ -822,6 +822,14 @@ export function stripShellWrapper(command: string): string {
}
return newCommand;
}
const scriptPattern =
/^\s*(?:(?:\S+\/)?(?:sh|bash|zsh))\s+([a-zA-Z0-9_\-./]+\.sh)\s*$/i;
const scriptMatch = command.match(scriptPattern);
if (scriptMatch) {
return scriptMatch[1];
}
return command.trim();
}