pi-mono/packages/server/README.md
Mario Zechner 48dd1e2f0f feat(coding-agent): port the experimental client/server to pi-durable
Session workers now host a durable Harness over a per-session sqlite storage.
The server keeps each session in <sessionDir>/<id>/ with meta.json and
session.sqlite and never opens the storage itself. Workers hold retirement
while the Harness task graph has live tasks and resume interrupted work on
open.

Transcript serves the root conversation's durable view directly, Models
follows the conversation's agent document, and AgentController maps prompt,
steer, follow-up, queue cancellation, abort, and compaction onto the root
conversation, plus waitForPrompt. navigate, nextRun, and resume are dropped
(see experimental/services/README.md TODO). The durable TUI and the worker
share harness-setup.ts.

pi-server exports its own SessionMetadata and no longer depends on
pi-agent-core. Chord accepts readonly JSON types in service contracts without
walking them, which recursive JSON types like pi-ai's made too deep.
2026-10-01 14:45:01 +02:00

4.8 KiB

@earendil-works/pi-server

Experimental local server that routes clients to application-hosted durable Sessions.

The current slice supports server- and Session-scoped facet-service routing and multi-presentation attachment. RoutedServerServiceHost.attachClient() creates one connection-scoped server service endpoint with narrow attachment-management capabilities. RoutedSessionHandle.attachClient() returns a presentation-scoped Session capability. Its invokeService() forwards an opaque service/member envelope to the selected Session endpoint; the server validates the attachment route but does not load the facet contract.

  • server service calls and subscriptions route opaquely through the connection's RoutedServerServiceAttachment;
  • the application-owned SessionDirectory projects the private catalog into replicated presentation-safe state;
  • the application-owned SessionManagement creates, removes, attaches, and detaches Sessions without exposing route IDs in business results;
  • attachment changes are published out of band after the router installs or clears the live route;
  • Session service calls route through invokeService without server-side business-payload decoding;
  • service subscription updates remain scoped to the requesting attachment;
  • application observations such as transcripts route as ordinary service state without server-owned business schemas.

A Session may have multiple presentation attachments. Repeating attach from one connection is idempotent; every successful attachment has a server-generated attachmentId delivered only as routing control data. Session requests carry { serverId, sessionId, attachmentId }, and the server rejects stale or mismatched routes. Losing a connection rejects its local responses but releases its attachment only after admitted service calls settle. The host decides when zero presentation demand and worker-local Harness activity permit worker retirement. Server shutdown closes every routed Session handle, releasing its worker and Session writer ownership.

import { randomUUID } from "node:crypto";
import {
  type RoutedServerServiceHost,
  type RoutedSessionHandle,
  type ServerHost,
  type SessionMetadata,
  SessionNotFoundError,
} from "@earendil-works/pi-server";
import { createUnixServer, getUnixSocketPath } from "@earendil-works/pi-server/unix";

interface StoredSession extends SessionMetadata {
  path: string;
}

async function startServer(
  serverServices: RoutedServerServiceHost,
  sessions: Map<string, StoredSession>,
  openRoutedSession: (session: StoredSession) => Promise<RoutedSessionHandle>,
) {
  const host: ServerHost<StoredSession> = {
    serverServices,
    async resolveSession(sessionId) {
      const metadata = sessions.get(sessionId);
      if (!metadata) throw new SessionNotFoundError(`Unknown session: ${sessionId}`);
      return metadata;
    },
    openSession: (metadata) => openRoutedSession(metadata),
  };

  const serverId = randomUUID();
  const server = createUnixServer(host, {
    serverId,
    path: getUnixSocketPath(serverId, "/run/user/1000/pi"),
  });
  await server.start();
  return server;
}

Applications supply a required server service host, a bounded Session resolver, and a routed Session factory. SessionMetadata only requires an id; applications may extend it with their own storage fields. Session discovery and management are application-owned services; the protocol server only asks the resolver for metadata when routing an attachment. The host owns acquiring the worker-local Session and Harness. Failures are cleaned up in that worker. Neither an open JavaScript Session nor a Harness crosses the process boundary.

serverId is a logical identity supplied by the launcher, not a socket address. The Unix preset requires an explicit physical path; getUnixSocketPath() derives one from a caller-selected directory. Choose a short, private runtime directory rather than deriving the route from an unbounded home-directory path. A long-lived launcher can reuse the same ID and path when replacing a server process.

Server composes transports through ServerListener; peer authentication remains application policy and is not implemented by the experimental Unix transport. The Unix submodule provides createUnixListener() and createUnixServer(). Low-level routed-envelope validation, CBOR, and framing come from @earendil-works/pi-protocol; Chord owns service-control parsing, error codes, snapshots and updates, and each subscription's replicated-state encoder.

Server and worker lifecycle is managed outside the public Pi protocol. The replaceable application server converts connection attachments into private demand updates; the worker combines generation-tagged demand with authoritative Harness activity. The experimental coordinator only supplies stable routing and reports generic server-generation connection changes.