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.
- package/README.md +101 -15
- package/README.zh-CN.md +68 -13
- package/dist/bin/hub.d.ts +16 -0
- package/dist/bin/hub.d.ts.map +1 -0
- package/dist/bin/hub.js +41 -0
- package/dist/bin/hub.js.map +1 -0
- package/dist/handlers/account.d.ts +43 -0
- package/dist/handlers/account.d.ts.map +1 -0
- package/dist/handlers/account.js +59 -0
- package/dist/handlers/account.js.map +1 -0
- package/dist/handlers/io.d.ts +20 -1
- package/dist/handlers/io.d.ts.map +1 -1
- package/dist/handlers/io.js +57 -2
- package/dist/handlers/io.js.map +1 -1
- package/dist/handlers/replay.d.ts +79 -0
- package/dist/handlers/replay.d.ts.map +1 -0
- package/dist/handlers/replay.js +256 -0
- package/dist/handlers/replay.js.map +1 -0
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +83 -68
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +23 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +67 -6
- package/dist/handlers/slash.js.map +1 -1
- package/dist/index.js +47 -17
- package/dist/index.js.map +1 -1
- package/dist/remote/broadcast.d.ts +47 -0
- package/dist/remote/broadcast.d.ts.map +1 -0
- package/dist/remote/broadcast.js +121 -0
- package/dist/remote/broadcast.js.map +1 -0
- package/dist/remote/config.d.ts +32 -0
- package/dist/remote/config.d.ts.map +1 -0
- package/dist/remote/config.js +65 -0
- package/dist/remote/config.js.map +1 -0
- package/dist/remote/endpoint.d.ts +43 -0
- package/dist/remote/endpoint.d.ts.map +1 -0
- package/dist/remote/endpoint.js +222 -0
- package/dist/remote/endpoint.js.map +1 -0
- package/dist/remote/hub-server.d.ts +41 -0
- package/dist/remote/hub-server.d.ts.map +1 -0
- package/dist/remote/hub-server.js +346 -0
- package/dist/remote/hub-server.js.map +1 -0
- package/dist/server.d.ts +62 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +99 -11
- package/dist/server.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +17 -1
- package/dist/utils.js.map +1 -1
- package/docs/ARCHITECTURE.md +47 -15
- package/docs/BACKLOG.md +3 -1
- package/docs/DEVELOPMENT.md +26 -0
- package/docs/PROTOCOL.md +67 -27
- package/docs/REMOTE-CLIENTS.md +264 -0
- package/docs/REPLAY-GUIDE.md +131 -0
- package/docs/TROUBLESHOOTING.md +51 -6
- package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
- package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
- package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
- package/docs/proposals/0001-tail-session-replay.md +136 -0
- package/docs/proposals/0002-plan-quota-usage.md +81 -0
- 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
|
|
91
|
+
/** Capabilities advertised by connected clients (Zed, JetBrains, remote). */
|
|
84
92
|
clientCapabilities: ClientCapabilities;
|
|
85
93
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
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
|
|
55
|
+
/** Capabilities advertised by connected clients (Zed, JetBrains, remote). */
|
|
47
56
|
clientCapabilities = {};
|
|
48
57
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"}` +
|