diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 6785e8f2..3e714bd0 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -3719,6 +3719,7 @@ export class TeamProvisioningService { this.liveTeamAgentRuntimeMetadataCache.delete(teamName); this.liveTeamAgentRuntimeMetadataInFlightByTeam.delete(teamName); this.runtimeProcessRowsForUsageSnapshotByTeam.delete(teamName); + this.runtimeProcessUsageStatsCacheByPid.clear(); } private cloneMemberSpawnStatusesSnapshot( @@ -25329,23 +25330,30 @@ export class TeamProvisioningService { let processRows: RuntimeTelemetryProcessTableRow[] = []; let processTableAvailable = true; - try { - processRows = - this.normalizeRuntimeProcessRowsForTelemetry( - await this.withRuntimeTelemetryTimeout( - listRuntimeProcessTableForCurrentPlatform(), - TeamProvisioningService.RUNTIME_PROCESS_TABLE_TIMEOUT_MS, - 'process table runtime snapshot' - ), - process.platform === 'win32' ? 'wsl' : 'native' - ) ?? []; - } catch (error) { - processTableAvailable = false; - logger.debug( - `[${teamName}] Failed to read process table for runtime snapshot: ${ - error instanceof Error ? error.message : String(error) - }` - ); + const shouldReadProcessTable = this.shouldReadProcessTableForLiveRuntimeMetadata({ + metadataByMember, + launchSnapshot: persistedLaunchSnapshot, + paneInfoById, + }); + if (shouldReadProcessTable) { + try { + processRows = + this.normalizeRuntimeProcessRowsForTelemetry( + await this.withRuntimeTelemetryTimeout( + listRuntimeProcessTableForCurrentPlatform(), + TeamProvisioningService.RUNTIME_PROCESS_TABLE_TIMEOUT_MS, + 'process table runtime snapshot' + ), + process.platform === 'win32' ? 'wsl' : 'native' + ) ?? []; + } catch (error) { + processTableAvailable = false; + logger.debug( + `[${teamName}] Failed to read process table for runtime snapshot: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } } this.runtimeProcessRowsForUsageSnapshotByTeam.set(teamName, { expiresAtMs: Date.now() + this.getAgentRuntimeSnapshotCacheTtlMs(teamName, runId), @@ -25746,6 +25754,34 @@ export class TeamProvisioningService { return normalizedRows; } + private shouldReadProcessTableForLiveRuntimeMetadata(params: { + metadataByMember: ReadonlyMap; + launchSnapshot: PersistedTeamLaunchSnapshot | null | undefined; + paneInfoById: ReadonlyMap; + }): boolean { + for (const [memberName, metadata] of params.metadataByMember.entries()) { + if (metadata.agentId?.trim()) { + return true; + } + const paneId = metadata.tmuxPaneId?.trim() ?? ''; + if (paneId && params.paneInfoById.has(paneId)) { + return true; + } + const launchRuntimePid = params.launchSnapshot?.members[memberName]?.runtimePid; + if ( + (typeof metadata.metricsPid === 'number' && + Number.isFinite(metadata.metricsPid) && + metadata.metricsPid > 0) || + (typeof launchRuntimePid === 'number' && + Number.isFinite(launchRuntimePid) && + launchRuntimePid > 0) + ) { + return true; + } + } + return false; + } + private async readRuntimeProcessRowsForUsageSnapshot( teamName: string, options: { includeWindowsHostRows?: boolean } = {} diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 40ea9195..3ad5868f 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -3335,7 +3335,7 @@ describe('TeamProvisioningService', () => { getConfig: vi.fn(async () => ({ members: [ { name: 'team-lead', agentType: 'team-lead' }, - { name: 'alice', model: 'gpt-5.4-mini' }, + { name: 'alice', model: 'gpt-5.4-mini', agentId: 'alice@runtime-team' }, ], })), }; @@ -3363,7 +3363,7 @@ describe('TeamProvisioningService', () => { getConfig: vi.fn(async () => ({ members: [ { name: 'team-lead', agentType: 'team-lead' }, - { name: 'alice', model: 'gpt-5.4-mini' }, + { name: 'alice', model: 'gpt-5.4-mini', agentId: 'alice@runtime-team' }, ], })), }; @@ -3385,6 +3385,26 @@ describe('TeamProvisioningService', () => { expect(listRuntimeProcessTableForCurrentPlatform).toHaveBeenCalledTimes(1); }); + it('skips live process table reads when runtime metadata has no verifiable handle', async () => { + const svc = new TeamProvisioningService(); + (svc as any).configReader = { + getConfig: vi.fn(async () => ({ + members: [ + { name: 'team-lead', agentType: 'team-lead' }, + { name: 'alice', model: 'gpt-5.4-mini' }, + ], + })), + }; + + const metadata = (await (svc as any).getLiveTeamAgentRuntimeMetadata('runtime-team')) as Map< + string, + unknown + >; + + expect(metadata.has('alice')).toBe(true); + expect(listRuntimeProcessTableForCurrentPlatform).not.toHaveBeenCalled(); + }); + it('uses a longer live runtime metadata cache for persisted teams without a tracked run', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-05-03T12:00:00.000Z')); @@ -3393,7 +3413,7 @@ describe('TeamProvisioningService', () => { getConfig: vi.fn(async () => ({ members: [ { name: 'team-lead', agentType: 'team-lead' }, - { name: 'alice', model: 'gpt-5.4-mini' }, + { name: 'alice', model: 'gpt-5.4-mini', agentId: 'alice@runtime-team' }, ], })), }; @@ -3421,7 +3441,7 @@ describe('TeamProvisioningService', () => { getConfig: vi.fn(async () => ({ members: [ { name: 'team-lead', agentType: 'team-lead' }, - { name: 'alice', model: 'gpt-5.4-mini' }, + { name: 'alice', model: 'gpt-5.4-mini', agentId: 'alice@runtime-team' }, ], })), }; @@ -3441,7 +3461,7 @@ describe('TeamProvisioningService', () => { getConfig: vi.fn(async () => ({ members: [ { name: 'team-lead', agentType: 'team-lead' }, - { name: 'alice', model: 'gpt-5.4-mini' }, + { name: 'alice', model: 'gpt-5.4-mini', agentId: 'alice@runtime-team' }, ], })), }; @@ -3462,7 +3482,12 @@ describe('TeamProvisioningService', () => { getConfig: vi.fn(async () => ({ members: [ { name: 'team-lead', agentType: 'team-lead' }, - { name: 'alice', providerId: 'opencode', model: 'gpt-5.4-mini' }, + { + name: 'alice', + providerId: 'opencode', + model: 'gpt-5.4-mini', + agentId: 'alice@runtime-team', + }, ], })), }; @@ -4532,7 +4557,10 @@ describe('TeamProvisioningService', () => { (TeamProvisioningService as any).RUNTIME_PROCESS_TABLE_TIMEOUT_MS = 5; (svc as any).configReader = { getConfig: vi.fn(async () => ({ - members: [{ name: 'team-lead', agentType: 'team-lead' }], + members: [ + { name: 'team-lead', agentType: 'team-lead' }, + { name: 'alice', model: 'gpt-5.4-mini', agentId: 'alice@runtime-team' }, + ], })), }; (svc as any).aliveRunByTeam.set('runtime-team', 'run-1');