agent-relay 3.1.5 → 3.1.7
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/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +130 -107
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/dist/gateway.d.ts +16 -1
- package/packages/openclaw/dist/gateway.d.ts.map +1 -1
- package/packages/openclaw/dist/gateway.js +101 -13
- package/packages/openclaw/dist/gateway.js.map +1 -1
- package/packages/openclaw/package.json +2 -2
- package/packages/openclaw/skill/SKILL.md +167 -12
- package/packages/openclaw/src/gateway.ts +114 -15
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/client.js +6 -8
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/workflows/builder.d.ts +3 -1
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +1 -0
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +15 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +146 -117
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/scripts/bundle-agent-relay.mjs +11 -1
- package/packages/sdk/src/client.ts +6 -8
- package/packages/sdk/src/workflows/builder.ts +4 -1
- package/packages/sdk/src/workflows/runner.ts +173 -119
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.cjs
CHANGED
|
@@ -9455,7 +9455,8 @@ function isExplicitPath(binaryPath) {
|
|
|
9455
9455
|
function detectPlatformSuffix() {
|
|
9456
9456
|
const platformMap = {
|
|
9457
9457
|
darwin: { arm64: "darwin-arm64", x64: "darwin-x64" },
|
|
9458
|
-
linux: { arm64: "linux-arm64", x64: "linux-x64" }
|
|
9458
|
+
linux: { arm64: "linux-arm64", x64: "linux-x64" },
|
|
9459
|
+
win32: { x64: "win32-x64" }
|
|
9459
9460
|
};
|
|
9460
9461
|
return platformMap[process.platform]?.[process.arch] ?? null;
|
|
9461
9462
|
}
|
|
@@ -9527,15 +9528,12 @@ function resolveDefaultBinaryPath() {
|
|
|
9527
9528
|
const binDir = import_node_path.default.resolve(moduleDir, "..", "bin");
|
|
9528
9529
|
const suffix = detectPlatformSuffix();
|
|
9529
9530
|
if (suffix) {
|
|
9530
|
-
const
|
|
9531
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
9532
|
+
const platformBinary = import_node_path.default.join(binDir, `agent-relay-broker-${suffix}${ext}`);
|
|
9531
9533
|
if (import_node_fs.default.existsSync(platformBinary)) {
|
|
9532
9534
|
return platformBinary;
|
|
9533
9535
|
}
|
|
9534
9536
|
}
|
|
9535
|
-
const bundled = import_node_path.default.join(binDir, brokerExe);
|
|
9536
|
-
if (import_node_fs.default.existsSync(bundled)) {
|
|
9537
|
-
return bundled;
|
|
9538
|
-
}
|
|
9539
9537
|
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
9540
9538
|
const standaloneBroker = import_node_path.default.join(homeDir, ".agent-relay", "bin", brokerExe);
|
|
9541
9539
|
if (import_node_fs.default.existsSync(standaloneBroker)) {
|
|
@@ -47575,6 +47573,7 @@ var WorkflowRunner = class _WorkflowRunner {
|
|
|
47575
47573
|
relayOptions;
|
|
47576
47574
|
cwd;
|
|
47577
47575
|
summaryDir;
|
|
47576
|
+
executor;
|
|
47578
47577
|
/** @internal exposed for CLI signal-handler shutdown only */
|
|
47579
47578
|
relay;
|
|
47580
47579
|
relaycast;
|
|
@@ -47618,6 +47617,7 @@ var WorkflowRunner = class _WorkflowRunner {
|
|
|
47618
47617
|
this.cwd = options.cwd ?? process.cwd();
|
|
47619
47618
|
this.summaryDir = options.summaryDir ?? import_node_path8.default.join(this.cwd, ".relay", "summaries");
|
|
47620
47619
|
this.workersPath = import_node_path8.default.join(this.cwd, ".agent-relay", "team", "workers.json");
|
|
47620
|
+
this.executor = options.executor;
|
|
47621
47621
|
}
|
|
47622
47622
|
// ── Progress logging ────────────────────────────────────────────────────
|
|
47623
47623
|
/** Log a progress message with elapsed time since run start. */
|
|
@@ -48351,109 +48351,111 @@ ${err.suggestion}`);
|
|
|
48351
48351
|
config3.swarm.channel = channel;
|
|
48352
48352
|
await this.db.updateRun(runId, { config: config3 });
|
|
48353
48353
|
}
|
|
48354
|
-
this.
|
|
48355
|
-
|
|
48356
|
-
|
|
48357
|
-
|
|
48358
|
-
this.
|
|
48359
|
-
|
|
48360
|
-
|
|
48361
|
-
|
|
48362
|
-
|
|
48363
|
-
|
|
48364
|
-
|
|
48365
|
-
|
|
48366
|
-
|
|
48367
|
-
|
|
48368
|
-
|
|
48369
|
-
|
|
48370
|
-
|
|
48371
|
-
|
|
48372
|
-
|
|
48373
|
-
|
|
48374
|
-
|
|
48375
|
-
|
|
48376
|
-
|
|
48377
|
-
|
|
48378
|
-
listener
|
|
48379
|
-
|
|
48380
|
-
|
|
48381
|
-
|
|
48382
|
-
|
|
48383
|
-
|
|
48384
|
-
|
|
48385
|
-
|
|
48386
|
-
|
|
48387
|
-
|
|
48388
|
-
|
|
48354
|
+
if (!this.executor) {
|
|
48355
|
+
this.log("Resolving Relaycast API key...");
|
|
48356
|
+
await this.ensureRelaycastApiKey(channel);
|
|
48357
|
+
this.log("API key resolved");
|
|
48358
|
+
if (this.relayApiKeyAutoCreated && this.relayApiKey) {
|
|
48359
|
+
this.log(`Workspace created \u2014 follow this run in Relaycast:`);
|
|
48360
|
+
this.log(` Observer: https://observer.relaycast.dev/?key=${this.relayApiKey}`);
|
|
48361
|
+
this.log(` Channel: ${channel}`);
|
|
48362
|
+
}
|
|
48363
|
+
this.log("Starting broker...");
|
|
48364
|
+
const brokerBaseName = import_node_path8.default.basename(this.cwd) || "workflow";
|
|
48365
|
+
const brokerName = `${brokerBaseName}-${runId.slice(0, 8)}`;
|
|
48366
|
+
this.relay = new AgentRelay({
|
|
48367
|
+
...this.relayOptions,
|
|
48368
|
+
brokerName,
|
|
48369
|
+
channels: [channel],
|
|
48370
|
+
env: this.getRelayEnv(),
|
|
48371
|
+
// Workflows spawn agents across multiple waves; each spawn requires a PTY +
|
|
48372
|
+
// Relaycast registration. 60s is too tight when the broker is saturated with
|
|
48373
|
+
// long-running PTY processes from earlier steps. 120s gives room to breathe.
|
|
48374
|
+
requestTimeoutMs: this.relayOptions.requestTimeoutMs ?? 12e4
|
|
48375
|
+
});
|
|
48376
|
+
this.relay.onWorkerOutput = ({ name, chunk }) => {
|
|
48377
|
+
const listener = this.ptyListeners.get(name);
|
|
48378
|
+
if (listener)
|
|
48379
|
+
listener(chunk);
|
|
48380
|
+
const stripped = _WorkflowRunner.stripAnsi(chunk);
|
|
48381
|
+
const shortName = name.replace(/-[a-f0-9]{6,}$/, "");
|
|
48382
|
+
let activity;
|
|
48383
|
+
if (/Read\(/.test(stripped)) {
|
|
48384
|
+
const m = stripped.match(/Read\(\s*~?([^\s)"']{8,})/);
|
|
48385
|
+
if (m) {
|
|
48386
|
+
const base = import_node_path8.default.basename(m[1]);
|
|
48387
|
+
activity = base.length >= 3 ? `Reading ${base}` : "Reading file...";
|
|
48388
|
+
} else {
|
|
48389
|
+
activity = "Reading file...";
|
|
48390
|
+
}
|
|
48391
|
+
} else if (/Edit\(/.test(stripped)) {
|
|
48392
|
+
const m = stripped.match(/Edit\(\s*~?([^\s)"']{8,})/);
|
|
48393
|
+
if (m) {
|
|
48394
|
+
const base = import_node_path8.default.basename(m[1]);
|
|
48395
|
+
activity = base.length >= 3 ? `Editing ${base}` : "Editing file...";
|
|
48396
|
+
} else {
|
|
48397
|
+
activity = "Editing file...";
|
|
48398
|
+
}
|
|
48399
|
+
} else if (/Bash\(/.test(stripped)) {
|
|
48400
|
+
const m = stripped.match(/Bash\(\s*(.{1,40})/);
|
|
48401
|
+
activity = m ? `Running: ${m[1].trim()}...` : "Running command...";
|
|
48402
|
+
} else if (/Explore\(/.test(stripped)) {
|
|
48403
|
+
const m = stripped.match(/Explore\(\s*(.{1,50})/);
|
|
48404
|
+
activity = m ? `Exploring: ${m[1].replace(/\).*/, "").trim()}` : "Exploring codebase...";
|
|
48405
|
+
} else if (/Task\(/.test(stripped)) {
|
|
48406
|
+
activity = "Running sub-agent...";
|
|
48407
|
+
} else if (/Sublimating|Thinking|Coalescing|Cultivating/.test(stripped)) {
|
|
48408
|
+
const m = stripped.match(/(\d+)s/);
|
|
48409
|
+
activity = m ? `Thinking... (${m[1]}s)` : "Thinking...";
|
|
48389
48410
|
}
|
|
48390
|
-
|
|
48391
|
-
|
|
48392
|
-
|
|
48393
|
-
const base = import_node_path8.default.basename(m[1]);
|
|
48394
|
-
activity = base.length >= 3 ? `Editing ${base}` : "Editing file...";
|
|
48395
|
-
} else {
|
|
48396
|
-
activity = "Editing file...";
|
|
48411
|
+
if (activity && this.lastActivity.get(name) !== activity) {
|
|
48412
|
+
this.lastActivity.set(name, activity);
|
|
48413
|
+
this.log(`[${shortName}] ${activity}`);
|
|
48397
48414
|
}
|
|
48398
|
-
}
|
|
48399
|
-
|
|
48400
|
-
|
|
48401
|
-
|
|
48402
|
-
const
|
|
48403
|
-
|
|
48404
|
-
}
|
|
48405
|
-
|
|
48406
|
-
|
|
48407
|
-
|
|
48408
|
-
|
|
48409
|
-
}
|
|
48410
|
-
|
|
48411
|
-
this.lastActivity.
|
|
48412
|
-
this.
|
|
48413
|
-
|
|
48414
|
-
|
|
48415
|
-
|
|
48416
|
-
|
|
48417
|
-
|
|
48418
|
-
|
|
48419
|
-
|
|
48420
|
-
|
|
48421
|
-
|
|
48422
|
-
|
|
48423
|
-
|
|
48424
|
-
}
|
|
48425
|
-
|
|
48426
|
-
|
|
48427
|
-
this.
|
|
48428
|
-
|
|
48429
|
-
|
|
48430
|
-
|
|
48415
|
+
};
|
|
48416
|
+
this.relay.onMessageReceived = (msg) => {
|
|
48417
|
+
const body = msg.text.length > 120 ? msg.text.slice(0, 117) + "..." : msg.text;
|
|
48418
|
+
const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
|
|
48419
|
+
const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
|
|
48420
|
+
this.log(`[msg] ${fromShort} \u2192 ${toShort}: ${body}`);
|
|
48421
|
+
};
|
|
48422
|
+
this.relay.onAgentSpawned = (agent) => {
|
|
48423
|
+
if (!this.activeAgentHandles.has(agent.name)) {
|
|
48424
|
+
this.log(`[spawned] ${agent.name} (${agent.runtime})`);
|
|
48425
|
+
}
|
|
48426
|
+
};
|
|
48427
|
+
this.relay.onAgentExited = (agent) => {
|
|
48428
|
+
this.lastActivity.delete(agent.name);
|
|
48429
|
+
this.lastIdleLog.delete(agent.name);
|
|
48430
|
+
if (!this.activeAgentHandles.has(agent.name)) {
|
|
48431
|
+
this.log(`[exited] ${agent.name} (code: ${agent.exitCode ?? "?"})`);
|
|
48432
|
+
}
|
|
48433
|
+
};
|
|
48434
|
+
this.relay.onAgentIdle = ({ name, idleSecs }) => {
|
|
48435
|
+
const bucket = Math.floor(idleSecs / 30) * 30;
|
|
48436
|
+
if (bucket >= 30 && this.lastIdleLog.get(name) !== bucket) {
|
|
48437
|
+
this.lastIdleLog.set(name, bucket);
|
|
48438
|
+
const shortName = name.replace(/-[a-f0-9]{6,}$/, "");
|
|
48439
|
+
this.log(`[idle] ${shortName} silent for ${bucket}s`);
|
|
48440
|
+
}
|
|
48441
|
+
};
|
|
48442
|
+
this.relaycast = void 0;
|
|
48443
|
+
this.relaycastAgent = void 0;
|
|
48444
|
+
this.unsubBrokerStderr = this.relay.onBrokerStderr((line) => {
|
|
48445
|
+
console.log(`[broker] ${line}`);
|
|
48446
|
+
});
|
|
48447
|
+
this.log(`Creating channel: ${channel}...`);
|
|
48448
|
+
if (isResume) {
|
|
48449
|
+
await this.createAndJoinRelaycastChannel(channel);
|
|
48450
|
+
} else {
|
|
48451
|
+
await this.createAndJoinRelaycastChannel(channel, workflow2.description);
|
|
48431
48452
|
}
|
|
48432
|
-
|
|
48433
|
-
|
|
48434
|
-
|
|
48435
|
-
|
|
48436
|
-
this.
|
|
48437
|
-
const shortName = name.replace(/-[a-f0-9]{6,}$/, "");
|
|
48438
|
-
this.log(`[idle] ${shortName} silent for ${bucket}s`);
|
|
48453
|
+
this.log("Channel ready");
|
|
48454
|
+
if (isResume) {
|
|
48455
|
+
this.postToChannel(`Workflow **${workflow2.name}** resumed \u2014 ${pendingCount} pending steps`);
|
|
48456
|
+
} else {
|
|
48457
|
+
this.postToChannel(`Workflow **${workflow2.name}** started \u2014 ${workflow2.steps.length} steps, pattern: ${config3.swarm.pattern}`);
|
|
48439
48458
|
}
|
|
48440
|
-
};
|
|
48441
|
-
this.relaycast = void 0;
|
|
48442
|
-
this.relaycastAgent = void 0;
|
|
48443
|
-
this.unsubBrokerStderr = this.relay.onBrokerStderr((line) => {
|
|
48444
|
-
console.log(`[broker] ${line}`);
|
|
48445
|
-
});
|
|
48446
|
-
this.log(`Creating channel: ${channel}...`);
|
|
48447
|
-
if (isResume) {
|
|
48448
|
-
await this.createAndJoinRelaycastChannel(channel);
|
|
48449
|
-
} else {
|
|
48450
|
-
await this.createAndJoinRelaycastChannel(channel, workflow2.description);
|
|
48451
|
-
}
|
|
48452
|
-
this.log("Channel ready");
|
|
48453
|
-
if (isResume) {
|
|
48454
|
-
this.postToChannel(`Workflow **${workflow2.name}** resumed \u2014 ${pendingCount} pending steps`);
|
|
48455
|
-
} else {
|
|
48456
|
-
this.postToChannel(`Workflow **${workflow2.name}** started \u2014 ${workflow2.steps.length} steps, pattern: ${config3.swarm.pattern}`);
|
|
48457
48459
|
}
|
|
48458
48460
|
const agentMap = /* @__PURE__ */ new Map();
|
|
48459
48461
|
for (const agent of config3.agents) {
|
|
@@ -48768,6 +48770,26 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
48768
48770
|
return value !== void 0 ? String(value) : _match;
|
|
48769
48771
|
});
|
|
48770
48772
|
try {
|
|
48773
|
+
if (this.executor?.executeDeterministicStep) {
|
|
48774
|
+
const result = await this.executor.executeDeterministicStep(step, resolvedCommand, this.cwd);
|
|
48775
|
+
const failOnError = step.failOnError !== false;
|
|
48776
|
+
if (failOnError && result.exitCode !== 0) {
|
|
48777
|
+
throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
|
|
48778
|
+
}
|
|
48779
|
+
const output2 = step.captureOutput !== false ? result.output : `Command completed (exit code ${result.exitCode})`;
|
|
48780
|
+
state.row.status = "completed";
|
|
48781
|
+
state.row.output = output2;
|
|
48782
|
+
state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
48783
|
+
await this.db.updateStep(state.row.id, {
|
|
48784
|
+
status: "completed",
|
|
48785
|
+
output: output2,
|
|
48786
|
+
completedAt: state.row.completedAt,
|
|
48787
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
48788
|
+
});
|
|
48789
|
+
await this.persistStepOutput(runId, step.name, output2);
|
|
48790
|
+
this.emit({ type: "step:completed", runId, stepName: step.name, output: output2 });
|
|
48791
|
+
return;
|
|
48792
|
+
}
|
|
48771
48793
|
const output = await new Promise((resolve3, reject) => {
|
|
48772
48794
|
const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
|
|
48773
48795
|
stdio: "pipe",
|
|
@@ -49041,7 +49063,7 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
49041
49063
|
}
|
|
49042
49064
|
this.log(`[${step.name}] Spawning agent "${agentDef.name}" (cli: ${agentDef.cli})`);
|
|
49043
49065
|
const resolvedStep = { ...step, task: resolvedTask };
|
|
49044
|
-
const output = await this.spawnAndWait(agentDef, resolvedStep, timeoutMs);
|
|
49066
|
+
const output = this.executor ? await this.executor.executeAgentStep(resolvedStep, agentDef, resolvedTask, timeoutMs) : await this.spawnAndWait(agentDef, resolvedStep, timeoutMs);
|
|
49045
49067
|
this.log(`[${step.name}] Agent "${agentDef.name}" exited`);
|
|
49046
49068
|
if (step.verification) {
|
|
49047
49069
|
this.runVerification(step.verification, output, step.name, resolvedTask);
|
|
@@ -50334,7 +50356,8 @@ var WorkflowBuilder = class {
|
|
|
50334
50356
|
const config3 = this.toConfig();
|
|
50335
50357
|
const runner = new WorkflowRunner({
|
|
50336
50358
|
cwd: options.cwd,
|
|
50337
|
-
relay: options.relay
|
|
50359
|
+
relay: options.relay,
|
|
50360
|
+
executor: options.executor
|
|
50338
50361
|
});
|
|
50339
50362
|
const isDryRun = options.dryRun ?? !!process.env.DRY_RUN;
|
|
50340
50363
|
if (isDryRun) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
4
4
|
"description": "Real-time agent-to-agent communication system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -174,13 +174,13 @@
|
|
|
174
174
|
},
|
|
175
175
|
"homepage": "https://github.com/AgentWorkforce/relay#readme",
|
|
176
176
|
"dependencies": {
|
|
177
|
-
"@agent-relay/config": "3.1.
|
|
178
|
-
"@agent-relay/hooks": "3.1.
|
|
179
|
-
"@agent-relay/sdk": "3.1.
|
|
180
|
-
"@agent-relay/telemetry": "3.1.
|
|
181
|
-
"@agent-relay/trajectory": "3.1.
|
|
182
|
-
"@agent-relay/user-directory": "3.1.
|
|
183
|
-
"@agent-relay/utils": "3.1.
|
|
177
|
+
"@agent-relay/config": "3.1.7",
|
|
178
|
+
"@agent-relay/hooks": "3.1.7",
|
|
179
|
+
"@agent-relay/sdk": "3.1.7",
|
|
180
|
+
"@agent-relay/telemetry": "3.1.7",
|
|
181
|
+
"@agent-relay/trajectory": "3.1.7",
|
|
182
|
+
"@agent-relay/user-directory": "3.1.7",
|
|
183
|
+
"@agent-relay/utils": "3.1.7",
|
|
184
184
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
185
185
|
"@relaycast/sdk": "^0.4.0",
|
|
186
186
|
"chokidar": "^5.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/acp-bridge",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
4
4
|
"description": "ACP (Agent Client Protocol) bridge for Agent Relay - expose relay agents to ACP-compatible editors like Zed",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@agent-relay/sdk": "3.1.
|
|
49
|
+
"@agent-relay/sdk": "3.1.7",
|
|
50
50
|
"@agentclientprotocol/sdk": "^0.12.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/hooks",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
4
4
|
"description": "Hook emitter, registry, and trajectory hooks for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"test:watch": "vitest"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@agent-relay/config": "3.1.
|
|
41
|
-
"@agent-relay/trajectory": "3.1.
|
|
42
|
-
"@agent-relay/sdk": "3.1.
|
|
40
|
+
"@agent-relay/config": "3.1.7",
|
|
41
|
+
"@agent-relay/trajectory": "3.1.7",
|
|
42
|
+
"@agent-relay/sdk": "3.1.7"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/memory",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
4
4
|
"description": "Semantic memory storage and retrieval system for agent-relay with multiple backend support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/hooks": "3.1.
|
|
25
|
+
"@agent-relay/hooks": "3.1.7"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
1
2
|
import type { SendMessageInput } from '@agent-relay/sdk';
|
|
2
3
|
import { type GatewayConfig } from './types.js';
|
|
3
4
|
/**
|
|
@@ -21,6 +22,11 @@ export interface GatewayOptions {
|
|
|
21
22
|
*/
|
|
22
23
|
relaySender?: RelaySender;
|
|
23
24
|
}
|
|
25
|
+
interface DeviceIdentity {
|
|
26
|
+
publicKeyB64: string;
|
|
27
|
+
privateKeyObj: KeyObject;
|
|
28
|
+
deviceId: string;
|
|
29
|
+
}
|
|
24
30
|
/** @internal */
|
|
25
31
|
export declare class OpenClawGatewayClient {
|
|
26
32
|
private ws;
|
|
@@ -43,7 +49,15 @@ export declare class OpenClawGatewayClient {
|
|
|
43
49
|
private static readonly MAX_CONSECUTIVE_FAILURES;
|
|
44
50
|
private static readonly BASE_RECONNECT_MS;
|
|
45
51
|
private static readonly MAX_RECONNECT_MS;
|
|
46
|
-
|
|
52
|
+
/** Slow retry interval after pairing rejection or max failures (60s). */
|
|
53
|
+
private static readonly PAIRING_RETRY_MS;
|
|
54
|
+
constructor(token: string, port: number, device?: DeviceIdentity);
|
|
55
|
+
/**
|
|
56
|
+
* Create a client with a persisted device identity (loaded from disk or
|
|
57
|
+
* freshly generated and saved). This ensures the same device ID is reused
|
|
58
|
+
* across restarts so the OpenClaw gateway can pair it once.
|
|
59
|
+
*/
|
|
60
|
+
static create(token: string, port: number): Promise<OpenClawGatewayClient>;
|
|
47
61
|
/** Connect and authenticate. Resolves when chat.send is ready, rejects on timeout or error. */
|
|
48
62
|
connect(): Promise<void>;
|
|
49
63
|
private clearConnectTimeout;
|
|
@@ -99,4 +113,5 @@ export declare class InboundGateway {
|
|
|
99
113
|
private startControlServer;
|
|
100
114
|
private handleControlRequest;
|
|
101
115
|
}
|
|
116
|
+
export {};
|
|
102
117
|
//# sourceMappingURL=gateway.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
|
1
|
+
import { createHash, createPrivateKey, generateKeyPairSync, sign } from 'node:crypto';
|
|
2
|
+
import { chmod, readFile, rename, writeFile, mkdir } from 'node:fs/promises';
|
|
2
3
|
import { createServer } from 'node:http';
|
|
4
|
+
import { join } from 'node:path';
|
|
3
5
|
import { RelayCast } from '@relaycast/sdk';
|
|
4
6
|
import WebSocket from 'ws';
|
|
7
|
+
import { openclawHome } from './config.js';
|
|
5
8
|
import { DEFAULT_OPENCLAW_GATEWAY_PORT } from './types.js';
|
|
6
9
|
import { SpawnManager } from './spawn/manager.js';
|
|
7
10
|
function normalizeChannelName(channel) {
|
|
@@ -19,6 +22,62 @@ function generateDeviceIdentity() {
|
|
|
19
22
|
deviceId,
|
|
20
23
|
};
|
|
21
24
|
}
|
|
25
|
+
/** Path to persisted device identity file. */
|
|
26
|
+
function deviceIdentityPath() {
|
|
27
|
+
return join(openclawHome(), 'workspace', 'relaycast', 'device.json');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load a persisted device identity from disk, or generate and persist a new one.
|
|
31
|
+
* This ensures the same device ID survives restarts so the OpenClaw gateway
|
|
32
|
+
* can pair it once and recognize it on subsequent connections.
|
|
33
|
+
*/
|
|
34
|
+
async function loadOrCreateDeviceIdentity() {
|
|
35
|
+
const filePath = deviceIdentityPath();
|
|
36
|
+
// Attempt to load existing identity (no existsSync — just try the read)
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
39
|
+
const persisted = JSON.parse(raw);
|
|
40
|
+
const privateKeyObj = createPrivateKey({
|
|
41
|
+
key: Buffer.from(persisted.privateKeyPkcs8B64, 'base64'),
|
|
42
|
+
format: 'der',
|
|
43
|
+
type: 'pkcs8',
|
|
44
|
+
});
|
|
45
|
+
// Ensure permissions are tight even if file was created with looser perms
|
|
46
|
+
await chmod(filePath, 0o600).catch(() => { });
|
|
47
|
+
console.log(`[openclaw-ws] Loaded persisted device identity (deviceId=${persisted.deviceId.slice(0, 12)}...)`);
|
|
48
|
+
return {
|
|
49
|
+
publicKeyB64: persisted.publicKeyB64,
|
|
50
|
+
privateKeyObj,
|
|
51
|
+
deviceId: persisted.deviceId,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// ENOENT is expected on first run; other errors mean corruption
|
|
56
|
+
if (err.code !== 'ENOENT') {
|
|
57
|
+
console.warn(`[openclaw-ws] Failed to load device identity, generating new: ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Generate fresh and persist via atomic write-then-rename
|
|
61
|
+
const identity = generateDeviceIdentity();
|
|
62
|
+
const pkcs8Der = identity.privateKeyObj.export({ type: 'pkcs8', format: 'der' });
|
|
63
|
+
const persisted = {
|
|
64
|
+
publicKeyB64: identity.publicKeyB64,
|
|
65
|
+
privateKeyPkcs8B64: Buffer.from(pkcs8Der).toString('base64'),
|
|
66
|
+
deviceId: identity.deviceId,
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const dir = join(openclawHome(), 'workspace', 'relaycast');
|
|
70
|
+
await mkdir(dir, { recursive: true });
|
|
71
|
+
const tmpPath = filePath + '.tmp';
|
|
72
|
+
await writeFile(tmpPath, JSON.stringify(persisted, null, 2) + '\n', { mode: 0o600 });
|
|
73
|
+
await rename(tmpPath, filePath);
|
|
74
|
+
console.log(`[openclaw-ws] Persisted new device identity (deviceId=${identity.deviceId.slice(0, 12)}...)`);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.warn(`[openclaw-ws] Could not persist device identity: ${err instanceof Error ? err.message : String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
return identity;
|
|
80
|
+
}
|
|
22
81
|
function signConnectPayload(device, params) {
|
|
23
82
|
// v3 payload format: v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily
|
|
24
83
|
const payload = [
|
|
@@ -61,10 +120,21 @@ export class OpenClawGatewayClient {
|
|
|
61
120
|
static MAX_CONSECUTIVE_FAILURES = 5;
|
|
62
121
|
static BASE_RECONNECT_MS = 3_000;
|
|
63
122
|
static MAX_RECONNECT_MS = 30_000;
|
|
64
|
-
|
|
123
|
+
/** Slow retry interval after pairing rejection or max failures (60s). */
|
|
124
|
+
static PAIRING_RETRY_MS = 60_000;
|
|
125
|
+
constructor(token, port, device) {
|
|
65
126
|
this.token = token;
|
|
66
127
|
this.port = port;
|
|
67
|
-
this.device = generateDeviceIdentity();
|
|
128
|
+
this.device = device ?? generateDeviceIdentity();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create a client with a persisted device identity (loaded from disk or
|
|
132
|
+
* freshly generated and saved). This ensures the same device ID is reused
|
|
133
|
+
* across restarts so the OpenClaw gateway can pair it once.
|
|
134
|
+
*/
|
|
135
|
+
static async create(token, port) {
|
|
136
|
+
const device = await loadOrCreateDeviceIdentity();
|
|
137
|
+
return new OpenClawGatewayClient(token, port, device);
|
|
68
138
|
}
|
|
69
139
|
/** Connect and authenticate. Resolves when chat.send is ready, rejects on timeout or error. */
|
|
70
140
|
async connect() {
|
|
@@ -123,10 +193,11 @@ export class OpenClawGatewayClient {
|
|
|
123
193
|
console.warn(`[openclaw-ws] Disconnected: ${code} ${reasonStr}`);
|
|
124
194
|
const wasAuthenticated = this.authenticated;
|
|
125
195
|
this.authenticated = false;
|
|
126
|
-
// Detect pairing rejection
|
|
127
|
-
if (code === 1008
|
|
196
|
+
// Detect pairing rejection: code 1008 (Policy Violation) with pairing reason
|
|
197
|
+
if (code === 1008 && /pairing|not.paired/i.test(reasonStr)) {
|
|
128
198
|
console.error('[openclaw-ws] Connection closed due to pairing policy. Device is not paired.');
|
|
129
|
-
console.error(
|
|
199
|
+
console.error(`[openclaw-ws] Device ID: ${this.device.deviceId.slice(0, 16)}...`);
|
|
200
|
+
console.error('[openclaw-ws] Run: openclaw devices approve <requestId> (check gateway logs for requestId)');
|
|
130
201
|
this.pairingRejected = true;
|
|
131
202
|
}
|
|
132
203
|
// Reject all pending RPCs
|
|
@@ -143,7 +214,7 @@ export class OpenClawGatewayClient {
|
|
|
143
214
|
this.connectReject = null;
|
|
144
215
|
this.connectResolve = null;
|
|
145
216
|
}
|
|
146
|
-
if (!this.stopped
|
|
217
|
+
if (!this.stopped) {
|
|
147
218
|
this.scheduleReconnect();
|
|
148
219
|
}
|
|
149
220
|
});
|
|
@@ -240,7 +311,13 @@ export class OpenClawGatewayClient {
|
|
|
240
311
|
const errStr = msg.error ? JSON.stringify(msg.error) : 'Authentication rejected';
|
|
241
312
|
const isPairing = /pairing.required|not.paired/i.test(errStr);
|
|
242
313
|
if (isPairing) {
|
|
314
|
+
const errObj = msg.error;
|
|
315
|
+
const requestId = errObj?.requestId ?? errObj?.request_id ?? '';
|
|
243
316
|
console.error('[openclaw-ws] Pairing rejected — device is not paired with the OpenClaw gateway.');
|
|
317
|
+
if (requestId) {
|
|
318
|
+
console.error(`[openclaw-ws] Approve this device: openclaw devices approve ${requestId}`);
|
|
319
|
+
}
|
|
320
|
+
console.error(`[openclaw-ws] Device ID: ${this.device.deviceId.slice(0, 16)}...`);
|
|
244
321
|
console.error('[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ~/.openclaw/openclaw.json gateway.auth.token');
|
|
245
322
|
this.pairingRejected = true;
|
|
246
323
|
}
|
|
@@ -315,14 +392,25 @@ export class OpenClawGatewayClient {
|
|
|
315
392
|
});
|
|
316
393
|
}
|
|
317
394
|
scheduleReconnect() {
|
|
318
|
-
if (this.stopped || this.
|
|
395
|
+
if (this.stopped || this.reconnectTimer)
|
|
319
396
|
return;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
397
|
+
// After pairing rejection or max failures, switch to slow periodic retry
|
|
398
|
+
// so the gateway can self-heal once pairing is approved externally.
|
|
399
|
+
if (this.pairingRejected || this.consecutiveFailures >= OpenClawGatewayClient.MAX_CONSECUTIVE_FAILURES) {
|
|
400
|
+
if (this.consecutiveFailures === OpenClawGatewayClient.MAX_CONSECUTIVE_FAILURES) {
|
|
401
|
+
console.warn(`[openclaw-ws] ${this.consecutiveFailures} consecutive failures — switching to slow retry (every 60s).`);
|
|
402
|
+
console.warn('[openclaw-ws] Check that the OpenClaw gateway is running and OPENCLAW_GATEWAY_TOKEN is correct.');
|
|
403
|
+
}
|
|
404
|
+
this.consecutiveFailures++;
|
|
405
|
+
console.log(`[openclaw-ws] Slow retry in ${OpenClawGatewayClient.PAIRING_RETRY_MS / 1000}s...`);
|
|
406
|
+
this.reconnectTimer = setTimeout(() => {
|
|
407
|
+
this.reconnectTimer = null;
|
|
408
|
+
this.pairingRejected = false; // Clear flag so connect attempt proceeds
|
|
409
|
+
this.doConnect();
|
|
410
|
+
}, OpenClawGatewayClient.PAIRING_RETRY_MS);
|
|
324
411
|
return;
|
|
325
412
|
}
|
|
413
|
+
this.consecutiveFailures++;
|
|
326
414
|
const delay = Math.min(OpenClawGatewayClient.BASE_RECONNECT_MS * Math.pow(2, this.consecutiveFailures - 1), OpenClawGatewayClient.MAX_RECONNECT_MS);
|
|
327
415
|
console.log(`[openclaw-ws] Reconnecting in ${delay / 1000}s (attempt ${this.consecutiveFailures})...`);
|
|
328
416
|
this.reconnectTimer = setTimeout(() => {
|
|
@@ -404,7 +492,7 @@ export class InboundGateway {
|
|
|
404
492
|
const token = this.config.openclawGatewayToken ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
405
493
|
const port = this.config.openclawGatewayPort ?? DEFAULT_OPENCLAW_GATEWAY_PORT;
|
|
406
494
|
if (token) {
|
|
407
|
-
this.openclawClient =
|
|
495
|
+
this.openclawClient = await OpenClawGatewayClient.create(token, port);
|
|
408
496
|
try {
|
|
409
497
|
await this.openclawClient.connect();
|
|
410
498
|
console.log('[gateway] OpenClaw gateway WebSocket client ready');
|