Path: blob/main/extensions/copilot/src/extension/intents/node/chronicleIntent.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as l10n from '@vscode/l10n';6import type * as vscode from 'vscode';7import { IAuthenticationService } from '../../../platform/authentication/common/authentication';8import { ICopilotTokenManager } from '../../../platform/authentication/common/copilotTokenManager';9import { ChatLocation } from '../../../platform/chat/common/commonTypes';10import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';11import { IChatDebugFileLoggerService } from '../../../platform/chat/common/chatDebugFileLoggerService';12import { type SessionRow, type RefRow, ISessionStore } from '../../../platform/chronicle/common/sessionStore';13import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';14import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';15import { IChatEndpoint } from '../../../platform/networking/common/networking';16import { IGitService } from '../../../platform/git/common/gitService';17import { CancellationToken } from '../../../util/vs/base/common/cancellation';18import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';19import { LanguageModelChatMessage } from '../../../vscodeTypes';20import { type AnnotatedRef, type AnnotatedSession, type SessionFileInfo, type SessionTurnInfo, SESSIONS_QUERY_SQLITE, buildRefsQuery, buildFilesQuery, buildTurnsQuery, buildStandupPrompt } from '../../chronicle/common/standupPrompt';21import { SessionIndexingPreference } from '../../chronicle/common/sessionIndexingPreference';22import { CloudSessionStoreClient } from '../../chronicle/node/cloudSessionStoreClient';23import { IFetcherService } from '../../../platform/networking/common/fetcherService';24import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';25import { IToolsService } from '../../tools/common/toolsService';26import { ToolName } from '../../tools/common/toolNames';27import { Conversation } from '../../prompt/common/conversation';28import { IBuildPromptContext } from '../../prompt/common/intents';29import { ChatTelemetryBuilder } from '../../prompt/node/chatParticipantTelemetry';30import { IDocumentContext } from '../../prompt/node/documentContext';31import { DefaultIntentRequestHandler } from '../../prompt/node/defaultIntentRequestHandler';32import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo, IntentLinkificationOptions } from '../../prompt/node/intents';33import { PromptRenderer, RendererIntentInvocation } from '../../prompts/node/base/promptRenderer';34import { ChroniclePrompt } from '../../prompts/node/panel/chroniclePrompt';35import { reindexSessions } from '../../chronicle/node/sessionReindexer';3637/** Cloud SQL dialect sessions query. */38const SESSIONS_QUERY_CLOUD = `SELECT *39FROM sessions40WHERE updated_at >= now() - INTERVAL '1 day'41ORDER BY updated_at DESC42LIMIT 100`;4344const SUBCOMMANDS = ['standup', 'tips', 'improve', 'reindex'] as const;45type ChronicleSubcommand = typeof SUBCOMMANDS[number];4647export class ChronicleIntent implements IIntent {4849static readonly ID = 'chronicle';50readonly id = ChronicleIntent.ID;51readonly description = l10n.t('Session history tools and insights (standup, tips, improve)');52get locations(): ChatLocation[] {53return this._configService.getExperimentBasedConfig(ConfigKey.LocalIndexEnabled, this._expService) ? [ChatLocation.Panel] : [];54}5556readonly commandInfo: IIntentSlashCommandInfo = {57allowsEmptyArgs: true,58};5960constructor(61@IEndpointProvider private readonly endpointProvider: IEndpointProvider,62@ISessionStore private readonly _sessionStore: ISessionStore,63@ICopilotTokenManager private readonly _tokenManager: ICopilotTokenManager,64@IAuthenticationService private readonly _authService: IAuthenticationService,65@IGitService _gitService: IGitService,66@IConfigurationService private readonly _configService: IConfigurationService,67@IInstantiationService private readonly _instantiationService: IInstantiationService,68@ITelemetryService private readonly _telemetryService: ITelemetryService,69@IExperimentationService private readonly _expService: IExperimentationService,70@IFetcherService private readonly _fetcherService: IFetcherService,71@IChatDebugFileLoggerService private readonly _debugLogService: IChatDebugFileLoggerService,72) {73this._indexingPreference = new SessionIndexingPreference(this._configService);74}7576private readonly _indexingPreference: SessionIndexingPreference;7778/** Stashed system prompt for tool-calling subcommands (tips, free-form). */79private _pendingSystemPrompt: string | undefined;8081async handleRequest(82conversation: Conversation,83request: vscode.ChatRequest,84stream: vscode.ChatResponseStream,85token: CancellationToken,86documentContext: IDocumentContext | undefined,87_agentName: string,88location: ChatLocation,89chatTelemetry: ChatTelemetryBuilder,90): Promise<vscode.ChatResult> {91if (!this._configService.getExperimentBasedConfig(ConfigKey.LocalIndexEnabled, this._expService)) {92stream.markdown(l10n.t('Session search is not available yet.'));93return {};94}9596// Route by command name (e.g. 'chronicle:standup') or fall back to parsing the prompt97const { subcommand, rest } = this._resolveSubcommand(request);9899switch (subcommand) {100case 'standup':101return this._handleStandup(rest, stream, request, token);102case 'tips':103return this._handleTips(rest, stream, request, token, conversation, documentContext, location, chatTelemetry);104case 'reindex':105return this._handleReindex(rest, stream, token);106case 'improve':107stream.markdown(l10n.t('`/chronicle {0}` is not yet implemented. Try `/chronicle:standup` or `/chronicle:tips`.', subcommand));108return {};109default:110return this._handleFreeForm(request.prompt ?? '', stream, request, token, conversation, documentContext, location, chatTelemetry);111}112}113114/**115* Resolve the subcommand from the request command (e.g. 'chronicle:standup')116* or fall back to parsing the prompt text for backwards compatibility.117*/118private _resolveSubcommand(request: vscode.ChatRequest): { subcommand: ChronicleSubcommand | string; rest: string | undefined } {119// Prefer explicit command routing (e.g. /chronicle:standup)120if (request.command) {121const colonIdx = request.command.indexOf(':');122if (colonIdx !== -1) {123return {124subcommand: request.command.slice(colonIdx + 1).toLowerCase(),125rest: request.prompt?.trim() || undefined,126};127}128}129130// Fall back to parsing the prompt (for bare /chronicle or /chronicle standup)131const trimmed = request.prompt?.trim() ?? '';132if (!trimmed) {133return { subcommand: 'standup', rest: undefined };134}135const spaceIdx = trimmed.indexOf(' ');136if (spaceIdx === -1) {137return { subcommand: trimmed.toLowerCase(), rest: undefined };138}139return {140subcommand: trimmed.slice(0, spaceIdx).toLowerCase(),141rest: trimmed.slice(spaceIdx + 1).trim() || undefined,142};143}144145private async _handleReindex(146rest: string | undefined,147stream: vscode.ChatResponseStream,148token: CancellationToken,149): Promise<vscode.ChatResult> {150const force = rest?.toLowerCase().includes('force') ?? false;151const statsBefore = this._sessionStore.getStats();152const startTime = Date.now();153154stream.progress(l10n.t('Discovering sessions...'));155156const result = await reindexSessions(157this._sessionStore,158this._debugLogService,159(message: string) => stream.progress(message),160token,161force,162);163164const statsAfter = this._sessionStore.getStats();165166const lines: string[] = [];167if (result.cancelled) {168lines.push(l10n.t('Reindex cancelled.'));169} else {170lines.push(l10n.t('Reindex complete.'));171}172173lines.push('');174lines.push(`| | ${l10n.t('Before')} | ${l10n.t('After')} | ${l10n.t('Delta')} |`);175lines.push('|---|---|---|---|');176lines.push(`| ${l10n.t('Sessions')} | ${statsBefore.sessions} | ${statsAfter.sessions} | +${statsAfter.sessions - statsBefore.sessions} |`);177lines.push(`| ${l10n.t('Turns')} | ${statsBefore.turns} | ${statsAfter.turns} | +${statsAfter.turns - statsBefore.turns} |`);178lines.push(`| ${l10n.t('Files')} | ${statsBefore.files} | ${statsAfter.files} | +${statsAfter.files - statsBefore.files} |`);179lines.push(`| ${l10n.t('Refs')} | ${statsBefore.refs} | ${statsAfter.refs} | +${statsAfter.refs - statsBefore.refs} |`);180lines.push('');181lines.push(l10n.t('{0} session(s) processed, {1} skipped.', result.processed, result.skipped));182183stream.markdown(lines.join('\n'));184185this._telemetryService.sendMSFTTelemetryEvent('chronicle', {186subcommand: 'reindex',187querySource: 'local',188force: String(force),189cancelled: String(result.cancelled),190}, {191localSessionCount: result.processed,192cloudSessionCount: 0,193totalSessionCount: result.processed + result.skipped,194skippedCount: result.skipped,195durationMs: Date.now() - startTime,196});197198return {};199}200201private async _handleStandup(202extra: string | undefined,203stream: vscode.ChatResponseStream,204request: vscode.ChatRequest,205token: CancellationToken,206): Promise<vscode.ChatResult> {207// Always query local SQLite (has current machine's sessions)208const localSessions = this._queryLocalStore();209210// Query cloud if user has cloud consent for any repo211let cloudSessions: { sessions: AnnotatedSession[]; refs: AnnotatedRef[] } = { sessions: [], refs: [] };212if (this._indexingPreference.hasCloudConsent()) {213cloudSessions = await this._queryCloudStore();214}215216// Merge and dedup by session ID (cloud wins on conflict since it has cross-machine data)217const seenIds = new Set<string>();218const sessions: AnnotatedSession[] = [];219const refs: AnnotatedRef[] = [];220221// Add cloud sessions first (higher priority)222for (const s of cloudSessions.sessions) {223if (!seenIds.has(s.id)) {224seenIds.add(s.id);225sessions.push(s);226}227}228// Add local sessions not already in cloud229for (const s of localSessions.sessions) {230if (!seenIds.has(s.id)) {231seenIds.add(s.id);232sessions.push(s);233}234}235// Merge refs (dedup by session_id + ref_type + ref_value)236const seenRefs = new Set<string>();237for (const r of [...cloudSessions.refs, ...localSessions.refs]) {238const key = `${r.session_id}:${r.ref_type}:${r.ref_value}`;239if (!seenRefs.has(key)) {240seenRefs.add(key);241refs.push(r);242}243}244245// Sort by updated_at descending, cap to 20246sessions.sort((a, b) => (b.updated_at ?? '').localeCompare(a.updated_at ?? ''));247const capped = sessions.slice(0, 20);248const cappedIds = new Set(capped.map(s => s.id));249const cappedRefs = refs.filter(r => cappedIds.has(r.session_id));250251// Fetch turns and files for capped sessions252let cappedTurns: SessionTurnInfo[] = [];253let cappedFiles: SessionFileInfo[] = [];254if (capped.length > 0) {255const ids = capped.map(s => s.id);256try {257cappedTurns = this._sessionStore.executeReadOnlyFallback(buildTurnsQuery(ids)) as unknown as SessionTurnInfo[];258} catch { /* non-fatal */ }259try {260cappedFiles = this._sessionStore.executeReadOnlyFallback(buildFilesQuery(ids)) as unknown as SessionFileInfo[];261} catch { /* non-fatal */ }262263// Fetch and merge cloud turns and files (only for capped sessions)264if (this._indexingPreference.hasCloudConsent()) {265const cloudDetail = await this._queryCloudTurnsAndFiles(ids);266267// Merge cloud turns (dedup by session_id + turn_index)268if (cloudDetail.turns.length > 0) {269const seenTurns = new Set(cappedTurns.map(t => `${t.session_id}:${t.turn_index}`));270for (const t of cloudDetail.turns) {271if (!seenTurns.has(`${t.session_id}:${t.turn_index}`)) {272cappedTurns.push(t);273}274}275}276277// Merge cloud files (dedup by session_id + file_path)278if (cloudDetail.files.length > 0) {279const seenFiles = new Set(cappedFiles.map(f => `${f.session_id}:${f.file_path}`));280for (const f of cloudDetail.files) {281if (!seenFiles.has(`${f.session_id}:${f.file_path}`)) {282cappedFiles.push(f);283}284}285}286}287}288289const standupPrompt = buildStandupPrompt(capped, cappedRefs, cappedTurns, cappedFiles, extra);290291if (capped.length === 0) {292stream.markdown(l10n.t('No sessions found. There\'s nothing to report for a standup.'));293return {};294}295296const localCount = capped.filter(s => s.source !== 'cloud').length;297const cloudCount = capped.filter(s => s.source === 'cloud').length;298299this._sendTelemetry('standup', localCount, cloudCount);300301if (cloudCount > 0 && localCount > 0) {302stream.progress(l10n.t('Generating standup from {0} cloud and {1} local session(s)...', cloudCount, localCount));303} else if (cloudCount > 0) {304stream.progress(l10n.t('Generating standup from {0} cloud session(s)...', cloudCount));305} else {306stream.progress(l10n.t('Generating standup from {0} local session(s)...', localCount));307}308309const model = request.model;310const messages = [311LanguageModelChatMessage.User(standupPrompt),312];313314try {315const response = await model.sendRequest(messages, {}, token);316317for await (const part of response.text) {318stream.markdown(part);319}320} catch (err) {321stream.markdown(l10n.t('Failed to generate standup. Please try again.'));322}323324return {};325}326327private async _handleTips(328extra: string | undefined,329stream: vscode.ChatResponseStream,330request: vscode.ChatRequest,331token: CancellationToken,332conversation: Conversation,333documentContext: IDocumentContext | undefined,334location: ChatLocation,335chatTelemetry: ChatTelemetryBuilder,336): Promise<vscode.ChatResult> {337const hasCloud = this._indexingPreference.hasCloudConsent();338const schema = this._getSchemaDescription(hasCloud);339340let prompt = `You have access to the session_store_sql tool that can execute read-only SQL queries against the user's Copilot session database.341342Your task: Analyze the user's Copilot usage patterns and provide personalized, actionable recommendations.343344Database schema:345346${schema}347348Instructions:3491. IMMEDIATELY call the session_store_sql tool to query sessions from the last 7 days. Do not explain what you will do first.3502. Query the turns table to understand what kinds of prompts the user writes and how conversations flow.3513. Query session_files to see which files and tools are used most frequently.3524. Query session_refs to see PR/issue/commit activity patterns.3535. Based on ALL this data, provide 3-5 specific, actionable tips grounded in actual usage patterns.354355Analysis dimensions to explore:356- **Prompting patterns**: Are user messages vague or specific? Do they provide context? Average turns per session?357- **Tool usage**: Which tools are used most? Are there underutilized tools that could help?358- **Session patterns**: How long are sessions? Are there many short abandoned sessions?359- **File patterns**: Which areas of the codebase get the most attention? Any repeated edits to the same files?360- **Workflow**: Is the user leveraging agent mode, inline chat, custom instructions, prompt files?361362Query guidelines:363- Only one query per call — do not combine multiple statements with semicolons.364- Always use LIMIT (max 100) in your queries and prefer aggregations (COUNT, GROUP BY) over raw row dumps.365- Use the turns table to understand conversation quality, not just session metadata.`;366367if (extra) {368prompt += `\n\nThe user is especially interested in: ${extra}`;369}370371this._pendingSystemPrompt = prompt;372this._sendTelemetry('tips', 0, 0);373return this._delegateToToolCallingHandler(conversation, request, stream, token, documentContext, location, chatTelemetry);374}375376private async _handleFreeForm(377userQuery: string,378stream: vscode.ChatResponseStream,379request: vscode.ChatRequest,380token: CancellationToken,381conversation: Conversation,382documentContext: IDocumentContext | undefined,383location: ChatLocation,384chatTelemetry: ChatTelemetryBuilder,385): Promise<vscode.ChatResult> {386const hasCloud = this._indexingPreference.hasCloudConsent();387const schema = this._getSchemaDescription(hasCloud);388389this._pendingSystemPrompt = `The user is asking about their Copilot session history. Use the session_store_sql tool to query the data and answer their question.390391${schema}392393User's question: ${userQuery}394395Use the session_store_sql tool to run queries. Start with a broad query, then drill down as needed.396- Only SELECT queries are allowed397- Only one query per call — do not combine multiple statements with semicolons398- Always use LIMIT (max 100) and prefer aggregations (COUNT, GROUP BY) over raw row dumps399- Query the **turns** table for conversation content (user_message, assistant_response) — this gives the richest insight into what happened400- Query **session_files** for file paths and tool usage patterns401- Query **session_refs** for PR/issue/commit links402- Join tables to correlate sessions with their turns, files, and refs for complete answers403- Present results in a clear, readable format with markdown tables or bullet points`;404405this._sendTelemetry('freeform', 0, 0);406return this._delegateToToolCallingHandler(conversation, request, stream, token, documentContext, location, chatTelemetry);407}408409private async _delegateToToolCallingHandler(410conversation: Conversation,411request: vscode.ChatRequest,412stream: vscode.ChatResponseStream,413token: CancellationToken,414documentContext: IDocumentContext | undefined,415location: ChatLocation,416chatTelemetry: ChatTelemetryBuilder,417): Promise<vscode.ChatResult> {418const handler = this._instantiationService.createInstance(419DefaultIntentRequestHandler,420this,421conversation,422request,423stream,424token,425documentContext,426location,427chatTelemetry,428{ maxToolCallIterations: 8, temperature: 0, confirmOnMaxToolIterations: false },429undefined,430);431return handler.getResult();432}433434private _sendTelemetry(subcommand: string, localSessionCount: number, cloudSessionCount: number): void {435const hasCloudConsent = this._indexingPreference.hasCloudConsent();436const querySource = hasCloudConsent ? (localSessionCount > 0 ? 'both' : 'cloud') : 'local';437/* __GDPR__438"chronicle" : {439"owner": "vijayu",440"comment": "Tracks chronicle subcommand usage, data sources, and query failures",441"subcommand": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The chronicle subcommand: standup, tips, freeform, or reindex." },442"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The data source used: local, cloud, both, or cloudRefs." },443"error": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "comment": "Truncated error message." },444"force": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether force mode was used (reindex only)." },445"cancelled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the operation was cancelled (reindex only)." },446"localSessionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of local sessions used." },447"cloudSessionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of cloud sessions used." },448"totalSessionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total sessions used." },449"skippedCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of sessions skipped during reindex." },450"durationMs": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Duration of the reindex operation in milliseconds." }451}452*/453this._telemetryService.sendMSFTTelemetryEvent('chronicle', {454subcommand,455querySource,456}, {457localSessionCount,458cloudSessionCount,459totalSessionCount: localSessionCount + cloudSessionCount,460});461}462463private _getSchemaDescription(hasCloud: boolean): string {464return hasCloud465? `Available tables (cloud SQL syntax):466- **sessions**: id, repository, branch, summary, agent_name (who created the session, e.g. 'VS Code', 'cli', 'Copilot Coding Agent', 'Copilot Code Review'), agent_description, created_at, updated_at (TIMESTAMP). NOTE: cwd is always NULL in the cloud. IMPORTANT: Always filter on **updated_at** (not created_at) for time ranges — some session types have created_at set to epoch zero. NOTE: summary and repository/branch may be NULL — always JOIN with turns to get actual content.467- **turns**: session_id, turn_index, user_message, assistant_response, timestamp (TIMESTAMP). The richest and most reliable source of what actually happened — the first turn (turn_index=0) user_message is effectively the session summary. Always JOIN sessions with turns for meaningful results.468- **session_files**: session_id, file_path, tool_name, turn_index. Tracks which files were read/edited and which tools were used.469- **session_refs**: session_id, ref_type (commit/pr/issue), ref_value, turn_index. Tracks PRs created, issues referenced, commits made.470471Use \`now() - INTERVAL '1 day'\` for date math, \`ILIKE\` for text search.472Always JOIN sessions with turns to get session content — do not rely on sessions.summary alone.`473: `Available tables (SQLite syntax — local):474- **sessions**: id, cwd (workspace folder path), repository, branch, summary, host_type, agent_name, agent_description, created_at, updated_at. NOTE: agent_name and agent_description may be empty for older sessions. summary may contain raw JSON — prefer JOINing with turns.user_message for text search.475- **turns**: session_id, turn_index, user_message, assistant_response (first ~1000 characters of the assistant reply, with an ellipsis if truncated — not the full response; may be empty for older sessions), timestamp. The richest source of what actually happened — always JOIN sessions with turns for meaningful results.476- **session_files**: session_id, file_path, tool_name, turn_index. Tracks which files were read/edited and which tools were used. May be empty for older sessions.477- **session_refs**: session_id, ref_type (commit/pr/issue), ref_value, turn_index. Tracks PRs created, issues referenced, commits made. May be empty for older sessions.478- **search_index**: FTS5 table. Use \`WHERE search_index MATCH 'query'\`479480Use \`datetime('now', '-1 day')\` for date math.481Join sessions with turns/files/refs using session_id for complete analysis.`;482}483484/**485* Query the local SQLite session store for sessions and refs.486*/487private _queryLocalStore(): { sessions: AnnotatedSession[]; refs: AnnotatedRef[] } {488try {489// Use fallback (no authorizer) since these are known-safe SELECT queries490const rawSessions = this._sessionStore.executeReadOnlyFallback(SESSIONS_QUERY_SQLITE) as unknown as SessionRow[];491const sessions: AnnotatedSession[] = rawSessions.map(s => ({ ...s, source: 'vscode' as const }));492493let refs: AnnotatedRef[] = [];494if (sessions.length > 0) {495const ids = sessions.map(s => s.id);496const rawRefs = this._sessionStore.executeReadOnlyFallback(buildRefsQuery(ids)) as unknown as RefRow[];497refs = rawRefs.map(r => ({ ...r, source: 'vscode' as const }));498}499500return { sessions, refs };501} catch (err) {502503this._telemetryService.sendMSFTTelemetryErrorEvent('chronicle', {504subcommand: 'standup',505querySource: 'local',506error: err instanceof Error ? err.message.substring(0, 100) : 'unknown',507}, {});508return { sessions: [], refs: [] };509}510}511512private async _queryCloudStore(): Promise<{ sessions: AnnotatedSession[]; refs: AnnotatedRef[] }> {513const empty = { sessions: [] as AnnotatedSession[], refs: [] as AnnotatedRef[] };514try {515const client = new CloudSessionStoreClient(this._tokenManager, this._authService, this._fetcherService);516517const sessionsResult = await client.executeQuery(SESSIONS_QUERY_CLOUD);518if (!sessionsResult || sessionsResult.rows.length === 0) {519return empty;520}521522const sessions: AnnotatedSession[] = sessionsResult.rows.map(r => ({523id: r.id as string,524summary: r.summary as string | undefined,525branch: r.branch as string | undefined,526repository: r.repository as string | undefined,527agent_name: r.agent_name as string | undefined,528agent_description: r.agent_description as string | undefined,529created_at: r.created_at as string | undefined,530updated_at: r.updated_at as string | undefined,531source: 'cloud' as const,532}));533534// Query refs for these sessions535const ids = sessions.map(s => s.id);536let refs: AnnotatedRef[] = [];537try {538const refsQuery = `SELECT session_id, ref_type, ref_value FROM session_refs WHERE session_id IN (${ids.map(s => `'${s.replace(/'/g, '\'\'')}'`).join(',')})`;539const refsResult = await client.executeQuery(refsQuery);540if (refsResult && refsResult.rows.length > 0) {541refs = refsResult.rows.map(r => ({542session_id: r.session_id as string,543ref_type: r.ref_type as 'commit' | 'pr' | 'issue',544ref_value: r.ref_value as string,545source: 'cloud' as const,546}));547}548} catch (refsErr) {549550this._telemetryService.sendMSFTTelemetryErrorEvent('chronicle', {551subcommand: 'standup',552querySource: 'cloudRefs',553error: refsErr instanceof Error ? refsErr.message.substring(0, 100) : 'unknown',554}, {});555}556557return { sessions, refs };558} catch (err) {559560this._telemetryService.sendMSFTTelemetryErrorEvent('chronicle', {561subcommand: 'standup',562querySource: 'cloud',563error: err instanceof Error ? err.message.substring(0, 100) : 'unknown',564}, {});565return empty;566}567}568569/**570* Query cloud turns and files for a specific set of session IDs (called after capping).571*/572private async _queryCloudTurnsAndFiles(sessionIds: string[]): Promise<{ turns: SessionTurnInfo[]; files: SessionFileInfo[] }> {573const empty = { turns: [] as SessionTurnInfo[], files: [] as SessionFileInfo[] };574try {575const client = new CloudSessionStoreClient(this._tokenManager, this._authService, this._fetcherService);576const inClause = sessionIds.map(s => `'${s.replace(/'/g, '\'\'')}'`).join(',');577578let turns: SessionTurnInfo[] = [];579try {580const turnsQuery = `SELECT session_id, turn_index, substring(user_message, 1, 120) as user_message, substring(assistant_response, 1, 200) as assistant_response FROM turns WHERE session_id IN (${inClause}) AND (user_message IS NOT NULL OR assistant_response IS NOT NULL) ORDER BY session_id, turn_index LIMIT 200`;581const turnsResult = await client.executeQuery(turnsQuery);582if (turnsResult && turnsResult.rows.length > 0) {583turns = turnsResult.rows.map(r => ({584session_id: r.session_id as string,585turn_index: r.turn_index as number,586user_message: r.user_message as string | undefined,587assistant_response: r.assistant_response as string | undefined,588}));589}590} catch { /* non-fatal */ }591592let files: SessionFileInfo[] = [];593try {594const filesQuery = `SELECT session_id, file_path, tool_name FROM session_files WHERE session_id IN (${inClause}) LIMIT 200`;595const filesResult = await client.executeQuery(filesQuery);596if (filesResult && filesResult.rows.length > 0) {597files = filesResult.rows.map(r => ({598session_id: r.session_id as string,599file_path: r.file_path as string,600tool_name: r.tool_name as string | undefined,601}));602}603} catch { /* non-fatal */ }604605return { turns, files };606} catch {607return empty;608}609}610611async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {612const { location, request } = invocationContext;613const endpoint = await this.endpointProvider.getChatEndpoint(request);614const systemPrompt = this._pendingSystemPrompt ?? '';615this._pendingSystemPrompt = undefined;616return this._instantiationService.createInstance(617ChronicleIntentInvocation, this, location, endpoint, request, systemPrompt618);619}620}621622class ChronicleIntentInvocation extends RendererIntentInvocation implements IIntentInvocation {623624readonly linkification: IntentLinkificationOptions = { disable: false };625626constructor(627intent: IIntent,628location: ChatLocation,629endpoint: IChatEndpoint,630private readonly request: vscode.ChatRequest,631private readonly systemPrompt: string,632@IInstantiationService private readonly instantiationService: IInstantiationService,633@IToolsService private readonly toolsService: IToolsService,634) {635super(intent, location, endpoint);636}637638async createRenderer(promptContext: IBuildPromptContext, endpoint: IChatEndpoint, _progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, _token: vscode.CancellationToken) {639return PromptRenderer.create(this.instantiationService, endpoint, ChroniclePrompt, {640endpoint,641promptContext,642systemPrompt: this.systemPrompt,643});644}645646getAvailableTools(): vscode.LanguageModelToolInformation[] | Promise<vscode.LanguageModelToolInformation[]> | undefined {647return this.toolsService.getEnabledTools(this.request, this.endpoint,648tool => tool.name === ToolName.SessionStoreSql649);650}651}652653654