Skip to content
Closed
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
8 changes: 4 additions & 4 deletions docs/modules/openclaw-channel/src.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ runtime entries from `index.*` at the extension root only, so the built

## Public surface

### [`createMoltzapChannelPlugin`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1226)
### [`createMoltzapChannelPlugin`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1235)

_Function_

Expand Down Expand Up @@ -63,15 +63,15 @@ to `agent:<name>`. Other colon-prefixed shapes are rejected.

**Returns:** The created moltzap channel plugin.

### [`default`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1256)
### [`default`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1265)

_Variable_

```ts
const plugin =
```

### [`moltzapChannelPlugin`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1253)
### [`moltzapChannelPlugin`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1262)

_Variable_

Expand All @@ -84,7 +84,7 @@ Shared singleton so a single registration reuses the same `activeClients`
closure across `startAccount` and `sendText`. Tests import this directly
to assert against that shared state.

### [`MoltzapChannelPlugin`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1244)
### [`MoltzapChannelPlugin`](https://github.com/chughtapan/moltzap/blob/main/packages/openclaw-channel/src/openclaw-entry.ts#L1253)

_TypeAlias_

Expand Down
8 changes: 4 additions & 4 deletions packages/openclaw-channel/src/MODULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ runtime entries from `index.*` at the extension root only, so the built

## Public surface

### [`createMoltzapChannelPlugin`](./openclaw-entry.ts#L1226)
### [`createMoltzapChannelPlugin`](./openclaw-entry.ts#L1235)

_Function_

Expand Down Expand Up @@ -58,15 +58,15 @@ to `agent:<name>`. Other colon-prefixed shapes are rejected.

**Returns:** The created moltzap channel plugin.

### [`default`](./openclaw-entry.ts#L1256)
### [`default`](./openclaw-entry.ts#L1265)

_Variable_

```ts
const plugin =
```

### [`moltzapChannelPlugin`](./openclaw-entry.ts#L1253)
### [`moltzapChannelPlugin`](./openclaw-entry.ts#L1262)

_Variable_

Expand All @@ -79,7 +79,7 @@ Shared singleton so a single registration reuses the same `activeClients`
closure across `startAccount` and `sendText`. Tests import this directly
to assert against that shared state.

### [`MoltzapChannelPlugin`](./openclaw-entry.ts#L1244)
### [`MoltzapChannelPlugin`](./openclaw-entry.ts#L1253)

_TypeAlias_

Expand Down
55 changes: 55 additions & 0 deletions packages/openclaw-channel/src/openclaw-entry.delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const OUTBOUND_TEXT = "Hello from outbound";
const AGENT_TEXT = "Hello nova";
const BEFORE_STOP_TEXT = "before stop";
const AFTER_STOP_TEXT = "after stop";
const AFTER_STATUS_FAILURE_TEXT = "after status failure";
const STATUS_FAILURE_ACCOUNT_ID = "status-failure-test";
const STATUS_CALLBACK_MESSAGE = "status callback failed";
const LOOKUP_FAILED_MESSAGE = "lookup failed";
const SERVER_REJECTED_MESSAGE = "Server rejected";
const INTERNAL_SERVER_ERROR_MESSAGE = "Internal server error";
Expand Down Expand Up @@ -160,6 +163,10 @@ describe("Flow 6: Outbound delivery - deliver callback + sendText", () => {
it("deliver reports transient RPC send failures", sendFailureIsReported);
it("a later delivery retries after a send failure", retriesAfterSendFailure);
it("stopAccount removes client from active pool", stopRemovesClient);
it(
"releases the gateway when the status callback throws",
statusFailureReleasesGateway,
);
it(
"property: resolveTarget normalizes generated agent names",
plainAgentNamesResolve,
Expand Down Expand Up @@ -614,6 +621,54 @@ function stopRemovesClient() {
});
}

// The host's setStatus is arbitrary caller code. Effect.sync surfaces a throw
// from it as a defect, which the connect path's catchAll does not observe, so
// this asserts on the binding rather than on the raised error.
function statusFailureReleasesGateway() {
return Effect.gen(function* () {
const fixture = createFakeChannelService({ ownAgentId: SELF_AGENT_ID });
const setStatus = vi.fn(() => {
throw new Error(STATUS_CALLBACK_MESSAGE);
});
const plugin = createMoltzapChannelPlugin({
createService: () => fixture.service,
});
const abortController = new AbortController();
abortControllers.push(abortController);

yield* Effect.ignore(
Effect.tryPromise({
try: () =>
plugin.gateway.startAccount({
cfg: makeCfg(),
accountId: STATUS_FAILURE_ACCOUNT_ID,
account: makeAccount(),
abortSignal: abortController.signal,
log: testLogger(),
setStatus,
}),
catch: (cause) => cause,
}),
);

expect(setStatus).toHaveBeenCalledTimes(1);
expect(fixture.state.closeCalls.count).toBe(1);

const result = yield* Effect.tryPromise({
try: () =>
plugin.outbound.sendText({
cfg: makeCfg(),
to: STOP_TARGET,
text: AFTER_STATUS_FAILURE_TEXT,
accountId: STATUS_FAILURE_ACCOUNT_ID,
}),
catch: (cause) =>
new DeliveryTestError({ message: "sendText failed", cause }),
});
expectFailureMessage(result, /not connected/i);
});
}

function plainAgentNamesResolve() {
return Effect.sync(() => {
fc.assert(
Expand Down
13 changes: 11 additions & 2 deletions packages/openclaw-channel/src/openclaw-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,14 @@ function startGatewayAccountEffect(
},
{ once: true },
);
yield* connectGatewayCore(core, service, ctx, setStatus);
// The binding is registered before connecting, so anything that escapes the
// connect path — including a host setStatus callback that throws, which
// Effect.sync surfaces as a defect rather than a failure — must release it.
yield* connectGatewayCore(core, service, ctx, setStatus).pipe(
Effect.onError(() =>
Effect.ignore(disconnectAndRemove(core, activeClients, accountId)),
),
);
});
}

Expand Down Expand Up @@ -799,9 +806,11 @@ function disconnectAndRemove(
activeClients: Map<string, OpenClawClientService>,
accountId: string,
) {
// A disconnect that fails must still drop the binding; leaving it registered
// would strand the account with no route to recovery.
return core
.disconnect()
.pipe(Effect.tap(() => Effect.sync(() => activeClients.delete(accountId))));
.pipe(Effect.ensuring(Effect.sync(() => activeClients.delete(accountId))));
}

interface RegisterInboundHandlerParams {
Expand Down
Loading