Repository navigation
feat(agent): honour _meta.sessionId on session/new, say where a session works (ADR1 PR 1, plan r10) - #11
Merged
Conversation
This was referenced Oct 10, 2026
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…ates (plan r9) (#10) ## Summary **Stacked on #9** (base `codex/linux-wsl-r8`). It adds sixteen commits on top of `d84d98a`: the three phase 0 gates of `docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md` that were still open after r8 (0.3 on the Rust side, 0.7, 0.8), and the parts of phase 1 that need no decision. Section 10.11 and appendix A of the plan have the full record. **No behaviour-changing decision is made here.** Where a decision is needed (the permission-mode mapping, the `--session-id` contract, what happens to `zypi export`, ...) the plan writes a proposal and says what it would cost. Section 10.11 ends with twelve of them, each with my preference; none is implemented. What the branch does: - **0.3, e2e against the real agent.** `python_agent_e2e.rs`: 10 tests drive the pager's ACP client against the real Python agent, and `tui-ci.yml` installs the agent first and sets `PI_E2E_REQUIRED=1` so a missing Python fails instead of passing silently. Its first run found two product bugs, both fixed here: the reason an agent failed to start was swallowed into `channel closed`, and `connect()` hung when the agent exited at once. - **0.7, can the pager lose its own disk reads (ADR1)?** Measured: with 1,000 sessions on Windows the first page of `session/list` went from 2.0 s to 0.11 s and `session/load` from 2.0 s to 0.12 s (a header is read without reading the file, and the repo lists a page at a time). ADR1's override condition did not trigger. The plan inventories the pager's 13 disk-read call sites in 8 files, adds a 13-case PTY matrix of the resume flags (`scripts/tui_pty/resume_matrix.py`: 9 PASS / 4 GAP, the GAPs are the work ADR1 would do) and a landing design with the sandbox ordering problem and a PR slicing. - **0.8, capability matrix and ACP version pairing** (appendix A): every row marked decided / proposed / open, the upgrade rules, and a per-method table of what each end supports. - **1.P1:** `session/list` is paged with an opaque cursor, the pager's picker follows `nextCursor`, and a session is looked up by id without reading every file. - **1.P4:** the pager sends no `mcp_servers`; the agent says which ones a client passed and that it ignored them. - **1.P7:** the permission round trip and `session/cancel` are covered over real stdio. - **Agent exits on its own:** the pager says so and why, instead of a bare `channel closed`. - **1.P3 input, not a change:** `scripts/tui_pty/mode_probe.py` records which permission modes the Python agent honours (12 scenarios on WSL2). Plan mode restricts nothing, Auto is always-approve in the agent, the settings "Default" is dropped, and of the six `--permission-mode` values only `bypassPermissions` does anything. ## Decisions that were open Plan section 10.11, last list. The three that unblock the most, and what became of them: 1. Start ADR1 with PR 1: the Python side only (`_meta.sessionId` honoured by `session/new`, the session's cwd in the `session/load` response). No TUI change, least risk. **Done in #11.** 2. 1.P3: hide Plan, Auto and Default (Shift+Tab becomes Normal <-> Always-Approve) and accept only the `--permission-mode` values that do something. Today Plan gives users a false sense of safety. **Done in #12.** 3. The sandbox ordering (the first of the plan's three options): drop "restore the sandbox profile a session saved", which never applies to a Python session. **Still open.** ## Not verified - The Rust e2e tests and the PTY scripts ran on Linux (WSL2) only; the latency numbers on Windows measure the Python side only. Cold disk, network file systems and more than 1,000 sessions were not measured. - Cross-directory `--resume <id>` (which directory the TUI uses for `@` search, diffs and git info), the settings "Default" path under a pty, and the `-m` / `--model` flag. - The landing design is on paper; everything uses the mock LLM. ## Review guide | Commit | What | |---|---| | `28fed94` | 1.P7: permission round trip and cancel over real stdio | | `54a38fe`, `1b9fb47` | harness: read a header without the file; list a page at a time; find by id | | `e944ed5` | `scripts/session_list_bench.py` | | `c1f02c8`, `9f7bc35` | 1.P1: Python pages `session/list`; the pager follows `nextCursor` | | `935e4b8`, `7becb49`, `9798e1d` | 1.P4: the agent names the ignored MCP servers; the pager sends none; docs | | `affda9e` | the pager notices an agent that exits, and says why a start failed | | `3fc182b` | 0.3: `python_agent_e2e.rs`, `tui-ci.yml`, manifest | | `babd75f` | 0.7: `resume_matrix.py` | | `bee3a36` | manifest: `pi-pager` `reference_passed` 5,602 | | `679cccb`, `d426253` | 1.P3 input: `mode_probe.py` | | `eef675b` | docs: plan 10.11, appendix A, B.4, `docs/baselines/tui.md`, CHANGELOG, AGENTS files | #9 was squash-merged first and this PR's base then moved to `main`. The PR still lists #9's commits, and GitHub could not merge it because files that both sides changed near each other conflict. `b0d001b` merges `main` into the branch with the `ours` strategy: the tree of `main` is the tree of #9's last commit, the base of this branch, so `main` holds nothing the branch lacks, and the branch's tree is unchanged. ## Verification - **CI.** The run on `eef675b` is green (TUI CI [run 38061734062](https://github.com/zy1233/pi-python/actions/runs/38061734062)). The new install step in `tui-ci.yml` ran for the first time, `python_agent_e2e`'s 10 tests really ran (a missing Python would fail them), and the 10 suites add up to **8,659 passed / 0 failed / 22 ignored** with `pi-pager` at 5,602: the numbers measured locally. `cargo test` took 23 m 19 s (32 m 40 s on #9), `Gates + cargo check` 7 m 9 s, Ruff 9 s, pytest on 3.11 / 3.12 / 3.13 about 2 minutes each. `tui-ci.yml` now also runs when `pi_agent_cli/**` changes. After #9 was squash-merged, `main` was merged into this branch (`b0d001b`, keeping the branch's tree, which is the tree of `eef675b`) and CI ran again: green too ([run 38074449935](https://github.com/zy1233/pi-python/actions/runs/38074449935); `cargo test` 21 m 48 s, 8,659 passed again). - Whole manifest on WSL2 (`python scripts/tui_baseline.py test`, scrubbed environment, `PI_E2E_PYTHON` and `PI_E2E_REQUIRED=1`): exit 0, 10 suites, **8,659 passed, 0 failed, 22 ignored** = #9's CI result (8,634) + 25 new `pi-pager` tests (10 e2e, 8 agent-exit, 6 paging, 1 for `mcp_servers`). `pi-pager`: 5,602. - pytest: 2,352 passed / 35 skipped on Linux (WSL2, Python 3.12.3), 2,351 passed / 36 skipped on Windows (Python 3.12.7); `scripts/tests/test_tui_pty.py` with `pyte`: 19 passed. - `ruff check .` and `ruff format --check .`: clean (226 files) with the project's Ruff 0.16.10 and with CI's pinned 0.16.0. - `resume_matrix.py`: 9 PASS / 4 GAP / 0 FAIL. `mode_probe.py`: 12 scenarios.
…on works (ADR1 PR 1)
ADR1 PR 1, the Python half (plan section 10.11): `zypi --session-id <uuid>` sends the id in
session/new's _meta.sessionId and the agent ignored it, picking its own, so the flag did nothing
on the Python agent (resume_matrix case 10). Two things the TUI will rely on once it stops
reading session files:
- session/new takes _meta.sessionId as the session's id. It must be a UUID in any form
uuid.UUID reads and is stored in canonical form; anything else is invalid_params, because the
id ends up in a file name ("../../outside" is refused). An id some session already has is
invalid_params too, whatever directory that session belongs to: JsonlSessionRepo.create
accepts options["id"] but does not refuse a used one (the file name starts with the creation
time, so a second file with the same id fits beside the first and either would answer a later
session/load). Requests for the same id at once make one session (an asyncio lock; two agent
processes on one home are not covered).
- session/new, load and resume answer with _meta["pi/cwd"], the directory the session works in.
load and resume bind the one saved with the session, not the client's, so a client that only
knows where it was started (zypi --resume <id> from another directory) can learn the real one.
Nothing in the TUI reads it yet.
JsonlSessionRepo.has_session(id) answers "is this id taken?" for the first of these. `find`
answers the same but reads every header for an id nobody has, which is the usual case: 1.7 s at
5,000 sessions. has_session opens a file only when its name does not already say it belongs to
another session (<timestamp>-<id>.jsonl, as the repo writes them), so a free id costs the
directory listing: 0.07 s at 1,000 sessions and 0.3 s at 5,000, on Windows and WSL2 alike, against
8-20 ms for a session whose id the agent picks. It takes one thing on trust: a file renamed by
hand to carry another id. Nearly all of the remaining cost is LocalExecutionEnv.list_dir building
a FileInfo per entry (os.listdir of the same 5,000 files: 2-11 ms); that is shared with
session/list and left alone here.
Measured against the real zypi on WSL2 (scripts/tui_pty/resume_matrix.py): `--session-id <fresh
uuid>` now starts the session with that id (case 10 closes: 10 PASS / 3 GAP). Case 11 stays a
gap on the pager's side: its in-use check reads the old layout and never sees the session, so it
sends session/new, the agent refuses, and the TUI shows "Session creation failed: Invalid params:
{...}" and stays up instead of exiting 1 (PR 2).
Tests: tests/test_session_chosen_id.py (18: id honoured and visible after a restart, canonical
form, bad values, id in use under another spelling / after a restart / from another directory,
five concurrent requests make one, pi/cwd in all three responses), two wire tests in
test_acp_stdio_contract.py, seven for has_session in test_harness_session_page.py.
scripts/session_list_bench.py gains the `new` / `new id` and free-id numbers.
Verified: pytest on Windows 2,378 passed / 36 skipped, on Linux (WSL2, ext4 clone) 2,379 passed /
35 skipped; ruff check and format --check clean. Eleven one-line mutations of the new code were each
caught by at least one test. Only mocks and the scripted agents, no real LLM; zypi ran on Linux only.
zy1233
force-pushed
the
codex/adr1-pr1-python
branch
from
October 10, 2026 18:36
1a9a336 to
03961fa
Compare
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…ds the merge order The ADR1 PR 1 branch and the mode_a branch were local when the plan and the baseline report were written, and said "not pushed" in eight places. They are pushed now: #11 (codex/adr1-pr1-python) on top of #10, #12 (codex/phase1-mode-a) on top of #11. Those places name the PRs instead; the r9 statements that were true when they were written stay, as the r10 entry already says. The r10 entry in the revision log gets the last step. The user asked to sync main to origin and merge the processed PRs. The stack has four (#9, #10, #11, #12), and the upper ones have nothing to merge into until the lower ones are in, so all four are merged, bottom-up, one squash each (the convention of #7 and #8), with the base moved to main before each merge and the tree of main compared with the branch's afterwards. Local main is 9 commits ahead of origin/main; all of them except one older docs commit (superseded by the revised one in #9) are already in #9's branch, so main is not pushed: after the merges it is reset to origin/main, and the old main stays on a backup branch. Docs only: the plan, docs/baselines/tui.md.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…rged (plan r10) PR #12's CI run counted what the local WSL2 run had: 8,683 passed / 0 failed / 22 ignored over the ten suites, pi-pager 5,626, each suite equal to its reference_passed. The baseline report and the comment in the manifest said the next CI run would confirm that; they now say it did, with the run. #10 (after main was merged into its branch) and #11 were green too, 8,659 over the ten suites. The r10 entry of the revision log gets what happened when the stack was merged. #9 was a squash, so #10's branch still held #9's original commits and GitHub reported conflicts in files both sides had changed near each other, in the same way. main was merged into #10's branch with the ours strategy (the tree of main is the tree of #9's last commit, so nothing is dropped), #11 and #12 were each rebased onto the new main before their squash, and after every merge the tree of main was compared with the branch's. A squash of the lower PR of a stack makes the next one conflict unless it is brought up to main first. Docs and a comment in the manifest only; the code is the tree CI already ran on.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…P3 (a), plan r10) (#12) ## Summary **Stacked on #11**, which is merged first; this branch is then rebased onto `main` and its base moved there. Three commits: plan 1.P3 (a) (the first option of decision 7 in section 10.11), a docs commit that names the PRs of the two r10 branches, and a docs commit that records the CI runs and how the stack was merged. Section 10.13 of `docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md` has the record. **This changes what users see.** The Python agent does two things about a tool call: it asks, or it approves everything. The TUI still offered Plan, Auto, a settings "Default" and six `--permission-mode` values. Measured against the real `zypi` on WSL2 (section 10.11): - Plan restricts nothing: the agent has no `session/set_mode` handler, the call fails unseen, and the screen goes on saying "plan". - Auto never asks, because the agent treats `auto` like `always-approve`, while the screen promises a classifier. - The settings' Default is a value the agent drops: after Always-Approve it says Default and the agent still approves everything. - Of the six `--permission-mode` values, only `bypassPermissions` did anything. All of them err the same way: the user thinks the agent is more careful than it is. This PR stops offering what the agent does not honour. **The Python agent is untouched.** **Hidden, not deleted.** `app/agent_modes.rs` holds three capability constants (`PLAN`, `AUTO`, `DEFAULT`, all `false`: "the agent honours this"); `AppView.{plan,auto,default}_mode_gate` and the mirrored `PagerLocalSnapshot` gates follow them. Test fixtures open the gates, so the hidden paths and their old tests still run, and flipping one constant brings its mode back together with its tests (decision 7 (b)). - **Shift+Tab** cycles Normal <-> Always-Approve (Auto sits between them only while its gate is on). The pre-session branch stages the flags `SessionCreated` reads, the session branch goes through `set_yolo_mode_inner`, and both persist. The managed-policy pin keeps Normal and shows the policy's warning. A stale Plan flag leaves through Normal with `session/set_mode(default)`. - **Settings:** no `plan_mode` row and no plan-nudge toggle; `permission_mode` offers Ask and Always approve only, and its description no longer mentions Default, Auto or a classifier; the plan nudge tip is not shown. `set_plan_mode` does nothing, and `set_permission_mode` turns Auto / Default into Ask. - **A saved** `permission_mode = "auto"` or `"default"` is shown as Ask and the file is left alone; a launch never starts in Auto. - **`--permission-mode`** takes `default` (no flag) and `bypassPermissions` (`--always-approve`). `plan`, `auto`, `acceptEdits` and `dontAsk` exit 2 with a message that says the flag would change nothing and lists the valid values. `pi-shell`'s `PermissionMode::VALID_VALUES` (agent-definition frontmatter) is untouched. ## Decision for you This is (a) of decision 7, the option you picked (`mode_a`). A real Plan mode ((b): the agent grows a `session/set_mode` handler and a read-only toolset) or a real Auto ((c)) would start from the three constants; neither is done here. ## Not verified, left - Only mocks and scripted agents, no real LLM; the real `zypi` ran on Linux (WSL2) only, so macOS and Windows runs of the TUI are not done. - Left, and listed in section 10.13: the settings search for "plan" still finds "Show contextual hints", and `permission_mode`'s search keywords still include "classifier" and "auto"; `--no-plan` is still in `--help`; the embedded upstream user guide (extracted to `<home>/docs/user-guide/`) still describes the old Plan / Auto cycle; the other permission settings (`remember_tool_approvals`, `default_selected_permission`) are not checked against the Python agent (candidate 14); the path that leaves a stale Plan flag is covered by a unit test only. ## Review guide | Files | What | |---|---| | `tui/crates/codegen/pi-pager/src/app/agent_modes.rs` (new) | the three constants, `permission_mode_flag_values()`, `launch_auto(...)`, the module doc with the three measured facts | | `app/app_view.rs`, `app/dispatch/modes.rs`, `dispatch/prompt.rs`, `dispatch/mod.rs`, `dispatch/settings/ui.rs`, `app/event_loop.rs`, `app/mod.rs` | the gates, the Shift+Tab ring (`next_ring_step_without_plan`, `dispatch_cycle_mode_without_plan`), the degrade paths, the plan nudge | | `settings/registry.rs`, `settings/defs.rs`, `views/settings_modal/state.rs`, `actions/defaults.rs` | settings rows and choices, the `CycleMode` action text | | `app/cli.rs` | `--permission-mode` value parser | | tests in `pi-pager` | 24 new (`agent_modes` 2, `cli` 3, `dispatch/tests/modes.rs` 13, `settings_modal` 3, `settings_e2e` 3) and 3 adjusted old ones (`rows_contain_categories_and_settings_through_pr_14`, `enum_choice_gated_off_covers_voice_and_permission`, `expanded_description_wraps_to_modal_width`) | | `scripts/tui_pty/mode_probe.py`, `scripts/tests/test_tui_pty.py` | the probe now has 10 scenarios, 4 refused values and a settings-page check; 4 new helper tests | | `scripts/tui_baseline.toml`, `docs/...`, `CHANGELOG.md`, `packages/pi-agent-cli/AGENTS.md` | `pi-pager` `reference_passed` 5,626; plan section 10.13, A.1, B.6; one CHANGELOG entry under Changed | ## Verification - **CI.** The run on the commit as first pushed (`bd44289`) is green ([TUI CI run 38074038191](https://github.com/zy1233/pi-python/actions/runs/38074038191)): the ten suites add up to **8,683 passed / 0 failed / 22 ignored** with `pi-pager` at 5,626 (13 ignored), each suite equal to its `reference_passed`, so the local numbers below held on the runner, including the `settings_e2e` test that was edited after the local manifest run (that binary alone: 254 passed). `cargo test` took 29 m 13 s, `Gates + cargo check` 5 m 46 s, Ruff 10 s, pytest on 3.11 / 3.12 / 3.13 about 2 minutes each. The branch was then rebased onto `main` after #11's squash and one docs commit was added; compared with the commit CI ran on, only three files differ (the plan, `docs/baselines/tui.md`, and a comment in `scripts/tui_baseline.toml`). - Whole manifest on WSL2 (`python scripts/tui_baseline.py test`, scrubbed environment): exit 0, 10 suites, **8,683 passed, 0 failed, 22 ignored** = #10's CI result (8,659) + 24 new `pi-pager` tests. `pi-pager`: 5,626. - pytest: 2,382 passed / 36 skipped on Windows, 2,383 passed / 35 skipped on Linux (WSL2); `scripts/tests/test_tui_pty.py` with `pyte`: 23 passed. - `ruff check .` and `ruff format --check .`: clean (227 files) with the project's Ruff 0.16.10 and with CI's pinned 0.16.0. - Twelve one-line mutations of the new code, each caught by at least one test. One was not seen at first: a Default listed in front of Ask in the picker, because the e2e test only pressed Down and the picker clamps at both ends; it presses Up as well now. - Against the real `zypi` (`scripts/tui_pty/mode_probe.py`): Shift+Tab goes Normal -> Always-Approve -> Normal -> Always-Approve (a write is asked about in Normal, not in Always-Approve); `--permission-mode default` and `bypassPermissions` behave as before; a saved `auto` or `default` shows nothing in the status bar and the write is asked about; a saved `always-approve` shows "always-approve" and is not asked; `plan`, `auto`, `acceptEdits` and `dontAsk` exit 2; the settings search for "plan" finds no Plan mode row, and the permission picker offers Ask and Always approve.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #10, which is merged first; this branch is then rebased onto
mainand its base moved there. One commit: ADR1 PR 1 ofdocs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md(section 10.11 has the design and the PR slicing, section 10.12 the record). It is the Python half only: nothing in the TUI changes. A client that never sends_meta.sessionIdsees one difference: the_meta["pi/cwd"]key in the responses that open a session.zypi --session-id <uuid>sends the id insession/new's_meta.sessionId, and the agent ignored it and picked its own, so the flag did nothing on the Python agent (resume_matrix.pycase 10). This makes it work, and gives the TUI two things it will rely on once it stops reading session files, plus one library helper for the first:session/newtakes_meta.sessionIdas the session's id. It has to be a UUID in any formuuid.UUIDreads, and it is stored in canonical form (lower case, hyphens). Anything else isinvalid_params, because the id ends up in a file name (../../outsideis refused). An id some session already has isinvalid_paramstoo, whatever directory that session belongs to:JsonlSessionRepo.createaccepts an id but does not refuse a used one, so a second file with the same id would fit beside the first and either could answer a latersession/load. Requests for the same id at once make one session (an asyncio lock).session/new,loadandresumeanswer with_meta["pi/cwd"], the directory the session works in.loadandresumebind the one saved with the session, not the client's, so a client that only knows where it was started (zypi --resume <id>from another directory) can learn the real one. Nothing in the TUI reads it yet.JsonlSessionRepo.has_session(id)answers "is this id taken?" without reading every header.findanswers the same, but for an id nobody has (the usual case) it reads all of them: 1.7 s at 5,000 sessions.has_sessionopens a file only when its name does not already say it belongs to another session (<timestamp>-<id>.jsonl, as the repo writes them), so a free id costs the directory listing: 0.07 s at 1,000 sessions and 0.3 s at 5,000, on Windows and WSL2 alike. It takes one thing on trust: a file renamed by hand to carry another id.Not done, not verified
pi/cwdand does not pre-check a duplicate--session-id, so case 11 of the matrix stays a GAP: the pager's in-use check reads the old layout and never sees the session, sendssession/new, the agent refuses, and the TUI showsSession creation failed: Invalid paramsand stays up instead of exiting 1. That is PR 2.has_sessionstill costs isLocalExecutionEnv.list_dirbuilding aFileInfoper entry (os.listdirof the same 5,000 files takes 2-11 ms). It is shared withsession/listand left alone (proposal 13 in the plan).zypiran on Linux (WSL2) only.Review guide
One commit, 12 files, +499 / -37.
packages/pi-agent-harness/.../session/jsonl_repo.py,tests/test_harness_session_page.pyhas_session, seven testspackages/pi-agent-cli/pi_agent_cli/agent.py_meta.sessionIdonsession/new, the per-id lock,_meta["pi/cwd"]onnew/load/resumepackages/pi-agent-cli/tests/test_session_chosen_id.py,test_acp_stdio_contract.pypi/cwdin all three responsesscripts/session_list_bench.py,scripts/tui_pty/resume_matrix.py,scripts/tests/test_tui_pty.py,scripts/tui_pty/README.mdnewand a free id; matrix case 10 loses its gap and case 11's gap text says what is seen now; the test that pinned "every fresh-id case is a gap" goesdocs/PLAN/...,CHANGELOG.md,packages/pi-agent-cli/AGENTS.mdVerification
1a9a336) is green: Ruff, pytest on 3.11 / 3.12 / 3.13 (about 2 minutes each), and the TUI job, which runs becausepi_agent_cli/**changed: 10 suites, 8,659 passed / 0 failed / 22 ignored withpi-pagerat 5,602, which includes the 10python_agent_e2etests that drive the pager's ACP client against this agent (TUI CI run 38073976529;cargo test32 m 53 s). The branch was then rebased ontomainafter feat: phase 1 contract work that needs no decision, and the phase 0 gates (plan r9) #10's squash; the tree of the rebased commit is the tree of1a9a336, so that result stands.ruff check .andruff format --check .: clean.zypion WSL2 (scripts/tui_pty/resume_matrix.py):--session-id <fresh uuid>now starts the session with that id; the matrix goes from 9 PASS / 4 GAP to 10 PASS / 3 GAP, no FAIL.