Skip to content

feat(phase 4): branch summary preservation in /tree (pi-style metadata) - #72

Merged
yogthos merged 1 commit into
mainfrom
feat/phase4-branch-summaries
May 21, 2026
Merged

feat(phase 4): branch summary preservation in /tree (pi-style metadata)#72
yogthos merged 1 commit into
mainfrom
feat/phase4-branch-summaries

Conversation

@yogthos

@yogthos yogthos commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Phase 4 of the 6-phase plan. Pruned sibling branches now get BranchSummary records (root id, parent id, count, preview) preserved on the session. /tree shows a 'Summarized branches' section after the linear tree so users can see what was discarded. Metadata-only MVP; richer LLM-summary variant is Phase 4b. 2 new tests, 646 pass.

Phase 4 of the 6-phase plan. Reference pattern: pi's
`packages/coding-agent/src/core/branch-summarization.ts` —
sibling branches dropped during compaction get preserved as
structured summary records so the user can see what was lost.
dirge implements the metadata-only variant (no LLM-generated
summary call yet) — the schema is forward-compatible if Phase 4b
adds LLM summaries later.

## Problem

Phase 2 added sibling-branch pruning with a chat notification:
"discarded N forked branches". Useful in the moment but the
information was lost forever. Users with branched sessions
couldn't see WHICH branches were dropped or any preview of what
was in them.

## Fix

New `BranchSummary` struct in `src/session/mod.rs`:

```rust
pub struct BranchSummary {
    pub root_id: CompactString,    // top of the pruned subtree
    pub parent_id: CompactString,  // still-present parent (may be dropped too)
    pub message_count: usize,
    pub preview: String,           // "[label] first 80 chars..."
    pub created_at: String,        // RFC3339
}
```

`Session::branch_summaries: Vec<BranchSummary>` with
`#[serde(default)]` for back-compat. Cleared by `reset_to_new`
(else fresh sessions would inherit phantom records).

### Capture during compress + rewind

Before pruning, walk the `to_prune` set and identify SUBTREE
ROOTS — nodes in `to_prune` whose direct parent was in
`dropped_set` (the closest dropped-active-path ancestor). For
each root, build a `BranchSummary`:

- `root_id` and `parent_id` for correlation
- Walk descendants to count the subtree size
- Preview = optional label (`[explore-alt]`) + first 80 chars of
  the root's message content

Identical algorithm in `Session::compress_reporting` and
`ui::mod.rs::rewind_session`.

### Surface in `/tree`

`render_tree` appends a "Summarized branches" section after the
linear tree render:

```
  * abc12345 user   "the active path…"
  abc12346 asst   "the next message…"

Summarized branches (1): pruned during compress/rewind
  └─ parent abc12345 · 2 msgs · [explore-alt] let me try a different…
```

Users browsing `/tree` after a compress can now see exactly
which branches were preserved and which were lost.

## Tests

2 new session tests, written failing first:

- `compress_records_branch_summary_for_pruned_siblings`:
  builds a session with u1 → [a1, u2, a2] linear + a sibling
  subtree (sib_alpha labeled "explore-alt" → sib_beta) rooted
  at u1, compresses past u1, asserts the resulting
  `branch_summaries` has 1 entry with the right parent_id (u1),
  message_count (2), and preview containing "explore-alt" or
  the alpha sibling's content.
- `reset_to_new_clears_branch_summaries`: `/clear`-equivalent
  wipes the summary list along with everything else.

Total: 646 pass (was 644), 0 fail, all build profiles clean.

## Not in Phase 4 (deferred to optional Phase 4b)

- LLM-generated summary content. Today the preview is the
  truncated content + label; richer one-sentence summaries
  would require routing the LLM client into `Session::compress`
  (currently sync) — bigger plumbing change. The
  `BranchSummary` schema can accommodate a future `summary:
  Option<String>` field without breaking back-compat.
- Restoration as agent context when the user navigates "back"
  to a summarized branch. Today summaries are display-only
  (`/tree`). Navigation-time injection would need a new tree
  op (e.g. `harness/peek-summary` or a system message
  injection) — out of scope for the metadata-only MVP.

## Test plan

- [x] `cargo test --features plugin` -> 646 pass.
- [x] `cargo build --all-features` -> compiles.
- [x] `cargo build --no-default-features` -> compiles.

## Up next: Phase 5 (per user request, skip Phase 6)

`/allow list | remove <idx> | add <tool> <pattern>` CRUD slash
command. User asked to skip Phase 6 (cost tracking).
@yogthos
yogthos merged commit 3ed4a1a into main May 21, 2026
1 check passed
@yogthos
yogthos deleted the feat/phase4-branch-summaries branch May 21, 2026 03:04
yogthos added a commit that referenced this pull request May 21, 2026
)

Phase 5 of the plan. No clean equivalent in opencode or pi — both
manage allowlist entries only via the interactive permission
prompt's "allow always" answer. dirge adds explicit CRUD so users
can inspect, manually add, drop one, or clear all entries without
editing the session JSON.

## Commands

- `/allow` or `/allow list` — show numbered entries
- `/allow add <tool> <pattern>` — add a (tool, pattern) entry,
  e.g. `/allow add bash 'cargo *'`
- `/allow remove <idx>` — drop one entry by 0-based index from
  the `list` output
- `/allow clear` — drop all entries

The `add` form preserves pattern strings containing spaces by
re-deriving the args from the raw `text` slice (rather than
relying on the SmallVec `parts[2]` which would get truncated at
the first whitespace). Same approach `/cd` uses for paths.

## State sync

Both surfaces stay aligned:
- `PermissionChecker::session_allowlist` (in-memory, used by
  `check` / `check_path`) — updated via the existing
  `add_session_allowlist` and the two new
  `remove_session_allowlist_at(idx)` + `clear_session_allowlist`.
- `Session::permission_allowlist` (persisted to JSON, restored
  on resume via `load_session_allowlist`) — mirrored
  manually in the slash handler so save/load round-trips show
  the user-edited list.

Without the session-side mirror, a `/allow add foo bar` would
stay in memory for this run but vanish on `-c` resume. Dedup at
the session level too so `save()` writes a clean list.

## Tests

3 new permission tests, written failing first:

- `remove_session_allowlist_at_returns_removed_entry`:
  remove(1) on [bash:cargo*, bash:git*, read:/tmp/*] yields
  Some(("bash","git *")) and the surviving entries shift down.
- `remove_session_allowlist_at_out_of_range_returns_none`:
  remove(99) returns None without panicking; existing entries
  intact.
- `clear_session_allowlist_empties_the_list`: clear() drops
  all entries.

The slash handler itself is not unit-tested (requires the full
UI loop fixtures); manual smoke test:

```
$ dirge --restrictive
> bash 'echo hi'
> (a) allow always
> /allow list      → [0] bash echo *
> /allow add read /tmp/*
> /allow list      → [0] bash echo *  [1] read /tmp/*
> /allow remove 0
> /allow list      → [0] read /tmp/*
> /allow clear
> /allow list      → empty
```

## Docs

`/help` text gets four new lines covering each subcommand.

## Test plan

- [x] `cargo test --features plugin` -> 649 pass (was 646).
- [x] `cargo build --all-features` -> compiles.
- [x] `cargo build --no-default-features` -> compiles.

## Plan status

| Phase | PR | Done |
|---|---|---|
| 1 | #69 | first-wins block + docs |
| 2 | #70 | sibling-branch pruning + notification |
| 3 | #71 | structured tool-call persistence |
| 4 | #72 | branch summary metadata in /tree |
| 5 | this | /allow CRUD |
| 6 | — | skipped per user request (cost tracking) |

5 of 6 phases complete; Phase 6 (cost tracking) skipped.

Co-authored-by: Yogthos <yogthos@gmail.com>
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…a) (dirge-code#72)

Phase 4 of the 6-phase plan. Reference pattern: pi's
`packages/coding-agent/src/core/branch-summarization.ts` —
sibling branches dropped during compaction get preserved as
structured summary records so the user can see what was lost.
dirge implements the metadata-only variant (no LLM-generated
summary call yet) — the schema is forward-compatible if Phase 4b
adds LLM summaries later.

## Problem

Phase 2 added sibling-branch pruning with a chat notification:
"discarded N forked branches". Useful in the moment but the
information was lost forever. Users with branched sessions
couldn't see WHICH branches were dropped or any preview of what
was in them.

## Fix

New `BranchSummary` struct in `src/session/mod.rs`:

```rust
pub struct BranchSummary {
    pub root_id: CompactString,    // top of the pruned subtree
    pub parent_id: CompactString,  // still-present parent (may be dropped too)
    pub message_count: usize,
    pub preview: String,           // "[label] first 80 chars..."
    pub created_at: String,        // RFC3339
}
```

`Session::branch_summaries: Vec<BranchSummary>` with
`#[serde(default)]` for back-compat. Cleared by `reset_to_new`
(else fresh sessions would inherit phantom records).

### Capture during compress + rewind

Before pruning, walk the `to_prune` set and identify SUBTREE
ROOTS — nodes in `to_prune` whose direct parent was in
`dropped_set` (the closest dropped-active-path ancestor). For
each root, build a `BranchSummary`:

- `root_id` and `parent_id` for correlation
- Walk descendants to count the subtree size
- Preview = optional label (`[explore-alt]`) + first 80 chars of
  the root's message content

Identical algorithm in `Session::compress_reporting` and
`ui::mod.rs::rewind_session`.

### Surface in `/tree`

`render_tree` appends a "Summarized branches" section after the
linear tree render:

```
  * abc12345 user   "the active path…"
  abc12346 asst   "the next message…"

Summarized branches (1): pruned during compress/rewind
  └─ parent abc12345 · 2 msgs · [explore-alt] let me try a different…
```

Users browsing `/tree` after a compress can now see exactly
which branches were preserved and which were lost.

## Tests

2 new session tests, written failing first:

- `compress_records_branch_summary_for_pruned_siblings`:
  builds a session with u1 → [a1, u2, a2] linear + a sibling
  subtree (sib_alpha labeled "explore-alt" → sib_beta) rooted
  at u1, compresses past u1, asserts the resulting
  `branch_summaries` has 1 entry with the right parent_id (u1),
  message_count (2), and preview containing "explore-alt" or
  the alpha sibling's content.
- `reset_to_new_clears_branch_summaries`: `/clear`-equivalent
  wipes the summary list along with everything else.

Total: 646 pass (was 644), 0 fail, all build profiles clean.

## Not in Phase 4 (deferred to optional Phase 4b)

- LLM-generated summary content. Today the preview is the
  truncated content + label; richer one-sentence summaries
  would require routing the LLM client into `Session::compress`
  (currently sync) — bigger plumbing change. The
  `BranchSummary` schema can accommodate a future `summary:
  Option<String>` field without breaking back-compat.
- Restoration as agent context when the user navigates "back"
  to a summarized branch. Today summaries are display-only
  (`/tree`). Navigation-time injection would need a new tree
  op (e.g. `harness/peek-summary` or a system message
  injection) — out of scope for the metadata-only MVP.

## Test plan

- [x] `cargo test --features plugin` -> 646 pass.
- [x] `cargo build --all-features` -> compiles.
- [x] `cargo build --no-default-features` -> compiles.

## Up next: Phase 5 (per user request, skip Phase 6)

`/allow list | remove <idx> | add <tool> <pattern>` CRUD slash
command. User asked to skip Phase 6 (cost tracking).

Co-authored-by: Yogthos <yogthos@gmail.com>
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…irge-code#73)

Phase 5 of the plan. No clean equivalent in opencode or pi — both
manage allowlist entries only via the interactive permission
prompt's "allow always" answer. dirge adds explicit CRUD so users
can inspect, manually add, drop one, or clear all entries without
editing the session JSON.

## Commands

- `/allow` or `/allow list` — show numbered entries
- `/allow add <tool> <pattern>` — add a (tool, pattern) entry,
  e.g. `/allow add bash 'cargo *'`
- `/allow remove <idx>` — drop one entry by 0-based index from
  the `list` output
- `/allow clear` — drop all entries

The `add` form preserves pattern strings containing spaces by
re-deriving the args from the raw `text` slice (rather than
relying on the SmallVec `parts[2]` which would get truncated at
the first whitespace). Same approach `/cd` uses for paths.

## State sync

Both surfaces stay aligned:
- `PermissionChecker::session_allowlist` (in-memory, used by
  `check` / `check_path`) — updated via the existing
  `add_session_allowlist` and the two new
  `remove_session_allowlist_at(idx)` + `clear_session_allowlist`.
- `Session::permission_allowlist` (persisted to JSON, restored
  on resume via `load_session_allowlist`) — mirrored
  manually in the slash handler so save/load round-trips show
  the user-edited list.

Without the session-side mirror, a `/allow add foo bar` would
stay in memory for this run but vanish on `-c` resume. Dedup at
the session level too so `save()` writes a clean list.

## Tests

3 new permission tests, written failing first:

- `remove_session_allowlist_at_returns_removed_entry`:
  remove(1) on [bash:cargo*, bash:git*, read:/tmp/*] yields
  Some(("bash","git *")) and the surviving entries shift down.
- `remove_session_allowlist_at_out_of_range_returns_none`:
  remove(99) returns None without panicking; existing entries
  intact.
- `clear_session_allowlist_empties_the_list`: clear() drops
  all entries.

The slash handler itself is not unit-tested (requires the full
UI loop fixtures); manual smoke test:

```
$ dirge --restrictive
> bash 'echo hi'
> (a) allow always
> /allow list      → [0] bash echo *
> /allow add read /tmp/*
> /allow list      → [0] bash echo *  [1] read /tmp/*
> /allow remove 0
> /allow list      → [0] read /tmp/*
> /allow clear
> /allow list      → empty
```

## Docs

`/help` text gets four new lines covering each subcommand.

## Test plan

- [x] `cargo test --features plugin` -> 649 pass (was 646).
- [x] `cargo build --all-features` -> compiles.
- [x] `cargo build --no-default-features` -> compiles.

## Plan status

| Phase | PR | Done |
|---|---|---|
| 1 | dirge-code#69 | first-wins block + docs |
| 2 | dirge-code#70 | sibling-branch pruning + notification |
| 3 | dirge-code#71 | structured tool-call persistence |
| 4 | dirge-code#72 | branch summary metadata in /tree |
| 5 | this | /allow CRUD |
| 6 | — | skipped per user request (cost tracking) |

5 of 6 phases complete; Phase 6 (cost tracking) skipped.

Co-authored-by: Yogthos <yogthos@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant