From 61db2d30eb5b7b434d8a954c22a7e09d6376af99 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:08:45 -0700 Subject: [PATCH 1/4] fix(registry): project daemon-bound seats' connection in the roster (TASK-432) A seat bound to a daemon machine is treated as a wrapper by manage-settings, but the roster only attached `connection` when the install label was `wrapper` or `host: 'byo'`. A running daemon-managed seat whose label was something else carried no connection, so Settings showed "Connection status unavailable". Use the same machine binding in the roster, so the seat gets its live signal from the machine heartbeat and the runtime key. Tests: a daemon-bound seat with a live machine and an active key projects connected; a daemon-bound seat with no signal projects never, not missing. With the fix reverted, both fail; with it, the roster suite passes 12/12. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../registry.pod-agents-activity.test.js | 34 +++++++++++++++++++ backend/routes/registry/pod-agents.ts | 10 ++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js index 8b424b77b8..059da2ddd7 100644 --- a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js +++ b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js @@ -225,6 +225,40 @@ describe('pod agents lastActiveAt derivation (#915)', () => { expect(res.body.agents[0].connection.lastSeenAt).toEqual(expect.any(String)); }); + it('projects a daemon-bound seat as a wrapper even when its install label is not wrapper', async () => { + AgentInstallation.getInstalledAgents.mockResolvedValue([baseInstallation({ + agentName: 'local', config: new Map([['runtime', { runtimeType: 'claude-code' }]]), + })]); + User.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([{ + _id: 'bot-1', username: 'local-default', botMetadata: { machineId: 'machine-1' }, agentRuntimeTokens: [], + }]) }) }); + AgentCredential.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([ + { kind: 'runtime', agentUserId: 'bot-1', status: 'active', lastUsedAt: new Date() }, + { kind: 'daemon', machineId: 'machine-1', status: 'active' }, + ]) }) }); + Machine.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([{ + machineId: 'machine-1', lastSeenAt: new Date(), + }]) }) }); + const res = await request(app).get(`/api/registry/pods/${POD_ID}/agents`); + expect(res.status).toBe(200); + expect(res.body.agents[0].connection.state).toBe('connected'); + expect(res.body.agents[0].connection.lastSeenAt).toEqual(expect.any(String)); + }); + + it('reports a daemon-bound seat with no live signal as never connected, not missing', async () => { + AgentInstallation.getInstalledAgents.mockResolvedValue([baseInstallation({ + agentName: 'local', config: new Map([['runtime', { runtimeType: 'claude-code' }]]), + })]); + User.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([{ + _id: 'bot-1', username: 'local-default', botMetadata: { machineId: 'machine-1' }, agentRuntimeTokens: [], + }]) }) }); + AgentCredential.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([]) }) }); + Machine.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([]) }) }); + const res = await request(app).get(`/api/registry/pods/${POD_ID}/agents`); + expect(res.status).toBe(200); + expect(res.body.agents[0].connection).toEqual({ state: 'never', lastSeenAt: null }); + }); + it('surfaces AgentRun activity for native agents with no heartbeats and no tokens', async () => { AgentInstallation.getInstalledAgents.mockResolvedValue([baseInstallation()]); AgentRun.aggregate.mockResolvedValue([ diff --git a/backend/routes/registry/pod-agents.ts b/backend/routes/registry/pod-agents.ts index 8f809dc5a3..84329584f3 100644 --- a/backend/routes/registry/pod-agents.ts +++ b/backend/routes/registry/pod-agents.ts @@ -381,16 +381,22 @@ podAgentsRouter.get('/pods/:podId/agents', auth, async (req: any, res: any) => { // Legacy local-cli installs are normalized to their harness name // (codex/claude-code) in the public payload. Host, not that label, // identifies a BYO listener whose credential use proves liveness. + // A seat bound to a daemon machine is a wrapper whatever its install + // label says: manage-settings projects it as `wrapper` for the same + // reason. Without this, a running daemon-managed seat carried no + // connection and Settings rendered "Connection status unavailable". + const machineId = user?.botMetadata?.machineId; + const daemonBound = Boolean(machineId); const hasRuntimeConnection = payload?.runtime?.host === 'byo' || runtimeType === 'wrapper' + || daemonBound || (runtimeType === 'webhook' && !payload?.runtime?.host); if (!connectionAvailable || !user || !hasRuntimeConnection || normalizeConfigMap(i.config)?.runtime?.webhookUrl) return payload; - const machineId = user.botMetadata?.machineId; return { ...payload, connection: deriveAgentConnection({ - runtimeType, + runtimeType: daemonBound ? 'wrapper' : runtimeType, credentials: credentialByAgent.get(String(user._id)) || [], machineCredentials: credentialByMachine.get(String(machineId)) || [], embeddedTokens: [...(user.agentRuntimeTokens || []), ...(i.runtimeTokens || [])], From a0754c3d7d27f1ce840aa644be950ace73a21291 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:12:22 -0700 Subject: [PATCH 2/4] fix(registry): project daemon-bound seats' runtime type as wrapper (TASK-432) The UX gate held #2859: the roster gave a daemon-bound seat a `wrapper` connection, but kept the harness label in runtime.runtimeType. Settings keys the machine-key action and the Reconnect link on that field, so the seat got neither. Project runtime.runtimeType as `wrapper` for daemon-bound seats; the adapter label is left as it was. The test now asserts the projected type. With the projection removed the test fails; with it, the suite passes 12/12. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../unit/routes/registry.pod-agents-activity.test.js | 1 + backend/routes/registry/pod-agents.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js index 059da2ddd7..86dda16ca1 100644 --- a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js +++ b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js @@ -243,6 +243,7 @@ describe('pod agents lastActiveAt derivation (#915)', () => { expect(res.status).toBe(200); expect(res.body.agents[0].connection.state).toBe('connected'); expect(res.body.agents[0].connection.lastSeenAt).toEqual(expect.any(String)); + expect(res.body.agents[0].runtime.runtimeType).toBe('wrapper'); }); it('reports a daemon-bound seat with no live signal as never connected, not missing', async () => { diff --git a/backend/routes/registry/pod-agents.ts b/backend/routes/registry/pod-agents.ts index 84329584f3..f5996b6fe8 100644 --- a/backend/routes/registry/pod-agents.ts +++ b/backend/routes/registry/pod-agents.ts @@ -385,16 +385,21 @@ podAgentsRouter.get('/pods/:podId/agents', auth, async (req: any, res: any) => { // label says: manage-settings projects it as `wrapper` for the same // reason. Without this, a running daemon-managed seat carried no // connection and Settings rendered "Connection status unavailable". + // The runtime type is projected as `wrapper` too: Settings keys its + // machine-key action and Reconnect link on that field, not on connection. const machineId = user?.botMetadata?.machineId; const daemonBound = Boolean(machineId); + const seatPayload = daemonBound + ? { ...payload, runtime: { ...payload?.runtime, runtimeType: 'wrapper' } } + : payload; const hasRuntimeConnection = payload?.runtime?.host === 'byo' || runtimeType === 'wrapper' || daemonBound || (runtimeType === 'webhook' && !payload?.runtime?.host); if (!connectionAvailable || !user || !hasRuntimeConnection - || normalizeConfigMap(i.config)?.runtime?.webhookUrl) return payload; + || normalizeConfigMap(i.config)?.runtime?.webhookUrl) return seatPayload; return { - ...payload, + ...seatPayload, connection: deriveAgentConnection({ runtimeType: daemonBound ? 'wrapper' : runtimeType, credentials: credentialByAgent.get(String(user._id)) || [], From dc59622c1ce7dfcea6b1313194a6949667d8d17b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:14:19 -0700 Subject: [PATCH 3/4] fix(registry): keep the harness label as adapter for daemon-bound seats (TASK-432) Projecting runtimeType as `wrapper` dropped the harness name the chat tag reads. Move the harness into `adapter` (keeping an existing adapter when present), and assert it in the daemon-bound test. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../unit/routes/registry.pod-agents-activity.test.js | 1 + backend/routes/registry/pod-agents.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js index 86dda16ca1..4d14ebe596 100644 --- a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js +++ b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js @@ -244,6 +244,7 @@ describe('pod agents lastActiveAt derivation (#915)', () => { expect(res.body.agents[0].connection.state).toBe('connected'); expect(res.body.agents[0].connection.lastSeenAt).toEqual(expect.any(String)); expect(res.body.agents[0].runtime.runtimeType).toBe('wrapper'); + expect(res.body.agents[0].runtime.adapter).toBe('claude-code'); }); it('reports a daemon-bound seat with no live signal as never connected, not missing', async () => { diff --git a/backend/routes/registry/pod-agents.ts b/backend/routes/registry/pod-agents.ts index f5996b6fe8..15802ca223 100644 --- a/backend/routes/registry/pod-agents.ts +++ b/backend/routes/registry/pod-agents.ts @@ -389,8 +389,13 @@ podAgentsRouter.get('/pods/:podId/agents', auth, async (req: any, res: any) => { // machine-key action and Reconnect link on that field, not on connection. const machineId = user?.botMetadata?.machineId; const daemonBound = Boolean(machineId); + // The harness label moves to `adapter`, so the chat tag keeps naming it. const seatPayload = daemonBound - ? { ...payload, runtime: { ...payload?.runtime, runtimeType: 'wrapper' } } + ? { ...payload, runtime: { + ...payload?.runtime, + runtimeType: 'wrapper', + adapter: payload?.runtime?.adapter || payload?.runtime?.runtimeType, + } } : payload; const hasRuntimeConnection = payload?.runtime?.host === 'byo' || runtimeType === 'wrapper' From c3e8ff782f127cb206131ccda67ae4462c6dff29 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 02:38:57 -0700 Subject: [PATCH 4/4] test(registry): age the runtime key so only machine liveness connects the seat (TASK-432) The daemon-bound claude-code test used a fresh runtime-key lastUsedAt, so it would still pass if the route classified the seat as claude-code instead of wrapper. Aging the key past the 5-minute window makes the machine heartbeat the only source that can produce connected. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../__tests__/unit/routes/registry.pod-agents-activity.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js index 4d14ebe596..330de9ddd8 100644 --- a/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js +++ b/backend/__tests__/unit/routes/registry.pod-agents-activity.test.js @@ -232,8 +232,9 @@ describe('pod agents lastActiveAt derivation (#915)', () => { User.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([{ _id: 'bot-1', username: 'local-default', botMetadata: { machineId: 'machine-1' }, agentRuntimeTokens: [], }]) }) }); + // The runtime key is aged past the 5-minute window, so only the live machine can make this seat connected. AgentCredential.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([ - { kind: 'runtime', agentUserId: 'bot-1', status: 'active', lastUsedAt: new Date() }, + { kind: 'runtime', agentUserId: 'bot-1', status: 'active', lastUsedAt: new Date(Date.now() - 10 * 60 * 1000) }, { kind: 'daemon', machineId: 'machine-1', status: 'active' }, ]) }) }); Machine.find.mockReturnValue({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue([{