feat(pi): per-session session-root + affinity via a pi extension#19
Merged
Conversation
`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>
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>
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.
What
opper launch pigroups each pi session into one Oppersessiontrace (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.How
data/pi-opper-extension/opper-session.ts— a pi extension auto-discovered from~/.pi/agent/extensions/. On everysession_startit derivestid = uuid5(NS, ctx.sessionManager.getSessionFile())(per-process fallback for ephemeral-p) and re-registers theopperprovider withX-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 unlessOPPER_API_KEY/OPPER_BASE_URLare set, so it never disturbs the user's own pi sessions.before_provider_requestcan only rewrite the request body, not headers, so headers are (re)applied viaregisterProvideron 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 theopperprovider + the extension transiently:withJsonKeyssnapshotsproviders.opperand 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 viaOPPER_BASE_URL/OPPER_API_KEYenv.Why it matters
Provider affinity is what makes prompt caching pay off across turns. On the
anthropicprovider 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 --noEmitclean; 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_URLon the spawn env (and noPI_CODING_AGENT_DIRoverride), andunconfigurecleanup.sessiontree with cache-read cost on turns 2+.sessiontrees (main + child subagent), each correctly rooted.~/.picontaining a user provider → the user provider survived, the opper provider + extension were removed on exit, and thesessionroot still formed.Depends on
task-api session-root span (opper-ai/opper#2921), already merged + deployed.
🤖 Generated with Claude Code