Skip to content

Commit 756bbc2

Browse files
committed
fix(sync): respect sidecar relevance and preserve metadata wakeups
1 parent 06a5cd8 commit 756bbc2

4 files changed

Lines changed: 133 additions & 8 deletions

File tree

internal/parser/opencode_change_feed.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,13 @@ func DrainOpenCodeJournal(
601601
}
602602

603603
case "session.updated.1", "session.created.1":
604-
// Recognized lifecycle events: note session existence, no state change.
604+
// Session metadata is durable source state, so apply it at the end
605+
// of this drain even when no settlement-bearing message exists.
605606
if len(status) >= OpenCodeCoverageMaxIDs && !hasID(status, m.AggregateID) {
606607
auditRequired = true
607608
break
608609
}
609-
if _, exists := status[m.AggregateID]; !exists {
610-
status[m.AggregateID] = openCodeStatusPending
611-
}
610+
status[m.AggregateID] = openCodeStatusReady
612611

613612
default:
614613
// Unrecognized event type or version: latch audit.
@@ -752,9 +751,7 @@ func ReduceOpenCodeJournalEvents(
752751
if len(status) >= OpenCodeCoverageMaxIDs && !hasID(status, e.AggregateID) {
753752
return next, true
754753
}
755-
if _, exists := status[e.AggregateID]; !exists {
756-
status[e.AggregateID] = openCodeStatusPending
757-
}
754+
status[e.AggregateID] = openCodeStatusReady
758755

759756
default:
760757
return next, true

internal/parser/opencode_change_feed_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,20 @@ func TestOpenCodeSettlementField(t *testing.T) {
303303
"assistant without completed must be pending")
304304
}
305305

306+
func TestOpenCodeSessionMetadataEventIsReady(t *testing.T) {
307+
cp, audit := ReduceOpenCodeJournalEvents(
308+
OpenCodeCoverageCheckpoint{},
309+
[]OpenCodeJournalEventInput{{
310+
RowID: 1, EventID: "session-update", AggregateID: "ses-metadata",
311+
Type: "session.updated.1",
312+
}},
313+
)
314+
assert.False(t, audit)
315+
assert.Contains(t, cp.ReadyIDs, "ses-metadata",
316+
"durable session metadata must reach the archive even without a settlement event")
317+
assert.NotContains(t, cp.PendingIDs, "ses-metadata")
318+
}
319+
306320
// TestOpenCodeTrailingEventsDoNotDowngrade verifies proof matrix row 4:
307321
// trailing events do not downgrade. A reference-model sequence of settlement
308322
// followed by a message.updated.1 and then session.updated.1 leaves the

internal/sync/opencode_coverage_coordinator.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,21 @@ func (e *Engine) wakeOpenCodePaths(
5050
}
5151
keys = uniqueCoverageKeys(keys)
5252
if len(keys) > 0 {
53+
missing := false
5354
for _, key := range keys {
55+
if !e.opencodeCoverage.pathRelevant(ctx, key, path) {
56+
continue
57+
}
58+
if _, err := os.Stat(key.DBPath); errors.Is(err, os.ErrNotExist) {
59+
e.opencodeCoverage.retire(key)
60+
missing = true
61+
continue
62+
}
5463
e.opencodeCoverage.wake(key, reasonNative)
5564
}
65+
if missing {
66+
remaining = append(remaining, path)
67+
}
5668
continue
5769
}
5870
remaining = append(remaining, path)
@@ -178,6 +190,31 @@ func (c *openCodeCoverageCoordinator) provider(agent parser.AgentType) parser.Pr
178190
return provider
179191
}
180192

193+
func (c *openCodeCoverageCoordinator) pathRelevant(
194+
ctx context.Context, key openCodeCoverageKey, path string,
195+
) bool {
196+
c.mu.RLock()
197+
provider := c.providers[key.Agent]
198+
c.mu.RUnlock()
199+
if provider == nil {
200+
if c.engine == nil {
201+
return true
202+
}
203+
provider = c.provider(key.Agent)
204+
}
205+
if provider == nil {
206+
return true
207+
}
208+
physical := path
209+
if idx := strings.LastIndex(path, "#"); idx > 0 {
210+
physical = path[:idx]
211+
}
212+
relevance, err := parser.ResolveChangedPathRelevance(
213+
ctx, provider, parser.ChangedPathRequest{Path: physical},
214+
)
215+
return err != nil || relevance != parser.ChangedPathNonData
216+
}
217+
181218
// Initialize registers and baselines every schema-admitted OpenCode-family
182219
// SQLite unit. It must run before startup reconciliation so the baseline keeps
183220
// events committed during startup visible to the first wake.

internal/sync/opencode_coverage_coordinator_test.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66
"os"
77
"path/filepath"
8+
"strings"
89
"sync/atomic"
910
"testing"
1011
"time"
@@ -54,7 +55,7 @@ func TestOpenCodeLateContainerAdmissionUsesJournalEvidence(t *testing.T) {
5455
require.Len(t, keys, 1)
5556
assert.True(t, engine.opencodeCoverage.worker.unit(keys[0]).committedCheckpoint().Initialized,
5657
"runtime admission must use the row-zero origin, not the baseline sentinel")
57-
require.True(t, engine.wakeOpenCodePath(t.Context(), dbPath+"-wal"))
58+
require.True(t, engine.wakeOpenCodePath(t.Context(), dbPath))
5859
key, admitted := engine.opencodeCoverage.keyForPath(dbPath)
5960
require.True(t, admitted)
6061
assert.Equal(t, parser.AgentOpenCode, key.Agent)
@@ -66,6 +67,7 @@ func TestOpenCodeLateContainerAdmissionUsesJournalEvidence(t *testing.T) {
6667

6768
func TestOpenCodeWakePathWakesEveryOverlappingUnit(t *testing.T) {
6869
dbPath := filepath.Join(t.TempDir(), "opencode.db")
70+
require.NoError(t, os.WriteFile(dbPath, nil, 0o644))
6971
first := openCodeCoverageKey{Agent: parser.AgentOpenCode, DBPath: dbPath}
7072
second := openCodeCoverageKey{Agent: parser.AgentKilo, DBPath: dbPath}
7173
var wakes atomic.Int32
@@ -90,6 +92,80 @@ func TestOpenCodeWakePathWakesEveryOverlappingUnit(t *testing.T) {
9092
c.stop()
9193
}
9294

95+
type coverageRelevanceProvider struct {
96+
parser.ProviderBase
97+
}
98+
99+
func (p *coverageRelevanceProvider) Parse(
100+
context.Context, parser.ParseRequest,
101+
) (parser.ParseOutcome, error) {
102+
return parser.ParseOutcome{}, nil
103+
}
104+
105+
func (p *coverageRelevanceProvider) ChangedPathRelevance(
106+
_ context.Context, req parser.ChangedPathRequest,
107+
) (parser.ChangedPathRelevance, error) {
108+
if strings.HasSuffix(req.Path, "-shm") {
109+
return parser.ChangedPathNonData, nil
110+
}
111+
if strings.HasSuffix(req.Path, "-wal") {
112+
return parser.ChangedPathNonData, nil
113+
}
114+
return parser.ChangedPathDataBearing, nil
115+
}
116+
117+
func TestOpenCodeWakePathHonorsSidecarRelevance(t *testing.T) {
118+
dbPath := filepath.Join(t.TempDir(), "opencode.db")
119+
require.NoError(t, os.WriteFile(dbPath, nil, 0o644))
120+
key := openCodeCoverageKey{Agent: parser.AgentOpenCode, DBPath: dbPath}
121+
var wakes atomic.Int32
122+
c := &openCodeCoverageCoordinator{
123+
units: map[openCodeCoverageKey]struct{}{key: {}},
124+
providers: map[parser.AgentType]parser.Provider{
125+
parser.AgentOpenCode: &coverageRelevanceProvider{
126+
ProviderBase: parser.ProviderBase{Caps: parser.Capabilities{
127+
Source: parser.SourceCapabilities{
128+
ChangedPathRelevance: parser.CapabilitySupported,
129+
},
130+
}},
131+
},
132+
},
133+
}
134+
c.worker = newOpenCodeCoverageWorker(t.Context(), nil, nil)
135+
c.worker.drain = func(
136+
context.Context, string, parser.OpenCodeCoverageCheckpoint,
137+
) (parser.OpenCodeFeedResult, error) {
138+
wakes.Add(1)
139+
return parser.OpenCodeFeedResult{
140+
Next: parser.OpenCodeCoverageCheckpoint{Initialized: true},
141+
}, nil
142+
}
143+
c.worker.Register(key)
144+
e := &Engine{opencodeCoverage: c}
145+
146+
assert.Empty(t, e.wakeOpenCodePaths(t.Context(), []string{dbPath + "-shm"}))
147+
assert.Empty(t, e.wakeOpenCodePaths(t.Context(), []string{dbPath + "-wal"}))
148+
assert.Zero(t, wakes.Load(), "non-data sidecars must not wake coverage")
149+
assert.Empty(t, e.wakeOpenCodePaths(t.Context(), []string{dbPath}))
150+
assert.Eventually(t, func() bool { return wakes.Load() == 1 }, time.Second, time.Millisecond)
151+
c.stop()
152+
}
153+
154+
func TestOpenCodeWatcherDeletedUnitReturnsPathToNormalSync(t *testing.T) {
155+
dbPath := filepath.Join(t.TempDir(), "opencode.db")
156+
key := openCodeCoverageKey{Agent: parser.AgentOpenCode, DBPath: dbPath}
157+
c := &openCodeCoverageCoordinator{
158+
units: map[openCodeCoverageKey]struct{}{key: {}},
159+
worker: newOpenCodeCoverageWorker(t.Context(), nil, nil),
160+
}
161+
c.worker.Register(key)
162+
e := &Engine{opencodeCoverage: c}
163+
164+
assert.Equal(t, []string{dbPath}, e.wakeOpenCodePaths(t.Context(), []string{dbPath}))
165+
assert.Empty(t, c.keysForPath(dbPath), "deleted units must be retired before normal processing")
166+
c.stop()
167+
}
168+
93169
func TestOpenCodeDeletedUnitLeavesRootForAuthoritativeReconciliation(t *testing.T) {
94170
root := t.TempDir()
95171
key := openCodeCoverageKey{
@@ -110,6 +186,7 @@ func TestOpenCodeDeletedUnitLeavesRootForAuthoritativeReconciliation(t *testing.
110186

111187
func TestSyncPathsDivertsAdmittedContainerToWake(t *testing.T) {
112188
dbPath := filepath.Join(t.TempDir(), "opencode.db")
189+
require.NoError(t, os.WriteFile(dbPath, nil, 0o644))
113190
key := openCodeCoverageKey{Agent: parser.AgentOpenCode, DBPath: dbPath}
114191
var wakes atomic.Int32
115192
c := &openCodeCoverageCoordinator{

0 commit comments

Comments
 (0)