fix(team): keep codex member work visible during recovery
This commit is contained in:
parent
e5ace8c7cb
commit
f19ed93d4b
10 changed files with 535 additions and 61 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> = {}): 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<string, TeamAgentRuntimeEntry>
|
||||
): 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import type { TeamAgentRuntimeEntry, TeamAgentRuntimeSnapshot } from '@shared/types';
|
||||
|
||||
type RuntimeLivenessKind = NonNullable<TeamAgentRuntimeEntry['livenessKind']>;
|
||||
|
||||
const WORK_SYNC_INACTIVE_LIVENESS_KINDS = new Set<RuntimeLivenessKind>([
|
||||
'permission_blocked',
|
||||
'runtime_process_candidate',
|
||||
'shell_only',
|
||||
'registered_only',
|
||||
'stale_metadata',
|
||||
'not_found',
|
||||
]);
|
||||
|
||||
export function isRuntimeEntryActiveForWorkSync(
|
||||
entry: Pick<TeamAgentRuntimeEntry, 'alive' | 'livenessKind'> | 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<TeamAgentRuntimeSnapshot, 'members'> | null | undefined
|
||||
): boolean {
|
||||
return Object.values(snapshot?.members ?? {}).some(isRuntimeEntryActiveForWorkSync);
|
||||
}
|
||||
|
|
@ -8,3 +8,7 @@ export {
|
|||
buildMemberWorkSyncRuntimeTurnSettledEnvironment,
|
||||
createMemberWorkSyncFeature,
|
||||
} from './composition/createMemberWorkSyncFeature';
|
||||
export {
|
||||
hasWorkSyncActiveRuntime,
|
||||
isRuntimeEntryActiveForWorkSync,
|
||||
} from './composition/memberWorkSyncTeamActivity';
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
import {
|
||||
buildMemberWorkSyncRuntimeTurnSettledEnvironment,
|
||||
createMemberWorkSyncFeature,
|
||||
hasWorkSyncActiveRuntime,
|
||||
type MemberWorkSyncFeatureFacade,
|
||||
registerMemberWorkSyncIpc,
|
||||
removeMemberWorkSyncIpc,
|
||||
|
|
@ -1780,27 +1781,55 @@ async function initializeServices(): Promise<void> {
|
|||
logger: createLogger('Feature:RecentProjects'),
|
||||
});
|
||||
runtimeProviderManagementFeature = createRuntimeProviderManagementFeature();
|
||||
const memberWorkSyncLogger = createLogger('Feature:MemberWorkSync');
|
||||
const hasMemberWorkSyncRuntimeActivity = async (teamName: string): Promise<boolean> => {
|
||||
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<boolean> => {
|
||||
if (
|
||||
teamProvisioningService.isTeamAlive(teamName) ||
|
||||
teamProvisioningService.hasProvisioningRun(teamName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return hasMemberWorkSyncRuntimeActivity(teamName);
|
||||
};
|
||||
const canDispatchMemberWorkSyncNudges = async (teamName: string): Promise<boolean> => {
|
||||
if (teamProvisioningService.isTeamAlive(teamName)) {
|
||||
return true;
|
||||
}
|
||||
return hasMemberWorkSyncRuntimeActivity(teamName);
|
||||
};
|
||||
const listMemberWorkSyncLifecycleActiveTeamNames = async (): Promise<string[]> => {
|
||||
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<void> {
|
|||
});
|
||||
},
|
||||
},
|
||||
logger: createLogger('Feature:MemberWorkSync'),
|
||||
logger: memberWorkSyncLogger,
|
||||
});
|
||||
teamProvisioningService.setRuntimeTurnSettledHookSettingsProvider((input) =>
|
||||
memberWorkSyncFeature
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{currentTask ? (
|
||||
{visibleCurrentTask ? (
|
||||
<CurrentTaskIndicator
|
||||
task={currentTask}
|
||||
task={visibleCurrentTask}
|
||||
borderColor={colors.border}
|
||||
activityLabel="working on"
|
||||
activityTimer={currentTaskTimer}
|
||||
|
|
@ -1070,9 +1081,9 @@ export const MemberCard = memo(function MemberCard({
|
|||
onOpenTask={onOpenTask}
|
||||
/>
|
||||
) : null}
|
||||
{reviewTask ? (
|
||||
{visibleReviewTask ? (
|
||||
<CurrentTaskIndicator
|
||||
task={reviewTask}
|
||||
task={visibleReviewTask}
|
||||
borderColor={colors.border}
|
||||
activityLabel={reviewTaskTimer ? 'reviewing' : 'review requested'}
|
||||
activityTimer={reviewTaskTimer}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export const MemberHoverCard = memo(function MemberHoverCard({
|
|||
const showCopyDiagnostics =
|
||||
hasMemberLaunchDiagnosticsError(launchDiagnosticsPayload) &&
|
||||
hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload);
|
||||
const reviewTask: TeamTaskWithKanban | null = tasks
|
||||
const reviewTaskCandidate: TeamTaskWithKanban | null = tasks
|
||||
? (tasks.find(
|
||||
(task) =>
|
||||
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 (
|
||||
<HoverCard openDelay={300} closeDelay={200}>
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
172
src/renderer/utils/__tests__/memberHelpers.test.ts
Normal file
172
src/renderer/utils/__tests__/memberHelpers.test.ts
Normal file
|
|
@ -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> = {}): 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> = {}): 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue