1587 lines
51 KiB
TypeScript
1587 lines
51 KiB
TypeScript
import { isLeadMember } from '@shared/utils/leadDetection';
|
|
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
|
|
|
import {
|
|
getParticipantAvatarUrlByIndex,
|
|
LEAD_PARTICIPANT_AVATAR_URL,
|
|
PARTICIPANT_AVATAR_URLS,
|
|
} from './memberAvatarCatalog';
|
|
import { isHealthyOpenCodeAppMcpConnectivityAdvisory } from './openCodeAdvisoryHealth';
|
|
|
|
import type {
|
|
LeadActivityState,
|
|
MemberLaunchState,
|
|
MemberRuntimeAdvisory,
|
|
MemberSpawnLivenessSource,
|
|
MemberSpawnStatus,
|
|
MemberSpawnStatusEntry,
|
|
MemberStatus,
|
|
ResolvedTeamMember,
|
|
TeamAgentRuntimeEntry,
|
|
TeamProviderId,
|
|
TeamReviewState,
|
|
TeamTaskStatus,
|
|
} from '@shared/types';
|
|
|
|
/**
|
|
* UI display name for a team member.
|
|
* "team-lead" → "lead"; everything else passes through unchanged.
|
|
* Data layer (store, IPC, backend) must keep the original name untouched.
|
|
*/
|
|
export function displayMemberName(name: string): string {
|
|
return name === 'team-lead' ? 'lead' : name;
|
|
}
|
|
|
|
function hashStringToIndex(str: string): number {
|
|
let hash = 5381;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
}
|
|
return Math.abs(hash);
|
|
}
|
|
|
|
export function agentAvatarUrl(name: string, size = 64): string {
|
|
void size;
|
|
const normalized = name.trim().toLowerCase();
|
|
if (normalized === 'team-lead' || normalized === 'lead') {
|
|
return LEAD_PARTICIPANT_AVATAR_URL;
|
|
}
|
|
|
|
// Temporarily disabled external avatar API.
|
|
// return `https://robohash.org/${encodeURIComponent(name)}?size=${size}x${size}`;
|
|
return getParticipantAvatarUrlByIndex(
|
|
hashStringToIndex(normalized) % PARTICIPANT_AVATAR_URLS.length
|
|
);
|
|
}
|
|
|
|
export const STATUS_DOT_COLORS: Record<MemberStatus, string> = {
|
|
active: 'bg-emerald-400',
|
|
idle: 'bg-zinc-400',
|
|
terminated: 'bg-red-400',
|
|
unknown: 'bg-zinc-600',
|
|
};
|
|
|
|
export function getMemberDotClass(
|
|
member: ResolvedTeamMember,
|
|
isTeamAlive?: boolean,
|
|
isTeamProvisioning?: boolean,
|
|
leadActivity?: LeadActivityState
|
|
): string {
|
|
if (member.status === 'terminated') return STATUS_DOT_COLORS.terminated;
|
|
if (member.removedAt) return STATUS_DOT_COLORS.terminated;
|
|
// Lead activity check BEFORE provisioning fallback — when the lead process
|
|
// is running (CLI logs present), show green even during provisioning.
|
|
if (leadActivity && isLeadMember(member)) {
|
|
return leadActivity === 'active'
|
|
? `${STATUS_DOT_COLORS.active} animate-pulse`
|
|
: STATUS_DOT_COLORS.active;
|
|
}
|
|
if (isTeamProvisioning) return STATUS_DOT_COLORS.unknown;
|
|
if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated;
|
|
// When team is alive, all non-terminated members are online
|
|
if (isTeamAlive) {
|
|
if (member.currentTaskId) return `${STATUS_DOT_COLORS.active} animate-pulse`;
|
|
return STATUS_DOT_COLORS.active;
|
|
}
|
|
if (member.status === 'unknown') return STATUS_DOT_COLORS.unknown;
|
|
if (member.currentTaskId) return STATUS_DOT_COLORS.active;
|
|
return member.status === 'active' ? STATUS_DOT_COLORS.active : STATUS_DOT_COLORS.idle;
|
|
}
|
|
|
|
export function getPresenceLabel(
|
|
member: ResolvedTeamMember,
|
|
isTeamAlive?: boolean,
|
|
isTeamProvisioning?: boolean,
|
|
leadActivity?: LeadActivityState,
|
|
leadContextPercent?: number
|
|
): string {
|
|
if (member.status === 'terminated') return 'terminated';
|
|
// Lead activity check before provisioning fallback (mirrors getMemberDotClass order).
|
|
if (leadActivity && isLeadMember(member)) {
|
|
if (leadActivity === 'active') {
|
|
return leadContextPercent != null && leadContextPercent > 0
|
|
? `processing (${Math.round(leadContextPercent)}%)`
|
|
: 'processing';
|
|
}
|
|
return 'ready';
|
|
}
|
|
if (isTeamProvisioning) return 'connecting';
|
|
if (isTeamAlive === false) return 'offline';
|
|
if (member.status === 'unknown') return 'idle';
|
|
return member.currentTaskId ? 'working' : 'idle';
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Spawn-status-aware helpers for progressive member card appearance */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export const SPAWN_DOT_COLORS: Record<MemberSpawnStatus, string> = {
|
|
offline: 'bg-zinc-600',
|
|
waiting: 'bg-zinc-400 animate-pulse',
|
|
spawning: 'bg-amber-400',
|
|
online: 'bg-emerald-400 animate-pulse',
|
|
error: 'bg-red-400',
|
|
skipped: 'bg-zinc-500',
|
|
};
|
|
|
|
export const SPAWN_PRESENCE_LABELS: Record<MemberSpawnStatus, string> = {
|
|
offline: 'offline',
|
|
waiting: 'starting',
|
|
spawning: 'starting',
|
|
online: 'ready',
|
|
error: 'spawn failed',
|
|
skipped: 'skipped',
|
|
};
|
|
|
|
const OPENCODE_RUNTIME_CANDIDATE_RELAUNCH_GRACE_MS = 5 * 60 * 1000;
|
|
export const MEMBER_STARTING_STALE_AFTER_MS = 2 * 60 * 1000;
|
|
const OPENCODE_BRIDGE_OUTCOME_UNKNOWN_AFTER_TIMEOUT_MESSAGE =
|
|
'OpenCode bridge outcome unknown after timeout, retrying/observing.';
|
|
|
|
function isLaunchStillStarting(
|
|
spawnStatus: MemberSpawnStatus | undefined,
|
|
spawnLaunchState: MemberLaunchState | undefined,
|
|
runtimeAlive: boolean | undefined,
|
|
keepRuntimePendingInStarting = false
|
|
): boolean {
|
|
if (spawnLaunchState === 'failed_to_start') {
|
|
return false;
|
|
}
|
|
if (spawnLaunchState === 'skipped_for_launch') {
|
|
return false;
|
|
}
|
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
|
return false;
|
|
}
|
|
if (spawnLaunchState === 'runtime_pending_bootstrap') {
|
|
if (runtimeAlive !== true) {
|
|
return true;
|
|
}
|
|
return keepRuntimePendingInStarting;
|
|
}
|
|
return spawnLaunchState === 'starting' || spawnStatus === 'waiting' || spawnStatus === 'spawning';
|
|
}
|
|
|
|
/**
|
|
* Returns dot class for a member during provisioning, respecting spawn status.
|
|
* Falls back to the existing `getMemberDotClass` when no spawn status is available.
|
|
*/
|
|
export function getSpawnAwareDotClass(
|
|
member: ResolvedTeamMember,
|
|
spawnStatus: MemberSpawnStatus | undefined,
|
|
spawnLaunchState: MemberLaunchState | undefined,
|
|
runtimeAlive: boolean | undefined,
|
|
isLaunchSettling = false,
|
|
isTeamAlive?: boolean,
|
|
isTeamProvisioning?: boolean,
|
|
leadActivity?: LeadActivityState
|
|
): string {
|
|
const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling;
|
|
if (isTeamAlive === false && !isTeamProvisioning) {
|
|
return STATUS_DOT_COLORS.terminated;
|
|
}
|
|
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
|
return SPAWN_DOT_COLORS.error;
|
|
}
|
|
if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') {
|
|
return SPAWN_DOT_COLORS.skipped;
|
|
}
|
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
|
return 'bg-amber-400 animate-pulse';
|
|
}
|
|
if (
|
|
isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals)
|
|
) {
|
|
return spawnStatus === 'spawning' ? SPAWN_DOT_COLORS.spawning : SPAWN_DOT_COLORS.waiting;
|
|
}
|
|
if (spawnLaunchState === 'runtime_pending_bootstrap' && spawnStatus === 'online') {
|
|
return SPAWN_DOT_COLORS.online;
|
|
}
|
|
if (spawnStatus === 'waiting') {
|
|
return SPAWN_DOT_COLORS.waiting;
|
|
}
|
|
if (spawnStatus === 'online') {
|
|
return SPAWN_DOT_COLORS.online;
|
|
}
|
|
if (spawnStatus === 'offline' && isTeamProvisioning) {
|
|
return SPAWN_DOT_COLORS.offline;
|
|
}
|
|
if (spawnStatus === 'spawning' && isTeamProvisioning) {
|
|
return SPAWN_DOT_COLORS.spawning;
|
|
}
|
|
return getMemberDotClass(member, isTeamAlive, isTeamProvisioning, leadActivity);
|
|
}
|
|
|
|
/**
|
|
* Returns presence label for a member during provisioning, respecting spawn status.
|
|
*/
|
|
export function getSpawnAwarePresenceLabel(
|
|
member: ResolvedTeamMember,
|
|
spawnStatus: MemberSpawnStatus | undefined,
|
|
spawnLaunchState: MemberLaunchState | undefined,
|
|
livenessSource: MemberSpawnLivenessSource | undefined,
|
|
runtimeAlive: boolean | undefined,
|
|
isLaunchSettling = false,
|
|
isTeamAlive?: boolean,
|
|
isTeamProvisioning?: boolean,
|
|
leadActivity?: LeadActivityState
|
|
): string {
|
|
const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling;
|
|
if (isTeamAlive === false && !isTeamProvisioning) {
|
|
return 'offline';
|
|
}
|
|
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
|
return SPAWN_PRESENCE_LABELS.error;
|
|
}
|
|
if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') {
|
|
return SPAWN_PRESENCE_LABELS.skipped;
|
|
}
|
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
|
return 'connecting';
|
|
}
|
|
if (
|
|
isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals)
|
|
) {
|
|
return 'starting';
|
|
}
|
|
if (spawnStatus === 'online' && keepLaunchSettlingVisuals) {
|
|
return SPAWN_PRESENCE_LABELS.online;
|
|
}
|
|
if (spawnStatus === 'online' && livenessSource === 'process') {
|
|
return 'online';
|
|
}
|
|
if (spawnStatus && isTeamProvisioning) {
|
|
return SPAWN_PRESENCE_LABELS[spawnStatus];
|
|
}
|
|
return getPresenceLabel(member, isTeamAlive, isTeamProvisioning, leadActivity);
|
|
}
|
|
|
|
/**
|
|
* Card container CSS classes based on spawn status (opacity + animation).
|
|
* Used by MemberCard wrapper for fade-in transitions.
|
|
*/
|
|
export function getSpawnCardClass(
|
|
spawnStatus: MemberSpawnStatus | undefined,
|
|
spawnLaunchState?: MemberLaunchState,
|
|
runtimeAlive?: boolean,
|
|
isLaunchSettling = false,
|
|
isTeamAlive?: boolean,
|
|
isTeamProvisioning?: boolean
|
|
): string {
|
|
const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling;
|
|
if (isTeamAlive === false && !isTeamProvisioning) {
|
|
return '';
|
|
}
|
|
if (
|
|
isLaunchStillStarting(spawnStatus, spawnLaunchState, runtimeAlive, keepLaunchSettlingVisuals)
|
|
) {
|
|
return 'member-waiting-shimmer';
|
|
}
|
|
if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') {
|
|
return 'opacity-70';
|
|
}
|
|
if (spawnLaunchState === 'runtime_pending_permission') {
|
|
return 'member-waiting-shimmer';
|
|
}
|
|
switch (spawnStatus) {
|
|
case 'offline':
|
|
return spawnLaunchState === 'starting' ? 'member-waiting-shimmer opacity-75' : 'opacity-40';
|
|
case 'waiting':
|
|
return 'member-waiting-shimmer';
|
|
case 'spawning':
|
|
return 'member-waiting-shimmer';
|
|
case 'online':
|
|
return 'animate-[member-fade-in_0.4s_ease-out]';
|
|
case 'error':
|
|
return 'opacity-80';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function formatRetryCountdown(ms: number): string {
|
|
const totalSeconds = Math.max(1, Math.ceil(ms / 1000));
|
|
if (totalSeconds < 60) {
|
|
return `${totalSeconds}s`;
|
|
}
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
if (minutes >= 60) {
|
|
const hours = Math.floor(minutes / 60);
|
|
const remainingMinutes = minutes % 60;
|
|
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
}
|
|
const seconds = totalSeconds % 60;
|
|
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
}
|
|
|
|
function getRuntimeAdvisoryRetryRemainingMs(
|
|
advisory: MemberRuntimeAdvisory,
|
|
nowMs: number
|
|
): number | null {
|
|
const retryUntilMs = advisory.retryUntil ? Date.parse(advisory.retryUntil) : Number.NaN;
|
|
if (!Number.isFinite(retryUntilMs)) {
|
|
return null;
|
|
}
|
|
const remainingMs = retryUntilMs - nowMs;
|
|
return remainingMs > 0 ? remainingMs : null;
|
|
}
|
|
|
|
function isRetryTimedApiAdvisory(
|
|
advisory: MemberRuntimeAdvisory,
|
|
providerId: TeamProviderId | undefined
|
|
): boolean {
|
|
return (
|
|
advisory.kind === 'api_error' &&
|
|
providerId === 'opencode' &&
|
|
(advisory.reasonCode === 'quota_exhausted' || advisory.reasonCode === 'rate_limited')
|
|
);
|
|
}
|
|
|
|
function formatRetryUntilUtc(value: string | undefined): string | null {
|
|
const retryUntilMs = value ? Date.parse(value) : Number.NaN;
|
|
if (!Number.isFinite(retryUntilMs)) {
|
|
return null;
|
|
}
|
|
const date = new Date(retryUntilMs);
|
|
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
return `${hours}:${minutes} UTC`;
|
|
}
|
|
|
|
function appendRuntimeAdvisoryRetryHint(
|
|
base: string,
|
|
advisory: MemberRuntimeAdvisory,
|
|
providerId: TeamProviderId | undefined
|
|
): string {
|
|
if (!isRetryTimedApiAdvisory(advisory, providerId)) {
|
|
return base;
|
|
}
|
|
const retryAt = formatRetryUntilUtc(advisory.retryUntil);
|
|
if (!retryAt) {
|
|
return base;
|
|
}
|
|
return `${base} Waiting for OpenCode retry or quota reset around ${retryAt}.`;
|
|
}
|
|
|
|
function getRuntimeAdvisoryProviderLabel(providerId: TeamProviderId | undefined): string | null {
|
|
switch (providerId) {
|
|
case 'anthropic':
|
|
return 'Anthropic';
|
|
case 'codex':
|
|
return 'Codex';
|
|
case 'gemini':
|
|
return 'Gemini';
|
|
case 'opencode':
|
|
return 'OpenCode';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function appendRuntimeAdvisoryRawMessage(
|
|
base: string,
|
|
message: string | undefined,
|
|
providerId?: TeamProviderId
|
|
): string {
|
|
const trimmed = formatRuntimeAdvisoryDisplayMessage(message, providerId);
|
|
if (trimmed === base) {
|
|
return base;
|
|
}
|
|
return trimmed ? `${base}\n\n${trimmed}` : base;
|
|
}
|
|
|
|
const OPENCODE_SESSION_REFRESH_REASON_PATTERN =
|
|
/\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/i;
|
|
const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN =
|
|
/(?:^|[_\s:;.\/()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;.\/(),-])/i;
|
|
const OPENCODE_SESSION_REFRESH_ANY_REASON_PATTERN =
|
|
/\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/gi;
|
|
const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN =
|
|
/\b(?:not_observed|pending|prompt_not_indexed|responded_tool_call|responded_visible_message|responded_non_visible_tool|responded_plain_text|permission_blocked|tool_error|empty_assistant_turn|prompt_delivered_no_assistant_message|session_stale|session_error|reconcile_failed)\b/g;
|
|
|
|
function isRecoverableOpenCodeSessionRefreshMessage(message: string | undefined): boolean {
|
|
const normalized = message?.trim().toLowerCase() ?? '';
|
|
const refreshText = stripOpenCodeGenericApiErrorPrefix(normalized);
|
|
const refreshMarkerText = refreshText.replace(/[.:\s-]+$/, '');
|
|
if (
|
|
refreshMarkerText === 'session_stale' ||
|
|
refreshMarkerText === 'opencode session changed; refreshing the session before retry' ||
|
|
refreshMarkerText === 'opencode session refresh scheduled after resolved behavior changed' ||
|
|
refreshMarkerText === 'opencode_prompt_delivery_session_refresh_scheduled' ||
|
|
refreshMarkerText === 'opencode_session_refresh_scheduled_after_resolved_behavior_changed'
|
|
) {
|
|
return true;
|
|
}
|
|
if (!OPENCODE_SESSION_REFRESH_REASON_PATTERN.test(refreshText)) {
|
|
return false;
|
|
}
|
|
const markerText = refreshText;
|
|
if (hasOpenCodeSessionRefreshFailureConflict(markerText)) {
|
|
return false;
|
|
}
|
|
const rawRemainder = markerText.replace(OPENCODE_SESSION_REFRESH_ANY_REASON_PATTERN, '');
|
|
const remainder = rawRemainder.replace(/[().,;:\s-]+/g, '');
|
|
if (remainder.length === 0) {
|
|
return true;
|
|
}
|
|
const staleLogProjectionContext =
|
|
normalized.includes('session is stale') ||
|
|
normalized.includes('stored session is stale') ||
|
|
normalized.includes('session reconcile skipped');
|
|
return staleLogProjectionContext && isBenignOpenCodeSessionRefreshRemainder(rawRemainder);
|
|
}
|
|
|
|
function stripOpenCodeGenericApiErrorPrefix(message: string): string {
|
|
return message.replace(/^opencode api error(?:[.:\s-]+|$)/i, '');
|
|
}
|
|
|
|
function isBenignOpenCodeSessionRefreshRemainder(rawRemainder: string): boolean {
|
|
if (OPENCODE_SESSION_REFRESH_FAILURE_PATTERN.test(rawRemainder)) {
|
|
return false;
|
|
}
|
|
const normalized = rawRemainder.replace(/[().,;:\s-]+/g, ' ').trim();
|
|
return (
|
|
normalized === 'opencode session is stale' ||
|
|
normalized ===
|
|
'opencode session is stale reading historical messages for log projection only' ||
|
|
normalized === 'opencode session reconcile skipped because the stored session is stale' ||
|
|
normalized === 'stored session is stale'
|
|
);
|
|
}
|
|
|
|
function hasOpenCodeSessionRefreshFailureConflict(value: string): boolean {
|
|
return OPENCODE_SESSION_REFRESH_FAILURE_PATTERN.test(
|
|
value.replace(OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN, 'state')
|
|
);
|
|
}
|
|
|
|
function canTreatAdvisoryAsOpenCodeSessionRefresh(
|
|
advisory: MemberRuntimeAdvisory | undefined
|
|
): boolean {
|
|
return (
|
|
Boolean(advisory) &&
|
|
(advisory?.reasonCode == null ||
|
|
advisory.reasonCode === 'backend_error' ||
|
|
advisory.reasonCode === 'unknown') &&
|
|
isRecoverableOpenCodeSessionRefreshMessage(advisory?.message)
|
|
);
|
|
}
|
|
|
|
function isOpenCodeRuntimeDeliveryAdvisoryMessage(message: string | undefined): boolean {
|
|
const displayMessage = formatRuntimeAdvisoryDisplayMessage(message, 'opencode');
|
|
return (
|
|
displayMessage.startsWith('OpenCode runtime delivery') ||
|
|
displayMessage.startsWith('OpenCode returned an empty assistant turn') ||
|
|
displayMessage.startsWith('OpenCode accepted the prompt') ||
|
|
displayMessage.startsWith('OpenCode bridge outcome unknown after timeout') ||
|
|
displayMessage.startsWith('OpenCode responded, but did not create') ||
|
|
displayMessage.startsWith('OpenCode created a reply without') ||
|
|
displayMessage.startsWith('OpenCode used tools, but did not create')
|
|
);
|
|
}
|
|
|
|
function formatRuntimeAdvisoryDisplayMessage(
|
|
message: string | undefined,
|
|
providerId?: TeamProviderId
|
|
): string {
|
|
const trimmed = message?.trim();
|
|
if (!trimmed) {
|
|
return '';
|
|
}
|
|
if (trimmed === 'empty_assistant_turn') {
|
|
return 'OpenCode returned an empty assistant turn.';
|
|
}
|
|
if (trimmed === 'prompt_delivered_no_assistant_message') {
|
|
return 'OpenCode accepted the prompt, but no assistant turn was recorded.';
|
|
}
|
|
if (trimmed === 'opencode_prompt_acceptance_unknown_after_bridge_timeout') {
|
|
return OPENCODE_BRIDGE_OUTCOME_UNKNOWN_AFTER_TIMEOUT_MESSAGE;
|
|
}
|
|
if (providerId === 'opencode' && isRecoverableOpenCodeSessionRefreshMessage(trimmed)) {
|
|
return 'OpenCode session changed; refreshing the session before retry.';
|
|
}
|
|
if (
|
|
trimmed === 'visible_reply_still_required' ||
|
|
trimmed === 'visible_reply_ack_only_still_requires_answer' ||
|
|
trimmed === 'plain_text_ack_only_still_requires_answer'
|
|
) {
|
|
return 'OpenCode responded, but did not create a visible message_send reply.';
|
|
}
|
|
if (
|
|
trimmed === 'visible_reply_destination_not_found_yet' ||
|
|
trimmed === 'visible_reply_missing_relayOfMessageId'
|
|
) {
|
|
return 'OpenCode created a reply without the required relayOfMessageId correlation.';
|
|
}
|
|
if (trimmed === 'visible_reply_missing_task_refs') {
|
|
return 'OpenCode created a reply without the required taskRefs metadata.';
|
|
}
|
|
if (trimmed === 'visible_reply_missing_task_refs_after_merge') {
|
|
return 'OpenCode created a reply without the required taskRefs metadata.';
|
|
}
|
|
if (trimmed === 'visible_reply_task_refs_merge_failed') {
|
|
return 'OpenCode created a reply without the required taskRefs metadata, and the app could not attach it automatically.';
|
|
}
|
|
if (trimmed === 'non_visible_tool_without_task_progress') {
|
|
return 'OpenCode used tools, but did not create a visible reply or task progress proof.';
|
|
}
|
|
if (
|
|
trimmed.startsWith(
|
|
'OpenCode bootstrap MCP did not complete required tools before assistant response:'
|
|
)
|
|
) {
|
|
return 'OpenCode runtime delivery did not complete.';
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
function formatRuntimeAdvisoryBaseLabel(
|
|
advisory: MemberRuntimeAdvisory,
|
|
providerId: TeamProviderId | undefined
|
|
): string {
|
|
const providerLabel = getRuntimeAdvisoryProviderLabel(providerId);
|
|
if (advisory.kind === 'api_error') {
|
|
if (providerId === 'opencode' && canTreatAdvisoryAsOpenCodeSessionRefresh(advisory)) {
|
|
return 'OpenCode session refresh';
|
|
}
|
|
switch (advisory.reasonCode) {
|
|
case 'quota_exhausted':
|
|
return providerLabel ? `${providerLabel} quota error` : 'Quota error';
|
|
case 'rate_limited':
|
|
return providerLabel ? `${providerLabel} rate limit` : 'Rate limit';
|
|
case 'auth_error':
|
|
return providerLabel ? `${providerLabel} auth error` : 'Auth error';
|
|
case 'codex_native_timeout':
|
|
return 'Codex native timeout';
|
|
case 'network_error':
|
|
return 'Network error';
|
|
case 'filesystem_error':
|
|
return 'Disk space error';
|
|
case 'provider_overloaded':
|
|
return providerLabel ? `${providerLabel} overload` : 'Provider overload';
|
|
case 'protocol_proof_missing':
|
|
return providerId === 'opencode' ? 'OpenCode proof missing' : 'Protocol proof missing';
|
|
case 'backend_error':
|
|
case 'unknown':
|
|
if (
|
|
providerId === 'opencode' &&
|
|
isOpenCodeRuntimeDeliveryAdvisoryMessage(advisory.message)
|
|
) {
|
|
return 'OpenCode delivery error';
|
|
}
|
|
return providerLabel ? `${providerLabel} API error` : 'API error';
|
|
default:
|
|
return 'API error';
|
|
}
|
|
}
|
|
|
|
switch (advisory.reasonCode) {
|
|
case 'quota_exhausted':
|
|
return providerLabel ? `${providerLabel} quota retry` : 'Quota retry';
|
|
case 'rate_limited':
|
|
return providerLabel ? `${providerLabel} rate limit` : 'Rate limit retry';
|
|
case 'auth_error':
|
|
return providerLabel ? `${providerLabel} auth retry` : 'Auth retry';
|
|
case 'codex_native_timeout':
|
|
return 'Codex native retry';
|
|
case 'network_error':
|
|
return 'Network retry';
|
|
case 'filesystem_error':
|
|
return 'Disk space retry';
|
|
case 'provider_overloaded':
|
|
return providerLabel ? `${providerLabel} overload retry` : 'Provider overload retry';
|
|
case 'protocol_proof_missing':
|
|
return providerId === 'opencode' ? 'OpenCode proof missing' : 'Protocol proof missing';
|
|
case 'backend_error':
|
|
case 'unknown':
|
|
return 'Provider retry';
|
|
default:
|
|
return 'retrying now';
|
|
}
|
|
}
|
|
|
|
function formatRuntimeAdvisoryTitle(
|
|
advisory: MemberRuntimeAdvisory,
|
|
providerId: TeamProviderId | undefined
|
|
): string {
|
|
const providerLabel = getRuntimeAdvisoryProviderLabel(providerId);
|
|
if (advisory.kind === 'api_error') {
|
|
if (providerId === 'opencode' && canTreatAdvisoryAsOpenCodeSessionRefresh(advisory)) {
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'OpenCode session changed; refreshing the session before retry.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
}
|
|
switch (advisory.reasonCode) {
|
|
case 'quota_exhausted':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
appendRuntimeAdvisoryRetryHint(
|
|
`${providerLabel ?? 'Provider'} quota exhausted.`,
|
|
advisory,
|
|
providerId
|
|
),
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'rate_limited':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
appendRuntimeAdvisoryRetryHint(
|
|
`${providerLabel ?? 'Provider'} rate limited the request.`,
|
|
advisory,
|
|
providerId
|
|
),
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'auth_error':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
`${providerLabel ?? 'Provider'} authentication error.`,
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'codex_native_timeout':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Codex native mailbox turn timed out. The runtime stopped this turn after its watchdog limit; it was not an automatic SDK retry.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'network_error':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Network or connectivity error.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'filesystem_error':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Local disk is full or unavailable.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'provider_overloaded':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Provider is temporarily overloaded.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'protocol_proof_missing':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
providerId === 'opencode'
|
|
? 'OpenCode delivery completed without required visible/progress proof.'
|
|
: 'Runtime delivery completed without required protocol proof.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'backend_error':
|
|
case 'unknown':
|
|
if (
|
|
providerId === 'opencode' &&
|
|
isOpenCodeRuntimeDeliveryAdvisoryMessage(advisory.message)
|
|
) {
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'OpenCode runtime delivery error.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
}
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
`${providerLabel ?? 'Provider'} API error.`,
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
default:
|
|
return advisory.message?.trim() || 'Provider API error.';
|
|
}
|
|
}
|
|
|
|
switch (advisory.reasonCode) {
|
|
case 'quota_exhausted':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
`${providerLabel ?? 'Provider'} quota exhausted. SDK is retrying automatically.`,
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'rate_limited':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
`${providerLabel ?? 'Provider'} rate limited the request. SDK is retrying automatically.`,
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'auth_error':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
`${providerLabel ?? 'Provider'} authentication issue. SDK is retrying automatically.`,
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'codex_native_timeout':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Codex native mailbox turn timed out. A retry window was reported by the runtime.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'network_error':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Network or connectivity issue. SDK is retrying automatically.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'filesystem_error':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Local disk is full or unavailable. SDK is retrying automatically.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'provider_overloaded':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'Provider is temporarily overloaded. SDK is retrying automatically.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'protocol_proof_missing':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
providerId === 'opencode'
|
|
? 'OpenCode delivery is waiting for required visible/progress proof.'
|
|
: 'Runtime delivery is waiting for required protocol proof.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
case 'backend_error':
|
|
case 'unknown':
|
|
return appendRuntimeAdvisoryRawMessage(
|
|
'The SDK is retrying this request after a provider or backend error.',
|
|
advisory.message,
|
|
providerId
|
|
);
|
|
default:
|
|
return (
|
|
advisory.message?.trim() ||
|
|
'The SDK is retrying this request after a provider or backend error.'
|
|
);
|
|
}
|
|
}
|
|
|
|
export function getMemberRuntimeAdvisoryLabel(
|
|
advisory: MemberRuntimeAdvisory | undefined,
|
|
providerId?: TeamProviderId,
|
|
nowMs = Date.now()
|
|
): string | null {
|
|
if (!advisory) {
|
|
return null;
|
|
}
|
|
const baseLabel = formatRuntimeAdvisoryBaseLabel(advisory, providerId);
|
|
const remainingMs = getRuntimeAdvisoryRetryRemainingMs(advisory, nowMs);
|
|
if (advisory.kind === 'api_error') {
|
|
if (remainingMs && isRetryTimedApiAdvisory(advisory, providerId)) {
|
|
return `${baseLabel} · retry ${formatRetryCountdown(remainingMs)}`;
|
|
}
|
|
return baseLabel;
|
|
}
|
|
if (advisory.kind !== 'sdk_retrying') {
|
|
return null;
|
|
}
|
|
if (!remainingMs) {
|
|
return baseLabel;
|
|
}
|
|
return `${baseLabel} · ${formatRetryCountdown(remainingMs)}`;
|
|
}
|
|
|
|
export function getMemberRuntimeAdvisoryTitle(
|
|
advisory: MemberRuntimeAdvisory | undefined,
|
|
providerId?: TeamProviderId
|
|
): string | undefined {
|
|
if (!advisory || (advisory.kind !== 'sdk_retrying' && advisory.kind !== 'api_error')) {
|
|
return undefined;
|
|
}
|
|
return formatRuntimeAdvisoryTitle(advisory, providerId);
|
|
}
|
|
|
|
export function getMemberRuntimeAdvisoryTone(
|
|
advisory: MemberRuntimeAdvisory | undefined,
|
|
providerId?: TeamProviderId
|
|
): 'error' | 'warning' | null {
|
|
if (!advisory) {
|
|
return null;
|
|
}
|
|
if (providerId === 'opencode' && canTreatAdvisoryAsOpenCodeSessionRefresh(advisory)) {
|
|
return 'warning';
|
|
}
|
|
if (advisory.reasonCode === 'protocol_proof_missing') {
|
|
return 'warning';
|
|
}
|
|
return advisory.kind === 'api_error' ? 'error' : 'warning';
|
|
}
|
|
|
|
export function getLaunchAwarePresenceLabel(
|
|
member: ResolvedTeamMember,
|
|
spawnStatus: MemberSpawnStatus | undefined,
|
|
spawnLaunchState: MemberLaunchState | undefined,
|
|
livenessSource: MemberSpawnLivenessSource | undefined,
|
|
runtimeAlive: boolean | undefined,
|
|
runtimeAdvisory: MemberRuntimeAdvisory | undefined,
|
|
isLaunchSettling = false,
|
|
isTeamAlive?: boolean,
|
|
isTeamProvisioning?: boolean,
|
|
leadActivity?: LeadActivityState
|
|
): string {
|
|
const basePresenceLabel = getSpawnAwarePresenceLabel(
|
|
member,
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
livenessSource,
|
|
runtimeAlive,
|
|
isLaunchSettling,
|
|
isTeamAlive,
|
|
isTeamProvisioning,
|
|
leadActivity
|
|
);
|
|
if (
|
|
basePresenceLabel === 'starting' ||
|
|
basePresenceLabel === 'connecting' ||
|
|
basePresenceLabel === 'spawn failed' ||
|
|
basePresenceLabel === 'skipped' ||
|
|
basePresenceLabel === 'offline' ||
|
|
basePresenceLabel === 'terminated'
|
|
) {
|
|
return basePresenceLabel;
|
|
}
|
|
const advisoryLabel = getMemberRuntimeAdvisoryLabel(runtimeAdvisory, member.providerId);
|
|
return advisoryLabel ?? basePresenceLabel;
|
|
}
|
|
|
|
export type MemberLaunchVisualState =
|
|
| 'queued'
|
|
| 'waiting'
|
|
| 'spawning'
|
|
| 'starting_stale'
|
|
| 'permission_pending'
|
|
| 'bootstrap_stalled'
|
|
| 'runtime_pending'
|
|
| 'shell_only'
|
|
| 'runtime_candidate'
|
|
| 'registered_only'
|
|
| 'stale_runtime'
|
|
| 'settling'
|
|
| 'error'
|
|
| 'skipped'
|
|
| null;
|
|
|
|
export interface MemberLaunchPresentation {
|
|
presenceLabel: string;
|
|
dotClass: string;
|
|
cardClass: string;
|
|
runtimeAdvisoryLabel: string | null;
|
|
runtimeAdvisoryTitle?: string;
|
|
runtimeAdvisoryTone: 'error' | 'warning' | null;
|
|
launchVisualState: MemberLaunchVisualState;
|
|
launchStatusLabel: string | null;
|
|
spawnBadgeLabel: string | null;
|
|
}
|
|
|
|
export function getMemberLaunchStatusLabel(visualState: MemberLaunchVisualState): string | null {
|
|
switch (visualState) {
|
|
case 'queued':
|
|
return 'queued';
|
|
case 'waiting':
|
|
return 'waiting to start';
|
|
case 'spawning':
|
|
return 'starting';
|
|
case 'starting_stale':
|
|
return 'starting stale';
|
|
case 'permission_pending':
|
|
return 'awaiting permission';
|
|
case 'bootstrap_stalled':
|
|
return 'bootstrap stalled';
|
|
case 'runtime_pending':
|
|
return 'waiting for bootstrap';
|
|
case 'shell_only':
|
|
return 'shell only';
|
|
case 'runtime_candidate':
|
|
return 'bootstrap unconfirmed';
|
|
case 'registered_only':
|
|
return 'registered';
|
|
case 'stale_runtime':
|
|
return 'stale runtime';
|
|
case 'settling':
|
|
return 'joining team';
|
|
case 'error':
|
|
return 'failed';
|
|
case 'skipped':
|
|
return 'skipped';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getLaunchVisualStateDotClass(visualState: MemberLaunchVisualState): string | null {
|
|
switch (visualState) {
|
|
case 'queued':
|
|
return SPAWN_DOT_COLORS.waiting;
|
|
case 'permission_pending':
|
|
case 'bootstrap_stalled':
|
|
case 'runtime_pending':
|
|
case 'runtime_candidate':
|
|
return 'bg-amber-400 animate-pulse';
|
|
case 'starting_stale':
|
|
return 'bg-amber-400';
|
|
case 'registered_only':
|
|
return STATUS_DOT_COLORS.terminated;
|
|
case 'shell_only':
|
|
return 'bg-amber-400';
|
|
case 'stale_runtime':
|
|
return STATUS_DOT_COLORS.terminated;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getCurrentRuntimeOfflineVisualState(
|
|
member: ResolvedTeamMember,
|
|
runtimeEntry: TeamAgentRuntimeEntry | undefined,
|
|
spawnStatus: MemberSpawnStatus | undefined,
|
|
spawnLaunchState: MemberLaunchState | undefined,
|
|
spawnRuntimeAlive: boolean | undefined,
|
|
spawnBootstrapConfirmed: boolean | undefined,
|
|
isTeamProvisioning: boolean | undefined
|
|
): MemberLaunchVisualState {
|
|
if (runtimeEntry?.livenessKind === 'registered_only') {
|
|
return 'registered_only';
|
|
}
|
|
if (
|
|
runtimeEntry?.livenessKind === 'stale_metadata' ||
|
|
runtimeEntry?.livenessKind === 'not_found'
|
|
) {
|
|
return 'stale_runtime';
|
|
}
|
|
if (
|
|
runtimeEntry?.alive === false &&
|
|
(runtimeEntry.livenessKind == null ||
|
|
runtimeEntry.livenessKind === 'runtime_process' ||
|
|
runtimeEntry.livenessKind === 'confirmed_bootstrap')
|
|
) {
|
|
return 'stale_runtime';
|
|
}
|
|
if (
|
|
spawnRuntimeAlive === false &&
|
|
(spawnStatus === 'online' || spawnLaunchState === 'confirmed_alive')
|
|
) {
|
|
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 {
|
|
return runtimeEntry?.alive === true && runtimeEntry.livenessKind === 'runtime_process';
|
|
}
|
|
|
|
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,
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
spawnRuntimeAlive,
|
|
runtimeEntry,
|
|
}: {
|
|
member: ResolvedTeamMember;
|
|
isTeamAlive?: boolean;
|
|
spawnStatus?: MemberSpawnStatus;
|
|
spawnLaunchState?: MemberLaunchState;
|
|
spawnRuntimeAlive?: boolean;
|
|
runtimeEntry?: TeamAgentRuntimeEntry;
|
|
}): boolean {
|
|
if (member.removedAt || member.status === 'terminated') {
|
|
return false;
|
|
}
|
|
if (isTeamAlive === false) {
|
|
return false;
|
|
}
|
|
if (spawnStatus === 'offline' || spawnStatus === 'error' || spawnStatus === 'skipped') {
|
|
return false;
|
|
}
|
|
if (
|
|
spawnLaunchState === 'failed_to_start' ||
|
|
spawnLaunchState === 'skipped_for_launch' ||
|
|
spawnLaunchState === 'runtime_pending_permission'
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
runtimeEntry?.livenessKind === 'shell_only' ||
|
|
runtimeEntry?.livenessKind === 'registered_only' ||
|
|
runtimeEntry?.livenessKind === 'stale_metadata' ||
|
|
runtimeEntry?.livenessKind === 'not_found'
|
|
) {
|
|
return false;
|
|
}
|
|
if (runtimeEntry?.alive === false) {
|
|
return false;
|
|
}
|
|
if (spawnRuntimeAlive === false) {
|
|
return false;
|
|
}
|
|
if (isCodexNativeProcessTeammate(member) && !hasLiveRuntimeProcessEvidence(runtimeEntry)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isQueuedOpenCodeLaunch(
|
|
member: ResolvedTeamMember,
|
|
spawnStatus: MemberSpawnStatus | undefined,
|
|
spawnLaunchState: MemberLaunchState | undefined,
|
|
runtimeEntry: TeamAgentRuntimeEntry | undefined,
|
|
isLaunchSettling: boolean,
|
|
isTeamProvisioning: boolean | undefined
|
|
): boolean {
|
|
if (member.providerId !== 'opencode') {
|
|
return false;
|
|
}
|
|
if (isTeamProvisioning !== true && !isLaunchSettling) {
|
|
return false;
|
|
}
|
|
if (spawnStatus !== 'waiting' || spawnLaunchState !== 'starting') {
|
|
return false;
|
|
}
|
|
|
|
// Only label lanes as queued before runtime evidence appears. Once the
|
|
// backend has any liveness signal, show the exact runtime state instead.
|
|
return runtimeEntry?.livenessKind == null;
|
|
}
|
|
|
|
function hasElapsedSinceIso(
|
|
value: string | undefined,
|
|
thresholdMs: number,
|
|
nowMs: number
|
|
): boolean {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
const parsed = Date.parse(value);
|
|
return Number.isFinite(parsed) && nowMs - parsed >= thresholdMs;
|
|
}
|
|
|
|
export function isMemberStartingStale({
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
spawnFirstSpawnAcceptedAt,
|
|
spawnUpdatedAt,
|
|
nowMs = Date.now(),
|
|
}: {
|
|
spawnStatus?: MemberSpawnStatus;
|
|
spawnLaunchState?: MemberLaunchState;
|
|
spawnFirstSpawnAcceptedAt?: string;
|
|
spawnUpdatedAt?: string;
|
|
nowMs?: number;
|
|
}): boolean {
|
|
if (
|
|
spawnLaunchState === 'failed_to_start' ||
|
|
spawnLaunchState === 'confirmed_alive' ||
|
|
spawnLaunchState === 'skipped_for_launch' ||
|
|
spawnLaunchState === 'runtime_pending_permission' ||
|
|
spawnStatus === 'error' ||
|
|
spawnStatus === 'online' ||
|
|
spawnStatus === 'skipped'
|
|
) {
|
|
return false;
|
|
}
|
|
if (spawnLaunchState !== 'starting' && spawnStatus !== 'waiting' && spawnStatus !== 'spawning') {
|
|
return false;
|
|
}
|
|
|
|
return hasElapsedSinceIso(
|
|
spawnFirstSpawnAcceptedAt ?? spawnUpdatedAt,
|
|
MEMBER_STARTING_STALE_AFTER_MS,
|
|
nowMs
|
|
);
|
|
}
|
|
|
|
function hasBootstrapStallDiagnostic(value: string | undefined): boolean {
|
|
const normalized = value?.trim().toLowerCase() ?? '';
|
|
return (
|
|
normalized.includes('no bootstrap check-in') ||
|
|
normalized.includes('bootstrap is unconfirmed') ||
|
|
normalized.includes('bootstrap unconfirmed')
|
|
);
|
|
}
|
|
|
|
export function isOpenCodeRelaunchActionable({
|
|
member,
|
|
spawnEntry,
|
|
runtimeEntry,
|
|
nowMs = Date.now(),
|
|
}: {
|
|
member: ResolvedTeamMember;
|
|
spawnEntry?: MemberSpawnStatusEntry;
|
|
runtimeEntry?: TeamAgentRuntimeEntry;
|
|
nowMs?: number;
|
|
}): boolean {
|
|
if (member.providerId !== 'opencode' || isLeadMember(member) || member.removedAt) {
|
|
return false;
|
|
}
|
|
if (spawnEntry?.launchState === 'starting' || spawnEntry?.status === 'spawning') {
|
|
return false;
|
|
}
|
|
if (spawnEntry?.launchState === 'runtime_pending_permission') {
|
|
return false;
|
|
}
|
|
if (!spawnEntry) {
|
|
const runtimeDiagnosticIsStuck = hasBootstrapStallDiagnostic(runtimeEntry?.runtimeDiagnostic);
|
|
return (
|
|
runtimeDiagnosticIsStuck ||
|
|
runtimeEntry?.livenessKind === 'registered_only' ||
|
|
runtimeEntry?.livenessKind === 'stale_metadata'
|
|
);
|
|
}
|
|
if (
|
|
spawnEntry?.launchState === 'failed_to_start' ||
|
|
spawnEntry?.launchState === 'skipped_for_launch' ||
|
|
spawnEntry?.status === 'error' ||
|
|
spawnEntry?.status === 'skipped'
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const livenessKind = runtimeEntry?.livenessKind ?? spawnEntry?.livenessKind;
|
|
const acceptedSpawnGraceElapsed = hasElapsedSinceIso(
|
|
spawnEntry?.firstSpawnAcceptedAt,
|
|
OPENCODE_RUNTIME_CANDIDATE_RELAUNCH_GRACE_MS,
|
|
nowMs
|
|
);
|
|
const hasExplicitBootstrapStall =
|
|
spawnEntry?.bootstrapStalled === true ||
|
|
hasBootstrapStallDiagnostic(spawnEntry?.runtimeDiagnostic) ||
|
|
hasBootstrapStallDiagnostic(runtimeEntry?.runtimeDiagnostic);
|
|
const launchIsNoLongerFresh =
|
|
spawnEntry.launchState === 'confirmed_alive' ||
|
|
spawnEntry.status === 'online' ||
|
|
acceptedSpawnGraceElapsed ||
|
|
hasExplicitBootstrapStall;
|
|
|
|
if (
|
|
livenessKind === 'registered_only' ||
|
|
livenessKind === 'stale_metadata' ||
|
|
livenessKind === 'not_found'
|
|
) {
|
|
return launchIsNoLongerFresh;
|
|
}
|
|
if (livenessKind === 'runtime_process') {
|
|
return hasExplicitBootstrapStall;
|
|
}
|
|
if (livenessKind !== 'runtime_process_candidate') {
|
|
return false;
|
|
}
|
|
|
|
return acceptedSpawnGraceElapsed || hasExplicitBootstrapStall;
|
|
}
|
|
|
|
export function buildMemberLaunchPresentation({
|
|
member,
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
spawnLivenessSource,
|
|
spawnRuntimeAlive,
|
|
spawnBootstrapConfirmed,
|
|
spawnBootstrapStalled,
|
|
spawnAgentToolAccepted,
|
|
spawnHardFailure,
|
|
spawnLivenessKind,
|
|
spawnFirstSpawnAcceptedAt,
|
|
spawnUpdatedAt,
|
|
runtimeAdvisory,
|
|
runtimeEntry,
|
|
isLaunchSettling = false,
|
|
isTeamAlive,
|
|
isTeamProvisioning,
|
|
leadActivity,
|
|
nowMs,
|
|
}: {
|
|
member: ResolvedTeamMember;
|
|
spawnStatus: MemberSpawnStatus | undefined;
|
|
spawnLaunchState: MemberLaunchState | undefined;
|
|
spawnLivenessSource: MemberSpawnLivenessSource | undefined;
|
|
spawnRuntimeAlive: boolean | undefined;
|
|
spawnBootstrapConfirmed?: boolean;
|
|
spawnBootstrapStalled?: boolean;
|
|
spawnAgentToolAccepted?: boolean;
|
|
spawnHardFailure?: boolean;
|
|
spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind'];
|
|
spawnFirstSpawnAcceptedAt?: string;
|
|
spawnUpdatedAt?: string;
|
|
runtimeAdvisory: MemberRuntimeAdvisory | undefined;
|
|
runtimeEntry?: TeamAgentRuntimeEntry;
|
|
isLaunchSettling?: boolean;
|
|
isTeamAlive?: boolean;
|
|
isTeamProvisioning?: boolean;
|
|
leadActivity?: LeadActivityState;
|
|
nowMs?: number;
|
|
}): MemberLaunchPresentation {
|
|
const currentRuntimeOfflineVisualState = getCurrentRuntimeOfflineVisualState(
|
|
member,
|
|
runtimeEntry,
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
spawnRuntimeAlive,
|
|
spawnBootstrapConfirmed,
|
|
isTeamProvisioning
|
|
);
|
|
const hasConfirmedSpawnLaunch =
|
|
spawnLaunchState === 'confirmed_alive' && spawnBootstrapConfirmed === true;
|
|
const suppressOpenCodeAppMcpAdvisory = isHealthyOpenCodeAppMcpConnectivityAdvisory({
|
|
providerId: member.providerId,
|
|
runtimeAdvisory,
|
|
spawnStatus,
|
|
launchState: spawnLaunchState,
|
|
runtimeAlive: spawnRuntimeAlive,
|
|
bootstrapConfirmed: spawnBootstrapConfirmed,
|
|
agentToolAccepted: spawnAgentToolAccepted,
|
|
hardFailure: spawnHardFailure,
|
|
livenessKind: spawnLivenessKind ?? runtimeEntry?.livenessKind,
|
|
runtimeEntry,
|
|
});
|
|
const displayRuntimeAdvisory = suppressOpenCodeAppMcpAdvisory ? undefined : runtimeAdvisory;
|
|
const effectiveSpawnStatus =
|
|
hasConfirmedSpawnLaunch &&
|
|
currentRuntimeOfflineVisualState == null &&
|
|
(spawnStatus === 'waiting' || spawnStatus === 'spawning')
|
|
? 'online'
|
|
: spawnStatus;
|
|
const effectiveSpawnRuntimeAlive =
|
|
currentRuntimeOfflineVisualState != null
|
|
? false
|
|
: hasConfirmedSpawnLaunch
|
|
? true
|
|
: spawnRuntimeAlive;
|
|
const presenceLabel = getLaunchAwarePresenceLabel(
|
|
member,
|
|
effectiveSpawnStatus,
|
|
spawnLaunchState,
|
|
spawnLivenessSource,
|
|
effectiveSpawnRuntimeAlive,
|
|
displayRuntimeAdvisory,
|
|
isLaunchSettling,
|
|
isTeamAlive,
|
|
isTeamProvisioning,
|
|
leadActivity
|
|
);
|
|
const baseDotClass = getSpawnAwareDotClass(
|
|
member,
|
|
effectiveSpawnStatus,
|
|
spawnLaunchState,
|
|
effectiveSpawnRuntimeAlive,
|
|
isLaunchSettling,
|
|
isTeamAlive,
|
|
isTeamProvisioning,
|
|
leadActivity
|
|
);
|
|
const cardClass = getSpawnCardClass(
|
|
effectiveSpawnStatus,
|
|
spawnLaunchState,
|
|
effectiveSpawnRuntimeAlive,
|
|
isLaunchSettling,
|
|
isTeamAlive,
|
|
isTeamProvisioning
|
|
);
|
|
const runtimeAdvisoryLabel = getMemberRuntimeAdvisoryLabel(
|
|
displayRuntimeAdvisory,
|
|
member.providerId
|
|
);
|
|
const runtimeAdvisoryTitle = getMemberRuntimeAdvisoryTitle(
|
|
displayRuntimeAdvisory,
|
|
member.providerId
|
|
);
|
|
const runtimeAdvisoryTone = getMemberRuntimeAdvisoryTone(
|
|
displayRuntimeAdvisory,
|
|
member.providerId
|
|
);
|
|
const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling;
|
|
const startingIsStale =
|
|
!hasConfirmedSpawnLaunch &&
|
|
isMemberStartingStale({
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
spawnFirstSpawnAcceptedAt,
|
|
spawnUpdatedAt,
|
|
nowMs,
|
|
});
|
|
|
|
let launchVisualState: MemberLaunchVisualState = null;
|
|
if (isTeamAlive !== false || isTeamProvisioning) {
|
|
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
|
launchVisualState = 'error';
|
|
} else if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') {
|
|
launchVisualState = 'skipped';
|
|
} else if (spawnLaunchState === 'runtime_pending_permission') {
|
|
launchVisualState = 'permission_pending';
|
|
} else if (spawnBootstrapStalled === true) {
|
|
launchVisualState = 'bootstrap_stalled';
|
|
} else if (currentRuntimeOfflineVisualState != null) {
|
|
launchVisualState = currentRuntimeOfflineVisualState;
|
|
} else if (runtimeEntry?.livenessKind === 'shell_only') {
|
|
launchVisualState = 'shell_only';
|
|
} else if (runtimeEntry?.livenessKind === 'runtime_process_candidate') {
|
|
launchVisualState = 'runtime_candidate';
|
|
} else if (!hasConfirmedSpawnLaunch && startingIsStale) {
|
|
launchVisualState = 'starting_stale';
|
|
} else if (
|
|
!hasConfirmedSpawnLaunch &&
|
|
isQueuedOpenCodeLaunch(
|
|
member,
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
runtimeEntry,
|
|
isLaunchSettling,
|
|
isTeamProvisioning
|
|
)
|
|
) {
|
|
launchVisualState = 'queued';
|
|
} else if (
|
|
!hasConfirmedSpawnLaunch &&
|
|
isLaunchStillStarting(
|
|
spawnStatus,
|
|
spawnLaunchState,
|
|
spawnRuntimeAlive,
|
|
keepLaunchSettlingVisuals
|
|
)
|
|
) {
|
|
launchVisualState = spawnStatus === 'spawning' ? 'spawning' : 'waiting';
|
|
} else if (
|
|
!hasConfirmedSpawnLaunch &&
|
|
spawnLaunchState === 'runtime_pending_bootstrap' &&
|
|
(runtimeEntry?.livenessKind === 'runtime_process' ||
|
|
(spawnStatus === 'online' && spawnRuntimeAlive === true))
|
|
) {
|
|
launchVisualState = 'runtime_pending';
|
|
} else if (isLaunchSettling && spawnLaunchState === 'confirmed_alive') {
|
|
launchVisualState = 'settling';
|
|
}
|
|
}
|
|
|
|
const launchStatusLabel = getMemberLaunchStatusLabel(launchVisualState);
|
|
const launchVisualStateDotClass = getLaunchVisualStateDotClass(launchVisualState);
|
|
const shouldShowLaunchStatusAsPresence =
|
|
launchVisualState === 'queued' ||
|
|
launchVisualState === 'starting_stale' ||
|
|
launchVisualState === 'permission_pending' ||
|
|
launchVisualState === 'bootstrap_stalled' ||
|
|
launchVisualState === 'runtime_pending' ||
|
|
launchVisualState === 'shell_only' ||
|
|
launchVisualState === 'runtime_candidate' ||
|
|
launchVisualState === 'registered_only' ||
|
|
launchVisualState === 'stale_runtime';
|
|
const displayPresenceLabel =
|
|
runtimeAdvisoryTone === 'error' && runtimeAdvisoryLabel
|
|
? runtimeAdvisoryLabel
|
|
: shouldShowLaunchStatusAsPresence
|
|
? (launchStatusLabel ?? presenceLabel)
|
|
: presenceLabel;
|
|
const spawnBadgeLabel =
|
|
spawnStatus && spawnStatus !== 'online'
|
|
? spawnStatus === 'waiting' || spawnStatus === 'spawning'
|
|
? startingIsStale
|
|
? 'starting stale'
|
|
: 'starting'
|
|
: spawnStatus
|
|
: null;
|
|
|
|
return {
|
|
presenceLabel: displayPresenceLabel,
|
|
dotClass:
|
|
runtimeAdvisoryTone === 'error'
|
|
? STATUS_DOT_COLORS.terminated
|
|
: (launchVisualStateDotClass ?? baseDotClass),
|
|
cardClass: launchVisualState === 'starting_stale' ? 'opacity-90' : cardClass,
|
|
runtimeAdvisoryLabel,
|
|
runtimeAdvisoryTitle,
|
|
runtimeAdvisoryTone,
|
|
launchVisualState,
|
|
launchStatusLabel,
|
|
spawnBadgeLabel,
|
|
};
|
|
}
|
|
|
|
export const TASK_STATUS_STYLES: Record<TeamTaskStatus, { bg: string; text: string }> = {
|
|
pending: { bg: 'bg-zinc-500/15', text: 'text-zinc-400' },
|
|
in_progress: { bg: 'bg-blue-500/15', text: 'text-blue-400' },
|
|
completed: { bg: 'bg-emerald-500/15', text: 'text-emerald-400' },
|
|
deleted: { bg: 'bg-red-500/15', text: 'text-red-400' },
|
|
};
|
|
|
|
export const TASK_STATUS_LABELS: Record<TeamTaskStatus, string> = {
|
|
pending: 'Pending',
|
|
in_progress: 'In Progress',
|
|
completed: 'Completed',
|
|
deleted: 'Deleted',
|
|
};
|
|
|
|
interface MemberColorInput {
|
|
name: string;
|
|
color?: string;
|
|
removedAt?: number | string | null;
|
|
agentType?: string;
|
|
role?: string;
|
|
}
|
|
|
|
interface MemberAvatarInput {
|
|
name: string;
|
|
removedAt?: number | string | null;
|
|
agentType?: string;
|
|
}
|
|
|
|
/**
|
|
* Build a consistent name→colorName map for all members.
|
|
* Active members receive colors sequentially from MEMBER_COLOR_PALETTE,
|
|
* which is pre-ordered for maximum visual contrast between consecutive entries.
|
|
* If a member has a stored color that hasn't been assigned yet, it is used instead.
|
|
* Maps "user" to a reserved color.
|
|
*/
|
|
export function buildMemberColorMap(members: MemberColorInput[]): Map<string, string> {
|
|
return buildTeamMemberColorMap(members, { preferProvidedColors: true });
|
|
}
|
|
|
|
export function buildMemberAvatarMap(members: readonly MemberAvatarInput[]): Map<string, string> {
|
|
const map = new Map<string, string>();
|
|
const activeMembers = members.filter((member) => !member.removedAt);
|
|
const leadMembers = activeMembers.filter((member) => isLeadMember(member));
|
|
const teammateMembers = activeMembers.filter((member) => !isLeadMember(member));
|
|
|
|
for (const [index, member] of leadMembers.entries()) {
|
|
map.set(member.name, index === 0 ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name));
|
|
}
|
|
|
|
for (const [index, member] of teammateMembers.entries()) {
|
|
map.set(
|
|
member.name,
|
|
getParticipantAvatarUrlByIndex(1 + (index % (PARTICIPANT_AVATAR_URLS.length - 1)))
|
|
);
|
|
}
|
|
|
|
for (const member of members) {
|
|
if (!map.has(member.name)) {
|
|
map.set(
|
|
member.name,
|
|
isLeadMember(member) ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name)
|
|
);
|
|
}
|
|
}
|
|
|
|
map.set('user', agentAvatarUrl('user'));
|
|
map.set('system', agentAvatarUrl('system'));
|
|
|
|
return map;
|
|
}
|
|
|
|
export function resolveMemberAvatarUrl(
|
|
member: MemberAvatarInput,
|
|
avatarMap?: ReadonlyMap<string, string>,
|
|
size = 64
|
|
): string {
|
|
return (
|
|
avatarMap?.get(member.name) ??
|
|
(isLeadMember(member) ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name, size))
|
|
);
|
|
}
|
|
|
|
export const KANBAN_COLUMN_DISPLAY: Record<
|
|
'review' | 'approved',
|
|
{ label: string; bg: string; text: string }
|
|
> = {
|
|
review: { label: 'In Review', bg: 'bg-amber-500/15', text: 'text-amber-400' },
|
|
approved: { label: 'Approved', bg: 'bg-emerald-500/15', text: 'text-emerald-400' },
|
|
};
|
|
|
|
export const REVIEW_STATE_DISPLAY: Record<
|
|
Exclude<TeamReviewState, 'none'>,
|
|
{ label: string; bg: string; text: string }
|
|
> = {
|
|
review: { label: 'In Review', bg: 'bg-amber-500/15', text: 'text-amber-400' },
|
|
needsFix: { label: 'Needs Fixes', bg: 'bg-rose-500/15', text: 'text-rose-400' },
|
|
approved: { label: 'Approved', bg: 'bg-emerald-500/15', text: 'text-emerald-400' },
|
|
};
|