feat(server): add ZCode provider for Z.AI coding-plan models - #8116
feat(server): add ZCode provider for Z.AI coding-plan models#8116aintoniodev wants to merge 1 commit into
Conversation
T3 Code can drive Codex, Claude, Cursor, Grok, and OpenCode, but not the ZCode CLI that ships with the Z.AI desktop app. Third-party clients that call the Z.AI API with a key also get a smaller quota than the official app. This adds a first-party zcode driver that launches the bundled zcode.cjs app-server with the desktop surface and the user's existing coding-plan login, then maps that protocol into T3 sessions. Made with Grok Build.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Effect Service Conventions: two findings in the new ZCode provider runtime (apps/server/src/provider/zcodeRuntime.ts). Both concern dependency acquisition and typed failures; the rest of the ZCode modules (driver shape, Services/ZCodeAdapter.ts, snapshot/enrich helpers, contracts changes) follow the existing provider conventions.
Posted via Macroscope — Effect Service Conventions
| }): Effect.Effect<ZcodeProtocolClient, Error, Scope.Scope> => | ||
| Effect.acquireRelease( | ||
| Effect.try({ | ||
| try: () => { | ||
| const argv = buildZcodeAppServerArgv({ | ||
| binaryPath: input.binaryPath, | ||
| env: input.environment, | ||
| }); | ||
| const plan = readZcodeDesktopPlan(input.environment); | ||
| const env = buildZcodeDesktopSpawnEnv(input.environment ?? process.env, plan); | ||
| const proc = spawn(argv.command, [...argv.args], { |
There was a problem hiding this comment.
The ZCode app-server process is spawned with node:child_process.spawn instead of being acquired from the Effect environment, so a runtime-backed dependency of two Effect services (makeZcodeAdapter, makeZcodeTextGeneration) is hidden from the layer graph and cannot be substituted in tests. Every other provider runtime in this repo does yield* ChildProcessSpawner.ChildProcessSpawner (opencodeRuntime.ts, Layers/CodexSessionRuntime.ts, acp/AcpSessionRuntime.ts), and ZCodeDriver already lists ChildProcessSpawner in ZCodeDriverEnv — but only the version probe uses it.
Suggest acquiring the spawner here and spawning through it (ChildProcess.make(command, args, { env, cwd })), letting ChildProcessSpawner.ChildProcessSpawner surface in this effect's requirements; that also removes the direct process.kill / setTimeout handling in ZcodeProtocolClient.close() in favour of the handle's lifecycle.
Posted via Macroscope — Effect Service Conventions
| export const startZcodeProtocolClient = (input: { | ||
| readonly binaryPath: string | null | undefined; | ||
| readonly cwd: string; | ||
| readonly environment?: NodeJS.ProcessEnv; | ||
| }): Effect.Effect<ZcodeProtocolClient, Error, Scope.Scope> => |
There was a problem hiding this comment.
The failure channel here (and in zcodeCreateSession / zcodeResumeSession below) is the global Error, constructed with new Error(...). Service failures should be declared with Schema.TaggedErrorClass and structured attributes, with message derived from those attributes — compare OpenCodeRuntimeError in opencodeRuntime.ts. This is also flagged by the repo's globalErrorInEffectFailure: "error" diagnostic in tsconfig.base.json.
The untyped Error then propagates into wrappers that copy cause.message into detail (Layers/ZCodeAdapter.ts startSession, textGeneration/ZCodeTextGeneration.ts), which the conventions call out explicitly. Suggest a ZcodeRuntimeError carrying stable context (e.g. operation/method plus cause) and mapping it to ProviderAdapterProcessError/TextGenerationError from those structural fields rather than from cause.message.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6adf406. Configure here.
| detail: cause instanceof Error ? cause.message : String(cause), | ||
| cause, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Stuck turn after send failure
High Severity
sendTurn publishes turn.started and marks the session running before session/send. If that request fails, the error propagates with no turn.aborted/turn.completed and without clearing activeTurnId, so the thread stays stuck running. OpenCode resets session state and emits turn.aborted on the same failure path.
Reviewed by Cursor Bugbot for commit 6adf406. Configure here.
| payload: { state: "cancelled" }, | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Interrupt leaves approvals open
High Severity
interruptTurn cancels the turn via session/stop but never settles pendingApprovals / pendingUserInputs or emits request.resolved. stopSessionInternal succeeds the Deferreds without client.reply or UI resolve events. Unlike Grok/Claude, open permission prompts can leave the thread unsettled and the ZCode process waiting.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6adf406. Configure here.
| proc.stdin.on("error", () => { | ||
| this.closed = true; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Unread stderr can deadlock
Medium Severity
The ZCode app-server is spawned with piped stderr, but ZcodeProtocolClient only reads stdout. Sibling providers drain stderr. If the CLI writes enough diagnostics to fill the pipe buffer, the child blocks and sessions/text-generation hang with no protocol progress.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6adf406. Configure here.
|
|
||
| client.setServerRequestHandler((request) => { | ||
| handleBuiltinZcodeServerRequest(client, request); | ||
| }); |
There was a problem hiding this comment.
Text gen drops server replies
Medium Severity
The text-generation server-request handler only runs handleBuiltinZcodeServerRequest and never replies when that returns false. Any non-builtin interaction/* request during commit/title generation leaves the app-server waiting until the 180s timeout instead of failing fast or auto-resolving.
Reviewed by Cursor Bugbot for commit 6adf406. Configure here.
| probe: { | ||
| installed: false, | ||
| version: null, | ||
| status: "error", | ||
| auth: { status: "unknown" }, | ||
| message: | ||
| "ZCode CLI is not installed. Install the ZCode desktop app from https://zcode.z.ai.", | ||
| }, |
There was a problem hiding this comment.
🟡 Medium Layers/ZCodeProvider.ts:211
A non-.cjs CLI that is found and executes --version but exits nonzero is reported as installed: false with “ZCode CLI is not installed,” so a transient version-probe failure is misdiagnosed as a missing installation. Keep installed true and report the version health-check failure instead.
probe: {
- installed: false,
+ installed: true,
version: null,
status: "error",
auth: { status: "unknown" },
- message:
- "ZCode CLI is not installed. Install the ZCode desktop app from https://zcode.z.ai.",
+ message: "Failed to execute ZCode CLI health check.",🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ZCodeProvider.ts around lines 211-218:
A non-`.cjs` CLI that is found and executes `--version` but exits nonzero is reported as `installed: false` with “ZCode CLI is not installed,” so a transient version-probe failure is misdiagnosed as a missing installation. Keep `installed` true and report the version health-check failure instead.
| proc.stdin.on("error", () => { | ||
| this.closed = true; | ||
| }); |
There was a problem hiding this comment.
🟡 Medium provider/zcodeRuntime.ts:158
A broken proc.stdin pipe leaves in-flight request() promises pending until their 30-second timeouts and can leave the child process running. The handler only sets closed, so close() immediately returns and never rejects pending requests or performs cleanup; invoke close() from the error handler instead.
- proc.stdin.on("error", () => {
- this.closed = true;
- });
+ proc.stdin.on("error", () => {
+ void this.close();
+ });🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/zcodeRuntime.ts around lines 158-160:
A broken `proc.stdin` pipe leaves in-flight `request()` promises pending until their 30-second timeouts and can leave the child process running. The handler only sets `closed`, so `close()` immediately returns and never rejects pending requests or performs cleanup; invoke `close()` from the error handler instead.
|
|
||
| constructor(proc: ChildProcessWithoutNullStreams) { | ||
| this.proc = proc; | ||
| this.reader = createInterface({ input: proc.stdout }); |
There was a problem hiding this comment.
🟠 High provider/zcodeRuntime.ts:146
The app-server eventually hangs and protocol requests time out when ZCode's piped stderr buffer fills, because ZcodeProtocolClient never consumes proc.stderr. Drain, log, or inherit stderr so diagnostics cannot block the child.
| this.reader = createInterface({ input: proc.stdout }); | |
| this.reader = createInterface({ input: proc.stdout }); | |
| proc.stderr.on("data", () => {}); |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/zcodeRuntime.ts around line 146:
The app-server eventually hangs and protocol requests time out when ZCode's piped `stderr` buffer fills, because `ZcodeProtocolClient` never consumes `proc.stderr`. Drain, log, or inherit `stderr` so diagnostics cannot block the child.
| const versionOutput = versionResult.success.value; | ||
| const version = parseGenericCliVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`); | ||
|
|
||
| if (versionOutput.code !== 0 && !cliPath.endsWith(".cjs")) { |
There was a problem hiding this comment.
🟠 High Layers/ZCodeProvider.ts:205
An authenticated desktop plan is reported as ready even when the resolved .cjs bundle exits nonzero, so a corrupt installation or runtime exception is advertised as usable. The versionOutput.code !== 0 && !cliPath.endsWith(".cjs") guard deliberately skips the failure path for every .cjs bundle; handle every nonzero exit as a failed health check instead.
| if (versionOutput.code !== 0 && !cliPath.endsWith(".cjs")) { | |
| if (versionOutput.code !== 0) { |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ZCodeProvider.ts around line 205:
An authenticated desktop plan is reported as `ready` even when the resolved `.cjs` bundle exits nonzero, so a corrupt installation or runtime exception is advertised as usable. The `versionOutput.code !== 0 && !cliPath.endsWith(".cjs")` guard deliberately skips the failure path for every `.cjs` bundle; handle every nonzero exit as a failed health check instead.
| function looksLikeElectronLauncher(path: string): boolean { | ||
| try { | ||
| const head = NodeFs.readFileSync(path, { encoding: "utf8" }).slice(0, 8_192); | ||
| return ( | ||
| head.includes("electron") || head.includes("ELECTRON_IS_DEV") || head.includes("app.asar") | ||
| ); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Medium Drivers/ZCodeExecutable.ts:26
looksLikeElectronLauncher reads and UTF-8-decodes the entire launcher on every resolution or status probe, so a large Electron executable blocks the server synchronously and allocates memory proportional to its full size. Open the file and read only a bounded 8 KiB prefix before checking the markers.
- try {
- const head = NodeFs.readFileSync(path, { encoding: "utf8" }).slice(0, 8_192);
+ let fd: number | undefined;
+ try {
+ fd = NodeFs.openSync(path, "r");
+ const prefix = Buffer.alloc(8_192);
+ const bytesRead = NodeFs.readSync(fd, prefix, 0, prefix.length, 0);
+ const head = prefix.toString("utf8", 0, bytesRead);
return (
head.includes("electron") || head.includes("ELECTRON_IS_DEV") || head.includes("app.asar")
);
} catch {
return false;
+ } finally {
+ if (fd !== undefined) NodeFs.closeSync(fd);
}
}🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Drivers/ZCodeExecutable.ts around lines 26-35:
`looksLikeElectronLauncher` reads and UTF-8-decodes the entire launcher on every resolution or status probe, so a large Electron executable blocks the server synchronously and allocates memory proportional to its full size. Open the file and read only a bounded 8 KiB prefix before checking the markers.
| env: environment, | ||
| }); | ||
| const versionArgs = | ||
| argv.args[0] && isZcodeNodeBundle(argv.args[0]) ? [argv.args[0], "--version"] : ["--version"]; |
There was a problem hiding this comment.
🟡 Medium Layers/ZCodeProvider.ts:111
A .js or .mjs bundle that executes successfully but exits non-zero for --version is reported as not installed, just like a failed CLI. runZcodeVersionCommand recognizes all three Node bundle extensions, but the status check only exempts .cjs via cliPath.endsWith(".cjs"); use isZcodeNodeBundle(cliPath) there so .js and .mjs receive the same handling.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ZCodeProvider.ts around line 111:
A `.js` or `.mjs` bundle that executes successfully but exits non-zero for `--version` is reported as not installed, just like a failed CLI. `runZcodeVersionCommand` recognizes all three Node bundle extensions, but the status check only exempts `.cjs` via `cliPath.endsWith(".cjs")`; use `isZcodeNodeBundle(cliPath)` there so `.js` and `.mjs` receive the same handling.
| yield* Effect.tryPromise({ | ||
| try: () => | ||
| ctx.client.request("session/send", { | ||
| sessionId: ctx.zcodeSessionId, | ||
| content: input.input ?? "", | ||
| }), | ||
| catch: (cause) => | ||
| new ProviderAdapterRequestError({ | ||
| provider: PROVIDER, | ||
| method: "session/send", | ||
| detail: cause instanceof Error ? cause.message : String(cause), | ||
| cause, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
🟠 High Layers/ZCodeAdapter.ts:721
When session/send rejects or times out, sendTurn returns an error but leaves ctx.activeTurnId set and ctx.session.status as "running"; listSessions then exposes a stuck turn and the next sendTurn reuses its stale turnId. Restore the session to ready and clear the active turn in the request's error path.
- yield* Effect.tryPromise({
+ yield* Effect.tryPromise({
try: () =>
ctx.client.request("session/send", {
sessionId: ctx.zcodeSessionId,
content: input.input ?? "",
@@
}),
});
+ }).pipe(
+ Effect.tapError(() =>
+ Effect.sync(() => {
+ const { activeTurnId: _cleared, ...readySession } = ctx.session;
+ ctx.activeTurnId = undefined;
+ ctx.assistantItemStarted = false;
+ ctx.session = { ...readySession, status: "ready" };
+ }),
+ ),
+ );🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ZCodeAdapter.ts around lines 721-734:
When `session/send` rejects or times out, `sendTurn` returns an error but leaves `ctx.activeTurnId` set and `ctx.session.status` as `"running"`; `listSessions` then exposes a stuck turn and the next `sendTurn` reuses its stale `turnId`. Restore the session to ready and clear the active turn in the request's error path.
| detail: cause instanceof Error ? cause.message : String(cause), | ||
| cause, | ||
| }), | ||
| }).pipe(Effect.ignore); |
There was a problem hiding this comment.
🟠 High Layers/ZCodeAdapter.ts:707
A rejected or timed-out session/setModel request is ignored, but ctx.currentModelId and ctx.session.model are still set to the requested model. Subsequent turns therefore suppress retries for that model while ZCode continues generating with the old one. Let the request failure propagate before updating the adapter state.
| }).pipe(Effect.ignore); | |
| }); |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ZCodeAdapter.ts around line 707:
A rejected or timed-out `session/setModel` request is ignored, but `ctx.currentModelId` and `ctx.session.model` are still set to the requested model. Subsequent turns therefore suppress retries for that model while ZCode continues generating with the old one. Let the request failure propagate before updating the adapter state.
| if (input.interactionMode === "plan") { | ||
| yield* Effect.tryPromise({ | ||
| try: () => | ||
| ctx.client.request("session/setMode", { | ||
| sessionId: ctx.zcodeSessionId, | ||
| mode: "plan", | ||
| }), | ||
| catch: (cause) => | ||
| new ProviderAdapterRequestError({ | ||
| provider: PROVIDER, | ||
| method: "session/setMode", | ||
| detail: cause instanceof Error ? cause.message : String(cause), | ||
| cause, | ||
| }), | ||
| }).pipe(Effect.ignore); | ||
| } |
There was a problem hiding this comment.
🟠 High Layers/ZCodeAdapter.ts:671
After a plan turn, later non-plan turns leave the persistent ZCode session in plan mode, so ordinary prompts continue running without the expected build/edit actions. sendTurn only calls session/setMode for interactionMode === "plan"; set the mode on every turn using mapRuntimeModeToZcodeMode(input.runtimeMode, input.interactionMode).
| if (input.interactionMode === "plan") { | |
| yield* Effect.tryPromise({ | |
| try: () => | |
| ctx.client.request("session/setMode", { | |
| sessionId: ctx.zcodeSessionId, | |
| mode: "plan", | |
| }), | |
| catch: (cause) => | |
| new ProviderAdapterRequestError({ | |
| provider: PROVIDER, | |
| method: "session/setMode", | |
| detail: cause instanceof Error ? cause.message : String(cause), | |
| cause, | |
| }), | |
| }).pipe(Effect.ignore); | |
| } | |
| const mode = mapRuntimeModeToZcodeMode(input.runtimeMode, input.interactionMode); | |
| yield* Effect.tryPromise({ | |
| try: () => | |
| ctx.client.request("session/setMode", { | |
| sessionId: ctx.zcodeSessionId, | |
| mode, | |
| }), | |
| catch: (cause) => | |
| new ProviderAdapterRequestError({ | |
| provider: PROVIDER, | |
| method: "session/setMode", | |
| detail: cause instanceof Error ? cause.message : String(cause), | |
| cause, | |
| }), | |
| }).pipe(Effect.ignore); |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ZCodeAdapter.ts around lines 671-686:
After a plan turn, later non-plan turns leave the persistent ZCode session in `plan` mode, so ordinary prompts continue running without the expected build/edit actions. `sendTurn` only calls `session/setMode` for `interactionMode === "plan"`; set the mode on every turn using `mapRuntimeModeToZcodeMode(input.runtimeMode, input.interactionMode)`.
| const modelIds = modelIdsFromProvider(selected.provider); | ||
| const credentials = readJsonFile(NodePath.join(home, "v2", "credentials.json")); | ||
| const authenticated = isRecord(credentials) | ||
| ? Object.keys(credentials).some( |
There was a problem hiding this comment.
🟠 High provider/zcodeDesktopAuth.ts:92
desktopPlan.authenticated becomes true when credentials.json contains an unrelated provider token, so checkZcodeProviderStatus reports ZCode as ready even though Z.AI session creation fails without credentials for the selected provider. The broad oauth: and token key checks must be restricted to credential keys associated with selected.id.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/zcodeDesktopAuth.ts around line 92:
`desktopPlan.authenticated` becomes `true` when `credentials.json` contains an unrelated provider token, so `checkZcodeProviderStatus` reports ZCode as ready even though Z.AI session creation fails without credentials for the selected provider. The broad `oauth:` and `token` key checks must be restricted to credential keys associated with `selected.id`.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a substantial new ZCode provider, including a subprocess protocol, persistent sessions, approvals, model switching, desktop credential/quota integration, and text-generation workflows across the server and clients. The scope and unresolved lifecycle, authorization, process-cleanup, and authentication-related risks warrant human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |


Problem
T3 Code can control Codex, Claude, Cursor, Grok, and OpenCode, but not ZCode. People with a Z.AI coding-plan subscription have to leave T3, and calling GLM through a third-party client (Pi, raw API keys) uses a smaller quota than the official ZCode app.
Change
Add a
zcodedriver that:zcode.cjs), not the Electron wrapperapp-server --surface desktopwith the existing coding-plan loginFocused tests cover CLI discovery, protocol parsing, settings defaults, and a mock session/turn.
Notes
ZCode is off until you enable it in Settings. Install the ZCode app and sign in there, or run
zcode login.Made with Grok Build.
Note
Medium Risk
Large new provider surface (child processes, local credential reads, permission bridging) with broad contract and UI touchpoints; behavior is mostly isolated behind the adapter pattern but subprocess and auth edge cases can affect session reliability.
Overview
Adds ZCode as a sixth built-in provider so T3 can drive the Z.ai desktop agent from web, mobile, and settings like the other CLIs.
On the server, a new
zcodedriver spawns the bundledzcode.cjsentry (not the Electronzcodelauncher) asapp-server --surface desktop, speaks line-delimited ZCode Protocol JSON, and maps sessions, streaming, tools, permissions, and user-input/plan prompts into T3 runtime events. Desktop auth is read from~/.zcodeso sessions use the same Z.AI coding-plan OAuth as the official app. Provider health checks probe CLI install/version and login state; ZCodeTextGeneration reuses the protocol for commit/PR/title helpers.Contracts add
ZCodeSettings(off by default), GLM model defaults/aliases, andzcode.protocol.eventas a raw runtime source. Web and mobile wire ZCode into the provider picker, settings cards, and icons. User docs cover install, binary path, and quota behavior; a mock app-server supports adapter tests.Reviewed by Cursor Bugbot for commit 6adf406. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add ZCode provider for Z.AI coding-plan models
ZCodeSettingsschema (enabledfalseby default,binaryPath,customModels) and contracts constants mappingZCODE_DRIVER_KINDto default modelsGLM-5.3andGLM-5-Turbo.ZCodeDriverinBUILT_IN_DRIVERSand adds ZCode to the web and mobile UI provider pickers with dedicated icons.makeZcodeTextGenerationin ZCodeTextGeneration.ts to fulfill text generation requests using a local ZCode runtime with a 180s timeout.ZcodeProtocolClientspawns a detached ZCode app-server child process; verifyclose()terminates the process group cleanly on non-Windows platforms.📊 Macroscope summarized 6adf406. 26 files reviewed, 28 issues evaluated, 8 issues filtered, 15 comments posted
🗂️ Filtered Issues
apps/server/src/provider/Drivers/ZCodeExecutable.ts — 1 comment posted, 2 evaluated, 1 filtered
whichOnPathtreats any regular file as a usable PATH command and returns it without checking executable permission. On Unix, a non-executable file namedzcodein an earlier PATH directory therefore shadows a valid executable in a later directory; the returned path is then spawned directly and fails withEACCESinstead of discovering the working CLI. [ Out of scope (post-validation triage) ]apps/server/src/provider/Layers/ZCodeAdapter.ts — 6 comments posted, 7 evaluated, 1 filtered
ctx.interruptedTurnIds, but no path ever removes IDs from this set. A long-lived session that repeatedly interrupts turns accumulates one permanent entry per interruption, causing unbounded memory growth until the whole session is stopped. [ Out of scope (post-validation triage) ]apps/server/src/provider/zcodeRuntime.ts — 2 comments posted, 13 evaluated, 6 filtered
ZcodeProtocolClientnever registers anerrorlistener onproc.spawn()reports failures such as a missing configured binary or nonexistentcwdasynchronously via the child processerrorevent, so the surroundingEffect.trycannot catch them; an unhandled EventEmittererrorthen terminates the server instead of returning a provider startup error. [ Out of scope ]closecallback callsfailAll, which setsclosed = true. If the child closes stdout without exiting, subsequent scope cleanup callsclose()and returns immediately at itsclosedguard, so the detached ZCode process is never sentSIGTERM/SIGKILLand can leak indefinitely. [ Already posted ]ZcodeProtocolClientnever registers anerrorlistener onproc. Node emits this event whenspawncannot start the command (for example, the configured ZCode binary orcwdno longer exists), andspawn()returns before that asynchronous failure, so the surroundingEffect.trycannot catch it. The unhandlederrorevent can terminate the entire server instead of returning a provider startup error. [ Already posted ]onLinetrusts any successfully parsed JSON as aZcodeInboundMessageand immediately readsmessage.id. A valid JSON line such asnullcauses aTypeErrorin the readline event callback, which is uncaught and can crash the server. The parsed value should be validated as a non-null object before property access. [ Exceeded comment limit ]idandmethodare server requests, but this branch treats one as a response whenever its ID matches an outstanding client request. Client and server request ID namespaces can overlap (for example, both sides may begin at1), so a runtime-preferences/permission request can incorrectly resolvesession/createwithundefinedand is never delivered or replied to, breaking the session. [ Out of scope ]idandmethodare server requests, but this branch reclassifies them as responses whenever the server-chosen ID happens to match a client request inpending. Bidirectional RPC peers have independent request-ID namespaces, so a normal collision (for example both sides using ID1) resolves the unrelated client request withundefinedand silently drops the server request instead of invokingserverRequestHandler. [ Exceeded comment limit ]