Skip to content

fix(cli): negotiate relayfile control-plane API before v3 calls#1324

Merged
khaliqgant merged 4 commits into
mainfrom
fix/control-plane-hello-negotiation
Jul 17, 2026
Merged

fix(cli): negotiate relayfile control-plane API before v3 calls#1324
khaliqgant merged 4 commits into
mainfrom
fix/control-plane-hello-negotiation

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

  • consume the fixed @relayfile/client@^0.10.27 final release
  • lock relay-side coverage for headerless /v1/hello discovery, fail-fast v1 incompatibility, transient socket retry, and stale-daemon replacement
  • exercise stale replacement against an external socket-owning child so exact-owner termination is covered
  • document the user-visible fix in the root changelog

Counterpart: AgentWorkforce/relayfile#354

Final-package gate

  • After relayfile#354 merges and @relayfile/client@0.10.27 is published on latest, replace the exact 0.10.27-rc.0 pin and lock entry with ^0.10.27, then rerun validation. This relay PR must not merge with the RC pin.

Validation

  • verified relayfile@latest and @relayfile/client@latest both resolve 0.10.27
  • confirmed zero 0.10.27-rc references remain in tracked manifests or lockfiles
  • npm exec vitest run packages/cli/src/cli/commands/integration-relayfile-contract.test.ts — 10 passed, 4 opt-in skipped
  • relayfile contract + integration cleanup journal + integration subscribe suites — 82 passed, 6 skipped
  • npm run typecheck — passed
  • npm run lint — passed with 38 pre-existing warnings and 0 errors
  • targeted Prettier check — passed
  • npx --yes npm@10.5.1 ci --dry-run --ignore-scripts — passed and resolves final client 0.10.27
  • npm ls @relayfile/client --all — resolves 0.10.27

The full repository Vitest run reached 1,255 passing and 11 skipped tests, then failed on the unrelated existing 5-second timeout in broker-lifecycle.test.ts (shuts the broker down when its compiled-binary node provider exits). This branch does not modify that test or its implementation, and the failure reproduces when the file is run alone.

@khaliqgant
khaliqgant requested a review from willwashburn as a code owner July 17, 2026 08:52
@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI pins @relayfile/client to 0.10.27-rc.0, documents relayfile control-plane compatibility handling, and expands integration contract tests for capability discovery, daemon readiness, incompatibility errors, and stale-daemon replacement.

Changes

Relayfile negotiation

Layer / File(s) Summary
Client version and release contract
CHANGELOG.md, packages/cli/package.json
Pins the relayfile client to 0.10.27-rc.0 and documents control-plane discovery, incompatible-daemon failures, and stale-daemon replacement.
Negotiation and daemon lifecycle tests
packages/cli/src/cli/commands/integration-relayfile-contract.test.ts
Adds unix-socket, fake-binary, and external-daemon fixtures, then tests hello negotiation, retries, structured incompatibility errors, and conditional stale-daemon replacement.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: size:L

Suggested reviewers: willwashburn

Poem

I’m a rabbit with a socket to tend,
Hello finds the version before headers descend.
Old daemons hop out, new ones appear,
Retry waits softly when readiness is near.
Patch notes bloom—safe paths are clear!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: negotiating the relayfile control-plane API before v3 calls.
Description check ✅ Passed The summary and validation sections cover the change and tests, though the template's Test Plan/Screenshots headings are not followed exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/control-plane-hello-negotiation

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.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c5e4c2cae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/package.json Outdated
"@agent-relay/utils": "10.6.3",
"@modelcontextprotocol/sdk": "^1.0.0",
"@relayfile/client": "^0.10.21",
"@relayfile/client": "0.10.27-rc.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace the RC client pin before releasing

If this commit is merged and released as written, every agent-relay installation will remain pinned to the 0.10.27-rc.0 prerelease even after the stable client is published, preventing users from receiving the finalized artifact or subsequent compatible fixes. The commit's mandatory-before-merge note explicitly requires replacing this with ^0.10.27, so update both manifests and the lockfile before landing it.

Useful? React with 👍 / 👎.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request updates the @relayfile/client dependency, documents integration fixes in the changelog, and introduces comprehensive contract tests for the relayfile control-plane hello negotiation. The feedback suggests improving test maintainability by extracting large embedded JavaScript code strings from the test helpers into separate fixture files to enable proper syntax highlighting and linting.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

response.end(JSON.stringify(body));
}

async function withFakeRelayfileBinary(

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.

medium

The helper functions withFakeRelayfileBinary (lines 42-94) and withExternalControlPlaneDaemon (lines 96-188) embed significant amounts of JavaScript code as multi-line strings. This practice can make the code harder to read, debug, and maintain, as it lacks syntax highlighting and linting within the string literal.

For better maintainability, consider extracting these scripts into separate fixture files (e.g., in a __fixtures__ directory) and loading them with fs.readFileSync. This would improve code organization and developer experience.

@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: 3

🤖 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 `@packages/cli/src/cli/commands/integration-relayfile-contract.test.ts`:
- Around line 138-166: Add a short readiness timeout to the promise around the
fake daemon startup, rejecting with a clear error and killing the child when
“ready” is not emitted. Track the timer and clear it in cleanup(), while
preserving the existing success, error, and exit handling in the child readiness
flow.
- Around line 314-325: Update the integration tests around
defaultRelayfileBridge and withExternalControlPlaneDaemon to expose the daemon
child process to the callback, then assert the stale daemon has actually exited
after a compatible replacement. In the incompatibility case, assert the retained
daemon process remains alive, covering both replacement termination and
preservation lifecycle behavior.
- Around line 297-312: Make the auto-started daemon fixture in the “retries a
transient missing socket” test create its socket only after the initial request
attempt, while ensuring creation occurs before startTimeoutMs expires. Keep the
existing ensureCompatible assertion and cleanup unchanged so the test
deterministically exercises missing-socket retry behavior.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 2dc3a9bd-6f99-4098-b8fc-b01ebe305783

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0f6a6 and 9c5e4c2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • CHANGELOG.md
  • packages/cli/package.json
  • packages/cli/src/cli/commands/integration-relayfile-contract.test.ts

Comment on lines +138 to +166
await new Promise<void>((resolve, reject) => {
let stderr = '';
const fail = (message: string) => {
cleanup();
reject(new Error(message));
};
const onData = (chunk: Buffer) => {
if (String(chunk).includes('ready')) {
cleanup();
resolve();
}
};
const onError = (error: Error) => fail(`failed to start fake relayfile daemon: ${error.message}`);
const onExit = (code: number | null, signal: NodeJS.Signals | null) =>
fail(`fake relayfile daemon exited before ready (code ${code}, signal ${signal}): ${stderr}`);
const cleanup = () => {
child.stdout?.off('data', onData);
child.stderr?.off('data', onStderr);
child.off('error', onError);
child.off('exit', onExit);
};
const onStderr = (chunk: Buffer) => {
stderr += String(chunk);
};
child.stdout?.on('data', onData);
child.stderr?.on('data', onStderr);
child.once('error', onError);
child.once('exit', onExit);
});

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound the fake daemon readiness wait.

If the child stays alive without emitting ready, this promise never settles and the suite hangs until its global timeout. Add a short timer that rejects and kills the child, clearing it from cleanup().

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type ChildProcess, spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@packages/cli/src/cli/commands/integration-relayfile-contract.test.ts` around
lines 138 - 166, Add a short readiness timeout to the promise around the fake
daemon startup, rejecting with a clear error and killing the child when “ready”
is not emitted. Track the timer and clear it in cleanup(), while preserving the
existing success, error, and exit handling in the child readiness flow.

Comment on lines +297 to +312
it('retries a transient missing socket while an auto-started daemon becomes ready', async () => {
await withFakeRelayfileBinary('0.10.27', async (binary) => {
const socketPath = join(tmpdir(), `rf-transient-${process.pid}-${++socketSequence}.sock`);
rmSync(socketPath, { force: true });
const bridge = defaultRelayfileBridge({
socketPath,
binary,
autoStart: true,
startTimeoutMs: 2000,
requestTimeoutMs: 250,
});

await expect(bridge.ensureCompatible()).resolves.toBeUndefined();
rmSync(socketPath, { force: true });
});
});

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the missing-socket retry deterministic.

The fixture may create its socket before the first request, allowing this test to pass without exercising a retry. Delay socket creation beyond the initial request attempt while keeping it within startTimeoutMs.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type ChildProcess, spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@packages/cli/src/cli/commands/integration-relayfile-contract.test.ts` around
lines 297 - 312, Make the auto-started daemon fixture in the “retries a
transient missing socket” test create its socket only after the initial request
attempt, while ensuring creation occurs before startTimeoutMs expires. Keep the
existing ensureCompatible assertion and cleanup unchanged so the test
deterministically exercises missing-socket retry behavior.

Comment on lines +314 to +325
it('replaces a stale v1 daemon only when the installed binary can serve v3', async () => {
await withFakeRelayfileBinary('0.10.27', async (binary) => {
await withExternalControlPlaneDaemon('0.10.19', 1, [1], async (socketPath) => {
const bridge = defaultRelayfileBridge({
socketPath,
binary,
autoStart: true,
startTimeoutMs: 2000,
requestTimeoutMs: 1000,
});
await expect(bridge.ensureCompatible()).resolves.toBeUndefined();
});

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Assert the stale daemon’s actual process state.

These results do not prove the lifecycle contract: an unlink-only replacement or premature termination of the retained daemon could still pass. Expose the child to the callback, then assert it exited after replacement and remains alive after incompatibility.

Also applies to: 329-345

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type ChildProcess, spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@packages/cli/src/cli/commands/integration-relayfile-contract.test.ts` around
lines 314 - 325, Update the integration tests around defaultRelayfileBridge and
withExternalControlPlaneDaemon to expose the daemon child process to the
callback, then assert the stale daemon has actually exited after a compatible
replacement. In the incompatibility case, assert the retained daemon process
remains alive, covering both replacement termination and preservation lifecycle
behavior.

@khaliqgant

Copy link
Copy Markdown
Member Author

E2E verification against published relayfile v0.10.27

Ran the real agent-relay integration subscribe --list path against published npm artifacts (not CI, not mocks), using this branch's ^0.10.27 pin. Relay CLI built from 32f072a3b in an isolated worktree; npm ci resolved @relayfile/client@0.10.27 from the lockfile.

Pin flip verified clean

  • packages/cli/package.json"@relayfile/client": "^0.10.27"
  • git grep 0.10.27-rc across the branch → 0 hits
  • lockfile → client-0.10.27.tgz with integrity hash (real latest tarball, not the RC)

(a) Cold start — no daemon running

$ agent-relay integration subscribe --list
{ "bindings": [ { "provider": "slack", "resource": "/slack/channels/…/messages/**", "channel": "general", … } ], "webhooks": [], "subscriptions": [] }

Daemon auto-started, /v1/hello negotiated → daemonVersion=0.10.27 apiVersion=3 supported=[1,2,3], v3 call returned bindings. ✅

(b) Stale daemon — the scenario relayfile#354 fixes

Staged a genuine v0.10.19 daemon (apiVersion=1, supported=[1], no v3) as the pre-existing socket owner, then ran the same command with 0.10.27 installed:

STALE daemon pid=75313 (v0.10.19) owns $RELAYFILE_SOCK
$ agent-relay integration subscribe --list
{ "bindings": [ … ], "webhooks": [], "subscriptions": [] }
stale pid 75313 after? -> GONE (replaced by 0.10.27)

Stale v1-only daemon detected → stopped → replaced with 0.10.27 → renegotiated → v3 call succeeded. ✅

Direct @relayfile/client@0.10.27 checks confirmed the same both ways:

[cold]  hello: daemonVersion=0.10.27 apiVersion=3 supported=[1,2,3]  → listBindings() OK
[stale] pre-existing: version=0.10.19 apiVersion=1 supported=[1]
        ensureReady() OK -> stale daemon was replaced
        replacement: version=0.10.27 apiVersion=3 supported=[1,2,3]  → listBindings() OK

Upstream: relayfile#354 merged (889df50), v0.10.27 published to latest (all 11 packages), relayfile + @relayfile/client both resolve latest → 0.10.27.

Note: this exercised the macOS lsof owner-lookup branch; the Linux /proc/net/unix branch is covered by unit fixtures + ubuntu CI.

@khaliqgant
khaliqgant merged commit 60995c7 into main Jul 17, 2026
13 checks passed
@khaliqgant
khaliqgant deleted the fix/control-plane-hello-negotiation branch July 17, 2026 09:47
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.

1 participant