84 KiB
Member Log Stream V2 Implementation Plan
Goal
Сделать в попапе участника новый формат логов, визуально и поведенчески близкий к Task Log Stream, но со scope "все доступные логи выбранного участника", а не "логи конкретной задачи".
Ключевой продуктовый ответ:
- Показываем логи только выбранного участника, а не всех агентов команды.
- Если у выбранного участника есть безопасно доступные Claude/OpenCode/Codex источники, они могут быть показаны в одном stream.
- Вариант 2 не обещает абсолютно полный provider-wide audit log для всех runtime. Для этого нужен вариант 3.
- Старый
MemberLogsTabне удаляем на первом шаге. Он остается fallback, чтобы снизить риск регрессий.
Decision
Выбран вариант 2:
🎯 8.5 🛡️ 8.5 🧠 6
Оценка изменений: примерно 1500-2300 строк вместе с тестами, если делать canonical feature slice, аккуратную source-port архитектуру, dedupe cumulative subagent snapshots, вынести OpenCode projection mapper, покрыть fallback boundary, добавить member tracking activation, OpenCode/renderer in-flight protection и аккуратно отделить provider-neutral message hygiene от board/task sanitization.
Почему не frontend-only:
- Старый
MemberLogsTabработает с session summaries и раскрытиемMemberExecutionLog. - Новый stream требует готовый normalized response: participants, segments, chunks, source metadata.
- Делать это в renderer означало бы тащить parsing/composition туда, где уже нет нужных main-process primitives и где выше риск UI freeze.
Почему не вариант 3:
- Полный member-wide Codex native stream требует отдельного trace index/read path по owner/member без taskId.
- Полный OpenCode multi-lane stream требует либо безопасного list-all-lanes contract, либо явного lane resolution в desktop.
- Это полезно, но заметно повышает риск смешать чужие логи или показать неполную картину как полную.
Options Considered
| Вариант | Оценка | Пример LOC | Что делает | Главный риск |
|---|---|---|---|---|
| 1. Frontend-only reuse старых member logs | 🎯 6 🛡️ 5 🧠 3 | 150-250 | Renderer берет getMemberLogs и пытается отрисовать похоже на stream |
Старый формат данных не равен stream, легко получить UI-псевдопаритет без реального качества |
| 2. Canonical feature slice плюс source ports | 🎯 8.5 🛡️ 9 🧠 7 | 1500-2300 | src/features/member-log-stream owns contracts/core/source ports/renderer, app shell only integrates it |
Нужно держать provider sources маленькими, dedupe cumulative logs, жестко ограничить response и не мигрировать task logs в этом PR |
| 3. Maximum provider/runtime log stream | 🎯 6 🛡️ 6 🧠 9 | 1000-1800+ | Отдельный provider-wide member log index для Claude/OpenCode/Codex | Высокий риск ошибок в Codex/OpenCode attribution и долгий rollout |
Current Code Facts
Точки интеграции в claude_team:
src/renderer/components/team/members/MemberDetailDialog.tsxсейчас показывает<MemberLogsTab teamName={teamName} memberName={member.name} />.src/renderer/components/team/members/MemberLogsTab.tsxвызываетapi.teams.getMemberLogs(...)и рендерит старые session logs.src/renderer/components/team/taskLogs/TaskLogStreamSection.tsxвызываетapi.teams.getTaskLogStream(teamName, taskId)и рендерит новый stream черезMemberExecutionLog.TaskLogStreamSectiononly subscribes toonTeamChange;TaskLogsPanelseparately enablessetTaskLogStreamTracking. Member popup needs its own tracking activation.TeamLogSourceTrackertracks activity only while at least one consumer is active. Its consumer counts are per team and per consumer name, so member stream should add a semanticmember_log_streamconsumer instead of starting a separate watcher layer.TaskLogStreamSection.normalizeResponse()preserves only known task response fields, so a reused copy can drop member fields likecoverage,warnings,metadata,truncated,generatedAtandsegment.source.BoardTaskLogParticipantis actor identity, not source identity. Provider/session/lane labels for member logs must stay inMemberLogStreamSegment.source, not in fake provider-specific participants.src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.tsвозвращаетBoardTaskLogStreamResponse.src/shared/types/team.tsуже содержитBoardTaskLogParticipant,BoardTaskLogSegment,BoardTaskLogStreamResponse.src/shared/types/team.tsуже содержитTeamMemberSnapshot.providerBackendId,selectedFastMode,resolvedFastMode,laneId,laneKind,laneOwnerProviderId, ноResolvedTeamMemberпока не объявляет эти поля.src/renderer/store/slices/teamSlice.tsвbuildResolvedMember()возвращает{ ...snapshot, status, messageCount, lastActiveAt }, поэтому runtime object уже несет эти поля. Это почти type-contract change, а не новая data mapping ветка.src/main/services/team/TeamMemberLogsFinder.tsуже умеет искать member logs и содержит attribution precedence.src/main/services/team/taskLogs/exact/BoardTaskExactLogStrictParser.tsумеет strict-parse JSONL transcript files с cache и concurrency.BoardTaskExactLogStrictParser.parseFiles()вызываетcache.retainOnly(uniquePaths), а underlyingBoardTaskActivityParseCache.retainOnly()удаляет не только parsed cache, но иinFlightentries вне текущего набора файлов. Поэтому parser instance нельзя шарить между task stream и member stream без изменения cache ownership.src/main/services/team/taskLogs/exact/BoardTaskExactLogChunkBuilder.tsстроитEnhancedChunk[], которые уже понимаетMemberExecutionLog.src/main/services/runtime/ClaudeMultimodelBridgeService.tsуже имеетgetOpenCodeTranscript(...), но пока безlaneId.CodexNativeTraceReader.readTaskRuns()is task-keyed and scansprocessed/<team>/<task>plus optionalincoming/<team>/<task>, then caps recent candidates before parsing. It has no member-wide owner index.CodexNativeTraceProjectoronly projects native tool events (toolSource === 'native'), not full Codex chat/runtime history.src/main/ipc/teams.ts,src/preload/index.ts,src/preload/constants/ipcChannels.tsуже имеют аналогичные handlers/channels дляgetTaskLogStream.initializeTeamHandlers()сейчас принимает много optional dependencies позиционно, иtest/main/ipc/teams.test.tsпередает их в этом порядке. Новую feature нельзя встраивать туда как очередной owned team service.src/renderer/api/httpClient.tsимеет browser-mode stub дляgetTaskLogStream; для member stream нужен такой же safe fallback, иначе можно сломать browser compile/test path.findRecentMemberLogFileRefsByMember()сейчас принимает третий аргумент только как numeric/nullmtimeSinceMs, не object options; не передаетforceRefresh; добавляет lead transcript до mtime filter; и возвращает refs безkind/sizeBytes/messageCount.- В OpenCode сейчас есть два mapper-like
toParsedMessage(): richer private mapper вOpenCodeTaskLogStreamSourceи более lossy mapper вOpenCodeTaskStallEvidenceSource. Канонический источник для shared mapper - task stream source, не stall monitor.
Дополнительный deep research зафиксирован в:
docs/team-management/member-log-stream-v2-research-addendum.md.
Самые важные уточнения оттуда:
- для Claude/member transcript использовать
TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember(), неfindMemberLogPaths(); - для OpenCode передавать
laneIdиз member snapshot вgetMemberLogStreamoptions; ResolvedTeamMembertype нужно явно расширить runtime/lane fields:providerBackendId,selectedFastMode,resolvedFastMode,laneId,laneKind,laneOwnerProviderId;- Codex native в варианте 2 должен быть skipped или partial native-tool trace, но не "полный Codex log";
- member stream source stack должен иметь отдельный
BoardTaskExactLogStrictParser, потому чтоparseFiles().retainOnly()вычищает cache иinFlightparse entries вне текущего набора файлов. - архитектурно лучше выбрать canonical feature slice:
src/features/member-log-streamowns contracts/core/application/source ports/renderer, while existing team surfaces remain thin app-shell integration points; - response нужно ограничивать на backend:
maxTranscriptFiles=40,maxSegments=30,maxChunks=250,maxSourceMessages=1200,maxMessagesPerSegment=300,openCodeMessageLimit=400; laneIdнельзя валидировать как member name, нужен отдельный optional validator с поддержкойsecondary:opencode:<member>.findRecentMemberLogFileRefsByMember()dedupe только byfilePath, а cumulative subagent snapshots могут дублировать turn history;- лучше расширить
MemberLogFileRefoptionalkind/sizeBytesи dedupe subagent refs bymemberName + sessionIdдо parse; - OpenCode projection conversion сейчас private внутри task-specific source, поэтому нужен маленький shared mapper вместо копипаста.
- shared OpenCode mapper нужно извлекать из
OpenCodeTaskLogStreamSource; mapper в stall monitor не подходит как источник, потому что он теряет часть content/tool-result семантики. maxChunksнедостаточен как единственный budget, потому что один AI chunk может содержать сотни tool calls/messages;- member popup live refresh должен слушать same-team
log-source-changeиtask-log-change, но неtool-activity. setMemberLogStreamTracking()should map to a newTeamLogSourceTrackerconsumermember_log_stream. Reusing the task-named API would work technically but creates semantic coupling.- Codex native member stream remains skipped in first PR because current trace reader is task-first and current projector is native-tool-only.
- новый IPC handler нужно регистрировать feature-owned способом и не сдвигать existing positional dependencies в
initializeTeamHandlers(); - browser-mode API fallback должен возвращать полный empty
MemberLogStreamResponse, а не бросать ошибку.
Точки интеграции в orchestrator:
runtime transcriptуже принимает--team,--member,--lane,--projection-only,--limit.resolveOpenCodeSessionRecordForCli()специально требует--lane, если есть несколько session records для одногоteam/member.- Поэтому orchestrator менять кодом не нужно для варианта 2, если desktop начнет передавать
laneIdтам, где он надежно известен.
Target User Experience
В попапе участника раздел Logs должен выглядеть как новый stream:
- header остается понятным для member popup:
Logs, неTask Log Stream; - визуальный формат chunks, tool calls, message blocks и spacing должен быть как в
Task Log Stream; - описание source не должно говорить "Task-scoped";
- при пустом stream показываем спокойный empty state;
- при ошибке нового stream показываем fallback-кнопку или inline fallback на старый
MemberLogsTab; - для одного участника chips обычно не нужны;
- если у участника несколько sessions/lanes, segment headers должны оставаться видимыми, чтобы не превратить разные сессии в один непрозрачный блок.
⚠️ Важное поведение: stream не должен показывать "логи всех агентов вместе". Он показывает только выбранного member. Если внутри выбранного member есть разные provider/runtime источники, они могут быть объединены только после безопасной attribution/resolution.
Target Architecture
flowchart LR
A["MemberDetailDialog"] --> B["MemberLogStreamSection"]
B --> C["member-log-stream renderer hook"]
C --> D["feature preload/API bridge"]
D --> E["feature IPC getMemberLogStream"]
E --> S["GetMemberLogStreamUseCase"]
S --> F["MemberLogStreamBudget"]
S --> G["ClaudeMemberTranscriptStreamSource"]
S --> H["OpenCodeMemberRuntimeStreamSource"]
S --> I["CodexNativeMemberTraceStreamSource"]
G --> J["TeamMemberLogsFinder recent refs"]
G --> K["Ref dedupe by member/session/kind"]
G --> L["Dedicated BoardTaskExactLogStrictParser"]
G --> M["BoardTaskExactLogChunkBuilder"]
H --> N["ClaudeMultimodelBridgeService"]
H --> O["OpenCodeRuntimeProjectionMapper"]
N --> P["agent_teams_orchestrator runtime transcript --lane"]
I --> R["skipped coverage in first PR"]
S --> Q["MemberLogStreamResponse"]
Q --> B
Main-process feature adapters own source discovery, parsing, chunking, cache, provider fallback, response budget and truncation semantics. Renderer feature code owns loading state, visual rendering, source text and fallback UI.
🧭 Architecture decision: create a canonical src/features/member-log-stream slice in the first implementation. This feature spans process boundaries, owns merge/budget/provider policy, needs transport wiring and has a provider roadmap, so it matches docs/FEATURE_ARCHITECTURE_STANDARD.md.
Compatibility rule:
- do not migrate existing task log stream or legacy
MemberLogsTabin this PR; - app shell may keep small compatibility methods in
api.teamsif that is the least disruptive integration point, but those methods should delegate to feature-owned contracts/channels/use case; - outside callers import only feature public entrypoints:
@features/member-log-stream/contracts,@features/member-log-stream/main,@features/member-log-stream/preload,@features/member-log-stream/renderer; - provider logic must live behind source ports, not in IPC handlers or React components.
Expected feature layout:
src/features/member-log-stream/
contracts/
api.ts
channels.ts
dto.ts
index.ts
normalize.ts
core/
domain/
policies/
models/
application/
ports/
use-cases/
main/
composition/
adapters/
input/ipc/
output/sources/
output/presenters/
infrastructure/
preload/
renderer/
adapters/
hooks/
ui/
utils/
Architecture Compliance Checklist
This feature must follow docs/FEATURE_ARCHITECTURE_STANDARD.md exactly enough to be reviewable by structure, imports and tests.
Clean Architecture mapping:
contracts/:MemberLogStreamResponse, request options, API fragment, IPC channels and normalize helpers only.core/domain/: pure policies for merge order, dedupe keys, source coverage, truncation decisions and render-safe source metadata.core/application/:GetMemberLogStreamUseCase,SetMemberLogStreamTrackingUseCase, source ports, cache/clock/logger ports and budget models.main/adapters/input/ipc/: validate IPC input, reject unknown option keys, call use cases and normalize IPC output.main/adapters/output/sources/: Claude/OpenCode/Codex source adapters that implementMemberLogStreamSource.main/infrastructure/: TTL/in-flight cache helpers, parser wrappers, bridge/runtime clients and filesystem-specific helpers.main/composition/:createMemberLogStreamFeature(...)wires dependencies and exposes a small facade.preload/:createMemberLogStreamBridge(...)only invokes feature channels and depends on contracts.renderer/hooks/: loading, tracking, team-change subscription, reload coalescing and fallback state.renderer/adapters/: DTO-to-view mapping and small renderer-only normalization.renderer/ui/: presentational components only, no store/API/Electron access.
Dependency direction:
- core/domain imports no process, framework, adapter or infrastructure code.
- core/application depends on domain models and ports, not on main/preload/renderer.
- main adapters depend inward on core/application and contracts.
- renderer UI depends on contracts/view models and local props only.
- app shell imports only public feature entrypoints.
src/main/ipc/teams.ts,src/preload/index.tsandsrc/renderer/components/team/members/MemberDetailDialog.tsxmay integrate the feature, but must not own member stream policy.
SOLID guardrails:
- SRP: one class/module has one reason to change. Provider IO changes source adapters; merge/budget changes core policies/use case; transport changes IPC/preload; visual changes renderer UI.
- OCP: adding
CodexPartialMemberTraceSourcelater means adding a new source adapter and registering it, not changing renderer or the use case switch logic. - LSP: every
MemberLogStreamSourcemust return the same result contract withincluded,partialorskipped; no adapter should throw for expected provider absence. - ISP: keep narrow ports: source loading, tracking activation, clock/logger/cache. Do not pass
TeamDataServiceor renderer member objects through core. - DIP: use cases depend on source/cache/logger ports. Concrete
TeamMemberLogsFinder,ClaudeMultimodelBridgeService, parsers and trace readers stay in main adapters/infrastructure.
DRY guardrails:
- Reuse the stream renderer through a public
@features/member-log-stream/rendererentrypoint instead of copyingTaskLogStreamSection. - Extract the OpenCode projection mapper from
OpenCodeTaskLogStreamSourceonce and reuse it for task/member streams. - Extract provider-neutral message hygiene helpers once, but keep board/task JSON cleanup scoped to task logs.
- Do not duplicate DTOs in
src/shared/types/team.tsand feature contracts. Feature contracts are the owner. - Do not duplicate feature API methods across
api.teamsand feature bridge unless theapi.teamsmethod is a thin compatibility delegate.
Lint guardrails:
- The repo already has generic feature boundary rules in
eslint.config.jsforsrc/features/*. - If implementation needs stricter member-log-stream-specific messages, mirror the
recent-projectsfeature-specific guard rails. - Targeted verification should include
pnpm exec eslint src/features/member-log-stream --cache --cache-location .eslintcache --cache-strategy contentplus fullpnpm lintbefore merge.
Project Standard Traceability Matrix
This matrix is the implementation checklist for CLAUDE.md, docs/FEATURE_ARCHITECTURE_STANDARD.md, existing eslint.config.js feature rules and the src/features/recent-projects reference slice.
| Standard requirement | Member-log-stream implementation rule | Verification |
|---|---|---|
New medium/large cross-process features live in src/features/<feature-name> |
Create src/features/member-log-stream and keep legacy team files as integration points only |
rg "@features/member-log-stream" src/main src/preload src/renderer shows public entrypoint imports only |
contracts/ owns cross-process API |
DTOs, API fragment types, channel constants and normalize helpers live only in feature contracts | No duplicated member stream DTOs in src/shared/types/team.ts; contracts tests cover fallback shape |
core/domain/ is pure |
Merge order, dedupe keys, coverage state, budget/truncation policy and source metadata are pure functions/models | Domain tests plus feature-core-domain-guards |
core/application/ owns use cases and ports |
GetMemberLogStreamUseCase, SetMemberLogStreamTrackingUseCase, source/cache/clock/logger/tracking ports |
Use-case tests with fake ports; no Electron, Fastify, React, Zustand or child process imports |
main/adapters/input/ owns transport translation |
IPC adapter validates unknown option keys, exact optional laneId, response normalization and fail-soft errors |
Feature IPC tests register/remove feature channels without initializeTeamHandlers() ownership |
main/adapters/output/ owns source adapters |
Claude/OpenCode/Codex adapters implement MemberLogStreamSource and translate runtime data to core models |
Adapter mapping tests for Claude refs, OpenCode projection and Codex skipped coverage |
main/infrastructure/ owns concrete IO |
Parser wrappers, runtime bridge clients, TTL/in-flight cache and filesystem helpers stay outside core | Source adapters are thin around infrastructure helpers |
main/composition/ is the composition root |
createMemberLogStreamFeature(...) wires sources, use cases, cache, logger and tracker facade |
App main imports @features/member-log-stream/main, not feature internals |
preload/ is a thin bridge |
createMemberLogStreamBridge() invokes feature channels and depends on contracts |
Preload does not import main composition or renderer code |
renderer/hooks/ orchestrate interaction |
Loading, tracking activation, reload coalescing, fallback and team-change cleanup live in hooks | Hook tests cover mount/unmount, team changes, reload coalescing and browser fallback |
renderer/ui/ is presentational |
ExecutionLogStreamView and section UI receive props/view models only |
feature-renderer-ui-guards; no imports from @renderer/api, store, Electron or main |
| Public entrypoints only | External code imports only @features/member-log-stream/contracts, /main, /preload, /renderer |
feature-public-entrypoints-only and targeted eslint |
Reference feature shape matches recent-projects |
Public indexes mirror the reference pattern: contracts/main/preload/renderer expose only supported surface | Review tree against src/features/recent-projects before implementation PR |
Public API And Types
Add feature-owned contracts in src/features/member-log-stream/contracts, not by mutating task stream semantics:
export type MemberLogStreamProvider = 'claude_transcript' | 'opencode_runtime' | 'codex_native_trace';
export type MemberLogStreamSource =
| 'member_transcript'
| 'member_mixed_runtime'
| 'member_runtime_only'
| 'member_empty';
export interface MemberLogStreamCoverage {
provider: MemberLogStreamProvider;
status: 'included' | 'partial' | 'skipped';
reason?: string;
}
export interface MemberLogStreamWarning {
code:
| 'opencode_ambiguous_lane'
| 'opencode_missing_runtime_session'
| 'opencode_runtime_unavailable'
| 'opencode_runtime_timeout'
| 'codex_member_wide_not_supported'
| 'large_log_window_limited'
| 'segment_message_window_limited'
| 'message_content_limited'
| 'unreadable_transcript_file';
message: string;
}
export interface MemberLogStreamMetadata {
scannedTranscriptFileCount: number;
includedTranscriptFileCount: number;
droppedSegmentCount: number;
droppedChunkCount: number;
droppedMessageCount: number;
}
export interface MemberLogStreamSegmentSource {
provider: MemberLogStreamProvider;
label: string;
sessionId?: string;
laneId?: string;
messageCount?: number;
truncated?: boolean;
}
export interface MemberLogStreamSegment extends BoardTaskLogSegment {
source: MemberLogStreamSegmentSource;
}
export interface MemberLogStreamResponse {
participants: BoardTaskLogParticipant[];
defaultFilter: 'all' | string;
segments: MemberLogStreamSegment[];
source: MemberLogStreamSource;
coverage: MemberLogStreamCoverage[];
warnings: MemberLogStreamWarning[];
truncated: boolean;
generatedAt: string;
metadata: MemberLogStreamMetadata;
}
Important TS boundary:
- Do not make
MemberLogStreamResponse extends BoardTaskLogStreamResponse. BoardTaskLogStreamResponse.sourceis a task-only union:transcript, OpenCode task fallback values and Codex task trace values.- Reusing that response type would either reject member-specific source values or pollute task stream semantics with member values.
- Share the low-level render units instead:
BoardTaskLogParticipant,BoardTaskLogSegmentandEnhancedChunk. - Prefer feature contracts for
MemberLogStreamResponse; export them through@features/member-log-stream/contracts. - If
src/shared/types/api.tsneeds a temporary compatibility reference forapi.teams, import the feature contract type there or use a narrow adapter type. Do not duplicate DTO definitions insrc/shared/types/team.tsand feature contracts.
Internal budget:
interface MemberLogStreamBudget {
maxTranscriptFiles: number;
maxSegments: number;
maxChunks: number;
maxSourceMessages: number;
maxMessagesPerSegment: number;
maxTotalContentChars: number;
maxMessageContentChars: number;
maxToolResultContentChars: number;
openCodeMessageLimit: number;
openCodeTimeoutMs: number;
}
Extend finder metadata in a backward-compatible way:
interface MemberLogFileRef {
memberName: string;
sessionId: string;
filePath: string;
mtimeMs: number;
sizeBytes?: number;
messageCount?: number;
kind?: 'lead_session' | 'member_session' | 'subagent';
}
This is needed because cumulative subagent snapshots can duplicate history. Existing callers can ignore the optional fields.
Also adjust findRecentMemberLogFileRefsByMember() to include requested member names in the attribution knownMembers set. Syntax-only IPC validation is not enough for removed or historical members if they are no longer present in current config/meta/inbox.
Do this without breaking existing finder callers. TeamMemberRuntimeAdvisoryService and existing tests already pass the third argument as positional mtimeSinceMs, including numeric values and null.
type FindRecentMemberLogFileRefsOptions =
| number
| null
| {
mtimeSinceMs?: number | null;
forceRefresh?: boolean;
};
Rules:
- numeric third arg still means
mtimeSinceMs; nullstill means no mtime window;- object form is added for member stream and can pass
forceRefresh; - only object form should bypass
discoverProjectSessions()cache; mtimeSinceMsmust apply to lead transcript refs as well as member/subagent candidates;- do not migrate advisory callers in the same PR unless needed by tests.
Recommended default:
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,
};
Add IPC/preload API:
getMemberLogStream(
teamName: string,
memberName: string,
options?: {
limitSegments?: number;
since?: string;
laneId?: string;
forceRefresh?: boolean;
}
): Promise<MemberLogStreamResponse>
Add member stream tracking API:
setMemberLogStreamTracking(teamName: string, enabled: boolean): Promise<void>
Why this is separate from setTaskLogStreamTracking():
TaskLogStreamSectionlistens toonTeamChange, butTaskLogsPanelis what enablesTeamLogSourceTracker.- Member popup has no
TaskLogsPanelwrapper. - Reusing the task-named method from member UI would work mechanically, but it creates semantic drift.
- A dedicated member tracking API can map to the same underlying watcher with a separate
member_log_streamconsumer count.
Default behavior:
limitSegmentsdefaults to 30 and is clamped to1..80.sinceis optional and only used as a performance hint, but invalid dates should be rejected by IPC validation.- First renderer implementation should not pass
sincefor background reload replacement. Without an explicit incremental-merge contract, replacing the visible stream with a since-filtered partial response can hide older segments. laneIdis optional and used only for safe OpenCode runtime projection.forceRefreshis optional and should be used only by renderer background reloads afterlog-source-change.- Handler validates
teamNameandmemberName. - Handler validates
laneIdseparately frommemberName, because valid runtime lanes can contain:. - Handler must trim but otherwise preserve
laneId; do not lowercase or rewrite it. - Handler validates
forceRefreshas boolean when present. - Put new validators in
src/main/ipc/guards.tsas exported functions, or define local validator result types.ValidationResultis currently not exported fromguards.ts. - Handler should not reject deleted/removed members just because they are absent from current config. Old logs are still useful.
- Handler rejects unknown option keys, matching the stricter
TEAM_GET_DATAoptions policy. - Tracking handler validates
teamNameand booleanenabled.
Suggested lane validator:
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 };
}
Main Process Implementation
Create the application use case and source ports inside src/features/member-log-stream/core/application. Main-process source adapters live in src/features/member-log-stream/main/adapters/output/sources.
Existing task log stream services can still be reused as infrastructure dependencies where appropriate, but member stream orchestration must not live inside src/main/ipc/teams.ts or a monolithic src/main/services/team class.
Recommended internal source-port 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;CodexNativeMemberTraceStreamSource, but only as skipped coverage adapter in first PR.
Recommended responsibilities:
- service normalizes already-validated options and budget;
- source classes discover and load their provider/runtime data;
- service merges source results in deterministic provider order;
- service calls provider sources fail-soft through
Promise.allSettled()or equivalent per-source try/catch; - service joins identical active requests by team/member/lane/limit/since key, with no long-lived response cache in the first PR;
- service enforces
maxSourceMessages,maxMessagesPerSegment,maxSegmentsandmaxChunks; - service enforces content-size budgets before chunk build, because one huge tool result can freeze the renderer even when message count is low;
- service uses provider-neutral
ParsedMessagehygiene/truncation helpers and must not apply task/board-specific JSON payload cleanup across all member logs; - service sorts segments by timestamp ascending in the response;
- service normalizes participant key to selected member;
- service returns structured coverage/warnings instead of throwing for partial provider failures.
Claude source responsibilities:
- discover transcript files for selected member through
TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember(); - extend/use optional ref metadata
kindandsizeBytes; - ensure finder
mtimeSinceMsfilters lead transcript refs too, not only collected member/subagent candidates; - dedupe cumulative subagent refs by
memberName + sessionId, keeping largestmessageCountwhen available, otherwise largestsizeBytes, then newestmtimeMs; - do not parse every candidate just to compute
messageCount; usemessageCountonly when existing attribution logic already knows it cheaply, otherwise usesizeBytesas the cumulative snapshot proxy; - cap candidate refs by
maxTranscriptFilesafter dedupe; - strict-parse only candidate files;
- trim very large parsed files with pair-aware message truncation before chunk build;
- trim or summarize oversized message content/tool-result content before chunk build, preserving tool ids and result pairing;
- do not reuse
sanitizeJsonLikeToolResultPayloads()wholesale for member stream, because board/task-specific sanitization can hide legitimate JSON tool outputs; - build one segment per session/log file by default;
- populate segment
sourcemetadata with provider/session/message counts, but never absolute file paths; - include sidechain chunks through existing chunk builder.
OpenCode source responsibilities:
- use
laneIdwhen present; - call
ClaudeMultimodelBridgeService.getOpenCodeTranscript()with--lane,--limitand a popup-specific timeout; - keep a small source-local TTL cache and
inFlightjoin keyed by team/member/lane/limit, matching the task source pattern closely enough to avoid repeated bridge calls; - treat ambiguous lane as skipped provider with warning;
- treat runtime timeout/binary errors as skipped or partial provider warnings, not whole-popup failure;
- map projection output into
MemberLogStreamSegment[]through extractedOpenCodeRuntimeProjectionMapper.
Codex source responsibilities in first PR:
- return coverage status
skipped; - add warning
codex_member_wide_not_supportedonly when useful for diagnostics; - do not scan all Codex trace directories yet.
- do not call
CodexNativeTraceReader.readTaskRuns()from member stream, because it requires task ids and returns only task-keyed native tool traces. - if a follow-up adds partial Codex support, call it
codex_native_trace_partialand capmaxTaskDirs,maxTraceCandidatesandmaxTraceRunsbefore parsing.
Important implementation constraints:
- Do not reuse one
BoardTaskExactLogStrictParserinstance between task stream and member stream unless cache ownership is changed.parseFiles()currently callsretainOnly(), so sharing the parser can evict both task-stream parsed cache and task-streaminFlightparse dedupe. - Do not use
findMemberLogPaths()as the main source. It lackssessionId,mtimeMs, and sorted recent refs. - Do not directly import renderer code into main.
- Do not manually construct
EnhancedChunkifBoardTaskExactLogChunkBuildercan build it. - Do not use task-specific filtering rules for member stream. Member stream scope is source/member attribution, not task interval.
- Do not reuse
CodexNativeTaskLogStreamSourcefor member logs. It is task-owner scoped and relies onreadTaskRuns({ taskIds }). - Do not use
OpenCodeTaskLogStreamSourcedirectly for member logs. It is task-window and task-marker aware. - Do not copy
toParsedMessage()into member source. Extract generic OpenCode projection mapping fromOpenCodeTaskLogStreamSourceand let both task/member sources import it. Do not base the shared mapper onOpenCodeTaskStallEvidenceSource, because that mapper is intentionally narrower and does not preserve all task-stream rendering data. - Keep main handler fail-soft. A single unreadable transcript file should become a warning, not a failed popup.
- Keep response bounded. Any cap hit sets
truncated: trueand warninglarge_log_window_limited. - If a single file/session exceeds message budget, use
segment_message_window_limitedand drop the oldest message window before chunk build. - Do not add member stream policy or service ownership to
initializeTeamHandlers(). The feature should have its own main composition and IPC registration path.
Segment rules:
- Segment id should be stable across refreshes: include provider, normalized team/member, session id or hashed file fingerprint, first timestamp.
- Do not put absolute file paths in segment ids.
- Segment timestamps come from first/last parsed message.
- Empty parsed files are ignored.
- Very large files are parsed through existing stream parser, not loaded wholesale in renderer.
- If chunks are empty after normalization, skip segment.
- If total chunks exceed
maxChunks, keep recent useful chunks and mark response truncated. - Do not text-truncate inside
EnhancedChunkin first PR. - Do not leave orphan tool results after message trimming. Keep matching assistant tool call by
sourceToolUseID, or drop the orphan result. - Do not synthesize fake
Processentries for OpenCode. Its projection should render through normal tool/output chunks. - Do not rely on collapsed UI state for safety.
MemberExecutionLogkeeps AI groups expanded by default and expanded tool rows can render full content.
Transport And Composition Integration
This part is easy to underestimate. The feature is not only a service and renderer component; it must pass through feature contracts, preload, browser fallback and IPC registration without disturbing existing task-log handlers.
Required transport edits:
- Add feature channel constants in
src/features/member-log-stream/contracts/channels.ts, for exampleMEMBER_LOG_STREAM_GETandMEMBER_LOG_STREAM_SET_TRACKING. - Prefer feature channel names such as
member-log-stream:getMemberLogStreamover newTEAM_*constants. If a compatibility alias is needed, it should still point to feature-owned contracts. - Add
MemberLogStreamApiinsrc/features/member-log-stream/contracts/api.ts. - Add
createMemberLogStreamBridge()insrc/features/member-log-stream/preload. - Add feature IPC registration in
src/features/member-log-stream/main/adapters/input/ipc/registerMemberLogStreamIpc.ts. - Register/remove feature IPC handlers from app main composition through
@features/member-log-stream/main, not throughregisterTeamHandlers(). - Add
createMemberLogStreamFeature(...)insrc/features/member-log-stream/main/composition. - Instantiate the feature facade in
src/main/index.tsor the existing main composition root after finder/parser/bridge/tracker dependencies are available. - Expose renderer access through a feature-friendly API path, for example
api.memberLogStream. Ifapi.teams.getMemberLogStream(...)is kept for compatibility, it must be a thin delegate to the feature API. - Add browser-mode fallback returning a complete empty
MemberLogStreamResponse, notnulland not a thrown error. - Add browser-mode no-op fallback for member stream tracking.
- Add
member_log_streamtoTeamLogSourceTrackingConsumer. - Map feature tracking enable to
TeamLogSourceTracker.enableTracking(teamName, 'member_log_stream'). - Map feature tracking disable to
TeamLogSourceTracker.disableTracking(teamName, 'member_log_stream'). - Keep the tracker consumer team-scoped, not member-scoped.
TeamLogSourceTrackeralready scopes watch targets by team sessions and uses reference counts, so multiple open member popups for the same team should increment/decrement the samemember_log_streamconsumer count. - Cleanup must run for the exact team used on mount. If
teamNamechanges while the dialog stays mounted, disable tracking for the previous team before enabling it for the next team.
Important integration rule:
🎯 8.5 🛡️ 9 🧠 4
Примерно 80-180 LOC.
Use feature-owned composition:
const memberLogStreamFeature = createMemberLogStreamFeature({
logsFinder,
logSourceTracker,
runtimeBridge,
logger,
});
registerMemberLogStreamIpc(ipcMain, memberLogStreamFeature);
Do not insert member stream dependencies into initializeTeamHandlers(). Current tests and likely other setup code pass optional team services positionally; adding this feature there would create a false ownership boundary and risk confusing service assignment failures.
Alternative:
🎯 7 🛡️ 9 🧠 7
Примерно 250-450 LOC.
Refactor all existing initializeTeamHandlers() dependencies to a dependency object. This is cleaner long-term for legacy team IPC, but it is a separate IPC hygiene PR and should not be bundled unless the implementation intentionally pays that cost.
Browser fallback shape:
const emptyMemberLogStreamResponse: MemberLogStreamResponse = {
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,
},
};
This keeps browser-mode tests honest while making it obvious that real stream loading is Electron-only.
Renderer Implementation
Refactor the stream UI so TaskLogStreamSection is not copied wholesale.
Recommended shape:
- Extract a generic render-only component, for example
ExecutionLogStreamView. - It receives
title,description,stream,loading,error,emptyTitle,emptyDescription,teamName,forceSegmentHeaders,boundedHistoryNote, and optionalbuildSegmentRenderKey. - It must not import
api.teams,MemberLogsTab, feature gates, provider sources or task/member loading hooks. - It should receive already loaded
teamMembersfrom the container, so pure view tests do not need the app store. TaskLogStreamSectionkeeps task-specific loading logic and task-specific descriptions.- New
MemberLogStreamSectionowns member-specific loading logic and descriptions. ExecutionLogStreamViewshould be exported from@features/member-log-stream/rendereras a public render primitive if legacyTaskLogStreamSectionneeds to reuse it.MemberDetailDialogrendersMemberLogStreamSectionin the existing Logs tab area.MemberLogsTabremains available as fallback.- Do not modify
MemberLogsTabbehavior in the first PR. It is also used byExecutionSessionsSectionin task logs, so changing it would widen the regression surface. - Put the renderer feature-gate decision at the
MemberDetailDialogLogs tab boundary.MemberLogStreamSectionshould not import or own the legacyMemberLogsTab. - On first-load failure in the new stream, show the member stream error and an explicit legacy fallback panel below it. Do not silently replace the new stream with old logs, because that hides QA failures.
- Do not add virtualization in the first PR.
MemberExecutionLogcurrently renders all groups, so protection must come from backendmaxSegments/maxChunks. - If
stream.truncatedis true or warninglarge_log_window_limitedexists, show a short note that the popup is showing the recent bounded stream. - Keep
normalizeResponse()generic enough to convert every segmentchunksthroughasEnhancedChunkArray. - Generic response normalization must preserve the original stream shape with object spread. Do not reconstruct only task fields, because member-only metadata and
segment.sourceare part of the contract. - Keep participant identity actor-based. For a selected member, prefer one member participant plus per-segment
sourcelabels; do not create provider/session pseudo-participants just to show Claude/OpenCode/Codex labels. - If source filtering is needed later, add a separate source filter. Do not overload participant chips with provider identity.
- Preserve current task-stream render-key behavior by default:
participantKey:firstChunkId. This keeps expanded state stable when a task segment grows. - For member stream, pass a source-aware render key based on safe
segment.idplus first chunk id or start timestamp. This avoids collisions between providers/sessions for the same participant. - Keep date handling consistent with task stream. Electron IPC should preserve
Dateobjects through structured clone, but renderer tests and browser-mode stubs may use JSON-like strings. - If
ExecutionLogStreamViewstarts accepting JSON-like fixtures, add one shared chunk date normalizer instead of ad hocnew Date()calls in each component. - Keep
describeStreamSource()source-specific. Do not reuse task copy in member popup.
Member-specific UI text:
- Loading:
Loading member log stream... - Empty:
No log stream entries were found for this member yet. - Error:
Failed to load member log stream - Source description should mention member-scoped transcript/runtime logs, not task-scoped logs.
Live refresh:
- Enable feature tracking bridge while
MemberLogStreamSectionis mounted, then disable it on unmount. Ifapi.teams.setMemberLogStreamTrackingis kept for compatibility, it delegates to the feature bridge. - Do not rely on
TaskLogsPanelto keepTeamLogSourceTrackeractive. Member popup can be opened without task logs panel mounted. - Do not call
setTaskLogStreamTracking()from member UI. It would work technically, but it couples member stream lifecycle to task stream naming and tests. - Reuse the existing team change subscription path for now. Do not create a second event bus in this PR.
- Reload on same-team
log-source-changewithforceRefresh: trueto bypass the finder discovery cache. - Reload on same-team
task-log-change, because that is a log freshness signal and does not include memberName. - Do not reload on
tool-activity. - Debounce 500-750ms, slightly more conservative than task stream.
- Background reload replaces the full bounded response. Do not request a
since-only response from renderer until the service exposes a response merge contract. - Coalesce duplicate background reloads while one member stream request is active. Keep at most one pending reload and run it after the active request settles.
- Background reload with
forceRefresh: trueshould not start a parallel request if a request for the same member key is already running. - Do not clear old stream on background refresh failure.
Provider Coverage
| Provider/runtime | Variant 2 coverage | Reliability | Notes |
|---|---|---|---|
| Claude JSONL transcript | Good | 🛡️ 9 | Existing member attribution, recent refs, session ids and JSONL parsing are enough for first release |
| OpenCode runtime projection | Good if session/lane is resolvable | 🛡️ 8 | Safe when desktop passes laneId; ambiguous records are skipped, not guessed |
| Codex native trace | Explicitly skipped in first PR | 🛡️ 8 | Honest coverage is safer than partial trace presented as full Codex logs |
Claude transcript path:
- Use
TeamMemberLogsFinderas the source locator. - Do not re-implement member attribution in renderer.
- Trust existing precedence: process/team metadata over routing sender over teammate id over text mention.
OpenCode path:
- Extend
ClaudeMultimodelBridgeService.getOpenCodeTranscript()params with optionallaneId. - If desktop knows a safe lane for the member, pass
--lane. Renderer can passmember.laneIdfromTeamMemberSnapshotthroughgetMemberLogStreamoptions. - If lane is unknown, calling without lane is allowed only as best-effort and must catch the orchestrator "pass --lane" error.
- On ambiguity, skip OpenCode runtime projection and add
opencode_ambiguous_lanewarning. - Do not merge multiple OpenCode lanes unless a later orchestrator contract exposes safe list-by-member output.
Codex native path:
- Do not claim full member-wide Codex support in variant 2.
- Existing trace headers include
ownerName, so partial member-wide native tool trace is possible as a follow-up. - Current
CodexNativeTraceReaderis task-first andCodexNativeTraceProjectoris native-tool-only. - First PR should return coverage status
skippedwithcodex_member_wide_not_supported. - Optional second PR can add
readMemberRuns()and label it as partial native tool trace, not full Codex logs.
Edge Cases And Mitigations
-
Member removed from team.
- Keep handler validation syntax-only for
memberName. - Allow logs finder to search historical logs.
- UI should still render if popup can open from historical data.
- Keep handler validation syntax-only for
-
Member renamed.
- Variant 2 uses exact current/historical name passed by caller.
- Do not guess aliases.
- Future work can add alias map from team config history.
-
Lead member.
- Use existing
isLeadMemberand participant role conventions. - Do not assume lead means non-sidechain in every transcript.
- Use existing
-
Multiple members with similar names.
- Do not use loose substring matching in the new service.
- Rely on
TeamMemberLogsFinderattribution signals.
-
OpenCode multiple lanes.
- Treat as ambiguous unless lane is known.
- Do not silently pick first record.
-
OpenCode runtime unavailable or slow.
- Missing binary returns transcript-only stream with warning, not hard error.
- Use popup-specific
openCodeTimeoutMs. - Return Claude transcript segments even when OpenCode times out.
- Add
opencode_runtime_timeoutwarning.
-
Huge member history.
- Limit transcript refs, source messages, per-segment messages, segment count and total chunks.
- Limit content characters as well as message count.
- Prefer most recent files first if finder supports mtime.
- Expose warning
large_log_window_limited. - Set
truncated: true. - Avoid rendering thousands of chunks in one popup.
7.1 Huge single tool result or output.
- A single parsed message can contain a massive tool result or markdown output.
- Apply maxMessageContentChars, maxToolResultContentChars and maxTotalContentChars before chunk build.
- Preserve tool call/result ids and replace truncated content with a clear bounded placeholder.
- Add message_content_limited warning and set truncated: true.
- Extract provider-neutral message hygiene helpers instead of applying board/task-specific JSON sanitization to every member log.
- JSON-looking Bash/API outputs should remain visible unless they exceed content budget, in which case they are truncated, not blanked.
-
Partial or malformed JSONL.
- Strict parser already skips invalid/unreadable records.
- Service should continue with remaining files.
-
Empty logs.
- Return
member_empty, no exception. - UI empty state should not look like failure.
- Return
-
Provider duplication.
- If OpenCode runtime projection duplicates transcript-derived entries, prefer transcript when there is a stable message uuid.
- If no stable uuid, do not attempt risky fuzzy dedupe in first release.
- Deduplicate cumulative Claude subagent snapshots before parse, using ref metadata.
-
Sorting across sessions.
- Response sorted ascending.
- Renderer may reverse for newest-first, matching task stream behavior.
- Mixed provider segments should be sorted by timestamp, not provider priority.
-
Cache invalidation.
- Cache key should include team, member, candidate file mtimes/sizes and runtime projection identity.
- Do not keep stale stream after log-source-change event.
- If no explicit service-level cache is added in first PR, document that dedicated parser/runtime caches are the only caches.
TeamMemberLogsFinderdiscovery cache has a 30s TTL, so member stream needs a force-refresh path afterlog-source-change.- OpenCode member runtime source should have its own short TTL and in-flight join so repeated live refreshes do not spawn duplicate
runtime transcriptcalls. forceRefreshmay bypass completed OpenCode cache, but should still join an existing in-flight call for the same team/member/lane/limit.
-
Renderer memory pressure.
- Keep
limitSegmentsplus backendmaxChunksand message budgets. - Avoid storing raw logs in React state.
- Store only normalized stream response.
- Defer virtualization until there is a real need for audit-sized popup history.
- Keep
-
Old fallback hiding new bugs.
- Fallback should be visible as fallback, not silently replace new stream in a way that masks errors during QA.
-
Browser mode API client.
src/renderer/api/httpClient.tsshould add unavailable stub like existinggetTaskLogStream.- Do not break browser-mode compile.
-
Unsafe or malformed
laneId.- Do not validate with
validateMemberName. - Allow colon-separated runtime lane ids.
- Reject NUL/newline and oversized values.
- Treat invalid
laneIdas IPC validation error, not as "no lane".
- Do not validate with
-
OpenCode mapper drift.
- Do not duplicate projection message mapping in the new member source.
- Extract generic mapper from
OpenCodeTaskLogStreamSource. - Keep task marker/window filtering inside the task source.
-
Tool call/result split by trimming.
- Truncate parsed messages before chunk build with pair-aware logic.
- If a retained tool result has
sourceToolUseID, retain the matching assistant tool call when possible. - If the matching call cannot fit the hard budget, drop the orphan result.
-
Live refresh noise.
- Member stream cannot filter
task-log-changeby member because the event has nomemberName. - Reload on same-team
task-log-changeonly while popup is mounted and debounced. - Skip
tool-activityreloads to avoid heavy parser churn.
- Member stream cannot filter
-
Feature IPC ownership drift.
initializeTeamHandlers()has many optional dependencies passed positionally.- Adding member stream there would both risk positional service drift and put new feature policy in legacy team IPC.
- Register member stream through feature-owned IPC/composition instead. Leave legacy dependency-object cleanup as a separate PR.
-
Browser mode fallback drift.
- Browser mode does not support Electron IPC, but the renderer API still must satisfy the feature API.
- Return a complete empty
MemberLogStreamResponsefrom the feature/browser fallback path. - Add a small test or compile check so shared type changes do not break browser fallback.
-
Historical member not in current config.
TeamMemberLogsFinderattribution usesknownMembers.- If the requested member is removed from config/meta/inbox, attribution can miss old files.
- Add requested member names into the finder attribution set for
findRecentMemberLogFileRefsByMember().
-
Mixed-provider segment headers.
- Plain
BoardTaskLogSegmenthas no provider/session label for renderer. - Use
MemberLogStreamSegment.sourcemetadata for safe labels. - Do not expose absolute transcript paths in segment metadata or warnings.
- Plain
-
Path leakage through stable ids.
- Segment ids are visible to renderer and can end up in test snapshots.
- Use a short hash/fingerprint of
filePath + mtimeMs + sizeBytes, not the absolute path itself. - Warnings should say counts/provider/session labels, not local filesystem paths.
-
Discovery cache stale after launch/log source changes.
- Finder discovery cache can keep old session ids briefly.
- Renderer should pass
forceRefresh: trueafter same-teamlog-source-change. - Service should pass that through with object-form finder options:
{ mtimeSinceMs, forceRefresh }. - For OpenCode runtime calls, keep source-local TTL/in-flight protection.
forceRefreshshould bypass completed cache only, not duplicate an active bridge call.
-
Validator placement/type drift.
src/main/ipc/guards.tscurrently keepsValidationResultlocal.- Prefer exporting concrete validators such as
validateOptionalRuntimeLaneId()andvalidateOptionalBooleanOption(). - If validators stay local in
teams.ts, avoid importing the privateValidationResulttype.
-
Date object assumptions in renderer.
groupTransformercalls.getTime()on chunk/step dates.- Existing task stream relies on Electron structured clone preserving dates.
- New shared tests should cover Date-shaped chunks and avoid accidentally feeding raw JSON date strings into
MemberExecutionLog. - If JSON-like chunks are needed in tests or browser mode, add one shared
normalizeEnhancedChunkDates()helper.
-
Fallback boundary drift.
MemberLogsTabis shared by the member popup and taskExecution Sessions.- Changing
MemberLogsTabitself can break task-log legacy browsing. - Keep the new-vs-old decision in
MemberDetailDialogonly. - Test renderer gate on/off at dialog level.
-
Hidden stream failure during QA.
- If the new stream error silently switches to old logs, failures can ship unnoticed.
- Show the error and a clearly labeled fallback.
- Background refresh failures can preserve the previous good stream, but initial load failures should be visible.
-
Resolved member lane type drift.
TeamMemberSnapshotalready has runtime/lane fields:providerBackendId,selectedFastMode,resolvedFastMode,laneId,laneKind,laneOwnerProviderId.ResolvedTeamMembercurrently does not declare them, even thoughbuildResolvedMember()spreads the snapshot.- Add explicit fields to the shared type instead of using casts in
MemberLogStreamSection. - Do not add duplicate renderer mapping unless tests prove a real runtime gap.
-
OpenCode bridge call churn.
- Live refresh can fire more than once while a popup is open.
- Mirror the task OpenCode source pattern with a short TTL cache and
inFlightjoin. - Cache both success and null briefly, and keep timeout warnings fail-soft.
-
Board/task sanitization leakage.
- Task stream has private helpers that clean board-tool JSON payloads.
- Member stream is wider and may legitimately contain JSON output from Bash/API tools.
- Reuse only provider-neutral clone/prune/truncate helpers, or rename board-specific cleanup so it cannot be accidentally applied globally.
-
Finder third-arg compatibility drift.
findRecentMemberLogFileRefsByMember()already has numeric/null positional callers.- Adding
forceRefreshas object-only third arg can break runtime advisory or live tests. - Add a compatibility parser that accepts
number,nulland{ mtimeSinceMs, forceRefresh }. - Member stream should use object form, existing advisory code can keep numeric/null form.
-
Lead transcript bypasses
mtimeSinceMs.- Current recent-ref finder applies
mtimeSinceMsto collected candidates, but lead transcript is pushed before that scan. - If selected member is lead and
sinceis used as a performance window, old lead logs can be returned unexpectedly. - Apply the same mtime filter to lead transcript stat before pushing the lead ref.
- Current recent-ref finder applies
-
Renderer duplicate reload pressure.
TaskLogStreamSectionignores stale responses withrequestSeqRef, but still can start parallel IPC calls.- Member stream requests are heavier because they can parse many files and call OpenCode runtime transcript.
- Coalesce active member stream loads in
MemberLogStreamSection, and join identical active requests inGetMemberLogStreamUseCase.
-
Segment render key collision.
- Task stream default render key uses
participantKey:firstChunkIdto preserve expanded state when a segment tail grows. - Member stream can have multiple providers/sessions for one participant, so that default can collide.
- Keep the task default, but let member stream pass a source-aware
buildSegmentRenderKey.
- Task stream default render key uses
-
Live tracking not activated.
TaskLogStreamSectionsubscribes toonTeamChange, butTaskLogsPanelenablessetTaskLogStreamTracking.- Member popup has no equivalent parent, so it can miss
task-log-changeandlog-source-changeevents if no other UI consumer is active. - Add
setMemberLogStreamTracking()and amember_log_streamtracker consumer. - Enable tracking only while
MemberLogStreamSectionis mounted and disable it on unmount.
-
IPC option typo drift.
- Existing
TEAM_GET_DATArejects unknown option keys before dispatching. - If
getMemberLogStreamsilently accepts unknown keys, typos inlaneId,sinceorforceRefreshcan create stale or ambiguous logs while tests still pass. - Use an allow-list:
limitSegments,since,laneId,forceRefresh. - Return
Unknown getMemberLogStream option: ${key}before calling the service.
- Existing
-
Participant/source identity drift.
BoardTaskLogParticipantdrives actor chips, color lookup and member labels.- If provider/session is encoded as participant identity, one selected member can appear as multiple people and filters become misleading.
- Keep
participantKeyactor-based, for examplemember:<normalizedName>or existing lead/unknown conventions. - Put provider/session/lane details only in
MemberLogStreamSegment.source.
-
Since-only reload hides older visible segments.
sinceis useful as a backend performance hint, but a since-filtered response is partial unless the API says otherwise.- Renderer background reload currently replaces state, it does not merge old and new segments.
- First PR should do full bounded background reloads, with
forceRefreshonlog-source-change. - Use
sinceonly in tests/service paths where the expected partial semantics are explicit.
-
Parser
retainOnly()in-flight eviction.BoardTaskExactLogStrictParser.parseFiles()callsretainOnly()before parsing.BoardTaskActivityParseCache.retainOnly()deletesinFlightentries outside the requested file set.- If task and member streams share a parser, one stream can remove the other's in-flight dedupe and cause duplicate reads or cache churn.
- Treat parser instance ownership as per stream source stack. Do not inject the existing task parser into the member stream source stack.
-
Wrong OpenCode mapper source.
OpenCodeTaskLogStreamSource.toParsedMessage()preserves content blocks, sanitized display text,toolUseResult,sourceToolUseID,sourceToolAssistantUUID,isMeta, subtype and level.OpenCodeTaskStallEvidenceSource.toParsedMessage()is narrower and maps non-string content to an empty array.- Extract the shared mapper from task stream source only; stall monitor migration can be a separate cleanup if needed.
-
Finder metadata overreach.
- Adding
messageCounttoMemberLogFileRefis useful only when that value is already cheaply known from existing attribution records. - Parsing every candidate file inside the finder just to count messages would double the IO before the strict parser runs.
- Keep finder metadata lightweight:
kind,sizeBytes,mtimeMsand optional existingmessageCount.
- Adding
-
Tracking consumer leaks or semantic drift.
TeamLogSourceTrackerkeeps reference counts by team and consumer name.- Reusing
task_log_streamfrom member popup is technically possible but makes task/member lifecycles indistinguishable in tests and diagnostics. - Add
member_log_streamas a separate consumer and ensure every mount enable has a matching unmount or team-change disable.
-
Generic renderer view grows side effects.
TaskLogStreamSectioncurrently owns fetch, debounce, stale-response protection and task copy.- If
ExecutionLogStreamViewimportsapi.teams, feature gates orMemberLogsTab, it stops being a reusable render primitive. - Keep the generic view pure: render normalized stream, participants, segment headers and state. Containers own loading, tracking and fallback.
-
Codex partial trace presented as complete.
CodexNativeTraceReaderis task-keyed andCodexNativeTraceProjectorkeeps only native tool events.- Reusing the task source for member logs would either require a task id or scan all task dirs, and the result still would not be full Codex chat history.
- First PR keeps Codex coverage
skipped. A later partial trace must be explicitly labeledcodex_native_trace_partial.
-
Runtime lane validation too strict or too loose.
validateMemberName()rejectssecondary:opencode:<member>because of:.- Do not lowercase or rewrite
laneId; orchestrator/session records may expect exact lane ids. - Add an optional lane validator that trims, accepts
primaryand colon-separated runtime lanes, rejects empty strings, NUL/newlines/control characters, path separators and length over 256.
-
DTO inheritance/source union drift.
BoardTaskLogStreamResponse.sourceis task-scoped and does not include member values.- Do not extend
BoardTaskLogStreamResponsefor member logs or broaden itssourceunion. - Define standalone
MemberLogStreamResponsewith shared participant/segment primitives, then test that task stream source values stay unchanged.
-
Feature boundary drift.
- The easiest regression is to put policy in
src/main/ipc/teams.ts, preload glue or React containers because those files already exist. - Keep the feature-owned policy in
src/features/member-log-stream/coreand provider IO in feature main adapters. - App-shell files may register, delegate or mount the feature, but they must not know merge order, budgets, source coverage rules or provider-specific parsing details.
- The easiest regression is to put policy in
Rollout And Feature Gates
Add separate gates:
- Main:
CLAUDE_TEAM_MEMBER_LOG_STREAM_READ_ENABLED, default true. - Renderer:
VITE_MEMBER_LOG_STREAM_UI_ENABLED, default true. - Put main gate in
src/features/member-log-stream/mainor feature contracts/helpers, following existing truthy/falsy parsing semantics. - Put renderer gate in
src/features/member-log-stream/rendereror a feature renderer helper. Do not readimport.meta.envoutside renderer code.
Fallback behavior:
- If main gate is off, handler returns empty response or unavailable result consistently.
- If renderer gate is off,
MemberDetailDialoguses oldMemberLogsTab. - If new stream errors, show error plus old logs fallback.
- If task log
Execution Sessionsrenders old session logs, it stays unchanged. It importsMemberLogsTabdirectly and should not be coupled to member stream rollout.
Rollback:
- Flip renderer gate off to restore old member logs UI.
- Flip main gate off if parsing causes unexpected load.
- No data migration needed.
Testing Plan
Main-process tests:
- Core domain policy tests cover merge order, source coverage status, budget decisions, dedupe keys and DTO-safe source metadata without main/preload/renderer imports.
- Core application use-case tests inject fake
MemberLogStreamSourceports, fake clock/logger/cache ports and verify fail-soft source behavior. - Core application tests prove adding a new source adapter does not require changing renderer or transport code.
GetMemberLogStreamUseCasereturns empty stream for no files.- It builds one or more segments from synthetic Claude JSONL files for selected member.
- It does not include logs attributed to another member.
- It skips unreadable/malformed files and records warnings.
- It respects
limitSegments. - It respects
maxTranscriptFiles,maxSegmentsandmaxChunks. - It respects
maxSourceMessagesandmaxMessagesPerSegment. - It respects
maxTotalContentChars,maxMessageContentCharsandmaxToolResultContentChars. - It sets
truncated: trueandlarge_log_window_limitedwhen budget is hit. - It records
message_content_limitedwhen content budget is hit. - It preserves JSON-looking tool output unless content budget requires truncation.
- It does not apply board/task-specific JSON payload sanitization globally.
- It records
segment_message_window_limitedwhen trimming one oversized segment. - It preserves tool call/result pairs when trimming, or drops orphan results.
- It preserves tool ids and source ids when truncating oversized content.
- It dedupes cumulative subagent refs by member/session before parse.
- It keeps segment ids stable across repeated calls.
- It does not expose absolute file paths in segment ids, metadata or warnings.
- It does not share parser cache with task stream service.
- It uses a member-owned
BoardTaskExactLogStrictParserso member parsing cannot callretainOnly()on the task stream parser or clear taskinFlightentries. - A task-stream parse in flight stays joined after a member-stream parse starts for a different file set.
- It handles removed member name without requiring current team membership.
- It augments finder attribution with requested member names for historical/removed members.
TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()can return optionalkindandsizeByteswithout breaking existing callers.TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()does not parse candidate transcript files just to computemessageCount.TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()keeps legacy numeric/null third-arg behavior.TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()supports object options{ mtimeSinceMs, forceRefresh }.TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()passesforceRefreshtodiscoverProjectSessions().TeamMemberLogsFinder.findRecentMemberLogFileRefsByMember()appliesmtimeSinceMsto lead transcript refs.- It merges source results in deterministic order.
- It joins identical active
GetMemberLogStreamUseCaserequests and does not add long-lived response cache. - It returns
MemberLogStreamSegment.sourcemetadata without leaking file paths. - Provider source tests cover Claude included, OpenCode skipped, Codex skipped.
- First-PR Codex skipped adapter does not call
CodexNativeTraceReader.readTaskRuns()and does not scan trace dirs. OpenCodeRuntimeProjectionMapperpreserves tool calls, tool results,sourceToolUseID,sourceToolAssistantUUID,toolUseResult,isMetaand sanitized text content.OpenCodeTaskLogStreamSourceandOpenCodeMemberRuntimeStreamSourceboth useOpenCodeRuntimeProjectionMapper.- Mapper fixture tests cover non-string content blocks so member stream does not regress to the narrower stall-monitor conversion.
OpenCodeMemberRuntimeStreamSourcejoins duplicate in-flight bridge calls for the same team/member/lane/limit.OpenCodeMemberRuntimeStreamSourcehonorsforceRefreshby bypassing completed TTL cache but still joining active in-flight work.ResolvedTeamMemberexposes runtime/lane fields without renderer casts:providerBackendId,selectedFastMode,resolvedFastMode,laneId,laneKind,laneOwnerProviderId.MemberLogStreamSectionpasseslaneIdonly for OpenCode members whose lane is owned by OpenCode.MemberLogStreamSectiondoes not pass stale lane-like fields for non-OpenCode members.
IPC/preload tests:
- Feature IPC tests register/remove feature channels through
registerMemberLogStreamIpc()and do not rely onsrc/main/ipc/teams.tsowning the handler. - Feature preload tests cover
createMemberLogStreamBridge()and import only feature contracts plus preload-safe helpers. getMemberLogStreamvalidatesteamNameandmemberName.- It accepts colon-separated
laneId, for examplesecondary:opencode:alice. - It accepts
primaryas a lane id. - It preserves exact lane id casing and punctuation when passing to the service.
- It rejects
laneIdwith NUL/newline/control characters/path separators or length over 256. - It clamps
limitSegmentsto1..80. - It rejects invalid
since. - It accepts boolean
forceRefreshand rejects non-boolean values. - It rejects unknown option keys before dispatching to
GetMemberLogStreamUseCase. - It registers and removes feature
MEMBER_LOG_STREAM_GETchannel. - It registers and removes feature
MEMBER_LOG_STREAM_SET_TRACKINGchannel. - It calls the feature facade/use case with normalized options.
- It maps member stream tracking to
TeamLogSourceTrackerconsumermember_log_stream. TeamLogSourceTrackersupportsmember_log_streamas a separate consumer fromtask_log_stream.- Multiple member stream mounts for the same team increment/decrement the same team-level consumer count without closing the watcher early.
- Disabling member stream tracking leaves the watcher alive if task stream or another consumer is still active.
- It does not add feature-owned services to existing positional
initializeTeamHandlers()dependencies. - Existing task stream IPC test still calls
BoardTaskLogStreamService.getTaskLogStream()with the correct arguments after adding separate feature IPC registration. - Browser-mode
httpClientreturns a complete emptyMemberLogStreamResponse. - Browser-mode
httpClientexposes a no-opsetMemberLogStreamTracking.
OpenCode tests:
ClaudeMultimodelBridgeService.getOpenCodeTranscript()passes--lanewhenlaneIdis provided.- It keeps old behavior when
laneIdis omitted. - It accepts popup-specific
timeoutMsand passes it toexecCli. - Member stream records
opencode_ambiguous_lanewhen orchestrator reports multiple records. - Member stream records
opencode_runtime_timeoutwithout failing Claude transcript segments. - Member stream includes OpenCode projection only for the selected member.
Renderer tests:
renderer/uicomponent tests pass props and never mock@renderer/apior@renderer/store.renderer/hookstests own API calls, tracking, team-change subscription and reload coalescing.- Feature renderer public entrypoint exports only supported components/hooks, and app shell does not deep-import
renderer/uiinternals. MemberLogStreamSectionshows loading, empty, error and populated states.- It renders chunks through the same execution-log renderer as task stream.
- It does not display
Task Log Streamcopy. - It keeps old stream during failed background refresh.
- It shows fallback old logs path when new stream fails.
MemberDetailDialogrendersMemberLogStreamSectionwhen renderer gate is on.MemberDetailDialogrenders oldMemberLogsTabwhen renderer gate is off.MemberDetailDialogfirst-load stream failure leaves the Logs tab active and shows explicit old logs fallback.- Existing
test/renderer/components/team/members/MemberDetailDialog.test.tsmocksMemberLogsTab; add a mock forMemberLogStreamSectioninstead of depending on the full stream component there. - It shows bounded-history note when
truncatedis true. - It reloads on same-team
log-source-changeandtask-log-change. - It sends
forceRefresh: trueforlog-source-changereloads. - It does not pass
sincefor renderer background replacement reloads in the first PR. - It calls the feature tracking bridge on mount and disables it on unmount.
- It disables member stream tracking for the previous team when
teamNamechanges. - It coalesces duplicate reloads while a member stream request is active.
- It does not reload on
tool-activity. - It keeps OpenCode segments with no
Process[]rendering normal tool/output items. TaskLogStreamSectionstill renders unchanged after extracting shared view.ExecutionLogStreamViewrenders task and member streams from the same segment/chunk shape without task-specific text.ExecutionLogStreamViewdoes not importapi.teams, feature gates,MemberLogsTabor store-bound loading hooks.ExecutionLogStreamViewhandles the same Date-shaped chunk objects thatTaskLogStreamSectionhandles today.ExecutionLogStreamViewnormalizes chunks without dropping unknown stream fields or member-specificsegment.source.ExecutionLogStreamViewpreserves task tail-growth expanded state with the existing default render key behavior.ExecutionLogStreamViewaccepts member source-awarebuildSegmentRenderKeyto avoid provider/session key collisions.- Shared type tests or compile checks prevent
MemberLogStreamResponsefrom extending or mutating taskBoardTaskLogStreamResponse.source. - Member stream participant identity remains actor/member identity; provider/session/lane labels render from
segment.source. - Member stream does not create provider-specific pseudo-participants for Claude/OpenCode/Codex.
Architecture/lint tests:
pnpm exec eslint src/features/member-log-stream --cache --cache-location .eslintcache --cache-strategy contentpasses.- Full
pnpm lintpasses with genericsrc/features/*guard rails. - No file outside
src/features/member-log-streamdeep-imports feature internals such ascore/**,main/adapters/**,preload/**orrenderer/ui/**. core/domainhas no imports from@main,@renderer,@preload, Electron, Fastify or child process modules.core/applicationimports only domain, contracts and ports, not concrete source adapters.renderer/uihas no imports from@renderer/api,@renderer/store,@mainor Electron.
Regression tests:
- Existing
BoardTaskLogStreamServicetests stay green. - Existing
TaskLogStreamSectiontests stay green. - Existing
MemberLogsTabtests stay green because the fallback component remains.
Suggested commands:
pnpm vitest test/features/member-log-stream/core/application/GetMemberLogStreamUseCase.test.ts
pnpm vitest test/features/member-log-stream/main/adapters/input/registerMemberLogStreamIpc.test.ts
pnpm vitest test/features/member-log-stream/renderer/MemberLogStreamSection.test.tsx
pnpm vitest test/main/services/team/TeamMemberLogsFinder.test.ts
pnpm vitest test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts
pnpm vitest test/renderer/components/team/taskLogs/TaskLogStreamSection.test.tsx
pnpm exec eslint src/features/member-log-stream --cache --cache-location .eslintcache --cache-strategy content
pnpm typecheck
Manual QA:
- Open participant popup for a Claude member with existing logs.
- Confirm UI looks like task stream but title/copy are member-specific.
- Open a member with no logs.
- Open a member while task activity/log-source events arrive.
- Test OpenCode member where one lane is resolvable.
- Test OpenCode member with ambiguous lane and confirm warning/fallback instead of wrong logs.
Acceptance Criteria
- Participant popup Logs section uses the new stream format.
- Selected member stream does not include unrelated teammates.
- Claude transcript logs render in the same chunk UI as Task Log Stream.
- OpenCode is included only when runtime session resolution is safe.
- Codex native limitation is visible in coverage/warnings and not misrepresented.
- Large histories are capped and marked truncated instead of freezing the popup.
- Oversized single segments are capped by message budget, not only chunk count.
laneIdis passed and validated separately from member name.- Cumulative subagent snapshot duplicates are not shown as repeated turns.
- Lead transcript refs respect
since/mtimeSinceMsthe same way member/subagent refs do. - OpenCode projection rendering is shared with task stream through a mapper, not copy-pasted.
- Shared OpenCode mapper is extracted from task stream source, not from the narrower stall-monitor mapper.
- Member stream parser ownership is isolated, including parsed cache and in-flight parse dedupe.
- Member stream reloads coalesce instead of launching parallel expensive IPC requests.
- Member stream activates log-source tracking while mounted, without depending on Task Logs panel.
- Member stream segment render keys are source-aware and do not collide across provider/session segments.
- Repeated OpenCode member refreshes join in-flight bridge calls instead of spawning duplicate
runtime transcriptprocesses. - Old
MemberLogsTabremains available as fallback. - No orchestrator code change is required for variant 2 unless desktop cannot pass existing
--lane. - Feature can be disabled with renderer/main gates.
src/features/member-log-streamfollowsdocs/FEATURE_ARCHITECTURE_STANDARD.md: contracts own DTO/channels, core is side-effect free, source ports live in application, provider IO lives in main adapters, renderer UI is presentational.- App shell imports only public feature entrypoints and does not deep-import feature internals.
- Clean Architecture dependency direction is preserved: core has no framework/runtime imports, adapters depend inward, and concrete provider IO is hidden behind ports.
- SOLID is visible in code structure: provider adapters are replaceable, use cases depend on abstractions, and UI/transport/provider parsing have separate reasons to change.
- DRY is visible in shared renderer primitive, shared OpenCode projection mapper and single owner for member stream DTOs.
- Tests cover the main risky branches above.
Explicit Non-goals
- Do not build a full provider-wide log audit system in variant 2.
- Do not add Codex member-wide trace index in variant 2.
- Do not merge all team agents into one participant popup stream.
- Do not remove old member logs UI in the first release.
- Do not silently pick an OpenCode lane when orchestrator says the session is ambiguous.
Implementation Order
- Create
src/features/member-log-streamwith contracts, core/application source ports, main composition, preload bridge and renderer public entrypoints. - Add feature-owned member stream DTOs,
MemberLogStreamSegment.sourcemetadata andResolvedTeamMemberruntime/lane fields. - Add feature IPC channel constants, feature preload bridge, browser-mode fallback and optional compatibility methods on
api.teamsif the existing member popup integration needs them. - Add IPC validation for
limitSegments,since,forceRefresh, optional runtimelaneIdand unknown option keys in the feature input adapter or shared guard helpers. - Add feature main facade/composition and register IPC handlers without shifting existing positional
initializeTeamHandlers()dependencies. - Add
MemberLogStreamBudgetand source-port interfaces. - Extend
MemberLogFileRefwith optionalkind/sizeBytes/messageCount, add backward-compatible finder options parsing, applymtimeSinceMsto lead refs, augment finder attribution with requested member names and add tests around cumulative subagent refs. Keep metadata cheap: do not parse every candidate file inside the finder just to computemessageCount. - Extract provider-neutral
ParsedMessagehygiene helpers without moving board/task JSON cleanup into member stream. - Add pair-aware message trimming helper for oversized transcript windows.
- Add content-size truncation helper that clones affected
ParsedMessageobjects before chunk build. - Add
ClaudeMemberTranscriptStreamSourceusing recent member refs, ref dedupe, message/content trimming and dedicated parser. - Extract
OpenCodeRuntimeProjectionMapperfromOpenCodeTaskLogStreamSourcewithout moving task-marker logic. Do not extract fromOpenCodeTaskStallEvidenceSource; its mapper is lossy for stream rendering. - Add
GetMemberLogStreamUseCasemerge/sort/budget/truncation layer, identical active request join, and export/instantiate it in feature main composition. - Add
CodexNativeMemberTraceStreamSourceas skipped coverage adapter. - Add renderer feature
ExecutionLogStreamViewrender-only extraction with shape-preserving normalization, source-aware key override andMemberLogStreamSectionwith tracking activation plus reload coalescing. - Wire
MemberDetailDialogbehind renderer feature gate with old fallback, importing only@features/member-log-stream/renderer. - Add OpenCode bridge
laneIdand timeout support. - Add
OpenCodeMemberRuntimeStreamSourcewith mapper reuse, short TTL cache and in-flight join. Leave stall-monitor mapper migration out of scope unless tests show a shared helper can be adopted without changing stall evidence behavior. - Add coverage/warnings and empty/error states.
- Add tests, including feature architecture import-boundary checks if the repo has a matching lint pattern.
- Manual QA with Claude and OpenCode teams.
- Decide separately whether to add partial Codex native tool trace or start variant 3.