From f19ed93d4bd08ff5a7fed42c22536a50426ae642 Mon Sep 17 00:00:00 2001 From: 777genius Date: Mon, 18 May 2026 23:31:58 +0300 Subject: [PATCH] fix(team): keep codex member work visible during recovery --- runtime.lock.json | 12 +- .../memberWorkSyncTeamActivity.test.ts | 106 +++++++++++ .../composition/memberWorkSyncTeamActivity.ts | 30 +++ src/features/member-work-sync/main/index.ts | 4 + src/main/index.ts | 61 +++++-- .../components/team/members/MemberCard.tsx | 31 +++- .../team/members/MemberHoverCard.tsx | 14 +- .../components/team/members/MemberList.tsx | 53 +++--- .../utils/__tests__/memberHelpers.test.ts | 172 ++++++++++++++++++ src/renderer/utils/memberHelpers.ts | 113 +++++++++++- 10 files changed, 535 insertions(+), 61 deletions(-) create mode 100644 src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts create mode 100644 src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts create mode 100644 src/renderer/utils/__tests__/memberHelpers.test.ts diff --git a/runtime.lock.json b/runtime.lock.json index 3e0f0015..c4ef8eda 100644 --- a/runtime.lock.json +++ b/runtime.lock.json @@ -1,27 +1,27 @@ { - "version": "0.0.37", - "sourceRef": "v0.0.37", + "version": "0.0.38", + "sourceRef": "v0.0.38", "sourceRepository": "777genius/agent_teams_orchestrator", "releaseRepository": "777genius/agent-teams-ai", "releaseTag": "v2.0.0", "assets": { "darwin-arm64": { - "file": "agent-teams-runtime-darwin-arm64-v0.0.37.tar.gz", + "file": "agent-teams-runtime-darwin-arm64-v0.0.38.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "darwin-x64": { - "file": "agent-teams-runtime-darwin-x64-v0.0.37.tar.gz", + "file": "agent-teams-runtime-darwin-x64-v0.0.38.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "linux-x64": { - "file": "agent-teams-runtime-linux-x64-v0.0.37.tar.gz", + "file": "agent-teams-runtime-linux-x64-v0.0.38.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "win32-x64": { - "file": "agent-teams-runtime-win32-x64-v0.0.37.zip", + "file": "agent-teams-runtime-win32-x64-v0.0.38.zip", "archiveKind": "zip", "binaryName": "claude-multimodel.exe" } diff --git a/src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts b/src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts new file mode 100644 index 00000000..d352c19f --- /dev/null +++ b/src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from 'vitest'; + +import { + hasWorkSyncActiveRuntime, + isRuntimeEntryActiveForWorkSync, +} from '../memberWorkSyncTeamActivity'; + +import type { TeamAgentRuntimeEntry, TeamAgentRuntimeSnapshot } from '@shared/types'; + +function createRuntimeEntry(overrides: Partial = {}): TeamAgentRuntimeEntry { + return { + memberName: 'alice', + alive: true, + restartable: true, + backendType: 'process', + providerId: 'codex', + providerBackendId: 'codex-native', + livenessKind: 'runtime_process', + pid: 46773, + updatedAt: '2026-05-18T19:44:48.000Z', + ...overrides, + }; +} + +function createRuntimeSnapshot( + members: Record +): TeamAgentRuntimeSnapshot { + return { + teamName: 'signal-ops-6', + updatedAt: '2026-05-18T19:44:48.000Z', + runId: null, + members, + }; +} + +describe('member work sync team activity', () => { + it('treats a verified runtime process as active', () => { + expect(isRuntimeEntryActiveForWorkSync(createRuntimeEntry())).toBe(true); + }); + + it('treats a confirmed bootstrap runtime entry as active', () => { + expect( + isRuntimeEntryActiveForWorkSync( + createRuntimeEntry({ + livenessKind: 'confirmed_bootstrap', + runtimeLastSeenAt: '2026-05-18T19:44:47.000Z', + }) + ) + ).toBe(true); + }); + + it('does not treat inactive liveness diagnostics as active by themselves', () => { + for (const livenessKind of [ + 'permission_blocked', + 'runtime_process_candidate', + 'shell_only', + 'registered_only', + 'stale_metadata', + 'not_found', + ] as const) { + expect(isRuntimeEntryActiveForWorkSync(createRuntimeEntry({ livenessKind }))).toBe(false); + } + }); + + it('does not treat a runtime candidate as active until it is alive', () => { + expect( + isRuntimeEntryActiveForWorkSync( + createRuntimeEntry({ + alive: false, + livenessKind: 'runtime_process_candidate', + }) + ) + ).toBe(false); + }); + + it('detects an active runtime among stale members', () => { + expect( + hasWorkSyncActiveRuntime( + createRuntimeSnapshot({ + alice: createRuntimeEntry({ alive: false, livenessKind: 'stale_metadata' }), + bob: createRuntimeEntry({ memberName: 'bob', livenessKind: 'runtime_process' }), + }) + ) + ).toBe(true); + }); + + it('returns false when no member has active runtime evidence', () => { + expect( + hasWorkSyncActiveRuntime( + createRuntimeSnapshot({ + alice: createRuntimeEntry({ alive: false, livenessKind: 'stale_metadata' }), + bob: createRuntimeEntry({ + memberName: 'bob', + alive: false, + livenessKind: 'registered_only', + }), + }) + ) + ).toBe(false); + }); + + it('handles missing snapshots as inactive', () => { + expect(hasWorkSyncActiveRuntime(null)).toBe(false); + expect(hasWorkSyncActiveRuntime(undefined)).toBe(false); + }); +}); diff --git a/src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts b/src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts new file mode 100644 index 00000000..a6a475a2 --- /dev/null +++ b/src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts @@ -0,0 +1,30 @@ +import type { TeamAgentRuntimeEntry, TeamAgentRuntimeSnapshot } from '@shared/types'; + +type RuntimeLivenessKind = NonNullable; + +const WORK_SYNC_INACTIVE_LIVENESS_KINDS = new Set([ + 'permission_blocked', + 'runtime_process_candidate', + 'shell_only', + 'registered_only', + 'stale_metadata', + 'not_found', +]); + +export function isRuntimeEntryActiveForWorkSync( + entry: Pick | null | undefined +): boolean { + if (entry?.alive !== true) { + return false; + } + if (!entry.livenessKind) { + return true; + } + return !WORK_SYNC_INACTIVE_LIVENESS_KINDS.has(entry.livenessKind); +} + +export function hasWorkSyncActiveRuntime( + snapshot: Pick | null | undefined +): boolean { + return Object.values(snapshot?.members ?? {}).some(isRuntimeEntryActiveForWorkSync); +} diff --git a/src/features/member-work-sync/main/index.ts b/src/features/member-work-sync/main/index.ts index 3adc9bf8..6f5f1fa8 100644 --- a/src/features/member-work-sync/main/index.ts +++ b/src/features/member-work-sync/main/index.ts @@ -8,3 +8,7 @@ export { buildMemberWorkSyncRuntimeTurnSettledEnvironment, createMemberWorkSyncFeature, } from './composition/createMemberWorkSyncFeature'; +export { + hasWorkSyncActiveRuntime, + isRuntimeEntryActiveForWorkSync, +} from './composition/memberWorkSyncTeamActivity'; diff --git a/src/main/index.ts b/src/main/index.ts index c9c71c37..8cc26c0c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -40,6 +40,7 @@ import { import { buildMemberWorkSyncRuntimeTurnSettledEnvironment, createMemberWorkSyncFeature, + hasWorkSyncActiveRuntime, type MemberWorkSyncFeatureFacade, registerMemberWorkSyncIpc, removeMemberWorkSyncIpc, @@ -1780,27 +1781,55 @@ async function initializeServices(): Promise { logger: createLogger('Feature:RecentProjects'), }); runtimeProviderManagementFeature = createRuntimeProviderManagementFeature(); + const memberWorkSyncLogger = createLogger('Feature:MemberWorkSync'); + const hasMemberWorkSyncRuntimeActivity = async (teamName: string): Promise => { + try { + const snapshot = await teamProvisioningService.getTeamAgentRuntimeSnapshot(teamName); + return hasWorkSyncActiveRuntime(snapshot); + } catch (error) { + memberWorkSyncLogger.warn('member work sync runtime activity check failed', { + teamName, + error: String(error), + }); + return false; + } + }; + const isTeamActiveForMemberWorkSync = async (teamName: string): Promise => { + if ( + teamProvisioningService.isTeamAlive(teamName) || + teamProvisioningService.hasProvisioningRun(teamName) + ) { + return true; + } + return hasMemberWorkSyncRuntimeActivity(teamName); + }; + const canDispatchMemberWorkSyncNudges = async (teamName: string): Promise => { + if (teamProvisioningService.isTeamAlive(teamName)) { + return true; + } + return hasMemberWorkSyncRuntimeActivity(teamName); + }; + const listMemberWorkSyncLifecycleActiveTeamNames = async (): Promise => { + const activeTeamNames: string[] = []; + for (const team of await teamDataService.listTeams()) { + if (team.deletedAt) { + continue; + } + if (await isTeamActiveForMemberWorkSync(team.teamName)) { + activeTeamNames.push(team.teamName); + } + } + return activeTeamNames; + }; memberWorkSyncFeature = createMemberWorkSyncFeature({ teamsBasePath: getTeamsBasePath(), configReader: new TeamConfigReader(), taskReader: new TeamTaskReader(), kanbanManager: new TeamKanbanManager(), membersMetaStore: new TeamMembersMetaStore(), - isTeamActive: (teamName) => - teamProvisioningService.isTeamAlive(teamName) || - teamProvisioningService.hasProvisioningRun(teamName), - canDispatchNudges: (teamName) => teamProvisioningService.isTeamAlive(teamName), - listLifecycleActiveTeamNames: async () => { - const teams = await teamDataService.listTeams(); - return teams - .filter( - (team) => - !team.deletedAt && - (teamProvisioningService.isTeamAlive(team.teamName) || - teamProvisioningService.hasProvisioningRun(team.teamName)) - ) - .map((team) => team.teamName); - }, + isTeamActive: isTeamActiveForMemberWorkSync, + canDispatchNudges: canDispatchMemberWorkSyncNudges, + listLifecycleActiveTeamNames: listMemberWorkSyncLifecycleActiveTeamNames, extraBusySignals: [ { isBusy: (input) => teamProvisioningService.getOpenCodeMemberDeliveryBusyStatus(input), @@ -1984,7 +2013,7 @@ async function initializeServices(): Promise { }); }, }, - logger: createLogger('Feature:MemberWorkSync'), + logger: memberWorkSyncLogger, }); teamProvisioningService.setRuntimeTurnSettledHookSettingsProvider((input) => memberWorkSyncFeature diff --git a/src/renderer/components/team/members/MemberCard.tsx b/src/renderer/components/team/members/MemberCard.tsx index 49c6fc87..f8c82df3 100644 --- a/src/renderer/components/team/members/MemberCard.tsx +++ b/src/renderer/components/team/members/MemberCard.tsx @@ -16,6 +16,7 @@ import { buildMemberLaunchPresentation, displayMemberName, isOpenCodeRelaunchActionable, + shouldDisplayMemberCurrentTask, } from '@renderer/utils/memberHelpers'; import { buildMemberLaunchDiagnosticsPayload, @@ -650,8 +651,18 @@ export const MemberCard = memo(function MemberCard({ selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : [] ); const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); + const showTaskActivity = shouldDisplayMemberCurrentTask({ + member, + isTeamAlive, + spawnStatus, + spawnLaunchState, + spawnRuntimeAlive, + runtimeEntry, + }); + const visibleCurrentTask = showTaskActivity ? currentTask : null; + const visibleReviewTask = showTaskActivity ? reviewTask : null; const presentationMember = - member.currentTaskId && !currentTask + member.currentTaskId && !visibleCurrentTask ? { ...member, currentTaskId: null, @@ -716,11 +727,11 @@ export const MemberCard = memo(function MemberCard({ workspacePath ? `Path: ${workspacePath}` : 'Path is not available yet.', member.gitBranch ? `Branch: ${member.gitBranch}` : null, ].filter((line): line is string => Boolean(line)); - const activityTask = currentTask ?? reviewTask ?? null; - const activityTitle = currentTask - ? `Current task: #${deriveTaskDisplayId(currentTask.id)}` - : reviewTask - ? `Reviewing task: #${deriveTaskDisplayId(reviewTask.id)}` + const activityTask = visibleCurrentTask ?? visibleReviewTask ?? null; + const activityTitle = visibleCurrentTask + ? `Current task: #${deriveTaskDisplayId(visibleCurrentTask.id)}` + : visibleReviewTask + ? `Reviewing task: #${deriveTaskDisplayId(visibleReviewTask.id)}` : undefined; const runtimeTelemetryTitle = buildRuntimeTelemetryTitle(runtimeEntry); const showRuntimeTelemetryTooltip = Boolean(runtimeTelemetryTitle); @@ -1060,9 +1071,9 @@ export const MemberCard = memo(function MemberCard({ ) : null} - {currentTask ? ( + {visibleCurrentTask ? ( ) : null} - {reviewTask ? ( + {visibleReviewTask ? ( task.reviewer === member.name && @@ -211,6 +211,18 @@ export const MemberHoverCard = memo(function MemberHoverCard({ getTeamTaskWorkflowColumn(task) === 'review' ) ?? null) : null; + const reviewTask = + reviewTaskCandidate && + shouldDisplayMemberCurrentTask({ + member, + isTeamAlive, + spawnStatus: spawnEntry?.status, + spawnLaunchState: spawnEntry?.launchState, + spawnRuntimeAlive: spawnEntry?.runtimeAlive, + runtimeEntry, + }) + ? reviewTaskCandidate + : null; return ( diff --git a/src/renderer/components/team/members/MemberList.tsx b/src/renderer/components/team/members/MemberList.tsx index 970773cf..3ce95f7c 100644 --- a/src/renderer/components/team/members/MemberList.tsx +++ b/src/renderer/components/team/members/MemberList.tsx @@ -769,32 +769,18 @@ export const MemberList = memo(function MemberList({ const isMemberActivityTimerRunning = useCallback( ( + member: ResolvedTeamMember, spawnEntry: MemberSpawnStatusEntry | undefined, runtimeEntry: TeamAgentRuntimeEntry | undefined ): boolean => { - if (isTeamAlive === false) return false; - if ( - spawnEntry?.status === 'offline' || - spawnEntry?.status === 'error' || - spawnEntry?.status === 'skipped' - ) { - return false; - } - if (spawnEntry?.runtimeAlive === false) { - return false; - } - if (runtimeEntry?.alive === false) { - return false; - } - if ( - runtimeEntry?.livenessKind === 'shell_only' || - runtimeEntry?.livenessKind === 'registered_only' || - runtimeEntry?.livenessKind === 'stale_metadata' || - runtimeEntry?.livenessKind === 'not_found' - ) { - return false; - } - return true; + return shouldDisplayMemberCurrentTask({ + member, + isTeamAlive, + spawnStatus: spawnEntry?.status, + spawnLaunchState: spawnEntry?.launchState, + spawnRuntimeAlive: spawnEntry?.runtimeAlive, + runtimeEntry, + }); }, [isTeamAlive] ); @@ -827,7 +813,7 @@ export const MemberList = memo(function MemberList({ for (const member of activeMembers) { const spawnEntry = memberSpawnStatuses?.get(member.name); const runtimeEntry = memberRuntimeEntries?.get(member.name); - const running = isMemberActivityTimerRunning(spawnEntry, runtimeEntry); + const running = isMemberActivityTimerRunning(member, spawnEntry, runtimeEntry); const currentTaskCandidate = member.currentTaskId ? (taskMap.get(member.currentTaskId) ?? null) : null; @@ -948,8 +934,23 @@ export const MemberList = memo(function MemberList({ : null; const reviewCandidate = reviewTaskByMember.get(member.name) ?? null; const reviewTask = - reviewCandidate && reviewCandidate.id !== currentTask?.id ? reviewCandidate : null; - const activityTimerRunning = isMemberActivityTimerRunning(spawnEntry, runtimeEntry); + reviewCandidate && + reviewCandidate.id !== currentTask?.id && + shouldDisplayMemberCurrentTask({ + member, + isTeamAlive, + spawnStatus: spawnEntry?.status, + spawnLaunchState: spawnEntry?.launchState, + spawnRuntimeAlive: spawnEntry?.runtimeAlive, + runtimeEntry, + }) + ? reviewCandidate + : null; + const activityTimerRunning = isMemberActivityTimerRunning( + member, + spawnEntry, + runtimeEntry + ); const currentTaskTimer = withActivityTimerRunId( currentTask ? deriveWorkActivityTimerAnchor(currentTask, { diff --git a/src/renderer/utils/__tests__/memberHelpers.test.ts b/src/renderer/utils/__tests__/memberHelpers.test.ts new file mode 100644 index 00000000..c656db29 --- /dev/null +++ b/src/renderer/utils/__tests__/memberHelpers.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it } from 'vitest'; + +import { buildMemberLaunchPresentation, shouldDisplayMemberCurrentTask } from '../memberHelpers'; + +import type { + MemberLaunchState, + MemberSpawnStatus, + ResolvedTeamMember, + TeamAgentRuntimeEntry, +} from '@shared/types'; + +function createMember(overrides: Partial = {}): ResolvedTeamMember { + return { + name: 'alice', + status: 'active', + currentTaskId: 'task-1', + taskCount: 1, + lastActiveAt: null, + messageCount: 0, + providerId: 'codex', + providerBackendId: 'codex-native', + role: 'developer', + ...overrides, + }; +} + +function createLiveRuntime(overrides: Partial = {}): TeamAgentRuntimeEntry { + return { + memberName: 'alice', + alive: true, + restartable: true, + backendType: 'process', + providerId: 'codex', + providerBackendId: 'codex-native', + livenessKind: 'runtime_process', + pid: 12345, + rssBytes: 128 * 1024 * 1024, + updatedAt: '2026-05-18T19:45:00.000Z', + ...overrides, + }; +} + +function createConfirmedCodexSpawn(): { + spawnStatus: MemberSpawnStatus; + spawnLaunchState: MemberLaunchState; + spawnRuntimeAlive: boolean; + spawnBootstrapConfirmed: boolean; +} { + return { + spawnStatus: 'online', + spawnLaunchState: 'confirmed_alive', + spawnRuntimeAlive: true, + spawnBootstrapConfirmed: true, + }; +} + +describe('member runtime presentation', () => { + it('hides Codex native task activity when no spawn or runtime snapshot has loaded', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: createMember(), + isTeamAlive: true, + }) + ).toBe(false); + }); + + it('hides Codex native task activity when confirmed spawn state has no live runtime evidence', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: createMember(), + isTeamAlive: true, + ...createConfirmedCodexSpawn(), + }) + ).toBe(false); + }); + + it('keeps Codex native task activity visible when the runtime process is live', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: createMember(), + isTeamAlive: true, + ...createConfirmedCodexSpawn(), + runtimeEntry: createLiveRuntime(), + }) + ).toBe(true); + }); + + it('hides Codex native task activity for runtime process candidates without verified process evidence', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: createMember(), + isTeamAlive: true, + ...createConfirmedCodexSpawn(), + runtimeEntry: createLiveRuntime({ + livenessKind: 'runtime_process_candidate', + rssBytes: undefined, + }), + }) + ).toBe(false); + }); + + it('marks stale confirmed Codex native spawn state as non-green runtime status', () => { + const presentation = buildMemberLaunchPresentation({ + member: createMember(), + spawnLivenessSource: 'heartbeat', + runtimeAdvisory: undefined, + isTeamAlive: true, + isTeamProvisioning: false, + ...createConfirmedCodexSpawn(), + }); + + expect(presentation.launchVisualState).toBe('stale_runtime'); + expect(presentation.presenceLabel).toBe('stale runtime'); + expect(presentation.dotClass).toContain('bg-red-400'); + }); + + it('marks Codex native members without runtime snapshots as stale after launch settles', () => { + const presentation = buildMemberLaunchPresentation({ + member: createMember(), + spawnStatus: undefined, + spawnLaunchState: undefined, + spawnRuntimeAlive: undefined, + spawnBootstrapConfirmed: undefined, + spawnLivenessSource: undefined, + runtimeAdvisory: undefined, + isTeamAlive: true, + isTeamProvisioning: false, + }); + + expect(presentation.launchVisualState).toBe('stale_runtime'); + expect(presentation.dotClass).toContain('bg-red-400'); + }); + + it('hides Codex native activity until runtime evidence arrives', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: createMember(), + isTeamAlive: true, + }) + ).toBe(false); + }); + + it('does not let a global launch settling state keep stale Codex native status green', () => { + const presentation = buildMemberLaunchPresentation({ + member: createMember(), + spawnLivenessSource: 'heartbeat', + runtimeAdvisory: undefined, + isTeamAlive: true, + isTeamProvisioning: false, + isLaunchSettling: true, + ...createConfirmedCodexSpawn(), + }); + + expect(presentation.launchVisualState).toBe('stale_runtime'); + expect(presentation.dotClass).toContain('bg-red-400'); + }); + + it('does not require runtime evidence for non-Codex teammates', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: createMember({ + providerId: 'anthropic', + providerBackendId: undefined, + }), + isTeamAlive: true, + spawnStatus: 'online', + spawnLaunchState: 'confirmed_alive', + spawnRuntimeAlive: true, + }) + ).toBe(true); + }); +}); diff --git a/src/renderer/utils/memberHelpers.ts b/src/renderer/utils/memberHelpers.ts index 0ff6b71c..7dcb7e0b 100644 --- a/src/renderer/utils/memberHelpers.ts +++ b/src/renderer/utils/memberHelpers.ts @@ -935,10 +935,13 @@ function getLaunchVisualStateDotClass(visualState: MemberLaunchVisualState): str } function getCurrentRuntimeOfflineVisualState( + member: ResolvedTeamMember, runtimeEntry: TeamAgentRuntimeEntry | undefined, spawnStatus: MemberSpawnStatus | undefined, spawnLaunchState: MemberLaunchState | undefined, - spawnRuntimeAlive: boolean | undefined + spawnRuntimeAlive: boolean | undefined, + spawnBootstrapConfirmed: boolean | undefined, + isTeamProvisioning: boolean | undefined ): MemberLaunchVisualState { if (runtimeEntry?.livenessKind === 'registered_only') { return 'registered_only'; @@ -963,9 +966,109 @@ function getCurrentRuntimeOfflineVisualState( ) { return 'stale_runtime'; } + if ( + shouldTreatCodexNativeRuntimeAsOffline({ + member, + runtimeEntry, + spawnStatus, + spawnLaunchState, + spawnRuntimeAlive, + spawnBootstrapConfirmed, + isTeamProvisioning, + }) + ) { + return 'stale_runtime'; + } return null; } +function isCodexNativeProcessTeammate(member: ResolvedTeamMember): boolean { + if (isLeadMember(member)) { + return false; + } + return ( + member.providerId === 'codex' && + (member.providerBackendId == null || member.providerBackendId === 'codex-native') + ); +} + +function hasLiveRuntimeProcessEvidence(runtimeEntry: TeamAgentRuntimeEntry | undefined): boolean { + if (runtimeEntry?.alive !== true) { + return false; + } + return ( + runtimeEntry.livenessKind == null || + runtimeEntry.livenessKind === 'runtime_process' || + runtimeEntry.livenessKind === 'confirmed_bootstrap' + ); +} + +function hasSpawnRuntimeLiveClaim({ + spawnStatus, + spawnLaunchState, + spawnRuntimeAlive, + spawnBootstrapConfirmed, +}: { + spawnStatus?: MemberSpawnStatus; + spawnLaunchState?: MemberLaunchState; + spawnRuntimeAlive?: boolean; + spawnBootstrapConfirmed?: boolean; +}): boolean { + return ( + spawnStatus === 'online' || + spawnLaunchState === 'confirmed_alive' || + spawnRuntimeAlive === true || + spawnBootstrapConfirmed === true + ); +} + +function shouldTreatCodexNativeRuntimeAsOffline({ + member, + runtimeEntry, + spawnStatus, + spawnLaunchState, + spawnRuntimeAlive, + spawnBootstrapConfirmed, + isTeamProvisioning, +}: { + member: ResolvedTeamMember; + runtimeEntry?: TeamAgentRuntimeEntry; + spawnStatus?: MemberSpawnStatus; + spawnLaunchState?: MemberLaunchState; + spawnRuntimeAlive?: boolean; + spawnBootstrapConfirmed?: boolean; + isTeamProvisioning?: boolean; +}): boolean { + if (!isCodexNativeProcessTeammate(member)) { + return false; + } + if (hasLiveRuntimeProcessEvidence(runtimeEntry)) { + return false; + } + if ( + isTeamProvisioning === true && + runtimeEntry == null && + !hasSpawnRuntimeLiveClaim({ + spawnStatus, + spawnLaunchState, + spawnRuntimeAlive, + spawnBootstrapConfirmed, + }) + ) { + return false; + } + return ( + runtimeEntry != null || + hasSpawnRuntimeLiveClaim({ + spawnStatus, + spawnLaunchState, + spawnRuntimeAlive, + spawnBootstrapConfirmed, + }) || + spawnStatus == null + ); +} + export function shouldDisplayMemberCurrentTask({ member, isTeamAlive, @@ -1011,6 +1114,9 @@ export function shouldDisplayMemberCurrentTask({ if (spawnRuntimeAlive === false) { return false; } + if (isCodexNativeProcessTeammate(member) && !hasLiveRuntimeProcessEvidence(runtimeEntry)) { + return false; + } return true; } @@ -1205,10 +1311,13 @@ export function buildMemberLaunchPresentation({ nowMs?: number; }): MemberLaunchPresentation { const currentRuntimeOfflineVisualState = getCurrentRuntimeOfflineVisualState( + member, runtimeEntry, spawnStatus, spawnLaunchState, - spawnRuntimeAlive + spawnRuntimeAlive, + spawnBootstrapConfirmed, + isTeamProvisioning ); const hasConfirmedSpawnLaunch = spawnLaunchState === 'confirmed_alive' && spawnBootstrapConfirmed === true;