Skip to content

feat: multi-session harness multiplexing — N concurrent sessions per process #220

Description

@cwiklik

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:

  1. process.env mutation → set credentials once at startup (or accept as function args, not env). This is the harness-side fix.
  2. stdoutTakeoverState → remove entirely (server mode doesn't need stdout capture) or scope per session.
  3. sessionResourceCleanups → key the Set by session ID.
  4. fileMutationQueues → scope per session (each session gets its own queue).
  5. 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

  • Introduce a SessionContext type threading session ID + credentials + scoped state
  • Fix the 5 global state categories listed above
  • Add a concurrent session smoke test (2+ sessions interleaved on one process)
  • Validate no cross-session state leakage under load
  • Update deployment model from KEDA ScaledJob to elastic pod pool

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions