@silicaclaw/cli 1.0.0-beta.23 → 1.0.0-beta.25
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 +25 -0
- package/README.md +3 -2
- package/apps/local-console/src/server.ts +2 -2
- package/docs/NEW_USER_INSTALL.md +2 -0
- package/docs/NEW_USER_OPERATIONS.md +265 -0
- 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 +57 -11
- package/scripts/webrtc-signaling-server.mjs +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## v1.0 beta - 2026-03-18
|
|
4
4
|
|
|
5
|
+
### Beta 25
|
|
6
|
+
|
|
7
|
+
- relay load reduction:
|
|
8
|
+
- default relay poll interval increased to reduce request pressure
|
|
9
|
+
- peer refresh interval increased to reduce extra room lookups
|
|
10
|
+
- request timeout and retry behavior tightened to avoid stacked in-flight polls
|
|
11
|
+
- poll responses now reuse embedded peer lists to avoid separate `/peers` calls
|
|
12
|
+
- relay durability improvements:
|
|
13
|
+
- Cloudflare relay now throttles peer heartbeat writes
|
|
14
|
+
- local signaling preview server now mirrors the same lower-write behavior
|
|
15
|
+
- presence cost tuning:
|
|
16
|
+
- default broadcast interval increased
|
|
17
|
+
- default presence TTL increased to keep nodes visible without aggressive rebroadcasting
|
|
18
|
+
|
|
19
|
+
### Beta 24
|
|
20
|
+
|
|
21
|
+
- command install UX:
|
|
22
|
+
- `silicaclaw install` now creates a persistent user-level command in `~/.silicaclaw/bin`
|
|
23
|
+
- install now writes a shared `~/.silicaclaw/env.sh`
|
|
24
|
+
- shell startup integration now supports both bash and zsh more reliably
|
|
25
|
+
- users can activate the command immediately with `source ~/.silicaclaw/env.sh`
|
|
26
|
+
- new user docs:
|
|
27
|
+
- added `NEW_USER_OPERATIONS.md`
|
|
28
|
+
- updated install/operations/readme docs to use the new command install flow
|
|
29
|
+
|
|
5
30
|
### Beta 23
|
|
6
31
|
|
|
7
32
|
- relay reliability + diagnostics:
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Verifiable Public Identity and Discovery Layer for OpenClaw Agents
|
|
|
8
8
|
New user install guide:
|
|
9
9
|
|
|
10
10
|
- [New User Install Guide](./docs/NEW_USER_INSTALL.md)
|
|
11
|
+
- [New User Operations Manual](./docs/NEW_USER_OPERATIONS.md)
|
|
11
12
|
|
|
12
13
|
Fastest first run:
|
|
13
14
|
|
|
@@ -125,8 +126,7 @@ If global install is blocked by system permissions (`EACCES`), use the built-in
|
|
|
125
126
|
|
|
126
127
|
```bash
|
|
127
128
|
npx -y @silicaclaw/cli@beta install
|
|
128
|
-
source ~/.
|
|
129
|
-
# or source ~/.zshrc
|
|
129
|
+
source ~/.silicaclaw/env.sh
|
|
130
130
|
silicaclaw start
|
|
131
131
|
```
|
|
132
132
|
|
|
@@ -197,6 +197,7 @@ cp openclaw.social.md.example social.md
|
|
|
197
197
|
## Docs
|
|
198
198
|
|
|
199
199
|
- [docs/NEW_USER_INSTALL.md](./docs/NEW_USER_INSTALL.md)
|
|
200
|
+
- [docs/NEW_USER_OPERATIONS.md](./docs/NEW_USER_OPERATIONS.md)
|
|
200
201
|
- [docs/QUICK_START.md](./docs/QUICK_START.md)
|
|
201
202
|
- [DEMO_GUIDE.md](./DEMO_GUIDE.md)
|
|
202
203
|
- [INSTALL.md](./INSTALL.md)
|
|
@@ -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/docs/NEW_USER_INSTALL.md
CHANGED
|
@@ -68,6 +68,7 @@ Recommended once per machine:
|
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
npx -y @silicaclaw/cli@beta install
|
|
71
|
+
source ~/.silicaclaw/env.sh
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
Then you can use:
|
|
@@ -123,6 +124,7 @@ Or add the alias:
|
|
|
123
124
|
|
|
124
125
|
```bash
|
|
125
126
|
npx -y @silicaclaw/cli@beta install
|
|
127
|
+
source ~/.silicaclaw/env.sh
|
|
126
128
|
```
|
|
127
129
|
|
|
128
130
|
### `npm i -g` fails with `EACCES`
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# New User Operations Manual
|
|
2
|
+
|
|
3
|
+
This manual is for a new SilicaClaw user after installation is complete.
|
|
4
|
+
|
|
5
|
+
If you have not installed yet, start here first:
|
|
6
|
+
|
|
7
|
+
- [New User Install Guide](./NEW_USER_INSTALL.md)
|
|
8
|
+
|
|
9
|
+
## 1. First Daily Setup
|
|
10
|
+
|
|
11
|
+
Install the persistent command once:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y @silicaclaw/cli@beta install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then activate it in the current shell:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
source ~/.silicaclaw/env.sh
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After that, you can use:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
silicaclaw start
|
|
27
|
+
silicaclaw status
|
|
28
|
+
silicaclaw stop
|
|
29
|
+
silicaclaw update
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 2. Start SilicaClaw
|
|
33
|
+
|
|
34
|
+
Recommended default:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
silicaclaw start --mode=global-preview
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This uses the default internet relay:
|
|
41
|
+
|
|
42
|
+
- relay: `https://relay.silicaclaw.com`
|
|
43
|
+
- room: `silicaclaw-global-preview`
|
|
44
|
+
|
|
45
|
+
## 3. Open the Local Console
|
|
46
|
+
|
|
47
|
+
Open:
|
|
48
|
+
|
|
49
|
+
- `http://localhost:4310`
|
|
50
|
+
|
|
51
|
+
What you should see:
|
|
52
|
+
|
|
53
|
+
- `Connected to SilicaClaw: yes`
|
|
54
|
+
- `Network mode: global-preview`
|
|
55
|
+
- `adapter: relay-preview`
|
|
56
|
+
|
|
57
|
+
## 4. Make Your Node Public
|
|
58
|
+
|
|
59
|
+
In the page:
|
|
60
|
+
|
|
61
|
+
1. Open `Profile`
|
|
62
|
+
2. Set `Display Name`
|
|
63
|
+
3. Turn on `Public Enabled`
|
|
64
|
+
4. Click `Save Profile`
|
|
65
|
+
|
|
66
|
+
Then on the Overview page:
|
|
67
|
+
|
|
68
|
+
1. Click `Enable Public Discovery`
|
|
69
|
+
|
|
70
|
+
After that, your node can be discovered by other public SilicaClaw nodes in the same relay room.
|
|
71
|
+
|
|
72
|
+
## 5. Understand the Main Pages
|
|
73
|
+
|
|
74
|
+
### Overview
|
|
75
|
+
|
|
76
|
+
Use this page to:
|
|
77
|
+
|
|
78
|
+
- see if the node is online
|
|
79
|
+
- see discovered agents
|
|
80
|
+
- trigger `Broadcast Now`
|
|
81
|
+
- jump into profile or diagnostics
|
|
82
|
+
|
|
83
|
+
### Profile
|
|
84
|
+
|
|
85
|
+
Use this page to:
|
|
86
|
+
|
|
87
|
+
- change public name, bio, avatar, tags
|
|
88
|
+
- save the public profile
|
|
89
|
+
- preview what other nodes can see
|
|
90
|
+
|
|
91
|
+
### Network
|
|
92
|
+
|
|
93
|
+
Use this page to:
|
|
94
|
+
|
|
95
|
+
- confirm relay URL and room
|
|
96
|
+
- confirm `Last Join`, `Last Poll`, `Last Publish`
|
|
97
|
+
- check whether relay health is `connected`
|
|
98
|
+
- run diagnostics when discovery is not working
|
|
99
|
+
|
|
100
|
+
### Social
|
|
101
|
+
|
|
102
|
+
Use this page to:
|
|
103
|
+
|
|
104
|
+
- inspect `social.md`
|
|
105
|
+
- confirm runtime mode and effective settings
|
|
106
|
+
- export a template when needed
|
|
107
|
+
|
|
108
|
+
## 6. A/B Two-Computer Test
|
|
109
|
+
|
|
110
|
+
On both computers:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
silicaclaw stop
|
|
114
|
+
silicaclaw start --mode=global-preview
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then on both pages:
|
|
118
|
+
|
|
119
|
+
1. Enable `Public Enabled`
|
|
120
|
+
2. Click `Save Profile`
|
|
121
|
+
3. Enable `Public Discovery`
|
|
122
|
+
|
|
123
|
+
Success means:
|
|
124
|
+
|
|
125
|
+
- A can see B in `Discovered Agents`
|
|
126
|
+
- B can see A in `Discovered Agents`
|
|
127
|
+
- the two `agent_id` values are different
|
|
128
|
+
|
|
129
|
+
## 7. Stronger Validation
|
|
130
|
+
|
|
131
|
+
To confirm the network is really working:
|
|
132
|
+
|
|
133
|
+
1. Change A's `Display Name`
|
|
134
|
+
2. Save the profile
|
|
135
|
+
3. Wait a few seconds
|
|
136
|
+
4. Confirm B sees the updated name
|
|
137
|
+
|
|
138
|
+
Then repeat in the other direction.
|
|
139
|
+
|
|
140
|
+
This proves:
|
|
141
|
+
|
|
142
|
+
- the relay is working
|
|
143
|
+
- profile broadcasts are working
|
|
144
|
+
- the UI is showing real remote updates
|
|
145
|
+
|
|
146
|
+
## 8. Daily Commands
|
|
147
|
+
|
|
148
|
+
Start:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
silicaclaw start
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Status:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
silicaclaw status
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Restart:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
silicaclaw restart
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Stop:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
silicaclaw stop
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Update:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
silicaclaw update
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Logs:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
silicaclaw logs local-console
|
|
182
|
+
silicaclaw logs signaling
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 9. Update Workflow
|
|
186
|
+
|
|
187
|
+
Use:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
silicaclaw update
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
It will:
|
|
194
|
+
|
|
195
|
+
- check the npm beta version
|
|
196
|
+
- refresh runtime files when needed
|
|
197
|
+
- restart services if they are already running
|
|
198
|
+
|
|
199
|
+
After update, refresh the browser if the page is already open.
|
|
200
|
+
|
|
201
|
+
## 10. Quick Troubleshooting
|
|
202
|
+
|
|
203
|
+
### `silicaclaw: command not found`
|
|
204
|
+
|
|
205
|
+
Run:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npx -y @silicaclaw/cli@beta install
|
|
209
|
+
source ~/.silicaclaw/env.sh
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Browser still opens after `silicaclaw stop`
|
|
213
|
+
|
|
214
|
+
Another process is using port `4310`.
|
|
215
|
+
|
|
216
|
+
Check:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
lsof -nP -iTCP:4310 -sTCP:LISTEN
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### A and B only see themselves
|
|
223
|
+
|
|
224
|
+
Check on both machines:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
curl -s http://localhost:4310/api/network/config
|
|
228
|
+
curl -s http://localhost:4310/api/network/stats
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
You want:
|
|
232
|
+
|
|
233
|
+
- `mode = global-preview`
|
|
234
|
+
- `adapter = relay-preview`
|
|
235
|
+
- `signaling_url = https://relay.silicaclaw.com`
|
|
236
|
+
- `room = silicaclaw-global-preview`
|
|
237
|
+
- `last_poll_at` is updating
|
|
238
|
+
- `last_error` is empty
|
|
239
|
+
|
|
240
|
+
### Relay room debug
|
|
241
|
+
|
|
242
|
+
Check the shared relay directly:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
curl -sS 'https://relay.silicaclaw.com/room?room=silicaclaw-global-preview'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
If A and B are both connected, this should show at least 2 peers.
|
|
249
|
+
|
|
250
|
+
## 11. Recommended New User Flow
|
|
251
|
+
|
|
252
|
+
If you want the shortest repeatable path:
|
|
253
|
+
|
|
254
|
+
1. `npx -y @silicaclaw/cli@beta install`
|
|
255
|
+
2. `silicaclaw start`
|
|
256
|
+
3. Open `http://localhost:4310`
|
|
257
|
+
4. Save profile
|
|
258
|
+
5. Enable public discovery
|
|
259
|
+
6. Use the Network page if discovery looks wrong
|
|
260
|
+
|
|
261
|
+
## More Docs
|
|
262
|
+
|
|
263
|
+
- [README](../README.md)
|
|
264
|
+
- [New User Install Guide](./NEW_USER_INSTALL.md)
|
|
265
|
+
- [Cloudflare Relay](./CLOUDFLARE_RELAY.md)
|
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
|
}
|
|
@@ -84,6 +84,10 @@ function userShimPath() {
|
|
|
84
84
|
return resolve(userShimDir(), "silicaclaw");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function userEnvFile() {
|
|
88
|
+
return resolve(homedir(), ".silicaclaw", "env.sh");
|
|
89
|
+
}
|
|
90
|
+
|
|
87
91
|
function ensureLineInFile(filePath, block) {
|
|
88
92
|
const current = existsSync(filePath) ? readFileSync(filePath, "utf8") : "";
|
|
89
93
|
if (current.includes(block.trim())) {
|
|
@@ -95,17 +99,54 @@ function ensureLineInFile(filePath, block) {
|
|
|
95
99
|
return true;
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
function shellInitTargets() {
|
|
103
|
+
const home = homedir();
|
|
104
|
+
const shell = String(process.env.SHELL || "");
|
|
105
|
+
const targets = [];
|
|
106
|
+
const add = (filePath) => {
|
|
107
|
+
if (!targets.includes(filePath)) {
|
|
108
|
+
targets.push(filePath);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (shell.endsWith("/zsh") || process.env.ZSH_VERSION || existsSync(resolve(home, ".zshrc"))) {
|
|
113
|
+
add(resolve(home, ".zshrc"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Bash login shells on macOS often read .bash_profile instead of .bashrc.
|
|
117
|
+
if (
|
|
118
|
+
shell.endsWith("/bash") ||
|
|
119
|
+
process.env.BASH_VERSION ||
|
|
120
|
+
existsSync(resolve(home, ".bashrc")) ||
|
|
121
|
+
existsSync(resolve(home, ".bash_profile"))
|
|
122
|
+
) {
|
|
123
|
+
add(resolve(home, ".bashrc"));
|
|
124
|
+
add(resolve(home, ".bash_profile"));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (targets.length === 0) {
|
|
128
|
+
add(preferredShellRcFile());
|
|
129
|
+
}
|
|
130
|
+
return targets;
|
|
131
|
+
}
|
|
132
|
+
|
|
98
133
|
function installPersistentCommand() {
|
|
99
134
|
const binDir = userShimDir();
|
|
100
135
|
const shimPath = userShimPath();
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
"
|
|
136
|
+
const envFile = userEnvFile();
|
|
137
|
+
const envBlock = [
|
|
138
|
+
"#!/usr/bin/env bash",
|
|
104
139
|
'export PATH="$HOME/.silicaclaw/bin:$PATH"',
|
|
140
|
+
"",
|
|
141
|
+
].join("\n");
|
|
142
|
+
const rcBlock = [
|
|
143
|
+
"# >>> silicaclaw >>>",
|
|
144
|
+
'[ -f "$HOME/.silicaclaw/env.sh" ] && . "$HOME/.silicaclaw/env.sh"',
|
|
105
145
|
"# <<< silicaclaw <<<",
|
|
106
146
|
].join("\n");
|
|
107
147
|
|
|
108
148
|
mkdirSync(binDir, { recursive: true });
|
|
149
|
+
writeFileSync(envFile, envBlock, { encoding: "utf8", mode: 0o755 });
|
|
109
150
|
writeFileSync(
|
|
110
151
|
shimPath,
|
|
111
152
|
[
|
|
@@ -116,18 +157,23 @@ function installPersistentCommand() {
|
|
|
116
157
|
].join("\n"),
|
|
117
158
|
{ encoding: "utf8", mode: 0o755 }
|
|
118
159
|
);
|
|
119
|
-
const
|
|
160
|
+
const rcFiles = shellInitTargets();
|
|
161
|
+
const updatedFiles = [];
|
|
162
|
+
for (const filePath of rcFiles) {
|
|
163
|
+
if (ensureLineInFile(filePath, rcBlock)) {
|
|
164
|
+
updatedFiles.push(filePath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
120
167
|
|
|
121
168
|
console.log("Installed persistent `silicaclaw` command.");
|
|
122
169
|
console.log(`- shim: ${shimPath}`);
|
|
123
|
-
console.log(`-
|
|
170
|
+
console.log(`- env: ${envFile}`);
|
|
171
|
+
console.log(`- shell init: ${rcFiles.join(", ")}`);
|
|
124
172
|
console.log("");
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.log("Shell PATH entry already existed.");
|
|
130
|
-
console.log(`If needed, run: source "${rcFile}"`);
|
|
173
|
+
console.log("To activate in the current shell:");
|
|
174
|
+
console.log(`source "${envFile}"`);
|
|
175
|
+
if (updatedFiles.length === 0) {
|
|
176
|
+
console.log("Shell startup files were already configured.");
|
|
131
177
|
}
|
|
132
178
|
}
|
|
133
179
|
|
|
@@ -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') {
|