Skip to content

fix: derive synced customization nonce from file content (build fix for vscode-engineering#3814) #335189

Description

@vs-code-engineering

Build failure

The Electron unit-test job failed on main with one deterministic failing test in SyncedCustomizationBundler:

1) SyncedCustomizationBundler
     bundles complete SKILL.md directories:
  AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to: '737882158'
      at syncedCustomizationBundler.test.js:200:12

The assertion assert.notStrictEqual(updatedResult!.ref.nonce, result.ref.nonce) expects the bundle nonce to change after a reference file inside a skill directory is edited, but the recomputed nonce was identical. The two subsequent macOS-stage failures (non-production check and Publish Log Files, both reporting a missing .build/logs) are cascades of the test job aborting.

Root cause

SyncedCustomizationBundler._bundle computed its change-detection nonce purely from file metadata:

entries.push({ ..., hashPart: `${hashKey}:${source.mtime}:${source.size}` });

The nonce therefore only reflects each file's mtime and size. When a file's content is edited to the same byte length within the filesystem's mtime resolution — as in the regression test, where references/notes.md changes from reference content (17 bytes) to updated reference (17 bytes) and the in-memory provider stamps both writes with the same Date.now() millisecond — the metadata is unchanged, so the nonce collides. The bundler then hits its "nothing changed" fast path and reuses the previous bundle, silently dropping the real content change from downstream agent-host syncs. This is a deterministic correctness bug, not a flake.

How the fix works

The nonce is now derived from actual file content instead of metadata. File contents are read before the nonce is computed, and each file contributes a length-sensitive content hash (hashBytes) to the nonce inputs. A same-size content edit now produces a different hash, so the nonce changes and the bundle is correctly rewritten and re-synced. The metadata (mtime/size) is no longer part of the nonce. The write-skip optimization is preserved: the fast path still short-circuits when the content-based nonce is unchanged. Reading contents up front is not extra work — the previous code already read every file's contents immediately afterward to write the plugin tree; only the ordering relative to the nonce check moved.

Rollback evaluation

Recommendation: Do not roll back. Public culprit: 400640316376fbbdc6e901a7f5c23810bd5a9329 (#334568) added the regression test that exposes this pre-existing metadata-only nonce; the nonce logic itself predates it (introduced in #333827, 42b530c885a). A reverting change would reintroduce the disposal-race log-noise fixes that #334568 also landed and would not correct the underlying content-insensitivity, so fix-forward is both safer and more complete. Owners to consult: @pwang347

Validation

Could not run npm ci, the build, or the unit test in this environment (dependency install and node execution are blocked by the sandbox). The change is verified by source inspection: hashBytes iterates every content byte seeded with the length, so the two 17-byte reference-file variants in the failing test hash differently, making updatedResult.ref.nonce !== result.ref.nonce hold. TypeScript usage is consistent (VSBuffer.buffer is a Uint8Array; numberHash is exported from base/common/hash).

Risk

Low and bounded. The change is confined to nonce derivation inside SyncedCustomizationBundler. Behavior is strictly more correct: the nonce now changes in more cases (content edits) and is stable across unrelated metadata churn (e.g. mtime-only touches no longer force a re-sync). No public API changes.

Recommended reviewer

Recommended owner: @pwang347

Fixes microsoft/vscode-engineering#3814

Generated by build-fix · opus48 · 373.7 AIC · ⌖ 13.6 AIC · ⊞ 11.7K · ◷


Note

This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch fix/synced-customization-nonce-content-3814-ad7e99b038e11199.

Original error: ERR_API: [2026-09-09T01:22:16.072Z] create pull request in microsoft/vscode failed (attempt 1)

Original error: Validation Failed: {"resource":"PullRequest","code":"custom","field":"fork_collab","message":"fork_collab Fork collab can't be granted by someone without permission"} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request
Retryable: false
Suggestion: This error cannot be resolved by retrying. Please check the error details and fix the underlying issue.

To create the pull request manually:

gh pr create --title "fix: derive synced customization nonce from file content (build fix for vscode-engineering#3814)" --base main --head vscodebot-pr:fix/synced-customization-nonce-content-3814-ad7e99b038e11199 --repo microsoft/vscode
Show patch preview (101 of 101 lines)
From d38d81c9ca98eac83ed27456a5e1100e8cd9b9ab Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: 333552ca38d5e15c8b734157159fcfcae9553c87
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 9 Sep 2026 01:15:48 +0000
Subject: [PATCH] fix: derive synced customization nonce from file content

Fixes microsoft/vscode-engineering#3814

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../agentHost/syncedCustomizationBundler.ts   | 36 +++++++++++++------
 1 file changed, 25 insertions(+), 11 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/syncedCustomizationBundler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/syncedCustomizationBundler.ts
index 6aea301f226..bbe57338000 100644
--- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/syncedCustomizationBundler.ts
+++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/syncedCustomizationBundler.ts
@@ -11,7 +11,7 @@ import { equals } from '../../../../../../base/common/objects.js';
 import { ResourceMap } from '../../../../../../base/common/map.js';
 import { basename, dirname, extUri } from '../../../../../../base/common/resources.js';
 import { URI } from '../../../../../../base/common/uri.js';
-import { hash } from '../../../../../../base/common/hash.js';
+import { hash, numberHash } from '../../../../../../base/common/hash.js';
 import { IFileService, IFileStatWithPartialMetadata } from '../../../../../../platform/files/common/files.js';
 import { ILogService } from '../../../../../../platform/log/common/log.js';
 import { IMcpServerConfiguration } from '../../../../../../platform/mcp/common/mcpPlatformTypes.js';
@@ -32,6 +32,15 @@ const FILE_OPERATION_CONCURRENCY = 10;
 const SKILL_DIRECTORY_IGNORE = new IgnoreFile('.git\nnode_modules\n', '/', undefined, true);
 const bundleSequencer = new SequencerByKey<string>();
 
+/** Computes a stable content hash over the given bytes (length-se
... (truncated)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions