fix(ci): apply frontend validation fixes

This commit is contained in:
777genius 2026-05-07 18:43:37 +03:00
parent 30a6e36976
commit 9d419626ef
42 changed files with 621 additions and 170 deletions

View file

@ -25,11 +25,11 @@ import {
} from '@shared/utils/idleNotificationSemantics'; } from '@shared/utils/idleNotificationSemantics';
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise'; import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
import { import {
isTeamTaskActivelyWorked, isTeamTaskActivelyWorked,
isTeamTaskNeedsFixActionable, isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState'; } from '@shared/utils/teamTaskState';
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
import { import {
buildInlineActivityEntries, buildInlineActivityEntries,

View file

@ -28,6 +28,8 @@ import type {
const LOG_PREVIEW_FALLBACK_WIDTH = 260; const LOG_PREVIEW_FALLBACK_WIDTH = 260;
const LOG_PREVIEW_FALLBACK_HEIGHT = 292; const LOG_PREVIEW_FALLBACK_HEIGHT = 292;
const NEW_LOG_HIGHLIGHT_MS = 1_000; const NEW_LOG_HIGHLIGHT_MS = 1_000;
const COMPACT_ROW_TEXT_LIMIT = 118;
const COMPACT_ROW_MIN_PREVIEW_LIMIT = 48;
interface StableRectLike { interface StableRectLike {
left: number; left: number;
@ -155,6 +157,18 @@ function compactPreviewText(item: MemberLogPreviewItem, displayTitle: string): s
return item.sourceLabel || 'Log event'; return item.sourceLabel || 'Log event';
} }
function truncateCompactRowPreview(
preview: string,
displayTitle: string,
relativeTime: string
): string {
const normalized = preview.replace(/\s+/g, ' ').trim();
const metaLength = displayTitle.length + relativeTime.length + (relativeTime ? 2 : 1);
const previewLimit = Math.max(COMPACT_ROW_MIN_PREVIEW_LIMIT, COMPACT_ROW_TEXT_LIMIT - metaLength);
if (normalized.length <= previewLimit) return normalized;
return `${normalized.slice(0, Math.max(0, previewLimit - 3)).trimEnd()}...`;
}
function setShellHidden(shell: HTMLDivElement): void { function setShellHidden(shell: HTMLDivElement): void {
shell.style.opacity = '0'; shell.style.opacity = '0';
shell.style.pointerEvents = 'none'; shell.style.pointerEvents = 'none';
@ -405,42 +419,53 @@ export const GraphMemberLogPreviewHud = ({
(memberName: string, item: MemberLogPreviewItem) => { (memberName: string, item: MemberLogPreviewItem) => {
const relativeTime = formatRelativeTime(item.timestamp); const relativeTime = formatRelativeTime(item.timestamp);
const displayTitle = compactDisplayTitle(item); const displayTitle = compactDisplayTitle(item);
const previewText = compactPreviewText(item, displayTitle); const fullPreviewText = compactPreviewText(item, displayTitle);
const previewText = truncateCompactRowPreview(fullPreviewText, displayTitle, relativeTime);
const titleText = relativeTime const titleText = relativeTime
? `${displayTitle} ${relativeTime} ${previewText}` ? `${displayTitle} ${relativeTime} ${fullPreviewText}`
: `${displayTitle} ${previewText}`; : `${displayTitle} ${fullPreviewText}`;
const isHighlighted = highlightedItemIds.has(item.id); const isHighlighted = highlightedItemIds.has(item.id);
const isError = item.tone === 'error';
const rowStateClassName = isHighlighted
? isError
? 'border-rose-300/75 bg-rose-950/35 shadow-[0_0_0_1px_rgba(253,164,175,0.30),0_0_18px_rgba(244,63,94,0.22)] hover:border-rose-300/80 hover:bg-rose-950/45'
: 'border-sky-300/70 bg-[rgba(14,34,62,0.74)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)] hover:border-sky-300/75 hover:bg-[rgba(14,34,62,0.82)]'
: isError
? 'border-rose-400/35 bg-rose-950/20 hover:border-rose-300/50 hover:bg-rose-950/30'
: 'border-white/10 bg-[rgba(8,14,28,0.52)] hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]';
const iconClassName = isError
? 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-rose-500/10'
: 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5';
const headerClassName = 'inline-flex h-5 items-center align-top';
const titleClassName = isError
? 'text-[11px] font-medium leading-[18px] text-rose-100'
: 'text-[11px] font-medium leading-[18px] text-slate-200';
const timeClassName = isError
? 'ml-1 text-[9px] font-normal leading-[18px] text-rose-300/70'
: 'ml-1 text-[9px] font-normal leading-[18px] text-slate-500';
const previewClassName = isError
? 'ml-1 break-words align-top text-[10px] leading-[18px] text-rose-100/85'
: 'ml-1 break-words align-top text-[10px] leading-[18px] text-slate-300/85';
return ( return (
<button <button
key={item.id} key={item.id}
type="button" type="button"
className={[ className={[
'block h-16 min-h-16 w-full min-w-0 overflow-hidden rounded-md border px-2.5 py-1.5 text-left text-slate-400 transition-[border-color,background-color,box-shadow] duration-500 hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]', 'block h-[68px] min-h-[68px] w-full min-w-0 overflow-hidden rounded-md border px-2.5 py-1.5 text-left text-slate-400 transition-[border-color,background-color,box-shadow] duration-500',
isHighlighted rowStateClassName,
? 'border-sky-300/70 bg-[rgba(14,34,62,0.74)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)]'
: 'border-white/10 bg-[rgba(8,14,28,0.52)]',
].join(' ')} ].join(' ')}
title={titleText} title={titleText}
onClick={() => openLogs(memberName)} onClick={() => openLogs(memberName)}
> >
<span <span className={iconClassName} aria-hidden="true">
className="float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5 align-top"
aria-hidden="true"
>
{itemIcon(item)} {itemIcon(item)}
</span> </span>
<span className="align-top text-[11px] font-medium leading-4 text-slate-200"> <span className={headerClassName}>
{displayTitle} <span className={titleClassName}>{displayTitle}</span>
</span> {relativeTime ? <span className={timeClassName}>{relativeTime}</span> : null}
{relativeTime ? (
<span className="ml-1 align-top text-[9px] font-normal leading-4 text-slate-500">
{relativeTime}
</span>
) : null}
<span className="ml-1 break-words align-top text-[10px] leading-4 text-slate-300/85">
{previewText}
</span> </span>
<span className={previewClassName}>{previewText}</span>
</button> </button>
); );
}, },
@ -494,7 +519,7 @@ export const GraphMemberLogPreviewHud = ({
) : ( ) : (
<button <button
type="button" type="button"
className="flex h-16 min-h-16 items-center rounded-md border border-dashed border-white/10 bg-[rgba(8,14,28,0.28)] px-3 text-left text-[11px] text-slate-400/60" className="flex h-[68px] min-h-[68px] items-center rounded-md border border-dashed border-white/10 bg-[rgba(8,14,28,0.28)] px-3 text-left text-[11px] text-slate-400/60"
onClick={() => openLogs(memberName)} onClick={() => openLogs(memberName)}
> >
{resolveEmptyText(preview, loading, error)} {resolveEmptyText(preview, loading, error)}

View file

@ -13,8 +13,8 @@ import {
buildMemberAvatarMap, buildMemberAvatarMap,
buildMemberLaunchPresentation, buildMemberLaunchPresentation,
} from '@renderer/utils/memberHelpers'; } from '@renderer/utils/memberHelpers';
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation'; import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react'; import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics'; import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics';

View file

@ -53,6 +53,48 @@ describe('memberLogPreviewExtractor', () => {
expect(result.items[1]?.preview).toBe('older answer'); expect(result.items[1]?.preview).toBe('older answer');
}); });
it('marks assistant runtime error text as error tone without flagging normal text', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'api-error',
timestamp: '2026-04-01T10:00:00.000Z',
content: [
{
type: 'text',
text: `API Error: 429
{"type":"error","error":{"type":"api_error","message":"Codex API error: 429"}}`,
},
],
}),
message({
uuid: 'normal-error-word',
timestamp: '2026-04-01T10:01:00.000Z',
content: [{ type: 'text', text: 'Reviewed the error handling path and it is covered.' }],
}),
],
});
expect(result.items[0]).toMatchObject({
kind: 'text',
title: 'Assistant',
preview: 'Reviewed the error handling path and it is covered.',
tone: 'neutral',
});
expect(result.items[1]).toMatchObject({
kind: 'text',
title: 'API error',
tone: 'error',
});
expect(result.items[1]?.preview).toContain('API Error: 429');
expect(result.items[1]?.preview).toContain('Codex API error: 429');
expect(result.items[1]?.preview).not.toContain('{"type"');
});
it('extracts readable inbound task and comment messages without agent-only blocks', () => { it('extracts readable inbound task and comment messages without agent-only blocks', () => {
const result = extractMemberLogPreviewItems({ const result = extractMemberLogPreviewItems({
provider: 'opencode_runtime', provider: 'opencode_runtime',
@ -293,6 +335,96 @@ Reply to this comment using MCP tool task_add_comment.
}); });
}); });
it('accepts OpenCode canonical callId/toolName tool calls defensively', () => {
const result = extractMemberLogPreviewItems({
provider: 'opencode_runtime',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'canonical-call',
timestamp: '2026-04-01T10:00:00.000Z',
content: '',
toolCalls: [
{
callId: 'fc-canonical',
toolName: 'agent-teams_task_add_comment',
input: {
taskId: '1dcfefd2-e505-4b1f-af22-0227c0aa551a',
text: 'Confirmed',
},
},
],
}),
message({
uuid: 'canonical-result',
type: 'user',
role: 'user',
timestamp: '2026-04-01T10:01:00.000Z',
content: '',
toolResults: [
{
toolUseId: 'fc-canonical',
content: JSON.stringify({
taskId: '1dcfefd2-e505-4b1f-af22-0227c0aa551a',
comment: {
id: 'comment-1',
author: 'jack',
text: 'Confirmed',
},
}),
isError: false,
},
],
}),
],
});
expect(result.items).toHaveLength(1);
expect(result.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Comment added',
preview: 'Comment by jack on #1dcfefd2: Confirmed',
});
});
it('marks nested structured error tool results without requiring is_error', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'api-result',
type: 'user',
role: 'user',
timestamp: '2026-04-01T10:01:00.000Z',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-api',
content: JSON.stringify({
type: 'error',
error: {
type: 'api_error',
message: 'Codex API error: 429',
},
}),
},
],
}),
],
});
expect(result.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Tool error',
preview: 'Codex API error: 429',
tone: 'error',
});
expect(result.items[0]?.preview).not.toContain('{"type"');
});
it('formats orphan comment result payloads without guessing add vs read semantics', () => { it('formats orphan comment result payloads without guessing add vs read semantics', () => {
const result = extractMemberLogPreviewItems({ const result = extractMemberLogPreviewItems({
provider: 'claude_transcript', provider: 'claude_transcript',

View file

@ -21,8 +21,10 @@ export interface MemberLogPreviewParsedMessage {
content: string | MemberLogPreviewContentBlock[]; content: string | MemberLogPreviewContentBlock[];
isMeta?: boolean; isMeta?: boolean;
toolCalls?: readonly { toolCalls?: readonly {
id: string; id?: string;
name: string; name?: string;
callId?: string;
toolName?: string;
input?: unknown; input?: unknown;
isTask?: boolean; isTask?: boolean;
}[]; }[];
@ -107,6 +109,20 @@ interface ToolUseContext {
input?: unknown; input?: unknown;
} }
interface ToolCallLike {
id?: string;
name?: string;
callId?: string;
toolName?: string;
input?: unknown;
}
interface NormalizedToolCall {
id: string;
name: string;
input?: unknown;
}
function timestampMs(value: Date | string): number { function timestampMs(value: Date | string): number {
const time = value instanceof Date ? value.getTime() : Date.parse(value); const time = value instanceof Date ? value.getTime() : Date.parse(value);
return Number.isFinite(time) ? time : UNKNOWN_TIMESTAMP_MS; return Number.isFinite(time) ? time : UNKNOWN_TIMESTAMP_MS;
@ -207,6 +223,18 @@ function textFromTextContentBlocks(value: unknown): string | null {
return text.trim().length > 0 ? text : null; return text.trim().length > 0 ? text : null;
} }
function textFromPreviewContent(content: string | MemberLogPreviewContentBlock[]): string {
if (typeof content === 'string') {
return content;
}
return content
.filter((block): block is Extract<MemberLogPreviewContentBlock, { type: 'text' }> => {
return block.type === 'text' && typeof block.text === 'string';
})
.map((block) => block.text)
.join(' ');
}
function unwrapAgentTeamsResponsePayload(payload: Record<string, unknown>): { function unwrapAgentTeamsResponsePayload(payload: Record<string, unknown>): {
payload: Record<string, unknown>; payload: Record<string, unknown>;
wrapperKey?: string; wrapperKey?: string;
@ -256,6 +284,127 @@ function recordFromUnknown(value: unknown): Record<string, unknown> | null {
return recordFromUnknownWithWrapper(value)?.payload ?? null; return recordFromUnknownWithWrapper(value)?.payload ?? null;
} }
function unknownPayloadLooksLikeError(value: unknown): boolean {
if (Array.isArray(value)) {
return value.some(unknownPayloadLooksLikeError);
}
const record = asRecord(value);
if (!record) {
return false;
}
const type = stringField(record, 'type')?.toLowerCase();
if (type === 'error' || type?.endsWith('_error')) {
return true;
}
if (record.ok === false || record.success === false) {
return true;
}
const error = record.error;
if (typeof error === 'string' && error.trim().length > 0) {
return true;
}
if (error === true) {
return true;
}
const errorRecord = asRecord(error);
if (
errorRecord &&
(stringField(errorRecord, 'type') ||
stringField(errorRecord, 'message') ||
stringField(errorRecord, 'code'))
) {
return true;
}
return typeof record.errorMessage === 'string' && record.errorMessage.trim().length > 0;
}
function payloadErrorMessage(payload: Record<string, unknown>): string | null {
const direct =
stringField(payload, 'error') ??
stringField(payload, 'errorMessage') ??
stringField(payload, 'message');
if (direct) {
return direct;
}
const nestedError = asRecord(payload.error);
if (!nestedError) {
return null;
}
const nestedType = stringField(nestedError, 'type')?.replace(/_/g, ' ');
const nestedCode = stringField(nestedError, 'code');
const nestedMessage = stringField(nestedError, 'message');
if (nestedMessage) {
return nestedMessage;
}
if (nestedType && nestedCode) {
return `${nestedType} ${nestedCode}`;
}
return nestedType ?? nestedCode;
}
function parseJsonObjectFromText(value: string): Record<string, unknown> | null {
const parsed = parseJsonLikeString(value);
const direct = asRecord(parsed);
if (direct) {
return direct;
}
const jsonStart = value.indexOf('{');
if (jsonStart <= 0) {
return null;
}
return asRecord(parseJsonLikeString(value.slice(jsonStart)));
}
function runtimeErrorTitle(value: string, payload: Record<string, unknown> | null): string {
const labelMatch = /^(api error|runtime error|provider error|tool error)\b/i.exec(value);
if (labelMatch?.[1]) {
const label = labelMatch[1].toLowerCase();
if (label === 'api error') return 'API error';
return `${label[0]?.toUpperCase()}${label.slice(1)}`;
}
const type = stringField(payload, 'type') ?? stringField(asRecord(payload?.error), 'type');
if (type?.toLowerCase().includes('api')) {
return 'API error';
}
return 'Runtime error';
}
function formatRuntimeErrorText(
value: string,
limit: number
): (ValuePreview & { title: string }) | null {
const compact = compactWhitespace(value);
if (!compact) {
return null;
}
const payload = parseJsonObjectFromText(compact);
const hasErrorSignal =
/^(api error|runtime error|provider error|tool error)\b/i.test(compact) ||
/\b(api|codex|claude|openai|anthropic)\s+api\s+error\b/i.test(compact) ||
/\b(api|codex|claude|openai|anthropic|provider)\s+error\s*:\s*\d{3}\b/i.test(compact) ||
unknownPayloadLooksLikeError(payload);
if (!hasErrorSignal) {
return null;
}
const title = runtimeErrorTitle(compact, payload);
const jsonStart = compact.indexOf('{');
const header = jsonStart > 0 ? compact.slice(0, jsonStart).trim() : '';
const payloadMessage = payload ? payloadErrorMessage(payload) : null;
const text =
payloadMessage && header && !header.toLowerCase().includes(payloadMessage.toLowerCase())
? `${header} - ${payloadMessage}`
: (payloadMessage ?? header);
return { ...truncatePreview(text || 'Runtime error', limit), title };
}
function findPriorityValue( function findPriorityValue(
record: Record<string, unknown>, record: Record<string, unknown>,
keys: readonly string[] keys: readonly string[]
@ -721,18 +870,8 @@ function formatRuntimePayload(
} }
function formatErrorPayload(payload: Record<string, unknown>): KnownPayloadPreview | null { function formatErrorPayload(payload: Record<string, unknown>): KnownPayloadPreview | null {
const error = if (unknownPayloadLooksLikeError(payload)) {
stringField(payload, 'error') ?? return { title: 'Tool error', text: payloadErrorMessage(payload) ?? 'Tool reported failure' };
stringField(payload, 'errorMessage') ??
stringField(payload, 'message');
if (
error &&
(payload.ok === false || payload.success === false || payload.error || payload.errorMessage)
) {
return { title: 'Tool error', text: error };
}
if (payload.ok === false || payload.success === false) {
return { title: 'Tool error', text: 'Tool reported failure' };
} }
return null; return null;
} }
@ -1059,16 +1198,7 @@ function extractTextPreview(
content: string | MemberLogPreviewContentBlock[], content: string | MemberLogPreviewContentBlock[],
textLimit: number textLimit: number
): { preview: string; truncated: boolean } | null { ): { preview: string; truncated: boolean } | null {
if (typeof content === 'string') { const text = textFromPreviewContent(content);
const text = truncatePreview(content, textLimit);
return text.preview.length > 0 ? text : null;
}
const text = content
.filter((block): block is Extract<MemberLogPreviewContentBlock, { type: 'text' }> => {
return block.type === 'text' && typeof block.text === 'string';
})
.map((block) => block.text)
.join(' ');
const preview = truncatePreview(text, textLimit); const preview = truncatePreview(text, textLimit);
return preview.preview.length > 0 ? preview : null; return preview.preview.length > 0 ? preview : null;
} }
@ -1204,12 +1334,35 @@ function isToolResultBlock(
); );
} }
function normalizeToolCall(toolCall: ToolCallLike): NormalizedToolCall | null {
const id =
typeof toolCall.id === 'string'
? toolCall.id.trim()
: typeof toolCall.callId === 'string'
? toolCall.callId.trim()
: '';
const name =
typeof toolCall.name === 'string'
? toolCall.name.trim()
: typeof toolCall.toolName === 'string'
? toolCall.toolName.trim()
: '';
if (!id || !name) {
return null;
}
return {
id,
name,
input: toolCall.input,
};
}
function buildToolUseContexts( function buildToolUseContexts(
messages: readonly MemberLogPreviewParsedMessage[] messages: readonly MemberLogPreviewParsedMessage[]
): Map<string, ToolUseContext> { ): Map<string, ToolUseContext> {
const contexts = new Map<string, ToolUseContext>(); const contexts = new Map<string, ToolUseContext>();
const addContext = (tool: { id: string; name: string; input?: unknown }): void => { const addContext = (tool: NormalizedToolCall): void => {
const id = tool.id.trim(); const id = tool.id;
if (!id || contexts.has(id)) return; if (!id || contexts.has(id)) return;
contexts.set(id, { contexts.set(id, {
id, id,
@ -1220,7 +1373,12 @@ function buildToolUseContexts(
}; };
for (const message of messages) { for (const message of messages) {
message.toolCalls?.forEach((toolCall) => addContext(toolCall)); message.toolCalls?.forEach((toolCall) => {
const normalized = normalizeToolCall(toolCall);
if (normalized) {
addContext(normalized);
}
});
if (!Array.isArray(message.content)) continue; if (!Array.isArray(message.content)) continue;
message.content.forEach((block) => { message.content.forEach((block) => {
if (!isToolUseBlock(block)) return; if (!isToolUseBlock(block)) return;
@ -1375,7 +1533,12 @@ function collectToolUseCandidates(input: {
); );
}; };
input.message.toolCalls?.forEach((toolCall, index) => addTool(toolCall, 100 + index)); input.message.toolCalls?.forEach((toolCall, index) => {
const normalized = normalizeToolCall(toolCall);
if (normalized) {
addTool(normalized, 100 + index);
}
});
if (Array.isArray(input.message.content)) { if (Array.isArray(input.message.content)) {
input.message.content.forEach((block, index) => { input.message.content.forEach((block, index) => {
if (!isToolUseBlock(block)) return; if (!isToolUseBlock(block)) return;
@ -1527,6 +1690,10 @@ export function extractMemberLogPreviewItems(
if (role === 'assistant') { if (role === 'assistant') {
const textPreview = extractTextPreview(message.content, textLimit); const textPreview = extractTextPreview(message.content, textLimit);
if (textPreview) { if (textPreview) {
const runtimeErrorPreview = formatRuntimeErrorText(
textFromPreviewContent(message.content),
textLimit
);
candidates.push( candidates.push(
buildCandidate({ buildCandidate({
provider: input.provider, provider: input.provider,
@ -1535,14 +1702,14 @@ export function extractMemberLogPreviewItems(
messageIndex, messageIndex,
blockIndex: 10, blockIndex: 10,
kind: 'text', kind: 'text',
title: 'Assistant', title: runtimeErrorPreview?.title ?? 'Assistant',
preview: textPreview.preview, preview: runtimeErrorPreview?.preview ?? textPreview.preview,
tone: 'neutral', tone: runtimeErrorPreview ? 'error' : 'neutral',
sourceLabel: input.sourceLabel, sourceLabel: input.sourceLabel,
sessionId: input.sessionId ?? message.sessionId, sessionId: input.sessionId ?? message.sessionId,
laneId: input.laneId, laneId: input.laneId,
token: 'assistant-text', token: 'assistant-text',
textTruncated: textPreview.truncated, textTruncated: runtimeErrorPreview?.truncated ?? textPreview.truncated,
}) })
); );
} }

View file

@ -1,4 +1,5 @@
import { decideMemberWorkSyncStatus } from '../domain'; import { decideMemberWorkSyncStatus } from '../domain';
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler'; import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts'; import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';

View file

@ -1,7 +1,8 @@
import { decideMemberWorkSyncStatus } from '../domain';
import { appendMemberWorkSyncAudit, reasonToAuditEvent } from './MemberWorkSyncAudit'; import { appendMemberWorkSyncAudit, reasonToAuditEvent } from './MemberWorkSyncAudit';
import { decideMemberWorkSyncNudgeActivation } from './MemberWorkSyncNudgeActivationPolicy'; import { decideMemberWorkSyncNudgeActivation } from './MemberWorkSyncNudgeActivationPolicy';
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler'; import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
import { decideMemberWorkSyncStatus } from '../domain';
import type { MemberWorkSyncOutboxItem, MemberWorkSyncStatus } from '../../contracts'; import type { MemberWorkSyncOutboxItem, MemberWorkSyncStatus } from '../../contracts';
import type { MemberWorkSyncAuditEventName, MemberWorkSyncUseCaseDeps } from './ports'; import type { MemberWorkSyncAuditEventName, MemberWorkSyncUseCaseDeps } from './ports';

View file

@ -12,8 +12,8 @@ import {
RuntimeTurnSettledIngestor, RuntimeTurnSettledIngestor,
type RuntimeTurnSettledTargetResolverPort, type RuntimeTurnSettledTargetResolverPort,
} from '../../core/application'; } from '../../core/application';
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
import { MemberWorkSyncTaskImpactResolver } from '../adapters/input/MemberWorkSyncTaskImpactResolver'; import { MemberWorkSyncTaskImpactResolver } from '../adapters/input/MemberWorkSyncTaskImpactResolver';
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink'; import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
import { TeamRuntimeTurnSettledTargetResolver } from '../adapters/output/TeamRuntimeTurnSettledTargetResolver'; import { TeamRuntimeTurnSettledTargetResolver } from '../adapters/output/TeamRuntimeTurnSettledTargetResolver';
import { TeamTaskAgendaSource } from '../adapters/output/TeamTaskAgendaSource'; import { TeamTaskAgendaSource } from '../adapters/output/TeamTaskAgendaSource';

View file

@ -1,8 +1,6 @@
import { createReadStream } from 'node:fs';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import readline from 'node:readline';
import { normalizeIdentityPath } from '@features/recent-projects/main/infrastructure/identity/normalizeIdentityPath'; import { normalizeIdentityPath } from '@features/recent-projects/main/infrastructure/identity/normalizeIdentityPath';
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath'; import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
@ -18,8 +16,9 @@ import type { ServiceContext } from '@main/services';
const CODEX_SESSION_FILE_PARSE_LIMIT = 500; const CODEX_SESSION_FILE_PARSE_LIMIT = 500;
const CODEX_PROJECT_CANDIDATE_LIMIT = 40; const CODEX_PROJECT_CANDIDATE_LIMIT = 40;
const CODEX_SESSION_FILE_SOURCE_TIMEOUT_MS = 3_500; const CODEX_SESSION_FILE_SOURCE_TIMEOUT_MS = 8_000;
const CODEX_SESSION_FILE_READ_BATCH_SIZE = 24; const CODEX_SESSION_FILE_READ_BATCH_SIZE = 24;
const CODEX_SESSION_METADATA_READ_LIMIT_BYTES = 128 * 1024;
interface CodexSessionFileEntry { interface CodexSessionFileEntry {
filePath: string; filePath: string;
@ -45,6 +44,14 @@ interface CodexSessionProjectSnapshot {
branchName?: string; branchName?: string;
} }
interface CodexSessionMetadata {
cwd: string;
source: unknown;
payloadTimestamp?: unknown;
eventTimestamp?: unknown;
branchName?: string;
}
function isInteractiveSource(source: unknown): boolean { function isInteractiveSource(source: unknown): boolean {
return source === 'vscode' || source === 'cli'; return source === 'vscode' || source === 'cli';
} }
@ -66,23 +73,47 @@ function getCodexHome(codexHome?: string): string {
return codexHome?.trim() || process.env.CODEX_HOME?.trim() || path.join(os.homedir(), '.codex'); return codexHome?.trim() || process.env.CODEX_HOME?.trim() || path.join(os.homedir(), '.codex');
} }
async function readFirstLine(filePath: string): Promise<string | null> { function extractJsonStringField(input: string, fieldName: string): string {
const stream = createReadStream(filePath, { encoding: 'utf8' }); const pattern = new RegExp(`"${fieldName}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`);
const lines = readline.createInterface({ const match = pattern.exec(input);
input: stream, if (!match) return '';
crlfDelay: Infinity,
});
try { try {
for await (const line of lines) { return JSON.parse(`"${match[1]}"`) as string;
return line; } catch {
} return '';
return null; }
}
function parseSessionMetadataPrefix(firstLine: string): CodexSessionMetadata | null {
const cwd = extractJsonStringField(firstLine, 'cwd').trim();
const source = extractJsonStringField(firstLine, 'source').trim();
if (!cwd || !source) return null;
return {
cwd,
source,
payloadTimestamp: extractJsonStringField(firstLine, 'timestamp'),
eventTimestamp: extractJsonStringField(firstLine, 'timestamp'),
branchName: extractJsonStringField(firstLine, 'branch').trim() || undefined,
};
}
async function readFirstLine(filePath: string): Promise<string | null> {
let handle: fs.FileHandle | null = null;
try {
handle = await fs.open(filePath, 'r');
const buffer = Buffer.allocUnsafe(CODEX_SESSION_METADATA_READ_LIMIT_BYTES);
const result = await handle.read(buffer, 0, buffer.length, 0);
if (result.bytesRead <= 0) return null;
const newlineIndex = buffer.subarray(0, result.bytesRead).indexOf(0x0a);
const endIndex = newlineIndex >= 0 ? newlineIndex : result.bytesRead;
return buffer.toString('utf8', 0, endIndex);
} catch { } catch {
return null; return null;
} finally { } finally {
lines.close(); await handle?.close().catch(() => undefined);
stream.destroy();
} }
} }
@ -130,28 +161,36 @@ function parseSessionSnapshot(
firstLine: string, firstLine: string,
mtimeMs: number mtimeMs: number
): CodexSessionProjectSnapshot | null { ): CodexSessionProjectSnapshot | null {
let event: CodexSessionEvent; let metadata: CodexSessionMetadata | null = null;
try { try {
event = JSON.parse(firstLine) as CodexSessionEvent; const event = JSON.parse(firstLine) as CodexSessionEvent;
metadata = {
cwd: typeof event.payload?.cwd === 'string' ? event.payload.cwd.trim() : '',
source: event.payload?.source,
payloadTimestamp: event.payload?.timestamp,
eventTimestamp: event.timestamp,
branchName:
typeof event.payload?.git?.branch === 'string' ? event.payload.git.branch.trim() : '',
};
} catch { } catch {
return null; metadata = parseSessionMetadataPrefix(firstLine);
} }
const cwd = typeof event.payload?.cwd === 'string' ? event.payload.cwd.trim() : ''; const cwd = metadata?.cwd ?? '';
if (!cwd || !isInteractiveSource(event.payload?.source) || isEphemeralProjectPath(cwd)) { if (!metadata || !cwd || !isInteractiveSource(metadata.source) || isEphemeralProjectPath(cwd)) {
return null; return null;
} }
const timestamp = const timestamp =
mtimeMs || normalizeTimestamp(event.payload?.timestamp) || normalizeTimestamp(event.timestamp); mtimeMs ||
const branchName = normalizeTimestamp(metadata.payloadTimestamp) ||
typeof event.payload?.git?.branch === 'string' ? event.payload.git.branch.trim() : ''; normalizeTimestamp(metadata.eventTimestamp);
return { return {
cwd, cwd,
source: event.payload?.source, source: metadata.source,
lastActivityAt: timestamp, lastActivityAt: timestamp,
branchName: branchName || undefined, branchName: metadata.branchName || undefined,
}; };
} }

View file

@ -356,10 +356,7 @@ export class TeamConfigReader {
} }
const existingRequest = TeamConfigReader.listTeamsInFlightByBasePath.get(teamsBasePath); const existingRequest = TeamConfigReader.listTeamsInFlightByBasePath.get(teamsBasePath);
if ( if (existingRequest?.generationAtStart === TeamConfigReader.listTeamsGeneration) {
existingRequest &&
existingRequest.generationAtStart === TeamConfigReader.listTeamsGeneration
) {
return cloneTeamSummaries(await existingRequest.promise); return cloneTeamSummaries(await existingRequest.promise);
} }

View file

@ -58,11 +58,11 @@ import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamMessageFeedService } from './TeamMessageFeedService'; import { TeamMessageFeedService } from './TeamMessageFeedService';
import { TeamMetaStore } from './TeamMetaStore'; import { TeamMetaStore } from './TeamMetaStore';
import { TeamSentMessagesStore } from './TeamSentMessagesStore'; import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { getTeamTaskWorkflowColumn, selectCurrentActiveTeamTask } from './teamTaskActiveState';
import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal'; import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal';
import { TeamTaskReader } from './TeamTaskReader'; import { TeamTaskReader } from './TeamTaskReader';
import { TeamTaskWriter } from './TeamTaskWriter'; import { TeamTaskWriter } from './TeamTaskWriter';
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver'; import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
import { getTeamTaskWorkflowColumn, selectCurrentActiveTeamTask } from './teamTaskActiveState';
import type { PersistedTaskChangePresenceIndex } from './cache/taskChangePresenceCacheTypes'; import type { PersistedTaskChangePresenceIndex } from './cache/taskChangePresenceCacheTypes';
import type { TaskChangePresenceRepository } from './cache/TaskChangePresenceRepository'; import type { TaskChangePresenceRepository } from './cache/TaskChangePresenceRepository';

View file

@ -1,9 +1,7 @@
import { createLogger } from '@shared/utils/logger';
import { getTeamsBasePath } from '@main/utils/pathDecoder'; import { getTeamsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import { TeamInboxReader } from './TeamInboxReader';
import { TeamMemberLogsFinder } from './TeamMemberLogsFinder';
import { import {
createOpenCodePromptDeliveryLedgerStore, createOpenCodePromptDeliveryLedgerStore,
type OpenCodePromptDeliveryLedgerRecord, type OpenCodePromptDeliveryLedgerRecord,
@ -13,6 +11,8 @@ import {
getOpenCodeLaneScopedRuntimeFilePath, getOpenCodeLaneScopedRuntimeFilePath,
readOpenCodeRuntimeLaneIndex, readOpenCodeRuntimeLaneIndex,
} from './opencode/store/OpenCodeRuntimeManifestEvidenceReader'; } from './opencode/store/OpenCodeRuntimeManifestEvidenceReader';
import { TeamInboxReader } from './TeamInboxReader';
import { TeamMemberLogsFinder } from './TeamMemberLogsFinder';
import type { MemberLogSummary, MemberRuntimeAdvisory, ResolvedTeamMember } from '@shared/types'; import type { MemberLogSummary, MemberRuntimeAdvisory, ResolvedTeamMember } from '@shared/types';

View file

@ -451,7 +451,7 @@ export class TeamMessageFeedService {
const existingRequest = this.inFlightByTeam.get(teamName); const existingRequest = this.inFlightByTeam.get(teamName);
const generationAtStart = this.getGeneration(teamName); const generationAtStart = this.getGeneration(teamName);
if (existingRequest && existingRequest.generationAtStart === generationAtStart) { if (existingRequest?.generationAtStart === generationAtStart) {
return existingRequest.promise; return existingRequest.promise;
} }

View file

@ -84,12 +84,6 @@ import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection'; import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
getTeamTaskWorkflowColumn,
isTeamTaskActivelyWorked,
isTeamTaskDeleted,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { import {
isTeamInternalControlMessageText, isTeamInternalControlMessageText,
stripExactInternalControlEchoPrefix, stripExactInternalControlEchoPrefix,
@ -104,6 +98,12 @@ import {
inferTeamProviderIdFromModel, inferTeamProviderIdFromModel,
normalizeOptionalTeamProviderId, normalizeOptionalTeamProviderId,
} from '@shared/utils/teamProvider'; } from '@shared/utils/teamProvider';
import {
getTeamTaskWorkflowColumn,
isTeamTaskActivelyWorked,
isTeamTaskDeleted,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { import {
extractToolPreview, extractToolPreview,
extractToolResultPreview, extractToolResultPreview,
@ -169,10 +169,6 @@ import {
type OpenCodePromptDeliveryLedgerStore, type OpenCodePromptDeliveryLedgerStore,
type OpenCodePromptDeliveryStatus, type OpenCodePromptDeliveryStatus,
} from './opencode/delivery/OpenCodePromptDeliveryLedger'; } from './opencode/delivery/OpenCodePromptDeliveryLedger';
import {
isActionRequiredOpenCodeRuntimeDeliveryReason,
selectOpenCodeRuntimeDeliveryReason,
} from './opencode/delivery/OpenCodeRuntimeDeliveryDiagnostics';
import { import {
decideOpenCodePromptDeliveryRepair, decideOpenCodePromptDeliveryRepair,
type OpenCodePromptDeliveryHardFailureKind, type OpenCodePromptDeliveryHardFailureKind,
@ -189,6 +185,10 @@ import {
OPENCODE_PROMPT_WATCHDOG_PER_TEAM_CONCURRENCY, OPENCODE_PROMPT_WATCHDOG_PER_TEAM_CONCURRENCY,
type OpenCodeVisibleReplyProof, type OpenCodeVisibleReplyProof,
} from './opencode/delivery/OpenCodePromptDeliveryWatchdog'; } from './opencode/delivery/OpenCodePromptDeliveryWatchdog';
import {
isActionRequiredOpenCodeRuntimeDeliveryReason,
selectOpenCodeRuntimeDeliveryReason,
} from './opencode/delivery/OpenCodeRuntimeDeliveryDiagnostics';
import { createRuntimeDeliveryJournalStore } from './opencode/delivery/RuntimeDeliveryJournal'; import { createRuntimeDeliveryJournalStore } from './opencode/delivery/RuntimeDeliveryJournal';
import { import {
type RuntimeDeliveryDestinationPort, type RuntimeDeliveryDestinationPort,

View file

@ -468,10 +468,7 @@ export class TeamTaskReader {
return cloned; return cloned;
} }
if ( if (TeamTaskReader.allTasksInFlight?.generationAtStart === TeamTaskReader.allTasksGeneration) {
TeamTaskReader.allTasksInFlight &&
TeamTaskReader.allTasksInFlight.generationAtStart === TeamTaskReader.allTasksGeneration
) {
const waitedAt = Date.now(); const waitedAt = Date.now();
const tasks = await TeamTaskReader.allTasksInFlight.promise; const tasks = await TeamTaskReader.allTasksInFlight.promise;
const cloned = cloneTasks(tasks); const cloned = cloneTasks(tasks);

View file

@ -1,12 +1,13 @@
import {
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
} from './OpenCodeBridgeCommandContract';
import type { import type {
OpenCodeBridgeCommandName, OpenCodeBridgeCommandName,
OpenCodeBridgeHandshake, OpenCodeBridgeHandshake,
OpenCodeBridgePeerIdentity, OpenCodeBridgePeerIdentity,
} from './OpenCodeBridgeCommandContract'; } from './OpenCodeBridgeCommandContract';
import {
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
} from './OpenCodeBridgeCommandContract';
import type { import type {
OpenCodeBridgeCommandExecutor, OpenCodeBridgeCommandExecutor,
OpenCodeBridgeHandshakePort, OpenCodeBridgeHandshakePort,

View file

@ -15,13 +15,13 @@ import {
validateRuntimeStoreManifest, validateRuntimeStoreManifest,
} from './RuntimeStoreManifest'; } from './RuntimeStoreManifest';
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
import type { import type {
OpenCodeAppManagedBootstrapCandidate, OpenCodeAppManagedBootstrapCandidate,
OpenCodeBootstrapEvidenceSource, OpenCodeBootstrapEvidenceSource,
} from '@shared/types/team'; } from '@shared/types/team';
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
const logger = createLogger('OpenCodeRuntimeManifestEvidenceReader'); const logger = createLogger('OpenCodeRuntimeManifestEvidenceReader');

View file

@ -596,7 +596,7 @@ function normalizeAppManagedBootstrapCandidate(
runtimeSessionId?: string; runtimeSessionId?: string;
} }
): OpenCodeAppManagedBootstrapCandidate | undefined { ): OpenCodeAppManagedBootstrapCandidate | undefined {
if (!value || value.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') { if (value?.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') {
return undefined; return undefined;
} }
if ( if (

View file

@ -9,8 +9,8 @@ import { TeamTranscriptSourceLocator } from '../taskLogs/discovery/TeamTranscrip
import { isBoardTaskExactLogsReadEnabled } from '../taskLogs/exact/featureGates'; import { isBoardTaskExactLogsReadEnabled } from '../taskLogs/exact/featureGates';
import { TeamKanbanManager } from '../TeamKanbanManager'; import { TeamKanbanManager } from '../TeamKanbanManager';
import { TeamMembersMetaStore } from '../TeamMembersMetaStore'; import { TeamMembersMetaStore } from '../TeamMembersMetaStore';
import { TeamTaskReader } from '../TeamTaskReader';
import { getTeamTaskWorkflowColumn, isTeamTaskActivelyWorked } from '../teamTaskActiveState'; import { getTeamTaskWorkflowColumn, isTeamTaskActivelyWorked } from '../teamTaskActiveState';
import { TeamTaskReader } from '../TeamTaskReader';
import { BoardTaskActivityBatchIndexer } from './BoardTaskActivityBatchIndexer'; import { BoardTaskActivityBatchIndexer } from './BoardTaskActivityBatchIndexer';
import { OpenCodeTaskStallEvidenceSource } from './OpenCodeTaskStallEvidenceSource'; import { OpenCodeTaskStallEvidenceSource } from './OpenCodeTaskStallEvidenceSource';

View file

@ -44,12 +44,12 @@ export class BoardTaskActivityRecordSource {
private async getTaskActivityIndex(teamName: string): Promise<TaskActivityIndex> { private async getTaskActivityIndex(teamName: string): Promise<TaskActivityIndex> {
const generation = this.getTranscriptDiscoveryGeneration(teamName); const generation = this.getTranscriptDiscoveryGeneration(teamName);
const cached = this.indexCache.get(teamName); const cached = this.indexCache.get(teamName);
if (cached && cached.generation === generation && cached.expiresAt > Date.now()) { if (cached?.generation === generation && cached.expiresAt > Date.now()) {
return cached; return cached;
} }
const existingInFlight = this.indexInFlight.get(teamName); const existingInFlight = this.indexInFlight.get(teamName);
if (existingInFlight && existingInFlight.generation === generation) { if (existingInFlight?.generation === generation) {
return await existingInFlight.promise; return await existingInFlight.promise;
} }

View file

@ -95,12 +95,12 @@ export class TeamTranscriptSourceLocator {
const generation = this.getGeneration(teamName); const generation = this.getGeneration(teamName);
const cached = this.contextCache.get(teamName); const cached = this.contextCache.get(teamName);
if (cached && cached.generation === generation && cached.expiresAt > Date.now()) { if (cached?.generation === generation && cached.expiresAt > Date.now()) {
return cached.value; return cached.value;
} }
const inFlight = this.contextInFlight.get(teamName); const inFlight = this.contextInFlight.get(teamName);
if (inFlight && inFlight.generation === generation) { if (inFlight?.generation === generation) {
return await inFlight.promise; return await inFlight.promise;
} }

View file

@ -214,7 +214,7 @@ export class CodexNativeTraceProjector {
>(); >();
for (const event of run.events) { for (const event of run.events) {
const projection = event.projection; const projection = event.projection;
if (!projection || projection.toolSource !== 'native') { if (projection?.toolSource !== 'native') {
continue; continue;
} }
if (!projection.itemId) { if (!projection.itemId) {

View file

@ -4,8 +4,8 @@ import type { TeamTaskWithKanban } from '@shared/types';
export { export {
getTeamTaskWorkflowColumn, getTeamTaskWorkflowColumn,
isTeamTaskFinalForCompletionNotification,
isTeamTaskActivelyWorked, isTeamTaskActivelyWorked,
isTeamTaskFinalForCompletionNotification,
isTeamTaskFinishedForDependency, isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable, isTeamTaskNeedsFixActionable,
isTeamTaskTerminalForActionableWork, isTeamTaskTerminalForActionableWork,

View file

@ -19,6 +19,7 @@ import {
import { api, isElectronMode } from '@renderer/api'; import { api, isElectronMode } from '@renderer/api';
import { confirm } from '@renderer/components/common/ConfirmDialog'; import { confirm } from '@renderer/components/common/ConfirmDialog';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
import { import {
formatProviderStatusText, formatProviderStatusText,
getProviderConnectionModeSummary, getProviderConnectionModeSummary,
@ -29,7 +30,6 @@ import {
isConnectionManagedRuntimeProvider, isConnectionManagedRuntimeProvider,
shouldShowProviderConnectAction, shouldShowProviderConnectAction,
} from '@renderer/components/runtime/providerConnectionUi'; } from '@renderer/components/runtime/providerConnectionUi';
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges'; import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector'; import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog'; import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
@ -373,21 +373,6 @@ const ProviderDetailSkeleton = (): React.JSX.Element => {
); );
}; };
const OpenCodeBetaBadge = (): React.JSX.Element => {
return (
<span
className="inline-flex h-4 shrink-0 items-center rounded border px-1.5 text-[9px] font-semibold uppercase leading-none"
style={{
borderColor: 'rgba(251, 191, 36, 0.32)',
backgroundColor: 'rgba(251, 191, 36, 0.12)',
color: '#fbbf24',
}}
>
beta
</span>
);
};
function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean { function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean {
return ( return (
providerLoading || providerLoading ||
@ -781,7 +766,6 @@ const InstalledBanner = ({
? getProviderLabel(provider.providerId) ? getProviderLabel(provider.providerId)
: provider.displayName} : provider.displayName}
</span> </span>
{provider.providerId === 'opencode' ? <OpenCodeBetaBadge /> : null}
</span> </span>
<span <span
className="text-xs" className="text-xs"

View file

@ -8,11 +8,11 @@ interface CodexLoginLinkCopyButtonProps {
size?: 'xs' | 'sm'; size?: 'xs' | 'sm';
} }
export function CodexLoginLinkCopyButton({ export const CodexLoginLinkCopyButton = ({
authUrl, authUrl,
disabled = false, disabled = false,
size = 'sm', size = 'sm',
}: CodexLoginLinkCopyButtonProps): React.JSX.Element | null { }: CodexLoginLinkCopyButtonProps): React.JSX.Element | null => {
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle'); const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle');
useEffect(() => { useEffect(() => {
@ -53,4 +53,4 @@ export function CodexLoginLinkCopyButton({
{copyState === 'copied' ? 'Copied' : copyState === 'failed' ? 'Copy failed' : 'Copy link'} {copyState === 'copied' ? 'Copied' : copyState === 'failed' ? 'Copy failed' : 'Copy link'}
</button> </button>
); );
} };

View file

@ -20,8 +20,9 @@ import {
resolveCodexRuntimeSelection, resolveCodexRuntimeSelection,
} from '@features/codex-runtime-profile/renderer'; } from '@features/codex-runtime-profile/renderer';
import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer'; import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
import { api } from '@renderer/api'; import { api } from '@renderer/api';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { import {
Dialog, Dialog,
@ -40,7 +41,6 @@ import {
SelectValue, SelectValue,
} from '@renderer/components/ui/select'; } from '@renderer/components/ui/select';
import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { AlertTriangle, Key, Link2, Loader2, Trash2 } from 'lucide-react'; import { AlertTriangle, Key, Link2, Loader2, Trash2 } from 'lucide-react';

View file

@ -7,11 +7,11 @@ import { useStore } from '@renderer/store';
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers';
import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils'; import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils';
import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity';
import { import {
getTeamTaskWorkflowColumn, getTeamTaskWorkflowColumn,
isTeamTaskNeedsFixActionable, isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState'; } from '@shared/utils/teamTaskState';
import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import type { TeamTaskWithKanban } from '@shared/types'; import type { TeamTaskWithKanban } from '@shared/types';

View file

@ -63,8 +63,8 @@ import {
getKnownSlashCommand, getKnownSlashCommand,
parseStandaloneSlashCommand, parseStandaloneSlashCommand,
} from '@shared/utils/slashCommands'; } from '@shared/utils/slashCommands';
import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages';
import { import {
AlertTriangle, AlertTriangle,
Check, Check,

View file

@ -59,7 +59,7 @@ export function shouldShowCodexReconnectPrompt({
); );
} }
export function CodexReconnectPrompt({ export const CodexReconnectPrompt = ({
authUrl, authUrl,
reconnectBusy, reconnectBusy,
onReconnect, onReconnect,
@ -67,7 +67,7 @@ export function CodexReconnectPrompt({
authUrl: string | null; authUrl: string | null;
reconnectBusy: boolean; reconnectBusy: boolean;
onReconnect: () => void; onReconnect: () => void;
}): React.JSX.Element { }): React.JSX.Element => {
return ( return (
<div <div
className="mt-2 rounded-md border px-2.5 py-2" className="mt-2 rounded-md border px-2.5 py-2"
@ -98,4 +98,4 @@ export function CodexReconnectPrompt({
</div> </div>
</div> </div>
); );
} };

View file

@ -28,8 +28,8 @@ import {
extractTaskRefsFromText, extractTaskRefsFromText,
stripEncodedTaskReferenceMetadata, stripEncodedTaskReferenceMetadata,
} from '@renderer/utils/taskReferenceUtils'; } from '@renderer/utils/taskReferenceUtils';
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
import { AlertTriangle, ChevronDown, ChevronRight, Search } from 'lucide-react'; import { AlertTriangle, ChevronDown, ChevronRight, Search } from 'lucide-react';
import type { InlineChip } from '@renderer/types/inlineChip'; import type { InlineChip } from '@renderer/types/inlineChip';

View file

@ -88,15 +88,15 @@ import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
import { AdvancedCliSection } from './AdvancedCliSection'; import { AdvancedCliSection } from './AdvancedCliSection';
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector'; import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import { CodexFastModeSelector } from './CodexFastModeSelector'; import { CodexFastModeSelector } from './CodexFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import { import {
clearInheritedMemberModelsUnavailableForProvider, clearInheritedMemberModelsUnavailableForProvider,
resolveProviderScopedMemberModel, resolveProviderScopedMemberModel,
} from './memberModelScope'; } from './memberModelScope';
import { OptionalSettingsSection } from './OptionalSettingsSection'; import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector';
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects'; import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
import { ProjectPathSelector } from './ProjectPathSelector';
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey'; import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
import { import {
buildReusableProviderPrepareModelResults, buildReusableProviderPrepareModelResults,

View file

@ -91,8 +91,8 @@ import { CronScheduleInput } from '../schedule/CronScheduleInput';
import { AdvancedCliSection } from './AdvancedCliSection'; import { AdvancedCliSection } from './AdvancedCliSection';
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector'; import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import { CodexFastModeSelector } from './CodexFastModeSelector'; import { CodexFastModeSelector } from './CodexFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import { EffortLevelSelector } from './EffortLevelSelector'; import { EffortLevelSelector } from './EffortLevelSelector';
import { resolveLaunchDialogPrefill } from './launchDialogPrefill'; import { resolveLaunchDialogPrefill } from './launchDialogPrefill';
import { import {
@ -100,8 +100,8 @@ import {
resolveProviderScopedMemberModel, resolveProviderScopedMemberModel,
} from './memberModelScope'; } from './memberModelScope';
import { OptionalSettingsSection } from './OptionalSettingsSection'; import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector';
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects'; import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
import { ProjectPathSelector } from './ProjectPathSelector';
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey'; import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
import { import {
buildReusableProviderPrepareModelResults, buildReusableProviderPrepareModelResults,

View file

@ -66,11 +66,11 @@ function getSourceLabel(source: DashboardRecentProjectSource): string {
} }
} }
function ProjectSourceBadge({ const ProjectSourceBadge = ({
source, source,
}: { }: {
source?: DashboardRecentProjectSource; source?: DashboardRecentProjectSource;
}): React.JSX.Element | null { }): React.JSX.Element | null => {
if (!source) { if (!source) {
return null; return null;
} }
@ -92,7 +92,7 @@ function ProjectSourceBadge({
))} ))}
</span> </span>
); );
} };
export type CwdMode = 'project' | 'custom'; export type CwdMode = 'project' | 'custom';

View file

@ -54,16 +54,16 @@ import {
} from '@renderer/utils/taskChangeRequest'; } from '@renderer/utils/taskChangeRequest';
import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils'; import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import {
getTeamTaskWorkflowColumn,
isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { import {
deriveTaskDisplayId, deriveTaskDisplayId,
formatTaskDisplayLabel, formatTaskDisplayLabel,
taskMatchesRef, taskMatchesRef,
} from '@shared/utils/taskIdentity'; } from '@shared/utils/taskIdentity';
import {
getTeamTaskWorkflowColumn,
isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { format, formatDistanceToNow } from 'date-fns'; import { format, formatDistanceToNow } from 'date-fns';
import { import {
AlignLeft, AlignLeft,

View file

@ -1,8 +1,8 @@
import { normalizePath } from '@renderer/utils/pathNormalize'; import { normalizePath } from '@renderer/utils/pathNormalize';
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath'; import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
import type { ComboboxOption } from '@renderer/components/ui/combobox';
import type { DashboardRecentProjectSource } from '@features/recent-projects/contracts'; import type { DashboardRecentProjectSource } from '@features/recent-projects/contracts';
import type { ComboboxOption } from '@renderer/components/ui/combobox';
import type { Project } from '@shared/types'; import type { Project } from '@shared/types';
export interface ProjectPathProject extends Project { export interface ProjectPathProject extends Project {

View file

@ -13,11 +13,11 @@ import {
buildTaskChangeRequestOptions, buildTaskChangeRequestOptions,
canDisplayTaskChangesForOptions, canDisplayTaskChangesForOptions,
} from '@renderer/utils/taskChangeRequest'; } from '@renderer/utils/taskChangeRequest';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { import {
isTeamTaskFinishedForDependency, isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable, isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState'; } from '@shared/utils/teamTaskState';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { import {
ArrowLeftFromLine, ArrowLeftFromLine,
ArrowRightFromLine, ArrowRightFromLine,

View file

@ -7,11 +7,11 @@ import {
TASK_STATUS_LABELS, TASK_STATUS_LABELS,
TASK_STATUS_STYLES, TASK_STATUS_STYLES,
} from '@renderer/utils/memberHelpers'; } from '@renderer/utils/memberHelpers';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { import {
getTeamTaskWorkflowColumn, getTeamTaskWorkflowColumn,
isTeamTaskNeedsFixActionable, isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState'; } from '@shared/utils/teamTaskState';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import type { TeamTaskWithKanban } from '@shared/types'; import type { TeamTaskWithKanban } from '@shared/types';

View file

@ -5,11 +5,11 @@ import {
REVIEW_STATE_DISPLAY, REVIEW_STATE_DISPLAY,
TASK_STATUS_LABELS, TASK_STATUS_LABELS,
} from '@renderer/utils/memberHelpers'; } from '@renderer/utils/memberHelpers';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { import {
getTeamTaskWorkflowColumn, getTeamTaskWorkflowColumn,
isTeamTaskNeedsFixActionable, isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState'; } from '@shared/utils/teamTaskState';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import type { TeamTaskWithKanban } from '@shared/types'; import type { TeamTaskWithKanban } from '@shared/types';

View file

@ -779,7 +779,7 @@ function isQueuedOpenCodeLaunch(
// Only label lanes as queued before runtime evidence appears. Once the // Only label lanes as queued before runtime evidence appears. Once the
// backend has any liveness signal, show the exact runtime state instead. // backend has any liveness signal, show the exact runtime state instead.
return runtimeEntry == null || runtimeEntry.livenessKind == null; return runtimeEntry?.livenessKind == null;
} }
function hasElapsedSinceIso( function hasElapsedSinceIso(

View file

@ -1133,8 +1133,8 @@ export interface RetryFailedOpenCodeSecondaryLanesResult {
attempted: string[]; attempted: string[];
confirmed: string[]; confirmed: string[];
pending: string[]; pending: string[];
failed: Array<{ memberName: string; error: string }>; failed: { memberName: string; error: string }[];
skipped: Array<{ memberName: string; reason: string }>; skipped: { memberName: string; reason: string }[];
} }
export type MemberSpawnLivenessSource = 'heartbeat' | 'process'; export type MemberSpawnLivenessSource = 'heartbeat' | 'process';

View file

@ -28,6 +28,7 @@ async function writeRollout(
source?: string; source?: string;
timestamp?: string; timestamp?: string;
branch?: string; branch?: string;
metadataPadding?: string;
}, },
mtime: Date mtime: Date
): Promise<void> { ): Promise<void> {
@ -43,6 +44,9 @@ async function writeRollout(
cwd: payload.cwd, cwd: payload.cwd,
source: payload.source ?? 'cli', source: payload.source ?? 'cli',
git: payload.branch ? { branch: payload.branch } : undefined, git: payload.branch ? { branch: payload.branch } : undefined,
...(payload.metadataPadding
? { base_instructions: { text: payload.metadataPadding } }
: {}),
}, },
})}\n${'x'.repeat(1024)}`, })}\n${'x'.repeat(1024)}`,
'utf8' 'utf8'
@ -110,6 +114,40 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => {
expect(identityResolver.resolve).toHaveBeenCalledWith('/Users/test/projects/alpha'); expect(identityResolver.resolve).toHaveBeenCalledWith('/Users/test/projects/alpha');
}); });
it('loads Codex projects from large session metadata lines without parsing the full line', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const updatedAt = new Date('2026-04-14T12:00:00.000Z');
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-large.jsonl'),
{
cwd: '/Users/test/projects/large',
metadataPadding: 'x'.repeat(160_000),
},
updatedAt
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
});
const result = await adapter.list();
expect(result.candidates).toEqual([
expect.objectContaining({
primaryPath: '/Users/test/projects/large',
sourceKind: 'codex',
}),
]);
});
it('deduplicates sessions by cwd and keeps the newest activity', async () => { it('deduplicates sessions by cwd and keeps the newest activity', async () => {
const codexHome = path.join(tempDir, '.codex'); const codexHome = path.join(tempDir, '.codex');
const logger = createLogger(); const logger = createLogger();

View file

@ -164,14 +164,16 @@ describe('GraphMemberLogPreviewHud', () => {
expect(row).not.toBeUndefined(); expect(row).not.toBeUndefined();
expect(row?.querySelector('.float-left')).not.toBeNull(); expect(row?.querySelector('.float-left')).not.toBeNull();
expect(row?.querySelector('.line-clamp-3')).toBeNull(); expect(row?.querySelector('.line-clamp-3')).toBeNull();
expect(row?.className).toContain('h-16'); expect(row?.className).toContain('h-[68px]');
expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-4'); expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-[18px]');
expect(row?.textContent).toContain('pnpm test'); expect(row?.textContent).toContain('pnpm test');
const errorRow = Array.from(host.querySelectorAll('button')).find((button) => const errorRow = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('OpenCode tool failed') button.textContent?.includes('OpenCode tool failed')
); );
expect(errorRow?.className).toContain('border-rose-400/35');
expect(errorRow?.querySelector('svg.text-rose-300')).not.toBeNull(); expect(errorRow?.querySelector('svg.text-rose-300')).not.toBeNull();
expect(errorRow?.querySelector('.text-rose-100')).not.toBeNull();
const resultRow = Array.from(host.querySelectorAll('button')).find((button) => const resultRow = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Tests passed') button.textContent?.includes('Tests passed')
@ -203,6 +205,73 @@ describe('GraphMemberLogPreviewHud', () => {
}); });
}); });
it('caps long visible rows while preserving the full preview in the title', async () => {
const node: GraphNode = {
id: 'member:alpha-team:alice',
kind: 'member',
label: 'alice',
state: 'active',
domainRef: { kind: 'member', teamName: 'alpha-team', memberName: 'alice' },
};
const longPreview =
'to team-lead inbox - #68a3a8cc blocked by dependencies and needs a follow-up investigation before merge final-token';
const alicePreview = basePreviewsByMember.get('alice')!;
mockedPreviewsByMember = new Map(basePreviewsByMember);
mockedPreviewsByMember.set('alice', {
...alicePreview,
items: [
{
id: 'preview-long',
kind: 'tool_use' as const,
provider: 'claude_transcript' as const,
timestamp: '2026-04-03T00:00:00.000Z',
title: 'Send message',
preview: longPreview,
tone: 'warning' as const,
},
],
overflowCount: 0,
});
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
<GraphMemberLogPreviewHud
teamName="alpha-team"
nodes={[node]}
getLogWorldRect={() => ({
left: 40,
top: 80,
right: 300,
bottom: 372,
width: 260,
height: 292,
})}
getCameraZoom={() => 1}
worldToScreen={(x, y) => ({ x, y })}
getViewportSize={() => ({ width: 1200, height: 800 })}
focusNodeIds={null}
/>
);
await Promise.resolve();
});
const row = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Send message')
);
expect(row?.textContent).toContain('...');
expect(row?.textContent).not.toContain('final-token');
expect(row?.getAttribute('title')).toContain('final-token');
act(() => {
root.unmount();
});
});
it('briefly highlights a newly appeared preview row', async () => { it('briefly highlights a newly appeared preview row', async () => {
const node: GraphNode = { const node: GraphNode = {
id: 'member:alpha-team:alice', id: 'member:alpha-team:alice',