Skip to content

Chat view: resume-binding fallback for session-restored terminals - #5791

Closed
lawrencecchen wants to merge 4 commits into
feat-agent-chat-viewfrom
feat-chat-resolver-fallback
Closed

lawrencecchen wants to merge 4 commits into
feat-agent-chat-viewfrom
feat-chat-resolver-fallback

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #5576. Fixes the dogfood blocker (Lawrence): opening the agent chat view on a session-restored terminal showed "No agent conversation" even though it had a real agent session.

Root cause

AgentChatTranscriptResolver resolved a panel's transcript only through the live RestorableAgentSessionIndex, which is not repopulated for a terminal restored after an app relaunch (the agent hasn't re-announced via the hook yet). So index.snapshot(...) returned nil → empty state.

Fix

The resolver now reconstructs the (kind, sessionId, cwd, env) lookup tuple from the panel's persisted SurfaceResumeBindingSnapshot (the same binding cmux uses to resume the agent; checkpointId is the agent session id) when the live index misses. It only returns the empty state when neither source yields a transcript-backed session. The presenter captures workspace.surfaceResumeBinding(panelId:) on the main actor and passes it through. Existing index path is unchanged (preferred when present).

Tests

Swift Testing in cmuxTests/AgentChatTranscriptResolverResumeBindingTests.swift (wired into the test target): Claude and Codex fallback locate the transcript with an empty index + a resume binding (temp-home fixtures); absent binding and a non-transcript kind both return nil.

Verification

Build-only compile (tagged DerivedData): BUILD SUCCEEDED. pbxproj normalized + checked + test-wiring lint ok. Folds into the gui dogfood tag for Lawrence to confirm on a restored terminal.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touches filesystem transcript resolution from persisted bindings (path traversal guards added) and changes when chat opens vs shows empty state; scope is localized to Agent Chat resolution.

Overview
Fixes “No agent conversation” on session-restored terminals by letting agent chat resolution use the panel’s persisted resume binding when the live RestorableAgentSessionIndex has no entry yet (typical after relaunch before the agent re-announces).

AgentChatPresenter now reads workspace.surfaceResumeBinding(panelId:) on the main actor and passes it into AgentChatTranscriptResolver.resolve. The resolver still prefers the live index; otherwise it rebuilds kind, session id, cwd, and env from SurfaceResumeBindingSnapshot (checkpointId as session id), with filename safety checks on binding ids and Claude/Codex-only kinds.

Claude lookup also gains the workflow-container path: when the session id is a container directory, it picks the newest sibling .jsonl in the project dir, matching the live index behavior. New cmuxTests/AgentChatTranscriptResolverResumeBindingTests cover fallback, workflow siblings, unsafe ids, and unsupported kinds; CI also adds CmuxAgentConversation to the headless SwiftPM test list.

Reviewed by Cursor Bugbot for commit dc82bb6. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes the chat view showing “No agent conversation” on session-restored terminals by falling back to the panel’s persisted resume binding when the live index is empty, with session-id validation and a Claude workflow-container fallback. Restored terminals now load the correct Claude/Codex transcript after relaunch.

  • Bug Fixes

    • Prefer the live RestorableAgentSessionIndex; if missing, rebuild (kind, sessionId, cwd, env) from SurfaceResumeBindingSnapshot and only show the empty state if both miss.
    • For Claude workflow/sub-agent bindings that point at a container directory, resolve to the newest sibling .jsonl in the same project.
    • Validate resume-binding session ids as safe filename components and restrict kinds to Claude/Codex; AgentChatPresenter passes workspace.surfaceResumeBinding(panelId:) to the resolver; tests cover fallback, unsafe ids, unsupported kinds, and the workflow case.
  • CI

    • Gate CmuxAgentConversation Swift package tests to catch Claude/Codex transcript parser regressions.

Written for commit dc82bb6. Summary will update on new commits.

Review in cubic

…n index is empty

Fixes the dogfood blocker where opening the chat view on a session-restored
terminal showed 'No agent conversation': the resolver only consulted the
live RestorableAgentSessionIndex, which isn't repopulated after an app
relaunch until the agent re-announces. It now reconstructs the (kind,
sessionId, cwd, env) lookup tuple from the panel's persisted
SurfaceResumeBindingSnapshot (checkpointId = the agent session id) when the
index misses, and only returns the empty state when neither source yields a
transcript-backed session. The presenter passes the panel's resume binding
through. Swift Testing coverage: Claude + Codex fallback locate the
transcript with an empty index; absent binding and non-transcript kinds
return nil.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 12, 2026 12:46am
cmux-staging Building Building Preview, Comment Jun 12, 2026 12:46am

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 82ed88e4-aa34-49e1-a614-68dac043c4bd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-chat-resolver-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@socket-security

socket-security Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm posthog-js is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: web/package.json → npm/posthog-js@1.373.4

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/posthog-js@1.373.4. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@greptile-apps

greptile-apps Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the "No agent conversation" empty state when the agent chat view is opened on a session-restored terminal after a relaunch, where the live RestorableAgentSessionIndex hasn't been repopulated yet. The fix reads the panel's persisted SurfaceResumeBindingSnapshot as a fallback source for (kind, sessionId, cwd, env), with path-traversal validation on the session ID and kind-gating to Claude/Codex only.

  • AgentChatPresenter captures workspace.surfaceResumeBinding(panelId:) on the main actor before the Task.detached IO hop and threads it through to the resolver; the live index is still preferred when present.
  • AgentChatTranscriptResolver gains a resumeBindingInputs path with isSafeSessionFilenameComponent validation, plus a new newestSiblingTranscript helper that mirrors the live index's resolvedClaudeWorkflowRecord logic so workflow/sub-agent panels also resolve correctly via the binding.
  • AgentChatTranscriptResolverResumeBindingTests (new) covers Claude/Codex fallback with temp-home fixtures, the workflow-container case, nil binding, unsafe session IDs, and unsupported kinds; CmuxAgentConversation is added to the CI standalone-package list.

Confidence Score: 5/5

Safe to merge — the fallback is read-only, gated to Claude/Codex kinds only, validates session IDs as safe filename components, and leaves the live-index path completely unchanged.

The change adds a well-bounded fallback: SurfaceResumeBindingSnapshot is nonisolated Sendable so the @MainActor → Task.detached capture is correct; the session-ID guard correctly blocks ., .., and path separators before the ID reaches any filesystem call; the workflow-container scan mirrors the live-index path and stops at the first matching project directory; and the new test suite covers all the described edge cases with real temp-home fixtures.

No files require special attention.

Important Files Changed

Filename Overview
Sources/AgentChat/AgentChatPresenter.swift Captures the panel's SurfaceResumeBindingSnapshot (which is nonisolated Sendable) on the main actor before the Task.detached hop; straightforward and correct.
Sources/AgentChat/AgentChatTranscriptResolver.swift Adds the two-source fallback pattern, session-ID safety check, and workflow-container resolution; logic is sound and the container scan correctly mirrors the live index behavior.
cmuxTests/AgentChatTranscriptResolverResumeBindingTests.swift Good coverage: Claude/Codex fallback with filesystem fixtures, workflow container selection, nil binding, path-traversal rejection, and unsupported kind; uses Swift Testing correctly.
cmux.xcodeproj/project.pbxproj Wires the new test file into the test target; change is minimal and correct.
.github/workflows/ci.yml Adds CmuxAgentConversation to the standalone-package resolution list, matching the new import in the test file.

Sequence Diagram

sequenceDiagram
    participant P as AgentChatPresenter (@MainActor)
    participant W as Workspace (@MainActor)
    participant D as Task.detached
    participant I as RestorableAgentSessionIndex
    participant R as AgentChatTranscriptResolver
    participant FS as Filesystem

    P->>W: surfaceResumeBinding(panelId:)
    W-->>P: SurfaceResumeBindingSnapshot?
    P->>D: detach(resumeBinding, workspaceId, panelId)
    D->>I: RestorableAgentSessionIndex.load()
    I-->>D: index
    D->>R: resolve(index, workspaceId, panelId, resumeBinding)
    alt Live index has entry
        R->>I: index.snapshot(workspaceId, panelId)
        I-->>R: AgentSessionSnapshot (kind, sessionId, cwd, env)
        R->>FS: locate transcript (.jsonl)
        FS-->>R: URL?
        R-->>D: Resolution(agentKind, sessionId, transcriptURL)
    else Index miss — session-restored terminal
        R->>R: resumeBindingInputs(binding)
        Note over R: validate checkpointId (safe filename component), gate kind to Claude/Codex only
        R->>FS: locate transcript (.jsonl or workflow-container sibling)
        FS-->>R: URL?
        R-->>D: Resolution(agentKind, sessionId, transcriptURL)
    else Neither source yields a session
        R-->>D: nil
    end
    D-->>P: Resolution?
    alt Resolution found
        P->>P: AgentChatWindowController.present(for:)
    else nil
        P->>P: presentNoSessionAlert()
    end
Loading

Reviews (3): Last reviewed commit: "CI: gate CmuxAgentConversation package t..." | Re-trigger Greptile

Comment on lines +142 to +150
/// Maps a resume binding's kind string to a `RestorableAgentKind`, limited
/// to the transcript-backed kinds the P1 parsers support.
private func restorableKind(fromBindingKind kind: String?) -> RestorableAgentKind? {
switch kind?.lowercased() {
case "claude": return .claude
case "codex": return .codex
default: return nil
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Dual maintenance point between restorableKind and resolve()

restorableKind(fromBindingKind:) re-implements the same string-to-enum mapping that RestorableAgentKind.init(rawValue:) already covers, and it must be kept in sync with the switch inputs.kind cases in resolve(). If a new transcript-backed kind (e.g., .gemini) is ever added to that switch, a developer has to remember to add it here too — there is no compile-time enforcement of the coupling. Because resolve() already has default: return nil for non-transcript-backed kinds, the filtering could live entirely in that switch, letting restorableKind simply delegate to RestorableAgentKind(rawValue: kind?.lowercased() ?? "").

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

lawrencecchen and others added 2 commits June 10, 2026 04:20
…review)

The fallback used SurfaceResumeBindingSnapshot.checkpointId directly as a
transcript filename component, but resume bindings are a trust boundary (the
public resume path does not validate the checkpoint id). A value with a path
separator or '..' could escape the selected project/sessions dir into another
reachable .jsonl. The fallback now applies the same safe-filename invariant
the live index path uses for Claude. Test covers ../, a/b, '.', '..'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fallback

Autoreview: a Claude workflow/sub-agent resume binding records a *container*
directory id, not a transcript id; the real .jsonl is a newer sibling with a
different id. The fallback now mirrors the live index path's
resolvedClaudeWorkflowRecord: when <id> is a container dir under a project
dir, it resolves to the newest sibling transcript. Test covers the
container-with-two-siblings case (newest wins).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Copy link
Copy Markdown

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

ℹ️ 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".

let id = String(child.dropLast(".jsonl".count))
guard id != excluded, !id.isEmpty else { continue }
let path = (projectDir as NSString).appendingPathComponent(child)
guard regularFileExists(path) else { continue }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore empty workflow sibling transcripts

For restored Claude workflow panels, this fallback can select the newest empty .jsonl sibling because it only checks that the path is a regular file. The live restore path this mirrors (RestorableAgentSessionIndex.newestClaudeSiblingTranscript) filters with regularNonEmptyFileExists, so when Claude leaves a freshly-created empty sibling next to an older transcript with content, View Chat opens a blank conversation even though the resumable transcript exists. Apply the same non-empty check before comparing modification times.

Useful? React with 👍 / 👎.

The resume-binding fallback added Packages/CmuxAgentConversation with Claude/
Codex transcript parser tests, but the Swift-package-unit-tests CI step's
PACKAGES array omitted it, so parser regressions compiled but never gated PRs.
Confirmed it resolves standalone (swift test, 14 tests, no GhosttyKit dep) and
added it to the array.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

lawrencecchen added a commit that referenced this pull request Jun 12, 2026
…f PR 5791)

When both the live hook index and the workspace's in-memory restored
snapshot miss (a terminal restored after an app relaunch whose snapshot
was consumed or never captured), the resolver now reconstructs the
(kind, sessionId, cwd, env) lookup tuple from the panel's persisted
SurfaceResumeBindingSnapshot (checkpointId is the agent session id the
resume path uses). Binding checkpoint ids are validated as safe filename
components (trust boundary). Also ports the Claude workflow-container
fallback: when the recorded id is a container directory, the newest
sibling transcript in the same project dir resolves instead.

Ported from #5791 (stacked on
the closed feat-agent-chat-view base), adapted to the current resolver
API; Swift Testing coverage included and wired into the test target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Ported into #5736 as commit ea36eb8 (the resolver now falls back to the panel's persisted resume binding when the live index misses, adapted to the Go-daemon architecture, with equivalent Swift Testing coverage). This branch's base (feat-agent-chat-view, PR 5576) was closed as superseded, so closing this one too.

This branch was successfully deployed

1 active deployment
Preview – cmux — dc82bb63 Deployed Jun 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant