Skip to content

fix: group chat process cleanup, Codex cost tracking, moderator @mention enforcement#437

Open
openasocket wants to merge 24 commits intoRunMaestro:mainfrom
openasocket:feat/group-chat-enhancement
Open

fix: group chat process cleanup, Codex cost tracking, moderator @mention enforcement#437
openasocket wants to merge 24 commits intoRunMaestro:mainfrom
openasocket:feat/group-chat-enhancement

Conversation

@openasocket
Copy link
Contributor

@openasocket openasocket commented Feb 22, 2026

Summary

  • Process cleanup on delete: killModerator() and clearAllParticipantSessions() silently failed because batch mode spawns processes with timestamp-suffixed session IDs while the active sessions map stores prefix-only IDs. Added killByPrefix() to ProcessManager — fixes zombie processes and the observed y\n flood after delete.
  • Codex session ID + cost display: Restored display-only synthetic session IDs (codex-0-*) so participant cards show an ID instead of "pending". Resume logic skips these prefixed IDs. Added OpenAI model pricing table (GPT-4o through GPT-5.3-codex) to calculate cost from token counts since Codex CLI doesn't report cost directly.
  • Moderator @mention enforcement: Added mandatory ## MANDATORY: Always @mention agents when delegating section to moderator system prompt. Prevents the moderator from discussing agent tasks without actually routing messages via @mentions. Updated synthesis prompt for critical analysis and fusion instead of simple concatenation.

Test plan

  • All 628 tests pass across 21 affected test files
  • Create group chat, add agents, delete — verify processes are actually killed
  • Verify Codex participant shows session ID (not "pending") and cost accumulates
  • Verify moderator consistently @mentions agents when delegating tasks

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ability to add fresh participants to group chats with new participant modal.
    • Enhanced @mention autocomplete to show available agent types alongside existing sessions.
    • Improved group chat moderator synthesis workflow with clearer delegation and fusion guidance.
  • Bug Fixes

    • Fixed session recovery handling during participant exit.
    • Improved error messages for moderator, participant, and agent unavailability scenarios.

openasocket and others added 24 commits February 21, 2026 14:22
Enable group chat to spawn new agent instances by type when no matching
session exists in the sidebar. When a user or moderator @mentions an
agent name that doesn't match an existing session, the system now falls
back to matching against AGENT_DEFINITIONS and spawns a fresh instance
with default configuration.

- Add addFreshParticipant() to group-chat-agent.ts (clean spawn, no
  session overrides)
- Add agent type fallback in routeUserMessage() and
  routeModeratorResponse() in group-chat-router.ts
- Add groupChat:addFreshParticipant IPC handler and preload exposure
- Update test to include new IPC handler channel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add "+" button in participants panel header and AddParticipantModal
with two modes: "Use existing session" (inherits config) and "Create
fresh agent" (clean defaults). Wires modal through modalStore, App.tsx,
AppModals, and useGroupChatHandlers. Adds missing addFreshParticipant
type to global.d.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…arsing

Add skipBatchForReadOnly guard in buildAgentArgs to prevent
--dangerously-bypass-approvals-and-sandbox from conflicting with
--sandbox read-only when Codex runs as a read-only moderator.

Handle item.completed type:"error" events in Codex output parser
(e.g., account warnings, sandbox violations) instead of silently
swallowing them as system events.

Replace OpenCode-specific error patterns in Codex with proper
OpenAI patterns: model not found, model access denied, stream
disconnection, and thread not found.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rticipant categories

Add agents:getAvailable IPC endpoint that returns installed, non-hidden
agent types. Enhance GroupChatInput mention dropdown to show three
categories: existing participants (disabled), available sessions, and
agent types that can be spawned fresh (with "New" badge). Section headers
appear when multiple categories are present. Keyboard navigation skips
disabled participant items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added 6 test cases in a new describe('addFreshParticipant') block:
- spawns fresh agent with default config (no session overrides)
- generates unique session ID with group-chat- prefix
- rejects duplicate participant names
- requires moderator to be active
- throws if agent is not available
- uses os.homedir() as default cwd

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…outer

Add 6 tests in describe('agent type fallback') covering: session-first
resolution (backward compat), agent type fallback when no session exists,
fresh participant default cwd (os.homedir()), ignoring unavailable agents,
both ID and name mention matching via mentionMatches(), and moderator
response fallback. Mocked getVisibleAgentDefinitions via vi.mock with
importActual passthrough.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 13 tests covering rendering, session filtering (terminal
exclusion, already-added exclusion), fresh agent selection,
existing session selection, error handling, and modal close.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Input

Add 5 tests covering agent type items with "New" badge, participant
disabled state, session/agent-type deduplication, and dropdown filtering
for the enhanced @mention autocomplete feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updated 7 generic error messages across group-chat-agent.ts and
group-chat-router.ts to provide actionable guidance: agent unavailable
now suggests installing or using a different agent, spawn failures
suggest checking configuration, and moderator-not-active suggests
restarting the moderator. Updated corresponding test assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…oup chat

Add validation module (validation.ts) with UUID, participant name, message
content, base64 image, custom args, and env vars validators. Apply validation
at the top of every group chat IPC handler. Add defense-in-depth path
containment check to getGroupChatDir(). 45 new unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent state corruption from concurrent operations:
- Chat-level locks prevent delete/update during message processing
- Synthesis guards prevent double-trigger from duplicate exit handlers
- Pending participants merge instead of overwrite across rounds
- Spawn failure resets participant UI state to idle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…roup chat

- Add explicit null guards for processManager/agentDetector in groupChat IPC
  handlers (sendToModerator, addParticipant, sendToParticipant, addFreshParticipant)
  replacing silent ?? undefined coalescing with thrown errors
- Add killAllModerators() to group-chat-moderator.ts that kills all active
  moderator processes and clears session tracking maps
- Wire group chat cleanup (clearAllParticipantSessionsGlobal + killAllModerators)
  into the quit handler's performCleanup() to prevent zombie processes on app exit
- Add SSH wrapping to spawnModeratorSynthesis() matching the pattern used in
  routeUserMessage() so synthesis runs remotely on SSH-configured moderators
- Add tests for killAllModerators and quit handler group chat cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…g in group chat

Wrap untrusted content (chat history, user messages, agent responses) in XML-style
boundary tags to prevent prompt injection attacks in moderator and participant prompts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… remove debug logging

- Wrap deleteGroupChatHistoryEntry() and clearGroupChatHistory() in
  enqueueWrite() to prevent read-modify-write race conditions on
  concurrent history operations
- Add missing fields to resetGroupChatState() (moderatorUsage,
  groupChatExecutionQueue, groupChatStagedImages, groupChatReadOnlyMode)
  to prevent stale state when switching chats
- Remove ~100 console.log('[GroupChat:Debug]') statements from
  group-chat-router.ts, replacing key state transitions with
  logger.debug() and converting console.error/warn to logger equivalents
- Remove 11 debug console.log/warn statements from groupChat.ts IPC
  handler, replace emitParticipantState warning with logger.warn()
- Add TODO comment for unimplemented image embedding in sendToModerator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nt buffer loss

When Codex uses tool calls during a moderator turn, the first agent_message
sets resultEmitted=true after flushing. If a second agent_message arrives
(the final one with @mentions), the subsequent usage event skips the flush
because resultEmitted is already true, leaving stale reasoning text in the
buffer. Reset the flag on each new agent_message so the latest text is
always flushed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ew agent_message

Covers the scenario where Codex tool calls cause an interim buffer flush,
then a second agent_message with @mentions arrives. Verifies that
resultEmitted is reset so the subsequent usage event flushes the updated
text (preventing stale reasoning from being routed instead of the real response).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ngs)

Moderator and participant processes now resume prior agent sessions
instead of rebuilding full prompts each turn. On resume, only the new
message/delegation is sent (~200-400 tokens vs ~2,200 tokens full
prompt). Falls back to full prompt when no stored session ID or agent
lacks resume support. Exit listener detects session_not_found errors
and clears stored session ID for automatic fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y sandbox bypass, improve moderator @mention rules

- Add new wrapped format support for Codex CLI v0.103.0+ (task_started, agent_reasoning,
  agent_message, token_count, tool_call, tool_result events)
- Fix agent-args readOnlyMode to only strip sandbox-bypass flags, preserving --skip-git
- Enhance moderator system prompt with explicit @mention routing rules and agent addition flow
- Add conductorProfile callback for group chat conductor customization
- Add groupChatLock mock to exit-listener tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-json output

Adds 8 new tests to StdoutHandler.test.ts verifying the outputParser
path for session-id emission, which was previously untested (all mocks
returned null). Includes 3 integration tests using real ClaudeOutputParser.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…resume

Codex wrapped format (v0.103.0+) `task_started` events were generating
synthetic `codex-0-<timestamp>` session IDs. These IDs can't be used with
`codex exec resume` — instead of erroring, Codex silently starts a new
session with no prior context (verified manually on v0.103.0).

Real session IDs come from `thread.started` events (legacy format), which
Codex v0.103.0 uses by default. Removing synthetic ID generation ensures:
- Legacy format: real thread_id UUIDs are captured and resume works
- Wrapped format: no session ID stored, safe fallback to full prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… participant)

Adds 4 tests verifying that Claude Code and Codex agents resume
independently in group chat with their distinct resume arg formats:
- Claude: --resume <session_id>
- Codex: resume <session_id> (no -- prefix)

Tests cover: independent resume args, batch mode prefixes, synthesis
round isolation, and full-prompt-to-resume lifecycle transitions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…on resume

Automates the previously manual "app restart" test checklist item with 4 new
tests verifying session IDs survive disk persistence and resume works after
simulated restart: single-agent moderator+participant resume, synthesis resume,
listGroupChats enumeration, and mixed Claude+Codex restart flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4 new tests verify resume args (`--resume <sessionId>`) pass through
the SSH wrapper correctly for moderator routing, participant delegation,
synthesis spawning, and the negative case (first turn without resume).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…isplay, moderator @mention enforcement

Three fixes for group chat reliability:

1. Process cleanup on delete: killModerator() and clearAllParticipantSessions()
   used exact session ID matching but batch mode spawns processes with timestamp
   suffixes. Added killByPrefix() to ProcessManager so delete kills all matching
   processes instead of silently failing (root cause of zombie processes and y-flood).

2. Codex session ID and cost: Restored display-only synthetic session IDs
   (codex-0-*) so UI shows an ID instead of "pending". Resume logic skips these
   prefixed IDs. Added OpenAI model pricing table to calculate cost from token
   counts since Codex CLI doesn't report cost directly.

3. Moderator prompt: Added mandatory @mention enforcement section and delegation
   strategy to prevent moderator from discussing agent tasks without actually
   routing messages. Updated synthesis prompt for critical analysis and fusion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Introduces a comprehensive group chat participant management system with fresh participant addition, operation locking, input validation, process batch termination, and moderator synthesis enhancements. Spans new modules (locking, validation), expanded IPC handlers, process manager capabilities, UI components for participant addition, and updated output parsing to support new Codex format versions.

Changes

Cohort / File(s) Summary
Group Chat Locking & Synchronization
src/main/group-chat/group-chat-lock.ts, src/__tests__/main/group-chat/group-chat-lock.test.ts
Introduces concurrency guards with acquireChatLock/releaseChatLock for operation locking, isSynthesisInProgress/markSynthesisStarted/clearSynthesisInProgress for synthesis state tracking, and auto-release of stale locks (5-minute timeout). Comprehensive test coverage validates locking behavior, state transitions, and integration scenarios.
Fresh Participant Addition
src/main/group-chat/group-chat-agent.ts, src/__tests__/main/group-chat/group-chat-agent.test.ts
Adds public addFreshParticipant() function for creating fresh agent instances without session overrides. Updates error messages for moderator/agent availability. Introduces clearAllParticipantSessions() batch cleanup using killByPrefix(). Tests verify unique session ID generation, spawn invocation, and error handling.
Input Validation Module
src/main/group-chat/validation.ts, src/__tests__/main/group-chat/validation.test.ts
New validation module centralizing IPC input checks: validateGroupChatId(), validateParticipantName(), validateMessageContent(), validateBase64Image(), validateCustomArgs(), sanitizeCustomEnvVars(). Covers type checks, length constraints, unsafe characters, shell metacharacters, and environment variable name restrictions.
Moderator Management & Process Cleanup
src/main/group-chat/group-chat-moderator.ts, src/__tests__/main/group-chat/group-chat-moderator.test.ts
Extends IProcessManager with killByPrefix(prefix: string) method. Introduces killAllModerators() for batch termination of all moderator sessions with session mapping cleanup. Tests verify single-session and multi-session termination, clearing of session state, and graceful handling of empty sessions.
IPC Handlers & Preload APIs
src/main/ipc/handlers/agents.ts, src/main/ipc/handlers/groupChat.ts, src/main/preload/agents.ts, src/main/preload/groupChat.ts, src/__tests__/main/ipc/handlers/agents.test.ts, src/__tests__/main/ipc/handlers/groupChat.test.ts
Adds agents:getAvailable handler returning lightweight agent list. Expands groupChat handler with groupChat:addFreshParticipant. Both backed by corresponding preload APIs (agents.getAvailable, groupChat.addFreshParticipant). Handlers integrate validation and process manager wiring.
Process Manager & Lifecycle Integration
src/main/process-manager/ProcessManager.ts, src/main/app-lifecycle/quit-handler.ts, src/__tests__/main/app-lifecycle/quit-handler.test.ts
Adds killByPrefix() method to ProcessManager for batch process termination. QuitHandler integrates clearAllParticipantSessionsGlobal() and killAllModerators() dependencies for group chat cleanup on application exit, with call order enforcement and optional dependency handling.
Exit Listener & Session Recovery
src/main/process-listeners/exit-listener.ts, src/main/process-listeners/types.ts, src/__tests__/main/process-listeners/exit-listener.test.ts
Integrates groupChatLock dependency for lock/synthesis cleanup. Enhances moderator exit handling with session recovery checks, lock release, and synthesis progress clearing. Participant exit paths now handle recovery scenarios and proper cleanup. Adds getPendingParticipants() to ProcessListenerDependencies.
Router, Locking & State Management
src/main/group-chat/group-chat-router.ts, src/__tests__/main/group-chat/group-chat-router.test.ts
Integrates locking (acquireChatLock/releaseChatLock) and synthesis guards at operation entry points. Adds conductor profile injection via setGetConductorProfileCallback(). Implements fresh participant fallback via addFreshParticipant() when direct session match unavailable. Expands moderator/synthesis prompts with boundary-wrapped content, available agent types, and resume mode support. Extensive test coverage validates agent fallback, state persistence, resume flows, and SSH wrapping.
Group Chat Storage
src/main/group-chat/group-chat-storage.ts
Adds path traversal prevention in getGroupChatDir(). Refactors deleteGroupChatHistoryEntry() and clearGroupChatHistory() to execute within per-chat write queues, maintaining async behavior while delegating I/O inside queued callbacks.
Participant Addition UI Components
src/renderer/components/AddParticipantModal.tsx, src/renderer/components/GroupChatParticipants.tsx, src/renderer/components/GroupChatRightPanel.tsx, src/__tests__/renderer/components/AddParticipantModal.test.tsx
New AddParticipantModal component with fresh agent and existing session modes, agent detection, session filtering (excludes terminals and already-added), and mode-specific submission handlers. GroupChatParticipants and GroupChatRightPanel add onAddParticipant callback for modal triggering. Comprehensive test coverage validates filtering, state transitions, error handling, and UI flows.
Modal & Input Enhancement
src/renderer/components/GroupChatInput.tsx, src/renderer/components/AppModals.tsx, src/renderer/stores/modalStore.ts, src/renderer/constants/modalPriorities.ts, src/__tests__/renderer/components/GroupChatInput.test.tsx
Introduces categorized @mention autocomplete (sessions, agent-types, participants) with IPC-fetched agent availability, duplicate detection, and keyboard navigation for selectable items. AppModals wires addGroupChatParticipant modal with participant flow handlers. ModalStore adds addGroupChatParticipant entry with open/close actions. Modal priority set to 635 (ADD_GROUP_CHAT_PARTICIPANT).
Handler Integration & Setup
src/renderer/hooks/groupChat/useGroupChatHandlers.ts, src/renderer/App.tsx, src/__tests__/setup.ts, src/main/index.ts
Hook adds four new handlers: handleOpenAddParticipantModal(), handleCloseAddParticipantModal(), handleAddExistingParticipant(), handleAddFreshParticipant() with error toasts and modal state management. App.tsx wires modal and handler props into AppModals and GroupChat panels. Setup.ts adds agents.getAvailable mock. Main index.ts integrates all new group chat lifecycle dependencies (locks, callbacks, cleanup).
Output Parsing & Format Support
src/main/parsers/codex-output-parser.ts, src/main/parsers/error-patterns.ts, src/__tests__/main/parsers/codex-output-parser.test.ts, src/__tests__/main/process-manager/handlers/StdoutHandler.test.ts, src/main/process-manager/handlers/StdoutHandler.ts
Expands Codex parser to support wrapped message format (v0.103.0+) with config echoes, task_started, agent_reasoning, agent_message, token_count, tool_call/result handling. Introduces model pricing calculation (ModelPricing, getModelPricing, calculateCost). Refines error detection and usage extraction. StdoutHandler adds result emission control via resultEmitted flag reset on new agent_message, removes debug logs.
Configuration & Prompts
src/prompts/group-chat-moderator-system.md, src/prompts/group-chat-moderator-synthesis.md, src/prompts/group-chat-participant-request.md, src/renderer/global.d.ts, src/renderer/stores/groupChatStore.ts
Moderator system prompt expands delegation strategy, synthesis workflow, @mention rules, and agent-adding guidance. Synthesis prompt shifts to content-boundary framing, structured fusion process, and quality criteria. Participant prompt adds content boundary documentation. Global.d.ts adds agents.getAvailable and groupChat.addFreshParticipant types. GroupChatStore adds moderatorUsage, groupChatExecutionQueue, groupChatStagedImages, groupChatReadOnlyMode fields.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely summarizes three main changes: group chat process cleanup, Codex cost tracking, and moderator @mention enforcement. It directly reflects the primary objectives stated in the PR description.
Docstring Coverage ✅ Passed Docstring coverage is 84.75% 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 (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@greptile-apps
Copy link

greptile-apps bot commented Feb 22, 2026

Greptile Summary

This PR fixes three critical group chat issues:

Process cleanup on delete

  • Added killByPrefix() to ProcessManager to kill processes matching a session ID prefix
  • Fixed killModerator() and clearAllParticipantSessions() to use prefix-based killing
  • Root cause: Batch mode spawns processes with timestamp suffixes (e.g., group-chat-{id}-moderator-1771743188276) but the active sessions map stores only the prefix (group-chat-{id}-moderator), causing exact-match kills to silently fail
  • Result: Eliminates zombie processes and the observed y\n flood after chat deletion

Codex session ID and cost tracking

  • Restored display-only synthetic session IDs (codex-0-{timestamp}) so participant cards show an ID instead of "pending"
  • Resume logic skips these prefixed IDs to avoid silent session creation bugs
  • Added comprehensive OpenAI model pricing table (GPT-4o through GPT-5.3-codex variants) to calculate cost from token counts
  • Codex CLI doesn't report cost directly, so cost is computed from input_tokens, output_tokens, cached_input_tokens, and model-specific pricing
  • Added support for new wrapped message format (v0.103.0+): { id: 'N', msg: { type, ... } }

Moderator @mention enforcement

  • Added prominent "MANDATORY: Always @mention agents when delegating" section to moderator system prompt
  • Updated synthesis prompt to emphasize critical analysis and fusion over simple concatenation
  • Prevents moderator from discussing agent tasks without actually routing messages via @mentions

Additional improvements

  • Added chat-level operation locks to prevent concurrent delete/update during message processing
  • Comprehensive input validation for all IPC handlers (UUID format, shell injection protection, size limits)
  • Agent type fallback: Users can now @mention agent types (e.g., @Codex) even when no sidebar session exists
  • New AddParticipantModal component for choosing between existing sessions vs. fresh agent instances
  • Removed debug console.log statements throughout group chat code
  • Added 628 passing tests across 21 test files

Confidence Score: 5/5

  • Safe to merge - well-tested changes with comprehensive test coverage and clear improvements to reliability
  • All 628 tests pass, changes are well-isolated with proper error handling, fixes real production bugs (zombie processes, missing session IDs), adds important security validation, and includes extensive test coverage for new functionality
  • No files require special attention - all changes are well-implemented and thoroughly tested

Important Files Changed

Filename Overview
src/main/process-manager/ProcessManager.ts Added killByPrefix() method to kill processes by session ID prefix - clean implementation, well-documented, fixes zombie processes in group chat batch mode
src/main/parsers/codex-output-parser.ts Added model pricing table, cost calculation logic, synthetic session IDs for display, and support for new wrapped format (v0.103.0+) - comprehensive implementation with good test coverage
src/prompts/group-chat-moderator-system.md Added mandatory @mention enforcement section at top of prompt, improved synthesis instructions to focus on critical analysis and fusion instead of simple concatenation
src/main/group-chat/group-chat-lock.ts New file implementing chat-level operation locks to prevent concurrent delete/update during processing - includes stale lock timeout (5min) for auto-recovery
src/main/group-chat/validation.ts New file with comprehensive input validation for IPC boundary - validates UUIDs, participant names, message content, images, and custom args/env vars against injection attacks
src/main/ipc/handlers/groupChat.ts Added validation calls to all IPC handlers, lock checking before destructive operations, addFreshParticipant handler, improved error messages
src/main/group-chat/group-chat-router.ts Major refactor adding agent type fallback, chat locks, synthesis guards, removed debug console.log statements - complex changes but well-tested

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User sends message to group chat] --> B{Acquire chat lock}
    B -->|Lock failed| C[Error: Chat busy]
    B -->|Lock acquired| D[Validate input]
    D --> E{Auto-add @mentioned agents?}
    E -->|Session exists| F[Add from existing session]
    E -->|Agent type only| G[Add fresh agent instance]
    E -->|No match| H[Continue]
    F --> H
    G --> H
    H --> I[Spawn moderator batch process]
    I --> J[Moderator processes message]
    J --> K{Contains @mentions?}
    K -->|Yes| L[Route to participants]
    K -->|No| M[Direct response to user]
    L --> N[Spawn participant batch processes]
    N --> O{All responses received?}
    O -->|No| P[Wait for responses]
    O -->|Yes| Q{Synthesis guard check}
    Q -->|Already running| R[Skip duplicate synthesis]
    Q -->|Not running| S[Mark synthesis started]
    S --> T[Spawn synthesis process]
    T --> U[Moderator fuses responses]
    U --> V[Clear synthesis guard]
    V --> M
    M --> W[Release chat lock]
    
    X[Delete group chat] --> Y{Check lock}
    Y -->|Locked| Z[Error: Chat busy]
    Y -->|Unlocked| AA[Kill moderator by prefix]
    AA --> AB[Kill all participants by prefix]
    AB --> AC[Delete chat data]
    
    style B fill:#fff3cd
    style Q fill:#fff3cd
    style AA fill:#d1ecf1
    style AB fill:#d1ecf1
Loading

Last reviewed commit: f7b1265

Copy link

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
src/__tests__/main/ipc/handlers/groupChat.test.ts (1)

100-132: ⚠️ Potential issue | 🟠 Major

Type the mock to include killByPrefix.

The object literal includes killByPrefix, but the declared type for mockProcessManager doesn’t, which will fail TypeScript’s excess property checks.

✅ Proposed fix
 	let mockProcessManager: {
 		spawn: ReturnType<typeof vi.fn>;
 		write: ReturnType<typeof vi.fn>;
 		kill: ReturnType<typeof vi.fn>;
+		killByPrefix: ReturnType<typeof vi.fn>;
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/ipc/handlers/groupChat.test.ts` around lines 100 - 132,
The mockProcessManager declared type is missing the killByPrefix property while
the object literal sets it; update the mockProcessManager type to include
killByPrefix (e.g., add killByPrefix: ReturnType<typeof vi.fn>) so the
declaration matches the object created in beforeEach and TypeScript no longer
errors on excess/missing properties for mockProcessManager.
🧹 Nitpick comments (13)
src/renderer/global.d.ts (1)

1776-1786: Parameter order differs from addParticipant.

addParticipant uses (id, name, agentId, cwd) (lines 1765-1770) while addFreshParticipant uses (id, agentId, name, cwd). This inconsistency could cause confusion for callers. If intentional (perhaps to emphasize that agentId is more central for fresh spawns), consider documenting this difference. Otherwise, aligning the parameter order would improve API consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/global.d.ts` around lines 1776 - 1786, The two APIs have
inconsistent parameter order: addParticipant(id, name, agentId, cwd) vs
addFreshParticipant(id, agentId, name, cwd); change addFreshParticipant’s
declaration and any implementations/callsites to use the same order as
addParticipant (id, name, agentId, cwd) to keep the API consistent, and update
any documentation/tests that rely on the old order; if the original order was
intentional, instead add a comment/docstring to addFreshParticipant clarifying
the different ordering.
src/main/preload/groupChat.ts (1)

115-116: Consider consistency: addFreshParticipant argument order differs from addParticipant.

The preload method addParticipant uses (id, name, agentId, cwd) while addFreshParticipant uses (id, agentId, name, cwd). While the handler correctly receives and reorders these arguments before invoking the actual function, the inconsistent parameter ordering in the preload API is confusing for developers. Consider aligning both methods to use the same argument order for better API consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/preload/groupChat.ts` around lines 115 - 116, The preload API is
inconsistent: addParticipant(id, name, agentId, cwd) vs addFreshParticipant(id,
agentId, name, cwd); change addFreshParticipant to the same parameter order as
addParticipant (id, name, agentId, cwd), update its ipcRenderer.invoke call to
pass arguments in that order, and update the corresponding IPC handler for
'groupChat:addFreshParticipant' (or the code that reorders its args before
calling the underlying function) so it expects and forwards (id, name, agentId,
cwd) consistently with addParticipant.
src/main/ipc/handlers/groupChat.ts (2)

770-771: Incorrect validator used for entryId.

validateGroupChatId(entryId) validates that entryId is a UUID, which is correct if entry IDs are UUIDs. However, the function name is misleading since it's being used for a history entry ID, not a group chat ID. Consider creating a separate validateEntryId function or a more generic validateUUID function for clarity.

Proposed refactor to use a generic UUID validator

In src/main/group-chat/validation.ts, add:

+/**
+ * Validate that a value is a valid UUID v4.
+ */
+export function validateUUID(value: unknown, fieldName = 'value'): string {
+	if (typeof value !== 'string' || !UUID_V4_REGEX.test(value)) {
+		throw new Error(`Invalid ${fieldName}: must be a valid UUID`);
+	}
+	return value;
+}

Then in the handler:

-			validateGroupChatId(entryId);
+			validateUUID(entryId, 'entry ID');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/groupChat.ts` around lines 770 - 771, The code
incorrectly reuses validateGroupChatId for the history entry ID; add a clear
UUID validator (e.g., validateEntryId or validateUUID) in the group-chat
validation module and replace the validateGroupChatId(entryId) call with the new
validator; update any imports/usages in the handler (groupChatId =
validateGroupChatId(groupChatId); validateEntryId(entryId) or
validateUUID(entryId)) so intent is clear and validators are not misleading.

440-441: Acknowledged TODO for image embedding.

The TODO comment about image embedding in moderator prompts is clear. Consider tracking this in an issue if not already done.

Would you like me to help create an issue to track the image embedding feature for moderator prompts?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/groupChat.ts` around lines 440 - 441, Create a tracked
issue for implementing image embedding in moderator prompts: describe the
current state (images saved separately via the groupChat:saveImage handler and a
TODO in the async moderator prompt handler with signature async (id: string,
message: string, images?: string[], readOnly?: boolean)), list acceptance
criteria (embed images inline in moderator prompt payloads, preserve image
ordering and alt text, fallback to separate storage when embedding fails),
include required API changes (modify the moderator prompt handler to
accept/serialize embedded image data or URLs), add tests and UI/UX notes, and
tag with a feature/enhancement label and assign or link to the relevant
maintainer for prioritization.
src/main/group-chat/group-chat-lock.ts (1)

75-82: Consider consolidating forceReleaseChatLock with releaseChatLock.

Both functions have identical implementations (calling chatLocks.delete(chatId)). The semantic distinction ("unconditional" vs regular release) doesn't manifest in the code. Consider whether one function with clear documentation would suffice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/group-chat-lock.ts` around lines 75 - 82, Both
forceReleaseChatLock and releaseChatLock simply call chatLocks.delete(chatId),
so consolidate by removing one of them (preferably keep releaseChatLock for the
simpler name) and update all references to the removed symbol to call the
retained function; adjust JSDoc on the retained function to document
unconditional behavior if needed and run/update any callers/tests that
referenced forceReleaseChatLock to use releaseChatLock instead.
src/main/group-chat/validation.ts (2)

95-109: Consider validating base64 format, not just size.

The function validates type and size but doesn't verify the string is valid base64. Invalid base64 will fail later during decode. A regex check could catch malformed input earlier.

🛡️ Add base64 format validation
+const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/;
+
 export function validateBase64Image(data: unknown, maxSizeBytes = 20 * 1024 * 1024): string {
 	if (typeof data !== 'string') {
 		throw new Error('Invalid image data: must be a string');
 	}
+
+	// Strip data URL prefix if present (e.g., "data:image/png;base64,")
+	const base64Data = data.includes(',') ? data.split(',')[1] : data;
+
+	if (!BASE64_REGEX.test(base64Data)) {
+		throw new Error('Invalid image data: not valid base64');
+	}

 	// Approximate decoded size: base64 uses ~4/3 ratio
-	const estimatedSize = data.length * 0.75;
+	const estimatedSize = base64Data.length * 0.75;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/validation.ts` around lines 95 - 109, The
validateBase64Image function currently only checks type and estimated size but
doesn't confirm the string is valid base64; add a format validation step in
validateBase64Image that verifies the input matches a base64 pattern (optionally
allowing data URI prefix removal) and rejects malformed strings before size
checking, or attempt a safe decode (e.g., try Buffer.from(..., 'base64') and
ensure re-encoding matches) to catch invalid base64 and throw a clear error
message when validation fails.

10-10: UUID validation regex allows UUID v1-5, not just v4.

The UUID_V4_REGEX pattern matches any valid UUID format (v1-v5) since it doesn't validate the version nibble. While this works, the name is misleading.

🧹 Either rename to UUID_REGEX or add version validation
-const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+// Matches standard UUID format (any version)
+const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

Or for strict v4 validation:

-const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+// Strict UUID v4: version nibble = 4, variant nibble = 8/9/a/b
+const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/validation.ts` at line 10, The constant UUID_V4_REGEX
currently matches any UUID version because it doesn't validate the version
nibble; update the code so the name and behavior align: either rename
UUID_V4_REGEX to UUID_REGEX (and update any references) if you intend to accept
all UUID versions, or change the regex to enforce v4 specifically by requiring
the version character to be "4" (e.g., the 13th hex digit) and adjust the
constant name to remain UUID_V4_REGEX; locate and edit the UUID_V4_REGEX
declaration in validation.ts and update usages accordingly.
src/renderer/components/AddParticipantModal.tsx (2)

108-129: Minor: Early returns don't reset isSubmitting state.

The early returns on lines 114, 116, and 119 don't reset isSubmitting to false. While canSubmit should prevent reaching these paths, if logic changes, the button could remain permanently disabled.

🧹 Add isSubmitting reset to early returns
 		try {
 			if (mode === 'existing') {
-				if (!selectedSessionId) return;
+				if (!selectedSessionId) {
+					setIsSubmitting(false);
+					return;
+				}
 				const session = sessions.find((s) => s.id === selectedSessionId);
-				if (!session) return;
+				if (!session) {
+					setIsSubmitting(false);
+					return;
+				}
 				onAddExisting(session.id, session.name, session.toolType, session.cwd);
 			} else {
-				if (!selectedAgentId) return;
+				if (!selectedAgentId) {
+					setIsSubmitting(false);
+					return;
+				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AddParticipantModal.tsx` around lines 108 - 129, In
handleSubmit ensure isSubmitting is reset on the early-return paths: before each
early return in the 'existing' branch when selectedSessionId is falsy or session
not found, and before the early return in the 'fresh' branch when
selectedAgentId is falsy, call setIsSubmitting(false) (and optionally setError
with a helpful message) so the component doesn't remain stuck in submitting
state; update the closure that defines handleSubmit to call
setIsSubmitting(false) right before those return points (symbols: handleSubmit,
setIsSubmitting, selectedSessionId, sessions, selectedAgentId, AGENT_TILES,
onAddExisting, onAddFresh).

22-22: Unused prop: groupChatId is declared but never referenced.

The groupChatId prop is defined in AddParticipantModalProps and destructured in the component parameters but is never used in the component body.

🧹 Remove unused prop
 interface AddParticipantModalProps {
 	theme: Theme;
 	isOpen: boolean;
-	groupChatId: string;
 	sessions: Session[];
 	participants: GroupChatParticipant[];
 	onClose: () => void;
 	onAddExisting: (sessionId: string, name: string, agentId: string, cwd: string) => void;
 	onAddFresh: (agentId: string, name: string) => void;
 }

 export function AddParticipantModal({
 	theme,
 	isOpen,
-	groupChatId,
 	sessions,
 	participants,

Also applies to: 30-39

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AddParticipantModal.tsx` at line 22, Remove the
unused prop "groupChatId": delete it from the AddParticipantModalProps interface
and from the AddParticipantModal component's parameter destructuring, and remove
any other references or propTypes/defaults named groupChatId in that component.
Ensure the component compiles after removing the identifier and update any
callers only if they were passing groupChatId (remove the argument) to avoid
unused prop warnings.
src/main/parsers/codex-output-parser.ts (2)

45-71: Pricing data is hardcoded and will become stale.

Model pricing changes frequently. Consider externalizing this to a config file or adding a comment with the last-updated date and a reminder to verify periodically.

The comment on line 37-38 mentions "updated 2026-02-21" which is helpful. Consider also adding a TODO or linking to the source URL for easier verification:

 /**
  * OpenAI model pricing per million tokens (USD)
- * Source: https://openai.com/api/pricing/ (updated 2026-02-21)
+ * Source: https://openai.com/api/pricing/
+ * Last verified: 2026-02-21
+ * TODO: Consider moving to external config or fetching dynamically
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/parsers/codex-output-parser.ts` around lines 45 - 71, The
MODEL_PRICING constant contains hardcoded rates that will go stale; either move
this object into a configuration source (e.g., JSON/YAML loaded at runtime) and
update code that references MODEL_PRICING to read from that config, or at
minimum add a clear metadata comment above MODEL_PRICING with the last-updated
date, a TODO to verify pricing regularly, and a link to the authoritative
pricing URL so maintainers can refresh values; refer to the MODEL_PRICING symbol
in src/main/parsers/codex-output-parser.ts when making changes.

76-86: Prefix matching order may cause incorrect pricing for new model variants.

The prefix matching iterates in insertion order, so gpt-5 will match before gpt-5.1 for any model starting with gpt-5 that isn't an exact match. For example, a hypothetical gpt-5.1-codex-turbo would match gpt-5 pricing instead of gpt-5.1-codex.

🔧 Sort prefixes by length (longest first) for correct matching
 function getModelPricing(model: string): ModelPricing {
 	if (MODEL_PRICING[model]) {
 		return MODEL_PRICING[model];
 	}
-	for (const [prefix, pricing] of Object.entries(MODEL_PRICING)) {
+	// Sort by prefix length descending to match most specific first
+	const sortedEntries = Object.entries(MODEL_PRICING).sort(
+		([a], [b]) => b.length - a.length
+	);
+	for (const [prefix, pricing] of sortedEntries) {
 		if (model.startsWith(prefix)) {
 			return pricing;
 		}
 	}
 	return MODEL_PRICING['default'];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/parsers/codex-output-parser.ts` around lines 76 - 86,
getModelPricing may return the wrong tier because prefix iteration uses
insertion order; change the fallback loop that iterates
Object.entries(MODEL_PRICING) to first collect the prefixes (excluding exact
matches), sort them by descending length, then iterate that sorted list and use
model.startsWith(prefix) to find the most specific match; keep the existing
exact lookup MODEL_PRICING[model] and final default return the same.
src/renderer/components/AppModals.tsx (1)

1541-1557: Consider extracting the IIFE into a local variable for clarity.

The immediately-invoked function expression (IIFE) pattern inside JSX works but is less readable than computing the value before the return. This is a minor style suggestion.

♻️ Optional refactor
+	const addParticipantGroupChat = showAddParticipantModal
+		? groupChats.find((c) => c.id === showAddParticipantModal)
+		: null;
+
 	return (
 		<>
 			{/* ... other modals ... */}

 			{/* --- ADD PARTICIPANT MODAL --- */}
-			{showAddParticipantModal && (() => {
-				const addParticipantGroupChat = groupChats.find(
-					(c) => c.id === showAddParticipantModal
-				);
-				return (
-					<AddParticipantModal
-						theme={theme}
-						isOpen={!!showAddParticipantModal}
-						groupChatId={showAddParticipantModal}
-						sessions={sessions}
-						participants={addParticipantGroupChat?.participants || []}
-						onClose={onCloseAddParticipantModal}
-						onAddExisting={onAddExistingParticipant}
-						onAddFresh={onAddFreshParticipant}
-					/>
-				);
-			})()}
+			{showAddParticipantModal && (
+				<AddParticipantModal
+					theme={theme}
+					isOpen={!!showAddParticipantModal}
+					groupChatId={showAddParticipantModal}
+					sessions={sessions}
+					participants={addParticipantGroupChat?.participants || []}
+					onClose={onCloseAddParticipantModal}
+					onAddExisting={onAddExistingParticipant}
+					onAddFresh={onAddFreshParticipant}
+				/>
+			)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AppModals.tsx` around lines 1541 - 1557, Compute the
JSX for the add-participant modal before the component return instead of using
the IIFE: read showAddParticipantModal and find the matching group chat with
groupChats.find(...) (store into a local addParticipantGroupChat variable),
build the <AddParticipantModal ... /> element (setting isOpen based on
!!showAddParticipantModal and passing groupChatId, participants from
addParticipantGroupChat?.participants || [], sessions,
onCloseAddParticipantModal, onAddExistingParticipant, onAddFreshParticipant),
store that element in a local variable (e.g., addParticipantModalElement) and
then render it in JSX with {showAddParticipantModal &&
addParticipantModalElement}; this removes the inline IIFE while preserving all
props and behavior.
src/main/group-chat/group-chat-router.ts (1)

501-503: Empty catch blocks silently swallow all errors, not just "unavailable" agents.

The empty catch { } blocks will suppress all exceptions including unexpected ones (e.g., network errors, parsing failures). Consider logging at debug level for observability.

🔧 Proposed fix
 				} catch {
-					// Skip unavailable agents
+					// Skip unavailable agents - expected when agent is not installed
+					logger.debug(`Agent ${def.id} not available, skipping`, LOG_CONTEXT);
 				}

Also applies to: 1341-1344

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/group-chat-router.ts` around lines 501 - 503, Replace the
empty catch that contains the comment "// Skip unavailable agents" with a catch
that captures the error (e.g., catch (err)) and logs it at debug level instead
of swallowing it, then continue skipping the agent; e.g., call an existing debug
logger (logger.debug or this.logger.debug) with a concise message and the
error/context (agent id or index) so unexpected failures are observable. Apply
the same change to the other empty catch at the second occurrence (the block
around lines 1341-1344).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/group-chat/group-chat-moderator.ts`:
- Around line 280-296: The killAllModerators function currently calls
processManager.kill(sessionId) but activeModeratorSessions holds prefix IDs so
timestamp-suffixed processes won’t be terminated; change the call to
processManager.killByPrefix(sessionId) when processManager is provided, keep the
powerManager.removeBlockReason(`groupchat:${groupChatId}`) and the
session/sessionActivity clears, and update the killAllModerators unit tests to
expect killByPrefix to be invoked for each sessionId prefix instead of kill.

In `@src/main/group-chat/group-chat-router.ts`:
- Around line 1108-1109: The synthesis error paths call clearSynthesisInProgress
but never release the chat lock, leaving the conversation locked; update each
synthesis failure branch (the code paths that currently call
clearSynthesisInProgress at the synthesis spawn/error locations tied to
routeModeratorResponse and the synthesis flow) to also call
releaseChatLock(groupChatId) before returning/throwing so the chat lock is
always released; specifically add releaseChatLock(groupChatId) alongside
clearSynthesisInProgress in the error handlers found around the synthesis
spawn/error sites (the branches referenced in your review at the lines that call
clearSynthesisInProgress) to guarantee locks are freed on all failure paths.

In `@src/main/process-manager/ProcessManager.ts`:
- Around line 194-208: killByPrefix currently increments killed for every
sessionId that startsWith(prefix) even though this.kill(sessionId) can return
false; change the logic to call this.kill(sessionId) and only increment killed
when that call returns a truthy/success value. Update the loop in killByPrefix
(referencing killByPrefix, this.kill, this.processes, and sessionId) so it
checks the boolean result of this.kill(sessionId) and increments killed only on
success, then return the accurate killed count.

In `@src/renderer/components/AddParticipantModal.tsx`:
- Around line 76-80: The catch block in AddParticipantModal currently only logs
failures to console (inside the try/catch around agent detection) so users still
see "No agents available"; update the catch to set a user-facing error state
(e.g., setAgentDetectionError or setDetectionError) and leave
setIsDetecting(false) in finally, and also forward the exception to your error
monitoring (e.g., captureException or Sentry.captureException) so it’s recorded;
then update the component render logic that shows "No agents available" to
prefer displaying the new error state message when present.

In `@src/renderer/components/GroupChatRightPanel.tsx`:
- Line 10: The "Add Participant" button in the GroupChatRightPanel component
lacks keyboard focus handling; update the element that renders the Plus
icon/button inside GroupChatRightPanel to include tabIndex={0} (or tabIndex={-1}
if it should be programmatically focusable only), add the "outline-none" class
to match renderer focus rules, and add an accessible label via aria-label (e.g.,
aria-label="Add participant"); apply the same pattern (tabIndex + outline-none
and optional aria-label) to the other interactive elements referenced in this
file (the ones rendering PanelRightClose and other similar buttons).

In `@src/renderer/hooks/groupChat/useGroupChatHandlers.ts`:
- Around line 658-677: The parameter sessionId in handleAddExistingParticipant
is unused; either remove it from the function signature and any callers (keeping
the function to derive id from useGroupChatStore.getState().activeGroupChatId),
or if the caller should supply the session id, replace the local id assignment
with const id = sessionId and ensure callers pass the sessionId; update
references to useGroupChatStore, window.maestro.groupChat.addParticipant, and
useModalStore.getState().closeModal('addGroupChatParticipant') accordingly so
the function signature and implementation remain consistent.

---

Outside diff comments:
In `@src/__tests__/main/ipc/handlers/groupChat.test.ts`:
- Around line 100-132: The mockProcessManager declared type is missing the
killByPrefix property while the object literal sets it; update the
mockProcessManager type to include killByPrefix (e.g., add killByPrefix:
ReturnType<typeof vi.fn>) so the declaration matches the object created in
beforeEach and TypeScript no longer errors on excess/missing properties for
mockProcessManager.

---

Nitpick comments:
In `@src/main/group-chat/group-chat-lock.ts`:
- Around line 75-82: Both forceReleaseChatLock and releaseChatLock simply call
chatLocks.delete(chatId), so consolidate by removing one of them (preferably
keep releaseChatLock for the simpler name) and update all references to the
removed symbol to call the retained function; adjust JSDoc on the retained
function to document unconditional behavior if needed and run/update any
callers/tests that referenced forceReleaseChatLock to use releaseChatLock
instead.

In `@src/main/group-chat/group-chat-router.ts`:
- Around line 501-503: Replace the empty catch that contains the comment "//
Skip unavailable agents" with a catch that captures the error (e.g., catch
(err)) and logs it at debug level instead of swallowing it, then continue
skipping the agent; e.g., call an existing debug logger (logger.debug or
this.logger.debug) with a concise message and the error/context (agent id or
index) so unexpected failures are observable. Apply the same change to the other
empty catch at the second occurrence (the block around lines 1341-1344).

In `@src/main/group-chat/validation.ts`:
- Around line 95-109: The validateBase64Image function currently only checks
type and estimated size but doesn't confirm the string is valid base64; add a
format validation step in validateBase64Image that verifies the input matches a
base64 pattern (optionally allowing data URI prefix removal) and rejects
malformed strings before size checking, or attempt a safe decode (e.g., try
Buffer.from(..., 'base64') and ensure re-encoding matches) to catch invalid
base64 and throw a clear error message when validation fails.
- Line 10: The constant UUID_V4_REGEX currently matches any UUID version because
it doesn't validate the version nibble; update the code so the name and behavior
align: either rename UUID_V4_REGEX to UUID_REGEX (and update any references) if
you intend to accept all UUID versions, or change the regex to enforce v4
specifically by requiring the version character to be "4" (e.g., the 13th hex
digit) and adjust the constant name to remain UUID_V4_REGEX; locate and edit the
UUID_V4_REGEX declaration in validation.ts and update usages accordingly.

In `@src/main/ipc/handlers/groupChat.ts`:
- Around line 770-771: The code incorrectly reuses validateGroupChatId for the
history entry ID; add a clear UUID validator (e.g., validateEntryId or
validateUUID) in the group-chat validation module and replace the
validateGroupChatId(entryId) call with the new validator; update any
imports/usages in the handler (groupChatId = validateGroupChatId(groupChatId);
validateEntryId(entryId) or validateUUID(entryId)) so intent is clear and
validators are not misleading.
- Around line 440-441: Create a tracked issue for implementing image embedding
in moderator prompts: describe the current state (images saved separately via
the groupChat:saveImage handler and a TODO in the async moderator prompt handler
with signature async (id: string, message: string, images?: string[], readOnly?:
boolean)), list acceptance criteria (embed images inline in moderator prompt
payloads, preserve image ordering and alt text, fallback to separate storage
when embedding fails), include required API changes (modify the moderator prompt
handler to accept/serialize embedded image data or URLs), add tests and UI/UX
notes, and tag with a feature/enhancement label and assign or link to the
relevant maintainer for prioritization.

In `@src/main/parsers/codex-output-parser.ts`:
- Around line 45-71: The MODEL_PRICING constant contains hardcoded rates that
will go stale; either move this object into a configuration source (e.g.,
JSON/YAML loaded at runtime) and update code that references MODEL_PRICING to
read from that config, or at minimum add a clear metadata comment above
MODEL_PRICING with the last-updated date, a TODO to verify pricing regularly,
and a link to the authoritative pricing URL so maintainers can refresh values;
refer to the MODEL_PRICING symbol in src/main/parsers/codex-output-parser.ts
when making changes.
- Around line 76-86: getModelPricing may return the wrong tier because prefix
iteration uses insertion order; change the fallback loop that iterates
Object.entries(MODEL_PRICING) to first collect the prefixes (excluding exact
matches), sort them by descending length, then iterate that sorted list and use
model.startsWith(prefix) to find the most specific match; keep the existing
exact lookup MODEL_PRICING[model] and final default return the same.

In `@src/main/preload/groupChat.ts`:
- Around line 115-116: The preload API is inconsistent: addParticipant(id, name,
agentId, cwd) vs addFreshParticipant(id, agentId, name, cwd); change
addFreshParticipant to the same parameter order as addParticipant (id, name,
agentId, cwd), update its ipcRenderer.invoke call to pass arguments in that
order, and update the corresponding IPC handler for
'groupChat:addFreshParticipant' (or the code that reorders its args before
calling the underlying function) so it expects and forwards (id, name, agentId,
cwd) consistently with addParticipant.

In `@src/renderer/components/AddParticipantModal.tsx`:
- Around line 108-129: In handleSubmit ensure isSubmitting is reset on the
early-return paths: before each early return in the 'existing' branch when
selectedSessionId is falsy or session not found, and before the early return in
the 'fresh' branch when selectedAgentId is falsy, call setIsSubmitting(false)
(and optionally setError with a helpful message) so the component doesn't remain
stuck in submitting state; update the closure that defines handleSubmit to call
setIsSubmitting(false) right before those return points (symbols: handleSubmit,
setIsSubmitting, selectedSessionId, sessions, selectedAgentId, AGENT_TILES,
onAddExisting, onAddFresh).
- Line 22: Remove the unused prop "groupChatId": delete it from the
AddParticipantModalProps interface and from the AddParticipantModal component's
parameter destructuring, and remove any other references or propTypes/defaults
named groupChatId in that component. Ensure the component compiles after
removing the identifier and update any callers only if they were passing
groupChatId (remove the argument) to avoid unused prop warnings.

In `@src/renderer/components/AppModals.tsx`:
- Around line 1541-1557: Compute the JSX for the add-participant modal before
the component return instead of using the IIFE: read showAddParticipantModal and
find the matching group chat with groupChats.find(...) (store into a local
addParticipantGroupChat variable), build the <AddParticipantModal ... /> element
(setting isOpen based on !!showAddParticipantModal and passing groupChatId,
participants from addParticipantGroupChat?.participants || [], sessions,
onCloseAddParticipantModal, onAddExistingParticipant, onAddFreshParticipant),
store that element in a local variable (e.g., addParticipantModalElement) and
then render it in JSX with {showAddParticipantModal &&
addParticipantModalElement}; this removes the inline IIFE while preserving all
props and behavior.

In `@src/renderer/global.d.ts`:
- Around line 1776-1786: The two APIs have inconsistent parameter order:
addParticipant(id, name, agentId, cwd) vs addFreshParticipant(id, agentId, name,
cwd); change addFreshParticipant’s declaration and any implementations/callsites
to use the same order as addParticipant (id, name, agentId, cwd) to keep the API
consistent, and update any documentation/tests that rely on the old order; if
the original order was intentional, instead add a comment/docstring to
addFreshParticipant clarifying the different ordering.

Comment on lines +280 to +296
/**
* Kills all active moderator processes and clears session tracking.
* Used during application shutdown to prevent zombie processes.
*
* @param processManager - The process manager for killing processes (optional)
*/
export function killAllModerators(processManager?: IProcessManager): void {
for (const [groupChatId, sessionId] of activeModeratorSessions) {
if (processManager) {
processManager.kill(sessionId);
}
// Remove power block reason for each moderator
powerManager.removeBlockReason(`groupchat:${groupChatId}`);
}
activeModeratorSessions.clear();
sessionActivityTimestamps.clear();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

killAllModerators still uses exact IDs; batch-mode moderators will survive.

activeModeratorSessions stores prefixes, so kill(sessionId) won’t match timestamp-suffixed processes. Use killByPrefix here too (and update the killAllModerators test expectations accordingly).

🔧 Proposed fix
-export function killAllModerators(processManager?: IProcessManager): void {
-	for (const [groupChatId, sessionId] of activeModeratorSessions) {
-		if (processManager) {
-			processManager.kill(sessionId);
-		}
-		// Remove power block reason for each moderator
-		powerManager.removeBlockReason(`groupchat:${groupChatId}`);
-	}
-	activeModeratorSessions.clear();
-	sessionActivityTimestamps.clear();
-}
+export function killAllModerators(processManager?: IProcessManager): void {
+	for (const [groupChatId, sessionIdPrefix] of activeModeratorSessions) {
+		if (processManager) {
+			processManager.killByPrefix(sessionIdPrefix);
+		}
+		// Remove power block reason for each moderator
+		powerManager.removeBlockReason(`groupchat:${groupChatId}`);
+	}
+	activeModeratorSessions.clear();
+	sessionActivityTimestamps.clear();
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/group-chat-moderator.ts` around lines 280 - 296, The
killAllModerators function currently calls processManager.kill(sessionId) but
activeModeratorSessions holds prefix IDs so timestamp-suffixed processes won’t
be terminated; change the call to processManager.killByPrefix(sessionId) when
processManager is provided, keep the
powerManager.removeBlockReason(`groupchat:${groupChatId}`) and the
session/sessionActivity clears, and update the killAllModerators unit tests to
expect killByPrefix to be invoked for each sessionId prefix instead of kill.

Comment on lines +1108 to +1109
// Release chat lock - conversation round is complete
releaseChatLock(groupChatId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all calls to releaseChatLock
rg -n 'releaseChatLock' src/main/group-chat/

Repository: RunMaestro/Maestro

Length of output: 610


🏁 Script executed:

#!/bin/bash
# Read the file around the specified lines to understand the context
wc -l src/main/group-chat/group-chat-router.ts

Repository: RunMaestro/Maestro

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Get context around lines 1108-1109
sed -n '1050,1150p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 4279


🏁 Script executed:

#!/bin/bash
# View context around the other releaseChatLock calls
sed -n '600,750p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 6556


🏁 Script executed:

#!/bin/bash
# Search for clearSynthesisInProgress
rg -n 'clearSynthesisInProgress' src/main/group-chat/

Repository: RunMaestro/Maestro

Length of output: 675


🏁 Script executed:

#!/bin/bash
# Search for synthesis-related functions and lock handling
rg -n 'synthesis|pendingParticipantResponses' src/main/group-chat/group-chat-router.ts | head -40

Repository: RunMaestro/Maestro

Length of output: 2986


🏁 Script executed:

#!/bin/bash
# View the synthesis spawn function
sed -n '1227,1500p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 12412


🏁 Script executed:

#!/bin/bash
# Check if there's any lock release in the synthesis-related code after line 1484
sed -n '1484,1550p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 2956


🏁 Script executed:

#!/bin/bash
# Check what happens after a synthesis moderator response - search for synthesis-related response handling
rg -n 'synthesis.*response|isSynthesisRound' src/main/group-chat/group-chat-router.ts

Repository: RunMaestro/Maestro

Length of output: 140


🏁 Script executed:

#!/bin/bash
# Search for all places where routeModeratorResponse is called to see the full synthesis flow
rg -n 'routeModeratorResponse' src/main/group-chat/ -B 2 -A 2

Repository: RunMaestro/Maestro

Length of output: 1052


🏁 Script executed:

#!/bin/bash
# Check the moderator response routing to verify if synthesis responses properly release locks
sed -n '850,920p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 2882


🏁 Script executed:

#!/bin/bash
# Verify if there's any lock release in synthesis error handling or check the flow completely
sed -n '1240,1265p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 1255


🏁 Script executed:

#!/bin/bash
# View the full routeModeratorResponse function starting from line 691
sed -n '691,850p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 6902


🏁 Script executed:

#!/bin/bash
# Continue viewing routeModeratorResponse
sed -n '850,950p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 4133


🏁 Script executed:

#!/bin/bash
# Continue viewing to see the end of routeModeratorResponse and lock release logic
sed -n '950,1050p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 5013


🏁 Script executed:

#!/bin/bash
# Continue to see the end of routeModeratorResponse
sed -n '1050,1150p' src/main/group-chat/group-chat-router.ts | cat -n

Repository: RunMaestro/Maestro

Length of output: 4279


Verify lock is released in all synthesis error paths.

The lock is correctly released when the synthesis moderator responds with no mentions (which goes through routeModeratorResponse's final path at line 1060). However, synthesis spawn error paths at lines 1252, 1261, 1275, 1293, and 1484 call clearSynthesisInProgress but do not call releaseChatLock. If synthesis fails to spawn or encounters errors, the lock remains held indefinitely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/group-chat/group-chat-router.ts` around lines 1108 - 1109, The
synthesis error paths call clearSynthesisInProgress but never release the chat
lock, leaving the conversation locked; update each synthesis failure branch (the
code paths that currently call clearSynthesisInProgress at the synthesis
spawn/error locations tied to routeModeratorResponse and the synthesis flow) to
also call releaseChatLock(groupChatId) before returning/throwing so the chat
lock is always released; specifically add releaseChatLock(groupChatId) alongside
clearSynthesisInProgress in the error handlers found around the synthesis
spawn/error sites (the branches referenced in your review at the lines that call
clearSynthesisInProgress) to guarantee locks are freed on all failure paths.

Comment on lines +194 to +208
/**
* Kill all processes whose session ID starts with a given prefix.
* Used by group chat to kill batch-spawned processes (moderator/participant)
* whose session IDs have timestamp/UUID suffixes.
*/
killByPrefix(prefix: string): number {
let killed = 0;
for (const [sessionId] of this.processes) {
if (sessionId.startsWith(prefix)) {
this.kill(sessionId);
killed++;
}
}
return killed;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Count only successful kills.

kill() can return false; incrementing unconditionally may overreport. Safer to increment only on success.

🔧 Suggested tweak
 	killByPrefix(prefix: string): number {
 		let killed = 0;
 		for (const [sessionId] of this.processes) {
-			if (sessionId.startsWith(prefix)) {
-				this.kill(sessionId);
-				killed++;
-			}
+			if (sessionId.startsWith(prefix) && this.kill(sessionId)) {
+				killed++;
+			}
 		}
 		return killed;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/process-manager/ProcessManager.ts` around lines 194 - 208,
killByPrefix currently increments killed for every sessionId that
startsWith(prefix) even though this.kill(sessionId) can return false; change the
logic to call this.kill(sessionId) and only increment killed when that call
returns a truthy/success value. Update the loop in killByPrefix (referencing
killByPrefix, this.kill, this.processes, and sessionId) so it checks the boolean
result of this.kill(sessionId) and increments killed only on success, then
return the accurate killed count.

Comment on lines +76 to +80
} catch (err) {
console.error('Failed to detect agents:', err);
} finally {
setIsDetecting(false);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error is logged but user receives no feedback.

When agent detection fails, the error is logged to console but the user sees "No agents available" which is misleading. Per coding guidelines, exceptions should either bubble up to Sentry or be handled with user-facing feedback.

🛠️ Suggested fix: Set error state on detection failure
 			} catch (err) {
-				console.error('Failed to detect agents:', err);
+				setError('Failed to detect available agents. Please try again.');
 			} finally {
 				setIsDetecting(false);
 			}
📝 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
} catch (err) {
console.error('Failed to detect agents:', err);
} finally {
setIsDetecting(false);
}
} catch (err) {
setError('Failed to detect available agents. Please try again.');
} finally {
setIsDetecting(false);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AddParticipantModal.tsx` around lines 76 - 80, The
catch block in AddParticipantModal currently only logs failures to console
(inside the try/catch around agent detection) so users still see "No agents
available"; update the catch to set a user-facing error state (e.g.,
setAgentDetectionError or setDetectionError) and leave setIsDetecting(false) in
finally, and also forward the exception to your error monitoring (e.g.,
captureException or Sentry.captureException) so it’s recorded; then update the
component render logic that shows "No agents available" to prefer displaying the
new error state message when present.


import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { PanelRightClose } from 'lucide-react';
import { PanelRightClose, Plus } from 'lucide-react';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add focus handling to the new Add Participant button.

Please add tabIndex and an outline-none class (and optionally an aria-label) to align with the renderer focus rules.

♿ Suggested update
 				{onAddParticipant && (
 					<button
 						onClick={onAddParticipant}
-						className="flex items-center justify-center p-2 rounded hover:bg-white/5 transition-colors w-10 shrink-0"
+						tabIndex={0}
+						className="flex items-center justify-center p-2 rounded hover:bg-white/5 transition-colors w-10 shrink-0 outline-none"
+						aria-label="Add participant"
 						title="Add participant"
 					>
 						<Plus className="w-4 h-4 opacity-50" />
 					</button>
 				)}
As per coding guidelines "src/renderer/**/*.{tsx,jsx}": Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly.

Also applies to: 59-60, 84-85, 281-289

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/GroupChatRightPanel.tsx` at line 10, The "Add
Participant" button in the GroupChatRightPanel component lacks keyboard focus
handling; update the element that renders the Plus icon/button inside
GroupChatRightPanel to include tabIndex={0} (or tabIndex={-1} if it should be
programmatically focusable only), add the "outline-none" class to match renderer
focus rules, and add an accessible label via aria-label (e.g., aria-label="Add
participant"); apply the same pattern (tabIndex + outline-none and optional
aria-label) to the other interactive elements referenced in this file (the ones
rendering PanelRightClose and other similar buttons).

Comment on lines +658 to +677
const handleAddExistingParticipant = useCallback(
async (sessionId: string, name: string, agentId: string, cwd: string) => {
const { activeGroupChatId } = useGroupChatStore.getState();
const id = activeGroupChatId;
if (!id) return;

try {
await window.maestro.groupChat.addParticipant(id, name, agentId, cwd);
useModalStore.getState().closeModal('addGroupChatParticipant');
} catch (err) {
notifyToast({
type: 'error',
title: 'Group Chat',
message: err instanceof Error ? err.message : 'Failed to add participant',
});
throw err; // Re-throw so the modal can display the error
}
},
[]
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the addParticipant IPC handler signature in main process
rg -n 'addParticipant' --type ts -A 15 src/main/ipc/handlers/groupChat.ts | head -60

# Also check preload
echo "=== Preload definitions ==="
rg -n 'addParticipant' --type ts src/main/preload/groupChat.ts

Repository: RunMaestro/Maestro

Length of output: 2136


Unused parameter: sessionId should be removed or used.

The sessionId parameter is declared in the function signature but never used. The function derives id from activeGroupChatId instead and passes it to the IPC call. Either remove the unused sessionId parameter or replace the store-derived id with sessionId if that was the intended pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/groupChat/useGroupChatHandlers.ts` around lines 658 - 677,
The parameter sessionId in handleAddExistingParticipant is unused; either remove
it from the function signature and any callers (keeping the function to derive
id from useGroupChatStore.getState().activeGroupChatId), or if the caller should
supply the session id, replace the local id assignment with const id = sessionId
and ensure callers pass the sessionId; update references to useGroupChatStore,
window.maestro.groupChat.addParticipant, and
useModalStore.getState().closeModal('addGroupChatParticipant') accordingly so
the function signature and implementation remain consistent.

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