Skip to content

feat(voice): add experimental committed-turn gateway - #8427

Closed
jyaunches wants to merge 3 commits into
mainfrom
issue-8378-experimental-voice-gateway
Closed

jyaunches wants to merge 3 commits into
mainfrom
issue-8378-experimental-voice-gateway

Conversation

@jyaunches

@jyaunches jyaunches commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add the first experimental voice-gateway capability slice: one authenticated local runtime can create an expiring session and stream one committed text turn through a NemoClaw-owned OpenClaw boundary. The service remains hidden, foreground-only, loopback-only, OpenClaw-specific, and outside supported VoiceClaw or general voice claims.

Related Issue

Fixes #8378
Parent: #8275

Changes

  • Add hidden nemoclaw internal voice-gateway serve routing behind the exact NEMOCLAW_EXPERIMENTAL_VOICE_GATEWAY=1 gate.
  • Separate private credential loading, HTTP/NDJSON transport, in-memory session and turn state, OpenClaw WebSocket protocol handling, and foreground service lifetime.
  • Enforce owner-controlled credential files, random session grants, fixed operator configuration, loopback-only destinations, operator.read/operator.write, bounded inputs/queues/responses/deadlines, exact session/run correlation, duplicate and overlap rejection, cleanup, and content-free diagnostics.
  • Add deterministic domain, adapter, composed integration, command-routing, variant-doc, and package-contract evidence.
  • Document the OpenClaw-only experimental operational and credential-lifecycle contract.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Docs updated for user-facing behavior changes
  • Docs not applicable — justification:
  • Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging)
  • Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: Independent final nine-category review passed commit 0eb18814bf378105a96259277d2aad77b6127a10 with no findings after credential-path, OpenClaw protocol, queue, response, duration, and backpressure hardening.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Documentation Writer Review

  • Documentation writer subagent reviewed the completed changes
  • Result: docs-updated
  • Evidence: docs/reference/commands.mdx; src/commands/internal/README.md
  • Agent: Pi CLI

DGX Station Hardware Evidence

  • Tested on DGX Station
  • Tested commit: Not applicable
  • Station profile/scenario: Not applicable
  • Result: Not applicable
  • Supporting evidence: Not applicable

Verification

  • PR description includes a Signed-off-by: line and every commit appears as Verified in GitHub
  • Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run validate:pr passed after refreshing origin/main when hooks were skipped or unavailable
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — 60 focused CLI, integration, internal-command, documentation-routing, and package-contract tests passed after rebasing to current origin/main.
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result: Focused behavior tests, npm run build:cli, npm run typecheck:cli, npm run checks:repository, npm run validate:pr, npm run docs, production npm audit, and git diff --check passed. Full live runtime validation remains for CI/E2E selection.
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without warnings (doc changes only) — build passed with 0 errors; two existing Fern warnings remain.
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Julie Yaunches jyaunches@nvidia.com

Summary by CodeRabbit

  • New Features

    • Added an opt-in, hidden experimental voice gateway with loopback-only access.
    • Supports authenticated sessions, streamed voice turns, health checks, bounded timeouts, and graceful shutdown.
    • Added secure private credential-file validation and OpenClaw voice-agent connectivity.
  • Documentation

    • Documented setup, credentials, limits, diagnostics, and compatibility use cases.
  • Tests

    • Added comprehensive unit, integration, security, CLI, and documentation coverage.

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f1fd7368-3560-4667-8b46-e65447b93c45

📥 Commits

Reviewing files that changed from the base of the PR and between 77f42c4 and 0eb1881.

📒 Files selected for processing (6)
  • src/lib/adapters/fs/private-credential.test.ts
  • src/lib/adapters/http/voice-gateway.ts
  • src/lib/adapters/openclaw/voice-agent-client.test.ts
  • src/lib/adapters/openclaw/voice-agent-client.ts
  • src/lib/domain/voice/session-service.test.ts
  • src/lib/domain/voice/session-service.ts
💤 Files with no reviewable changes (1)
  • src/lib/adapters/fs/private-credential.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/domain/voice/session-service.ts

📝 Walkthrough

Walkthrough

Adds an opt-in, hidden Voice Gateway for OpenClaw. The gateway validates private credentials, binds to loopback, creates bounded voice sessions, streams sanitized responses over HTTP, and shuts down cleanly. Tests cover security, lifecycle, protocol handling, CLI wiring, documentation, and end-to-end behavior.

Changes

Voice Gateway

Layer / File(s) Summary
Credential and session lifecycle
src/lib/adapters/fs/*, src/lib/domain/voice/*
Adds secure credential-file validation and bounded voice-session authorization, streaming, cleanup, diagnostics, and tests.
OpenClaw voice protocol
src/lib/adapters/openclaw/*
Adds a loopback WebSocket client with challenge authentication, request correlation, event filtering, ordering, limits, timeouts, and lifecycle handling.
Authenticated HTTP streaming
src/lib/adapters/http/voice-gateway.*, test/voice-gateway-integration.test.ts
Adds health, session, deletion, and NDJSON turn routes with credential checks, request bounds, concurrency protection, disconnect cleanup, and integration coverage.
Experimental runtime and CLI wiring
src/lib/actions/voice-gateway/*, src/commands/internal/*, docs/reference/commands.mdx, test/internal-cli.test.ts, test/internal-commands-docs.test.ts, test/package-contract/cli/oclif-metadata.test.ts, ci/source-architecture-budget.json
Adds feature-flagged startup, command options, graceful shutdown, hidden-command metadata, OpenClaw-only documentation, package checks, and the updated architecture budget.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runtime
  participant InternalCLI
  participant VoiceGatewayAction
  participant VoiceGatewayHTTP
  participant VoiceSessionService
  participant OpenClawVoiceAgentClient
  Runtime->>InternalCLI: start hidden voice gateway command
  InternalCLI->>VoiceGatewayAction: pass trusted configuration
  VoiceGatewayAction->>VoiceGatewayHTTP: create loopback server
  Runtime->>VoiceGatewayHTTP: authenticate and create session
  VoiceGatewayHTTP->>VoiceSessionService: create grant-bound session
  Runtime->>VoiceGatewayHTTP: submit committed turn
  VoiceGatewayHTTP->>VoiceSessionService: start authorized turn
  VoiceSessionService->>OpenClawVoiceAgentClient: invoke chat turn
  OpenClawVoiceAgentClient-->>VoiceSessionService: filtered ordered events
  VoiceSessionService-->>VoiceGatewayHTTP: sanitized response events
  VoiceGatewayHTTP-->>Runtime: NDJSON stream
Loading

Possibly related issues

Possibly related PRs

  • NVIDIA/NemoClaw#8429 — Direct code-level connection to the same voice-gateway command, adapters, session service, tests, and documentation.

Suggested labels: integration: openclaw, area: cli, area: integrations, area: security, experimental, feature, voiceclaw

Suggested reviewers: apurvvkumaria, cv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: an experimental voice gateway for committed turns.
Linked Issues check ✅ Passed The implementation matches the gated loopback gateway, trusted configuration, bounded one-turn sessions, OpenClaw streaming, cleanup, diagnostics, and test requirements [#8378].
Out of Scope Changes check ✅ Passed All changes support the experimental committed-turn gateway, its security boundaries, documentation, routing, and tests; no excluded feature area is indicated.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-8378-experimental-voice-gateway

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@github-code-quality

github-code-quality Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit 0eb1881 in the issue-8378-experimen... branch remains at 96%, unchanged from commit bf429e9 in the main branch.

TypeScript / code-coverage/cli

The overall coverage in commit 0eb1881 in the issue-8378-experimen... branch remains at 82%, unchanged from commit bf429e9 in the main branch.

Show a code coverage summary of the most impacted files.
File main bf429e9 issue-8378-experimen... 0eb1881 +/-
src/lib/onboard/docker-cdi.ts 80% 70% -10%
src/lib/domain/.../connect-env.ts 97% 89% -8%
src/lib/credentials/store.ts 56% 55% -1%
src/lib/onboard...er-gpu-patch.ts 81% 80% -1%
src/lib/onboard/preflight.ts 79% 80% +1%
src/lib/adapter...agent-client.ts 0% 78% +78%
src/lib/adapter...oice-gateway.ts 0% 82% +82%
src/lib/adapter...e-credential.ts 0% 88% +88%
src/lib/domain/...sion-service.ts 0% 93% +93%
src/lib/actions...ateway/serve.ts 0% 95% +95%

Updated August 06, 2026 04:24 UTC

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Review Advisor — No blocking findings reported

Advisor assessment: No blocking advisor findings reported
Next action: No advisor follow-up needed.
Findings: 0 blockers · 0 warnings · 0 suggestions

Model lanes

  • GPT-5.6 Terra (primary): Completed · high confidence · 0 blockers · 0 warnings · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Failed after a partial review · low confidence · 1 blocker · 1 warning · 0 suggestions

Second-opinion terminology and E2E selections are advisory. They do not change the primary assessment or E2E / PR Gate.

4 semantic terminology decisions

Terminology decisions are advisory. They affect the assessment only when a separate finding identifies concrete semantic impact.

  • established — voice gateway at docs/reference/commands.mdx:3829: Use “voice gateway” consistently for this adapter and retain the gateway-type qualifiers where needed.
  • justified — committed turn at docs/reference/commands.mdx:3863: Retain “committed turn” where the one-turn and commit-ID constraint matters.
  • justified — deployment admission credential at docs/reference/commands.mdx:3840: Retain the modifier pair when describing the separate credential authorities and flows.
  • justified — experimental service at docs/reference/commands.mdx:3792: Retain “experimental service” while the documented gate and unsupported lifecycle constraints apply.

E2E guidance

Advisory only. E2E / PR Gate selects and runs jobs independently.

Recommended E2E: cloud-inference, security-posture

1 optional E2E recommendation
  • openclaw-tui-chat-correlation

Workflow run details

This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge.

@coderabbitai coderabbitai 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.

Actionable comments posted: 12

🧹 Nitpick comments (9)
src/lib/domain/voice/session-service.ts (2)

269-297: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

startTurn declares Promise<void> but throws synchronously on every validation failure.

Lines 276-283 throw VoiceSessionError before the function returns a promise. The current callers are safe: src/lib/adapters/http/voice-gateway.ts wraps the await in a try, and the unit test at line 136 asserts the synchronous throw with expect(() => ...).toThrowError.

The hazard is the mixed contract. A future caller that writes startTurn(...).catch(handler) without a surrounding try receives an uncaught exception instead of a rejection.

Mark the method async so all failures surface as rejections, and update the unit-test assertions to await expect(...).rejects. If you prefer to keep the synchronous throw, document it on the method so the contract is explicit.

🤖 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/lib/domain/voice/session-service.ts` around lines 269 - 297, Update
startTurn to be async so validation failures from authorize and
requireBoundedString, along with duplicate or active-turn checks, are returned
as promise rejections consistent with its Promise<void> contract. Update the
affected unit-test assertions to await expect(...).rejects rather than asserting
synchronous throws.

146-159: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Do not use label as a behavior switch.

label is a descriptive tag for every caller except "text". At line 155 it silently becomes a control flag that enables blank rejection. If a later change renames the literal at line 278, the whitespace check stops running. No type error and no other test fails.

Make the rule explicit in the signature.

♻️ Proposed refactor
-function requireBoundedString(value: unknown, label: string, maxBytes: number): string {
+function requireBoundedString(
+  value: unknown,
+  _label: string,
+  maxBytes: number,
+  options: { rejectBlank?: boolean } = {},
+): string {
   if (
     typeof value !== "string" ||
     value.length === 0 ||
     Buffer.byteLength(value) > maxBytes ||
     /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value)
   ) {
     throw new VoiceSessionError("invalid_request", 400);
   }
-  if (label === "text" && value.trim().length === 0) {
+  if (options.rejectBlank && value.trim().length === 0) {
     throw new VoiceSessionError("invalid_request", 400);
   }
   return value;
 }

Then update the call site at line 278:

const text = requireBoundedString(textValue, "text", VOICE_MAX_TURN_TEXT_BYTES, {
  rejectBlank: true,
});

As per coding guidelines: "Prefix intentionally unused variables with _."

🤖 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/lib/domain/voice/session-service.ts` around lines 146 - 159, Update
requireBoundedString to accept an explicit options parameter, such as
rejectBlank, instead of using the label value to control whitespace validation.
Apply the blank-rejection check from that option, update the text call site to
pass rejectBlank: true, and prefix any intentionally unused label or parameter
with an underscore.

Source: Coding guidelines

src/lib/adapters/fs/private-credential.ts (1)

27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why the walk stops at the first foreign-owned ancestor.

Line 27 ends the check at the first directory that the current user does not own. This skips the group/other-writable check for that directory and every directory above it. The behavior is correct for root-owned system paths such as /tmp, which are world-writable with the sticky bit. The reason is not visible in the code. Add a short comment so a later change does not remove the break or reorder it before the symlink check.

🤖 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/lib/adapters/fs/private-credential.ts` at line 27, In the
ancestor-walking logic around the uid ownership check, add a concise comment
explaining that encountering the first foreign-owned directory intentionally
stops traversal before applying group/other-writable checks, preserving safe
handling of root-owned paths such as sticky-bit /tmp; keep the break after the
symlink check and do not change the behavior.
test/voice-gateway-integration.test.ts (1)

176-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the invocation index by method rather than by position.

invocations[0] and invocations[1] depend on the exact order and count of frames the client sends. If the client adds a handshake or keepalive request, the indexes shift and the assertion fails or silently checks the wrong frame. Record the method alongside the params in FakeOpenClawSocket.send, then select the chat.send entry.

🤖 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 `@test/voice-gateway-integration.test.ts` around lines 176 - 177, Update
FakeOpenClawSocket.send to record each invocation’s method alongside its params,
then locate the chat.send entry by method before asserting its scopes, message,
and deliver fields. Remove the positional invocations[0]/invocations[1]
assumptions while preserving the existing expectations.
src/lib/adapters/http/voice-gateway.ts (1)

155-238: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider splitting the request handler into route functions.

The handler mixes routing, admission authentication, body validation, session creation, session deletion, and turn streaming in one async function. Extracting handleCreateSession, handleDeleteSession, and handleTurn keeps each unit small and matches the low-complexity guideline for TypeScript sources.

As per coding guidelines: "Keep function complexity low."

🤖 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/lib/adapters/http/voice-gateway.ts` around lines 155 - 238, Split the
inline request handler into focused route functions: extract session creation
into handleCreateSession, deletion into handleDeleteSession, and streaming into
handleTurn, preserving their existing authentication, validation,
session-service, response, and error behavior. Keep the main handler responsible
only for path dispatch and shared error handling, and reuse the existing routing
symbols and dependencies rather than changing the API contract.

Source: Coding guidelines

test/internal-commands-docs.test.ts (1)

102-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Select the OpenClaw reference by name.

renderedCommandReferences currently places commands.openclaw.generated.mdx at index 0 and appends references.slice(1). Find the entry by name instead of relying on this ordering, then assert absence for the remaining entries.

🤖 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 `@test/internal-commands-docs.test.ts` around lines 102 - 110, Update the test
around renderedCommandReferences to locate the OpenClaw reference by its name
rather than assuming it is at index 0. Assert the experimental voice gateway
invocation is present in that named reference, then check that all other
references do not contain it.

Source: Path instructions

src/lib/adapters/openclaw/voice-agent-client.test.ts (2)

233-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the oversize payload from the exported limit.

1_100_000 encodes an unstated relationship to VOICE_MAX_RESPONSE_TEXT_BYTES: one frame must stay under the limit and two must exceed it, and both must stay under MAX_NATIVE_FRAME_BYTES. If the domain constant changes, this test keeps passing for a different reason.

Import VOICE_MAX_RESPONSE_TEXT_BYTES and compute the size, for example "x".repeat(Math.ceil(VOICE_MAX_RESPONSE_TEXT_BYTES * 0.6)). The two-frame overflow intent then stays correct when the limit moves.

🤖 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/lib/adapters/openclaw/voice-agent-client.test.ts` around lines 233 - 251,
Update the oversized payload setup in the test’s queueMicrotask message loop to
import and derive each deltaText length from VOICE_MAX_RESPONSE_TEXT_BYTES,
using a value that keeps one frame below the limit while two frames exceed it
and remain below MAX_NATIVE_FRAME_BYTES. Remove the hardcoded 1_100_000 size.

Source: Path instructions


143-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the terminal-frame assertion.

expect(onText).not.toHaveBeenCalled() passes for two independent reasons: the post-terminal delta is ignored, and the final frame carries no accumulated text, so line 135 of voice-agent-client.ts skips the callback. The assertion cannot distinguish them.

Emit a delta before the final frame and assert that onText is called exactly once with only the pre-terminal text. The test then fails if a post-terminal frame ever leaks through.

This fixture also covers only the queued-drain path, because both chat frames arrive before the chat.send response sets the run id. The post-terminal protection for unqueued frames relies on listener removal in finish. Consider a second case that delivers the late delta after the run id is known.

🤖 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/lib/adapters/openclaw/voice-agent-client.test.ts` around lines 143 - 199,
Strengthen the terminal-frame test around OpenClawVoiceAgentClient.invoke by
adding a pre-terminal delta with text, then assert onText is called exactly once
with only that text after the final frame; retain the late delta to verify it is
ignored. Add a separate scenario that delivers the post-terminal delta after the
chat.send response establishes the run id, covering the listener-removal path in
finish in addition to the queued-drain path.

Source: Path instructions

src/lib/adapters/openclaw/voice-agent-client.ts (1)

73-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Extract the frame handlers from invoke.

invoke spans about 140 lines and holds six mutable turn variables plus four nested closures. The coding guidelines require low function complexity for TypeScript files. Extract consumeChatFrame and onMessage into a small private turn-reader type that owns expectedRunId, lastSequence, projectedText, terminal, and the queue. invoke then wires listeners and sends chat.send.

This is a readability change only. Defer it if you prefer to keep the experimental path stable.

🤖 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/lib/adapters/openclaw/voice-agent-client.ts` around lines 73 - 83,
Refactor invoke in OpenClawVoiceAgentClient by extracting consumeChatFrame and
onMessage into a small private turn-reader type that owns expectedRunId,
lastSequence, projectedText, terminal, queuedChatFrames, and queuedChatBytes.
Keep invoke responsible only for connecting, wiring listeners, and sending
chat.send, while preserving the existing frame-processing behavior and
experimental path semantics.

Source: Coding guidelines

🤖 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/lib/adapters/fs/private-credential.test.ts`:
- Around line 62-63: Remove the unused process.env.HOME assignment and
originalHome save from the test setup, and delete the corresponding conditional
restore block after readPrivateCredentialFile. Keep the test linear and retain
only necessary temporary-directory cleanup.

In `@src/lib/adapters/http/voice-gateway.ts`:
- Around line 128-129: Replace the request.once("aborted", ...) listener with a
request.once("close", ...) listener in the request body handling flow,
preserving the existing VoiceHttpError completion behavior and the separate
error listener.
- Around line 198-222: Update the response close handler near deliveryOpen and
streamStarted so disconnectTurn(voiceSessionId) runs only when the response has
started streaming and is not writableFinished. Keep rejected or otherwise
unstarted requests from invoking sessionService.disconnectTurn, while preserving
cleanup for authorized started turns.

In `@src/lib/adapters/openclaw/voice-agent-client.test.ts`:
- Around line 13-25: Remove the method-based conditionals from the fake socket
send implementations at the base socket and the TerminalSocket and
OversizedSocket overrides. Add a method-keyed responder map to FakeSocket that
handles the shared connect response, then have each subclass extend that map
with only its own chat.send responder using the existing response and fixture
helpers; preserve message recording and response dispatch behavior while
eliminating the duplicated connect responders.

In `@src/lib/adapters/openclaw/voice-agent-client.ts`:
- Around line 329-334: Handle already-aborted signals in both VoiceAgentClient
sites: in request at src/lib/adapters/openclaw/voice-agent-client.ts#L329-L334,
reject with agent_unavailable before registering the abort listener or calling
socket.send; in connect at
src/lib/adapters/openclaw/voice-agent-client.ts#L306-L306, invoke onAbort()
immediately when signal.aborted is true rather than only registering the
listener.
- Around line 195-208: In the chat.send continuation, add the existing settled
guard immediately before assigning expectedRunId and invoking
request.onRun(runId). Return without updating state or calling onRun when the
turn has already finished; leave the queuedChatFrames drain behavior unchanged.
- Around line 272-299: Update the connect handshake in the challenge handler to
preserve the challenge nonce and timestamp, compute the required protocol-v4
device signature, and include both nonce and signature under
connect.params.device alongside auth.token. Update the related test assertion so
it validates the populated device object instead of requiring connect.params to
omit device.

In `@src/lib/domain/voice/session-service.test.ts`:
- Line 51: Add a separate test that constructs VoiceSessionService without
injecting randomGrant, then creates two sessions through the public API and
closes the first binding between them. Assert each default grant matches the
43-character base64url format and that the grants differ; keep the existing
harness test focused on its injected stub rather than treating its literal
length as production-generator coverage.
- Around line 159-201: Move fake-timer cleanup for the tests “rejects a grant
for another or expired voice session (`#8378`)” and “bounds a running turn with a
stable content-free failure (`#8378`)” into the file’s afterEach hook, ensuring
vi.useRealTimers() runs even when assertions fail. Remove the trailing
vi.useRealTimers() calls from both test bodies while preserving their existing
fake-timer behavior.

In `@src/lib/domain/voice/session-service.ts`:
- Around line 395-409: Unify terminal-reason selection in the session flow by
adding a shared failure-reason helper next to emitTerminal, with
active.expiryReason taking precedence, followed by the response-limit condition,
then active.timedOut. Use this helper in both the success and catch paths so
invoke outcomes emit the same reason, and replace the duplicate
responseBytes/responseLimitExceeded checks with the shared condition while
preserving terminal emission behavior.
- Around line 262-267: Update disconnectTurn to require and validate the
request’s grant before aborting the matching active session, using the existing
authorization mechanism and returning without throwing when authorization fails.
Preserve the current session and voiceSessionId checks, and ensure the gateway
passes the grant through the close-handler cleanup path.

In `@test/voice-gateway-integration.test.ts`:
- Around line 43-52: The test file contains two conditionals that violate the CI
guardrail. In test/voice-gateway-integration.test.ts lines 43-52, update
FakeOpenClawSocket.send to select chat fixture events through a ternary-backed
chatFixtureEvents(sessionKey) helper and iterate the resulting array, which is
empty for non-chat.send methods; in lines 168-171, replace the
response.text.delta conditional with an events.filter(...).map(...) pipeline
when building runtimeOutput.

---

Nitpick comments:
In `@src/lib/adapters/fs/private-credential.ts`:
- Line 27: In the ancestor-walking logic around the uid ownership check, add a
concise comment explaining that encountering the first foreign-owned directory
intentionally stops traversal before applying group/other-writable checks,
preserving safe handling of root-owned paths such as sticky-bit /tmp; keep the
break after the symlink check and do not change the behavior.

In `@src/lib/adapters/http/voice-gateway.ts`:
- Around line 155-238: Split the inline request handler into focused route
functions: extract session creation into handleCreateSession, deletion into
handleDeleteSession, and streaming into handleTurn, preserving their existing
authentication, validation, session-service, response, and error behavior. Keep
the main handler responsible only for path dispatch and shared error handling,
and reuse the existing routing symbols and dependencies rather than changing the
API contract.

In `@src/lib/adapters/openclaw/voice-agent-client.test.ts`:
- Around line 233-251: Update the oversized payload setup in the test’s
queueMicrotask message loop to import and derive each deltaText length from
VOICE_MAX_RESPONSE_TEXT_BYTES, using a value that keeps one frame below the
limit while two frames exceed it and remain below MAX_NATIVE_FRAME_BYTES. Remove
the hardcoded 1_100_000 size.
- Around line 143-199: Strengthen the terminal-frame test around
OpenClawVoiceAgentClient.invoke by adding a pre-terminal delta with text, then
assert onText is called exactly once with only that text after the final frame;
retain the late delta to verify it is ignored. Add a separate scenario that
delivers the post-terminal delta after the chat.send response establishes the
run id, covering the listener-removal path in finish in addition to the
queued-drain path.

In `@src/lib/adapters/openclaw/voice-agent-client.ts`:
- Around line 73-83: Refactor invoke in OpenClawVoiceAgentClient by extracting
consumeChatFrame and onMessage into a small private turn-reader type that owns
expectedRunId, lastSequence, projectedText, terminal, queuedChatFrames, and
queuedChatBytes. Keep invoke responsible only for connecting, wiring listeners,
and sending chat.send, while preserving the existing frame-processing behavior
and experimental path semantics.

In `@src/lib/domain/voice/session-service.ts`:
- Around line 269-297: Update startTurn to be async so validation failures from
authorize and requireBoundedString, along with duplicate or active-turn checks,
are returned as promise rejections consistent with its Promise<void> contract.
Update the affected unit-test assertions to await expect(...).rejects rather
than asserting synchronous throws.
- Around line 146-159: Update requireBoundedString to accept an explicit options
parameter, such as rejectBlank, instead of using the label value to control
whitespace validation. Apply the blank-rejection check from that option, update
the text call site to pass rejectBlank: true, and prefix any intentionally
unused label or parameter with an underscore.

In `@test/internal-commands-docs.test.ts`:
- Around line 102-110: Update the test around renderedCommandReferences to
locate the OpenClaw reference by its name rather than assuming it is at index 0.
Assert the experimental voice gateway invocation is present in that named
reference, then check that all other references do not contain it.

In `@test/voice-gateway-integration.test.ts`:
- Around line 176-177: Update FakeOpenClawSocket.send to record each
invocation’s method alongside its params, then locate the chat.send entry by
method before asserting its scopes, message, and deliver fields. Remove the
positional invocations[0]/invocations[1] assumptions while preserving the
existing expectations.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6b0491f2-114c-4bc5-b998-a27e4c474ab4

📥 Commits

Reviewing files that changed from the base of the PR and between 26bd9f5 and 5b496a8.

📒 Files selected for processing (18)
  • ci/source-architecture-budget.json
  • docs/reference/commands.mdx
  • src/commands/internal/README.md
  • src/commands/internal/voice-gateway/serve.ts
  • src/lib/actions/voice-gateway/serve.test.ts
  • src/lib/actions/voice-gateway/serve.ts
  • src/lib/adapters/fs/private-credential.test.ts
  • src/lib/adapters/fs/private-credential.ts
  • src/lib/adapters/http/voice-gateway.test.ts
  • src/lib/adapters/http/voice-gateway.ts
  • src/lib/adapters/openclaw/voice-agent-client.test.ts
  • src/lib/adapters/openclaw/voice-agent-client.ts
  • src/lib/domain/voice/session-service.test.ts
  • src/lib/domain/voice/session-service.ts
  • test/internal-cli.test.ts
  • test/internal-commands-docs.test.ts
  • test/package-contract/cli/oclif-metadata.test.ts
  • test/voice-gateway-integration.test.ts

Comment thread src/lib/adapters/http/voice-gateway.ts Resolved
Comment thread src/lib/adapters/http/voice-gateway.ts Resolved
Comment thread src/lib/domain/voice/session-service.ts Resolved
Comment thread src/lib/domain/voice/session-service.ts Resolved
Comment thread test/voice-gateway-integration.test.ts Resolved
@coderabbitai coderabbitai Bot mentioned this pull request Aug 6, 2026
12 of 23 tasks
Signed-off-by: Julie Yaunches <jyaunches@nvidia.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.

[Voice gateway] Route one committed turn for an authenticated runtime session

1 participant