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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ All notable changes to Agent Relay will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [Unreleased - Patch]

### Fixed

- Broker node connections recover when inventory acknowledgements stop even while WebSocket pongs continue, with bounded retries during outages.

## [12.2.0] - 2026-09-15

Expand Down
1,121 changes: 952 additions & 169 deletions crates/broker/src/node_control.rs

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion crates/broker/src/node_control/registration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,15 @@ async fn registration_gate_case(response: &str) {
})
.await
.expect("registration case must terminate after its bounded protocol exchange");
assert_eq!(result, ControlRunResult::Disconnected);
// Only a correlated `inventory.sync` reply proves application liveness,
// and this fixture never sends one (accepted or not) — it exercises the
// registration gate, not the inventory-ack liveness deadline.
assert_eq!(
result,
ControlRunResult::Disconnected {
application_ready: false,
}
);
assert!(
event_rx.try_recv().is_err(),
"no unexpected or duplicate runtime events"
Expand Down
55 changes: 55 additions & 0 deletions tests/fixtures/pr-proof-inventory-http-fixture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest';
import {
BROKER_NAME,
BROKER_TYPE,
BROKER_IDENTITY_HASH,
brokerHttpResponse,
} from '../relayflows/cases/1591-application-ack-reconnect/http-fixture.mjs';

const registration = () => ({
name: BROKER_NAME,
type: BROKER_TYPE,
metadata: { identity_key: BROKER_IDENTITY_HASH },
});

describe('inventory liveness HTTP fixture (unit only)', () => {
it('preserves the valid broker registration response', () => {
expect(brokerHttpResponse('POST', '/v1/agents', registration())).toEqual({
status: 200,
body: {
ok: true,
data: {
id: 'agent_proof_broker',
workspace_id: 'ws_proof',
name: BROKER_NAME,
token: 'at_proof',
status: 'active',
created_at: '2026-09-14T00:00:00Z',
},
},
});
});
it.each([
null,
{},
{ ...registration(), name: 'wrong' },
{ ...registration(), type: 'agent' },
{ ...registration(), metadata: {} },
{ ...registration(), metadata: { identity_key: 'wrong' } },
])('rejects invalid registration %j', (body) => {
expect(brokerHttpResponse('POST', '/v1/agents', body)).toMatchObject({
status: 400,
body: { ok: false, error: { code: 'fixture_registration_invalid' } },
});
});
it.each([
['GET', '/v1/agents'],
['POST', '/v1/agent'],
['PATCH', '/v1/agents/worker'],
])('rejects unexpected route %s %s', (method, route) => {
expect(brokerHttpResponse(method, route, registration())).toMatchObject({
status: 404,
body: { ok: false },
});
});
});
21 changes: 21 additions & 0 deletions tests/relayflows/cases/1591-application-ack-reconnect/case.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": 1,
"id": "1591-application-ack-reconnect",
"kind": "bugfix",
"title": "Reconnect when application acknowledgements stop",
"runner": {
"command": ["node", "tests/relayflows/cases/1591-application-ack-reconnect/run.mjs"]
},
"timeoutSeconds": 240,
"expected": {
"base": {
"outcome": "bug",
"signature": "application_ack_stall_not_detected"
},
"head": {
"outcome": "fixed",
"signature": "application_ack_stall_reconnects"
}
},
"requirements": ["broker-linux-x64"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createHash } from 'node:crypto';

export const BROKER_NAME = 'relayflow-inventory-ack';
export const BROKER_TYPE = 'human';
// Synthetic isolated-fixture identity, never a real workspace credential.
export const BROKER_IDENTITY = 'relayflow-inventory-ack-fixture-identity';
export const BROKER_IDENTITY_HASH = createHash('sha256').update(BROKER_IDENTITY).digest('hex');

/** Validate the HTTP bootstrap independently of the WebSocket liveness oracle. */
export function brokerHttpResponse(method, pathname, body) {
if (method !== 'POST' || pathname !== '/v1/agents') {
return {
status: 404,
body: { ok: false, error: { code: 'fixture_route_not_found', message: 'Unexpected fixture route' } },
};
}
if (
body?.name !== BROKER_NAME ||
body?.type !== BROKER_TYPE ||
body?.metadata?.identity_key !== BROKER_IDENTITY_HASH
) {
return {
status: 400,
body: {
ok: false,
error: {
code: 'fixture_registration_invalid',
message: 'Unexpected registration name, type, or identity metadata',
},
},
};
}
return {
status: 200,
body: {
ok: true,
data: {
id: 'agent_proof_broker',
workspace_id: 'ws_proof',
name: BROKER_NAME,
token: 'at_proof',
status: 'active',
created_at: '2026-09-14T00:00:00Z',
},
},
};
}
Loading
Loading