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
11 changes: 10 additions & 1 deletion .agents/skills/using-agent-relay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,19 @@ Prefer `send_dm` for lead/worker coordination. Use `post_message` when the
whole channel needs the update. Use `reply_to_thread` for follow-ups on a
specific message.

Choose the injection mode deliberately. Omit `mode` (or use `mode: "wait"`)
for normal coordination: Relay queues the message until the recipient reaches
a safe idle boundary, so it can remain unread while that agent is busy. Use
`mode: "steer"` only when immediate injection justifies interrupting active
work. In either mode, a successful send and message ID confirm enqueue, not
consumption; call `get_message_readers(message_id: "...")` before interpreting
silence as acknowledgement or refusal.

Examples:

```text
send_dm(to: "Lead", text: "STATUS: Auth routes are implemented; running tests next.")
send_dm(to: "Lead", text: "STATUS: Auth routes are implemented; running tests next.", mode: "wait")
send_dm(to: "Lead", text: "URGENT: Stop the deploy.", mode: "steer")
post_message(channel: "general", text: "The API endpoints are ready for review.")
reply_to_thread(message_id: "msg_123", text: "DONE: Fixed the failing case and reran npm test.")
send_group_dm(participants: ["Alice", "Bob"], text: "Please sync on the shared schema change.")
Expand Down
11 changes: 10 additions & 1 deletion .claude/skills/using-agent-relay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,19 @@ Prefer `send_dm` for lead/worker coordination. Use `post_message` when the
whole channel needs the update. Use `reply_to_thread` for follow-ups on a
specific message.

Choose the injection mode deliberately. Omit `mode` (or use `mode: "wait"`)
for normal coordination: Relay queues the message until the recipient reaches
a safe idle boundary, so it can remain unread while that agent is busy. Use
`mode: "steer"` only when immediate injection justifies interrupting active
work. In either mode, a successful send and message ID confirm enqueue, not
consumption; call `get_message_readers(message_id: "...")` before interpreting
silence as acknowledgement or refusal.

Examples:

```text
send_dm(to: "Lead", text: "STATUS: Auth routes are implemented; running tests next.")
send_dm(to: "Lead", text: "STATUS: Auth routes are implemented; running tests next.", mode: "wait")
send_dm(to: "Lead", text: "URGENT: Stop the deploy.", mode: "steer")
post_message(channel: "general", text: "The API endpoints are ready for review.")
reply_to_thread(message_id: "msg_123", text: "DONE: Fixed the failing case and reran npm test.")
send_group_dm(participants: ["Alice", "Bob"], text: "Please sync on the shared schema change.")
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `send_dm` and `agent-relay message dm send` receipts now name the exact requested and resolved recipients.
- `send_dm` and `agent-relay message dm send` now report unconfirmed enqueue without claiming delivery, including when recipient resolution is unavailable.
- `get_message_readers` and `agent-relay message inbox get_readers` now surface a queued-or-unread signal for an empty reader list.
- `send_dm` mode docs and `agent-relay message dm send --mode` help now explain that `wait` injects on idle while `steer` injects immediately and may interrupt active work.
- Fleet brokers now periodically renew authoritative worker inventory, including empty snapshots that clear stale server entries, so quiet agents remain active in Relaycast instead of aging offline while their node is healthy.

## [11.6.0] - 2026-08-13
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ agent-relay fleet spawn codex \
agent-relay fleet spawn codex --name api-worker --task "Review the current diff."

agent-relay message dm send api-worker "Detailed task instructions"
# Wake an idle worker immediately instead of queueing for its next tool boundary.
# wait is the default: it queues for the recipient's next safe idle boundary and
# can remain unread while that recipient is busy. steer requests immediate
# injection and may interrupt active work. A send ID confirms enqueue only;
# use `message inbox get_readers <id>` to confirm that the recipient consumed it.
agent-relay message dm send api-worker "Please check Relay now." --mode steer
agent-relay message inbox check --limit 20
agent-relay fleet release api-worker --reason "Work accepted"
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/src/cli/agent-relay-mcp.startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,35 @@ describe('createAgentRelayMcpServer', () => {
expect(promptResult.messages[0].content.text).not.toContain('workspace.create');
});

it('sends an unresolved DM from an agent-token-only session', async () => {
const { mod, mocks } = await loadAgentRelayMcpModule();

mod.createAgentRelayMcpServer({
agentToken: 'at_live_token_only',
agentName: 'TokenWorker',
});
const server = mocks.serverInstances[0];
const result = await server.tools.get('send_dm')?.handler({
to: 'chief',
text: 'token-scoped send',
});

expect(result.structuredContent).toMatchObject({
id: 'dm_1',
delivery: {
status: 'recipient_unresolved',
requestedRecipient: 'chief',
resolvedRecipient: null,
recipientMatched: null,
},
});
expect(result.structuredContent).not.toHaveProperty('target');
const agentRelay = mocks.relayInstances.find(
(instance) => instance.config.apiKey === 'at_live_token_only'
);
expect(agentRelay?.as).toHaveBeenCalledWith('at_live_token_only', { autoHeartbeatMs: false });
});

it('returns a created workspace key when local session persistence fails', async () => {
const { mod, mocks } = await loadAgentRelayMcpModule();
mocks.persistWorkspaceSession.mockImplementationOnce(() => {
Expand Down Expand Up @@ -654,6 +683,43 @@ describe('createAgentRelayMcpServer', () => {
expect(mocks.agentRelayMessagingCommands.getInvocation).toHaveBeenNthCalledWith(2, 'spawn', 'inv_nested');
});

it('times out when a persona spawn invocation lookup never settles', async () => {
vi.useFakeTimers();
try {
const { mod, mocks } = await loadAgentRelayMcpModule();
mocks.agentRelayMessagingCommands.getInvocation.mockImplementation(
async () => new Promise<never>(() => undefined)
);

mod.createAgentRelayMcpServer({
agentToken: 'at_live_fleet',
agentName: 'orchestrator',
});
const server = mocks.serverInstances[0];
const spawn = server.tools.get('spawn')?.handler({
name: 'PersonaWorker',
persona: 'reviewer',
});
const outcome = Promise.race([
spawn?.then(
() => 'resolved unexpectedly',
(error: unknown) => (error instanceof Error ? error.message : String(error))
),
new Promise<string>((resolve) =>
setTimeout(() => resolve('still pending after the persona spawn deadline'), 130_001)
),
]);

await vi.advanceTimersByTimeAsync(130_001);

await expect(outcome).resolves.toBe(
'Persona spawn timed out before broker registration and harness readiness.'
);
} finally {
vi.useRealTimers();
}
});

it('surfaces a nested verified spawn failure from the workforce persona result', async () => {
const { mod, mocks } = await loadAgentRelayMcpModule();
mocks.agentRelayMessagingCommands.invoke.mockResolvedValueOnce({
Expand Down
Loading
Loading