Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion connectors/openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ actors:
auth_token_env: "${OPENCLAW_TOKEN}" # optional — bearer token (env var ref)
default_channel: "engineering" # optional — default delivery channel
default_to: "team-lead" # optional — default recipient
lane: "main" # optional — OpenClaw lane for concurrency control
```

### Config options
Expand All @@ -31,6 +32,7 @@ actors:
| `auth_token_env` | `string` | no | — | Bearer token for auth. Supports `${ENV_VAR}` syntax |
| `default_channel` | `string` | no | — | Default channel for message delivery |
| `default_to` | `string` | no | — | Default recipient for message delivery |
| `lane` | `string` | no | — | OpenClaw lane for concurrency control. Can be overridden per-route. |

## Events accepted

Expand All @@ -48,7 +50,8 @@ The connector builds a message string from the event and sends it as:
"wakeMode": "now",
"deliver": true,
"channel": "engineering",
"to": "team-lead"
"to": "team-lead",
"lane": "main"
}
```

Expand All @@ -64,6 +67,7 @@ These fields can be set in the route's `then.config`:
| `deliver` | `boolean` | `false` | Whether to deliver the message to a channel |
| `channel` | `string` | — | Override the actor's `default_channel` for this route |
| `to` | `string` | — | Override the actor's `default_to` for this route |
| `lane` | `string` | — | Override the actor's `lane` for this route. Controls OpenClaw concurrency. |
| `launch_prompt` | `string` | — | Resolved from route's `with.prompt_file`; appended to the message |

### Template interpolation
Expand Down
83 changes: 83 additions & 0 deletions connectors/openclaw/src/__tests__/target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,89 @@ describe('OpenClawTarget', () => {
expect(result.status).toBe('rejected');
});

// ─── #140 — Lane support ────────────────────────────────────────────────

it('passes lane from connector config in request body', async () => {
const laneTarget = new OpenClawTarget();
await laneTarget.init({
id: 'openclaw-agent',
connector: '@orgloop/connector-openclaw',
config: {
base_url: 'http://localhost:18789',
agent_id: 'test-agent',
lane: 'main',
},
});

const event = createTestEvent();
await laneTarget.deliver(event, {});

const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.lane).toBe('main');
});

it('passes lane from route config in request body', async () => {
const event = createTestEvent();
await target.deliver(event, { lane: 'background' });

const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.lane).toBe('background');
});

it('route config lane overrides connector config lane', async () => {
const laneTarget = new OpenClawTarget();
await laneTarget.init({
id: 'openclaw-agent',
connector: '@orgloop/connector-openclaw',
config: {
base_url: 'http://localhost:18789',
agent_id: 'test-agent',
lane: 'main',
},
});

const event = createTestEvent();
await laneTarget.deliver(event, { lane: 'background' });

const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.lane).toBe('background');
});

it('omits lane from request body when not configured', async () => {
const event = createTestEvent();
await target.deliver(event, {});

const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body).not.toHaveProperty('lane');
});

it('includes lane in callback-first delivery', async () => {
const laneTarget = new OpenClawTarget();
await laneTarget.init({
id: 'openclaw-agent',
connector: '@orgloop/connector-openclaw',
config: {
base_url: 'http://localhost:18789',
agent_id: 'test-agent',
lane: 'main',
},
});

const event = createTestEvent({
source: 'coding-agent',
type: 'actor.stopped',
payload: {
meta: { openclaw_callback_session_key: 'callback:sess-abc' },
},
});

await laneTarget.deliver(event, {});

const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.sessionKey).toBe('callback:sess-abc');
expect(body.lane).toBe('main');
});

// ─── #66 — Dynamic threadId ──────────────────────────────────────────────

it('passes threadId when route config has thread_id', async () => {
Expand Down
8 changes: 8 additions & 0 deletions connectors/openclaw/src/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ interface OpenClawConfig {
agent_id?: string;
default_channel?: string;
default_to?: string;
lane?: string;
}

export class OpenClawTarget implements ActorConnector {
Expand All @@ -73,6 +74,7 @@ export class OpenClawTarget implements ActorConnector {
private agentId?: string;
private defaultChannel?: string;
private defaultTo?: string;
private lane?: string;
private httpAgent: HttpAgent | null = null;
private fetch: typeof globalThis.fetch = globalThis.fetch;

Expand All @@ -82,6 +84,7 @@ export class OpenClawTarget implements ActorConnector {
this.agentId = cfg.agent_id;
this.defaultChannel = cfg.default_channel;
this.defaultTo = cfg.default_to;
this.lane = cfg.lane;

if (cfg.auth_token_env) {
this.authToken = resolveEnvVar(cfg.auth_token_env);
Expand All @@ -104,6 +107,9 @@ export class OpenClawTarget implements ActorConnector {
const rawThreadId = routeConfig.thread_id as string | undefined;
const threadId = rawThreadId ? interpolateTemplate(rawThreadId, event) : undefined;

// #140 — Lane support: route config overrides connector-level default
const lane = (routeConfig.lane as string) ?? this.lane;

// #91 — Callback-first delivery: check event payload for callback metadata
const callbackSessionKey = this.resolveCallbackSessionKey(event);
const callbackAgentId = this.resolveCallbackAgentId(event);
Expand All @@ -117,6 +123,7 @@ export class OpenClawTarget implements ActorConnector {
deliver: routeConfig.deliver ?? false,
channel: (routeConfig.channel as string) ?? this.defaultChannel,
to: (routeConfig.to as string) ?? this.defaultTo,
...(lane !== undefined ? { lane } : {}),
...(threadId !== undefined ? { threadId } : {}),
...(routeConfig.model ? { model: routeConfig.model } : {}),
...(routeConfig.thinking ? { thinking: routeConfig.thinking } : {}),
Expand All @@ -136,6 +143,7 @@ export class OpenClawTarget implements ActorConnector {
deliver: routeConfig.deliver ?? false,
channel: (routeConfig.channel as string) ?? this.defaultChannel,
to: (routeConfig.to as string) ?? this.defaultTo,
...(lane !== undefined ? { lane } : {}),
...(threadId !== undefined ? { threadId } : {}),
...(routeConfig.model ? { model: routeConfig.model } : {}),
...(routeConfig.thinking ? { thinking: routeConfig.thinking } : {}),
Expand Down