Skip to content

Commit fe2e87f

Browse files
committed
fix(telemetry): close exec funnel gaps without counting usage errors
Pre-try setup failures and unconfigured exec now emit a failed session_end so every cli_start(surface exec) is paired. Missing-prompt usage errors stay emission-free: exit 2 with no run is not a session.
1 parent 26499bc commit fe2e87f

3 files changed

Lines changed: 298 additions & 34 deletions

File tree

‎src/exec/runner.ts‎

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import {
103103
loadState,
104104
saveState,
105105
type ConnectedMcpServer,
106+
type RunState,
106107
} from "../session/state.js";
107108
import { resolveExecRunStatus, type RunSink } from "../session/run-sink.js";
108109
import { 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;

‎src/index.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ export async function mainWithRunners(
9494
"No provider configured. Run `corbits` (interactive) once to complete setup, " +
9595
"or pass --provider / --model with credentials.\n",
9696
);
97+
// cli_start (surface exec) already emitted above while runExec never
98+
// runs on this branch — emit a minimal failed session_end so the
99+
// funnel stays paired instead of orphaning the start.
100+
const { execSessionEndProperties } = await import("./exec/runner.js");
101+
getTelemetry().capture(
102+
"session_end",
103+
execSessionEndProperties(undefined, Date.now(), 0),
104+
);
97105
exitCode = 2;
98106
} else {
99107
exitCode = await runners.runOnboarding(config);

0 commit comments

Comments
 (0)