Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/2026-08-12-agent-status-open-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Open tasks — viewport-relative cursor + server-side agent sub-status

Register for the work started 2026-08-12. Plans: [`plans/2026-08-12-viewport-relative-cursor-positioning.md`](./plans/2026-08-12-viewport-relative-cursor-positioning.md) and [`plans/2026-08-12-agent-sub-status-server-side.md`](./plans/2026-08-12-agent-sub-status-server-side.md), both in [#537](https://github.com/RonenMars/threadbase-streamer/pull/537).
Evidence: [`2026-08-12-claude-pty-capture.md`](./2026-08-12-claude-pty-capture.md).

## Done

| What | Where |
|---|---|
| Both plans, five and three review rounds | [streamer#537](https://github.com/RonenMars/threadbase-streamer/pull/537) |
| PlanCup implemented — viewport-relative `H`/`f`/`A`/`B` with clamps | [mobile#654](https://github.com/RonenMars/threadbase-mobile/pull/654) — green, mergeable |
| PTY capture (output-heavy turn, no tool use) | `~/.threadbase/captures/`, README committed |
| PlanIndicator Part A (streamer) | branch `feat/agent-sub-status`, uncommitted |

## Blocked / awaiting a decision

**Merge [mobile#654](https://github.com/RonenMars/threadbase-mobile/pull/654).** 10/10 CI green, `MERGEABLE`, body records verification item 4 as satisfied. Nothing outstanding.

**Second PTY capture — tool-heavy turn.** In progress with `consultant-for-streamer`. The only artifact that can still change anything. Settles, in priority order:
1. `↑` vs `↓` — whether the arrow tracks direction-of-dominance. Decides whether the struck "different sub-field" argument is permanently dead.
2. `hooks…` — needs a turn that actually triggers a hook (a Stop hook is cheapest).
3. The `thinking` state — `WORKING` matching while `TOKENS` does not; sample early in a turn.
4. Whether any deferred #652 sequence (`S`/`T`/`L`/`M`/`A`/`D`/`f`) fires mid-turn.

Until it lands, **both plans rest on one output-dominated turn with no tool use**, and that qualifier belongs on every citation.

## PlanIndicator — remaining work

### Part A (streamer) — branch `feat/agent-sub-status`, not yet a PR

Complete and verified: `AgentPhase` union, `subStatus` on both types, unconditional-key serialisation, `onPhaseChange` callback, clear at `markReady`, deduping `setPhase`, derive wired into the existing scrape pass, scoped `session_phase` frame, pty-host event + protocol v3, compatibility doc.
`tsc` clean, 21 tests, contract tests verified against deliberately-broken variants.

Committed as `0ea09bc`, pushed, **deliberately not a PR** — four verification gaps remain, tracked in [streamer#541](https://github.com/RonenMars/threadbase-streamer/issues/541):

- [ ] Integration test through a fake PTY, mirroring `__tests__/session-status-line.test.ts:54`.
- [ ] Assert the scrape pass stays non-fatal — a throw inside `detectLivePrompts` is swallowed into a `warn` at both call sites, so a regression there is silent.
- [ ] Direct `setPhase` / `markReady` unit tests — the change-guard and the turn-end clear, currently covered only indirectly.
- [ ] Full-suite run.

### Part A2 (streamer) — the Claude derive. **Blocked on capture 2.**

`parseAgentPhase`'s Claude branch deliberately returns `null`. Reporting no phase is the correct pre-feature behaviour; a guessed phase is worse than none.
Do not write it until the marker grammar is re-verified — and reuse `terminalChrome`'s grammar rather than re-deriving glyph classes, which is how #647 drifted.

### Part B (mobile) — [mobile#653](https://github.com/RonenMars/threadbase-mobile/issues/653). **Blocked on Part A shipping.**

Not "add a field and render it". Ordered by risk:

1. ~~Clearing contract~~ — resolved in Part A (always-emit, explicit `null`).
2. Cache-handler reconciliation — [mobile#655](https://github.com/RonenMars/threadbase-mobile/issues/655), independent, see below.
3. Add `subStatus` to `types/api.ts` **and** to `SessionPresentationInput` (`lib/sessionPresentation.ts:58-72`) — that type is standalone, not derived from `Session`, so adding it in one place leaves the derive blind.
4. Gate on `presentation.live`, **not** raw `status` — `deriveSessionPresentation` has branches that never consult `status`, so a raw gate can render a phase beside a badge reading "Idle" or "External".
5. Render via the existing colour token if possible; a phase-specific colour is ~20 files across 17 theme objects. Never a hardcoded hex.
6. Question-card suppression — copy `ThinkingBubble.tsx:107-134`, which covers both the structured and PTY-scraped gates.
7. No client-side time decay. The app's idiom is `processLiveness === 'gone'`: the server decides liveness and says so.

Tree mode (`TreeRow.tsx:63`) is deliberately out of scope.

## Filed, unstarted

| Issue | Summary |
|---|---|
| [mobile#655](https://github.com/RonenMars/threadbase-mobile/issues/655) | `LiveConversationView.tsx:143` replaces the whole session object where two other writers merge, wiping REST-only fields while mounted. Pre-existing; P2 (badge cannot be mislabelled — `lifecycle`/`ownership` are always on the frame). Worth landing before Part B. |
| [mobile#656](https://github.com/RonenMars/threadbase-mobile/issues/656) | `processLiveness: 'gone'` is never emitted by the streamer, so three mobile production paths are unreachable — including the whole `stale` branch. Open question: whether a vanished external session lingers in the eager cache. Turns on whether React Query's focus manager is wired to `AppState`. |
| [streamer#539](https://github.com/RonenMars/threadbase-streamer/issues/539) | `CODEX_BUSY_STATUS_RE`'s `\b` does not stop a path from false-hitting; a Codex session under a dir named `Working`/`Starting` never reads as ready. |
| [streamer#541](https://github.com/RonenMars/threadbase-streamer/issues/541) | The four verification gaps in Part A, above. Blocks its PR. |
| [mobile#668](https://github.com/RonenMars/threadbase-mobile/issues/668) | The deferred `VirtualTerminal` CSI family, filed as one issue because the capture shows none of it is reachable mid-turn. P3, unblocked only by a tool-heavy capture. |
| [mobile#669](https://github.com/RonenMars/threadbase-mobile/issues/669) | `useTerminalStream`'s WS effect omits `provider` from its deps, so live frames are filtered by a stale provider's chrome rules. Independent of #647, which is why it survived that PR's abandonment. |

**The #652 deferred family is uniformly downgraded, not reordered** — [mobile#668](https://github.com/RonenMars/threadbase-mobile/issues/668). The capture shows `S`/`T`/`L`/`M`/`A`/`D`/`f` never emitted and `r`/`J`/`c`/`ESC7`/`ESC8` firing once at startup. Nothing in it is reachable mid-turn. Only capture 2 can change that.

## Deliberately not filed

- **Putting PTY geometry on the wire.** The follow-up is *conditional*, not scheduled: whoever adds resize adds the wire field in the same change, which is the policy `src/pty-host/protocol.ts:38-41` already states. Filing it would create a TODO nobody actions for a value that cannot currently vary.
- **Housekeeping from the capture session** — `~/dev/pty-capture-tmp/.remember/` and `~/.claude/projects/-Users-ronenmars-dev-pty-capture-tmp/`. Conversation history, safe to delete, not repo work.
- **The stale `tb-mobile-worktrees/feat-agent-sub-status` worktree** at the closed #647 branch (`0a58200c`). Do not reuse it; it carries the client-side implementation this work replaces.

## Method notes earned here

Four defects were caught *after* sign-off. All four came from measurement, not from reading. Both rules below are in the plans and were each violated by the person who had just written them down.

**Run the test that killed the thing you are replacing, before you propose the replacement.**
Stated as a construction practice, not a review one — the failure happens at proposal time. Three successive attempts to promote an item out of the deferred queue (`2J`, `CSI r`, `ESC7`/`ESC8`) were the same artifact relabelled, each proposed by someone who had written the disqualifying caveat into the same message. *Stating a caveat is not applying it.*

**Ask per statistic whether it is sensitive to how the file was assembled.**
The answer differs between statistics on one line of evidence: frequencies were inflated by snapshot duplication, while the maximum and the distinct set were exact. Over-correcting into a blanket "magnitude is unreliable" nearly discarded `max = 40`, the one figure the constant rests on. **Under-trusting is the quieter failure** — it never yields a wrong answer, it just discards a right one and nobody goes back to check.

**Hand disagreements over as evidence, not conclusions.** Offsets and per-snapshot counts can be re-run by the recipient; an unfalsifiable claim can only be accepted or rejected. That is also what makes a claim cheap to test against yourself.

**A zero is evidence of absence only if the same command returns non-zero on a file you know contains a match.** Five distinct ways to produce a clean-looking zero were hit for real here; they are catalogued in the capture README.
134 changes: 134 additions & 0 deletions docs/2026-08-12-claude-pty-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<!-- Copied from ~/.threadbase/captures/README.md so the evidence survives the artifact.
The raw bytes (36 snapshots + a 1.1 MB concatenation) are NOT version-controlled and
live outside any repo, in a tree the threadbase installer owns. Two PRs cite the
figures below — threadbase-mobile#654 and this repo's #537 — so the numbers are
quoted here and inline in those PRs rather than only pointed at. -->

# Claude Code PTY capture — 2026-08-12

Raw PTY bytes from a live Claude Code session, escapes intact. Produced to settle open
questions in PlanCup (tb-mobile #652) and PlanIndicator (tb-streamer #538).

## Files

- `snap-NNN.raw` — 36 distinct snapshots of `session.outputBuffer`, polled ~0.7s apart
through one full turn via `GET /api/sessions/:id/output`.
- `claude-v2.1.228-turn.raw` — all snapshots concatenated (1,123,773 bytes). **Snapshots
overlap heavily; see "Counting" before trusting any number taken from this file.**

## Provenance

- Claude Code **v2.1.228**, spawned by tb-streamer at its fixed **120x40** geometry.
- Server flags in effect: `--model claude-opus-4-8 --effort high`.
- Prompt: "Write out the numbers 1 through 60, each on its own line, with a one-sentence
interesting fact about that number." Pure text generation — **no tool use**.
- Throwaway project dir `~/dev/pty-capture-tmp`; session stopped after capture.

## Findings

Real footer status lines recovered (escapes stripped):

```
·Marinating… (25s · ↓ 1.1k tokens) mid-turn
✻Baked for 26s end-of-turn
```

Markers: `↓` 31, `↑` **0**, `tokens` 31, `hooks…` **0**, `esc to interrupt` **0**.
The absent `↑` matters — `parseStatusLine`'s fixture uses `↑ 3.4k tokens`, so the
"input vs output arrow" distinction is **contradicted by this capture**. Likely the arrow
tracks whichever direction dominates the turn (this one was output-heavy), but that is
untested. A tool-heavy turn should settle it.

**Absolute cursor rows addressed: only 1, 37 and 40. Maximum exactly 40** — the 120x40
geometry is measured here, not assumed.

### CSI final bytes — corrected reading

```
during the turn: G 51327 B 10075 C 8222 m 8213 H 4395 K 3805
startup only: r 1 J 1 c 1 ESC7 1 ESC8 1
never emitted: S T L M A D f
```

The startup row deserves care. A naive count over the concatenated file reports **30** for
each of `r`, `J`, `c`, `ESC7`, `ESC8`. That is an artifact: each snapshot is a *full ring
buffer dump*, so every snapshot that has not yet wrapped replays the session's startup
bytes. Verified per-snapshot — `CSI r` sits at offset 2 in exactly 30 of 36 snapshots and
is absent from the six 65536-byte wrapped ones; `ESC7`/`ESC8` sit at offsets 0 and 5 in the
same 30. **True count for each is 1, at startup.**

The whole startup sequence is these 8 bytes, once:

```
ESC 7 ESC [ r ESC 8 (save cursor, reset margins to full screen, restore cursor)
```

Every `CSI r` in the capture is **argument-less** (verified: the only parameter string
present is empty). Bare `ESC[r` *resets* margins to the full screen — it does not set a
scroll region. So an emulator that models only the full screen and ignores `r` reaches the
correct end state; `case 'r': break` is right, not a defect. Likewise a startup `2J`
annihilates an empty grid, which is harmless.

**Consequence for the tb-mobile #652 follow-ups: nothing in the deferred family is
reachable mid-turn in this capture.** `S`/`T`/`L`/`M`/`A`/`D`/`f` never appear at all, and
`r`/`J`/DECSC/DECRC appear exactly once each during terminal init. The queue should be
uniformly downgraded pending a capture that shows any of them firing mid-session — there is
no item here that deserves promoting above the others.

## Caveats — read before citing

1. **One turn, one version, no tool use.** A tool-heavy turn may emit sequences this one
does not. Absence here is not absence in general. `hooks…` and the tokens-absent
"thinking" state were never observed for exactly this reason.
2. **The ring buffer is a 64KB byte-level tail cut** (`pty-manager.ts:820-825`), so a
snapshot can begin mid-escape and mid-UTF-8. At most one malformed sequence per
snapshot head.
3. **Snapshots overlap.** Presence/absence is reliable; magnitude is not. Derive
per-snapshot before quoting any count — see the startup artifact above.

## Counting

Two independent ways to get a wrong number out of this file:

- **`grep -c` counts matching LINES, not occurrences**, and this capture contains **zero
newlines** — it is one unterminated line. `grep -acoE $'\033\\[[0-9;]*r'` returns `1`
where the true occurrence count is `30`. `-c` silently overrides `-o`. Use
`grep -aoE … | wc -l`. A zero is unaffected (zero lines containing ⇒ zero occurrences),
but every non-zero count taken with `-c` understates.
- **Snapshot overlap inflates**, as above. The two errors push in opposite directions and
can cancel, which is worse than either alone.

## Five ways this capture yields a clean-looking zero

Every one of these was hit for real while analysing it or the code around it:

1. **Binary-file silence.** `file(1)` calls a raw dump `data`; grep then skips it and
prints nothing. `tb-streamer/src/pty-manager.ts` trips this too — it contains a raw
byte, so plain `grep` returns empty for patterns that are certainly present. Use `-a`.
2. **Malformed pattern that errors like a miss.** `grep` here resolves to **ugrep**, and
neither it nor `/usr/bin/grep` accepts `$'\x1b\['` — that is ESC plus a *literal* `[`,
read as an unterminated bracket expression. It errors; add `2>/dev/null` and it becomes
an innocent-looking zero. Use `$'\033\\['` (double backslash) or `-F`.
3. **Text that is never contiguous in the stream you search.** Regexing the raw bytes for
`↓ N tokens` returns zero while 31 `↓` characters are present — the footer is assembled
by absolute cursor moves and exists only after rendering. This is the documented reason
`parseStatusLine` reads rendered lines rather than raw bytes. Strip CSI first.
4. **`-c` on a file with no newlines**, as above.
5. **Searching a field name instead of its values.** `activity` looked unpopulated in
tb-streamer because its only assignment builds the value in a different file from the
rest of the response shape; grepping the enum member (`active_writing`) found it. Grep
the values a field can take, not just its name.

**The rule that covers all five: a zero is evidence of absence only if the same command
returns non-zero on a file you know contains a match.** Always run the positive control.

```sh
printf '\033[40;1Htest\033[5S\n' > /tmp/poscontrol.raw
/usr/bin/grep -aoE $'\033\\[[0-9;]*S' /tmp/poscontrol.raw | wc -l # must print 1
/usr/bin/grep -aoE $'\033\\[[0-9;]*S' claude-v2.1.228-turn.raw | wc -l

/usr/bin/grep -aoE $'\033\\[[0-9;]*[A-Za-z]' claude-v2.1.228-turn.raw \
| sed 's/.*\[//; s/[0-9;]*//' | sort | uniq -c | sort -rn
```

`CSI S` = **0** against that control = 1. That is the citation for #652's deferral.
Loading
Loading