@claw-link/gateway-host 0.3.6 → 0.3.8
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/bin/cli.js +2 -2
- package/package.json +1 -1
- package/scripts/install.js +141 -28
- package/src/worker.js +26 -11
package/bin/cli.js
CHANGED
|
@@ -104,8 +104,8 @@ async function main() {
|
|
|
104
104
|
' transport-test Verify content-addressed Capsule relay (host ⇄ central sync + integrity)',
|
|
105
105
|
' p2p-test Verify the native QUIC P2P transport end-to-end on this machine (loopback)',
|
|
106
106
|
' start Start the background service',
|
|
107
|
-
' stop Stop the background service',
|
|
108
|
-
' restart Restart the
|
|
107
|
+
' stop Stop the background service (finishes the in-flight job first)',
|
|
108
|
+
' restart Restart the service — waits for in-flight jobs ([--force] [--timeout <s>])',
|
|
109
109
|
' update Refresh the service to this CLI’s version — waits for in-flight jobs to finish',
|
|
110
110
|
' (clhost update [--force] [--timeout <seconds>])',
|
|
111
111
|
' setup install + add-agent in one flow',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claw-link/gateway-host",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "ClawLink Host Gateway — a secure, outbound-only worker that bridges a local agent CLI (OpenClaw, Hermes, Claude, Codex, Cursor) to your ClawLink agents. No inbound ports; authenticated per-agent by a Host Token.",
|
|
5
5
|
"homepage": "https://claw-link.co",
|
|
6
6
|
"bin": {
|
package/scripts/install.js
CHANGED
|
@@ -139,9 +139,9 @@ async function run() {
|
|
|
139
139
|
onWait: (s) => process.stdout.write(`\r … still working: ${s.activeJobs} job(s) in flight `),
|
|
140
140
|
});
|
|
141
141
|
if (live && live.busy) process.stdout.write('\n');
|
|
142
|
-
console.log(
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
if (!r.restarted) console.log(' ⚠ Could not refresh the service automatically — run: clhost restart');
|
|
143
|
+
else if (r.confirmed) console.log(` ✓ Refreshed — now running v${r.version} (pid ${r.pid}).`);
|
|
144
|
+
else console.log(` ⚠ Restart triggered but the new worker hasn't come up on v${VERSION} yet — check \`clhost status\` in a few seconds.`);
|
|
145
145
|
reportCli();
|
|
146
146
|
console.log('\n Commands: clhost status · clhost agents · clhost add-agent · clhost transport-test\n');
|
|
147
147
|
return;
|
|
@@ -190,9 +190,10 @@ async function addAgentCmd() {
|
|
|
190
190
|
rl.close();
|
|
191
191
|
if (agent) {
|
|
192
192
|
saveAgent(cfg, agent);
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
const r = await gracefulServiceRestartCLI();
|
|
194
|
+
const note = !r.restarted ? ' Run `clhost restart` (or start) to apply.'
|
|
195
|
+
: r.confirmed ? ' Service restarted.' : ' Restarting — check `clhost status`.';
|
|
196
|
+
console.log(`\n ✓ Added agent ${agent.agent_id} (${agent.runtime}).` + note + '\n');
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -212,8 +213,10 @@ async function removeAgentCmd() {
|
|
|
212
213
|
cfg.agents = cfg.agents.filter((a) => !(a.agent_id && (a.agent_id === target || a.agent_id.startsWith(target))));
|
|
213
214
|
if (cfg.agents.length === before) { console.log(` No agent matched '${target}'.`); return; }
|
|
214
215
|
config.save(cfg);
|
|
215
|
-
const
|
|
216
|
-
|
|
216
|
+
const r = await gracefulServiceRestartCLI();
|
|
217
|
+
const note = !r.restarted ? ' Run `clhost restart` to apply.'
|
|
218
|
+
: r.confirmed ? ' Service restarted.' : ' Restarting — check `clhost status`.';
|
|
219
|
+
console.log(` ✓ Removed agent '${target}'.` + note);
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
function printAgents(cfg) {
|
|
@@ -269,9 +272,11 @@ async function retokenCmd() {
|
|
|
269
272
|
agent.runtime = resp.runtime;
|
|
270
273
|
}
|
|
271
274
|
config.save(cfg);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
rl.close(); // release stdin before the (possibly long) graceful drain so Ctrl-C is handled by our cleanup
|
|
276
|
+
const r = await gracefulServiceRestartCLI();
|
|
277
|
+
const note = !r.restarted ? ' Run `clhost restart` to apply.'
|
|
278
|
+
: r.confirmed ? ' Service restarted.' : ' Restarting — check `clhost status`.';
|
|
279
|
+
console.log(`\n ✓ Updated token for ${agent.agent_id} (…${token.slice(-4)}).` + note + '\n');
|
|
275
280
|
} finally { rl.close(); }
|
|
276
281
|
}
|
|
277
282
|
|
|
@@ -296,13 +301,33 @@ function serviceControl(action) {
|
|
|
296
301
|
if (platform === 'win32') {
|
|
297
302
|
if (action === 'stop') return execSafe('schtasks /End /TN ClawLinkHost') !== null;
|
|
298
303
|
if (action === 'start') return execSafe('schtasks /Run /TN ClawLinkHost') !== null;
|
|
299
|
-
if (action === 'restart') {
|
|
304
|
+
if (action === 'restart') {
|
|
305
|
+
execSafe('schtasks /End /TN ClawLinkHost');
|
|
306
|
+
// /End is async, and the task is MultipleInstancesPolicy=IgnoreNew — so an immediate /Run can be
|
|
307
|
+
// SILENTLY DROPPED while the old instance is still stopping (the Windows analogue of the macOS
|
|
308
|
+
// bootout→bootstrap race). Give it a brief, LANGUAGE-NEUTRAL pause to stop before /Run: `ping -n
|
|
309
|
+
// 4 127.0.0.1` ≈ 3s (parsing schtasks' status would depend on the localized word "Running"; and
|
|
310
|
+
// `timeout` needs an interactive console). If /Run still doesn't take, restartAndConfirm's
|
|
311
|
+
// confirm-then-reinstall fallback recovers.
|
|
312
|
+
execSafe('ping -n 4 127.0.0.1');
|
|
313
|
+
return execSafe('schtasks /Run /TN ClawLinkHost') !== null;
|
|
314
|
+
}
|
|
300
315
|
}
|
|
301
316
|
} catch { /* fall through */ }
|
|
302
317
|
return false;
|
|
303
318
|
}
|
|
304
319
|
|
|
305
|
-
function serviceControlCmd(action) {
|
|
320
|
+
async function serviceControlCmd(action) {
|
|
321
|
+
// `restart` is graceful — drain in-flight jobs first (override with --force / --timeout <sec>).
|
|
322
|
+
// `stop` stays prompt: its SIGTERM already lets the worker finish in-flight work (2nd signal forces).
|
|
323
|
+
if (action === 'restart') {
|
|
324
|
+
const { force, timeoutMs } = parseDrainArgs();
|
|
325
|
+
const r = await gracefulServiceRestartCLI({ force, timeoutMs });
|
|
326
|
+
if (!r.restarted) console.log(' ⚠ Could not restart the service (is it installed? run `clhost install`).');
|
|
327
|
+
else if (r.confirmed) console.log(` ✓ Service restarted — running v${r.version} (pid ${r.pid}).`);
|
|
328
|
+
else console.log(' ⚠ Restart triggered but the worker hasn’t reported yet — check `clhost status`.');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
306
331
|
const ok = serviceControl(action);
|
|
307
332
|
console.log(ok ? ` ✓ Service ${action}ed.` : ` ⚠ Could not ${action} the service (is it installed? run \`clhost install\`).`);
|
|
308
333
|
}
|
|
@@ -538,6 +563,9 @@ async function drainToIdle({ timeoutMs = 15 * 60 * 1000, force = false, onWait }
|
|
|
538
563
|
if (force) return { hadWorker: true, drained: false, forced: true };
|
|
539
564
|
|
|
540
565
|
control.requestDrain();
|
|
566
|
+
// NOTE: the Ctrl-C cleanup that clears the drain flag on abort lives in withDrainGuard(), which
|
|
567
|
+
// wraps this call AND the restart — so there's no window (between drain finishing and the restart)
|
|
568
|
+
// where an interrupt could leave the worker drained. Don't install a handler here.
|
|
541
569
|
const startedAt = Date.now();
|
|
542
570
|
let waited = false;
|
|
543
571
|
while (Date.now() - startedAt < timeoutMs) {
|
|
@@ -551,20 +579,92 @@ async function drainToIdle({ timeoutMs = 15 * 60 * 1000, force = false, onWait }
|
|
|
551
579
|
return { hadWorker: true, drained: false, timedOut: true };
|
|
552
580
|
}
|
|
553
581
|
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
582
|
+
// Run an async drain+restart sequence under a Ctrl-C guard: if the operator interrupts at ANY point
|
|
583
|
+
// (during the drain wait OR the restart), clear the drain flag FIRST so the still-running worker
|
|
584
|
+
// resumes claiming — never left wedged — then exit. Spanning the whole sequence closes the gap that
|
|
585
|
+
// a per-drainToIdle handler would leave open between draining and restarting.
|
|
586
|
+
async function withDrainGuard(fn) {
|
|
587
|
+
const onInt = () => { try { control.clearDrain(); } catch { /* ignore */ } process.exit(130); };
|
|
588
|
+
process.on('SIGINT', onInt);
|
|
589
|
+
process.on('SIGTERM', onInt);
|
|
590
|
+
try { return await fn(); }
|
|
591
|
+
finally { process.off('SIGINT', onInt); process.off('SIGTERM', onInt); }
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// After triggering a restart, wait until the NEW worker actually publishes status (a different pid
|
|
595
|
+
// than before and — if given — the expected version), so a caller never reports success while the old
|
|
596
|
+
// process is still winding down or a launchd unload/reload is mid-flight. Best-effort: confirmed=false
|
|
597
|
+
// on timeout. This is what makes `clhost update`'s "runs vX" honest instead of premature.
|
|
598
|
+
async function waitForWorkerUp(prevPid, wantVersion, timeoutMs = 25000) {
|
|
599
|
+
const startedAt = Date.now();
|
|
600
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
601
|
+
await sleep(1000);
|
|
602
|
+
const s = control.liveStatus();
|
|
603
|
+
if (s && s.pid !== prevPid && (!wantVersion || String(s.version) === String(wantVersion))) {
|
|
604
|
+
return { confirmed: true, version: s.version, pid: s.pid };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const s = control.liveStatus();
|
|
608
|
+
return { confirmed: false, version: s && s.version, pid: s && s.pid };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Restart the service and CONFIRM the new worker came up. Prefer an IN-PLACE restart
|
|
612
|
+
// (kickstart -k / systemctl restart / schtasks) — it never unloads the job (no "Service: stopped"
|
|
613
|
+
// window like bootout→bootstrap) and relaunches from the stable app dir. Only if that fails to bring
|
|
614
|
+
// a worker up (service not loaded yet, or a stale Node path in the plist after an nvm/Node upgrade)
|
|
615
|
+
// fall back to a full (re)install, which rewrites the plist. Clears the drain flag unless a fresh
|
|
616
|
+
// worker is confirmed (which clears it on boot) so nothing is ever left drained.
|
|
617
|
+
async function restartAndConfirm({ stageCode = false, wantVersion, onRestart } = {}) {
|
|
618
|
+
if (stageCode) syncAppDir(); // copy the new code into the stable app dir the service runs from
|
|
619
|
+
const prevPid = (control.liveStatus() || {}).pid;
|
|
620
|
+
let notified = false;
|
|
621
|
+
const notify = () => { if (!notified && typeof onRestart === 'function') { notified = true; onRestart(); } };
|
|
622
|
+
let restarted = serviceControl('restart');
|
|
623
|
+
if (restarted) notify();
|
|
624
|
+
let up = restarted ? await waitForWorkerUp(prevPid, wantVersion) : { confirmed: false };
|
|
625
|
+
if (!up.confirmed) {
|
|
626
|
+
const reinstalled = installService(); // rewrites plist/unit (handles a moved Node) + reload
|
|
627
|
+
if (reinstalled) { restarted = true; notify(); up = await waitForWorkerUp(prevPid, wantVersion); }
|
|
628
|
+
}
|
|
629
|
+
if (!up.confirmed) control.clearDrain();
|
|
630
|
+
return { restarted, ...up };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// `update` path — drain, then stage new code + restart, confirming the new version is actually live.
|
|
557
634
|
async function gracefulRestart(opts = {}) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
635
|
+
return withDrainGuard(async () => {
|
|
636
|
+
const drain = await drainToIdle(opts);
|
|
637
|
+
const r = await restartAndConfirm({ stageCode: true, wantVersion: VERSION, onRestart: opts.onRestart });
|
|
638
|
+
return { ...drain, ...r };
|
|
639
|
+
});
|
|
562
640
|
}
|
|
563
641
|
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
async
|
|
642
|
+
// Config-change restart (add/remove/retoken agent, `clhost restart`): drain first, then restart in
|
|
643
|
+
// place (no code re-stage — same version), confirming the worker actually bounced (new pid).
|
|
644
|
+
async function gracefulServiceRestart(opts = {}) {
|
|
645
|
+
return withDrainGuard(async () => {
|
|
646
|
+
const drain = await drainToIdle(opts);
|
|
647
|
+
const r = await restartAndConfirm({ stageCode: false, onRestart: opts.onRestart });
|
|
648
|
+
return { ...drain, ...r };
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Console wrapper around gracefulServiceRestart: prints a one-time "waiting" notice + a live progress
|
|
653
|
+
// line while a job finishes, so config commands never silently hang. Returns the restart result.
|
|
654
|
+
async function gracefulServiceRestartCLI(opts = {}) {
|
|
655
|
+
const live = control.liveStatus();
|
|
656
|
+
const willWait = live && live.busy && !opts.force;
|
|
657
|
+
if (willWait) console.log(' ⏳ An agent is mid-job — restarting once it finishes (Ctrl-C to cancel; it stays on the current version)…');
|
|
658
|
+
const r = await gracefulServiceRestart({
|
|
659
|
+
...opts,
|
|
660
|
+
onWait: (s) => process.stdout.write(`\r … still working: ${s.activeJobs} job(s) in flight `),
|
|
661
|
+
});
|
|
662
|
+
if (willWait) process.stdout.write('\n');
|
|
663
|
+
return r;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Shared `--force` / `--timeout <seconds>` parser for update() and the restart command.
|
|
667
|
+
function parseDrainArgs() {
|
|
568
668
|
const args = process.argv.slice(3);
|
|
569
669
|
const force = args.includes('--force') || args.includes('-f');
|
|
570
670
|
const ti = args.findIndex((a) => a === '--timeout' || a === '-t');
|
|
@@ -574,6 +674,14 @@ async function update() {
|
|
|
574
674
|
if (Number.isFinite(secs) && secs > 0) timeoutMs = secs * 1000;
|
|
575
675
|
else console.log(` ⚠ Ignoring invalid --timeout '${args[ti + 1]}' (expected seconds) — using the default.`);
|
|
576
676
|
}
|
|
677
|
+
return { force, timeoutMs };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// `update` — refresh the background service to the version of THIS CLI. GRACEFUL: it waits for any
|
|
681
|
+
// in-flight job to finish before restarting (override with `--force`, cap the wait with
|
|
682
|
+
// `--timeout <seconds>`). To update the CLI itself: npm i -g @claw-link/gateway-host@latest.
|
|
683
|
+
async function update() {
|
|
684
|
+
const { force, timeoutMs } = parseDrainArgs();
|
|
577
685
|
|
|
578
686
|
console.log(` Refreshing the background service to v${VERSION}…`);
|
|
579
687
|
const live = control.liveStatus();
|
|
@@ -588,10 +696,15 @@ async function update() {
|
|
|
588
696
|
if (live && live.busy && !force) process.stdout.write('\n');
|
|
589
697
|
|
|
590
698
|
if (!r.restarted) { console.log(' ⚠ Could not refresh automatically — run: clhost restart'); return; }
|
|
591
|
-
if (r.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
699
|
+
if (r.confirmed) {
|
|
700
|
+
// The success line now reflects what the new worker ACTUALLY reports (pid + version), not a guess.
|
|
701
|
+
const drained = r.hadWorker && !r.forced && !r.timedOut;
|
|
702
|
+
console.log(` ✓ ${drained ? 'Finished in-flight work, then restarted' : 'Restarted'} — now running v${r.version} (pid ${r.pid}).`);
|
|
703
|
+
if (r.timedOut) console.log(' ⚠ (A job was still running after the wait — it may have been interrupted.)');
|
|
704
|
+
} else {
|
|
705
|
+
console.log(` ⚠ Restart triggered but the new worker hasn't come up on v${VERSION} yet` +
|
|
706
|
+
(r.version ? ` (still seeing v${r.version})` : '') + ` — check \`clhost status\` in a few seconds (\`clhost logs\` if it doesn't appear).`);
|
|
707
|
+
}
|
|
595
708
|
console.log(' (To upgrade the `clhost` CLI you type: npm i -g @claw-link/gateway-host@latest)\n');
|
|
596
709
|
}
|
|
597
710
|
|
package/src/worker.js
CHANGED
|
@@ -27,6 +27,8 @@ const TRANSPORT_MIN_BYTES = Number(process.env.CLAWHOST_TRANSPORT_MIN) || 1024;
|
|
|
27
27
|
// forces immediate exit). The graceful-update path pre-drains to idle, so a restart's signal
|
|
28
28
|
// finds the worker idle and exits at once; this only bounds a direct stop/restart/Ctrl-C.
|
|
29
29
|
const SHUTDOWN_DRAIN_MS = Number(process.env.CLAWHOST_SHUTDOWN_DRAIN_MS) || JOB_MAX_MS;
|
|
30
|
+
// Ceiling for the exponential backoff after consecutive claim() errors (transient Supabase 5xx).
|
|
31
|
+
const CLAIM_BACKOFF_CAP_MS = Number(process.env.CLAWHOST_CLAIM_BACKOFF_CAP_MS) || 30 * 1000;
|
|
30
32
|
|
|
31
33
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32
34
|
let stopping = false;
|
|
@@ -156,6 +158,7 @@ async function runAgentLoop(agentCfg, cfg, p2p) {
|
|
|
156
158
|
|
|
157
159
|
let inFlight = 0;
|
|
158
160
|
const starts = []; // job start timestamps for the rolling rate-limit window
|
|
161
|
+
let claimFails = 0; // consecutive claim() errors → exponential backoff w/ jitter (transient 5xx)
|
|
159
162
|
|
|
160
163
|
while (!stopping) {
|
|
161
164
|
const now = Date.now();
|
|
@@ -164,12 +167,13 @@ async function runAgentLoop(agentCfg, cfg, p2p) {
|
|
|
164
167
|
// Draining (graceful update/shutdown): keep finishing in-flight jobs, but don't claim new ones.
|
|
165
168
|
const draining = stopping || drainFlag;
|
|
166
169
|
const canRun = !draining && inFlight < agentCfg.concurrency && starts.length < agentCfg.rate_limit_per_min;
|
|
170
|
+
let claimErrored = false;
|
|
167
171
|
if (canRun) {
|
|
168
172
|
let job = null;
|
|
169
173
|
claimsInFlight++; // "busy" for the whole claim round-trip — closes the mid-claim drain race
|
|
170
174
|
try {
|
|
171
|
-
try { const r = await bridge.claim(); job = r && r.job; }
|
|
172
|
-
catch (e) { logger.warn(`claim failed (${agentCfg.agent_id}): ${e.message}`); }
|
|
175
|
+
try { const r = await bridge.claim(); job = r && r.job; claimFails = 0; } // success (job or empty) resets backoff
|
|
176
|
+
catch (e) { claimErrored = true; claimFails++; logger.warn(`claim failed (${agentCfg.agent_id}) [x${claimFails}]: ${e.message}`); }
|
|
173
177
|
if (job) {
|
|
174
178
|
inFlight++; starts.push(Date.now()); activeJobIds.add(job.id);
|
|
175
179
|
// Watchdog: guarantees the slot is reclaimed and the job is failed back even
|
|
@@ -191,7 +195,15 @@ async function runAgentLoop(agentCfg, cfg, p2p) {
|
|
|
191
195
|
}
|
|
192
196
|
if (job) continue; // filled a slot — try to fill remaining concurrency immediately
|
|
193
197
|
}
|
|
194
|
-
|
|
198
|
+
// Poll interval — but after repeated claim errors (a Supabase blip), back off exponentially with
|
|
199
|
+
// full jitter (capped) so N agents don't hammer the bridge in lockstep. Resets on the next success.
|
|
200
|
+
let waitMs = cfg.poll_interval_ms || 1500;
|
|
201
|
+
if (claimErrored && claimFails > 1) {
|
|
202
|
+
const base = cfg.poll_interval_ms || 1500;
|
|
203
|
+
const ceil = Math.min(base * 2 ** (claimFails - 1), CLAIM_BACKOFF_CAP_MS);
|
|
204
|
+
waitMs = base + Math.floor(Math.random() * Math.max(0, ceil - base));
|
|
205
|
+
}
|
|
206
|
+
await sleep(waitMs);
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
clearInterval(hb);
|
|
@@ -209,18 +221,14 @@ async function startWorker() {
|
|
|
209
221
|
|
|
210
222
|
logger.info(`ClawLink Host Gateway v${VERSION} — ${agents.length} agent(s): ${agents.map((a) => `${(a.agent_id || 'pending').slice(0, 8)}…(${a.runtime || 'resolving'})`).join(', ')}`);
|
|
211
223
|
|
|
212
|
-
// H2H data plane: bring up the native QUIC transport once per host (shared by all agents). If no
|
|
213
|
-
// prebuilt native core exists for this platform, every transfer routes through the central relay.
|
|
214
224
|
const p2p = new P2P(logger);
|
|
215
|
-
const p2pAddr = await p2p.start();
|
|
216
|
-
logger.info(p2pAddr
|
|
217
|
-
? `H2H transport: native P2P active (${p2pAddr})`
|
|
218
|
-
: 'H2H transport: central relay (no native core for this platform — install acceleration later)');
|
|
219
225
|
|
|
220
226
|
// Advertise liveness + in-flight count so `clhost update`/`restart` can wait for this host to go
|
|
221
227
|
// idle before restarting, and pick up the CLI's drain flag (stop claiming, finish current work).
|
|
222
|
-
//
|
|
223
|
-
//
|
|
228
|
+
// Do this BEFORE the (possibly slow) P2P init, so a restart is CONFIRMED fast — waitForWorkerUp sees
|
|
229
|
+
// the new pid/version within ~1s even while iroh is still binding. Clear any stale flag first; and
|
|
230
|
+
// only ever honor a flag written AFTER this boot, so a leftover flag from a crashed update (or a
|
|
231
|
+
// clearDrain that failed) can never wedge a fresh worker. (pushStatus never touches p2p.)
|
|
224
232
|
control.clearDrain(); // drop any stale flag left by a previous life FIRST…
|
|
225
233
|
const bootAt = Date.now(); // …then anchor "now": only a drain requested after this point is honored.
|
|
226
234
|
const pushStatus = () => {
|
|
@@ -236,6 +244,13 @@ async function startWorker() {
|
|
|
236
244
|
const statusTimer = setInterval(pushStatus, 2000);
|
|
237
245
|
statusTimer.unref();
|
|
238
246
|
|
|
247
|
+
// H2H data plane: bring up the native QUIC transport once per host (shared by all agents). If no
|
|
248
|
+
// prebuilt native core exists for this platform, every transfer routes through the central relay.
|
|
249
|
+
const p2pAddr = await p2p.start();
|
|
250
|
+
logger.info(p2pAddr
|
|
251
|
+
? `H2H transport: native P2P active (${p2pAddr})`
|
|
252
|
+
: 'H2H transport: central relay (no native core for this platform — install acceleration later)');
|
|
253
|
+
|
|
239
254
|
// Graceful shutdown: stop claiming, let in-flight jobs finish (keeping P2P alive for them), then
|
|
240
255
|
// exit. A second signal forces an immediate exit; SHUTDOWN_DRAIN_MS bounds the wait.
|
|
241
256
|
let stopSignals = 0;
|