@stage-labs/metro 0.1.0-beta.300 → 0.1.0-beta.301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.300",
3
+ "version": "0.1.0-beta.301",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.300",
3
+ "version": "0.1.0-beta.301",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -39,8 +39,11 @@ export type RunnerFailureCode = keyof typeof FAILURE;
39
39
  const ERROR_CODES = ['ENOENT', 'EACCES', 'EPERM', 'ENOEXEC', 'ENOMEM', 'ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND', 'ERR_DLOPEN_FAILED', 'ECONNREFUSED', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'CERT_HAS_EXPIRED', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'] as const;
40
40
  const SIGNALS = ['SIGTERM', 'SIGKILL', 'SIGSEGV', 'SIGABRT', 'SIGBUS', 'SIGILL', 'SIGINT', 'SIGHUP', 'SIGPIPE'] as const;
41
41
 
42
+ const ERROR_NAME_RE = /^[A-Z][A-Za-z0-9]{0,39}Error$/;
43
+
42
44
  export interface RunnerDiagnostic {
43
45
  source: 'exception' | 'stderr';
46
+ errorName?: string;
44
47
  errorCode?: (typeof ERROR_CODES)[number];
45
48
  httpStatus?: number;
46
49
  exitCode?: number;
@@ -49,14 +52,18 @@ export interface RunnerDiagnostic {
49
52
 
50
53
  const boundedNumber = (value: unknown, min: number, max: number): number | undefined => typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max ? value : undefined;
51
54
 
55
+ const errorNameOf = (value: unknown): string | undefined => typeof value === 'string' && value !== 'Error' && ERROR_NAME_RE.test(value) ? value : undefined;
56
+
52
57
  export function runnerDiagnostic(raw: unknown): RunnerDiagnostic | undefined {
53
58
  if (!isRecord(raw) || (raw.source !== 'exception' && raw.source !== 'stderr')) return undefined;
54
59
  const errorCode = ERROR_CODES.find((code) => code === raw.errorCode);
55
60
  const signal = SIGNALS.find((value) => value === raw.signal);
56
61
  const httpStatus = boundedNumber(raw.httpStatus, 400, 599);
57
62
  const exitCode = boundedNumber(raw.exitCode, 0, 255);
63
+ const errorName = errorNameOf(raw.errorName);
58
64
  return {
59
65
  source: raw.source,
66
+ ...(errorName === undefined ? {} : { errorName }),
60
67
  ...(errorCode === undefined ? {} : { errorCode }),
61
68
  ...(httpStatus === undefined ? {} : { httpStatus }),
62
69
  ...(exitCode === undefined ? {} : { exitCode }),
@@ -68,7 +75,7 @@ export function runnerFailureSummary(code: RunnerFailureCode, tool: string | nul
68
75
  const summary = code === 'tool_error' && name(tool) !== null ? `${tool} failed. See its result in Conversations.` : FAILURE[code];
69
76
  const safe = runnerDiagnostic(diagnostic);
70
77
  if (safe === undefined) return summary;
71
- const details = [safe.errorCode, safe.httpStatus === undefined ? null : `HTTP ${safe.httpStatus}`, safe.exitCode === undefined ? null : `exit ${safe.exitCode}`, safe.signal].filter((value) => value !== undefined && value !== null);
78
+ const details = [safe.errorName, safe.errorCode, safe.httpStatus === undefined ? null : `HTTP ${safe.httpStatus}`, safe.exitCode === undefined ? null : `exit ${safe.exitCode}`, safe.signal].filter((value) => value !== undefined && value !== null);
72
79
  return details.length === 0 ? summary : `${summary} Observed: ${details.join(', ')} (${safe.source === 'stderr' ? 'startup stderr' : 'exception'}).`;
73
80
  }
74
81
 
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.300"
2
+ "version": "0.1.0-beta.301"
3
3
  }
@@ -39,8 +39,11 @@ export type RunnerFailureCode = keyof typeof FAILURE;
39
39
  const ERROR_CODES = ['ENOENT', 'EACCES', 'EPERM', 'ENOEXEC', 'ENOMEM', 'ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND', 'ERR_DLOPEN_FAILED', 'ECONNREFUSED', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'CERT_HAS_EXPIRED', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'] as const;
40
40
  const SIGNALS = ['SIGTERM', 'SIGKILL', 'SIGSEGV', 'SIGABRT', 'SIGBUS', 'SIGILL', 'SIGINT', 'SIGHUP', 'SIGPIPE'] as const;
41
41
 
42
+ const ERROR_NAME_RE = /^[A-Z][A-Za-z0-9]{0,39}Error$/;
43
+
42
44
  export interface RunnerDiagnostic {
43
45
  source: 'exception' | 'stderr';
46
+ errorName?: string;
44
47
  errorCode?: (typeof ERROR_CODES)[number];
45
48
  httpStatus?: number;
46
49
  exitCode?: number;
@@ -49,14 +52,18 @@ export interface RunnerDiagnostic {
49
52
 
50
53
  const boundedNumber = (value: unknown, min: number, max: number): number | undefined => typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max ? value : undefined;
51
54
 
55
+ const errorNameOf = (value: unknown): string | undefined => typeof value === 'string' && value !== 'Error' && ERROR_NAME_RE.test(value) ? value : undefined;
56
+
52
57
  export function runnerDiagnostic(raw: unknown): RunnerDiagnostic | undefined {
53
58
  if (!isRecord(raw) || (raw.source !== 'exception' && raw.source !== 'stderr')) return undefined;
54
59
  const errorCode = ERROR_CODES.find((code) => code === raw.errorCode);
55
60
  const signal = SIGNALS.find((value) => value === raw.signal);
56
61
  const httpStatus = boundedNumber(raw.httpStatus, 400, 599);
57
62
  const exitCode = boundedNumber(raw.exitCode, 0, 255);
63
+ const errorName = errorNameOf(raw.errorName);
58
64
  return {
59
65
  source: raw.source,
66
+ ...(errorName === undefined ? {} : { errorName }),
60
67
  ...(errorCode === undefined ? {} : { errorCode }),
61
68
  ...(httpStatus === undefined ? {} : { httpStatus }),
62
69
  ...(exitCode === undefined ? {} : { exitCode }),
@@ -68,7 +75,7 @@ export function runnerFailureSummary(code: RunnerFailureCode, tool: string | nul
68
75
  const summary = code === 'tool_error' && name(tool) !== null ? `${tool} failed. See its result in Conversations.` : FAILURE[code];
69
76
  const safe = runnerDiagnostic(diagnostic);
70
77
  if (safe === undefined) return summary;
71
- const details = [safe.errorCode, safe.httpStatus === undefined ? null : `HTTP ${safe.httpStatus}`, safe.exitCode === undefined ? null : `exit ${safe.exitCode}`, safe.signal].filter((value) => value !== undefined && value !== null);
78
+ const details = [safe.errorName, safe.errorCode, safe.httpStatus === undefined ? null : `HTTP ${safe.httpStatus}`, safe.exitCode === undefined ? null : `exit ${safe.exitCode}`, safe.signal].filter((value) => value !== undefined && value !== null);
72
79
  return details.length === 0 ? summary : `${summary} Observed: ${details.join(', ')} (${safe.source === 'stderr' ? 'startup stderr' : 'exception'}).`;
73
80
  }
74
81
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.300",
2
+ "version": "0.1.0-beta.301",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -72,13 +72,24 @@ function processFailure(diagnostic: RunnerDiagnostic | undefined): RunnerFailure
72
72
  return diagnostic?.exitCode !== undefined || diagnostic?.signal !== undefined ? 'process_exit' : null;
73
73
  }
74
74
 
75
+ function namedFailure(value: unknown, source: RunnerDiagnostic['source']): SessionFailure {
76
+ const errorName = runnerDiagnostic({ source, errorName: isRecord(value) ? value.name : undefined })?.errorName;
77
+ return errorName === undefined ? { code: 'session_error' } : { code: 'session_error', diagnostic: { source, errorName } };
78
+ }
79
+
80
+ export function debugFailure(stage: string, err: unknown): void {
81
+ if (process.env.METRO_RUNNER_DEBUG !== '1') return;
82
+ const text = err instanceof Error ? err.stack ?? `${err.name}: ${err.message}` : String(err);
83
+ process.stderr.write(`metro agent debug (${stage}, this terminal only): ${text.slice(0, 4_000)}\n`);
84
+ }
85
+
75
86
  function classify(value: unknown, source: RunnerDiagnostic['source']): SessionFailure {
76
87
  const message = errorText(value);
77
88
  const known = KNOWN.find((code) => message === runnerFailureSummary(code));
78
89
  if (known !== undefined) return { code: known };
79
90
  const diagnostic = evidence(isRecord(value) ? value : {}, message, source);
80
91
  const code = diagnosticCode(message, diagnostic) ?? TEXT_FAILURES.find(([, pattern]) => pattern.test(message))?.[0] ?? processFailure(diagnostic) ?? 'session_error';
81
- return code === 'session_error' ? { code } : { code, ...(diagnostic === undefined ? {} : { diagnostic }) };
92
+ return code === 'session_error' ? namedFailure(value, source) : { code, ...(diagnostic === undefined ? {} : { diagnostic }) };
82
93
  }
83
94
 
84
95
  export function sessionFailure(err: unknown): SessionFailure {
@@ -88,7 +99,7 @@ export function sessionFailure(err: unknown): SessionFailure {
88
99
  let result: SessionFailure = { code: 'session_error' };
89
100
  for (let depth = 0; depth < 4; depth++) {
90
101
  const next = classify(current, 'exception');
91
- if (next.code !== 'session_error') result = next;
102
+ if (next.code !== 'session_error' || (result.code === 'session_error' && result.diagnostic === undefined)) result = next;
92
103
  if (!isRecord(current) || current.cause === undefined) break;
93
104
  current = current.cause;
94
105
  }
@@ -3,7 +3,7 @@ import { log } from '@metro-labs/core/log';
3
3
  import { Activity, failureSummary } from './activity.js';
4
4
  import { startAgent, type RunningAgent } from './app.js';
5
5
  import { runnerConfig } from './config.js';
6
- import { sessionFailure } from './errors.js';
6
+ import { debugFailure, sessionFailure } from './errors.js';
7
7
 
8
8
  let agent: RunningAgent | null = null;
9
9
  let starting: Promise<RunningAgent> | null = null;
@@ -46,9 +46,11 @@ try {
46
46
  await agent.done;
47
47
  if (!exiting) activity.fail('The Agent SDK session ended unexpectedly.');
48
48
  } catch (err) {
49
+ debugFailure('session', err);
49
50
  if (!exiting) activity.fail(err);
50
51
  }
51
52
  } catch (err) {
53
+ debugFailure('start', err);
52
54
  const failure = sessionFailure(err);
53
55
  log.error({ reason: failure.code, diagnostic: failure.diagnostic, summary: failureSummary(err) }, 'sdk-runner: the Agent SDK session failed to start');
54
56
  }
@@ -18,7 +18,7 @@ import type { SessionStore } from './session-store.js';
18
18
  import { completedCommand, SessionWatch, startedCommand, uuidsOf } from './session-watch.js';
19
19
  import { TaskRecovery } from './task-recovery.js';
20
20
  import { METRO_SERVER, type MetroTools } from './tool-proxy.js';
21
- import { failureSummary, SessionError, StartupDiagnostics } from './errors.js';
21
+ import { debugFailure, failureSummary, SessionError, StartupDiagnostics } from './errors.js';
22
22
 
23
23
  const STARTUP_WAIT = 'CLAUDE_CODE_MCP_STARTUP_WAIT_MS';
24
24
  export const COMPACT_AT = 120_000;
@@ -240,6 +240,7 @@ export class Runner {
240
240
  this.switching = this.switching
241
241
  .then(() => this.applyModel(model))
242
242
  .catch((err: unknown) => {
243
+ debugFailure('model switch', err);
243
244
  const failure = new SessionError(this.diagnostics.failure(err));
244
245
  this.parts.activity?.fail(failure);
245
246
  log.warn({ reason: failure.failure.code, diagnostic: failure.failure.diagnostic, summary: failureSummary(failure) }, 'sdk-runner: the session could not take the model the Model page picked');