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

52 KiB

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:

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:

BoardTaskActivityTranscriptReader
  -> BoardTaskActivityRecordSource
    -> BoardTaskActivityService
    -> BoardTaskExactLogsService
    -> BoardTaskExactLogDetailService

This is explicitly not the desired graph:

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:
api.teams.getTaskExactLogSummaries(teamName, taskId)
  1. IPC calls:
BoardTaskExactLogsService.getTaskExactLogSummaries(teamName, taskId)
  1. Service gets:
  • active + deleted tasks from TeamTaskReader
  • activity records from BoardTaskActivityRecordSource
  1. Service derives exact-log summaries from activity records only

  2. Renderer shows exact-log summary cards first

  3. On expand, renderer asks for one exact detail:

api.teams.getTaskExactLogDetail(teamName, taskId, exactLogId, sourceGeneration)
  1. 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[]
  1. Renderer reuses MemberExecutionLog

New Shared DTOs

IPC DTOs

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:

type BoardTaskExactLogSummariesResponse = {
  items: BoardTaskExactLogSummary[]
}

Preferred detail result shape:

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:

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:

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 ParsedMessages
  • 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:

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:

teams.getTaskExactLogSummaries(
  teamName: string,
  taskId: string
): Promise<BoardTaskExactLogSummariesResponse>
teams.getTaskExactLogDetail(
  teamName: string,
  taskId: string,
  exactLogId: string,
  expectedSourceGeneration: string
): Promise<BoardTaskExactLogDetailResult>

Suggested IPC channel:

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:

<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:

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

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

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

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

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

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
  1. BoardTaskExactLogChunkBuilder

    • builds renderable EnhancedChunk[]
    • never merges adjacent candidates into one cross-bundle AI chunk
    • no crash on minimal bundles
  2. 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
  3. 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.