Skip to content

fix(db): promote orphan subagent sessions to sidebar roots - #725

Merged
wesm merged 5 commits into
kenn-io:mainfrom
rodboev:pr/orphan-subagent-recovery
Jun 18, 2026
Merged

fix(db): promote orphan subagent sessions to sidebar roots#725
wesm merged 5 commits into
kenn-io:mainfrom
rodboev:pr/orphan-subagent-recovery

Conversation

@rodboev

@rodboev rodboev commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

fix(db): promote orphan subagent sessions to sidebar roots via IncludeOrphans flag

Summary

  • Adds IncludeOrphans to SessionFilter so sidebar reads can promote missing-parent canonical child sessions as synthetic roots instead of silently dropping them.
  • Unifies canonical-root selection across the paginated and unpaginated sidebar paths, using the same canonical child relationship set for subagent, fork, and continuation.
  • Keeps flat list behavior distinct from root grouping: subagent and fork remain hidden from normal root lists, while continuation stays attached to its live parent for sidebar grouping.
  • Applies sidebar orphan behavior across SQLite, PostgreSQL, and DuckDB by setting IncludeOrphans in all three sidebar entrypoints.
  • Adds paginated SQLite and PostgreSQL coverage for nested missing-parent orphans, soft-deleted-parent non-promotion, and live-parent continuation nesting.

Scope

Changed files:

  • internal/db/sessions.go - SQLite sidebar opts into orphan recovery and paginated roots compose base filtering with shared canonical-root logic.
  • internal/db/query_dialect.go, internal/db/query_dialect_test.go - shared canonical-root predicates, dialect relationship sets, and SQL-shape coverage.
  • internal/db/filter_test.go - SQLite orphan, continuation, and paginated sidebar regression coverage.
  • internal/postgres/sessions.go, internal/postgres/store_test.go - PostgreSQL sidebar parity and pgtest coverage.
  • internal/duckdb/store.go - DuckDB sidebar opts into orphan recovery and uses the populated canonical child set.

No schema change. No frontend change. No dataVersion bump required.

Validation

Focused checks run locally:

go test -tags "fts5,kit_posthog_disabled" go.kenn.io/agentsview/internal/db -run "TestBuildSessionFilterSQLRendersIncludeChildrenCTE|TestIncludeOrphans|TestSidebarSessionIndex" -count=1
go test -tags "kit_posthog_disabled" go.kenn.io/agentsview/internal/duckdb -run "TestDuckDBStoreContract/sessions_cursors_and_metadata" -count=1
go test -tags "fts5,pgtest" ./internal/postgres/... -run "TestStoreGetSidebarSessionIndex.*Orphan|TestStoreGetSidebarSessionIndexPaginatesByDescendantFreshness|TestStoreGetSidebarSessionIndexPaginatesContinuationsAsDescendants" -count=1
go vet ./internal/db/...
go vet ./internal/postgres/...

Fixes #382

@roborev-ci

roborev-ci Bot commented Jun 18, 2026

Copy link
Copy Markdown

roborev: Combined Review (c688988)

Medium-risk backend parity and sidebar root eligibility issues remain.

Medium

  • internal/postgres/sessions.go:423
    SQLite sidebar now forces IncludeOrphans, but the PostgreSQL sidebar path still only sets IncludeChildren, and its paginated root query still uses the old child-excluding root filter. pg serve will continue hiding orphan subagent/fork sessions, so sidebar behavior diverges across supported backends.
    Fix: Mirror the SQLite IncludeOrphans and root-candidate logic in the PostgreSQL sidebar implementation, with PostgreSQL parity coverage.

  • internal/db/sessions.go:362
    The paginated/starred SQLite sidebar can now promote children of soft-deleted parents as root groups. rootWhere no longer excludes child relationships, and buildCanonicalRootWhere treats “no non-deleted parent” as root eligibility, while the unpaginated CTE only promotes rows with no parent row at all.
    Fix: Make the canonical root predicate explicit: normal roots are non-child relationships, and orphan promotion is only child relationship AND NOT EXISTS any parent row; add paginated sidebar coverage for soft-deleted parents.


Panel: ci_default_security | Synthesis: codex, 8s | Members: codex_default (codex/default, done, 4m40s), codex_security (codex/security, done, 41s) | Total: 5m29s

@rodboev

rodboev commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

roborev triage (c688988)

sessions.go:423 — PostgreSQL sidebar parity
Out of scope. This PR fixes orphan subagent recovery in the SQLite sidebar path (issue #382). The PostgreSQL sidebar in internal/postgres/sessions.go has its own independent query path and would need separate changes. PG parity for orphan recovery is a valid follow-up but not a regression introduced by this PR.

sessions.go:362 — soft-deleted parent promotion
Not a real issue. The CTE's orphan UNION promotes rows where NOT EXISTS (SELECT 1 FROM sessions p WHERE p.id = s.parent_session_id), meaning the parent row must be entirely absent, not merely soft-deleted. A child of a soft-deleted parent still has a parent row in the table, so it will not be incorrectly promoted as a root group by the orphan recovery logic.

@mjacobs mjacobs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orphan-as-root approach (option (a) from #382) is the right call, and the unpaginated SQLite path reads correctly.

roborev's two findings should block merge and I agree with both: the PostgreSQL sidebar not getting the fix (parity), and the paginated path promoting children of soft-deleted parents. Not repeating them here.

A few additional points, inline where localized:

  • The soft-deleted divergence exists because the PR ships two independent promotion implementations — worth consolidating to one shared definition (note on buildCanonicalRootWhere).
  • The new tests exercise the CTE path, not the paginated sidebar that actually ships (note on the tests).
  • Small relationship-set drift between the two implementations (note on the orphan seed).

Comment thread internal/db/sessions.go
Comment thread internal/db/query_dialect.go Outdated
Comment thread internal/db/filter_test.go
@rodboev

rodboev commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review requests in the latest push:

  1. PostgreSQL sidebar now matches the SQLite orphan-root behavior.

    • internal/postgres/sessions.go now forces IncludeOrphans for sidebar reads and uses the same paginated root-candidate logic.
  2. The canonical root definition is now shared instead of duplicated.

    • Moved the child/orphan root helpers into the shared session filter builder so SQLite and PostgreSQL both reuse the same rule.
  3. Removed the orphan-promotion relationship drift.

    • Orphan promotion now applies only to child relationships (subagent, fork).
    • continuation remains a normal root/descendant path and is no longer treated as an orphan-promoted child.
  4. Added coverage for the shipping paginated paths.

    • SQLite paginated sidebar tests now cover nested orphan trees and confirm soft-deleted parents do not promote children.
    • PostgreSQL parity tests cover orphan root pagination and the soft-deleted-parent case as well.

Validation run locally:

  • go test -tags "fts5" ./internal/db/... -run "TestIncludeOrphans|TestSidebarSessionIndex" -count=1
  • go test -tags "fts5,pgtest" ./internal/postgres/... -run "TestStoreGetSidebarSessionIndex.*Orphan|TestStoreGetSidebarSessionIndexPaginatesByDescendantFreshness|TestStoreGetSidebarSessionIndexPaginatesContinuationsAsDescendants" -count=1
  • go vet ./internal/db/...
  • go vet ./internal/postgres/...

@roborev-ci

roborev-ci Bot commented Jun 18, 2026

Copy link
Copy Markdown

roborev: Combined Review (3ba2ff0)

Medium severity regression found; no security issues reported.

Medium

  • internal/db/query_dialect.go:284 - BuildCanonicalRootWhere now uses the subagent/fork relationship set for paginated sidebar root detection, so continuation rows with loaded parents become independent root candidates. This can make a root with a continuation counted and paged twice, regressing sidebar pagination and the existing PostgreSQL continuation pagination test.

    Fix: Use a separate canonical-root child relationship set that includes continuation when the parent exists, while keeping list-session filtering scoped to subagent/fork if continuations should remain top-level there.


Panel: ci_default_security | Synthesis: codex, 6s | Members: codex_default (codex/default, done, 4m31s), codex_security (codex/security, done, 40s) | Total: 5m17s

@rodboev

rodboev commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up fix in the latest push:

The shared canonical-root refactor accidentally changed paginated sidebar grouping for continuation sessions. That made a continuation with an existing parent become its own root candidate, which regressed sidebar pagination/index behavior and broke the existing continuation descendant tests.

Adjusted the split so the two semantics stay distinct:

  1. Orphan promotion still uses the narrower child set (subagent, fork).
  2. Canonical root grouping uses a separate child set that still includes continuation, so continuations remain attached to their existing parent/root group when the parent exists.

That keeps the reviewer-requested orphan promotion change while preserving the pre-existing continuation pagination behavior.

Validation run locally:

  • go test -tags "fts5" ./internal/db/... -run "TestBuildSessionFilterSQLRendersIncludeChildrenCTE|TestIncludeOrphans|TestSidebarSessionIndex" -count=1
  • go test -tags "fts5" ./internal/server/... -run "TestSidebarIndexPaginationTreatsContinuationsAsDescendants|TestSidebarIndexPaginatesByDescendantFreshness" -count=1
  • go test -tags "fts5,pgtest" ./internal/postgres/... -run "TestStoreGetSidebarSessionIndex.*Orphan|TestStoreGetSidebarSessionIndexPaginatesByDescendantFreshness|TestStoreGetSidebarSessionIndexPaginatesContinuationsAsDescendants" -count=1
  • go vet ./internal/db/...
  • go vet ./internal/postgres/...

@roborev-ci

roborev-ci Bot commented Jun 18, 2026

Copy link
Copy Markdown

roborev: Combined Review (35bc929)

No issues found.


Panel: ci_default_security | Synthesis: codex | Members: codex_default (codex/default, done, 5m5s), codex_security (codex/security, done, 56s) | Total: 6m1s

@mjacobs mjacobs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed — the shared-QueryDialect refactor is the right call: it closes the SQLite/PG parity gap at the seam rather than per-path, and splitting sidebar vs canonical relationship sets is a clean way to keep continuations attached. I confirmed the soft-deleted-parent non-promotion is deliberate and now consistent for subagent/fork across both paths.

A few residuals (inline where localized), all minor:

  • One continuation-only divergence is left between the paginated and unpaginated paths — inline note on the orphan seed.
  • DuckDB is the third sidebar backend and didn't get the change — inline note.
  • Tiny cleanup: in BuildCanonicalRootWhere, base already promotes every orphan (NOT (canonicalChild AND parent_exists) is true whenever the parent is absent), so the includeOrphans OR-branch is a no-op and the parameter is effectively dead — worth dropping, or making it actually gate, so a future false caller can't silently re-diverge from the unpaginated path.
  • Tests: the soft-deleted-parent assertion lives only in a pgtest (behind the build tag, so not in the default unit run), and live-parent continuation isn't exercised through the paged path — a SQLite-layer test for both would guard exactly the cases this PR turns on.

Comment thread internal/db/query_dialect.go Outdated
Comment thread internal/db/query_dialect.go Outdated
@rodboev

rodboev commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the residual sidebar-root feedback in the latest push:

  1. The unpaginated IncludeChildren CTE now uses the same canonical-root rule as the paginated sidebar.

    • Canonical children are subagent, fork, and continuation.
    • Missing-parent canonical children are promoted only when IncludeOrphans is set.
    • Continuations with live or soft-deleted parents no longer drift between paginated and unpaginated behavior.
  2. DuckDB now matches the SQLite and PostgreSQL sidebar entrypoints.

    • DuckDBQueryDialect has the same canonical child relationship set.
    • DuckDB GetSidebarSessionIndex sets IncludeOrphans = true with IncludeChildren = true.
  3. Added paginated SQLite coverage for the cases called out in review.

    • nested missing-parent orphan recovery
    • soft-deleted parent continuation exclusion
    • live-parent continuation nesting

Focused validation passed locally:

go test -tags "fts5,kit_posthog_disabled" go.kenn.io/agentsview/internal/db -run "TestBuildSessionFilterSQLRendersIncludeChildrenCTE|TestIncludeOrphans|TestSidebarSessionIndex" -count=1
go test -tags "kit_posthog_disabled" go.kenn.io/agentsview/internal/duckdb -run "TestDuckDBStoreContract/sessions_cursors_and_metadata" -count=1

@roborev-ci

roborev-ci Bot commented Jun 18, 2026

Copy link
Copy Markdown

roborev: Combined Review (24fa56f)

No issues found.


Panel: ci_default_security | Synthesis: codex | Members: codex_default (codex/default, done, 7m42s), codex_security (codex/security, done, 1m0s) | Total: 8m42s

@wesm
wesm merged commit b93deda into kenn-io:main Jun 18, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Subagent sessions invisible when their parent transcript is missing

3 participants