Skip to content

feat: token-efficient system prompt delivery for CLI agents#596

Merged
pedramamini merged 13 commits intoRunMaestro:rcfrom
jSydorowicz21:feat/token-efficient-system-prompt-delivery
Mar 25, 2026
Merged

feat: token-efficient system prompt delivery for CLI agents#596
pedramamini merged 13 commits intoRunMaestro:rcfrom
jSydorowicz21:feat/token-efficient-system-prompt-delivery

Conversation

@jSydorowicz21
Copy link
Contributor

@jSydorowicz21 jSydorowicz21 commented Mar 20, 2026

Summary

  • Separate Maestro system prompt from user message text when spawning CLI agents
  • For Claude Code, deliver system context via --append-system-prompt flag (proper system prompt) instead of embedding in user message
  • For other agents without equivalent flags, fall back to embedding in prompt
  • On Windows, text-only messages now use raw stdin instead of stream-json API format wrapping (stream-json still used for images)

Changes

  • Add appendSystemPrompt field to ProcessConfig (renderer types, preload, global.d.ts)
  • Add supportsAppendSystemPrompt capability flag to AgentCapabilities (true for Claude Code, false for others)
  • Route system prompt delivery in process:spawn handler: CLI arg for supported agents, embed fallback for others
  • Update all 3 renderer call sites (agentStore, useInputProcessing, useMergeTransferHandlers) to pass system prompt separately
  • Add hasImages param to getStdinFlags for conditional stream-json stdin on Windows
  • Update tests for new spawn config shape and stdin flag behavior

Test plan

  • New Claude Code session: verify --append-system-prompt appears in spawn args, user message is clean
  • Non-Claude agent (Codex/OpenCode): verify system prompt still embedded in user message (fallback)
  • Windows text-only message: verify raw stdin (not stream-json wrapped)
  • Windows image message: verify stream-json stdin preserved
  • SSH remote session: verify system prompt in args, user message via stdin passthrough
  • Resumed session: verify no system prompt sent (same as before)
  • Type check passes (npm run lint)

Summary by CodeRabbit

  • New Features

    • Added a capability flag to opt into separate system-prompt delivery (append vs embed).
  • Behavior Changes

    • System-prompt delivery now selects CLI-arg, file, or embedded delivery and redacts appended prompt content in logs.
    • Windows stdin transport now distinguishes text-only vs image-containing messages when choosing raw vs structured input.
  • Tests

    • Expanded tests covering appended-prompt delivery and image-aware stdin behavior across platforms.
  • Documentation

    • Minor docs/notes added.

Separate system prompt from user message text when spawning CLI agents.
For Claude Code, use --append-system-prompt flag to deliver system context
as a proper system prompt instead of embedding it in the user message.
For other agents, fall back to embedding in the prompt (no equivalent flag).

On Windows, text-only messages to Claude Code now use raw stdin instead
of stream-json API format wrapping. Stream-json is still used when images
are present (required for base64 transport).

Changes:
- Add appendSystemPrompt field to ProcessConfig (renderer, preload, global types)
- Add supportsAppendSystemPrompt capability flag to AgentCapabilities
- Route system prompt delivery in process:spawn handler (args vs embed fallback)
- Update all 3 renderer call sites to pass system prompt separately
- Add hasImages param to getStdinFlags for conditional stream-json stdin
- Update tests for new spawn config shape and stdin flag behavior
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

Added a new AgentCapabilities flag supportsAppendSystemPrompt, plumbing to pass appendSystemPrompt through renderer → IPC → spawn (CLI arg or temp file on Windows) with fallback embedding; stdin selection now considers hasImages; multiple call sites and tests updated to propagate new flags.

Changes

Cohort / File(s) Summary
Agent capability types & defaults
src/main/agents/capabilities.ts, src/renderer/hooks/agent/useAgentCapabilities.ts, src/renderer/types/index.ts
Added supportsAppendSystemPrompt to AgentCapabilities and DEFAULT_CAPABILITIES; enabled for claude-code.
ProcessConfig / preload types
src/main/preload/process.ts, src/renderer/global.d.ts, src/renderer/types/index.ts
Added appendSystemPrompt?: string to ProcessConfig and added sendPromptViaStdin? / sendPromptViaStdinRaw? typing in preload.
IPC spawn handler
src/main/ipc/handlers/process.ts
process:spawn accepts appendSystemPrompt; computes effectivePrompt; if agent supports append mode sends --append-system-prompt (or --append-system-prompt-file on Windows using a temp file), otherwise embeds the system prompt into prompt; added temp-file lifecycle, redacted logging, and systemPromptDelivery metadata.
Stdin logic & helper
src/renderer/utils/spawnHelpers.ts, src/__tests__/renderer/utils/spawnHelpers.test.ts
getStdinFlags signature gains hasImages: boolean; Windows JSON stdin now requires stream-json support AND images; tests added/updated for Windows with and without images.
Call-site plumbing for stdin & append prompt
src/renderer/hooks/agent/useAgentExecution.ts, src/renderer/services/inlineWizardConversation.ts, src/renderer/components/Wizard/services/conversationManager.ts
Updated getStdinFlags calls to include hasImages: false where flows never send images.
Spawn path changes to separate system prompt
src/renderer/hooks/agent/useMergeTransferHandlers.ts, src/renderer/hooks/input/useInputProcessing.ts, src/renderer/stores/agentStore.ts, src/__tests__/renderer/stores/agentStore.test.ts
Stopped inlining Maestro system prompt into prompt for new sessions; capture substituted system prompt as appendSystemPrompt and pass to spawn; adjusted logging and tests to expect appendSystemPrompt field.
Main tests & expectations
src/__tests__/main/agents/capabilities.test.ts, src/__tests__/renderer/services/inlineWizardConversation.test.ts, src/__tests__/renderer/utils/spawnHelpers.test.ts, src/__tests__/main/ipc/handlers/process.test.ts
Updated tests to include new capability key, changed Windows text-only stdin/CLI-arg expectations for claude-code, added comprehensive appendSystemPrompt delivery tests for local and SSH paths, and updated stdin-related tests for hasImages.
Cue executor SSH stdin handling
src/main/cue/cue-executor.ts
Preserve and use sshStdinScript when present (write full script to child stdin) otherwise fallback to writing raw prompt.
Docs & misc
AGENTS.md, Plans/rippling-inventing-lamport.md
Minor newline fix and new plan doc describing the changes and verification steps.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Renderer (hook/store)
    participant IPC as Main IPC Handler
    participant AgentMgr as Agent Capabilities
    participant Spawn as Process Spawn

    Renderer->>Renderer: build prompt, substitute maestro system prompt -> appendSystemPrompt?
    Renderer->>IPC: spawn({ prompt, appendSystemPrompt, images?, isSshSession, supportsStreamJsonInput })
    IPC->>AgentMgr: check capabilities.supportsAppendSystemPrompt
    alt supportsAppendSystemPrompt == true
        IPC->>Spawn: spawn args + "--append-system-prompt <redacted>", prompt (user)
        Spawn->>Spawn: agent receives separate system prompt arg
    else
        IPC->>IPC: effectivePrompt = embed appendSystemPrompt into prompt
        IPC->>Spawn: spawn args, prompt = effectivePrompt
        Spawn->>Spawn: agent receives combined prompt
    end
Loading
sequenceDiagram
    participant Caller as Renderer call-site
    participant Helper as spawnHelpers
    participant Platform as Platform detection

    Caller->>Helper: getStdinFlags({isSshSession, supportsStreamJsonInput, hasImages})
    Helper->>Platform: detect Windows?
    Platform-->>Helper: platform result
    alt Windows && !isSsh && supportsStreamJsonInput && hasImages
        Helper-->>Caller: { sendPromptViaStdin: true, sendPromptViaStdinRaw: false }
    else if Windows && !isSsh
        Helper-->>Caller: { sendPromptViaStdin: false, sendPromptViaStdinRaw: true }
    else
        Helper-->>Caller: { sendPromptViaStdin: false, sendPromptViaStdinRaw: false }
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 I split the prompt into two neat parts,
One flutters in args, one nestles in hearts.
If agents can listen, I tuck it clean,
Else I stitch it where the agents have seen.
On Windows I peek if images play — then choose the scene.

🚥 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 'feat: token-efficient system prompt delivery for CLI agents' accurately and directly describes the main change: enabling CLI agents to receive system prompts via separate mechanisms (CLI flags or temp files) rather than embedding them in user messages, thereby reducing token usage.
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 unit tests (beta)
  • Create PR with unit tests

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.

- Add supportsAppendSystemPrompt to capabilities test expected keys
- Update inline wizard tests: text-only messages now use raw stdin
  instead of stream-json on Windows (no images = no stream-json)
…ages param

- Redact --append-system-prompt value from logged args and fullCommand
  to avoid leaking large system prompt content into Sentry logs
- Add logger.warn when system prompt is dropped because effectivePrompt
  is empty on the fallback (non-append-system-prompt) path
- Make hasImages required in getStdinFlags to prevent silent default
  behavior; add explicit false at all non-image call sites
@jSydorowicz21 jSydorowicz21 reopened this Mar 20, 2026
@jSydorowicz21 jSydorowicz21 marked this pull request as ready for review March 20, 2026 23:52
@greptile-apps
Copy link

greptile-apps bot commented Mar 21, 2026

Greptile Summary

This PR improves token efficiency for CLI agents by separating the Maestro system prompt from the user message at spawn time. For Claude Code, the system prompt is now delivered via --append-system-prompt (CLI flag), which allows the model to cache it as a proper system prompt rather than treating it as user-turn tokens. For other agents that lack an equivalent flag, the existing embed-in-user-message fallback is preserved. A secondary change makes Windows stdin transport image-aware: text-only messages now use raw stdin (sendPromptViaStdinRaw) instead of stream-json format, reserving the --input-format stream-json path for messages that actually contain images.

Key changes:

  • capabilities.ts: adds supportsAppendSystemPrompt flag; only claude-code is true
  • process.ts (IPC handler): new routing block delivers system prompt via CLI arg, temp file (Windows), or embed fallback
  • spawnHelpers.ts: getStdinFlags gains a hasImages param that controls sendPromptViaStdin vs sendPromptViaStdinRaw
  • agentStore.ts, useInputProcessing.ts, useMergeTransferHandlers.ts: all three renderer spawn call-sites updated to pass appendSystemPrompt separately
  • cue-executor.ts: SSH stdin script is now written directly to the child process when present (minor correctness fix)
  • P1 concern: The Windows local path uses --append-system-prompt-file <tempfile> instead of the inline --append-system-prompt. This variant flag is not referenced in any Claude Code documentation within the codebase and its support has not been confirmed. If Claude Code does not accept --append-system-prompt-file, all Windows new-session spawns will silently lose the system prompt (or fail to start).
  • P2: Plans/rippling-inventing-lamport.md (an internal planning artifact) is committed and should be removed.

Confidence Score: 4/5

  • Safe to merge after confirming --append-system-prompt-file is a valid Claude Code CLI flag on Windows.
  • The overall design is sound: prior review concerns about Windows command-line length limits are directly addressed, the sendPromptViaStdinRaw behavior change is intentional and covered by tests, and all three renderer call-sites are consistently updated. The single blocking concern is the unverified --append-system-prompt-file flag. If it turns out Claude Code doesn't support this flag, Windows sessions silently lose the system prompt or fail to start — a real production regression. Once that flag's support is confirmed (or an alternative Windows delivery mechanism is chosen), this PR is ready to merge.
  • src/main/ipc/handlers/process.ts — specifically the Windows --append-system-prompt-file code path (lines 228–263).

Important Files Changed

Filename Overview
src/main/ipc/handlers/process.ts Core change: adds system prompt delivery routing (--append-system-prompt for CLI, --append-system-prompt-file for Windows local, embed fallback for others). P1 concern: --append-system-prompt-file flag is not verified as a real Claude Code flag.
src/renderer/utils/spawnHelpers.ts Adds hasImages param to getStdinFlags; text-only Windows messages now use sendPromptViaStdinRaw instead of sendPromptViaStdin (stream-json). Behavior change is intentional and tested, but was flagged in prior review threads.
src/renderer/stores/agentStore.ts Updated processQueuedItem to separate appendSystemPrompt from prompt for new sessions; properly calls getStdinFlags with hasImages for both message and command spawn paths.
src/renderer/hooks/agent/useMergeTransferHandlers.ts Adds appendSystemPrompt to the context-transfer spawn call; note that sendPromptViaStdin/sendPromptViaStdinRaw are still absent from this path, but this is pre-existing and scoped outside this PR's fix set.
src/main/agents/capabilities.ts Adds supportsAppendSystemPrompt to all agent capability definitions; claude-code is the only agent with true. Clean, conservative defaults for all other agents.
src/tests/main/ipc/handlers/process.test.ts Adds 6 new test cases covering CLI-arg, embed-fallback, SSH, and no-prompt delivery paths. Tests mock processManager.spawn and verify the arg structure but do not run the actual Claude Code binary to validate --append-system-prompt-file.
Plans/rippling-inventing-lamport.md Internal planning document that describes three issues from a prior review cycle, all of which appear to have been addressed in the current code. Should be removed before merge.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[process:spawn IPC\nwith appendSystemPrompt] --> B{agent.supportsAppendSystemPrompt?}

    B -- No --> C{effectivePrompt non-empty?}
    C -- Yes --> D[Embed: prepend system prompt\nto user message]
    C -- No --> E[Use system prompt\nas sole content]

    B -- Yes --> F{isWindows AND\nnot SSH session?}
    F -- Yes --> G[Write system prompt\nto temp file\nmaestro-sysprompt-id.txt]
    G --> H[Pass --append-system-prompt-file\nwith temp path\nSchedule 30s cleanup]
    F -- No --> I[Pass --append-system-prompt\ninline in args]

    D --> J[Spawn agent with\neffectivePrompt]
    E --> J
    H --> J
    I --> J

    J --> K{SSH session?}
    K -- Yes --> L[Prompt delivered via\nSSH stdin script]
    K -- No --> M{Windows local?}
    M -- Yes, images --> N[sendPromptViaStdin\nstream-json format]
    M -- Yes, no images --> O[sendPromptViaStdinRaw\nraw text stdin]
    M -- No --> P[Prompt in CLI args]
Loading

Reviews (3): Last reviewed commit: "style: fix prettier formatting in proces..." | Re-trigger Greptile

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

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/stores/agentStore.ts (1)

299-345: ⚠️ Potential issue | 🟠 Major

Mirror the Windows stdin routing in queued spawns.

useInputProcessing now sets sendPromptViaStdin / sendPromptViaStdinRaw, but processQueuedItem still doesn't. Once a turn enters the queue, Windows falls back to the old argv-based path: text-only follow-ups stop using raw stdin, and image-bearing Claude/factory-droid turns never opt into stream-json stdin. That makes queued behavior diverge from immediate sends and can regress on long/quoted prompts or image delivery.

💡 Suggested fix
 import { gitService } from '../services/git';
 import { filterYoloArgs } from '../utils/agentArgs';
+import { getStdinFlags } from '../utils/spawnHelpers';
@@
 			const hasImages = item.images && item.images.length > 0;
 			const hasText = item.text && item.text.trim();
 			const isImageOnlyMessage = item.type === 'message' && hasImages && !hasText;
+			const isSshSession =
+				!!session.sshRemoteId || !!session.sessionSshRemoteConfig?.enabled;
@@
 				console.log('[processQueuedItem] Spawning agent with queued message:', {
@@
 					args: spawnArgs,
 				});
+
+				const stdinFlags = getStdinFlags({
+					isSshSession,
+					supportsStreamJsonInput: agent.capabilities?.supportsStreamJsonInput ?? false,
+					hasImages,
+				});
 
 				await window.maestro.process.spawn({
@@
 					prompt: effectivePrompt,
 					images: hasImages ? item.images : undefined,
 					appendSystemPrompt,
+					...stdinFlags,
 					agentSessionId: tabAgentSessionId ?? undefined,
@@
 					await window.maestro.process.spawn({
+						...getStdinFlags({
+							isSshSession,
+							supportsStreamJsonInput: agent.capabilities?.supportsStreamJsonInput ?? false,
+							hasImages: false,
+						}),
 						sessionId: targetSessionId,
 						toolType: session.toolType,
 						cwd: session.cwd,
 						command: commandToUse,
 						args: spawnArgs,
 						prompt: substitutedPrompt,
 						appendSystemPrompt: appendSystemPromptForCommand,

Also applies to: 406-447

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

In `@src/renderer/stores/agentStore.ts` around lines 299 - 345, processQueuedItem
is not passing the stdin-routing flags set by useInputProcessing, causing queued
turns to fall back to argv-based prompts; update the spawn call inside
processQueuedItem to include sendPromptViaStdin and sendPromptViaStdinRaw (and
any other flags useInputProcessing computes) using the same logic/values you use
for immediate sends so image-bearing Claude/factory-droid and raw-stdin flows
behave identically for queued messages; apply the same change to the equivalent
spawn block around lines 406-447 so both queued spawn sites mirror
useInputProcessing decisions.
🤖 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/renderer/types/index.ts`:
- Line 771: The AgentCapabilities definition in the renderer hook is missing the
required supportsAppendSystemPrompt flag and types/index.ts marks it optional;
update the hook file (useAgentCapabilities.ts) to add
supportsAppendSystemPrompt: boolean to the AgentCapabilities interface and
include it in DEFAULT_CAPABILITIES, and change the declaration in
src/renderer/types/index.ts so supportsAppendSystemPrompt is a non-optional
boolean to match the main-process canonical type.

---

Outside diff comments:
In `@src/renderer/stores/agentStore.ts`:
- Around line 299-345: processQueuedItem is not passing the stdin-routing flags
set by useInputProcessing, causing queued turns to fall back to argv-based
prompts; update the spawn call inside processQueuedItem to include
sendPromptViaStdin and sendPromptViaStdinRaw (and any other flags
useInputProcessing computes) using the same logic/values you use for immediate
sends so image-bearing Claude/factory-droid and raw-stdin flows behave
identically for queued messages; apply the same change to the equivalent spawn
block around lines 406-447 so both queued spawn sites mirror useInputProcessing
decisions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0a00f751-6fc6-4a36-99fd-3ab7582a8ccf

📥 Commits

Reviewing files that changed from the base of the PR and between 4ae5cc9 and 196779d.

📒 Files selected for processing (16)
  • src/__tests__/main/agents/capabilities.test.ts
  • src/__tests__/renderer/services/inlineWizardConversation.test.ts
  • src/__tests__/renderer/stores/agentStore.test.ts
  • src/__tests__/renderer/utils/spawnHelpers.test.ts
  • src/main/agents/capabilities.ts
  • src/main/ipc/handlers/process.ts
  • src/main/preload/process.ts
  • src/renderer/components/Wizard/services/conversationManager.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/agent/useAgentExecution.ts
  • src/renderer/hooks/agent/useMergeTransferHandlers.ts
  • src/renderer/hooks/input/useInputProcessing.ts
  • src/renderer/services/inlineWizardConversation.ts
  • src/renderer/stores/agentStore.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/spawnHelpers.ts

- Use system prompt as sole content instead of dropping it when
  effectivePrompt is empty on the fallback path
- Log systemPromptDelivery method and effectivePromptLength in
  spawn info log for debugging embedded vs cli-arg delivery
- Add supportsAppendSystemPrompt to renderer hook AgentCapabilities
  interface and DEFAULT_CAPABILITIES for type consistency
- Local supported agent: --append-system-prompt in CLI args, clean prompt
- Local unsupported agent: system prompt embedded in user message
- Local unsupported + empty prompt: system prompt used as sole content
- SSH supported agent: --append-system-prompt in remote exec script
- SSH unsupported agent: system prompt embedded in SSH stdin
- No appendSystemPrompt: flag not added, prompt unchanged
- Fix useAgentCapabilities test fixture for new capability field
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.

🧹 Nitpick comments (2)
src/__tests__/main/ipc/handlers/process.test.ts (2)

1963-1990: Verify edge case: undefined vs empty string for prompt.

The test uses prompt: '' (empty string) to verify the "no user message" branch. However, the handler condition else if (effectivePrompt) would also be falsy for undefined. Consider adding a test case with prompt: undefined to ensure both cases are covered.

💡 Additional test case
it('should use system prompt as sole content when prompt is undefined for unsupported agents', async () => {
	const mockAgent = {
		id: 'codex',
		name: 'Codex',
		requiresPty: false,
		capabilities: {
			supportsAppendSystemPrompt: false,
		},
	};

	mockAgentDetector.getAgent.mockResolvedValue(mockAgent);
	mockProcessManager.spawn.mockReturnValue({ pid: 12345, success: true });

	const handler = handlers.get('process:spawn');
	await handler!({} as any, {
		sessionId: 'session-1',
		toolType: 'codex',
		cwd: '/home/user/project',
		command: 'codex',
		args: [],
		// prompt: undefined (not provided)
		appendSystemPrompt: 'You are Maestro system prompt content',
	});

	const spawnCall = mockProcessManager.spawn.mock.calls[0][0];
	expect(spawnCall.prompt).toBe('You are Maestro system prompt content');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/ipc/handlers/process.test.ts` around lines 1963 - 1990,
Add a new test that mirrors the existing "should use system prompt as sole
content when no user prompt for unsupported agents" but omits the prompt field
(i.e., leave prompt undefined) to exercise the branch where effectivePrompt is
undefined; use the same mockAgent (id 'codex'), mockAgentDetector.getAgent
mockResolvedValue, and mockProcessManager.spawn return, call the
handlers.get('process:spawn') handler with no prompt property and
appendSystemPrompt set, then assert that the spawnCall.prompt (from
mockProcessManager.spawn.mock.calls[0][0]) equals the appendSystemPrompt string.

1882-1891: Consider extracting duplicate mockSshRemote fixture.

This mockSshRemote object duplicates the one defined at line 1158. For test maintainability, consider extracting it to a shared constant at the top of the describe block or in a test fixtures file.

♻️ Suggested refactor
 describe('process IPC handlers', () => {
 	let handlers: Map<string, Function>;
+	// Shared SSH remote fixture for tests
+	const mockSshRemote = {
+		id: 'remote-1',
+		name: 'Dev Server',
+		host: 'dev.example.com',
+		port: 22,
+		username: 'devuser',
+		privateKeyPath: '~/.ssh/id_ed25519',
+		enabled: true,
+		remoteEnv: { REMOTE_VAR: 'remote-value' },
+	};

Then remove the duplicate declarations in SSH remote execution and appendSystemPrompt delivery describe blocks.

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

In `@src/__tests__/main/ipc/handlers/process.test.ts` around lines 1882 - 1891,
Extract the duplicated mockSshRemote fixture into a single shared constant and
reuse it in both tests: define a top-level const mockSshRemote = { id:
'remote-1', name: 'Dev Server', host: 'dev.example.com', port: 22, username:
'devuser', privateKeyPath: '~/.ssh/id_ed25519', enabled: true, remoteEnv: {} }
inside the outer describe (or a fixtures file) and remove the duplicate inline
object declarations from the "SSH remote execution" and "appendSystemPrompt
delivery" describe blocks so both tests reference the same mockSshRemote
constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/main/ipc/handlers/process.test.ts`:
- Around line 1963-1990: Add a new test that mirrors the existing "should use
system prompt as sole content when no user prompt for unsupported agents" but
omits the prompt field (i.e., leave prompt undefined) to exercise the branch
where effectivePrompt is undefined; use the same mockAgent (id 'codex'),
mockAgentDetector.getAgent mockResolvedValue, and mockProcessManager.spawn
return, call the handlers.get('process:spawn') handler with no prompt property
and appendSystemPrompt set, then assert that the spawnCall.prompt (from
mockProcessManager.spawn.mock.calls[0][0]) equals the appendSystemPrompt string.
- Around line 1882-1891: Extract the duplicated mockSshRemote fixture into a
single shared constant and reuse it in both tests: define a top-level const
mockSshRemote = { id: 'remote-1', name: 'Dev Server', host: 'dev.example.com',
port: 22, username: 'devuser', privateKeyPath: '~/.ssh/id_ed25519', enabled:
true, remoteEnv: {} } inside the outer describe (or a fixtures file) and remove
the duplicate inline object declarations from the "SSH remote execution" and
"appendSystemPrompt delivery" describe blocks so both tests reference the same
mockSshRemote constant.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5710295d-fe93-4c09-bcbd-0cf6469e6179

📥 Commits

Reviewing files that changed from the base of the PR and between e5f3244 and 1cf133a.

📒 Files selected for processing (1)
  • src/__tests__/main/ipc/handlers/process.test.ts

@jSydorowicz21
Copy link
Contributor Author

@greptile re-review

…imit

On Windows, CreateProcess has a ~32K char limit. Large system prompts
passed via --append-system-prompt could exceed this. Use
--append-system-prompt-file with a temp file for Windows local execution.

SSH sessions are exempt since the command runs inside a stdin script on
the remote host, not through the Windows CLI. The temp file is cleaned
up 5 seconds after spawn (agent reads it at startup).
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: 3

🤖 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/ipc/handlers/process.ts`:
- Around line 492-499: The current guard prevents building a stream-json payload
on image-only turns because it requires effectivePrompt; update the logic in the
block around stdinInput, hasImages, and
agent?.capabilities?.supportsStreamJsonInput so that when hasImages is true and
the agent supports stream-json you always call buildStreamJsonMessage using
(effectivePrompt ?? '') as the text field (e.g.,
buildStreamJsonMessage(effectivePrompt ?? '', config.images!)), append a
trailing '\n', and still ensure sshArgs includes '--input-format',
'stream-json'; remove the effectivePrompt truthiness check so image-only turns
include images for stream-json agents.
- Around line 229-233: The Windows temp system-prompt file is keyed only by
config.sessionId causing cross-spawn races and silent unlink failures; change
creation to use a unique per-spawn filename (e.g., include process.pid +
timestamp or a UUID) when assigning systemPromptTempFile and add it to
finalArgs, ensure the cleanup routine is tied to that specific spawn (not a
shared session key) and only swallow ENOENT when unlinking while reporting any
other fs errors via the project's error-reporting utility (e.g.,
captureException/processLogger.error); update both occurrences (the write and
the unlink/cleanup logic around systemPromptTempFile) and reference the symbol
names systemPromptTempFile, config.sessionId, finalArgs so the per-spawn
lifecycle is deterministic and non-silent on real failures.
- Around line 225-245: Resolve the SSH remote before deciding how to deliver the
system prompt: move or change the logic that sets isSshSession (currently using
config.sessionSshRemoteConfig?.enabled) to use the resolved result from
getSshRemoteConfig() (or explicitly call getSshRemoteConfig() and await it
before the appendSystemPrompt branch), and if SSH was requested but
getSshRemoteConfig() fails, either fail fast with an error or treat it as a
non-local session consistently; then update the branches that build finalArgs
(the --append-system-prompt-file path using systemPromptTempFile for Windows and
the inline --append-system-prompt path) to base their choice on the resolved SSH
state so the Windows temp-file workaround is used only when truly local and not
when SSH was intended but unavailable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dce40b82-d49a-44cb-b9e3-66b84c25dce5

📥 Commits

Reviewing files that changed from the base of the PR and between 1cf133a and 10fafc7.

📒 Files selected for processing (2)
  • src/__tests__/main/ipc/handlers/process.test.ts
  • src/main/ipc/handlers/process.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/main/ipc/handlers/process.test.ts

@jSydorowicz21
Copy link
Contributor Author

Good to go

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.

♻️ Duplicate comments (3)
src/main/ipc/handlers/process.ts (3)

623-631: ⚠️ Potential issue | 🟠 Major

Surface temp-file cleanup failures.

Line 625 swallows every unlinkSync error. Any non-ENOENT failure leaves a prompt-bearing temp file behind with zero signal. Ignore only ENOENT, and report the rest.

🔧 Minimal fix
-import { addBreadcrumb } from '../../utils/sentry';
+import { addBreadcrumb, captureException } from '../../utils/sentry';
...
 						try {
 							fs.unlinkSync(systemPromptTempFile!);
-						} catch {
-							// Ignore - temp dir cleanup will handle it
+						} catch (error) {
+							if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
+								captureException(error, {
+									operation: 'system-prompt-tempfile-cleanup',
+									sessionId: config.sessionId,
+									tempFile: systemPromptTempFile,
+								});
+							}
 						}

As per coding guidelines "Do not silently swallow errors with try-catch blocks that only log" and "Use Sentry utilities for explicit reporting".

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

In `@src/main/ipc/handlers/process.ts` around lines 623 - 631, The temp-file
cleanup currently swallows all unlinkSync errors; update the setTimeout cleanup
block that references systemPromptTempFile and fs.unlinkSync to only ignore
ENOENT, and for any other error capture/report it (e.g., call
Sentry.captureException(err) and/or processLogger.error with context about
systemPromptTempFile) so failures are surfaced instead of silently swallowed.

498-503: ⚠️ Potential issue | 🟠 Major

Don't require text to send stream-json images over SSH.

Line 500 still drops image-only turns for stream-json agents. With appendSystemPrompt separated, effectivePrompt can be empty, this block is skipped, and the file-based image path is disabled for these agents, so the remote spawn sends no images.

🔧 Minimal fix
-						if (hasImages && effectivePrompt && agent?.capabilities?.supportsStreamJsonInput) {
+						if (hasImages && agent?.capabilities?.supportsStreamJsonInput) {
 							// Stream-json agent (Claude Code): embed images in the stdin message
-							stdinInput = buildStreamJsonMessage(effectivePrompt, config.images!) + '\n';
+							stdinInput = buildStreamJsonMessage(effectivePrompt ?? '', config.images!) + '\n';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/process.ts` around lines 498 - 503, The current branch
skips sending stream-json image payloads when effectivePrompt is empty; change
the condition so that when hasImages and
agent?.capabilities?.supportsStreamJsonInput are true you always construct a
stream-json stdin message (use effectivePrompt || '' when calling
buildStreamJsonMessage) and set stdinInput to that + '\n', rather than requiring
effectivePrompt to be truthy; keep the existing check that adds '--input-format'
to sshArgs unchanged and still reference config.images when building the message
(symbols: effectivePrompt, hasImages,
agent?.capabilities?.supportsStreamJsonInput, stdinInput,
buildStreamJsonMessage, sshArgs, config.images).

225-248: ⚠️ Potential issue | 🟠 Major

Resolve SSH before choosing the Windows system-prompt delivery path.

Line 225 uses sessionSshRemoteConfig.enabled as a proxy for “this spawn will be remote”. If getSshRemoteConfig() later returns no config, this handler falls back to a local Windows spawn after already choosing inline --append-system-prompt, so the temp-file workaround and the local shell path are both skipped. Please branch on the resolved remote, or fail fast when SSH was requested but unavailable.

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

In `@src/main/ipc/handlers/process.ts` around lines 225 - 248, The code currently
uses config.sessionSshRemoteConfig?.enabled to decide Windows vs inline
system-prompt delivery before the actual SSH resolution; change that to branch
on the resolved SSH config (the value returned by getSshRemoteConfig or the
resolvedRemote variable used later) instead of the raw config flag, and if SSH
was requested but getSshRemoteConfig() returns null/undefined, fail fast
(throw/return an error) so you never take the local-Windows temp-file path
incorrectly; update the logic around isSshSession, systemPromptTempFile and
finalArgs (the --append-system-prompt-file vs --append-system-prompt branch) to
use the resolved SSH boolean or error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/ipc/handlers/process.ts`:
- Around line 623-631: The temp-file cleanup currently swallows all unlinkSync
errors; update the setTimeout cleanup block that references systemPromptTempFile
and fs.unlinkSync to only ignore ENOENT, and for any other error capture/report
it (e.g., call Sentry.captureException(err) and/or processLogger.error with
context about systemPromptTempFile) so failures are surfaced instead of silently
swallowed.
- Around line 498-503: The current branch skips sending stream-json image
payloads when effectivePrompt is empty; change the condition so that when
hasImages and agent?.capabilities?.supportsStreamJsonInput are true you always
construct a stream-json stdin message (use effectivePrompt || '' when calling
buildStreamJsonMessage) and set stdinInput to that + '\n', rather than requiring
effectivePrompt to be truthy; keep the existing check that adds '--input-format'
to sshArgs unchanged and still reference config.images when building the message
(symbols: effectivePrompt, hasImages,
agent?.capabilities?.supportsStreamJsonInput, stdinInput,
buildStreamJsonMessage, sshArgs, config.images).
- Around line 225-248: The code currently uses
config.sessionSshRemoteConfig?.enabled to decide Windows vs inline system-prompt
delivery before the actual SSH resolution; change that to branch on the resolved
SSH config (the value returned by getSshRemoteConfig or the resolvedRemote
variable used later) instead of the raw config flag, and if SSH was requested
but getSshRemoteConfig() returns null/undefined, fail fast (throw/return an
error) so you never take the local-Windows temp-file path incorrectly; update
the logic around isSshSession, systemPromptTempFile and finalArgs (the
--append-system-prompt-file vs --append-system-prompt branch) to use the
resolved SSH boolean or error path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 44775e96-dd0e-4934-a5e7-18617dd73683

📥 Commits

Reviewing files that changed from the base of the PR and between 1cf133a and 2acfd1d.

📒 Files selected for processing (2)
  • src/__tests__/main/ipc/handlers/process.test.ts
  • src/main/ipc/handlers/process.ts
✅ Files skipped from review due to trivial changes (1)
  • src/tests/main/ipc/handlers/process.test.ts

@pedramamini
Copy link
Collaborator

Code Review — PR #596

@jSydorowicz21 — solid PR overall. Clean separation of system prompt from user message, good test coverage, and sensible Windows temp-file workaround. Three issues worth addressing:


🔴 Critical: processQueuedItem missing stdin routing flags

src/renderer/stores/agentStore.ts — Both spawn calls in processQueuedItem (message path ~line 340 and command path ~line 440) are missing sendPromptViaStdin / sendPromptViaStdinRaw. Every other spawn site in this PR calls getStdinFlags() and forwards the result.

Queued messages are common (any message sent while the agent is busy). On Windows, the first turn goes through stdin correctly, but queued turns silently regress to CLI-arg delivery — re-introducing the exact length/escaping bugs this PR was written to fix.

Fix: Import getStdinFlags and compute + spread the flags in both spawn calls, same pattern as useInputProcessing.


🟡 Important: ProcessConfig in preload missing new fields

src/main/preload/process.ts — The preload ProcessConfig interface doesn't declare sendPromptViaStdin or sendPromptViaStdinRaw. The global.d.ts version does (lines 48-49), so it works at runtime, but the preload type is the canonical export. Type drift waiting to happen.

Fix: Add both optional booleans to the preload ProcessConfig.


🟡 Important: Temp file cleanup swallows all errors

src/main/ipc/handlers/process.ts:625-631 — The catch {} on fs.unlinkSync(systemPromptTempFile!) silently swallows everything. Per project guidelines, only ENOENT should be ignored — other failures (permissions, disk full) should hit Sentry so you know a prompt-bearing file is stuck on disk.

// Current
} catch {
    // Ignore - temp dir cleanup will handle it
}

// Should be
} catch (error) {
    if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
        captureException(error, {
            operation: 'system-prompt-tempfile-cleanup',
            sessionId: config.sessionId,
        });
    }
}

CodeRabbit findings I dismissed as false positives

  • "SSH resolution happens after system prompt delivery" — Not an issue. The renderer controls stdin flags; the main process gates the Windows path with !config.sessionSshRemoteConfig?.enabled.
  • "Image-only turns dropped for stream-json" — Safe today because DEFAULT_IMAGE_ONLY_PROMPT is always set for image-only turns in the renderer. Fragile but not a current bug.

Issue #1 is the one I'd fix before merge — it's a real regression on Windows for queued messages. #2 and #3 are worth fixing but not blockers.

- Pass sendPromptViaStdin/sendPromptViaStdinRaw to both spawn calls in
  agentStore processQueuedItem (message and command branches)
- Add sendPromptViaStdin/sendPromptViaStdinRaw to ProcessConfig interface
  in preload/process.ts for type safety
- Replace bare catch {} in temp file cleanup with ENOENT-only ignore
  and captureException for unexpected errors
- cue-executor: capture sshStdinScript from wrapSpawnWithSsh and write
  it to stdin for SSH large-prompt mode (was silently dropping prompts)
- global.d.ts: add supportsAppendSystemPrompt to both AgentCapabilities
  interfaces for type safety through the preload bridge
- process.ts: move temp file cleanup to creation site with 30s safety
  net so the file is removed even if spawn throws
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.

🧹 Nitpick comments (2)
src/main/ipc/handlers/process.ts (2)

516-520: Consider future-proofing the image-only turn handling.

The condition at line 518 requires effectivePrompt to be truthy. If an image-only turn arrives with an empty prompt and no system prompt, the stream-json payload won't be built for stream-json agents.

As noted in the review discussion, this is currently safe because DEFAULT_IMAGE_ONLY_PROMPT is always set by the renderer. However, a defensive check using effectivePrompt ?? '' would make this more robust against future changes.

♻️ Optional defensive improvement
-if (hasImages && effectivePrompt && agent?.capabilities?.supportsStreamJsonInput) {
+if (hasImages && agent?.capabilities?.supportsStreamJsonInput) {
 	// Stream-json agent (Claude Code): embed images in the stdin message
-	stdinInput = buildStreamJsonMessage(effectivePrompt, config.images!) + '\n';
+	stdinInput = buildStreamJsonMessage(effectivePrompt ?? '', config.images!) + '\n';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/process.ts` around lines 516 - 520, The current branch
that builds the stream-json payload short-circuits when effectivePrompt is
falsy, so for image-only turns the payload isn't created for stream-json agents;
update the logic around
effectivePrompt/hasImages/agent?.capabilities?.supportsStreamJsonInput in the
handler (references: effectivePrompt, hasImages,
agent?.capabilities?.supportsStreamJsonInput, buildStreamJsonMessage,
stdinInput) to use a defensive default (e.g. pass effectivePrompt ?? '' into
buildStreamJsonMessage and set stdinInput from effectivePrompt ?? '') so
image-only turns still produce a stream-json stdin message.

244-250: Add sessionId to the Sentry exception context.

Per the reviewer's recommendation in PR objectives, including sessionId in the captureException context would improve debuggability for problematic temp file cleanup failures.

♻️ Suggested improvement
 captureException(
 	cleanupErr instanceof Error ? cleanupErr : new Error(String(cleanupErr)),
 	{
 		context: 'systemPromptTempFile cleanup (safety)',
 		file: tempFileToClean,
+		sessionId: config.sessionId,
 	}
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/process.ts` around lines 244 - 250, The Sentry capture
call for temp file cleanup (captureException) is missing the sessionId in its
context; update the context object passed to captureException (in the process.ts
handler where cleanupErr and tempFileToClean are handled) to also include
sessionId so the payload contains { context: 'systemPromptTempFile cleanup
(safety)', file: tempFileToClean, sessionId: sessionId } (use the existing
variable that holds the current session id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/ipc/handlers/process.ts`:
- Around line 516-520: The current branch that builds the stream-json payload
short-circuits when effectivePrompt is falsy, so for image-only turns the
payload isn't created for stream-json agents; update the logic around
effectivePrompt/hasImages/agent?.capabilities?.supportsStreamJsonInput in the
handler (references: effectivePrompt, hasImages,
agent?.capabilities?.supportsStreamJsonInput, buildStreamJsonMessage,
stdinInput) to use a defensive default (e.g. pass effectivePrompt ?? '' into
buildStreamJsonMessage and set stdinInput from effectivePrompt ?? '') so
image-only turns still produce a stream-json stdin message.
- Around line 244-250: The Sentry capture call for temp file cleanup
(captureException) is missing the sessionId in its context; update the context
object passed to captureException (in the process.ts handler where cleanupErr
and tempFileToClean are handled) to also include sessionId so the payload
contains { context: 'systemPromptTempFile cleanup (safety)', file:
tempFileToClean, sessionId: sessionId } (use the existing variable that holds
the current session id).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35a7b46d-d9eb-453f-b325-1ff9854ad630

📥 Commits

Reviewing files that changed from the base of the PR and between e91e4e6 and f3ceeeb.

📒 Files selected for processing (1)
  • src/main/ipc/handlers/process.ts

@jSydorowicz21
Copy link
Contributor Author

@greptile give this a re-review

}
}
}, 30_000);
finalArgs = [...finalArgs, '--append-system-prompt-file', systemPromptTempFile];
Copy link

Choose a reason for hiding this comment

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

P1 --append-system-prompt-file is not a documented Claude Code flag

The Windows code path passes --append-system-prompt-file <tempfilepath> to Claude Code, but this flag is not documented anywhere in the codebase and does not appear to be a real Claude Code CLI flag. The capability comment (supportsAppendSystemPrompt: true, // --append-system-prompt flag) only references --append-system-prompt.

If Claude Code does not support --append-system-prompt-file, it will either:

  1. Fail to start with an "unknown option" error (breaking all Windows Claude Code sessions), or
  2. Silently ignore the flag (delivering no system prompt to the agent)

The unit test at process.test.ts:1922 only checks that the arg is present in the spawned args array — it does not actually invoke the Claude Code binary to verify the flag is accepted.

Before merging, please confirm that --append-system-prompt-file is a supported Claude Code CLI flag (e.g., by checking claude --help output or the Claude Code changelog). If it is not supported, consider alternatives such as sending the system prompt via a prepended stdin message (before the user prompt), similar to how the SSH path inlines it.

Comment on lines +349 to +357
const appendPromptIdx = finalArgs.indexOf('--append-system-prompt');
const argsToLog =
appendPromptIdx !== -1
? [
...finalArgs.slice(0, appendPromptIdx + 1),
`<${finalArgs[appendPromptIdx + 1]?.length ?? 0} chars>`,
...finalArgs.slice(appendPromptIdx + 2),
]
: finalArgs;
Copy link

Choose a reason for hiding this comment

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

P2 Log redaction does not cover --append-system-prompt-file args

finalArgs.indexOf('--append-system-prompt') uses exact element matching on the array. On Windows, the args contain '--append-system-prompt-file' (a distinct string), so appendPromptIdx will always be -1 and argsToLog will equal finalArgs — including the temp file path.

The system prompt content itself is not in the args (it's in the file), so there is no sensitive data being logged. However, it may be worth extending the check to also redact the file path on Windows for consistency:

const appendPromptIdx =
  finalArgs.indexOf('--append-system-prompt') !== -1
    ? finalArgs.indexOf('--append-system-prompt')
    : finalArgs.indexOf('--append-system-prompt-file');

@jSydorowicz21
Copy link
Contributor Author

@pedramamini good to go, two remaining greptile feedbacks are confirm the flag exists (it does) and potentially redact the path but I believe that's important info

@pedramamini pedramamini merged commit f6abf93 into RunMaestro:rc Mar 25, 2026
4 checks passed
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.

2 participants