Skip to content

Commit 3a3e106

Browse files
committed
fix(sync): bound Omnigent scheduled reconciliation
Scheduled reconciliation must stay proportional to the changed member batch instead of reparsing every conversation in the shared database. Full membership scans remain available to archive audits and forced recovery, where their deletion proof is intentional.\n\nUnsupported Omnigent schemas cannot safely prove that archived conversations disappeared. Treating that state as non-authoritative lets healthy reconciliation work finish while preserving the existing archive.
1 parent abbb14d commit 3a3e106

5 files changed

Lines changed: 241 additions & 31 deletions

File tree

internal/parser/omnigent_provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ func streamOmnigentMemberMatches(
200200
defer conn.Close()
201201
schema, err := detectOmnigentSchema(conn)
202202
if err != nil {
203+
if omnigentSchemaUnsupported(err) {
204+
return nonAuthoritativeDiscoveryError(
205+
AgentOmnigent, "unsupported schema", err,
206+
)
207+
}
203208
return err
204209
}
205210
idExpr := omnigentIDExpr(schema, "id")

internal/parser/streaming_discovery.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ func (err DiscoveryIncompleteError) Error() string {
2828
return string(err.Provider) + " discovery incomplete: " + err.Reason
2929
}
3030

31+
// DiscoveryNonAuthoritativeError reports a source state that can be skipped
32+
// without failing reconciliation but cannot prove that archived sources are
33+
// absent. Callers may commit sources yielded by other providers, but must not
34+
// tombstone this provider's stored sessions from the incomplete membership.
35+
type DiscoveryNonAuthoritativeError struct {
36+
Provider AgentType
37+
Reason string
38+
}
39+
40+
func (err DiscoveryNonAuthoritativeError) Error() string {
41+
if err.Provider == "" {
42+
return "discovery non-authoritative: " + err.Reason
43+
}
44+
return string(err.Provider) + " discovery non-authoritative: " + err.Reason
45+
}
46+
3147
type discoveryIncompleteCause struct {
3248
incomplete DiscoveryIncompleteError
3349
cause error
@@ -38,6 +54,19 @@ func (err discoveryIncompleteCause) Unwrap() []error {
3854
return []error{err.incomplete, err.cause}
3955
}
4056

57+
type discoveryNonAuthoritativeCause struct {
58+
nonAuthoritative DiscoveryNonAuthoritativeError
59+
cause error
60+
}
61+
62+
func (err discoveryNonAuthoritativeCause) Error() string {
63+
return err.nonAuthoritative.Error()
64+
}
65+
66+
func (err discoveryNonAuthoritativeCause) Unwrap() []error {
67+
return []error{err.nonAuthoritative, err.cause}
68+
}
69+
4170
type discoveryYieldError struct{ cause error }
4271

4372
func (err discoveryYieldError) Error() string { return err.cause.Error() }
@@ -59,6 +88,22 @@ func incompleteDiscoveryError(
5988
}
6089
}
6190

91+
func nonAuthoritativeDiscoveryError(
92+
provider AgentType, reason string, cause error,
93+
) error {
94+
var nonAuthoritative DiscoveryNonAuthoritativeError
95+
if errors.As(cause, &nonAuthoritative) {
96+
return cause
97+
}
98+
return discoveryNonAuthoritativeCause{
99+
nonAuthoritative: DiscoveryNonAuthoritativeError{
100+
Provider: provider,
101+
Reason: reason + ": " + cause.Error(),
102+
},
103+
cause: cause,
104+
}
105+
}
106+
62107
func discoveryYieldCause(err error) (error, bool) {
63108
var yieldErr discoveryYieldError
64109
if !errors.As(err, &yieldErr) {

internal/parser/types_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,8 +676,8 @@ func TestPeriodicReconcileCapability(t *testing.T) {
676676
assert.True(t, optedIn[AgentOpenHands])
677677
assert.True(t, optedIn[AgentAider])
678678
// Omnigent's event tracker deliberately observes only bounded row-ID and
679-
// timestamp windows. Scheduled authoritative reconciliation covers
680-
// same-fingerprint in-place edits through the streamed provider pass.
679+
// timestamp windows. Scheduled reconciliation replays those bounded
680+
// cursors; the daily archive audit performs authoritative discovery.
681681
assert.True(t, optedIn[AgentOmnigent])
682682
// Cowork's provider WatchPlan registers its root recursively
683683
// (coworkWatchRoots Recursive:true overrides legacy ShallowWatch), so

internal/sync/engine.go

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3050,33 +3050,38 @@ func (e *Engine) ReconcileWatchRoots(
30503050
func (e *Engine) ReconcileWatchRootsWithStats(
30513051
ctx context.Context, roots []string, full bool,
30523052
) (SyncStats, int, error) {
3053-
return e.reconcileScopedWatchRoots(ctx, "", roots, full, false)
3053+
return e.reconcileScopedWatchRoots(ctx, "", roots, full, false, true)
30543054
}
30553055

30563056
func (e *Engine) reconcileWatchRoots(
30573057
ctx context.Context, roots []string, full, force bool,
30583058
) error {
3059-
_, _, err := e.reconcileScopedWatchRoots(ctx, "", roots, full, force)
3059+
_, _, err := e.reconcileScopedWatchRoots(
3060+
ctx, "", roots, full, force, true,
3061+
)
30603062
return err
30613063
}
30623064

3063-
// ReconcileProviderRoots reconciles the given roots for a single provider. It
3064-
// bypasses the cross-provider expansion in logicalRootsForWatchRoots and
3065-
// restricts both discovery and deletion to that provider, so a shallow-watched
3066-
// agent's scheduled pass never enumerates or tombstones another agent's
3067-
// sessions under an overlapping root.
3065+
// ReconcileProviderRoots runs the bounded scheduled pass for one provider. It
3066+
// bypasses the cross-provider expansion in logicalRootsForWatchRoots so a
3067+
// shallow-watched agent never enumerates or tombstones another agent's sessions
3068+
// under an overlapping root. Providers whose scheduled discovery is
3069+
// non-authoritative leave deletion proof to the archive audit.
30683070
func (e *Engine) ReconcileProviderRoots(
30693071
ctx context.Context, agent parser.AgentType, roots []string,
30703072
) error {
30713073
if agent == "" {
30723074
return e.reconcileWatchRoots(ctx, roots, false, false)
30733075
}
3074-
_, _, err := e.reconcileScopedWatchRoots(ctx, agent, roots, false, false)
3076+
_, _, err := e.reconcileScopedWatchRoots(
3077+
ctx, agent, roots, false, false, false,
3078+
)
30753079
return err
30763080
}
30773081

30783082
func (e *Engine) reconcileScopedWatchRoots(
3079-
ctx context.Context, agent parser.AgentType, roots []string, full, force bool,
3083+
ctx context.Context, agent parser.AgentType, roots []string, full, force,
3084+
forceFullOmnigent bool,
30803085
) (SyncStats, int, error) {
30813086
var logicalRoots []string
30823087
var excludedRemoteRoots int
@@ -3093,7 +3098,7 @@ func (e *Engine) reconcileScopedWatchRoots(
30933098
return SyncStats{}, 0, nil
30943099
}
30953100
stats, metrics, tombstoned, err := e.reconcileWatchRootsStreamed(
3096-
ctx, agent, logicalRoots, full, force,
3101+
ctx, agent, logicalRoots, full, force, forceFullOmnigent,
30973102
)
30983103
metrics.ExcludedRemoteRoots = excludedRemoteRoots
30993104
if stats.Synced > 0 || tombstoned > 0 {
@@ -3130,7 +3135,8 @@ func (e *Engine) ReconciliationRootsForAgent(agent string) []string {
31303135
}
31313136

31323137
func (e *Engine) reconcileWatchRootsStreamed(
3133-
ctx context.Context, agent parser.AgentType, roots []string, full, force bool,
3138+
ctx context.Context, agent parser.AgentType, roots []string, full, force,
3139+
forceFullOmnigent bool,
31343140
) (stats SyncStats, metrics ReconciliationMetrics, tombstoned int, retErr error) {
31353141
if err := ctx.Err(); err != nil {
31363142
return SyncStats{Aborted: true}, metrics, 0, err
@@ -3185,8 +3191,9 @@ func (e *Engine) reconcileWatchRootsStreamed(
31853191
scope.agent = agent
31863192
}
31873193
preContainerStates := e.captureSQLiteContainerStates(nil)
3188-
providers, completedScopes, failedRoots, failures, discoveryErr, err := e.streamReconciliationCandidates(
3189-
ctx, scope, spool,
3194+
providers, completedScopes, nonAuthoritativeProviders, failedRoots,
3195+
failures, discoveryErr, err := e.streamReconciliationCandidates(
3196+
ctx, scope, spool, forceFullOmnigent,
31903197
)
31913198
stats.providerFailures = failures
31923199
if err != nil {
@@ -3198,9 +3205,9 @@ func (e *Engine) reconcileWatchRootsStreamed(
31983205
cleaned = true
31993206
return stats, metrics, 0, err
32003207
}
3201-
baselineEligibleProviders := make(map[parser.AgentType]struct{}, len(completedScopes))
3208+
authoritativeProviders := make(map[parser.AgentType]struct{}, len(completedScopes))
32023209
for _, completed := range completedScopes {
3203-
baselineEligibleProviders[completed.agent] = struct{}{}
3210+
authoritativeProviders[completed.agent] = struct{}{}
32043211
}
32053212
e.beginStreamingSQLiteContainerPass(preContainerStates)
32063213
e.finishStreamingSQLiteContainerDiscovery()
@@ -3272,7 +3279,7 @@ func (e *Engine) reconcileWatchRootsStreamed(
32723279
break
32733280
}
32743281
baselineCandidates, baselineAdmitted := eligibleReconciliationBaselines(
3275-
page, baselineTracker.list(), baselineEligibleProviders,
3282+
page, baselineTracker.list(), authoritativeProviders,
32763283
)
32773284
if err := e.baselineReconciliationCandidates(
32783285
ctx, baselineCandidates, baselineAdmitted,
@@ -3344,7 +3351,7 @@ func (e *Engine) reconcileWatchRootsStreamed(
33443351
if retErr == nil && ctx.Err() == nil && !stats.Aborted &&
33453352
stats.Failed == 0 && stats.providerFailures == 0 {
33463353
tombstoned, retErr = e.tombstoneMissingWatchSourcesForAgentLocked(
3347-
ctx, roots, agent, spool,
3354+
ctx, roots, agent, nonAuthoritativeProviders, spool,
33483355
)
33493356
} else if canTombstoneCompletedScopes && ctx.Err() == nil &&
33503357
!stats.Aborted && stats.Failed == 0 {
@@ -3371,7 +3378,7 @@ func (e *Engine) tombstoneCompletedReconciliationScopesLocked(
33713378
) (deleted int, retErr error) {
33723379
for _, scope := range incomplete.completed {
33733380
count, err := e.tombstoneMissingWatchSourcesForAgentLocked(
3374-
ctx, scope.roots, scope.agent, spool,
3381+
ctx, scope.roots, scope.agent, nil, spool,
33753382
)
33763383
deleted += count
33773384
if err == nil {
@@ -3435,15 +3442,18 @@ func eligibleReconciliationBaselines(
34353442

34363443
func (e *Engine) streamReconciliationCandidates(
34373444
ctx context.Context, scope *rootSyncScope, spool reconciliationSpoolStore,
3445+
forceFullOmnigent bool,
34383446
) (
34393447
map[parser.AgentType]parser.Provider,
34403448
[]reconciliationProviderScope,
3449+
map[parser.AgentType]struct{},
34413450
[]string,
34423451
int,
34433452
error,
34443453
error,
34453454
) {
34463455
providers := make(map[parser.AgentType]parser.Provider)
3456+
nonAuthoritativeProviders := make(map[parser.AgentType]struct{})
34473457
var completedScopes []reconciliationProviderScope
34483458
var failedRoots []string
34493459
var failures int
@@ -3474,7 +3484,8 @@ func (e *Engine) streamReconciliationCandidates(
34743484
}
34753485
provider := factory.NewProvider(parser.ProviderConfig{
34763486
Roots: roots, Machine: e.machine, PathRewriter: e.pathRewriter,
3477-
ForceFullDiscovery: agent == parser.AgentOmnigent,
3487+
ForceFullDiscovery: agent == parser.AgentOmnigent &&
3488+
forceFullOmnigent,
34783489
})
34793490
providers[agent] = provider
34803491
if provider.Capabilities().Source.StreamingDiscovery != parser.CapabilitySupported {
@@ -3509,10 +3520,18 @@ func (e *Engine) streamReconciliationCandidates(
35093520
})
35103521
if err != nil {
35113522
if spoolErr != nil {
3512-
return providers, completedScopes, failedRoots, failures, discoveryErr, spoolErr
3523+
return providers, completedScopes, nonAuthoritativeProviders,
3524+
failedRoots, failures, discoveryErr, spoolErr
35133525
}
35143526
if ctx.Err() != nil {
3515-
return providers, completedScopes, failedRoots, failures, discoveryErr, ctx.Err()
3527+
return providers, completedScopes, nonAuthoritativeProviders,
3528+
failedRoots, failures, discoveryErr, ctx.Err()
3529+
}
3530+
var nonAuthoritative parser.DiscoveryNonAuthoritativeError
3531+
if errors.As(err, &nonAuthoritative) {
3532+
log.Printf("%s provider streaming discovery: %v", agent, err)
3533+
nonAuthoritativeProviders[agent] = struct{}{}
3534+
continue
35163535
}
35173536
log.Printf("%s provider streaming discovery: %v", agent, err)
35183537
failures++
@@ -3522,14 +3541,19 @@ func (e *Engine) streamReconciliationCandidates(
35223541
))
35233542
continue
35243543
}
3544+
if agent == parser.AgentOmnigent && !forceFullOmnigent {
3545+
nonAuthoritativeProviders[agent] = struct{}{}
3546+
continue
3547+
}
35253548
completedScopes = append(completedScopes, reconciliationProviderScope{
35263549
agent: agent,
35273550
roots: append([]string(nil), roots...),
35283551
})
35293552
}
35303553
slices.Sort(failedRoots)
35313554
failedRoots = slices.Compact(failedRoots)
3532-
return providers, completedScopes, failedRoots, failures, discoveryErr, nil
3555+
return providers, completedScopes, nonAuthoritativeProviders,
3556+
failedRoots, failures, discoveryErr, nil
35333557
}
35343558

35353559
type reconciliationProviderScope struct {
@@ -3876,13 +3900,16 @@ func (e *Engine) tombstoneMissingWatchSourcesLocked(
38763900
roots []string,
38773901
spool reconciliationSpoolStore,
38783902
) (deleted int, retErr error) {
3879-
return e.tombstoneMissingWatchSourcesForAgentLocked(ctx, roots, "", spool)
3903+
return e.tombstoneMissingWatchSourcesForAgentLocked(
3904+
ctx, roots, "", nil, spool,
3905+
)
38803906
}
38813907

38823908
func (e *Engine) tombstoneMissingWatchSourcesForAgentLocked(
38833909
ctx context.Context,
38843910
roots []string,
38853911
agentFilter parser.AgentType,
3912+
nonAuthoritativeProviders map[parser.AgentType]struct{},
38863913
spool reconciliationSpoolStore,
38873914
) (deleted int, retErr error) {
38883915
if e.pathRewriter != nil {
@@ -3895,6 +3922,9 @@ func (e *Engine) tombstoneMissingWatchSourcesForAgentLocked(
38953922
if agentFilter != "" && agent != agentFilter {
38963923
continue
38973924
}
3925+
if _, nonAuthoritative := nonAuthoritativeProviders[agent]; nonAuthoritative {
3926+
continue
3927+
}
38983928
var provider parser.Provider
38993929
var replacementIndex reconciliationSpoolStore
39003930
ownsReplacementIndex := false

0 commit comments

Comments
 (0)