* fix(sessions): resolve branchy compression lineage to the freshest tip (#3751)
Compression parents can have multiple continuation-looking children when a stale
segment is resumed after a newer compressed branch already exists. The previous
projection followed the newest DIRECT child only, so it could hide the deeper
branch with the latest real activity and make a conversation look missing/stale
after compaction or session rotation.
- compression_tip() now walks ALL reachable continuation descendants (DFS) and
selects the freshest importable (messageful) tip by (last_activity, depth).
- read_session_lineage_metadata() expands descendants from the materialized
ancestors via the parent index (scoped, 20-hop cap), pulls per-session message
stats from the messages table, and exposes a canonical _lineage_tip_id so the
WebUI sidebar collapse picks the same tip as the projection.
Backward-compat hardening (two release-gate Codex findings, both fixed + tested):
The new message-stats / tip-scoring code must not raise on older/minimal or
non-standard state.db schemas, which previously collapsed the whole projection:
- messages table with NO `timestamp` column → MAX(timestamp) raised in SQL →
read_session_lineage_metadata returned {} (lost all lineage metadata).
- ISO-8601 TEXT messages.timestamp → float()/raw comparison raised TypeError;
in read_importable_agent_session_rows that propagated through compression_tip
and get_cli_sessions() swallowed it, hiding ALL imported agent rows.
Fixes:
- PRAGMA table_info(messages) detection: require session_id, only SELECT
MAX(timestamp) when present (else NULL + COUNT only); fall back to message_count.
- new _as_score() helper (first numerically-coercible value, else next
candidate e.g. started_at) used at EVERY tip-scoring / sort site in both
compression_tip() and freshest_continuation_tip(), plus the projection sort.
- regression tests: lineage metadata survives REAL/absent/TEXT messages.timestamp,
and read_importable_agent_session_rows survives a TEXT timestamp (no empty hide).
Co-authored-by: ai-ag2026 <[email protected]>
* docs(changelog): stamp v0.51.306 — Release JV (stage-a2 #3751)
---------
Co-authored-by: nesquena-hermes <[email protected]>
Opus pre-release findings on #1370 applied:
SHOULD-FIX 1: Tightened parent_session_id exposure to only emit when
the parent's end_reason is in {compression, cli_close}. Without this,
two distinct WebUI sessions sharing a non-continuation parent (e.g.
'user_stop') would get clustered by frontend's _sessionLineageKey
(which falls through to parent_session_id when _lineage_root_id is
missing) and incorrectly collapsed into a single sidebar row.
Updated assertions in:
- tests/test_session_lineage_metadata_api.py::
test_non_compression_state_db_parent_does_not_create_sidebar_lineage
- tests/test_pr1370_lineage_metadata_perf_and_orphan.py::
test_non_compression_parent_does_not_extend_lineage
SHOULD-FIX 2: Chunked the IN-clause to 500 vars to stay under
SQLITE_MAX_VARIABLE_NUMBER. Python 3.9 ships sqlite 3.31 with the
default limit of 999. A power user with 2000+ sessions in the
sidebar would hit OperationalError, the silent except-wrapper would
swallow it, and lineage collapse would never work. Added
test_in_clause_chunked_for_large_session_set with SQL interception
to lock the invariant in source.
PR addition (per user directive — Opus + my review, no second
independent review round needed for combined batch):
#1372 from @NocGeek — fix: persist manual cron run results.
Self-contained 89 LOC fix split out from the held #1352. Mirrors the
scheduled-cron path (cron/scheduler.py:1334-1364) exactly: saves
output, marks job complete, treats empty response as soft failure
with matching error string. 2 behavioral tests using sys.modules
monkeypatch to mock cron.scheduler.run_job. CI not yet attached
because branch is brand-new; ran the new tests + adjacent suites
locally — all pass.
Final test count: 3471 passing, 0 failed.
Also adds 2 more regression tests for the perf-fix invariants:
- test_in_clause_chunked_for_large_session_set
- test_two_children_sharing_non_continuation_parent_not_collapsed
Bundles:
- #1370 fix: expose session lineage metadata in API (@dso2ng)
Pre-release fixes applied:
1) Perf: replaced full table scan with parameterized WHERE id IN (...)
query. Original code did SELECT id, parent_session_id, end_reason
FROM sessions on every sidebar refresh. Measured 9ms cached scan at
1000 rows in production (up to ~450ms cold-cache); scales linearly.
New approach hits PRIMARY KEY + idx_sessions_parent — 50x faster
at 1000 rows, ~0.2ms regardless of total row count. Depth-bounded
to 20 hops to cap query count under pathological data.
2) Orphan-parent guard: suppress parent_session_id in API output when
the referenced parent row doesn't exist in state.db. The frontend's
#1358 _sessionLineageKey falls through to parent_session_id when
_lineage_root_id is missing — orphan references would create
never-collapsing single-row groups in the sidebar.
3) Regression suite (5 tests in
test_pr1370_lineage_metadata_perf_and_orphan.py):
- Pins the no-full-scan invariant by intercepting all SQL queries
and asserting no SELECT FROM sessions without a WHERE clause
- Pins orphan-parent suppression
- Pins cycle termination via threading.Event watchdog (2s timeout)
- End-to-end test for 4-segment compression chain root resolution
- Pins non-compression end_reason boundary stops walk