Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/node/chronicleIntent.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as l10n from '@vscode/l10n';
7
import type * as vscode from 'vscode';
8
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
9
import { ICopilotTokenManager } from '../../../platform/authentication/common/copilotTokenManager';
10
import { ChatLocation } from '../../../platform/chat/common/commonTypes';
11
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
12
import { IChatDebugFileLoggerService } from '../../../platform/chat/common/chatDebugFileLoggerService';
13
import { type SessionRow, type RefRow, ISessionStore } from '../../../platform/chronicle/common/sessionStore';
14
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
15
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
16
import { IChatEndpoint } from '../../../platform/networking/common/networking';
17
import { IGitService } from '../../../platform/git/common/gitService';
18
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
19
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
20
import { LanguageModelChatMessage } from '../../../vscodeTypes';
21
import { type AnnotatedRef, type AnnotatedSession, type SessionFileInfo, type SessionTurnInfo, SESSIONS_QUERY_SQLITE, buildRefsQuery, buildFilesQuery, buildTurnsQuery, buildStandupPrompt } from '../../chronicle/common/standupPrompt';
22
import { SessionIndexingPreference } from '../../chronicle/common/sessionIndexingPreference';
23
import { CloudSessionStoreClient } from '../../chronicle/node/cloudSessionStoreClient';
24
import { IFetcherService } from '../../../platform/networking/common/fetcherService';
25
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
26
import { IToolsService } from '../../tools/common/toolsService';
27
import { ToolName } from '../../tools/common/toolNames';
28
import { Conversation } from '../../prompt/common/conversation';
29
import { IBuildPromptContext } from '../../prompt/common/intents';
30
import { ChatTelemetryBuilder } from '../../prompt/node/chatParticipantTelemetry';
31
import { IDocumentContext } from '../../prompt/node/documentContext';
32
import { DefaultIntentRequestHandler } from '../../prompt/node/defaultIntentRequestHandler';
33
import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo, IntentLinkificationOptions } from '../../prompt/node/intents';
34
import { PromptRenderer, RendererIntentInvocation } from '../../prompts/node/base/promptRenderer';
35
import { ChroniclePrompt } from '../../prompts/node/panel/chroniclePrompt';
36
import { reindexSessions } from '../../chronicle/node/sessionReindexer';
37
38
/** Cloud SQL dialect sessions query. */
39
const SESSIONS_QUERY_CLOUD = `SELECT *
40
FROM sessions
41
WHERE updated_at >= now() - INTERVAL '1 day'
42
ORDER BY updated_at DESC
43
LIMIT 100`;
44
45
const SUBCOMMANDS = ['standup', 'tips', 'improve', 'reindex'] as const;
46
type ChronicleSubcommand = typeof SUBCOMMANDS[number];
47
48
export class ChronicleIntent implements IIntent {
49
50
static readonly ID = 'chronicle';
51
readonly id = ChronicleIntent.ID;
52
readonly description = l10n.t('Session history tools and insights (standup, tips, improve)');
53
get locations(): ChatLocation[] {
54
return this._configService.getExperimentBasedConfig(ConfigKey.LocalIndexEnabled, this._expService) ? [ChatLocation.Panel] : [];
55
}
56
57
readonly commandInfo: IIntentSlashCommandInfo = {
58
allowsEmptyArgs: true,
59
};
60
61
constructor(
62
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
63
@ISessionStore private readonly _sessionStore: ISessionStore,
64
@ICopilotTokenManager private readonly _tokenManager: ICopilotTokenManager,
65
@IAuthenticationService private readonly _authService: IAuthenticationService,
66
@IGitService _gitService: IGitService,
67
@IConfigurationService private readonly _configService: IConfigurationService,
68
@IInstantiationService private readonly _instantiationService: IInstantiationService,
69
@ITelemetryService private readonly _telemetryService: ITelemetryService,
70
@IExperimentationService private readonly _expService: IExperimentationService,
71
@IFetcherService private readonly _fetcherService: IFetcherService,
72
@IChatDebugFileLoggerService private readonly _debugLogService: IChatDebugFileLoggerService,
73
) {
74
this._indexingPreference = new SessionIndexingPreference(this._configService);
75
}
76
77
private readonly _indexingPreference: SessionIndexingPreference;
78
79
/** Stashed system prompt for tool-calling subcommands (tips, free-form). */
80
private _pendingSystemPrompt: string | undefined;
81
82
async handleRequest(
83
conversation: Conversation,
84
request: vscode.ChatRequest,
85
stream: vscode.ChatResponseStream,
86
token: CancellationToken,
87
documentContext: IDocumentContext | undefined,
88
_agentName: string,
89
location: ChatLocation,
90
chatTelemetry: ChatTelemetryBuilder,
91
): Promise<vscode.ChatResult> {
92
if (!this._configService.getExperimentBasedConfig(ConfigKey.LocalIndexEnabled, this._expService)) {
93
stream.markdown(l10n.t('Session search is not available yet.'));
94
return {};
95
}
96
97
// Route by command name (e.g. 'chronicle:standup') or fall back to parsing the prompt
98
const { subcommand, rest } = this._resolveSubcommand(request);
99
100
switch (subcommand) {
101
case 'standup':
102
return this._handleStandup(rest, stream, request, token);
103
case 'tips':
104
return this._handleTips(rest, stream, request, token, conversation, documentContext, location, chatTelemetry);
105
case 'reindex':
106
return this._handleReindex(rest, stream, token);
107
case 'improve':
108
stream.markdown(l10n.t('`/chronicle {0}` is not yet implemented. Try `/chronicle:standup` or `/chronicle:tips`.', subcommand));
109
return {};
110
default:
111
return this._handleFreeForm(request.prompt ?? '', stream, request, token, conversation, documentContext, location, chatTelemetry);
112
}
113
}
114
115
/**
116
* Resolve the subcommand from the request command (e.g. 'chronicle:standup')
117
* or fall back to parsing the prompt text for backwards compatibility.
118
*/
119
private _resolveSubcommand(request: vscode.ChatRequest): { subcommand: ChronicleSubcommand | string; rest: string | undefined } {
120
// Prefer explicit command routing (e.g. /chronicle:standup)
121
if (request.command) {
122
const colonIdx = request.command.indexOf(':');
123
if (colonIdx !== -1) {
124
return {
125
subcommand: request.command.slice(colonIdx + 1).toLowerCase(),
126
rest: request.prompt?.trim() || undefined,
127
};
128
}
129
}
130
131
// Fall back to parsing the prompt (for bare /chronicle or /chronicle standup)
132
const trimmed = request.prompt?.trim() ?? '';
133
if (!trimmed) {
134
return { subcommand: 'standup', rest: undefined };
135
}
136
const spaceIdx = trimmed.indexOf(' ');
137
if (spaceIdx === -1) {
138
return { subcommand: trimmed.toLowerCase(), rest: undefined };
139
}
140
return {
141
subcommand: trimmed.slice(0, spaceIdx).toLowerCase(),
142
rest: trimmed.slice(spaceIdx + 1).trim() || undefined,
143
};
144
}
145
146
private async _handleReindex(
147
rest: string | undefined,
148
stream: vscode.ChatResponseStream,
149
token: CancellationToken,
150
): Promise<vscode.ChatResult> {
151
const force = rest?.toLowerCase().includes('force') ?? false;
152
const statsBefore = this._sessionStore.getStats();
153
const startTime = Date.now();
154
155
stream.progress(l10n.t('Discovering sessions...'));
156
157
const result = await reindexSessions(
158
this._sessionStore,
159
this._debugLogService,
160
(message: string) => stream.progress(message),
161
token,
162
force,
163
);
164
165
const statsAfter = this._sessionStore.getStats();
166
167
const lines: string[] = [];
168
if (result.cancelled) {
169
lines.push(l10n.t('Reindex cancelled.'));
170
} else {
171
lines.push(l10n.t('Reindex complete.'));
172
}
173
174
lines.push('');
175
lines.push(`| | ${l10n.t('Before')} | ${l10n.t('After')} | ${l10n.t('Delta')} |`);
176
lines.push('|---|---|---|---|');
177
lines.push(`| ${l10n.t('Sessions')} | ${statsBefore.sessions} | ${statsAfter.sessions} | +${statsAfter.sessions - statsBefore.sessions} |`);
178
lines.push(`| ${l10n.t('Turns')} | ${statsBefore.turns} | ${statsAfter.turns} | +${statsAfter.turns - statsBefore.turns} |`);
179
lines.push(`| ${l10n.t('Files')} | ${statsBefore.files} | ${statsAfter.files} | +${statsAfter.files - statsBefore.files} |`);
180
lines.push(`| ${l10n.t('Refs')} | ${statsBefore.refs} | ${statsAfter.refs} | +${statsAfter.refs - statsBefore.refs} |`);
181
lines.push('');
182
lines.push(l10n.t('{0} session(s) processed, {1} skipped.', result.processed, result.skipped));
183
184
stream.markdown(lines.join('\n'));
185
186
this._telemetryService.sendMSFTTelemetryEvent('chronicle', {
187
subcommand: 'reindex',
188
querySource: 'local',
189
force: String(force),
190
cancelled: String(result.cancelled),
191
}, {
192
localSessionCount: result.processed,
193
cloudSessionCount: 0,
194
totalSessionCount: result.processed + result.skipped,
195
skippedCount: result.skipped,
196
durationMs: Date.now() - startTime,
197
});
198
199
return {};
200
}
201
202
private async _handleStandup(
203
extra: string | undefined,
204
stream: vscode.ChatResponseStream,
205
request: vscode.ChatRequest,
206
token: CancellationToken,
207
): Promise<vscode.ChatResult> {
208
// Always query local SQLite (has current machine's sessions)
209
const localSessions = this._queryLocalStore();
210
211
// Query cloud if user has cloud consent for any repo
212
let cloudSessions: { sessions: AnnotatedSession[]; refs: AnnotatedRef[] } = { sessions: [], refs: [] };
213
if (this._indexingPreference.hasCloudConsent()) {
214
cloudSessions = await this._queryCloudStore();
215
}
216
217
// Merge and dedup by session ID (cloud wins on conflict since it has cross-machine data)
218
const seenIds = new Set<string>();
219
const sessions: AnnotatedSession[] = [];
220
const refs: AnnotatedRef[] = [];
221
222
// Add cloud sessions first (higher priority)
223
for (const s of cloudSessions.sessions) {
224
if (!seenIds.has(s.id)) {
225
seenIds.add(s.id);
226
sessions.push(s);
227
}
228
}
229
// Add local sessions not already in cloud
230
for (const s of localSessions.sessions) {
231
if (!seenIds.has(s.id)) {
232
seenIds.add(s.id);
233
sessions.push(s);
234
}
235
}
236
// Merge refs (dedup by session_id + ref_type + ref_value)
237
const seenRefs = new Set<string>();
238
for (const r of [...cloudSessions.refs, ...localSessions.refs]) {
239
const key = `${r.session_id}:${r.ref_type}:${r.ref_value}`;
240
if (!seenRefs.has(key)) {
241
seenRefs.add(key);
242
refs.push(r);
243
}
244
}
245
246
// Sort by updated_at descending, cap to 20
247
sessions.sort((a, b) => (b.updated_at ?? '').localeCompare(a.updated_at ?? ''));
248
const capped = sessions.slice(0, 20);
249
const cappedIds = new Set(capped.map(s => s.id));
250
const cappedRefs = refs.filter(r => cappedIds.has(r.session_id));
251
252
// Fetch turns and files for capped sessions
253
let cappedTurns: SessionTurnInfo[] = [];
254
let cappedFiles: SessionFileInfo[] = [];
255
if (capped.length > 0) {
256
const ids = capped.map(s => s.id);
257
try {
258
cappedTurns = this._sessionStore.executeReadOnlyFallback(buildTurnsQuery(ids)) as unknown as SessionTurnInfo[];
259
} catch { /* non-fatal */ }
260
try {
261
cappedFiles = this._sessionStore.executeReadOnlyFallback(buildFilesQuery(ids)) as unknown as SessionFileInfo[];
262
} catch { /* non-fatal */ }
263
264
// Fetch and merge cloud turns and files (only for capped sessions)
265
if (this._indexingPreference.hasCloudConsent()) {
266
const cloudDetail = await this._queryCloudTurnsAndFiles(ids);
267
268
// Merge cloud turns (dedup by session_id + turn_index)
269
if (cloudDetail.turns.length > 0) {
270
const seenTurns = new Set(cappedTurns.map(t => `${t.session_id}:${t.turn_index}`));
271
for (const t of cloudDetail.turns) {
272
if (!seenTurns.has(`${t.session_id}:${t.turn_index}`)) {
273
cappedTurns.push(t);
274
}
275
}
276
}
277
278
// Merge cloud files (dedup by session_id + file_path)
279
if (cloudDetail.files.length > 0) {
280
const seenFiles = new Set(cappedFiles.map(f => `${f.session_id}:${f.file_path}`));
281
for (const f of cloudDetail.files) {
282
if (!seenFiles.has(`${f.session_id}:${f.file_path}`)) {
283
cappedFiles.push(f);
284
}
285
}
286
}
287
}
288
}
289
290
const standupPrompt = buildStandupPrompt(capped, cappedRefs, cappedTurns, cappedFiles, extra);
291
292
if (capped.length === 0) {
293
stream.markdown(l10n.t('No sessions found. There\'s nothing to report for a standup.'));
294
return {};
295
}
296
297
const localCount = capped.filter(s => s.source !== 'cloud').length;
298
const cloudCount = capped.filter(s => s.source === 'cloud').length;
299
300
this._sendTelemetry('standup', localCount, cloudCount);
301
302
if (cloudCount > 0 && localCount > 0) {
303
stream.progress(l10n.t('Generating standup from {0} cloud and {1} local session(s)...', cloudCount, localCount));
304
} else if (cloudCount > 0) {
305
stream.progress(l10n.t('Generating standup from {0} cloud session(s)...', cloudCount));
306
} else {
307
stream.progress(l10n.t('Generating standup from {0} local session(s)...', localCount));
308
}
309
310
const model = request.model;
311
const messages = [
312
LanguageModelChatMessage.User(standupPrompt),
313
];
314
315
try {
316
const response = await model.sendRequest(messages, {}, token);
317
318
for await (const part of response.text) {
319
stream.markdown(part);
320
}
321
} catch (err) {
322
stream.markdown(l10n.t('Failed to generate standup. Please try again.'));
323
}
324
325
return {};
326
}
327
328
private async _handleTips(
329
extra: string | undefined,
330
stream: vscode.ChatResponseStream,
331
request: vscode.ChatRequest,
332
token: CancellationToken,
333
conversation: Conversation,
334
documentContext: IDocumentContext | undefined,
335
location: ChatLocation,
336
chatTelemetry: ChatTelemetryBuilder,
337
): Promise<vscode.ChatResult> {
338
const hasCloud = this._indexingPreference.hasCloudConsent();
339
const schema = this._getSchemaDescription(hasCloud);
340
341
let prompt = `You have access to the session_store_sql tool that can execute read-only SQL queries against the user's Copilot session database.
342
343
Your task: Analyze the user's Copilot usage patterns and provide personalized, actionable recommendations.
344
345
Database schema:
346
347
${schema}
348
349
Instructions:
350
1. IMMEDIATELY call the session_store_sql tool to query sessions from the last 7 days. Do not explain what you will do first.
351
2. Query the turns table to understand what kinds of prompts the user writes and how conversations flow.
352
3. Query session_files to see which files and tools are used most frequently.
353
4. Query session_refs to see PR/issue/commit activity patterns.
354
5. Based on ALL this data, provide 3-5 specific, actionable tips grounded in actual usage patterns.
355
356
Analysis dimensions to explore:
357
- **Prompting patterns**: Are user messages vague or specific? Do they provide context? Average turns per session?
358
- **Tool usage**: Which tools are used most? Are there underutilized tools that could help?
359
- **Session patterns**: How long are sessions? Are there many short abandoned sessions?
360
- **File patterns**: Which areas of the codebase get the most attention? Any repeated edits to the same files?
361
- **Workflow**: Is the user leveraging agent mode, inline chat, custom instructions, prompt files?
362
363
Query guidelines:
364
- Only one query per call — do not combine multiple statements with semicolons.
365
- Always use LIMIT (max 100) in your queries and prefer aggregations (COUNT, GROUP BY) over raw row dumps.
366
- Use the turns table to understand conversation quality, not just session metadata.`;
367
368
if (extra) {
369
prompt += `\n\nThe user is especially interested in: ${extra}`;
370
}
371
372
this._pendingSystemPrompt = prompt;
373
this._sendTelemetry('tips', 0, 0);
374
return this._delegateToToolCallingHandler(conversation, request, stream, token, documentContext, location, chatTelemetry);
375
}
376
377
private async _handleFreeForm(
378
userQuery: string,
379
stream: vscode.ChatResponseStream,
380
request: vscode.ChatRequest,
381
token: CancellationToken,
382
conversation: Conversation,
383
documentContext: IDocumentContext | undefined,
384
location: ChatLocation,
385
chatTelemetry: ChatTelemetryBuilder,
386
): Promise<vscode.ChatResult> {
387
const hasCloud = this._indexingPreference.hasCloudConsent();
388
const schema = this._getSchemaDescription(hasCloud);
389
390
this._pendingSystemPrompt = `The user is asking about their Copilot session history. Use the session_store_sql tool to query the data and answer their question.
391
392
${schema}
393
394
User's question: ${userQuery}
395
396
Use the session_store_sql tool to run queries. Start with a broad query, then drill down as needed.
397
- Only SELECT queries are allowed
398
- Only one query per call — do not combine multiple statements with semicolons
399
- Always use LIMIT (max 100) and prefer aggregations (COUNT, GROUP BY) over raw row dumps
400
- Query the **turns** table for conversation content (user_message, assistant_response) — this gives the richest insight into what happened
401
- Query **session_files** for file paths and tool usage patterns
402
- Query **session_refs** for PR/issue/commit links
403
- Join tables to correlate sessions with their turns, files, and refs for complete answers
404
- Present results in a clear, readable format with markdown tables or bullet points`;
405
406
this._sendTelemetry('freeform', 0, 0);
407
return this._delegateToToolCallingHandler(conversation, request, stream, token, documentContext, location, chatTelemetry);
408
}
409
410
private async _delegateToToolCallingHandler(
411
conversation: Conversation,
412
request: vscode.ChatRequest,
413
stream: vscode.ChatResponseStream,
414
token: CancellationToken,
415
documentContext: IDocumentContext | undefined,
416
location: ChatLocation,
417
chatTelemetry: ChatTelemetryBuilder,
418
): Promise<vscode.ChatResult> {
419
const handler = this._instantiationService.createInstance(
420
DefaultIntentRequestHandler,
421
this,
422
conversation,
423
request,
424
stream,
425
token,
426
documentContext,
427
location,
428
chatTelemetry,
429
{ maxToolCallIterations: 8, temperature: 0, confirmOnMaxToolIterations: false },
430
undefined,
431
);
432
return handler.getResult();
433
}
434
435
private _sendTelemetry(subcommand: string, localSessionCount: number, cloudSessionCount: number): void {
436
const hasCloudConsent = this._indexingPreference.hasCloudConsent();
437
const querySource = hasCloudConsent ? (localSessionCount > 0 ? 'both' : 'cloud') : 'local';
438
/* __GDPR__
439
"chronicle" : {
440
"owner": "vijayu",
441
"comment": "Tracks chronicle subcommand usage, data sources, and query failures",
442
"subcommand": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The chronicle subcommand: standup, tips, freeform, or reindex." },
443
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The data source used: local, cloud, both, or cloudRefs." },
444
"error": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "comment": "Truncated error message." },
445
"force": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether force mode was used (reindex only)." },
446
"cancelled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the operation was cancelled (reindex only)." },
447
"localSessionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of local sessions used." },
448
"cloudSessionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of cloud sessions used." },
449
"totalSessionCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total sessions used." },
450
"skippedCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of sessions skipped during reindex." },
451
"durationMs": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Duration of the reindex operation in milliseconds." }
452
}
453
*/
454
this._telemetryService.sendMSFTTelemetryEvent('chronicle', {
455
subcommand,
456
querySource,
457
}, {
458
localSessionCount,
459
cloudSessionCount,
460
totalSessionCount: localSessionCount + cloudSessionCount,
461
});
462
}
463
464
private _getSchemaDescription(hasCloud: boolean): string {
465
return hasCloud
466
? `Available tables (cloud SQL syntax):
467
- **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.
468
- **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.
469
- **session_files**: session_id, file_path, tool_name, turn_index. Tracks which files were read/edited and which tools were used.
470
- **session_refs**: session_id, ref_type (commit/pr/issue), ref_value, turn_index. Tracks PRs created, issues referenced, commits made.
471
472
Use \`now() - INTERVAL '1 day'\` for date math, \`ILIKE\` for text search.
473
Always JOIN sessions with turns to get session content — do not rely on sessions.summary alone.`
474
: `Available tables (SQLite syntax — local):
475
- **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.
476
- **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.
477
- **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.
478
- **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.
479
- **search_index**: FTS5 table. Use \`WHERE search_index MATCH 'query'\`
480
481
Use \`datetime('now', '-1 day')\` for date math.
482
Join sessions with turns/files/refs using session_id for complete analysis.`;
483
}
484
485
/**
486
* Query the local SQLite session store for sessions and refs.
487
*/
488
private _queryLocalStore(): { sessions: AnnotatedSession[]; refs: AnnotatedRef[] } {
489
try {
490
// Use fallback (no authorizer) since these are known-safe SELECT queries
491
const rawSessions = this._sessionStore.executeReadOnlyFallback(SESSIONS_QUERY_SQLITE) as unknown as SessionRow[];
492
const sessions: AnnotatedSession[] = rawSessions.map(s => ({ ...s, source: 'vscode' as const }));
493
494
let refs: AnnotatedRef[] = [];
495
if (sessions.length > 0) {
496
const ids = sessions.map(s => s.id);
497
const rawRefs = this._sessionStore.executeReadOnlyFallback(buildRefsQuery(ids)) as unknown as RefRow[];
498
refs = rawRefs.map(r => ({ ...r, source: 'vscode' as const }));
499
}
500
501
return { sessions, refs };
502
} catch (err) {
503
504
this._telemetryService.sendMSFTTelemetryErrorEvent('chronicle', {
505
subcommand: 'standup',
506
querySource: 'local',
507
error: err instanceof Error ? err.message.substring(0, 100) : 'unknown',
508
}, {});
509
return { sessions: [], refs: [] };
510
}
511
}
512
513
private async _queryCloudStore(): Promise<{ sessions: AnnotatedSession[]; refs: AnnotatedRef[] }> {
514
const empty = { sessions: [] as AnnotatedSession[], refs: [] as AnnotatedRef[] };
515
try {
516
const client = new CloudSessionStoreClient(this._tokenManager, this._authService, this._fetcherService);
517
518
const sessionsResult = await client.executeQuery(SESSIONS_QUERY_CLOUD);
519
if (!sessionsResult || sessionsResult.rows.length === 0) {
520
return empty;
521
}
522
523
const sessions: AnnotatedSession[] = sessionsResult.rows.map(r => ({
524
id: r.id as string,
525
summary: r.summary as string | undefined,
526
branch: r.branch as string | undefined,
527
repository: r.repository as string | undefined,
528
agent_name: r.agent_name as string | undefined,
529
agent_description: r.agent_description as string | undefined,
530
created_at: r.created_at as string | undefined,
531
updated_at: r.updated_at as string | undefined,
532
source: 'cloud' as const,
533
}));
534
535
// Query refs for these sessions
536
const ids = sessions.map(s => s.id);
537
let refs: AnnotatedRef[] = [];
538
try {
539
const refsQuery = `SELECT session_id, ref_type, ref_value FROM session_refs WHERE session_id IN (${ids.map(s => `'${s.replace(/'/g, '\'\'')}'`).join(',')})`;
540
const refsResult = await client.executeQuery(refsQuery);
541
if (refsResult && refsResult.rows.length > 0) {
542
refs = refsResult.rows.map(r => ({
543
session_id: r.session_id as string,
544
ref_type: r.ref_type as 'commit' | 'pr' | 'issue',
545
ref_value: r.ref_value as string,
546
source: 'cloud' as const,
547
}));
548
}
549
} catch (refsErr) {
550
551
this._telemetryService.sendMSFTTelemetryErrorEvent('chronicle', {
552
subcommand: 'standup',
553
querySource: 'cloudRefs',
554
error: refsErr instanceof Error ? refsErr.message.substring(0, 100) : 'unknown',
555
}, {});
556
}
557
558
return { sessions, refs };
559
} catch (err) {
560
561
this._telemetryService.sendMSFTTelemetryErrorEvent('chronicle', {
562
subcommand: 'standup',
563
querySource: 'cloud',
564
error: err instanceof Error ? err.message.substring(0, 100) : 'unknown',
565
}, {});
566
return empty;
567
}
568
}
569
570
/**
571
* Query cloud turns and files for a specific set of session IDs (called after capping).
572
*/
573
private async _queryCloudTurnsAndFiles(sessionIds: string[]): Promise<{ turns: SessionTurnInfo[]; files: SessionFileInfo[] }> {
574
const empty = { turns: [] as SessionTurnInfo[], files: [] as SessionFileInfo[] };
575
try {
576
const client = new CloudSessionStoreClient(this._tokenManager, this._authService, this._fetcherService);
577
const inClause = sessionIds.map(s => `'${s.replace(/'/g, '\'\'')}'`).join(',');
578
579
let turns: SessionTurnInfo[] = [];
580
try {
581
const 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`;
582
const turnsResult = await client.executeQuery(turnsQuery);
583
if (turnsResult && turnsResult.rows.length > 0) {
584
turns = turnsResult.rows.map(r => ({
585
session_id: r.session_id as string,
586
turn_index: r.turn_index as number,
587
user_message: r.user_message as string | undefined,
588
assistant_response: r.assistant_response as string | undefined,
589
}));
590
}
591
} catch { /* non-fatal */ }
592
593
let files: SessionFileInfo[] = [];
594
try {
595
const filesQuery = `SELECT session_id, file_path, tool_name FROM session_files WHERE session_id IN (${inClause}) LIMIT 200`;
596
const filesResult = await client.executeQuery(filesQuery);
597
if (filesResult && filesResult.rows.length > 0) {
598
files = filesResult.rows.map(r => ({
599
session_id: r.session_id as string,
600
file_path: r.file_path as string,
601
tool_name: r.tool_name as string | undefined,
602
}));
603
}
604
} catch { /* non-fatal */ }
605
606
return { turns, files };
607
} catch {
608
return empty;
609
}
610
}
611
612
async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {
613
const { location, request } = invocationContext;
614
const endpoint = await this.endpointProvider.getChatEndpoint(request);
615
const systemPrompt = this._pendingSystemPrompt ?? '';
616
this._pendingSystemPrompt = undefined;
617
return this._instantiationService.createInstance(
618
ChronicleIntentInvocation, this, location, endpoint, request, systemPrompt
619
);
620
}
621
}
622
623
class ChronicleIntentInvocation extends RendererIntentInvocation implements IIntentInvocation {
624
625
readonly linkification: IntentLinkificationOptions = { disable: false };
626
627
constructor(
628
intent: IIntent,
629
location: ChatLocation,
630
endpoint: IChatEndpoint,
631
private readonly request: vscode.ChatRequest,
632
private readonly systemPrompt: string,
633
@IInstantiationService private readonly instantiationService: IInstantiationService,
634
@IToolsService private readonly toolsService: IToolsService,
635
) {
636
super(intent, location, endpoint);
637
}
638
639
async createRenderer(promptContext: IBuildPromptContext, endpoint: IChatEndpoint, _progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, _token: vscode.CancellationToken) {
640
return PromptRenderer.create(this.instantiationService, endpoint, ChroniclePrompt, {
641
endpoint,
642
promptContext,
643
systemPrompt: this.systemPrompt,
644
});
645
}
646
647
getAvailableTools(): vscode.LanguageModelToolInformation[] | Promise<vscode.LanguageModelToolInformation[]> | undefined {
648
return this.toolsService.getEnabledTools(this.request, this.endpoint,
649
tool => tool.name === ToolName.SessionStoreSql
650
);
651
}
652
}
653
654