# FEATURE-BATCH-PLAN — v1.1.0 (JIT plan, 2026-07-07)

Operator handed a 7-item batch. Decisions locked (2026-07-07):
- **Item 3 (gateway threads):** new **Messages** tab, per-gateway (two-level: gateway
  list → that gateway's unified in/out message thread; row tap deep-links into the
  endpoint's digest at the message).
- **Release strategy:** bugs first, ONE branch (`feature-batch-v1.1.0`), ship together as
  **v1.1.0**. Order below.

Gate for every item: `app/gradlew test` (JVM suite) + `cargo test` (host + rust) +
`traceable-reqs check` all green, evidence tagged in the SAME commit.

Scanner note (traceable-reqs#9): the checker defers `.kt` — Kotlin evidence is tagged
in-tree but gates only at `doc`. Host-Rust evidence (items 6, and the item-3 host RPC)
gates at `doc+impl+unit(+int)` for real.

---

## 1. #2 — Digest view flicker + expanded-card collapse  (BUG, app)

**Root cause (confirmed by read):** `EndpointViewModel.publish()` rebuilds a fresh
`List<TimelineRow>` on every follow `Delta`/`Timeout` tick (idle liveness belt re-resyncs
each 15s). `EndpointScreen` renders `itemsIndexed(rows)` with **no stable key**, and
`DigestInputRow` holds `expanded` in `remember(row.text)` inside the disposable item
scope. Any list churn re-binds a shifted row → `remember` re-inits → expanded collapses;
the wholesale recompose is the visible "flicker."

**Fix:**
1. Stable per-row identity: add `TimelineRow.stableKey` (msg-id / seq / turn-ordinal+kind);
   render with `items(rows, key = { it.stableKey })`.
2. Hoist expansion state out of the item into `EndpointViewModel`
   (`expanded: StateFlow<Set<String>>` + `toggleExpanded(key)`); `DigestInputRow` reads/
   writes via callbacks. Survives every republish.
3. Idle-tick conflation: `publish()` no-ops when the interlaced list value-equals the
   current `_rows` (StateFlow already conflates equal values — make the output stable so an
   unchanged tick emits nothing). Auto-scroll keys on the last row's `stableKey`, not
   `rows.size`, and only when already tailing.

**Req:** new `REQ-HAZARD-DIGEST-CARD-COLLAPSE` (an idle digest refresh never collapses an
expanded card nor churns unchanged rows). doc gate (kt tests deferred). KNOWN-HAZARDS §3.2.

## 2. #6 — Chunked `<EVENT-PART>` messages not reassembled  (BUG, host Rust)

**Root cause (confirmed):** public messaging contract — *"on the listener stream an
oversized line splits into `<EVENT-PART seq="K/M" id="…">` chunks the receiver reassembles;
`api poll` always emits one whole envelope, never chunked."* The host reassembles nowhere.
`event::split_frames` splits on `</EVENT>`, so an `<EVENT-PART …>chunk</EVENT-PART>` frame
passes through un-split; `parse_event` then appends a bogus `</EVENT>` and emits a garbled
event whose body carries the leaked `</EVENT-PART>` tag (matches the 2026-07-07 screenshot:
`…foreground ser</EVENT-PART>`). Bug is exclusive to `listen_loop` (main.rs:282); the poll
drain (main.rs:213) is guaranteed whole envelopes. Triage = **bucket 1 (documented,
unimplemented)** — no doyle ask.

**Fix (rust/link-proto/src/event.rs + host/src/main.rs):**
- Parse `<EVENT-PART seq="K/M" id="…">chunkbody</EVENT-PART>` frames (top-level tokenizer
  that recognizes BOTH `</EVENT>` and `</EVENT-PART>` terminators, so a chunk body that
  itself contains `<EVENT`/`</EVENT>` fragments is never mis-split).
- `Reassembler` keyed by `id`: buffer chunk bodies by `K`; when `count == M`, concat in `K`
  order → the whole composed `<EVENT…>…</EVENT>` → `parse_event` → `route_inbound`.
  Idempotent on re-delivered `K`; whole `<EVENT>` frames pass straight through.
- `listen_loop` holds one `Reassembler` across its lines; drop the id buffer on child exit.

**Req:** new `REQ-EVENT-PART-REASSEMBLY` — doc+impl+unit+int (host loopback int: feed a
chunked stream, assert exactly one routed event with the joined body and no leaked tag).

## 3. #7 — Stale-handle hardening  (app Kotlin)

Generalize the v1.0.6 voice-only stale-handle redial: ANY RPC failure on a `Connected`
handle marks it stale → supervisor redials that host, and the UI stops showing "link: up"
(new "link: stale" state) until the redial reconnects. Design finalized against
`LinkSupervisor`/`HostLinkState`/`LinkNative` when reached.

**Req:** new `REQ-HAZARD-STALE-LINK-STALL` (a dead-but-cached Connected handle never
strands an RPC path or shows a false "up"). doc gate (kt deferred). KNOWN-HAZARDS §4.2.
Retag the voice redial tests onto it.

---

## 4. #5 — Arbitrary text selection in the digest view  (app)

Wrap the digest timeline content in `SelectionContainer` so message/agent/digest text is
selectable. Verify selection doesn't eat the `OutlinedCard` expand tap (SelectionContainer
+ clickable coexist; test on-device). Tag `REQ-ENDPOINT-VIEW-INTERLACE`.

## 5. #1 — Markdown formatting + XML syntax highlighting  (app)

New `app/…/render/` package, clean-room:
- `Markdown.kt`: markdown → `AnnotatedString` (bold `**`, italic `*`/`_`, inline `` `code` ``,
  fenced ``` blocks, headings `#`, `-`/`*` lists, `>` quote, `[t](u)` links). Pure, unit-testable.
- `CodeHighlight.kt`: XML syntax highlighting → span-colored `AnnotatedString` (tags, attr
  names, attr values, comments, text). Applied to fenced `xml`/`html` blocks and detected
  `<EVENT>`-style bodies.
Apply in `AgentBubble`, `TimelineRow.Message` body, `DigestInput` expanded body. Theme-aware
colors. **Req:** new `REQ-RICH-RENDER` — doc gate (kt render tests tagged, deferred).

## 6. #4 — Browse: group by node, not subnet  (app + model)

Regroup the browse view: **by node (machine)** → sub-grouped by `<project>/` → a single-line
"shared subnets: A, B" under each machine. Mirrors the `spt endpoint list` CLI layout.
- Add pure `EndpointDirectory.groupByNode(subnets): List<BrowsedNode>` projecting the
  existing merged+overlaid model (keep `merge` + `DigestRouter` inputs intact — regroup is a
  presentation layer over them). `BrowsedNode{node,label,sharedSubnets,projects:[BrowsedProject
  {project,endpoints}]}`.
- Rework `BrowseScreen` rendering to node→project→endpoint.
**Req:** widen `REQ-BROWSER-LIVE` (title: "grouped by node/project, shared-subnets line") +
DESIGN/CONTEXT note that node grouping supersedes subnet grouping for presentation. Host impl
half already gated; kt regroup tagged, deferred.

## 7. #3 — Messages tab: per-gateway in/out threads  (app + host RPC)

New **Messages** top-level tab.
- **Host RPC:** add `Request::HistoryFetchAll { limit }` → all history rows across every
  endpoint for this host, each carrying its `endpoint` id (HistoryStore already per-endpoint;
  enumerate + union). Real gated Rust evidence.
- **Phone:** `MessagesViewModel` lists paired gateways; opening one shows its unified in/out
  message thread (rows = `HistoryRow`, NO digest content), each labeled by endpoint. Row tap
  deep-links to `EndpointScreen(endpointId, scrollToMsgId)` — add an optional anchor param that
  scrolls the digest timeline to that msg-id.
**Req:** new `REQ-GATEWAY-THREAD` — doc+impl+unit (host RPC) + kt UI (deferred).

---

## Close-out
- Bump app/host `versionName` + `version` + `manifest.toml` to **1.1.0** (release-tag=host-bump
  invariant). Update README/CHANGELOG. `traceable-reqs check` green. Then `gh release create
  v1.1.0` with apk + adapter.spt. Do NOT `spt adapter update` without operator go (restarts the
  live gateway).
- On-device verify each bug fix over adb (enlyzeam / ZY22K2S8V5) before release.
- Checkpoint cadence: commune + self-clear between major items per memory.
