@stage-labs/metro 0.1.0-beta.282 → 0.1.0-beta.283
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/daemon/src/channels/inbound.ts +17 -18
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +6 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +28 -19
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +3 -3
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -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.283",
|
|
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",
|
|
@@ -11,6 +11,7 @@ import { replyMeta } from './addressed.js';
|
|
|
11
11
|
import {
|
|
12
12
|
capSet,
|
|
13
13
|
displayNameMeta,
|
|
14
|
+
missingAttachmentNames,
|
|
14
15
|
senderMeta,
|
|
15
16
|
tsMeta,
|
|
16
17
|
takeMediaCtx,
|
|
@@ -56,6 +57,7 @@ interface InboundDeps {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const ATTACH_TIMEOUT_MS = 15_000;
|
|
60
|
+
const LATE_ATTACH_WINDOW_MS = 15 * 60_000;
|
|
59
61
|
const DEDUPE_TTL_MS = 30_000;
|
|
60
62
|
const DEDUPE_MAX = 2_000;
|
|
61
63
|
const ALLOWED_LINES_MAX = 2_000;
|
|
@@ -149,27 +151,24 @@ export class InboundRelay {
|
|
|
149
151
|
private async flushPendingFallback(id: string): Promise<void> {
|
|
150
152
|
const e = this.pendingAttachments.get(id);
|
|
151
153
|
if (!e) return;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
const missing = missingAttachmentNames(e);
|
|
155
|
+
if (!missing.length) {
|
|
156
|
+
this.pendingAttachments.delete(id);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
e.timer = setTimeout(() => { this.pendingAttachments.delete(id); }, LATE_ATTACH_WINDOW_MS);
|
|
160
|
+
const ctx = takeMediaCtx(e);
|
|
158
161
|
await this.notify('notifications/claude/channel', {
|
|
159
162
|
content:
|
|
160
|
-
(
|
|
161
|
-
`[attachment(s) still downloading: ${
|
|
163
|
+
(ctx.text ? `${ctx.text}\n` : '') +
|
|
164
|
+
`[attachment(s) still downloading: ${missing.join(', ')}; they follow in a separate note]`,
|
|
162
165
|
meta: {
|
|
163
|
-
line:
|
|
164
|
-
from:
|
|
165
|
-
station:
|
|
166
|
-
...
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
from_name: e.fromName,
|
|
170
|
-
...displayNameMeta(e.fromDisplayName),
|
|
171
|
-
...e.reply,
|
|
172
|
-
...e.callMeta?.(),
|
|
166
|
+
line: ctx.line,
|
|
167
|
+
from: ctx.from,
|
|
168
|
+
station: ctx.station,
|
|
169
|
+
...senderMeta(ctx),
|
|
170
|
+
...ctx.reply,
|
|
171
|
+
...ctx.callMeta?.(),
|
|
173
172
|
},
|
|
174
173
|
});
|
|
175
174
|
}
|
|
@@ -62,6 +62,12 @@ export const senderMeta = (c: MediaCtx): Record<string, string> => ({
|
|
|
62
62
|
...displayNameMeta(c.fromDisplayName),
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
export function missingAttachmentNames(buf: PendingMsg): string[] {
|
|
66
|
+
return buf.attachments
|
|
67
|
+
.filter((_, i) => !buf.saved.has(i))
|
|
68
|
+
.map((a) => a.name ?? a.kind ?? 'attachment');
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
export function takeMediaCtx(buf: PendingMsg): MediaCtx {
|
|
66
72
|
const ctx: MediaCtx = {
|
|
67
73
|
line: buf.line,
|
|
@@ -12,13 +12,20 @@ import { errMsg } from '@metro-labs/core/log';
|
|
|
12
12
|
|
|
13
13
|
export type { SavedAttachment };
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
];
|
|
21
|
-
const
|
|
15
|
+
interface FetchSchedule {
|
|
16
|
+
delaysMs: readonly number[];
|
|
17
|
+
requestTimeoutMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const READ_SCHEDULE: FetchSchedule = { delaysMs: [400, 800], requestTimeoutMs: 9_000 };
|
|
21
|
+
export const INBOUND_FETCH_SCHEDULE: FetchSchedule = {
|
|
22
|
+
delaysMs: [
|
|
23
|
+
2_000, 4_000, 8_000, 15_000, 30_000, 30_000,
|
|
24
|
+
60_000, 60_000, 60_000, 60_000, 60_000, 60_000,
|
|
25
|
+
],
|
|
26
|
+
requestTimeoutMs: 120_000,
|
|
27
|
+
};
|
|
28
|
+
export const REMOTE_FETCH_ATTEMPTS = READ_SCHEDULE.delaysMs.length + 1;
|
|
22
29
|
const SWARM_FALLBACK_GATEWAY = 'https://download.gateway.ethswarm.org/bzz/';
|
|
23
30
|
const SWARM_BZZ_URL = /^https:\/\/[^/]+\/bzz\/([0-9a-f]{64}(?:[0-9a-f]{64})?)\/?$/i;
|
|
24
31
|
|
|
@@ -83,15 +90,16 @@ interface DecodedAttachment {
|
|
|
83
90
|
|
|
84
91
|
function remoteUrls(url: string): string[] {
|
|
85
92
|
const ref = SWARM_BZZ_URL.exec(url)?.[1];
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
if (ref === undefined) return [url];
|
|
94
|
+
const fallback = `${SWARM_FALLBACK_GATEWAY}${ref.toLowerCase()}/`;
|
|
95
|
+
return url.toLowerCase().replace(/\/?$/, '/') === fallback ? [url] : [url, fallback];
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
|
|
98
|
+
function withTimeout<T>(work: Promise<T>, ms: number, url: string): Promise<T> {
|
|
91
99
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
92
100
|
const expired = new Promise<never>((_resolve, reject) => {
|
|
93
101
|
timer = setTimeout(() => {
|
|
94
|
-
reject(new Error(`no answer within ${ms / 1000}s`));
|
|
102
|
+
reject(new Error(`no answer from ${url} within ${ms / 1000}s`));
|
|
95
103
|
}, ms);
|
|
96
104
|
});
|
|
97
105
|
return Promise.race([work, expired]).finally(() => {
|
|
@@ -99,28 +107,29 @@ function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
|
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
function loadFrom(r: RemoteEntry, url: string): Promise<DecodedAttachment> {
|
|
110
|
+
function loadFrom(r: RemoteEntry, url: string, timeoutMs: number): Promise<DecodedAttachment> {
|
|
103
111
|
return withTimeout(
|
|
104
112
|
RemoteAttachmentCodec.load<DecodedAttachment>(
|
|
105
113
|
{ ...toRemoteDescriptor(r), url },
|
|
106
114
|
loadRegistry,
|
|
107
115
|
),
|
|
108
|
-
|
|
116
|
+
timeoutMs,
|
|
117
|
+
url,
|
|
109
118
|
);
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
async function loadRemote(
|
|
113
122
|
r: RemoteEntry,
|
|
114
|
-
|
|
123
|
+
schedule: FetchSchedule,
|
|
115
124
|
): Promise<DecodedAttachment> {
|
|
116
125
|
const urls = remoteUrls(r.url);
|
|
117
126
|
let last = '';
|
|
118
|
-
for (const delay of [0, ...
|
|
127
|
+
for (const delay of [0, ...schedule.delaysMs]) {
|
|
119
128
|
if (delay > 0) await sleep(delay);
|
|
120
129
|
const errors: string[] = [];
|
|
121
130
|
for (const url of urls) {
|
|
122
131
|
try {
|
|
123
|
-
return await loadFrom(r, url);
|
|
132
|
+
return await loadFrom(r, url, schedule.requestTimeoutMs);
|
|
124
133
|
} catch (err) {
|
|
125
134
|
errors.push(errMsg(err));
|
|
126
135
|
}
|
|
@@ -128,7 +137,7 @@ async function loadRemote(
|
|
|
128
137
|
last = errors.join('; ');
|
|
129
138
|
}
|
|
130
139
|
throw new Error(
|
|
131
|
-
`xmtp remote attachment fetch failed after ${
|
|
140
|
+
`xmtp remote attachment fetch failed after ${schedule.delaysMs.length + 1} attempts: ${last}`,
|
|
132
141
|
);
|
|
133
142
|
}
|
|
134
143
|
|
|
@@ -136,10 +145,10 @@ export async function saveRemoteAttachment(
|
|
|
136
145
|
r: RemoteEntry,
|
|
137
146
|
messageId: string,
|
|
138
147
|
index = 0,
|
|
139
|
-
|
|
148
|
+
schedule: FetchSchedule = READ_SCHEDULE,
|
|
140
149
|
): Promise<SavedAttachment> {
|
|
141
150
|
if (r.contentLength) assertAttachmentSize(r.contentLength);
|
|
142
|
-
const decoded = await loadRemote(r,
|
|
151
|
+
const decoded = await loadRemote(r, schedule);
|
|
143
152
|
return saveBufferToCache(decoded.data, messageId, index, {
|
|
144
153
|
mime: decoded.mimeType,
|
|
145
154
|
name: decoded.filename ?? r.filename,
|
|
@@ -3,7 +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
|
-
|
|
6
|
+
INBOUND_FETCH_SCHEDULE,
|
|
7
7
|
saveInlineAttachment,
|
|
8
8
|
saveRemoteAttachment,
|
|
9
9
|
type RemoteEntry,
|
|
@@ -208,7 +208,7 @@ function remoteStaticPayload(
|
|
|
208
208
|
ctx: EnvelopeCtx,
|
|
209
209
|
): Record<string, unknown> {
|
|
210
210
|
const kind = IMG_RE.test(c.url) ? 'image' : 'file';
|
|
211
|
-
reportAttachment(saveRemoteAttachment(c, ctx.msgId, 0,
|
|
211
|
+
reportAttachment(saveRemoteAttachment(c, ctx.msgId, 0, INBOUND_FETCH_SCHEDULE), { station: 'xmtp', account: ctx.accountId, line: ctx.line, forId: ctx.baseId, index: 0 });
|
|
212
212
|
return {
|
|
213
213
|
...base,
|
|
214
214
|
text: `[${kind}: ${c.filename ?? c.url}]`,
|
|
@@ -224,7 +224,7 @@ function multiRemoteEnvelope(
|
|
|
224
224
|
): Record<string, unknown> {
|
|
225
225
|
const m = c as { attachments?: RemoteEntry[] };
|
|
226
226
|
(Array.isArray(m.attachments) ? m.attachments : []).forEach((r, i) => {
|
|
227
|
-
reportAttachment(saveRemoteAttachment(r, ctx.msgId, i,
|
|
227
|
+
reportAttachment(saveRemoteAttachment(r, ctx.msgId, i, INBOUND_FETCH_SCHEDULE), { station: 'xmtp', account: ctx.accountId, line: ctx.line, forId: ctx.baseId, index: i });
|
|
228
228
|
});
|
|
229
229
|
return multiRemotePayload(base, typeId, c);
|
|
230
230
|
}
|
package/runtime/runtime.json
CHANGED