Skip to content

Commit 5994e21

Browse files
authored
refactor: drive sync and remote-sync policy from provider capabilities (#1282)
Replaces four hard-coded agent switches in the sync engine and two hard-coded Trae checks in remote sync with declarative provider capabilities. `parser.Capabilities` gains a `Sync ProviderSyncSemantics` block declaring per-provider cache and freshness policy: whether the fingerprint hash joins the skip-cache key, whether freshness requires a stored-row hash match, whether a skip-cache entry can be fresh before a stored row exists, and how unchanged results from shared SQLite containers are dropped (mtime vs mtime+hash). The engine reads these declarations instead of switching on agent type; the deleted switch rationales move to the declaration and consumption-site comments. `parser.AgentDef` gains `RemoteSyncExcluded`, replacing the `def.Type == parser.AgentTrae` checks in `internal/remotesync/resolve.go` and `internal/ssh/resolve.go`. There is no behavior change: declarations reproduce the previous switch memberships exactly, and a registry-wide parity test (`TestProviderSyncSemanticsDeclarations`) asserts every registered agent's declared semantics, including zero values for undeclared agents. One removed code path — the `agent == ""` fallback in the cache-key helper — is dead-code elimination: provider resolution by `file.Agent` precedes it and fails first. Why now: a pending Omnigent provider PR adds more consumers of both capabilities; landing the refactor separately keeps that PR reviewable. Reviewers should look at: `internal/parser/capabilities.go` (the new types), `internal/sync/engine.go` (the four consumption sites), and `internal/parser/capabilities_sync_test.go` (the parity table). Co-authored-by: Wes McKinney <wesm@users.noreply.github.com>
1 parent 30a35fc commit 5994e21

26 files changed

Lines changed: 311 additions & 106 deletions

internal/parser/aider_provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,5 +707,8 @@ func aiderProviderCapabilities() Capabilities {
707707
ToolResults: CapabilitySupported,
708708
PerMessageTokenUsage: CapabilitySupported,
709709
},
710+
Sync: ProviderSyncSemantics{
711+
UnchangedResults: UnchangedResultMTimeAndHash,
712+
},
710713
}
711714
}

internal/parser/capabilities.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@ const (
2121
type Capabilities struct {
2222
Source SourceCapabilities
2323
Content ContentCapabilities
24+
Sync ProviderSyncSemantics
25+
}
26+
27+
// UnchangedResultPolicy controls how the engine compares parsed members from a
28+
// shared source with their stored rows. The zero value keeps every result.
29+
type UnchangedResultPolicy uint8
30+
31+
const (
32+
UnchangedResultNone UnchangedResultPolicy = iota
33+
UnchangedResultMTime
34+
UnchangedResultMTimeAndHash
35+
)
36+
37+
// ProviderSyncSemantics declares stable provider-wide cache and result
38+
// freshness policy. Its zero value opts out of every specialized behavior.
39+
type ProviderSyncSemantics struct {
40+
FingerprintHashInCacheKey bool
41+
FingerprintHashRequiredForFreshness bool
42+
// SkipCacheFreshWithoutStoredRow permits a matching skip-cache entry to
43+
// remain fresh before this provider has persisted a row for the source.
44+
SkipCacheFreshWithoutStoredRow bool
45+
UnchangedResults UnchangedResultPolicy
2446
}
2547

2648
// SourceCapabilities declares optional source mechanics implemented by a
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package parser
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
// TestProviderSyncSemanticsDeclarations verifies that every registered
10+
// provider's declared Capabilities().Sync matches the engine's historical
11+
// per-agent sync behavior. Providers not listed in wantSync are expected to
12+
// carry the zero-value ProviderSyncSemantics.
13+
func TestProviderSyncSemanticsDeclarations(t *testing.T) {
14+
wantSync := map[AgentType]ProviderSyncSemantics{
15+
AgentClaude: {
16+
FingerprintHashInCacheKey: true,
17+
FingerprintHashRequiredForFreshness: true,
18+
SkipCacheFreshWithoutStoredRow: true,
19+
},
20+
AgentCodex: {
21+
FingerprintHashInCacheKey: true,
22+
FingerprintHashRequiredForFreshness: true,
23+
SkipCacheFreshWithoutStoredRow: true,
24+
},
25+
AgentDevin: {
26+
FingerprintHashInCacheKey: true,
27+
FingerprintHashRequiredForFreshness: true,
28+
},
29+
AgentQoder: {
30+
FingerprintHashInCacheKey: true,
31+
FingerprintHashRequiredForFreshness: true,
32+
},
33+
AgentWindsurf: {
34+
FingerprintHashInCacheKey: true,
35+
FingerprintHashRequiredForFreshness: true,
36+
},
37+
AgentHermes: {
38+
FingerprintHashRequiredForFreshness: true,
39+
},
40+
AgentGemini: {
41+
FingerprintHashRequiredForFreshness: true,
42+
},
43+
AgentZed: {
44+
UnchangedResults: UnchangedResultMTime,
45+
},
46+
AgentKiro: {
47+
UnchangedResults: UnchangedResultMTime,
48+
},
49+
AgentTrae: {
50+
UnchangedResults: UnchangedResultMTimeAndHash,
51+
},
52+
AgentAider: {
53+
UnchangedResults: UnchangedResultMTimeAndHash,
54+
},
55+
AgentShelley: {
56+
UnchangedResults: UnchangedResultMTimeAndHash,
57+
},
58+
AgentOpenCode: {
59+
UnchangedResults: UnchangedResultMTimeAndHash,
60+
},
61+
AgentKilo: {
62+
UnchangedResults: UnchangedResultMTimeAndHash,
63+
},
64+
AgentMiMoCode: {
65+
UnchangedResults: UnchangedResultMTimeAndHash,
66+
},
67+
AgentIcodemate: {
68+
UnchangedResults: UnchangedResultMTimeAndHash,
69+
},
70+
}
71+
72+
for _, factory := range ProviderFactories() {
73+
agent := factory.Definition().Type
74+
t.Run(string(agent), func(t *testing.T) {
75+
assert.Equal(t, wantSync[agent], factory.Capabilities().Sync)
76+
})
77+
}
78+
}

internal/parser/claude_provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,5 +649,10 @@ func claudeProviderCapabilities() Capabilities {
649649
Model: CapabilitySupported,
650650
StopReason: CapabilitySupported,
651651
},
652+
Sync: ProviderSyncSemantics{
653+
FingerprintHashInCacheKey: true,
654+
FingerprintHashRequiredForFreshness: true,
655+
SkipCacheFreshWithoutStoredRow: true,
656+
},
652657
}
653658
}

internal/parser/codex_provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,5 +836,10 @@ func codexProviderCapabilities() Capabilities {
836836
TerminationStatus: CapabilitySupported,
837837
Model: CapabilitySupported,
838838
},
839+
Sync: ProviderSyncSemantics{
840+
FingerprintHashInCacheKey: true,
841+
FingerprintHashRequiredForFreshness: true,
842+
SkipCacheFreshWithoutStoredRow: true,
843+
},
839844
}
840845
}

internal/parser/devin_provider.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,5 +642,9 @@ func devinProviderCapabilities() Capabilities {
642642
PerMessageTokenUsage: CapabilitySupported,
643643
Model: CapabilitySupported,
644644
},
645+
Sync: ProviderSyncSemantics{
646+
FingerprintHashInCacheKey: true,
647+
FingerprintHashRequiredForFreshness: true,
648+
},
645649
}
646650
}

internal/parser/gemini_provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,5 +649,8 @@ func geminiProviderCapabilities() Capabilities {
649649
PerMessageTokenUsage: CapabilitySupported,
650650
Model: CapabilitySupported,
651651
},
652+
Sync: ProviderSyncSemantics{
653+
FingerprintHashRequiredForFreshness: true,
654+
},
652655
}
653656
}

internal/parser/hermes_provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,5 +1592,8 @@ func hermesProviderCapabilities() Capabilities {
15921592
AggregateUsageEvents: CapabilitySupported,
15931593
Model: CapabilitySupported,
15941594
},
1595+
Sync: ProviderSyncSemantics{
1596+
FingerprintHashRequiredForFreshness: true,
1597+
},
15951598
}
15961599
}

internal/parser/kiro_provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,5 +787,8 @@ func kiroProviderCapabilities() Capabilities {
787787
ToolCalls: CapabilitySupported,
788788
ToolResults: CapabilitySupported,
789789
},
790+
Sync: ProviderSyncSemantics{
791+
UnchangedResults: UnchangedResultMTime,
792+
},
790793
}
791794
}

internal/parser/opencode_provider.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -736,8 +736,8 @@ func (s openCodeFormatSourceSet) Fingerprint(
736736
return SourceFingerprint{}, fmt.Errorf("stat %s: source is a directory", path)
737737
}
738738
fingerprint.Size = info.Size()
739-
// No content hash: no engine freshness gate consumes it for this
740-
// family (see providerFingerprintHashRequiredForFreshness), and the
739+
// No content hash: this family declares FingerprintHashRequiredForFreshness
740+
// false, so no engine freshness gate consumes it, and the
741741
// authoritative storage fingerprint is computed by Parse and compared
742742
// post-parse by dropUnchangedSharedSQLiteResults. Computing it here
743743
// re-read and re-hashed every message and part file of the session on
@@ -1126,5 +1126,8 @@ func openCodeFormatProviderCapabilities() Capabilities {
11261126
PerMessageTokenUsage: CapabilitySupported,
11271127
Model: CapabilitySupported,
11281128
},
1129+
Sync: ProviderSyncSemantics{
1130+
UnchangedResults: UnchangedResultMTimeAndHash,
1131+
},
11291132
}
11301133
}

0 commit comments

Comments
 (0)