Skip to content

Add in-process FFI transport for Rust and TypeScript SDKs#1915

Draft
SteveSandersonMS wants to merge 33 commits into
mainfrom
stevesa/ffi-inproc-rust-ts
Draft

Add in-process FFI transport for Rust and TypeScript SDKs#1915
SteveSandersonMS wants to merge 33 commits into
mainfrom
stevesa/ffi-inproc-rust-ts

Conversation

@SteveSandersonMS

Copy link
Copy Markdown
Contributor

Summary

Ports the C# in-process FFI hosting transport (merged in #1901) to the Rust and TypeScript/Node SDKs, mirroring the .NET RuntimeConnection.ForInProcess() API. Both SDKs load the runtime cdylib and speak JSON-RPC over its C ABI (copilot_runtime_host_start / connection_open / connection_write / connection_close / host_shutdown) instead of spawning a stdio/TCP child process. Framing is unchanged — it's a transport swap over the existing LSP Content-Length JSON-RPC codec.

Rust

  • New Transport::InProcess variant; COPILOT_SDK_DEFAULT_CONNECTION (inprocess/stdio/unset) selects the default, mirroring .NET's ResolveDefaultConnection.
  • rust/src/ffi.rs: libloading C-ABI bindings, AsyncRead/AsyncWrite bridge between the outbound callback and connection_write, ordered teardown.
  • build.rs / embeddedcli.rs: best-effort extract + rename of prebuilds/<platform>/runtime.nodelibcopilot_runtime.{so,dylib} / copilot_runtime.dll next to the CLI (mirrors the .NET .targets).
  • Discovery: flat natural lib name, else prebuilds/<node-platform>-<arch>/runtime.node.
  • In-process E2E test + test-inprocess CI job.

TypeScript

  • RuntimeConnection.forInProcess() + InProcessRuntimeConnection.
  • nodejs/src/ffiRuntimeHost.ts: koffi loads runtime.node directly from the @github/copilot-<platform> package (no rename); async host_start, registered outbound callback bridged to vscode-jsonrpc streams.
  • COPILOT_SDK_DEFAULT_CONNECTION default + in-process E2E test + transport CI matrix cell.

CI

Both workflows gain an inprocess transport cell that sets COPILOT_SDK_DEFAULT_CONNECTION=inprocess, matching dotnet-sdk-tests.yml.

Status / known gaps (draft — letting CI surface these)

  • Dedicated in-process E2E tests pass locally for both SDKs; Node client.e2e (incl. auth + list-models) passes fully in-process.
  • Node MCP tests time out in-process (mcp_and_agents): the in-process host addon runs in the SDK's own process and reads process.env, so the harness-supplied proxy-redirect env (passed as options.env) doesn't reach MCP-related host-side paths the way it does for a spawned stdio child. Needs follow-up on env propagation for the in-process worker/host.
  • Rust default (bundled, no COPILOT_CLI_PATH) path is untested locally because the GitHub-release CLI tarball doesn't yet ship runtime.node; CI uses the npm platform package via COPILOT_CLI_PATH.

🤖 Generated with in-process FFI port; opening as draft to let CI run the full matrix.

id: setup-copilot

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.94.0"

- uses: Swatinem/rust-cache@v2
Comment thread rust/src/ffi.rs
fn bind<'lib, T>(
lib: &'lib Library,
symbol: &[u8],
library_path: &Path,
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

SteveSandersonMS and others added 2 commits July 7, 2026 13:49
Ports the C# in-process FFI hosting transport to the Rust and Node SDKs,
mirroring the .NET `RuntimeConnection.ForInProcess()` API. Both load the
runtime cdylib and speak JSON-RPC over its C ABI (host_start/connection_*)
instead of spawning a stdio/TCP child; framing is unchanged (a transport swap
over the existing LSP Content-Length codec).

Rust:
- `Transport::InProcess` variant + `COPILOT_SDK_DEFAULT_CONNECTION` default
- `src/ffi.rs`: libloading C-ABI bindings, AsyncRead/AsyncWrite bridge, teardown
- build.rs/embeddedcli extract+rename runtime.node -> libcopilot_runtime.*
- in-process E2E test + CI matrix cell

TypeScript:
- `RuntimeConnection.forInProcess()` + `InProcessRuntimeConnection`
- `src/ffiRuntimeHost.ts`: koffi loads runtime.node from node_modules directly
- `COPILOT_SDK_DEFAULT_CONNECTION` default + in-process E2E test + CI matrix cell

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/ffi-inproc-rust-ts branch from 9587efd to 259911f Compare July 7, 2026 13:49
@github-actions

This comment has been minimized.

Per-client options that lower to environment variables are not honored by the
in-process transport, because the native runtime loads into the shared host
process and its worker inherits that process's ambient environment. Match .NET:
the in-process path no longer wires `env`, `telemetry`, `gitHubToken`, or
`baseDirectory` into the FFI worker (previously it passed a partially-honored env
block); the worker now inherits the ambient host environment. This is tracked as
a shared cross-SDK gap in #1934 (workaround: set the variables on the host
process environment).

- client.ts: startInProcessFfi passes no per-client env; buildRuntimeEnv is now
  child-process (stdio/tcp) only.
- types.ts: document + mark @experimental that in-process ignores env-lowered
  options; point to #1934.
- E2E harness: for in-process, mirror the per-test redirects/home/credentials
  onto the real process env (Node process.env writes reach native getenv), route
  auth via GH_TOKEN/GITHUB_TOKEN, disable HMAC so host-side auth picks the Bearer
  token the replay snapshots expect, and restore the mutated entries afterwards.
- Narrow the dedicated in-process E2E test to the single explicit ForInProcess
  smoke test, matching .NET; env-var resolution is covered by the inprocess CI
  matrix cell running the full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1915 · sonnet46 2M

Comment thread nodejs/src/types.ts
* corresponding environment variables on the host process instead. See
* https://github.com/github/copilot-sdk/issues/1934.
*/
forInProcess(): InProcessRuntimeConnection {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency note — option validation approach differs from .NET

The .NET SDK (added in #1901) throws ArgumentException at construction time if Environment or Telemetry options are set alongside ForInProcess():

// dotnet/src/Client.cs
if (options.Environment is not null)
    throw new ArgumentException("...per-client values. Set the variables on the host process environment instead.");
if (options.Telemetry is not null)
    throw new ArgumentException("...Configure telemetry via the host process environment...");

TypeScript currently documents the limitation in the @experimental tag but silently ignores env, telemetry, gitHubToken, and baseDirectory at runtime without any error or warning. This means a caller who sets gitHubToken and then switches to in-process transport will see silent auth failures rather than a clear error.

Worth considering whether to match .NET's fail-fast approach (throw during CopilotClient construction when incompatible options are detected) so the SDK surface is consistent across languages. Rust takes a third path: it explicitly builds the env and passes it to host_start via the C ABI, so those options are actually honored there.

My previous commit mirrored the per-test environment onto process.env once at
context construction. Since a test file contains several createSdkTestContext
calls (each with its own CapiProxy on a distinct port) and process.env is a
single global, the last-constructed context won — so every in-process worker
inherited the wrong proxy URL and requests hit an unconfigured/torn-down proxy
(ECONNREFUSED / "ReplayingCapiProxy not yet initialized"). That regressed the
in-process cell from 9 to 32 failures.

Mirror per-test instead: apply this context's environment in beforeEach (the
client auto-starts on first use inside the test body, so the worker spawns under
the right values) and restore in afterEach. vitest runs tests within a file
serially and separate files in separate forks, so the global process.env is
coherent for the running test.

Also neutralize the ambient COPILOT_HMAC_KEY/CAPI_HMAC_KEY at module load (the
analogue of .NET's InProcessEnvIsolation ModuleInitializer): in-process auth
resolves host-side in this process and ranks HMAC above the GitHub token, and
the key can be captured as early as client construction — before any beforeEach
runs — so clearing it only per-test is too late. Gated to the in-process
transport; stdio/tcp resolve auth in their own process where the token outranks
HMAC.

Verified locally with an ambient COPILOT_HMAC_KEY set: the previously-failing
multi-context files (rpc, session, rpc_session_state) now pass in-process, and
their embedded stdio/tcp sub-tests are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1915 · sonnet46 2.4M

Comment thread nodejs/src/client.ts
* values. Configure the in-process runtime via the host process environment instead.
* See https://github.com/github/copilot-sdk/issues/1934.
*/
private async startInProcessFfi(): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency note: The Rust SDK's build_ffi_environment (rust/src/lib.rs:1439) explicitly applies per-client options — github_tokenCOPILOT_SDK_AUTH_TOKEN, telemetry, base_directory, custom env — to the in-process transport. This method silently ignores those same options, inheriting only the host process's ambient environment.\n\nThis creates a cross-SDK behavioral difference: a user who sets gitHubToken: 'ghp_...' will have auth work automatically in the Rust SDK but will need to manually set COPILOT_SDK_AUTH_TOKEN in the process environment for the TypeScript SDK.\n\nFor short-term alignment with .NET's approach, consider adding validation that throws when options that can't be honored are set in combination with forInProcess(), e.g.:\nts\nif (this.options.gitHubToken) {\n throw new Error(\n 'CopilotClientOptions.gitHubToken is not currently supported with the in-process transport. ' +\n 'Set COPILOT_SDK_AUTH_TOKEN on the host process environment instead. ' +\n 'See https://github.com/github/copilot-sdk/issues/1934.'\n );\n}\n\nThis avoids silent misconfiguration until #1934 is resolved.

Comment thread rust/src/lib.rs Outdated
});
}

fn build_ffi_environment(options: &ClientOptions) -> Vec<(String, String)> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency note: This Rust build_ffi_environment correctly applies per-client options (github_token, telemetry, base_directory, custom env) to the in-process transport, which is more user-friendly than .NET's approach (which rejects Environment/Telemetry with ArgumentException and silently ignores GitHubToken).\n\nHowever, the TypeScript SDK in this same PR takes a third approach: it silently ignores all per-client options for in-process transport (tracked in #1934). It would be worth adding a comment here or in the Rust client docs noting that the Rust in-process transport honors per-client options, since this is a divergence from the TypeScript behavior in this PR.

Four tests exercise behavior the in-process (FFI) transport cannot support,
because the runtime loads into the shared host process. Skip them when
COPILOT_SDK_DEFAULT_CONNECTION=inprocess (via a shared `isInProcessTransport`
guard / it.skipIf); they remain covered by the default (stdio) cell. This
matches .NET, which pins the equivalent tests to stdio.

- telemetry file export: telemetry lowers to env vars the in-process worker
  can't receive per-client (#1934).
- copilot_request_cancel_error "fires ctx.signal": a CopilotRequestHandler
  registers a process-wide LLM inference provider, so a second in-process client
  can't register while another holds it.
- rpc_workspace_checkpoints unknown-checkpoint: readCheckpoint is answered by
  the native runtime in-process, which decodes the id as u32 and rejects the
  MAX_SAFE_INTEGER sentinel (runtime gap, #1934).
- client.test.ts child-process-kill: no child process exists in-process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Bring the Rust in-process transport in line with the .NET and TypeScript SDKs.

Product:
- Transport::InProcess passes no per-client env to the FFI worker; the worker
  inherits this host process's ambient environment. Per-client options that lower
  to env vars (env/env_remove, telemetry, github_token, base_directory) are not
  honored in-process, matching .NET/TS; documented as experimental, pointing at
  #1934. Removes the now-unused build_ffi_environment.

E2E harness:
- InProcessEnvGuard mirrors each test's environment onto the real process
  environment (which the in-process worker inherits) and restores it on drop,
  plus disables ambient HMAC so host-side auth picks the token the snapshots
  expect. Forces serial execution in-process (concurrency 1) so the process-wide
  env mutation is coherent; stdio/tcp are unchanged.
- skip_inprocess guard skips tests for features the in-process transport can't
  support; applied to the telemetry file-export test (telemetry lowers to env
  vars) and the unknown-checkpoint test (readCheckpoint decodes the id as u32
  natively in-process and rejects the i64::MAX sentinel).

CI:
- The in-process job now runs the whole E2E suite over the in-process transport
  (serially) instead of only the smoke test, mirroring the .NET/TS inprocess cell.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

SteveSandersonMS and others added 2 commits July 7, 2026 15:23
The in-process FFI host has no libuv handle of its own, so koffi delivers inbound
(server→client) frames only when the event loop turns. The keep-alive timer used
a 60s interval, which kept the loop from exiting but let it park between ticks.
When the SDK issues a bare request and only awaits the response — e.g. the
sequential `session.rpc.permissions.*` calls — there is no other loop activity,
so on macOS the inbound response frame sat undelivered until the next tick and
the round-trip stalled past the test timeout. Streaming turns kept the loop busy
and so were unaffected, which is why only a handful of RPC-only tests hung and
only on macOS.

Shorten the pump interval so inbound frames are serviced within a few
milliseconds. The empty callback is cheap and only runs while a connection is
open. This is the koffi analogue of the .NET host's thread-safe Channel
callback, which wakes its reader immediately regardless of loop state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The `test` and `bundle` jobs are 3-OS matrices but their names omitted the OS,
so all three cells showed identical names. The in-process job named itself
"(in-process transport)" without the OS. Align all with the Node/.NET convention
"(<os>, <transport>)":
- test: "Rust SDK Tests (${{ matrix.os }}, default)"
- test-inprocess: "Rust SDK Tests (ubuntu-latest, inprocess)"
- bundle: "Rust SDK Bundled CLI Build (${{ matrix.os }})"

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

The native outbound callback previously wrote inbound frames straight to the
receive stream, which synchronously drives frame parsing and JSON-RPC dispatch —
and can re-enter the SDK's write path — all while still on the native call stack.
For most flows this worked, but the `session.rpc.permissions.*` round-trips hung
on macOS (silent 30s timeouts), consistent with a re-entrant delivery stalling
against an in-flight connection_write.

Copy the frame bytes eagerly (the native pointer is only valid during the call)
but defer delivery to the receive stream to a setImmediate drain on a clean
stack. This keeps the callback minimal and non-reentrant — the koffi analogue of
the .NET host's thread-safe Channel callback, which enqueues and returns
immediately. Combined with the short keep-alive pump, queued frames are delivered
within a few milliseconds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1915 · sonnet46 2M

Comments that could not be inline-anchored

nodejs/src/client.ts:639

Cross-SDK consistency: missing eager validation for inprocess + incompatible options

The .NET SDK (reference implementation since #1901) throws an ArgumentException in the constructor when Environment or Telemetry options are set alongside InProcessRuntimeConnection — see Client.cs ValidateEnvironmentOptions(). The new TypeScript implementation only documents the limitation in JSDoc but silently ignores those options, which can be confusing.

Suggest adding a "fail fast" chec…

rust/src/lib.rs:1012

Cross-SDK consistency: missing eager validation for Transport::InProcess + incompatible options

The .NET SDK (reference since #1901) rejects incompatible options early with ArgumentException. The TypeScript implementation (also added in this PR) has the same gap. For Rust, suggest adding a validation block after the External checks, mirroring the pattern here:

if matches!(options.transport, Transport::InProcess) {
    if options.github_token.is_some() {
        return Err(Er…

</details>

SteveSandersonMS and others added 2 commits July 7, 2026 15:57
The in-process job was ubuntu-only, so the transport was never exercised on
macOS. Make it a matrix over ubuntu-latest and macos-latest (excluding windows,
matching dotnet-sdk-tests.yml, pending in-process SQLite file-locking on
shutdown), and pass --test-threads=1 so libtest also runs serially (the harness
mirrors each test's environment onto the shared process environment).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root-cause fix for hung session.rpc.permissions.* round-trips on macOS: the SDK
called connection_write synchronously, blocking the single JS thread inside the
native call. koffi delivers inbound frames (the response, or a server→client
request) on a secondary thread and queues them to run when the event loop gets a
turn — but a synchronous write never yields, so when the runtime produced the
response during the write, main and worker deadlocked. koffi's own docs warn
about exactly this. Issue the write via koffi's async FFI call so the event loop
stays free to service inbound callbacks; Node's Writable still serializes writes,
preserving frame order.

Also guard both Node-API callbacks (the native outbound callback and the async
write completion) with try/catch that logs, since a throw across the FFI boundary
is otherwise swallowed and only surfaces as a DEP0168 "Uncaught Node-API callback
exception" warning. This both hardens teardown (a late inbound frame after
dispose can no longer escape) and reveals the underlying error when it occurs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

The in-process cell is green, but teardown emitted repeated DEP0168 "Uncaught
Node-API callback exception" warnings. They come from inside koffi, not the
SDK's callbacks (the guards added alongside never logged): koffi delivers the
native outbound callback from a secondary thread by queuing it onto the event
loop, and at teardown one such delivery can still be queued when dispose() calls
koffi.unregister synchronously. Unregistering while a queued call is pending
makes koffi invoke a freed callback and raise inside its own native code, which
no JS try/catch can intercept.

Defer the unregister to a setImmediate: the native connection and host are
already closed by then, so no new deliveries originate, and a pending delivery
fires in libuv's poll phase with the callback still valid (it no-ops since we're
disposed) before the check-phase setImmediate frees the slot. The immediate is
unref'd so it never keeps the process alive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Adds env-gated (COPILOT_FFI_TRACE=1) stderr tracing of FFI frame writes, write
completions, inbound arrivals, deliveries, and an event-loop heartbeat, with
JSON-RPC id/method correlation. Enabled on the Node inprocess CI cell to capture
exactly where the macOS session.rpc.permissions.* round-trips stall (write vs
inbound delivery vs loop parking). To be reverted once the root cause is fixed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

SteveSandersonMS and others added 2 commits July 7, 2026 17:09
Trace analysis showed inbound frame delivery stops mid-stream on macOS (a
streaming turn's events flow then halt), not just on idle bare RPCs. The async
(libuv-threadpool) write variant competes with koffi's blocking inbound callback
relay, so revert connection_write to a synchronous call matching the .NET host
and drop the async broker pump. Also add a repro scenario that blasts a rapid
burst of background-thread callbacks including a 23638-byte payload (the size
that preceded the stall) to test whether koffi drops frames under burst on macOS.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the one untested koffi interaction matching real in-process usage: the
inbound callback (blocking the reader thread via napi_tsfn_blocking) synchronously
re-enters native (like connection_write from inside feedInbound). If this passes on
macOS, koffi is fully exonerated and the stall is runtime/worker-side.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

koffi 3.0 rewrote call preparation/execution ("vastly improved performance") and
distributes prebuilt binaries as @koromix/koffi-<platform> optionalDependency
subpackages (so `npm ci --ignore-scripts` still resolves the native binary).
Testing whether the newer callback/threading machinery fixes the macOS/Windows
in-process RPC stalls. Our koffi API surface (load/func/pointer/proto/register/
unregister/decode/array) is unchanged across the major bump.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Remove the COPILOT_FFI_TRACE frame-level instrumentation from FfiRuntimeHost
(writes/inbound/heartbeat and the log_dropped_count binding) now that the FFI
transport is proven to deliver every frame. Restore the plain 1s keep-alive and
synchronous writes.

Add COPILOT_EVENT_TRACE session-event tracing at the client's
session.event / session.lifecycle notification handlers, logging each dispatched
event's type. This pinpoints whether the in-process macOS stall is a missing
session.idle (runtime never emits it) versus an emitted-but-not-dispatched event,
for the approve-all-short-circuit + shell-tool turn that wedges sendAndWait.
Enabled on the Node inprocess CI cell.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Log the full sessionId (not truncated to 8 chars, which was ambiguous across
sessions sharing a prefix) and add a trace line inside sendAndWait's event
listener, to conclusively determine whether the in-process macOS hang is caused
by session.idle events arriving for an unregistered session ("no-session" drop)
vs sendAndWait's own listener not receiving them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

SteveSandersonMS and others added 4 commits July 7, 2026 21:50
…mac hang

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…esis

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

SteveSandersonMS and others added 2 commits July 7, 2026 22:10
…st (match .NET)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…expose bug)

Make in-process connection_write async so the JS main thread is never blocked in
a synchronous write while koffi holds the cdylib reader thread in a blocking
inbound callback (bidirectional deadlock). Reverts the noresult test's abort so
the failing disconnect-while-pending pattern is exercised against the transport fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

…estId

Instrument feedInbound entry/exit and a keepalive heartbeat, plus log the parked
requestId when a no-result permission handler skips the reply, to determine at the
macOS/Windows in-process wedge whether koffi stops invoking the inbound callback
(worker/cdylib side) or invokes it but stalls inside (JS side).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

…quest causation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Root cause of the macOS/Windows-only in-process stall: koffi services its
threadsafe-function broker (which marshals foreign-thread outbound callbacks to
the JS main thread) ONLY while a koffi async FFI call is in flight; a plain JS
timer does not pump it. During an idle stretch mid-turn (worker awaiting a model
HTTP response, no client->server writes), the next outbound frame — the model
response and everything after — sits undelivered until the next koffi call,
wedging the session for 30s. Linux's libuv services the broker differently, so it
only reproduced off-Linux.

Keep exactly one cheap async FFI call (log_dropped_count) continuously in flight
so the broker always pumps; the in-flight libuv request also keeps the event loop
alive, replacing the keep-alive timer. Restores the no-result permission test to
its real disconnect-while-pending pattern (no longer needs to reply to pass).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

…3-core)

Test whether the in-process wedge is CPU-count dependent (tokio in the runtime
cdylib uses Builder::new_multi_thread() = one worker per core; macos-latest is
3-core M1). macos-latest-xlarge is the same Apple-Silicon arch with 6 cores, so a
pass would isolate core count from OS/arch as the trigger. Only the runner changes
vs the last failing run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅ (Node + Rust) / ⏳ (Python, Go, Java)

This PR correctly ports the in-process FFI transport from .NET (#1901) to TypeScript/Node and Rust. The API shapes are well-aligned across those three SDKs:

SDK Transport type Factory / constructor COPILOT_SDK_DEFAULT_CONNECTION
.NET InProcessRuntimeConnection RuntimeConnection.ForInProcess()
Node/TS InProcessRuntimeConnection RuntimeConnection.forInProcess() ✅ (this PR)
Rust Transport::InProcess enum variant ✅ (this PR)
Python ❌ missing
Go ❌ missing
Java ❌ missing

What Python would need

# New dataclass
`@dataclass`
class InProcessRuntimeConnection(RuntimeConnection):
    """Hosts the runtime in-process via FFI. Construct via RuntimeConnection.for_in_process()."""

# New factory on RuntimeConnection
`@staticmethod`
def for_in_process() -> InProcessRuntimeConnection:
    return InProcessRuntimeConnection()

Plus exporting InProcessRuntimeConnection from __init__.py, and a COPILOT_SDK_DEFAULT_CONNECTION check in the client startup path (mirroring the for_stdio default resolution).

What Go would need

// InProcessConnection hosts the runtime in-process via FFI.
type InProcessConnection struct{}
func (InProcessConnection) runtimeConnection() {}

Plus a COPILOT_SDK_DEFAULT_CONNECTION env-var check in NewClient / connection resolution.

What Java would need

A new useInProcess (or a RuntimeConnection-style discriminated type) in CopilotClientOptions, plus COPILOT_SDK_DEFAULT_CONNECTION handling in CliServerManager.


This PR is clearly scoped to Node + Rust — the gaps above are not blockers for merging. Flagging them here so follow-up issues/PRs can be tracked. The naming and env-var conventions are consistent with .NET, which is the right call.

Generated by SDK Consistency Review Agent for issue #1915 · sonnet46 1.5M ·

SteveSandersonMS and others added 2 commits July 7, 2026 23:18
…rker-count theory

Reverts mac runner to macos-latest. If Linux under taskset -c 0 (=> tokio default
worker_threads=1 via available_parallelism, which honors CPU affinity on Linux)
reproduces the in-process wedge, the trigger is tokio worker-thread count, not
OS/arch. Linux runs are fast and reliable, making this a cleaner test than swapping
mac runner sizes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-count threshold

Linux on 1 CPU reproduced the in-process wedge (tokio worker_threads=1), confirming
the trigger is tokio worker-thread count. Test 2 CPUs to locate the threshold at
which it stops wedging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants