feat: add native T3 runtime adapter - #2
Conversation
📝 WalkthroughWalkthroughThe change adds required interaction-mode configuration, typed facade dispatch fields, optional model options, and a native runtime adapter. The adapter manages RPC sessions, command projection, project and thread operations, and synchronized shell/detail stream reconciliation. ChangesRuntime integration
Estimated code review effort: 5 (Critical) | ~90+ minutes Sequence Diagram(s)sequenceDiagram
participant NativeRuntime
participant RuntimeClientSession
participant OrchestrationWebSocket
participant ShellStream
participant DetailStream
NativeRuntime->>RuntimeClientSession: open authorized session
RuntimeClientSession->>OrchestrationWebSocket: acquire socket URL and connect
NativeRuntime->>RuntimeClientSession: dispatch projected command
RuntimeClientSession->>OrchestrationWebSocket: send RPC command
NativeRuntime->>ShellStream: subscribe to shell stream
NativeRuntime->>DetailStream: subscribe to detail stream
ShellStream-->>NativeRuntime: return sequenced shell state
DetailStream-->>NativeRuntime: return sequenced detail state
NativeRuntime->>NativeRuntime: reconcile streams by sequence
NativeRuntime-->>NativeRuntime: emit native thread snapshot
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ee87e68. Configure here.
| throw new NativeRuntimeAdapterError("projection_invalid"); | ||
| } | ||
| detail = alignedDetail; | ||
| shell = alignedShell; |
There was a problem hiding this comment.
Aligned replay skips shell guard
Medium Severity
After alignInitialVersions, the first reconciled emission uses alignedInitialSequence with latestVersionAt detail/shell pairing but never runs shellCanAdvanceDetail or the pending-only shell-ahead checks used elsewhere. catchUpDetailThrough also marks detail validated through targetSequence when the stream hits synchronized, even if detail versions never reached that sequence, so stale detail can be published under a newer shell watermark.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit ee87e68. Configure here.
There was a problem hiding this comment.
WAIVED — verified false positive. The fresh lagging-stream subscription replays from the explicit afterSequence and its per-stream completion marker proves there is no omitted detail event through the target. The exact shell-first detail-only AB regression and initial replay regression pass; an independent adversarial reviewer revalidated this after the timeout hardening.
| return false; | ||
| } | ||
|
|
||
| async function catchUpDetailThrough( |
There was a problem hiding this comment.
🟠 High src/nativeRuntime.ts:466
catchUpDetailThrough discards snapshot sequences from the detail replay stream, so alignInitialVersions does not advance targetSequence when the detail catch-up returns a snapshot newer than the current target. When the detail stream replays a snapshot at sequence 20 while the target is 11, the function returns { deleted: false } without reporting sequence 20, so the caller marks detail validated only through 11 and emits a snapshot at 11 instead of 20. The analogous catchUpShellThrough returns the latest snapshotSequence for exactly this reason, but catchUpDetailThrough does not propagate it. Consider returning the latest snapshot sequence from catchUpDetailThrough and updating targetSequence in alignInitialVersions when the detail catch-up returns a newer sequence.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/nativeRuntime.ts around line 466:
`catchUpDetailThrough` discards snapshot sequences from the detail replay stream, so `alignInitialVersions` does not advance `targetSequence` when the detail catch-up returns a snapshot newer than the current target. When the detail stream replays a snapshot at sequence 20 while the target is 11, the function returns `{ deleted: false }` without reporting sequence 20, so the caller marks detail validated only through 11 and emits a snapshot at 11 instead of 20. The analogous `catchUpShellThrough` returns the latest `snapshotSequence` for exactly this reason, but `catchUpDetailThrough` does not propagate it. Consider returning the latest snapshot sequence from `catchUpDetailThrough` and updating `targetSequence` in `alignInitialVersions` when the detail catch-up returns a newer sequence.
| if (next.result.done) { | ||
| detailDone = true; | ||
| } else { |
There was a problem hiding this comment.
🟠 High src/nativeRuntime.ts:666
When a WebSocket stream closes cleanly before its synchronization marker arrives, reconcileThread silently exits and getThread returns undefined (or subscribeThread ends) as though the thread does not exist, instead of surfacing a transport_unavailable error. Additionally, if one stream completes after synchronizing while the other remains open, the loop continues indefinitely waiting for updates that can never arrive, and may keep emitting pending-only shell updates using stale detail.
Both detailDone and shellDone are set without verifying that synchronization occurred. The guard at line 692 throws when detailDone && !detailSynchronized, but only inside a shellSynchronized block, so a detail stream that closes before synchronizing is not checked when the shell stream is also unsynchronized. Likewise, shellDone without shellSynchronized is never checked. This diverges from readShellSnapshot and the catch-up helpers, which treat premature iterator completion as a transport failure. Consider throwing transport_unavailable whenever either iterator returns done before its corresponding synchronized flag is set.
- if (next.result.done) {
- detailDone = true;
- } else {
+ if (next.result.done) {
+ detailDone = true;
+ if (!detailSynchronized && detailFailure === undefined) {
+ throw new NativeRuntimeAdapterError("transport_unavailable");
+ }
+ } else {🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/nativeRuntime.ts around lines 666-668:
When a WebSocket stream closes cleanly before its synchronization marker arrives, `reconcileThread` silently exits and `getThread` returns `undefined` (or `subscribeThread` ends) as though the thread does not exist, instead of surfacing a `transport_unavailable` error. Additionally, if one stream completes after synchronizing while the other remains open, the loop continues indefinitely waiting for updates that can never arrive, and may keep emitting pending-only shell updates using stale detail.
Both `detailDone` and `shellDone` are set without verifying that synchronization occurred. The guard at line 692 throws when `detailDone && !detailSynchronized`, but only inside a `shellSynchronized` block, so a detail stream that closes before synchronizing is not checked when the shell stream is also unsynchronized. Likewise, `shellDone` without `shellSynchronized` is never checked. This diverges from `readShellSnapshot` and the catch-up helpers, which treat premature iterator completion as a transport failure. Consider throwing `transport_unavailable` whenever either iterator returns `done` before its corresponding `synchronized` flag is set.
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/facade.ts`:
- Around line 97-99: Update the ClientOrchestrationCommand validation used by
spawnCommand so model option values accept the same string | boolean contract as
ModelSelection.options[].value before Schema.decodeUnknownSync runs. Prefer
aligning the decoded schema; otherwise project boolean values before decoding
while preserving string values and the existing facade boundary behavior.
In `@src/nativeRuntime.ts`:
- Around line 718-764: Extract the pure emission-pair selection logic from
reconcileThread into a dedicated helper using detailVersions, shellVersions,
alignedInitialSequence, and threadId as inputs. Have it return the selected
detail/shell/commonSequence pair, an explicit wait result for unresolved skew,
or an absent result when the thread is missing; preserve the existing
projection_invalid error and pending-only advancement rules. Replace the current
inline block with this helper so reconcileThread retains only stream
coordination and state updates.
- Around line 948-950: Update the createProject, startThread, and startTurn
handlers so projectCommand, spawnCommand, and turnCommand are constructed inside
their respective dispatch callbacks. Catch Schema.decodeUnknownSync failures at
that boundary and map them to NativeRuntimeAdapterError with the bare
"command_rejected" code, ensuring malformed commands never reach
AmbiguousDispatchError or expose user-provided input.
- Around line 213-225: Bound both connection phases using a new connectTimeoutMs
field on T3NativeRuntimeOptions, threading it through openSession into
createDefaultSessionFactory. Apply the timeout to factory.connect and
session.ready, mapping expiry to transport_unavailable while preserving the
existing scope-close cleanup path for pending sockets.
- Around line 549-580: Bound the initial alignment loop around
catchUpDetailThrough and catchUpShellThrough with an attempt limit or deadline,
incrementing and checking it on each iteration before opening new subscriptions.
When the bound is exceeded, throw
NativeRuntimeAdapterError("transport_unavailable"); preserve the existing
deleted result and snapshotSequence advancement behavior for attempts that
remain within the bound.
In `@test/facade.contract.test.ts`:
- Around line 144-145: Remove the broad JSON.stringify(evidence) assertion
checking for the substring "true" in the evidence contract test. Retain the
fastMode absence assertion, which specifically verifies that the option is not
exposed.
In `@test/native-runtime-adapter.test.ts`:
- Line 534: Await both rejected-promise assertions in
test/native-runtime-adapter.test.ts at lines 534-534 and 1024-1024: add await to
the expect(...).rejects chains for runtime.getThread("thread-1") and
sessionFactory.connect(connection), respectively, so both rejection outcomes are
actually verified.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a7145af4-aa4e-4af5-8351-553fb05ddaaf
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
package.jsonsrc/config.tssrc/facade.tssrc/nativeRuntime.tstest/config.test.tstest/facade.contract.test.tstest/facade.send.test.tstest/facade.spawn.test.tstest/facade.wait.test.tstest/native-runtime-adapter.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
- GitHub Check: Cursor Bugbot
- GitHub Check: Macroscope - Correctness Check
🔇 Additional comments (18)
test/facade.send.test.ts (1)
4-8: LGTM!Also applies to: 44-44, 58-59, 78-79, 121-121, 136-137, 182-182, 219-219, 284-284, 340-340, 354-355, 416-416, 471-471, 528-528, 636-636, 686-686, 738-738, 791-791
test/facade.spawn.test.ts (1)
4-8: LGTM!Also applies to: 57-57, 171-171, 256-256, 337-337, 434-434, 514-514, 580-580, 634-634, 723-723, 817-817
test/facade.wait.test.ts (1)
4-8: LGTM!Also applies to: 104-104, 192-192, 287-287, 345-345, 385-385, 446-446, 497-497, 551-551, 610-610, 660-660, 713-713, 764-764, 826-826, 878-878, 930-930, 984-984, 1038-1038, 1089-1089, 1146-1146, 1199-1199, 1247-1247
src/config.ts (1)
8-8: LGTM!Also applies to: 19-19, 30-30, 81-95
src/facade.ts (1)
1-5: LGTM!Also applies to: 40-41, 63-64, 108-109, 211-214, 397-397, 493-493, 580-581, 590-591
test/config.test.ts (1)
11-11: LGTM!Also applies to: 33-33, 42-51
test/facade.contract.test.ts (1)
5-13: LGTM!Also applies to: 15-60, 63-143, 148-186
src/nativeRuntime.ts (8)
36-51: LGTM!Also applies to: 122-175
188-212: LGTM!Also applies to: 226-251
253-344: LGTM!
346-397: LGTM!
399-464: LGTM!
466-537: LGTM!
585-717: LGTM!Also applies to: 765-800, 802-837
839-947: LGTM!Also applies to: 951-997
test/native-runtime-adapter.test.ts (2)
21-205: LGTM!
207-533: LGTM!Also applies to: 540-1003, 1005-1023, 1027-1047
package.json (1)
14-16: 📐 Maintainability & Code QualityNo change needed
effect@4.0.0-beta.102exposes the required subpaths, includingeffect/unstable/socket/Socket, and the common core subpaths used bysrc/nativeRuntime.ts.
| readonly options?: ReadonlyArray<{ | ||
| readonly id: string; | ||
| readonly value: string; | ||
| readonly value: string | boolean; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Locate the model selection option value schema in `@t3tools/runtime-client` typings.
set -uo pipefail
fd -t f -e d.ts . node_modules/@t3tools/runtime-client 2>/dev/null | head -50
# Search declaration files for the option value type and the command schema.
rg -n -C 6 'modelSelection|ModelSelection' --iglob '*.d.ts' node_modules/@t3tools/runtime-client 2>/dev/null | head -120
rg -n -C 10 'ClientOrchestrationCommand' --iglob '*.d.ts' node_modules/@t3tools/runtime-client 2>/dev/null | head -80Repository: EtanHey/t3layer
Length of output: 153
🏁 Script executed:
#!/bin/bash
set -uo pipefail
# Locate relevant source files and inspect package dependency/version declarations.
printf 'Candidate files:\n'
git ls-files | rg '(^src/(facade|nativeRuntime)\.ts$|test/facade\.contract\.test\.ts$|package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$)' || true
printf '\nFacade excerpt:\n'
if [ -f src/facade.ts ]; then sed -n '80,115p' src/facade.ts | cat -n; fi
printf '\nnativeRuntime spawnCommand excerpt:\n'
if [ -f src/nativeRuntime.ts ]; then sed -n '830,895p' src/nativeRuntime.ts | cat -n; fi
printf '\nRuntime-client dependency references:\n'
rg -n '"`@t3tools/runtime-client`"|`@t3tools/runtime-client`' package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || trueRepository: EtanHey/t3layer
Length of output: 4071
🌐 Web query:
EtanHey t3code runtime-client v0.0.31-rpc.2 ModelSelection options value bool
💡 Result:
The query appears to reference components or configuration options within a software project, likely related to the pingdotgg/t3code ecosystem or similar TypeScript-based agent/runtime tooling [1][2][3]. While v0.0.31 of the t3code project exists [4][5], and there are various "runtime-client" packages and "ModelSelection" patterns across different developer tools (such as honeycomb, sipp, or lmstudio) [6][7][8], there is no public, authoritative documentation confirming a specific configuration option for "ModelSelection" that takes a boolean value in a package named runtime-client version v0.0.31-rpc.2 [1][4][5]. It is highly likely that the term "ModelSelection" refers to a custom interface or an internal configuration object used within a specific, perhaps private or highly specialized, implementation [3][6]. If you are working with a specific codebase, you should check the type definitions (e.g.,.d.ts files) within your local node_modules or the specific source repository for that package, as it may be an undocumented or internal-only API change introduced in that specific version [4].
Citations:
- 1: https://github.com/pingdotgg/t3code/tree/main/packages
- 2: https://github.com/pingdotgg/t3code
- 3: https://github.com/pingdotgg/t3code/blob/main/AGENTS.md
- 4: pingdotgg/t3code@v0.0.30...v0.0.31
- 5: pingdotgg/t3code@v0.0.31-nightly.20260729.944...v0.0.31-nightly.20260729.946
- 6: https://github.com/legioncodeinc/honeycomb/blob/main/src/daemon/runtime/inference/model-client-factory.ts
- 7: https://www.sipp.sh/docs/reference/runtime-options.html
- 8: https://github.com/lmstudio-ai/lms/blob/efce9967/src/subcommands/runtime/select.ts
🏁 Script executed:
#!/bin/bash
set -uo pipefail
printf 'Package manager / dependency availability:\n'
ls -la | rg '(^node_modules$|^pnpm-lock\.yaml$|^package-lock\.json$|^yarn\.lock$|package\.json$)' || true
node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).dependencies?{}:{})" 2>/dev/null || true
printf '\nLocate runtime-client files anywhere in checked-in fs:\n'
git ls-files | rg 'runtime-client|t3tools' || true
printf '\nInspect relevant tests around boolean option:\n'Repository: EtanHey/t3layer
Length of output: 365
🌐 Web query:
github.com EtanHey t3code package.json "@t3tools/runtime-client" "0.0.31-rpc.2"
💡 Result:
The repository github.com/EtanHey/t3code does not exist. The main repository for the "T3 Code" project—a web GUI for coding agents—is maintained under the organization pingdotgg as github.com/pingdotgg/t3code [1][2]. Etan Heyman (GitHub profile: github.com/EtanHey) is a contributor to the pingdotgg/t3code repository [3][4][5]. Regarding the specific package version mentioned, @t3tools/runtime-client 0.0.31-rpc.2, there is no evidence of this package name or version in the pingdotgg/t3code codebase. The project uses a monorepo structure where client-side runtime logic is located in the packages/client-runtime directory [6][7], and dependencies are typically managed via workspace protocols or internal catalogs rather than external npm-style versioning like 0.0.31-rpc.2 [8][9][10]. The project relies heavily on the Effect-TS ecosystem [11][12][8].
Citations:
- 1: https://github.com/pingdotgg/t3code
- 2: https://t3.codes/
- 3: fix: tighten node engine range for node:sqlite compat (#206) pingdotgg/t3code#1096
- 4: https://github.com/pingdotgg/t3code/releases/tag/v0.0.12
- 5: https://github.com/EtanHey
- 6: https://github.com/pingdotgg/t3code/tree/main/packages/client-runtime
- 7: https://github.com/pingdotgg/t3code/tree/main/packages/client-runtime/src/rpc
- 8: https://github.com/pingdotgg/t3code/blob/main/package.json
- 9: Simplify workspace package builds and deps pingdotgg/t3code#2676
- 10: pingdotgg/t3code@b440dd1
- 11: https://github.com/pingdotgg/t3code/blob/main/packages/client-runtime/src/rpc/protocol.ts
- 12: https://github.com/pingdotgg/t3code/blob/main/packages/client-runtime/src/rpc/session.ts
Validate boolean model options against the runtime-client schema before decoding.
spawnCommand passes input.modelSelection unchanged to Schema.decodeUnknownSync(ClientOrchestrationCommand). If ClientOrchestrationCommand still accepts only string option values, a boolean option from ModelSelection can throw and bypass the facade boundary. Align the decoded schema with ModelSelection.options[].value: string | boolean, or add a pre-decode projection that converts/supports booleans.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/facade.ts` around lines 97 - 99, Update the ClientOrchestrationCommand
validation used by spawnCommand so model option values accept the same string |
boolean contract as ModelSelection.options[].value before
Schema.decodeUnknownSync runs. Prefer aligning the decoded schema; otherwise
project boolean values before decoding while preserving string values and the
existing facade boundary behavior.
There was a problem hiding this comment.
WAIVED — verified false positive against the pinned rpc.2 artifact. @t3tools/runtime-client ModelSelection.options[].value is Schema.Union(string, boolean) in dist/index.d.mts, and the boolean dispatch contract test passes.
| } | ||
| } | ||
|
|
||
| async function readShellSnapshot( |
There was a problem hiding this comment.
🟠 High src/nativeRuntime.ts:926
readShellSnapshot awaits iterator.next() with no timeout, so listProjects() hangs indefinitely when the shell subscription connects but never emits a snapshot or completion marker. Unlike catchUpShellThrough and catchUpDetailThrough, which wrap each iterator.next() call in withTransportTimeout using a deadline, readShellSnapshot awaits the raw promise. The session opened by listProjects is never closed because the function never returns. Consider applying the same timeout wrapper used by the other catch-up helpers.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/nativeRuntime.ts around line 926:
`readShellSnapshot` awaits `iterator.next()` with no timeout, so `listProjects()` hangs indefinitely when the shell subscription connects but never emits a snapshot or completion marker. Unlike `catchUpShellThrough` and `catchUpDetailThrough`, which wrap each `iterator.next()` call in `withTransportTimeout` using a deadline, `readShellSnapshot` awaits the raw promise. The session opened by `listProjects` is never closed because the function never returns. Consider applying the same timeout wrapper used by the other catch-up helpers.
| const previousShell = shellVersions | ||
| .at(-2) | ||
| ?.snapshot.threads.find((candidate) => candidate.id === threadId); | ||
| const provenPendingOnlyShellAdvance = |
There was a problem hiding this comment.
🟠 High src/nativeRuntime.ts:875
In reconcileThread, the proven pending-only shell advance emits the current (stale) detail at the shell's sequence even though the matching detail event has not yet arrived. When that detail item later arrives at the same sequence, commonSequence <= lastEmitted suppresses it, so subscribers permanently miss the detail update (e.g. an assistant message). Consider waiting for the detail stream to catch up to that sequence before emitting, or allowing a corrected same-sequence emission.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/nativeRuntime.ts around line 875:
In `reconcileThread`, the proven pending-only shell advance emits the current (stale) detail at the shell's sequence even though the matching detail event has not yet arrived. When that detail item later arrives at the same sequence, `commonSequence <= lastEmitted` suppresses it, so subscribers permanently miss the detail update (e.g. an assistant message). Consider waiting for the detail stream to catch up to that sequence before emitting, or allowing a corrected same-sequence emission.


Outcome
Adds the production NativeRuntime adapter over the immutable T3 Code runtime-client artifact and aligns the facade contract with native runtime modes/options.
Scope
runtime-client-v0.0.31-rpc.2release artifactP3 identity/hierarchy, P4 lifecycle RPCs, P5 recovery, P6 MCP, and P7 UI remain out of scope.
Verification
tsc --noEmitgit diff --checkNext gate
After merge, run the live programmatic spawn → wait → send → wait proof against the connected T3 Code Alpha environment.
Note
High Risk
Large new real-time orchestration layer (auth, WebSocket sessions, stream reconciliation) plus a breaking
createT3FacadeAPI change; incorrect watermark logic could yield stale or missing thread updates.Overview
Introduces a production
NativeRuntimeimplementation that talks to T3 Code through the pinned@t3tools/runtime-clientartifact: scoped WebSocket RPC sessions (one-use socket URLs), orchestration command mapping for projects/spawn/send, andreconcileThreadto merge shell vs detail subscriptions into monotonicNativeThreadObservationsnapshots (catch-up, pending-only shell advances, resume boundaries, timeouts, sanitized errors).Facade/config alignment: experiment config now requires
interactionMode: "default";createT3Facademust receiveruntimeMode/interactionMode(no default options) andsendforwards those onstartTurn.ModelSelection.optionsis optional and may use boolean values; evidence usesoptionCountonly.Adds
effectdependency and broad contract + adapter test coverage (~115 tests).Reviewed by Cursor Bugbot for commit a74a0e7. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
"default"supported for interaction mode.Bug Fixes
Note
Add native T3 runtime adapter backed by WebSocket session factory
createT3NativeRuntime— a concreteNativeRuntimeimplementation that dispatches project/thread/turn commands over a WebSocket-backed RPC session, with connection and alignment timeouts and structured error codes viaNativeRuntimeAdapterError.createDefaultSessionFactorywith lazy factory loading, readiness waiting, scope management, and best-effort cleanup.reconcileThreadcoordinates shell and detail projections to yield consistentNativeThreadSnapshotvalues, with skew tolerance and deletion handling.createT3Facadeto requireruntimeModeandinteractionModeat construction time and forwards both fields instartTurndispatches and evidence records.createConfigto require and validate aninteractionMode: "default"field onExperimentConfig.createT3Facadeoptions are no longer optional — callers that relied on the default empty object will need to provideruntimeModeandinteractionMode.Macroscope summarized a74a0e7.