Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/sdk/src/__tests__/messaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,13 +515,13 @@ describe('RelaycastMessagingClient', () => {
name: 'builder-2',
status: 'offline',
live: false,
activeAgents: 0,
handlersLive: false,
maxAgents: 4,
load: 0,
lastHeartbeatAt: '2026-06-16T09:55:00.000Z',
createdAt: '2026-06-16T08:00:00.000Z',
});
expect(nodes[1].activeAgents).toBeUndefined();
expect(nodes[2]).toMatchObject({
name: 'builder-3',
status: 'unknown',
Expand Down Expand Up @@ -549,13 +549,14 @@ describe('RelaycastMessagingClient', () => {
expect(toRelayNode({ name: 'builder-6', tags: ['factory'] }).repoKeys).toBeUndefined();
expect(toRelayNode({ name: 'builder-unbounded', max_agents: 0, load: null }).load).toBeUndefined();

await expect(client.nodes.get('builder-2')).resolves.toMatchObject({
const offlineNode = await client.nodes.get('builder-2');
expect(offlineNode).toMatchObject({
name: 'builder-2',
status: 'offline',
live: false,
activeAgents: 0,
load: 0,
});
expect(offlineNode.activeAgents).toBeUndefined();
});

it('delegates write operations through an agent client and normalizes responses', async () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/sdk/src/__tests__/relaycast-translate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';

import { toRelayNode } from '../messaging/relaycast-translate.js';

describe('toRelayNode fleet liveness', () => {
it('omits stale active-agent load for an offline node', () => {
const node = toRelayNode({ status: 'offline', live: false, active_agents: 0 });
expect(node.activeAgents).toBeUndefined();
});

it('preserves measured zero for a live node', () => {
const node = toRelayNode({ status: 'online', live: true, active_agents: 0 });
expect(node.activeAgents).toBe(0);
});

it('omits active-agent load when liveness is unconfirmed', () => {
const node = toRelayNode({ status: 'online', active_agents: 4 });
expect(node.activeAgents).toBeUndefined();
});
});
12 changes: 9 additions & 3 deletions packages/sdk/src/messaging/relaycast-translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,22 @@ export function toRelayCapability(raw: unknown): RelayCapability {
export function toRelayNode(raw: unknown): RelayNode {
const node = (raw ?? {}) as Record<string, unknown>;
const rawStatus = readStr(node, 'status');
const status = rawStatus === 'online' || rawStatus === 'offline' ? rawStatus : 'unknown';
const live = readBoolean(node, 'live');
// Relaycast may retain a stale numeric load after a provider goes offline.
// Preserve zero as a real measurement only while liveness is authoritative.
const activeAgents =
live !== true || status === 'offline' ? undefined : readNumber(node, 'activeAgents', 'active_agents');
return {
id: readStr(node, 'id', 'node_id'),
nodeId: readStr(node, 'nodeId', 'node_id'),
name: readStr(node, 'name') ?? '',
status: rawStatus === 'online' || rawStatus === 'offline' ? rawStatus : 'unknown',
live: readBoolean(node, 'live'),
status,
live,
capabilities: Array.isArray(node.capabilities) ? node.capabilities.map(toRelayNodeCapability) : [],
repoKeys: readRepoKeys(node),
maxAgents: readNumber(node, 'maxAgents', 'max_agents'),
activeAgents: readNumber(node, 'activeAgents', 'active_agents'),
activeAgents,
handlersLive: readBoolean(node, 'handlersLive', 'handlers_live'),
load: readNumber(node, 'load'),
lastHeartbeatAt: readStr(node, 'lastHeartbeatAt', 'last_heartbeat_at'),
Expand Down
12 changes: 12 additions & 0 deletions tests/relayflows/cases/1610-sdk-node-load-liveness/case.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"id": "1610-sdk-node-load-liveness",
"kind": "bugfix",
"title": "Do not report stale active-agent load for offline nodes",
"runner": { "command": ["node", "tests/relayflows/cases/1610-sdk-node-load-liveness/run.mjs"] },
"timeoutSeconds": 900,
"expected": {
"base": { "outcome": "bug", "signature": "offline_node_load_reported_as_measurement" },
"head": { "outcome": "fixed", "signature": "offline_node_load_omitted" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expect, it } from 'vitest';
import { toRelayNode } from '../packages/sdk/src/messaging/relaycast-translate.js';

it('omits activeAgents for offline nodes and preserves live zero', () => {
const offline = toRelayNode({ status: 'offline', live: false, active_agents: 0 });
expect(offline.activeAgents).toBeUndefined();
const live = toRelayNode({ status: 'online', live: true, active_agents: 0 });
expect(live.activeAgents).toBe(0);
});
67 changes: 67 additions & 0 deletions tests/relayflows/cases/1610-sdk-node-load-liveness/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { access, mkdir, copyFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const arm = process.env.RELAY_PR_PROOF_ARM;
const targetDir = process.env.RELAY_PR_PROOF_TARGET_DIR;
const resultPath = process.env.RELAY_PR_PROOF_RESULT_PATH;
if (!['base', 'head'].includes(arm) || !targetDir || !resultPath)
throw new Error('RelayFlow proof environment is incomplete');
const caseDir = path.dirname(fileURLToPath(import.meta.url));
async function pathExists(candidate) {
try {
await access(candidate);
return true;
} catch {
return false;
}
}
function run(command, args, cwd) {
const result = spawnSync(command, args, {
cwd,
env: process.env,
encoding: 'utf8',
maxBuffer: 16 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
});
process.stdout.write(result.stdout ?? '');
process.stderr.write(result.stderr ?? '');
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}`);
}
// Modelled on the self-sufficient 1619-placement-liveness proof: the target
// checkout may be a sparse arm without node_modules, so install before
// resolving Vitest instead of assuming target/node_modules/vitest exists.
const vitest = path.join(targetDir, 'node_modules', 'vitest', 'vitest.mjs');
if (!(await pathExists(vitest))) run('npm', ['ci', '--no-audit', '--no-fund'], targetDir);
const proofDir = path.join(targetDir, '.relay-pr-proof');
await mkdir(proofDir, { recursive: true });
await copyFile(path.join(caseDir, 'probe.test.mts'), path.join(proofDir, 'node-load.test.mts'));
await writeFile(
path.join(proofDir, 'vitest.config.mts'),
"import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', include: ['.relay-pr-proof/node-load.test.mts'] } });\n"
);
const result = spawnSync(
process.execPath,
[vitest, 'run', '--config', path.join(proofDir, 'vitest.config.mts'), '--reporter=verbose'],
{ cwd: targetDir, env: process.env, encoding: 'utf8', stdio: 'inherit' }
);
const failed = result.status !== 0;
if ((arm === 'base') !== failed) throw new Error(`unexpected ${arm} result: ${result.status}`);
await writeFile(
resultPath,
JSON.stringify(
{
version: 1,
caseId: '1610-sdk-node-load-liveness',
arm,
outcome: arm === 'base' ? 'bug' : 'fixed',
signature: arm === 'base' ? 'offline_node_load_reported_as_measurement' : 'offline_node_load_omitted',
details: 'SDK translation distinguishes unreachable liveness from measured zero.',
},
null,
2
) + '\n'
);
Loading