pi-app-server
v2.0.0
Published
Session multiplexer for pi-coding-agent
Maintainers
Readme
pi-server
Session multiplexer for pi-coding-agent. Exposes N independent AgentSession instances through WebSocket and stdio transports.
Note: This is a standalone pi server package, not an extension/skills/themes bundle.
Features
- Dual transport: WebSocket (port 3141) + stdio (JSON lines)
- Session lifecycle: Create, delete, list, switch sessions
- Command execution: Deterministic lane serialization per session
- Idempotent replay: Atomic outcome storage with free replay lookups
- Optimistic concurrency: Session versioning for conflict detection
- Extension UI: Full round-trip support for
select,confirm,input,editor,interview - Resource governance: Rate limiting, session limits, message size limits
- Graceful shutdown: Drain in-flight commands, notify clients
- Protocol versioning:
serverVersion+protocolVersionfor compatibility checks
Installation
npm install pi-app-serverQuick Start
WebSocket
# Start server
npx pi-server
# Connect with wscat
wscat -c ws://localhost:3141// Create and use a session
ws> {"type":"create_session","sessionId":"my-session"}
ws> {"type":"switch_session","sessionId":"my-session"}
ws> {"id":"cmd-1","type":"prompt","sessionId":"my-session","message":"Hello!"}stdio
echo '{"type":"create_session","sessionId":"test"}
{"type":"switch_session","sessionId":"test"}
{"id":"cmd-1","type":"prompt","sessionId":"test","message":"Hello!"}' | npx pi-serverArchitecture
src/
├── server.ts # transports, connection lifecycle, routing glue
├── session-manager.ts # orchestration: coordinates stores, engines, sessions
├── command-router.ts # session command handlers, routing
├── command-classification.ts # pure command classification (timeout, mutation)
├── command-replay-store.ts # idempotency, duplicate detection, outcome history
├── session-version-store.ts # monotonic version counters per session
├── command-execution-engine.ts # lane serialization, dependency waits, timeouts
├── resource-governor.ts # limits, rate controls, health/metrics
├── extension-ui.ts # pending UI request tracking
├── server-ui-context.ts # ExtensionUIContext for remote clients
├── validation.ts # command validation
└── types.ts # wire protocol types + SessionResolver interfaceCore invariants
- For each admitted command, there is exactly one terminal response.
- For each session ID, there is at most one live
AgentSession. - Subscriber session sets are always a subset of active sessions.
- Session version is monotonic and mutation-sensitive.
- Fingerprint excludes retry identity (
id,idempotencyKey) for semantic equivalence.
Key abstractions
SessionResolver— Interface for session access (enables test doubles, future clustering)CommandReplayStore— Idempotency and duplicate detectionSessionVersionStore— Optimistic concurrency via version countersCommandExecutionEngine— Deterministic lane serialization and timeout management
Protocol
See PROTOCOL.md for the normative wire contract.
Command → Response
Every command receives exactly one response:
{"id": "cmd-1", "type": "prompt", "sessionId": "s1", "message": "hello"}
{"id": "cmd-1", "type": "response", "command": "prompt", "success": true}Event Broadcast
Events flow session → subscribers:
{"type": "event", "sessionId": "s1", "event": {"type": "agent_start", ...}}Extension UI Round-Trip
- Extension calls
ui.select()→ server creates pending request - Server broadcasts
extension_ui_requestevent withrequestId - Client sends
extension_ui_responsecommand with samerequestId - Server resolves pending promise → extension continues
Idempotency & Replay
// First request with idempotency key
{"id": "cmd-1", "type": "list_sessions", "idempotencyKey": "key-1"}
{"id": "cmd-1", "type": "response", "command": "list_sessions", "success": true, ...}
// Retry with same key → replayed (free, no rate limit charge)
{"id": "cmd-2", "type": "list_sessions", "idempotencyKey": "key-1"}
{"id": "cmd-2", "type": "response", "command": "list_sessions", "success": true, "replayed": true, ...}Timeout semantics (ADR-0001)
- Timeout is a terminal stored outcome (
timedOut: true), not an indeterminate placeholder. - Replay of the same command identity returns the same timeout response.
- Late underlying completion does not overwrite the stored timeout outcome.
Development
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test # Unit tests (83)
npm run test:integration # Integration tests (26)
npm run test:fuzz # Fuzz tests (17)
# Module tests (141)
node --experimental-vm-modules dist/test-command-classification.js
node --experimental-vm-modules dist/test-session-version-store.js
node --experimental-vm-modules dist/test-command-replay-store.js
node --experimental-vm-modules dist/test-command-execution-engine.js
# Type check + lint
npm run check
# Full CI
npm run ciRelease Process
This project uses release-please for automated versioning.
Automated Flow
- Push to
main→ release-please creates/updates a release PR - Merge the release PR → Creates GitHub release + git tag
- Release published → GitHub Action publishes to npm with provenance
Manual Release Check
npm run release:checkThis validates:
package.jsonhas required fieldsdist/exists with compiled files- Entry point has correct shebang
npm packproduces expected files- Full CI passes
Documentation
| Document | Purpose | |----------|---------| | AGENTS.md | Crystallized learnings, patterns, anti-patterns | | PROTOCOL.md | Normative wire contract | | ADR-0001 | Atomic outcome storage (timeout semantics) | | ADR-0007 | Session persistence | | ADR-0009 | Connection authentication (planned) | | ADR-0010 | Circuit breaker for LLM calls | | ROADMAP.md | Phase tracking and milestones |
License
MIT
