@@ -103,6 +103,7 @@ import {
103103 loadState,
104104 saveState,
105105 type ConnectedMcpServer,
106+ type RunState,
106107} from "../session/state.js";
107108import { resolveExecRunStatus, type RunSink } from "../session/run-sink.js";
108109import { createRunSummary } from "../session/hooks.js";
@@ -452,23 +453,57 @@ export async function runExec(config: Config): Promise<ExecResult> {
452453 provider: config.providerName,
453454 model: config.model,
454455 };
455- liveTelemetry.capture(
456- "session_end",
457- execSessionEndProperties(result, Date.now(), 0),
458- );
456+ // Deliberate funnel gap: a missing prompt is a usage error (exit 2) and
457+ // no run ever started, so there is no session to close. Emitting a
458+ // failed session_end here would pollute failed counts with invocations
459+ // that never ran.
459460 return result;
460461 }
461462
462463 const sessionId =
463464 config.sessionId.length > 0 ? config.sessionId : generateSessionId();
464465 const startedAt = Date.now();
465466 const workdir = sessionContextDir(config.cwd, sessionId);
466- await initSessionDir(config.cwd, sessionId);
467- const prior =
468- config.sessionId.length > 0
469- ? await loadState(config.cwd, config.sessionId)
470- : undefined;
471- const priorState = prior?.kind === "ok" ? prior.state : undefined;
467+ // Setup runs before the main try below: initSessionDir/loadState hit disk
468+ // before any session_end coverage exists, so an EACCES/EROFS here would
469+ // reject with zero session_end and orphan the cli_start funnel. Emit a
470+ // minimal failed session_end on this window instead of letting it throw.
471+ let priorState: RunState | undefined;
472+ try {
473+ await initSessionDir(config.cwd, sessionId);
474+ const prior =
475+ config.sessionId.length > 0
476+ ? await loadState(config.cwd, config.sessionId)
477+ : undefined;
478+ priorState = prior?.kind === "ok" ? prior.state : undefined;
479+ } catch (err) {
480+ const message = formatCaughtError(err);
481+ logger.error("exec setup failed: {error}", { error: message });
482+ stderr.write(`Error: ${message}\n`);
483+ liveTelemetry.capture(
484+ "session_end",
485+ execSessionEndProperties(undefined, startedAt, 0),
486+ );
487+ return {
488+ exitCode: 1,
489+ sessionId,
490+ text: "",
491+ error: message,
492+ status: "failed",
493+ durationMs: Date.now() - startedAt,
494+ turnsUsed: 0,
495+ toolCallCount: 0,
496+ tokenUsage: {
497+ input: 0,
498+ output: 0,
499+ cacheRead: 0,
500+ cacheWrite: 0,
501+ thinking: 0,
502+ },
503+ provider: config.providerName,
504+ model: config.model,
505+ };
506+ }
472507
473508 let connectedMcp: ConnectedMcpServer[] = [];
474509 let agent: Agent | null = null;
0 commit comments