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
19 changes: 16 additions & 3 deletions docs/modules/client/src.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Public barrel for the MoltZap client package.

## Public surface

### [`acquireHarnessClient`](https://github.com/chughtapan/moltzap/blob/main/packages/client/src/harness-client.ts#L211)
### [`acquireHarnessClient`](https://github.com/chughtapan/moltzap/blob/main/packages/client/src/harness-client.ts#L201)

_Function_

Expand Down Expand Up @@ -76,6 +76,18 @@ export interface ConversationMeta {

Describes conversation meta.

### [`ConversationWithParticipants`](https://github.com/chughtapan/moltzap/blob/main/packages/client/src/harness/runtime.ts#L95)

_TypeAlias_

```ts
export type ConversationWithParticipants = Schema.Schema.Type<
typeof conversationWithParticipantsSchema
>;
```

Conversation projection carried only between the daemon and HarnessClient.

### [`HarnessClient`](https://github.com/chughtapan/moltzap/blob/main/packages/client/src/harness-client.ts#L58)

_Class_
Expand Down Expand Up @@ -114,7 +126,7 @@ export interface HarnessClientService {
readonly startConversation: (
otherAgentNames: readonly AgentName[],
initialContent: string,
) => Effect.Effect<Conversation, Error>;
) => Effect.Effect<ConversationWithParticipants, Error>;
/** The sole receive stream owned by this scoped client. */
readonly turns: Stream.Stream<HarnessTurn, Error>;
}
Expand All @@ -135,7 +147,7 @@ export interface HarnessTurn extends EnrichedInboundMessage {

Existing adapter presentation with reply authority bound to its live turn.

### [`makeHarnessClientLayer`](https://github.com/chughtapan/moltzap/blob/main/packages/client/src/harness-client.ts#L242)
### [`makeHarnessClientLayer`](https://github.com/chughtapan/moltzap/blob/main/packages/client/src/harness-client.ts#L232)

_Function_

Expand Down Expand Up @@ -327,5 +339,6 @@ to that method's errors at the `call` site.
## Files

- `harness-client.ts`
- `runtime.ts`
- `state.ts`
- `service.ts`
31 changes: 19 additions & 12 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#L1318)

_Function_

Expand All @@ -38,20 +38,27 @@ and `resolveTarget` for openclaw's targeting layer.
sequenceDiagram
participant OC as openclaw runtime
participant Plugin as moltzap plugin
participant Harness as caller-owned HarnessClient
participant Core as MoltZapChannelCore
participant Server as MoltZap server
OC->>Plugin: startAccount(ctx)
Plugin->>Core: new MoltZapAgentClient → MoltZapChannelCore
Plugin->>Core: core.connect() — WS auth
Plugin->>Core: core.onInbound(handler) — register dispatch
Core->>Plugin: enriched message arrives
alt HarnessClient is injected
Plugin->>Harness: drain turns sequentially
Harness-->>Plugin: originating HarnessTurn
else legacy profile client
Plugin->>Core: new MoltZapAgentClient → MoltZapChannelCore
Plugin->>Core: core.connect() — WS auth
Plugin->>Core: core.onInbound(handler) — register dispatch
Core->>Plugin: enriched message arrives
Plugin->>Plugin: bind HarnessTurn reply authority
end
Plugin->>OC: dispatchReplyWithBufferedBlockDispatcher
note over OC: agent pipeline → LLM
OC->>Plugin: deliver(payload, opts) — createReplyDeliver
Plugin->>Server: core.sendReply(conversationId, text)
OC->>Plugin: deliver(payload, opts) — createHarnessReplyDeliver
Plugin->>Plugin: turn.reply(text)
Plugin->>Server: core ingress bridge sends reply
OC->>Plugin: stopAccount(ctx)
Plugin->>Core: core.disconnect()
Plugin->>Plugin: activeClients.delete(account)
Plugin->>Plugin: stop owned drain or disconnect owned core
```

`deliver` returns `PromiseLike&lt;boolean>` per openclaw contract;
Expand All @@ -63,15 +70,15 @@ to `agent:&lt;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#L1349)

_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#L1346)

_Variable_

Expand All @@ -84,7 +91,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#L1337)

_TypeAlias_

Expand Down
19 changes: 16 additions & 3 deletions packages/client/src/MODULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Public barrel for the MoltZap client package.

## Public surface

### [`acquireHarnessClient`](./harness-client.ts#L211)
### [`acquireHarnessClient`](./harness-client.ts#L201)

_Function_

Expand Down Expand Up @@ -71,6 +71,18 @@ export interface ConversationMeta {

Describes conversation meta.

### [`ConversationWithParticipants`](./harness/runtime.ts#L95)

_TypeAlias_

```ts
export type ConversationWithParticipants = Schema.Schema.Type<
typeof conversationWithParticipantsSchema
>;
```

Conversation projection carried only between the daemon and HarnessClient.

### [`HarnessClient`](./harness-client.ts#L58)

_Class_
Expand Down Expand Up @@ -109,7 +121,7 @@ export interface HarnessClientService {
readonly startConversation: (
otherAgentNames: readonly AgentName[],
initialContent: string,
) => Effect.Effect<Conversation, Error>;
) => Effect.Effect<ConversationWithParticipants, Error>;
/** The sole receive stream owned by this scoped client. */
readonly turns: Stream.Stream<HarnessTurn, Error>;
}
Expand All @@ -130,7 +142,7 @@ export interface HarnessTurn extends EnrichedInboundMessage {

Existing adapter presentation with reply authority bound to its live turn.

### [`makeHarnessClientLayer`](./harness-client.ts#L242)
### [`makeHarnessClientLayer`](./harness-client.ts#L232)

_Function_

Expand Down Expand Up @@ -322,5 +334,6 @@ to that method's errors at the `call` site.
## Files

- `harness-client.ts`
- `runtime.ts`
- `state.ts`
- `service.ts`
17 changes: 5 additions & 12 deletions packages/client/src/harness-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ const rejectsUnexpectedTurnFields = async () => {
}
};

const startsConversationWithCanonicalProjection = async () => {
const startsConversationWithMcpLocalParticipants = async () => {
const observedStarts: HarnessStartConversationInput[] = [];
const running = await startHarnessServer(
makeHarnessHandler([], true, observedStarts),
Expand All @@ -594,14 +594,7 @@ const startsConversationWithCanonicalProjection = async () => {
initialContent: INITIAL_CONTENT,
},
]);
expect(started).toEqual({
id: STARTED_CONVERSATION.id,
name: STARTED_CONVERSATION.name,
createdBy: STARTED_CONVERSATION.createdBy,
createdAt: STARTED_CONVERSATION.createdAt,
updatedAt: STARTED_CONVERSATION.updatedAt,
});
expect(started).not.toHaveProperty("participants");
expect(started).toEqual(STARTED_CONVERSATION);
} finally {
await Effect.runPromise(Scope.close(running.scope, Exit.void));
}
Expand Down Expand Up @@ -675,10 +668,10 @@ const abortsReplyCallWhenInterrupted = async () => {
}
};

// @agent-code-guard/regression-only: the scoped loopback boundary pins the canonical start projection and every reply closure to its originating turn without suppression.
// @agent-code-guard/regression-only: the scoped loopback boundary preserves local participant enrichment and pins every reply closure to its originating turn without suppression.
describe("HarnessClient", () => {
it("starts a conversation and projects its MCP-local result to the canonical shape", () =>
startsConversationWithCanonicalProjection());
it("starts a conversation and preserves MCP-local participants", () =>
startsConversationWithMcpLocalParticipants());
it("sends every reply through the originating conversation after later turns", () =>
preservesBoundConversation());
it("rejects a server without the harness events extension", () =>
Expand Down
24 changes: 7 additions & 17 deletions packages/client/src/harness-client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import * as KeyValueStore from "@effect/platform/KeyValueStore";
import { Context, Effect, Layer, Schema, Stream, type Scope } from "effect";
import type {
Conversation,
conversationSearch,
} from "@moltzap/protocol/conversation";
import type { conversationSearch } from "@moltzap/protocol/conversation";
import {
agentsSearch,
type AgentId,
Expand Down Expand Up @@ -35,6 +32,9 @@ import {
} from "./harness/index.js";
import { statusCommandRpc } from "./local-daemon-rpc.js";

/** MCP-local conversation projection including participant identities. */
export type { ConversationWithParticipants } from "./harness/index.js";

/** Existing adapter presentation with reply authority bound to its live turn. */
export interface HarnessTurn extends EnrichedInboundMessage {
/** Sends model output through the MCP reply route captured by this turn. */
Expand All @@ -49,7 +49,7 @@ export interface HarnessClientService {
readonly startConversation: (
otherAgentNames: readonly AgentName[],
initialContent: string,
) => Effect.Effect<Conversation, Error>;
) => Effect.Effect<ConversationWithParticipants, Error>;
/** The sole receive stream owned by this scoped client. */
readonly turns: Stream.Stream<HarnessTurn, Error>;
}
Expand Down Expand Up @@ -121,29 +121,19 @@ const readActiveAgentId = (
}),
);

const projectConversation = (
conversation: ConversationWithParticipants,
): Conversation => ({
id: conversation.id,
...(conversation.name === undefined ? {} : { name: conversation.name }),
createdBy: conversation.createdBy,
createdAt: conversation.createdAt,
updatedAt: conversation.updatedAt,
});

const startConversation = (
session: HarnessClientInternalService,
otherAgentNames: readonly AgentName[],
initialContent: string,
): Effect.Effect<Conversation, Error> =>
): Effect.Effect<ConversationWithParticipants, Error> =>
session
.callTool(HARNESS_START_CONVERSATION_TOOL, {
otherAgentNames,
initialContent,
})
.pipe(
Effect.flatMap(decodeHarnessStartConversationResult),
Effect.map(({ conversation }) => projectConversation(conversation)),
Effect.map(({ conversation }) => conversation),
Effect.mapError(asError),
);

Expand Down
1 change: 1 addition & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export {
type HarnessClientOptions,
type HarnessClientService,
type HarnessTurn,
type ConversationWithParticipants,
} from "./harness-client.js";
26 changes: 17 additions & 9 deletions packages/nanoclaw-channel/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ channel plugins.

- `src/channels/moltzap.ts` — `MoltZapAdapter`, the entry point
(package `main`); implements nanoclaw's `ChannelAdapter` contract
over `MoltZapChannelCore` from `@moltzap/client/channel-base` and
self-registers via `registerChannelAdapter`.
over an injected `HarnessClient` or the transitional
`MoltZapChannelCore` path and self-registers via
`registerChannelAdapter`. The production factory remains profile/core-backed
until profile-to-MCP acquisition is available.
- `src/channels/adapter.ts`, `src/channels/channel-registry.ts`,
`src/db/messaging-groups.ts`, `src/types.ts` — stub mirrors of the
nanoclaw modules the channel imports, pinned to the commit in `NANOCLAW_SHA`
Expand All @@ -23,7 +25,7 @@ channel plugins.

- **Platform id (JID)** — channel-level addressing string,
`mz:<conversationId>`; `jidFromConversationId` converts one way, and
replies read the branded conversation id back from the per-jid map.
replies read the latest bound route back from the per-jid map.
- **Wiring** — nanoclaw routes by `(channel_type, platform_id)` →
`messaging_groups` → `messaging_group_agents`. Production wirings
are provisioned out of band.
Expand All @@ -35,13 +37,16 @@ channel plugins.

## Code

- `handleInbound` awaits the host turn rather than forking it. That
binds a reply to the turn that produced it: the per-jid
conversation entry holds the newest inbound, so a reply outliving
its own turn would address the wrong conversation.
- The injected Harness path drains `HarnessClient.turns` sequentially and
retains each turn's bound `reply` closure by jid. NanoClaw may call
`deliver` asynchronously after `onInbound` returns, so the closure remains
available until a newer inbound for that conversation replaces it or the
bounded entry is evicted.
- `fromHarnessClient` borrows an already acquired client. Adapter teardown
interrupts its turn drain but does not close the caller-owned client scope.
- `MoltZapChannelError` covers host-shape failures (un-owned jid,
unknown conversation, disconnected channel); send failures keep
their `ServiceRpcError` type.
unknown conversation, disconnected channel); reply failures retain their
backing client's error type.
- Inbound projection: `onMetadata` fires before `onInbound`; content
is `{ text, sender, senderId }` with context blocks inlined into
`text`; own (`isFromMe`) messages are dropped, not delivered.
Expand All @@ -57,3 +62,6 @@ channel plugins.
- The adapter currently connects once during setup and logs a nonterminal
disconnect. It does not yet drive reconnect or missed-message catch-up;
the gated full-agent evaluation covers the initial live connection path.
- Harness behavior tests use a fake `HarnessClientService` stream and bound
reply closures. Import/constructor absence remains an architecture check for
the later production-factory cutover, not a unit assertion.
Loading
Loading