v1.1.0Pi v1.1.0 源码分析Analysis of Pi v1.1.0 · 2026-10-07

serverserver

实验性的 pi 服务端experimental server package for pi

Run modesInfraRun modes

← → 翻页 · F 全屏 · O 目录 · T 明暗 · L 语言← → navigate · F fullscreen · O overview · T theme · L language

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.001
01 · Server、SessionRouter 与可替换的监听器01 · Server, SessionRouter and pluggable listenersRun modes

Server、SessionRouter 与可替换的监听器Server, SessionRouter and pluggable listeners

pi-server: 1,958 lines — handshake, route-based dispatch, attachments to the app's Sessionspackages/server/srctransports/unix → "./unix"testing/ → "./testing"依赖与使用者Dependencies and users4index.ts根入口 ".":export * 自 errors、listener、server、typesroot ".": export * from errors, listener, server, types576server.tsServer:连接、握手、请求分派Server: connections, handshake, dispatchtoProtocolError · failProtocol312session-router.tsSessionRouter:Session 与附着(内部)SessionRouter: Sessions, attachments (internal)runForClient · releaseAttachment68types.tsServerHost 契约ServerHost contract57errors.ts5 个错误子类5 error subclasses8listener.tsServerListener37connection.tsByteConnection(内部)ByteConnection (internal)476createUnixServergetUnixSocketPath · createUnixListener绑定、发布、身份校验bind, publish, identity checks420createTestServerTestServerHost · TestHarnessProtocolTestClient · Deferred@earendil-works/chord ^1.1.0服务控制解码、状态编码器、Context 与取消service-control decoding, state encoder, Context and abort@earendil-works/pi-protocol ^1.1.0ClientMessageDecoder · encodeServerMessage · isServerIdserver.tscreateUnixServer:~/.pi/server/<id>.sockcreateUnixServer: ~/.pi/server/<id>.sockservices/server.tsRoutedServerServiceHost:服务器级服务RoutedServerServiceHost: server-wide servicessession-worker-manager.tsRoutedSessionHandle:每个 Session 一个 workerRoutedSessionHandle: one worker per Sessionradius-relay.ts中继来的连接交给 server.accept()relayed connections go to server.accept()依赖Depends on使用者:coding-agent 的 experimental/(4 个文件导入它)Used by: coding-agent experimental/ (4 importing files)目录可用 PI_SERVER_DIR 覆盖。PI_SERVER_DIR overrides the directory.Server 本身不懂任何服务契约,也不保存 Session:它只核对路由,把调用交给应用通过 ServerHost 提供的能力。The Server knows no service contract and stores no Session: it checks routes and hands calls to capabilities the app supplies through ServerHost.标签 = 行数tag = lines核心coreSession 路由Session routingUnix / 使用方Unix / users测试与依赖testing & deps

监听器负责传输层的认证;实验性的 Unix 监听器不做认证,只依靠文件权限。Listeners own transport-level authentication; the experimental Unix listener does none and relies on file permissions.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.002
02 · ServerHost:应用提供的能力02 · ServerHost: capabilities the app providesRun modes

ServerHost:应用提供的能力ServerHost: capabilities the app provides

ServerHost: every capability the app hands to the Server«app»ServerHost<TMetadata>+serverServices: RoutedServerServiceHost+resolveSession(sessionId, ctx): TMetadata+openSession(metadata, ctx): RoutedSessionHandle«app»RoutedServerServiceHost+attachClient(presentation, ctx)«per conn»RoutedServerServiceAttachment+invokeService(call, publish, ctx)+release(ctx)«per Session»RoutedSessionHandle+attachClient(ctx): RoutedSessionAttachment+terminated?: Promise<Error | undefined>+close(ctx): Promise<void>«per attach»RoutedSessionAttachment+invokeService(call, publish, ctx)+release(ctx)«from Server»RoutedServerPresentation+attachSession(sessionId, ctx)+detachSession(ctx)+prepareSessionRemoval(sessionId, ctx)serverServices握手时at handshakeopenSession()每次 attachper attach作为参数passed in握手时:At handshake: serverServices.attachClient(presentation) 为每个连接建一个服务器级端点;连接断开时 release()。serverServices.attachClient(presentation) builds one server-wide endpoint per connection; release() when it drops.attach 时:On attach: resolveSession → openSession(同一 Session 只开一次)→ handle.attachClient() 为这次附着取得一个租约。resolveSession → openSession (once per Session) → handle.attachClient() takes a lease for this attachment.terminated:terminated: 意外退出时以错误 resolve,正常关闭时以 undefined resolve;Server 随即让该 Session 的所有附着失效。resolves with an error on unexpected exit, undefined after a normal close; the Server then invalidates every attachment.publish:publish: invokeService 的第二个参数;订阅的更新经它回到 Server,再编码成 service_update。invokeService's second argument; subscription updates come back through it and become service_update.coding-agent:serverServices 来自 services/server.ts;openSession 交给 SessionWorkerManager,每个 Session 跑在自己的 worker 进程里。coding-agent: serverServices comes from services/server.ts; openSession goes to SessionWorkerManager, each Session running in its own worker process.服务器级server-wideSession 级Session levelServer 提供给应用Server → app返回returns参数argument

服务器级端点拿到一个 RoutedServerPresentation:attachSession、detachSession、prepareSessionRemoval——应用的管理服务就是用它们 attach 的。The server-wide endpoint receives a RoutedServerPresentation: attachSession, detachSession, prepareSessionRemoval; the app's management service attaches through these.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.003
03 · 连接的五个阶段03 · Five connection stagesRun modes

连接的五个阶段Five connection stages

Five stages of a server connection: the handshake must finish within 5 sfinishHandshake(state, hello)server.tsfailProtocol(state, error)server.tsdisconnect(state):只做一次disconnect(state): runs onceawaitingHellohandshakingreadyclosingclosed超时 5 s · 首条不是 hello · 坏帧timeout 5 s · first is not hello · bad frame传输 onClose / onErrortransport onClose / onError收到 hellogot hellohello 已发出hello sent版本检查Version check不支持 → hello_error { code: "version" }unsupported → hello_error { code: "version" }serverServices.attachClient(presentation)presentation = { attachSession, detachSession, prepareSessionRemoval }再检查一次Check again期间关闭或断开 → release() 刚得到的端点,就此结束closed or dropped meanwhile → release() the new endpoint and stopsend hello { version, serverId }发送成功 → stage = ready,清掉握手定时器sent → stage = ready, clear the handshake timer握手期间到达的请求挂在 state.handshake 之后,ready 时才处理,不会丢。Requests arriving during the handshake wait on state.handshake and run once ready: none are lost.stage = closing;把 hello_error { error } 编码成最后一帧,stage = closing; encode hello_error { error } as the final frame,connection.close(finalFrame) 发完它再关闭,然后 disconnect。connection.close(finalFrame) sends it, then closes; then disconnect.握手之后的协议错误(例如第二个 hello)也走这条路。Protocol errors after the handshake (a second hello) take this path too.ProtocolValidationError → invalid_request。ProtocolValidationError → invalid_request.stage = closed,清掉握手定时器stage = closed, clear the handshake timerabort 所有进行中的请求:"Client disconnected"abort every active request: "Client disconnected"清空订阅编码器;连接数变化 → onConnectionCountChangedclear subscription encoders; onConnectionCountChangedsessions.disconnect() 与 serverServices.release() 并行sessions.disconnect() and serverServices.release() in parallelServer.close():先关所有监听器,再把每个连接置为 closing 并关闭,最后 SessionRouter.close() 关闭所有托管的 Session。Server.close(): close every listener, set each connection to closing and close it, then SessionRouter.close() closes every hosted Session.12341234前进advance协议失败protocol failure可以处理请求serving requests清理cleanup

握手期间到达的请求挂在 state.handshake 之后,ready 时才处理,不会丢。Requests that arrive during the handshake wait on state.handshake and run once ready; none are lost.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.004
04 · 一个请求的处理04 · Handling one requestRun modes

一个请求的处理Handling one request

handleRequest: check, route to a Session or server-wide services, and send subscription updates after the responsehandleRequest(state, envelope)server.ts失败时On failuretoProtocolError(error)server.tshandleCancel(state, envelope)server.tsinvalid_requestinvalid_requestwrong_serverinvalid_requestactiveRequests.has(id)parseServiceCall(envelope.call)Chord 校验调用的形状Chord checks the call's shapeactiveRequests.set(id, { controller, target })context = withAbortSignal(controller.signal)target.serverId !== this.serverId订阅 id 已有编码器subscription id already has an encoder路由Route"sessionId" in target ? sessions.executeServiceCall : serverServices.invokeService订阅:必须返回快照Subscribe: a snapshot is requiredencodeSnapshot,安装 ServiceStateEncoderencodeSnapshot, install a ServiceStateEncoder发送回应,再冲刷 pendingUpdatesSend the response, then flush pendingUpdates之后的更新直接编码成 service_updatelater updates go straight out as service_updatefinally:从 activeRequests 删掉自己(只删自己那一项)。finally: remove its own entry from activeRequests.还没回应:Not yet responded:response { ok: false, error };已 abort → cancelled,否则 toProtocolErrorresponse { ok: false, error }; aborted → cancelled, else toProtocolError已经回应(冲刷更新时出错):Already responded (failed while flushing):onError,然后关闭这条连接——订阅流不能半途断掉onError, then close the connection: a stream must not break halfway安装了编码器却没回应成功 → 移除编码器。Encoder installed but no response → the encoder is removed.退订成功 → 删掉对应的编码器,之后的更新被丢弃。A successful unsubscribe deletes the encoder; later updates are dropped.ServerError · RemoteServiceError原样:code + messageas is: code + messageProtocolValidationErrorinvalid_request + message其它任何错误anything elseinternal_error "Internal server error" + onErrorserverId 不是本服务器 → 忽略;id 与 target 都相同 →serverId of another server → ignored; same id and target →controller.abort(AbortError);处理器看到 abort 后回应 cancelled。controller.abort(AbortError); the handler then answers cancelled.取消从不单独回应:它只影响原请求的回应。A cancel never gets its own response; it only shapes the original one.同一连接上的请求并发处理;只有 attach / detach 与 Session 调用的“准入”按连接串行(见 SessionRouter)。Requests on one connection run concurrently; only attach / detach and the admission of Session calls are serialised per connection (see SessionRouter).12345678请求路径request path错误回应error responses错误映射error mapping取消cancellation

错误回应用 toProtocolError() 映射:ServerError / RemoteServiceError 原样,ProtocolValidationError → invalid_request,其它 → internal_error。Error responses are mapped by toProtocolError(): ServerError / RemoteServiceError as is, ProtocolValidationError → invalid_request, anything else → internal_error.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.005
05 · SessionRouter:attach 与准入05 · SessionRouter: attach and admissionRun modes

SessionRouter:attach 与准入SessionRouter: attach and admission

SessionRouter: attach serialised per connection, every Session call checked against its attachmentacquire → open(sessionId)session-router.tsexecuteServiceCall(call, target, client, …)session-router.tsattachClient(client, sessionId)正在关闭 → ServerDrainingErrorclosing → ServerDrainingErrorrunForClient(client, op)按连接排队:前一个操作结束后才开始queued per connection: starts after the previous op已附着同一个 Session?Already attached to this Session?是 → 直接返回(幂等)yes → return at once (idempotent)acquire(sessionId)已托管 → 正在打开 → open()hosted → opening → open()释放当前附着Release the current attachmentpublish = false:新路由马上会推送publish = false: the new route followshandle.attachClient(ctx)新附着 id = randomUUID(),拿到租约new attachment id = randomUUID(), take the leasepublishAttachment(client, target)attachment { serverId, sessionId, attachmentId }session_not_attachedserver_draininghostedSessionsopeningSessionsattachmentsByClientclientOperations拿到租约后再查一次:Session 已失效、连接已断或服务器在关闭 → 释放租约并 ServerDrainingError。After the lease, check again: Session invalidated, client gone or server closing → release it and ServerDrainingError.host.resolveSession(sessionId)得到元数据;找不到 / 有歧义由应用抛 SessionNotFound / Ambiguousmetadata; the app throws SessionNotFound / Ambiguoushost.openSession(metadata)得到 RoutedSessionHandle;期间开始关闭 → handle.close() 并拒绝a RoutedSessionHandle; closing meanwhile → handle.close() and rejecthostedSessions.set(metadata.id, …)并监听 handle.terminated → invalidate()and watch handle.terminated → invalidate()openingSessions 让并发的 attach 共享同一次打开;打开失败时所有人一起失败,之后可以重试。openingSessions lets concurrent attaches share one open; a failure fails them all, and a later retry may succeed.runForClient → requireAttachmentsessionId 与 attachmentId 都要等于本连接的当前附着sessionId and attachmentId must equal this connection's attachmentlease.invokeService(call, publish, ctx)只在队列里“准入”;结果在队列外等待,所以调用可以并发only admission is queued; the result is awaited outside, so calls run concurrentlytrackOperation(attachment, result)释放附着前会等这些调用结算release waits for these calls to settle不匹配、过期或服务器级 target → session_not_attached;关闭中或连接已断 → server_draining。mismatched, stale or server-level target → session_not_attached; closing or client gone → server_draining.内部状态Internal state123123排队queueingSession 与租约Session and lease路由route拒绝rejections

只有准入排队;调用结果在队列外等待,所以同一 Session 上的调用可以并发。Only admission is queued; results are awaited outside the queue, so calls on one Session run concurrently.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.006
06 · 附着的结束06 · How attachments endRun modes

附着的结束How attachments end

Six ways an attachment ends, all through one release path触发TriggersreleaseAttachment(attachment)之后:Session 本身Afterwards: the Session itself关闭服务器时On shutdownSessionRouter.close()detachSession()推送 attachment: nullpushes attachment: nullattach 另一个 Sessionattach another Session不推送 null,随后推送新路由no null; the new route follows连接断开connection lost不推送(已经没有连接)no push (no connection left)handle.terminatedinvalidate:所有附着,推送 nullinvalidate: every attachment, push nullprepareSessionRemoval()所有附着 + handle.close()every attachment + handle.close()Server.close()全部附着 + 全部 handle.close()all attachments + every handle.close()releasing ??= …只释放一次;并发调用共享这个 Promisereleased once; concurrent callers share the promiseallSettled(attachment.operations)先等已准入的调用结算wait for admitted calls to settlelease.release(ctx)租约仍在获取中 → 先等 acquiringstill acquiring → await it firstclearAttachment(…, publish)从 Session 与连接上摘掉;publish 时推送 nulldetach from Session and client; null if publishrelease 失败也一定 clearAttachment(finally)。clearAttachment runs even if release fails (finally).断开 / detach / 切换drop / detach / switchSession 仍被托管;是否退役由应用决定stays hosted; the app decides on retirementterminated从 hostedSessions 删掉;下次 attach 重新打开removed from hostedSessions; reopened on next attach删除 / 关闭服务器removal / shutdownhandle.close():释放 worker 与写入权handle.close(): frees the worker and writer等进行中的连接操作和打开wait for client ops and opens释放每个 Session 的每个附着release every attachment关闭每个 handleclose every handle有失败 → AggregateErrorfailures → AggregateErrorServer.closed 因此 reject。so Server.closed rejects.README:连接断开时,本地的回应立即拒绝,但附着要等已准入的服务调用结算后才释放。README: losing a connection rejects its local responses, but the attachment is released only after admitted service calls settle.1234连接触发connection triggersSession 失效Session failure释放与关闭release and close

连接断开只释放附着,Session 仍被托管;什么时候让 worker 退役由应用决定。A dropped connection only releases the attachment; the Session stays hosted, and the app decides when its worker retires.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.007
07 · Unix 监听器07 · The Unix listenerRun modes

Unix 监听器The Unix listener

The Unix listener: bind privately, publish atomically with a hard link, remove only its own socketUnixListener.start(accept)transports/unix/listener.ts例子:README 的目录Example: the README's directoryremoveStaleSocket / isSocketLiveclose():只删自己的 socketclose(): remove only its own socketUnixByteConnectionmkdir(dir, { recursive, mode: 0o700 })私有目录a private directoryremoveStaleSocket(path) · (bindPath)不是 socket → 拒绝;还活着 → "already running"not a socket → refuse; alive → "already running"listen(bindPath)bind-<sha256(path) 前 8 位>,与公共路径同目录bind-<first 8 hex of sha256(path)>, same directorylstat(bindPath) → { dev, ino }记住自己 socket 的身份remember the identity of its own socketlink(bindPath, path)硬链接发布:原子,且目标已存在时失败publish by hard link: atomic, fails if the target existschmod(path, 0o600)只有属主能连接(Windows 上跳过)owner-only connections (skipped on Windows)unlink(bindPath)只留下公共路径only the public path remains任何一步失败:关闭 net.Server,删掉未发布的 bind 路径,再按身份清理。Any step fails: close the net.Server, remove the unpublished bind path, clean up by identity./run/user/1000/pi/bind-0842f0390f8fad5b-d9cb-469f-a165-70867728950e.sock// ⑤ link 之后两者是同一个 inode,⑦ 删掉 bind// ⑤ after link both are one inode; ⑦ drops bind// getUnixSocketPath(serverId, dir)bind-0842f039 = sha256("/run/user/1000/pi/<id>.sock") 的前 8 位(构建时计算)bind-0842f039 = first 8 hex of sha256 of the public path (computed at build time)连得上connects活着 → 拒绝启动alive → refuse to start1000 ms 没反应no answer in 1000 ms也当作活着(宁可不删)treated as alive too (never delete)ECONNREFUSED · ENOENT · …陈旧:改名 stale-xxxxxx,核对 dev/ino 后才 unlinkstale: rename to stale-xxxxxx, unlink after a dev/ino check改名期间路径被换掉 → 换回去并报错。If the path changed meanwhile, put it back and fail.lstat(path) 的 dev/ino 等于记下的身份 → 改名 cleanup-xxxxxx → 再核对 → unlink;lstat(path) dev/ino equals the remembered identity → rename to cleanup-xxxxxx → recheck → unlink;否则保留(可能是接替它的新服务器的 socket)。otherwise leave it (it may belong to the server that replaced this one).maxPendingBytes 默认 4 × 帧上限,且必须 ≥ 帧上限 + 4;超过 → 断开慢的对端。maxPendingBytes: 4 × the frame limit by default, ≥ limit + 4; beyond → drop the slow peer.close(final):写完队列,end(final);5000 ms 后强制 destroy。close(final): drain the queue, end(final); force destroy after 5000 ms.README:对端认证是应用的策略,实验性的 Unix 传输不做认证——依靠 0700 目录与 0600 socket 的文件权限。README: peer authentication is application policy and not implemented by the experimental Unix transport; it relies on 0700 / 0600 file permissions.1234567

活性探测:连得上或 1000 ms 没反应都算“还活着”——宁可启动失败,也不删别人的 socket。Liveness probe: connecting, or no answer within 1000 ms, both count as alive; better to fail start-up than delete another server's socket.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.008
08 · 错误码与测试工具08 · Error codes and the testing kitRun modes

错误码与测试工具Error codes and the testing kit

Errors that may cross the protocol boundary, and the ./testing kitServerError 的子类(errors.ts)ServerError subclasses (errors.ts)./testing:测试工具./testing: the testing kitsrc/testingwrong_serversession_not_foundsession_ambiguoussession_not_attachedserver_drainingconformance · 20protocol · 6server · 6unix · 6listener · 2unix-connection · 1类Classcode在哪里产生Where it arisesWrongServerError"Request was addressed to another server"handleRequest:在访问任何仓库之前handleRequest: before any repository accessSessionNotFoundError"Session was not found"应用的 resolveSession 抛出thrown by the app's resolveSessionSessionAmbiguousError"Session ID matches more than one session"应用的 resolveSession 抛出(一个 id 对应多个)thrown by the app's resolveSession (one id, several)SessionNotAttachedError"Session is not attached to this client"requireAttachment:路由不匹配或已过期requireAttachment: mismatched or stale routeServerDrainingError"Server is draining"关闭中 attach / 调用 / 删除,或连接已断attach / call / removal while closing, or client gone应用也可以直接 throw new ServerError(code, message):code 还可以是 Chord 的 8 个 RemoteServiceErrorCode 之一。Apps may also throw new ServerError(code, message); code may also be one of Chord's 8 RemoteServiceErrorCodes.其它任何异常 → internal_error "Internal server error",细节只交给 onError。Anything else → internal_error "Internal server error"; details go only to onError.createTestServer(options)未启动的 Server,默认 serverId …0001an unstarted Server, default serverId …0001TestServerHost内存 ServerHostan in-memory ServerHostTestHarness · createTestServerServices假的 Session 与服务器级服务fake Sessions and server-wide servicesProtocolTestClient按帧收发、断言的协议客户端a frame-level client for assertionsconnectUnixTestClient(path)经真实 Unix socket 连接connect over a real Unix socketDeferred<T>手动控制时序control timing by hand传输实现可以拿它跑同一套一致性测试。Transport implementations can run the same conformance tests.测试(共 41 个 test)Tests (41 test cases)conformance.test.ts 覆盖多附着、过期路由、跨服务器请求、未知 / 有歧义的 Session、Harness 终止与重开、关闭时的清理失败。conformance.test.ts covers multiple attachments, stale routes, cross-server requests, unknown / ambiguous Sessions, Harness termination and reopening, and cleanup failures on shutdown.错误errors测试testing

wrong_server 在访问任何仓库之前检查:发错服务器的请求不会触发 resolveSession。wrong_server is checked before any repository access: a request for another server never triggers resolveSession.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.009
扩展点Extension pointspi-coding-agent

server 的扩展点Where to plug into server

1
ServerHostServerHost

应用接入点:serverServices、resolveSession、openSession。The app's entry point: serverServices, resolveSession, openSession.

2
RoutedSessionHandle · RoutedSessionAttachmentRoutedSessionHandle · RoutedSessionAttachment

把 Session 放在任何地方:同进程、worker 进程或远端。Host a Session anywhere: in process, in a worker process, or remotely.

3
ServerListenerServerListener

接入新的传输:start(accept) 交出已认证的字节连接。Add a transport: start(accept) hands over authenticated byte connections.

4
ServerError(code, message)ServerError(code, message)

给客户端一个可识别的错误码和消息。Give clients a recognisable error code and message.

5
./testing./testing

用 createTestServer 与 ProtocolTestClient 测试你的宿主或传输。Test your host or transport with createTestServer and ProtocolTestClient.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.010
←
1 / 10