From b29adf10080a651c7bf1d2082476ba25b5408d0a Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 04:11:59 +0300 Subject: [PATCH] fix(team): match suffixed live agents during reconcile --- .../services/team/TeamProvisioningService.ts | 18 ++++++------ .../team/TeamProvisioningService.test.ts | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index f2aa0343..90c3e39f 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -12666,14 +12666,12 @@ export class TeamProvisioningService { current.bootstrapConfirmed = true; current.lastHeartbeatAt = current.lastHeartbeatAt ?? bootstrapMember.lastHeartbeatAt; } - const matchedRuntimeNames = [...configMembers].filter((name) => { - if (name === expected) return true; - const parsed = parseNumericSuffixName(name); - return parsed !== null && parsed.suffix >= 2 && parsed.base === expected; - }); - const runtimeAlive = - liveAgentNames.has(expected) || - matchedRuntimeNames.some((runtimeName) => liveAgentNames.has(runtimeName)); + const matchedConfigNames = [...configMembers].filter((name) => + matchesTeamMemberIdentity(name, expected) + ); + const runtimeAlive = [...liveAgentNames].some((name) => + matchesTeamMemberIdentity(name, expected) + ); const heartbeatMessage = leadInboxMessages.find((message) => { if (typeof message.from !== 'string' || message.from.trim() !== expected) return false; if ( @@ -12705,9 +12703,9 @@ export class TeamProvisioningService { current.sources = { ...(current.sources ?? {}), processAlive: runtimeAlive || undefined, - configRegistered: matchedRuntimeNames.length > 0 || undefined, + configRegistered: matchedConfigNames.length > 0 || undefined, configDrift: - heartbeatMessage != null && matchedRuntimeNames.length === 0 + heartbeatMessage != null && matchedConfigNames.length === 0 ? true : current.sources?.configDrift, inboxHeartbeat: heartbeatMessage != null ? true : current.sources?.inboxHeartbeat, diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index e43b8bfb..4a7bb356 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -5802,6 +5802,34 @@ describe('TeamProvisioningService', () => { }); }); + it('treats suffixed live runtime names as alive during persisted launch reconcile', async () => { + const teamName = 'suffixed-live-runtime-team'; + const leadSessionId = 'lead-session'; + writeLaunchConfig(teamName, '/Users/test/proj', leadSessionId, ['alice']); + writeLaunchState(teamName, leadSessionId, { + alice: { + launchState: 'runtime_pending_bootstrap', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: false, + hardFailure: false, + hardFailureReason: undefined, + firstSpawnAcceptedAt: new Date(Date.now() - 5_000).toISOString(), + }, + }); + + const svc = new TeamProvisioningService(); + (svc as any).getLiveTeamAgentNames = vi.fn(async () => new Set(['alice-2'])); + + const result = await svc.getMemberSpawnStatuses(teamName); + + expect(result.statuses.alice).toMatchObject({ + status: 'online', + launchState: 'runtime_pending_bootstrap', + runtimeAlive: true, + }); + }); + it('returns persisted expectedMembers as the union of expected and materialized launch members', async () => { const teamName = 'persisted-union-member-spawn-team'; const teamDir = path.join(tempTeamsBase, teamName);