diff --git a/packages/agent-graph/src/canvas/draw-agents.ts b/packages/agent-graph/src/canvas/draw-agents.ts index 6ee99403..a14dbcd2 100644 --- a/packages/agent-graph/src/canvas/draw-agents.ts +++ b/packages/agent-graph/src/canvas/draw-agents.ts @@ -654,7 +654,7 @@ function drawAvatar( isLead: boolean, avatarUrl?: string ): void { - const avatarR = r * 0.6; + const avatarR = r * AGENT_DRAW.avatarRadiusScale; // Try to draw avatar image if (avatarUrl) { diff --git a/packages/agent-graph/src/constants/canvas-constants.ts b/packages/agent-graph/src/constants/canvas-constants.ts index 30f6363f..90c576a7 100644 --- a/packages/agent-graph/src/constants/canvas-constants.ts +++ b/packages/agent-graph/src/constants/canvas-constants.ts @@ -58,9 +58,9 @@ export const FORCE = { export const NODE = { /** Lead agent radius */ - radiusLead: 32, + radiusLead: 38, /** Team member radius */ - radiusMember: 24, + radiusMember: 30, /** Process node radius */ radiusProcess: 14, /** Cross-team ghost node radius */ @@ -110,6 +110,7 @@ export const AGENT_DRAW = { sparkScale: 0.45, sparkViewBox: 256, subIconScale: 0.45, + avatarRadiusScale: 0.74, } as const; // ─── Context ring (lead node only) ───────────────────────────────────────── diff --git a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts index a76d369c..eefda424 100644 --- a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts @@ -466,8 +466,8 @@ export class TeamGraphAdapter { launchStatusLabel: leadLaunchPresentation?.launchStatusLabel ?? undefined, contextUsage: percent != null ? Math.max(0, Math.min(1, percent / 100)) : undefined, avatarUrl: leadMember - ? resolveMemberAvatarUrl(leadMember, avatarMap, 64) - : agentAvatarUrl(leadName, 64), + ? resolveMemberAvatarUrl(leadMember, avatarMap, 96) + : agentAvatarUrl(leadName, 96), pendingApproval, activeTool: activeTool ? { @@ -567,7 +567,7 @@ export class TeamGraphAdapter { launchStatusLabel: isTeamVisualOnline ? (launchPresentation.launchStatusLabel ?? undefined) : undefined, - avatarUrl: resolveMemberAvatarUrl(member, avatarMap, 64), + avatarUrl: resolveMemberAvatarUrl(member, avatarMap, 96), currentTaskId: member.currentTaskId ?? undefined, currentTaskSubject: member.currentTaskId ? data.tasks.find((t) => t.id === member.currentTaskId)?.subject diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 49b368a4..4bf7fae1 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1626,6 +1626,25 @@ function hasOpenCodeRuntimeLivenessMarker( ); } +function hasOpenCodeRuntimeEntryHandle( + value: + | Pick + | undefined + | null +): boolean { + if (!value) { + return false; + } + const pid = typeof value.pid === 'number' && Number.isFinite(value.pid) && value.pid > 0; + const runtimePid = + typeof value.runtimePid === 'number' && + Number.isFinite(value.runtimePid) && + value.runtimePid > 0; + const runtimeSessionId = + typeof value.runtimeSessionId === 'string' && value.runtimeSessionId.trim().length > 0; + return pid || runtimePid || runtimeSessionId || hasOpenCodeRuntimeLivenessMarker(value); +} + function isRecoverablePersistedOpenCodeRuntimeCandidate( member: PersistedTeamLaunchMemberState | undefined | null ): boolean { @@ -9618,6 +9637,20 @@ export class TeamProvisioningService { ); const existingLane = existingLaneIndex >= 0 ? run.mixedSecondaryLanes[existingLaneIndex] : null; + if (run.pendingMemberRestarts.has(memberName)) { + throw new Error(`Restart for teammate "${memberName}" is already in progress`); + } + if (existingLane?.state === 'queued' || existingLane?.state === 'launching') { + throw new Error(`Restart for teammate "${memberName}" is already in progress`); + } + + const hasRuntimeEvidence = await this.hasOpenCodeMemberRuntimeEvidenceForControlledRelaunch({ + teamName, + memberName: memberSpec.name, + laneId: nextLane.laneId, + existingLane, + }); + if (existingLane) { await this.stopSingleMixedSecondaryRuntimeLane(run, existingLane, 'relaunch'); } @@ -9629,7 +9662,10 @@ export class TeamProvisioningService { laneState.state = 'queued'; laneState.result = null; laneState.warnings = []; - laneState.diagnostics = options?.reason ? [`controlled_reattach:${options.reason}`] : []; + laneState.diagnostics = [ + ...(options?.reason ? [`controlled_reattach:${options.reason}`] : []), + ...(!hasRuntimeEvidence ? ['fresh_relaunch:no_runtime_evidence'] : []), + ]; if (existingLaneIndex >= 0) { run.mixedSecondaryLanes[existingLaneIndex] = laneState; @@ -9647,6 +9683,45 @@ export class TeamProvisioningService { await this.launchSingleMixedSecondaryLane(run, laneState); } + private async hasOpenCodeMemberRuntimeEvidenceForControlledRelaunch(params: { + teamName: string; + memberName: string; + laneId: string; + existingLane: MixedSecondaryRuntimeLaneState | null; + }): Promise { + const laneResultMember = + params.existingLane?.result?.members[params.memberName] ?? + Object.values(params.existingLane?.result?.members ?? {}).find( + (member) => member.memberName?.trim() === params.memberName + ); + if (hasOpenCodeRuntimeHandle(laneResultMember)) { + return true; + } + + const persistedSnapshot = await this.launchStateStore.read(params.teamName).catch(() => null); + const persistedMember = + persistedSnapshot?.members[params.memberName] ?? + Object.values(persistedSnapshot?.members ?? {}).find( + (member) => member.laneId === params.laneId + ); + if ( + hasOpenCodeRuntimeHandle(persistedMember) || + hasOpenCodeRuntimeLivenessMarker(persistedMember) + ) { + return true; + } + + const liveRuntimeByMember = await this.getLiveTeamAgentRuntimeMetadata(params.teamName).catch( + () => new Map() + ); + const liveRuntimeMember = + liveRuntimeByMember.get(params.memberName) ?? + [...liveRuntimeByMember.entries()].find(([candidateName]) => + matchesObservedMemberNameForExpected(candidateName, params.memberName) + )?.[1]; + return hasOpenCodeRuntimeEntryHandle(liveRuntimeMember); + } + async detachOpenCodeOwnedMemberLane(teamName: string, memberName: string): Promise { const run = this.getMutableAliveRunOrThrow(teamName); const laneIndex = run.mixedSecondaryLanes.findIndex((lane) => @@ -16512,6 +16587,7 @@ export class TeamProvisioningService { run: ProvisioningRun, lane: MixedSecondaryRuntimeLaneState ): Promise { + const requestedDiagnostics = [...lane.diagnostics]; const adapter = this.getOpenCodeRuntimeAdapter(); if (!adapter) { const message = 'OpenCode runtime adapter is not registered for mixed team launch.'; @@ -16535,10 +16611,10 @@ export class TeamProvisioningService { }, }, warnings: [], - diagnostics: [message], + diagnostics: [...requestedDiagnostics, message], }; lane.warnings = []; - lane.diagnostics = [message]; + lane.diagnostics = [...requestedDiagnostics, message]; await this.publishMixedSecondaryLaneStatusChange(run, lane); lane.state = 'finished'; return; @@ -16560,7 +16636,7 @@ export class TeamProvisioningService { lane.state = 'launching'; lane.runId = lane.runId ?? randomUUID(); lane.warnings = []; - lane.diagnostics = [...migration.diagnostics]; + lane.diagnostics = [...requestedDiagnostics, ...migration.diagnostics]; const laneCwd = lane.member.cwd?.trim() || run.request.cwd; this.setSecondaryRuntimeRun({ teamName: run.teamName, @@ -16610,10 +16686,11 @@ export class TeamProvisioningService { } lane.result = result; lane.warnings = [...result.warnings]; - lane.diagnostics = [...migration.diagnostics, ...result.diagnostics]; + lane.diagnostics = [...requestedDiagnostics, ...migration.diagnostics, ...result.diagnostics]; if (isDefinitiveOpenCodePreLaunchFailure(result, lane.member.name)) { const diagnostics = [ + ...requestedDiagnostics, ...migration.diagnostics, ...collectRuntimeLaunchFailureDiagnostics(result, lane.member.name), ]; @@ -16656,7 +16733,7 @@ export class TeamProvisioningService { diagnostics: [message], }; lane.warnings = []; - lane.diagnostics = [...migration.diagnostics, message]; + lane.diagnostics = [...requestedDiagnostics, ...migration.diagnostics, message]; await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName: run.teamName, diff --git a/src/renderer/assets/participant-avatars/01.png b/src/renderer/assets/participant-avatars/01.png index 4128d3b0..df776c6a 100644 Binary files a/src/renderer/assets/participant-avatars/01.png and b/src/renderer/assets/participant-avatars/01.png differ diff --git a/src/renderer/assets/participant-avatars/02.png b/src/renderer/assets/participant-avatars/02.png index 15575859..a4478433 100644 Binary files a/src/renderer/assets/participant-avatars/02.png and b/src/renderer/assets/participant-avatars/02.png differ diff --git a/src/renderer/assets/participant-avatars/03.png b/src/renderer/assets/participant-avatars/03.png index a5e00bcd..aa5c49b9 100644 Binary files a/src/renderer/assets/participant-avatars/03.png and b/src/renderer/assets/participant-avatars/03.png differ diff --git a/src/renderer/assets/participant-avatars/04.png b/src/renderer/assets/participant-avatars/04.png index f984db69..e174d49c 100644 Binary files a/src/renderer/assets/participant-avatars/04.png and b/src/renderer/assets/participant-avatars/04.png differ diff --git a/src/renderer/assets/participant-avatars/05.png b/src/renderer/assets/participant-avatars/05.png index a9795962..36db6099 100644 Binary files a/src/renderer/assets/participant-avatars/05.png and b/src/renderer/assets/participant-avatars/05.png differ diff --git a/src/renderer/assets/participant-avatars/06.png b/src/renderer/assets/participant-avatars/06.png index 71950d32..77664676 100644 Binary files a/src/renderer/assets/participant-avatars/06.png and b/src/renderer/assets/participant-avatars/06.png differ diff --git a/src/renderer/assets/participant-avatars/07.png b/src/renderer/assets/participant-avatars/07.png index 8f23fb86..e375112c 100644 Binary files a/src/renderer/assets/participant-avatars/07.png and b/src/renderer/assets/participant-avatars/07.png differ diff --git a/src/renderer/assets/participant-avatars/08.png b/src/renderer/assets/participant-avatars/08.png index c7ada81e..42c137a9 100644 Binary files a/src/renderer/assets/participant-avatars/08.png and b/src/renderer/assets/participant-avatars/08.png differ diff --git a/src/renderer/assets/participant-avatars/09.png b/src/renderer/assets/participant-avatars/09.png index 8f4abe98..3c50cee5 100644 Binary files a/src/renderer/assets/participant-avatars/09.png and b/src/renderer/assets/participant-avatars/09.png differ diff --git a/src/renderer/assets/participant-avatars/10.png b/src/renderer/assets/participant-avatars/10.png index bee2490e..9e433a6c 100644 Binary files a/src/renderer/assets/participant-avatars/10.png and b/src/renderer/assets/participant-avatars/10.png differ diff --git a/src/renderer/assets/participant-avatars/11.png b/src/renderer/assets/participant-avatars/11.png index e77da7e4..88d9e562 100644 Binary files a/src/renderer/assets/participant-avatars/11.png and b/src/renderer/assets/participant-avatars/11.png differ diff --git a/src/renderer/assets/participant-avatars/12.png b/src/renderer/assets/participant-avatars/12.png index 32ee4912..80055539 100644 Binary files a/src/renderer/assets/participant-avatars/12.png and b/src/renderer/assets/participant-avatars/12.png differ diff --git a/src/renderer/assets/participant-avatars/13.png b/src/renderer/assets/participant-avatars/13.png index 9b774e24..1a4f6174 100644 Binary files a/src/renderer/assets/participant-avatars/13.png and b/src/renderer/assets/participant-avatars/13.png differ diff --git a/src/renderer/components/team/members/MemberDetailDialog.tsx b/src/renderer/components/team/members/MemberDetailDialog.tsx index 4eaea9d7..3828f04c 100644 --- a/src/renderer/components/team/members/MemberDetailDialog.tsx +++ b/src/renderer/components/team/members/MemberDetailDialog.tsx @@ -46,6 +46,37 @@ import type { TeamTaskWithKanban, } from '@shared/types'; +const OPENCODE_NO_RUNTIME_EVIDENCE_MESSAGE = + 'No OpenCode runtime session was recorded. Relaunch this teammate to start a fresh OpenCode session.'; + +function hasOpenCodeRuntimeEvidence(runtimeEntry: TeamAgentRuntimeEntry | undefined): boolean { + const hasPid = + typeof runtimeEntry?.pid === 'number' && + Number.isFinite(runtimeEntry.pid) && + runtimeEntry.pid > 0; + const hasRuntimePid = + typeof runtimeEntry?.runtimePid === 'number' && + Number.isFinite(runtimeEntry.runtimePid) && + runtimeEntry.runtimePid > 0; + const hasRuntimeSessionId = + typeof runtimeEntry?.runtimeSessionId === 'string' && + runtimeEntry.runtimeSessionId.trim().length > 0; + const hasRuntimeLiveness = + runtimeEntry?.livenessKind === 'runtime_process' || + runtimeEntry?.livenessKind === 'runtime_process_candidate' || + runtimeEntry?.livenessKind === 'permission_blocked'; + return Boolean(hasPid || hasRuntimePid || hasRuntimeSessionId || hasRuntimeLiveness); +} + +function isOpenCodeNoRuntimeEvidenceFailure( + member: ResolvedTeamMember, + spawnEntry: MemberSpawnStatusEntry | undefined, + runtimeEntry: TeamAgentRuntimeEntry | undefined +): boolean { + const failed = spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.status === 'error'; + return member.providerId === 'opencode' && failed && !hasOpenCodeRuntimeEvidence(runtimeEntry); +} + interface MemberDetailDialogProps { open: boolean; member: ResolvedTeamMember | null; @@ -165,6 +196,13 @@ export const MemberDetailDialog = ({ const launchErrorMessage = launchDiagnosticsPayload ? getMemberLaunchDiagnosticsErrorMessage(launchDiagnosticsPayload) : undefined; + const openCodeNoRuntimeEvidence = member + ? isOpenCodeNoRuntimeEvidenceFailure(member, spawnEntry, runtimeEntry) + : false; + const effectiveLaunchErrorMessage = openCodeNoRuntimeEvidence + ? OPENCODE_NO_RUNTIME_EVIDENCE_MESSAGE + : launchErrorMessage; + const restartButtonLabel = openCodeNoRuntimeEvidence ? 'Relaunch OpenCode' : 'Restart'; useEffect(() => { if (!open || !member) { @@ -284,10 +322,10 @@ export const MemberDetailDialog = ({ {restartError ? (
{restartError}
- ) : launchErrorMessage ? ( + ) : effectiveLaunchErrorMessage ? (
- - {launchErrorMessage} + + {effectiveLaunchErrorMessage} {launchDiagnosticsPayload && showCopyDiagnostics ? ( )} - Restart + {restartButtonLabel} )}