Skip to content

fix(agent-runtime): discover native sessions in configured sessionDir - #1764

Merged
vastsa merged 4 commits into
vastsa:mainfrom
Frank-zhu2000:fix/1732-project-session-dir
Oct 20, 2026
Merged

vastsa merged 4 commits into
vastsa:mainfrom
Frank-zhu2000:fix/1732-project-session-dir

Conversation

@Frank-zhu2000

Copy link
Copy Markdown
Contributor

Summary

Fixes #1732

When Pi is configured with a sessionDir (project .pi/settings.json or global ~/.pi/agent/settings.json), new sessions are written there, but Desktop only discovered native sessions under ~/.pi/agent/sessions. Those sessions therefore disappeared from history after a restart and could not be reopened.

Root cause: native session discovery in packages/agent-runtime/src/native-pi-session.ts walked only the default session root and never resolved the sessionDir setting that the Pi CLI uses.

Fix:

  • Resolve sessionDir through the pinned coding-agent SettingsManager.getSessionDir() (same resolution and ~ expansion as the Pi CLI). An absolute global sessionDir is scanned even with no project; a relative one is resolved against each known local project path. remote-project: ids are skipped and the project list is capped at 500. The default session root is always kept.
  • Configured directories are scanned top-level only and admit regular *.jsonl files only, so nested files outside the default root and symlinks (including an in-directory alias of another session file) are not listed. Native-id dedup emits each id once.
  • Real directories that already admitted a session during a list are remembered, so detail and fork of that session keep working if a later list call passes an empty project list.
  • Desktop session list, search, and live-work now fetch local project paths and forward them to the sidecar (session-search.ts, session-ipc.ts, live-work-backend-port.ts, sidecar.ts). A failing projects.list is logged and falls back to [] (default root and absolute global sessionDir still list); session.list handlers are attached in the same turn so a fast host rejection is not an unhandled rejection. Unreadable sessionDir settings are logged once per path per scan.
  • Trust is unchanged: it still follows the session header cwd, and untrusted project resources stay browseable and read-only.

Specs / ADRs / E2E

  • ADR 0254 (native Pi session continuation): amendment for configured sessionDir discovery.
  • docs/spec/03-runtime/04-data-storage.md §12 and docs/spec/05-security/01-security.md: discovery roots, top-level/regular-file rule, trust gate.
  • New E2E scenario E2E-SESSION-native-pi-project-session-dir in docs/spec/06-delivery/04-e2e-test-plan.md; source-regression covered by the unit tests below, full Desktop journey marked Draft.

Tests

New cases were written failing first, then fixed:

  • packages/agent-runtime/src/native-pi-session.test.ts (45 pass): project-local sessionDir, absolute global sessionDir with no project, relative global sessionDir only for a known local project, remote project id ignored, nested file and escaping symlink excluded, in-root *.jsonl alias listed once and openable, default root still listed, detail/fork after an empty follow-up list, unreadable settings, fork byte identity, untrusted project stays read-only.
  • apps/desktop/test/native-pi-sessions.test.mjs and apps/desktop/test/live-work-scope.test.mjs: project path forwarding, failed projects.list, same-turn rejection handling, live-work forwarding.

Local run of the PR workflows (ci.yml jobs js, rust, trusted-extensions-e2e, pr-base.yml, and docs-check.yml) on head 0b0e83e47, which contains latest upstream/main 87a48b953; Node 24.21.0, pnpm 12.8.1, CI=true:

step result
pnpm check:pr-base ancestry + its unit test pass
pnpm install --frozen-lockfile, pnpm build:js pass
typecheck, pnpm lint, architecture check, pi-patch-hash pass
docs agent-policy, docs check, docs build pass
cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features pass
trusted-extensions E2E (incl. browser-capture-resize and transcript-render-disclosure) pass, SUMMARY 57/57 passed
pnpm -r --if-present test 4113 desktop tests: 4094 pass / 12 fail / 7 skipped — baseline, see below
cargo test -p host-core --locked 963 pass / 1 fail / 1 ignored — baseline, see below

Pre-existing local failures (same on merge-base)

Both failing steps were re-run on the merge-base 87a48b953 (current upstream/main) and fail identically there:

  • pnpm -r --if-present test — the same 12 apps/desktop *-user-path / plugin-view-bounds-sync tests fail on both trees with Electron binary missing from scripts/e2e/boot.mjs: pnpm install --frozen-lockfile does not download the Electron binary, and ci.yml installs it later in the E2E job. Head 4113 / 4094 / 12; base 4109 / 4090 / 12. The extra head tests (4 desktop, 6 agent-runtime) all pass.
  • cargo test -p host-core --locked — tools::tests::bash_inherits_user_login_path fails with every injected login-PATH entry is present on both trees (963 passed, 1 failed, 1 ignored each), because this host's login shell PATH does not satisfy the test. This PR changes nothing under crates/.

NOT RUN

  • ci.yml / rust-windows (windows-latest) — no Windows host locally. The diff touches no Rust code; the Ubuntu host-core suite ran. Remaining risk is low.

Compatibility / security

No migration. Users without a sessionDir setting see the same list as before. Discovery in configured directories is top-level and regular-file only, symlinks are not followed for listing, and every opened file must still resolve inside an allowed session root, so the change does not widen file access beyond the directories Pi itself writes sessions to.

AI disclosure

This change was prepared with AI assistance (code, tests, docs, and this description). The diff was reviewed, and every check listed above was actually run locally; nothing is reported as passing that was not executed.

Pi stores sessions in a project or global sessionDir when that setting
is set. Desktop only walked ~/.pi/agent/sessions, so those files never
appeared in history and could not be opened.

Resolve the same setting the Pi CLI uses and scan those directories
for known projects, while keeping the default session root.

fixes vastsa#1732
An in-root *.jsonl symlink was scanned as a second file. Both names
resolved to one stable id, dedup deleted that record, and detail
returned NOT_FOUND. A later list with an empty project path list also
rebuilt containment from that snapshot, so an already admitted custom
directory failed closed with PERMISSION_DENIED.

Scan only regular files, keep the winner when a duplicate summary uses
its id, and remember directories that already admitted a file. Log
unreadable sessionDir settings and a failed projects.list without
failing the Desktop list. Attach session-list handlers in the same
turn as the project-path fetch.
@muzimu217

Copy link
Copy Markdown
Contributor

Reviewed in an isolated worktree: native-pi-session.test.ts 45/45 pass, including six new cases that map exactly onto the failure modes in #1732.

This is the right fix for the issue I verified earlier — the desktop never consulted sessionDir, so creation and discovery disagreed. Reading the implementation:

Resolution is delegated to Pi, not re-implemented. rawSessionDir uses SettingsManager.create(cwd, agentDir) and getSessionDir(), so project .pi/settings.json and global settings resolve with Pi's own precedence. Relative values resolve against the project cwd (projectSessionDir) while only an absolute global value is treated as global (globalAbsoluteSessionDir) — that matches Pi's documented semantics and avoids the classic mistake of resolving a relative project path against the process cwd.

Containment is preserved. sessionRoots() builds the allowed set from the default root plus the realpathed configured dirs plus admittedRoots, and every candidate goes through the existing inside() check. admittedRoots is only a memo of roots that already admitted a file — it does not create a new read path, it prevents open/fork from failing when a later list() is called with an empty project list (the test "opens a project sessionDir session after a later empty project list" pins exactly that).

Degradation is sane. An unreadable settings file logs once per path and still lists the default root; a missing configured dir contributes no files and no containment root. MAX_NATIVE_PROJECT_PATHS = 500 bounds the per-list settings reads.

Two non-blocking notes:

  1. list() mutates this.projectPaths as a side effect, so two concurrent callers with different project lists race. The service is presumably used serially per window, but if two windows share the instance, one window's project list can leak into the other's containment set for the duration of a call. Worth a comment stating the single-caller assumption, or taking the paths as a parameter through the whole call.
  2. topLevelJsonl intentionally does not recurse ("custom session directories are flat"), which matches how Pi writes them — but a user who points sessionDir at a directory that contains subdirectories will see those sessions ignored. That is defensible; a line in the error/log or docs would save a confused report later.

Nothing blocking from my side.

list() stored the caller's project paths on the service, so a second
list() could replace them before discovery finished and scan the other
caller's sessionDir. Keep the paths on the call, and keep admitted
roots so a later empty project list can still open and fork a session
that was already found.

A configured sessionDir is a flat scan. Note in the code and in the
sessionDir docs that sessions in its subdirectories are ignored.

@vastsa vastsa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed #1732 against the base: native session discovery only walked the default root, while Pi writes configured sessionDir sessions elsewhere. This change resolves settings through Pi's SettingsManager, forwards local project paths from the existing session list/search/live-work entry points, and keeps the configured scan limited to top-level regular JSONL files. Trust/read-only behavior remains in place.

Independent local validation on this head: native-pi-session.test.ts 46/46, Desktop session/live-work targeted tests 32/32, agent-runtime TypeScript check passed, and git diff --check passed. I approve this fix.

@vastsa
vastsa merged commit cc68837 into vastsa:main Oct 20, 2026
6 checks passed
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.

[Bug] Session history does not respect custom project-local sessionDir

3 participants