zcode-acp-server 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +101 -15
  2. package/README.zh-CN.md +68 -13
  3. package/dist/bin/hub.d.ts +16 -0
  4. package/dist/bin/hub.d.ts.map +1 -0
  5. package/dist/bin/hub.js +41 -0
  6. package/dist/bin/hub.js.map +1 -0
  7. package/dist/handlers/account.d.ts +43 -0
  8. package/dist/handlers/account.d.ts.map +1 -0
  9. package/dist/handlers/account.js +59 -0
  10. package/dist/handlers/account.js.map +1 -0
  11. package/dist/handlers/io.d.ts +20 -1
  12. package/dist/handlers/io.d.ts.map +1 -1
  13. package/dist/handlers/io.js +57 -2
  14. package/dist/handlers/io.js.map +1 -1
  15. package/dist/handlers/replay.d.ts +79 -0
  16. package/dist/handlers/replay.d.ts.map +1 -0
  17. package/dist/handlers/replay.js +256 -0
  18. package/dist/handlers/replay.js.map +1 -0
  19. package/dist/handlers/session.d.ts.map +1 -1
  20. package/dist/handlers/session.js +83 -68
  21. package/dist/handlers/session.js.map +1 -1
  22. package/dist/handlers/slash.d.ts +23 -1
  23. package/dist/handlers/slash.d.ts.map +1 -1
  24. package/dist/handlers/slash.js +67 -6
  25. package/dist/handlers/slash.js.map +1 -1
  26. package/dist/index.js +47 -17
  27. package/dist/index.js.map +1 -1
  28. package/dist/remote/broadcast.d.ts +47 -0
  29. package/dist/remote/broadcast.d.ts.map +1 -0
  30. package/dist/remote/broadcast.js +121 -0
  31. package/dist/remote/broadcast.js.map +1 -0
  32. package/dist/remote/config.d.ts +32 -0
  33. package/dist/remote/config.d.ts.map +1 -0
  34. package/dist/remote/config.js +65 -0
  35. package/dist/remote/config.js.map +1 -0
  36. package/dist/remote/endpoint.d.ts +43 -0
  37. package/dist/remote/endpoint.d.ts.map +1 -0
  38. package/dist/remote/endpoint.js +222 -0
  39. package/dist/remote/endpoint.js.map +1 -0
  40. package/dist/remote/hub-server.d.ts +41 -0
  41. package/dist/remote/hub-server.d.ts.map +1 -0
  42. package/dist/remote/hub-server.js +346 -0
  43. package/dist/remote/hub-server.js.map +1 -0
  44. package/dist/server.d.ts +62 -7
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +99 -11
  47. package/dist/server.js.map +1 -1
  48. package/dist/utils.d.ts +1 -1
  49. package/dist/utils.d.ts.map +1 -1
  50. package/dist/utils.js +17 -1
  51. package/dist/utils.js.map +1 -1
  52. package/docs/ARCHITECTURE.md +47 -15
  53. package/docs/BACKLOG.md +3 -1
  54. package/docs/DEVELOPMENT.md +26 -0
  55. package/docs/PROTOCOL.md +67 -27
  56. package/docs/REMOTE-CLIENTS.md +264 -0
  57. package/docs/REPLAY-GUIDE.md +131 -0
  58. package/docs/TROUBLESHOOTING.md +51 -6
  59. package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
  60. package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
  61. package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
  62. package/docs/proposals/0001-tail-session-replay.md +136 -0
  63. package/docs/proposals/0002-plan-quota-usage.md +81 -0
  64. package/package.json +5 -2
@@ -0,0 +1,346 @@
1
+ /**
2
+ * zcode-acp-hub — machine-level singleton for remote access.
3
+ *
4
+ * The hub is the ONLY public entry point (the port a tunnel maps). It does
5
+ * exactly three things (ADR-0002): token auth, instance discovery, and
6
+ * byte-level WebSocket proxying from a remote client to one bridge's loopback
7
+ * ACP endpoint. It holds no session state and understands no ACP — a proxied
8
+ * connection stays bound to one instance for its whole lifetime.
9
+ *
10
+ * Bridges register via POST /api/register every 10s (the registration doubles
11
+ * as the heartbeat; entries older than the heartbeat TTL are pruned). A client
12
+ * that needs an immediately-honest list (e.g. a phone app's pull-to-refresh)
13
+ * passes ?probe=1 to /api/instances: the hub TCP-probes each registered
14
+ * loopback port and prunes unreachable bridges before answering — no periodic
15
+ * probing, the cost is paid only when someone refreshes. When no instance is
16
+ * registered and no proxy is active for `idleExitMs`, the hub exits — the
17
+ * next bridge re-spawns it on demand.
18
+ */
19
+ import { createHash, timingSafeEqual } from "node:crypto";
20
+ import { createServer } from "node:http";
21
+ import net from "node:net";
22
+ import { WebSocket, WebSocketServer } from "ws";
23
+ import { AGENT_INFO, compareVersions, log, warn } from "../utils.js";
24
+ const HEARTBEAT_TIMEOUT_MS = 30_000;
25
+ const IDLE_EXIT_MS = 10 * 60_000;
26
+ const PING_INTERVAL_MS = 30_000;
27
+ /** Per-instance TCP probe timeout for /api/instances?probe=1. */
28
+ const PROBE_TIMEOUT_MS = 500;
29
+ const MAX_BODY_BYTES = 1024 * 1024;
30
+ /** Constant-time token compare (hash both to equal length first). */
31
+ function tokenEquals(a, b) {
32
+ const ha = createHash("sha256").update(a).digest();
33
+ const hb = createHash("sha256").update(b).digest();
34
+ return timingSafeEqual(ha, hb);
35
+ }
36
+ function authorized(req, url, token) {
37
+ const header = req.headers.authorization;
38
+ if (header?.startsWith("Bearer "))
39
+ return tokenEquals(header.slice(7), token);
40
+ const query = url.searchParams.get("token");
41
+ return query !== null && tokenEquals(query, token);
42
+ }
43
+ function setCors(res) {
44
+ // The web UI is deployed as a separate origin; the token is the boundary.
45
+ res.setHeader("Access-Control-Allow-Origin", "*");
46
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
47
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
48
+ }
49
+ async function readJson(req) {
50
+ const chunks = [];
51
+ let size = 0;
52
+ try {
53
+ // The async iterator rejects when the client aborts mid-body — a truncated
54
+ // POST must degrade to "invalid body", not reject into the event loop.
55
+ for await (const chunk of req) {
56
+ size += chunk.length;
57
+ if (size > MAX_BODY_BYTES)
58
+ return null;
59
+ chunks.push(chunk);
60
+ }
61
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
62
+ return typeof parsed === "object" && parsed !== null
63
+ ? parsed
64
+ : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function validSessions(raw) {
71
+ if (!Array.isArray(raw))
72
+ return null;
73
+ const out = [];
74
+ for (const s of raw) {
75
+ const rec = s;
76
+ if (typeof rec?.sessionId !== "string")
77
+ return null;
78
+ out.push({
79
+ sessionId: rec.sessionId,
80
+ ...(typeof rec.title === "string" ? { title: rec.title } : {}),
81
+ updatedAt: typeof rec.updatedAt === "number" ? rec.updatedAt : Date.now(),
82
+ });
83
+ }
84
+ return out;
85
+ }
86
+ /**
87
+ * TCP-probe a bridge's loopback endpoint. Loopback refusals are instant, so
88
+ * the timeout only guards pathological cases; a bare connect+destroy is
89
+ * harmless to the bridge's HTTP server.
90
+ */
91
+ function portOpen(port, timeoutMs) {
92
+ return new Promise((resolve) => {
93
+ const socket = new net.Socket();
94
+ const done = (ok) => {
95
+ socket.destroy();
96
+ resolve(ok);
97
+ };
98
+ socket.setTimeout(timeoutMs);
99
+ socket.once("connect", () => done(true));
100
+ socket.once("timeout", () => done(false));
101
+ socket.once("error", () => done(false));
102
+ socket.connect(port, "127.0.0.1");
103
+ });
104
+ }
105
+ /**
106
+ * Start the hub. Resolves once listening; rejects on bind failure (including
107
+ * EADDRINUSE when another hub already owns the port).
108
+ */
109
+ export function startHub(options) {
110
+ const { port, host, token, heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, idleExitMs = IDLE_EXIT_MS, pingIntervalMs = PING_INTERVAL_MS, onIdleExit, } = options;
111
+ const instances = new Map();
112
+ const proxyPairs = new Set();
113
+ const timers = [];
114
+ let idleSince = null;
115
+ const wss = new WebSocketServer({ noServer: true });
116
+ const server = createServer((req, res) => {
117
+ // Async handler failures (malformed URL, aborted body) must never escape
118
+ // into the event loop — warn and drop the connection.
119
+ void handleHttp(req, res).catch((e) => {
120
+ warn(`hub: request failed: ${e instanceof Error ? e.message : String(e)}`);
121
+ res.destroy();
122
+ });
123
+ });
124
+ server.on("upgrade", (req, socket, head) => {
125
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
126
+ if (url.pathname !== "/acp") {
127
+ socket.destroy();
128
+ return;
129
+ }
130
+ if (!authorized(req, url, token)) {
131
+ warn("hub: unauthorized WS upgrade rejected");
132
+ socket.destroy();
133
+ return;
134
+ }
135
+ const entry = instances.get(url.searchParams.get("instance") ?? "");
136
+ if (!entry) {
137
+ warn("hub: WS upgrade for unknown instance rejected");
138
+ socket.destroy();
139
+ return;
140
+ }
141
+ // Dial the bridge's loopback endpoint before accepting the client side,
142
+ // so a dead bridge fails the upgrade instead of half-opening a pipe.
143
+ const bridge = new WebSocket(`ws://127.0.0.1:${entry.port}/acp`);
144
+ bridge.once("open", () => {
145
+ wss.handleUpgrade(req, socket, head, (client) => startProxy(client, bridge));
146
+ });
147
+ bridge.once("error", (e) => {
148
+ warn(`hub: dial bridge :${entry.port} failed: ${e.message}`);
149
+ socket.destroy();
150
+ });
151
+ });
152
+ async function handleHttp(req, res) {
153
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
154
+ setCors(res);
155
+ if (req.method === "OPTIONS") {
156
+ res.writeHead(204);
157
+ res.end();
158
+ return;
159
+ }
160
+ if (url.pathname === "/api/health") {
161
+ res.writeHead(200, { "Content-Type": "text/plain" });
162
+ res.end("ok");
163
+ return;
164
+ }
165
+ if (url.pathname === "/api/instances" && req.method === "GET") {
166
+ if (!authorized(req, url, token)) {
167
+ res.writeHead(401, { "Content-Type": "text/plain" });
168
+ res.end("unauthorized");
169
+ return;
170
+ }
171
+ // On-demand liveness probe (?probe=1): verify every registered bridge's
172
+ // loopback port and prune the unreachable ones before answering, so a
173
+ // client refresh gets an honest list instead of waiting out the
174
+ // heartbeat TTL (hard-killed bridges never unregister).
175
+ if (["1", "true"].includes((url.searchParams.get("probe") ?? "").toLowerCase())) {
176
+ const probes = await Promise.all(Array.from(instances.entries(), async ([id, entry]) => ({
177
+ id,
178
+ ok: await portOpen(entry.port, PROBE_TIMEOUT_MS),
179
+ })));
180
+ for (const { id, ok } of probes) {
181
+ if (!ok) {
182
+ instances.delete(id);
183
+ idleSince = null; // re-arm the idle clock on membership change
184
+ log(`hub: pruned instance ${id} (probe: endpoint unreachable)`);
185
+ }
186
+ }
187
+ }
188
+ const list = Array.from(instances.values())
189
+ .sort((a, b) => a.startedAt - b.startedAt)
190
+ .map((e) => ({
191
+ id: e.id,
192
+ port: e.port,
193
+ pid: e.pid,
194
+ startedAt: e.startedAt,
195
+ workspace: e.workspace,
196
+ sessions: e.sessions,
197
+ }));
198
+ res.writeHead(200, { "Content-Type": "application/json" });
199
+ res.end(JSON.stringify(list));
200
+ return;
201
+ }
202
+ if ((url.pathname === "/api/register" || url.pathname === "/api/unregister") &&
203
+ req.method === "POST") {
204
+ const body = await readJson(req);
205
+ if (!body || typeof body.token !== "string" || !tokenEquals(body.token, token)) {
206
+ res.writeHead(401, { "Content-Type": "text/plain" });
207
+ res.end("unauthorized");
208
+ return;
209
+ }
210
+ const id = typeof body.id === "string" ? body.id : "";
211
+ if (!id) {
212
+ res.writeHead(400, { "Content-Type": "text/plain" });
213
+ res.end("missing id");
214
+ return;
215
+ }
216
+ if (url.pathname === "/api/register") {
217
+ const bridgePort = typeof body.port === "number" ? body.port : 0;
218
+ const sessions = validSessions(body.sessions);
219
+ if (!(bridgePort >= 1 && bridgePort <= 65535) || !sessions) {
220
+ res.writeHead(400, { "Content-Type": "text/plain" });
221
+ res.end("invalid register payload");
222
+ return;
223
+ }
224
+ const prev = instances.get(id);
225
+ instances.set(id, {
226
+ id,
227
+ port: bridgePort,
228
+ pid: typeof body.pid === "number" ? body.pid : 0,
229
+ startedAt: prev?.startedAt ?? Date.now(),
230
+ workspace: typeof body.workspace === "string" ? body.workspace : "",
231
+ sessions,
232
+ lastSeen: Date.now(),
233
+ });
234
+ }
235
+ else {
236
+ instances.delete(id);
237
+ idleSince = null; // re-arm the idle clock on membership change
238
+ }
239
+ // Version self-upgrade: a bridge NEWER than this hub just registered,
240
+ // so this process is running stale code. Reply first (the bridge
241
+ // re-spawns the hub from its own, newer dist when it sees `restarting`),
242
+ // then exit. Equal/older/absent versions never trigger a restart.
243
+ const stale = url.pathname === "/api/register" &&
244
+ typeof body.version === "string" &&
245
+ compareVersions(body.version, AGENT_INFO.version) > 0;
246
+ res.writeHead(200, { "Content-Type": "application/json" });
247
+ res.end(JSON.stringify(stale ? { ok: true, restarting: true } : { ok: true }));
248
+ if (stale) {
249
+ log(`hub: bridge ${body.version} is newer than hub ${AGENT_INFO.version} — restarting to upgrade`);
250
+ const restart = setTimeout(() => {
251
+ void close().finally(() => onIdleExit?.());
252
+ }, 500);
253
+ restart.unref();
254
+ }
255
+ return;
256
+ }
257
+ res.writeHead(404, { "Content-Type": "text/plain" });
258
+ res.end("not found");
259
+ }
260
+ function startProxy(client, bridge) {
261
+ const pair = { client, bridge };
262
+ proxyPairs.add(pair);
263
+ const teardown = () => {
264
+ if (!proxyPairs.delete(pair))
265
+ return;
266
+ client.close();
267
+ bridge.close();
268
+ };
269
+ // Forward with the frame's binary flag intact: `ws.send(buffer)` defaults
270
+ // to a BINARY frame, and the SDK's WS server drops non-text frames.
271
+ const forward = (to) => (data, isBinary) => {
272
+ if (to.readyState === WebSocket.OPEN)
273
+ to.send(data, { binary: isBinary });
274
+ };
275
+ client.on("message", forward(bridge));
276
+ bridge.on("message", forward(client));
277
+ client.on("close", teardown);
278
+ client.on("error", teardown);
279
+ bridge.on("close", teardown);
280
+ bridge.on("error", teardown);
281
+ }
282
+ // Prune instances whose bridge stopped heartbeating (crash or Zed exit).
283
+ const pruner = setInterval(() => {
284
+ const now = Date.now();
285
+ for (const [id, entry] of instances) {
286
+ if (now - entry.lastSeen > heartbeatTimeoutMs) {
287
+ instances.delete(id);
288
+ idleSince = null;
289
+ log(`hub: pruned instance ${id} (heartbeat timeout)`);
290
+ }
291
+ }
292
+ }, Math.min(heartbeatTimeoutMs / 2, 5_000));
293
+ pruner.unref();
294
+ timers.push(pruner);
295
+ // Keepalive pings on both legs — tunnels (notably Cloudflare) drop idle WS.
296
+ const pinger = setInterval(() => {
297
+ for (const { client, bridge } of proxyPairs) {
298
+ if (client.readyState === WebSocket.OPEN)
299
+ client.ping();
300
+ if (bridge.readyState === WebSocket.OPEN)
301
+ bridge.ping();
302
+ }
303
+ }, pingIntervalMs);
304
+ pinger.unref();
305
+ timers.push(pinger);
306
+ // Idle exit: with nothing registered and nobody proxied, the hub exits; the
307
+ // next bridge re-spawns it on demand (see endpoint.ts).
308
+ const idleCheck = setInterval(() => {
309
+ if (instances.size > 0 || proxyPairs.size > 0) {
310
+ idleSince = null;
311
+ return;
312
+ }
313
+ if (idleSince === null)
314
+ idleSince = Date.now();
315
+ if (Date.now() - idleSince >= idleExitMs) {
316
+ log("hub: idle for too long with no instances — exiting");
317
+ clearInterval(idleCheck);
318
+ void close().finally(() => onIdleExit?.());
319
+ }
320
+ }, Math.min(idleExitMs / 4, 10_000));
321
+ idleCheck.unref();
322
+ timers.push(idleCheck);
323
+ async function close() {
324
+ for (const t of timers)
325
+ clearInterval(t);
326
+ for (const { client, bridge } of proxyPairs) {
327
+ client.terminate();
328
+ bridge.terminate();
329
+ }
330
+ proxyPairs.clear();
331
+ wss.close();
332
+ server.closeAllConnections?.();
333
+ await new Promise((resolve) => server.close(() => resolve()));
334
+ }
335
+ return new Promise((resolve, reject) => {
336
+ server.once("error", reject);
337
+ server.listen(port, host, () => {
338
+ // port 0 binds an ephemeral port — report the actual one (used by tests).
339
+ const addr = server.address();
340
+ const bound = typeof addr === "object" && addr !== null ? addr.port : port;
341
+ log(`hub: listening on ${host}:${bound} (instances: 0)`);
342
+ resolve({ port: bound, close });
343
+ });
344
+ });
345
+ }
346
+ //# sourceMappingURL=hub-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-server.js","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import type * as acp from "@agentclientprotocol/sdk";
10
10
  import { ZcodeBackend } from "./backend/index.js";
11
11
  import { BackgroundTaskListener } from "./handlers/background-tasks.js";
12
+ import { ClientRegistry } from "./remote/broadcast.js";
12
13
  /** Client capabilities advertised in the initialize request. */
13
14
  export interface ClientCapabilities {
14
15
  fs?: {
@@ -72,6 +73,13 @@ export declare class ZcodeAcpServer {
72
73
  * passed through unchanged. */
73
74
  mcpServers?: acp.McpServer[];
74
75
  }>;
76
+ /**
77
+ * Session cwds (acp_sid → cwd), recorded at session/new and lazy-recovery.
78
+ * Unlike pendingSessions this survives materialization — the hub discovery
79
+ * payload needs a workspace label for live sessions, whose pending entries
80
+ * are deleted on first use.
81
+ */
82
+ readonly sessionCwds: Map<string, string>;
75
83
  /** Currently running turns, keyed by the ACP request id. */
76
84
  readonly pendingTurns: Map<number, PendingTurn>;
77
85
  /**
@@ -80,18 +88,44 @@ export declare class ZcodeAcpServer {
80
88
  * concurrent prompts from both missing each other and registering at once.
81
89
  */
82
90
  readonly preemptLocks: Map<string, Promise<void>>;
83
- /** Capabilities advertised by the connected client (Zed, JetBrains, ...). */
91
+ /** Capabilities advertised by connected clients (Zed, JetBrains, remote). */
84
92
  clientCapabilities: ClientCapabilities;
85
93
  /**
86
- * Connection-scoped AgentContext, captured at `connect()` time. Held so that
87
- * background listeners can push `session/update` notifications OUTSIDE a
88
- * request handler (e.g. when a background task completes after `session/
89
- * prompt` has already returned). Null before `connect()` runs (only during
90
- * the brief startup window — sessions can't be created before connect).
94
+ * All connected ACP clients (the stdio editor plus any remote WebSocket
95
+ * clients). Handlers push notifications through `clients.broadcast()` so
96
+ * every attached client sees the same stream; the registry replaces the old
97
+ * single `acpClient` reference. Background listeners use it to push
98
+ * `session/update` notifications outside request handlers.
99
+ */
100
+ readonly clients: ClientRegistry;
101
+ /**
102
+ * Lightweight session summaries for the remote hub's discovery API
103
+ * (acp_sid → { title, updatedAt, hasActivity }). In-memory only — the hub
104
+ * holds no business state and the bridge dies with its editor, so persistence
105
+ * would buy nothing. Maintained by `touchSessionSummary` at session
106
+ * registration, title set, and turn completion. `hasActivity` gates the
107
+ * discovery payload: an editor restart auto-resumes its stored placeholder,
108
+ * materializing an empty backend session — never-used sessions stay invisible
109
+ * to remote clients until first real use.
91
110
  */
92
- acpClient: acp.AgentContext | null;
111
+ readonly sessionSummaries: Map<string, {
112
+ title?: string;
113
+ updatedAt: number;
114
+ hasActivity?: boolean;
115
+ }>;
93
116
  /** Session titles already set, to enforce set-once (acp_sid → title). */
94
117
  readonly sessionTitles: Map<string, string>;
118
+ /**
119
+ * Sessions verified as loaded in the CURRENT backend subprocess — populated
120
+ * only after a successful session/create or session/resume RPC. A bare
121
+ * `registerSession` mapping does NOT qualify: the backend answers
122
+ * `session/messages` only for sessions it has loaded, so `session/load`
123
+ * must not skip the resume RPC for a mapping that was never loaded (e.g.
124
+ * re-registered from the durable store by an early ensureRealSession
125
+ * caller, or left behind by a failed resume) — the replay would silently
126
+ * come back empty.
127
+ */
128
+ readonly backendLoadedSessions: Set<string>;
95
129
  /**
96
130
  * Sessions eligible for auto-title on first end_turn. Only `session/new`
97
131
  * populates this — resumed/loaded sessions already carry a title, so their
@@ -143,6 +177,19 @@ export declare class ZcodeAcpServer {
143
177
  * background-task listener needs the reverse lookup to address notifications).
144
178
  */
145
179
  registerSession(acpSid: string, zcodeSid: string): void;
180
+ /** Update a session's discovery summary (title sticky once set). */
181
+ touchSessionSummary(acpSid: string, title?: string): void;
182
+ /**
183
+ * Mark a session as having real interaction (a prompt turn ran, or history
184
+ * was replayed on load). Gates the hub discovery payload — never-used
185
+ * sessions stay invisible to remote clients until first use.
186
+ */
187
+ markSessionActive(acpSid: string): void;
188
+ /**
189
+ * Best-effort workspace label for the hub discovery payload: first known
190
+ * session cwd, else the bridge process cwd.
191
+ */
192
+ workspaceLabel(): string;
146
193
  /**
147
194
  * Ensure a background-task listener is registered for the session. Idempotent
148
195
  * — returns the existing listener if already registered (covers resume/load
@@ -171,6 +218,14 @@ export declare class ZcodeAcpServer {
171
218
  * `session/request_permission`.
172
219
  */
173
220
  supportsElicitationForm(): boolean;
221
+ /**
222
+ * OR-merge capabilities from a newly connected client. Each connection runs
223
+ * its own `initialize`; boolean capabilities are unioned across clients so a
224
+ * feature advertised by ANY attached client (Zed or a remote one) enables the
225
+ * richer interaction path, and `_meta` flags (e.g. terminal_output) merge
226
+ * shallowly. Idempotent for re-connecting clients with equal capabilities.
227
+ */
228
+ mergeClientCapabilities(caps: ClientCapabilities): void;
174
229
  /** Handle `initialize`: negotiate version + declare agent capabilities. */
175
230
  initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse>;
176
231
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;AAErD,OAAO,EAIL,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACzD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,6BAA6B;AAC7B,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,cAAc;IACzB,iEAAiE;IACjE,OAAO,EAAE,YAAY,GAAG,IAAI,CAAQ;IACpC,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,sBAA6B;IAChD;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,sBAA6B;IACtD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe;aAGf,MAAM;mBACA,OAAO,CAAC,MAAM,CAAC;QAC1B;;;;uCAI+B;qBAClB,GAAG,CAAC,SAAS,EAAE;OAE5B;IACJ;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,sBAA6B;IACjD,4DAA4D;IAC5D,QAAQ,CAAC,YAAY,2BAAkC;IACvD;;;;OAIG;IACH,QAAQ,CAAC,YAAY,6BAAoC;IACzD,6EAA6E;IAC7E,kBAAkB,EAAE,kBAAkB,CAAM;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,iBAAwB;IACxC;;;;;;;;;OASG;IACH,QAAQ,CAAC,gBAAgB;gBAEb,MAAM;mBAAa,MAAM;sBAAgB,OAAO;OACxD;IACJ,yEAAyE;IACzE,QAAQ,CAAC,aAAa,sBAA6B;IACnD;;;;;;;;;OASG;IACH,QAAQ,CAAC,qBAAqB,cAAqB;IACnD;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,cAAqB;IACnD,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,sBAA6B;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,sBAA6B;IACrD,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,6EAGZ;IACJ,gEAAgE;IAChE,QAAQ,CAAC,UAAU,sBAA6B;IAChD;;;;;;OAMG;IACH,QAAQ,CAAC,mBAAmB,sCAA6C;IACzE;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,sBAA6B;IACtD,2FAA2F;IAC3F,OAAO,CAAC,UAAU,CAAc;IAEhC,sDAAsD;IACtD,MAAM,IAAI,MAAM;IAIhB,gFAAgF;IAChF,aAAa,IAAI,YAAY;IAQ7B,0DAA0D;IAC1D,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI9C;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMvD,oEAAoE;IACpE,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAWzD;;;;OAIG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IASvC;;;OAGG;IACH,cAAc,IAAI,MAAM;IAIxB;;;;;;;OAOG;IACH,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,sBAAsB;IAWlE,iFAAiF;IACjF,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAInD;;;;;;OAMG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBrF,gFAAgF;IAChF,sBAAsB,IAAI,OAAO;IAKjC;;;;;OAKG;IACH,uBAAuB,IAAI,OAAO;IAIlC;;;;;;OAMG;IACH,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAgBvD,2EAA2E;IACrE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;CAiCjF"}
package/dist/server.js CHANGED
@@ -8,6 +8,8 @@
8
8
  */
9
9
  import { loadZcodeCredentials, mergeEnvWithCreds, resolveZcodeCommand, ZcodeBackend, } from "./backend/index.js";
10
10
  import { BackgroundTaskListener } from "./handlers/background-tasks.js";
11
+ import { enqueueSessionSend } from "./handlers/io.js";
12
+ import { ClientRegistry } from "./remote/broadcast.js";
11
13
  import { AGENT_INFO, PROTOCOL_VERSION, log } from "./utils.js";
12
14
  export class ZcodeAcpServer {
13
15
  /** The ZCode subprocess client (lazy — spawned on first use). */
@@ -35,6 +37,13 @@ export class ZcodeAcpServer {
35
37
  * share one `session/create` instead of creating two backend sessions.
36
38
  */
37
39
  pendingSessions = new Map();
40
+ /**
41
+ * Session cwds (acp_sid → cwd), recorded at session/new and lazy-recovery.
42
+ * Unlike pendingSessions this survives materialization — the hub discovery
43
+ * payload needs a workspace label for live sessions, whose pending entries
44
+ * are deleted on first use.
45
+ */
46
+ sessionCwds = new Map();
38
47
  /** Currently running turns, keyed by the ACP request id. */
39
48
  pendingTurns = new Map();
40
49
  /**
@@ -43,18 +52,40 @@ export class ZcodeAcpServer {
43
52
  * concurrent prompts from both missing each other and registering at once.
44
53
  */
45
54
  preemptLocks = new Map();
46
- /** Capabilities advertised by the connected client (Zed, JetBrains, ...). */
55
+ /** Capabilities advertised by connected clients (Zed, JetBrains, remote). */
47
56
  clientCapabilities = {};
48
57
  /**
49
- * Connection-scoped AgentContext, captured at `connect()` time. Held so that
50
- * background listeners can push `session/update` notifications OUTSIDE a
51
- * request handler (e.g. when a background task completes after `session/
52
- * prompt` has already returned). Null before `connect()` runs (only during
53
- * the brief startup window — sessions can't be created before connect).
58
+ * All connected ACP clients (the stdio editor plus any remote WebSocket
59
+ * clients). Handlers push notifications through `clients.broadcast()` so
60
+ * every attached client sees the same stream; the registry replaces the old
61
+ * single `acpClient` reference. Background listeners use it to push
62
+ * `session/update` notifications outside request handlers.
54
63
  */
55
- acpClient = null;
64
+ clients = new ClientRegistry();
65
+ /**
66
+ * Lightweight session summaries for the remote hub's discovery API
67
+ * (acp_sid → { title, updatedAt, hasActivity }). In-memory only — the hub
68
+ * holds no business state and the bridge dies with its editor, so persistence
69
+ * would buy nothing. Maintained by `touchSessionSummary` at session
70
+ * registration, title set, and turn completion. `hasActivity` gates the
71
+ * discovery payload: an editor restart auto-resumes its stored placeholder,
72
+ * materializing an empty backend session — never-used sessions stay invisible
73
+ * to remote clients until first real use.
74
+ */
75
+ sessionSummaries = new Map();
56
76
  /** Session titles already set, to enforce set-once (acp_sid → title). */
57
77
  sessionTitles = new Map();
78
+ /**
79
+ * Sessions verified as loaded in the CURRENT backend subprocess — populated
80
+ * only after a successful session/create or session/resume RPC. A bare
81
+ * `registerSession` mapping does NOT qualify: the backend answers
82
+ * `session/messages` only for sessions it has loaded, so `session/load`
83
+ * must not skip the resume RPC for a mapping that was never loaded (e.g.
84
+ * re-registered from the durable store by an early ensureRealSession
85
+ * caller, or left behind by a failed resume) — the replay would silently
86
+ * come back empty.
87
+ */
88
+ backendLoadedSessions = new Set();
58
89
  /**
59
90
  * Sessions eligible for auto-title on first end_turn. Only `session/new`
60
91
  * populates this — resumed/loaded sessions already carry a title, so their
@@ -119,6 +150,38 @@ export class ZcodeAcpServer {
119
150
  registerSession(acpSid, zcodeSid) {
120
151
  this.sessionMap.set(acpSid, zcodeSid);
121
152
  this.acpSidByZcodeSid.set(zcodeSid, acpSid);
153
+ this.touchSessionSummary(acpSid);
154
+ }
155
+ /** Update a session's discovery summary (title sticky once set). */
156
+ touchSessionSummary(acpSid, title) {
157
+ const existing = this.sessionSummaries.get(acpSid);
158
+ this.sessionSummaries.set(acpSid, {
159
+ title: title ?? existing?.title,
160
+ updatedAt: Date.now(),
161
+ // A title only exists once the session produced content (auto-title on
162
+ // first end_turn, or a stored title adopted on resume/load).
163
+ hasActivity: existing?.hasActivity || title !== undefined,
164
+ });
165
+ }
166
+ /**
167
+ * Mark a session as having real interaction (a prompt turn ran, or history
168
+ * was replayed on load). Gates the hub discovery payload — never-used
169
+ * sessions stay invisible to remote clients until first use.
170
+ */
171
+ markSessionActive(acpSid) {
172
+ const existing = this.sessionSummaries.get(acpSid);
173
+ this.sessionSummaries.set(acpSid, {
174
+ title: existing?.title,
175
+ updatedAt: Date.now(),
176
+ hasActivity: true,
177
+ });
178
+ }
179
+ /**
180
+ * Best-effort workspace label for the hub discovery payload: first known
181
+ * session cwd, else the bridge process cwd.
182
+ */
183
+ workspaceLabel() {
184
+ return this.sessionCwds.values().next().value ?? process.cwd();
122
185
  }
123
186
  /**
124
187
  * Ensure a background-task listener is registered for the session. Idempotent
@@ -151,14 +214,16 @@ export class ZcodeAcpServer {
151
214
  * the bridge on a notification failure.
152
215
  */
153
216
  async notifyByZcodeSid(zcodeSid, update) {
154
- const cx = this.acpClient;
155
- if (!cx)
217
+ if (this.clients.size === 0)
156
218
  return false;
157
219
  const acpSid = this.resolveAcpSid(zcodeSid);
158
220
  if (!acpSid)
159
221
  return false;
160
222
  try {
161
- await cx.notify("session/update", { sessionId: acpSid, update });
223
+ // Broadcast notify swallows per-client failures internally (warn only).
224
+ // Serialized through the replay guard so a background emission queues
225
+ // behind an in-flight replay batch for the same session.
226
+ await enqueueSessionSend(acpSid, () => this.clients.broadcast().notify("session/update", { sessionId: acpSid, update }));
162
227
  return true;
163
228
  }
164
229
  catch (e) {
@@ -180,10 +245,33 @@ export class ZcodeAcpServer {
180
245
  supportsElicitationForm() {
181
246
  return this.clientCapabilities.elicitation?.form != null;
182
247
  }
248
+ /**
249
+ * OR-merge capabilities from a newly connected client. Each connection runs
250
+ * its own `initialize`; boolean capabilities are unioned across clients so a
251
+ * feature advertised by ANY attached client (Zed or a remote one) enables the
252
+ * richer interaction path, and `_meta` flags (e.g. terminal_output) merge
253
+ * shallowly. Idempotent for re-connecting clients with equal capabilities.
254
+ */
255
+ mergeClientCapabilities(caps) {
256
+ const cur = this.clientCapabilities;
257
+ const next = { ...cur, ...caps };
258
+ next.fs = {
259
+ readTextFile: cur.fs?.readTextFile || caps.fs?.readTextFile,
260
+ writeTextFile: cur.fs?.writeTextFile || caps.fs?.writeTextFile,
261
+ };
262
+ next.terminal = cur.terminal || caps.terminal;
263
+ next.elicitation = {
264
+ form: cur.elicitation?.form || caps.elicitation?.form,
265
+ url: cur.elicitation?.url || caps.elicitation?.url,
266
+ };
267
+ if (cur._meta || caps._meta)
268
+ next._meta = { ...cur._meta, ...caps._meta };
269
+ this.clientCapabilities = next;
270
+ }
183
271
  /** Handle `initialize`: negotiate version + declare agent capabilities. */
184
272
  async initialize(params) {
185
273
  const clientInfo = params.clientInfo ?? null;
186
- this.clientCapabilities = params.clientCapabilities ?? {};
274
+ this.mergeClientCapabilities(params.clientCapabilities ?? {});
187
275
  log(`initialize: client protocolVersion=${params.protocolVersion}` +
188
276
  `, client=${clientInfo?.name ?? "unknown"}` +
189
277
  `, version=${clientInfo?.version ?? "unknown"}` +