Skip to content

feat(pi): per-session session-root + affinity via a pi extension#19

Merged
mattias-lundell merged 4 commits into
mainfrom
feat/pi-session-root
Jun 16, 2026
Merged

feat(pi): per-session session-root + affinity via a pi extension#19
mattias-lundell merged 4 commits into
mainfrom
feat/pi-session-root

Conversation

@mattias-lundell

@mattias-lundell mattias-lundell commented Jun 15, 2026

Copy link
Copy Markdown
Member

What

opper launch pi groups each pi session into one Opper session trace (and pins it to one provider for prompt-cache reuse) — the same outcome the Hermes plugin gives — via a shipped pi extension, so sub-sessions are handled correctly and the user's own pi config is preserved.

Evolved across the branch's commits: per-launch static header → session extension → (final) runs in the user's real ~/.pi, not an isolated home.

How

  • data/pi-opper-extension/opper-session.ts — a pi extension auto-discovered from ~/.pi/agent/extensions/. On every session_start it derives tid = uuid5(NS, ctx.sessionManager.getSessionFile()) (per-process fallback for ephemeral -p) and re-registers the opper provider with X-Opper-Trace-Id = X-Opper-Parent-Span-Id = tid. So each pi session — /new, /resume, /fork, /reset, or a child pi process — gets its own session-root tree, mirroring the Hermes plugin's per-subagent rotation. It is a no-op unless OPPER_API_KEY/OPPER_BASE_URL are set, so it never disturbs the user's own pi sessions.
    • pi's before_provider_request can only rewrite the request body, not headers, so headers are (re)applied via registerProvider on the session boundary. A partial { headers } registration replaces the provider config and drops the apiKey/baseUrl, so the extension re-supplies both from env.
  • pi.ts — runs pi in the user's real ~/.pi (their skills, settings, MCP servers, other providers all load), adding the opper provider + the extension transiently: withJsonKeys snapshots providers.opper and the extension file is save/restored, so a one-off launch leaves nothing behind and a pre-existing same-named file is put back. The session URL + key are passed via OPPER_BASE_URL/OPPER_API_KEY env.

Why it matters

Provider affinity is what makes prompt caching pay off across turns. On the anthropic provider this is real — measured ~11× cost drop on a cache-read vs the cold first call. The session-root grouping keeps a multi-turn pi run as one tree, while sub-agents/forks land as their own trees instead of being flattened together.

Test

  • tsc --noEmit clean; full suite green (373). Pi tests assert: session URL + extension shipped mid-launch, config + extension restored on exit (one-off launch leaves nothing; a pre-existing extension is put back), sibling providers preserved, OPPER_API_KEY/OPPER_BASE_URL on the spawn env (and no PI_CODING_AGENT_DIR override), and unconfigure cleanup.
  • Verified live:
    • A 5-turn anthropic session → one session tree with cache-read cost on turns 2+.
    • A launch that spawned a child pi via bashtwo separate session trees (main + child subagent), each correctly rooted.
    • A launch over a real ~/.pi containing a user provider → the user provider survived, the opper provider + extension were removed on exit, and the session root still formed.

Depends on

task-api session-root span (opper-ai/opper#2921), already merged + deployed.

🤖 Generated with Claude Code

mattias-lundell and others added 2 commits June 15, 2026 19:17
`opper launch pi` now mints one UUID per launch and sets it as both
X-Opper-Trace-Id and X-Opper-Parent-Span-Id on the `opper` provider entry.
Pi forwards provider `headers` verbatim on every request, so the launch's
calls group into one Opper trace, pin to one provider for prompt-cache
reuse, and — because parent == trace — make task-api auto-create a single
`session` root span (opper-ai/opper#2921), rendering the launch as one
tree instead of N sibling roots.

Mirrors the Hermes provider plugin's per-process fallback (#17), but via
Pi's first-class provider `headers` config — no plugin file needed.

Headers are added only on the launch path; configure() stays header-free
and the existing providers.opper snapshot reverts them on exit, so plain
`pi` runs never inherit a fixed trace id.

Verified live: a one-shot `opper launch pi` produced a trace named
"session" with a `workflow` root span and the `llm` turn nested under it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the per-launch static header with a shipped pi session extension, so
each pi session — /new, /resume, /fork, /reset, or a child pi process — gets its
OWN X-Opper-Trace-Id (uuid5 of pi's session file) and thus its own `session`
root tree, instead of one flat id for the whole launch. Mirrors the Hermes
provider plugin's per-subagent rotation.

- data/pi-opper-extension/opper-session.ts: hooks session_start and re-registers
  the `opper` provider with per-session trace headers. pi's
  before_provider_request can't set headers, and a partial {headers}
  registration drops the apiKey/baseUrl, so it re-supplies both from
  OPPER_API_KEY / OPPER_BASE_URL (env, exported at spawn; key stays off disk).
- pi.ts: run pi against an isolated PI_CODING_AGENT_DIR (Opper-managed pi-home)
  like Hermes' HERMES_HOME — write models.json + ship the extension there, pass
  the key via env. Drops the user-config snapshot/restore; the user's real
  ~/.pi is never touched.

Verified live: a launch that spawned a child pi produced two separate `session`
trees (main: root + 2 turns; child: root + 1 turn), each correctly rooted.
tsc clean; suite green (372).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mattias-lundell mattias-lundell changed the title feat(pi): per-launch session-root / provider-affinity headers feat(pi): per-session session-root + affinity via a pi extension Jun 16, 2026
Launching into an isolated PI_CODING_AGENT_DIR ignored the user's own pi setup
(skills, settings, MCP servers, other providers). Run in the real ~/.pi/agent
instead and add the `opper` provider + session extension transiently
(snapshot/restore via withJsonKeys + a file save/restore), so the user's config
is fully loaded and a one-off launch leaves nothing behind. The extension is a
no-op unless OPPER_API_KEY / OPPER_BASE_URL are set, so it can't disturb the
user's own pi sessions.

Verified live: with a pre-existing user provider in models.json, a launch
preserved it, removed the opper provider + extension on exit, and still produced
a per-session `session` root trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The launch wrote the real key into models.json (restored on exit). Since the
extension re-registers from OPPER_API_KEY env and pi resolves $OPPER_API_KEY from
the same env, the on-disk literal was redundant — write the env ref so the real
key never lands in the user's config. configure() (persistent direct-use setup)
still writes the literal, since direct `pi` usage has no OPPER_API_KEY exported.

Verified live on the real-home build: launch resolves the key from env,
sub-sessions still separate into their own session trees, and a one-off launch
leaves models.json + the extension clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mattias-lundell mattias-lundell merged commit bcae05c into main Jun 16, 2026
5 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.

1 participant