Skip to content

Commit b727827

Browse files
committed
Keep degraded polling provider scoped through fallback
1 parent 4826ee1 commit b727827

7 files changed

Lines changed: 228 additions & 41 deletions

File tree

cmd/agentsview/archive_write_backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@ func (b *localArchiveWriteBackend) PGPushWatch(
925925
OnPollingRequired: func(obligation syncpkg.PollingObligation) error {
926926
return poller.AddObligation(pollingObligation{
927927
Key: obligation.Key,
928+
Agent: obligation.Agent,
928929
Roots: obligation.Roots,
929930
Probe: obligation.Probe,
930931
DegradedProbe: obligation.DegradedProbe,

cmd/agentsview/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ func runServe(cfg config.Config, opts serveOptions) {
331331
OnPollingRequired: func(obligation sync.PollingObligation) error {
332332
return unwatchedPoller.AddObligation(pollingObligation{
333333
Key: obligation.Key,
334+
Agent: obligation.Agent,
334335
Roots: obligation.Roots,
335336
Probe: obligation.Probe,
336337
DegradedProbe: obligation.DegradedProbe,
@@ -2582,6 +2583,11 @@ func collectProviderWatchRoots(
25822583
def.Type, root, probeErr)
25832584
degradedProbe = nil
25842585
}
2586+
} else if def.Type == parser.AgentOpenCode {
2587+
degradedProbe = lateBoundDegradedPollProbe{
2588+
provider: provider,
2589+
root: root,
2590+
}
25852591
}
25862592
_, err := os.Stat(root)
25872593
exists := err == nil

cmd/agentsview/main_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,32 @@ func TestLateBoundDegradedPollProbeActivatesWhenOpenCodeRootBecomesSQLite(t *tes
7777
assert.NotEmpty(t, state)
7878
}
7979

80+
func TestLateBoundDegradedPollProbeFallsBackWhenOpenCodeRootBecomesHybrid(t *testing.T) {
81+
root := t.TempDir()
82+
provider, ok := parser.NewProvider(parser.AgentOpenCode, parser.ProviderConfig{
83+
Roots: []string{root},
84+
})
85+
require.True(t, ok)
86+
probe := lateBoundDegradedPollProbe{provider: provider, root: root}
87+
88+
db, err := sql.Open("sqlite3", filepath.Join(root, "opencode.db"))
89+
require.NoError(t, err)
90+
t.Cleanup(func() { require.NoError(t, db.Close()) })
91+
require.NoError(t, db.Ping())
92+
_, err = db.Exec(`CREATE TABLE probe_state (id INTEGER PRIMARY KEY)`)
93+
require.NoError(t, err)
94+
95+
state, err := probe.DegradedPollingState(t.Context())
96+
require.NoError(t, err)
97+
assert.NotEmpty(t, state)
98+
99+
require.NoError(t,
100+
os.MkdirAll(filepath.Join(root, "storage", "session"), 0o755))
101+
102+
_, err = probe.DegradedPollingState(t.Context())
103+
require.ErrorIs(t, err, parser.ErrUnsupportedProviderFeature)
104+
}
105+
80106
func TestServeRuntimeRecordWriteFailureWarnsVisibleAfterSlowStartup(t *testing.T) {
81107
out, err := runServeRuntimeWarningHelper(t, true, 1200*time.Millisecond)
82108
require.NoError(t, err, string(out))
@@ -567,12 +593,24 @@ func (f *fakeUnwatchedPollSyncer) ReconcileWatchRoots(
567593
return nil
568594
}
569595

596+
func (f *fakeUnwatchedPollSyncer) ReconcileProviderRoots(
597+
ctx context.Context, _ parser.AgentType, roots []string,
598+
) error {
599+
return f.ReconcileWatchRoots(ctx, roots, false)
600+
}
601+
570602
func TestPollUnwatchedRootsOnceUsesScopedAuthoritativeReconciliation(t *testing.T) {
571603
fake := &fakeUnwatchedPollSyncer{}
572604
roots := []string{"/tmp/claude", "/tmp/codex"}
573605

574-
pollUnwatchedRootsOnce(t.Context(), fake, roots)
575-
pollUnwatchedRootsOnce(t.Context(), fake, roots)
606+
pollUnwatchedRootsOnce(t.Context(), fake, []pollingObligation{{
607+
Key: "roots",
608+
Roots: roots,
609+
}})
610+
pollUnwatchedRootsOnce(t.Context(), fake, []pollingObligation{{
611+
Key: "roots",
612+
Roots: roots,
613+
}})
576614

577615
require.Equal(t, 2, fake.calls)
578616
assert.Equal(t, roots, fake.callRoots[0])

cmd/agentsview/unwatched_poll.go

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var errUnwatchedPollStopped = errors.New("unwatched poll coordinator stopped")
2020

2121
type unwatchedPollSyncer interface {
2222
ReconcileWatchRoots(context.Context, []string, bool) error
23+
ReconcileProviderRoots(context.Context, parser.AgentType, []string) error
2324
}
2425

2526
type unwatchedPollAdd struct {
@@ -30,6 +31,7 @@ type unwatchedPollAdd struct {
3031

3132
type pollingObligation struct {
3233
Key string
34+
Agent parser.AgentType
3335
Roots []string
3436
// Probe mirrors sync.PollingObligation.Probe: the physical watcher path
3537
// whose availability gates this obligation's reconciliation Roots. When
@@ -173,8 +175,9 @@ func (c *sharedUnwatchedPollCoordinator) run() {
173175
} else {
174176
obligations[request.obligation.Key] = request.obligation
175177
}
176-
c.setPollObligations(obligations)
178+
snapshot := sortedPollObligations(obligations)
177179
c.pollMu.Lock()
180+
c.pollObligations = snapshot
178181
c.pollRevisions[request.obligation.Key]++
179182
delete(c.pollStates, request.obligation.Key)
180183
c.pollMu.Unlock()
@@ -188,19 +191,17 @@ func (c *sharedUnwatchedPollCoordinator) run() {
188191
}
189192
}
190193

191-
func (c *sharedUnwatchedPollCoordinator) setPollObligations(
194+
func sortedPollObligations(
192195
obligations map[string]pollingObligation,
193-
) {
196+
) []pollingObligation {
194197
snapshot := make([]pollingObligation, 0, len(obligations))
195198
for _, obligation := range obligations {
196199
snapshot = append(snapshot, obligation)
197200
}
198201
slices.SortFunc(snapshot, func(a, b pollingObligation) int {
199202
return strings.Compare(a.Key, b.Key)
200203
})
201-
c.pollMu.Lock()
202-
c.pollObligations = snapshot
203-
c.pollMu.Unlock()
204+
return snapshot
204205
}
205206

206207
func (c *sharedUnwatchedPollCoordinator) currentPollObligations() []pollingObligation {
@@ -232,18 +233,18 @@ func (c *sharedUnwatchedPollCoordinator) runPollWorker() {
232233
return
233234
}
234235
obligations, priorStates, priorRevisions := c.currentPollSnapshot()
235-
roots, nextStates := c.preparePollRun(
236+
selected, nextStates := c.preparePollRun(
236237
c.workerCtx, obligations, priorStates,
237238
)
238-
if len(roots) == 0 {
239+
if len(selected) == 0 {
239240
continue
240241
}
241-
log.Printf("polling %d unwatched root(s)", len(roots))
242+
log.Printf("polling %d unwatched root(s)", len(unwatchedPollObligationSliceRoots(selected)))
242243
c.doWork(func() {
243244
if c.workerCtx.Err() != nil {
244245
return
245246
}
246-
if err := pollUnwatchedRootsOnce(c.workerCtx, c.engine, roots); err != nil {
247+
if err := pollUnwatchedRootsOnce(c.workerCtx, c.engine, selected); err != nil {
247248
return
248249
}
249250
c.commitPollStates(nextStates, priorRevisions)
@@ -270,10 +271,10 @@ func (c *sharedUnwatchedPollCoordinator) runPollWorker() {
270271
// deferred scope as an authoritative empty discovery and tombstone its
271272
// sessions.
272273
func availableUnwatchedPollRoots(obligations []pollingObligation) []string {
273-
roots, _ := availableUnwatchedPollRootsWithState(
274+
selected, _ := availableUnwatchedPollObligationsWithState(
274275
context.Background(), obligations, nil,
275276
)
276-
return roots
277+
return unwatchedPollObligationSliceRoots(selected)
277278
}
278279

279280
func unwatchedPollObligationRoots(obligations map[string]pollingObligation) []string {
@@ -288,6 +289,18 @@ func unwatchedPollObligationRoots(obligations map[string]pollingObligation) []st
288289
return unwatchedPollRoots(owned)
289290
}
290291

292+
func unwatchedPollObligationSliceRoots(obligations []pollingObligation) []string {
293+
owned := make(map[string]struct{})
294+
for _, obligation := range obligations {
295+
for _, root := range obligation.Roots {
296+
if root != "" {
297+
owned[root] = struct{}{}
298+
}
299+
}
300+
}
301+
return unwatchedPollRoots(owned)
302+
}
303+
291304
func unwatchedPollRoots(owned map[string]struct{}) []string {
292305
roots := make([]string, 0, len(owned))
293306
for root := range owned {
@@ -298,14 +311,47 @@ func unwatchedPollRoots(owned map[string]struct{}) []string {
298311
}
299312

300313
func pollUnwatchedRootsOnce(
301-
ctx context.Context, engine unwatchedPollSyncer, roots []string,
314+
ctx context.Context, engine unwatchedPollSyncer, obligations []pollingObligation,
302315
) error {
303-
if len(roots) == 0 {
316+
if len(obligations) == 0 {
304317
return nil
305318
}
306-
if err := engine.ReconcileWatchRoots(ctx, roots, false); err != nil {
307-
log.Printf("polling unwatched roots: %v", err)
308-
return err
319+
watchRoots := make(map[string]struct{})
320+
byAgent := make(map[parser.AgentType]map[string]struct{})
321+
for _, obligation := range obligations {
322+
target := watchRoots
323+
if obligation.Agent != "" {
324+
if byAgent[obligation.Agent] == nil {
325+
byAgent[obligation.Agent] = make(map[string]struct{})
326+
}
327+
target = byAgent[obligation.Agent]
328+
}
329+
for _, root := range obligation.Roots {
330+
if root != "" {
331+
target[root] = struct{}{}
332+
}
333+
}
334+
}
335+
if roots := unwatchedPollRoots(watchRoots); len(roots) > 0 {
336+
if err := engine.ReconcileWatchRoots(ctx, roots, false); err != nil {
337+
log.Printf("polling unwatched roots: %v", err)
338+
return err
339+
}
340+
}
341+
agents := make([]parser.AgentType, 0, len(byAgent))
342+
for agent := range byAgent {
343+
agents = append(agents, agent)
344+
}
345+
slices.Sort(agents)
346+
for _, agent := range agents {
347+
roots := unwatchedPollRoots(byAgent[agent])
348+
if len(roots) == 0 {
349+
continue
350+
}
351+
if err := engine.ReconcileProviderRoots(ctx, agent, roots); err != nil {
352+
log.Printf("polling unwatched %s roots: %v", agent, err)
353+
return err
354+
}
309355
}
310356
return nil
311357
}
@@ -326,8 +372,8 @@ func (c *sharedUnwatchedPollCoordinator) preparePollRun(
326372
ctx context.Context,
327373
obligations []pollingObligation,
328374
prior map[string]string,
329-
) ([]string, map[string]string) {
330-
return availableUnwatchedPollRootsWithState(
375+
) ([]pollingObligation, map[string]string) {
376+
return availableUnwatchedPollObligationsWithState(
331377
ctx, obligations, prior,
332378
)
333379
}
@@ -349,15 +395,14 @@ func (c *sharedUnwatchedPollCoordinator) commitPollStates(
349395
}
350396
}
351397

352-
func availableUnwatchedPollRootsWithState(
398+
func availableUnwatchedPollObligationsWithState(
353399
ctx context.Context,
354400
obligations []pollingObligation,
355401
prior map[string]string,
356-
) ([]string, map[string]string) {
357-
candidates := make(map[string]struct{})
402+
) ([]pollingObligation, map[string]string) {
358403
blocked := make(map[string]struct{})
359-
candidateOwners := make(map[string][]string)
360404
pendingStates := make(map[string]string)
405+
selected := make([]pollingObligation, 0, len(obligations))
361406
for _, obligation := range obligations {
362407
probeMissing := false
363408
if obligation.Probe != "" {
@@ -382,33 +427,42 @@ func availableUnwatchedPollRootsWithState(
382427
pendingStates[obligation.Key] = state
383428
}
384429
}
430+
roots := make([]string, 0, len(obligation.Roots))
385431
for _, root := range obligation.Roots {
386432
if root == "" {
387433
continue
388434
}
389435
if _, err := os.Stat(root); err == nil {
390-
candidates[root] = struct{}{}
391-
candidateOwners[root] = append(candidateOwners[root], obligation.Key)
436+
roots = append(roots, root)
392437
}
393438
}
394-
}
395-
for root := range candidates {
396-
if overlapsDeferredScope(filepath.Clean(root), blocked) {
397-
delete(candidates, root)
439+
if len(roots) == 0 {
440+
continue
398441
}
442+
selected = append(selected, pollingObligation{
443+
Key: obligation.Key,
444+
Agent: obligation.Agent,
445+
Roots: roots,
446+
Probe: obligation.Probe,
447+
DegradedProbe: obligation.DegradedProbe,
448+
})
399449
}
400-
if len(pendingStates) == 0 {
401-
return unwatchedPollRoots(candidates), nil
402-
}
403-
committed := make(map[string]string)
404-
for root := range candidates {
405-
for _, key := range candidateOwners[root] {
406-
if state, ok := pendingStates[key]; ok {
407-
committed[key] = state
450+
filtered := make([]pollingObligation, 0, len(selected))
451+
for _, obligation := range selected {
452+
deferred := false
453+
for _, root := range obligation.Roots {
454+
if overlapsDeferredScope(filepath.Clean(root), blocked) {
455+
deferred = true
456+
break
408457
}
409458
}
459+
if deferred {
460+
delete(pendingStates, obligation.Key)
461+
continue
462+
}
463+
filtered = append(filtered, obligation)
410464
}
411-
return unwatchedPollRoots(candidates), committed
465+
return filtered, pendingStates
412466
}
413467

414468
func clonePollStates(states map[string]string) map[string]string {

0 commit comments

Comments
 (0)