Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d6fb8b7
cli: skip bare top-level directory entries when extracting archives
connor4312 May 21, 2026
7381c1e
fix: remove unsupported decommit_pooled_pages flag (#317794)
deepak1556 May 21, 2026
97e51c0
Temporarily disable agenthost.enabled again by default (#317797)
roblourens May 21, 2026
df5bd39
more robust css rules for issue reporter
Giuspepe May 21, 2026
d1a4f1b
fix: guard _resize against disposed _resizeDebouncer (fixes #317787) …
vs-code-engineering[bot] May 21, 2026
3d13ddb
Merge pull request #317804 from microsoft/issue-reporter-robust-css
Giuspepe May 21, 2026
eb2bca7
build: persist ADO buildId/definitionId on published build records (#…
bryanchen-d May 21, 2026
ce2a4a6
address review: only skip empty-relative-path entries when they are d…
connor4312 May 21, 2026
bd01ba3
Revert: Use copilot/copilot-utility-small for terminal-tool steering …
roblourens May 21, 2026
9cd7264
feat(copilot): opt-in HTTP cache for the Node fetch fetcher (#317721)
deepak1556 May 21, 2026
5bd54fc
Merge pull request #317792 from microsoft/connor4312/fix-extract-safe…
connor4312 May 21, 2026
9e7a624
Enhance session customization handling and simplify sync logic (#317700)
DonJayamanne May 21, 2026
bed9e75
Show a warning when showing the rendered markdown diff
mjbvz May 21, 2026
b541e17
Add Model, ConversationId, and RequestId to the gent.tool.responsele…
tbogoodnews May 21, 2026
615de17
Merge pull request #307886 from yogeshwaran-c/feat/watch-copy-all-306116
yogeshwaran-c May 21, 2026
b0bad8c
Add turn to the GHCP telmetry event response.success
tbogoodnews May 21, 2026
f4c17b3
Potential fix for pull request finding
mjbvz May 21, 2026
e7137a3
Integrating MXC for windows sandboxing (#317669)
dileepyavan May 21, 2026
db05611
Report existing turn in response success telemetry
tbogoodnews May 21, 2026
05d3f35
[cherry-pick] Reverting fetch web tool changes for sandboxing (#317199)
vs-code-engineering[bot] May 21, 2026
507d09d
Merge pull request #317829 from microsoft/dev/mjbvz/main-llama
mjbvz May 21, 2026
bc13ab1
Chronicle: Add cost-tips command (#317809)
vijayupadya May 21, 2026
8725a47
Fix tool telemetry test spy setup
tbogoodnews May 21, 2026
1cdf8fa
Merge branch 'main' into tbogoodnews/add-turns-conversationid-to-tool…
tbogoodnews May 21, 2026
7680cf4
Browser: support element selection in subframes (#317405)
kycutler May 21, 2026
907ffc4
Fix entitlements rendering incorrectly (#317857)
lramos15 May 21, 2026
46050be
Add browser emulation toolbar (#317831)
kycutler May 21, 2026
a2d2b10
Long context pricing (#317820)
lramos15 May 21, 2026
127d936
Fix duplicate terminal tool render from pastTenseMessage in agent hos…
roblourens May 21, 2026
c883dd5
refactor and add tests for agentCustomizationContentExpander (#317773)
aeschli May 21, 2026
65c56ba
copilot: truncate long lines in read_file results (#317862)
connor4312 May 21, 2026
8bda8dc
Merge pull request #317855 from tbogoodnews/tbogoodnews/add-turns-con…
connor4312 May 21, 2026
650352e
agent-host: stop config pickers flashing during resolveSessionConfig …
connor4312 May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion build/azure-pipelines/common/createBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ async function main(): Promise<void> {
const queuedBy = getEnv('BUILD_QUEUEDBY');
const sourceBranch = getEnv('BUILD_SOURCEBRANCH');
const version = _version + (quality === 'stable' ? '' : `-${quality}`);
const buildId = process.env['BUILD_BUILDID'];
const definitionId = process.env['SYSTEM_DEFINITIONID'];

console.log('Creating build...');
console.log('Quality:', quality);
Expand All @@ -49,7 +51,9 @@ async function main(): Promise<void> {
firstReleaseTimestamp: null,
history: [
{ event: 'created', timestamp }
]
],
buildId,
definitionId
};

const aadCredentials = new ClientAssertionCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, () => Promise.resolve(process.env['AZURE_ID_TOKEN']!));
Expand Down
18 changes: 16 additions & 2 deletions cli/src/util/extract_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@ pub fn prepare_extraction_root(root: &Path) -> Result<PathBuf, WrappedError> {
}

/// Joins an archive entry's relative path under `root_canonical` after
/// rejecting any entry that would land outside the root. Rejects absolute
/// paths, drive prefixes, and `..` traversal that escapes the root.
/// rejecting any entry that would land outside the root. Rejects empty
/// entries, absolute paths, drive prefixes, and `..` traversal that escapes
/// the root.
pub fn safe_extract_join(root_canonical: &Path, entry: &Path) -> Result<PathBuf, WrappedError> {
if entry.as_os_str().is_empty() {
return Err(wrap(
"empty extraction entry",
"refusing to extract empty archive entry".to_string(),
));
}

for component in entry.components() {
if matches!(component, Component::Prefix(_) | Component::RootDir) {
return Err(wrap(
Expand Down Expand Up @@ -170,6 +178,12 @@ mod tests {
assert_eq!(ok, root.join("a/b/c"));
}

#[test]
fn safe_extract_join_rejects_empty_relative() {
let root = fs::canonicalize(std::env::temp_dir()).unwrap();
assert!(safe_extract_join(&root, Path::new("")).is_err());
}

#[cfg(unix)]
#[test]
fn validate_symlink_rejects_escapes() {
Expand Down
68 changes: 68 additions & 0 deletions cli/src/util/tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ where
entry_path.into_owned()
};

// Skip bare top-level directory entries (e.g. "vscode-server-linux-x64/")
// once their single segment has been stripped. They contribute no file
// to extract, and the directory will be created on demand for nested
// entries below. Only directory entries are skipped; non-directory
// entries with an empty relative path fall through to
// `safe_extract_join`, which rejects them.
if relative.as_os_str().is_empty() && entry.header().entry_type().is_dir() {
return Ok(());
}

let path = safe_extract_join(&canonical_root, &relative)?;

if let Some(p) = path.parent() {
Expand Down Expand Up @@ -133,3 +143,61 @@ pub fn has_gzip_header(path: &Path) -> std::io::Result<(File, bool)> {

Ok((file, header[0] == 0x1f && header[1] == 0x8b))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::util::io::SilentCopyProgress;
use flate2::write::GzEncoder;
use flate2::Compression;
use tar::{Builder, Header};

fn write_dir_entry(builder: &mut Builder<GzEncoder<Vec<u8>>>, name: &str) {
let mut header = Header::new_gnu();
header.set_size(0);
header.set_entry_type(tar::EntryType::Directory);
header.set_mode(0o755);
header.set_cksum();
builder.append_data(&mut header, name, std::io::empty()).unwrap();
}

fn write_file_entry(builder: &mut Builder<GzEncoder<Vec<u8>>>, name: &str, contents: &[u8]) {
let mut header = Header::new_gnu();
header.set_size(contents.len() as u64);
header.set_entry_type(tar::EntryType::Regular);
header.set_mode(0o644);
header.set_cksum();
builder.append_data(&mut header, name, contents).unwrap();
}

/// Regression test for #317660: a tarball with a bare top-level directory
/// entry (e.g. `prefix/`) plus files underneath used to fail extraction
/// with "extraction path resolves outside root" because the bare entry
/// produced an empty relative path after segment-stripping, whose parent
/// walked above the extraction root.
#[test]
fn decompress_tarball_with_bare_top_level_dir_entry() {
let tmp = tempfile::tempdir().unwrap();
let tar_path = tmp.path().join("archive.tar.gz");

let buf: Vec<u8> = {
let encoder = GzEncoder::new(Vec::new(), Compression::default());
let mut builder = Builder::new(encoder);
write_dir_entry(&mut builder, "prefix/");
write_file_entry(&mut builder, "prefix/file.txt", b"hello");
write_file_entry(&mut builder, "prefix/sub/nested.txt", b"world");
builder.into_inner().unwrap().finish().unwrap()
};

fs::write(&tar_path, &buf).unwrap();

let out_dir = tmp.path().join("out");
fs::create_dir_all(&out_dir).unwrap();

let file = fs::File::open(&tar_path).unwrap();
decompress_tarball(file, &out_dir, SilentCopyProgress()).expect("extraction should succeed");

assert_eq!(fs::read(out_dir.join("file.txt")).unwrap(), b"hello");
assert_eq!(fs::read(out_dir.join("sub/nested.txt")).unwrap(), b"world");
}
}
9 changes: 9 additions & 0 deletions cli/src/util/zipper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ where
let outpath: PathBuf = match file.enclosed_name() {
Some(path) => {
let relative: PathBuf = path.iter().skip(skip_segments_no).collect();
// Skip bare top-level directory entries that become empty once
// their single segment has been stripped. Only directory entries
// are skipped; non-directory entries with an empty relative path
// fall through to `safe_extract_join`, which rejects them.
if relative.as_os_str().is_empty()
&& (file.is_dir() || file.name().ends_with('/'))
{
continue;
}
safe_extract_join(&canonical_root, &relative)?
}
None => continue,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
name: chronicle:cost-tips
description: Get personalized tips to reduce token usage and Copilot cost
---
Analyze my recent chat session history and give me personalized, data-grounded tips to reduce token usage and Copilot cost. Use the **chronicle** skill — it documents the `copilot_sessionStoreSql` tool, the session-store schema, and the Cost Tips workflow for finding expensive sessions, token-heavy patterns, and concrete habit changes.
74 changes: 74 additions & 0 deletions extensions/copilot/assets/prompts/skills/chronicle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,80 @@ Analysis dimensions to explore:

If the session store has little data, acknowledge that and suggest features to try based on what configuration you found in the workspace.

### Cost Tips

When the user asks for cost tips, ways to reduce token usage, or how to lower Copilot spend (e.g. `/chronicle:cost-tips`):

The goal is **personalized, data-grounded recommendations** for reducing token usage — not a generic checklist. Every tip must point to a specific pattern you observed in their data.

**Cost-relevant schema (in addition to the Database Schema section below)**

- **Cloud DuckDB only** — the local SQLite store does **not** record per-event token usage and has no `events` table. If the active backend is local, gate all token queries and tell the user that real token-level analysis requires enabling cloud sync (`chat.sessionSync.enabled`).
- **events** (cloud): per-event billing — rows where `type = 'assistant.usage'` carry `usage_input_tokens`, `usage_output_tokens`, `usage_model`. To break spend down by agent type, JOIN `events e` to `sessions s ON s.id = e.session_id` and group by `s.agent_name`.
- **sessions.agent_name** / **agent_description** (both backends): values like `VS Code agent` (VS Code chat), `Copilot CLI`, `Copilot Coding Agent`, `Copilot Code Review`, or custom agents/subagents. Use to break spend down by agent type.
- Use `LENGTH(user_message)` on `turns` (or `LENGTH(user_content)` on `events` where `type = 'user.message'`) to find oversized pastes.

**Step 1: Investigate cost and token patterns**

Use `copilot_sessionStoreSql` with `action: "query"`. What to investigate depends on the active backend.

*Cloud (DuckDB) — start with agent-type awareness:*

The session store mixes session types via `sessions.agent_name` (join events to sessions on `session_id` to get the agent for any per-event analysis). Your advice is only useful if you know which agents the user actually runs, so this is the **first** thing to learn.

- **Enumerate every agent in use.** Run e.g. `SELECT agent_name, agent_description, COUNT(*) AS n FROM sessions WHERE updated_at > now() - INTERVAL '30 days' GROUP BY 1, 2 ORDER BY n DESC` so you see the full inventory — official agents and any custom agents/subagents in `agent_description`. Do not assume.
- **Decide which to advise on.** Include any agent type the user can make cheaper: `VS Code agent` (VS Code chat — usually the dominant agent), `Copilot CLI` (interactive terminal), `Copilot Coding Agent` (autonomous cloud tasks), custom agents and subagents. **Always exclude** `agent_name = 'Copilot Code Review'` and any other agent the user does not drive interactively.
- **Tailor advice per agent.** VS Code agent tips (compaction, model picker, fresh chats, `.github/copilot-instructions.md`, custom skills/agents) look different from CLI tips (compaction, model switching, subagent delegation), Coding Agent tips (prompt scoping, smaller task framing), and custom-agent tips (slimming tool lists, narrowing prompts).

Then drill into cost patterns (filter `events` rows by `type = 'assistant.usage'` for billable rows):

- **Token-heavy sessions and turns** — sum `usage_input_tokens` and `usage_output_tokens` per session and per model from `events` where `type = 'assistant.usage'`. Which sessions burned the most tokens? Which models?
- **Input-to-output ratios** — when input tokens dwarf output tokens, the user is paying to re-send a bloated context every turn. Strongest signal that compaction, smaller working sets, or fresh sessions would help.
- **Model mix** — break down spend by `usage_model`. Are premium models being used for routine work (renames, simple edits, status checks) that a cheaper model could handle?
- **Per-turn growth** — within long sessions, does `usage_input_tokens` keep climbing turn-over-turn? Strong signal that compaction wasn't used.
- **Oversized pastes** — `LENGTH(user_content)` on `events` where `type = 'user.message'` to find user messages that should have been file references (also visible in `session_files` as repeated reads of the same path within one session).
- **Group cost breakdowns by `agent_name`** (and `agent_description` where useful) in at least one query so the user sees where their spend actually goes — and so you spot if a single custom agent dominates.

*Local (SQLite) — no token data; use proxies:*

- **Long sessions without compaction** — sessions with many turns and no rows in `checkpoints` (each `checkpoints` row is a successful compaction). `LEFT JOIN checkpoints c ON c.session_id = s.id WHERE c.session_id IS NULL` + a turn-count threshold gives prime candidates.
- **Late compaction** — for sessions that *do* have checkpoints, compare `checkpoints.checkpoint_number` and `created_at` against the session's turn count. A first compaction at turn 60 of an 80-turn session is far less helpful than one at turn 25.
- **Repeated large file reads** — in `session_files`, look for the same file read many times within one session, or across sessions.
- **Tool-call thrash** — sessions with many turns and repeated tool calls often indicate the agent rediscovered the same context multiple times.
- **Oversized pastes** — use `LENGTH(user_message)` on `turns` to find very long user messages that should have been file references.

*Both backends:*

- **Long-running sessions** — sessions with many turns or that span many hours drag a growing context window across every turn.
- **Repeated work** — the same file/topic showing up in many sessions, or the same agent stumbling block recurring (suggesting a custom skill, agent, or `copilot-instructions.md` entry would let the model do the work in one shot).
- **Subagent usage** — are heavyweight investigations being run in the main session (paying for their tokens to live in main context) when they could be delegated to a subagent that returns only a summary?

Drill into a few of the most expensive sessions and read the actual conversation turns to understand *why* they were expensive. Don't just report aggregates — explain the cause.

**Step 2: Map findings to features and habits**

If the current workspace has a `.github/` folder, check `.github/copilot-instructions.md`, `.github/skills/`, and `.github/agents/` to see what custom configuration already exists. Do NOT look outside the workspace. Cost-relevant capabilities to keep in mind:

- Mid-session compaction (e.g. `/compact`) to shrink the context window; for users who never compact, this is often the single biggest win.
- Model picker — switch to a cheaper model for routine work; check whether premium models are being used for simple tasks.
- Starting a fresh chat instead of continuing a bloated session.
- Subagents/delegation for offloading heavy research into a sub-context whose tokens don't accrete into the main session.
- Custom skills (`.github/skills/`) and custom agents (`.github/agents/`) so repeated workflows don't re-derive context each time.
- `.github/copilot-instructions.md` to encode project conventions the model otherwise has to be told every session.
- For cloud-enabled users, the Copilot usage view to inspect current premium-request spend.

**Step 3: Provide tips**

Give the user 3-5 specific, actionable tips. Each tip should:

- **Be grounded in their data** — reference a specific session, file, model, or pattern you observed (with rough numbers when you have them: turn counts, token totals, file-read counts, etc.).
- **Be non-obvious** — skip basics any returning user already knows. Assume they know compaction and fresh chats exist; help them notice they're not *using* them where it would matter.
- **Quantify the win when possible** — "compacting around turn 30 of that 80-turn session would have shaved ~X input tokens off every subsequent turn" is far better than "consider compacting".
- **Be concrete** — name the workflow change, command, or config file edit. If the suggestion is a custom skill or agent, sketch what it would cover.
- **Match the agent type** — if a finding is specific to one `agent_name`, say so. Don't propose CLI-only fixes for findings from Coding Agent sessions, and vice versa.

If the session store has little data (e.g., cloud store is empty, or only a handful of local sessions), say so plainly and offer 2-3 non-obvious cost-saving habits anchored in available features rather than fabricating findings. If the user is on local-only storage, end by noting that enabling `chat.sessionSync.enabled` unlocks per-event token analysis for sharper future tips.

### Search

When the user asks to search, find, or look up past sessions by keyword (e.g. `/chronicle:search <query>`):
Expand Down
8 changes: 4 additions & 4 deletions extensions/copilot/chat-lib/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion extensions/copilot/chat-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dependencies": {
"@microsoft/tiktokenizer": "^1.0.10",
"@sinclair/typebox": "^0.34.41",
"@vscode/copilot-api": "^0.2.19",
"@vscode/copilot-api": "^0.4.0",
"@vscode/l10n": "^0.0.18",
"@vscode/prompt-tsx": "^0.4.0-alpha.8",
"@vscode/tree-sitter-wasm": "0.0.5-php.2",
Expand Down
8 changes: 4 additions & 4 deletions extensions/copilot/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6619,6 +6619,13 @@
"local"
]
},
{
"path": "./assets/prompts/chronicle-cost-tips.prompt.md",
"when": "github.copilot.sessionSearch.enabled",
"sessionTypes": [
"local"
]
},
{
"path": "./assets/prompts/chronicle-reindex.prompt.md",
"when": "github.copilot.sessionSearch.enabled",
Expand Down Expand Up @@ -6885,7 +6892,7 @@
"@opentelemetry/sdk-trace-node": "^2.5.1",
"@opentelemetry/semantic-conventions": "^1.39.0",
"@sinclair/typebox": "^0.34.41",
"@vscode/copilot-api": "^0.3.1",
"@vscode/copilot-api": "^0.4.0",
"@vscode/extension-telemetry": "^1.5.1",
"@vscode/l10n": "^0.0.18",
"@vscode/prompt-tsx": "^0.4.0-alpha.8",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider {
"requestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },
"gitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id if available" },
"associatedRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Another request ID that this request is associated with (eg, the originating request of a summarization request)." },
"turn": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How many turns have been made in the conversation.", "isMeasurement": true },
"reasoningEffort": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning effort level" },
"reasoningSummary": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning summary level" },
"fetcher": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The fetcher used for the request" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvide
"requestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },
"gitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id if available" },
"associatedRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Another request ID that this request is associated with (eg, the originating request of a summarization request)." },
"turn": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How many turns have been made in the conversation.", "isMeasurement": true },
"reasoningEffort": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning effort level" },
"reasoningSummary": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning summary level" },
"fetcher": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The fetcher used for the request" },
Expand Down
Loading
Loading