From 405882531b8f53b74fd8376cfee142d36d891701 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:32:28 -0700 Subject: [PATCH] fix(onboarding): a hosted agent going live completes "Connect your first agent" QA 2026-10-16 (fresh account, production f8b43625): after "Run it here" put a hosted agent live in My Workspace, and it answered, the board's TASK-001 "Connect your first agent" stayed pending. The card completes when a runtime token is used for the first time (starterTaskService, #916). That is a BYO agent's first check-in, and a Durable Object agent's too, because its worker polls with a token. A native-engine hosted agent has no token: turns run in-process, and it is live once provisioned. So nothing ever completed the card. - POST /api/hosted/provision, native branch: fire-and-forget completeConnectAgentStarterTask for the agent's pod, after the provisioned mark. The call never throws and is never awaited, so a board write cannot fail a provision. - The Durable Object path is unchanged: provision there doesn't yet mean listening, and the worker's first token use completes the card. - Tests: native provision completes it in the agent's pod (a type-safe mutation that drops the call fails it); Durable Object provision does not. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0157AG2wo8mJYqKezXNs48Ya --- backend/__tests__/unit/routes/hosted.test.js | 20 ++++++++++++++++++++ backend/routes/hosted.ts | 9 +++++++++ backend/services/starterTaskService.ts | 6 ++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/unit/routes/hosted.test.js b/backend/__tests__/unit/routes/hosted.test.js index a09d7ccc57..301b0cbb39 100644 --- a/backend/__tests__/unit/routes/hosted.test.js +++ b/backend/__tests__/unit/routes/hosted.test.js @@ -66,6 +66,10 @@ jest.mock('../../../services/nativeRuntimeService', () => ({ })); const mockMarkByoOpened = jest.fn(); jest.mock('../../../services/onboardingMarks', () => ({ markByoOpened: (...args) => mockMarkByoOpened(...args) })); +const mockCompleteConnectAgent = jest.fn(); +jest.mock('../../../services/starterTaskService', () => ({ + completeConnectAgentStarterTask: (...args) => mockCompleteConnectAgent(...args), +})); jest.mock('../../../routes/registry/tokens', () => ({ revokeRuntimeTokensForAgent: (...args) => mockRevokeRuntimeTokens(...args), issueRuntimeTokenForAgent: (...args) => mockIssueToken(...args), @@ -285,6 +289,9 @@ describe('/api/hosted', () => { }); expect(mockHosted.provisionAgent).toHaveBeenCalledWith({ agentName: 'scout', instanceId: 'demo', runtimeToken: 'cm_agent_secret' }); expect(installation.config.get('hosted').provisionedAt).toBeInstanceOf(Date); + // The worker's first use of its token completes the onboarding card, as a + // BYO agent's does; provision does not claim it is listening yet. + expect(mockCompleteConnectAgent).not.toHaveBeenCalled(); }); it('rotates: revokes the old credential rows, clears the stored hashes, mints fresh', async () => { @@ -467,6 +474,19 @@ describe('/api/hosted on the native engine', () => { expect(JSON.stringify(res.body)).not.toMatch(/cm_agent_/); }); + // QA 2026-10-16: "Connect your first agent" stayed pending after a hosted + // agent went live, because the card completes on a runtime token's first + // use and a native hosted agent has no token. Provision is its live moment. + it('provision completes the "Connect your first agent" card in the agent\'s pod', async () => { + mockFindOne.mockResolvedValue(nativeInstallation()); + const res = await request(app).post('/api/hosted/provision').send({ agentName: 'scout' }); + expect(res.status).toBe(200); + expect(mockCompleteConnectAgent).toHaveBeenCalledTimes(1); + const [{ podIds, agentLabel }] = mockCompleteConnectAgent.mock.calls[0]; + expect(podIds.map(String)).toEqual(['pod-1']); + expect(agentLabel).toBeTruthy(); + }); + it('provision of a native install 503s when the native runtime is unavailable, even with the worker up', async () => { mockHosted.isConfigured.mockReturnValue(true); mockNativeAvailable.mockReturnValue(false); diff --git a/backend/routes/hosted.ts b/backend/routes/hosted.ts index bf3b9b23c8..b0c724ca8f 100644 --- a/backend/routes/hosted.ts +++ b/backend/routes/hosted.ts @@ -21,6 +21,7 @@ const auth = require('../middleware/auth'); const { AgentInstallation } = require('../models/AgentRegistry'); const User = require('../models/User'); const AgentIdentityService = require('../services/agentIdentityService'); +const { completeConnectAgentStarterTask } = require('../services/starterTaskService'); const hostedRuntime = require('../services/hostedRuntimeService'); const { isNativeRuntimeAvailable } = require('../services/nativeRuntimeService'); const { @@ -258,6 +259,14 @@ router.post('/provision', hostedRateLimit, auth, async (req: any, res: any) => { { _id: installation._id }, { $set: { 'config.hosted.provisionedAt': hosted.provisionedAt || new Date() } }, ); + // Live from here, and with no runtime token there is no first use to + // complete "Connect your first agent" the way a BYO or Durable Object + // agent's does (starterTaskService). QA 2026-10-16: the card sat pending + // while the hosted agent answered. Never throws, never awaited. + void completeConnectAgentStarterTask({ + podIds: [installation.podId], + agentLabel: agentUser.botMetadata?.displayName || installation.displayName || agentName, + }); return res.json({ provisioned: true, engine, diff --git a/backend/services/starterTaskService.ts b/backend/services/starterTaskService.ts index 6a52931c5f..a2bc2c86e3 100644 --- a/backend/services/starterTaskService.ts +++ b/backend/services/starterTaskService.ts @@ -10,8 +10,10 @@ * starter task done" while the card sat in Pending forever. * * Caller contract: fire-and-forget from the token-auth first-use paths - * (agentRuntimeAuth HTTP + agentWebSocketService WS). Never throws; a board - * write must not be able to fail an auth request. + * (agentRuntimeAuth HTTP + agentWebSocketService WS), and from the native + * hosted provision (routes/hosted.ts), where no token exists to be first-used + * and the agent is live once provisioned. Never throws; a board write must not + * be able to fail an auth or provision request. */ import mongoose from 'mongoose'; import Task from '../models/Task';