Skip to content

feat(tui): zypi export reads the session through the agent (plan decision 6) - #18

Merged
zy1233 merged 2 commits into
mainfrom
feat/export-via-acp-replay
Oct 13, 2026
Merged

zy1233 merged 2 commits into
mainfrom
feat/export-via-acp-replay

Conversation

@zy1233

@zy1233 zy1233 commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

zypi export <id> read the session files of the old Rust agent, so it failed for every session that the Python agent stored. It now asks the agent, as a resume does. This is plan decision 6, option (a).

zypi export <id>
  connect()                              # starts the Python agent
  session/load(id, cwd)
    collect session/update (isReplay)
    done when: the answer came AND no replayed update came for 500 ms
    before the answer: 60 s of silence is an error
  AcpUpdateTracker -> ScrollbackState -> render_blocks_to_markdown   # same path as /export
  AgentProcessGuard drop: cancel + join    # also when the command fails
 pi-pager/src/
-├── export_cmd.rs        # read the old Rust session store
+└── export_cmd/
+    ├── mod.rs           # entry and rendering
+    ├── replay.rs        # agent and collection of the replayed updates
+    └── tests.rs         # 18 tests, paused tokio clock

The answer to session/load can come before the last replayed messages. So the answer alone does not end the collection: only the quiet period does, and only a replayed update restarts it. Permission requests get Cancelled, other requests from the agent get method-not-found. isReplay is read from the notification _meta and from the update _meta (the Python agent sets it on the update).

Evidence

  • Before: resume_matrix.py on main (plan section 10.12): 10 PASS / 3 GAP. Case 12, zypi export <id>, was GAP: Session '<id>' not found., exit 1.
    After: 11 PASS / 2 GAP / 0 FAIL on WSL2. Case 12 passes. Case 13 (unknown id) passes as before. The two GAP that remain are --resume <title> and --session-id <id in use>; this PR does not touch them. Cases 12 and 13 now also fail when a process is left behind.

  • Time (debug build, WSL2; three runs each, the session file is not changed):

    Entries File zypi export Agent alone (session/load)
    2,000 1.4 MB 2.5 s 0.24 s
    5,000 3.6 MB 5.6-5.7 s 0.53 s
    20,000 14.2 MB 62.7-64.1 s 2.5 s

    The agent is fast. The rest is zypi turning the messages into text, and that grows faster than the size. A release build was not measured.

  • Tests: 18 unit tests in export_cmd::tests. 15 run on a paused tokio clock: they cover the order of the updates, the quiet period, the silence limit, requests from the agent, refusals, and an agent that exits or says nothing. 3 cover the rendering. 1 end-to-end test, export_replays_a_stored_session_through_an_agent_of_its_own, against the real Python agent; python_agent_e2e passes 11 of 11. 1 new test in scripts/tests/test_tui_pty.py.

  • Checks that ran locally (on the code of the final commit; the last edits were documentation only): tui_baseline.py check exit 0 (warnings 0, residue words unchanged, dependency graph 980 = ceiling, cargo check --workspace --tests 0 errors). ruff check and ruff format --check pass. pytest: Windows 2,517 passed, 39 skipped, 31 deselected; WSL 2,540 passed, 16 skipped, 31 deselected.

  • CI (on e3f3458: TUI CI run, CI run): every job passes. Ruff. pytest 3.11, 3.12 and 3.13. Gates + cargo check: warnings 0, leader 245 of 245, x.ai 304 of 304, graph 980 of 980. cargo test: 8,684 passed, 0 failed, 22 ignored (pi-pager: 5,627 passed, 13 ignored). Release build baseline is manual and was skipped.

  • Not verified: Windows and macOS (only WSL2 ran zypi export). Release-build time. A real model (the agent is a mock; export does not call the model).

  • Found, not fixed (details in plan section 10.15, "continuation 6"): the Python agent loops forever on a session file with repeated entry ids; scripts/session_list_bench.py writes such a file above 4,096 entries; the pager reads isReplay only from the notification _meta, the agent sends it on the update (the resume path was not checked).

Merge Danger

Door: two-way

The change reads session files and writes none. A revert restores the old behaviour, which fails for every session of the Python agent.

Blast Radius: export

zypi export now needs a working agent command, as a resume does; it did not before. A very large session takes about a minute on a debug build. Nothing else in the pager changes. The new test-util feature of tokio is a dev-dependency only.

Merged on the owner's order, into V0.5 (v0.5.0). The CHANGELOG entry moves from [Unreleased] to [0.5.0], and the zypi export line leaves the known issues, in the commit that follows (it is part of #19, which merges after this PR). tui/ changed, so the release size is measured again on the final tree (release_baseline, condition 2) and CI must pass on the tag commit (condition 1).

`zypi export <id>` read the session files of the old Rust agent. For
every session that the Python agent stored, it answered "Session '<id>'
not found." (plan decision 6, option (a)). It now asks the agent, as a
resume does: it starts the agent, sends `session/load`, collects the
`session/update` messages that the agent replays, and renders them with
the same code as `/export`. Output to stdout, to a file and to the
clipboard is as before.

- `export_cmd` becomes a directory: `mod.rs` (entry and rendering),
  `replay.rs` (agent and collection), `tests.rs` (18 tests).
- The answer to `session/load` can arrive before the last replayed
  messages. The command waits until no replayed message has come for
  500 ms. An agent that says nothing for 60 s is an error. A permission
  request gets `Cancelled`, any other request gets method-not-found.
  The agent is cancelled and joined when the command ends, also when it
  fails.
- The `isReplay` marker is read from the notification `_meta` and from
  the update `_meta` (the Python agent sets it on the update).
- `resume_matrix.py` cases 12 and 13 now require a success or a clear
  refusal, and no process left behind. The user guide, the README of
  `scripts/tui_pty`, `tui-support.md` and the CHANGELOG are updated.

Measured on a debug build in WSL2: 2,000 entries 2.5 s, 5,000 entries
5.7 s, 20,000 entries 63 s. The agent alone needs 0.24 s, 0.53 s and
2.5 s. Tests: 18 unit tests (paused tokio time), 1 end-to-end test with
the real Python agent, matrix 11 PASS / 2 GAP / 0 FAIL.

The plan (section 10.15, continuation 6) has the details and three
findings that this change does not fix.
PR 18 is open and CI passes on e3f3458: Ruff, pytest 3.11 to 3.13,
Gates + cargo check (warnings 0, graph 980 of 980), cargo test
(8684 passed, 0 failed, 22 ignored). Replace "this PR" with the link.
@zy1233
zy1233 merged commit ec7d455 into main Oct 13, 2026
7 checks passed
@zy1233
zy1233 deleted the feat/export-via-acp-replay branch October 13, 2026 05:41
zy1233 added a commit that referenced this pull request Oct 13, 2026
main now has #18 (zypi export, ec7d455). The CHANGELOG entry, the revision line r12
of the plan and the revision line and sections of the execution log were changed by
both PRs at the same place. The text of #18 (step 6) comes first, then the text of
this PR (step 7). The code did not conflict.
zy1233 added a commit that referenced this pull request Oct 13, 2026
## Summary

`[ui.status_line]` drew nothing with the Python agent. The row waited for a `session_status` message that only the old Rust agent sent. Now `zypi` builds the row from what it already knows. There is no change on the wire and no change in Python. This is plan decision 15, option (b). Option (a), a new message from the Python agent, was chosen first and did not pass the review below.

```text
shell_status_context()
  start from the status the agent sent              # it wins; empty if none
  fill the empty fields from ClientOwnedFields:
      cwd, session id, model id and name, version
      session name: always the client's, because a rename happens here
  fill_from_clocks:
      cost.total_duration_ms = time since the session was attached
      turn.started_at_ms     = start of the running turn
  left empty: context, the rest of cost, branch, repo   # nobody on this side can know them
```

```diff
 the row is built again when
   the source agent changes
+  cwd, session id, session name, model, or "a turn runs" changes   # ClientOwnedFields
   a forced or due refresh arrives
```

The flag "a turn runs" is the reason for the last line. A command row runs on every tick of a turn and on no tick after it. Without the flag, the last run would keep the turn in its payload, and a timer would stay on a built-in row after the turn.

**Why not option (a).** DESIGN section 8.5 lists five points for a `pi/` extension; one failed point is enough to refuse it. Two failed: (1) what the Python agent could truthfully send is known to `zypi` already, or has a standard place in ACP (`agentInfo.version`, `usage_update`); the agent itself knows neither the context window nor a price. (4) The message type would live in `pi-shell`, an old runtime crate. The user then chose (b).

## Evidence

- **Before:** `status_line_probe.py` (r11, plan section 10.14): 4 runs, the row was not drawn with the Python agent and a command script did not run.
  **After:** 4/4 runs pass on a real debug `zypi` (WSL2, 23 s). Without a status from the agent: the row is on the screen; a script runs and gets session id, cwd, model, version, `workspace.current_dir`; `context_window` is an empty object and `cost` has only `total_duration_ms` (no `used_percentage`, `context_tokens`, `context_window_size`, `total_cost_usd`). With a status from the agent (the stand-in `status_proxy_agent.py`): the row shows what the agent sent.
- **Tests:** 16 new unit tests: 9 in `status_line_tests.rs` (what `zypi` fills in, what an agent status keeps, the two clocks) and 7 in `dispatch/tests/status_line.rs` (when the row is built again, what a script is given). 2 tests rewritten (they said the row waits for the agent). 1 adjusted: `only_a_row_that_can_change_mid_turn_holds_the_loop_awake_for_one`. The start of a turn now makes the row stale once, on purpose; the old test looked for ticks right after the turn start and failed. It now lets that one rebuild happen first. A built-in row still asks for no tick; a command row still asks for one.
- **Checks that ran locally** (WSL2, debug build; on the code of the final commit, the last edits are documentation only): the `pi-pager` suite of the manifest (`--lib`, `settings_e2e`, `grok_home_paths`, `selection_model_public_api`, `python_agent_e2e`, with the manifest environment): 5,624 passed, 0 failed, 13 ignored (the reference is 5,608; the 16 new tests are the difference). `tui_baseline.py check` exit 0: warnings 0, residue words unchanged, graph 980 = ceiling, `cargo check --workspace --tests` 0 errors. `ruff check` and `ruff format --check` pass. `pytest`: Windows 2,519 passed, 39 skipped, 31 deselected; WSL 2,542 passed, 16 skipped, 31 deselected.
- **A failure that this change did not cause:** my first run of the suite had 1 failure, `pin_ambiguous_title_errors_before_sandbox`. `main` and #18 fail the same way on my machine when the whole `--lib` suite runs, and the test passes alone. The cause is a leftover directory in my real `~/.pi-python/sessions`, from an earlier run that did not finish. The test uses fixed session ids. In a whole-suite run, another test pins `grok_home()` to the real home first, so the test writes there. An id that exists in two directories cannot be found by title (`duplicate_legacy_id_is_not_title_addressable` tests this), so the call returns `Ok` where the test wants an error. With an empty `HOME` the whole `--lib` suite passes (5,340 passed, 0 failed); with only `sessions/` copied in, it fails; with only `config.toml` and `trusted_folders.toml` copied in, it passes. After I removed the directory, the suite passed (above). CI runners are clean, so CI does not see it. I did not change the fixture.
- **CI** (on `bfd5425`: [TUI CI run](https://github.com/zy1233/pi-python/actions/runs/38348219064), [CI run](https://github.com/zy1233/pi-python/actions/runs/38348219013)): every job passes. `Ruff`. `pytest` 3.11, 3.12 and 3.13. `Gates + cargo check`: warnings 0, `leader` 245 of 245, `x.ai` 304 of 304, graph 980 of 980. `cargo test`: 8,681 passed, 0 failed, 22 ignored (`pi-pager`: 5,624 passed, 13 ignored, the same as my rerun). `Release build baseline` is manual and was skipped.
- **CI after merging `main`** (on `1fa18cf`, where #18 is merged into this branch; [TUI CI run 38353039701](https://github.com/zy1233/pi-python/actions/runs/38353039701), [CI run 38353039688](https://github.com/zy1233/pi-python/actions/runs/38353039688)): every job passes. `Gates + cargo check`: warnings 0, `leader` 245 of 245, `x.ai` 304 of 304, graph 980 of 980. `cargo test`: 8,700 passed, 0 failed, 22 ignored (`pi-pager`: 5,643 passed, 13 ignored). That is the base of 8,665 plus the 19 tests of #18 and the 16 tests of this PR.
- **Not verified:** Windows and macOS (the probe runs on Linux only). A real model (the agent is a mock; the data in the row does not depend on the model). A real agent that sends `session_status` (only the stand-in does).

## Merge Danger

**Door:** two-way

No data format, no wire message and no Python code changes. A revert brings back the old state: no row with the Python agent.

**Blast Radius:** status row

Only a user who sets `[ui.status_line]` sees a difference. The row now appears where it did not. `context` and `cost` items stay empty with the Python agent; the guide page says so. The row is built once more at the start of a turn and once more at its end: one more build for a built-in row, one more run of the script for a command row. The agent status still wins when an agent sends one.

Merged on the owner's order, after #18, into V0.5 (`v0.5.0`). #18 and this PR changed the same documentation lines (`CHANGELOG.md`, the plan, the execution log), so the branch has one merge of `main` (`1fa18cf`); the code did not conflict. A documentation PR that follows moves both CHANGELOG entries from `[Unreleased]` to `[0.5.0]` and narrows the `[ui.status_line]` known issue (the `context` and `cost` items stay empty). `tui/` changed, so the release build and the binary size are checked again on this tree (release dry run [run 38353038777](https://github.com/zy1233/pi-python/actions/runs/38353038777), `release_baseline` [run 38353042677](https://github.com/zy1233/pi-python/actions/runs/38353042677); both started on `1fa18cf`).
zy1233 added a commit that referenced this pull request Oct 13, 2026
… r12, part 8) (#20)

The owner asked to merge #18 and #19 and to put both into V0.5. Both are on
main now (ec7d455 and 9058ab3). This commit moves their two CHANGELOG
entries from [Unreleased] to [0.5.0], because the release page shows only
the section of the tag's version.

- CHANGELOG.md: the Fixed entry (zypi export) and the Changed entry (status
  row) move into [0.5.0]. The date is the release day (2026-10-13). The known
  issue about zypi export goes. The known issue about [ui.status_line] now
  says what is left: no context or cost items. Release notes preview with the
  awk program of release.yml: 49,429 B, five groups.
- Plan: the status of exit conditions 1 to 3 on the final tui/ tree (CI and
  TUI CI green on 1fa18cf and on main, zypi 420,667,200 B, release dry run
  green), the new status of decisions 6 and 15, and the known issues.
- Execution log: part 8.
- docs/baselines/tui.md: the new release size and build time.

Local pytest and cargo tests were not run again, as the owner asked. Ruff
passes. CI ran the tests on the same tui/ and packages/ trees that the tag
will have, because this commit changes documents only.
zy1233 added a commit that referenced this pull request Oct 13, 2026
…raws nothing (#21)

The Added entry for scripts/tui_pty/status_line_probe.py said that the row
draws nothing with the Python agent. #19 changed that, and the Changed entry
of the same release says so. The release page would have shown both. The
entry now says what the probe found when it was written and what it checks
now.

I searched the [0.5.0] section for other lines that #18 or #19 made old, with
these words: export, status_line, status row, session_status, resume_matrix,
GAP, probe, tui_pty. This was the only one.

The release notes grow from 49,429 B to 49,640 B (awk program of
release.yml). The plan and the execution log have the new number.
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