Skip to content

fix(#5571): keep fork children out of compression stitch - #5582

Closed
rodboev wants to merge 3 commits into
nesquena:masterfrom
rodboev:pr/5571-fork-compression-stitch-skip
Closed

rodboev wants to merge 3 commits into
nesquena:masterfrom
rodboev:pr/5571-fork-compression-stitch-skip

Conversation

@rodboev

@rodboev rodboev commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Thinking Path

  • Clear Conversation preserves genuine fork parent links so sidebar nesting and the "Forked from" label keep working after a clear.
  • The display stitch has overlapping fork states: a plain fork must not stitch its original non-fork parent, while a fork that later gets compressed can have one or more fork-sourced compression snapshots.
  • _webui_sidecar_lineage_messages_for_display now allows a fork root to stitch fork-sourced snapshot parents and stops at the first non-fork ancestor, preserving fork compression history without resurrecting the original parent.

What Changed

  • api/routes.py: allow compressed fork continuations to stitch fork-sourced snapshots while keeping ordinary fork children isolated from non-fork parents.
  • tests/test_session_lineage_full_transcript.py: keep the original fork-with-later-snapshot-parent regression and add a multiply-compressed fork regression that proves fork snapshots are restored without appending the original parent.

Why It Matters

A cleared fork child stays independent if its original parent is compressed later, while a compressed fork continuation still shows the pre-compression turns that belong to that fork.

Verification

  • pytest tests/test_session_lineage_full_transcript.py -v --timeout=60, 11 passed.
  • pytest tests/test_issue5532_clear_truncation_watermark.py -v --timeout=60, 7 passed.

Full-suite CI context, not a required local check: pytest tests/ -v --timeout=60.

Upstream

Closes #5571.

Model Used

GPT 5.5 via Codex CLI

@greptile-apps

greptile-apps Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a new guard to _webui_sidecar_lineage_messages_for_display that prevents a fork child from stitching a non-fork compression snapshot parent while still allowing multiply-compressed fork continuations to walk their full chain of fork-sourced snapshots. The root session's session_source is read once before the loop; if it is "fork", any parent whose session_source is not "fork" triggers an early break.

  • api/routes.py: Two lines added before the existing loop body — root_is_fork is set from the root session's source, and a conditional break stops traversal the moment a non-fork parent is encountered.
  • tests/test_session_lineage_full_transcript.py: Four new regression tests cover (1) fork child isolated from a snapshot-flagged non-fork parent, (2) multiply-compressed fork walking both snapshots without entering the original parent, and (3/4) the _merged_webui_lineage_messages_for_display guard for session_source="fork" and relationship_type="child_session" independently.

Confidence Score: 5/5

Safe to merge — the change is a targeted two-line guard with well-bounded scope and four new regression tests validating both isolation and multi-hop stitching

The guard reads only immutable session attributes, does not alter data writes, and is exercised by tests that cover the previously-broken fork-isolation path, the multi-hop compressed-fork path, and the two independent merged-lineage guards. No call sites were left unguarded and the existing snapshot-traversal path for non-fork sessions is unchanged

No files require special attention

Important Files Changed

Filename Overview
api/routes.py Adds root_is_fork flag and a conditional break in the snapshot-traversal loop; logic is correct for both isolation and multi-hop fork-compression chains
tests/test_session_lineage_full_transcript.py Adds four targeted regression tests — fork-isolation against snapshot parent, multi-hop fork-compression chain, and separate merged-lineage guard tests for source and relationship; all cases are meaningful and non-vacuous

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["_webui_sidecar_lineage_messages_for_display(session)"] --> B["root_is_fork = session.session_source == 'fork'"]
    B --> C["Loop: load parent via parent_session_id"]
    C --> D{parent exists AND\npre_compression_snapshot?}
    D -- No --> E["Break — no more snapshots"]
    D -- Yes --> F{root_is_fork AND\nparent_source != 'fork'?}
    F -- Yes --> G["Break — stop before non-fork parent\n(fork isolation guard)"]
    F -- No --> H{First hop AND\nsession already contains\nparent messages as prefix?}
    H -- Yes --> I["Return session_messages as-is\n(no duplication)"]
    H -- No --> J["Append parent to segments\nAdvance current = parent"]
    J --> C
    E --> K{Any segments collected?}
    K -- No --> L["Return session.messages"]
    K -- Yes --> M["Merge segments in reverse order\nthen append session messages"]
    M --> N["Return stitched transcript"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["_webui_sidecar_lineage_messages_for_display(session)"] --> B["root_is_fork = session.session_source == 'fork'"]
    B --> C["Loop: load parent via parent_session_id"]
    C --> D{parent exists AND\npre_compression_snapshot?}
    D -- No --> E["Break — no more snapshots"]
    D -- Yes --> F{root_is_fork AND\nparent_source != 'fork'?}
    F -- Yes --> G["Break — stop before non-fork parent\n(fork isolation guard)"]
    F -- No --> H{First hop AND\nsession already contains\nparent messages as prefix?}
    H -- Yes --> I["Return session_messages as-is\n(no duplication)"]
    H -- No --> J["Append parent to segments\nAdvance current = parent"]
    J --> C
    E --> K{Any segments collected?}
    K -- No --> L["Return session.messages"]
    K -- Yes --> M["Merge segments in reverse order\nthen append session messages"]
    M --> N["Return stitched transcript"]
Loading

Reviews (4): Last reviewed commit: "Preserve compressed fork history while i..." | Re-trigger Greptile

Comment thread tests/test_session_lineage_full_transcript.py
Comment thread tests/test_session_lineage_full_transcript.py Outdated
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — RED ⛔ (fork-exclusion fixes #5571 but makes COMPRESSED fork continuations lose their pre-compression history — CORE data loss)

Certified head: sha:f35bbe8b (rebased onto current master, git apply clean) · PR: #5582 · rodboev, fix(#5571): keep fork children out of compression stitch
Verdict: The 3-line guard correctly stops a pure fork from stitching its original parent (fixes #5571), but it early-returns on session_source=="fork" BEFORE checking the direct parent — and a fork that later gets COMPRESSED keeps session_source="fork" (compression rotation never clears it), so its continuation skips stitching its legitimate pre_compression_snapshot parent and loses the pre-compression history. Codex verified with a direct probe (returned only ["after compression only"]).

What I ran (rebased worktree /tmp/wt-rebase-5582)

Gate Result
Rebase onto current master ✅ git apply clean
Codex (reproduce) SHIP-WITH-FIXES — 1 CORE (compressed-fork continuation loses history); Codex probed it, I confirmed the code
Full pytest suite ✅ rc=0 green (the compressed-fork edge is NOT test-covered — Codex's direct probe caught it)
PR's own test ✅ 10/10 (test_session_lineage_full_transcript.py) — but none cover fork-then-compress

Findings

⛔ CORE (I + Codex CONFIRMED) — compressed fork continuations lose pre-compression history (api/routes.py:8034): the guard if source == "fork": return session_messages fires BEFORE the parent-load loop (8038+). But session_source="fork" (stamped by /api/session/branch at routes.py:13938, persisted models.py:1180/1443) is NEVER cleared by compression rotation (streaming.py:8276-8286 sets pre_compression_snapshot=False + parent_session_id=old_sid, leaves session_source alone). So a session that was forked and THEN compressed still has source=="fork" → the early return skips stitching its direct pre_compression_snapshot parent → the continuation displays only post-compression turns, losing the archived pre-compression transcript. Codex's direct probe returned only ["after compression only"] for such a session; origin/master (no early return) would stitch the parent. Fork-provenance and compression-continuation edges OVERLAP — session_source alone can't separate them. Fix (Codex-exact): don't early-return solely on session_source=="fork" — check the DIRECT parent first (if it's a pre_compression_snapshot, stitch parent+child), and use fork-provenance only to avoid walking further back into the ORIGINAL fork parent. Add a regression: child.session_source=="fork" + direct parent.pre_compression_snapshot=True → returns parent+child, but does NOT walk into the original fork parent.

✅ The #5571 intent is right (keep): a pure fork must not stitch its original conversation parent — that part is correct. The gap is that the guard is too coarse (position + sole-condition) for the fork-then-compress case.

Recommendation to the next agent / author

RED — gate-fail/changes-requested (1 CORE data-loss): move the fork check AFTER the direct-parent pre_compression_snapshot check — stitch a direct snapshot parent even for source=="fork" sessions, and use session_source=="fork" only to stop walking back into the ORIGINAL (non-snapshot) fork parent. The #5571 fix (pure fork doesn't stitch its conversation parent) is correct; it just needs to not clobber the legitimate compression-continuation stitch for a fork-then-compressed session. Add the fork-then-compress regression test. concept 4/5 (real #5571 fix; one overlapping-provenance edge). Author @rodboev (T1). crit=3, data-integrity. (Gate value: same family as #5556 — a lineage/clear/compression fix must handle OVERLAPPING provenance edges; session_source=="fork" and pre_compression_snapshot parent are not mutually exclusive (fork-then-compress), so a sole-condition early return drops a legitimate stitch. Enumerate the overlap matrix, don't gate on one flag.)


Gate-certifier layer (warm-up → gate → release). I do not merge/tag/deploy. Rebased onto current master; confirmed compression rotation (streaming.py:8276-8286) leaves session_source='fork' set, and the routes.py:8034 early-return (before parent-load) therefore skips a compressed fork's legitimate pre_compression_snapshot parent stitch → history loss (Codex probe: only post-compression turns). Suite green (edge not test-covered). Fix: check direct snapshot parent before the fork early-return. Cert valid for sha:f35bbe8b.

@nesquena-hermes nesquena-hermes added gate-fail Gate found blocking issue(s); fix-spec in comment; awaiting fix/re-push changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address labels Jul 4, 2026
@nesquena-hermes nesquena-hermes added the size:M Medium PR (≤10 files, ≤250 LOC) label Jul 4, 2026
@rodboev
rodboev force-pushed the pr/5571-fork-compression-stitch-skip branch from f35bbe8 to 4d3d488 Compare July 5, 2026 06:46
@rodboev

rodboev commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Good catch. I pushed 4d3d488. The sidecar stitch now checks the direct snapshot parent first, but for fork roots it only accepts a fork-sourced snapshot parent and then stops before walking back to the original parent (api/routes.py lines 8055-8075). That keeps the original #5571 isolation case intact while restoring compressed fork history.

I also added test_webui_compressed_fork_stitches_direct_snapshot_parent beside the existing snapshot-parent isolation test (tests/test_session_lineage_full_transcript.py lines 279-329), and updated the PR body with the local validation.

Comment thread api/routes.py Outdated
@rodboev
rodboev force-pushed the pr/5571-fork-compression-stitch-skip branch from 4d3d488 to 3cd031e Compare July 5, 2026 06:54
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — GREEN ✅ · CONVERGED (round-1 fork-then-compressed history loss fixed)

Certified head: sha:fc1b5888 (clean rebase, branch gate-rebase/5582-fork-compression-stitch) · PR: #5582 · rodboev, fix(#5571): keep fork children out of compression stitch
Verdict: Round-2 CONVERGED. My round-1 CORE (the if source=="fork": return early-return fired BEFORE the parent-load, so a fork-then-compressed session lost its pre-compression history) is fixed by restructuring the parent-walk so a compression-snapshot parent IS stitched even for forks, while a pure fork still stops at its original conversation parent. Codex SAFE, full suite green.

What I ran (rebased worktree /tmp/wt-rebase-5582b)

Gate Result
Rebase onto current master ✅ git apply clean
Codex (reproduce) SAFE TO SHIP — 0 findings; round-1 CORE confirmed fixed
Full pytest suite ✅ 12103 passed, 0 failed
lineage tests ✅ 11/11

Findings — round-1 CORE CLOSED

✅ Both cases now correct: the fork check moved from an unconditional pre-parent-load early-return to a captured root_is_fork = source=="fork" + a restructured parent-walk: load parent → if not parent.pre_compression_snapshot: break (only stitch compression snapshots) → if root_is_fork and parent_source != "fork": break (stop when a fork's chain reaches the ORIGINAL non-fork conversation parent). So:

  • a fork-then-COMPRESSED session (child session_source=="fork", direct parent pre_compression_snapshot=True) now STITCHES the snapshot parent → pre-compression history preserved (my round-1 CORE closed);
  • a pure fork still does NOT stitch its original conversation parent (Compression stitch should skip fork children (session_source==fork) to avoid post-clear resurrection corner #5571 fixed) — breaks at root_is_fork and parent_source!=fork;
  • a normal (non-fork) compression continuation still stitches.
    Codex confirmed the walk terminates (seen-set + break conditions sound), no over-stitch/infinite loop, is_safe_session_id+seen guards intact. 11 lineage + full suite green.

Recommendation to the next agent

Ready to merge — use branch gate-rebase/5582-fork-compression-stitch (sha:fc1b5888), NOT the PR's stale head 3cd031e4. Converged data-integrity fix — fork children no longer inherit their origin conversation (#5571) AND fork-then-compressed sessions keep their pre-compression history (round-1 CORE fixed). Codex SAFE + 11 lineage tests + full suite green. Backend lineage-logic, no visible surface (no Fable-UX leg applies). concept 4/5. Author @rodboev (T1). crit=3, data-integrity.


Gate-certifier layer (warm-up → gate → release). I do not merge/tag/deploy. Rebased onto current master; round-1 fork-then-compressed history-loss fixed (parent-walk now stitches pre_compression_snapshot parent before the fork-isolation break; root_is_fork+parent_source!=fork stops only at the original conversation parent), pure-fork isolation intact (#5571), Codex SAFE + 11 lineage + full suite green (0 failed). Backend (no Fable surface). Cert valid for sha:fc1b5888.

@nesquena-hermes nesquena-hermes added gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent and removed changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address gate-fail Gate found blocking issue(s); fix-spec in comment; awaiting fix/re-push labels Jul 5, 2026
nesquena-hermes added a commit that referenced this pull request Jul 5, 2026
release #5582: keep fork children out of compression stitch
pull Bot pushed a commit to AmirulAndalib/hermes-webui that referenced this pull request Jul 5, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.883 — thanks @rodboev! 🎉

A fork's full-transcript display no longer resurrects its original (non-fork) parent's messages after that parent is compressed. When the root session is a fork, the lineage stitch now allows fork-sourced snapshot parents but stops at the first non-fork ancestor — so a compressed fork continuation still shows its own pre-compression turns, while an ordinary fork child stays isolated from the original. Non-fork history display is unchanged.

Gate: Codex SAFE TO SHIP (display-only path, non-fork stitch byte-identical, no legitimate turns dropped, legacy missing-source falls back to prior behavior); full suite 12110 passed; 18 lineage/clear tests incl. a new multiply-compressed-fork regression. Merged via #5625.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent size:M Medium PR (≤10 files, ≤250 LOC)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compression stitch should skip fork children (session_source==fork) to avoid post-clear resurrection corner

2 participants