Skip to content

Commit 7b130fa

Browse files
committed
fix(sync): refresh inherited Omnigent metadata
Unsupported parser outcomes may be persisted as clean skip entries, so a cold tracker must preserve that result instead of turning restart validation into a failure. Root workspace and branch changes also affect archived descendants even when their own source rows are unchanged. Resolving those virtual sources through the archive’s indexed parent relationships keeps incremental correctness proportional to the affected subagent tree.
1 parent 3a3e106 commit 7b130fa

4 files changed

Lines changed: 268 additions & 1 deletion

File tree

internal/db/sessions.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2251,6 +2251,85 @@ func (db *DB) ListSessionIDsByFilePath(path, agent string) ([]string, error) {
22512251
return ids, nil
22522252
}
22532253

2254+
const descendantSessionRootBatchSize = 100
2255+
2256+
// ListActiveDescendantSessionSourcePaths returns the source paths of active
2257+
// descendants already linked beneath parentIDs. Both the seed and recursive
2258+
// steps use idx_sessions_parent, so work scales with the affected subagent
2259+
// trees rather than every archived session.
2260+
func (db *DB) ListActiveDescendantSessionSourcePaths(
2261+
ctx context.Context,
2262+
machine, agent string,
2263+
parentIDs []string,
2264+
) ([]string, error) {
2265+
if err := ctx.Err(); err != nil {
2266+
return nil, err
2267+
}
2268+
seen := make(map[string]struct{})
2269+
var paths []string
2270+
for start := 0; start < len(parentIDs); start += descendantSessionRootBatchSize {
2271+
end := min(start+descendantSessionRootBatchSize, len(parentIDs))
2272+
batch := parentIDs[start:end]
2273+
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(batch)), ",")
2274+
query := `
2275+
WITH RECURSIVE descendants(id, file_path) AS (
2276+
SELECT id, file_path
2277+
FROM sessions INDEXED BY idx_sessions_parent
2278+
WHERE parent_session_id IS NOT NULL
2279+
AND parent_session_id IN (` + placeholders + `)
2280+
AND machine = ? AND agent = ? AND deleted_at IS NULL
2281+
UNION
2282+
SELECT s.id, s.file_path
2283+
FROM sessions AS s INDEXED BY idx_sessions_parent
2284+
JOIN descendants AS d ON s.parent_session_id = d.id
2285+
WHERE s.parent_session_id IS NOT NULL
2286+
AND s.machine = ? AND s.agent = ? AND s.deleted_at IS NULL
2287+
)
2288+
SELECT file_path
2289+
FROM descendants
2290+
WHERE file_path IS NOT NULL AND file_path <> ''
2291+
ORDER BY file_path`
2292+
args := make([]any, 0, len(batch)+4)
2293+
for _, id := range batch {
2294+
args = append(args, id)
2295+
}
2296+
args = append(args, machine, agent, machine, agent)
2297+
rows, err := db.getReader().QueryContext(ctx, query, args...)
2298+
if err != nil {
2299+
return nil, fmt.Errorf(
2300+
"listing active descendant session sources: %w", err,
2301+
)
2302+
}
2303+
for rows.Next() {
2304+
var path string
2305+
if err := rows.Scan(&path); err != nil {
2306+
_ = rows.Close()
2307+
return nil, fmt.Errorf(
2308+
"scanning active descendant session source: %w", err,
2309+
)
2310+
}
2311+
if _, exists := seen[path]; exists {
2312+
continue
2313+
}
2314+
seen[path] = struct{}{}
2315+
paths = append(paths, path)
2316+
}
2317+
if err := rows.Err(); err != nil {
2318+
_ = rows.Close()
2319+
return nil, fmt.Errorf(
2320+
"iterating active descendant session sources: %w", err,
2321+
)
2322+
}
2323+
if err := rows.Close(); err != nil {
2324+
return nil, fmt.Errorf(
2325+
"closing active descendant session sources: %w", err,
2326+
)
2327+
}
2328+
}
2329+
sort.Strings(paths)
2330+
return paths, nil
2331+
}
2332+
22542333
const storedSourcePathHintRootBatchSize = 100
22552334

22562335
// StoredSourcePathHintScope identifies one affected stored-source prefix.

internal/parser/omnigent_provider.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,20 @@ func IsOmnigentContainerSource(source SourceRef) bool {
245245
return ok && src.Container != "" && src.MemberID == ""
246246
}
247247

248+
// OmnigentMemberSessionID returns the archived session identity addressed by a
249+
// virtual Omnigent member source. The sync engine uses it to look up already
250+
// archived descendants whose cwd and branch inherit from a changed root.
251+
func OmnigentMemberSessionID(source SourceRef) (string, bool) {
252+
if source.Provider != AgentOmnigent {
253+
return "", false
254+
}
255+
src, ok := source.Opaque.(multiSessionSource)
256+
if !ok || src.MemberID == "" {
257+
return "", false
258+
}
259+
return omnigentIDPrefix + src.MemberID, true
260+
}
261+
248262
func omnigentLegacySessionIDs(
249263
src multiSessionSource, results []ParseResult,
250264
) []string {
@@ -1145,6 +1159,13 @@ func (t *omnigentChangeTracker) restoreCachedContainer(
11451159
defer conn.Close()
11461160
schema, err := detectOmnigentSchema(conn)
11471161
if err != nil {
1162+
if omnigentSchemaUnsupported(err) {
1163+
// Unsupported parse outcomes are intentionally skip-cached. A
1164+
// restart may validate that cache entry with a cold tracker; the
1165+
// known unsupported state still proves the cached skip, but cannot
1166+
// seed supported-schema change cursors.
1167+
return false, nil
1168+
}
11481169
return false, err
11491170
}
11501171
conversationRowID, conversationTail, err :=

internal/sync/engine.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,21 @@ func (e *Engine) classifyProviderChangedPath(
11031103
}
11041104
continue
11051105
}
1106+
if agentType == parser.AgentOmnigent {
1107+
sources, err = e.expandOmnigentInheritedMetadataSources(
1108+
ctx, provider, sources,
1109+
)
1110+
if err != nil {
1111+
classificationErr = errors.Join(
1112+
classificationErr,
1113+
fmt.Errorf(
1114+
"%s provider dependent-source classification for %q: %w",
1115+
def.Type, path, err,
1116+
),
1117+
)
1118+
continue
1119+
}
1120+
}
11061121
for _, source := range sources {
11071122
sourcePath := providerDiscoveredPath(source)
11081123
if sourcePath == "" {
@@ -1148,6 +1163,60 @@ func (e *Engine) classifyProviderChangedPath(
11481163
return files, classificationErr
11491164
}
11501165

1166+
func (e *Engine) expandOmnigentInheritedMetadataSources(
1167+
ctx context.Context,
1168+
provider parser.Provider,
1169+
sources []parser.SourceRef,
1170+
) ([]parser.SourceRef, error) {
1171+
resolver, ok := provider.(parser.ReconciliationSourceResolver)
1172+
if !ok || len(sources) == 0 {
1173+
return sources, nil
1174+
}
1175+
seenSources := make(map[string]struct{}, len(sources))
1176+
parentIDs := make([]string, 0, len(sources))
1177+
seenParents := make(map[string]struct{}, len(sources))
1178+
for _, source := range sources {
1179+
if path := providerDiscoveredPath(source); path != "" {
1180+
seenSources[path] = struct{}{}
1181+
}
1182+
id, member := parser.OmnigentMemberSessionID(source)
1183+
if !member {
1184+
continue
1185+
}
1186+
id = applyIDPrefixToID(e.idPrefix, id)
1187+
if _, exists := seenParents[id]; exists {
1188+
continue
1189+
}
1190+
seenParents[id] = struct{}{}
1191+
parentIDs = append(parentIDs, id)
1192+
}
1193+
paths, err := e.db.ListActiveDescendantSessionSourcePaths(
1194+
ctx, e.machine, string(parser.AgentOmnigent), parentIDs,
1195+
)
1196+
if err != nil {
1197+
return nil, err
1198+
}
1199+
for _, path := range paths {
1200+
if _, exists := seenSources[path]; exists {
1201+
continue
1202+
}
1203+
source, found, err := resolver.SourceForReconciliation(ctx, path, "")
1204+
if err != nil {
1205+
return nil, err
1206+
}
1207+
if !found {
1208+
continue
1209+
}
1210+
sourcePath := providerDiscoveredPath(source)
1211+
if sourcePath == "" {
1212+
continue
1213+
}
1214+
seenSources[sourcePath] = struct{}{}
1215+
sources = append(sources, source)
1216+
}
1217+
return sources, nil
1218+
}
1219+
11511220
func storedSourceDBHintScopes(
11521221
scopes []parser.StoredSourceHintScope,
11531222
) []db.StoredSourcePathHintScope {

internal/sync/omnigent_integration_test.go

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,92 @@ func TestAuditOmnigentDetectsMultiWorkspaceMetadataOnlyEdit(t *testing.T) {
13031303
"authoritative reconciliation must refresh multi-workspace metadata")
13041304
}
13051305

1306+
func TestSyncPathsOmnigentRootMetadataRefreshesExistingSubagent(t *testing.T) {
1307+
if testing.Short() {
1308+
t.Skip("skipping integration test")
1309+
}
1310+
observed := make(map[int]int64)
1311+
for _, archiveSize := range []int{130, 1030} {
1312+
t.Run(fmt.Sprintf("archive_%d", archiveSize), func(t *testing.T) {
1313+
root := t.TempDir()
1314+
dbPath := writeOmnigentSplitSyncDB(t, root, archiveSize)
1315+
writer, err := sql.Open("sqlite3", dbPath)
1316+
require.NoError(t, err)
1317+
_, err = writer.Exec(`
1318+
UPDATE conversations
1319+
SET parent_conversation_id = 'conv_0000',
1320+
root_conversation_id = 'conv_0000'
1321+
WHERE workspace_id = 0 AND id = 'conv_0001'`)
1322+
require.NoError(t, err)
1323+
_, err = writer.Exec(`
1324+
UPDATE omnigent_conversation_metadata
1325+
SET workspace = '/work/before', git_branch = 'main'
1326+
WHERE workspace_id = 0 AND id = 'conv_0000'`)
1327+
require.NoError(t, err)
1328+
_, err = writer.Exec(`
1329+
UPDATE omnigent_conversation_metadata
1330+
SET kind = 2, workspace = '', git_branch = ''
1331+
WHERE workspace_id = 0 AND id = 'conv_0001'`)
1332+
require.NoError(t, err)
1333+
require.NoError(t, writer.Close())
1334+
1335+
archive := dbtest.OpenTestDB(t)
1336+
var resultCount atomic.Int64
1337+
factory := omnigentParseCountingFactory{
1338+
delegate: omnigentDefaultProviderFactory(t),
1339+
count: new(atomic.Int64),
1340+
results: &resultCount,
1341+
}
1342+
engine := sync.NewEngine(archive, sync.EngineConfig{
1343+
AgentDirs: map[parser.AgentType][]string{
1344+
parser.AgentOmnigent: {root},
1345+
},
1346+
Machine: "local",
1347+
ProviderFactories: []parser.ProviderFactory{factory},
1348+
})
1349+
t.Cleanup(engine.Close)
1350+
syncOmnigentArchive(t, engine, archive, archiveSize)
1351+
childID := "omnigent:0:conv_0001"
1352+
before, err := archive.GetSession(t.Context(), childID)
1353+
require.NoError(t, err)
1354+
require.NotNil(t, before)
1355+
assert.Equal(t, "/work/before", before.Cwd)
1356+
assert.Equal(t, "before", before.Project)
1357+
assert.Equal(t, "main", before.GitBranch)
1358+
1359+
writer, err = sql.Open("sqlite3", dbPath)
1360+
require.NoError(t, err)
1361+
_, err = writer.Exec(`
1362+
UPDATE conversations
1363+
SET updated_at = ?
1364+
WHERE workspace_id = 0 AND id = 'conv_0000'`,
1365+
time.Now().Unix(),
1366+
)
1367+
require.NoError(t, err)
1368+
_, err = writer.Exec(`
1369+
UPDATE omnigent_conversation_metadata
1370+
SET workspace = '/work/after', git_branch = 'review'
1371+
WHERE workspace_id = 0 AND id = 'conv_0000'`)
1372+
require.NoError(t, err)
1373+
require.NoError(t, writer.Close())
1374+
1375+
resultCount.Store(0)
1376+
require.NoError(t, engine.SyncPathsContext(
1377+
t.Context(), []string{dbPath},
1378+
))
1379+
observed[archiveSize] = resultCount.Load()
1380+
after, err := archive.GetSession(t.Context(), childID)
1381+
require.NoError(t, err)
1382+
require.NotNil(t, after)
1383+
assert.Equal(t, "/work/after", after.Cwd)
1384+
assert.Equal(t, "after", after.Project)
1385+
assert.Equal(t, "review", after.GitBranch)
1386+
})
1387+
}
1388+
assert.Equal(t, observed[130], observed[1030],
1389+
"dependent metadata refresh work must not grow with unrelated conversations")
1390+
}
1391+
13061392
func TestScheduledOmnigentReconciliationIsBoundedByChangedMembers(t *testing.T) {
13071393
if testing.Short() {
13081394
t.Skip("skipping integration test")
@@ -1610,7 +1696,19 @@ func TestSyncOmnigentUnsupportedSchemaPreservesArchive(t *testing.T) {
16101696
require.NoError(t, err)
16111697
require.NoError(t, writer.Close())
16121698

1613-
engine.SyncAll(context.Background(), nil)
1699+
firstUnsupported := engine.SyncAll(context.Background(), nil)
1700+
assert.Zero(t, firstUnsupported.Failed)
1701+
engine.Close()
1702+
restarted := sync.NewEngine(archive, sync.EngineConfig{
1703+
AgentDirs: map[parser.AgentType][]string{
1704+
parser.AgentOmnigent: {root},
1705+
},
1706+
Machine: "local",
1707+
})
1708+
t.Cleanup(restarted.Close)
1709+
secondUnsupported := restarted.SyncAll(context.Background(), nil)
1710+
assert.Zero(t, secondUnsupported.Failed,
1711+
"a cached unsupported source must remain a clean skip")
16141712
after, err := archive.GetSession(context.Background(), "omnigent:conv_0000")
16151713
require.NoError(t, err)
16161714
require.NotNil(t, after, "unsupported source must not retire archived sessions")

0 commit comments

Comments
 (0)