Skip to content

Commit 5cb3bd5

Browse files
committed
Merge remote-tracking branch 'origin/main' into agent-session/local-protocol
2 parents 6bd12e9 + 01635dd commit 5cb3bd5

18 files changed

Lines changed: 783 additions & 326 deletions

.github/scripts/gemini-lifecycle-manager.cjs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,41 @@ module.exports = async ({ github, context, core }) => {
4141
now.getTime() - NO_RESPONSE_DAYS * 24 * 60 * 60 * 1000,
4242
);
4343

44+
const maintainerCache = new Map();
45+
async function isMaintainer(user, association) {
46+
if (user?.type === 'Bot') return true;
47+
if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(association)) return true;
48+
49+
const username = user?.login;
50+
if (!username) return false;
51+
52+
if (maintainerCache.has(username)) {
53+
return maintainerCache.get(username);
54+
}
55+
56+
try {
57+
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
58+
owner,
59+
repo,
60+
username,
61+
});
62+
// Permission can be admin, write, read, none.
63+
// Roles like 'maintain' or 'triage' often map to 'write' or 'read' in the top-level field.
64+
const isM =
65+
['admin', 'write'].includes(data.permission) ||
66+
['admin', 'maintain', 'write'].includes(data.role_name);
67+
68+
maintainerCache.set(username, isM);
69+
return isM;
70+
} catch (err) {
71+
core.warning(
72+
`Could not check permissions for ${username}: ${err.message}`,
73+
);
74+
maintainerCache.set(username, false);
75+
return false;
76+
}
77+
}
78+
4479
async function processItems(query, callback) {
4580
core.info(`Searching: ${query}`);
4681
try {
@@ -83,10 +118,7 @@ module.exports = async ({ github, context, core }) => {
83118
const lastComment = comments[0];
84119
if (
85120
lastComment &&
86-
!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
87-
lastComment.author_association,
88-
) &&
89-
lastComment.user?.type !== 'Bot'
121+
!(await isMaintainer(lastComment.user, lastComment.author_association))
90122
) {
91123
core.info(
92124
`Removing ${NEED_INFO_LABEL} from #${item.number} due to contributor response.`,
@@ -188,11 +220,7 @@ module.exports = async ({ github, context, core }) => {
188220
await processItems(
189221
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" -label:"status/pr-nudge-sent" created:${prCloseThreshold.toISOString()}..${nudgeThreshold.toISOString()}`,
190222
async (pr) => {
191-
if (
192-
['OWNER', 'MEMBER', 'COLLABORATOR'].includes(pr.author_association) ||
193-
pr.user?.type === 'Bot'
194-
)
195-
return;
223+
if (await isMaintainer(pr.user, pr.author_association)) return;
196224

197225
core.info(`Nudging PR #${pr.number} for contribution policy.`);
198226
if (!dryRun) {
@@ -216,11 +244,7 @@ module.exports = async ({ github, context, core }) => {
216244
await processItems(
217245
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" created:<${prCloseThreshold.toISOString()}`,
218246
async (pr) => {
219-
if (
220-
['OWNER', 'MEMBER', 'COLLABORATOR'].includes(pr.author_association) ||
221-
pr.user?.type === 'Bot'
222-
)
223-
return;
247+
if (await isMaintainer(pr.user, pr.author_association)) return;
224248

225249
core.info(
226250
`Closing PR #${pr.number} per contribution policy (no 'help wanted').`,

docs/changelogs/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ on GitHub.
1818
| [Preview](preview.md) | Experimental features ready for early feedback. |
1919
| [Stable](latest.md) | Stable, recommended for general use. |
2020

21+
## Announcements: v0.41.0 - 2026-05-05
22+
23+
- **Real-time Voice Mode:** Implemented real-time voice mode with cloud and
24+
local backends
25+
([#24174](https://github.com/google-gemini/gemini-cli/pull/24174) by
26+
@Abhijit-2592).
27+
- **Secure Environment Loading:** Enforced workspace trust and secured .env
28+
loading in headless mode
29+
([#25814](https://github.com/google-gemini/gemini-cli/pull/25814) by
30+
@ehedlund).
31+
- **Advanced Shell Validation:** Enhanced shell command validation and added
32+
core tools allowlist for improved security
33+
([#25720](https://github.com/google-gemini/gemini-cli/pull/25720) by @galz10).
34+
2135
## Announcements: v0.40.0 - 2026-04-28
2236

2337
- **Offline Search and Themes:** Bundled ripgrep for offline search support and

docs/changelogs/latest.md

Lines changed: 108 additions & 166 deletions
Large diffs are not rendered by default.

packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { type SessionMetrics } from '../contexts/SessionContext.js';
1313
import {
1414
ToolCallDecision,
1515
getShellConfiguration,
16+
isWindows,
1617
type WorktreeSettings,
1718
} from '@google/gemini-cli-core';
1819

@@ -22,6 +23,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
2223
return {
2324
...actual,
2425
getShellConfiguration: vi.fn(),
26+
isWindows: vi.fn(),
2527
};
2628
});
2729

@@ -44,6 +46,7 @@ vi.mock('../contexts/ConfigContext.js', async (importOriginal) => {
4446
});
4547

4648
const getShellConfigurationMock = vi.mocked(getShellConfiguration);
49+
const isWindowsMock = vi.mocked(isWindows);
4750
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
4851

4952
const renderWithMockedStats = async (
@@ -106,6 +109,7 @@ describe('<SessionSummaryDisplay />', () => {
106109
argsPrefix: ['-c'],
107110
shell: 'bash',
108111
});
112+
isWindowsMock.mockReturnValue(false);
109113
});
110114

111115
it('renders the summary display with a title', async () => {
@@ -149,7 +153,7 @@ describe('<SessionSummaryDisplay />', () => {
149153
);
150154
const output = lastFrame();
151155

152-
// Standard UUID characters should not be escaped/quoted by default for bash.
156+
// Standard UUID characters are NOT wrapped in double quotes on non-Windows.
153157
expect(output).toContain('gemini --resume 1234-abcd-5678-efgh');
154158
unmount();
155159
});
@@ -167,7 +171,8 @@ describe('<SessionSummaryDisplay />', () => {
167171
unmount();
168172
});
169173

170-
it('renders a standard UUID-formatted session ID in the footer (powershell)', async () => {
174+
it('renders a standard UUID-formatted session ID in the footer (powershell) on Windows', async () => {
175+
isWindowsMock.mockReturnValue(true);
171176
getShellConfigurationMock.mockReturnValue({
172177
executable: 'powershell.exe',
173178
argsPrefix: ['-NoProfile', '-Command'],
@@ -181,9 +186,8 @@ describe('<SessionSummaryDisplay />', () => {
181186
);
182187
const output = lastFrame();
183188

184-
// PowerShell doesn't wraps UUID in single quotes because
185-
// it contains no special characters.
186-
expect(output).toContain('gemini --resume 1234-abcd-5678-efgh');
189+
// PowerShell doesn't wrap UUID in quotes by default, but we wrap it in double quotes on Windows.
190+
expect(output).toContain('gemini --resume "1234-abcd-5678-efgh"');
187191
unmount();
188192
});
189193

@@ -201,7 +205,8 @@ describe('<SessionSummaryDisplay />', () => {
201205
);
202206
const output = lastFrame();
203207

204-
// PowerShell wraps in single quotes and escapes internal single quotes by doubling them
208+
// PowerShell wraps in single quotes and escapes internal single quotes by doubling them.
209+
// Since it's already quoted, we don't add redundant double quotes.
205210
expect(output).toContain("gemini --resume '''; rm -rf / #'");
206211
unmount();
207212
});

packages/cli/src/ui/components/SessionSummaryDisplay.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import type React from 'react';
88
import { StatsDisplay } from './StatsDisplay.js';
99
import { useSessionStats } from '../contexts/SessionContext.js';
1010
import { useConfig } from '../contexts/ConfigContext.js';
11-
import { escapeShellArg, getShellConfiguration } from '@google/gemini-cli-core';
11+
import {
12+
escapeShellArg,
13+
getShellConfiguration,
14+
isWindows,
15+
} from '@google/gemini-cli-core';
1216

1317
interface SessionSummaryDisplayProps {
1418
duration: string;
@@ -24,11 +28,17 @@ export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
2428
const worktreeSettings = config.getWorktreeSettings();
2529

2630
const escapedSessionId = escapeShellArg(stats.sessionId, shell);
27-
let footer = `To resume this session: gemini --resume ${escapedSessionId}`;
31+
const footerSessionId =
32+
isWindows() &&
33+
!escapedSessionId.startsWith('"') &&
34+
!escapedSessionId.startsWith("'")
35+
? `"${escapedSessionId}"`
36+
: escapedSessionId;
37+
let footer = `To resume this session: gemini --resume ${footerSessionId}`;
2838

2939
if (worktreeSettings) {
3040
footer =
31-
`To resume work in this worktree: cd ${escapeShellArg(worktreeSettings.path, shell)} && gemini --resume ${escapedSessionId}\n` +
41+
`To resume work in this worktree: cd ${escapeShellArg(worktreeSettings.path, shell)} && gemini --resume ${footerSessionId}\n` +
3242
`To remove manually: git worktree remove ${escapeShellArg(worktreeSettings.path, shell)}`;
3343
}
3444

packages/core/src/agent/content-utils.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,17 @@ describe('contentPartsToGeminiParts', () => {
192192
const content = [
193193
{ type: 'custom_widget', payload: 123 },
194194
] as unknown as ContentPart[];
195+
196+
const warnSpy = vi.spyOn(debugLogger, 'warn');
195197
const result = contentPartsToGeminiParts(content);
198+
199+
expect(warnSpy).toHaveBeenCalled();
196200
expect(result).toHaveLength(1);
197201
expect(result[0]).toEqual({
198202
text: JSON.stringify({ type: 'custom_widget', payload: 123 }),
199203
});
204+
205+
warnSpy.mockRestore();
200206
});
201207
});
202208

packages/core/src/context/contextManager.barrier.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ describe('ContextManager Sync Pressure Barrier Tests', () => {
6060

6161
// Verify Episode 0 (System) was pruned, so we now start with a sentinel due to role alternation
6262
expect(projection[0].role).toBe('user');
63-
expect(projection[0].parts![0].text).toBe(
64-
'[Continuing from previous AI thoughts...]',
65-
);
63+
expect(projection[0].parts![0].text).toContain('User turn 17');
6664

6765
// Filter out synthetic Yield nodes (they are model responses without actual tool/text bodies)
6866
const contentNodes = projection.filter(

packages/core/src/context/contextManager.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ export class ContextManager {
7272
event.targets,
7373
event.returnedNodes,
7474
);
75-
this.evaluateTriggers(new Set());
75+
// We explicitly DO NOT call evaluateTriggers here.
76+
// The Context Manager is a one-way assembly line. It only evaluates triggers
77+
// when fundamentally new organic context is added via PristineHistoryUpdated.
78+
// Re-evaluating after a processor finishes creates infinite feedback loops if
79+
// the processor fails to reduce the token count below the threshold.
7680
});
7781

7882
this.historyObserver.start();
@@ -126,10 +130,15 @@ export class ContextManager {
126130
// Walk backwards finding nodes that fall out of the retained budget
127131
for (let i = this.buffer.nodes.length - 1; i >= 0; i--) {
128132
const node = this.buffer.nodes[i];
133+
const priorTokens = rollingTokens;
129134
rollingTokens += this.env.tokenCalculator.calculateConcreteListTokens([
130135
node,
131136
]);
132-
if (rollingTokens > this.sidecar.config.budget.retainedTokens) {
137+
138+
// Loose Boundary Policy: If this node is the one that pushes us over the retained limit,
139+
// we KEEP it to prevent aggressive undershooting. We only age out nodes that are
140+
// strictly *older* than the boundary node.
141+
if (priorTokens > this.sidecar.config.budget.retainedTokens) {
133142
// Only age out if not protected
134143
if (!protectedIds.has(node.id)) {
135144
agedOutNodes.add(node.id);

0 commit comments

Comments
 (0)