95 KiB
Member Log Stream V2 Research Addendum
Scope
Этот документ углубляет места с самой низкой уверенностью из основного плана:
- OpenCode lane/session resolution;
- Codex native member-wide feasibility;
- Claude/member transcript attribution;
- parser/cache safety;
- architecture placement;
- renderer performance budget;
- oversized message/content budget;
- IPC validation and abuse limits;
- IPC registration/composition safety;
- exact stream DTO/render contract;
- cumulative subagent snapshot dedupe;
- OpenCode projection mapper extraction;
- message/window truncation semantics;
- member popup live refresh event policy;
- API shape между renderer, preload и main.
Вывод после code research: вариант 2 остается правильным, но детали нужно сделать строже. Особенно важно не использовать findMemberLogPaths() как основной источник и не вызывать OpenCode transcript без laneId, если lane уже известен.
Executive Findings
| Зона | Было | Стало после research | Решение |
|---|---|---|---|
| Claude/member transcript | Уверенность высокая | Еще выше: есть готовый findRecentMemberLogFileRefsByMember() с mtime, sessionId и сортировкой |
Использовать его, не findMemberLogPaths() |
| OpenCode lane | Средняя уверенность | TeamMemberSnapshot уже несет providerBackendId/selectedFastMode/resolvedFastMode/laneId/laneKind/laneOwnerProviderId, renderer получает это через spread |
Типизировать ResolvedTeamMember, передавать laneId в getMemberLogStream и bridge |
| Codex native | Низкая уверенность | readTaskRuns() физически task-keyed, а projector keeps only native tool events |
First PR skipped; partial trace only as separate phase with honest label |
| Parser cache | Средняя уверенность | parseFiles() вызывает retainOnly(), а retainOnly() чистит и cache, и inFlight entries |
Держать отдельный parser instance для member stream |
| UI reuse | Средняя уверенность | TaskLogStreamSection содержит task-specific copy и loading logic |
Вынести generic view, оставить source-specific containers |
| Architecture placement | Уверенность высокая | Feature standard прямо подходит: cross-process feature, own policy, transport wiring, more than one adapter, provider roadmap | Делать src/features/member-log-stream canonical slice, with thin app-shell integration |
| Render pressure | Низкая уверенность | MemberExecutionLog рендерит все groups и держит их expanded by default |
Ограничивать backend response budget |
| IPC validation | Средняя уверенность | laneId содержит :, значит validateMemberName не подходит |
Добавить отдельную optional lane validator |
| Cumulative subagent logs | Низкая уверенность | findRecentMemberLogFileRefsByMember() dedupe только by filePath, а subagent snapshots могут быть cumulative |
Добавить/использовать ref metadata и dedupe by member/session/kind перед parse |
| OpenCode projection mapping | Средняя уверенность | Есть richer mapper в task source и lossy mapper в stall monitor | Вынести generic mapper из task source, не из stall monitor |
| Finder ref metadata | Средняя уверенность | Current refs не несут kind/sizeBytes, но full parse ради messageCount удвоит IO |
Добавлять lightweight metadata и не считать messageCount полным parse |
| Renderer extraction | Средняя уверенность | TaskLogStreamSection смешивает generic stream view и task-specific copy/reload |
Вынести ExecutionLogStreamView, оставить containers source-specific |
| Budget semantics | Низкая уверенность | maxChunks не ограничивает большой AI chunk с сотнями tool calls |
Добавить message budget и pair-aware trimming before chunk build |
| Content budget | Низкая уверенность | One huge tool result can render through DisplayItemList even with low message count |
Add content-char budgets before chunk build |
| Live refresh | Средняя уверенность | member stream не знает taskId, а task-log-change не несет memberName |
Reload on same-team log-source-change and task-log-change, debounced |
| IPC composition | Уверенность высокая | initializeTeamHandlers() positional deps make legacy team IPC a bad owner for this feature |
Register feature IPC through @features/member-log-stream/main, do not add service to team handlers |
| Browser fallback | Средняя уверенность | Browser API must satisfy feature API even without Electron IPC | Вернуть complete empty MemberLogStreamResponse, как task stream fallback |
| Historical members | Средняя уверенность | Finder attribution uses knownMembers from config/meta/inbox, not necessarily removed popup member |
Add requested names to attribution set inside recent-ref finder |
| Finder options compatibility | Средняя уверенность | findRecentMemberLogFileRefsByMember() уже используется positional mtimeSinceMs callers |
Add backward-compatible third-arg parser, not object-only signature |
| Lead transcript mtime window | Новая находка | mtimeSinceMs сейчас фильтрует candidates, но lead transcript добавляется до этого filter |
Apply mtime window consistently to lead and candidate refs |
| Segment metadata | Средняя уверенность | BoardTaskLogSegment has no provider/session label, so mixed sources become opaque |
Use MemberLogStreamSegment.source metadata without file paths |
| Segment render keys | Новая находка | Task stream key uses participantKey:firstChunkId, good for tail growth but risky across member sources |
Generic view gets caller-provided segment key builder |
| Renderer reload pressure | Новая находка | Task stream drops stale responses by request seq but still allows parallel IPC calls | Member stream should coalesce in-flight reloads |
| Live tracking activation | Новая находка | TaskLogStreamSection subscribes, but TaskLogsPanel enables TeamLogSourceTracker; member popup has no equivalent parent |
Add member stream tracking activation while popup section is mounted |
| IPC option strictness | Новая находка | TEAM_GET_DATA rejects unknown option keys before dispatch |
Reject unknown member-stream option keys too |
| Participant/source identity | Новая находка | BoardTaskLogParticipant is actor identity, while provider/session/lane is source identity |
Keep participant actor-based and render provider/session from segment.source |
| Since reload semantics | Новая находка | Renderer state replacement plus since-filtered partial response can hide older visible segments | First PR uses full bounded background reloads, not client-side incremental merge |
| Chunk date shape | Средняя уверенность | Renderer group transformer expects Date objects, while tests can use JSON-like fixtures | Keep task stream assumption, add shared normalizer only if needed |
| Parser in-flight ownership | Новая находка | Shared parser can delete another stream's active parse dedupe through retainOnly() |
Parser ownership is per stream service unless cache API is redesigned |
| OpenCode mapper source | Новая находка | Stall-monitor mapper drops non-string content blocks and toolUseResult |
Shared mapper source must be OpenCodeTaskLogStreamSource |
| Tracker activation | Новая находка | TeamLogSourceTracker uses team/consumer reference counts and only runs while consumers are active |
Add member_log_stream consumer, not a member-specific watcher |
| Runtime lane validation | Новая находка | Existing member validator rejects :, but lane ids can be secondary:opencode:<member> |
Add optional lane validator that preserves exact lane id |
| Generic renderer purity | Новая находка | TaskLogStreamSection mixes fetch/debounce/copy/render helpers |
Extract render-only view and keep API/fallback/gates in containers |
0. Architecture Boundary Decision
Repo Standard Tension
docs/FEATURE_ARCHITECTURE_STANDARD.md говорит, что full feature slice нужен, когда feature spans process boundaries, имеет business rules, transport bridge, more than one adapter и provider roadmap.
Member Log Stream V2 подходит под этот стандарт:
- feature spans main/preload/renderer;
- feature owns merge, dedupe, budget and provider coverage policy;
- feature needs transport wiring;
- feature has multiple source adapters: Claude, OpenCode, Codex skipped/partial later;
- roadmap явно ведет к variant 3/provider extensibility.
Текущая реальность кода все еще важна:
- task stream уже живет в
src/main/services/team/taskLogs/stream; - member popup уже живет в
src/renderer/components/team/members; - team IPC уже централизован в
src/main/ipc/teams.ts; api.teamsуже содержитgetMemberLogs,getTaskLogStream,getLogsForTask.
Поэтому правильная стратегия не "перетащить весь team logs слой", а создать feature slice для новой member stream capability and keep old surfaces as integration points.
Options
A. Canonical feature slice with thin legacy integration
🎯 8.5 🛡️ 9 🧠 7
Примерно 1500-2300 LOC.
Плюсы:
- соответствует feature standard;
- contracts/core/application/source ports становятся clean architecture boundary;
- provider sources расширяются по OCP;
- старый task stream and legacy
MemberLogsTabне мигрируются в этом PR; - app shell imports only public feature entrypoints.
Минусы:
- больше файлов и немного больше boilerplate;
- нужно аккуратно сделать compatibility with existing
MemberDetailDialogand app API; - нужно добавить import-boundary discipline from the start.
B. Legacy extension in current team services
🎯 7 🛡️ 6.5 🧠 5
Примерно 550-850 LOC.
Плюсы:
- минимальный blast radius;
- ближе к существующему
Task Log Stream; - быстрее внедряется.
Минусы:
- легко получить большой
MemberLogStreamServiceс provider-specific ветками; - хуже соответствует feature architecture standard;
- future variant 3 будет сложнее выделять.
C. Existing team surface plus source ports
🎯 8 🛡️ 8 🧠 6
Примерно 1300-2050 LOC, если включить ref metadata dedupe, mapper extraction, renderer shared view, member tracking activation, dialog-level fallback tests, OpenCode in-flight protection, renderer reload coalescing и provider-neutral message hygiene extraction.
Плюсы:
- renderer-facing API is feature-owned, for example
api.memberLogStream; existingapi.teamscan remain only as a thin compatibility delegate if needed by current popup wiring; - implementation остается рядом с existing
taskLogs/stream; - provider/runtime логика делится на отдельные source classes;
- часть будущей миграции к feature slice будет подготовлена.
Минусы:
- это все еще не full canonical feature slice;
- нужно дисциплинированно не смешать orchestration, provider IO и UI DTO в одном файле.
Recommendation
Выбрать A для первого PR.
Правило:
src/features/member-log-stream/contractsowns DTOs, channels, normalize helpers and API fragment types;core/domainowns pure policies: merge order, budget decisions, dedupe keys and coverage semantics;core/applicationownsMemberLogStreamSourceports and the use case;main/adapters/output/sourcesowns Claude/OpenCode/Codex source adapters;main/adapters/input/ipcowns validation and IPC translation;preloadowns thin bridge;renderer/uiowns presentational components only;- old
MemberLogsTab, task stream and task exact logs are not migrated in this PR; - if existing
api.teamsis needed for compatibility, it delegates to feature contracts/use case and does not own DTO/policy.
Exact Standard Compliance
The feature should be reviewable against docs/FEATURE_ARCHITECTURE_STANDARD.md without special exceptions:
contracts/owns only DTOs, API fragment types, channel constants and normalize helpers.core/domain/owns pure policies: source merge order, source coverage state, dedupe keys, budget decisions and safe source metadata.core/application/owns use cases and ports:MemberLogStreamSource, clock/logger/cache/tracking ports and response models.main/composition/wires concrete adapters and exposesMemberLogStreamFeatureFacade.main/adapters/input/ipc/owns IPC validation and transport translation.main/adapters/output/sources/owns Claude/OpenCode/Codex source adapters.main/infrastructure/owns runtime bridge helpers, parser wrappers, TTL/in-flight cache and filesystem details.preload/owns only a thin feature bridge.renderer/hooks/owns API calls, tracking activation, team-change subscriptions and reload coalescing.renderer/ui/owns presentational components only.
Clean Architecture dependency rule:
- domain imports no application/adapters/infrastructure/framework/process code;
- application imports no main/preload/renderer or concrete IO;
- adapters import inward to application/domain and outward to infrastructure;
- renderer UI receives props and view models, never direct API/store/Electron access;
- app shell imports only public entrypoints.
SOLID and DRY application:
- SRP:
GetMemberLogStreamUseCasecoordinates use case flow only; provider IO stays in source adapters; renderer UI only renders. - OCP: adding Codex partial/full later means adding or swapping a source adapter, not rewriting renderer or use case branches.
- LSP: all sources honor the same
included | partial | skippedresult shape and fail softly for expected absence. - ISP: core ports stay narrow. Do not pass
TeamDataService, Electron events or renderer member snapshots through core. - DIP: use cases depend on ports; concrete finder/bridge/parser implementations live outside core.
- DRY: one DTO owner in feature contracts, one stream render primitive, one OpenCode projection mapper, one provider-neutral message hygiene helper set.
Lint status:
- Generic feature guard rails already exist in
eslint.config.jsforsrc/features/*. - Implementation should rely on those first and add member-log-stream-specific guard rails only if generic messages are not strict enough.
- Targeted lint command:
pnpm exec eslint src/features/member-log-stream --cache --cache-location .eslintcache --cache-strategy content.
Project standard cross-check:
- Top-level
CLAUDE.mdsays new medium/large features default tosrc/features/<feature-name>and must followdocs/FEATURE_ARCHITECTURE_STANDARD.md. docs/FEATURE_ARCHITECTURE_STANDARD.mdrequires the full slice when a feature spans process boundaries, owns business policy, has transport wiring, has multiple adapters or has a provider roadmap. Member log stream matches all five.eslint.config.jsalready enforces generic public-entrypoint, core-domain, core-application, preload and renderer-UI guards forsrc/features/*.src/features/recent-projectsconfirms the public entrypoint pattern:contracts/index.ts,main/index.ts,preload/index.tsandrenderer/index.tsexpose only supported surface.src/features/CLAUDE.mdis referenced byCLAUDE.md, but is absent in this worktree, so the binding standard for this plan is the top-levelCLAUDE.mdplusdocs/FEATURE_ARCHITECTURE_STANDARD.md.
0.1 Source-Port Design
Чтобы не нарушить SRP/OCP, GetMemberLogStreamUseCase не должен сам знать все детали Claude/OpenCode/Codex.
Нужен internal interface:
interface MemberLogStreamSourceInput {
teamName: string;
memberName: string;
laneId?: string;
budget: MemberLogStreamBudget;
sinceMs?: number | null;
forceRefresh?: boolean;
}
interface MemberLogStreamSourceResult {
provider: MemberLogStreamProvider;
status: 'included' | 'partial' | 'skipped';
segments: MemberLogStreamSegment[];
warnings: MemberLogStreamWarning[];
}
interface MemberLogStreamSource {
readonly provider: MemberLogStreamProvider;
load(input: MemberLogStreamSourceInput): Promise<MemberLogStreamSourceResult>;
}
First PR sources:
ClaudeMemberTranscriptStreamSource;OpenCodeMemberRuntimeStreamSource;CodexNativeMemberTraceStreamSourceonly as skipped coverage adapter, no heavy trace scan.
GetMemberLogStreamUseCase responsibilities:
- normalize already-validated options;
- call sources fail-soft, preferably with
Promise.allSettled()plus deterministic merge order; - merge source results;
- enforce global budget;
- sort final segments;
- build response metadata and warnings.
It should not:
- parse OpenCode CLI output directly;
- scan Codex trace directories directly;
- know renderer copy;
- know old
MemberLogsTabfallback details.
0.2 Renderer Performance Budget
Facts
MemberExecutionLog:
- transforms all chunks into conversation groups;
- reverses all groups;
- renders all groups;
- keeps everything expanded by default unless user collapses groups;
- has no virtualization.
Repo already depends on @tanstack/react-virtual, and ActivityTimeline uses it. But adding virtualization to log stream in first PR would be a separate UI behavior project.
Decision
Do not add virtualization in first PR.
Instead enforce backend budget:
const DEFAULT_MEMBER_LOG_STREAM_BUDGET = {
maxTranscriptFiles: 40,
maxSegments: 30,
maxChunks: 250,
maxSourceMessages: 1200,
maxMessagesPerSegment: 300,
maxTotalContentChars: 800_000,
maxMessageContentChars: 80_000,
maxToolResultContentChars: 120_000,
openCodeMessageLimit: 400,
openCodeTimeoutMs: 5_000,
};
Budget semantics:
- candidate transcript refs are newest-first before capping;
- source segments are sorted by timestamp before final response;
- global merge keeps newest useful content if sources exceed budget;
- message budget is enforced before chunk build when one file/session is too large;
- chunk budget is enforced after chunk build by dropping oldest whole chunks/segments first;
- response includes
truncated: trueand warninglarge_log_window_limitedwhen any cap is hit; - renderer shows a short "showing recent log stream" note from warnings.
- content-char budgets are enforced before chunk build so a single huge markdown/tool result cannot freeze the popup after expansion.
Why this is safer:
- prevents popup freezes;
- avoids new virtualization bugs;
- keeps v2 focused on correct data/source behavior.
Future:
- add
Load olderonly after source cursor semantics exist; - add virtualization only if product needs audit-sized member streams in popup.
0.2.1 Pair-Aware Truncation
Facts
ChunkBuilder groups all "AI" category messages until the next real user/system/compact boundary. A single AI chunk can contain many assistant messages, tool calls and tool results.
So this is not enough:
maxChunks: 250
One chunk can still become a large displayItems list in MemberExecutionLog.
Decision
Add message-level budget:
maxSourceMessages: 1200;
maxMessagesPerSegment: 300;
Truncation should happen in this order:
- Dedupe transcript refs.
- Sort candidate refs newest-first and cap files.
- Parse selected files.
- For each file/session, trim parsed messages before chunk build if it exceeds
maxMessagesPerSegment. - Build chunks.
- Drop oldest whole segments/chunks to satisfy
maxSegments,maxChunks, andmaxSourceMessages.
Pair-aware rule:
- if retaining a meta user tool-result message with
sourceToolUseID, also retain the assistant message containing the matchingtoolCalls.id; - if retaining the matching assistant would exceed the hard message budget, drop the orphan result instead of rendering an unpaired result;
- preserve chronological order after expansion;
- mark response
truncated: true; - add warning
segment_message_window_limited.
Do not attempt text-level truncation inside EnhancedChunk in first PR. It is safer to drop old message windows than to mutate renderer-specific chunk internals.
0.2.3 Content-Size Budget
Facts
MemberExecutionLog renders AI groups expanded by default. DisplayItemList previews are short, but expanded items can render full markdown/tool result content. So these budgets are not sufficient by themselves:
maxChunks: 250;
maxMessagesPerSegment: 300;
One tool result can still contain hundreds of thousands of characters.
Decision
Add content budgets before chunk build:
maxTotalContentChars: 800_000;
maxMessageContentChars: 80_000;
maxToolResultContentChars: 120_000;
Rules:
- apply after message-window trimming and before
BoardTaskExactLogChunkBuilder.buildBundleChunks(); - preserve
uuid,sourceToolUseID,sourceToolAssistantUUID, tool call ids and tool result ids; - replace oversized text/content fields with a short placeholder that states the content was truncated for popup display;
- do not mutate file-backed parser cache arrays in place. Clone the affected
ParsedMessageobjects before truncating; - set
truncated: true; - add warning
message_content_limited.
Risk rating:
🎯 8 🛡️ 8 🧠 5
Approx 120-220 LOC.
This is safer than relying on UI collapse/expand behavior, because the current execution log starts expanded.
0.2.4 ParsedMessage Hygiene Boundary
Facts
BoardTaskLogStreamService already has useful private message hygiene helpers:
cloneBlock();cloneMessageContent();mergeMessages();pruneEmptyInternalToolResultMessages();retainSyntheticToolUseAssistants().
It also has task-specific cleanup:
sanitizeJsonLikeToolResultPayloads();sanitizeToolResultContent();sanitizeToolResultPayloadValue().
That task cleanup is not fully provider-neutral. It is designed around board/task tool payloads and can replace JSON-like tool result payloads with '' when it cannot extract a board-tool display string. In member-wide logs, a JSON-looking tool result can be a legitimate Bash/API/tool output. Applying task cleanup wholesale would risk hiding useful member logs.
Options
A. Reuse task sanitization as-is for member stream
🎯 5 🛡️ 4 🧠 3
Approx 20-80 LOC.
Fast, but unsafe. It can hide useful non-board JSON outputs in a member-wide stream.
B. Extract provider-neutral hygiene and add member-specific truncation
🎯 8.5 🛡️ 9 🧠 6
Approx 180-340 LOC.
Create a small shared main-process helper, for example:
src/main/services/team/taskLogs/stream/ParsedMessageStreamHygiene.ts
Provider-neutral exports:
- clone message/content blocks without mutating parser-cache objects;
- prune empty internal tool-result messages;
- retain synthetic tool-use assistants by clearing only the synthetic model marker;
- count/truncate message content by char budget;
- preserve
uuid,toolCalls.id,toolResults.toolUseId,sourceToolUseID,sourceToolAssistantUUIDandtoolUseResult.toolUseId.
Keep board/task cleanup either private to BoardTaskLogStreamService or export it with a name that makes the scope explicit, such as sanitizeBoardToolResultPayloads().
C. Skip content hygiene and rely only on message-window limits
🎯 6 🛡️ 5 🧠 2
Approx 0-60 LOC.
Too weak. One retained tool result can still be huge.
Decision
Use option B.
Implementation rules:
- do not run board-specific JSON payload cleanup across all member logs;
- truncate oversized strings/arrays recursively but preserve linking ids;
- when truncating
toolUseResult.content,toolUseResult.message,toolUseResult.file.content,oldString,newString, orstderr/stdout, keep the surrounding object shape; - replace text with a compact placeholder that includes original char count and retained char count;
- clone only changed messages to keep memory reasonable;
- test with a JSON Bash output to confirm it is truncated if huge but not blanked just because it is JSON.
Risk rating:
🎯 8.5 🛡️ 9 🧠 6
Approx 180-340 LOC.
0.2.2 MemberExecutionLog Process Filtering
Facts
MemberExecutionLog passes memberName into AIExecutionGroup.
AIExecutionGroup filters only group.processes by p.team?.memberName; it does not filter the raw AI steps or tool executions.
That means:
- OpenCode projection segments with no
Process[]still render normal tools/output; - Claude chunks with team
Process[]avoid showing another member's subagent panels; - synthetic/fake
Processobjects are not required for member stream.
Decision
Do not synthesize fake Process entries for OpenCode member stream.
Render OpenCode through normal tool/output chunks from projected messages. Only use Process[] when real process metadata already exists.
Add a renderer regression test that a segment with actor.memberName and no processes still displays tool/output items.
0.3 IPC Validation And Limits
Facts
Existing validators:
validateTeamNamefitsteamName;validateMemberNamefits member names;validateTaskIdis task-only;laneIdcan contain:, for examplesecondary:opencode:alice, sovalidateMemberNameis wrong for lane.
Correct IPC Policy
Add a local helper near the handler, or a shared guard if reused:
export function validateOptionalRuntimeLaneId(value: unknown) {
if (value == null) return { valid: true, value: undefined };
if (typeof value !== 'string') return { valid: false, error: 'laneId must be a string' };
const trimmed = value.trim();
if (!trimmed) return { valid: true, value: undefined };
if (trimmed.length > 256) return { valid: false, error: 'laneId exceeds max length (256)' };
if (/[\0-\x1F\x7F/\\]/.test(trimmed)) {
return { valid: false, error: 'laneId contains invalid characters' };
}
return { valid: true, value: trimmed };
}
Options policy:
limitSegments: integer, clamp to1..80, default 30;since: if present, must parse as valid date, otherwise return IPC error;laneId: optional, max 256, no control chars or path separators, allowprimaryand colon-separated ids likesecondary:opencode:alice;forceRefresh: optional boolean;- unknown option keys rejected.
Do not lowercase or otherwise normalize laneId. It should be trimmed only, because the orchestrator/runtime record lookup may treat lane ids as exact identities.
Additional code research: TEAM_GET_DATA validates options with an allow-list and rejects unknown keys before dispatching to the service/worker. Member log stream should follow that stricter style because option typos can otherwise disable laneId, since or forceRefresh silently.
Recommended allow-list:
const allowed = new Set(['limitSegments', 'since', 'laneId', 'forceRefresh']);
Return Unknown getMemberLogStream option: ${key} for extra keys.
Security:
- do not expose transcript file paths in renderer response;
- warnings can mention counts and provider names, not absolute paths;
- service can log paths to main logger if needed.
0.4 Exact Stream DTO And Renderer Contract
Facts
BoardTaskLogSegment is already the right low-level render unit:
interface BoardTaskLogSegment {
id: string;
participantKey: string;
actor: BoardTaskLogActor;
startTimestamp: string;
endTimestamp: string;
chunks: EnhancedChunk[];
}
MemberExecutionLog only needs EnhancedChunk[] plus optional member visual props. It does not know task ids, provider ids, or source metadata.
Additional type research:
BoardTaskLogStreamResponse.sourceis a task-only union: transcript, OpenCode task runtime fallback/attribution and Codex task trace fallback values.BoardTaskLogStreamResponse.runtimeProjectionis also task-specific, with attribution/heuristic/trace counters that are not the right member coverage model.BoardTaskLogSegmenthas nosourcefield, so it is safe as the render primitive but not enough as the member DTO by itself.- Therefore
MemberLogStreamResponseshould be a standalone shared response type, notextends BoardTaskLogStreamResponseand not anOmit<BoardTaskLogStreamResponse, ...>unless the omitted fields are fully replaced.
Recommended DTO shape:
type MemberLogStreamSource =
| 'member_transcript'
| 'member_mixed_runtime'
| 'member_runtime_only'
| 'member_empty';
interface MemberLogStreamResponse {
participants: BoardTaskLogParticipant[];
defaultFilter: 'all' | string;
segments: MemberLogStreamSegment[];
source: MemberLogStreamSource;
coverage: MemberLogStreamCoverage[];
warnings: MemberLogStreamWarning[];
truncated: boolean;
generatedAt: string;
metadata: MemberLogStreamMetadata;
}
This keeps task stream source semantics untouched and avoids having member-only source values leak into task UI copy or task tests.
TaskLogStreamSection currently owns too much:
- response normalization through
asEnhancedChunkArray; - participant visual mapping;
- segment key building;
- segment headers;
- participant chips;
- loading, empty and error copy;
- live reload behavior;
- task-specific
describeStreamSource()text.
Decision
Extract generic render-only view, not a generic data loader:
interface ExecutionLogStreamViewProps<TStream extends ExecutionLogStreamLike> {
title: string;
description: string;
stream: TStream | null;
loading: boolean;
error: string | null;
emptyTitle: string;
emptyDescription?: string;
teamName: string;
forceSegmentHeaders?: boolean;
boundedHistoryNote?: string | null;
}
Keep loaders separate:
TaskLogStreamSectionloads task stream and keeps task-status reload behavior.MemberLogStreamSectionloads member stream and passeslaneId/budget options.
This avoids accidentally bringing task-specific reload and copy into member popup.
0.4.1 Member Segment Metadata
Facts
BoardTaskLogSegment is enough for rendering chunks, but it does not identify provider source or safe session/lane labels:
interface BoardTaskLogSegment {
id: string;
participantKey: string;
actor: BoardTaskLogActor;
startTimestamp: string;
endTimestamp: string;
chunks: EnhancedChunk[];
}
For task stream this is acceptable because the whole section is task-scoped and source text is global. For member stream, multiple Claude sessions and OpenCode lane projection can appear in one popup. Without segment metadata, the UI either hides important context or parses segment.id, which is brittle.
Decision
Use a member-specific extension:
interface MemberLogStreamSegmentSource {
provider: MemberLogStreamProvider;
label: string;
sessionId?: string;
laneId?: string;
messageCount?: number;
truncated?: boolean;
}
interface MemberLogStreamSegment extends BoardTaskLogSegment {
source: MemberLogStreamSegmentSource;
}
Then:
MemberLogStreamResponse.segmentsusesMemberLogStreamSegment[];MemberLogStreamResponsestays standalone and does not extendBoardTaskLogStreamResponse;ExecutionLogStreamViewremains generic over baseBoardTaskLogSegment;MemberLogStreamSectioncan pass a segment label renderer that usessegment.source;- task stream stays unchanged;
- segment source metadata never includes absolute file paths.
- segment ids use provider, normalized team/member, session id and a short hash/fingerprint, not raw absolute paths.
Risk rating:
🎯 8.5 🛡️ 8.5 🧠 4
Approx 40-90 LOC.
This is safer than overloading segment.id or adding provider labels into chunk text.
Path rule:
- hash
filePath + mtimeMs + sizeBytesif a file fingerprint is needed; - never send
filePathto renderer inid,source,warningsormetadata; - main logger may record file paths for diagnostics, but response DTO should not.
0.4.2 Chunk Date Shape
Facts
MemberExecutionLog calls transformChunksToConversation(), and groupTransformer uses Date methods on chunk and semantic-step timestamps. Existing task stream works because Electron IPC/preload returns structured-clone data and the service tests usually pass Date-shaped chunks.
Additional code research: src/renderer/api/httpClient.ts already has an ISO date JSON reviver and comments that Electron IPC preserves Date instances via structured clone while HTTP JSON needs rehydration. So the runtime date risk is lower than expected for normal app and browser-mode API paths.
The remaining risk is mostly tests and ad hoc fixtures. Renderer unit tests often use lightweight segment fixtures, and browser fallback has no real chunks. If the new generic view starts accepting JSON-like chunk fixtures with ISO strings without going through the HTTP client's reviver, it can fail in durationMs = endTime.getTime() - startTime.getTime().
Decision
Do not add broad date rehydration by default in the first implementation. Keep the same runtime assumption as task stream.
But add a clear guardrail:
ExecutionLogStreamViewtests should use Date-shapedEnhancedChunkfixtures when they expect real rendering;- if a JSON-like fixture is needed, add one shared helper such as
normalizeEnhancedChunkDates()and use it from both task/member stream normalization; - do not scatter ad hoc
new Date()calls across render components.
Updated risk rating:
🎯 9 🛡️ 8.5 🧠 3
Approx 0-120 LOC depending on whether a normalizer is needed.
This keeps variant 2 aligned with existing task stream behavior without hiding a serialization assumption.
0.4.3 Renderer Normalization And Source Headers
Facts
TaskLogStreamSection.normalizeResponse() currently reconstructs only known task-stream fields:
return {
participants: response.participants,
defaultFilter: response.defaultFilter,
source: response.source,
runtimeProjection: response.runtimeProjection,
segments: ...
};
If this exact function is reused for MemberLogStreamResponse, it can accidentally drop member-only fields:
coverage;warnings;truncated;generatedAt;metadata;segment.source.
TaskLogStreamSection also hides segment headers when there is only one participant:
participants.length > 1 || selectedParticipantKey !== 'all'
That is correct for task logs, but wrong for member logs. Member stream often has one participant, while the important context is per-segment provider/session/lane source.
Additional UI-model research: BoardTaskLogParticipant is actor identity. The renderer uses participant labels for chips, member badges and color lookup. Provider/runtime/session identity is a different axis. If member stream encodes provider/session as participant keys, one selected member can appear as multiple people and the "All" filter becomes misleading.
Options
A. Reuse task view logic as-is
🎯 5 🛡️ 4 🧠 2
Approx 40-100 LOC.
Low effort, but it can hide provider/session labels and silently drop response metadata.
B. Generic view preserves stream shape and accepts a segment-header renderer
🎯 9 🛡️ 9 🧠 5
Approx 120-240 LOC.
The generic view should:
- normalize chunks with
asEnhancedChunkArray; - preserve the rest of the stream object with object spread, including
coverage,warnings,metadata,truncated,generatedAtandsegment.source; - let callers pass
forceSegmentHeaders; - let callers pass
renderSegmentMarkerorgetSegmentMetaLabel; - keep task copy/source text in
TaskLogStreamSection; - keep member provider/session labels in
MemberLogStreamSection. - not import
api.teams, feature gates,MemberLogsTab, provider sources or task/member loading hooks; - receive already loaded
teamMembersfrom the container so the pure view can be tested without the global store; - keep fallback UI outside the generic view, because fallback policy is different for task vs member.
For member stream:
forceSegmentHeaders: true;- label each segment with
segment.source.label; - optionally show safe
sessionIdshort prefix orlaneId, never file path; - show bounded-history/coverage warnings outside the repeated segment blocks.
- keep
participantKeyactor-based, for examplemember:<normalizedName>, and do not createclaude:<member>/opencode:<member>pseudo-participants; - if source filtering is needed later, add a separate source filter instead of overloading participant chips.
Additional renderer key research: current TaskLogStreamSection.buildStableSegmentRenderKey() uses participantKey:firstChunkId, not segment.id. That preserves expanded/collapsed React state when a live refresh extends the tail of the same segment and changes the segment id. Existing tests cover this behavior.
For member stream, the same default key can collide more easily because different providers/sessions can have one selected participant and similar first chunk ids. Do not change the task default blindly.
Add a caller-provided key strategy:
buildSegmentRenderKey?: (segment: TSegment) => string;
Rules:
- task stream default keeps current
participantKey:firstChunkIdbehavior; - member stream passes a source-aware key such as
${segment.id}:${segment.chunks[0]?.id ?? segment.startTimestamp}; segment.idmust already be path-safe and stable across refreshes;- tests should cover both task tail-growth state preservation and member source collision avoidance.
C. Duplicate task stream UI into member component
🎯 7 🛡️ 6 🧠 4
Approx 180-320 LOC.
Works quickly, but drift from task stream UI will grow and future variant 3 migration gets harder.
Decision
Use option B.
Risk rating:
🎯 9 🛡️ 9 🧠 5
Approx 160-300 LOC.
0.4.4 Renderer Request Coalescing
Facts
TaskLogStreamSection uses requestSeqRef to ignore stale responses. That prevents stale UI writes, but it does not prevent parallel IPC calls when visibility reload, log-source-change, and task-log-change happen close together.
For task logs this is acceptable because the source is task-scoped. For member stream, one request can parse many files and may also call OpenCode runtime transcript. Parallel duplicate calls are a bigger performance risk.
Decision
MemberLogStreamSection should coalesce active loads:
- keep one active request for the current
teamName/memberName/laneId/optionskey; - if a background reload is requested while active, set a pending reload flag instead of starting a second IPC call;
- when the active request finishes, run at most one pending background reload;
- if the component unmounts or key changes, ignore old results and clear pending reload state;
- initial foreground load still shows loading, background reload keeps old stream visible.
This complements backend/source in-flight protection. It is not a replacement for it, because multiple windows or tests can still call IPC directly.
Risk rating:
🎯 8.5 🛡️ 8.5 🧠 5
Approx 80-160 LOC with tests.
0.4.4.1 Since And Full-Response Replacement
Facts
getMemberLogStream options include since as a useful backend performance hint. But the renderer state model is full response replacement: setStream(response). A since-filtered response is partial unless the API explicitly says it includes enough prior segments to replace the visible stream.
Decision
First PR should not do client-side incremental merge.
- Initial load requests a full bounded stream.
- Background reload requests a full bounded stream too.
log-source-changebackground reload addsforceRefresh: true, but notsince.sinceremains validated at IPC and covered in service tests, but UI should not use it for replacement reloads until there is an explicit merge contract.- If incremental reload is added later, response metadata must say whether it is
completeorpartial, and renderer must merge by source-aware segment id.
Risk rating:
🎯 8.5 🛡️ 9 🧠 3
Approx 0-60 LOC, mostly tests and avoiding a tempting optimization.
This prevents a subtle UI regression where fresh logs appear, but older visible segments disappear after a background reload.
0.4.5 Member Popup Fallback Boundary
Facts
MemberDetailDialog currently renders old logs directly:
<MemberLogsTab teamName={teamName} memberName={member.name} />
The same old MemberLogsTab is also used by task logs through ExecutionSessionsSection, where it is labeled as legacy session-centric transcript browsing. That means changing MemberLogsTab itself is not a member-popup-only change.
Existing dialog tests live at:
test/renderer/components/team/members/MemberDetailDialog.test.ts
That test file mocks MemberLogsTab, so replacing the import without updating the mock can cause noisy test failures that are unrelated to the stream logic.
Options
A. Replace MemberLogsTab internals with new stream
🎯 5 🛡️ 5 🧠 4
Approx 120-260 LOC.
This looks small, but it affects task Execution Sessions too. It also removes the clean rollback path.
B. Gate at MemberDetailDialog and keep MemberLogsTab untouched
🎯 9 🛡️ 9 🧠 4
Approx 80-180 LOC.
This is the safest first implementation:
- renderer feature gate on: render
MemberLogStreamSection; - renderer feature gate off: render old
MemberLogsTab; - initial stream error: show error and explicit old logs fallback;
- background refresh error: keep last good stream and surface the error state unobtrusively.
Task Execution Sessions remains untouched because it still imports MemberLogsTab directly.
C. Let MemberLogStreamSection import and own MemberLogsTab
🎯 6 🛡️ 6 🧠 5
Approx 100-220 LOC.
This hides fallback inside the new component, but makes the boundary muddier. It also makes it easier to accidentally couple legacy UI copy and new stream copy.
Decision
Use option B.
Implementation guardrails:
- keep fallback decision in
MemberDetailDialog, not inside the stream renderer; - do not change
MemberLogsTabbehavior in the first PR; - add a
MemberLogStreamSectionmock inMemberDetailDialogtests; - test gate-on and gate-off rendering;
- test that first-load stream failure keeps the Logs tab active and shows explicit old logs fallback;
- keep
ExecutionSessionsSectionexpectations unchanged.
Risk rating:
🎯 9 🛡️ 9 🧠 4
Approx 80-180 LOC.
This reduces the highest accidental renderer regression risk without changing the backend design.
0.5 Cumulative Subagent Snapshot Dedupe
Facts
findLogsForTask() already has this warning in code:
- in-process teammates can produce cumulative JSONL snapshots;
- the largest file is a superset of smaller files;
- task flow dedupes subagent snapshots by
sessionId + memberNameand keeps the largestmessageCount.
But findRecentMemberLogFileRefsByMember() currently returns:
interface MemberLogFileRef {
memberName: string;
sessionId: string;
filePath: string;
mtimeMs: number;
}
It dedupes only exact filePath. That is not enough for member stream, because parsing multiple cumulative snapshots can duplicate the same turn several times.
Another discovered edge case: attribution uses knownMembers, built from current config, member meta and inbox names. If the popup is opened for a historical/removed member that is no longer present in those sources, syntax-only IPC validation still will not make attribution recognize that name.
Options
A. Use refs as-is and rely on chunk/message ids
🎯 5 🛡️ 5 🧠 3
Approx 0-80 LOC.
Low implementation cost, but duplicate stream entries are likely in cumulative snapshot cases.
B. Extend MemberLogFileRef with optional metadata and dedupe before parse
🎯 8 🛡️ 8 🧠 5
Approx 120-220 LOC.
Add optional fields without breaking existing callers:
interface MemberLogFileRef {
memberName: string;
sessionId: string;
filePath: string;
mtimeMs: number;
sizeBytes?: number;
messageCount?: number;
kind?: 'lead_session' | 'member_session' | 'subagent';
}
Then ClaudeMemberTranscriptStreamSource can:
- group subagent refs by
memberName + sessionId; - keep largest
messageCountwhen available, otherwise largestsizeBytes, tie-break by newestmtimeMs; - keep root member session refs separately;
- cap after dedupe, not before.
Do not stream every candidate only to compute messageCount in first PR. Use it when a caller already has it; otherwise sizeBytes is the cheap proxy for cumulative JSONL snapshots.
Also change findRecentMemberLogFileRefsByMember() so it adds requested member names to the attribution set before calling getCachedSubagentAttribution() / getCachedMemberSessionAttribution().
Suggested shape:
const attributionMembers = new Set(knownMembers);
for (const key of requestedMembersByKey.keys()) {
attributionMembers.add(key);
}
Use attributionMembers only for attribution. Keep the returned memberName as the caller's requested casing from requestedMembersByKey.
C. Parse all refs, then dedupe by message uuid
🎯 6 🛡️ 7 🧠 7
Approx 180-350 LOC.
More robust when metadata is missing, but expensive and can still miss duplicates if synthetic/tool result uuids differ.
Recommendation
Use B.
This is the most important correction to the earlier plan. Without it, variant 2 can be correct by attribution but noisy by duplication.
Add the requested-member attribution augmentation to B. It is small, but it is what makes removed-member popup logs actually possible instead of only syntactically allowed.
0.6 OpenCode Projection Mapper Boundary
Facts
ClaudeMultimodelBridgeService.getOpenCodeTranscript() returns transcript.logProjection.messages.
OpenCodeTaskLogStreamSource already has working private mapping from OpenCodeRuntimeTranscriptLogMessage to ParsedMessage:
- content block conversion;
- tool call/result conversion;
sourceToolUseID;sourceToolAssistantUUID;toolUseResult;- sanitized text content.
There is also a separate toParsedMessage() in OpenCodeTaskStallEvidenceSource, but it is not equivalent. It maps non-string content to [] and does not build toolUseResult. That is acceptable for stall evidence rows, but not for a user-visible log stream.
But that file also contains task-specific logic:
- task marker matching;
- task windows;
- attribution records;
- foreign team marker filtering;
- task fallback heuristics.
Options
A. Copy the private mapper into member source
🎯 7 🛡️ 6 🧠 4
Approx 150-220 LOC.
Fast, but mapper drift is likely.
B. Extract generic mapper and make both task/member sources use it
🎯 8.5 🛡️ 8.5 🧠 6
Approx 180-300 LOC.
Create a small file near stream sources, for example:
// src/main/services/team/taskLogs/stream/OpenCodeRuntimeProjectionMapper.ts
export function mapOpenCodeProjectionMessagesToParsedMessages(
messages: OpenCodeRuntimeTranscriptLogMessage[]
): ParsedMessage[];
export function countProjectionToolCalls(messages: ParsedMessage[]): ProjectionToolCounts;
Only move generic projection conversion. Do not move task marker/window logic.
Extraction source must be OpenCodeTaskLogStreamSource, not OpenCodeTaskStallEvidenceSource.
Implementation detail:
- first move the task-source mapper to the shared file without behavior changes;
- update
OpenCodeTaskLogStreamSourceto call the shared mapper; - then let
OpenCodeMemberRuntimeStreamSourcecall the same mapper; - leave stall monitor as-is unless a separate test-backed cleanup proves identical behavior is intended there.
C. Reuse OpenCodeTaskLogStreamSource
🎯 3 🛡️ 3 🧠 4
Approx 80-160 LOC.
Wrong abstraction. It is task-scoped by design.
Recommendation
Use B. This is a small refactor, but it reduces long-term drift and gives member stream the exact same OpenCode rendering semantics as task stream.
Important: do not use the stall-monitor mapper as the shared base. That would make the implementation look shared while silently degrading member stream content rendering.
0.6.1 OpenCode Runtime Call Budget
Facts
OpenCodeTaskLogStreamSource already protects task stream fallback with:
- a small cache keyed by task/window/attribution;
- an
inFlightmap so concurrent calls join the same bridge request; CACHE_TTL_MS = 1_500;- transcript limits of
200for heuristic and500for attributed mode.
Member stream will reload on same-team log-source-change and task-log-change. Even with debounce, those events can arrive close together while a popup is open. Calling runtime transcript for every refresh can make the popup feel slow and can add unnecessary process churn.
Options
A. No OpenCode member-source cache
🎯 7 🛡️ 6 🧠 2
Approx 0-40 LOC.
Simple, but repeated refreshes can spawn repeated bridge calls. Timeout protects the popup from hanging, but not from extra work.
B. Add member-source TTL cache and in-flight join
🎯 8.5 🛡️ 8.5 🧠 4
Approx 80-160 LOC.
Use a cache key like:
teamName::memberName::laneId-or-none::limit
Rules:
- default TTL
1_500ms, matching task OpenCode fallback; forceRefreshcan bypass completed cache, but should still join an existing in-flight request for the same key;- cache both
nulland successful responses briefly, because repeated failures should not spawn repeated CLI calls; - do not share this cache with
OpenCodeTaskLogStreamSource; - keep timeout handling inside the bridge/source, not renderer.
C. Add a long cache in ClaudeMultimodelBridgeService
🎯 5 🛡️ 5 🧠 6
Approx 120-240 LOC.
This centralizes caching but risks changing behavior for status/stall/task callers that use the same bridge for different freshness expectations.
Decision
Use option B.
This keeps OpenCode member stream responsive without changing bridge semantics for other callers.
Risk rating:
🎯 8.5 🛡️ 8.5 🧠 4
Approx 80-160 LOC.
0.7 Live Refresh Policy
Facts
TeamChangeEvent has:
log-source-changewithout task id;task-log-changewithtaskId, but nomemberName;tool-activity, which can be frequent and is intended for live tool indicators;- no event that means "logs for member X changed".
Task stream can reload only for one taskId. Member stream cannot do that safely.
Additional code research: task logs do not rely on onTeamChange subscription alone.
TaskLogStreamSectionlistens toonTeamChangeand schedules reloads.TaskLogsPanelseparately callsapi.teams.setTaskLogStreamTracking(teamName, true)while task log activity tracking is relevant.- That IPC maps to
TeamLogSourceTracker.enableTracking(teamName, 'task_log_stream'). TeamLogSourceTrackeronly watches transcript/log freshness sources while at least one consumer is active.- Member popup has no
TaskLogsPanelparent, so just addingonTeamChangeinMemberLogStreamSectioncan miss fresh events when no other UI has enabled tracking.
Decision
For an open member popup:
- enable log-source tracking while the stream section is mounted;
- reload on same-team
log-source-change; - pass
forceRefresh: trueforlog-source-change, becauseTeamMemberLogsFinderdiscovery cache has a 30s TTL; - do not pass
sincefrom renderer for replacement reloads in the first PR; - reload on same-team
task-log-change, because it is still a log-source freshness signal; - do not reload on
tool-activity; - debounce at least as strongly as task stream, preferably 500-750ms for member stream;
- do not clear existing stream on background refresh failure;
- reload on visibility return only if popup is still open.
Risk rating:
🎯 8 🛡️ 8 🧠 4
Approx 40-80 LOC.
This may reload for tasks owned by other members, but only while the member popup is open and with debounce. That is safer than missing fresh logs because task-log-change lacks member attribution.
Tracking Activation Options
A. Reuse setTaskLogStreamTracking() from member popup
🎯 8 🛡️ 7 🧠 2
Approx 20-50 LOC.
This works because it increments the same task_log_stream consumer count. The downside is semantic drift: member UI would call a task-named API.
B. Add setMemberLogStreamTracking() mapped to a new tracker consumer
🎯 8.5 🛡️ 9 🧠 4
Approx 90-170 LOC.
Add:
- feature
MEMBER_LOG_STREAM_SET_TRACKINGchannel; - feature API method or thin compatibility delegate;
- feature preload bridge and browser no-op fallback;
TeamLogSourceTrackingConsumer | 'member_log_stream';- IPC handler that calls
TeamLogSourceTracker.enableTracking(teamName, 'member_log_stream').
This keeps UI language correct and lets task/member stream lifecycles have separate consumer counts while sharing the same underlying watcher.
Important implementation detail from code:
TeamLogSourceTrackerconsumer counts are per team and consumer name, not per member.- That is correct for member stream because the watcher scope is team/session-level.
- Do not add one watcher per member popup. Add one
member_log_streamconsumer and rely on reference counts for multiple popups. - If the component remounts with a different
teamName, cleanup must disable the previous team before enabling the next team. - Disabling
member_log_streammust not close the watcher whiletask_log_stream,change_presence,tool_activityorstall_monitorstill has a positive count.
C. Do not enable tracking from member popup
🎯 5 🛡️ 4 🧠 1
Approx 0 LOC.
This is unreliable. It only works when another part of the UI has already enabled log-source tracking.
Tracking Decision
Use B.
It adds a little transport work, but it removes a hidden dependency on TaskLogsPanel and avoids calling task-named APIs from member logs.
0.8 IPC And Composition Integration
Facts
src/main/ipc/teams.ts currently has many positional dependencies and already owns legacy team APIs. That is exactly why member log stream should not be added there as another owned service.
Current legacy facts still matter:
src/main/ipc/teams.tsowns existing team handlers;- task stream still uses
TEAM_GET_TASK_LOG_STREAM; src/preload/index.tsandsrc/renderer/api/httpClient.tsalready expose legacy team API fallbacks;src/main/index.tsis the composition point where feature facades can be instantiated.
Options
A. Feature-owned IPC and composition
🎯 8.5 🛡️ 9 🧠 4
Approx 80-180 LOC for transport wiring.
This is the correct first PR choice after adopting the canonical feature slice.
Implementation rule:
const memberLogStreamFeature = createMemberLogStreamFeature({
logsFinder,
logSourceTracker,
runtimeBridge,
logger,
});
registerMemberLogStreamIpc(ipcMain, memberLogStreamFeature);
Feature-owned files:
src/features/member-log-stream/contracts/channels.ts;src/features/member-log-stream/contracts/api.ts;src/features/member-log-stream/preload/createMemberLogStreamBridge.ts;src/features/member-log-stream/main/composition/createMemberLogStreamFeature.ts;src/features/member-log-stream/main/adapters/input/ipc/registerMemberLogStreamIpc.ts.
Test rule:
- feature IPC registration test expects feature channel registration/removal;
- app-shell integration test imports only
@features/member-log-stream/main; - existing task stream handler tests remain unchanged and prove legacy team IPC was not disturbed.
B. Add member stream into initializeTeamHandlers()
🎯 5 🛡️ 4 🧠 2
Approx 30-70 LOC.
This is no longer the recommended path. It is locally cheap, but it violates feature ownership and risks shifting positional dependencies in initializeTeamHandlers().
Do not choose this.
C. Convert legacy team initializer to a dependency object
🎯 7 🛡️ 9 🧠 7
Approx 250-450 LOC.
This is a useful legacy IPC hygiene refactor, but it is separate from member log stream. It should not be bundled unless the PR explicitly pays that cost.
Required Feature Handler Contract
Add feature-owned channels, for example:
export const MEMBER_LOG_STREAM_GET = 'member-log-stream:getMemberLogStream';
export const MEMBER_LOG_STREAM_SET_TRACKING = 'member-log-stream:setTracking';
Add register/remove symmetry in feature input adapter:
ipcMain.handle(MEMBER_LOG_STREAM_GET, handleGetMemberLogStream);
ipcMain.handle(MEMBER_LOG_STREAM_SET_TRACKING, handleSetMemberLogStreamTracking);
ipcMain.removeHandler(MEMBER_LOG_STREAM_GET);
ipcMain.removeHandler(MEMBER_LOG_STREAM_SET_TRACKING);
Add feature API and preload shape:
getMemberLogStream(
teamName: string,
memberName: string,
options?: {
limitSegments?: number;
since?: string;
laneId?: string;
forceRefresh?: boolean;
}
): Promise<MemberLogStreamResponse>
Handler behavior:
- validate
teamNamewithvalidateTeamName; - validate
memberNamewithvalidateMemberName, but do not require current team membership; - validate
laneIdwith the new runtime-lane validator, not withvalidateMemberName; - clamp
limitSegmentsbefore passing options to service; - reject invalid
since; - validate
forceRefreshas boolean when present; - reject unknown option keys;
- call the feature facade/use case with normalized options.
Validator placement note:
src/main/ipc/guards.tskeepsValidationResultlocal today;- safest implementation is to export concrete helpers like
validateOptionalRuntimeLaneId()andvalidateOptionalBooleanOption()fromguards.ts; - if validators stay local in the feature input adapter, do not import a non-exported
ValidationResulttype.
Browser Fallback
The renderer API/browser fallback must satisfy the feature API even when Electron IPC is unavailable. If api.teams compatibility delegates are kept, they must delegate to the same fallback shape.
Return a complete empty response:
{
participants: [],
defaultFilter: 'all',
segments: [],
source: 'member_empty',
coverage: [],
warnings: [],
truncated: false,
generatedAt: new Date().toISOString(),
metadata: {
scannedTranscriptFileCount: 0,
includedTranscriptFileCount: 0,
droppedSegmentCount: 0,
droppedChunkCount: 0,
droppedMessageCount: 0,
},
}
Do not throw in browser fallback. Throwing would make renderer code need an Electron-only branch even though task stream already uses a safe empty fallback.
Recommendation
Use option A in the implementation PR.
🛡️ The main bug risk is not the new stream logic here. It is accidentally pulling the feature back into legacy team IPC/service ownership and weakening the feature boundary.
1. Claude Transcript Attribution
Facts
TeamMemberLogsFinder уже содержит лучший source для member stream:
findRecentMemberLogFileRefsByMember(
teamName: string,
memberNames: readonly string[],
mtimeSinceMs?: number | null
): Promise<MemberLogFileRef[]>
Дополнительный compatibility факт: этот method уже используется не только будущим member stream.
TeamMemberRuntimeAdvisoryServiceвызывает его с третьим positional numeric arg.- Existing live tests вызывают его с
nullи numericmtimeSinceMs. discoverProjectSessions(teamName, { forceRefresh })уже поддерживает cache bypass.getLogSourceWatchContext()иgetLiveLogSourceWatchContext()уже передаютforceRefreshв discovery path.- Current method calls
discoverProjectSessions(teamName)without options, so member stream needs object-form options only to addforceRefresh. - Current lead transcript branch pushes the lead ref after
fs.stat()but before anymtimeSinceMscheck. - Current refs have only
memberName,sessionId,filePathandmtimeMs.
Значит для member stream нельзя делать breaking object-only замену третьего аргумента.
Нужен backward-compatible options parser:
type FindRecentMemberLogFileRefsOptions =
| number
| null
| {
mtimeSinceMs?: number | null;
forceRefresh?: boolean;
};
Implementation rule:
- numeric third arg остается
mtimeSinceMs; nullостается "без mtime window";- object third arg включает
mtimeSinceMsиforceRefresh; - only object form can bypass discovery cache;
- existing advisory callers do not need changes.
Он возвращает:
{
memberName: string;
sessionId: string;
filePath: string;
mtimeMs: number;
kind?: 'lead_session' | 'member_session' | 'subagent';
sizeBytes?: number;
messageCount?: number;
}
Metadata rule:
kindandsizeBytesare cheap and should be added by the finder;messageCountis optional and should be filled only when existing attribution/session metadata already knows it;- do not parse full JSONL files in
TeamMemberLogsFinderjust to computemessageCount, becauseClaudeMemberTranscriptStreamSourcewill strict-parse the selected refs afterward.
Почему это лучше, чем findMemberLogPaths():
- уже сортирует refs по
mtimeMs desc; - дедуплицирует
filePath; - возвращает
sessionId, который нужен для stable segment id; - умеет lead transcript;
- использует ту же attribution precedence, что и старые member logs;
- принимает
mtimeSinceMs, что дает cheap performance window.
Correct Implementation
🎯 9 🛡️ 9 🧠 4
Примерно 120-220 LOC для Claude-only member stream service.
Service должен:
- Вызывать
findRecentMemberLogFileRefsByMember(teamName, [memberName], { mtimeSinceMs: sinceMs, forceRefresh }). - Dedupe cumulative subagent refs before cap, using
kind + memberName + sessionId, thenmessageCount,sizeBytes,mtimeMs. - Сразу применять cap по ref count после dedupe: default
maxTranscriptFiles = 40. - Парсить только capped refs через отдельный
BoardTaskExactLogStrictParser. - Для каждого ref строить отдельный segment.
- Ограничивать total chunks глобальным budget, default
maxChunks = 250. - Сортировать response segments по
startTimestamp asc, чтобы renderer мог reverse как task stream. - Возвращать warning
large_log_window_limited, если refs/chunks больше cap.
Risks
- Если member был переименован, finder ищет только переданное имя.
- Если
knownMembersне содержит historical removed member, attribution может не найти старые logs. - Если заменить third arg object-only, можно сломать runtime advisory и live tests, которые уже передают numeric/null.
- В текущем коде lead transcript добавляется до candidate scan и не проходит
mtimeSinceMsfilter. - Если finder начнет считать
messageCountчерез full parse, он удвоит IO и сделает popup тяжелее на больших историях. - Text mention остается low-confidence fallback. Это уже существующий риск, не новый.
Mitigation
- Не требовать current config membership в IPC handler.
- В plan добавить future alias map, но не делать в v2.
- В coverage явно писать
claude_transcript included/partial. - Расширять third arg finder-а через compatibility parser, а не менять его на object-only.
- При object/numeric
mtimeSinceMsприменять time window и к lead transcript, и к member/subagent candidates. - Если lead transcript старше
mtimeSinceMs, не возвращать его в recent refs. - Добавить tests на numeric,
nullи object options формы. - Добавить tests, что
forceRefreshпередается вdiscoverProjectSessions()только через object form. - Добавить tests, что optional metadata не ломает existing advisory callers и не требует full parse внутри finder.
2. OpenCode Lane Resolution
Facts
В репо уже есть deterministic lane identity:
buildPlannedMemberLaneIdentity({
leadProviderId,
member,
})
Для mixed team с non-OpenCode lead и OpenCode member lane становится:
secondary:opencode:<memberName>
TeamMemberResolver уже кладет в TeamMemberSnapshot:
laneId;laneKind;laneOwnerProviderId.
Renderer получает resolved member через spread snapshot:
return {
...snapshot,
status,
messageCount,
lastActiveAt,
}
Но ResolvedTeamMember type сейчас явно не содержит runtime/lane fields, хотя фактический объект их несет. Это хрупко для новой реализации.
Дополнительный code research:
src/shared/types/team.tsобъявляетTeamMemberSnapshot.laneId/laneKind/laneOwnerProviderId;src/shared/types/team.tsтакже объявляетTeamMemberSnapshot.providerBackendId/selectedFastMode/resolvedFastMode;src/shared/types/team.tsне объявляет эти поля наResolvedTeamMember;src/renderer/store/slices/teamSlice.tsделаетreturn { ...snapshot, status, messageCount, lastActiveAt }, поэтому runtime object уже несет lane fields;- renderer code уже обращается к lane fields в некоторых utility paths, но без полноценного
ResolvedTeamMembercontract это легко превращается в касты и drift.
Correct Implementation
🎯 8.5 🛡️ 9 🧠 4
Примерно 180-320 LOC.
Нужно сделать так:
- Расширить
ResolvedTeamMembertype полями:providerBackendId?: TeamProviderBackendId;selectedFastMode?: TeamFastMode;resolvedFastMode?: boolean;laneId?: string;laneKind?: 'primary' | 'secondary';laneOwnerProviderId?: TeamProviderId. Data mapping almost does not change, becausebuildResolvedMember()already spreadssnapshot.
- Расширить
getMemberLogStreamoptions:
{
limitSegments?: number;
since?: string;
laneId?: string;
}
- В
MemberLogStreamSectionпередаватьmember.laneId, если:member.providerId === 'opencode';member.laneOwnerProviderId === 'opencode';member.laneIdнепустой. Не передаватьlaneIdдля non-OpenCode members, даже если в старом snapshot случайно остался lane-like field.
- В
ClaudeMultimodelBridgeService.getOpenCodeTranscript()добавить optionallaneId. - В тот же params добавить optional
timeoutMs, чтобы member popup не зависал на provider probe path. - Bridge должен append:
if (params.laneId?.trim()) {
args.push('--lane', params.laneId.trim());
}
- Если
laneIdнеизвестен, можно сделать best-effort no-lane call, но только с catch ambiguity error. - Ambiguity превращать в warning
opencode_ambiguous_lane, не в hard failure. - Timeout/runtime missing превращать в
opencode_runtime_timeoutилиopencode_runtime_unavailable, не в hard failure. - Add a small member-source TTL cache and in-flight join around OpenCode runtime transcript calls.
Why This Is Safer
Orchestrator уже защищает от unsafe resolution:
Multiple OpenCode session records exist ... pass --lane to select one
Desktop должен уважать это, а не обходить.
OpenCode Member Stream Semantics
Для member stream нельзя использовать task-specific logic из OpenCodeTaskLogStreamSource:
- не применять task marker spans;
- не применять task work intervals;
- не фильтровать по task owner;
- не читать task attribution records как основной source.
Правильный member OpenCode segment:
- получить projection messages за выбранный member/lane;
- cap по
limit, defaultopenCodeMessageLimit = 400; - cap по времени, default
openCodeTimeoutMs = 5_000, потому что текущий bridge path используетexecClitimeout and can otherwise delay the popup; - build chunks через
BoardTaskExactLogChunkBuilder; - segment id включает
teamName,laneId,memberName,sessionId; - source coverage:
opencode_runtime included; - если projection пустой:
opencode_runtime skipped.
Implementation detail:
- не использовать
OpenCodeTaskLogStreamSourceнапрямую, потому что он task-specific; - вынести reusable projection-to-ParsedMessage helpers из
OpenCodeTaskLogStreamSourceв small mapper, если без копипаста не обойтись; - покрыть старый task source тестами после extraction, чтобы не сломать task stream.
Risks
ResolvedTeamMembertype drift может скрыть runtime/lane fields от TS.- Старые teams могут не иметь lane metadata.
- OpenCode primary-only teams могут использовать
laneId: primary. - Runtime transcript может быть stale или пустым.
Mitigation
- API принимает laneId от renderer, но main может recompute fallback lane identity later.
- Для v2 достаточно renderer laneId + safe no-lane fallback.
- Не merge multiple lanes.
- Не показывать OpenCode как complete, если он skipped/partial.
3. Codex Native Member-wide Feasibility
Facts
CodexNativeTraceReader хранит traces здесь:
.member-work-sync/runtime-hooks/codex-native-traces/
processed/<team>/<task>/*.jsonl
incoming/<team>/<task>/*.jsonl.tmp
Header содержит:
teamName;taskId;ownerName;runId;cwd;startedAt.
Значит member filtering по ownerName технически возможен.
Но текущий API:
readTaskRuns({ teamName, taskIds, includeIncoming })
task-first. Он не умеет:
- перечислить все task dirs для team;
- выбрать последние N runs по ownerName;
- ограничить scan без taskId;
- возвращать member-wide coverage.
Current implementation detail:
- It only lists
processed/<team>/<task>and optionalincoming/<team>/<task>. - It caps latest candidates after task-dir collection, currently to 10 files.
- It dedupes by
team/task/runId, preferring non-partial/newer candidates. - That strategy is safe for a known task id, but not enough for member-wide history because there is no bounded owner index.
Еще важнее: CodexNativeTraceProjector проектирует только native tool events, а не полный разговор Codex. То есть даже если сделать member-wide reader, это будет "Codex native tool trace", а не полный Codex log stream.
Options
A. Keep Codex skipped in first implementation
🎯 9 🛡️ 9 🧠 2
Примерно 20-40 LOC для coverage warning.
Плюсы:
- самый надежный первый релиз;
- нет риска тяжелого scan по trace tree;
- честное product обещание.
Минусы:
- Codex участники не получат новый native trace в member popup.
B. Add partial Codex native tool trace as phase 2
🎯 7 🛡️ 7 🧠 7
Примерно 250-450 LOC.
Новый метод:
readMemberRuns({
teamName,
ownerName,
includeIncoming,
limitRuns,
maxTaskDirs,
})
Алгоритм:
- Resolve trace root.
- List
processed/<team>task dirs and optionalincoming/<team>task dirs. - Collect jsonl/jsonl.tmp candidates with stat.
- Sort by
mtimeMs desc. - Cap candidates before full parse.
- Parse headers/runs.
- Filter normalized
ownerName. - Deduplicate by
team/task/runId, prefer non-partial and newer mtime. - Project via
CodexNativeTraceProjector. - Return segment with source label
codex_native_trace_partial.
Additional required caps:
maxTaskDirsbefore candidate collection;maxTraceCandidatesbefore full parse;maxTraceRunsafter owner filtering/dedupe;- default
includeIncoming: falseunless the member is currently active or the UI explicitly requests live partial files.
Минусы:
- scanning all task dirs может быть тяжелым;
- trace может быть native-tool-only и выглядеть неполным;
- incoming partial files требуют аккуратного JSONL handling.
- scanning by owner without an index can become O(number of tasks), so it should not ride inside the first PR.
C. Full Codex member-wide log support
🎯 5 🛡️ 5 🧠 9
Примерно 700-1200+ LOC.
Нужно строить отдельный index по Codex runtime/session events, не только native tool traces. Это уже вариант 3.
Recommendation
Для варианта 2:
- first PR: Codex skipped with explicit
codex_member_wide_not_supported; - optional second PR: partial native tool trace with honest UI label;
- do not present Codex native trace as full logs.
4. Parser Cache Safety
Facts
BoardTaskExactLogStrictParser.parseFiles() делает:
this.cache.retainOnly(new Set(uniquePaths));
BoardTaskExactLogsParseCache delegates to BoardTaskActivityParseCache, whose retainOnly() removes both:
- parsed cache entries;
- active
inFlightparse promises outside the retained file set.
Это значит: если один service instance парсит task files, а другой потом member files на том же parser instance, второй вызов может вычистить не только cache первого, но и его active parse dedupe. В худшем случае это не ломает correctness напрямую, но создает duplicate reads, timing-sensitive cache churn и flaky performance под параллельными reload.
Correct Implementation
🎯 9 🛡️ 9 🧠 3
Примерно 20-60 LOC.
Для v2:
ClaudeMemberTranscriptStreamSourceполучает собственныйBoardTaskExactLogStrictParser.- Не шарить parser instance с
BoardTaskLogStreamService. - Не инжектить task-stream parser в тестах как "удобный shared mock", потому что это маскирует ownership rule.
- Если понадобится общий parser, сначала redesign cache API:
retainOnly()должен стать owner-scoped или LRU-based.
Future improvement:
- заменить
retainOnly()на bounded LRU cache; - либо добавить namespace/owner key для task/member cache ownership;
- тогда task/member services смогут безопасно шарить parser.
Но это не нужно для первого релиза.
5. API Shape Refinement
Previous Draft
Изначально было достаточно:
getMemberLogStream(teamName, memberName, options?)
Updated API
После research лучше зафиксировать:
getMemberLogStream(
teamName: string,
memberName: string,
options?: {
limitSegments?: number;
since?: string;
laneId?: string;
forceRefresh?: boolean;
}
): Promise<MemberLogStreamResponse>
Почему laneId в options:
- renderer уже знает выбранного member object;
- это самый дешевый и точный path для OpenCode secondary lanes;
- main все равно валидирует strings;
- если поля нет, main может fallback to no-lane best-effort.
Почему forceRefresh в options:
discoverProjectSessions()кэширует discovery на 30 секунд;- same-team
log-source-changeозначает, что session ids/source context could have changed; - renderer can pass
forceRefresh: trueonly for that event path; - regular task-log-change reloads can keep the cache.
Почему не передавать весь member:
- меньше IPC surface;
- меньше drift между renderer snapshot и main truth;
- no need to trust UI for provider selection.
6. Correct First Implementation Sequence
Updated safest order:
- Create
src/features/member-log-streamwith contracts, core/domain, core/application, main composition, preload bridge and renderer public entrypoints. - Add feature-owned
MemberLogStreamResponse,MemberLogStreamSegment.source, response metadata, warnings andResolvedTeamMemberruntime/lane fields. Do not extend or broadenBoardTaskLogStreamResponse.source. - Add feature IPC channels, feature preload methods, browser fallback and optional compatibility methods on
api.teamsforgetMemberLogStreamplussetMemberLogStreamTracking. - Add IPC validation helpers in the feature input adapter or shared guards, including exact-preserving optional
laneIdvalidation. Register feature handlers append-only without shifting existinginitializeTeamHandlers()positional dependencies. - Add core source-port interfaces and budget constants.
- Extend
MemberLogFileRefwith optionalkind/sizeBytes/messageCount, add backward-compatiblefindRecentMemberLogFileRefsByMember()third-arg options parsing, applymtimeSinceMsto lead refs, augment requested members into attribution, keepmessageCountcheap-only, then add ref dedupe tests. - Add
ClaudeMemberTranscriptStreamSourceusingfindRecentMemberLogFileRefsByMember, ref dedupe and dedicated parser instance owned by member stream. - Extract provider-neutral
ParsedMessagehygiene helpers and keep board/task JSON cleanup scoped. - Add pair-aware message-window trimming and content-size truncation before chunk build.
- Extract
OpenCodeRuntimeProjectionMapperfromOpenCodeTaskLogStreamSourcewithout moving task-window logic. Do not use the narrower stall-monitor mapper as the base. - Add
GetMemberLogStreamUseCasewith merge, sort, budget, truncation and identical active request join layer. - Add source-local OpenCode member TTL cache and in-flight join.
- Export/instantiate the feature facade in main composition and register/remove the new IPC handler.
- Add renderer feature
ExecutionLogStreamViewrender-only extraction with shape-preserving normalization and caller-provided segment key strategy. - Add
MemberLogStreamSectionwith tracking activation, reload coalescing, actor/source identity separation and without importing oldMemberLogsTab. - Wire
MemberDetailDialoggate-on/gate-off boundary with oldMemberLogsTabfallback, importing only@features/member-log-stream/renderer. - Add OpenCode bridge
laneIdandtimeoutMs. - Add
OpenCodeMemberRuntimeStreamSourceusing the shared mapper, while leaving stall-monitor mapper migration as a separate optional cleanup. - Add coverage/warnings and backend truncation semantics.
- Keep Codex skipped in first PR.
- Optional follow-up PR:
CodexNativeTraceReader.readMemberRuns.
7. Revised Risk Ratings
| Risk | Before | After research | Notes |
|---|---|---|---|
| Showing another member's Claude logs | 🛡️ 7 | 🛡️ 8 | Existing finder has strong attribution precedence |
| OpenCode wrong lane | 🛡️ 6 | 🛡️ 8 | Pass laneId from snapshot, respect orchestrator ambiguity |
| Codex completeness claim | 🛡️ 5 | 🛡️ 8 | Mark skipped/partial honestly |
| Codex member-wide scan cost | 🛡️ 4 | 🛡️ 9 | First PR does not scan task-keyed trace tree for member-wide owner history |
| Parser cache regression | 🛡️ 6 | 🛡️ 9 | Dedicated parser instance avoids parsed cache eviction |
| Parser in-flight eviction | 🛡️ 5 | 🛡️ 9 | Same ownership rule avoids deleting another stream's active parse dedupe |
| UI copy drift from task stream | 🛡️ 7 | 🛡️ 8 | Generic view extraction avoids duplicated renderer logic |
| Popup freeze on large history | 🛡️ 4 | 🛡️ 8 | Backend budget avoids rendering audit-sized logs |
| Architecture drift | 🛡️ 5 | 🛡️ 9 | Canonical feature slice plus source ports keeps provider logic isolated and follows repo standard |
| Feature boundary deep imports | 🛡️ 5 | 🛡️ 9 | Generic src/features/* lint guard rails plus public-entrypoint imports protect Clean Architecture direction |
| IPC lane validation | 🛡️ 6 | 🛡️ 8.5 | Dedicated lane validator handles secondary:opencode:* safely without rewriting exact ids |
| Cumulative subagent duplicates | 🛡️ 5 | 🛡️ 8 | Ref metadata plus pre-parse dedupe avoids repeated turns |
| OpenCode mapper drift | 🛡️ 6 | 🛡️ 8.5 | Shared projection mapper avoids copy-pasted conversion |
| Wrong OpenCode mapper base | 🛡️ 5 | 🛡️ 9 | Extract from task source, not from the narrower stall-monitor mapper |
| Finder metadata overreach | 🛡️ 5 | 🛡️ 8 | kind/sizeBytes stay cheap; no full parse just for messageCount |
| Oversized single segment | 🛡️ 4 | 🛡️ 8 | Message budget catches huge chunks that maxChunks cannot |
| Live refresh misses | 🛡️ 6 | 🛡️ 8 | Same-team task-log-change reload covers task freshness signals |
| IPC dependency shift | 🛡️ 5 | 🛡️ 8 | Append-only service injection avoids breaking positional handler setup |
| Browser fallback compile drift | 🛡️ 6 | 🛡️ 8 | Full empty response keeps feature API and any compatibility delegate satisfied outside Electron |
| Removed member attribution | 🛡️ 5 | 🛡️ 8 | Requested member names are added to finder attribution set |
| Finder options compatibility | 🛡️ 6 | 🛡️ 9 | Backward-compatible third-arg parser keeps advisory and live tests stable |
| Lead mtime window bypass | 🛡️ 6 | 🛡️ 9 | Apply mtimeSinceMs to lead transcript before pushing lead ref |
| Mixed source context loss | 🛡️ 6 | 🛡️ 8 | Member segment metadata gives UI safe provider/session labels |
| Segment key collisions | 🛡️ 6 | 🛡️ 8.5 | Generic view supports task default key and member source-aware override |
| OpenCode popup delay | 🛡️ 5 | 🛡️ 8 | Popup-specific timeout and fail-soft source handling avoid blocking Claude transcript logs |
| Renderer duplicate reloads | 🛡️ 5 | 🛡️ 8.5 | Member section coalesces active loads and runs at most one pending reload |
| Live tracking not activated | 🛡️ 4 | 🛡️ 9 | Dedicated setMemberLogStreamTracking() activates TeamLogSourceTracker while popup stream is mounted |
| Tracking consumer leak | 🛡️ 5 | 🛡️ 8.5 | Team-level member_log_stream consumer uses existing reference counting and cleanup tests |
| Stale discovery after launch | 🛡️ 5 | 🛡️ 8 | forceRefresh on log-source-change bypasses 30s finder discovery cache |
| Huge single tool output | 🛡️ 4 | 🛡️ 8 | Content-char budgets protect renderer beyond message/chunk count |
| Renderer Date shape drift | 🛡️ 6 | 🛡️ 8.5 | HTTP client already revives ISO dates, remaining risk is mostly test fixtures |
| DTO source union drift | 🛡️ 5 | 🛡️ 9 | Standalone MemberLogStreamResponse avoids mutating task BoardTaskLogStreamResponse.source semantics |
| Member popup fallback regression | 🛡️ 6 | 🛡️ 9 | Gate at MemberDetailDialog, leave shared MemberLogsTab and task Execution Sessions untouched |
| Resolved member runtime/lane type drift | 🛡️ 6 | 🛡️ 9 | Shared type explicitly declares runtime/lane fields already present at runtime |
| OpenCode bridge call churn | 🛡️ 5 | 🛡️ 8 | Short source-local TTL and in-flight join mirror task source behavior |
| Board/task sanitization leakage | 🛡️ 5 | 🛡️ 9 | Extract provider-neutral message hygiene, keep board JSON cleanup scoped to task stream |
| IPC option typo drift | 🛡️ 5 | 🛡️ 9 | Unknown member-stream options are rejected with an allow-list before service dispatch |
| Participant/source identity drift | 🛡️ 5 | 🛡️ 9 | Actor participants stay actor-based; provider/session labels come from segment.source |
| Since-only replacement reload | 🛡️ 5 | 🛡️ 9 | Renderer does full bounded reloads until partial-response merge is explicit |
| Generic view side effects | 🛡️ 5 | 🛡️ 9 | View stays render-only; containers own API calls, gates, tracking and fallback |
8. Test Additions From Research
Add these tests beyond the original plan:
- Core domain policy tests run without main/preload/renderer imports.
- Core application use-case tests use fake source/cache/clock/logger ports and no concrete provider adapters.
- Feature IPC tests exercise
registerMemberLogStreamIpc()/remove handler through feature main input adapter. - Feature preload tests exercise
createMemberLogStreamBridge()with contracts-only dependencies. - Renderer UI tests pass props/view models and do not mock
@renderer/apior@renderer/store. - Targeted lint for
src/features/member-log-streampasses under generic feature boundary rules. - App shell integration imports only
@features/member-log-stream/main,@features/member-log-stream/preload,@features/member-log-stream/rendereror@features/member-log-stream/contracts. ClaudeMemberTranscriptStreamSourceusesfindRecentMemberLogFileRefsByMember, notfindMemberLogPaths.- It dedupes cumulative subagent refs before parsing and caps after dedupe.
- It augments attribution with requested member names so removed/historical members can still resolve.
- It enforces global
maxSegmentsandmaxChunks. - It enforces
maxSourceMessagesandmaxMessagesPerSegment. - It enforces content-char budgets without mutating parser cache arrays.
- It trims oversized message windows without orphaning tool results.
- It truncates oversized content while preserving ids needed for tool linking.
- It preserves ordinary JSON-looking tool outputs unless the content budget requires truncation.
- It does not apply board/task JSON cleanup globally to member stream messages.
- It creates stable segment ids from
sessionId/fileFingerprint/startTimestamp. - It returns
MemberLogStreamSegment.sourcelabels without absolute file paths. - It hashes file fingerprints and does not expose absolute paths in ids/warnings/metadata.
- It does not share parser cache with task stream.
- It uses a member-owned parser so member
parseFiles().retainOnly()cannot clear task-streaminFlightparse entries. - Task-stream parse in-flight dedupe still works while member stream parses a different file set.
- First-PR Codex skipped adapter does not call
CodexNativeTraceReader.readTaskRuns()and does not scanprocessed/<team>/<task>directories. MemberLogFileRefoptionalkind/sizeBytes/messageCountremains backward compatible with advisory callers.MemberLogFileRef.messageCountis not computed by full parsing insideTeamMemberLogsFinder.TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()keeps legacy numeric/null third-arg behavior.TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()supports object options{ mtimeSinceMs, forceRefresh }.TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()passesforceRefreshthrough todiscoverProjectSessions().TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()appliesmtimeSinceMsto lead transcript refs.OpenCodeRuntimeProjectionMapperpreserves tool calls, tool results, content blocks,sourceToolUseID,sourceToolAssistantUUID,toolUseResult,isMeta, and sanitized text content.OpenCodeTaskLogStreamSourceandOpenCodeMemberRuntimeStreamSourceboth callOpenCodeRuntimeProjectionMapper.- Mapper fixtures prove non-string content blocks are not dropped like the stall-monitor mapper would drop them.
ResolvedTeamMemberexposes runtime/lane fields in TS:providerBackendId,selectedFastMode,resolvedFastMode,laneId,laneKind,laneOwnerProviderId.MemberLogStreamSectionpasseslaneIdfor OpenCode member only whenlaneOwnerProviderId === 'opencode'.MemberLogStreamSectiondoes not pass stale lane-like fields for non-OpenCode members.MemberLogStreamSectionenablessetMemberLogStreamTracking(teamName, true)while mounted and disables it on unmount.MemberLogStreamSectiondisables tracking for the previous team whenteamNamechanges.MemberLogStreamSectioncoalesces duplicate background reloads while one request is active.OpenCodeMemberRuntimeStreamSourcejoins duplicate in-flight bridge calls.OpenCodeMemberRuntimeStreamSourcebypasses completed cache onforceRefreshbut still joins in-flight work.MemberDetailDialogrendersMemberLogStreamSectionwhen renderer gate is on.MemberDetailDialogrenders oldMemberLogsTabwhen renderer gate is off.MemberDetailDialogfirst-load stream error keeps Logs active and shows explicit old logs fallback.ExecutionSessionsSectionremains unchanged and still renders legacyMemberLogsTab.MemberLogStreamSectionreloads on same-teamlog-source-changeandtask-log-change, but nottool-activity.MemberLogStreamSectionpassesforceRefresh: trueforlog-source-changereloads only.MemberLogStreamSectiondoes not passsinceduring renderer background replacement reloads in the first PR.ExecutionLogStreamViewtests cover Date-shaped chunks and document whether JSON-like chunk normalization is supported.ExecutionLogStreamViewpreserves task tail-growth expanded state with task default keys.ExecutionLogStreamViewsupports a member source-awarebuildSegmentRenderKeyoverride to avoid provider/session collisions.ExecutionLogStreamViewpreserves unknown stream fields and membersegment.sourcewhile normalizing chunks.- Shared type tests or compile checks keep
MemberLogStreamResponsestandalone and keepBoardTaskLogStreamResponse.sourcetask-only. MemberLogStreamSectionrenders provider/session/lane labels fromsegment.source, not from participant labels.MemberLogStreamSectionkeeps actor participant identity stable for one selected member across multiple providers/sessions.- A member stream segment with
actor.memberNameand noProcess[]still renders tool/output items. - IPC rejects invalid
since, clampslimitSegments, and accepts colon-containing lane ids. - IPC accepts
primaryas a lane id. - IPC preserves exact lane id casing/punctuation when passing options to the service.
- IPC rejects lane ids with control characters, NUL, newline,
/,\or length over 256. - IPC accepts boolean
forceRefreshand rejects non-boolean values. - IPC rejects unknown
getMemberLogStreamoption keys before dispatching to the service. - IPC validation helpers do not import non-exported
ValidationResult. - IPC registers/removes feature
MEMBER_LOG_STREAM_GETchannel. - IPC registers/removes feature
MEMBER_LOG_STREAM_SET_TRACKINGchannel. - IPC handler calls the feature facade/use case with normalized options.
- IPC tracking handler validates
teamNameand booleanenabled, then maps toTeamLogSourceTrackerconsumermember_log_stream. TeamLogSourceTrackertreatsmember_log_streamas a separate consumer fromtask_log_stream.- Multiple member stream mounts for one team keep the watcher alive until all member consumers disable tracking.
- Disabling
member_log_streamdoes not stop tracking whiletask_log_stream,change_presence,tool_activityorstall_monitoris still active. - Existing
initializeTeamHandlers()positional setup remains unchanged for task stream and exact-log services. - Existing task stream IPC handler test remains a wiring smoke test proving the feature did not move legacy handler ownership.
- Browser-mode
httpClientreturns a complete empty member stream response. ClaudeMultimodelBridgeService.getOpenCodeTranscript()appends--laneonly when provided.ClaudeMultimodelBridgeService.getOpenCodeTranscript()honors member-popuptimeoutMs.- OpenCode timeout/runtime missing becomes a warning and does not fail Claude transcript rendering.
- OpenCode ambiguity error becomes warning, not failed member popup.
- Codex member stream coverage is
skippedunless explicit partial trace phase is implemented.
Final Recommendation
Грамотная реализация варианта 2:
🎯 8.5 🛡️ 8.5 🧠 6
Примерно 1500-2300 LOC вместе с тестами.
First implementation should be:
- Claude transcript stream complete enough for selected member;
- OpenCode runtime stream lane-aware and safe;
- Codex native explicitly skipped or partial-only behind separate follow-up;
- old member logs fallback kept;
- fallback decision kept at
MemberDetailDialog, not inside sharedMemberLogsTab; - task
Execution Sessionskept unchanged; - feature gated;
- member stream tracking activated while popup Logs stream is mounted;
- backend budget enforced before renderer;
- renderer duplicate reloads coalesced while one member stream request is active;
- provider-neutral message hygiene extracted separately from board/task JSON sanitization;
- pair-aware message trimming used for oversized single segments;
- content-size budgets applied before chunk build;
- cumulative subagent refs deduped before parse;
- requested member names added to finder attribution set;
- finder third arg extended through backward-compatible numeric/null/object parser;
mtimeSinceMsapplied to lead transcript refs too;src/features/member-log-streamfollows canonical feature layout with contracts/core/main/preload/renderer public entrypoints;- app shell imports only public feature entrypoints and does not deep-import source adapters or use case internals;
- member segments include safe provider/session metadata;
- member stream uses source-aware segment render keys;
- OpenCode projection conversion extracted into shared mapper;
- OpenCode runtime transcript calls protected by source-local TTL and in-flight join;
- feature IPC registration is separate from legacy
initializeTeamHandlers()service injection; - browser fallback returns a complete empty stream response;
log-source-changereloads useforceRefresh;- source-port architecture used inside the canonical member-log-stream feature slice;
- no orchestrator code changes.
Это дает максимальный прирост UX с низким риском неправильных логов.