Problem
The harness runs one session per process (KEDA ScaledJob model). At scale this means 1 pod per active session — 10,000 concurrent sessions = 10,000 pods. The harness spends 80-90% of wall-clock time idle, waiting for LLM responses and tool calls, yet each session exclusively owns a full pod.
Root cause
Pi was designed as a single-user CLI agent. runTurn is not concurrent-safe — five categories of global mutable state prevent multiple sessions from sharing a process:
| Problem |
Location |
Severity |
process.env.ANTHROPIC_API_KEY mutation per call |
harness/src/run-turn.ts |
Showstopper — concurrent sessions with different auth tokens corrupt each other |
stdoutTakeoverState — global process.stdout.write replacement |
pi-fork: coding-agent/src/core/output-guard.ts |
Showstopper — one session's capture swallows another's output |
sessionResourceCleanups — global Set, cleanupSessionResources() fires ALL |
pi-fork: ai/src/session-resources.ts |
Showstopper — cleanup for session A tears down session B's resources |
fileMutationQueues — global serialization queue |
pi-fork: coding-agent/src/core/tools/file-mutation-queue.ts |
Race — concurrent sessions serialize through same promise chain |
commandResultCache — shell results cached for process lifetime |
pi-fork: coding-agent/src/core/resolve-config-value.ts |
Stale data — session B reads session A's cached command output |
4 of 5 are in pi-fork, 1 is in the harness.
Proposed fix
Each fix is mechanical — replace module-level globals with a per-session context object threaded through the call chain:
process.env mutation → set credentials once at startup (or accept as function args, not env). This is the harness-side fix.
stdoutTakeoverState → remove entirely (server mode doesn't need stdout capture) or scope per session.
sessionResourceCleanups → key the Set by session ID.
fileMutationQueues → scope per session (each session gets its own queue).
commandResultCache → scope per session or add TTL.
Target architecture
┌─────────────────────┐
│ Harness Pod (×N) │
│ │
│ session-A ─┐ │
│ session-B ──┤ Pi │ N concurrent sessions
│ session-C ──┤ loop │ per warm process
│ ... ─┘ │
└──────────┬──────────┘
│
┌──────────┴──────────┐
│ Sandbox Pool │ shared, leased
│ (kubectl exec) │ unchanged
└─────────────────────┘
- A fixed/elastic pool of harness pods replaces KEDA ScaledJob (create/destroy per session).
- Each harness process serves ~50-100 concurrent sessions.
- Session state stays in Redis (already external).
- Sandbox pool continues unchanged — sessions lease sandboxes as today.
Impact
| Metric |
Current (1:1) |
Multiplexed (N:1) |
| Pods for 10K concurrent |
10,000 |
100-200 |
| Activation latency |
1.5-3.4s (cold start) |
~10-50ms (Redis session load) |
| Resource for 10K concurrent |
~20,000 CPU |
~400 CPU |
This is the single largest density lever available — roughly 100× improvement in pod count and resource footprint.
Scope
Related
Problem
The harness runs one session per process (KEDA ScaledJob model). At scale this means 1 pod per active session — 10,000 concurrent sessions = 10,000 pods. The harness spends 80-90% of wall-clock time idle, waiting for LLM responses and tool calls, yet each session exclusively owns a full pod.
Root cause
Pi was designed as a single-user CLI agent.
runTurnis not concurrent-safe — five categories of global mutable state prevent multiple sessions from sharing a process:process.env.ANTHROPIC_API_KEYmutation per callharness/src/run-turn.tsstdoutTakeoverState— globalprocess.stdout.writereplacementpi-fork: coding-agent/src/core/output-guard.tssessionResourceCleanups— global Set,cleanupSessionResources()fires ALLpi-fork: ai/src/session-resources.tsfileMutationQueues— global serialization queuepi-fork: coding-agent/src/core/tools/file-mutation-queue.tscommandResultCache— shell results cached for process lifetimepi-fork: coding-agent/src/core/resolve-config-value.ts4 of 5 are in pi-fork, 1 is in the harness.
Proposed fix
Each fix is mechanical — replace module-level globals with a per-session context object threaded through the call chain:
process.envmutation → set credentials once at startup (or accept as function args, not env). This is the harness-side fix.stdoutTakeoverState→ remove entirely (server mode doesn't need stdout capture) or scope per session.sessionResourceCleanups→ key the Set by session ID.fileMutationQueues→ scope per session (each session gets its own queue).commandResultCache→ scope per session or add TTL.Target architecture
Impact
This is the single largest density lever available — roughly 100× improvement in pod count and resource footprint.
Scope
SessionContexttype threading session ID + credentials + scoped stateRelated