Skip to content

fix(cli): persist remove-agent + session rename before responding#1055

Merged
pedramamini merged 1 commit into
rcfrom
fix/1054-cli-remove-rename-persist
May 30, 2026
Merged

fix(cli): persist remove-agent + session rename before responding#1055
pedramamini merged 1 commit into
rcfrom
fix/1054-cli-remove-rename-persist

Conversation

@pedramamini
Copy link
Copy Markdown
Collaborator

@pedramamini pedramamini commented May 29, 2026

Summary

Extends the synchronous sessions:setMany flush pattern introduced in #1014 to the two remaining CLI-driven IPC handlers that bypass the renderer's 2s debounced persistence path:

  • maestro:remoteDeleteSession — was relying on useDebouncedPersistence to eventually persist the removal. A CLI remove-agent immediately followed by list agents could read the stale on-disk state until the debounce fired.
  • maestro:remoteRenameSession — same race for the renamed name; show agent would see the old name for up to 2s.

Both handlers now await window.maestro.sessions.setMany(updates, removeIds) before signaling success. The subsequent debounced flush is idempotent (it just rewrites the same merged set), so there's no churn.

Scope notes

The issue also called out that the web-server-factory's setDeleteSessionCallback is fire-and-forget (returns true before the renderer even sees the event), which is technically still a tiny race even with the disk flush in place. I left that pattern alone for now — the reporter rated it as low real-world impact, and the user-observable reproduction (CLI follow-up reads stale disk state) is fully closed by the renderer-side flush. Happy to follow up with a request-response refactor for the delete callback (matching rename / create / updateCwd) if desired.

Closes #1054
Refs #1013 #1014

Test plan

  • npm run lint passes
  • npx vitest run unit tests pass (1570 in the related suites)
  • Pre-push full suite: 29,749 passed
  • Manual smoke: start Maestro desktop, run maestro-cli remove-agent <id>list agents immediately, confirm agent is gone
  • Manual smoke: maestro-cli session rename <id> "New Name"show agent <id> immediately, confirm new name surfaces
  • Inspect ~/Library/Application Support/Maestro/maestro-sessions.json between the CLI call and the 2s debounce window to confirm disk is up-to-date

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of session deletion and renaming operations by ensuring changes are immediately persisted to disk, preventing potential data loss if the app exits unexpectedly during these operations.

Review Change Stack

Mirrors the synchronous setMany flush pattern from #1014 for the
remoteDeleteSession and remoteRenameSession handlers, so CLI consumers
that hit the disk-backed session store immediately after the operation
no longer see stale state during the 2s debounced flush window.

Closes #1054
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

Two session remote event handlers in the renderer now await immediate disk persistence before responding to CLI clients, closing persistence race conditions where the CLI received success responses before the renderer's debounced storage flush could complete. Delete and rename operations both synchronously flush their mutations via window.maestro.sessions.setMany.

Changes

Session Remote Event Persistence

Layer / File(s) Summary
Immediate persistence for delete and rename session handlers
src/renderer/hooks/remote/useAppRemoteEventListeners.ts
maestro:remoteDeleteSession now awaits setMany([], [sessionId]) after removing the session from state to flush the deletion to disk. maestro:remoteRenameSession is made async and awaits setMany([{...session, name: newName}], []) before sending the success response. Both include error logging for persistence failures.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #1013: This PR addresses the same class of persistence race bugs that the original issue reported—making IPC handlers persist session mutations immediately instead of relying on debounced flushes.

Poem

🐰 Sessions delete, rename with grace,
No more stale data win the race,
Await setMany before reply,
Disk stays fresh, no race goodbye! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding synchronous persistence before responding to CLI remove-agent and session rename operations.
Linked Issues check ✅ Passed The PR successfully implements both persistence fixes requested in #1054: awaiting sessions.setMany before responding for remoteDeleteSession and remoteRenameSession handlers.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the two persistence races described in #1054; no unrelated modifications are present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1054-cli-remove-rename-persist

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR extends the synchronous sessions:setMany flush pattern to the maestro:remoteDeleteSession and maestro:remoteRenameSession IPC handlers, closing a 2-second race window where CLI follow-up reads could observe stale on-disk session state before the debounced persistence timer fired.

  • Delete handler: Correctly calls setMany([], [sessionId]) after removing the session from React state. Because the delete callback is already fire-and-forget at the IPC layer, a setMany failure here is low-impact.
  • Rename handler: Calls setMany([{ ...session, name: newName }], []) before sending the response channel reply, but sendRemoteRenameSessionResponse(responseChannel, true) is unconditional - it fires even when setMany throws - so the CLI receives a success signal despite the disk not being updated.
  • Error handling: Both new catch blocks log via logger.error but do not call captureException, diverging from the project's Sentry guidelines.

Confidence Score: 3/5

The rename handler sends a success response unconditionally even when the disk flush fails, meaning the exact race the PR is fixing can still occur on any setMany error path.

The rename handler's unconditional sendRemoteRenameSessionResponse(responseChannel, true) call undermines the core guarantee: if setMany throws, the CLI receives a success signal but the disk still has the old name, leaving a follow-up show agent reading stale data - the identical symptom the PR set out to eliminate. The delete handler and the overall approach are sound; it is specifically the rename error path that breaks the contract.

src/renderer/hooks/remote/useAppRemoteEventListeners.ts - the rename handler's post-flush response logic needs a second look.

Important Files Changed

Filename Overview
src/renderer/hooks/remote/useAppRemoteEventListeners.ts Adds synchronous disk flush (setMany) to the remoteDeleteSession and remoteRenameSession IPC handlers to close a 2s race window. The delete flush is correct, but the rename handler sends a success response unconditionally even when setMany throws, defeating the PR's guarantee. Both catch blocks also silently suppress errors instead of forwarding them to Sentry.

Sequence Diagram

sequenceDiagram
    participant CLI
    participant WebServer as Web Server Factory
    participant Renderer as Renderer (IPC handler)
    participant ReactState as React State (setSessions)
    participant Disk as Disk (setMany)

    Note over CLI,Disk: maestro:remoteRenameSession (after this PR)
    CLI->>WebServer: rename session
    WebServer->>Renderer: dispatch CustomEvent
    Renderer->>ReactState: setSessions (schedules update)
    Renderer->>Disk: "await setMany([{...session, name}], [])"
    alt setMany succeeds
        Disk-->>Renderer: ok
        Renderer->>CLI: sendRemoteRenameSessionResponse(true)
    else setMany throws
        Disk-->>Renderer: error
        Renderer->>Renderer: logger.error (swallowed)
        Renderer->>CLI: sendRemoteRenameSessionResponse(true) - bug: still signals success
    end

    Note over CLI,Disk: maestro:remoteDeleteSession (after this PR)
    CLI->>WebServer: remove-agent (fire-and-forget)
    WebServer->>Renderer: dispatch CustomEvent
    Renderer->>ReactState: setSessions (schedules removal)
    Renderer->>Disk: await setMany([], [sessionId])
    Disk-->>Renderer: ok / error (logged, no response channel)
Loading

Reviews (1): Last reviewed commit: "fix(cli): persist remove-agent and sessi..." | Re-trigger Greptile

Comment on lines +1145 to 1151
try {
await window.maestro.sessions.setMany([{ ...session, name: newName } as any], []);
} catch (persistErr) {
logger.error('[Remote] Failed to persist session rename:', undefined, persistErr);
}

window.maestro.process.sendRemoteRenameSessionResponse(responseChannel, 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.

P1 Rename signals success even when disk flush fails

The sendRemoteRenameSessionResponse(responseChannel, true) call is unconditional - it fires after the catch block regardless of whether setMany succeeded or threw. This defeats the PR's central guarantee: a CLI consumer that calls rename followed immediately by show agent will still see the stale name if setMany throws, because the success signal arrived but the disk was never updated. The fix should either send false on persistence failure or re-throw and let the caller handle it.

Comment on lines +1072 to +1076
try {
await window.maestro.sessions.setMany([], [sessionId]);
} catch (persistErr) {
logger.error('[Remote] Failed to persist session removal:', undefined, persistErr);
}
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 Per the project's Sentry guidelines in CLAUDE.md, unexpected errors should not be silently swallowed - they should reach Sentry. Here setMany failures are logged via logger.error but are never forwarded to captureException, so any production disk-write failures will be invisible in the error-tracking dashboard. If persistence failure is considered recoverable, the catch block should at minimum call captureException(persistErr) alongside the log.

Suggested change
try {
await window.maestro.sessions.setMany([], [sessionId]);
} catch (persistErr) {
logger.error('[Remote] Failed to persist session removal:', undefined, persistErr);
}
try {
await window.maestro.sessions.setMany([], [sessionId]);
} catch (persistErr) {
logger.error('[Remote] Failed to persist session removal:', undefined, persistErr);
captureException(persistErr, { extra: { sessionId, operation: 'remoteDeleteSession' } });
}

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/renderer/hooks/remote/useAppRemoteEventListeners.ts (1)

1072-1076: ⚡ Quick win

Add captureException for persistence failures to align with codebase patterns.

Both persistence error handlers log but don't report to Sentry. Other error paths in this file consistently use captureException (e.g., lines 286, 525, 749). Per coding guidelines, unexpected errors should be captured.

Proposed fix
 	// Flush the removal to disk synchronously...
 	try {
 		await window.maestro.sessions.setMany([], [sessionId]);
 	} catch (persistErr) {
+		captureException(persistErr, { extra: { event: 'maestro:remoteDeleteSession', sessionId } });
 		logger.error('[Remote] Failed to persist session removal:', undefined, persistErr);
 	}
 	// Flush the rename to disk before signaling success...
 	try {
 		await window.maestro.sessions.setMany([{ ...session, name: newName } as any], []);
 	} catch (persistErr) {
+		captureException(persistErr, { extra: { event: 'maestro:remoteRenameSession', sessionId, newName } });
 		logger.error('[Remote] Failed to persist session rename:', undefined, persistErr);
 	}

As per coding guidelines: "Use Sentry utilities (captureException, captureMessage) from src/utils/sentry.ts for explicit error reporting with context."

Also applies to: 1145-1149

🤖 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 `@src/renderer/hooks/remote/useAppRemoteEventListeners.ts` around lines 1072 -
1076, The persistence error handlers call logger.error but don't report to
Sentry — import and use captureException from the Sentry utilities and call it
with the caught error and a short context message; specifically, in the catch
blocks surrounding window.maestro.sessions.setMany (the persistErr variable)
replace or augment logger.error('[Remote] Failed to persist session removal:',
undefined, persistErr) with a captureException(persistErr, { level: 'error',
extra: { action: 'persistSessionRemoval', sessionId } }) (and do the same for
the other identical block around the second setMany call) so the errors are
recorded in Sentry along with contextual info.
🤖 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.

Nitpick comments:
In `@src/renderer/hooks/remote/useAppRemoteEventListeners.ts`:
- Around line 1072-1076: The persistence error handlers call logger.error but
don't report to Sentry — import and use captureException from the Sentry
utilities and call it with the caught error and a short context message;
specifically, in the catch blocks surrounding window.maestro.sessions.setMany
(the persistErr variable) replace or augment logger.error('[Remote] Failed to
persist session removal:', undefined, persistErr) with a
captureException(persistErr, { level: 'error', extra: { action:
'persistSessionRemoval', sessionId } }) (and do the same for the other identical
block around the second setMany call) so the errors are recorded in Sentry along
with contextual info.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a2140e4e-1902-4b21-95ee-d9cc179b56a4

📥 Commits

Reviewing files that changed from the base of the PR and between ebe65a1 and 16b5b9d.

📒 Files selected for processing (1)
  • src/renderer/hooks/remote/useAppRemoteEventListeners.ts

@pedramamini pedramamini merged commit cadd4cc into rc May 30, 2026
5 checks passed
@pedramamini pedramamini deleted the fix/1054-cli-remove-rename-persist branch May 30, 2026 01:49
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