Skip to content

Fix unbounded Vault JSONL history scans - #4536

Closed
austinywang wants to merge 20 commits into
mainfrom
issue-4535-vault-unbounded-scans
Closed

austinywang wants to merge 20 commits into
mainfrom
issue-4535-vault-unbounded-scans

Conversation

@austinywang

@austinywang austinywang commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a shared SessionIndexStore.forEachJSONLine streaming primitive with strict maxBytes, optional maxLines, forward/reverse direction, stop summaries, and a per-record autoreleasepool around Data.subdata, JSON decoding, and the caller body.
  • Port Antigravity Vault indexing to a bounded reverse scan of append-only history.jsonl, so the sorted/deduped set is capped by a page-sized byte/line budget instead of the whole file.
  • Port Antigravity transcript preview to the same bounded reverse reader with a fixed preview scan cap and truncation marker when the scan cap is hit.

Reproduction / verified code shape

  • Sources/SessionIndexRegisteredAgents.swift:466-549 on current main: loadAntigravityHistoryEntries used forEachJSONLine(url: historyURL, maxBytes: Int.max), built the full latestBySessionID dictionary, sorted all latestBySessionID.values, then sliced with dropFirst(offset).prefix(limit).
  • Sources/SessionIndexStore.swift:1010-1045 on current main: the JSONL loop read chunks, created lineData with leftover.subdata(in:), decoded with JSONSerialization.jsonObject(with:), and invoked body(obj) without a per-line autoreleasepool.
  • Sources/SessionIndexView.swift:1284-1317 on current main: Antigravity preview called SessionIndexStore.forEachJSONLine(url: url, maxBytes: Int.max) and only stopped after collecting maxPreviewTurns matching records, so nonmatching history rows could force a full-file scan.

Design decision

Shared primitive. The bug class was duplicated scan policy, so the JSONL reader now owns byte limits, line limits, traversal direction, and autorelease pressure; callers choose budgets instead of open-coding unbounded loops.

Verification

  • Added SessionIndexJSONLStreamTests; the first commit adds a failing regression where maxLines: 3 should visit only three JSONL records.
  • Added reverse-order and byte-cap coverage so Antigravity's latest-history path has deterministic line/byte-bound behavior.
  • Local tests/builds were not run because repo/task instructions prohibit local test/build execution for this task.

Perf notes

No RSS benchmark was collected. The deterministic before/after signal is line/byte boundedness: before the regression's maxLines: 3 path visited all 10 records; after the fix it visits 3 and reports .maxLines. Reverse byte-cap coverage asserts the reader does not read beyond the supplied byte budget.


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


Note

Medium Risk
Changes session list ordering/pagination and preview truncation for Antigravity history; behavior is bounded and tested but affects how large histories are indexed and displayed.

Overview
Replaces unbounded Vault history.jsonl reads with a shared SessionIndexStore.forEachJSONLine helper that enforces maxBytes, optional maxLines, forward/reverse traversal, per-line autoreleasepool, and a stop summary (bytes, lines, reason).

Antigravity session listing now scans newest-first with a byte budget tied to offset + limit (full max window when search/cwd filters apply), keeps reverse discovery order instead of sorting the whole deduped set, merges sparse rows to backfill title/cwd, and stops early once the page has enough stable metadata—so duplicate lines do not burn the whole file budget.

Antigravity transcript preview uses the same bounded reverse reader (fixed byte/line caps) and inserts a localized “Preview truncated” event when limits or turn caps are hit. Adds SessionIndexJSONLStreamTests and Antigravity pagination/metadata tests; expands sessionIndex.preview.truncated localizations.

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


Summary by cubic

Fixes unbounded Vault JSONL history scans by adding a capped streaming reader and moving Antigravity listing/preview to reverse, budgeted scans that stop once page metadata stabilizes. Preserves newest-first history ordering, makes pagination deterministic, and prepends a localized “Preview truncated” when limits are hit. Addresses Linear 4535.

  • Bug Fixes

    • Added SessionIndexStore.forEachJSONLine with maxBytes, optional maxLines, forward/reverse traversal, per-record autoreleasepool, and a summary (bytes read, lines visited, stop reason).
    • Antigravity session listing: reverse-scans history.jsonl with byte budgets scaled to offset + limit (uses max window for search/cwd filters), preserves reverse discovery order with a discovery-index tiebreaker, backfills title/cwd from older rows, stops once target count and metadata are stable, and skips duplicate rows.
    • Transcript preview: uses the same bounded reverse scan with fixed byte/line caps and inserts a localized “Preview truncated” event when turn, byte, or line limits stop the stream.
    • Tests/localization: added SessionIndexJSONLStreamTests; expanded PiVaultAgentPersistenceTests for pagination/backfill/sparse-search/duplicate-row/ordering cases; broadened sessionIndex.preview.truncated localizations.
  • Refactors

    • Updated Swift file length budget for CI and adjusted CLAUDE.md separators to avoid conflict-marker false positives.

Written for commit 4b73d17. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Configurable byte/line scan budgets and per-request scan limits; streaming now returns a summary (bytes, lines, stop reason) and supports forward/reverse traversal.
  • Bug Fixes
    • History search and transcript previews use bounded reverse scanning to avoid unbounded reads and improve responsiveness.
    • Truncation markers added when scans stop due to limits or preview count.
    • Registered-agent pagination now returns newest sessions in stable reverse-discovery order.
  • Tests
    • Added tests for forward/reverse streaming, byte/line limits, stop reasons, ordering, pagination, and metadata backfill.

Review Change Stack

@vercel

vercel Bot commented May 22, 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 14, 2026 11:54pm
cmux-staging Building Building Preview, Comment Jun 14, 2026 11:54pm

@coderabbitai

coderabbitai Bot commented May 22, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replace unbounded forward JSONL scanning with bounded, directional streaming that returns summaries and stop reasons; apply bounded reverse scans with computed byte/line budgets to antigravity history and preview loading; add tests verifying limit enforcement and stop reasons.

Changes

Bounded JSONL streaming for antigravity history and preview

Layer / File(s) Summary
JSONL streaming primitives
Sources/SessionIndexStore.swift
New SessionIndexJSONLStreamDirection, SessionIndexJSONLStreamStopReason, and SessionIndexJSONLStreamSummary. forEachJSONLine now returns a summary, supports direction and maxLines, and implements forward and reverse chunked streaming with a shared per-line processing helper.
Antigravity history scan limits & pagination
Sources/SessionIndexRegisteredAgents.swift
Adds file-private min/max byte and line budgets, per-request scaling, antigravityHistoryScanLimits(offset:limit:), switches antigravity history scanning to bounded reverse traversal, and preserves reverse-discovery order when paging.
Antigravity preview reverse scanning
Sources/SessionIndexView.swift
Adds preview scan constants and updates loadAntigravityHistorySynchronously to use forEachJSONLine with direction: .reverse and bounds, collect reversed matches, remap to final turns, and insert truncation markers when scan limits stop the scan.
JSONL streaming & pagination tests
cmuxTests/SessionIndexJSONLStreamTests.swift, cmuxTests/PiVaultAgentPersistenceTests.swift
Tests for forward maxLines, reverse maxLines, reverse maxBytes, byte-boundary behavior, pagination stability, and metadata backfill behavior for antigravity history.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #4535: Matches the unbounded-scan fix by introducing bounded, directional JSONL streaming and applying it to antigravity history and preview.

Poem

I hop the file from tail to head with care,
bytes and lines kept modest in my lair,
reverse I bound to find the freshest thread,
paginate steady, no wild scans to dread,
— a rabbit, pleased with tidy history fare 🐇


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error SessionTranscriptLoader static constants (antigravityPreviewMaxScanBytes, etc.) are implicitly @MainActor but accessed from Task.detached, violating actor isolation. Mark static let constants in SessionTranscriptLoader with nonisolated, matching SessionIndexRegisteredAgents.swift pattern.
Cmux Full Internationalization ❌ Error New sessionIndex.preview.truncated key in Localizable.xcstrings has translations only for en, ja but catalog supports 20 locales, missing 18 required locale translations. Add translations for all 18 missing locales (ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant) to sessionIndex.preview.truncated in Localizable.xcstrings.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix unbounded Vault JSONL history scans' directly and accurately summarizes the main change: adding bounded streaming for JSONL reads and fixing unbounded scans.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Blocking Runtime ✅ Passed No new blocking primitives introduced. PR adds bounded JSONL streaming with scan budgets; pre-existing locks/sleeps untouched.
Cmux No Hacky Sleeps ✅ Passed All PR changes are Swift files. The custom check applies only to TypeScript, JavaScript, shell, and non-Swift build/runtime scripts. Swift timing primitives are covered by a separate rule.
Cmux Swift Concurrency ✅ Passed PR does not introduce new legacy async patterns; forEachJSONLine closure is appropriate for synchronous disk I/O, and pre-existing patterns unchanged.
Cmux Swift @Concurrent ✅ Passed PR does not violate concurrent annotation rules. New I/O primitives are synchronous. Async helpers use Task.detached with priority for proper actor hops.
Cmux Swift File And Package Boundaries ✅ Passed Line additions are under 250-line threshold (186, 28, 78 lines to oversized files). Focused bug-fix bounding unbounded JSONL scans with clear extraction path.
Cmux Swift Logging ✅ Passed No print/debugPrint/dump/NSLog in production code. Logger properly declared (nonisolated private let) with os.log import. Single logger usage has privacy protection (.public).
Cmux User-Facing Error Privacy ✅ Passed All new user-facing strings are generic and safe. Detailed errors remain internal; users see only generic messages like "Couldn't load transcript" and "Preview truncated".
Cmux Swiftui State Layout ✅ Passed No new @Published/@observable state patterns introduced. Existing SessionIndexStore unchanged. No GeometryReader additions. State mutations properly in event handlers, not render body.
Cmux Architecture Rethink ✅ Passed PR adds JSONL streaming with clear ownership via new enums/structs. No mutable flags, observers, locks, or split UI lifecycle. Local transient state does not violate architectural rules.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR introduces bounded JSONL scanning and metadata backfill, not new standalone windows. No NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup created.
Description check ✅ Passed PR description provides comprehensive summary, rationale, and design decisions; covers what changed, why, and includes testing/verification notes and performance analysis.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-4535-vault-unbounded-scans

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.

Comment thread Sources/SessionIndexRegisteredAgents.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/SessionIndexStore.swift Outdated
@greptile-apps

greptile-apps Bot commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces unbounded history.jsonl reads with a shared SessionIndexStore.forEachJSONLine primitive that enforces maxBytes, optional maxLines, forward/reverse traversal, per-record autoreleasepool, and a stop summary. Antigravity session listing and transcript preview are both ported to bounded reverse scans with early-stop when stable metadata fills the page.

  • Streaming primitive (SessionIndexStore): chunked reverse reader assembles cross-chunk lines via a carry buffer; shouldProcessReverseCarry correctly detects whether the carry starts at a line boundary; processJSONLineData gates on maxLines before incrementing linesVisited, consistent with the documented contract.
  • Session listing (SessionIndexRegisteredAgents): reverse scan with scaled byte budget (offset + limit × 256 KB, clamped to 2–24 MB), stable-target early-stop across duplicate rows, and mergingMissingFields to backfill title/cwd from older records; the outer-loop break prevents unnecessary root scans once the target is satisfied.
  • Transcript preview (SessionIndexView): reverse scan bounded at 24 MB / 12 000 lines; the truncation marker is now correctly inserted at index 0 (older end) of the reversed turn list; didHitTurnLimit, .maxBytes, and .maxLines all trigger the marker.

Confidence Score: 4/5

Safe to merge for active sessions; dormant sessions in large history files may now show an empty preview pane with only a truncation banner.

The streaming primitive and session-listing path are correct and well-tested. The one real concern is loadAntigravityHistorySynchronously: it now reads tail-first with a hard 12 000-line / 24 MB cap, so any session whose most-recent record sits beyond that window delivers zero turns plus 'Preview truncated' — previously the forward scan would have found those records regardless of file size. This is an intentional performance trade-off, but it introduces a user-visible regression for dormant sessions in vaults with heavy ongoing activity.

Sources/SessionIndexView.swift — specifically loadAntigravityHistorySynchronously and whether the shared antigravityHistoryMaximumScanLines constant is the right cap for the preview path.

Important Files Changed

Filename Overview
Sources/SessionIndexStore.swift Adds forward/reverse JSONL streaming with maxBytes, maxLines, per-line autoreleasepool, and stop summary; chunked reverse-read logic is correct (carry assembly, shouldProcessReverseCarry boundary check, empty-range guard).
Sources/SessionIndexRegisteredAgents.swift Ports Antigravity listing to bounded reverse scan with per-entry metadata merging, stable-target early-stop, and per-root break; multi-root tiebreaker changed from sessionId lexicographic to cross-root discovery order (affects equal-timestamp ties only).
Sources/SessionIndexView.swift Antigravity preview switched to bounded reverse scan; sessions dormant beyond the 12 000-line / 24 MB tail window produce 'Preview truncated' with zero turns — a regression for old sessions in large history files.
cmuxTests/SessionIndexJSONLStreamTests.swift New Swift Testing suite; covers forward/reverse maxLines, reverse maxBytes, chunk-boundary carry assembly, and trailing-line flush at exact byte budget.
cmuxTests/PiVaultAgentPersistenceTests.swift Migrated from XCTest to Swift Testing; adds pagination, metadata backfill, sparse-search, and duplicate-row tests for Antigravity listing.
Resources/Localizable.xcstrings Adds all 20 supported locale translations for sessionIndex.preview.truncated; coverage is complete.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[forEachJSONLine\nmaxBytes · maxLines · direction] --> B{direction}
    B -- forward --> C[streamJSONLinesForward\nread chunks left to right\nflush trailing line at EOF]
    B -- reverse --> D[streamJSONLinesReverse\nseek from fileSize backward\nbuffer = chunk + carry]
    C --> E[processJSONLineData\ncheck maxLines\n++ linesVisited\nautoreleasepool → body]
    D --> E
    E -- stoppedByBody --> F[return .stoppedByBody]
    E -- maxLines hit --> G[return .maxLines]
    E -- nil continue --> H{more data within budget?}
    H -- yes --> C
    H -- yes --> D
    H -- no or EOF --> I{reachedEOF or didReadEntireFile?}
    I -- yes leftover --> E
    I -- no --> J[return .maxBytes]
    I -- yes no leftover --> K[return .completed]
    D --> L[shouldProcessReverseCarry\nposition==0 flush\nposition>0 peek byte before window]
    L -- is newline flush --> E
    L -- not newline skip --> M[return .maxBytes or .completed]
    subgraph Antigravity Listing
        N[loadAntigravityHistoryEntries\noffset+limit budget] --> A
        A --> O[merge metadata\nstable-target early-stop\nbreak across roots]
        O --> P[sort by modified\ntiebreak reverseOrderIndex]
        P --> Q[dropFirst prefix page]
    end
    subgraph Antigravity Preview
        R[loadAntigravityHistorySynchronously\n24MB 12000 lines reverse] --> A
        A --> S[collect matching turns\nreverse order]
        S --> T[reverse re-index\ninsert truncation marker at 0]
    end
Loading

Reviews (11): Last reviewed commit: "fix: preserve antigravity history orderi..." | Re-trigger Greptile

Comment thread Sources/SessionIndexView.swift
Comment thread Sources/SessionIndexStore.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 22, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/SessionIndexRegisteredAgents.swift`:
- Around line 533-540: The current reverse-scan returns early when
latestBySessionID[sessionId] is non-nil, causing newer-but-partial metadata to
block filling missing fields from older records; update the logic in the loop
that handles latestBySessionID and sessionIDsInReverseHistoryOrder so that when
latestBySessionID[sessionId] exists you merge missing fields (e.g., title, cwd)
from the current metadata into the stored metadata instead of immediately
returning false, and only append sessionId and increment the count when you
first create the entry; ensure the merge stops once all required fields are
populated or after processing older records so the entry is complete before
counting toward the target.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f2a5d4cd-f041-4aef-8941-486d9df37da8

📥 Commits

Reviewing files that changed from the base of the PR and between 59f0005 and 2730c99.

📒 Files selected for processing (5)
  • Sources/SessionIndexRegisteredAgents.swift
  • Sources/SessionIndexStore.swift
  • Sources/SessionIndexView.swift
  • cmuxTests/PiVaultAgentPersistenceTests.swift
  • cmuxTests/SessionIndexJSONLStreamTests.swift

Comment thread Sources/SessionIndexRegisteredAgents.swift Outdated
@austinywang
austinywang dismissed coderabbitai[bot]’s stale review May 22, 2026 05:54

Dismissed after the requested metadata-backfill fix landed in e6f2e9f, the review thread was resolved, and CodeRabbit reported success on the latest commit.

@lawrencecchen

Copy link
Copy Markdown
Contributor

Non-blocking review notes from a follow-up Codex pass — all small, no need to block on them:

  1. [P2] cmuxTests/SessionIndexJSONLStreamTests.swift is not in cmux.xcodeproj (no PBXFileReference / PBXSourcesBuildPhase entry — PiVaultAgentPersistenceTests.swift is, but the new file is not). The new max-lines / max-bytes / reverse regressions won't be compiled or executed by the cmuxTests target. Consider wiring it into the test target so CI actually runs the coverage you added.

  2. [P2] Preview vs index scan-window mismatch: Sources/SessionIndexRegisteredAgents.swift:600-601 caps the indexed reverse scan at antigravityHistoryMaximumScanBytes = 24 MiB, while Sources/SessionIndexView.swift:1056,1299-1303 caps the preview's reverse scan at antigravityPreviewMaxScanBytes = 16 MiB. A row that the list legitimately surfaced from the 24 MiB window but whose record sits >16 MiB from EOF will open into a preview that only shows the truncation marker. Either align the preview budget with the index's upper bound or seek from the listed record's known offset.

  3. [P3] Forward streaming may drop a trailing line when EOF lands exactly on maxBytes. In streamJSONLinesForward (Sources/SessionIndexStore.swift:1056-1108), when summary.bytesRead == maxBytes the while exits without ever entering the empty-read branch, so reachedEOF stays false and the trailing-line flush (lines 1095-1106) is skipped — stop reason becomes .maxBytes and the final line (no trailing newline, fully within the byte budget) is silently dropped. Low-likelihood today since Antigravity history rows almost always end in \n and budgets are generous, but easy to fix by also flushing when summary.bytesRead == maxBytes && summary.bytesRead == fileSize or by detecting the exact-cap EOF.

Not requesting changes — flagging for awareness.

@lawrencecchen

Copy link
Copy Markdown
Contributor

Empirical confirmation of the codex-review finding above: ran xcodebuild test -scheme cmux-unit -only-testing:cmuxTests/SessionIndexJSONLStreamTests against this branch HEAD (e6f2e9fc4) on an AWS macOS 15.7.4 builder:

Test Suite 'cmuxTests.xctest' passed
    Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds

Same result at the red commit 36674ca4e — 0 tests executed in both. The regression test is silently skipped because SessionIndexJSONLStreamTests.swift isn't a member of the cmuxTests target's PBXSourcesBuildPhase.

I added a one-shot lint that catches this class of bug in seconds. Reproducer from any cmux checkout:

./skills/regression-hunt/scripts/lint-pbxproj-test-wiring.sh --repo-root .

On this branch it currently reports:

lint-pbxproj-test-wiring: 3 test file(s) not wired into cmux.xcodeproj/project.pbxproj
  - SessionIndexJSONLStreamTests.swift (hits=0)
  - SessionIndexViewTests.swift (hits=0)
  - SidebarMarkdownRendererTests.swift (hits=0)

(The latter two are pre-existing on main and unrelated to this PR — filing https://github.com/manaflow-ai/cmux/issues/ separately for those.)

The lint script and the broader regression-hunt skill are in https://github.com/manaflow-ai/cmuxterm-hq/tree/main/skills/regression-hunt and the script is being proposed for CI wiring in a follow-up PR.

lawrencecchen added a commit that referenced this pull request May 22, 2026
#4562)

* ci: lint that every cmuxTests Swift file is wired into pbxproj

Catches the class of bug surfaced during the
#4529 investigation: a
test file added to the worktree without a matching entry in
cmux.xcodeproj/project.pbxproj is silently ignored by Xcode and
never compiles or runs on CI. Both bot reviews and
`xcodebuild test -only-testing:cmuxTests/<TestClass>` pass with
"Executed 0 tests" — so the missing wiring is indistinguishable
from a clean two-commit red/green regression test until a real
user hits the bug the test was supposed to catch.

This is the RED commit of a two-commit pattern: introducing the
lint immediately flags two pre-existing orphans on main
(SessionIndexViewTests.swift and SidebarMarkdownRendererTests.swift)
and the new `workflow-guard-tests` step turns red. The follow-up
commit wires those two files into the cmuxTests target so the lint
goes green.

Refs #4559

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: wire SessionIndexViewTests and SidebarMarkdownRendererTests + document pitfalls

GREEN commit of the two-commit pattern started in the previous commit.

`scripts/lint-pbxproj-test-wiring.sh` (added previously) flagged two
pre-existing test files on `main` that have never compiled or run on
CI because they were never added to the cmuxTests target:

  - cmuxTests/SessionIndexViewTests.swift
  - cmuxTests/SidebarMarkdownRendererTests.swift

Both contain real-looking XCTest coverage (Claude local-command-caveat
title formatting, markdown inline-attribute preservation). Wiring them
into `cmux.xcodeproj/project.pbxproj` so they actually run, which also
closes #4559 and lets the
new `workflow-guard-tests` lint step go green on this PR.

Also adds two CLAUDE.md "Pitfalls" entries based on what fell out of
the #4529 investigation:

  - Foundation/SwiftUI/AttributeGraph/WebKit semantics change silently
    between macOS versions (concrete `URL.deletingLastPathComponent`
    example from #4529). Recommends AWS M4 Pro builders for empirical
    repro and points to the `regression-hunt` skill.
  - Test files in cmuxTests/ must be wired into project.pbxproj or
    they're silently skipped. References the new lint script and the
    PR #4536 incident that surfaced the class of bug.

Self-test of `tests/test_ci_pbxproj_test_wiring.sh` also got a small
hardening: the synthetic sandbox pbxproj no longer mentions the
orphan test file by name in a comment, which used to produce a
spurious `hits=1` and mask the failure detection inside the wrapper.

Closes #4559

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: handle .registered SessionAgent case in test helper to compile

cmuxTests/SessionIndexViewTests.swift was not in the test target until
the previous commit wired it into project.pbxproj. While it was orphan,
the SessionAgent enum gained a `.registered(RegisteredSessionAgent)`
case (Sources/SessionIndexModels.swift:44) that the test's
`defaultSpecificsForTesting` switch never accounted for. With the test
file now actually compiling on CI, the switch fails:

  SessionIndexViewTests.swift:391:9: error: switch must be exhaustive
  note: add missing case: '.registered(_)'

The test's call sites only ever pass built-in agents (`.claude`,
`.grok`). Adding a `fatalError` on `.registered` keeps the switch
exhaustive without inventing fake `CmuxVaultAgentRegistration` data
that future readers would have to reconcile; if anyone extends the
suite to cover Vault-registered agents, the fatalError points them at
the missing helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: tighten pbxproj lint to require target membership, not just file reference

Earlier version of the lint counted any pbxproj line that mentioned the
test file basename. That's too permissive: a file can have a
PBXFileReference and a group children entry but still not be a member
of the cmuxTests target's PBXSourcesBuildPhase, in which case Xcode
silently skips it — the exact failure mode that lets a regression test
land green without ever running.

Switch to counting only lines that end with "<basename>.swift in
Sources */", which appear in:
  1. the PBXBuildFile entry, and
  2. the cmuxTests target's PBXSourcesBuildPhase files list.

A target member has hits >= 2 in both. A "group-only" file (referenced
in the project tree but not part of the test target) has hits = 0,
which is exactly the silent-skip case the lint must catch.

Also adds a new (c) sandbox case in
`tests/test_ci_pbxproj_test_wiring.sh` that drops a file with a
PBXFileReference + group child but no PBXBuildFile / SourcesBuildPhase
entry, and asserts the lint flags it. Without this case the wrapper
would still pass against the looser bare-filename check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: scope pbxproj test-wiring check to cmuxTests target's Sources phase

Earlier iterations counted matches in the whole pbxproj. That accepted two
silent-skip cases:

  1. A file with PBXFileReference + group child but no PBXBuildFile /
     SourcesBuildPhase entry (filename appears 2x globally, but the file is
     not a member of any target — Xcode does not compile it).
  2. A file wired into the wrong target (e.g. cmuxUITests instead of
     cmuxTests). `<file>.swift in Sources` appears 2x in the pbxproj — once
     in PBXBuildFile, once inside cmuxUITests' Sources phase — but Xcode
     still does not compile it into the cmuxTests bundle, so the regression
     test never runs.

Resolve the cmuxTests PBXNativeTarget and its Sources build phase UUID,
slice that phase block, and look for the file's `in Sources` entry only
inside that block. Threshold becomes 1 hit (membership) rather than a
global count, which is exactly what determines whether Xcode compiles the
file into cmuxTests.

Tighten the test wrapper to exercise all three failure modes against
synthetic pbxprojs that include a real cmuxTests PBXNativeTarget + Sources
phase stub. Verified by inverse: with this change reverted on a scratch
copy of the real pbxproj, the lint misses the wrong-target case; with it
applied, the lint flags it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ci: anchor pbxproj membership match against suffix-overlap false negatives

The previous check looked for `<base> in Sources` inside the cmuxTests
Sources phase. That's vulnerable to filename-suffix overlap: if the lint
target is a substring of another wired file, the longer match still
satisfies the grep. The repo already contains an overlapping pair —
`SearchIndexTests.swift` is a suffix of `SettingsSearchIndexTests.swift`
— so accidentally removing `SearchIndexTests.swift` from the cmuxTests
Sources phase would pass the lint.

Switch to a fixed-string match against the full PBX comment
`/* <base> in Sources */`. The leading `/* ` and trailing ` */`
disambiguate the basename. Verified by inverse: scratch-removing
`SearchIndexTests.swift` from the real cmuxTests Sources phase now flags
the file specifically, while `SettingsSearchIndexTests.swift` is
unaffected.

Add a fifth sandbox case (e) in the wrapper that wires `PrefixFooTests.swift`
but leaves `FooTests.swift` orphan, and asserts the lint flags only the
orphan. Without this fixture a later loosening of the grep would not be
caught.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6edf4c3. Configure here.

Comment thread Sources/SessionIndexRegisteredAgents.swift Outdated
Comment thread Sources/SessionIndexRegisteredAgents.swift

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants