Skip to content

Add offline agent notes CLI - #6485

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
feature-offline-agent-notes
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
feature-offline-agent-notes

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add cmux notes for storing offline agent notes without a cmux socket.
  • Add cmux notes flush to submit pending notes into an agent terminal once cmux is available.
  • Cover no-socket add/list and socket-backed flush with CLI integration tests.

Tests

  • python3 scripts/normalize-pbxproj.py && scripts/check-pbxproj.sh
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv
  • python3 scripts/check-test-determinism.py --strict
  • ./tests/test_ci_pbxproj_test_wiring.sh
  • CMUX_ALLOW_LOCAL_XCODEBUILD=1 xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-offnot-tests -only-testing:cmuxTests/CLINotifyProcessIntegrationRegressionTests/testOfflineNotesAddAndListWorkWithoutSocket -only-testing:cmuxTests/CLINotifyProcessIntegrationRegressionTests/testOfflineNotesFlushSubmitsPendingNotesAndMarksThemFlushed test

Dogfood

@vercel

vercel Bot commented Jun 20, 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 20, 2026 6:11am
cmux-staging Building Building Preview, Comment Jun 20, 2026 6:11am

@coderabbitai

coderabbitai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new offline agent notes subsystem to the cmux CLI. A new OfflineAgentNotesStore persists note records to a JSON file with POSIX permission hardening. A new CMUXCLI+OfflineAgentNotes.swift file implements the full notes command with add, list, clear, flush, and path subcommands, wired into cmux.swift routing and registered in the Xcode project, with integration tests covering offline and socket-backed flush flows.

Changes

Offline Agent Notes Feature

Layer / File(s) Summary
Data model, store init, CRUD, and secure persistence
CLI/CMUXCLI+OfflineAgentNotes.swift
OfflineAgentNoteRecord and OfflineAgentNotesState define the persisted data shapes. OfflineAgentNotesStore implements path selection via CMUX_OFFLINE_AGENT_NOTES_PATH, atomic JSON writes with POSIX 700/600 permissions, and all CRUD operations: add, notes (filtered/sorted), clear, and markFlushed.
CLI help text, socket-independence check, and subcommand dispatch
CLI/CMUXCLI+OfflineAgentNotes.swift
offlineNotesUsageHelp() provides subcommand documentation. offlineNotesCommandDoesNotNeedSocket allows all subcommands except flush to run without a socket. runOfflineNotesCommand parses the subcommand string and dispatches to the appropriate handler.
Subcommand handlers: add, list, clear, flush, and output helpers
CLI/CMUXCLI+OfflineAgentNotes.swift
runOfflineNotesAdd parses metadata flags and enqueues a note with environment defaults. runOfflineNotesList and runOfflineNotesClear filter by agent/all and emit JSON or human output. runOfflineNotesFlush resolves handles via SocketClient, builds a prompt via offlineNotesPrompt, sends surface.send_text, and calls markFlushed. Includes offlineNotePayload and shortOfflineNoteID helpers.
cmux.swift dispatch, command names, and help text
CLI/cmux.swift
Adds an early socket-independent branch for note/notes commands, a main switch case routing to runOfflineNotesCommand, extensions to the recognized command name list, per-command help dispatch, and a notes <add|list|flush|clear|path> entry in the top-level help.
Xcode project registration
cmux.xcodeproj/project.pbxproj
Registers CMUXCLI+OfflineAgentNotes.swift in the cmux-cli target and CMUXOfflineAgentNotesCommandTests.swift in the cmuxTests target via PBXBuildFile, PBXFileReference, group membership, and PBXSourcesBuildPhase entries.
Integration tests: offline add/list and socket-backed flush
cmuxTests/CMUXOfflineAgentNotesCommandTests.swift
Tests offline notes add + notes list --json without a socket, asserting note fields and nil flushed_at. Tests socket-backed notes flush using a mock server to verify surface.send_text payload, then confirms pending queue is empty and flushed_at is populated in --all listing.

Sequence Diagram(s)

sequenceDiagram
  participant User as User / CI
  participant cmux as cmux.swift router
  participant FlushHandler as runOfflineNotesFlush
  participant Store as OfflineAgentNotesStore
  participant FS as FileSystem (~/.cmuxterm/...)
  participant Socket as SocketClient

  User->>cmux: cmux notes add --agent codex "some text"
  cmux->>FlushHandler: runOfflineNotesCommandWithoutSocket (no socket needed)
  FlushHandler->>Store: add(text, agent, ...)
  Store->>FS: load() + append record + save(0600)
  Store-->>User: OK note=<id>

  User->>cmux: cmux notes flush --agent codex
  cmux->>FlushHandler: runOfflineNotesCommand(client, ...)
  FlushHandler->>Store: notes(includeFlushed: false, agent: "codex")
  Store->>FS: load()
  FS-->>Store: [OfflineAgentNoteRecord]
  Store-->>FlushHandler: pending records
  FlushHandler->>Socket: normalizeHandle (workspace/surface/window)
  Socket-->>FlushHandler: resolved handles
  FlushHandler->>Socket: surface.send_text(offlineNotesPrompt)
  Socket-->>FlushHandler: OK
  FlushHandler->>Store: markFlushed(ids:)
  Store->>FS: save updated state
  FlushHandler-->>User: OK flushed=N
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#4562: Adds a CI/pbxproj wiring lint that checks new cmuxTests/*.swift files are included in the cmuxTests Sources build phase, which directly applies to this PR's registration of CMUXOfflineAgentNotesCommandTests.swift in project.pbxproj.

Poem

🐰 Hop hop, the notes won't get lost anymore,
Tucked in a JSON file behind a locked door,
Permissions set tight, 0600 and snug,
Then flushed to the agent with one little shrug.
The rabbit remembers when sockets were gone —
Now offline, online, the notes carry on! 🗒️


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 File And Package Boundaries ❌ Error File mixes persistence, networking, parsing, and socket protocol code in one file. OfflineAgentNotesStore (persistence layer) and CMUXCLI command dispatch should be separated into a small SwiftPM p... Extract OfflineAgentNotesStore to a new CmuxOfflineNotes SwiftPM package; keep CLI command routing in CMUXCLI extension that depends on it, meeting the package-boundary signals (stable domain, schema ownership, isolated testability).
Cmux Full Internationalization ❌ Error PR introduces 382 lines of production CLI code with user-facing strings not routed through String(localized:defaultValue:). Examples: usage help text, 7+ error messages, output strings ("OK note=..... Route user-facing text through String(localized:defaultValue:) with matching localization keys in Resources/Localizable.xcstrings covering all 19 supported locales (en, ja, zh-Hans, zh-Hant, ko, de, es, fr, it, pt-BR, ru, ar, bs, da, km,...
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add offline agent notes CLI' clearly and concisely describes the main feature being introduced—a new offline agent notes capability for the CLI.
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 Actor Isolation ✅ Passed Code is in sync CLI tool context (cmux-cli), not a MainActor-bound app. OfflineAgentNoteRecord/OfflineAgentNotesState are pure value data models (Codable, no async access). OfflineAgentNotesStore i...
Cmux Swift Blocking Runtime ✅ Passed New production code in CMUXCLI+OfflineAgentNotes.swift uses synchronous file I/O and JSON serialization with no blocking primitives (semaphores, waits, sleeps, locks, polling); test code uses XCTes...
Cmux Expensive Synchronous Load ✅ Passed The PR adds lightweight synchronous JSON file reads for offline notes, not expensive loaders. The load() method reads a single small JSON file with no per-record syscalls, unlike the expensive Rest...
Cmux Cache Substitution Correctness ✅ Passed OfflineAgentNotesStore performs fresh reads from persistent state via load() which directly reads from disk (Data(contentsOf:)) with no in-memory cache. Cold cache handled correctly with fileExists...
Cmux No Hacky Sleeps ✅ Passed Check applies only to TypeScript, JavaScript, and shell files. This PR contains only Swift code and Xcode project files, which are out of scope per runtime-no-hacky-sleeps.md.
Cmux Algorithmic Complexity ✅ Passed Offline notes feature operates on small, ephemeral collections (expected 1-100 items), not scalable workspace/pane/session trackers. No nested scans, no per-item rescans; all operations are linear...
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: no DispatchQueue for ordinary work, no Combine app state, no completion handlers, no fire-and-forget Tasks. Uses synchronous throwing functions for file I/O...
Cmux Swift @Concurrent ✅ Passed All new Swift code contains only synchronous functions with no async operations, @concurrent annotations, or isolation violations. The file I/O and network operations are appropriately synchronous...
Cmux Swiftpm Lockfiles ✅ Passed PR adds only Swift CLI source code and tests to Xcode project, with no new SwiftPM dependencies. Root Xcode Package.resolved lockfile was added (created), which satisfies the lockfile inclusion req...
Cmux Swift Logging ✅ Passed All print() calls are CLI command output (allowed), no NSLog/debugPrint/dump, no Logger issues, no sensitive data exposure. Complies with swift-logging.md rules.
Cmux User-Facing Error Privacy ✅ Passed All user-facing messages are generic and product-oriented. Error messages, status outputs, and help text contain no credentials, vendor names, provider-specific flags, billing details, or internal...
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI code, only CLI/Foundation-based Swift files. SwiftUI state layout check is not applicable.
Cmux Architecture Rethink ✅ Passed No architectural violations found. Code uses simple, immutable data models with clear single source of truth (JSON file), atomic persistence, no synchronization primitives, and follows existing soc...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds CLI offline notes feature with Foundation-only code; no NSWindow, NSPanel, NSWindowController, SwiftUI Window/WindowGroup, or auxiliary window creation detected.
Cmux Source Artifacts ✅ Passed All changed files are intentional hand-written source, configuration, and test files (Swift source, Xcode project config, test code) with clear product/test system purposes; no artifact patterns de...
Cmux No Test Or Debug Seam In Production Source ✅ Passed Check not applicable: PR modifies only CLI/ (not Sources/) and cmuxTests/ (explicitly excluded). No production Sources/ files are modified.
Description check ✅ Passed The PR description covers the main changes, testing approach, and validation steps comprehensively against the template.
✨ 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 feature-offline-agent-notes

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.

@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 3 potential issues.

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 eab7475. Configure here.

Comment thread CLI/CMUXCLI+OfflineAgentNotes.swift
let sfId = try normalizeSurfaceHandle(surfaceArg, client: client, workspaceHandle: wsId, windowHandle: winId)
if let sfId { params["surface_id"] = sfId }
_ = try client.sendV2(method: "surface.send_text", params: params)
try store.markFlushed(ids: Set(notes.map(\.id)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flush marks notes after send

Medium Severity

notes flush calls surface.send_text before persisting flushedAt. If markFlushed/save fails after a successful send, the CLI exits with an error while notes stay pending, so a retry delivers the same bundled prompt to the agent again.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eab7475. Configure here.

)
var state = try load()
state.notes.append(note)
try save(state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Concurrent note writes lose data

Medium Severity

OfflineAgentNotesStore updates use load-modify-save on a shared JSON file with no locking. Overlapping notes add, flush, or clear processes can overwrite each other’s changes, dropping notes or leaving flush state inconsistent.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eab7475. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds cmux notes (cmux note) for queuing offline agent reminders to a local JSON store (~/.cmuxterm/offline-agent-notes.json, overridable via CMUX_OFFLINE_AGENT_NOTES_PATH) and flushing them to an agent terminal surface via surface.send_text once cmux is running. The add, list, clear, and path subcommands bypass the socket entirely; flush requires a live cmux instance.

  • New file CMUXCLI+OfflineAgentNotes.swift — OfflineAgentNotesStore (load/save to JSON with 0o600 file permissions) plus CLI subcommand handlers for add, list, flush, clear, and path, following the existing CMUXCLI extension pattern.
  • cmux.swift dispatch — early socket-bypass guard for non-flush subcommands mirrors the existing settings early-dispatch idiom; socket-backed case \"notes\", \"note\" handles flush.
  • Integration tests cover add/list without a socket and a full flush round-trip against a mock socket server that validates surface.send_text params.

Confidence Score: 4/5

Safe to merge after clarifying the flush --all re-delivery behaviour; no changes to auth, core app state, or the socket protocol beyond the new surface.send_text call.

The new flush --all flag retrieves and re-sends notes that were previously flushed, causing the agent to receive duplicate prompts for work it already processed. The behaviour is internally consistent with how --all works on list and clear, but unlike those read-only or local-destructive operations, flushing produces an observable external side effect. This should be resolved — either document that --all intentionally resends, add a guard, or use a distinct flag for resend.

CLI/CMUXCLI+OfflineAgentNotes.swift — specifically the flush --all path in runOfflineNotesFlush

Important Files Changed

Filename Overview
CLI/CMUXCLI+OfflineAgentNotes.swift New 382-line file adding offline agent notes store and CLI subcommands; flush --all re-delivers already-flushed notes due to includeFlushed: true being passed to the notes query
CLI/cmux.swift Adds early socket-bypass dispatch and socket-backed case for notes/note commands, following the same pattern as settings; no issues
cmuxTests/CMUXOfflineAgentNotesCommandTests.swift Integration tests cover add/list without socket and flush with mock socket server; no issues in the test scaffolding
cmux.xcodeproj/project.pbxproj Adds production source and test file references to the Xcode project; no issues
.github/swift-file-length-budget.tsv Budget for CLI/cmux.swift updated from 34337 to 34354 to account for the 17-line addition to cmux.swift

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[cmux notes subcommand] --> B{needs socket?}
    B -- "add / list / clear / path" --> C[runOfflineNotesCommandWithoutSocket]
    B -- "flush" --> D[Connect to cmux socket]
    C --> E[OfflineAgentNotesStore]
    E --> F[(~/.cmuxterm/offline-agent-notes.json)]
    D --> G[runOfflineNotesFlush]
    G --> H{--dry-run?}
    H -- yes --> I[Print prompt to stdout]
    H -- no --> J[normalizeWindow/Workspace/Surface handles]
    J --> K[surface.send_text RPC]
    K --> L[store.markFlushed]
    L --> F
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[cmux notes subcommand] --> B{needs socket?}
    B -- "add / list / clear / path" --> C[runOfflineNotesCommandWithoutSocket]
    B -- "flush" --> D[Connect to cmux socket]
    C --> E[OfflineAgentNotesStore]
    E --> F[(~/.cmuxterm/offline-agent-notes.json)]
    D --> G[runOfflineNotesFlush]
    G --> H{--dry-run?}
    H -- yes --> I[Print prompt to stdout]
    H -- no --> J[normalizeWindow/Workspace/Surface handles]
    J --> K[surface.send_text RPC]
    K --> L[store.markFlushed]
    L --> F
Loading

Reviews (2): Last reviewed commit: "Add offline agent notes CLI" | Re-trigger Greptile

Comment on lines +42 to +63
func add(
text: String,
agent: String?,
cwd: String?,
workspaceId: String?,
surfaceId: String?,
now: Date = Date()
) throws -> OfflineAgentNoteRecord {
let note = OfflineAgentNoteRecord(
id: UUID().uuidString.lowercased(),
text: text,
agent: Self.nonEmpty(agent),
cwd: Self.nonEmpty(cwd),
workspaceId: Self.nonEmpty(workspaceId),
surfaceId: Self.nonEmpty(surfaceId),
createdAt: now.timeIntervalSince1970,
flushedAt: nil
)
var state = try load()
state.notes.append(note)
try save(state)
return note

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.

P1 TOCTOU race: concurrent invocations silently drop notes

add(), markFlushed(), and clear() all follow a load → modify → save pattern with no file lock. When two cmux notes add calls overlap (e.g., two agents queueing notes at the same time), both processes read the same on-disk state, each appends its note to its own in-memory copy, and the second .atomic write silently discards the first write's note. The caller gets an "OK" response with no indication of loss. For an offline notes tool that multiple agents may hit concurrently during connectivity gaps, this is a silent data-loss race.

The standard fix for a CLI that shares a JSON file across processes is to acquire an exclusive POSIX file lock (flock on the file descriptor) around every read-modify-write cycle in load()/save(), or to delegate storage to a channel that serializes writes (e.g., an append-only approach).

Comment on lines +211 to +218
let note = try store.add(
text: text,
agent: agent ?? environment["CMUX_AGENT_LAUNCH_KIND"],
cwd: cwd ?? environment["PWD"],
workspaceId: workspace ?? environment["CMUX_WORKSPACE_ID"],
surfaceId: surface ?? environment["CMUX_SURFACE_ID"]
)
let pendingCount = try store.notes().count

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 Redundant load() immediately after add(): store.notes().count triggers a second disk read just to count pending notes. The add() method already saves the updated state; returning the pending count from add() directly, or having runOfflineNotesAdd count from the returned state, avoids one extra round-trip to disk per invocation.

Suggested change
let note = try store.add(
text: text,
agent: agent ?? environment["CMUX_AGENT_LAUNCH_KIND"],
cwd: cwd ?? environment["PWD"],
workspaceId: workspace ?? environment["CMUX_WORKSPACE_ID"],
surfaceId: surface ?? environment["CMUX_SURFACE_ID"]
)
let pendingCount = try store.notes().count
let note = try store.add(
text: text,
agent: agent ?? environment["CMUX_AGENT_LAUNCH_KIND"],
cwd: cwd ?? environment["PWD"],
workspaceId: workspace ?? environment["CMUX_WORKSPACE_ID"],
surfaceId: surface ?? environment["CMUX_SURFACE_ID"]
)
// Derive count from the note just added rather than re-loading the file
let pendingCount = (try store.notes()).count

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!

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

🤖 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 `@CLI/CMUXCLI`+OfflineAgentNotes.swift:
- Around line 60-63: Add cross-process locking to prevent race conditions in the
read-modify-write operations across the methods `add`, `clear`, and
`markFlushed`. Each of these methods currently performs a load-modify-save
sequence without holding an exclusive lock, which allows concurrent invocations
to lose updates through last-writer-wins conflicts. Implement an exclusive lock
mechanism (such as file-based locking or a lock file) that is acquired before
calling `load()` and held until after `save()` completes in all three methods.
This ensures that only one process can be modifying the notes state at a time.
- Around line 66-79: The notes function in notes(includeFlushed:agent:) performs
multiple full passes over the collection with separate filter and sort
operations, which is inefficient in this hot path. Combine the two filter
operations into a single pass by merging the includeFlushed and agent filtering
logic together, and delay or conditionally apply the sort operation only when
the sorted result is actually needed. Additionally, review the call site in
runOfflineNotesAdd where notes().count is invoked and refactor it to use a
dedicated method that counts filtered notes without performing the expensive
sort operation, following the coding guidelines to avoid repeated full scans
over unbounded collections.
- Around line 119-122: The two setAttributes calls are using try? which silently
ignores any permission-setting failures, potentially leaving the directory and
file with weaker permissions than intended while reporting success to the
caller. Replace both try? with try on the setAttributes calls for directory.path
and storeURL.path to ensure permission-hardening errors are properly propagated
up the call stack rather than being silently swallowed.

In `@cmuxTests/CMUXOpenCommandTests.swift`:
- Line 309: The XCTAssertNotNil assertion in the test only validates that the
first note in allNotes has the flushed_at field populated, which can miss
partial flush-marking bugs where some notes lack this field. Update the
assertion to iterate through all items in the allNotes collection and verify
that each one has a non-nil flushed_at value (as a Double) rather than just
checking the first element with the allNotes.first accessor.
- Line 226: The current assertion at line 226 in CMUXOpenCommandTests.swift uses
optional casting with XCTAssertNil which passes when the key is missing, masking
payload-shape regressions. Instead of relying on the cast failure, explicitly
assert that the flushed_at key exists in the notes.first dictionary and its
value is NSNull (representing JSON null). This ensures the test validates the
actual payload structure rather than just a missing or null value.
🪄 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: dd89cea4-1477-40ed-afa0-796a3e16e2ee

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc31f2 and eab7475.

📒 Files selected for processing (4)
  • CLI/CMUXCLI+OfflineAgentNotes.swift
  • CLI/cmux.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CMUXOpenCommandTests.swift

Comment on lines +60 to +63
var state = try load()
state.notes.append(note)
try save(state)
return note

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add cross-process locking around read-modify-write operations.

add, clear, and markFlushed each do load() then save() without an exclusive lock, so parallel cmux notes invocations can drop updates via last-writer-wins.

💡 Suggested direction
+// Introduce an exclusive lock file (e.g. "<store>.lock") and wrap all
+// load-modify-save sequences in one critical section.
+// add(...)
+// clear(...)
+// markFlushed(...)

Also applies to: 84-96, 99-106, 108-123

🤖 Prompt for 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.

In `@CLI/CMUXCLI`+OfflineAgentNotes.swift around lines 60 - 63, Add cross-process
locking to prevent race conditions in the read-modify-write operations across
the methods `add`, `clear`, and `markFlushed`. Each of these methods currently
performs a load-modify-save sequence without holding an exclusive lock, which
allows concurrent invocations to lose updates through last-writer-wins
conflicts. Implement an exclusive lock mechanism (such as file-based locking or
a lock file) that is acquired before calling `load()` and held until after
`save()` completes in all three methods. This ensures that only one process can
be modifying the notes state at a time.

Comment on lines +66 to +79
func notes(includeFlushed: Bool = false, agent: String? = nil) throws -> [OfflineAgentNoteRecord] {
let normalizedAgent = Self.nonEmpty(agent)?.lowercased()
return try load().notes
.filter { includeFlushed || $0.flushedAt == nil }
.filter { note in
guard let normalizedAgent else { return true }
return note.agent?.lowercased() == normalizedAgent
}
.sorted { lhs, rhs in
if lhs.createdAt == rhs.createdAt {
return lhs.id < rhs.id
}
return lhs.createdAt < rhs.createdAt
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Collapse repeated collection passes in the offline notes hot path.

notes() currently performs multiple full passes (filter + filter + sorted), and runOfflineNotesAdd calls notes().count, forcing an unnecessary sort for a count-only operation.

As per coding guidelines, this path should avoid “repeated full scans” and “multiple filter/sorted passes” over unbounded user-owned notes.

Also applies to: 218-218

🤖 Prompt for 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.

In `@CLI/CMUXCLI`+OfflineAgentNotes.swift around lines 66 - 79, The notes function
in notes(includeFlushed:agent:) performs multiple full passes over the
collection with separate filter and sort operations, which is inefficient in
this hot path. Combine the two filter operations into a single pass by merging
the includeFlushed and agent filtering logic together, and delay or
conditionally apply the sort operation only when the sorted result is actually
needed. Additionally, review the call site in runOfflineNotesAdd where
notes().count is invoked and refactor it to use a dedicated method that counts
filtered notes without performing the expensive sort operation, following the
coding guidelines to avoid repeated full scans over unbounded collections.

Source: Coding guidelines

Comment on lines +119 to +122
try? fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)
let data = try encoder.encode(state)
try data.write(to: storeURL, options: .atomic)
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: storeURL.path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not ignore permission-hardening failures.

Swallowing errors from setAttributes can leave directory/file permissions weaker than intended while reporting success.

🔧 Proposed fix
-        try? fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)
+        try fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)
@@
-        try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: storeURL.path)
+        try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: storeURL.path)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try? fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)
let data = try encoder.encode(state)
try data.write(to: storeURL, options: .atomic)
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: storeURL.path)
try fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)
let data = try encoder.encode(state)
try data.write(to: storeURL, options: .atomic)
try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: storeURL.path)
🤖 Prompt for 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.

In `@CLI/CMUXCLI`+OfflineAgentNotes.swift around lines 119 - 122, The two
setAttributes calls are using try? which silently ignores any permission-setting
failures, potentially leaving the directory and file with weaker permissions
than intended while reporting success to the caller. Replace both try? with try
on the setAttributes calls for directory.path and storeURL.path to ensure
permission-hardening errors are properly propagated up the call stack rather
than being silently swallowed.

Comment thread cmuxTests/CMUXOpenCommandTests.swift Outdated
XCTAssertEqual(notes.count, 1)
XCTAssertEqual(notes.first?["agent"] as? String, "codex")
XCTAssertEqual(notes.first?["text"] as? String, "Check the release notes when online")
XCTAssertNil(notes.first?["flushed_at"] as? Double)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert explicit JSON null for flushed_at instead of relying on cast failure.

XCTAssertNil(notes.first?["flushed_at"] as? Double) also passes when the key is missing, so this can hide payload-shape regressions.

Suggested test hardening
-        XCTAssertNil(notes.first?["flushed_at"] as? Double)
+        let first = try XCTUnwrap(notes.first)
+        XCTAssertTrue(first.keys.contains("flushed_at"))
+        XCTAssertTrue(first["flushed_at"] is NSNull)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
XCTAssertNil(notes.first?["flushed_at"] as? Double)
let first = try XCTUnwrap(notes.first)
XCTAssertTrue(first.keys.contains("flushed_at"))
XCTAssertTrue(first["flushed_at"] is NSNull)
🤖 Prompt for 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.

In `@cmuxTests/CMUXOpenCommandTests.swift` at line 226, The current assertion at
line 226 in CMUXOpenCommandTests.swift uses optional casting with XCTAssertNil
which passes when the key is missing, masking payload-shape regressions. Instead
of relying on the cast failure, explicitly assert that the flushed_at key exists
in the notes.first dictionary and its value is NSNull (representing JSON null).
This ensures the test validates the actual payload structure rather than just a
missing or null value.

Comment thread cmuxTests/CMUXOpenCommandTests.swift Outdated
let allPayload = try XCTUnwrap(Self.jsonObject(from: all.stdout))
let allNotes = try XCTUnwrap(allPayload["notes"] as? [[String: Any]])
XCTAssertEqual(allNotes.count, 2)
XCTAssertNotNil(allNotes.first?["flushed_at"] as? Double)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify all flushed notes have flushed_at populated.

The assertion only checks the first item, so the test can miss partial flush-marking bugs.

Suggested assertion update
-        XCTAssertNotNil(allNotes.first?["flushed_at"] as? Double)
+        XCTAssertTrue(allNotes.allSatisfy { ($0["flushed_at"] as? Double) != nil }, "\(allNotes)")
🤖 Prompt for 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.

In `@cmuxTests/CMUXOpenCommandTests.swift` at line 309, The XCTAssertNotNil
assertion in the test only validates that the first note in allNotes has the
flushed_at field populated, which can miss partial flush-marking bugs where some
notes lack this field. Update the assertion to iterate through all items in the
allNotes collection and verify that each one has a non-nil flushed_at value (as
a Double) rather than just checking the first element with the allNotes.first
accessor.

@lawrencecchen
lawrencecchen force-pushed the feature-offline-agent-notes branch from eab7475 to 42ce1e5 Compare June 20, 2026 05:40

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

🤖 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 `@CLI/cmux.swift`:
- Line 34187: The notes command synopsis at line 34187 omits several available
flags (--cwd, --all, --dry-run) that are documented in detailed help and are
shown in other command synopses like tree, top, and memory. Update the notes
command synopsis to include all possible flags for consistency, or alternatively
simplify it to use [options] as a placeholder. Match the style used by other
commands in the help listing to ensure uniform documentation format.

In `@CLI/CMUXCLI`+OfflineAgentNotes.swift:
- Around line 335-336: The sequence of calling client.sendV2 followed by
store.markFlushed is not atomic and may cause duplicate deliveries if sendV2
succeeds but markFlushed fails. To fix this, wrap the markFlushed call in a
do-catch block to handle potential failures (such as disk full or permission
errors), emit a warning log when markFlushed fails while still treating the send
as partially successful, and consider pre-writing a "flushing" state to the
store before calling sendV2 so that failed flushes can be recovered on retry
without duplication.
🪄 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: eabe26b9-1fd7-4ac4-ba00-93ef9be4cbdc

📥 Commits

Reviewing files that changed from the base of the PR and between eab7475 and 42ce1e5.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (4)
  • CLI/CMUXCLI+OfflineAgentNotes.swift
  • CLI/cmux.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CMUXOfflineAgentNotesCommandTests.swift

Comment thread CLI/cmux.swift
tree [--all] [--workspace <id|ref|index>] [--window <id|ref|index>]
top [--all] [--workspace <id|ref|index>] [--window <id|ref|index>] [--processes] [--sort <cpu|mem|proc>] [--flat] [--format <tree|tsv>]
memory [--all] [--workspace <id|ref|index>] [--groups <count>]
notes <add|list|flush|clear|path> [--agent <name>] [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider listing all subcommand flags for consistency.

The one-line synopsis omits several flags that are available in the detailed help: --cwd (used by add), --all (used by list/clear/flush), and --dry-run (used by flush). Other commands in this help listing (tree, top, memory) show all their possible options. For consistency, consider:

-          notes <add|list|flush|clear|path> [--agent <name>] [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]
+          notes <add|list|flush|clear|path> [--agent <name>] [--cwd <path>] [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] [--all] [--dry-run]

Alternatively, simplify to notes <add|list|flush|clear|path> [options] if the full flag list is too verbose. Detailed help is available via cmux notes --help, so this is a minor UX improvement rather than a functional issue.

🤖 Prompt for 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.

In `@CLI/cmux.swift` at line 34187, The notes command synopsis at line 34187 omits
several available flags (--cwd, --all, --dry-run) that are documented in
detailed help and are shown in other command synopses like tree, top, and
memory. Update the notes command synopsis to include all possible flags for
consistency, or alternatively simplify it to use [options] as a placeholder.
Match the style used by other commands in the help listing to ensure uniform
documentation format.

Comment on lines +335 to +336
_ = try client.sendV2(method: "surface.send_text", params: params)
try store.markFlushed(ids: Set(notes.map(\.id)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Non-atomic send and mark sequence may cause duplicate delivery on partial failure.

If sendV2 succeeds but markFlushed throws (e.g., disk full, permission issue), notes are delivered to the terminal but remain marked as pending. A subsequent retry would re-send the same notes.

Consider catching markFlushed errors and emitting a warning while still reporting partial success, or pre-writing a "flushing" state before send:

💡 Suggested direction
             _ = try client.sendV2(method: "surface.send_text", params: params)
-            try store.markFlushed(ids: Set(notes.map(\.id)))
+            do {
+                try store.markFlushed(ids: Set(notes.map(\.id)))
+            } catch {
+                // Notes were delivered but local state update failed.
+                // Warn user but don't fail the command to avoid confusion.
+                fputs("warning: notes delivered but failed to update local state: \(String(describing: error))\n", stderr)
+            }
🤖 Prompt for 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.

In `@CLI/CMUXCLI`+OfflineAgentNotes.swift around lines 335 - 336, The sequence of
calling client.sendV2 followed by store.markFlushed is not atomic and may cause
duplicate deliveries if sendV2 succeeds but markFlushed fails. To fix this, wrap
the markFlushed call in a do-catch block to handle potential failures (such as
disk full or permission errors), emit a warning log when markFlushed fails while
still treating the send as partially successful, and consider pre-writing a
"flushing" state to the store before calling sendV2 so that failed flushes can
be recovered on retry without duplication.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Closing this because #6485 changed the desktop CLI/runtime, but the requested feature is iOS-only. I am moving the work to an iOS-scoped branch.

@lawrencecchen
lawrencecchen deleted the feature-offline-agent-notes branch June 20, 2026 06:01
@lawrencecchen
lawrencecchen restored the feature-offline-agent-notes branch July 18, 2026 10:19

This branch was successfully deployed

1 active deployment
Preview – cmux — 42ce1e52 Deployed Jun 20, 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