@stage-labs/metro 0.1.0-beta.300 → 0.1.0-beta.302
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 +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/runner-activity.ts +8 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +4 -1
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +3 -3
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/runner-activity.ts +8 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/sdk-runner/src/errors.ts +13 -2
- package/runtime/sdk-runner/src/main.ts +3 -1
- package/runtime/sdk-runner/src/runner.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.302",
|
|
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
|
|
|
@@ -34,7 +34,10 @@ function ensureUser(): AgentUser | null {
|
|
|
34
34
|
openMetroHome();
|
|
35
35
|
forgetAgentUser();
|
|
36
36
|
const user = agentUser();
|
|
37
|
-
if (user !== null)
|
|
37
|
+
if (user !== null) {
|
|
38
|
+
run(...asUser(user, 'chmod', ['711', user.home]));
|
|
39
|
+
run(...asUser(user, 'sh', ['-c', 'test ! -d "$1" || chmod go-w "$1"', 'sh', join(user.home, '.metro')]));
|
|
40
|
+
}
|
|
38
41
|
return user;
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -55,15 +55,15 @@ function parseCheck(raw: string): UpdateCheck {
|
|
|
55
55
|
return { current: body.current, latest: body.latest, newer: body.newer === true };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
async function check(bin: string, now = Date.now()): Promise<UpdateCheck> {
|
|
59
|
-
if (cached !== null && now - cached.at < CHECK_TTL_MS) return cached.check;
|
|
58
|
+
async function check(bin: string, fresh = false, now = Date.now()): Promise<UpdateCheck> {
|
|
59
|
+
if (!fresh && cached !== null && now - cached.at < CHECK_TTL_MS) return cached.check;
|
|
60
60
|
const out = await runCli(bin, ['update', '--check'], CHECK_TIMEOUT_MS);
|
|
61
61
|
cached = { at: now, check: parseCheck(out.stdout) };
|
|
62
62
|
return cached.check;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
async function apply(bin: string, deps: UpdateApiDeps): Promise<UpdateResult> {
|
|
66
|
-
const before = await check(bin);
|
|
66
|
+
const before = await check(bin, true);
|
|
67
67
|
if (!before.newer) return { updated: false, version: before.current, restarting: false };
|
|
68
68
|
const out = await runCli(bin, ['update'], UPDATE_TIMEOUT_MS);
|
|
69
69
|
cached = null;
|
package/runtime/runtime.json
CHANGED
|
@@ -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
|
|
|
@@ -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' ?
|
|
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');
|