@stage-labs/metro 0.1.0-beta.280 → 0.1.0-beta.282

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.280",
3
+ "version": "0.1.0-beta.282",
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.280",
3
+ "version": "0.1.0-beta.282",
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",
@@ -158,7 +158,7 @@ export class InboundRelay {
158
158
  await this.notify('notifications/claude/channel', {
159
159
  content:
160
160
  (e.text ? `${e.text}\n` : '') +
161
- `[attachment(s) could not be fetched in time: ${names}]`,
161
+ `[attachment(s) still downloading: ${names}; they follow in a separate note]`,
162
162
  meta: {
163
163
  line: e.line,
164
164
  from: e.from,
@@ -124,11 +124,13 @@ export class GitHubSkills {
124
124
 
125
125
  rows(activation = this.activation(), loaded = this.loaded(activation)): ClaudeSkill[] {
126
126
  if (loaded === null) return [];
127
+ const origin = loaded.source;
127
128
  const shadowed = activation.shadowed;
128
129
  return loaded.skills.map((skill) => ({
129
130
  id: `github:${skill.name}`, name: skill.name, title: skill.name, description: skill.description,
130
131
  path: join(skillRelease(this.root, loaded.id), 'skills', skill.name, 'SKILL.md'), editable: false,
131
132
  updatedAt: loaded.createdAt, managed: true, shadowed: shadowed.includes(skill.name),
133
+ ...(origin === null ? {} : { github: { repository: origin.repository, commit: origin.commit, folder: origin.folder } }),
132
134
  }));
133
135
  }
134
136
 
@@ -20,6 +20,7 @@ export interface ClaudeSkill {
20
20
  updatedAt: string | null;
21
21
  managed?: boolean;
22
22
  shadowed?: boolean;
23
+ github?: { repository: string; commit: string; folder: string };
23
24
  }
24
25
 
25
26
  const frontmatterField = (text: string, field: string): string => {
@@ -12,8 +12,15 @@ import { errMsg } from '@metro-labs/core/log';
12
12
 
13
13
  export type { SavedAttachment };
14
14
 
15
- export const REMOTE_FETCH_ATTEMPTS = 3;
16
- const REMOTE_FETCH_BACKOFF_MS = 400;
15
+ const READ_RETRY_DELAYS_MS: readonly number[] = [400, 800];
16
+ export const REMOTE_FETCH_ATTEMPTS = READ_RETRY_DELAYS_MS.length + 1;
17
+ export const INBOUND_RETRY_DELAYS_MS: readonly number[] = [
18
+ 2_000, 4_000, 8_000, 15_000, 30_000, 30_000,
19
+ 60_000, 60_000, 60_000, 60_000, 60_000, 60_000,
20
+ ];
21
+ const REMOTE_FETCH_TIMEOUT_MS = 120_000;
22
+ const SWARM_FALLBACK_GATEWAY = 'https://download.gateway.ethswarm.org/bzz/';
23
+ const SWARM_BZZ_URL = /^https:\/\/[^/]+\/bzz\/([0-9a-f]{64}(?:[0-9a-f]{64})?)\/?$/i;
17
24
 
18
25
  const sleep = (ms: number): Promise<void> =>
19
26
  new Promise((resolve) => setTimeout(resolve, ms));
@@ -74,22 +81,54 @@ interface DecodedAttachment {
74
81
  data: Uint8Array;
75
82
  }
76
83
 
77
- async function loadRemote(r: RemoteEntry): Promise<DecodedAttachment> {
84
+ function remoteUrls(url: string): string[] {
85
+ const ref = SWARM_BZZ_URL.exec(url)?.[1];
86
+ const fallback = ref === undefined ? url : `${SWARM_FALLBACK_GATEWAY}${ref}/`;
87
+ return fallback === url ? [url] : [url, fallback];
88
+ }
89
+
90
+ function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
91
+ let timer: ReturnType<typeof setTimeout> | undefined;
92
+ const expired = new Promise<never>((_resolve, reject) => {
93
+ timer = setTimeout(() => {
94
+ reject(new Error(`no answer within ${ms / 1000}s`));
95
+ }, ms);
96
+ });
97
+ return Promise.race([work, expired]).finally(() => {
98
+ clearTimeout(timer);
99
+ });
100
+ }
101
+
102
+ function loadFrom(r: RemoteEntry, url: string): Promise<DecodedAttachment> {
103
+ return withTimeout(
104
+ RemoteAttachmentCodec.load<DecodedAttachment>(
105
+ { ...toRemoteDescriptor(r), url },
106
+ loadRegistry,
107
+ ),
108
+ REMOTE_FETCH_TIMEOUT_MS,
109
+ );
110
+ }
111
+
112
+ async function loadRemote(
113
+ r: RemoteEntry,
114
+ retryDelaysMs: readonly number[],
115
+ ): Promise<DecodedAttachment> {
116
+ const urls = remoteUrls(r.url);
78
117
  let last = '';
79
- for (let attempt = 1; attempt <= REMOTE_FETCH_ATTEMPTS; attempt += 1) {
80
- try {
81
- return await RemoteAttachmentCodec.load<DecodedAttachment>(
82
- toRemoteDescriptor(r),
83
- loadRegistry,
84
- );
85
- } catch (err) {
86
- last = errMsg(err);
87
- if (attempt < REMOTE_FETCH_ATTEMPTS)
88
- await sleep(REMOTE_FETCH_BACKOFF_MS * attempt);
118
+ for (const delay of [0, ...retryDelaysMs]) {
119
+ if (delay > 0) await sleep(delay);
120
+ const errors: string[] = [];
121
+ for (const url of urls) {
122
+ try {
123
+ return await loadFrom(r, url);
124
+ } catch (err) {
125
+ errors.push(errMsg(err));
126
+ }
89
127
  }
128
+ last = errors.join('; ');
90
129
  }
91
130
  throw new Error(
92
- `xmtp remote attachment fetch failed after ${REMOTE_FETCH_ATTEMPTS} attempts: ${last}`,
131
+ `xmtp remote attachment fetch failed after ${retryDelaysMs.length + 1} attempts: ${last}`,
93
132
  );
94
133
  }
95
134
 
@@ -97,9 +136,10 @@ export async function saveRemoteAttachment(
97
136
  r: RemoteEntry,
98
137
  messageId: string,
99
138
  index = 0,
139
+ retryDelaysMs: readonly number[] = READ_RETRY_DELAYS_MS,
100
140
  ): Promise<SavedAttachment> {
101
141
  if (r.contentLength) assertAttachmentSize(r.contentLength);
102
- const decoded = await loadRemote(r);
142
+ const decoded = await loadRemote(r, retryDelaysMs);
103
143
  return saveBufferToCache(decoded.data, messageId, index, {
104
144
  mime: decoded.mimeType,
105
145
  name: decoded.filename ?? r.filename,
@@ -3,6 +3,7 @@ import type { Reply } from '@xmtp/content-type-reply';
3
3
  import { errMsg } from '@metro-labs/core/log';
4
4
  import { transcribeAndEmit } from './transcribe.js';
5
5
  import {
6
+ INBOUND_RETRY_DELAYS_MS,
6
7
  saveInlineAttachment,
7
8
  saveRemoteAttachment,
8
9
  type RemoteEntry,
@@ -207,7 +208,7 @@ function remoteStaticPayload(
207
208
  ctx: EnvelopeCtx,
208
209
  ): Record<string, unknown> {
209
210
  const kind = IMG_RE.test(c.url) ? 'image' : 'file';
210
- reportAttachment(saveRemoteAttachment(c, ctx.msgId, 0), { station: 'xmtp', account: ctx.accountId, line: ctx.line, forId: ctx.baseId, index: 0 });
211
+ reportAttachment(saveRemoteAttachment(c, ctx.msgId, 0, INBOUND_RETRY_DELAYS_MS), { station: 'xmtp', account: ctx.accountId, line: ctx.line, forId: ctx.baseId, index: 0 });
211
212
  return {
212
213
  ...base,
213
214
  text: `[${kind}: ${c.filename ?? c.url}]`,
@@ -223,7 +224,7 @@ function multiRemoteEnvelope(
223
224
  ): Record<string, unknown> {
224
225
  const m = c as { attachments?: RemoteEntry[] };
225
226
  (Array.isArray(m.attachments) ? m.attachments : []).forEach((r, i) => {
226
- reportAttachment(saveRemoteAttachment(r, ctx.msgId, i), { station: 'xmtp', account: ctx.accountId, line: ctx.line, forId: ctx.baseId, index: i });
227
+ reportAttachment(saveRemoteAttachment(r, ctx.msgId, i, INBOUND_RETRY_DELAYS_MS), { station: 'xmtp', account: ctx.accountId, line: ctx.line, forId: ctx.baseId, index: i });
227
228
  });
228
229
  return multiRemotePayload(base, typeId, c);
229
230
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.280"
2
+ "version": "0.1.0-beta.282"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.280",
2
+ "version": "0.1.0-beta.282",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",