agent-ecosystem/docs/iterations/iteration-08-exact-task-logs-reuse-existing-renderer.md

1768 lines
52 KiB
Markdown

# Iteration 08 - Exact Task Logs Reusing Existing Execution Renderer
> Historical note
> This document captures the planned scope and architecture at iteration time.
> It is not the source of truth for the final runtime contract.
This iteration adds a new **Exact Task Logs** subsection under task logs and intentionally reuses the existing execution-log renderer that already works well in the app.
The goal is **not** to invent a new log UI.
The real problem was never the renderer. The real problem was that the old task log discovery path was:
- session-centric
- heuristic-heavy
- not strict enough about what truly belongs to a task
The new explicit board-task linkage from iteration 07 already solved the **selection** problem.
This iteration uses that explicit linkage to feed a **task-scoped transcript slice** into the existing execution renderer.
That means:
- keep `Task Activity` as the compact, explicit summary feed
- add `Exact Task Logs` that visually looks like the current rich logs/execution cards
- keep `Execution Sessions` as a separate legacy/session-centric block
---
## Decision Record
### Top 3 options
1. **Reuse the existing execution renderer, but feed it a new explicit task-scoped filtered message slice** - `🎯 10 🛡️ 9 🧠 6` - примерно `550-950` строк
This is the chosen direction.
2. **Keep `Task Activity` only as summary, and add inline tool-details drawers per row** - `🎯 8 🛡️ 9 🧠 5` - примерно `350-650` строк
Simpler, but still not the same browsing experience the user already likes.
3. **Build a new custom task log renderer from scratch** - `🎯 3 🛡️ 5 🧠 9` - примерно `900-1600` строк
Rejected. This is a bicycle. It is slower, riskier, and likely worse than the existing renderer.
### Chosen direction
- Keep `Task Activity` as the compact explicit summary
- Add `Exact Task Logs`
- Render `Exact Task Logs` using the same existing execution-log rendering pipeline
- Build a new explicit task-scoped message-selection layer
- Reuse renderer primitives only, not legacy session-browsing containers
- Do **not** reuse the old heuristic session-finding logic as the source of truth
### Why this is the right direction
- The renderer already solves:
- tool call cards
- tool-result pairing
- text output display
- expandable items
- ordering and visual hierarchy
- The existing UX is already liked by the user
- Reusing the renderer lowers design risk
- The new explicit metadata gives us a reliable source for task scoping
The correct architecture is:
- **reuse the renderer**
- **replace the selection logic**
Not:
- reuse the old selection logic
- or rewrite the renderer
---
## Core UX Goal
Inside the task popup:
1. `Task Activity`
- short explicit summary rows
- compact semantic view
2. `Exact Task Logs`
- rich task-scoped transcript rendering
- same visual style as the current logs/execution UI
- exact tools, outputs, and grouped items
3. `Execution Sessions`
- current legacy/session-centric browser
- still useful for exploration
- no longer treated as the primary truth for task scoping
This gives users:
- a fast summary
- exact readable logs
- a fallback exploration view
---
## Important Clarification: Which Renderer We Actually Reuse
The correct renderer to reuse is **not** `CliLogsRichView`.
`CliLogsRichView` is for:
- stream-json CLI tails
- provisioning / live runtime logs
It expects a different source model.
The renderer path that matches the desired UX in task/session views is:
- `MemberExecutionLog`
- `transformChunksToConversation(...)`
- `enhanceAIGroup(...)`
- `DisplayItemList`
- `LastOutputDisplay`
That is the execution/session renderer family the user is referring to.
So the plan is:
- **reuse the execution renderer path**
- **not** the CLI stream-json renderer path
---
## Main Architectural Insight
The new exact log view must reuse the old renderer **without reintroducing old selection bugs**.
That means we cannot simply:
- ask `TeamMemberLogsFinder` for sessions
- reuse `MemberLogsTab`
- or render whole sessions again
We also should **not** blindly render entire AI response groups from the transcript.
Why:
- the same AI response can contain both relevant and unrelated tools
- if we render the entire unfiltered group, we leak unrelated actions back into the task view
- that would partially recreate the same problem we just solved
So the right architecture is:
1. Find exact task-linked source refs using explicit metadata
2. Resolve those refs into message-level anchors
3. Build a **filtered transcript slice** that contains only task-relevant messages/blocks
4. Convert that filtered slice into `EnhancedChunk[]`
5. Render with the existing execution renderer
The renderer stays the same.
The message-selection layer becomes explicit and strict.
---
## Scope
### Goals
- Add `Exact Task Logs` under `Task Logs`
- Reuse the current execution renderer style
- Build exact logs only from explicit task-linked transcript metadata
- Support:
- board-task tools
- lifecycle rows
- explicit board actions
- ambient execution text/output already linked to the task
- Avoid showing unrelated tools from the same session/AI response
### Non-Goals
- Replacing `Task Activity`
- Deleting `Execution Sessions`
- Retroactively fixing all historical logs without explicit metadata
- Reusing heuristic session overlap as primary selection
- Building a brand-new renderer
---
## Key Product Rules
### Rule 1 - `Task Activity` stays
`Task Activity` remains the compact summary feed.
It is still valuable because:
- it is fast to scan
- it shows actor/task relation cleanly
- it keeps the event-level summary readable
### Rule 2 - `Exact Task Logs` is the readable drill-down
`Exact Task Logs` is where users read the actual tool/output flow.
It should look and feel like the existing execution/log UI.
### Rule 3 - `Execution Sessions` remains legacy
`Execution Sessions` still exists because:
- it is useful for broad exploration
- it has previews and session browsing
- it can still show context the exact feed intentionally omits
But it is no longer the primary source for task scoping.
---
## Naming Decisions
### UI names
Use:
- outer section: `Task Logs`
- subsection 1: `Task Activity`
- subsection 2: `Exact Task Logs`
- subsection 3: `Execution Sessions`
This naming is explicit and easy to understand:
- summary
- exact logs
- session browser
### Service names
Use:
- `BoardTaskActivityRecordSource`
- `BoardTaskExactLogsService`
- `BoardTaskExactLogSummarySelector`
- `BoardTaskExactLogDetailSelector`
- `BoardTaskExactLogChunkBuilder`
### Shared DTO names
Use:
- `BoardTaskExactLogSummary`
- `BoardTaskExactLogDetail`
- `BoardTaskExactLogActor`
- `BoardTaskExactLogSource`
### Why this naming
- `Exact Task Logs` is user-facing and immediately understandable
- `BoardTaskActivityRecordSource` is more honest than `...Service` because this layer only supplies internal records
- `BoardTaskExactLogsService` is specific enough to avoid mixing with legacy task logs
- `Summary` + `Detail` is better than a single eager `Bundle` DTO because the renderer should load heavy exact details lazily
---
## Layered Design
This slice must preserve separation of concerns.
### 1. Explicit activity source layer
Responsibility:
- read explicit task-linked transcript metadata
- produce internal task activity records
Suggested main-only type:
```ts
type BoardTaskActivityRecord = {
timestamp: string
task: {
locator: BoardTaskLocator
resolution: 'resolved' | 'deleted' | 'unresolved' | 'ambiguous'
taskId?: string
displayId?: string
}
linkKind: 'execution' | 'lifecycle' | 'board_action'
targetRole: 'subject' | 'related'
actor: {
memberName?: string
role: 'member' | 'lead' | 'unknown'
sessionId: string
agentId?: string
isSidechain: boolean
}
actorContext: {
relation: 'same_task' | 'other_active_task' | 'idle' | 'ambiguous'
activeTask?: BoardTaskLocator
activePhase?: 'work' | 'review'
activeExecutionSeq?: number
}
action?: ParsedBoardTaskToolAction
source: {
filePath: string
messageUuid: string
toolUseId?: string
sourceOrder: number
}
}
```
This is **main-only** and not an IPC DTO.
Why this shape is better than `taskId: string`:
- it preserves unresolved and deleted states
- it avoids forcing early loss of locator semantics
- it lets both summary and exact-log readers consume the same lower-level record source
### 2. Exact-log summary selection layer
Responsibility:
- start from explicit activity records
- build lightweight exact-log summaries
- never parse transcript messages
This is the most important new layer in iteration 08 because it keeps initial popup load cheap and removes transcript parsing from the summary path entirely.
### 3. Exact-log detail selection layer
Responsibility:
- start from one summary + explicit activity records
- parse only the referenced transcript messages
- build one filtered task-scoped message slice for one requested exact detail
### 4. Chunk-building layer
Responsibility:
- turn the filtered message slice into `EnhancedChunk[]`
- keep the existing execution renderer happy
### 5. UI rendering layer
Responsibility:
- render exact bundle details with the current execution renderer
- not decide task membership
---
## Why We Need an Internal Record Layer First
It is tempting to let `BoardTaskExactLogsService` depend directly on `BoardTaskActivityEntry`.
That would be simpler in the short term, but it is the wrong dependency direction.
`BoardTaskActivityEntry` is a shared UI-facing DTO.
`Exact Task Logs` needs a lower-level source model.
So the better architecture is:
- `BoardTaskActivityRecordSource`
- main-only
- internal source of explicit task-linked facts
- `BoardTaskActivityService`
- maps records -> `BoardTaskActivityEntry`
- `BoardTaskExactLogsService`
- maps records -> lightweight exact-log summaries
- `BoardTaskExactLogDetailService`
- maps one exact summary + parsed transcript -> one renderable exact detail
This avoids coupling a new main-side service to a renderer DTO.
This is a strong SRP / DIP move and worth doing now.
### Critical reuse boundary
The new exact path must **not** introduce a second competing low-level reader for board-task transcript metadata.
That means:
- `BoardTaskActivityTranscriptReader` remains the single owner of:
- `boardTaskLinks[]` parsing
- `boardTaskToolActions[]` parsing
- file-level metadata parse caching for explicit board-task transcript metadata
- `BoardTaskActivityRecordSource` is extracted from the current summary path and becomes the single owner of:
- transcript metadata discovery
- task lookup and target-task filtering
- resolved internal activity records
- all of:
- `BoardTaskActivityService`
- `BoardTaskExactLogsService`
- `BoardTaskExactLogDetailService`
depend on the same `BoardTaskActivityRecordSource`
This is the desired dependency graph:
```ts
BoardTaskActivityTranscriptReader
-> BoardTaskActivityRecordSource
-> BoardTaskActivityService
-> BoardTaskExactLogsService
-> BoardTaskExactLogDetailService
```
This is explicitly **not** the desired graph:
```ts
BoardTaskActivityTranscriptReader -> BoardTaskActivityService
parseBoardTaskLinks again elsewhere -> BoardTaskExactLogsService
```
Why this matters:
- summary and exact views must agree on what explicit task-linked records exist
- task-resolution behavior must not drift between two separate low-level readers
- metadata parsing bugs must be fixed once
- caches should stay shared where possible
So iteration 08 should extract and reuse the existing explicit-record path.
It should not create another parallel JSONL-metadata reader just for exact logs.
---
## Data Flow
### End-to-end flow
1. Renderer asks for exact task logs:
```ts
api.teams.getTaskExactLogSummaries(teamName, taskId)
```
2. IPC calls:
```ts
BoardTaskExactLogsService.getTaskExactLogSummaries(teamName, taskId)
```
3. Service gets:
- active + deleted tasks from `TeamTaskReader`
- activity records from `BoardTaskActivityRecordSource`
4. Service derives exact-log summaries **from activity records only**
5. Renderer shows exact-log summary cards first
6. On expand, renderer asks for one exact detail:
```ts
api.teams.getTaskExactLogDetail(teamName, taskId, exactLogId, sourceGeneration)
```
7. Detail service:
- reloads the matching explicit summary anchor
- derives the minimal referenced file set for that one summary
- parses only those transcript files into strict `ParsedMessage[]`
- builds one filtered bundle slice
- converts it into `EnhancedChunk[]`
8. Renderer reuses `MemberExecutionLog`
---
## New Shared DTOs
### IPC DTOs
```ts
type BoardTaskExactLogActor = {
memberName?: string
role: 'member' | 'lead' | 'unknown'
sessionId: string
agentId?: string
isSidechain: boolean
}
type BoardTaskExactLogSource = {
filePath: string
messageUuid: string
toolUseId?: string
sourceOrder: number
}
type BoardTaskExactLogSummary =
{
id: string
timestamp: string
actor: BoardTaskExactLogActor
source: BoardTaskExactLogSource
linkKinds: ('execution' | 'lifecycle' | 'board_action')[]
} & (
| { canLoadDetail: true; sourceGeneration: string }
| { canLoadDetail: false }
)
type BoardTaskExactLogDetail = {
id: string
chunks: EnhancedChunk[]
}
```
### Why summaries + lazy detail is the safer v1 design
Repo-local finding:
- `Execution Sessions` already uses a lazy expand-to-load-details interaction model
- `EnhancedChunk[]` is an accepted IPC shape in this app
- but returning `EnhancedChunk[]` eagerly for every exact bundle would be materially heavier than the current execution-session path
So the safer v1 direction is:
- initial load -> lightweight `BoardTaskExactLogSummary[]`
- expand one row -> fetch one `BoardTaskExactLogDetail`
This keeps:
- initial popup payload smaller
- refresh cost lower
- parity with the existing interaction model the user already likes
### Why `canLoadDetail` is better than `hasRenderableDetail`
Summary stage no longer parses transcript content.
That is a feature, not a limitation:
- it keeps summary load cheap
- it prevents summary-stage parser drift
- it avoids lying with overconfident renderability claims
So the summary flag should be capability-oriented:
- `canLoadDetail = true` means the app has enough explicit anchor/source information to attempt detail loading
- it does **not** guarantee that strict detail reconstruction will succeed
- if `canLoadDetail = false`, the summary must not carry a meaningless `sourceGeneration`
If detail later fails because the transcript row is malformed or missing, returning `missing` is still correct.
### Source-generation coherence contract
Lazy summaries + detail introduce one real risk:
- summaries are loaded at time `T1`
- transcript files change
- detail is requested at time `T2`
- the same `exactLogId` may now refer to a different filtered slice or to nothing at all
So exact logs need an explicit coherence token.
Preferred response shape:
```ts
type BoardTaskExactLogSummariesResponse = {
items: BoardTaskExactLogSummary[]
}
```
Preferred detail result shape:
```ts
type BoardTaskExactLogDetailResult =
| { status: 'ok'; detail: BoardTaskExactLogDetail }
| { status: 'stale' }
| { status: 'missing' }
```
Why this is better than `null`:
- renderer can distinguish stale summary data from a genuinely missing bundle
- UI can refresh summaries automatically on `stale`
- debugging is easier than with a single ambiguous nullish path
### Why `sourceGeneration` belongs on each summary, not on the whole response
Earlier drafts used one response-level generation token for the whole task.
That is weaker.
Why:
- exact detail is loaded one bundle at a time
- one task can reference many transcript files
- one unrelated file mutation should not stale every open summary card
So the safer contract is:
- each `BoardTaskExactLogSummary` carries its own `sourceGeneration`
- detail validates against that per-summary generation
- the summaries response does not need a single coarse global generation token in v1
This narrows stale invalidation to the actual files that back one summary.
### Why not reuse global `TeamLogSourceTracker.logSourceGeneration` directly
Repo-local finding:
- `TeamLogSourceTracker` already computes a broad project-level `logSourceGeneration`
- that generation changes for any tracked transcript source movement
That pattern is useful, but it is too broad as the primary exact-log coherence token.
If exact logs reuse the global generation directly, then:
- an unrelated transcript file change can invalidate all open exact-log details
- exact detail requests become noisier and more frequently stale than necessary
So exact logs should use a **narrower source generation**:
- derive `sourceGeneration` from the exact summary source set used for one requested summary
- typically hash normalized `(filePath, size, mtimeMs)` for the referenced transcript files
### Why `linkKinds` is an array
One exact-log summary/detail can legitimately originate from multiple explicit links that collapse into the same rendered bundle.
Example:
- same tool call produced both `subject` and `related` links
- same transcript message had both an execution link and a board-action link relevant to the target task
The bundle should render once, not duplicate.
### File-local exact-detail boundary
Repo-local finding:
- existing tool/result linking in `SessionParser`, `ToolExecutionBuilder`, and the execution renderer pipeline works over one provided message slice
- bundle identity already includes `filePath`
- `MemberExecutionLog` itself only consumes `EnhancedChunk[]` and a display `memberName`
So v1 should keep a strict boundary:
- one exact summary belongs to one transcript file
- one exact detail request parses at most that summary's referenced file set
- no cross-file hunt for a missing paired `tool_use` or `tool_result`
This is the safer rule because cross-file pairing would immediately reintroduce guesswork and drift.
If a future transcript shape ever truly requires cross-file pairing, that should be a separate iteration with its own invariants and tests.
---
## Exact Selection Rules
This is the most critical part of the design.
### Principle
Select only what is explicitly attributable to the target task.
Never reintroduce broad session heuristics as the exact-log source.
### Critical anti-bug rule
The selector must work on **explicit source refs first**, and only then read transcript content.
It must never scan a transcript file first and try to rediscover task relevance from nearby content.
### Step 1 - Start from explicit activity records
Only records whose resolved target task matches the requested task are eligible.
### Step 2 - Derive exact message anchors
Each eligible record becomes one anchor candidate.
Suggested internal shape:
```ts
type BoardTaskExactLogAnchor =
| {
kind: 'tool'
filePath: string
sessionId: string
toolUseId: string
sourceMessageUuid: string
}
| {
kind: 'message'
filePath: string
sessionId: string
messageUuid: string
}
```
### Step 3 - Collapse multiple records into stable bundles
Deduplicate anchors aggressively:
- same `filePath + toolUseId` -> one tool bundle
- same `filePath + messageUuid` -> one message bundle
### Anchor precedence rule
If both anchors exist for the same source:
- tool anchor: `filePath + toolUseId`
- message anchor: `filePath + messageUuid`
then the **tool anchor wins** and the message anchor must not create a second bundle for the same tool execution.
This is required because one task-linked tool result can also carry an explicitly linked message UUID.
Without precedence, the same action can render twice:
- once as a tool bundle
- once as a message bundle
That would be a real regression.
This avoids duplicate rendering when:
- multiple links point to the same tool
- link/unlink emits both subject + related rows
- one activity message contains multiple links for the same target task
### Step 4 - Build summaries from anchors only
Summary stage must stop here.
For each surviving anchor:
- compute stable summary identity
- aggregate `linkKinds`
- derive actor label and source metadata
- compute per-summary `sourceGeneration`
- set `canLoadDetail` conservatively
⚠️ Summary stage must **not** parse transcript content.
That keeps:
- popup open cheaper
- correctness easier to reason about
- stale invalidation scoped to one summary
### Step 5 - Build filtered message slice only on detail request
This is where the old bugs must not come back.
#### For tool bundles
Include only:
- the assistant `tool_use` block with the matching `toolUseId`
- the internal user `tool_result` block with the same `toolUseId`
- explicit assistant text output only when the same assistant message is itself explicitly linked to the task
Do **not** automatically include every other tool in the same AI response.
#### For ambient execution/message bundles
Include only:
- the explicitly linked message itself
- optionally, paired assistant output blocks from the same message if the linked message is assistant content
Do **not** expand to unrelated neighboring transcript messages by default.
### Why this stricter filtering is necessary
If we simply render the whole AI response group, we can leak:
- unrelated board tools
- unrelated read/search tools
- unrelated support actions from the same response
That would make the task logs look rich, but wrong.
Exact logs must be:
- rich
- but still task-scoped
---
## Exact Filtering Strategy
The filtered slice should use **synthetic filtered `ParsedMessage` copies**, not raw original messages unchanged.
That means:
- copy the original message metadata
- keep only the relevant content blocks
- preserve `uuid`, `timestamp`, `requestId`, sidechain flags, session metadata
- drop unrelated blocks
### Critical consistency rule for synthetic messages
After block filtering, derived message fields must be **recomputed**, not blindly copied.
That includes:
- `toolCalls`
- `toolResults`
- `sourceToolUseID`
- `sourceToolAssistantUUID`
- `toolUseResult`
If we keep the original derived fields after dropping unrelated blocks, the renderer can silently reintroduce unrelated tool cards even though the filtered content looked correct.
That is one of the highest-risk implementation mistakes in this iteration.
### Research-backed note: what the renderer actually reads
From the current code:
- assistant-side tool cards are derived primarily from assistant content blocks (`tool_use`)
- internal user tool results are derived primarily from `msg.toolResults`
- `ChunkBuilder` and `SemanticStepExtractor` do **not** rely on exactly the same fields on both sides
Implication:
- assistant filtered messages must preserve correct assistant content blocks
- internal user filtered messages must rebuild `toolResults[]` correctly
- copying stale derived fields is especially dangerous on the internal user side
- `toolUseResult` needs explicit handling because renderer/tool-content helpers use it for richer cards
Suggested helper:
```ts
function filterParsedMessageForTaskAnchor(args: {
message: ParsedMessage
anchor: BoardTaskExactLogAnchor
explicitlyLinkedMessageIds: Set<string>
}): ParsedMessage | null
```
Rules:
- assistant message:
- keep `tool_use` blocks only when `block.id === anchor.toolUseId`
- keep `text` blocks only when the message UUID is explicitly linked for the same target task
- drop unrelated `tool_use` blocks
- drop unrelated thinking blocks in v1
- internal user message:
- keep `tool_result` blocks only when `block.tool_use_id === anchor.toolUseId`
- rebuild `toolResults[]` only for that tool
- keep `sourceToolUseID` only when it matches
- keep `sourceToolAssistantUUID` only when the paired assistant message is present in the same bundle
- keep `toolUseResult` only when it can be proven to belong to the same surviving `toolUseId`
- if that proof is missing, drop `toolUseResult` instead of risking leaked payload from another tool
- ordinary user/system message:
- keep only if explicitly linked by `messageUuid`
This preserves correctness and still allows the renderer to work.
### `toolUseResult` preservation policy
Repo-local finding:
- `displayItemBuilder` uses `toolUseResult` while building linked tool items
- `toolContentChecks` uses `toolUseResult` to decide whether richer content exists for read/write/edit-style tools
- `ToolResultExtractor` also treats `toolUseResult` as an alternate result carrier
So `toolUseResult` is not optional sugar.
It can materially affect what the renderer shows.
Safe v1 rule:
- keep `toolUseResult` only when:
- the filtered internal-user message still points to exactly one surviving `toolUseId`
- that `toolUseId` matches `sourceToolUseID` or an equivalent explicit enriched field
- otherwise:
- drop `toolUseResult`
Why this is safer:
- false negatives only degrade richness for one tool card
- false positives can leak payload from a different tool execution into the current exact bundle
For exact task logs, false negative is preferable to false positive.
### Streaming assistant dedupe rule
Another repo-local finding:
- `parseJsonlFile(...)` parses streaming assistant entries as separate `ParsedMessage`s
- `deduplicateByRequestId(...)` exists, but it is not automatically applied by the general renderer pipeline
- if exact logs do nothing, the same assistant response can survive more than once inside one bundle
That can cause:
- duplicated output rows
- duplicated tool-use blocks from intermediate streaming entries
- unstable exact bundles for the same task over time
So the exact-log path must add an explicit dedupe step:
- after synthetic filtering
- before chunk building
- per bundle candidate
- keep only the last surviving assistant message for a given `requestId`
Important:
- do not dedupe across different bundles
- do not dedupe by `requestId` before filtering, because different streaming snapshots may survive differently after block filtering
The safe sequence is:
1. parse strict file-local `ParsedMessage[]`
2. build one filtered synthetic bundle slice
3. dedupe assistant streaming entries by `requestId` inside that slice
4. build chunks from that deduped bundle slice
This should be pinned with tests.
### Strict timestamp and source-fidelity rule
The exact-log path must not become looser than the summary path about malformed transcript rows.
Important repo-local finding:
- the current explicit activity reader already skips rows without a real transcript `timestamp`
- the generic `parseJsonlFile(...)` path currently falls back to `new Date()` when raw transcript `timestamp` is missing
That fallback is acceptable for broad session utilities, but it is **not** acceptable for exact task logs.
If exact logs silently synthesize “now” for malformed transcript rows, we get:
- unstable ordering across reads
- bundles that appear newer than they really are
- drift between `Task Activity` and `Exact Task Logs`
So the exact-log path must use a **strict timestamp policy**:
- missing or malformed raw transcript timestamp -> drop the exact-log row or exact-log message
- never synthesize current time
Preferred implementation direction:
- add a small exact-log-specific strict parser wrapper
- optionally, only if it stays clearly isolated, extend low-level JSONL parsing with an opt-in strict mode used exclusively by exact logs
Rejected shortcut:
- parse with the permissive default path and try to detect synthetic timestamps later
That shortcut is not reliable because the fallback timestamp becomes indistinguishable from a valid parsed timestamp after parsing.
Important repo-local constraint:
- `parseJsonlFile(...)` is used broadly across the app
- changing its default permissive behavior would create unrelated blast radius
So the safer v1 direction is:
- keep the global permissive parser unchanged
- add an exact-log-specific strict wrapper or opt-in exact mode
- contain the stricter behavior inside the exact-log path only
### Classification rule for synthetic filtered messages
The plan relies on the current `MessageClassifier` behavior:
- filtered internal user tool-result messages are still classified into the AI path
- they are not rendered as user bubbles as long as they remain internal/meta user messages
This is good for the chosen design, but it is a dependency that must be pinned with tests.
If this classifier behavior changes later, exact logs can silently degrade.
---
## Chunk Building Strategy
### Chosen direction
Reuse:
- `ChunkBuilder.buildChunks(...)`
- `transformChunksToConversation(...)`
- `enhanceAIGroup(...)`
- `MemberExecutionLog`
### Pre-flight checkpoint
Before coding the bundle builder, confirm with tests that:
- filtered internal user messages still classify into the expected AI path in `MessageClassifier`
- filtered assistant + internal user slices still produce the expected tool cards in `MemberExecutionLog`
- filtered tool-result-only bundles still render meaningfully even when no paired assistant tool-use survives
- filtered bundles with multiple assistant streaming snapshots collapse to one stable assistant row per `requestId`
- `toolUseResult`-backed richer tool cards still work when the surviving bundle truly owns that tool result
- `toolUseResult` is dropped when ownership is ambiguous
This must be verified, not assumed.
### Important rule
Build chunks from the **filtered slice**, not from the entire session.
### Bundle isolation rule
Build chunks **per requested exact bundle detail**, not from a concatenated multi-bundle slice.
Why:
- `ChunkBuilder` buffers adjacent AI-category messages together
- if two anchors are concatenated before chunk building, separate exact bundles can accidentally merge into one AI chunk
- that would produce unstable visual grouping and leak unrelated context between bundles
So the correct sequence is:
1. derive one exact detail candidate
2. build one filtered message slice for that candidate
3. build chunks for that candidate only
4. map to one `BoardTaskExactLogDetail`
Suggested builder:
```ts
class BoardTaskExactLogChunkBuilder {
constructor(private readonly chunkBuilder: ChunkBuilder = new ChunkBuilder()) {}
buildBundleChunks(messages: ParsedMessage[]): EnhancedChunk[] {
return this.chunkBuilder.buildChunks(messages, [], { includeSidechain: true })
}
}
```
### Why not pass subagents/processes in v1
The exact log slice is already strict and synthetic.
Passing full process linkage into this slice creates extra coupling and raises contamination risk.
In v1:
- pass no additional processes
- render only what is explicitly in the filtered message slice
That is safer and easier to reason about.
### Why no `SessionParser` as the main entrypoint
`SessionParser` is useful for whole-session views, but it is not the ideal entrypoint here.
For exact logs we want:
- file-local parsed messages
- no whole-session grouping assumptions
- no extra session-level work unless needed
So the preferred path in v1 is:
- parse raw transcript files into `ParsedMessage[]`
- then run exact-bundle selection on top
Do not start from a full `SessionDetail` pipeline unless implementation proves it is actually simpler without correctness cost.
---
## Why We Should Not Reuse `MemberLogsTab`
`MemberLogsTab` is valuable, but it is the wrong source layer for exact logs.
It still depends on:
- session summaries
- session overlap
- task work intervals
- preview logic
- owner-session assumptions
That logic remains useful for `Execution Sessions`, but should not be reused as the source for exact task logs.
Correct reuse target:
- renderer primitives
Wrong reuse target:
- legacy session discovery
### Renderer reuse boundary
Reusing the existing renderer means reusing its current visual behavior too.
That is intentional in v1:
- exact details render through `MemberExecutionLog`
- item ordering follows that component's existing behavior
- no ongoing/session-status affordances are added
- no extra subagent/process enrichment is injected beyond what exists in the filtered chunk slice
This keeps iteration 08 focused on the hard problem - correct task-scoped selection - instead of accidentally starting a parallel renderer redesign.
---
## New Main-Side Services
### 1. `BoardTaskActivityRecordSource`
Responsibility:
- read transcript metadata
- resolve task-linked records
- expose internal activity records
Potential implementation:
- extract common lower-level logic from current `BoardTaskActivityService`
- keep `BoardTaskActivityService` as record -> DTO mapper
### 2. `BoardTaskExactLogSummarySelector`
Responsibility:
- take activity records only
- group them by exact-log anchor
- produce lightweight exact-log summaries
Important:
- this selector owns anchor precedence
- this selector must not parse transcript files
- this selector computes per-summary `sourceGeneration`
- computing `sourceGeneration` may stat referenced files, but it must not parse transcript content
- this selector decides `canLoadDetail` conservatively from anchor shape and record fidelity
### 3. `BoardTaskExactLogDetailSelector`
Responsibility:
- take one exact-log summary + strict parsed transcript messages
- produce one filtered message slice for one requested exact detail
Important:
- this selector owns derived-field recomputation requirements for filtered messages
- this selector must not return raw original `ParsedMessage` arrays when block filtering happened
### 4. `BoardTaskExactLogChunkBuilder`
Responsibility:
- convert filtered message bundles into `EnhancedChunk[]`
Important:
- one bundle in, one bundle out
- no cross-bundle chunk building
### 5. `BoardTaskExactLogsService`
Responsibility:
- orchestrate the exact-log summary flow
- expose IPC-facing `BoardTaskExactLogSummariesResponse`
Important:
- this service consumes `BoardTaskActivityRecordSource`
- it must not directly parse `boardTaskLinks[]` from JSONL lines itself
- it must not parse transcript messages in the summary path
- it may read file metadata needed for per-summary `sourceGeneration`
- it should not own a second explicit-metadata parser
### 6. `BoardTaskExactLogDetailService`
Responsibility:
- resolve one exact bundle summary into one renderable exact detail
- expose IPC-facing `BoardTaskExactLogDetailResult`
Important:
- this service consumes `BoardTaskActivityRecordSource`
- this service consumes `BoardTaskExactLogDetailSelector`
- this service owns strict per-bundle filtering
- this service owns per-bundle assistant `requestId` dedupe before chunk building
- this service returns `stale` or `missing` instead of guessing when a requested bundle can no longer be rendered safely
---
## Proposed File Touchpoints
### `claude_team` main
Add:
- `src/main/services/team/taskLogs/activity/BoardTaskActivityRecordSource.ts`
- `src/main/services/team/taskLogs/exact/BoardTaskExactLogsService.ts`
- `src/main/services/team/taskLogs/exact/BoardTaskExactLogDetailService.ts`
- `src/main/services/team/taskLogs/exact/BoardTaskExactLogSummarySelector.ts`
- `src/main/services/team/taskLogs/exact/BoardTaskExactLogDetailSelector.ts`
- `src/main/services/team/taskLogs/exact/BoardTaskExactLogChunkBuilder.ts`
- `src/main/services/team/taskLogs/exact/BoardTaskExactLogsParseCache.ts`
Touch:
- `src/main/ipc/teams.ts`
- `src/main/ipc/handlers.ts`
- `src/main/index.ts`
- `src/preload/index.ts`
- `src/preload/constants/ipcChannels.ts`
- `src/shared/types/api.ts`
- `src/shared/types/team.ts`
### `claude_team` renderer
Add:
- `src/renderer/components/team/taskLogs/ExactTaskLogsSection.tsx`
- `src/renderer/components/team/taskLogs/ExactTaskLogCard.tsx`
Touch:
- `src/renderer/components/team/taskLogs/TaskLogsPanel.tsx`
### `agent_teams_orchestrator`
No new write-side contract is required for this iteration if iteration 07 metadata is already present.
Only touch write-side if a concrete missing field is discovered during implementation.
That is an explicit scope guard.
---
## IPC Plan
Add:
```ts
teams.getTaskExactLogSummaries(
teamName: string,
taskId: string
): Promise<BoardTaskExactLogSummariesResponse>
teams.getTaskExactLogDetail(
teamName: string,
taskId: string,
exactLogId: string,
expectedSourceGeneration: string
): Promise<BoardTaskExactLogDetailResult>
```
Suggested IPC channel:
```ts
TEAM_GET_TASK_EXACT_LOG_SUMMARIES = 'team:getTaskExactLogSummaries'
TEAM_GET_TASK_EXACT_LOG_DETAIL = 'team:getTaskExactLogDetail'
```
These methods must be:
- independent from `getLogsForTask(...)`
- independent from the legacy worker fallback path
- explicit-metadata only in v1
- browser-safe in the same way as other team-only methods:
- summaries -> `{ items: [] }`
- detail -> `{ status: 'missing' }`
### Return shape rule
The API should:
- return lightweight summaries from the summary endpoint
- return already-built `EnhancedChunk[]` only from the detail endpoint
- never return raw messages plus renderer-side building instructions
Why:
- chunk building belongs to the main-side service layer
- renderer should stay simple
- this keeps exact-log selection and filtering logic out of the renderer
- this keeps the initial popup payload materially smaller
### Ordering rule
Returned summaries must be sorted deterministically by:
1. explicit source timestamp
2. `filePath`
3. `sourceOrder`
4. `toolUseId`
5. `id`
This avoids UI drift when multiple transcript rows share the same minute/second bucket.
---
## Renderer Plan
### `TaskLogsPanel`
Target composition:
```tsx
<TaskActivitySection ... />
<ExactTaskLogsSection ... />
<ExecutionSessionsSection ... />
```
### `ExactTaskLogsSection`
Responsibilities:
- fetch `teams.getTaskExactLogSummaries(...)`
- load independently from `ExecutionSessionsSection`
- show loading / error / empty state
- render one card per exact log summary
### Exact-log loading policy
Exact logs are materially heavier than summary rows.
So the safe v1 loading policy is:
- load when the task popup opens and the section becomes visible
- if the section is collapsed, do not keep a blind frequent poll running
- if the task is active and the section is expanded, a slower revalidation loop is acceptable
- manual refresh is acceptable and should be easy to add
This is better than unconditional frequent polling because exact logs require:
- explicit record lookup
- transcript file parsing for exact detail
- synthetic message filtering
- chunk building
Those costs are much higher than the summary feed.
### `ExactTaskLogCard`
Responsibilities:
- show timestamp + actor label
- show source metadata if helpful
- lazy-load detail on expand
- render the loaded detail via `MemberExecutionLog`
- keep the expand control disabled when `canLoadDetail === false`
Example:
```tsx
if (summary.canLoadDetail) {
const detail = await api.teams.getTaskExactLogDetail(
teamName,
taskId,
summary.id,
summary.sourceGeneration
)
if (detail.status === 'ok') {
return <MemberExecutionLog chunks={detail.detail.chunks} memberName={summary.actor.memberName} />
}
}
```
### Actor label rule
Fix the current weak UX:
- if `memberName` exists -> show it
- else if `isSidechain === false` -> show `lead session`
- else -> show `unknown actor`
This is much safer and more readable than the current fallback.
---
## Empty State Policy
If there are explicit activity rows but no exact renderable summaries:
- do **not** silently disappear
- show a clear empty state such as:
`Exact task-scoped transcript groups are not available for these activity rows yet.`
If no explicit activity exists:
`No explicit task-linked logs found in transcript metadata.`
This matters because:
- summary-only history is still useful
- users should not assume the feature is broken
---
## Performance Plan
This slice can get expensive if implemented naively.
### Required v1 protections
1. Parse cache by `filePath + mtimeMs + size`
2. In-flight dedupe for concurrent reads
3. Deduplicate anchors before building summaries
4. In the summary path, do not parse transcript content at all
5. In the detail path, do not parse the same file repeatedly inside one request
6. In the detail path, derive referenced file paths from explicit activity records first, then parse only that subset
7. Avoid unconditional high-frequency polling for exact logs
8. Share the explicit metadata reader/record source with the summary path instead of re-reading metadata in a second pipeline
9. Keep exact detail lazy, not eager, in v1
### Nice-to-have only if needed later
- per-task result cache
- cross-service parsed transcript cache reuse
Do not over-engineer that before profiling.
---
## Consistency Rules
### Rule 1 - Exact logs are explicit-link only
Do not add:
- work-interval fallback
- mention matching
- owner fallback
- “close enough” neighboring tool inference
### Rule 2 - Exact logs and summary use the same explicit source
`Task Activity` and `Exact Task Logs` should derive from the same underlying explicit activity records, not from separate competing interpretations.
That means:
- same `BoardTaskActivityRecordSource`
- same explicit transcript metadata semantics
- same target-task resolution rules
The two views may diverge in presentation.
They must not diverge in their low-level notion of “this transcript source is explicitly linked to this task”.
### Rule 3 - Summary selector is the single source of truth for summary identity
`exactLogId` and per-summary `sourceGeneration` must come from one place only:
- `BoardTaskExactLogSummarySelector`
That means:
- `BoardTaskExactLogsService` uses it to emit summaries
- `BoardTaskExactLogDetailService` uses the same selector to rebuild summaries before loading detail
- detail service must not recompute ids with its own string concatenation rules
- detail service must not recompute generations with a different file-ordering rule
Why this matters:
- summary/detail drift is otherwise easy to introduce silently
- one tiny id-format change can turn every detail request into `missing`
- one tiny generation-ordering change can turn valid detail requests into false `stale`
If a helper is extracted, it should stay below both services and be reused by both.
### Rule 4 - Exact logs may be stricter than summary
This is acceptable.
Some summary rows may not yield rich exact summaries or rich exact details if:
- the row is too minimal
- the source message is malformed
- the source message is non-renderable in the existing pipeline
That is better than rendering the wrong thing.
### Rule 5 - Exact detail reconstruction is file-local in v1
Exact detail reconstruction must stay file-local.
That means:
- one summary anchor resolves to one `source.filePath`
- detail service only parses that summary's referenced files
- missing pair data in another transcript file is treated as absent, not searched globally
Why this matters:
- it matches the current execution renderer and tool-linking assumptions
- it keeps `sourceGeneration` honest
- it avoids a hidden return of broad transcript heuristics
---
## Edge Cases
### 1. Same tool call linked to two tasks
Example:
- `task_link`
- `task_unlink`
Behavior:
- both tasks may show the same exact tool bundle
- the bundle must render once per task, not duplicate within one task
### 2. One transcript message contains multiple relevant links
Behavior:
- collapse into one exact log bundle
- preserve all relevant `linkKinds` in metadata
### 2b. One tool execution has both a tool anchor and a message anchor
Behavior:
- render exactly one exact bundle
- the tool anchor wins
- the message anchor is absorbed into the same bundle metadata
### 3. Same AI response contains relevant and irrelevant tools
Behavior:
- render only the filtered relevant blocks
- do not include the whole raw AI response
### 3b. Same assistant message contains both relevant text and unrelated tool calls
Behavior:
- keep the explicitly linked text
- drop unrelated tool calls
- rebuild derived assistant-side tool structures from the surviving blocks only
### 4. Lead-session row without actor name
Behavior:
- show `lead session`
- not `unknown actor`
### 5. Missing paired `tool_use`
Behavior:
- if `tool_result` exists but paired assistant `tool_use` cannot be found, render what is available
- do not guess missing tool input
- do not search other transcript files for the missing pair in v1
### 6. Missing timestamp / malformed row
Behavior:
- skip malformed rows
- do not synthesize “current time”
### 7. Execution-only ambient rows
Behavior:
- may render as exact text/output-only bundles
- no fake tool payload should be attached
---
## Suggested Internal Helper Shapes
### Bundle source model
```ts
type BoardTaskExactLogBundleCandidate = {
id: string
timestamp: string
actor: BoardTaskExactLogActor
source: BoardTaskExactLogSource
records: BoardTaskActivityRecord[]
} & (
| { canLoadDetail: true; sourceGeneration: string }
| { canLoadDetail: false }
)
type BoardTaskExactLogDetailCandidate = {
id: string
timestamp: string
actor: BoardTaskExactLogActor
source: BoardTaskExactLogSource
records: BoardTaskActivityRecord[]
filteredMessages: ParsedMessage[]
}
```
### Bundle identity rule
Use:
- tool bundle id: `tool:${filePath}:${toolUseId}`
- message bundle id: `message:${filePath}:${messageUuid}`
Do not use timestamps as the primary identity.
Timestamps are for ordering, not identity.
### Summary source-of-truth rule for actor label
`MemberExecutionLog` only receives `chunks` plus one optional `memberName`.
So v1 should not try to rediscover actor identity from filtered exact-detail messages.
The authoritative actor label for the exact-log card should come from the summary/record side:
- exact summary owns the visible actor label
- exact detail rendering reuses that summary actor label
- detail reconstruction should not override it based on incidental filtered message content
### Selector skeleton
```ts
class BoardTaskExactLogSummarySelector {
selectSummaries(args: {
records: BoardTaskActivityRecord[]
}): BoardTaskExactLogBundleCandidate[] {
// 1. derive anchors from explicit records
// 2. apply tool-anchor-over-message precedence
// 3. dedupe anchors
// 4. compute per-summary sourceGeneration
// 5. return one candidate per summary
}
}
class BoardTaskExactLogDetailSelector {
selectDetail(args: {
summary: BoardTaskExactLogSummary
records: BoardTaskActivityRecord[]
parsedMessagesByFile: Map<string, ParsedMessage[]>
}): BoardTaskExactLogDetailCandidate | null {
// 1. rebuild the matching anchor from explicit records
// 2. parse only the files referenced by that summary
// 3. build filtered synthetic ParsedMessage[] for that one anchor
// 4. return one detail candidate or null
}
}
```
### Service skeleton
```ts
class BoardTaskExactLogsService {
async getTaskExactLogSummaries(
teamName: string,
taskId: string
): Promise<BoardTaskExactLogSummariesResponse> {
// 1. get explicit activity records
// 2. build exact summaries from records only
// 3. sort deterministically
// 4. map summary response
}
}
class BoardTaskExactLogDetailService {
async getTaskExactLogDetail(
teamName: string,
taskId: string,
exactLogId: string,
expectedSourceGeneration: string
): Promise<BoardTaskExactLogDetailResult> {
// 1. rebuild the matching summary from explicit records
// 2. if summary.canLoadDetail !== true -> return { status: 'missing' }
// 3. compare expectedSourceGeneration with recomputed summary.sourceGeneration
// 4. if mismatch -> return { status: 'stale' }
// 5. parse only the summary's referenced files via strict parser
// 6. build one filtered detail candidate
// 7. dedupe assistant streaming rows by requestId
// 8. build chunks
// 9. return one detail DTO or { status: 'missing' }
}
}
```
---
## Rollout Plan
### Feature gates
Use separate read/UI gates:
- `CLAUDE_TEAM_BOARD_TASK_EXACT_LOGS_READ_ENABLED`
- `VITE_BOARD_TASK_EXACT_LOGS_UI_ENABLED`
Do not reuse the iteration 07 gates directly.
This lets us:
- validate main-side behavior first
- then enable renderer independently
### Rollout stages
#### Stage 1 - Main-side exact bundle service
- build record source
- build exact summaries
- add tests
- no UI yet
#### Stage 2 - IPC + preload
- expose `getTaskExactLogSummaries(...)`
- expose `getTaskExactLogDetail(...)`
- add integration tests
#### Stage 3 - Renderer section
- add `ExactTaskLogsSection`
- wire into task popup
- keep disabled by UI flag initially
#### Stage 4 - Manual shadow validation
Compare:
- `Task Activity`
- `Exact Task Logs`
- `Execution Sessions`
for several real teams and transcript shapes.
---
## Testing Plan
### Main tests
Add focused tests for:
1. `BoardTaskActivityRecordSource`
- explicit record extraction matches existing activity semantics
2. Exact-log selectors
become two focused test units:
`BoardTaskExactLogSummarySelector`
- dedupes repeated refs
- applies tool-anchor-over-message precedence
- computes stable per-summary `sourceGeneration`
- does not parse transcript content
- sets `canLoadDetail` conservatively
- omits `sourceGeneration` when `canLoadDetail === false`
`BoardTaskExactLogDetailSelector`
- filters unrelated tools from same AI response
- keeps filtered internal-user results in the AI rendering path
- keeps paired tool_use + tool_result
- preserves explicit assistant text when linked
- rebuilds derived fields after block filtering
- keeps `toolUseResult` only for the surviving matching tool result
- dedupes assistant streaming entries by `requestId` after filtering
- never searches outside the summary's file-local source set for missing pairs
3. `BoardTaskExactLogChunkBuilder`
- builds renderable `EnhancedChunk[]`
- never merges adjacent candidates into one cross-bundle AI chunk
- no crash on minimal bundles
4. `BoardTaskExactLogsService`
- returns sorted summaries
- empty when feature disabled
- returns `{ items: [] }` for unknown task
- does not invoke transcript parsing in the summary path
- does not touch the exact-log strict parser or transcript parse cache in the summary path
- emits stable per-summary `sourceGeneration` values
- never emits `sourceGeneration` for non-expandable summaries
5. `BoardTaskExactLogDetailService`
- returns `status: 'missing'` immediately for non-expandable summaries
- returns `status: 'stale'` when requested generation no longer matches
- returns `status: 'missing'` for unknown bundle
- returns `status: 'ok'` with renderable detail for valid bundle id
- does not guess missing tool ownership
- reuses the summary actor label instead of re-deriving actor identity from filtered detail messages
### IPC tests
- `teams.getTaskExactLogSummaries(...)` happy path
- `teams.getTaskExactLogDetail(...)` happy path
- `teams.getTaskExactLogDetail(...)` stale-generation path
- browser fallback shape
- disabled flag path
- malformed transcript path
### Renderer tests
- `ExactTaskLogsSection`
- loading
- error
- empty
- renders one or more exact summaries
- reloads summaries on `stale` detail response
### Manual validation
Use real scenarios:
1. normal owner task with lifecycle + comments + review
2. external actor touches another task
3. `task_link` / `task_unlink`
4. lead-session rows without `agentName`
5. task with explicit summary rows but no exact renderable detail
6. summary/detail drift after transcript update
---
## Definition of Done
This iteration is done when:
- task popup shows:
- `Task Activity`
- `Exact Task Logs`
- `Execution Sessions`
- `Exact Task Logs` visually uses the same execution-log renderer family the user already likes
- exact logs are sourced from explicit task-linked transcript selection
- exact logs do **not** depend on legacy heuristic task/session discovery
- unrelated tools from the same AI response are not leaked into the exact view
- exact-log details are lazy-loaded, not eagerly transferred for every summary row
- main-side and renderer tests pass
- old `Execution Sessions` remains intact and isolated
---
## Final Decision Summary
The best path is:
- **reuse the existing execution renderer**
- **do not reuse the old heuristic log discovery**
- **insert a strict explicit task-scoped transcript selection layer**
This preserves the good UX while finally making task log attribution reliable.