Skip to content

Add append-only cmux-tui session journal - #9434

Closed
lawrencecchen wants to merge 135 commits into
feat-terminal-multiviewfrom
feat-cmux-tui-session-journal
Closed

lawrencecchen wants to merge 135 commits into
feat-terminal-multiviewfrom
feat-cmux-tui-session-journal

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add one append-only session journal for resource mutations, terminal output, geometry, focus, resize and viewport observations, producer events, hook delivery, checkpoints, and sealed history.
  • Add CLI and typed SDK subscriptions with cursor resume, kind, class, subject, sensitivity, and compiled byte-regex filters over kind, subjects, payload, whole records, or terminal output.
  • Add a purpose-built 1.1 MiB release cmux-tui-hook helper and merge-preserving installers for Codex, Claude Code, Gemini CLI, Cursor Agent, Grok, Hermes Agent, OpenCode, Amp, and Pi.
  • Preserve each provider object under payload.native, add versioned common fields under payload.normalized, and index stable agent_tree, agent_node, and agent_parent subjects.
  • Keep the only public protocol at cmux.protocol/1.

Mechanism

  • SQLite WAL is the private durability and indexing boundary. Projection changes, journal records, subject indexes, and idempotency receipts commit atomically.
  • SQLite remains synchronous on one serialized writer actor with independent read-only workers. An async SQLite wrapper would add scheduler hops without changing SQLite's single-writer boundary.
  • Terminal ingress has its own 1,024-item bounded lane, 64 KiB chunks, adjacent coalescing to 256 KiB, and group commits. Durable producers use a separate 256-item lane. Both feed one ordered writer.
  • Terminal readers copy bytes after releasing the terminal lock. Provider hooks, normalization, SQLite writes, downstream hook execution, and subscribers never run on PTY, parser, render, or frontend input threads.
  • One read-only WAL tailer decodes live records once into a shared ring bounded by 8,192 records and 128 MiB. Exact subjects use an append-only (kind, id, sequence) index.
  • Bounded replay queues its final stream_end after admitted records. Overflow and cancellation retain destructive purge semantics.
  • Coding-agent helpers retry transient connection admission for one bounded 800 ms receipt window with the same idempotency key. This prevents both loss and duplicates above the server's 64-connection cap.
  • Durable hooks compile regex once, persist cursors and attempts, exclude causal loops by default, and execute absolute argv without a shell under bounded timeout and a fixed 4 to 32 worker pool.
  • Checkpoints contain content-addressed VT replay blobs. A pure reducer verifies restoration previews without mutating the live session.

Performance

Final-head release probes on cd7ce7af8487b5d7771938217ecc48e4c969d300:

  • 64 agents and 2,048 completion events: 2,131 durable appends/s, p50 19.2 ms, p95 65.1 ms, p99 67.7 ms, max 67.9 ms. All 2,048 downstream hook children completed with zero retries or abandonment.
  • Simultaneous terminal traffic: 32 MiB at 68.7 MiB/s with 37.8 microseconds maximum enqueue time. Terminal-only ingress sustained 101.6 MiB/s over 64 MiB.
  • Terminal journal writer: 18,089 records/s and 70.7 MiB/s for 64 MiB.
  • Compiled payload regex: 40.8 million records/s. Exact terminal-byte regex: 31.4 GiB/s over 2 GiB.
  • Indexed tail reads: 45.5 million reads/s and 520.8 times the linear near-tail path.

External helper process benchmark on journal commit 23bb4a9b5ac434f44988ebf0253739fbc175684a, whose helper code is unchanged by the base merge:

  • 9.0 ms median and 19.2 ms p99 durable receipt. Concurrency 64 sustained 576 processes/s. Concurrency 128 produced exactly 1,024 records from 1,024 helpers with no loss or duplication.

Verification

  • Rust 1.94 rustfmt and strict Clippy with warnings denied pass.
  • Full core suite: 985 passed, 5 manual performance probes ignored, 0 failed. All 5 release performance probes then passed explicitly.
  • Terminal exit ownership: 42 host unit tests plus 5 focused recovery integrations passed, including short-lived launch, final-byte ordering, late renderers, and stalled readers.
  • Rust SDK: 78 passed. Agent installer: 8 passed. Helper retry/parser: 4 passed. Agent topology and normalization: 13 passed.
  • Live final-head dogfood proved the exact --session ... --jsonl session current journal subscribe path, compiled payload filtering, durable helper receipt, native and normalized payloads, indexed agent tree subjects, and the single cmux.protocol/1 envelope.
  • Installed all nine adapters on the dogfood machine. Reinstallation is idempotent, unrelated hooks remain, Hermes retains cmux-irc, and the installed helper byte-matches the final debug helper.
  • All seven generated SDK drift and package checks passed earlier in this branch.

Stacked boundary

This remains stacked on #9387. The branch contains that PR's exact current head plus current main. PR 9387 owns durable terminal content identity and client-local placements; this PR consumes those identities for journal subjects and restoration.

After PR 9387 merges, this PR should retarget to main. The remaining product boundaries are frontend/window identity and presence, live attach versus cold replay, remote creation reconciliation, retention policy for terminal bytes, and crash expiry for frontend sizing leases.

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

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 Plus

Run ID: 14f68b59-0b45-4df1-b404-cc8f46b81368

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
📝 Walkthrough

Walkthrough

The change adds a durable SQLite-backed session journal, migrates resource events, routes resource commits through journal records, adds hooks and checkpoints, exposes journal operations through the server, SDKs, and CLI, and updates terminal ownership, projection, geometry, and protocol behavior.

Changes

Session journal

Layer / File(s) Summary
Journal persistence and resource integration
cmux-tui/crates/cmux-tui-core/src/workspace_registry/*, cmux-tui/crates/cmux-tui-core/src/journal_checkpoint.rs
Adds typed journal storage, schema migration, resource-event conversion, checkpoints, segments, hooks, and restore preview.
Journal runtime and server flow
cmux-tui/crates/cmux-tui-core/src/{journal_kernel.rs,journal_ingress.rs,journal_hooks.rs,mux.rs,server.rs}
Adds journal tailing, ingress, hook dispatch, filtering, cursor replay, live streaming, capability validation, and backpressured delivery.
Terminal runtime and projection
cmux-tui/crates/cmux-tui-core/src/{surface.rs,model.rs,mux/resource_content.rs,mux/resource_topology.rs,resource_api.rs,resource_router/*}
Adds catalog-owned shared terminal runtimes, multiview terminal tabs, detached terminal lifecycle, terminal projection, and terminal-scoped projections.
SDK bindings
cmux-tui/bindings/{cpp,go,java,python,rust,typescript,zig}/...
Adds journal models, filters, stream operations, strict record decoding, terminal projection methods, and multiview terminal snapshots.
CLI, frontend, and protocol contract
cmux-tui/crates/cmux-tui/src/{app.rs,cli/*,session/*}, cmux-tui/spec/*, cmux-tui/scripts/*
Adds frontend journaling, geometry ownership, journal commands, capability negotiation, catalog entries, specifications, and validation updates.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • manaflow-ai/cmux#9387: Adds the terminal multiview and catalog infrastructure extended here with session journaling.
  • manaflow-ai/cmux#8776: Adds the client-sizing and geometry paths extended here with explicit terminal geometry ownership.
  • manaflow-ai/cmux#8717: Adds durable WorkspaceRegistry and Mux infrastructure extended here with journal-backed events.

Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error Checkpoint listing at journal_extensions.rs:1797-1800 issues one DB lookup and JSON/hash decode per checkpoint; 1024-event ingress batches repeat topology expansion per terminal at :633. Load checkpoint rows with one paginated query and decode in one pass; batch topology expansion for all terminal IDs in one SQL query or add a benchmark-backed bound.
Cmux User-Facing Error Privacy ❌ Error Production errors expose a session value in CLI details and raw manifest parser details in API validation messages. Remove session values from error details and replace raw manifest parsing errors with generic sanitized messages.
Cmux Full Internationalization ❌ Error New English API errors in server.rs and CLI help/diagnostics in cli.rs/wire.rs are hard-coded; no locale source or web/messages entries changed. Route new API messages and CLI copy through a locale-aware source, then add complete translations for every supported locale.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (21 passed)
Check name Status Explanation
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 The PR adds only two Swift call-site lines and removes helper/test code; it adds no actor, Sendable, protocol, logger, or UI-store isolation declarations.
Cmux Swift Blocking Runtime ✅ Passed The PR diff contains 126 changed paths and no *.swift files, so it introduces no production Swift blocking or timing synchronization.
Cmux Browser Automation Off-Main ✅ Passed The cumulative PR diff changes no Swift browser socket files, worker policy, router, or policy tests; it adds no browser automation command or main-actor WebKit wait.
Cmux Expensive Synchronous Load ✅ Passed The diff changes no Swift-family files and no expensive-loader terms; all changes are under cmux-tui in Rust, SDK, and documentation files.
Cmux Cache Substitution Correctness ✅ Passed The PR changes only TypeScript production files; they add journal decoding/streaming, projection mutation, and snapshot fields, with no fresh-read-to-cache substitution in a persistence, history, u...
Cmux No Hacky Sleeps ✅ Passed No added fixed waits appear in TypeScript, JavaScript, or shell; the new wall-clock polling is confined to the smoke-test script, and existing release/package sleeps are unchanged.
Cmux Swift Concurrency ✅ Passed The pull request diff against origin/main contains no .swift files or Swift-related paths, so it introduces no cmux-owned Swift concurrency patterns.
Cmux Swift @Concurrent ✅ Passed The PR changes Rust files only. The reviewed commit has zero changed Swift paths and no Swift concurrency annotations to assess.
Cmux Swift Package Boundaries ✅ Passed The full PR diff contains no Swift or SwiftPM manifest changes; it changes Rust, SDK, docs, and tests only, so the Swift package-boundary rule is not applicable.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes no Package.swift, package-local Package.resolved, cmux.xcodeproj, Xcode lockfile, or .gitignore files; workflow edits are Rust Cargo-only.
Cmux Swift Logging ✅ Passed The complete PR diff contains 126 changed paths and no Swift or Package.swift files, so the Swift logging rule is not applicable.
Cmux Swiftui State Layout ✅ Passed The complete PR range from merge-base origin/main to HEAD contains no Swift files, so it introduces no SwiftUI state or layout changes covered by the rule.
Cmux Architecture Rethink ✅ Passed The PR diff from merge-base 1927f13 to HEAD contains no .swift files; the six changed paths are Rust, so the Swift architecture rule is not applicable.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR range contains no changed Swift files; it only changes cmux-tui paths. The auxiliary-window shortcut check is therefore not applicable.
Cmux Source Artifacts ✅ Passed The diff changes six Rust source/test files only. Hunks contain code and test refactors, with no logs, screenshots, caches, scratch directories, build output, or copied artifacts.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The PR diff contains no Swift files under any production Sources/ path, so this Swift production-source seam check is not applicable.
Cmux No Ambient Global State ✅ Passed The PR diff against origin/main contains no Swift paths; all six files changed in the final commit are Rust, so the production Swift ambient-global-state rule is not applicable.
Title check ✅ Passed The title clearly identifies the primary change: adding an append-only cmux-tui session journal.
Description check ✅ Passed The description thoroughly covers the journal design, scope, testing, performance results, and remaining boundaries, although several template sections are omitted.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-cmux-tui-session-journal

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.

@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: 4f738f2e80

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

Comment on lines +1137 to +1138
WHERE resource_revision > ?1
ORDER BY resource_revision ASC",

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 Bound replay queries over the append-only journal

When a client resumes a long-lived session from a far-behind cursor, this query now reads every subsequent journal row and collects them into one Vec; open_session_event_stream then materializes all of those batches in initial_items before returning the open response. Since this change also removes the previous 4,096-row/16 MiB retention boundary, a stale cursor can make memory use and response latency grow without limit and can eventually stall or OOM the daemon. Page the replay or fall back to a snapshot once a bounded replay threshold is exceeded.

Useful? React with 👍 / 👎.

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

🤖 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 `@cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs`:
- Around line 1532-1543: Update the durable commit path around
append_resource_journal_record to bind resource_revision and
previous_resource_revision together using the same tuple pattern derived from
project_resource, eliminating the resource_revision.expect call. Preserve the
existing values for both Some and None cases while ensuring an unexpected
absence cannot panic inside the SQLite transaction.

In `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/effect_store.rs`:
- Around line 1092-1103: The commit path should reuse the already-built
serde_json::Value instead of parsing outcome_json again. Update
commit_resource_effect_patch_in_transaction and both callers to accept and pass
the outcome Value alongside outcome_json, then provide that Value directly to
append_resource_journal_record; remove the from_str re-parsing and its
associated failure path.

In `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs`:
- Around line 139-186: Update create_session_journal_schema and the
session_journal_reject_delete trigger to define a retention plan for unbounded
journal growth: add a recorded sealed watermark and allow deletes only for rows
below that watermark via the trigger’s condition, while continuing to reject all
other deletes. Ensure the schema preserves append-only behavior for unsealed
entries and supports future sealed-segment pruning without dropping the trigger.
- Around line 399-406: Update append_journal_record so committed_at_ms always
comes from a single unix_epoch_ms() clock read, while occurred_at_ms remains the
caller-supplied append.occurred_at_ms value, including zero. Remove the
conditional sentinel logic and preserve the existing validation and record
insertion flow.

In `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs`:
- Around line 635-658: Extend the freshly written journal-record test after
validating journal subjects to call registry.resource_events_after(0). Assert it
returns exactly one batch with revision 1 and the expected committed resource
deltas, covering decoding through append_resource_journal_record rather than
only inspecting the raw session_journal row.

In `@cmux-tui/spec/session-journal.md`:
- Around line 121-125: Update the sensitivity section in session-journal.md to
define the ordering of public, metadata, sensitive, and secret, explicitly state
whether max_sensitivity: "sensitive" includes lower levels, and specify the
journal.read.sensitive permission behavior for each level. Ensure filtering
never exposes secret records without authorization and does not omit records
permitted by the rule.
- Around line 197-226: Expand the hook manifest specification around the
versioned manifest and runner behavior to define who may install or update
manifests, how executable paths in exec.argv are authorized through trust or
allowlist rules, and how execution is sandboxed, including environment and
filesystem restrictions. Require these authorization and isolation checks before
the parser or dispatcher accepts and runs a manifest.
🪄 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 Plus

Run ID: 447eab59-b725-4d59-8de8-ca5468c2c165

📥 Commits

Reviewing files that changed from the base of the PR and between 84f5755 and 4f738f2.

📒 Files selected for processing (12)
  • cmux-tui/crates/cmux-tui-core/src/lib.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/effect_store.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/resource_store.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/terminal_exit_store.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs
  • cmux-tui/spec/README.md
  • cmux-tui/spec/commands.md
  • cmux-tui/spec/programmability.md
  • cmux-tui/spec/resource-api-v1.md
  • cmux-tui/spec/session-journal.md

Comment on lines +1532 to +1543
append_resource_journal_record(
&tx,
resource_revision.expect("resource revision for projected workspace mutation"),
previous_resource_revision,
&mutation.origin,
&mutation.id,
event_kind,
None,
result,
&resource_deltas,
)?;
prune_resource_events(&tx)?;
resource_store::prune_resource_mutations(&tx)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Remove the expect from the durable commit path.

resource_revision is guaranteed to be Some only because it is derived from the same project_resource flag as previous_resource_revision. The compiler does not enforce that link, so a later change to this function can turn line 1534 into a panic inside a SQLite transaction. Bind resource_revision in the same tuple pattern instead.

♻️ Proposed change
         if let (
             Some(previous_topology),
             Some(previous_resource_revision),
             Some(sqlite_resource_revision),
-        ) = (previous_topology.as_ref(), previous_resource_revision, sqlite_resource_revision)
+            Some(resource_revision),
+        ) = (
+            previous_topology.as_ref(),
+            previous_resource_revision,
+            sqlite_resource_revision,
+            resource_revision,
+        )
         {
@@
             append_resource_journal_record(
                 &tx,
-                resource_revision.expect("resource revision for projected workspace mutation"),
+                resource_revision,
                 previous_resource_revision,
📝 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
append_resource_journal_record(
&tx,
resource_revision.expect("resource revision for projected workspace mutation"),
previous_resource_revision,
&mutation.origin,
&mutation.id,
event_kind,
None,
result,
&resource_deltas,
)?;
prune_resource_events(&tx)?;
resource_store::prune_resource_mutations(&tx)?;
append_resource_journal_record(
&tx,
resource_revision,
previous_resource_revision,
&mutation.origin,
&mutation.id,
event_kind,
None,
result,
&resource_deltas,
)?;
resource_store::prune_resource_mutations(&tx)?;
🤖 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 `@cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs` around lines 1532 -
1543, Update the durable commit path around append_resource_journal_record to
bind resource_revision and previous_resource_revision together using the same
tuple pattern derived from project_resource, eliminating the
resource_revision.expect call. Preserve the existing values for both Some and
None cases while ensuring an unexpected absence cannot panic inside the SQLite
transaction.

Comment on lines 1092 to 1103
let outcome = serde_json::from_str::<Value>(outcome_json)?;
append_resource_journal_record(
transaction,
revision,
previous_revision,
"resource-api",
idempotency_key,
operation,
Some(patch),
&outcome,
deltas,
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Pass the outcome as a Value instead of re-parsing outcome_json.

Both callers build the outcome as a serde_json::Value and then canonicalize it (lines 936-937 and 983-984). Line 1092 parses that canonical string back into a Value inside the transaction. The round trip repeats work on the commit path and adds a parse failure branch that cannot be triggered by valid input.

Pass the already-built Value into commit_resource_effect_patch_in_transaction next to outcome_json, as commit_resource_effect already does at line 818.

🤖 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 `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/effect_store.rs` around
lines 1092 - 1103, The commit path should reuse the already-built
serde_json::Value instead of parsing outcome_json again. Update
commit_resource_effect_patch_in_transaction and both callers to accept and pass
the outcome Value alongside outcome_json, then provide that Value directly to
append_resource_journal_record; remove the from_str re-parsing and its
associated failure path.

Comment on lines +139 to +186
pub(super) fn create_session_journal_schema(transaction: &Transaction<'_>) -> anyhow::Result<()> {
transaction.execute_batch(
"CREATE TABLE IF NOT EXISTS session_journal (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT UNIQUE NOT NULL,
schema_version INTEGER NOT NULL CHECK(schema_version > 0),
kind TEXT NOT NULL,
class TEXT NOT NULL CHECK(class IN ('state', 'observation', 'effect', 'checkpoint')),
replay_policy TEXT NOT NULL CHECK(replay_policy IN ('required', 'advisory', 'never')),
occurred_at_ms INTEGER NOT NULL CHECK(occurred_at_ms >= 0),
committed_at_ms INTEGER NOT NULL CHECK(committed_at_ms >= 0),
producer_json TEXT NOT NULL CHECK(json_valid(producer_json)),
authority_json TEXT CHECK(authority_json IS NULL OR json_valid(authority_json)),
causation_id TEXT,
correlation_id TEXT,
causation_depth INTEGER NOT NULL CHECK(causation_depth >= 0),
subjects_json TEXT NOT NULL CHECK(json_valid(subjects_json)),
sensitivity TEXT NOT NULL CHECK(sensitivity IN ('public', 'metadata', 'sensitive', 'secret')),
payload_json TEXT NOT NULL CHECK(json_valid(payload_json)),
resource_revision INTEGER UNIQUE,
previous_resource_revision INTEGER,
CHECK(
(resource_revision IS NULL AND previous_resource_revision IS NULL)
OR (
resource_revision IS NOT NULL
AND previous_resource_revision IS NOT NULL
AND resource_revision = previous_resource_revision + 1
)
)
);
CREATE INDEX IF NOT EXISTS session_journal_by_kind_sequence
ON session_journal(kind, sequence);
CREATE INDEX IF NOT EXISTS session_journal_by_correlation_sequence
ON session_journal(correlation_id, sequence)
WHERE correlation_id IS NOT NULL;
CREATE TRIGGER IF NOT EXISTS session_journal_reject_update
BEFORE UPDATE ON session_journal
BEGIN
SELECT RAISE(ABORT, 'session journal is append-only');
END;
CREATE TRIGGER IF NOT EXISTS session_journal_reject_delete
BEFORE DELETE ON session_journal
BEGIN
SELECT RAISE(ABORT, 'session journal is append-only');
END;",
)?;
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Confirm the retention plan for an append-only, unbounded session_journal.

This PR removes the resource_events row-count and byte-budget pruning and stores one journal row per resource revision forever. The session_journal_reject_delete trigger blocks every DELETE, so no future pruning path can trim rows while the trigger exists. A long-lived session database now grows without bound, and payload_json holds the full result and changes for every mutation.

Two consequences to plan for:

  • Storage growth is unbounded for long sessions.
  • Any later retention or sealed-segment work must first drop or bypass this trigger, which weakens the append-only guarantee unless the trigger allows an explicit retention path.

Consider a WHEN clause on the delete trigger that permits deletion only below a recorded sealed watermark, so retention does not require dropping the invariant.

🤖 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 `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs`
around lines 139 - 186, Update create_session_journal_schema and the
session_journal_reject_delete trigger to define a retention plan for unbounded
journal growth: add a recorded sealed watermark and allow deletes only for rows
below that watermark via the trigger’s condition, while continuing to reject all
other deletes. Ensure the schema preserves append-only behavior for unsealed
entries and supports future sealed-segment pruning without dropping the trigger.

Comment on lines +399 to +406
fn append_journal_record(
transaction: &Transaction<'_>,
append: &JournalAppend<'_>,
) -> anyhow::Result<u64> {
validate_identifier("journal event id", append.event_id)?;
validate_identifier("journal event kind", append.kind)?;
let committed_at_ms =
if append.occurred_at_ms == 0 { unix_epoch_ms()? } else { append.occurred_at_ms };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Read the commit clock for committed_at_ms instead of copying occurred_at_ms.

committed_at_ms is set to append.occurred_at_ms whenever that value is non-zero. The column then never records the actual insert time, and occurred_at_ms == 0 acts as a sentinel that is indistinguishable from a genuine epoch timestamp. Migrated legacy records store occurred_at_ms = 0 and a committed_at_ms of "now", while live records store two identical values.

Read the clock once for committed_at_ms and keep occurred_at_ms as the caller-supplied value.

♻️ Proposed change
-    let committed_at_ms =
-        if append.occurred_at_ms == 0 { unix_epoch_ms()? } else { append.occurred_at_ms };
+    let committed_at_ms = unix_epoch_ms()?;
📝 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
fn append_journal_record(
transaction: &Transaction<'_>,
append: &JournalAppend<'_>,
) -> anyhow::Result<u64> {
validate_identifier("journal event id", append.event_id)?;
validate_identifier("journal event kind", append.kind)?;
let committed_at_ms =
if append.occurred_at_ms == 0 { unix_epoch_ms()? } else { append.occurred_at_ms };
fn append_journal_record(
transaction: &Transaction<'_>,
append: &JournalAppend<'_>,
) -> anyhow::Result<u64> {
validate_identifier("journal event id", append.event_id)?;
validate_identifier("journal event kind", append.kind)?;
let committed_at_ms = unix_epoch_ms()?;
🤖 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 `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs`
around lines 399 - 406, Update append_journal_record so committed_at_ms always
comes from a single unix_epoch_ms() clock read, while occurred_at_ms remains the
caller-supplied append.occurred_at_ms value, including zero. Remove the
conditional sentinel logic and preserve the existing validation and record
insertion flow.

Comment on lines 635 to +658
assert_eq!(
registry
.connection
.query_row("SELECT COUNT(*) FROM resource_events", [], |row| row.get::<_, i64>(0))
.query_row("SELECT COUNT(*) FROM session_journal", [], |row| row.get::<_, i64>(0))
.unwrap(),
1
);
let journal = registry.session_journal_after(0, 1).unwrap();
assert_eq!(journal.records[0].kind, "workspace.create");
for (kind, id) in [
("workspace", workspace(1, "one", "One").public_id.to_string()),
("screen", screen_id(1).to_string()),
("pane", pane_id(1).to_string()),
("tab", tab_id(1).to_string()),
("terminal", terminal_resource(TERMINAL_ONE).to_string()),
] {
assert!(
journal.records[0]
.subjects
.iter()
.any(|subject| subject.kind == kind && subject.id == id),
"missing {kind} journal subject {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.

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add an assertion for resource_events_after on a freshly written journal record.

This test now verifies the journal row directly. No test in this file verifies that resource_events_after still decodes a record written through append_resource_journal_record. That read path extracts changes with json_extract(payload_json, '$.changes'), so a payload shape change would break the public resource-event contract without failing any test here. The migration test in session_journal.rs only covers a migrated row.

Add an assertion that registry.resource_events_after(0) returns one batch with revision == 1 and the committed deltas.

🤖 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 `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs` around lines
635 - 658, Extend the freshly written journal-record test after validating
journal subjects to call registry.resource_events_after(0). Assert it returns
exactly one batch with revision 1 and the expected committed resource deltas,
covering decoding through append_resource_journal_record rather than only
inspecting the raw session_journal row.

Comment thread cmux-tui/spec/session-journal.md
Comment thread cmux-tui/spec/session-journal.md Outdated
@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

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

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

Comment on lines +847 to 857
append_resource_journal_record(
&tx,
revision,
previous_revision,
"resource-api",
idempotency_key,
operation,
None,
&outcome_value,
deltas,
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Journal effect receipts without resource deltas

When commit_success_without_changes handles terminal input, browser pointer actions, or similar ephemeral effects—or when commit_known_failure records any failure—it passes deltas: None, so this append is skipped even though the durable effect receipt is committed. Consequently session.journal.subscribe never reports these accepted effect outcomes, preventing hook/audit consumers from observing them; append a journal record with nullable resource revisions and publish the journal wake even when no projection delta exists.

Useful? React with 👍 / 👎.

Comment on lines +388 to +391
collect_subjects(result, &mut subjects);
collect_subjects(changes, &mut subjects);
expand_topology_subjects(transaction, &mut subjects)?;
let subjects = subjects.into_iter().collect::<Vec<_>>();

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 Keep generated subject lists within the wire limit

When a topology mutation covers a sufficiently large session, this unbounded collection can include every tab, terminal/browser, pane, screen, and workspace; a single pane with roughly 128 tabs already exceeds the SessionJournalRecord.subjects contract of 256 entries. The server still emits that record, but the Zig binding explicitly rejects more than 256 subjects in bindings/zig/src/resource.zig:3895, terminating otherwise valid journal consumption, so the stored envelope and published API limit need to agree.

Useful? React with 👍 / 👎.

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

🤖 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 `@cmux-tui/bindings/cpp/src/resource.cpp`:
- Around line 1071-1076: Replace the sequential JournalSensitivity checks in the
max_sensitivity mapping with an exhaustive switch, assigning the existing
encoded values for public_ and metadata and explicitly marking secret as
unreachable because it is rejected earlier. Ensure any future enum value
produces a compile-time diagnostic rather than defaulting to sensitive, matching
the sibling classes mapping.

In `@cmux-tui/bindings/go/operations.go`:
- Around line 360-375: Update cmux-tui/bindings/go/operations.go lines 360-375
in Session.Journal to call options.validate() before constructing request
parameters and return any error immediately. Add SessionJournalOptions.validate
in cmux-tui/bindings/go/options.go lines 75-97 to reject simultaneous Cursor and
Start, MaxSensitivity set to JournalSensitivitySecret, and subjects with both
Kind and ID unset, using the binding’s existing client-side argument error type.

In `@cmux-tui/bindings/java/src/com/cmux/Session.java`:
- Around line 481-495: Update the journal decoding logic around
Client.requireExactFields to reject missing or null values for all required
journal fields, including authority and payload, rather than checking only key
presence. Ensure string fields decoded through Wire.string—such as principal_id,
id, kind, and related producer, authority, or subject values—also reject empty
strings using the binding-wide non-empty string contract, matching Go validation
behavior.

In `@cmux-tui/bindings/python/cmux/resources.py`:
- Around line 1711-1787: Update the Session.journal decoding flow around
_journal_record and ResourceStream.next to validate the envelope cursor before
returning the record. Compare record.sequence with the next expected stream
cursor revision, and raise ProtocolError when it does not match; preserve normal
decoding when the sequence is correct.

In `@cmux-tui/bindings/typescript/src/options.ts`:
- Around line 71-78: Update SessionJournalOptions.classes to use the exported
JournalClass type from models.ts instead of duplicating the literal union,
adding the type-only import from ./models.js; leave maxSensitivity as its
explicit subset excluding "secret".

In `@cmux-tui/bindings/typescript/src/resources.ts`:
- Around line 1320-1353: Update the Object.freeze literal returned by the
session journal decoder to include a satisfies SessionJournalRecord assertion,
matching the existing sessionEvent literals at lines 1249, 1265, and 1273. Keep
the current fields and runtime decoding unchanged while enabling compile-time
validation of the returned object shape.
- Around line 1292-1319: Rename the inner authority record binding in the
authority IIFE so it does not shadow the outer authority result, and rename the
subject-loop value binding so it does not shadow the sessionJournalRecord value
parameter; update all references within their respective blocks while preserving
behavior.
- Around line 1026-1041: Update journalOptionsFields to reject options that
provide both cursor and start, and reject any subject filter lacking both kind
and id, before serializing or dispatching the request. Use the existing
TypeScript error-construction conventions and preserve current serialization for
valid SessionJournalOptions.

In `@cmux-tui/bindings/zig/src/resource.zig`:
- Around line 887-906: Update the public SessionJournalRecord.subjects field to
use a const slice of JournalSubject, matching the immutable decoded collections
elsewhere in the file; keep decodeSessionJournalRecord behavior unchanged.
- Around line 3786-3794: Remove the duplicate journalNullableDecimal helper and
update both of its callers to use the existing requiredNullableDecimalU64
function, preserving the missing-field, null, and decimalU64 conversion
behavior.
- Around line 19300-19326: Extend the wire-format test around request_object and
its filter to assert the encoded kinds, classes, and subjects arrays supplied by
the test setup. Retrieve each array using the corresponding filter keys and
validate both their lengths and element values/shapes, covering the encoder
branches in encodeSessionJournalOptions without changing existing start or
max_sensitivity assertions.

In `@cmux-tui/crates/cmux-tui-core/src/server.rs`:
- Around line 6758-6802: Add tests for both `run_session_journal_stream`
termination branches that emit reason `gap`: force a journal page read error and
create an empty page where `last_sequence < head_sequence` by sealing or
truncating retention below the subscriber cursor. Assert the stream sends a
terminal message with reason `gap` and the expected journal cursor, while
preserving existing replay, resume, transport-rejection, and foreign-cursor
coverage.
- Around line 6638-6653: Update both error handlers in the session journal
subscription flow around session_journal_reader and session_journal_page: log
the full underlying error internally, but replace error.to_string() in
ResourceError::operation_failed details with a stable, safe reason code or
minimal sanitized diagnostic. Keep the user-facing operation.failed response
free of raw SQLite, anyhow, SQL, and journal path details.
- Around line 6803-6835: Move the resource stream overflow refresh out of the
per-record loop in the replay handler, placing a single update after all records
in the page have been processed using the final stream.last_sequence cursor.
Keep cancellation and item delivery checks running for every record, including
records rejected by stream.filter.matches, and preserve the existing
early-return behavior.

In `@cmux-tui/crates/cmux-tui/src/cli/command.rs`:
- Around line 2551-2562: Add the mirrored invalid argument cases to the
rejection table in the journal subscribe parsing test: include --sequence
without --cursor-session, non-canonical or out-of-range sequence values such as
"01" and "18446744073709551616", and a subject kind containing uppercase
characters. Keep each case routed through parse and assert that it returns an
error.

In `@cmux-tui/crates/cmux-tui/src/cli/wire.rs`:
- Around line 187-193: Extract the duplicated lifecycle-envelope condition and
write/error mapping from both call sites in the wire command flow into a helper
such as write_lifecycle_envelope. Have it accept the exposure flag, output mode,
and Value, return Ok when no write is needed, and return Err(3) after emitting
the existing stdout error on failure; replace both inline blocks with calls to
this helper.

In `@cmux-tui/spec/cli.md`:
- Line 153: Update the `session journal subscribe` grammar entry to replace
`[FILTERS]` with the concrete `--kinds`, `--classes`, `--subjects`, and
`--max-sensitivity` flags, and document cursor resumption using
`--cursor-session` and `--sequence` rather than `--from`. Match the explicit
flag formatting used by the surrounding CLI entries.

In `@cmux-tui/spec/resource-api-v1.md`:
- Around line 318-323: Update the documentation for session.journal.subscribe in
both cmux-tui/spec/resource-api-v1.md and the operations catalog to explicitly
reject filter.max_sensitivity="secret", while preserving the existing rule that
cursor and start are mutually exclusive.

In `@cmux-tui/spec/resource-operations-v1.json`:
- Around line 526-532: Add a constraint to the session.journal.subscribe
operation documenting that subscribers never receive records whose sensitivity
is secret, even when no filter is provided. Reuse the existing secret-exclusion
wording or constraint structure from JournalFilter and attach it directly to the
subscribe operation without changing the sensitivity schema.
- Around line 8231-8236: Add the trusted-local access constraint to the
session.journal.subscribe operation’s constraints in the specification, using
the same wording as the existing session.shutdown constraint and preserving the
current cursor/start constraints.

In `@cmux-tui/spec/session-journal.md`:
- Around line 127-175: Update cmux-tui/spec/session-journal.md lines 127-175 to
state that cursor and start are mutually exclusive and each subjects filter
entry must include kind, id, or both. Update
cmux-tui/bindings/python/cmux/resources.py lines 243-271 in _journal_options to
reject simultaneous cursor/start, max_sensitivity set to secret, and subject
entries missing both kind and id, matching Session::journal validation in
handles.rs.
- Around line 127-175: Update the Subscription API contract to state that
`start` and `cursor` are mutually exclusive, and that every `subjects` filter
entry must specify at least a `kind` or an `id`. Ensure these validation rules
match the client-side checks in `Session::journal` and are reflected by binding
option validation such as `_journal_options`.
🪄 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 Plus

Run ID: e46a9ccc-7f82-43e6-ad26-ee54411a175d

📥 Commits

Reviewing files that changed from the base of the PR and between 4f738f2 and b96c2b6.

📒 Files selected for processing (58)
  • cmux-tui/bindings/ERGONOMICS.md
  • cmux-tui/bindings/conformance/runner.py
  • cmux-tui/bindings/cpp/.cmux-resource-api.json
  • cmux-tui/bindings/cpp/include/cmux/resource.hpp
  • cmux-tui/bindings/cpp/src/resource.cpp
  • cmux-tui/bindings/cpp/src/resource_models.cpp
  • cmux-tui/bindings/go/.cmux-resource-api.json
  • cmux-tui/bindings/go/internal/wirev1/operations.go
  • cmux-tui/bindings/go/operations.go
  • cmux-tui/bindings/go/options.go
  • cmux-tui/bindings/go/values.go
  • cmux-tui/bindings/java/.cmux-resource-api.json
  • cmux-tui/bindings/java/src/com/cmux/Options.java
  • cmux-tui/bindings/java/src/com/cmux/Session.java
  • cmux-tui/bindings/java/src/com/cmux/SessionJournalRecord.java
  • cmux-tui/bindings/java/src/com/cmux/internal/Operations.java
  • cmux-tui/bindings/python/.cmux-resource-api.json
  • cmux-tui/bindings/python/cmux/_operations.py
  • cmux-tui/bindings/python/cmux/models.py
  • cmux-tui/bindings/python/cmux/options.py
  • cmux-tui/bindings/python/cmux/resources.py
  • cmux-tui/bindings/rust/.cmux-resource-api.json
  • cmux-tui/bindings/rust/src/resource/client.rs
  • cmux-tui/bindings/rust/src/resource/handles.rs
  • cmux-tui/bindings/rust/src/resource/mod.rs
  • cmux-tui/bindings/rust/src/resource/ops.rs
  • cmux-tui/bindings/rust/src/resource/options.rs
  • cmux-tui/bindings/rust/src/resource/typed_stream.rs
  • cmux-tui/bindings/rust/tests/catalog_manifest.rs
  • cmux-tui/bindings/rust/tests/operation_reachability.rs
  • cmux-tui/bindings/typescript/.cmux-resource-api.json
  • cmux-tui/bindings/typescript/src/internal/operations.ts
  • cmux-tui/bindings/typescript/src/models.ts
  • cmux-tui/bindings/typescript/src/options.ts
  • cmux-tui/bindings/typescript/src/resources.ts
  • cmux-tui/bindings/zig/.cmux-resource-api.json
  • cmux-tui/bindings/zig/src/resource.zig
  • cmux-tui/crates/cmux-tui-core/src/mux.rs
  • cmux-tui/crates/cmux-tui-core/src/resource.rs
  • cmux-tui/crates/cmux-tui-core/src/resource_router.rs
  • cmux-tui/crates/cmux-tui-core/src/server.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs
  • cmux-tui/crates/cmux-tui/src/cli.rs
  • cmux-tui/crates/cmux-tui/src/cli/command.rs
  • cmux-tui/crates/cmux-tui/src/cli/wire.rs
  • cmux-tui/scripts/check-resource-api-boundary.py
  • cmux-tui/scripts/test_check_resource_api_boundary.py
  • cmux-tui/spec/README.md
  • cmux-tui/spec/bindings.md
  • cmux-tui/spec/cli.md
  • cmux-tui/spec/inventory.json
  • cmux-tui/spec/programmability.md
  • cmux-tui/spec/resource-api-v1.json
  • cmux-tui/spec/resource-api-v1.md
  • cmux-tui/spec/resource-operations-v1.json
  • cmux-tui/spec/resource-operations-v1.md
  • cmux-tui/spec/session-journal.md

Comment on lines +1071 to +1076
if (filter.max_sensitivity) {
const char* value = "sensitive";
if (*filter.max_sensitivity == JournalSensitivity::public_) value = "public";
if (*filter.max_sensitivity == JournalSensitivity::metadata) value = "metadata";
encoded_filter.emplace("max_sensitivity", Json(value));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use an exhaustive switch for max_sensitivity, matching the sibling classes mapping.

The classes filter mapping in the same function uses a switch with one case per JournalClass value, so a new enum value forces a compile-time diagnostic. The max_sensitivity mapping instead uses sequential if checks that default to "sensitive" when neither public_ nor metadata matches. If JournalSensitivity gains a new non-secret value later, it silently encodes as "sensitive" instead of failing to compile or erroring out.

Convert the if-cascade to a switch over JournalSensitivity (with an explicit case JournalSensitivity::secret: marked unreachable, since it is rejected earlier in the function) for consistency and future-proofing.

♻️ Proposed refactor
     if (filter.max_sensitivity) {
-        const char* value = "sensitive";
-        if (*filter.max_sensitivity == JournalSensitivity::public_) value = "public";
-        if (*filter.max_sensitivity == JournalSensitivity::metadata) value = "metadata";
-        encoded_filter.emplace("max_sensitivity", Json(value));
+        const char* value = "";
+        switch (*filter.max_sensitivity) {
+            case JournalSensitivity::public_: value = "public"; break;
+            case JournalSensitivity::metadata: value = "metadata"; break;
+            case JournalSensitivity::sensitive: value = "sensitive"; break;
+            case JournalSensitivity::secret:
+                return make_error(
+                    ErrorCode::invalid_argument,
+                    "secret journal records are unavailable in v1");
+        }
+        encoded_filter.emplace("max_sensitivity", Json(value));
     }
📝 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
if (filter.max_sensitivity) {
const char* value = "sensitive";
if (*filter.max_sensitivity == JournalSensitivity::public_) value = "public";
if (*filter.max_sensitivity == JournalSensitivity::metadata) value = "metadata";
encoded_filter.emplace("max_sensitivity", Json(value));
}
if (filter.max_sensitivity) {
const char* value = "";
switch (*filter.max_sensitivity) {
case JournalSensitivity::public_: value = "public"; break;
case JournalSensitivity::metadata: value = "metadata"; break;
case JournalSensitivity::sensitive: value = "sensitive"; break;
case JournalSensitivity::secret:
return make_error(
ErrorCode::invalid_argument,
"secret journal records are unavailable in v1");
}
encoded_filter.emplace("max_sensitivity", Json(value));
}
🤖 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 `@cmux-tui/bindings/cpp/src/resource.cpp` around lines 1071 - 1076, Replace the
sequential JournalSensitivity checks in the max_sensitivity mapping with an
exhaustive switch, assigning the existing encoded values for public_ and
metadata and explicitly marking secret as unreachable because it is rejected
earlier. Ensure any future enum value produces a compile-time diagnostic rather
than defaulting to sensitive, matching the sibling classes mapping.

Comment on lines +360 to +375
func (s *Session) Journal(ctx context.Context, options SessionJournalOptions) (*Stream[SessionJournalRecord], error) {
input := s.route.params()
if options.Cursor != nil {
input[wirev1.FieldCursor] = options.Cursor
}
if options.Start != nil {
input[wirev1.FieldStart] = *options.Start
}
if options.Filter != nil {
input["filter"] = options.Filter
}
merge(input, options.Extra)
return openStream(
ctx, s.client, wirev1.SessionJournalSubscribe, input, decodeSessionJournalRecord,
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The Go binding omits the journal option validation that the C++ and Java bindings enforce. The C++ binding validates three rules in SessionJournalOptions::to_params (cmux-tui/bindings/cpp/src/resource.cpp Lines 1018-1081), and the Java binding validates the same rules in Options.JournalFilter, Options.SessionJournal, and Session.journal. The Go binding validates none of them, so invalid option combinations reach the server and fail there instead of at the call site.

  • cmux-tui/bindings/go/operations.go#L360-L375: call options.validate() at the top of Session.Journal and return the error before building params.
  • cmux-tui/bindings/go/options.go#L75-L97: add a validate() method on SessionJournalOptions that rejects Cursor and Start set together, rejects Filter.MaxSensitivity == JournalSensitivitySecret, and rejects any JournalSubjectFilter with both Kind and ID nil.
🛡️ Proposed validation method for options.go
func (o SessionJournalOptions) validate() error {
	if o.Cursor != nil && o.Start != nil {
		return &InvalidArgumentError{
			Message: "journal cursor and start are mutually exclusive",
		}
	}
	if o.Filter == nil {
		return nil
	}
	if o.Filter.MaxSensitivity != nil &&
		*o.Filter.MaxSensitivity == JournalSensitivitySecret {
		return &InvalidArgumentError{
			Message: "secret journal records are unavailable in v1",
		}
	}
	for _, subject := range o.Filter.Subjects {
		if subject.Kind == nil && subject.ID == nil {
			return &InvalidArgumentError{
				Message: "journal subject filters require kind or id",
			}
		}
	}
	return nil
}

Replace InvalidArgumentError with the error type this binding already uses for client-side argument rejection.

📍 Affects 2 files
  • cmux-tui/bindings/go/operations.go#L360-L375 (this comment)
  • cmux-tui/bindings/go/options.go#L75-L97
🤖 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 `@cmux-tui/bindings/go/operations.go` around lines 360 - 375, Update
cmux-tui/bindings/go/operations.go lines 360-375 in Session.Journal to call
options.validate() before constructing request parameters and return any error
immediately. Add SessionJournalOptions.validate in
cmux-tui/bindings/go/options.go lines 75-97 to reject simultaneous Cursor and
Start, MaxSensitivity set to JournalSensitivitySecret, and subjects with both
Kind and ID unset, using the binding’s existing client-side argument error type.

Comment on lines +481 to +495
Client.requireExactFields(
fields,
"session journal record",
"sequence", "event_id", "schema_version", Wire.KIND, "class", "replay",
"occurred_at_ms", "committed_at_ms", "producer", "authority",
"causation_id", "correlation_id", "causation_depth", "subjects",
"sensitivity", "payload", "resource_revision", "previous_resource_revision"
);
for (String required : List.of("authority", "payload")) {
if (!fields.containsKey(required)) {
throw new ProtocolError(
"session journal record omitted required field " + required
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect requireExactFields, integer, and requiredNullableString in the Java Client.
set -euo pipefail

fd -t f 'Client.java' cmux-tui/bindings/java --exec ast-grep outline {} --items all

fd -t f 'Client.java' cmux-tui/bindings/java --exec rg -n -A 20 'static .*requireExactFields|static .*requiredNullableString|static int integer' {}

Repository: manaflow-ai/cmux

Length of output: 11088


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Session.java relevant sections =="
sed -n '440,535p' cmux-tui/bindings/java/src/com/cmux/Session.java

echo "== Client.java exact/string helpers =="
sed -n '3530,3705p' cmux-tui/bindings/java/src/com/cmux/Client.java

echo "== Go journal string validation =="
rg -n -B3 -A12 'invalid journal (producer|authority|subject)|producer: string|authority: string|subject' cmux-tui/bindings/go/operations.go cmux-tui/bindings/java/src/com/cmux/Session.java

Repository: manaflow-ai/cmux

Length of output: 23363


Align Java journal decoding with Go’s required-field and non-empty validation.

requireExactFields only rejects unknown keys, so fields.get("authority") and JsonVale of("payload") can be null and accepted instead of “session journal record omitted required field …”. Use required present/non-null checks for both, like the Go decoder does for the 18 journal fields.

Wire.string(raw.get("principal_id"), "journal principal_id"), "id", "kind", etc. accepts empty strings, while Go returns invalid journal producer/authority/subject when those strings are empty. Keep the Java helper non-empty or use the same binding-wide non-empty string contract.

🤖 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 `@cmux-tui/bindings/java/src/com/cmux/Session.java` around lines 481 - 495,
Update the journal decoding logic around Client.requireExactFields to reject
missing or null values for all required journal fields, including authority and
payload, rather than checking only key presence. Ensure string fields decoded
through Wire.string—such as principal_id, id, kind, and related producer,
authority, or subject values—also reject empty strings using the binding-wide
non-empty string contract, matching Go validation behavior.

Comment on lines +1711 to +1787
def _journal_record(value: Any) -> SessionJournalRecord:
payload = _mapping(value, "session journal record")
_strict_object(
payload,
(
"sequence", "event_id", "schema_version", "kind", "class", "replay",
"occurred_at_ms", "committed_at_ms", "producer", "authority",
"causation_id", "correlation_id", "causation_depth", "subjects",
"sensitivity", "payload", "resource_revision",
"previous_resource_revision",
),
"session journal record",
)
for required in ("authority", "payload"):
if required not in payload:
raise ProtocolError(
f"session journal record omitted required field {required}"
)
producer = _mapping(payload.get("producer"), "journal producer")
_strict_object(producer, ("kind", "id"), "journal producer")
authority_value = payload.get("authority")
authority: Optional[JournalAuthority] = None
if authority_value is not None:
authority_payload = _mapping(authority_value, "journal authority")
_strict_object(
authority_payload,
("principal_id", "lease_id", "generation", "role"),
"journal authority",
)
authority = JournalAuthority(
_required_string(authority_payload, "principal_id"),
_required_string(authority_payload, "lease_id"),
_required_string(authority_payload, "generation"),
_required_string(authority_payload, "role"),
)
subject_values = payload.get("subjects")
if not isinstance(subject_values, list):
raise ProtocolError("journal subjects must be an array")
subjects = []
for subject_value in subject_values:
subject = _mapping(subject_value, "journal subject")
_strict_object(subject, ("kind", "id"), "journal subject")
subjects.append(
JournalSubject(
_required_string(subject, "kind"),
_required_string(subject, "id"),
)
)
return SessionJournalRecord(
_required_decimal(payload, "sequence"),
_required_string(payload, "event_id"),
_required_positive_uint32(payload, "schema_version"),
_required_string(payload, "kind"),
_required_enum(payload, "class", ("state", "observation", "effect", "checkpoint")), # type: ignore[arg-type]
_required_enum(payload, "replay", ("required", "advisory", "never")), # type: ignore[arg-type]
_required_decimal(payload, "occurred_at_ms"),
_required_decimal(payload, "committed_at_ms"),
JournalProducer(
_required_string(producer, "kind"),
_required_string(producer, "id"),
),
authority,
_required_nullable_string(payload, "causation_id"),
_required_nullable_string(payload, "correlation_id"),
_required_uint16(payload, "causation_depth"),
tuple(subjects),
_required_enum(
payload,
"sensitivity",
("public", "metadata", "sensitive", "secret"),
), # type: ignore[arg-type]
payload["payload"],
_required_nullable_decimal(payload, "resource_revision"),
_required_nullable_decimal(payload, "previous_resource_revision"),
)


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the shared stream-opening implementation for envelope cursor validation.
ast-grep outline cmux-tui/bindings/python/cmux --items all --match '_open_stream'
rg -n -B3 -A40 'def _open_stream' cmux-tui/bindings/python/cmux

Repository: manaflow-ai/cmux

Length of output: 3942


🏁 Script executed:

#!/bin/bash
# Inspect the relevant stream implementation and session journal subscription path.
set -euo pipefail

echo "== files mentioning open_stream / ResourceStream =="
rg -n -B2 -A20 'class .*Stream|def open_stream|ResourceStream|open_stream\(' cmux-tui/bindings/python/cmux

echo
echo "== journal subscription path =="
rg -n -B5 -A25 'def journal|session journal|_journal_record' cmux-tui/bindings/python/cmux/resources.py

echo
echo "== source around stream implementation =="
sed -n '1,220p' cmux-tui/bindings/python/cmux/connection.py
echo
sed -n '2760,2835p' cmux-tui/bindings/python/cmux/resources.py

Repository: manaflow-ai/cmux

Length of output: 49931


🏁 Script executed:

#!/bin/bash
# Inspect stream decoding lifecycle and all sequence/cursor handling.
set -euo pipefail

echo "== ResourceStream next implementation =="
sed -n '1154,1186p' cmux-tui/bindings/python/cmux/_protocol.py

echo
echo "== sequence / revision / cursor references =="
rg -n 'sequence|cursor|revision|envelope' cmux-tui/bindings/python cmux-tui/bindings/python/cpp 2>/dev/null || true

echo
echo "== envelope_cursor references in repository =="
rg -n 'envelope_cursor|journal sequence does not match' . || true

Repository: manaflow-ai/cmux

Length of output: 50373


🏁 Script executed:

#!/bin/bash
# Inspect stream push consumption and cursor validation without a broad repository scan.
set -euo pipefail

echo "== _StreamState push/consumed implementation =="
sed -n '137,220p' cmux-tui/bindings/python/cmux/_protocol.py

echo
echo "== _decode_stream_item_envelope implementation =="
sed -n '1444,1491p' cmux-tui/bindings/python/cmux/_protocol.py

echo
echo "== dispatch stream enqueue =="
sed -n '905,937p' cmux-tui/bindings/python/cmux/_protocol.py

echo
echo "== focused tests for stream cursor/sequence validation =="
sed -n '2717,2945p' cmux-tui/bindings/python/tests/test_resource_api.py

Repository: manaflow-ai/cmux

Length of output: 16027


🏁 Script executed:

#!/bin/bash
# Locate and inspect only the missing _StreamState push implementation evidence.
set -euo pipefail

rg -n "def push|def consumed" cmux-tui/bindings/python/cmux/_protocol.py cmux-tui/bindings/python/cmux/aio.py cmux-tui/bindings/python/cmux/raw/client.py || true

sed -n '220,290p' cmux-tui/bindings/python/cmux/_protocol.py

Repository: manaflow-ai/cmux

Length of output: 2874


🏁 Script executed:

#!/bin/bash
# Inspect StreamState push/failure flow for stream items.
set -euo pipefail
sed -n '269,330p' cmux-tui/bindings/python/cmux/_protocol.py

Repository: manaflow-ai/cmux

Length of output: 2293


Add envelope-cursor/sequence validation for Session.journal.

Session.journal decodes only the payload, while ResourceStream.next returns StreamItem.item without comparing record.sequence to the stream cursor revision. Match the C++ decoder by rejecting a journal record when record.sequence is not the next cursor-revision value.

🧰 Tools
🪛 Ruff (0.16.0)

[warning] 1711-1711: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 1726-1728: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 1748-1748: Avoid specifying long messages outside the exception class

(TRY003)

🤖 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 `@cmux-tui/bindings/python/cmux/resources.py` around lines 1711 - 1787, Update
the Session.journal decoding flow around _journal_record and ResourceStream.next
to validate the envelope cursor before returning the record. Compare
record.sequence with the next expected stream cursor revision, and raise
ProtocolError when it does not match; preserve normal decoding when the sequence
is correct.

Comment on lines +71 to +78
export interface SessionJournalOptions extends RequestOptions {
readonly cursor?: Cursor;
readonly start?: "tail" | "beginning";
readonly kinds?: readonly string[];
readonly classes?: readonly ("state" | "observation" | "effect" | "checkpoint")[];
readonly subjects?: readonly JournalSubjectFilter[];
readonly maxSensitivity?: "public" | "metadata" | "sensitive";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the exported JournalClass union for classes.

models.ts exports JournalClass with the same four members. Duplicating the literal union here lets the two drift when a journal class is added. Keep maxSensitivity as an explicit subset, because it must exclude "secret".

♻️ Proposed refactor
 export interface SessionJournalOptions extends RequestOptions {
   readonly cursor?: Cursor;
   readonly start?: "tail" | "beginning";
   readonly kinds?: readonly string[];
-  readonly classes?: readonly ("state" | "observation" | "effect" | "checkpoint")[];
+  readonly classes?: readonly JournalClass[];
   readonly subjects?: readonly JournalSubjectFilter[];
   readonly maxSensitivity?: "public" | "metadata" | "sensitive";
 }

Add the import at the top of the file:

import type { JournalClass } from "./models.js";
📝 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
export interface SessionJournalOptions extends RequestOptions {
readonly cursor?: Cursor;
readonly start?: "tail" | "beginning";
readonly kinds?: readonly string[];
readonly classes?: readonly ("state" | "observation" | "effect" | "checkpoint")[];
readonly subjects?: readonly JournalSubjectFilter[];
readonly maxSensitivity?: "public" | "metadata" | "sensitive";
}
import type { JournalClass } from "./models.js";
export interface SessionJournalOptions extends RequestOptions {
readonly cursor?: Cursor;
readonly start?: "tail" | "beginning";
readonly kinds?: readonly string[];
readonly classes?: readonly JournalClass[];
readonly subjects?: readonly JournalSubjectFilter[];
readonly maxSensitivity?: "public" | "metadata" | "sensitive";
}
🤖 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 `@cmux-tui/bindings/typescript/src/options.ts` around lines 71 - 78, Update
SessionJournalOptions.classes to use the exported JournalClass type from
models.ts instead of duplicating the literal union, adding the type-only import
from ./models.js; leave maxSensitivity as its explicit subset excluding
"secret".

Comment thread cmux-tui/spec/cli.md Outdated

session list
session <selector> open|show|snapshot|events|ping|shutdown
session <selector> journal subscribe [--from tail|beginning] [FILTERS]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spell out the journal subscribe flags instead of [FILTERS].

[FILTERS] is a placeholder with no definition in the shown grammar. Readers cannot tell from this line that the actual flags are --kinds, --classes, --subjects, and --max-sensitivity, or that cursor resumption uses --cursor-session/--sequence instead of --from. List the concrete flags, matching the style used for other entries in this table (for example, line 199's --pointer-frame-seq <decimal>).

📝 Proposed doc clarification
-session <selector> journal subscribe [--from tail|beginning] [FILTERS]
+session <selector> journal subscribe [--from tail|beginning | --cursor-session <id> --sequence <n>]
+    [--kinds <glob,...>] [--classes state|observation|effect|checkpoint,...]
+    [--subjects <kind:id>,...] [--max-sensitivity public|metadata|sensitive|secret]
📝 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
session <selector> journal subscribe [--from tail|beginning] [FILTERS]
session <selector> journal subscribe [--from tail|beginning | --cursor-session <id> --sequence <n>]
[--kinds <glob,...>] [--classes state|observation|effect|checkpoint,...]
[--subjects <kind:id>,...] [--max-sensitivity public|metadata|sensitive|secret]
🤖 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 `@cmux-tui/spec/cli.md` at line 153, Update the `session journal subscribe`
grammar entry to replace `[FILTERS]` with the concrete `--kinds`, `--classes`,
`--subjects`, and `--max-sensitivity` flags, and document cursor resumption
using `--cursor-session` and `--sequence` rather than `--from`. Match the
explicit flag formatting used by the surrounding CLI entries.

Comment thread cmux-tui/spec/resource-api-v1.md Outdated
Comment on lines +318 to +323
`session.journal.subscribe` is the trusted-local append-only feed. Omitting a
cursor tails from the captured head; `start:"beginning"` replays retained
history first. Its cursor generation is the immutable session ID and its
revision is the journal sequence. Filters are server-side and never alter
cursor order. A slow subscriber receives `stream_end` with reason `gap` and
the last recoverable cursor, then reconnects from that cursor.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the operations catalog encodes the journal parameter constraints.
set -euo pipefail

fd -t f 'resource-operations-v1.json' cmux-tui/spec --exec jq '.operations["session.journal.subscribe"] // "operation absent"' {}

fd -t f 'resource-operations-v1.md' cmux-tui/spec --exec rg -n -C4 'journal' {}

Repository: manaflow-ai/cmux

Length of output: 3480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate spec files =="
fd -t f 'JournalFilter|resource-operations-v1.json|resource-api-v1.md|resource-operations-v1.md' cmux-tui/spec | sort

echo "== resource-operations-v1.json JournalFilter/schema refs =="
python3 - <<'PY'
import json, pathlib
p=pathlib.Path('cmux-tui/spec/resource-operations-v1.json')
data=json.loads(p.read_text())
refs = data.get('schemas', {})
for name in ['SessionJournalRecord','JournalFilter','Cursor','StreamOpened']:
    if name in refs:
        print(f'-- schema {name} --')
        print(json.dumps(refs[name], indent=2)[:2000])
print("-- references to sensitivity or secret --")
for key,val in data.get('schemas', {}).items():
    text=json.dumps(val)
    if 'sensitivity' in text or 'secret' in text:
        print(key)
PY

echo "== resource-operations-v1.md JournalFilter content =="
rg -n -C5 'JournalFilter|sensitivity|secret|cursor and start|validation.invalid|validation' cmux-tui/spec/resource-operations-v1.md || true

echo "== current api paragraph =="
sed -n '300,340p' cmux-tui/spec/resource-api-v1.md

Repository: manaflow-ai/cmux

Length of output: 3055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate spec files =="
fd -t f 'JournalFilter|resource-operations-v1.json|resource-api-v1.md|resource-operations-v1.md' cmux-tui/spec | sort

echo "== resource-operations-v1.json JournalFilter/schema refs =="
python3 - <<'PY'
import json, pathlib
p=pathlib.Path('cmux-tui/spec/resource-operations-v1.json')
data=json.loads(p.read_text())
refs = data.get('schemas', {})
for name in ['SessionJournalRecord','JournalFilter','Cursor','StreamOpened']:
    if name in refs:
        print(f'-- schema {name} --')
        print(json.dumps(refs[name], indent=2)[:2000])
print("-- references to sensitivity or secret --")
for key,val in data.get('schemas', {}).items():
    text=json.dumps(val, sort_keys=True)
    if 'sensitivity' in text or 'secret' in text:
        print(key)
        print(text[:2000])
PY

echo "== resource-operations-v1.md JournalFilter content =="
rg -n -C5 'JournalFilter|sensitivity|secret|cursor and start|validation.invalid|validation' cmux-tui/spec/resource-operations-v1.md || true

echo "== current api paragraph =="
sed -n '300,340p' cmux-tui/spec/resource-api-v1.md

Repository: manaflow-ai/cmux

Length of output: 3055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resource operations markdown relevant sections =="
argoffile cmux-tui/spec/resource-operations-v1.md -- -
rg -n -C6 'JournalFilter|sensitivity|cursor and start|validation.invalid|operation.failed|session.journal.subscribe|constraints' cmux-tui/spec/resource-operations-v1.md || true

echo "== resource operations json relevant sections =="
python3 - <<'PY'
import json
from pathlib import Path
data=json.loads(Path('cmux-tui/spec/resource-operations-v1.json').read_text())
for path in [
    data.get('operations',{}).get('session.journal.subscribe'),
    data.get('schemas',{}).get('JournalFilter'),
    data.get('schemas',{}).get('JournalFilterItem'),
    data.get('schemas',{}).get('JournalFilterRecord'),
]:
    print("---")
    print(json.dumps(path, indent=2)[:4000])
PY

echo "== server validation around journal subscribe =="
rg -n -C8 'session\.journal\.subscribe|journal.subscribe|cursor|start|validation\.invalid|max_sensitivity|secret|sensitivity' cmux-tui/crates cmux-tui/bindings -g '*.rs' -g '*.cpp' -g '*.h' || true

echo "== Java Journal options =="
fd -t f '.*' cmux-tui/bindings/java -e java | xargs -r rg -n -C6 'Journal|JournalFilter|max_sensitivity|sensitivity|secret|cursor|start' || true

Repository: manaflow-ai/cmux

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resource operations markdown relevant sections =="
rg -n -C6 'JournalFilter|sensitivity|cursor and start|validation.invalid|operation.failed|session.journal.subscribe|constraints' cmux-tui/spec/resource-operations-v1.md || true

echo "== resource operations json relevant sections =="
python3 - <<'PY'
import json
from pathlib import Path
data=json.loads(Path('cmux-tui/spec/resource-operations-v1.json').read_text())
for path in [
    data.get('operations',{}).get('session.journal.subscribe'),
    data.get('schemas',{}).get('JournalFilter'),
    data.get('schemas',{}).get('JournalFilterItem'),
    data.get('schemas',{}).get('JournalFilterRecord'),
]:
    print("---")
    print(json.dumps(path, indent=2)[:8000])
PY

echo "== server validation around journal subscribe =="
rg -n -C8 'session\.journal\.subscribe|journal.subscribe|cursor|start|validation\.invalid|max_sensitivity|secret|sensitivity' cmux-tui/crates cmux-tui/bindings -g '*.rs' -g '*.cpp' -g '*.h' -g '*.java' || true

echo "== Java Journal options files =="
fd -H -t f '.*' cmux-tui/bindings/java -e java 2>/dev/null | xargs -r rg -n -C6 'Journal|JournalFilter|max_sensitivity|sensitivity|secret|cursor|start' || true

Repository: manaflow-ai/cmux

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file size candidates =="
wc -l cmux-tui/spec/resource-operations-v1.md cmux-tui/spec/resource-api-v1.md cmux-tui/crates/cmux-tui-core/src/resource_router.rs cmux-tui/bindings/cpp/src/resource.cpp || true

echo "== resource operations json operations block =="
sed -n '80,150p' cmux-tui/spec/resource-operations-v1.json

echo "== resource operations markdown session journal area =="
sed -n '920,975p' cmux-tui/spec/resource-operations-v1.md

echo "== resource operations markdown any journal filter text =="
sed -n '945,970p' cmux-tui/spec/resource-operations-v1.md
rg -n -C10 'JournalFilter|sensitivity|secret|max_sensitivity' cmux-tui/bindings/java -g '*.java' || true

echo "== server journal subscribe validation exact locations =="
sed -n '590,620p' cmux-tui/crates/cmux-tui-core/src/resource_router.rs
rg -n -C3 'session\.journal\.subscribe|JOURNAL_SUBSCRIBE|validation\.invalid|cursor and start|cursor and start|secret|sensitivity|JournalFilter' cmux-tui/crates/cmux-tui-core/src/resource_router.rs

echo "== c++ exact journal area =="
sed -n '1000,1090p' cmux-tui/bindings/cpp/src/resource.cpp

Repository: manaflow-ai/cmux

Length of output: 29693


Document the rejected session.journal.subscribe parameter combinations.

The operations catalog specifies that cursor and start are mutually exclusive, but it does not encode the filter.max_sensitivity prohibition. Add the secret-filter constraint to cmux-tui/spec/resource-api-v1.md and the operations catalog.

🤖 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 `@cmux-tui/spec/resource-api-v1.md` around lines 318 - 323, Update the
documentation for session.journal.subscribe in both
cmux-tui/spec/resource-api-v1.md and the operations catalog to explicitly reject
filter.max_sensitivity="secret", while preserving the existing rule that cursor
and start are mutually exclusive.

Comment on lines +526 to +532
"sensitivity": {
"required": true,
"type": {
"kind": "ref",
"name": "JournalSensitivity"
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

State the secret-exclusion guarantee on the record or the stream, not only on the filter.

sensitivity refs JournalSensitivity, which includes secret (line 223). The rule that subscribers never receive secret records appears only as a JournalFilter constraint at line 400. A reader of SessionJournalRecord or of session.journal.subscribe alone cannot see that guarantee, and a subscription that omits filter does not reference JournalFilter at all.

Add the same guarantee as a constraint on session.journal.subscribe, so the delivery rule is stated on the operation that delivers the records.

🛡️ Proposed constraint addition on the operation
         "constraints": [
           "cursor and start are mutually exclusive.",
           "Omitting both starts at the current head and delivers only later records.",
+          "V1 trusted-local subscriptions never deliver secret records, with or without filter.",
           "For this operation Cursor.generation is the immutable session ID and Cursor.revision is the journal sequence."
         ]
🤖 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 `@cmux-tui/spec/resource-operations-v1.json` around lines 526 - 532, Add a
constraint to the session.journal.subscribe operation documenting that
subscribers never receive records whose sensitivity is secret, even when no
filter is provided. Reuse the existing secret-exclusion wording or constraint
structure from JournalFilter and attach it directly to the subscribe operation
without changing the sensitivity schema.

Comment thread cmux-tui/spec/resource-operations-v1.json
Comment thread cmux-tui/spec/session-journal.md
@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@socket-security

socket-security Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​wait-timeout@​0.2.110010093100100
Addedcargo/​jsonschema@​0.30.0100100100100100

View full report

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmux-tui/spec/session-journal.md (1)

168-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add operation.unsupported to the catalog, or correct this prose.

This paragraph describes a resident process returning operation.unsupported for pre-negotiation clients, but resource-operations-v1.json's errors object does not define this code, and session.journal.subscribe's own errors list does not include it. A typed client generated from the catalog has no details schema and no declared code path for this error.

Add operation.unsupported to the catalog's errors object and to the operations that can raise it, or clarify that this behavior is not yet part of the versioned protocol contract.

#!/bin/bash
# Description: Confirm whether operation.unsupported exists anywhere in the catalog or server code.
set -euo pipefail

rg -n 'operation\.unsupported' cmux-tui/spec cmux-tui/crates cmux-tui/bindings 2>/dev/null || echo "no matches found"
🤖 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 `@cmux-tui/spec/session-journal.md` around lines 168 - 183, Add
operation.unsupported to the versioned protocol catalog’s errors object and
include it in the declared errors for session.journal.subscribe and any other
affected operations, defining its details schema so typed clients can handle it.
Ensure the catalog matches the documented pre-negotiation behavior; otherwise
revise the journal subscription prose to remove that undocumented guarantee.
cmux-tui/crates/cmux-tui/src/cli/command.rs (1)

3672-3674: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Verify the catalog counts against the ten new journal operations.

This test asserts 106 CLI cases and 113 catalog operations. The exclusion list at lines 3742-3751 names 7 operations, so expected holds 113 - 7 = 106 operations and line 3755 requires seen to equal that exact set. The cases vector adds only session.journal.subscribe, yet this PR adds ten further journal operations (session.journal.append, the producer, hook, checkpoint, restore, and segment operations) to cmux-tui/bindings/cpp/.cmux-resource-api.json and routes all of them in parse_session. If cmux-tui/spec/resource-operations-v1.json also declares those ten operations, then 113 is stale and both the count assertion and the set comparison at line 3755 fail.

Confirm the catalog operation total, and add a cases entry for every new journal operation or extend the exclusion list deliberately.

#!/bin/bash
# Description: Count catalog operations and list the journal operations the CLI test must cover.
set -euo pipefail

fd -t f 'resource-operations-v1.json' cmux-tui/spec --exec sh -c '
  echo "total operations:"; jq ".operations | length" {};
  echo "journal operations:"; jq -r ".operations | keys[] | select(startswith(\"session.journal\"))" {}'

echo "== journal operations referenced in the CLI test cases =="
rg -n 'session\.journal' cmux-tui/crates/cmux-tui/src/cli/command.rs
🤖 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 `@cmux-tui/crates/cmux-tui/src/cli/command.rs` around lines 3672 - 3674, Verify
the operation total from operation_catalog against the current resource
specification, then update the CLI test near the cases vector and its count
assertions to include every newly declared session.journal operation, including
append, producer, hook, checkpoint, restore, and segment operations. Keep the
exclusion list intentional and ensure the expected/seen set comparison remains
consistent with the complete catalog.
♻️ Duplicate comments (4)
cmux-tui/spec/session-journal.md (2)

146-162: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

State the sensitivity ordering and the default when max_sensitivity is omitted.

This section lists public, metadata, and sensitive as accepted max_sensitivity values, and separately says Unix clients may read through sensitive while remote clients are capped at metadata. It never states the ordering explicitly (whether sensitive includes public and metadata records), and it never states the default behavior when max_sensitivity is omitted from the filter.

By contrast, JournalHookFilter in the operations catalog explicitly states "max_sensitivity defaults to metadata." Add the same explicit ordering and default statement here for JournalFilter, since ambiguous filtering can expose sensitive/secret payloads or silently drop permitted records.

🤖 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 `@cmux-tui/spec/session-journal.md` around lines 146 - 162, Update the
JournalFilter sensitivity description to explicitly define the ordering, stating
that higher thresholds include all lower-sensitivity records, and state that
omitted max_sensitivity defaults to metadata. Keep the existing client-specific
caps and secret-record restriction unchanged.

139-152: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document that cursor and start are mutually exclusive and that a subject filter requires kind or id.

This section never states that cursor and start cannot both be set on session.journal.subscribe, even though the operations catalog enforces this as a hard constraint and the Rust binding (Session::journal in handles.rs) rejects it client-side. It also never states that each subjects filter entry needs at least kind or id, even though JournalSubjectFilter.constraints in the catalog requires it.

State both rules explicitly here so every binding implements the same input validation.

🤖 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 `@cmux-tui/spec/session-journal.md` around lines 139 - 152, Update the session
journal subscription documentation to explicitly state that cursor and start are
mutually exclusive. Also document that every subjects filter entry must specify
at least a kind or an id, matching the validation enforced by Session::journal
and JournalSubjectFilter.constraints.
cmux-tui/spec/cli.md (1)

153-164: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Spell out the remaining journal subscribe flags instead of [FILTERS].

Line 153 still uses the placeholder [FILTERS]. Readers cannot tell that the real flags are --kinds, --classes, --subjects, and --max-sensitivity, or that cursor resumption uses --cursor-session/--sequence instead of --from. The worked examples in session-journal.md (lines 169-175) show these concrete flags, but this grammar line does not.

List the concrete flags here, matching the explicit style used for other entries in this table.

📝 Proposed doc clarification
-session <selector> journal subscribe [--from tail|beginning] [FILTERS]
-  [--regex <pattern>] [--regex-field kind|subjects|payload|record] [--ignore-case]
+session <selector> journal subscribe [--from tail|beginning | --cursor-session <id> --sequence <n>]
+    [--kinds <glob,...>] [--classes state|observation|effect|checkpoint,...]
+    [--subjects <kind:id>,...] [--max-sensitivity public|metadata|sensitive]
+    [--regex <pattern>] [--regex-field kind|subjects|payload|record] [--ignore-case]
🤖 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 `@cmux-tui/spec/cli.md` around lines 153 - 164, Update the `journal subscribe`
grammar entry to replace `[FILTERS]` with the concrete `--kinds`, `--classes`,
`--subjects`, and `--max-sensitivity` options, and document cursor resumption
with `--cursor-session` and `--sequence` instead of `--from`. Match the explicit
flag style used by the surrounding command entries.
cmux-tui/spec/resource-operations-v1.json (1)

8864-8942: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restate the secret-exclusion guarantee on session.journal.subscribe itself.

filter is optional on this operation. The secret-exclusion guarantee currently lives only in JournalFilter.constraints (line 905), so a subscription that omits filter never references that guarantee at all. State the guarantee directly on the operation's own constraints array, since it describes what the stream will ever deliver, independent of whether a filter is supplied.

🛡️ Proposed constraint addition
         "constraints": [
           "cursor and start are mutually exclusive.",
           "Omitting both starts at the current head and delivers only later records.",
+          "This stream never delivers secret records, with or without filter; remote transports are capped at metadata.",
           "For this operation Cursor.generation is the immutable session ID and Cursor.revision is the journal sequence."
         ]
🤖 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 `@cmux-tui/spec/resource-operations-v1.json` around lines 8864 - 8942, Update
the constraints array for session.journal.subscribe to state directly that the
stream never delivers secret entries, regardless of whether the optional filter
is supplied. Keep the existing cursor/start constraints unchanged and do not
rely solely on JournalFilter.constraints for this guarantee.
🤖 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 `@cmux-tui/bindings/go/options.go`:
- Line 88: Replace the bare string type of Field with a named JournalRegexField
type, define constants for the supported values kind, subjects, payload, and
record, and update the surrounding options declarations consistently with the
existing JournalClass and JournalSensitivity patterns. Ensure callers can use
the constants and invalid plural values such as “kinds” are not represented as
valid field constants.

In `@cmux-tui/bindings/java/src/com/cmux/Options.java`:
- Around line 97-109: Add a two-argument JournalRegexFilter constructor taking
pattern and field that delegates to the existing three-argument constructor with
caseSensitive defaulting to true. Preserve the current null-field handling and
validation in the compact JournalRegexFilter constructor, matching the overload
pattern used by JournalFilter, ClientSizing, and LayoutUndo.

In `@cmux-tui/bindings/zig/src/resource.zig`:
- Around line 5937-5955: Extend the existing journal filter tests around
encodeSessionJournalOptions to supply filter.regex and assert the encoded regex
object contains pattern, the default field, and default case_sensitive values.
Add boundary coverage verifying empty and over-1024-byte patterns return
error.InvalidJournalFilter, while a 1-1024-byte pattern encodes successfully.

In `@cmux-tui/crates/cmux-tui-core/src/journal_checkpoint.rs`:
- Around line 251-269: The upsert branch in apply_resource_change currently
sorts the target collection after every change. Remove that per-change sort,
track collections modified during the record, and sort each touched collection
once after all changes for the record have been applied, preserving the existing
index-then-id ordering used by the values.sort_by comparator.
- Around line 207-210: Update the snapshot cursor handling around the resource
revision assignment to validate that the existing “cursor” value is an object or
null before setting “revision”; if it is any other JSON type, return an error
instead of using panicking Value index assignment. Preserve creating an empty
cursor object when the entry is absent, and propagate the error through
restore_preview.

In `@cmux-tui/crates/cmux-tui-core/src/journal_hooks.rs`:
- Around line 448-453: Capture the child process group ID before waiting, then
use that stored ID for termination. In the Ok(Some(status)) success path of the
wait_timeout flow, terminate the process group before the child is reaped rather
than calling terminate_hook_process_group afterward; update terminate_hook_child
similarly wherever it runs after child.wait().
- Around line 185-228: Add an advancement guard in the scan loop after computing
scanned_to in the journal hook processing flow: if scanned_to is less than or
equal to hook.cursor_sequence, stop processing and return without scheduling
further deliveries. Keep normal scheduling and cursor updates unchanged when the
page advances, preventing non-progressing pages from spinning.
- Line 400: Guard manifest.exec.argv for emptiness before the Command
construction and any argv slicing in the journal hook delivery flow. Return a
delivery error through the existing error path so the worker still reaches
finish_journal_hook_delivery and completed.send(key), while preserving normal
execution for non-empty argv.
- Around line 341-354: Update the worker closure in the journal-hook delivery
spawn to install a drop guard that sends the DeliveryKey through completed
whenever the closure exits, including during panic unwinding from
execute_delivery. Remove the normal-path-only completed.send(key) call and
ensure the guard owns the key and sender without changing the existing delivery
completion logic.
- Around line 166-169: Update the existing-hook branch in the hook
synchronization logic around hook.manifest assignment to recompile the hook’s
CompiledHookFilter from the replacement manifest before continuing. Store the
newly compiled filter with the updated manifest, and remove or skip the hook
when compilation fails so no stale filter remains.

In `@cmux-tui/crates/cmux-tui-core/src/journal_kernel.rs`:
- Around line 32-48: Update JournalDocument::new to return a Result instead of
panicking on either serde_json serialization failure, and propagate that error
through the mux-session-journal-fanout path. When document construction fails,
mark the fanout state unavailable and stop advancing/subscriber processing
rather than allowing the tailer thread to unwind while state.available remains
true.
- Around line 386-427: Update the tailer logic around the reader.after
pagination and wait loop to track a retry-needed state: when a read fails, retry
after the one-second wait timeout instead of waiting only for a requested_epoch
change, while preserving Unavailable reporting until recovery. Do not advance
last_sequence while collecting pages; advance it only as records are
successfully published under the kernel state update, so discarded records are
read again after a later-page failure.
- Around line 259-266: Update the record selection in the journal read path
around SharedJournalRead::Page to use partition_point on the ascending record
sequences to locate the first record greater than sequence, then take at most
limit records from that position. Preserve cloning and page construction while
avoiding a full state.records scan for each read.

In `@cmux-tui/crates/cmux-tui-core/src/mux.rs`:
- Around line 4400-4416: Update put_journal_producer to call
journal_kernel.install_producer before workspace_registry.put_journal_producer,
matching the validation-before-durable-write order used by
append_journal_ingress. First verify install_producer is idempotent and rejects
invalid manifests without side effects; preserve the existing commit result and
publish_journal_event behavior after the reordered operations.
- Around line 4547-4569: Bound record accumulation in journal_restore_preview
with an explicit maximum replay count, stopping pagination once the cap is
reached even if the journal head continues advancing. Mark the returned preview
payload as truncated when records were omitted, while preserving complete
previews when the checkpoint tail fits within the cap. Update the
session-journal specification and binding models if the public payload gains a
truncated field.

In `@cmux-tui/crates/cmux-tui-core/src/server.rs`:
- Around line 6897-6913: The journal error handling must stop exposing raw
anyhow text in user-visible responses. In server.rs lines 6897-6913, update
journal_extension_error to match typed journal errors, return stable reason
codes without details.error, and log the underlying error; in lines 6994-7004,
log the catch-up reader error and return reason_code journal_reader_unavailable
instead of error.to_string().

In `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/journal_extensions.rs`:
- Around line 219-236: Scope journal idempotency receipts by origin: update the
journal_operation_receipts and journal_ingress_receipts schemas to store origin
and include it in each composite primary key, then update operation_receipt and
ingress_receipt lookups and writes to use the caller’s public_client_id-derived
origin with the idempotency key. Preserve payload fingerprint validation while
ensuring receipts from different origins cannot be reused.
- Around line 941-964: Replace the per-item query_session_journal_after call
inside the rows.into_iter() mapping with one bounded batch fetch covering all
collected delivery sequences, from the minimum through maximum sequence (or a
single sequence-selection query). Index the fetched journal records by sequence,
then reuse that index while constructing each JournalHookDelivery and preserve
the existing missing-record and conversion errors.
- Around line 294-302: Remove the duplicated journal_event_index DDL and
backfill from the current migration, and reuse the existing
session_journal::create_session_journal_schema or a dedicated
create_journal_event_index_schema helper. Ensure the shared schema path remains
aligned with ensure_journal_event_index_schema and still performs the required
backfill from session_journal.
- Around line 1487-1496: Update the seal path’s trigger management to use
idempotent DROP and CREATE statements for session_journal_reject_delete, so
sealing succeeds whether the trigger is currently present or absent.

In `@cmux-tui/crates/cmux-tui/src/cli/command.rs`:
- Around line 2128-2130: Move the `--event-json` object validation into the
`journal append` command arm, before `apply_journal_hook_context`, so non-object
JSON always returns the same local `UsageError` regardless of
`CMUX_JOURNAL_HOOK_ID`, `CMUX_JOURNAL_HOOK_NAME`, or
`CMUX_JOURNAL_HOOK_DESCRIPTION`; keep the existing context application behavior
for valid object events.
- Around line 2816-2839: Expand the tests around apply_journal_hook_context to
cover the four untested branches: filter empty environment values, preserve an
existing non-null causation_id, avoid appending a duplicate hook subject, and
return a usage error when subjects is not an array. Add focused cases or
assertions alongside journal_append_inherits_scoped_hook_causation while
preserving the existing all-values-present behavior.

In `@cmux-tui/crates/cmux-tui/src/cli/wire.rs`:
- Around line 153-161: Update the error construction in the journal subscription
handling around global.session so details.session is included only when the user
explicitly supplied a session name. Do not substitute the "main" fallback;
preserve the capability and restart_session details and omit the session field
for socket-selected or otherwise unnamed targets.

In `@cmux-tui/crates/cmux-tui/tests/resource_cli_v1.rs`:
- Around line 827-833: Extend the journal subscription test near
journal_subscription_rejects_a_stale_session_before_sending_the_new_envelope to
cover a capable server: have the fake server respond to identify with the
session-journal-v1 capability, then handle the subsequent
session.journal.subscribe request, and assert both requests are received in
order.

---

Outside diff comments:
In `@cmux-tui/crates/cmux-tui/src/cli/command.rs`:
- Around line 3672-3674: Verify the operation total from operation_catalog
against the current resource specification, then update the CLI test near the
cases vector and its count assertions to include every newly declared
session.journal operation, including append, producer, hook, checkpoint,
restore, and segment operations. Keep the exclusion list intentional and ensure
the expected/seen set comparison remains consistent with the complete catalog.

In `@cmux-tui/spec/session-journal.md`:
- Around line 168-183: Add operation.unsupported to the versioned protocol
catalog’s errors object and include it in the declared errors for
session.journal.subscribe and any other affected operations, defining its
details schema so typed clients can handle it. Ensure the catalog matches the
documented pre-negotiation behavior; otherwise revise the journal subscription
prose to remove that undocumented guarantee.

---

Duplicate comments:
In `@cmux-tui/spec/cli.md`:
- Around line 153-164: Update the `journal subscribe` grammar entry to replace
`[FILTERS]` with the concrete `--kinds`, `--classes`, `--subjects`, and
`--max-sensitivity` options, and document cursor resumption with
`--cursor-session` and `--sequence` instead of `--from`. Match the explicit flag
style used by the surrounding command entries.

In `@cmux-tui/spec/resource-operations-v1.json`:
- Around line 8864-8942: Update the constraints array for
session.journal.subscribe to state directly that the stream never delivers
secret entries, regardless of whether the optional filter is supplied. Keep the
existing cursor/start constraints unchanged and do not rely solely on
JournalFilter.constraints for this guarantee.

In `@cmux-tui/spec/session-journal.md`:
- Around line 146-162: Update the JournalFilter sensitivity description to
explicitly define the ordering, stating that higher thresholds include all
lower-sensitivity records, and state that omitted max_sensitivity defaults to
metadata. Keep the existing client-specific caps and secret-record restriction
unchanged.
- Around line 139-152: Update the session journal subscription documentation to
explicitly state that cursor and start are mutually exclusive. Also document
that every subjects filter entry must specify at least a kind or an id, matching
the validation enforced by Session::journal and
JournalSubjectFilter.constraints.
🪄 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 Plus

Run ID: 87375819-67eb-4fd3-b03d-5090f8139c0a

📥 Commits

Reviewing files that changed from the base of the PR and between b96c2b6 and 7cc5695.

⛔ Files ignored due to path filters (1)
  • cmux-tui/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (47)
  • cmux-tui/Cargo.toml
  • cmux-tui/bindings/cpp/.cmux-resource-api.json
  • cmux-tui/bindings/cpp/include/cmux/resource.hpp
  • cmux-tui/bindings/cpp/src/resource.cpp
  • cmux-tui/bindings/go/.cmux-resource-api.json
  • cmux-tui/bindings/go/options.go
  • cmux-tui/bindings/java/.cmux-resource-api.json
  • cmux-tui/bindings/java/src/com/cmux/Options.java
  • cmux-tui/bindings/java/src/com/cmux/Session.java
  • cmux-tui/bindings/python/.cmux-resource-api.json
  • cmux-tui/bindings/python/cmux/options.py
  • cmux-tui/bindings/python/cmux/resources.py
  • cmux-tui/bindings/rust/.cmux-resource-api.json
  • cmux-tui/bindings/rust/src/resource/handles.rs
  • cmux-tui/bindings/rust/src/resource/options.rs
  • cmux-tui/bindings/typescript/.cmux-resource-api.json
  • cmux-tui/bindings/typescript/src/options.ts
  • cmux-tui/bindings/typescript/src/resources.ts
  • cmux-tui/bindings/zig/.cmux-resource-api.json
  • cmux-tui/bindings/zig/src/resource.zig
  • cmux-tui/crates/cmux-tui-core/Cargo.toml
  • cmux-tui/crates/cmux-tui-core/src/journal_checkpoint.rs
  • cmux-tui/crates/cmux-tui-core/src/journal_hooks.rs
  • cmux-tui/crates/cmux-tui-core/src/journal_kernel.rs
  • cmux-tui/crates/cmux-tui-core/src/lib.rs
  • cmux-tui/crates/cmux-tui-core/src/mux.rs
  • cmux-tui/crates/cmux-tui-core/src/resource.rs
  • cmux-tui/crates/cmux-tui-core/src/resource_router.rs
  • cmux-tui/crates/cmux-tui-core/src/server.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/journal_extensions.rs
  • cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs
  • cmux-tui/crates/cmux-tui/src/cli.rs
  • cmux-tui/crates/cmux-tui/src/cli/command.rs
  • cmux-tui/crates/cmux-tui/src/cli/wire.rs
  • cmux-tui/crates/cmux-tui/tests/resource_cli_v1.rs
  • cmux-tui/scripts/check-resource-api-boundary.py
  • cmux-tui/scripts/test_check_resource_api_boundary.py
  • cmux-tui/spec/cli.md
  • cmux-tui/spec/commands.md
  • cmux-tui/spec/inventory.json
  • cmux-tui/spec/programmability.md
  • cmux-tui/spec/resource-api-v1.json
  • cmux-tui/spec/resource-api-v1.md
  • cmux-tui/spec/resource-operations-v1.json
  • cmux-tui/spec/resource-operations-v1.md
  • cmux-tui/spec/session-journal.md

Comment thread cmux-tui/bindings/go/options.go Outdated
}
type JournalRegexFilter struct {
Pattern string `json:"pattern"`
Field string `json:"field,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Give Field a named type with constants, as the neighbouring journal fields have.

Classes uses []JournalClass and MaxSensitivity uses *JournalSensitivity, but Field is a bare string. The C++ and Java bindings both define a JournalRegexField enum, and the CLI validates the value against kind, subjects, payload, and record. A Go caller who passes "kinds" learns about the mistake only from the server.

♻️ Proposed fix
+type JournalRegexField string
+
+const (
+	JournalRegexFieldKind     JournalRegexField = "kind"
+	JournalRegexFieldSubjects JournalRegexField = "subjects"
+	JournalRegexFieldPayload  JournalRegexField = "payload"
+	JournalRegexFieldRecord   JournalRegexField = "record"
+)
+
 type JournalRegexFilter struct {
-	Pattern       string `json:"pattern"`
-	Field         string `json:"field,omitempty"`
-	CaseSensitive *bool  `json:"case_sensitive,omitempty"`
+	Pattern       string            `json:"pattern"`
+	Field         JournalRegexField `json:"field,omitempty"`
+	CaseSensitive *bool             `json:"case_sensitive,omitempty"`
 }
🤖 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 `@cmux-tui/bindings/go/options.go` at line 88, Replace the bare string type of
Field with a named JournalRegexField type, define constants for the supported
values kind, subjects, payload, and record, and update the surrounding options
declarations consistently with the existing JournalClass and JournalSensitivity
patterns. Ensure callers can use the constants and invalid plural values such as
“kinds” are not represented as valid field constants.

Comment on lines +97 to +109
public record JournalRegexFilter(
String pattern,
JournalRegexField field,
boolean caseSensitive
) {
public JournalRegexFilter {
Objects.requireNonNull(pattern, "pattern");
field = field == null ? JournalRegexField.RECORD : field;
if (pattern.isEmpty() || pattern.getBytes(java.nio.charset.StandardCharsets.UTF_8).length > 1024) {
throw new IllegalArgumentException("journal regex must contain 1 to 1024 UTF-8 bytes");
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a convenience constructor that applies the documented regex defaults.

The compact constructor defaults a null field to RECORD, but caseSensitive is a primitive with no default path. Every caller must pass all three components, so new JournalRegexFilter("ready", null, false) reads like a default construction while it selects case-insensitive matching. The C++ struct defaults case_sensitive to true, and the CLI sends true unless --ignore-case is present.

This file already uses overloads for the same purpose in JournalFilter, ClientSizing, and LayoutUndo.

♻️ Proposed fix
     public record JournalRegexFilter(
         String pattern,
         JournalRegexField field,
         boolean caseSensitive
     ) {
+        public JournalRegexFilter(String pattern) {
+            this(pattern, JournalRegexField.RECORD, true);
+        }
+
+        public JournalRegexFilter(String pattern, JournalRegexField field) {
+            this(pattern, field, true);
+        }
+
         public JournalRegexFilter {
             Objects.requireNonNull(pattern, "pattern");
             field = field == null ? JournalRegexField.RECORD : field;
📝 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
public record JournalRegexFilter(
String pattern,
JournalRegexField field,
boolean caseSensitive
) {
public JournalRegexFilter {
Objects.requireNonNull(pattern, "pattern");
field = field == null ? JournalRegexField.RECORD : field;
if (pattern.isEmpty() || pattern.getBytes(java.nio.charset.StandardCharsets.UTF_8).length > 1024) {
throw new IllegalArgumentException("journal regex must contain 1 to 1024 UTF-8 bytes");
}
}
}
public record JournalRegexFilter(
String pattern,
JournalRegexField field,
boolean caseSensitive
) {
public JournalRegexFilter(String pattern) {
this(pattern, JournalRegexField.RECORD, true);
}
public JournalRegexFilter(String pattern, JournalRegexField field) {
this(pattern, field, true);
}
public JournalRegexFilter {
Objects.requireNonNull(pattern, "pattern");
field = field == null ? JournalRegexField.RECORD : field;
if (pattern.isEmpty() || pattern.getBytes(java.nio.charset.StandardCharsets.UTF_8).length > 1024) {
throw new IllegalArgumentException("journal regex must contain 1 to 1024 UTF-8 bytes");
}
}
}
🤖 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 `@cmux-tui/bindings/java/src/com/cmux/Options.java` around lines 97 - 109, Add
a two-argument JournalRegexFilter constructor taking pattern and field that
delegates to the existing three-argument constructor with caseSensitive
defaulting to true. Preserve the current null-field handling and validation in
the compact JournalRegexFilter constructor, matching the overload pattern used
by JournalFilter, ClientSizing, and LayoutUndo.

Comment on lines +5937 to +5955
if (filter.regex) |regex_filter| {
if (regex_filter.pattern.len == 0 or regex_filter.pattern.len > 1024) {
return error.InvalidJournalFilter;
}
var encoded = raw.wire.Object.init(allocator);
try encoded.put(
try allocator.dupe(u8, "pattern"),
.{ .string = try allocator.dupe(u8, regex_filter.pattern) },
);
try encoded.put(
try allocator.dupe(u8, "field"),
.{ .string = try allocator.dupe(u8, @tagName(regex_filter.field)) },
);
try encoded.put(
try allocator.dupe(u8, "case_sensitive"),
.{ .bool = regex_filter.case_sensitive },
);
try encoded_filter.put("regex", .{ .object = encoded });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add test coverage for the new regex journal filter.

encodeSessionJournalOptions gains a regex branch that validates pattern length (1-1024 bytes) and encodes pattern, field, and case_sensitive. No test exercises this branch: the existing journal filter test at line 19280 does not set filter.regex, so the encoded regex object, its default field, its default case_sensitive, and the error.InvalidJournalFilter boundary for an empty or over-long pattern are all untested.

Add assertions for a supplied regex filter and a boundary test for an invalid pattern.

Also applies to: 19280-19359

🤖 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 `@cmux-tui/bindings/zig/src/resource.zig` around lines 5937 - 5955, Extend the
existing journal filter tests around encodeSessionJournalOptions to supply
filter.regex and assert the encoded regex object contains pattern, the default
field, and default case_sensitive values. Add boundary coverage verifying empty
and over-1024-byte patterns return error.InvalidJournalFilter, while a
1-1024-byte pattern encodes successfully.

Comment on lines +207 to +210
if let Some(revision) = record.resource_revision {
snapshot.entry("cursor").or_insert_with(|| json!({}))["revision"] =
Value::String(revision.to_string());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid the panicking index assignment on cursor.

snapshot.entry("cursor").or_insert_with(|| json!({}))["revision"] = ... uses IndexMut<&str> on a Value. That implementation panics when the value is present and is neither an object nor null. A checkpoint whose session_snapshot.cursor is a string or a number therefore aborts the request thread instead of returning an error. restore_preview runs on the session.journal.restore.preview request path.

Fail closed with an error instead.

🛡️ Proposed change
     if let Some(revision) = record.resource_revision {
-        snapshot.entry("cursor").or_insert_with(|| json!({}))["revision"] =
-            Value::String(revision.to_string());
+        let cursor = snapshot.entry("cursor").or_insert_with(|| json!({}));
+        let cursor = cursor
+            .as_object_mut()
+            .context("checkpoint session_snapshot cursor is not an object")?;
+        cursor.insert("revision".into(), Value::String(revision.to_string()));
     }
📝 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
if let Some(revision) = record.resource_revision {
snapshot.entry("cursor").or_insert_with(|| json!({}))["revision"] =
Value::String(revision.to_string());
}
if let Some(revision) = record.resource_revision {
let cursor = snapshot.entry("cursor").or_insert_with(|| json!({}));
let cursor = cursor
.as_object_mut()
.context("checkpoint session_snapshot cursor is not an object")?;
cursor.insert("revision".into(), Value::String(revision.to_string()));
}
🤖 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 `@cmux-tui/crates/cmux-tui-core/src/journal_checkpoint.rs` around lines 207 -
210, Update the snapshot cursor handling around the resource revision assignment
to validate that the existing “cursor” value is an object or null before setting
“revision”; if it is any other JSON type, return an error instead of using
panicking Value index assignment. Preserve creating an empty cursor object when
the entry is absent, and propagate the error through restore_preview.

Comment on lines +251 to +269
match kind {
"upsert" => {
let Some(value) = change.get("value") else { return Ok(false) };
if let Some(existing) =
values.iter_mut().find(|candidate| candidate["id"].as_str() == Some(id))
{
*existing = value.clone();
} else {
values.push(value.clone());
}
values.sort_by(|left, right| {
left["index"]
.as_u64()
.unwrap_or(u64::MAX)
.cmp(&right["index"].as_u64().unwrap_or(u64::MAX))
.then_with(|| left["id"].as_str().cmp(&right["id"].as_str()))
});
Ok(true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sort each collection once per record, not once per change.

apply_resource_change re-sorts the whole target collection after every upsert. A record that upserts k items into a collection of n items costs O(k · n log n), and restore_preview calls this for every replayed record. Apply all changes for one record first, then sort each touched collection once.

As per path instructions: "repeated sort/filter/map work in hot paths".

🤖 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 `@cmux-tui/crates/cmux-tui-core/src/journal_checkpoint.rs` around lines 251 -
269, The upsert branch in apply_resource_change currently sorts the target
collection after every change. Remove that per-change sort, track collections
modified during the record, and sort each touched collection once after all
changes for the record have been applied, preserving the existing index-then-id
ordering used by the values.sort_by comparator.

Source: Path instructions

Comment on lines +1487 to +1496
tx.execute_batch("DROP TRIGGER session_journal_reject_delete;")?;
tx.execute(
"DELETE FROM session_journal WHERE sequence <= ?1",
[i64::try_from(through_sequence)?],
)?;
tx.execute_batch(
"CREATE TRIGGER session_journal_reject_delete
BEFORE DELETE ON session_journal
BEGIN SELECT RAISE(ABORT, 'session journal is append-only'); END;",
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use DROP TRIGGER IF EXISTS in the seal path.

tx.execute_batch("DROP TRIGGER session_journal_reject_delete;") fails if the trigger is absent. A database created before the trigger existed, or one where a prior recovery path removed it, then aborts every seal request. The recreate statement also lacks IF NOT EXISTS.

🛡️ Proposed change
-        tx.execute_batch("DROP TRIGGER session_journal_reject_delete;")?;
+        tx.execute_batch("DROP TRIGGER IF EXISTS session_journal_reject_delete;")?;
@@
         tx.execute_batch(
-            "CREATE TRIGGER session_journal_reject_delete
+            "CREATE TRIGGER IF NOT EXISTS session_journal_reject_delete
              BEFORE DELETE ON session_journal
              BEGIN SELECT RAISE(ABORT, 'session journal is append-only'); END;",
         )?;
📝 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
tx.execute_batch("DROP TRIGGER session_journal_reject_delete;")?;
tx.execute(
"DELETE FROM session_journal WHERE sequence <= ?1",
[i64::try_from(through_sequence)?],
)?;
tx.execute_batch(
"CREATE TRIGGER session_journal_reject_delete
BEFORE DELETE ON session_journal
BEGIN SELECT RAISE(ABORT, 'session journal is append-only'); END;",
)?;
tx.execute_batch("DROP TRIGGER IF EXISTS session_journal_reject_delete;")?;
tx.execute(
"DELETE FROM session_journal WHERE sequence <= ?1",
[i64::try_from(through_sequence)?],
)?;
tx.execute_batch(
"CREATE TRIGGER IF NOT EXISTS session_journal_reject_delete
BEFORE DELETE ON session_journal
BEGIN SELECT RAISE(ABORT, 'session journal is append-only'); END;",
)?;
🤖 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 `@cmux-tui/crates/cmux-tui-core/src/workspace_registry/journal_extensions.rs`
around lines 1487 - 1496, Update the seal path’s trigger management to use
idempotent DROP and CREATE statements for session_journal_reject_delete, so
sealing succeeds whether the trigger is currently present or absent.

Comment on lines +2128 to +2130
let object = event
.as_object_mut()
.ok_or_else(|| UsageError::new("--event-json must contain a JSON object"))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Local validation of --event-json depends on ambient environment variables.

apply_journal_hook_context returns early at line 2125 when all three environment variables are absent or empty. The object check at lines 2128-2130 then never runs. cmux session <selector> journal append --event-json '"text"' therefore fails locally with a usage error when CMUX_JOURNAL_HOOK_ID is set, and reaches the server when it is not. Validate the event shape in the journal append arm so the local error is deterministic.

♻️ Proposed fix in the journal append arm
         [selector, "journal", "append"] => {
             selectors.insert("session", "session", selector)?;
             let mut event = parse_json_flag(flags, "event-json")?;
+            if !event.is_object() {
+                return Err(UsageError::new("--event-json must contain a JSON object"));
+            }
             let hook_id = std::env::var("CMUX_JOURNAL_HOOK_ID").ok();
🤖 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 `@cmux-tui/crates/cmux-tui/src/cli/command.rs` around lines 2128 - 2130, Move
the `--event-json` object validation into the `journal append` command arm,
before `apply_journal_hook_context`, so non-object JSON always returns the same
local `UsageError` regardless of `CMUX_JOURNAL_HOOK_ID`,
`CMUX_JOURNAL_HOOK_NAME`, or `CMUX_JOURNAL_HOOK_DESCRIPTION`; keep the existing
context application behavior for valid object events.

Comment on lines +2816 to +2839
#[test]
fn journal_append_inherits_scoped_hook_causation() {
let mut event = json!({
"producer_id":"demo",
"subjects":[{"kind":"workspace","id":"ws_test"}],
"payload":{},
});
apply_journal_hook_context(
&mut event,
Some("demo_hook"),
Some("event_hook_started"),
Some("demo_hook:1:event_source"),
)
.unwrap();
assert_eq!(event["causation_id"], "event_hook_started");
assert_eq!(event["correlation_id"], "demo_hook:1:event_source");
assert_eq!(
event["subjects"],
json!([
{"kind":"workspace","id":"ws_test"},
{"kind":"hook","id":"demo_hook"},
])
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the remaining branches of apply_journal_hook_context.

This test covers only the case where all three values are present and subjects is a well-formed array. Four branches stay untested: an empty environment value is filtered at lines 2122-2124, an existing non-null causation_id is preserved at line 2132, a duplicate hook subject is not appended at lines 2146-2152, and a non-array subjects value returns a usage error at line 2145.

💚 Proposed additional assertions
         assert_eq!(
             event["subjects"],
             json!([
                 {"kind":"workspace","id":"ws_test"},
                 {"kind":"hook","id":"demo_hook"},
             ])
         );
+
+        // Empty values are ignored and existing context is preserved.
+        let mut existing = json!({"causation_id":"kept","subjects":[{"kind":"hook","id":"demo_hook"}]});
+        apply_journal_hook_context(&mut existing, Some("demo_hook"), Some("replaced"), Some(""))
+            .unwrap();
+        assert_eq!(existing["causation_id"], "kept");
+        assert!(existing.get("correlation_id").is_none());
+        assert_eq!(existing["subjects"].as_array().unwrap().len(), 1);
+
+        let mut not_an_object = json!("text");
+        assert!(
+            apply_journal_hook_context(&mut not_an_object, Some("demo_hook"), None, None).is_err()
+        );
+        let mut bad_subjects = json!({"subjects":{}});
+        assert!(
+            apply_journal_hook_context(&mut bad_subjects, Some("demo_hook"), None, None).is_err()
+        );
📝 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
#[test]
fn journal_append_inherits_scoped_hook_causation() {
let mut event = json!({
"producer_id":"demo",
"subjects":[{"kind":"workspace","id":"ws_test"}],
"payload":{},
});
apply_journal_hook_context(
&mut event,
Some("demo_hook"),
Some("event_hook_started"),
Some("demo_hook:1:event_source"),
)
.unwrap();
assert_eq!(event["causation_id"], "event_hook_started");
assert_eq!(event["correlation_id"], "demo_hook:1:event_source");
assert_eq!(
event["subjects"],
json!([
{"kind":"workspace","id":"ws_test"},
{"kind":"hook","id":"demo_hook"},
])
);
}
#[test]
fn journal_append_inherits_scoped_hook_causation() {
let mut event = json!({
"producer_id":"demo",
"subjects":[{"kind":"workspace","id":"ws_test"}],
"payload":{},
});
apply_journal_hook_context(
&mut event,
Some("demo_hook"),
Some("event_hook_started"),
Some("demo_hook:1:event_source"),
)
.unwrap();
assert_eq!(event["causation_id"], "event_hook_started");
assert_eq!(event["correlation_id"], "demo_hook:1:event_source");
assert_eq!(
event["subjects"],
json!([
{"kind":"workspace","id":"ws_test"},
{"kind":"hook","id":"demo_hook"},
])
);
// Empty values are ignored and existing context is preserved.
let mut existing = json!({"causation_id":"kept","subjects":[{"kind":"hook","id":"demo_hook"}]});
apply_journal_hook_context(&mut existing, Some("demo_hook"), Some("replaced"), Some(""))
.unwrap();
assert_eq!(existing["causation_id"], "kept");
assert!(existing.get("correlation_id").is_none());
assert_eq!(existing["subjects"].as_array().unwrap().len(), 1);
let mut not_an_object = json!("text");
assert!(
apply_journal_hook_context(&mut not_an_object, Some("demo_hook"), None, None).is_err()
);
let mut bad_subjects = json!({"subjects":{}});
assert!(
apply_journal_hook_context(&mut bad_subjects, Some("demo_hook"), None, None).is_err()
);
}
🤖 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 `@cmux-tui/crates/cmux-tui/src/cli/command.rs` around lines 2816 - 2839, Expand
the tests around apply_journal_hook_context to cover the four untested branches:
filter empty environment values, preserve an existing non-null causation_id,
avoid appending a duplicate hook subject, and return a usage error when subjects
is not an array. Add focused cases or assertions alongside
journal_append_inherits_scoped_hook_causation while preserving the existing
all-values-present behavior.

Comment on lines +153 to +161
let session = global.session.as_deref().unwrap_or("main");
let error = json!({
"code":"operation.unsupported",
"message":"resident session does not support journal subscriptions; restart it with this cmux-tui binary",
"details":{
"capability":capability,
"session":session,
"action":"restart_session"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Do not report a fabricated session name in the error details.

global.session.as_deref().unwrap_or("main") reports session: "main" when the user named no session. The user may have selected the target with --socket or CMUX_TUI_SOCKET, so the reported identifier is invented rather than supplied. Omit the field when the user supplied no session.

🛡️ Proposed fix
-    let session = global.session.as_deref().unwrap_or("main");
-    let error = json!({
-        "code":"operation.unsupported",
-        "message":"resident session does not support journal subscriptions; restart it with this cmux-tui binary",
-        "details":{
-            "capability":capability,
-            "session":session,
-            "action":"restart_session"
-        },
-        "retryable":false
-    });
+    let mut details = json!({"capability":capability,"action":"restart_session"});
+    if let Some(session) = global.session.as_deref() {
+        details["session"] = Value::String(session.to_string());
+    }
+    let error = json!({
+        "code":"operation.unsupported",
+        "message":"resident session does not support journal subscriptions; restart it with this cmux-tui binary",
+        "details":details,
+        "retryable":false
+    });

Based on learnings from the coding guidelines: "Do not expose raw upstream errors, stack traces, third-party request IDs, billing item or customer IDs, or team IDs unless the user supplied the exact ID."

📝 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
let session = global.session.as_deref().unwrap_or("main");
let error = json!({
"code":"operation.unsupported",
"message":"resident session does not support journal subscriptions; restart it with this cmux-tui binary",
"details":{
"capability":capability,
"session":session,
"action":"restart_session"
},
let mut details = json!({"capability":capability,"action":"restart_session"});
if let Some(session) = global.session.as_deref() {
details["session"] = Value::String(session.to_string());
}
let error = json!({
"code":"operation.unsupported",
"message":"resident session does not support journal subscriptions; restart it with this cmux-tui binary",
"details":details,
"retryable":false
});
🤖 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 `@cmux-tui/crates/cmux-tui/src/cli/wire.rs` around lines 153 - 161, Update the
error construction in the journal subscription handling around global.session so
details.session is included only when the user explicitly supplied a session
name. Do not substitute the "main" fallback; preserve the capability and
restart_session details and omit the session field for socket-selected or
otherwise unnamed targets.

Source: Coding guidelines

Comment on lines +827 to +833
#[cfg(unix)]
#[test]
fn journal_subscription_rejects_a_stale_session_before_sending_the_new_envelope() {
let dir = unique_temp_dir("journal-stale-session");
fs::create_dir_all(&dir).unwrap();
let socket = dir.join("mux.sock");
let listener = transport::listen(&socket).unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the capable-server case for journal capability negotiation.

This test proves that a stale server is rejected. No test proves that a server advertising session-journal-v1 receives the journal request after the preflight. The gate in required_server_capability now covers eleven operations, so a regression that rejects a capable server, or that drops the real request after a successful preflight, would pass this suite.

Extend the fake server to answer the identify request with session-journal-v1 present, then answer the following session.journal.subscribe request, and assert that both requests arrive in order.

Do you want me to generate that test?

🤖 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 `@cmux-tui/crates/cmux-tui/tests/resource_cli_v1.rs` around lines 827 - 833,
Extend the journal subscription test near
journal_subscription_rejects_a_stale_session_before_sending_the_new_envelope to
cover a capable server: have the fake server respond to identify with the
session-journal-v1 capability, then handle the subsequent
session.journal.subscribe request, and assert both requests are received in
order.

…rnal

# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-resource-api.json
#	cmux-tui/bindings/go/.cmux-resource-api.json
#	cmux-tui/bindings/java/.cmux-resource-api.json
#	cmux-tui/bindings/python/.cmux-resource-api.json
#	cmux-tui/bindings/rust/.cmux-resource-api.json
#	cmux-tui/bindings/rust/src/resource/mod.rs
#	cmux-tui/bindings/rust/tests/catalog_manifest.rs
#	cmux-tui/bindings/typescript/.cmux-resource-api.json
#	cmux-tui/bindings/zig/.cmux-resource-api.json
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/terminal_exit_store.rs
#	cmux-tui/scripts/test_check_resource_api_boundary.py
#	cmux-tui/spec/commands.md
#	cmux-tui/spec/resource-api-v1.md
#	cmux-tui/spec/resource-operations-v1.md
…at-cmux-tui-session-journal

# Conflicts:
#	.github/workflows/cmux-tui.yml
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/include/cmux/raw/generated/models.hpp
#	cmux-tui/bindings/go/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/go/raw/generated_commands.go
#	cmux-tui/bindings/go/raw/generated_events.go
#	cmux-tui/bindings/go/raw/generated_metadata.go
#	cmux-tui/bindings/go/raw/generated_presence_test.go
#	cmux-tui/bindings/go/raw/generated_types.go
#	cmux-tui/bindings/java/src/com/cmux/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/java/src/com/cmux/raw/Protocol.java
#	cmux-tui/bindings/python/cmux/raw/_generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/python/cmux/raw/_generated/_schema.py
#	cmux-tui/bindings/python/cmux/raw/_generated/metadata.py
#	cmux-tui/bindings/rust-sidebar/tests/runtime.rs
#	cmux-tui/bindings/rust/src/client.rs
#	cmux-tui/bindings/rust/src/codec.rs
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/rust/src/resource/client.rs
#	cmux-tui/bindings/rust/tests/mock_server.rs
#	cmux-tui/bindings/typescript/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/typescript/src/raw/generated/commands.ts
#	cmux-tui/bindings/typescript/src/raw/generated/events.ts
#	cmux-tui/bindings/typescript/src/raw/generated/index.ts
#	cmux-tui/bindings/typescript/src/raw/generated/metadata.ts
#	cmux-tui/bindings/typescript/src/raw/generated/types.ts
#	cmux-tui/bindings/zig/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/zig/src/raw/generated/protocol.zig
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_protocol.rs
#	cmux-tui/crates/cmux-tui-core/tests/browser_runtime.rs
#	cmux-tui/crates/cmux-tui/src/session/mod.rs
#	cmux-tui/crates/cmux-tui/src/session/remote.rs
…at-cmux-tui-session-journal

# Conflicts:
#	.github/workflows/cmux-tui.yml
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@blacksmith-sh

This comment has been minimized.

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

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