@silicaclaw/cli 1.0.0-beta.24 → 1.0.0-beta.26
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/CHANGELOG.md +21 -0
- package/apps/local-console/src/server.ts +2 -2
- package/package.json +1 -1
- package/packages/network/dist/relayPreview.d.ts +9 -0
- package/packages/network/dist/relayPreview.js +88 -42
- package/packages/network/src/relayPreview.ts +89 -41
- package/scripts/silicaclaw-cli.mjs +27 -8
- package/scripts/webrtc-signaling-server.mjs +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## v1.0 beta - 2026-03-18
|
|
4
4
|
|
|
5
|
+
### Beta 26
|
|
6
|
+
|
|
7
|
+
- install command resilience:
|
|
8
|
+
- `silicaclaw install` no longer fails hard when shell startup files are not writable
|
|
9
|
+
- install still creates the command shim and `~/.silicaclaw/env.sh`
|
|
10
|
+
- users now get a manual one-line fallback when rc file updates are blocked by permissions
|
|
11
|
+
|
|
12
|
+
### Beta 25
|
|
13
|
+
|
|
14
|
+
- relay load reduction:
|
|
15
|
+
- default relay poll interval increased to reduce request pressure
|
|
16
|
+
- peer refresh interval increased to reduce extra room lookups
|
|
17
|
+
- request timeout and retry behavior tightened to avoid stacked in-flight polls
|
|
18
|
+
- poll responses now reuse embedded peer lists to avoid separate `/peers` calls
|
|
19
|
+
- relay durability improvements:
|
|
20
|
+
- Cloudflare relay now throttles peer heartbeat writes
|
|
21
|
+
- local signaling preview server now mirrors the same lower-write behavior
|
|
22
|
+
- presence cost tuning:
|
|
23
|
+
- default broadcast interval increased
|
|
24
|
+
- default presence TTL increased to keep nodes visible without aggressive rebroadcasting
|
|
25
|
+
|
|
5
26
|
### Beta 24
|
|
6
27
|
|
|
7
28
|
- command install UX:
|
|
@@ -51,8 +51,8 @@ import {
|
|
|
51
51
|
import { CacheRepo, IdentityRepo, LogRepo, ProfileRepo, SocialRuntimeRepo } from "@silicaclaw/storage";
|
|
52
52
|
import { registerSocialRoutes } from "./socialRoutes";
|
|
53
53
|
|
|
54
|
-
const BROADCAST_INTERVAL_MS =
|
|
55
|
-
const PRESENCE_TTL_MS = Number(process.env.PRESENCE_TTL_MS ||
|
|
54
|
+
const BROADCAST_INTERVAL_MS = Number(process.env.BROADCAST_INTERVAL_MS || 20_000);
|
|
55
|
+
const PRESENCE_TTL_MS = Number(process.env.PRESENCE_TTL_MS || 90_000);
|
|
56
56
|
const NETWORK_MAX_MESSAGE_BYTES = Number(process.env.NETWORK_MAX_MESSAGE_BYTES || 64 * 1024);
|
|
57
57
|
const NETWORK_DEDUPE_WINDOW_MS = Number(process.env.NETWORK_DEDUPE_WINDOW_MS || 90_000);
|
|
58
58
|
const NETWORK_DEDUPE_MAX_ENTRIES = Number(process.env.NETWORK_DEDUPE_MAX_ENTRIES || 10_000);
|
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@ type RelayPreviewOptions = {
|
|
|
12
12
|
pollIntervalMs?: number;
|
|
13
13
|
maxFutureDriftMs?: number;
|
|
14
14
|
maxPastDriftMs?: number;
|
|
15
|
+
requestTimeoutMs?: number;
|
|
16
|
+
peerRefreshIntervalMs?: number;
|
|
15
17
|
};
|
|
16
18
|
type RelayPeer = {
|
|
17
19
|
peer_id: string;
|
|
@@ -102,6 +104,7 @@ type RelayDiagnostics = {
|
|
|
102
104
|
peers_refresh_attempted: number;
|
|
103
105
|
peers_refresh_succeeded: number;
|
|
104
106
|
publish_succeeded: number;
|
|
107
|
+
poll_skipped_inflight: number;
|
|
105
108
|
};
|
|
106
109
|
};
|
|
107
110
|
export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
@@ -116,6 +119,8 @@ export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
116
119
|
private readonly pollIntervalMs;
|
|
117
120
|
private readonly maxFutureDriftMs;
|
|
118
121
|
private readonly maxPastDriftMs;
|
|
122
|
+
private readonly requestTimeoutMs;
|
|
123
|
+
private readonly peerRefreshIntervalMs;
|
|
119
124
|
private readonly envelopeCodec;
|
|
120
125
|
private readonly topicCodec;
|
|
121
126
|
private started;
|
|
@@ -137,6 +142,8 @@ export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
137
142
|
private lastPeerRefreshAt;
|
|
138
143
|
private lastErrorAt;
|
|
139
144
|
private lastError;
|
|
145
|
+
private pollInFlight;
|
|
146
|
+
private currentPollDelayMs;
|
|
140
147
|
private stats;
|
|
141
148
|
constructor(options?: RelayPreviewOptions);
|
|
142
149
|
start(): Promise<void>;
|
|
@@ -153,5 +160,7 @@ export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
153
160
|
private get;
|
|
154
161
|
private post;
|
|
155
162
|
private requestJson;
|
|
163
|
+
private updatePeersFromList;
|
|
164
|
+
private scheduleNextPoll;
|
|
156
165
|
}
|
|
157
166
|
export {};
|
|
@@ -20,6 +20,8 @@ class RelayPreviewAdapter {
|
|
|
20
20
|
pollIntervalMs;
|
|
21
21
|
maxFutureDriftMs;
|
|
22
22
|
maxPastDriftMs;
|
|
23
|
+
requestTimeoutMs;
|
|
24
|
+
peerRefreshIntervalMs;
|
|
23
25
|
envelopeCodec;
|
|
24
26
|
topicCodec;
|
|
25
27
|
started = false;
|
|
@@ -41,6 +43,8 @@ class RelayPreviewAdapter {
|
|
|
41
43
|
lastPeerRefreshAt = 0;
|
|
42
44
|
lastErrorAt = 0;
|
|
43
45
|
lastError = null;
|
|
46
|
+
pollInFlight = false;
|
|
47
|
+
currentPollDelayMs = 0;
|
|
44
48
|
stats = {
|
|
45
49
|
publish_attempted: 0,
|
|
46
50
|
publish_sent: 0,
|
|
@@ -69,6 +73,7 @@ class RelayPreviewAdapter {
|
|
|
69
73
|
peers_refresh_attempted: 0,
|
|
70
74
|
peers_refresh_succeeded: 0,
|
|
71
75
|
publish_succeeded: 0,
|
|
76
|
+
poll_skipped_inflight: 0,
|
|
72
77
|
};
|
|
73
78
|
constructor(options = {}) {
|
|
74
79
|
this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -83,11 +88,14 @@ class RelayPreviewAdapter {
|
|
|
83
88
|
this.bootstrapHints = dedupe(options.bootstrapHints || []);
|
|
84
89
|
this.bootstrapSources = dedupe(options.bootstrapSources || []);
|
|
85
90
|
this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
|
|
86
|
-
this.pollIntervalMs = options.pollIntervalMs ??
|
|
91
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
87
92
|
this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
|
|
88
93
|
this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
|
|
94
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 4000;
|
|
95
|
+
this.peerRefreshIntervalMs = options.peerRefreshIntervalMs ?? 30_000;
|
|
89
96
|
this.envelopeCodec = new jsonMessageEnvelopeCodec_1.JsonMessageEnvelopeCodec();
|
|
90
97
|
this.topicCodec = new jsonTopicCodec_1.JsonTopicCodec();
|
|
98
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
91
99
|
}
|
|
92
100
|
async start() {
|
|
93
101
|
if (this.started)
|
|
@@ -97,9 +105,7 @@ class RelayPreviewAdapter {
|
|
|
97
105
|
this.started = true;
|
|
98
106
|
await this.refreshPeers();
|
|
99
107
|
await this.pollOnce();
|
|
100
|
-
this.
|
|
101
|
-
this.pollOnce().catch(() => { });
|
|
102
|
-
}, this.pollIntervalMs);
|
|
108
|
+
this.scheduleNextPoll(this.pollIntervalMs);
|
|
103
109
|
this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
|
|
104
110
|
}
|
|
105
111
|
catch (error) {
|
|
@@ -111,7 +117,7 @@ class RelayPreviewAdapter {
|
|
|
111
117
|
if (!this.started)
|
|
112
118
|
return;
|
|
113
119
|
if (this.poller) {
|
|
114
|
-
|
|
120
|
+
clearTimeout(this.poller);
|
|
115
121
|
this.poller = null;
|
|
116
122
|
}
|
|
117
123
|
try {
|
|
@@ -206,51 +212,48 @@ class RelayPreviewAdapter {
|
|
|
206
212
|
};
|
|
207
213
|
}
|
|
208
214
|
async pollOnce() {
|
|
215
|
+
if (this.pollInFlight) {
|
|
216
|
+
this.stats.poll_skipped_inflight += 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.pollInFlight = true;
|
|
209
220
|
await this.maybeRefreshJoin("poll");
|
|
210
221
|
this.stats.poll_attempted += 1;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
try {
|
|
223
|
+
const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
|
|
224
|
+
this.lastPollAt = Date.now();
|
|
225
|
+
this.stats.poll_succeeded += 1;
|
|
226
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
227
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
228
|
+
for (const message of messages) {
|
|
229
|
+
this.signalingMessagesReceivedTotal += 1;
|
|
230
|
+
this.onEnvelope(message?.envelope);
|
|
231
|
+
}
|
|
232
|
+
if (Array.isArray(payload?.peers)) {
|
|
233
|
+
this.updatePeersFromList(payload.peers);
|
|
234
|
+
}
|
|
235
|
+
else if (!this.lastPeerRefreshAt || Date.now() - this.lastPeerRefreshAt >= this.peerRefreshIntervalMs) {
|
|
236
|
+
await this.refreshPeers();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.currentPollDelayMs = Math.min(15_000, Math.max(this.pollIntervalMs, this.currentPollDelayMs * 2));
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
this.pollInFlight = false;
|
|
245
|
+
if (this.started) {
|
|
246
|
+
this.scheduleNextPoll(this.currentPollDelayMs);
|
|
247
|
+
}
|
|
218
248
|
}
|
|
219
|
-
await this.refreshPeers();
|
|
220
249
|
}
|
|
221
250
|
async refreshPeers() {
|
|
222
251
|
this.stats.peers_refresh_attempted += 1;
|
|
223
252
|
const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
|
|
224
253
|
this.lastPeerRefreshAt = Date.now();
|
|
225
254
|
this.stats.peers_refresh_succeeded += 1;
|
|
226
|
-
const peerIds = Array.isArray(payload?.peers) ? payload.peers
|
|
227
|
-
|
|
228
|
-
await this.joinRoom("self_missing_from_peers");
|
|
229
|
-
}
|
|
230
|
-
const now = Date.now();
|
|
231
|
-
const next = new Map();
|
|
232
|
-
for (const peerId of peerIds) {
|
|
233
|
-
if (peerId === this.peerId)
|
|
234
|
-
continue;
|
|
235
|
-
const existing = this.peers.get(peerId);
|
|
236
|
-
if (!existing) {
|
|
237
|
-
this.recordDiscovery("peer_joined", { peer_id: peerId });
|
|
238
|
-
}
|
|
239
|
-
next.set(peerId, {
|
|
240
|
-
peer_id: peerId,
|
|
241
|
-
status: "online",
|
|
242
|
-
first_seen_at: existing?.first_seen_at ?? now,
|
|
243
|
-
last_seen_at: now,
|
|
244
|
-
messages_seen: existing?.messages_seen ?? 0,
|
|
245
|
-
reconnect_attempts: existing?.reconnect_attempts ?? 0,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
for (const peerId of this.peers.keys()) {
|
|
249
|
-
if (!next.has(peerId)) {
|
|
250
|
-
this.recordDiscovery("peer_removed", { peer_id: peerId });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
this.peers = next;
|
|
255
|
+
const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
|
|
256
|
+
this.updatePeersFromList(peerIds);
|
|
254
257
|
}
|
|
255
258
|
onEnvelope(envelope) {
|
|
256
259
|
this.stats.received_total += 1;
|
|
@@ -337,7 +340,7 @@ class RelayPreviewAdapter {
|
|
|
337
340
|
this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
|
|
338
341
|
}
|
|
339
342
|
async maybeRefreshJoin(reason) {
|
|
340
|
-
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(
|
|
343
|
+
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(45_000, this.pollIntervalMs * 6)) {
|
|
341
344
|
await this.joinRoom(reason);
|
|
342
345
|
}
|
|
343
346
|
}
|
|
@@ -355,11 +358,15 @@ class RelayPreviewAdapter {
|
|
|
355
358
|
if (!endpoint)
|
|
356
359
|
continue;
|
|
357
360
|
try {
|
|
361
|
+
const controller = new AbortController();
|
|
362
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
358
363
|
const response = await fetch(`${endpoint}${path}`, {
|
|
359
364
|
method,
|
|
360
365
|
headers: method === "POST" ? { "content-type": "application/json" } : undefined,
|
|
361
366
|
body: method === "POST" ? JSON.stringify(body) : undefined,
|
|
367
|
+
signal: controller.signal,
|
|
362
368
|
});
|
|
369
|
+
clearTimeout(timeout);
|
|
363
370
|
if (!response.ok) {
|
|
364
371
|
throw new Error(`${method} ${path} failed (${response.status})`);
|
|
365
372
|
}
|
|
@@ -380,5 +387,44 @@ class RelayPreviewAdapter {
|
|
|
380
387
|
}
|
|
381
388
|
throw new Error(errors.join(" | "));
|
|
382
389
|
}
|
|
390
|
+
updatePeersFromList(values) {
|
|
391
|
+
const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
|
|
392
|
+
if (!peerIds.includes(this.peerId)) {
|
|
393
|
+
void this.joinRoom("self_missing_from_peers").catch(() => { });
|
|
394
|
+
}
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const next = new Map();
|
|
397
|
+
for (const peerId of peerIds) {
|
|
398
|
+
if (peerId === this.peerId)
|
|
399
|
+
continue;
|
|
400
|
+
const existing = this.peers.get(peerId);
|
|
401
|
+
if (!existing) {
|
|
402
|
+
this.recordDiscovery("peer_joined", { peer_id: peerId });
|
|
403
|
+
}
|
|
404
|
+
next.set(peerId, {
|
|
405
|
+
peer_id: peerId,
|
|
406
|
+
status: "online",
|
|
407
|
+
first_seen_at: existing?.first_seen_at ?? now,
|
|
408
|
+
last_seen_at: now,
|
|
409
|
+
messages_seen: existing?.messages_seen ?? 0,
|
|
410
|
+
reconnect_attempts: existing?.reconnect_attempts ?? 0,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
for (const peerId of this.peers.keys()) {
|
|
414
|
+
if (!next.has(peerId)) {
|
|
415
|
+
this.recordDiscovery("peer_removed", { peer_id: peerId });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
this.peers = next;
|
|
419
|
+
}
|
|
420
|
+
scheduleNextPoll(delayMs) {
|
|
421
|
+
if (this.poller) {
|
|
422
|
+
clearTimeout(this.poller);
|
|
423
|
+
}
|
|
424
|
+
const jitterMs = Math.floor(Math.random() * 400);
|
|
425
|
+
this.poller = setTimeout(() => {
|
|
426
|
+
this.pollOnce().catch(() => { });
|
|
427
|
+
}, Math.max(1000, delayMs + jitterMs));
|
|
428
|
+
}
|
|
383
429
|
}
|
|
384
430
|
exports.RelayPreviewAdapter = RelayPreviewAdapter;
|
|
@@ -22,6 +22,8 @@ type RelayPreviewOptions = {
|
|
|
22
22
|
pollIntervalMs?: number;
|
|
23
23
|
maxFutureDriftMs?: number;
|
|
24
24
|
maxPastDriftMs?: number;
|
|
25
|
+
requestTimeoutMs?: number;
|
|
26
|
+
peerRefreshIntervalMs?: number;
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
type RelayPeer = {
|
|
@@ -114,6 +116,7 @@ type RelayDiagnostics = {
|
|
|
114
116
|
peers_refresh_attempted: number;
|
|
115
117
|
peers_refresh_succeeded: number;
|
|
116
118
|
publish_succeeded: number;
|
|
119
|
+
poll_skipped_inflight: number;
|
|
117
120
|
};
|
|
118
121
|
};
|
|
119
122
|
|
|
@@ -133,6 +136,8 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
133
136
|
private readonly pollIntervalMs: number;
|
|
134
137
|
private readonly maxFutureDriftMs: number;
|
|
135
138
|
private readonly maxPastDriftMs: number;
|
|
139
|
+
private readonly requestTimeoutMs: number;
|
|
140
|
+
private readonly peerRefreshIntervalMs: number;
|
|
136
141
|
private readonly envelopeCodec: MessageEnvelopeCodec;
|
|
137
142
|
private readonly topicCodec: TopicCodec;
|
|
138
143
|
|
|
@@ -155,6 +160,8 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
155
160
|
private lastPeerRefreshAt = 0;
|
|
156
161
|
private lastErrorAt = 0;
|
|
157
162
|
private lastError: string | null = null;
|
|
163
|
+
private pollInFlight = false;
|
|
164
|
+
private currentPollDelayMs = 0;
|
|
158
165
|
|
|
159
166
|
private stats: RelayDiagnostics["stats"] = {
|
|
160
167
|
publish_attempted: 0,
|
|
@@ -184,6 +191,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
184
191
|
peers_refresh_attempted: 0,
|
|
185
192
|
peers_refresh_succeeded: 0,
|
|
186
193
|
publish_succeeded: 0,
|
|
194
|
+
poll_skipped_inflight: 0,
|
|
187
195
|
};
|
|
188
196
|
|
|
189
197
|
constructor(options: RelayPreviewOptions = {}) {
|
|
@@ -201,11 +209,14 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
201
209
|
this.bootstrapHints = dedupe(options.bootstrapHints || []);
|
|
202
210
|
this.bootstrapSources = dedupe(options.bootstrapSources || []);
|
|
203
211
|
this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
|
|
204
|
-
this.pollIntervalMs = options.pollIntervalMs ??
|
|
212
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
205
213
|
this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
|
|
206
214
|
this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
|
|
215
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 4000;
|
|
216
|
+
this.peerRefreshIntervalMs = options.peerRefreshIntervalMs ?? 30_000;
|
|
207
217
|
this.envelopeCodec = new JsonMessageEnvelopeCodec();
|
|
208
218
|
this.topicCodec = new JsonTopicCodec();
|
|
219
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
async start(): Promise<void> {
|
|
@@ -215,9 +226,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
215
226
|
this.started = true;
|
|
216
227
|
await this.refreshPeers();
|
|
217
228
|
await this.pollOnce();
|
|
218
|
-
this.
|
|
219
|
-
this.pollOnce().catch(() => {});
|
|
220
|
-
}, this.pollIntervalMs);
|
|
229
|
+
this.scheduleNextPoll(this.pollIntervalMs);
|
|
221
230
|
this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
|
|
222
231
|
} catch (error) {
|
|
223
232
|
this.stats.start_errors += 1;
|
|
@@ -228,7 +237,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
228
237
|
async stop(): Promise<void> {
|
|
229
238
|
if (!this.started) return;
|
|
230
239
|
if (this.poller) {
|
|
231
|
-
|
|
240
|
+
clearTimeout(this.poller);
|
|
232
241
|
this.poller = null;
|
|
233
242
|
}
|
|
234
243
|
try {
|
|
@@ -325,17 +334,37 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
325
334
|
}
|
|
326
335
|
|
|
327
336
|
private async pollOnce(): Promise<void> {
|
|
337
|
+
if (this.pollInFlight) {
|
|
338
|
+
this.stats.poll_skipped_inflight += 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.pollInFlight = true;
|
|
328
342
|
await this.maybeRefreshJoin("poll");
|
|
329
343
|
this.stats.poll_attempted += 1;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
344
|
+
try {
|
|
345
|
+
const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
|
|
346
|
+
this.lastPollAt = Date.now();
|
|
347
|
+
this.stats.poll_succeeded += 1;
|
|
348
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
349
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
350
|
+
for (const message of messages) {
|
|
351
|
+
this.signalingMessagesReceivedTotal += 1;
|
|
352
|
+
this.onEnvelope(message?.envelope);
|
|
353
|
+
}
|
|
354
|
+
if (Array.isArray(payload?.peers)) {
|
|
355
|
+
this.updatePeersFromList(payload.peers);
|
|
356
|
+
} else if (!this.lastPeerRefreshAt || Date.now() - this.lastPeerRefreshAt >= this.peerRefreshIntervalMs) {
|
|
357
|
+
await this.refreshPeers();
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
this.currentPollDelayMs = Math.min(15_000, Math.max(this.pollIntervalMs, this.currentPollDelayMs * 2));
|
|
361
|
+
throw error;
|
|
362
|
+
} finally {
|
|
363
|
+
this.pollInFlight = false;
|
|
364
|
+
if (this.started) {
|
|
365
|
+
this.scheduleNextPoll(this.currentPollDelayMs);
|
|
366
|
+
}
|
|
337
367
|
}
|
|
338
|
-
await this.refreshPeers();
|
|
339
368
|
}
|
|
340
369
|
|
|
341
370
|
private async refreshPeers(): Promise<void> {
|
|
@@ -343,33 +372,8 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
343
372
|
const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
|
|
344
373
|
this.lastPeerRefreshAt = Date.now();
|
|
345
374
|
this.stats.peers_refresh_succeeded += 1;
|
|
346
|
-
const peerIds = Array.isArray(payload?.peers) ? payload.peers
|
|
347
|
-
|
|
348
|
-
await this.joinRoom("self_missing_from_peers");
|
|
349
|
-
}
|
|
350
|
-
const now = Date.now();
|
|
351
|
-
const next = new Map<string, RelayPeer>();
|
|
352
|
-
for (const peerId of peerIds) {
|
|
353
|
-
if (peerId === this.peerId) continue;
|
|
354
|
-
const existing = this.peers.get(peerId);
|
|
355
|
-
if (!existing) {
|
|
356
|
-
this.recordDiscovery("peer_joined", { peer_id: peerId });
|
|
357
|
-
}
|
|
358
|
-
next.set(peerId, {
|
|
359
|
-
peer_id: peerId,
|
|
360
|
-
status: "online",
|
|
361
|
-
first_seen_at: existing?.first_seen_at ?? now,
|
|
362
|
-
last_seen_at: now,
|
|
363
|
-
messages_seen: existing?.messages_seen ?? 0,
|
|
364
|
-
reconnect_attempts: existing?.reconnect_attempts ?? 0,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
for (const peerId of this.peers.keys()) {
|
|
368
|
-
if (!next.has(peerId)) {
|
|
369
|
-
this.recordDiscovery("peer_removed", { peer_id: peerId });
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
this.peers = next;
|
|
375
|
+
const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
|
|
376
|
+
this.updatePeersFromList(peerIds);
|
|
373
377
|
}
|
|
374
378
|
|
|
375
379
|
private onEnvelope(envelope: unknown): void {
|
|
@@ -457,7 +461,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
457
461
|
}
|
|
458
462
|
|
|
459
463
|
private async maybeRefreshJoin(reason: string): Promise<void> {
|
|
460
|
-
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(
|
|
464
|
+
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(45_000, this.pollIntervalMs * 6)) {
|
|
461
465
|
await this.joinRoom(reason);
|
|
462
466
|
}
|
|
463
467
|
}
|
|
@@ -477,11 +481,15 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
477
481
|
const endpoint = this.signalingEndpoints[index]?.replace(/\/+$/, "");
|
|
478
482
|
if (!endpoint) continue;
|
|
479
483
|
try {
|
|
484
|
+
const controller = new AbortController();
|
|
485
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
480
486
|
const response = await fetch(`${endpoint}${path}`, {
|
|
481
487
|
method,
|
|
482
488
|
headers: method === "POST" ? { "content-type": "application/json" } : undefined,
|
|
483
489
|
body: method === "POST" ? JSON.stringify(body) : undefined,
|
|
490
|
+
signal: controller.signal,
|
|
484
491
|
});
|
|
492
|
+
clearTimeout(timeout);
|
|
485
493
|
if (!response.ok) {
|
|
486
494
|
throw new Error(`${method} ${path} failed (${response.status})`);
|
|
487
495
|
}
|
|
@@ -501,4 +509,44 @@ export class RelayPreviewAdapter implements NetworkAdapter {
|
|
|
501
509
|
}
|
|
502
510
|
throw new Error(errors.join(" | "));
|
|
503
511
|
}
|
|
512
|
+
|
|
513
|
+
private updatePeersFromList(values: unknown[]): void {
|
|
514
|
+
const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
|
|
515
|
+
if (!peerIds.includes(this.peerId)) {
|
|
516
|
+
void this.joinRoom("self_missing_from_peers").catch(() => {});
|
|
517
|
+
}
|
|
518
|
+
const now = Date.now();
|
|
519
|
+
const next = new Map<string, RelayPeer>();
|
|
520
|
+
for (const peerId of peerIds) {
|
|
521
|
+
if (peerId === this.peerId) continue;
|
|
522
|
+
const existing = this.peers.get(peerId);
|
|
523
|
+
if (!existing) {
|
|
524
|
+
this.recordDiscovery("peer_joined", { peer_id: peerId });
|
|
525
|
+
}
|
|
526
|
+
next.set(peerId, {
|
|
527
|
+
peer_id: peerId,
|
|
528
|
+
status: "online",
|
|
529
|
+
first_seen_at: existing?.first_seen_at ?? now,
|
|
530
|
+
last_seen_at: now,
|
|
531
|
+
messages_seen: existing?.messages_seen ?? 0,
|
|
532
|
+
reconnect_attempts: existing?.reconnect_attempts ?? 0,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
for (const peerId of this.peers.keys()) {
|
|
536
|
+
if (!next.has(peerId)) {
|
|
537
|
+
this.recordDiscovery("peer_removed", { peer_id: peerId });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
this.peers = next;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private scheduleNextPoll(delayMs: number): void {
|
|
544
|
+
if (this.poller) {
|
|
545
|
+
clearTimeout(this.poller);
|
|
546
|
+
}
|
|
547
|
+
const jitterMs = Math.floor(Math.random() * 400);
|
|
548
|
+
this.poller = setTimeout(() => {
|
|
549
|
+
this.pollOnce().catch(() => {});
|
|
550
|
+
}, Math.max(1000, delayMs + jitterMs));
|
|
551
|
+
}
|
|
504
552
|
}
|
|
@@ -89,14 +89,18 @@ function userEnvFile() {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function ensureLineInFile(filePath, block) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
try {
|
|
93
|
+
const current = existsSync(filePath) ? readFileSync(filePath, "utf8") : "";
|
|
94
|
+
if (current.includes(block.trim())) {
|
|
95
|
+
return { changed: false, error: null };
|
|
96
|
+
}
|
|
97
|
+
const next = `${current.replace(/\s*$/, "")}\n\n${block}\n`;
|
|
98
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
99
|
+
writeFileSync(filePath, next, "utf8");
|
|
100
|
+
return { changed: true, error: null };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
95
103
|
}
|
|
96
|
-
const next = `${current.replace(/\s*$/, "")}\n\n${block}\n`;
|
|
97
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
98
|
-
writeFileSync(filePath, next, "utf8");
|
|
99
|
-
return true;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function shellInitTargets() {
|
|
@@ -159,10 +163,15 @@ function installPersistentCommand() {
|
|
|
159
163
|
);
|
|
160
164
|
const rcFiles = shellInitTargets();
|
|
161
165
|
const updatedFiles = [];
|
|
166
|
+
const failedFiles = [];
|
|
162
167
|
for (const filePath of rcFiles) {
|
|
163
|
-
|
|
168
|
+
const result = ensureLineInFile(filePath, rcBlock);
|
|
169
|
+
if (result.changed) {
|
|
164
170
|
updatedFiles.push(filePath);
|
|
165
171
|
}
|
|
172
|
+
if (result.error) {
|
|
173
|
+
failedFiles.push({ filePath, error: result.error });
|
|
174
|
+
}
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
console.log("Installed persistent `silicaclaw` command.");
|
|
@@ -175,6 +184,16 @@ function installPersistentCommand() {
|
|
|
175
184
|
if (updatedFiles.length === 0) {
|
|
176
185
|
console.log("Shell startup files were already configured.");
|
|
177
186
|
}
|
|
187
|
+
if (failedFiles.length > 0) {
|
|
188
|
+
console.log("");
|
|
189
|
+
console.log("Some shell startup files could not be updated automatically:");
|
|
190
|
+
for (const item of failedFiles) {
|
|
191
|
+
console.log(`- ${item.filePath}: ${item.error}`);
|
|
192
|
+
}
|
|
193
|
+
console.log("");
|
|
194
|
+
console.log("You can add this line manually to one shell startup file:");
|
|
195
|
+
console.log('[ -f "$HOME/.silicaclaw/env.sh" ] && . "$HOME/.silicaclaw/env.sh"');
|
|
196
|
+
}
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
function isNpxRun() {
|
|
@@ -5,6 +5,7 @@ import { randomUUID, createHash } from 'crypto';
|
|
|
5
5
|
const port = Number(process.env.PORT || process.env.WEBRTC_SIGNALING_PORT || 4510);
|
|
6
6
|
const PEER_STALE_MS = Number(process.env.WEBRTC_SIGNALING_PEER_STALE_MS || 120000);
|
|
7
7
|
const SIGNAL_DEDUPE_WINDOW_MS = Number(process.env.WEBRTC_SIGNALING_DEDUPE_WINDOW_MS || 60000);
|
|
8
|
+
const TOUCH_WRITE_INTERVAL_MS = Number(process.env.WEBRTC_SIGNALING_TOUCH_WRITE_INTERVAL_MS || 30000);
|
|
8
9
|
|
|
9
10
|
/** @type {Map<string, {peers: Map<string, {last_seen_at:number}>, queues: Map<string, any[]>, relay_queues: Map<string, any[]>, signal_fingerprints: Map<string, number>}>} */
|
|
10
11
|
const rooms = new Map();
|
|
@@ -90,10 +91,13 @@ function cleanupRoom(roomId) {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function touchPeer(room, peerId) {
|
|
94
|
+
const ts = now();
|
|
95
|
+
const previous = room.peers.get(peerId)?.last_seen_at || 0;
|
|
96
|
+
const shouldWrite = !previous || ts - previous >= TOUCH_WRITE_INTERVAL_MS;
|
|
93
97
|
if (!room.peers.has(peerId)) {
|
|
94
|
-
room.peers.set(peerId, { last_seen_at:
|
|
98
|
+
room.peers.set(peerId, { last_seen_at: ts });
|
|
95
99
|
} else {
|
|
96
|
-
room.peers.get(peerId).last_seen_at =
|
|
100
|
+
room.peers.get(peerId).last_seen_at = shouldWrite ? ts : previous;
|
|
97
101
|
}
|
|
98
102
|
if (!room.queues.has(peerId)) {
|
|
99
103
|
room.queues.set(peerId, []);
|
|
@@ -101,6 +105,7 @@ function touchPeer(room, peerId) {
|
|
|
101
105
|
if (!room.relay_queues.has(peerId)) {
|
|
102
106
|
room.relay_queues.set(peerId, []);
|
|
103
107
|
}
|
|
108
|
+
return shouldWrite;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
function isValidSignalPayload(body) {
|
|
@@ -198,7 +203,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
198
203
|
|
|
199
204
|
const queue = room.queues.get(peerId) || [];
|
|
200
205
|
room.queues.set(peerId, []);
|
|
201
|
-
return json(res, 200, { ok: true, messages: queue });
|
|
206
|
+
return json(res, 200, { ok: true, messages: queue, peers: Array.from(room.peers.keys()) });
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
if (req.method === 'GET' && url.pathname === '/relay/poll') {
|
|
@@ -215,7 +220,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
215
220
|
|
|
216
221
|
const queue = room.relay_queues.get(peerId) || [];
|
|
217
222
|
room.relay_queues.set(peerId, []);
|
|
218
|
-
return json(res, 200, { ok: true, messages: queue });
|
|
223
|
+
return json(res, 200, { ok: true, messages: queue, peers: Array.from(room.peers.keys()) });
|
|
219
224
|
}
|
|
220
225
|
|
|
221
226
|
if (req.method === 'POST' && url.pathname === '/join') {
|