Skip to content

Commit 805a91d

Browse files
fix(sync): refresh vscode copilot workspace metadata
VS Code Copilot was provider-aware for workspace.json freshness, but this stack still runs legacy sync writes. Without mirroring that freshness in the legacy process path, metadata-only workspace renames could be classified but then skipped against the unchanged chat transcript. Move the Copilot IDE providers into shadow compare on their migration branch, preserve .jsonl priority during provider changed-path classification, and store composite workspace freshness for VS Code Copilot sessions while both shapes run. Validation: go test -tags "fts5" ./internal/sync -run 'TestSyncPathsVSCodeCopilot(JSONLPriority|WorkspaceMetadataRefreshesProject)' -count=1; go test -tags "fts5" ./internal/parser -run 'Test(VSCodeCopilotProvider|VisualStudioCopilotProvider|ProviderMigrationModes)' -count=1; go test -tags "fts5" ./internal/sync -count=1; go test -tags "fts5" ./internal/parser -count=1; go vet ./...; git diff --check
1 parent 64a3cd1 commit 805a91d

4 files changed

Lines changed: 170 additions & 6 deletions

File tree

internal/parser/provider_migration.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ var providerMigrationModes = map[AgentType]ProviderMigrationMode{
3030
AgentIflow: ProviderMigrationShadowCompare,
3131
AgentAmp: ProviderMigrationShadowCompare,
3232
AgentZencoder: ProviderMigrationShadowCompare,
33-
AgentVSCodeCopilot: ProviderMigrationLegacyOnly,
34-
AgentVSCopilot: ProviderMigrationLegacyOnly,
33+
AgentVSCodeCopilot: ProviderMigrationShadowCompare,
34+
AgentVSCopilot: ProviderMigrationShadowCompare,
3535
AgentPi: ProviderMigrationShadowCompare,
3636
AgentQwen: ProviderMigrationShadowCompare,
3737
AgentCommandCode: ProviderMigrationShadowCompare,

internal/parser/vscode_copilot_provider.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func (s vscodeCopilotSourceSet) SourcesForChangedPath(
181181
if len(sources) > 0 {
182182
return sources, nil
183183
}
184-
source, ok := s.sourceRefForChangedPath(root, req.Path)
184+
source, ok := s.sourceRefForChangedPath(root, req)
185185
if ok {
186186
return []SourceRef{source}, nil
187187
}
@@ -322,8 +322,13 @@ func (s vscodeCopilotSourceSet) sourceRef(root, path string) (SourceRef, bool) {
322322
}
323323

324324
func (s vscodeCopilotSourceSet) sourceRefForChangedPath(
325-
root, path string,
325+
root string,
326+
req ChangedPathRequest,
326327
) (SourceRef, bool) {
328+
path := req.Path
329+
if req.EventKind != "remove" && vscodeCopilotJSONLPreferredOver(path) {
330+
return SourceRef{}, false
331+
}
327332
if source, ok := s.sourceRef(root, path); ok {
328333
return source, true
329334
}
@@ -471,6 +476,14 @@ func vscodeCopilotPreferredExistingPath(path string) string {
471476
return ""
472477
}
473478

479+
func vscodeCopilotJSONLPreferredOver(path string) bool {
480+
base, ok := strings.CutSuffix(path, ".json")
481+
if !ok {
482+
return false
483+
}
484+
return IsRegularFile(base + ".jsonl")
485+
}
486+
474487
func vscodeCopilotSourceHash(path, workspacePath string) (string, error) {
475488
hash, err := hashJSONLSourceFile(path)
476489
if err != nil {

internal/sync/engine.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4350,6 +4350,11 @@ func (e *Engine) processFile(
43504350
// of staying skipped on the unchanged transcript mtime.
43514351
mtime = vibeEffectiveInfo(file.Path, info).ModTime().UnixNano()
43524352
}
4353+
if file.Agent == parser.AgentVSCodeCopilot {
4354+
mtime = e.vscodeCopilotEffectiveInfo(file.Path, info).
4355+
ModTime().
4356+
UnixNano()
4357+
}
43534358
cacheSkip := e.shouldCacheSkip(file)
43544359

43554360
// Skip files cached from a previous sync (parse errors
@@ -5865,7 +5870,8 @@ func (e *Engine) processZencoder(
58655870
func (e *Engine) processVSCodeCopilot(
58665871
file parser.DiscoveredFile, info os.FileInfo,
58675872
) processResult {
5868-
if e.shouldSkipByPath(file.Path, info) {
5873+
effectiveInfo := e.vscodeCopilotEffectiveInfo(file.Path, info)
5874+
if !file.ForceParse && e.shouldSkipByPath(file.Path, effectiveInfo) {
58695875
return processResult{skip: true}
58705876
}
58715877

@@ -5878,8 +5884,10 @@ func (e *Engine) processVSCodeCopilot(
58785884
if sess == nil {
58795885
return processResult{}
58805886
}
5887+
sess.File.Size = effectiveInfo.Size()
5888+
sess.File.Mtime = effectiveInfo.ModTime().UnixNano()
58815889

5882-
hash, err := ComputeFileHash(file.Path)
5890+
hash, err := e.vscodeCopilotCompositeHash(file.Path)
58835891
if err == nil {
58845892
sess.File.Hash = hash
58855893
}
@@ -5895,6 +5903,70 @@ func (e *Engine) processVSCodeCopilot(
58955903
}
58965904
}
58975905

5906+
func (e *Engine) vscodeCopilotEffectiveInfo(
5907+
path string, info os.FileInfo,
5908+
) os.FileInfo {
5909+
workspacePath := e.vscodeCopilotWorkspaceManifestPath(path)
5910+
if workspacePath == "" {
5911+
return info
5912+
}
5913+
workspaceInfo, err := os.Stat(workspacePath)
5914+
if err != nil || workspaceInfo.IsDir() {
5915+
return info
5916+
}
5917+
size := info.Size() + workspaceInfo.Size()
5918+
mtime := info.ModTime().UnixNano()
5919+
if workspaceMtime := workspaceInfo.ModTime().UnixNano(); workspaceMtime > mtime {
5920+
mtime = workspaceMtime
5921+
}
5922+
return fakeSnapshotInfo{fSize: size, fMtime: mtime}
5923+
}
5924+
5925+
func (e *Engine) vscodeCopilotCompositeHash(path string) (string, error) {
5926+
chatHash, err := ComputeFileHash(path)
5927+
if err != nil {
5928+
return "", err
5929+
}
5930+
workspacePath := e.vscodeCopilotWorkspaceManifestPath(path)
5931+
if workspacePath == "" {
5932+
return chatHash, nil
5933+
}
5934+
if info, err := os.Stat(workspacePath); err != nil || info.IsDir() {
5935+
return chatHash, nil
5936+
}
5937+
workspaceHash, err := ComputeFileHash(workspacePath)
5938+
if err != nil {
5939+
return "", err
5940+
}
5941+
return ComputeHash(strings.NewReader(
5942+
"chat\x00" + chatHash + "\x00workspace\x00" + workspaceHash,
5943+
))
5944+
}
5945+
5946+
func (e *Engine) vscodeCopilotWorkspaceManifestPath(path string) string {
5947+
path = filepath.Clean(path)
5948+
for _, root := range e.agentDirs[parser.AgentVSCodeCopilot] {
5949+
if root == "" {
5950+
continue
5951+
}
5952+
root = filepath.Clean(root)
5953+
rel, ok := isUnder(root, path)
5954+
if !ok {
5955+
continue
5956+
}
5957+
parts := strings.Split(filepath.ToSlash(rel), "/")
5958+
if len(parts) != 4 ||
5959+
parts[0] != "workspaceStorage" ||
5960+
parts[2] != "chatSessions" ||
5961+
(!strings.HasSuffix(parts[3], ".json") &&
5962+
!strings.HasSuffix(parts[3], ".jsonl")) {
5963+
continue
5964+
}
5965+
return filepath.Join(root, "workspaceStorage", parts[1], "workspace.json")
5966+
}
5967+
return ""
5968+
}
5969+
58985970
func (e *Engine) processOpenClaw(
58995971
file parser.DiscoveredFile, info os.FileInfo,
59005972
) processResult {

internal/sync/engine_integration_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6799,6 +6799,85 @@ func TestSyncPathsVSCodeCopilotJSONLPriority(t *testing.T) {
67996799
assert.Equal(t, 0, len(page.Sessions), "expected 0 sessions (.json skipped), got %d", len(page.Sessions))
68006800
}
68016801

6802+
func TestSyncPathsVSCodeCopilotWorkspaceMetadataRefreshesProject(t *testing.T) {
6803+
if testing.Short() {
6804+
t.Skip("skipping integration test")
6805+
}
6806+
6807+
dir := t.TempDir()
6808+
vscDir := filepath.Join(dir, "vscode")
6809+
hashDir := filepath.Join(vscDir, "workspaceStorage", "abc123")
6810+
chatDir := filepath.Join(hashDir, "chatSessions")
6811+
workspacePath := filepath.Join(hashDir, "workspace.json")
6812+
6813+
database := dbtest.OpenTestDB(t)
6814+
engine := sync.NewEngine(database, sync.EngineConfig{
6815+
AgentDirs: map[parser.AgentType][]string{
6816+
parser.AgentVSCodeCopilot: {vscDir},
6817+
},
6818+
Machine: "local",
6819+
})
6820+
6821+
writeWorkspace := func(name string) {
6822+
t.Helper()
6823+
dbtest.WriteTestFile(t, workspacePath, fmt.Appendf(nil,
6824+
`{"folder":"file:///Users/alice/code/%s"}`,
6825+
name,
6826+
))
6827+
}
6828+
6829+
uuid := "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
6830+
session := fmt.Sprintf(
6831+
`{"version":1,"sessionId":"%s",`+
6832+
`"creationDate":1704103200000,`+
6833+
`"lastMessageDate":1704103260000,`+
6834+
`"requests":[{"requestId":"r1",`+
6835+
`"message":{"text":"hello"},`+
6836+
`"response":[{"value":"hi"}],`+
6837+
`"timestamp":1704103200000}]}`,
6838+
uuid,
6839+
)
6840+
jsonlPath := filepath.Join(chatDir, uuid+".jsonl")
6841+
6842+
writeWorkspace("one")
6843+
dbtest.WriteTestFile(
6844+
t, jsonlPath,
6845+
[]byte(`{"kind":0,"v":`+session+`}`),
6846+
)
6847+
6848+
engine.SyncPaths([]string{jsonlPath})
6849+
assertSessionState(
6850+
t, database, "vscode-copilot:"+uuid,
6851+
func(sess *db.Session) {
6852+
assert.Equal(t, "one", sess.Project)
6853+
},
6854+
)
6855+
6856+
info, err := os.Stat(jsonlPath)
6857+
require.NoError(t, err, "stat vscode copilot session")
6858+
engine.InjectSkipCache(map[string]int64{
6859+
jsonlPath: info.ModTime().UnixNano(),
6860+
})
6861+
6862+
writeWorkspace("two")
6863+
engine.SyncPaths([]string{workspacePath})
6864+
assertSessionState(
6865+
t, database, "vscode-copilot:"+uuid,
6866+
func(sess *db.Session) {
6867+
assert.Equal(t, "two", sess.Project)
6868+
},
6869+
)
6870+
6871+
writeWorkspace("three")
6872+
engine.SyncPaths([]string{jsonlPath, workspacePath})
6873+
assertSessionState(
6874+
t, database, "vscode-copilot:"+uuid,
6875+
func(sess *db.Session) {
6876+
assert.Equal(t, "three", sess.Project)
6877+
},
6878+
)
6879+
}
6880+
68026881
func TestPiSessionIntegration(t *testing.T) {
68036882
if testing.Short() {
68046883
t.Skip("skipping integration test")

0 commit comments

Comments
 (0)