zcode-acp-server 0.48.0 → 0.48.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 +1 -1
- package/dist/backend/adapter.d.ts +129 -0
- package/dist/backend/adapter.d.ts.map +1 -0
- package/dist/backend/adapter.js +71 -0
- package/dist/backend/adapter.js.map +1 -0
- package/dist/backend/client.d.ts +14 -103
- package/dist/backend/client.d.ts.map +1 -1
- package/dist/backend/client.js +63 -413
- package/dist/backend/client.js.map +1 -1
- package/dist/backend/index.d.ts +3 -1
- package/dist/backend/index.d.ts.map +1 -1
- package/dist/backend/index.js +2 -0
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/jsonrpc-child.d.ts +128 -0
- package/dist/backend/jsonrpc-child.d.ts.map +1 -0
- package/dist/backend/jsonrpc-child.js +397 -0
- package/dist/backend/jsonrpc-child.js.map +1 -0
- package/dist/code-fingerprint.json +2 -2
- package/dist/config/options.d.ts.map +1 -1
- package/dist/config/options.js +14 -4
- package/dist/config/options.js.map +1 -1
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +9 -4
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +8 -4
- package/dist/handlers/slash.js.map +1 -1
- package/dist/remote/settings-endpoint.d.ts.map +1 -1
- package/dist/remote/settings-endpoint.js +18 -0
- package/dist/remote/settings-endpoint.js.map +1 -1
- package/dist/server.d.ts +8 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -1
- package/docs/ARCHITECTURE.md +8 -6
- 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":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAK9D,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,aAAa,EACd,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,UAAU,EAAgB,aAAa,EAAE,MAAM,YAAY,CAAC;AAQ1E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8BAAsB,YAAa,YAAW,cAAc;IAC1D,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IACpD,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuB;IAItD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IACnE,OAAO,CAAC,UAAU,CAAS;IAC3B;sEACkE;IAClE,OAAO,CAAC,aAAa,CAAiB;IACtC,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAA6B;IAE7C,SAAS,aAAa,IAAI,EAAE,mBAAmB;IA+B/C;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,KAAK;IAgCb,4EAA4E;IAC5E,SAAS,CAAC,aAAa,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI;IAkB7C,OAAO,CAAC,cAAc;IAQtB,SAAS,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAgB9C,oEAAoE;IACpE,SAAS,CAAC,QAAQ,CAAC,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAE5F;;;;OAIG;IACH,SAAS,CAAC,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO;IAI3D,6DAA6D;IAC7D,SAAS,CAAC,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMjF,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI;IASvE,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI;IAOzE,4DAA4D;IAC5D,kBAAkB,IAAI,aAAa,EAAE;IAKrC;;;;OAIG;IACH,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,IAAI;IAKlD,4EAA4E;IAC5E,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAIrD,8DAA8D;IAC9D,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAMnE,wEAAwE;IACxE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI9D;;;;;OAKG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5D,OAAO,CAAC,UAAU;IAYlB;;;;;;OAMG;IACG,OAAO,CACX,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,SAAS,SAAQ,GAChB,OAAO,CAAC,aAAa,CAAC;IAkCzB;;;;;;;;OAQG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC5B,6DAA6D;IAC7D,OAAO,CAAC,YAAY;IAapB,IAAI,MAAM,IAAI,OAAO,CAEpB;CACF"}
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AA4BxC,MAAM,OAAgB,YAAY;IAGvB,IAAI,CAAe;IACT,IAAI,CAAS;IACf,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC5C,cAAc,GAAoB,EAAE,CAAC;IACtD,6EAA6E;IAC7E,4EAA4E;IAC5E,6DAA6D;IAC5C,SAAS,GAAG,IAAI,GAAG,EAA8B,CAAC;IAC3D,UAAU,GAAG,KAAK,CAAC;IAC3B;sEACkE;IAC1D,aAAa,GAAG,aAAa,CAAC;IACtC,mFAAmF;IAC3E,QAAQ,GAAwB,IAAI,CAAC;IAE7C,YAAsB,IAAyB;QAC7C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;YACnD,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;YACjC,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,QAAQ,EAAE,IAAI,EAAE,sDAAsD;SACvE,CAAC,CAAC;QACH,qEAAqE;QACrE,sEAAsE;QACtE,wEAAwE;QACxE,+CAA+C;QAC/C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC5B,MAAM,IAAI,GACP,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAC9C,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,4BAA4B,IAAI,CAAC,QAAQ,4BAA4B,IAAI,CAAC,SAAS,EAAE;gBAC7G,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC;YAClB,IAAI,CAAC,cAAc,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QACH,0EAA0E;QAC1E,4EAA4E;QAC5E,wEAAwE;QACxE,yEAAyE;QACzE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACnC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,yBAAyB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,GAAG,CAAC,oBAAoB,IAAI,CAAC,QAAQ,SAAS,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;IAClE,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,aAAa;QACnB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC;QAC9B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QACjC,IAAI,CAAC,SAAS,IAAI,CAAC,UAAU;YAAE,OAAO;QACtC,2EAA2E;QAC3E,MAAM,MAAM,GAAG;0BACO,SAAS;2BACR,UAAU;;;;;;;;;;;;;;KAchC,CAAC;QACF,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE;YACtD,KAAK,EAAE,QAAQ;YACf,QAAQ,EAAE,IAAI,EAAE,mDAAmD;SACpE,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,kCAAkC;IAE1B,WAAW;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAChC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YACjC,OAAO;QACT,CAAC;QACD,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9C,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACrB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,IAAI,GAAiB,CAAC;YACtB,IAAI,CAAC;gBACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiB,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,2BAA2B;YACrC,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC,CAAC;IAC7D,CAAC;IAEO,KAAK,CAAC,GAAiB;QAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QAC1B,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC;QAClB,IAAI,EAAE,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC7C,wEAAwE;YACxE,mEAAmE;YACnE,IAAI,CAAC,cAAc,CAAC,EAAY,EAAE,GAA+B,CAAC,CAAC;YACnE,OAAO;QACT,CAAC;QACD,IAAI,EAAE,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC7C,sEAAsE;YACtE,yEAAyE;YACzE,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAY,CAAC,EAAE,CAAC;gBACnC,IAAI,CAAC,cAAc,CAAC,EAAY,EAAE,GAA+B,CAAC,CAAC;YACrE,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,GAAkB;oBACzB,EAAE;oBACF,MAAM;oBACN,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAA4B;iBACtD,CAAC;gBACF,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;oBACnC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;YACD,OAAO;QACT,CAAC;QACD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,2CAA2C;YAC3C,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAA4B,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IAED,4EAA4E;IAClE,aAAa,CAAC,EAAc;QACpC,MAAM,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC;QACzB,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACtD,IAAI,GAAG,EAAE,CAAC;YACR,sEAAsE;YACtE,mCAAmC;YACnC,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;gBAChC,IAAI,CAAC;oBACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;gBAC3B,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,IAAI,CACF,wCAAwC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CACrF,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAEO,cAAc,CAAC,EAAU,EAAE,IAAmB;QACpD,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACxB,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;IAES,cAAc,CAAC,MAAc;QACrC,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,2BAA2B,MAAM,GAAG,CAAC,CAAC;QAC3C,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC,CAAC,OAAO,CAAC;gBACR,EAAE,EAAE,CAAC;gBACL,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,uCAAuC,EAAE;aACxE,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAOD;;;;OAIG;IACO,mBAAmB,CAAC,IAAmB;QAC/C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,6DAA6D;IACnD,gBAAgB,CAAC,GAA4B;QACrD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,oDAAoD;IAEpD,qBAAqB,CAAC,SAAiB,EAAE,QAAuB;QAC9D,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACxC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACrC,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACpB,CAAC;IAED,uBAAuB,CAAC,SAAiB,EAAE,QAAuB;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACrB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;YAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACvD,CAAC;IAED,4DAA4D;IAC5D,kBAAkB;QAChB,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACnE,CAAC;IAED;;;;OAIG;IACH,qBAAqB,CAAC,IAAqB;QACzC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC9B,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,4EAA4E;IAC5E,SAAS,CAAC,EAAmB,EAAE,MAAe;QAC5C,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,8DAA8D;IAC9D,SAAS,CAAC,EAAmB,EAAE,IAAY,EAAE,OAAe;QAC1D,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,uCAAuC;IAEvC,wEAAwE;IACxE,MAAM,CAAC,MAAc,EAAE,MAAgC;QACrD,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,MAAc,EAAE,MAAgC;QACnD,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;IAC9E,CAAC;IAEO,UAAU,CAAC,GAA4B;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC9B,IAAI,CAAC,uCAAuC,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,oEAAoE;QACpE,0EAA0E;QAC1E,4BAA4B;QAC5B,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,CACX,EAAU,EACV,MAAc,EACd,MAAgC,EAChC,SAAS,GAAG,KAAK;QAEjB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,uCAAuC,EAAE,EAAE,CAAC;QACzF,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,EAAE;YACrD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;oBAC5B,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;gBACjD,CAAC;YACH,CAAC,EAAE,SAAS,CAAC,CAAC;YACd,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC;YACH,mEAAmE;YACnE,sEAAsE;YACtE,yDAAyD;YACzD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;YAC9B,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS;gBAAE,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;YAC/D,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,OAAO;gBACL,EAAE;gBACF,KAAK,EAAE;oBACL,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,yBAAyB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;iBAC3F;aACF,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,kCAAkC;IAElC;;;;;;;;OAQG;IACH,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI,CAAC,GAAG;YAAE,OAAO;QACtB,IAAI,CAAC;YACH,kBAAkB;YAClB,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU;gBAAE,OAAO;YACtD,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,qBAAqB;YAC/B,CAAC;YACD,kCAAkC;YAClC,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;gBACpD,IAAI,KAAoC,CAAC;gBACzC,MAAM,IAAI,GAAG,GAAG,EAAE;oBAChB,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,kDAAkD;oBACvE,OAAO,CAAC,IAAI,CAAC,CAAC;gBAChB,CAAC,CAAC;gBACF,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBACxB,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBACtB,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;oBAClC,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjB,CAAC,EAAE,IAAI,CAAC,CAAC;YACX,CAAC,CAAC,CAAC;YACH,IAAI,MAAM;gBAAE,OAAO;YACnB,yCAAyC;YACzC,IAAI,CAAC;gBACH,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;oBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC7E,CAAC;YAAC,MAAM,CAAC;gBACP,eAAe;YACjB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,mEAAmE;YACnE,wEAAwE;YACxE,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,CAAC;IACH,CAAC;IAED,6DAA6D;IACrD,YAAY;QAClB,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QACzB,IAAI,CAAC,EAAE;YAAE,OAAO;QAChB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,eAAe;YACjB,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/config/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/config/options.ts"],"names":[],"mappings":"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"}
|
package/dist/config/options.js
CHANGED
|
@@ -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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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.
|
|
523
|
+
if (backendCapabilities(server.backendKind).quota &&
|
|
524
|
+
server.quotaDock &&
|
|
515
525
|
receiverRoot !== undefined &&
|
|
516
526
|
server.marttyConnectionRoots.has(receiverRoot)) {
|
|
517
527
|
options.push({
|