@claw-link/gateway-host 0.3.4 → 0.3.5
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/src/adapters/base.js +12 -1
- package/src/adapters/cli.js +2 -2
- package/src/bridge.js +2 -0
- package/src/worker.js +18 -1
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.5",
|
|
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/src/adapters/base.js
CHANGED
|
@@ -45,7 +45,7 @@ function buildArgv(template, vars) {
|
|
|
45
45
|
* This returns an async iterator of events. The prompt is passed via stdin when
|
|
46
46
|
* `stdin` is true (preferred — keeps it out of argv/process listings entirely).
|
|
47
47
|
*/
|
|
48
|
-
async function* spawnStreaming({ binary, argv, cwd, env, timeoutMs = 600000, stdinInput = null, parseLine }) {
|
|
48
|
+
async function* spawnStreaming({ binary, argv, cwd, env, timeoutMs = 600000, stdinInput = null, parseLine, signal = null }) {
|
|
49
49
|
if (!binary) throw new Error('no runtime binary configured');
|
|
50
50
|
|
|
51
51
|
// Windows: npm CLIs are `.cmd`/`.bat` shims, and modern Node REFUSES to spawn those
|
|
@@ -73,6 +73,17 @@ async function* spawnStreaming({ binary, argv, cwd, env, timeoutMs = 600000, std
|
|
|
73
73
|
? setTimeout(() => { killed = true; try { child.kill('SIGKILL'); } catch { /* gone */ } }, timeoutMs)
|
|
74
74
|
: null;
|
|
75
75
|
|
|
76
|
+
// Abort (the run was cancelled): stop the agent promptly — SIGTERM, then SIGKILL if it lingers.
|
|
77
|
+
const onAbort = () => {
|
|
78
|
+
killed = true;
|
|
79
|
+
try { child.kill('SIGTERM'); } catch { /* gone */ }
|
|
80
|
+
setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* gone */ } }, 3000).unref();
|
|
81
|
+
};
|
|
82
|
+
if (signal) {
|
|
83
|
+
if (signal.aborted) onAbort();
|
|
84
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
if (stdinInput != null) {
|
|
77
88
|
try { child.stdin.write(stdinInput); } catch { /* ignore */ }
|
|
78
89
|
}
|
package/src/adapters/cli.js
CHANGED
|
@@ -90,7 +90,7 @@ function makeCliAdapter(cfg) {
|
|
|
90
90
|
name: cfg.name,
|
|
91
91
|
binaries: cfg.binaries,
|
|
92
92
|
|
|
93
|
-
async *run({ agent, job, logger }) {
|
|
93
|
+
async *run({ agent, job, logger, signal }) {
|
|
94
94
|
const binary = agent.binary || cfg.binaries[0];
|
|
95
95
|
const userPrompt = job.content || '';
|
|
96
96
|
const systemPrompt = job.system_prompt || '';
|
|
@@ -135,7 +135,7 @@ function makeCliAdapter(cfg) {
|
|
|
135
135
|
: genericLineParser;
|
|
136
136
|
for await (const evt of spawnStreaming({
|
|
137
137
|
binary, argv: argvFor(rid), cwd: agent.work_dir, timeoutMs,
|
|
138
|
-
stdinInput: cfg.promptViaStdin ? fullPrompt : null, parseLine,
|
|
138
|
+
stdinInput: cfg.promptViaStdin ? fullPrompt : null, parseLine, signal,
|
|
139
139
|
})) {
|
|
140
140
|
if (evt.type === 'chunk') emittedAny = true;
|
|
141
141
|
yield evt;
|
package/src/bridge.js
CHANGED
|
@@ -41,6 +41,8 @@ class Bridge {
|
|
|
41
41
|
stream(jobId, seq, type, content) { return this._post('stream', { job_id: jobId, seq, type, content }); }
|
|
42
42
|
complete(jobId, finalContent, metadata) { return this._post('complete', { job_id: jobId, final_content: finalContent, metadata: metadata || {} }); }
|
|
43
43
|
fail(jobId, error) { return this._post('fail', { job_id: jobId, error: String(error).slice(0, 1000) }); }
|
|
44
|
+
// Poll a running job's status so the worker can abort the agent if the run was cancelled.
|
|
45
|
+
jobStatus(jobId) { return this._post('job_status', { job_id: jobId }); }
|
|
44
46
|
// AHP data plane: request short-lived signed URLs to sync content-addressed Capsule bytes.
|
|
45
47
|
capsulePut(path) { return this._post('capsule_put', { path }); }
|
|
46
48
|
capsuleGet(path) { return this._post('capsule_get', { path }); }
|
package/src/worker.js
CHANGED
|
@@ -56,11 +56,24 @@ async function processJob(bridge, agentCfg, job, p2p) {
|
|
|
56
56
|
// H2H: pull in any large upstream bytes (P2P, DB floor) before the runtime sees the prompt.
|
|
57
57
|
await resolveTransport(bridge, p2p, job);
|
|
58
58
|
const adapter = getAdapter(agentCfg.runtime);
|
|
59
|
+
// Cancellation: poll the job's status while it runs; if the run was cancelled (or the job errored/
|
|
60
|
+
// expired server-side), abort the child so the agent STOPS promptly instead of grinding on.
|
|
61
|
+
const ac = new AbortController();
|
|
62
|
+
const cancelPoll = setInterval(() => {
|
|
63
|
+
bridge.jobStatus(job.id)
|
|
64
|
+
.then((r) => {
|
|
65
|
+
if (r && ['cancelled', 'error', 'expired'].includes(String(r.status)) && !ac.signal.aborted) {
|
|
66
|
+
logger.info(`job ${job.id} ${r.status} — stopping the agent`);
|
|
67
|
+
ac.abort();
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.catch(() => { /* transient — keep polling */ });
|
|
71
|
+
}, 4000);
|
|
59
72
|
let seq = 0;
|
|
60
73
|
let finalText = '';
|
|
61
74
|
let toolCalls = [];
|
|
62
75
|
try {
|
|
63
|
-
for await (const evt of adapter.run({ agent: agentCfg, job, logger })) {
|
|
76
|
+
for await (const evt of adapter.run({ agent: agentCfg, job, logger, signal: ac.signal })) {
|
|
64
77
|
if (!evt || !evt.type) continue;
|
|
65
78
|
// Structured tool calls aren't streamed as a chunk — collected for the final
|
|
66
79
|
// metadata so the server can extract rich UI (clawlink_ui).
|
|
@@ -69,6 +82,7 @@ async function processJob(bridge, agentCfg, job, p2p) {
|
|
|
69
82
|
try { await bridge.stream(job.id, seq++, evt.type, String(evt.content ?? '')); }
|
|
70
83
|
catch (e) { logger.warn(`stream seq ${seq} failed: ${e.message}`); }
|
|
71
84
|
}
|
|
85
|
+
if (ac.signal.aborted) { logger.info(`job ${job.id} cancelled — not completing`); return; }
|
|
72
86
|
// H2H: store a large result for P2P fetch by the next node's host (inline copy stays the floor).
|
|
73
87
|
// We add+ref it before complete(), but ANNOUNCE only AFTER complete() persists the capsule — so
|
|
74
88
|
// the directory can verify this host is the producer before listing it.
|
|
@@ -88,8 +102,11 @@ async function processJob(bridge, agentCfg, job, p2p) {
|
|
|
88
102
|
if (offerCid) await p2p.announce(bridge, offerCid);
|
|
89
103
|
logger.info(`job ${job.id} done (${finalText.length} chars${transport ? `, P2P-offered ${transport.cid.slice(0, 14)}…` : ''})`);
|
|
90
104
|
} catch (e) {
|
|
105
|
+
if (ac.signal.aborted) { logger.info(`job ${job.id} cancelled — agent stopped`); return; }
|
|
91
106
|
logger.error(`job ${job.id} failed: ${e.message}`);
|
|
92
107
|
try { await bridge.fail(job.id, e.message); } catch { /* ignore */ }
|
|
108
|
+
} finally {
|
|
109
|
+
clearInterval(cancelPoll);
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
112
|
|