Skip to content

Conversation

@ThomasK33
Copy link
Member

Summary

Persist completed sub-agent task artifacts (reports + transcripts) into the parent session directory so they remain retrievable after workspace cleanup/restart. Adds a minimal UI affordance to view archived transcripts directly from completed task tool calls.

Background

Today, sub-agent workspaces are cleaned up after agent_report, which deletes ~/.mux/sessions/<taskId>. Completed reports were only retrievable via an in-memory TTL cache, and transcripts were not viewable once the workspace was removed.

Implementation

  • Persist completed reports under the parent/ancestor session dir (subagent-reports.json + per-task payload).
  • Update TaskService and task tools to load completed reports from disk (restart-safe) and to validate descendant scope via persisted ancestry.
  • Archive transcripts on workspace removal into the parent session dir (subagent-transcripts.json + chat.jsonl / partial.json).
  • Roll up nested sub-agent artifacts (patches/reports/transcripts) from intermediate task workspaces into the parent before deleting the child session dir.
  • Add workspace.getSubagentTranscript ORPC endpoint and a small transcript dialog + “View transcript” button in TaskToolCall.

Validation

  • make static-check

Risks

  • Workspace removal now performs additional best-effort file IO (copy/merge) before deleting session dirs. Errors are logged and do not block cleanup.
  • Artifact roll-up merges metadata across session dirs; last-write-wins behavior is used to avoid losing newer artifacts.

📋 Implementation Plan

Persist sub-agent reports + transcripts after cleanup

Context / Why

Sub-agent workspaces (created via the task tool) are currently deleted after the sub-agent calls agent_report (git worktree + ~/.mux/sessions/<taskId>). The report payload is delivered back to the parent, but any later task_await/UI lookups rely on an in-memory 1-hour cache (completedReportsByTaskId) inside TaskService.

Goals:

  • Persist completed sub-agent reports and chat history to the parent session directory (like subagent-patches/).
  • Make completed reports restart-safe and always retrievable (no reliance on a 1-hour TTL).
  • Enable the UI to show a completed sub-agent’s full transcript, even after the child workspace/session was cleaned up.

Non-goals:

  • Don’t keep sub-agent worktrees around (still delete the git worktree on completion).
  • Don’t auto-apply patches (existing task_apply_git_patch flow stays explicit).

Evidence (repo facts)

  • TaskService caches completed reports for 1 hour: COMPLETED_REPORT_CACHE_TTL_MS = 60 * 60 * 1000 and completedReportsByTaskId is the fallback when a task workspace is removed. (src/node/services/taskService.ts)
  • waitForAgentReport() rejects if the task workspace is missing from config unless the in-memory cache hits. (src/node/services/taskService.ts)
  • Reported tasks are auto-deleted once leaf + patch artifact not pending: cleanupReportedLeafTask()workspaceService.remove(workspaceId, true). (src/node/services/taskService.ts)
  • Workspaces are removed by deleting the runtime workspace and deleting the session dir: fsPromises.rm(config.getSessionDir(workspaceId), { recursive: true, force: true }). (src/node/services/workspaceService.ts)
  • Patch artifacts are already persisted in the parent session dir via subagent-patches.json + subagent-patches/<childTaskId>/series.mbox. (src/node/services/subagentGitPatchArtifacts.ts)
  • Chat history lives in chat.jsonl under the session dir and is parsed line-by-line, skipping malformed JSON. (src/node/services/historyService.ts)
  • UI currently only “opens task workspace” if it still exists; otherwise it just copies the taskId. (src/browser/components/tools/TaskToolCall.tsx)

Plan

1) Add a persistent “subagent artifacts” store in the parent session dir

Create a new node service that mirrors the patch artifact pattern (atomic JSON + per-task subdirectory):

New files (names can be tweaked, but keep them consistent and explicit):

  • src/node/services/subagentReportArtifacts.ts
  • src/node/services/subagentTranscriptArtifacts.ts

On-disk layout (inside parent session dir):

  • subagent-reports.json
  • subagent-reports/<childTaskId>/report.md (or report.json)
  • subagent-transcripts.json
  • subagent-transcripts/<childTaskId>/chat.jsonl
  • subagent-transcripts/<childTaskId>/partial.json (optional, only if exists)

Metadata schemas (TypeScript interfaces + defensive versioning):

  • SubagentReportArtifact:
    • childTaskId, parentWorkspaceId
    • createdAtMs, updatedAtMs
    • title?, reportMarkdown
    • ancestorWorkspaceIds: string[] (used for scope checks after cleanup)
  • SubagentTranscriptArtifact:
    • childTaskId, parentWorkspaceId
    • createdAtMs, updatedAtMs
    • chatPath (relative to parent session dir preferred), partialPath?
    • messageCount? (optional; can be computed lazily)

Implementation notes:

  • Follow the patch artifact pattern:
    • read*ArtifactsFile() returns empty { version, artifactsByChildTaskId: {} } on ENOENT.
    • Write via write-file-atomic.
    • Guard concurrent writers via workspaceFileLocks.withLock(workspaceId, …).
  • Prefer storing relative paths (relative to parent session dir) to avoid absolute-path portability issues.

Net new LoC (product code): ~200–300


2) Persist reports at agent_report time (and stop relying on TTL)

Update src/node/services/taskService.ts:

  1. In finalizeAgentTaskReport():

    • After determining parentWorkspaceId (and before cleanup), write the report artifact into the parent’s session dir.
    • Persist ancestorWorkspaceIds alongside the report so scope checks can be preserved after the child workspace is deleted.

    Shape:

    const parentSessionDir = this.config.getSessionDir(parentWorkspaceId);
    const ancestorWorkspaceIds = this.listAncestorWorkspaceIdsUsingParentById(parentById, childWorkspaceId);
    
    await upsertSubagentReportArtifact({
      workspaceId: parentWorkspaceId,
      workspaceSessionDir: parentSessionDir,
      childTaskId: childWorkspaceId,
      updater: (existing) => ({
        ...(existing ?? {}),
        childTaskId: childWorkspaceId,
        parentWorkspaceId,
        createdAtMs: existing?.createdAtMs ?? Date.now(),
        updatedAtMs: Date.now(),
        title: reportArgs.title,
        reportMarkdown: reportArgs.reportMarkdown,
        ancestorWorkspaceIds,
      }),
    });
  2. Replace the “1-hour TTL cache as source of truth” with “disk as source of truth”:

    • Keep completedReportsByTaskId only as a small in-memory LRU (optional) or remove it entirely.
    • Update waitForAgentReport():
      • If cache miss and workspace entry is missing from config, try loading from the requesting workspace’s session dir:
        • config.getSessionDir(options.requestingWorkspaceId)
        • readSubagentReportArtifact(workspaceSessionDir, taskId)
      • If found, return { reportMarkdown, title }.
  3. Update descendant-scope checks to consult the persisted report artifact:

    • filterDescendantAgentTaskIds() / isDescendantAgentTask() should, on “task not in config”, attempt:
      • Read report artifact from config.getSessionDir(ancestorWorkspaceId)
      • Validate artifact.ancestorWorkspaceIds.includes(ancestorWorkspaceId)

This removes the need for time-based expiry; the report stays available as long as the parent session exists.

Net new/changed LoC (product code): ~150–250


3) Persist transcripts when the child workspace is removed (copy chat.jsonl before deletion)

Update src/node/services/workspaceService.ts inside remove():

  • After metadataResult.success and after the timing/usage rollups (already present), but before:

    const sessionDir = this.config.getSessionDir(workspaceId);
    await fsPromises.rm(sessionDir, { recursive: true, force: true });
  • If metadata.parentWorkspaceId is set:

    1. Copy the child’s session chat files into the parent session:
      • ~/.mux/sessions/<workspaceId>/chat.jsonl~/.mux/sessions/<parentId>/subagent-transcripts/<workspaceId>/chat.jsonl
      • partial.json similarly (if present)
    2. Upsert a SubagentTranscriptArtifact entry for <workspaceId>.

Defensive behavior:

  • If chat/partial files don’t exist, still write a transcript artifact with status: "missing" (or omit paths) rather than throwing.
  • Never block workspace removal forever—copy best-effort and continue.

Roll-up for nested tasks (recommended)

If a sub-agent itself spawned sub-agents, its session dir may contain subagent-*.json + directories for its children. Before deleting that session dir, roll those artifacts up into the parent session dir:

  • Merge child’s subagent-patches.json into parent’s subagent-patches.json (and copy directories).
  • Merge child’s subagent-reports.json into parent’s subagent-reports.json.
  • Merge child’s subagent-transcripts.json into parent’s subagent-transcripts.json.

This ensures artifacts survive even if intermediate task workspaces are also cleaned up.

Why roll-up matters

cleanupReportedLeafTask() can delete a reported parent task after its descendants finish. If that parent task had stored artifacts for its own children (grandchildren from the original root’s perspective), those artifacts would otherwise be deleted with the parent’s session directory.

Net new/changed LoC (product code): ~200–350 (includes roll-up logic)


4) UI: “View transcript” for completed tasks (even after cleanup)

Because the task workspace no longer exists in config, the UI cannot open it. Add an explicit archived transcript viewer.

4a) Backend API / IPC

Add a new workspace/task API endpoint (ORPC) such as:

  • workspace.getSubagentTranscript({ taskId: string }){ success: true, messages: MuxMessage[], title?: string }

Backend behavior:

  • Identify the requesting workspaceId from the IPC context.
  • Authorize:
    • If the task still exists in config: use taskService.isDescendantAgentTask(requester, taskId).
    • If not: consult subagent-reports.json / subagent-transcripts.json in the requester’s session dir and validate ancestor chain.
  • Load transcript from the archived chat.jsonl path (and merge partial.json if present).

4b) Frontend dialog

Add a dialog component that:

  • Fetches MuxMessage[] via the new API.
  • Uses a local StreamingMessageAggregator instance to compute DisplayedMessage[].
  • Renders with existing MessageRenderer.

Suggested location:

  • src/browser/components/tools/SubagentTranscriptDialog.tsx (invoked from task tool UIs).

4c) Add affordances in task tool renderers

Update src/browser/components/tools/TaskToolCall.tsx:

  • In TaskToolCall and TaskAwaitResult, add a small button next to the taskId when:
    • result.status === "completed" and a transcript artifact exists (or optimistically show and handle errors).

Net new/changed LoC (product code): ~250–450


Approaches & LoC estimates

Approach A (recommended): Full persistence + UI transcript viewer

  • Persist report artifacts (restart-safe)
  • Copy transcripts on workspace removal
  • Roll-up nested artifacts
  • Add UI transcript dialog

Estimated net LoC: ~800–1,350 (product code)

Approach B (smaller): Persistence only (no UI yet)

  • Persist report artifacts + transcripts
  • Update waitForAgentReport() to read from disk
  • No transcript viewer; users inspect files manually

Estimated net LoC: ~400–700 (product code)


Acceptance criteria

  • A completed task’s report is retrievable via task_await even after:
    • the child workspace was cleaned up, and
    • the app was restarted (no in-memory cache).
  • A completed task’s transcript is viewable in the UI even after the task workspace/session dir is deleted.
  • Nested tasks do not lose artifacts when intermediate task workspaces are cleaned up.
  • Workspace deletion remains best-effort and never crashes the app (errors logged, removal still completes).

Generated with mux • Model: openai:gpt-5.2 • Thinking: xhigh • Cost: $12.78

@github-actions github-actions bot added the enhancement New feature or functionality label Jan 30, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24e2fd75bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Member Author

@codex review

Addressed the transcript archival/retrieval issues:

  • Transcript index entries no longer require chat.jsonl; partial-only archives are readable.
  • workspace.getSubagentTranscript now falls back to reading the live task session when the archive isn’t available yet (common while patch artifacts are pending).

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0fd1efa97e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Member Author

@codex review

Gated the subagent transcript fetch behind the dialog open state to avoid unnecessary I/O for closed dialogs.

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- Make transcript dialog layout scroll correctly
- Pass parent workspaceId through transcript rendering to enable nested drill-down
- Make Storybook play test robust to multiple 'View transcript' buttons
- Wait for transcript load before asserting message content
- Assert on stable user message text
Use a matcher function that inspects element.textContent to avoid failures when MarkdownRenderer splits text across elements.
Assert transcript rendering via data-message-block instead of brittle text matching.
The app can render multiple View transcript buttons across stories; scope the click to the specific taskId used in this story.
Copy link
Member Author

@codex review

Follow-ups since last round:

  • Transcript loading is now gated on dialog open.
  • Storybook transcript viewer story + test runner scoping fixes.
  • Pass parent workspaceId to MessageRenderer so nested transcript drill-down works after roll-up.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8fd8efe6b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33 ThomasK33 force-pushed the persist-subagent-artifacts branch from f8fd8ef to a86cf2c Compare January 31, 2026 10:25
Copy link
Member Author

@codex review

Addressed the two threads:

  • Guarded malformed ancestorWorkspaceIds usage in descendant checks.
  • getSubagentTranscript now probes transcript artifacts in descendant workspaces (covers grandchild drill-down before roll-up).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant