zcode-acp-server 0.48.0 → 0.49.0

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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/backend/adapter.d.ts +129 -0
  3. package/dist/backend/adapter.d.ts.map +1 -0
  4. package/dist/backend/adapter.js +71 -0
  5. package/dist/backend/adapter.js.map +1 -0
  6. package/dist/backend/client.d.ts +14 -103
  7. package/dist/backend/client.d.ts.map +1 -1
  8. package/dist/backend/client.js +63 -413
  9. package/dist/backend/client.js.map +1 -1
  10. package/dist/backend/index.d.ts +3 -1
  11. package/dist/backend/index.d.ts.map +1 -1
  12. package/dist/backend/index.js +2 -0
  13. package/dist/backend/index.js.map +1 -1
  14. package/dist/backend/jsonrpc-child.d.ts +128 -0
  15. package/dist/backend/jsonrpc-child.d.ts.map +1 -0
  16. package/dist/backend/jsonrpc-child.js +397 -0
  17. package/dist/backend/jsonrpc-child.js.map +1 -0
  18. package/dist/code-fingerprint.json +2 -2
  19. package/dist/config/options.d.ts.map +1 -1
  20. package/dist/config/options.js +14 -4
  21. package/dist/config/options.js.map +1 -1
  22. package/dist/handlers/session.d.ts.map +1 -1
  23. package/dist/handlers/session.js +9 -4
  24. package/dist/handlers/session.js.map +1 -1
  25. package/dist/handlers/slash.d.ts.map +1 -1
  26. package/dist/handlers/slash.js +8 -4
  27. package/dist/handlers/slash.js.map +1 -1
  28. package/dist/remote/settings-endpoint.d.ts.map +1 -1
  29. package/dist/remote/settings-endpoint.js +64 -1
  30. package/dist/remote/settings-endpoint.js.map +1 -1
  31. package/dist/server.d.ts +8 -1
  32. package/dist/server.d.ts.map +1 -1
  33. package/dist/server.js +7 -0
  34. package/dist/server.js.map +1 -1
  35. package/dist/settings/workflow.d.ts +25 -0
  36. package/dist/settings/workflow.d.ts.map +1 -1
  37. package/dist/settings/workflow.js +61 -2
  38. package/dist/settings/workflow.js.map +1 -1
  39. package/docs/ARCHITECTURE.md +8 -6
  40. package/package.json +1 -1
@@ -0,0 +1,128 @@
1
+ /**
2
+ * JsonRpcChild — shared stdio JSON-RPC subprocess transport for backends
3
+ * (ADR-0023).
4
+ *
5
+ * Every backend adapter reuses one read-loop multiplexer, watchdog, and
6
+ * process-group lifecycle:
7
+ * - responses (id, no method) → resolve the matching pending request
8
+ * - id + method → our response (id registered) or a
9
+ * server→client request (hook first, then queue)
10
+ * - method + no id → notification (backend-specific hook)
11
+ *
12
+ * Wire dialects differ per backend: zcode sends BARE frames (no `jsonrpc`
13
+ * field — the backend's strict validator rejects it) while a standard
14
+ * JSON-RPC 2.0 backend sends versioned frames. Subclasses override
15
+ * {@link decorateOutbound} for the write dialect and
16
+ * {@link handleNotification} / {@link handleServerRequest} for inbound
17
+ * routing.
18
+ */
19
+ import { type ChildProcess } from "node:child_process";
20
+ import type { BackendAdapter, BackendCapabilities, EventListener, ServerRequest } from "./adapter.js";
21
+ import type { ZcodeEvent, ZcodeResponse } from "./types.js";
22
+ export interface JsonRpcChildOptions {
23
+ argv: string[];
24
+ env: NodeJS.ProcessEnv;
25
+ /** Backend name used in error strings (e.g. "zcode" → "zcode backend pipe broken"). */
26
+ name: string;
27
+ /** Human-readable label for the start log line. */
28
+ logLabel: string;
29
+ /** CLI display name for the ENOENT hint. */
30
+ cliLabel: string;
31
+ /** Env var users can set to point at the CLI binary (for the ENOENT hint). */
32
+ binEnvVar: string;
33
+ }
34
+ export declare abstract class JsonRpcChild implements BackendAdapter {
35
+ /** Feature flags of the concrete backend — each subclass binds its kind's table. */
36
+ abstract readonly capabilities: BackendCapabilities;
37
+ readonly proc: ChildProcess;
38
+ protected readonly name: string;
39
+ private readonly pending;
40
+ private readonly serverRequests;
41
+ private readonly listeners;
42
+ private readerDead;
43
+ /** Monotonic id for fire-and-forget sends (send()). Uses a high range to
44
+ * avoid collisions with the server's request ids (low range). */
45
+ private sendIdCounter;
46
+ /** Watchdog process that kills the backend group if this bridge dies (SIGKILL). */
47
+ private watchdog;
48
+ protected constructor(opts: JsonRpcChildOptions);
49
+ /**
50
+ * Spawn a tiny detached watchdog that kills the backend process group if this
51
+ * bridge process disappears.
52
+ *
53
+ * The detached/kill(-pid) cleanup in `close()` only runs when the bridge
54
+ * exits cleanly enough for the signal handlers to fire (SIGTERM/SIGINT/etc).
55
+ * If the bridge is SIGKILLed (Zed force-kill on reconnect, crash, OOM), the
56
+ * handler never runs and the backend subprocess group is orphaned. The
57
+ * watchdog closes that gap: it polls the bridge pid every 2s and, once the
58
+ * bridge is gone, sends SIGKILL to the backend process group, then exits.
59
+ *
60
+ * The watchdog is its own process-group leader (detached) and `unref`'d, so
61
+ * it never holds the event loop open and is not part of the backend group it
62
+ * kills. It self-terminates as soon as the backend process exits, so a
63
+ * normal shutdown leaves no lingering watchdog.
64
+ */
65
+ private startWatchdog;
66
+ private startReader;
67
+ private route;
68
+ /** Deliver a backend event to every listener registered for its session. */
69
+ protected dispatchEvent(ev: ZcodeEvent): void;
70
+ private resolvePending;
71
+ protected markReaderDead(reason: string): void;
72
+ /** Route an inbound notification. Unhandled methods are ignored. */
73
+ protected abstract handleNotification(method: string, params: Record<string, unknown>): void;
74
+ /**
75
+ * Intercept an unmatched server→client request before it queues. Return
76
+ * true when consumed (e.g. a handshake auto-reply, or an arrival-time
77
+ * responder) so the original never reaches the interaction queue verbatim.
78
+ */
79
+ protected handleServerRequest(_req: ServerRequest): boolean;
80
+ /** Adapt an outbound frame to the backend's wire dialect. */
81
+ protected decorateOutbound(msg: Record<string, unknown>): Record<string, unknown>;
82
+ registerEventListener(sessionId: string, listener: EventListener): void;
83
+ unregisterEventListener(sessionId: string, listener: EventListener): void;
84
+ /** Non-blocking drain of pending server→client requests. */
85
+ pollServerRequests(): ServerRequest[];
86
+ /**
87
+ * Re-queue server→client requests that belong to a different session (prepended
88
+ * to preserve arrival order). Used by `handleServerRequests` to put back
89
+ * requests it popped but doesn't own.
90
+ */
91
+ requeueServerRequests(reqs: ServerRequest[]): void;
92
+ /** Reply to a backend server→client request with a result (id + result). */
93
+ sendReply(id: number | string, result: unknown): void;
94
+ /** Reply to a backend server→client request with an error. */
95
+ sendError(id: number | string, code: number, message: string): void;
96
+ /** Fire-and-forget notification to the backend (no id, no response). */
97
+ notify(method: string, params?: Record<string, unknown>): void;
98
+ /**
99
+ * Send a message with an id but WITHOUT registering a pending response
100
+ * (fire-and-forget). Some backends route by id presence, so carrying an id
101
+ * is more robust than a bare notify. If the backend replies, the reader's
102
+ * `resolvePending` finds no pending entry and safely discards it.
103
+ */
104
+ send(method: string, params?: Record<string, unknown>): void;
105
+ private writeFrame;
106
+ /**
107
+ * Synchronous request/response: register a pending promise, send, await.
108
+ * Other notifications arriving during the wait are routed async by the
109
+ * reader loop (they don't get swallowed).
110
+ *
111
+ * Returns `{error}` on dead backend, broken pipe, or timeout — never throws.
112
+ */
113
+ request(id: number, method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<ZcodeResponse>;
114
+ /**
115
+ * Kill the whole backend process group and wait for it to die.
116
+ *
117
+ * SIGTERM → wait up to 3s → SIGKILL if still alive. Note `proc.killed` is
118
+ * NOT set by `process.kill(-pid)` (group signal), so we track liveness via
119
+ * `exitCode === null` instead. Async so the caller can `await` a full reap
120
+ * before the parent exits (an unref'd timer could be skipped on fast exit,
121
+ * leaving orphans).
122
+ */
123
+ close(): Promise<void>;
124
+ /** Terminate the watchdog process if it is still running. */
125
+ private killWatchdog;
126
+ get isDead(): boolean;
127
+ }
128
+ //# sourceMappingURL=jsonrpc-child.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonrpc-child.d.ts","sourceRoot":"","sources":["../../src/backend/jsonrpc-child.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,397 @@
1
+ /**
2
+ * JsonRpcChild — shared stdio JSON-RPC subprocess transport for backends
3
+ * (ADR-0023).
4
+ *
5
+ * Every backend adapter reuses one read-loop multiplexer, watchdog, and
6
+ * process-group lifecycle:
7
+ * - responses (id, no method) → resolve the matching pending request
8
+ * - id + method → our response (id registered) or a
9
+ * server→client request (hook first, then queue)
10
+ * - method + no id → notification (backend-specific hook)
11
+ *
12
+ * Wire dialects differ per backend: zcode sends BARE frames (no `jsonrpc`
13
+ * field — the backend's strict validator rejects it) while a standard
14
+ * JSON-RPC 2.0 backend sends versioned frames. Subclasses override
15
+ * {@link decorateOutbound} for the write dialect and
16
+ * {@link handleNotification} / {@link handleServerRequest} for inbound
17
+ * routing.
18
+ */
19
+ import { spawn } from "node:child_process";
20
+ import { createInterface } from "node:readline";
21
+ import process from "node:process";
22
+ import { log, warn } from "../utils.js";
23
+ export class JsonRpcChild {
24
+ proc;
25
+ name;
26
+ pending = new Map();
27
+ serverRequests = [];
28
+ // Per-session listener SET so a long-lived session listener (e.g. background
29
+ // task monitor) can coexist with a per-turn EventStreamListener. Each event
30
+ // is delivered to every registered listener for the session.
31
+ listeners = new Map();
32
+ readerDead = false;
33
+ /** Monotonic id for fire-and-forget sends (send()). Uses a high range to
34
+ * avoid collisions with the server's request ids (low range). */
35
+ sendIdCounter = 1_000_000_000;
36
+ /** Watchdog process that kills the backend group if this bridge dies (SIGKILL). */
37
+ watchdog = null;
38
+ constructor(opts) {
39
+ this.name = opts.name;
40
+ this.proc = spawn(opts.argv[0], opts.argv.slice(1), {
41
+ stdio: ["pipe", "pipe", "ignore"],
42
+ env: opts.env,
43
+ detached: true, // own process group → kill(-pid) reaps the whole tree
44
+ });
45
+ // Spawn failures (ENOENT when the CLI can't be resolved) arrive here
46
+ // asynchronously — without a listener the bridge dies on an unhandled
47
+ // 'error' event. Mark the backend dead so requests fail with a JSON-RPC
48
+ // error instead of crashing the whole process.
49
+ this.proc.on("error", (err) => {
50
+ const hint = err.code === "ENOENT"
51
+ ? `${this.proc.spawnfile} not found — install the ${opts.cliLabel}, put it on PATH, or set ${opts.binEnvVar}`
52
+ : err.message;
53
+ this.markReaderDead(`spawn failed: ${hint}`);
54
+ });
55
+ // Node stream write errors (EPIPE on a closed stdin) are emitted as async
56
+ // 'error' events, NOT thrown synchronously — without a listener the process
57
+ // crashes with an unhandled 'error' event. Catch them here and mark the
58
+ // reader dead so the rest of the bridge stops talking to a gone backend.
59
+ this.proc.stdin?.on("error", (err) => {
60
+ this.readerDead = true;
61
+ warn(`backend: stdin error: ${err.message}`);
62
+ });
63
+ this.startReader();
64
+ this.startWatchdog();
65
+ log(`backend: started ${opts.logLabel} (pid=${this.proc.pid})`);
66
+ }
67
+ /**
68
+ * Spawn a tiny detached watchdog that kills the backend process group if this
69
+ * bridge process disappears.
70
+ *
71
+ * The detached/kill(-pid) cleanup in `close()` only runs when the bridge
72
+ * exits cleanly enough for the signal handlers to fire (SIGTERM/SIGINT/etc).
73
+ * If the bridge is SIGKILLed (Zed force-kill on reconnect, crash, OOM), the
74
+ * handler never runs and the backend subprocess group is orphaned. The
75
+ * watchdog closes that gap: it polls the bridge pid every 2s and, once the
76
+ * bridge is gone, sends SIGKILL to the backend process group, then exits.
77
+ *
78
+ * The watchdog is its own process-group leader (detached) and `unref`'d, so
79
+ * it never holds the event loop open and is not part of the backend group it
80
+ * kills. It self-terminates as soon as the backend process exits, so a
81
+ * normal shutdown leaves no lingering watchdog.
82
+ */
83
+ startWatchdog() {
84
+ const bridgePid = process.pid;
85
+ const backendPid = this.proc.pid;
86
+ if (!bridgePid || !backendPid)
87
+ return;
88
+ // Inline script: poll bridge liveness, kill backend group on bridge death.
89
+ const script = `
90
+ const bridgePid = ${bridgePid};
91
+ const backendPid = ${backendPid};
92
+ const tick = () => {
93
+ // Bridge gone? → reap the whole backend process group, then exit.
94
+ try { process.kill(bridgePid, 0); }
95
+ catch {
96
+ try { process.kill(-backendPid, 'SIGKILL'); } catch {}
97
+ process.exit(0);
98
+ }
99
+ // backend already exited? → watchdog has no job left.
100
+ try { process.kill(-backendPid, 0); }
101
+ catch { process.exit(0); }
102
+ };
103
+ setInterval(tick, 2000);
104
+ tick();
105
+ `;
106
+ this.watchdog = spawn(process.execPath, ["-e", script], {
107
+ stdio: "ignore",
108
+ detached: true, // own process group, not part of the backend group
109
+ });
110
+ this.watchdog.unref();
111
+ }
112
+ // ---------- read loop ----------
113
+ startReader() {
114
+ const stdout = this.proc.stdout;
115
+ if (!stdout) {
116
+ this.markReaderDead("no stdout");
117
+ return;
118
+ }
119
+ const rl = createInterface({ input: stdout });
120
+ rl.on("line", (line) => {
121
+ const trimmed = line.trim();
122
+ if (!trimmed)
123
+ return;
124
+ let msg;
125
+ try {
126
+ msg = JSON.parse(trimmed);
127
+ }
128
+ catch {
129
+ return; // unparseable line: ignore
130
+ }
131
+ this.route(msg);
132
+ });
133
+ rl.on("close", () => this.markReaderDead("stdout closed"));
134
+ }
135
+ route(msg) {
136
+ const method = msg.method;
137
+ const id = msg.id;
138
+ if (id !== undefined && method === undefined) {
139
+ // Response (id, no method) → resolve pending request. A late reply to a
140
+ // fire-and-forget send has no pending entry and is safely dropped.
141
+ this.resolvePending(id, msg);
142
+ return;
143
+ }
144
+ if (id !== undefined && method !== undefined) {
145
+ // id + method: our pending response wins the race; else a hook gets a
146
+ // chance (dialect-specific auto-reply), else it queues as server→client.
147
+ if (this.pending.has(id)) {
148
+ this.resolvePending(id, msg);
149
+ }
150
+ else {
151
+ const req = {
152
+ id,
153
+ method,
154
+ params: (msg.params ?? {}),
155
+ };
156
+ if (!this.handleServerRequest(req)) {
157
+ this.serverRequests.push(req);
158
+ }
159
+ }
160
+ return;
161
+ }
162
+ if (method !== undefined) {
163
+ // Notification — dialect-specific routing.
164
+ this.handleNotification(method, (msg.params ?? {}));
165
+ }
166
+ }
167
+ /** Deliver a backend event to every listener registered for its session. */
168
+ dispatchEvent(ev) {
169
+ const sid = ev.sessionId;
170
+ const set = sid ? this.listeners.get(sid) : undefined;
171
+ if (set) {
172
+ // Iterate a snapshot so a listener that (un)registers during dispatch
173
+ // doesn't mutate the set under us.
174
+ for (const listener of [...set]) {
175
+ try {
176
+ listener.handleEvent(ev);
177
+ }
178
+ catch (e) {
179
+ warn(`backend: listener.handleEvent threw: ${e instanceof Error ? e.message : String(e)}`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ resolvePending(id, resp) {
185
+ const p = this.pending.get(id);
186
+ if (!p)
187
+ return;
188
+ clearTimeout(p.timer);
189
+ this.pending.delete(id);
190
+ p.resolve(resp);
191
+ }
192
+ markReaderDead(reason) {
193
+ if (this.readerDead)
194
+ return;
195
+ this.readerDead = true;
196
+ warn(`backend: reader exited (${reason})`);
197
+ for (const [, p] of this.pending) {
198
+ clearTimeout(p.timer);
199
+ p.resolve({
200
+ id: 0,
201
+ error: { message: `${this.name} backend reader exited (backend dead)` },
202
+ });
203
+ }
204
+ this.pending.clear();
205
+ }
206
+ /**
207
+ * Intercept an unmatched server→client request before it queues. Return
208
+ * true when consumed (e.g. a handshake auto-reply, or an arrival-time
209
+ * responder) so the original never reaches the interaction queue verbatim.
210
+ */
211
+ handleServerRequest(_req) {
212
+ return false;
213
+ }
214
+ /** Adapt an outbound frame to the backend's wire dialect. */
215
+ decorateOutbound(msg) {
216
+ return msg;
217
+ }
218
+ // ---------- listeners / server requests ----------
219
+ registerEventListener(sessionId, listener) {
220
+ let set = this.listeners.get(sessionId);
221
+ if (!set) {
222
+ set = new Set();
223
+ this.listeners.set(sessionId, set);
224
+ }
225
+ set.add(listener);
226
+ }
227
+ unregisterEventListener(sessionId, listener) {
228
+ const set = this.listeners.get(sessionId);
229
+ if (!set)
230
+ return;
231
+ set.delete(listener);
232
+ if (set.size === 0)
233
+ this.listeners.delete(sessionId);
234
+ }
235
+ /** Non-blocking drain of pending server→client requests. */
236
+ pollServerRequests() {
237
+ if (this.serverRequests.length === 0)
238
+ return [];
239
+ return this.serverRequests.splice(0, this.serverRequests.length);
240
+ }
241
+ /**
242
+ * Re-queue server→client requests that belong to a different session (prepended
243
+ * to preserve arrival order). Used by `handleServerRequests` to put back
244
+ * requests it popped but doesn't own.
245
+ */
246
+ requeueServerRequests(reqs) {
247
+ if (reqs.length === 0)
248
+ return;
249
+ this.serverRequests.unshift(...reqs);
250
+ }
251
+ /** Reply to a backend server→client request with a result (id + result). */
252
+ sendReply(id, result) {
253
+ this.writeFrame({ id, result });
254
+ }
255
+ /** Reply to a backend server→client request with an error. */
256
+ sendError(id, code, message) {
257
+ this.writeFrame({ id, error: { code, message } });
258
+ }
259
+ // ---------- send / request ----------
260
+ /** Fire-and-forget notification to the backend (no id, no response). */
261
+ notify(method, params) {
262
+ this.writeFrame({ method, params });
263
+ }
264
+ /**
265
+ * Send a message with an id but WITHOUT registering a pending response
266
+ * (fire-and-forget). Some backends route by id presence, so carrying an id
267
+ * is more robust than a bare notify. If the backend replies, the reader's
268
+ * `resolvePending` finds no pending entry and safely discards it.
269
+ */
270
+ send(method, params) {
271
+ this.writeFrame({ id: this.sendIdCounter++, method, params: params ?? {} });
272
+ }
273
+ writeFrame(msg) {
274
+ const stdin = this.proc.stdin;
275
+ if (!stdin || stdin.destroyed) {
276
+ warn("backend: frame dropped (stdin closed)");
277
+ return;
278
+ }
279
+ // Write errors (EPIPE) are delivered via the stdin 'error' listener
280
+ // installed in the constructor (synchronous try/catch cannot catch them);
281
+ // no try/catch needed here.
282
+ stdin.write(JSON.stringify(this.decorateOutbound(msg)) + "\n");
283
+ }
284
+ /**
285
+ * Synchronous request/response: register a pending promise, send, await.
286
+ * Other notifications arriving during the wait are routed async by the
287
+ * reader loop (they don't get swallowed).
288
+ *
289
+ * Returns `{error}` on dead backend, broken pipe, or timeout — never throws.
290
+ */
291
+ async request(id, method, params, timeoutMs = 30000) {
292
+ if (this.readerDead) {
293
+ return { id, error: { message: `${this.name} backend reader exited (backend dead)` } };
294
+ }
295
+ const promise = new Promise((resolve) => {
296
+ const timer = setTimeout(() => {
297
+ if (this.pending.delete(id)) {
298
+ resolve({ id, error: { message: "timeout" } });
299
+ }
300
+ }, timeoutMs);
301
+ this.pending.set(id, { resolve, timer });
302
+ });
303
+ try {
304
+ // Fast-fail a closed pipe BEFORE the pending wait: writeFrame only
305
+ // warns and drops (fire-and-forget semantics), which would leave this
306
+ // request hanging until timeoutMs with readerDead unset.
307
+ const stdin = this.proc.stdin;
308
+ if (!stdin || stdin.destroyed)
309
+ throw new Error("stdin closed");
310
+ this.writeFrame({ id, method, params: params ?? {} });
311
+ }
312
+ catch (e) {
313
+ this.pending.delete(id);
314
+ this.readerDead = true;
315
+ return {
316
+ id,
317
+ error: {
318
+ message: `${this.name} backend pipe broken: ${e instanceof Error ? e.message : String(e)}`,
319
+ },
320
+ };
321
+ }
322
+ return promise;
323
+ }
324
+ // ---------- lifecycle ----------
325
+ /**
326
+ * Kill the whole backend process group and wait for it to die.
327
+ *
328
+ * SIGTERM → wait up to 3s → SIGKILL if still alive. Note `proc.killed` is
329
+ * NOT set by `process.kill(-pid)` (group signal), so we track liveness via
330
+ * `exitCode === null` instead. Async so the caller can `await` a full reap
331
+ * before the parent exits (an unref'd timer could be skipped on fast exit,
332
+ * leaving orphans).
333
+ */
334
+ async close() {
335
+ const proc = this.proc;
336
+ if (!proc.pid)
337
+ return;
338
+ try {
339
+ // Already exited?
340
+ if (proc.exitCode !== null || proc.signalCode)
341
+ return;
342
+ try {
343
+ process.kill(-proc.pid, "SIGTERM");
344
+ }
345
+ catch {
346
+ return; // group already gone
347
+ }
348
+ // Wait up to 3s for a clean exit.
349
+ const exited = await new Promise((resolve) => {
350
+ let timer;
351
+ const done = () => {
352
+ clearTimeout(timer); // don't let the timeout keep the event loop alive
353
+ resolve(true);
354
+ };
355
+ proc.once("exit", done);
356
+ timer = setTimeout(() => {
357
+ proc.removeListener("exit", done);
358
+ resolve(false);
359
+ }, 3000);
360
+ });
361
+ if (exited)
362
+ return;
363
+ // Still alive → SIGKILL the whole group.
364
+ try {
365
+ if (proc.pid && proc.exitCode === null)
366
+ process.kill(-proc.pid, "SIGKILL");
367
+ }
368
+ catch {
369
+ // already gone
370
+ }
371
+ }
372
+ finally {
373
+ // Stop the watchdog — it would self-exit on its next tick once the
374
+ // backend group is gone, but killing it here avoids the up-to-2s delay.
375
+ this.killWatchdog();
376
+ }
377
+ }
378
+ /** Terminate the watchdog process if it is still running. */
379
+ killWatchdog() {
380
+ const wd = this.watchdog;
381
+ if (!wd)
382
+ return;
383
+ this.watchdog = null;
384
+ if (wd.pid && wd.exitCode === null) {
385
+ try {
386
+ process.kill(wd.pid, "SIGTERM");
387
+ }
388
+ catch {
389
+ // already gone
390
+ }
391
+ }
392
+ }
393
+ get isDead() {
394
+ return this.readerDead;
395
+ }
396
+ }
397
+ //# sourceMappingURL=jsonrpc-child.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonrpc-child.js","sourceRoot":"","sources":["../../src/backend/jsonrpc-child.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  {
2
- "fingerprint": "e72828b13a1a6a6f146f100e240f65a8076b1ffaee85180ae1eb18ec504043ee",
3
- "fileCount": 101
2
+ "fingerprint": "46865404a2a22d973866a200518cc4e517346735645692c53687ead2ce32bad5",
3
+ "fileCount": 103
4
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/config/options.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/config/options.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@
10
10
  * reflects the new state.
11
11
  */
12
12
  import { readFileSync } from "node:fs";
13
+ import { backendCapabilities } from "../backend/adapter.js";
13
14
  import { recordModelChoice } from "../lazy-sessions.js";
14
15
  import { clientConnectionRoot, CONFIG_DISPATCH, CONFIG_META, log, warn, ZCODE_CREDS_PATH, zcodePersonalProviderPath, } from "../utils.js";
15
16
  import { configProviderIdFor } from "./account-provider.js";
@@ -178,9 +179,17 @@ export function loadAllModels() {
178
179
  if (!providerSelectable(pid, p))
179
180
  continue;
180
181
  const providerName = p.name ?? pid;
181
- const ids = new Set(Object.keys(p.models ?? {}));
182
- for (const modelId of personal?.modelsByProvider.get(pid) ?? [])
183
- ids.add(modelId);
182
+ // Custom providers: once the personal config tracks the provider's
183
+ // models, THAT list is authoritative. Additions AND deletions land only
184
+ // there (config.json stopped syncing), and the backend registry knows no
185
+ // other source for the provider's models — a config.json-only entry is a
186
+ // deleted-model ghost that fails on switch (observed 2026-09-28). Builtin
187
+ // providers keep the union: their catalog comes from the account
188
+ // snapshot, and a personal rule is an override, not a complete list.
189
+ const personalIds = personal?.modelsByProvider.get(pid);
190
+ const ids = personalIds !== undefined && !isBuiltinProvider(pid)
191
+ ? new Set(personalIds)
192
+ : new Set([...Object.keys(p.models ?? {}), ...(personalIds ?? [])]);
184
193
  for (const modelId of ids) {
185
194
  out.push({ providerId: pid, providerName, modelId });
186
195
  }
@@ -511,7 +520,8 @@ export async function buildConfigOptions(server, zcodeSid, receiverRoot) {
511
520
  // extension through typecheck. `set_config_option` on it is a no-op.
512
521
  // Gated on the RECEIVER's connection identity, not process state — a
513
522
  // non-martty client must never see the spec-external string option.
514
- if (server.quotaDock &&
523
+ if (backendCapabilities(server.backendKind).quota &&
524
+ server.quotaDock &&
515
525
  receiverRoot !== undefined &&
516
526
  server.marttyConnectionRoots.has(receiverRoot)) {
517
527
  options.push({