import type { SessionManager } from "@earendil-works/pi-coding-agent"; import type { MessageConnection } from "vscode-jsonrpc"; import type WebSocket from "ws"; import type { WebSocketServer } from "ws"; import type { CharacterCard } from "../config/character-card.js"; import type { MessageTemplateKey } from "../config/message-templates.js"; import type { BoardStore } from "../data/board-store.js"; import type { ActiveGroupChatDescriptor } from "../data/discovery/active-descriptor.js"; import type { GroupChatState } from "../data/group-chat-state.js"; import type { ServerMessage } from "../protocol/messages.js"; import type { PublicMessageState } from "../protocol/public-message-state.js"; import type { WhisperMessageState } from "../protocol/whisper-message-state.js"; import type { WebSocketMessageReader, WebSocketMessageWriter } from "../protocol/ws-message-io.js"; /** 私有 globalThis 键,让 reload 后的扩展代码能找到槽位。 */ const RELOAD_HANDOFF_SYMBOL: unique symbol = Symbol.for("pi-tavern.reload-handoff"); export interface BufferedFrame { receivedAt: number; data: WebSocket.RawData; } /** * connection 延续三件套:JSON-RPC 连接实例跨 owner 移交 * (JoinAttempt → CharacterRuntime → reload 后新 runtime)——连接不重建 = * 库内序列单调,代际 id 不撞车(旧代际迟到响应不可能命中新请求)。 */ export interface CharacterJsonRpcTransfer { connection: MessageConnection; reader: WebSocketMessageReader; writer: WebSocketMessageWriter; } interface HeartbeatStateSnapshot { lastPongAt: number; } /** * 从旧 Extension Runtime 移交到 reload 后新 runtime 的资源。 * 一次性:take() 恰好成功一次;5s 过期后由 handoff 自身的 cleanup * 释放全部资源。 */ export interface CreatorReloadHandoff { kind: "creator"; piSessionId: string; expiresAt: number; webSocketServer: WebSocketServer; groupSessionManager: SessionManager; groupChatState: GroupChatState; /** 白板模型:store 实例随 handoff 传递(reload 进程内,缓存存活)。 */ boardStore: BoardStore; connections: Map; heartbeatStates: Map; activeDescriptor: ActiveGroupChatDescriptor; activeDescriptorPath: string; configMaxMessages: number; /** :欢迎文案(配置快照随 handoff 传递,reload 后 ready 行为一致)。 */ welcomeMessage: string; /** :群聊文案模板集(配置快照随 handoff 传递,reload 后渲染一致)。 */ messageTemplates: Record; characters: CharacterCard[]; publicMessages: PublicMessageState[]; /** :私信消息流快照(reload handoff 传递,与 publicMessages 同源)。 */ whisperMessages: WhisperMessageState[]; persistedCount: number; bufferedFrames: Map; bufferingHandlers: Map void; close: () => void }>; closedSessionIds: Set; /** 释放服务端、成员 socket 与活跃描述符。 */ cleanup: () => Promise; } export interface CharacterReloadHandoff { kind: "character"; piSessionId: string; expiresAt: number; groupChatId: string; socket: WebSocket; character: CharacterCard; /** (/):跨 reload 携带的游标文件路径。 */ cursorStorePath?: string; /** :增量拉取上下文窗口 getter(reload 移交后与 join 路径行为一致)。 */ getFetchContextWindow?: () => number; /** T5:群聊文案模板集快照(reload 移交后渲染一致)。 */ messageTemplates?: Record; /** 复评:reload 时重新加载磁盘配置所需路径(有则 takeHandoff 重载,失败保留快照)。 */ agentDir?: string; cwd?: string; pendingEvents: ServerMessage[]; debounceDueAt: number | null; /** 可选仅为兼容这些字段加入前创建的跨版本 reload handoff。 */ idleWindowDueAt?: number | null | undefined; idleWindowAbortEligible?: boolean | undefined; incrementPending?: boolean | undefined; lastPingAt: number; /** connection 延续:旧 runtime 的 JSON-RPC 连接实例(可选:兼容旧态 handoff)。 */ jsonrpc?: CharacterJsonRpcTransfer; bufferedFrames: BufferedFrame[]; bufferingHandlers: { message: (data: WebSocket.RawData) => void; close: () => void }; socketClosed: boolean; /** 关闭 socket 并丢弃未冲刷的挂起事件。 */ cleanup: () => Promise; } type ReloadHandoff = CreatorReloadHandoff | CharacterReloadHandoff; class ReloadHandoffRegistry { private handoff: ReloadHandoff | null = null; private expireTimer: NodeJS.Timeout | null = null; /** 发布交接;之前未被取走的交接先过期。 */ publish(handoff: ReloadHandoff): void { this.clearExpireTimer(); const previous = this.handoff; this.handoff = handoff; if (previous) { void previous.cleanup(); } const delay = Math.max(0, handoff.expiresAt - Date.now()); this.expireTimer = setTimeout(() => this.expire(), delay); this.expireTimer.unref?.(); } /** * 取走交接。只有同一 pi session 可以取;session 不匹配时返回 null, * 槽位留给真正的归属者。 */ take(piSessionId: string): ReloadHandoff | null { const handoff = this.handoff; if (!handoff || handoff.piSessionId !== piSessionId) { return null; } this.clearExpireTimer(); this.handoff = null; return handoff; } /** 若仍有交接则使其过期(测试也用)。 */ expireNow(): Promise { this.clearExpireTimer(); const handoff = this.handoff; this.handoff = null; if (handoff) { return handoff.cleanup(); } return Promise.resolve(); } private expire(): void { this.expireTimer = null; const handoff = this.handoff; this.handoff = null; if (handoff) { void handoff.cleanup(); } } private clearExpireTimer(): void { if (this.expireTimer) { clearTimeout(this.expireTimer); this.expireTimer = null; } } } /** * 以 Symbol.for 为键的进程级注册表:reload 后的扩展代码(全新模块实例) * 能找到旧 runtime 发布的槽位。 */ export function getReloadHandoffRegistry(): ReloadHandoffRegistry { const holder = globalThis as Record; const existing = holder[RELOAD_HANDOFF_SYMBOL]; if (existing) { return existing; } const registry = new ReloadHandoffRegistry(); holder[RELOAD_HANDOFF_SYMBOL] = registry; return registry; }