fix(core): dynamic session ID injection to resolve resume bugs (#24972)

This commit is contained in:
Tommaso Sciortino
2026-04-08 23:27:24 +00:00
committed by GitHub
parent 80764c8bb5
commit d06dba3538
33 changed files with 165 additions and 189 deletions
@@ -182,6 +182,7 @@ class SubAgentInvocation extends BaseToolInvocation<AgentInputs, ToolResult> {
{
operation: GeminiCliOperation.AgentCall,
logPrompts: this.context.config.getTelemetryLogPromptsEnabled(),
sessionId: this.context.config.getSessionId(),
attributes: {
[GEN_AI_AGENT_NAME]: this.definition.name,
[GEN_AI_AGENT_DESCRIPTION]: this.definition.description,
@@ -74,6 +74,7 @@ describe('LoggingContentGenerator', () => {
}),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true),
refreshUserQuotaIfStale: vi.fn().mockResolvedValue(undefined),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Config;
loggingContentGenerator = new LoggingContentGenerator(wrapped, config);
vi.useFakeTimers();
@@ -350,6 +350,7 @@ export class LoggingContentGenerator implements ContentGenerator {
{
operation: GeminiCliOperation.LLMCall,
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
sessionId: this.config.getSessionId(),
attributes: {
[GEN_AI_REQUEST_MODEL]: req.model,
[GEN_AI_PROMPT_NAME]: userPromptId,
@@ -440,6 +441,7 @@ export class LoggingContentGenerator implements ContentGenerator {
{
operation: GeminiCliOperation.LLMCall,
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
sessionId: this.config.getSessionId(),
attributes: {
[GEN_AI_REQUEST_MODEL]: req.model,
[GEN_AI_PROMPT_NAME]: userPromptId,
@@ -594,6 +596,7 @@ export class LoggingContentGenerator implements ContentGenerator {
{
operation: GeminiCliOperation.LLMCall,
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
sessionId: this.config.getSessionId(),
attributes: {
[GEN_AI_REQUEST_MODEL]: req.model,
},
+1 -1
View File
@@ -252,7 +252,7 @@ export * from './telemetry/index.js';
export * from './telemetry/billingEvents.js';
export { logBillingEvent } from './telemetry/loggers.js';
export * from './telemetry/constants.js';
export { sessionId, createSessionId } from './utils/session.js';
export { createSessionId } from './utils/session.js';
export * from './utils/compatibility.js';
export * from './utils/browser.js';
export { Storage } from './config/storage.js';
+23 -14
View File
@@ -51,8 +51,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
@@ -79,8 +79,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
@@ -161,8 +161,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
@@ -226,8 +226,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
const toolCall = {
request: { name: 'test-tool', args: {}, isClientInitiated: true },
tool: { name: 'test-tool' },
@@ -243,8 +243,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -273,8 +273,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -307,6 +307,7 @@ describe('policy.ts', () => {
isTrustedFolder: vi.fn().mockReturnValue(false),
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
@@ -339,8 +340,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -379,8 +380,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -420,8 +421,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -447,8 +448,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -473,8 +474,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -499,8 +500,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -540,8 +541,8 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
mockConfig as Config;
const mockMessageBus = {
@@ -583,6 +584,7 @@ describe('policy.ts', () => {
isTrustedFolder: vi.fn().mockReturnValue(false),
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config =
@@ -628,6 +630,7 @@ describe('policy.ts', () => {
.fn()
.mockReturnValue('/mock/project/policies'),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
const mockMessageBus = {
publish: vi.fn(),
@@ -659,6 +662,7 @@ describe('policy.ts', () => {
.fn()
.mockReturnValue('/mock/project/policies'),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
const mockMessageBus = {
publish: vi.fn(),
@@ -689,6 +693,7 @@ describe('policy.ts', () => {
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
getTargetDir: vi.fn().mockReturnValue('/mock/dir'),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
const mockMessageBus = {
publish: vi.fn(),
@@ -727,6 +732,7 @@ describe('policy.ts', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
const mockMessageBus = {
publish: vi.fn(),
@@ -766,6 +772,7 @@ describe('policy.ts', () => {
it('should return default denial message when no rule provided', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Config;
(mockConfig as unknown as { config: Config }).config = mockConfig;
@@ -779,6 +786,7 @@ describe('policy.ts', () => {
it('should return custom deny message if provided', () => {
const mockConfig = {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Config;
(mockConfig as unknown as { config: Config }).config = mockConfig;
@@ -840,7 +848,6 @@ describe('Plan Mode Denial Consistency', () => {
publish: vi.fn(),
subscribe: vi.fn(),
} as unknown as Mocked<MessageBus>;
mockConfig = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
toolRegistry: mockToolRegistry,
@@ -852,6 +859,7 @@ describe('Plan Mode Denial Consistency', () => {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.PLAN), // Key: Plan Mode
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
setApprovalMode: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config = mockConfig as Config;
@@ -933,6 +941,7 @@ describe('Plan Mode Denial Consistency', () => {
getApprovalMode: vi.fn().mockReturnValue(currentMode),
isTrustedFolder: vi.fn().mockReturnValue(false),
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
const mockMessageBus = {
@@ -177,6 +177,7 @@ describe('Scheduler (Orchestrator)', () => {
setApprovalMode: vi.fn(),
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config = mockConfig as Config;
@@ -1423,6 +1424,7 @@ describe('Scheduler MCP Progress', () => {
setApprovalMode: vi.fn(),
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config = mockConfig as Config;
+1
View File
@@ -197,6 +197,7 @@ export class Scheduler {
{
operation: GeminiCliOperation.ScheduleToolCalls,
logPrompts: this.context.config.getTelemetryLogPromptsEnabled(),
sessionId: this.context.config.getSessionId(),
},
async ({ metadata: spanMetadata }) => {
const requests = Array.isArray(request) ? request : [request];
@@ -218,6 +218,7 @@ describe('Scheduler Parallel Execution', () => {
setApprovalMode: vi.fn(),
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Mocked<Config>;
(mockConfig as unknown as { config: Config }).config = mockConfig as Config;
@@ -84,6 +84,7 @@ export class ToolExecutor {
{
operation: GeminiCliOperation.ToolCall,
logPrompts: this.config.getTelemetryLogPromptsEnabled(),
sessionId: this.config.getSessionId(),
attributes: {
[GEN_AI_TOOL_NAME]: toolName,
[GEN_AI_TOOL_CALL_ID]: callId,
+12 -9
View File
@@ -110,7 +110,7 @@ describe('runInDevTraceSpan', () => {
const fn = vi.fn(async () => 'result');
const result = await runInDevTraceSpan(
{ operation: GeminiCliOperation.LLMCall },
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
fn,
);
@@ -125,7 +125,7 @@ describe('runInDevTraceSpan', () => {
it('should set default attributes on the span metadata', async () => {
await runInDevTraceSpan(
{ operation: GeminiCliOperation.LLMCall },
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
async ({ metadata }) => {
expect(metadata.attributes[GEN_AI_OPERATION_NAME]).toBe(
GeminiCliOperation.LLMCall,
@@ -143,7 +143,7 @@ describe('runInDevTraceSpan', () => {
it('should set span attributes from metadata on completion', async () => {
await runInDevTraceSpan(
{ operation: GeminiCliOperation.LLMCall },
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
async ({ metadata }) => {
metadata.input = { query: 'hello' };
metadata.output = { response: 'world' };
@@ -169,9 +169,12 @@ describe('runInDevTraceSpan', () => {
it('should handle errors in the wrapped function', async () => {
const error = new Error('test error');
await expect(
runInDevTraceSpan({ operation: GeminiCliOperation.LLMCall }, async () => {
throw error;
}),
runInDevTraceSpan(
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
async () => {
throw error;
},
),
).rejects.toThrow(error);
expect(mockSpan.setStatus).toHaveBeenCalledWith({
@@ -189,7 +192,7 @@ describe('runInDevTraceSpan', () => {
}
const resultStream = await runInDevTraceSpan(
{ operation: GeminiCliOperation.LLMCall },
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
async () => testStream(),
);
@@ -212,7 +215,7 @@ describe('runInDevTraceSpan', () => {
}
const resultStream = await runInDevTraceSpan(
{ operation: GeminiCliOperation.LLMCall },
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
async () => errorStream(),
);
@@ -231,7 +234,7 @@ describe('runInDevTraceSpan', () => {
});
await runInDevTraceSpan(
{ operation: GeminiCliOperation.LLMCall },
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
async ({ metadata }) => {
metadata.input = 'trigger error';
},
+6 -3
View File
@@ -23,7 +23,6 @@ import {
SERVICE_DESCRIPTION,
SERVICE_NAME,
} from './constants.js';
import { sessionId } from '../utils/session.js';
import { truncateString } from '../utils/textUtils.js';
@@ -96,10 +95,14 @@ export interface SpanMetadata {
* @returns The result of the function.
*/
export async function runInDevTraceSpan<R>(
opts: SpanOptions & { operation: GeminiCliOperation; logPrompts?: boolean },
opts: SpanOptions & {
operation: GeminiCliOperation;
logPrompts?: boolean;
sessionId: string;
},
fn: ({ metadata }: { metadata: SpanMetadata }) => Promise<R>,
): Promise<R> {
const { operation, logPrompts, ...restOfSpanOpts } = opts;
const { operation, logPrompts, sessionId, ...restOfSpanOpts } = opts;
const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
return tracer.startActiveSpan(operation, restOfSpanOpts, async (span) => {
-2
View File
@@ -6,8 +6,6 @@
import { randomUUID } from 'node:crypto';
export const sessionId = randomUUID();
export function createSessionId(): string {
return randomUUID();
}