Skip to content
Open
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
54 changes: 32 additions & 22 deletions extensions/commonly/src/channel.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { readFileSync } from "node:fs";
import {
buildChannelConfigSchema,
createReplyPrefixContext,
DEFAULT_ACCOUNT_ID,
type ChannelPlugin,
type ReplyPayload,
} from "openclaw/plugin-sdk";

import { CommonlyClient } from "./client.js";
import { CommonlyWebSocket } from "./websocket.js";
import type { CommonlyEvent } from "./events.js";

import { CommonlyConfigSchema } from "./config-schema.js";
import { parseInlineDirectives } from "./directive-tags.js";
import type { CommonlyEvent } from "./events.js";
import { getCommonlyRuntime } from "./runtime.js";
import {
listCommonlyAccountIds,
resolveCommonlyAccount,
resolveDefaultCommonlyAccountId,
type ResolvedCommonlyAccount,
} from "./types.js";
import { parseInlineDirectives } from "./directive-tags.js";
import { readFileSync } from "node:fs";
import { CommonlyWebSocket } from "./websocket.js";

type CommonlyConnection = {
ws: CommonlyWebSocket;
Expand All @@ -29,7 +27,10 @@ type CommonlyConnection = {
const activeConnections = new Map<string, CommonlyConnection>();

const normalizePodId = (raw: string) =>
raw.replace(/^commonly:/i, "").replace(/^pod:/i, "").trim();
raw
.replace(/^commonly:/i, "")
.replace(/^pod:/i, "")
.trim();

const buildSummaryMessage = (summary?: CommonlyEvent["payload"]["summary"]): string => {
if (!summary) return "";
Expand Down Expand Up @@ -69,9 +70,11 @@ const formatEnsembleTurnBody = (event: CommonlyEvent): string => {
const lines: string[] = [];
lines.push(`Ensemble topic: ${context.topic}`);
lines.push(`Turn: ${context.turnNumber} (round ${context.roundNumber})`);
lines.push(context.isStarter
? "You are the starter. Provide the opening message."
: "You are responding to the ongoing discussion.");
lines.push(
context.isStarter
? "You are the starter. Provide the opening message."
: "You are responding to the ongoing discussion.",
);

const participants = event.payload?.participants || [];
if (participants.length > 0) {
Expand Down Expand Up @@ -179,7 +182,10 @@ export const commonlyPlugin: ChannelPlugin<ResolvedCommonlyAccount> = {
deliveryMode: "direct",
textChunkLimit: 8000,
sendText: async ({ to, text, threadId, accountId }) => {
const account = resolveCommonlyAccount({ cfg: getCommonlyRuntime().config.loadConfig(), accountId });
const account = resolveCommonlyAccount({
cfg: getCommonlyRuntime().config.loadConfig(),
accountId,
});
const client = new CommonlyClient({
baseUrl: account.baseUrl,
runtimeToken: account.runtimeToken,
Expand All @@ -198,7 +204,10 @@ export const commonlyPlugin: ChannelPlugin<ResolvedCommonlyAccount> = {
return { channel: "commonly", messageId: `${podId}:${Date.now()}` };
},
sendMedia: async ({ to, text, mediaUrl, threadId, accountId }) => {
const account = resolveCommonlyAccount({ cfg: getCommonlyRuntime().config.loadConfig(), accountId });
const account = resolveCommonlyAccount({
cfg: getCommonlyRuntime().config.loadConfig(),
accountId,
});
const client = new CommonlyClient({
baseUrl: account.baseUrl,
runtimeToken: account.runtimeToken,
Expand All @@ -207,10 +216,9 @@ export const commonlyPlugin: ChannelPlugin<ResolvedCommonlyAccount> = {
instanceId: account.instanceId,
});
const podId = normalizePodId(to);
const message = [
sanitizeOutboundText(text ?? ""),
mediaUrl?.trim() || "",
].filter(Boolean).join("\n");
const message = [sanitizeOutboundText(text ?? ""), mediaUrl?.trim() || ""]
.filter(Boolean)
.join("\n");
if (threadId) {
await client.postThreadComment(String(threadId), message);
return { channel: "commonly", messageId: String(threadId) };
Expand Down Expand Up @@ -345,17 +353,19 @@ export const commonlyPlugin: ChannelPlugin<ResolvedCommonlyAccount> = {
);
}
if (event._id) {
await client.ackEvent(event._id);
await client.ackEvent(event._id, event.payload?.deliveryId);
ctx.log?.info?.(`[${connectionKey}] summary.request acked id=${eventId}`);
}
return;
}

const rawContent = resolveInboundBody(event);
if (!rawContent) {
ctx.log?.info?.(`[${connectionKey}] event skipped (empty body) id=${eventId} type=${event.type}`);
ctx.log?.info?.(
`[${connectionKey}] event skipped (empty body) id=${eventId} type=${event.type}`,
);
if (event._id) {
await client.ackEvent(event._id);
await client.ackEvent(event._id, event.payload?.deliveryId);
ctx.log?.info?.(`[${connectionKey}] empty-body acked id=${eventId}`);
}
return;
Expand Down Expand Up @@ -495,8 +505,8 @@ export const commonlyPlugin: ChannelPlugin<ResolvedCommonlyAccount> = {
heartbeatTrigger: event.payload?.trigger,
});
ctx.log?.info?.(
`[${connectionKey}] message posted id=${eventId} pod=${podId} chars=${message.length} `
+ `postedId=${String(posted?.id || "n/a")}`,
`[${connectionKey}] message posted id=${eventId} pod=${podId} chars=${message.length} ` +
`postedId=${String(posted?.id || "n/a")}`,
);
if (event.type === "ensemble.turn" && !ensembleResponseSent) {
const ensembleId = event.payload?.ensembleId;
Expand Down Expand Up @@ -526,7 +536,7 @@ export const commonlyPlugin: ChannelPlugin<ResolvedCommonlyAccount> = {
});

if (event._id) {
await client.ackEvent(event._id);
await client.ackEvent(event._id, event.payload?.deliveryId);
ctx.log?.info?.(`[${connectionKey}] event acked id=${eventId} type=${event.type}`);
}
});
Expand Down
18 changes: 18 additions & 0 deletions extensions/commonly/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ describe("CommonlyClient", () => {
);
});

it("echoes the claimed deliveryId when acknowledging", async () => {
fetchMock.mockResolvedValue(createResponse());
const client = new CommonlyClient({
baseUrl: "http://localhost:5000",
runtimeToken: "rt",
});

await client.ackEvent("event-1", "claimed-child");

expect(fetchMock).toHaveBeenCalledWith(
"http://localhost:5000/api/agents/runtime/events/event-1/ack",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ deliveryId: "claimed-child" }),
}),
);
});

it("uses user token for user endpoints when provided", async () => {
fetchMock.mockResolvedValue(createResponse({ results: [] }));
const client = new CommonlyClient({
Expand Down
3 changes: 2 additions & 1 deletion extensions/commonly/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,11 @@ export class CommonlyClient {
/**
* Acknowledge an event
*/
async ackEvent(eventId: string): Promise<void> {
async ackEvent(eventId: string, deliveryId?: string): Promise<void> {
const res = await fetch(`${this.config.baseUrl}/api/agents/runtime/events/${eventId}/ack`, {
method: "POST",
headers: this.runtimeHeaders,
body: JSON.stringify(typeof deliveryId === "string" && deliveryId ? { deliveryId } : {}),
});
if (!res.ok) {
throw new Error(`Failed to ack event: ${res.status}`);
Expand Down
3 changes: 3 additions & 0 deletions extensions/commonly/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export type CommonlyEventType =
| (string & {});

export type CommonlyEventPayload = {
// ADR-026 D6: minted by the kernel when this event is claimed and echoed
// with the acknowledgement so a stale delivery cannot settle a replacement.
deliveryId?: string;
messageId?: string;
content?: string;
userId?: string;
Expand Down
Loading