Skip to content

Commit 4f6be66

Browse files
committed
fix(engines): improve error handling for CLI execution failures
- Skip empty memory entries silently to allow real errors to propagate - Simplify error messages for required file and workflow execution errors - Improve text wrapping logic to preserve line structure - Enhance CLI runner error detection to handle cases where processes exit with code 0 but contain errors - Add error tracking for JSON error events in engine providers - Remove redundant debug logging and improve error message formatting
1 parent 2b87664 commit 4f6be66

File tree

10 files changed

+162
-115
lines changed

10 files changed

+162
-115
lines changed

src/agents/memory/memory-store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export class MemoryStore {
1717

1818
async append(entry: MemoryEntry): Promise<void> {
1919
const normalized = this.normalizeEntry(entry);
20+
// Skip empty content silently - allows real errors to propagate
21+
if (!normalized.content.trim()) {
22+
return;
23+
}
2024
this.ensureRequiredFields(normalized);
2125
await this.adapter.append(normalized);
2226
this.adapter.analytics?.onAppend?.({ agentId: normalized.agentId, entry: normalized, source: this.storeSource });

src/cli/tui/routes/workflow/components/modals/error-modal.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,8 @@ export function ErrorModal(props: ErrorModalProps) {
1919
const dimensions = useTerminalDimensions()
2020

2121
const modalWidth = () => {
22-
const safeWidth = Math.max(50, (dimensions()?.width ?? 80) - 8)
23-
return Math.min(safeWidth, 80)
24-
}
25-
26-
const modalHeight = () => {
27-
const safeHeight = Math.max(15, (dimensions()?.height ?? 30) - 6)
28-
return Math.min(safeHeight, 25)
22+
const safeWidth = Math.max(40, (dimensions()?.width ?? 80) - 8)
23+
return Math.min(safeWidth, 70)
2924
}
3025

3126
useKeyboard((evt) => {
@@ -36,9 +31,6 @@ export function ErrorModal(props: ErrorModalProps) {
3631
}
3732
})
3833

39-
// Calculate content height (modal height minus header, footer, padding)
40-
const contentHeight = () => modalHeight() - 7
41-
4234
return (
4335
<ModalBase width={modalWidth()}>
4436
<ModalHeader title="Workflow Error" icon="!" iconColor={themeCtx.theme.error} />
@@ -47,10 +39,9 @@ export function ErrorModal(props: ErrorModalProps) {
4739
paddingRight={2}
4840
paddingTop={1}
4941
paddingBottom={1}
50-
height={contentHeight()}
51-
overflow="scroll"
5242
>
5343
<text fg={themeCtx.theme.error}>{props.message}</text>
44+
<text>{"\n\n"}Report issues: https://github.com/moazbuilds/CodeMachine-CLI/issues</text>
5445
</box>
5546
<box flexDirection="row" justifyContent="center" paddingBottom={1}>
5647
<box

src/cli/tui/shared/utils/text.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,12 @@ export function truncate(str: string, maxLength: number): string {
1111
}
1212

1313
/**
14-
* Wrap text to fit within a specified width
15-
*
16-
* @param text - Text to wrap
17-
* @param width - Maximum line width
18-
* @returns Array of wrapped lines
14+
* Wrap a single line of text to fit within a specified width
1915
*/
20-
export function wrapText(text: string, width: number): string[] {
21-
const words = text.split(" ")
16+
function wrapLine(line: string, width: number): string[] {
17+
if (line.length <= width) return [line]
18+
19+
const words = line.split(" ")
2220
const lines: string[] = []
2321
let currentLine = ""
2422

@@ -34,6 +32,29 @@ export function wrapText(text: string, width: number): string[] {
3432
return lines
3533
}
3634

35+
/**
36+
* Wrap text to fit within a specified width
37+
*
38+
* @param text - Text to wrap
39+
* @param width - Maximum line width
40+
* @returns Array of wrapped lines
41+
*/
42+
export function wrapText(text: string, width: number): string[] {
43+
// First split by newlines to preserve line structure
44+
const inputLines = text.split("\n")
45+
const result: string[] = []
46+
47+
for (const line of inputLines) {
48+
if (line === "") {
49+
result.push("")
50+
} else {
51+
result.push(...wrapLine(line, width))
52+
}
53+
}
54+
55+
return result
56+
}
57+
3758
/**
3859
* Repeat a character to create a line
3960
*/

src/infra/engines/providers/ccr/execution/runner.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ export async function runCcr(options: RunCcrOptions): Promise<RunCcrResult> {
161161
// Create telemetry capture instance
162162
const telemetryCapture = createTelemetryCapture('claude', model, prompt, workingDir); // Using 'claude' for telemetry since API is the same
163163

164+
// Track JSON error events (CCR may exit 0 even on errors)
165+
let capturedError: string | null = null;
166+
164167
let result;
165168
try {
166169
result = await spawnProcess({
@@ -182,6 +185,22 @@ export async function runCcr(options: RunCcrOptions): Promise<RunCcrResult> {
182185
// Capture telemetry data
183186
telemetryCapture.captureFromStreamJson(line);
184187

188+
// Check for error events (CCR may exit 0 even on errors like invalid model)
189+
try {
190+
const json = JSON.parse(line);
191+
// Check for error in result type
192+
if (json.type === 'result' && json.is_error && json.result && !capturedError) {
193+
capturedError = json.result;
194+
}
195+
// Check for error in assistant message
196+
if (json.type === 'assistant' && json.error && !capturedError) {
197+
const messageText = json.message?.content?.[0]?.text;
198+
capturedError = messageText || json.error;
199+
}
200+
} catch {
201+
// Ignore parse errors
202+
}
203+
185204
const formatted = formatStreamJsonLine(line);
186205
if (formatted) {
187206
onData?.(formatted + '\n');
@@ -212,7 +231,7 @@ export async function runCcr(options: RunCcrOptions): Promise<RunCcrResult> {
212231
throw error;
213232
}
214233

215-
if (result.exitCode !== 0) {
234+
if (result.exitCode !== 0 || capturedError) {
216235
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';
217236
const lines = errorOutput.split('\n').slice(0, 10);
218237

@@ -222,7 +241,9 @@ export async function runCcr(options: RunCcrOptions): Promise<RunCcrResult> {
222241
command: `${command} ${args.join(' ')}`,
223242
});
224243

225-
throw new Error(`CCR CLI exited with code ${result.exitCode}`);
244+
// Use captured JSON error if available, otherwise fall back to generic message
245+
const errorMessage = capturedError || `CCR CLI exited with code ${result.exitCode}`;
246+
throw new Error(errorMessage);
226247
}
227248

228249
// Log captured telemetry

src/infra/engines/providers/claude/execution/runner.ts

Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -173,18 +173,12 @@ export async function runClaude(options: RunClaudeOptions): Promise<RunClaudeRes
173173

174174
const { command, args } = buildClaudeExecCommand({ workingDir, prompt, model });
175175

176-
// Debug logging when LOG_LEVEL=debug
177-
if (process.env.LOG_LEVEL === 'debug') {
178-
console.error(`[DEBUG] Claude runner - prompt length: ${prompt.length}, lines: ${prompt.split('\n').length}`);
179-
console.error(`[DEBUG] Claude runner - args: ${args.join(' ')}, model: ${model ?? 'default'}`);
180-
console.error(`[DEBUG] Claude runner - command: ${command} ${args.join(' ')}`);
181-
console.error(`[DEBUG] Claude runner - working dir: ${workingDir}`);
182-
console.error(`[DEBUG] Claude runner - prompt preview: ${prompt.substring(0, 200)}...`);
183-
}
184-
185176
// Create telemetry capture instance
186177
const telemetryCapture = createTelemetryCapture('claude', model, prompt, workingDir);
187178

179+
// Track JSON error events (Claude may exit 0 even on errors)
180+
let capturedError: string | null = null;
181+
188182
let result;
189183
try {
190184
result = await spawnProcess({
@@ -206,6 +200,22 @@ export async function runClaude(options: RunClaudeOptions): Promise<RunClaudeRes
206200
// Capture telemetry data
207201
telemetryCapture.captureFromStreamJson(line);
208202

203+
// Check for error events (Claude may exit 0 even on errors like invalid model)
204+
try {
205+
const json = JSON.parse(line);
206+
// Check for error in result type
207+
if (json.type === 'result' && json.is_error && json.result && !capturedError) {
208+
capturedError = json.result;
209+
}
210+
// Check for error in assistant message
211+
if (json.type === 'assistant' && json.error && !capturedError) {
212+
const messageText = json.message?.content?.[0]?.text;
213+
capturedError = messageText || json.error;
214+
}
215+
} catch {
216+
// Ignore parse errors
217+
}
218+
209219
// Emit telemetry event if captured and callback provided
210220
if (onTelemetry) {
211221
const captured = telemetryCapture.getCaptured();
@@ -244,52 +254,53 @@ export async function runClaude(options: RunClaudeOptions): Promise<RunClaudeRes
244254
const message = err?.message ?? '';
245255
const notFound = err?.code === 'ENOENT' || /not recognized as an internal or external command/i.test(message) || /command not found/i.test(message);
246256
if (notFound) {
247-
const full = `${command} ${args.join(' ')}`.trim();
248257
const install = metadata.installCommand;
249258
const name = metadata.name;
250-
console.error(`[ERROR] ${name} CLI not found when executing: ${full}`);
251259
throw new Error(`'${command}' is not available on this system. Please install ${name} first:\n ${install}`);
252260
}
253261
throw error;
254262
}
255263

256-
if (result.exitCode !== 0) {
257-
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';
258-
259-
// Parse the error to provide a user-friendly message
260-
let errorMessage = `Claude CLI exited with code ${result.exitCode}`;
261-
try {
262-
// Try to parse stream-json output for specific error messages
263-
const lines = errorOutput.split('\n');
264-
for (const line of lines) {
265-
if (line.trim() && line.startsWith('{')) {
266-
const json = JSON.parse(line);
267-
268-
// Check for error in result type
269-
if (json.type === 'result' && json.is_error && json.result) {
270-
errorMessage = json.result;
271-
break;
272-
}
264+
// Check for errors - Claude may exit with code 0 even on errors (e.g., invalid model)
265+
if (result.exitCode !== 0 || capturedError) {
266+
// Use captured error from streaming, or fall back to parsing output
267+
let errorMessage = capturedError || `Claude CLI exited with code ${result.exitCode}`;
268+
269+
if (!capturedError) {
270+
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';
271+
try {
272+
// Try to parse stream-json output for specific error messages
273+
const lines = errorOutput.split('\n');
274+
for (const line of lines) {
275+
if (line.trim() && line.startsWith('{')) {
276+
const json = JSON.parse(line);
277+
278+
// Check for error in result type
279+
if (json.type === 'result' && json.is_error && json.result) {
280+
errorMessage = json.result;
281+
break;
282+
}
273283

274-
// Check for error in assistant message
275-
if (json.type === 'assistant' && json.error) {
276-
const messageText = json.message?.content?.[0]?.text;
277-
if (messageText) {
278-
errorMessage = messageText;
279-
} else if (json.error === 'rate_limit') {
280-
errorMessage = 'Rate limit reached. Please try again later.';
281-
} else {
282-
errorMessage = json.error;
284+
// Check for error in assistant message
285+
if (json.type === 'assistant' && json.error) {
286+
const messageText = json.message?.content?.[0]?.text;
287+
if (messageText) {
288+
errorMessage = messageText;
289+
} else if (json.error === 'rate_limit') {
290+
errorMessage = 'Rate limit reached. Please try again later.';
291+
} else {
292+
errorMessage = json.error;
293+
}
294+
break;
283295
}
284-
break;
285296
}
286297
}
287-
}
288-
} catch {
289-
// If parsing fails, use the raw error output
290-
const lines = errorOutput.split('\n').slice(0, 10);
291-
if (lines.length > 0 && lines[0]) {
292-
errorMessage = lines.join('\n');
298+
} catch {
299+
// If parsing fails, use the raw error output
300+
const lines = errorOutput.split('\n').slice(0, 10);
301+
if (lines.length > 0 && lines[0]) {
302+
errorMessage = lines.join('\n');
303+
}
293304
}
294305
}
295306

src/infra/engines/providers/codex/execution/runner.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ export async function runCodex(options: RunCodexOptions): Promise<RunCodexResult
161161
// Create telemetry capture instance
162162
const telemetryCapture = createTelemetryCapture('codex', model, prompt, workingDir);
163163

164+
// Track JSON error events (Codex may exit 0 even on errors)
165+
let capturedError: string | null = null;
166+
164167
let result;
165168
try {
166169
result = await spawnProcess({
@@ -182,7 +185,7 @@ export async function runCodex(options: RunCodexOptions): Promise<RunCodexResult
182185
// Capture telemetry data
183186
telemetryCapture.captureFromStreamJson(line);
184187

185-
// Capture session_id from thread.started event
188+
// Capture session_id from thread.started event and check for errors
186189
try {
187190
const json = JSON.parse(line);
188191
if (json.type === 'thread.started' && json.thread_id) {
@@ -191,6 +194,13 @@ export async function runCodex(options: RunCodexOptions): Promise<RunCodexResult
191194
onSessionId(json.thread_id);
192195
}
193196
}
197+
// Capture error events (Codex exits 0 even on errors like invalid model)
198+
if (json.type === 'error' && json.message && !capturedError) {
199+
capturedError = json.message;
200+
}
201+
if (json.type === 'turn.failed' && json.error?.message && !capturedError) {
202+
capturedError = json.error.message;
203+
}
194204
} catch {
195205
// Ignore parse errors
196206
}
@@ -233,27 +243,19 @@ export async function runCodex(options: RunCodexOptions): Promise<RunCodexResult
233243
const message = err?.message ?? '';
234244
const notFound = err?.code === 'ENOENT' || /not recognized as an internal or external command/i.test(message) || /command not found/i.test(message);
235245
if (notFound) {
236-
const full = `${command} ${args.join(' ')}`.trim();
237246
const install = metadata.installCommand;
238247
const name = metadata.name;
239-
console.error(`[ERROR] ${name} CLI not found when executing: ${full}`);
240248
throw new Error(`'${command}' is not available on this system. Please install ${name} first:\n ${install}`);
241249
}
242250
throw error;
243251
}
244252

245-
if (result.exitCode !== 0) {
246-
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';
253+
// Check for errors - Codex may exit with code 0 even on errors (e.g., invalid model)
254+
if (result.exitCode !== 0 || capturedError) {
255+
const errorOutput = capturedError || result.stderr.trim() || result.stdout.trim() || 'no error output';
247256
const lines = errorOutput.split('\n').slice(0, 10);
248257
const preview = lines.join('\n');
249-
250-
console.error('[ERROR] Codex CLI execution failed', {
251-
exitCode: result.exitCode,
252-
error: preview,
253-
command: `${command} ${args.join(' ')}`,
254-
});
255-
256-
throw new Error(`Codex CLI exited with code ${result.exitCode}\n\n${preview}`);
258+
throw new Error(preview);
257259
}
258260

259261
// Log captured telemetry

src/infra/engines/providers/cursor/execution/runner.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,8 @@ export async function runCursor(options: RunCursorOptions): Promise<RunCursorRes
241241
const message = err?.message ?? '';
242242
const notFound = err?.code === 'ENOENT' || /not recognized as an internal or external command/i.test(message) || /command not found/i.test(message);
243243
if (notFound) {
244-
const full = `${command} ${args.join(' ')}`.trim();
245244
const install = metadata.installCommand;
246245
const name = metadata.name;
247-
console.error(`[ERROR] ${name} CLI not found when executing: ${full}`);
248246
throw new Error(`'${command}' is not available on this system. Please install ${name} first:\n ${install}`);
249247
}
250248
throw error;
@@ -253,14 +251,8 @@ export async function runCursor(options: RunCursorOptions): Promise<RunCursorRes
253251
if (result.exitCode !== 0) {
254252
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';
255253
const lines = errorOutput.split('\n').slice(0, 10);
256-
257-
console.error('[ERROR] Cursor CLI execution failed', {
258-
exitCode: result.exitCode,
259-
error: lines.join('\n'),
260-
command: `${command} ${args.join(' ')}`,
261-
});
262-
263-
throw new Error(`Cursor CLI exited with code ${result.exitCode}`);
254+
const preview = lines.join('\n');
255+
throw new Error(preview);
264256
}
265257

266258
return {

0 commit comments

Comments
 (0)