Layer three additive pieces on top of the #7021 / #6931 continuation
predicate without changing its shape or CONTINUATION_STARTED_AT_TOLERANCE_SECONDS:
- _reset_from is a boundary marker alongside _branched_from/_delegate_from
(Hermes Agent stamps it in gateway/session_recovery.py and filters it in
_NON_CONTINUATION_CHILD_FILTER_SQL). The same predicate drives the sidebar
listing, lineage metadata/report and the transcript stitcher.
- _branch_markers() returns ('none' | 'markers' | 'unknown', markers);
unparsable, non-object, too-deep, or non-string marker payloads are
'unknown' and fail closed as a lineage boundary instead of degrading to
"no markers" (fail open).
- gateway_watcher._snapshot_hash() hashes the complete canonicalized published
payload, and the cheap fingerprint covers model_config, so a projection change
to title/created_at/metadata with fixed (id, updated_at, message_count) emits
sessions_changed.
The open/import transcript stitcher (get_state_db_session_messages with
stitch_continuations=True) runs its own lineage SELECT that omitted
session_source/model_config, so the shared _is_continuation_session guard
added for the listing path never fired there: an explicit branch/delegate
child (model_config._branched_from/_delegate_from) starting within the 2s
tolerance window got its rows stitched into the parent transcript on open.
Select both identity columns when the schema has them (NULL AS fallback for
older state.db schemas), so the same guard applies in the stitch path.
Tests (tests/test_session_lineage_metadata_api.py):
- branched/delegate children inside the tolerance stay out of the parent
transcript on open/import (fail before this fix, pass after);
- old-schema rows without the identity columns still stitch genuine
compression continuations (no crash, no behavior change).
Re-gate #7021: two silent sidebar regressions on the #6931 continuation
tolerance.
1. MUST-FIX 1 — branch marker in model_config. The session_source=='fork'
guard only covers WebUI-created forks (/api/session/branch). Explicit
Agent branches/delegates carry no session_source: they are marked in the
model_config JSON column (_branched_from / _delegate_from). The caller
queries did not select model_config at all, so an Agent branch starting
inside the 2s tolerance window of its parent's compression passed the
widened check and vanished from the sidebar. Fix: select model_config in
the classifier's caller queries (get_cli_sessions, lineage report, lineage
metadata) and reject a child whose _branched_from / _delegate_from points
at the parent being classified, BEFORE the tolerance is applied. The
marker must reference this parent: compression continuations inherit the
rotated agent's model_config verbatim (mirrors hermes-agent
_NON_CONTINUATION_CHILD_FILTER_SQL).
2. MUST-FIX 2 — unbounded title fallback removed. Titles are user-controlled
and non-unique, so parent_title == child_title beyond the tolerance window
collapsed independent same-title branches arbitrarily far apart. The
fallback is gone entirely; only the bounded 2s early-side tolerance
(CONTINUATION_STARTED_AT_TOLERANCE_SECONDS) remains, after the reliable
non-continuation guards.
Tests: same-title independent child outside tolerance stays visible (was
collapsed); _branched_from/_delegate_from children inside the tolerance stay
visible (reviewer's adversarial probe); unit coverage for marker parsing
(JSON string, dict, unparsable) and marker-points-at-other-session
preservation. All 4 new tests are RED on the previous head and GREEN here.
Closes#6931
_is_continuation_session() required float(child.started_at) >= float(parent.ended_at).
A compression/cli_close handoff can persist the continuation row a few ms BEFORE
the parent's ended_at lands (write-order race), so the strict comparison returned
False despite every other piece of evidence (direct parent_session_id link, same
source, end_reason=compression/cli_close, non-fork). The backend then labeled the
continuation as relationship_type=child_session, causing duplicate same-title
sidebar rows and spurious child-session entries (#6931).
Allow a small tolerance (2s) for handoff-consistent timing; beyond it, an exact
title match on the parent/child rows still confirms the same handoff. Fork,
cross-source, and non-compression/cli_close guards are preserved, and materially
overlapping children remain separate child sessions.
Adds API-level regression tests (sub-second overlap forms one lineage; material
overlap stays a child session; same-title evidence beyond tolerance) plus focused
unit coverage of the classifier.
Closes#6931
Converged after deep bounce cycles: Finding A (production regression — skipped the
state.db source correction before the CLI filter, dropping WebUI sessions with stale-CLI
sidecar flags) FIXED via _apply_sidebar_state_db_overrides(result) before the routes.py
filter on both index + full-scan paths; Finding B (flaky lineage test) FIXED via hermetic
_persisted_session_ids_snapshot fixture. 16 own tests deterministic. Code byte-faithful
from PR head e1a3ecf22eca.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
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
PR #1358 added the client-side lineage collapse helper, but
/api/sessions often did not include _lineage_root_id for the WebUI
JSON sessions visible in the sidebar. In that case the helper has no
grouping key and multiple same-title continuation rows remain visible.
This PR:
- Reads parent_session_id and end_reason from state.db.sessions for
the WebUI sidebar's session ids
- Walks the parent chain when end_reason is 'compression' or
'cli_close', producing _lineage_root_id and _compression_segment_count
- Cycle-detects via a 'seen' set
- Preserves projected lineage metadata on imported/gateway session rows
- Allows sidebar collapse to group cross-surface continuation chains
(CLI-close → WebUI continuation) while keeping non-continuation
parent rows flat
Co-authored-by: Dennis Soong <dso2ng@gmail.com>