@openclaw/clickclack 2026.7.2-beta.5 → 2026.7.2-beta.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.
@@ -1,4 +1,4 @@
1
- import { _ as clickClackMeta, b as listEnabledClickClackAccounts, c as resolveChannelId, d as createClickClackClient, g as clickClackConfigAdapter, h as CLICKCLACK_CHANNEL_ID, l as resolveWorkspaceId, m as clickClackConfigSchema, n as clickClackSetupAdapter, p as normalizeClickClackCorrelationId, r as clickClackSetupContract, t as clickClackSetupWizard, v as DEFAULT_ACCOUNT_ID, x as resolveClickClackAccount } from "./setup-surface-CD6MUC1g.js";
1
+ import { _ as clickClackMeta, b as listEnabledClickClackAccounts, c as resolveChannelId, d as createClickClackClient, g as clickClackConfigAdapter, h as CLICKCLACK_CHANNEL_ID, l as resolveWorkspaceId, m as clickClackConfigSchema, n as clickClackSetupAdapter, p as normalizeClickClackCorrelationId, r as clickClackSetupContract, t as clickClackSetupWizard, v as DEFAULT_ACCOUNT_ID, x as resolveClickClackAccount } from "./setup-surface-DAy8vDJK.js";
2
2
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
3
3
  import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
4
4
  import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
@@ -6,490 +6,176 @@ import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState }
6
6
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
7
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
8
8
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
9
+ import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import { buildMentionRegexes, normalizeMentionText } from "openclaw/plugin-sdk/channel-mention-gating";
9
12
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
10
13
  import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
11
14
  import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
12
- import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
13
- import { createHash, randomUUID } from "node:crypto";
14
15
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
15
16
  import { FormatCapabilityProfile, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
16
- //#region extensions/clickclack/src/runtime.ts
17
- /**
18
- * Runtime store for host-provided OpenClaw services used by the ClickClack
19
- * bundled plugin.
20
- */
21
- const { setRuntime: setClickClackRuntime, getRuntime: getClickClackRuntime } = createPluginRuntimeStore({
22
- pluginId: "clickclack",
23
- errorMessage: "ClickClack runtime not initialized"
24
- });
25
- //#endregion
26
- //#region extensions/clickclack/src/access.ts
27
- /**
28
- * Maps ClickClack senders and conversations onto the shared channel ingress
29
- * allowlist/command authorization contract.
30
- */
31
- const CHANNEL_ID$2 = "clickclack";
32
- function normalizeClickClackUserId(value) {
33
- const trimmed = value.trim();
34
- if (!trimmed) return null;
35
- const withoutProvider = trimmed.replace(/^(clickclack|cc):/i, "").trim();
36
- return withoutProvider.match(/^dm:(.+)$/i)?.[1]?.trim() || withoutProvider || null;
17
+ //#region extensions/clickclack/src/discussions/binding-generation.ts
18
+ const DISCUSSION_GENERATIONS_NAMESPACE = "discussion-binding-generations";
19
+ const MAX_PENDING_DISCUSSION_GENERATIONS = 1e4;
20
+ const storesByRuntime$2 = /* @__PURE__ */ new WeakMap();
21
+ function getGenerationStore(runtime) {
22
+ const existing = storesByRuntime$2.get(runtime);
23
+ if (existing) return existing;
24
+ const created = runtime.state.openSyncKeyedStore({
25
+ namespace: DISCUSSION_GENERATIONS_NAMESPACE,
26
+ maxEntries: MAX_PENDING_DISCUSSION_GENERATIONS,
27
+ overflowPolicy: "reject-new"
28
+ });
29
+ storesByRuntime$2.set(runtime, created);
30
+ return created;
37
31
  }
38
- const clickClackIngressIdentity = {
39
- key: "user-id",
40
- normalizeEntry: normalizeClickClackUserId,
41
- normalizeSubject: normalizeClickClackUserId,
42
- isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
43
- entryIdPrefix: "clickclack-user"
44
- };
45
- /**
46
- * Resolves whether a ClickClack message should enter the agent pipeline and
47
- * whether its command-style body may run tools.
48
- */
49
- async function resolveClickClackInboundAccess(params) {
50
- const runtime = getClickClackRuntime();
51
- const isDirect = Boolean(params.message.direct_conversation_id);
52
- const cfg = params.config;
53
- const shouldCheckCommand = runtime.channel.commands.shouldComputeCommandAuthorized(params.message.body, cfg);
54
- const resolved = await resolveStableChannelMessageIngress({
55
- channelId: CHANNEL_ID$2,
56
- accountId: params.account.accountId,
57
- identity: clickClackIngressIdentity,
58
- cfg,
59
- subject: { stableId: params.message.author_id },
60
- conversation: {
61
- kind: isDirect ? "direct" : "group",
62
- id: isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
63
- },
64
- allowFrom: params.account.allowFrom,
65
- dmPolicy: "allowlist",
66
- groupPolicy: "allowlist",
67
- command: shouldCheckCommand ? {
68
- cfg,
69
- modeWhenAccessGroupsOff: "configured"
70
- } : false
32
+ /** Reserves a generation so an interrupted channel create can be adopted on retry. */
33
+ function reserveDiscussionBindingGeneration(params) {
34
+ const store = getGenerationStore(params.runtime);
35
+ const existing = store.lookup(params.sessionKey);
36
+ if (existing?.destinationIdentity === params.destinationIdentity) return existing.generation;
37
+ const generation = (params.createGeneration ?? randomUUID)();
38
+ store.register(params.sessionKey, {
39
+ destinationIdentity: params.destinationIdentity,
40
+ generation
71
41
  });
72
- return {
73
- shouldDispatch: resolved.ingress.admission === "dispatch",
74
- commandAuthorized: resolved.commandAccess.requested ? resolved.commandAccess.authorized : resolved.senderAccess.allowed
75
- };
42
+ return generation;
43
+ }
44
+ /** Clears only the completed reservation; future opens must mint a new ownership ref. */
45
+ function clearDiscussionBindingGeneration(params) {
46
+ const store = getGenerationStore(params.runtime);
47
+ const existing = store.lookup(params.sessionKey);
48
+ if (!existing) return;
49
+ if (params.expectedGeneration && existing.generation !== params.expectedGeneration) return;
50
+ store.delete(params.sessionKey);
51
+ }
52
+ /** Quarantines a destination before the first fallible channel create. */
53
+ function recordPendingDiscussionOpen(params) {
54
+ const store = getGenerationStore(params.runtime);
55
+ const existing = store.lookup(params.sessionKey);
56
+ if (!existing || existing.generation !== params.generation) throw new Error("ClickClack discussion generation changed before channel creation");
57
+ store.register(params.sessionKey, {
58
+ ...existing,
59
+ pending: params.pending
60
+ });
61
+ }
62
+ function listPendingDiscussionOpens(runtime) {
63
+ return getGenerationStore(runtime).entries().flatMap((entry) => entry.value.pending ? [{
64
+ sessionKey: entry.key,
65
+ generation: entry.value.generation,
66
+ ...entry.value.pending
67
+ }] : []);
68
+ }
69
+ function hasPendingDiscussionOpenForDestination(params) {
70
+ const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
71
+ return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
76
72
  }
77
73
  //#endregion
78
- //#region extensions/clickclack/src/command-menu.ts
79
- const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
80
- const CLICKCLACK_MAX_COMMANDS = 100;
81
- const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
82
- const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
83
- function truncateCodePoints(value, maxLength) {
84
- return Array.from(value).slice(0, maxLength).join("");
74
+ //#region extensions/clickclack/src/discussions/naming.ts
75
+ function shortSessionHash(sessionKey) {
76
+ return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
85
77
  }
86
- function commandArgsHint(spec) {
87
- if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
88
- return spec.acceptsArgs ? "[args]" : "";
78
+ function slugifyLabel(label) {
79
+ return label.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80).replace(/-+$/gu, "");
89
80
  }
90
- function errorStatus(error) {
91
- if (!error || typeof error !== "object" || !("status" in error)) return;
92
- const status = error.status;
93
- return typeof status === "number" ? status : void 0;
81
+ function fallbackDiscussionLabel(sessionKey, agentId) {
82
+ const agentSegment = agentId?.trim() ? slugifyLabel(agentId) : "";
83
+ const hash = shortSessionHash(sessionKey).slice(0, 8);
84
+ return `s-${agentSegment ? `${agentSegment}-` : ""}${hash}`;
94
85
  }
95
- function mapNativeCommandSpecsToClickClackMenu(specs, log) {
96
- const commands = [];
97
- const seen = /* @__PURE__ */ new Set();
98
- const invalidNames = /* @__PURE__ */ new Set();
99
- for (const spec of specs) {
100
- if (spec.isAlias) continue;
101
- const command = spec.name.trim().toLowerCase();
102
- if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
103
- invalidNames.add(spec.name.trim() || "<empty>");
104
- continue;
86
+ function resolveDiscussionLabel(entry, sessionKey, agentId) {
87
+ return entry.label?.trim() || entry.displayName?.trim() || entry.subject?.trim() || fallbackDiscussionLabel(sessionKey, agentId);
88
+ }
89
+ function truncateDiscussionDisplayTitle(label) {
90
+ return Array.from(label).slice(0, 200).join("").trim();
91
+ }
92
+ function slugifyDiscussionLabel(label, sessionKey) {
93
+ return slugifyLabel(label) || fallbackDiscussionLabel(sessionKey);
94
+ }
95
+ function discussionSessionPeerId(identity) {
96
+ return `disc-${createHash("sha256").update([
97
+ identity.mainSessionKey,
98
+ identity.sessionId,
99
+ identity.serverBaseUrl,
100
+ identity.channelId,
101
+ identity.externalRef
102
+ ].join("\0")).digest("hex").slice(0, 32)}`;
103
+ }
104
+ function isDiscussionSessionKey(sessionKey) {
105
+ return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
106
+ }
107
+ function discussionExternalRef(installationId, mainSessionKey, sessionId, destinationIdentity, bindingGeneration) {
108
+ return `openclaw:${installationId}:${shortSessionHash([
109
+ mainSessionKey,
110
+ sessionId,
111
+ destinationIdentity,
112
+ bindingGeneration
113
+ ].join("\0"))}`;
114
+ }
115
+ function discussionCredentialFingerprint(token) {
116
+ return createHash("sha256").update(token).digest("hex");
117
+ }
118
+ function discussionSessionKey(params) {
119
+ return params.runtime.channel.routing.buildAgentSessionKey({
120
+ agentId: params.agentId,
121
+ channel: "clickclack",
122
+ accountId: params.accountId,
123
+ peer: {
124
+ kind: "channel",
125
+ id: discussionSessionPeerId(params)
105
126
  }
106
- if (seen.has(command)) continue;
107
- seen.add(command);
108
- const description = spec.description.trim() || command;
109
- commands.push({
110
- command,
111
- description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
112
- args_hint: commandArgsHint(spec)
127
+ });
128
+ }
129
+ //#endregion
130
+ //#region extensions/clickclack/src/discussions/binding-store.ts
131
+ function bindingMatchesSessionIncarnation(runtime, sessionKey, binding) {
132
+ const entry = runtime.agent.session.getSessionEntry({
133
+ sessionKey,
134
+ readConsistency: "latest"
135
+ });
136
+ return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId);
137
+ }
138
+ function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
139
+ const entry = runtime.agent.session.getSessionEntry({
140
+ sessionKey,
141
+ readConsistency: "latest"
142
+ });
143
+ return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
144
+ }
145
+ const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
146
+ const MAX_DISCUSSION_BINDINGS = 1e4;
147
+ const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
148
+ function channelKey(serverBaseUrl, channelId) {
149
+ return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
150
+ }
151
+ /** SQLite-backed session/channel bindings with a process-local inbound lookup index. */
152
+ var ClickClackDiscussionBindingStore = class {
153
+ #store;
154
+ #sessionByChannel = /* @__PURE__ */ new Map();
155
+ #mainByDiscussionSession = /* @__PURE__ */ new Map();
156
+ #runtime;
157
+ constructor(runtime) {
158
+ this.#runtime = runtime;
159
+ this.#store = runtime.state.openSyncKeyedStore({
160
+ namespace: DISCUSSION_BINDINGS_NAMESPACE,
161
+ maxEntries: MAX_DISCUSSION_BINDINGS,
162
+ overflowPolicy: "reject-new"
113
163
  });
164
+ for (const entry of this.#store.entries()) this.#index(entry.key, entry.value);
114
165
  }
115
- if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
116
- return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
117
- }
118
- async function syncClickClackCommandMenu(params) {
119
- try {
120
- const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
121
- await params.client.setBotCommands(commands);
122
- } catch (error) {
123
- const status = errorStatus(error);
124
- if (status === 403) {
125
- params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
126
- return;
127
- }
128
- if (status === 404) {
129
- params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
130
- return;
131
- }
132
- params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
133
- }
134
- }
135
- //#endregion
136
- //#region extensions/clickclack/src/activity.ts
137
- /**
138
- * Publishes agent activity (streamed commentary + tool progress) into
139
- * ClickClack as durable `agent_commentary` / `agent_tool` message rows,
140
- * coalesced so one logical step becomes one row instead of a row per frame.
141
- *
142
- * Ported from the clickglass agent-bridge sidecar, adapted from gateway
143
- * websocket frames to the in-process `replyOptions.onItemEvent` seam:
144
- *
145
- * - Commentary/reasoning arrives as cumulative text snapshots per item id.
146
- * Each segment becomes one durable row: POSTed when the segment starts
147
- * streaming and PATCHed (debounced) as the snapshot grows, so prose
148
- * interleaves chronologically with tool rows.
149
- * - Tool/step items can emit several frames (start/update/complete) for the
150
- * same call, sometimes with lane-prefixed ids (`tool:X`, `command:X`).
151
- * Normalize the prefix away to one key per call, POST one row on the first
152
- * frame, and PATCH it when a later frame carries a strictly longer body.
153
- */
154
- /** Debounce window for PATCHing streaming commentary snapshots. */
155
- const CLICKCLACK_COMMENTARY_FLUSH_MS = 700;
156
- /** Item kinds rendered as agent_tool rows; everything else is commentary. */
157
- const TOOL_ITEM_KINDS = /* @__PURE__ */ new Set([
158
- "tool",
159
- "command",
160
- "command_output",
161
- "patch",
162
- "search",
163
- "api"
164
- ]);
165
- /** Item kinds that never become durable rows (ephemeral or plumbing lanes). */
166
- const SKIPPED_ITEM_KINDS = /* @__PURE__ */ new Set(["lifecycle"]);
167
- /** Item kinds emitted as cumulative commentary snapshots. */
168
- const STREAMING_COMMENTARY_ITEM_KINDS = /* @__PURE__ */ new Set([
169
- "preamble",
170
- "commentary",
171
- "analysis",
172
- "thinking",
173
- "reasoning"
174
- ]);
175
- /** Provider-specific reasoning lanes normalized into one ClickClack label. */
176
- const THINKING_ITEM_KINDS = /* @__PURE__ */ new Set([
177
- "analysis",
178
- "thinking",
179
- "reasoning"
180
- ]);
181
- function normalizedItemKind(payload) {
182
- return payload.kind?.trim().toLowerCase() ?? "";
183
- }
184
- function commentaryBody(payload) {
185
- const text = payload.progressText?.trim() || payload.summary?.trim() || payload.meta?.trim() || "";
186
- if (!text) return "";
187
- const kind = normalizedItemKind(payload);
188
- if (THINKING_ITEM_KINDS.has(kind)) return `**Thinking**\n\n${text}`;
189
- return text;
190
- }
191
- function activityBody(payload) {
192
- const line = buildChannelProgressDraftLine({
193
- event: "item",
194
- itemId: payload.itemId,
195
- toolCallId: payload.toolCallId,
196
- itemKind: payload.kind,
197
- title: payload.title,
198
- name: payload.name,
199
- phase: payload.phase,
200
- status: payload.status,
201
- summary: payload.summary,
202
- progressText: payload.progressText,
203
- meta: payload.meta
204
- })?.text?.trim();
205
- if (line) return line;
206
- const head = payload.name?.trim() || payload.title?.trim();
207
- const text = payload.progressText?.trim() || payload.summary?.trim();
208
- if (head && text) return `**${head}**\n\n${text}`;
209
- if (text) return text;
210
- if (head) return head;
211
- return payload.status?.trim() || payload.kind?.trim() || "";
212
- }
213
- /**
214
- * Creates a per-turn activity publisher. Publishing is best-effort: transport
215
- * failures are reported through `onError` and never interrupt the reply turn.
216
- */
217
- function createClickClackActivityPublisher(params) {
218
- const flushMs = params.flushMs ?? CLICKCLACK_COMMENTARY_FLUSH_MS;
219
- const commentaryByItem = /* @__PURE__ */ new Map();
220
- const toolRows = /* @__PURE__ */ new Map();
221
- let provenance;
222
- let chain = Promise.resolve();
223
- const enqueue = (work) => {
224
- chain = chain.then(work).catch((error) => {
225
- params.onError?.(error);
226
- });
227
- return chain;
228
- };
229
- const postRow = (kind, body) => params.client.createActivityMessage({
230
- channelId: params.target.channelId,
231
- conversationId: params.target.conversationId,
232
- body,
233
- kind,
234
- turnId: params.turnId,
235
- provenance
236
- });
237
- const flushCommentary = (segmentKey) => {
238
- const segment = commentaryByItem.get(segmentKey);
239
- if (!segment) return Promise.resolve();
240
- if (segment.timer) {
241
- clearTimeout(segment.timer);
242
- segment.timer = void 0;
243
- }
244
- if (!segment.dirty || !segment.body.trim()) return Promise.resolve();
245
- segment.dirty = false;
246
- const body = segment.body;
247
- return enqueue(async () => {
248
- if (segment.messageId) {
249
- await params.client.updateMessageBody(segment.messageId, body);
250
- return;
251
- }
252
- const posted = await postRow("agent_commentary", body);
253
- segment.messageId = posted.id;
254
- });
255
- };
256
- const flushAllCommentary = () => {
257
- const flushes = [...commentaryByItem.keys()].map((key) => flushCommentary(key));
258
- return Promise.all(flushes).then(() => void 0);
259
- };
260
- const handleCommentary = (payload) => {
261
- const body = commentaryBody(payload);
262
- if (!body.trim()) return;
263
- const key = payload.itemId?.trim() || "turn";
264
- let segment = commentaryByItem.get(key);
265
- if (!segment) {
266
- segment = {
267
- body: "",
268
- dirty: false
269
- };
270
- commentaryByItem.set(key, segment);
271
- }
272
- if (body.length < segment.body.length || body === segment.body) return;
273
- segment.body = body;
274
- segment.dirty = true;
275
- if (!segment.timer) segment.timer = setTimeout(() => {
276
- segment.timer = void 0;
277
- flushCommentary(key);
278
- }, flushMs);
279
- };
280
- const toolRowKey = (payload) => {
281
- const toolCallId = payload.toolCallId?.trim();
282
- if (toolCallId) return toolCallId;
283
- return (payload.itemId?.trim() ?? "").replace(/^(tool|command):/, "");
284
- };
285
- const handleDiscreteItem = (payload) => {
286
- const body = activityBody(payload);
287
- if (!body) return;
288
- const kind = TOOL_ITEM_KINDS.has(payload.kind?.trim().toLowerCase() ?? "") ? "agent_tool" : "agent_commentary";
289
- flushAllCommentary();
290
- const key = `${kind}:${toolRowKey(payload)}`;
291
- const existing = toolRows.get(key);
292
- if (!existing || !toolRowKey(payload)) {
293
- const row = { body };
294
- if (toolRowKey(payload)) toolRows.set(key, row);
295
- enqueue(async () => {
296
- const posted = await postRow(kind, row.body);
297
- row.messageId = posted.id;
298
- row.sentBody = row.body;
299
- });
300
- return;
301
- }
302
- if (body.length <= existing.body.length) return;
303
- existing.body = body;
304
- enqueue(async () => {
305
- if (existing.messageId && existing.body !== existing.sentBody) {
306
- await params.client.updateMessageBody(existing.messageId, existing.body);
307
- existing.sentBody = existing.body;
308
- }
309
- });
310
- };
311
- return {
312
- onItemEvent: (payload) => {
313
- const kind = normalizedItemKind(payload);
314
- if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
315
- handleCommentary(payload);
316
- return;
317
- }
318
- if (SKIPPED_ITEM_KINDS.has(kind)) return;
319
- handleDiscreteItem(payload);
320
- },
321
- setProvenance: (next) => {
322
- provenance = next;
323
- },
324
- finalize: async () => {
325
- await flushAllCommentary();
326
- await chain;
327
- }
328
- };
329
- }
330
- //#endregion
331
- //#region extensions/clickclack/src/discussions/binding-generation.ts
332
- const DISCUSSION_GENERATIONS_NAMESPACE = "discussion-binding-generations";
333
- const MAX_PENDING_DISCUSSION_GENERATIONS = 1e4;
334
- const storesByRuntime$2 = /* @__PURE__ */ new WeakMap();
335
- function getGenerationStore(runtime) {
336
- const existing = storesByRuntime$2.get(runtime);
337
- if (existing) return existing;
338
- const created = runtime.state.openSyncKeyedStore({
339
- namespace: DISCUSSION_GENERATIONS_NAMESPACE,
340
- maxEntries: MAX_PENDING_DISCUSSION_GENERATIONS,
341
- overflowPolicy: "reject-new"
342
- });
343
- storesByRuntime$2.set(runtime, created);
344
- return created;
345
- }
346
- /** Reserves a generation so an interrupted channel create can be adopted on retry. */
347
- function reserveDiscussionBindingGeneration(params) {
348
- const store = getGenerationStore(params.runtime);
349
- const existing = store.lookup(params.sessionKey);
350
- if (existing?.destinationIdentity === params.destinationIdentity) return existing.generation;
351
- const generation = (params.createGeneration ?? randomUUID)();
352
- store.register(params.sessionKey, {
353
- destinationIdentity: params.destinationIdentity,
354
- generation
355
- });
356
- return generation;
357
- }
358
- /** Clears only the completed reservation; future opens must mint a new ownership ref. */
359
- function clearDiscussionBindingGeneration(params) {
360
- const store = getGenerationStore(params.runtime);
361
- const existing = store.lookup(params.sessionKey);
362
- if (!existing) return;
363
- if (params.expectedGeneration && existing.generation !== params.expectedGeneration) return;
364
- store.delete(params.sessionKey);
365
- }
366
- /** Quarantines a destination before the first fallible channel create. */
367
- function recordPendingDiscussionOpen(params) {
368
- const store = getGenerationStore(params.runtime);
369
- const existing = store.lookup(params.sessionKey);
370
- if (!existing || existing.generation !== params.generation) throw new Error("ClickClack discussion generation changed before channel creation");
371
- store.register(params.sessionKey, {
372
- ...existing,
373
- pending: params.pending
374
- });
375
- }
376
- function listPendingDiscussionOpens(runtime) {
377
- return getGenerationStore(runtime).entries().flatMap((entry) => entry.value.pending ? [{
378
- sessionKey: entry.key,
379
- generation: entry.value.generation,
380
- ...entry.value.pending
381
- }] : []);
382
- }
383
- function hasPendingDiscussionOpenForDestination(params) {
384
- const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
385
- return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
386
- }
387
- //#endregion
388
- //#region extensions/clickclack/src/discussions/naming.ts
389
- function shortSessionHash(sessionKey) {
390
- return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
391
- }
392
- function slugifyLabel(label) {
393
- return label.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80).replace(/-+$/gu, "");
394
- }
395
- function fallbackDiscussionLabel(sessionKey, agentId) {
396
- const agentSegment = agentId?.trim() ? slugifyLabel(agentId) : "";
397
- const hash = shortSessionHash(sessionKey).slice(0, 8);
398
- return `s-${agentSegment ? `${agentSegment}-` : ""}${hash}`;
399
- }
400
- function resolveDiscussionLabel(entry, sessionKey, agentId) {
401
- return entry.label?.trim() || entry.displayName?.trim() || entry.subject?.trim() || fallbackDiscussionLabel(sessionKey, agentId);
402
- }
403
- function truncateDiscussionDisplayTitle(label) {
404
- return Array.from(label).slice(0, 200).join("").trim();
405
- }
406
- function slugifyDiscussionLabel(label, sessionKey) {
407
- return slugifyLabel(label) || fallbackDiscussionLabel(sessionKey);
408
- }
409
- function discussionSessionPeerId(identity) {
410
- return `disc-${createHash("sha256").update([
411
- identity.mainSessionKey,
412
- identity.sessionId,
413
- identity.serverBaseUrl,
414
- identity.channelId,
415
- identity.externalRef
416
- ].join("\0")).digest("hex").slice(0, 32)}`;
417
- }
418
- function isDiscussionSessionKey(sessionKey) {
419
- return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
420
- }
421
- function discussionExternalRef(installationId, mainSessionKey, sessionId, destinationIdentity, bindingGeneration) {
422
- return `openclaw:${installationId}:${shortSessionHash([
423
- mainSessionKey,
424
- sessionId,
425
- destinationIdentity,
426
- bindingGeneration
427
- ].join("\0"))}`;
428
- }
429
- function discussionCredentialFingerprint(token) {
430
- return createHash("sha256").update(token).digest("hex");
431
- }
432
- function discussionSessionKey(params) {
433
- return params.runtime.channel.routing.buildAgentSessionKey({
434
- agentId: params.agentId,
435
- channel: "clickclack",
436
- accountId: params.accountId,
437
- peer: {
438
- kind: "channel",
439
- id: discussionSessionPeerId(params)
440
- }
441
- });
442
- }
443
- //#endregion
444
- //#region extensions/clickclack/src/discussions/binding-store.ts
445
- function bindingMatchesSessionIncarnation(runtime, sessionKey, binding) {
446
- const entry = runtime.agent.session.getSessionEntry({
447
- sessionKey,
448
- readConsistency: "latest"
449
- });
450
- return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId);
451
- }
452
- function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
453
- const entry = runtime.agent.session.getSessionEntry({
454
- sessionKey,
455
- readConsistency: "latest"
456
- });
457
- return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
458
- }
459
- const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
460
- const MAX_DISCUSSION_BINDINGS = 1e4;
461
- const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
462
- function channelKey(serverBaseUrl, channelId) {
463
- return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
464
- }
465
- /** SQLite-backed session/channel bindings with a process-local inbound lookup index. */
466
- var ClickClackDiscussionBindingStore = class {
467
- #store;
468
- #sessionByChannel = /* @__PURE__ */ new Map();
469
- #mainByDiscussionSession = /* @__PURE__ */ new Map();
470
- #runtime;
471
- constructor(runtime) {
472
- this.#runtime = runtime;
473
- this.#store = runtime.state.openSyncKeyedStore({
474
- namespace: DISCUSSION_BINDINGS_NAMESPACE,
475
- maxEntries: MAX_DISCUSSION_BINDINGS,
476
- overflowPolicy: "reject-new"
477
- });
478
- for (const entry of this.#store.entries()) this.#index(entry.key, entry.value);
479
- }
480
- get(sessionKey) {
481
- return this.#store.lookup(sessionKey);
482
- }
483
- hasCapacity(sessionKey) {
484
- return this.get(sessionKey) !== void 0 || this.#store.entries().length < MAX_DISCUSSION_BINDINGS;
485
- }
486
- getByChannel(serverBaseUrl, channelId) {
487
- const key = channelKey(serverBaseUrl, channelId);
488
- const sessionKey = this.#sessionByChannel.get(key);
489
- if (!sessionKey) return;
490
- const binding = this.get(sessionKey);
491
- if (!binding) {
492
- this.#sessionByChannel.delete(key);
166
+ get(sessionKey) {
167
+ return this.#store.lookup(sessionKey);
168
+ }
169
+ hasCapacity(sessionKey) {
170
+ return this.get(sessionKey) !== void 0 || this.#store.entries().length < MAX_DISCUSSION_BINDINGS;
171
+ }
172
+ getByChannel(serverBaseUrl, channelId) {
173
+ const key = channelKey(serverBaseUrl, channelId);
174
+ const sessionKey = this.#sessionByChannel.get(key);
175
+ if (!sessionKey) return;
176
+ const binding = this.get(sessionKey);
177
+ if (!binding) {
178
+ this.#sessionByChannel.delete(key);
493
179
  return;
494
180
  }
495
181
  return {
@@ -579,144 +265,662 @@ function resolveDiscussionBindingAccount(cfg, binding) {
579
265
  account
580
266
  };
581
267
  return {
582
- state: "active",
583
- account
268
+ state: "active",
269
+ account
270
+ };
271
+ }
272
+ /** Public embed/open URLs for one bound discussion channel. */
273
+ function discussionInfoForBinding(binding, account) {
274
+ const baseUrl = normalizedServerBaseUrl(account);
275
+ return {
276
+ state: "open",
277
+ embedUrl: `${baseUrl}/embed/channel/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}?openclawHostTheme=1`,
278
+ openUrl: `${baseUrl}/app/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`
279
+ };
280
+ }
281
+ //#endregion
282
+ //#region extensions/clickclack/src/discussions/revoked-channel-store.ts
283
+ const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
284
+ const MAX_REVOKED_CHANNELS = 1e5;
285
+ const storesByRuntime = /* @__PURE__ */ new WeakMap();
286
+ function revokedChannelKey(params) {
287
+ return [params.serverBaseUrl.replace(/\/+$/u, ""), params.channelId].join("\0");
288
+ }
289
+ function getStore(runtime) {
290
+ const existing = storesByRuntime.get(runtime);
291
+ if (existing) return existing;
292
+ const created = runtime.state.openSyncKeyedStore({
293
+ namespace: REVOKED_CHANNELS_NAMESPACE,
294
+ maxEntries: MAX_REVOKED_CHANNELS,
295
+ overflowPolicy: "reject-new"
296
+ });
297
+ storesByRuntime.set(runtime, created);
298
+ return created;
299
+ }
300
+ /** Records managed ownership before its live binding is released. */
301
+ function markClickClackDiscussionChannelRevoked(runtime, binding) {
302
+ const value = {
303
+ accountId: binding.accountId,
304
+ serverBaseUrl: binding.serverBaseUrl,
305
+ channelId: binding.channelId,
306
+ revokedAt: Date.now()
307
+ };
308
+ getStore(runtime).register(revokedChannelKey(value), value);
309
+ }
310
+ function markClickClackDiscussionChannelIdentityRevoked(params) {
311
+ const value = {
312
+ accountId: params.accountId,
313
+ serverBaseUrl: params.serverBaseUrl.replace(/\/+$/u, ""),
314
+ channelId: params.channelId,
315
+ revokedAt: Date.now()
316
+ };
317
+ getStore(params.runtime).register(revokedChannelKey(value), value);
318
+ }
319
+ function clearClickClackDiscussionChannelRevoked(params) {
320
+ getStore(params.runtime).delete(revokedChannelKey(params));
321
+ }
322
+ /** Distinguishes a released managed channel from a genuinely ordinary channel. */
323
+ function isClickClackDiscussionChannelRevoked(params) {
324
+ return Boolean(getStore(params.runtime).lookup(revokedChannelKey(params)));
325
+ }
326
+ //#endregion
327
+ //#region extensions/clickclack/src/discussions/routing.ts
328
+ function resolveClickClackDiscussionRoute(params) {
329
+ if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
330
+ const matched = getClickClackDiscussionBindingStore(params.runtime).getByChannel(params.serverBaseUrl, params.channelId);
331
+ if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
332
+ if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
333
+ if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
334
+ if (matched.binding.archived) return { state: "revoked" };
335
+ if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
336
+ if (!bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, matched.binding)) return { state: "revoked" };
337
+ const sessionKey = discussionSessionKey({
338
+ runtime: params.runtime,
339
+ agentId: matched.binding.agentId,
340
+ mainSessionKey: matched.sessionKey,
341
+ sessionId: matched.binding.sessionId,
342
+ accountId: params.accountId,
343
+ serverBaseUrl: matched.binding.serverBaseUrl,
344
+ channelId: matched.binding.channelId,
345
+ externalRef: matched.binding.externalRef
346
+ });
347
+ if (!sessionKey) return { state: "revoked" };
348
+ return {
349
+ state: "active",
350
+ route: {
351
+ agentId: matched.binding.agentId,
352
+ sessionKey,
353
+ systemPrompt: [
354
+ "You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
355
+ `The main session key is ${matched.sessionKey}.`,
356
+ "Observe it with sessions_history and session_status (using changesSince for incremental checks).",
357
+ "Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
358
+ "These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
359
+ ].join(" ")
360
+ }
361
+ };
362
+ }
363
+ //#endregion
364
+ //#region extensions/clickclack/src/group-policy.ts
365
+ /**
366
+ * Resolves the effective group policy for a ClickClack channel.
367
+ *
368
+ * Lookup order:
369
+ * 1. Exact channel ID in `groups`
370
+ * 2. Wildcard `'*'` entry in `groups`
371
+ * 3. Account-level `requireMention` / `mentionPatterns`
372
+ * 4. Backward-compatible default: { requireMention: false, mentionPatterns: [] }
373
+ */
374
+ function resolveClickClackGroupPolicy(params) {
375
+ const { account, channelId } = params;
376
+ const accountPolicy = {
377
+ requireMention: account.requireMention === true,
378
+ mentionPatterns: account.mentionPatterns ?? []
379
+ };
380
+ const wildcard = account.groups?.["*"];
381
+ const channelKey = channelId?.trim();
382
+ const exact = channelKey ? account.groups?.[channelKey] : void 0;
383
+ return {
384
+ requireMention: exact?.requireMention ?? wildcard?.requireMention ?? accountPolicy.requireMention,
385
+ mentionPatterns: exact?.mentionPatterns ?? wildcard?.mentionPatterns ?? accountPolicy.mentionPatterns
386
+ };
387
+ }
388
+ //#endregion
389
+ //#region extensions/clickclack/src/mention-facts.ts
390
+ /**
391
+ * Detects whether a ClickClack group message contains a direct mention of the
392
+ * current account.
393
+ *
394
+ * Pure helper – no side effects, no runtime imports.
395
+ */
396
+ const CLICKCLACK_MENTION_PATTERN = /(?:^|[^a-z0-9_@-])@([a-z0-9][a-z0-9_-]{1,31})(?![a-z0-9_-])/giu;
397
+ function buildLocalMentionRegexes(params) {
398
+ if (params.mentionPatterns.length === 0) return [];
399
+ const cfg = params.cfg;
400
+ return buildMentionRegexes({
401
+ ...cfg,
402
+ messages: {
403
+ ...cfg?.messages,
404
+ groupChat: {
405
+ ...cfg?.messages?.groupChat,
406
+ mentionPatterns: params.mentionPatterns
407
+ }
408
+ }
409
+ }, void 0, {
410
+ provider: "clickclack",
411
+ conversationId: params.channelId
412
+ });
413
+ }
414
+ function resolveMentionHandles(body) {
415
+ return [...body.matchAll(CLICKCLACK_MENTION_PATTERN)].map((match) => match[1]?.toLowerCase()).filter((handle) => Boolean(handle));
416
+ }
417
+ /**
418
+ * Builds mention facts for a ClickClack message.
419
+ *
420
+ * Rules:
421
+ * - DMs always have canDetectMention: false, wasMentioned: false
422
+ * (DMs bypass mention gating).
423
+ * - Group messages: canDetectMention: true when body text is available.
424
+ * - Checks the message body against shared and account-local mention patterns.
425
+ * - If botHandle is provided and the message body contains its ClickClack
426
+ * `@handle`, treat it as a mention.
427
+ * - Plain display names do not count unless explicitly configured as a pattern.
428
+ */
429
+ function resolveClickClackMentionFacts(params) {
430
+ const { isDirect, body, mentionPatterns, botHandle, cfg, agentId, channelId } = params;
431
+ if (isDirect) return {
432
+ canDetectMention: false,
433
+ wasMentioned: false
434
+ };
435
+ if (!body) return {
436
+ canDetectMention: true,
437
+ wasMentioned: false,
438
+ hasAnyMention: false
439
+ };
440
+ const sharedMentionRegexes = buildMentionRegexes(cfg, agentId, {
441
+ provider: "clickclack",
442
+ conversationId: channelId
443
+ });
444
+ const localMentionRegexes = buildLocalMentionRegexes({
445
+ cfg,
446
+ mentionPatterns,
447
+ channelId
448
+ });
449
+ const mentionRegexes = [...sharedMentionRegexes, ...localMentionRegexes];
450
+ const bodyForRegex = normalizeMentionText(body);
451
+ const hasConfiguredMention = mentionRegexes.some((regex) => regex.test(bodyForRegex));
452
+ const mentionHandles = resolveMentionHandles(body);
453
+ const normalizedBotHandle = botHandle?.replace(/^@/u, "").trim().toLowerCase();
454
+ return {
455
+ canDetectMention: true,
456
+ wasMentioned: (normalizedBotHandle ? mentionHandles.includes(normalizedBotHandle) : false) || hasConfiguredMention,
457
+ hasAnyMention: mentionHandles.length > 0 || hasConfiguredMention
458
+ };
459
+ }
460
+ //#endregion
461
+ //#region extensions/clickclack/src/runtime.ts
462
+ /**
463
+ * Runtime store for host-provided OpenClaw services used by the ClickClack
464
+ * bundled plugin.
465
+ */
466
+ const { setRuntime: setClickClackRuntime, getRuntime: getClickClackRuntime } = createPluginRuntimeStore({
467
+ pluginId: "clickclack",
468
+ errorMessage: "ClickClack runtime not initialized"
469
+ });
470
+ //#endregion
471
+ //#region extensions/clickclack/src/target.ts
472
+ /**
473
+ * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
474
+ */
475
+ function parseClickClackTarget(raw) {
476
+ const value = raw.trim();
477
+ if (!value) throw new Error("ClickClack target is required");
478
+ const [prefix, ...rest] = value.split(":");
479
+ const body = rest.join(":").trim();
480
+ if (prefix === "channel" && body) return {
481
+ chatType: "group",
482
+ kind: "channel",
483
+ id: body
484
+ };
485
+ if (prefix === "thread" && body) return {
486
+ chatType: "group",
487
+ kind: "thread",
488
+ id: body
489
+ };
490
+ if (prefix === "dm" && body) return {
491
+ chatType: "direct",
492
+ kind: "dm",
493
+ id: body
494
+ };
495
+ if (!body) return {
496
+ chatType: "group",
497
+ kind: "channel",
498
+ id: value
499
+ };
500
+ throw new Error(`Unsupported ClickClack target: ${raw}`);
501
+ }
502
+ /** Formats a parsed ClickClack target back into canonical target syntax. */
503
+ function buildClickClackTarget(target) {
504
+ return `${target.kind}:${target.id}`;
505
+ }
506
+ /** Normalizes user-entered ClickClack target text for channel routing. */
507
+ function normalizeClickClackTarget(raw) {
508
+ return buildClickClackTarget(parseClickClackTarget(raw));
509
+ }
510
+ /** Reports whether a target string can be offered to the ClickClack parser. */
511
+ function looksLikeClickClackTarget(raw) {
512
+ return /^(channel|thread|dm):/i.test(raw.trim()) || raw.trim().length > 0;
513
+ }
514
+ //#endregion
515
+ //#region extensions/clickclack/src/access.ts
516
+ /**
517
+ * Maps ClickClack senders and conversations onto the shared channel ingress
518
+ * allowlist/command authorization contract.
519
+ */
520
+ const CHANNEL_ID$2 = "clickclack";
521
+ function normalizeClickClackUserId(value) {
522
+ const trimmed = value.trim();
523
+ if (!trimmed) return null;
524
+ const withoutProvider = trimmed.replace(/^(clickclack|cc):/i, "").trim();
525
+ return withoutProvider.match(/^dm:(.+)$/i)?.[1]?.trim() || withoutProvider || null;
526
+ }
527
+ const clickClackIngressIdentity = {
528
+ key: "user-id",
529
+ normalizeEntry: normalizeClickClackUserId,
530
+ normalizeSubject: normalizeClickClackUserId,
531
+ isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
532
+ entryIdPrefix: "clickclack-user"
533
+ };
534
+ function resolveAccountAgentRoute(params) {
535
+ const runtime = getClickClackRuntime();
536
+ const peer = {
537
+ kind: params.isDirect ? "direct" : "channel",
538
+ id: params.target
539
+ };
540
+ const route = runtime.channel.routing.resolveAgentRoute({
541
+ cfg: params.cfg,
542
+ channel: CHANNEL_ID$2,
543
+ accountId: params.account.accountId,
544
+ peer
545
+ });
546
+ const agentId = normalizeAgentId(params.account.agentId ?? route.agentId);
547
+ if (agentId === route.agentId) return route;
548
+ const dmScope = params.cfg.session?.dmScope ?? "main";
549
+ const sessionKey = runtime.channel.routing.buildAgentSessionKey({
550
+ agentId,
551
+ mainKey: params.cfg.session?.mainKey,
552
+ channel: CHANNEL_ID$2,
553
+ accountId: params.account.accountId,
554
+ peer,
555
+ dmScope,
556
+ identityLinks: params.cfg.session?.identityLinks
557
+ });
558
+ const mainSessionKey = runtime.channel.routing.buildAgentSessionKey({
559
+ agentId,
560
+ mainKey: params.cfg.session?.mainKey,
561
+ channel: CHANNEL_ID$2,
562
+ accountId: params.account.accountId,
563
+ dmScope: "main"
564
+ });
565
+ return {
566
+ ...route,
567
+ agentId,
568
+ dmScope,
569
+ sessionKey,
570
+ mainSessionKey,
571
+ lastRoutePolicy: sessionKey === mainSessionKey ? "main" : "session"
584
572
  };
585
573
  }
586
- /** Public embed/open URLs for one bound discussion channel. */
587
- function discussionInfoForBinding(binding, account) {
588
- const baseUrl = normalizedServerBaseUrl(account);
574
+ function resolvePreparedInboundRoute(params) {
575
+ const runtime = getClickClackRuntime();
576
+ const isDirect = Boolean(params.message.direct_conversation_id);
577
+ const target = buildClickClackTarget(isDirect ? {
578
+ chatType: "direct",
579
+ kind: "dm",
580
+ id: params.message.author_id
581
+ } : {
582
+ chatType: "group",
583
+ kind: "channel",
584
+ id: params.message.channel_id ?? ""
585
+ });
586
+ const accountRoute = resolveAccountAgentRoute({
587
+ cfg: params.config,
588
+ account: params.account,
589
+ target,
590
+ isDirect
591
+ });
592
+ const discussionResolution = !isDirect && params.message.channel_id ? resolveClickClackDiscussionRoute({
593
+ runtime,
594
+ config: params.config,
595
+ accountId: params.account.accountId,
596
+ serverBaseUrl: params.account.baseUrl,
597
+ workspaceId: params.message.workspace_id,
598
+ channelId: params.message.channel_id
599
+ }) : { state: "unbound" };
600
+ const discussionRoute = discussionResolution.state === "active" ? discussionResolution.route : void 0;
589
601
  return {
590
- state: "open",
591
- embedUrl: `${baseUrl}/embed/channel/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}?openclawHostTheme=1`,
592
- openUrl: `${baseUrl}/app/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`
602
+ isDirect,
603
+ target,
604
+ route: discussionRoute ? {
605
+ ...accountRoute,
606
+ agentId: discussionRoute.agentId,
607
+ sessionKey: discussionRoute.sessionKey,
608
+ lastRoutePolicy: "session"
609
+ } : accountRoute,
610
+ discussionRoute,
611
+ revoked: discussionResolution.state === "revoked"
612
+ };
613
+ }
614
+ /**
615
+ * Resolves whether a ClickClack message should enter the agent pipeline and
616
+ * whether its command-style body may run tools.
617
+ */
618
+ async function resolveClickClackInboundAccess(params) {
619
+ const runtime = getClickClackRuntime();
620
+ const cfg = params.config;
621
+ const preparedRoute = resolvePreparedInboundRoute(params);
622
+ const shouldCheckCommand = runtime.channel.commands.shouldComputeCommandAuthorized(params.message.body, cfg);
623
+ const effectiveGroupPolicy = resolveClickClackGroupPolicy({
624
+ account: params.account,
625
+ channelId: params.message.channel_id
626
+ });
627
+ const mentionFacts = resolveClickClackMentionFacts({
628
+ isDirect: preparedRoute.isDirect,
629
+ body: params.message.body,
630
+ mentionPatterns: effectiveGroupPolicy.mentionPatterns,
631
+ botHandle: params.account.botHandle,
632
+ cfg,
633
+ agentId: preparedRoute.route.agentId,
634
+ channelId: params.message.channel_id
635
+ });
636
+ const allowTextCommands = params.account.replyMode === "agent" && runtime.channel.commands.shouldHandleTextCommands({
637
+ cfg,
638
+ surface: CHANNEL_ID$2,
639
+ commandSource: "text"
640
+ });
641
+ const resolved = await resolveStableChannelMessageIngress({
642
+ channelId: CHANNEL_ID$2,
643
+ accountId: params.account.accountId,
644
+ identity: clickClackIngressIdentity,
645
+ cfg,
646
+ subject: { stableId: params.message.author_id },
647
+ conversation: {
648
+ kind: preparedRoute.isDirect ? "direct" : "group",
649
+ id: preparedRoute.isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
650
+ },
651
+ allowFrom: params.account.allowFrom,
652
+ dmPolicy: "allowlist",
653
+ groupPolicy: "allowlist",
654
+ mentionFacts,
655
+ policy: { activation: {
656
+ requireMention: effectiveGroupPolicy.requireMention,
657
+ allowTextCommands
658
+ } },
659
+ command: shouldCheckCommand ? {
660
+ cfg,
661
+ modeWhenAccessGroupsOff: "configured"
662
+ } : false
663
+ });
664
+ return {
665
+ shouldDispatch: !preparedRoute.revoked && resolved.ingress.admission === "dispatch",
666
+ commandAuthorized: resolved.commandAccess.requested ? resolved.commandAccess.authorized : resolved.senderAccess.allowed,
667
+ requireMention: effectiveGroupPolicy.requireMention,
668
+ mentionFacts,
669
+ preparedRoute
593
670
  };
594
671
  }
595
672
  //#endregion
596
- //#region extensions/clickclack/src/discussions/revoked-channel-store.ts
597
- const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
598
- const MAX_REVOKED_CHANNELS = 1e5;
599
- const storesByRuntime = /* @__PURE__ */ new WeakMap();
600
- function revokedChannelKey(params) {
601
- return [params.serverBaseUrl.replace(/\/+$/u, ""), params.channelId].join("\0");
673
+ //#region extensions/clickclack/src/command-menu.ts
674
+ const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
675
+ const CLICKCLACK_MAX_COMMANDS = 100;
676
+ const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
677
+ const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
678
+ function truncateCodePoints(value, maxLength) {
679
+ return Array.from(value).slice(0, maxLength).join("");
602
680
  }
603
- function getStore(runtime) {
604
- const existing = storesByRuntime.get(runtime);
605
- if (existing) return existing;
606
- const created = runtime.state.openSyncKeyedStore({
607
- namespace: REVOKED_CHANNELS_NAMESPACE,
608
- maxEntries: MAX_REVOKED_CHANNELS,
609
- overflowPolicy: "reject-new"
610
- });
611
- storesByRuntime.set(runtime, created);
612
- return created;
681
+ function commandArgsHint(spec) {
682
+ if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
683
+ return spec.acceptsArgs ? "[args]" : "";
613
684
  }
614
- /** Records managed ownership before its live binding is released. */
615
- function markClickClackDiscussionChannelRevoked(runtime, binding) {
616
- const value = {
617
- accountId: binding.accountId,
618
- serverBaseUrl: binding.serverBaseUrl,
619
- channelId: binding.channelId,
620
- revokedAt: Date.now()
621
- };
622
- getStore(runtime).register(revokedChannelKey(value), value);
685
+ function errorStatus(error) {
686
+ if (!error || typeof error !== "object" || !("status" in error)) return;
687
+ const status = error.status;
688
+ return typeof status === "number" ? status : void 0;
623
689
  }
624
- function markClickClackDiscussionChannelIdentityRevoked(params) {
625
- const value = {
626
- accountId: params.accountId,
627
- serverBaseUrl: params.serverBaseUrl.replace(/\/+$/u, ""),
628
- channelId: params.channelId,
629
- revokedAt: Date.now()
630
- };
631
- getStore(params.runtime).register(revokedChannelKey(value), value);
690
+ function mapNativeCommandSpecsToClickClackMenu(specs, log) {
691
+ const commands = [];
692
+ const seen = /* @__PURE__ */ new Set();
693
+ const invalidNames = /* @__PURE__ */ new Set();
694
+ for (const spec of specs) {
695
+ if (spec.isAlias) continue;
696
+ const command = spec.name.trim().toLowerCase();
697
+ if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
698
+ invalidNames.add(spec.name.trim() || "<empty>");
699
+ continue;
700
+ }
701
+ if (seen.has(command)) continue;
702
+ seen.add(command);
703
+ const description = spec.description.trim() || command;
704
+ commands.push({
705
+ command,
706
+ description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
707
+ args_hint: commandArgsHint(spec)
708
+ });
709
+ }
710
+ if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
711
+ return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
632
712
  }
633
- function clearClickClackDiscussionChannelRevoked(params) {
634
- getStore(params.runtime).delete(revokedChannelKey(params));
713
+ async function syncClickClackCommandMenu(params) {
714
+ try {
715
+ const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
716
+ await params.client.setBotCommands(commands);
717
+ } catch (error) {
718
+ const status = errorStatus(error);
719
+ if (status === 403) {
720
+ params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
721
+ return;
722
+ }
723
+ if (status === 404) {
724
+ params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
725
+ return;
726
+ }
727
+ params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
728
+ }
635
729
  }
636
- /** Distinguishes a released managed channel from a genuinely ordinary channel. */
637
- function isClickClackDiscussionChannelRevoked(params) {
638
- return Boolean(getStore(params.runtime).lookup(revokedChannelKey(params)));
730
+ //#endregion
731
+ //#region extensions/clickclack/src/activity.ts
732
+ /**
733
+ * Publishes agent activity (streamed commentary + tool progress) into
734
+ * ClickClack as durable `agent_commentary` / `agent_tool` message rows,
735
+ * coalesced so one logical step becomes one row instead of a row per frame.
736
+ *
737
+ * Ported from the clickglass agent-bridge sidecar, adapted from gateway
738
+ * websocket frames to the in-process `replyOptions.onItemEvent` seam:
739
+ *
740
+ * - Commentary/reasoning arrives as cumulative text snapshots per item id.
741
+ * Each segment becomes one durable row: POSTed when the segment starts
742
+ * streaming and PATCHed (debounced) as the snapshot grows, so prose
743
+ * interleaves chronologically with tool rows.
744
+ * - Tool/step items can emit several frames (start/update/complete) for the
745
+ * same call, sometimes with lane-prefixed ids (`tool:X`, `command:X`).
746
+ * Normalize the prefix away to one key per call, POST one row on the first
747
+ * frame, and PATCH it when a later frame carries a strictly longer body.
748
+ */
749
+ /** Debounce window for PATCHing streaming commentary snapshots. */
750
+ const CLICKCLACK_COMMENTARY_FLUSH_MS = 700;
751
+ /** Item kinds rendered as agent_tool rows; everything else is commentary. */
752
+ const TOOL_ITEM_KINDS = /* @__PURE__ */ new Set([
753
+ "tool",
754
+ "command",
755
+ "command_output",
756
+ "patch",
757
+ "search",
758
+ "api"
759
+ ]);
760
+ /** Item kinds that never become durable rows (ephemeral or plumbing lanes). */
761
+ const SKIPPED_ITEM_KINDS = /* @__PURE__ */ new Set(["lifecycle"]);
762
+ /** Item kinds emitted as cumulative commentary snapshots. */
763
+ const STREAMING_COMMENTARY_ITEM_KINDS = /* @__PURE__ */ new Set([
764
+ "preamble",
765
+ "commentary",
766
+ "analysis",
767
+ "thinking",
768
+ "reasoning"
769
+ ]);
770
+ /** Provider-specific reasoning lanes normalized into one ClickClack label. */
771
+ const THINKING_ITEM_KINDS = /* @__PURE__ */ new Set([
772
+ "analysis",
773
+ "thinking",
774
+ "reasoning"
775
+ ]);
776
+ function normalizedItemKind(payload) {
777
+ return payload.kind?.trim().toLowerCase() ?? "";
778
+ }
779
+ function commentaryBody(payload) {
780
+ const text = payload.progressText?.trim() || payload.summary?.trim() || payload.meta?.trim() || "";
781
+ if (!text) return "";
782
+ const kind = normalizedItemKind(payload);
783
+ if (THINKING_ITEM_KINDS.has(kind)) return `**Thinking**\n\n${text}`;
784
+ return text;
785
+ }
786
+ function activityBody(payload) {
787
+ const line = buildChannelProgressDraftLine({
788
+ event: "item",
789
+ itemId: payload.itemId,
790
+ toolCallId: payload.toolCallId,
791
+ itemKind: payload.kind,
792
+ title: payload.title,
793
+ name: payload.name,
794
+ phase: payload.phase,
795
+ status: payload.status,
796
+ summary: payload.summary,
797
+ progressText: payload.progressText,
798
+ meta: payload.meta
799
+ })?.text?.trim();
800
+ if (line) return line;
801
+ const head = payload.name?.trim() || payload.title?.trim();
802
+ const text = payload.progressText?.trim() || payload.summary?.trim();
803
+ if (head && text) return `**${head}**\n\n${text}`;
804
+ if (text) return text;
805
+ if (head) return head;
806
+ return payload.status?.trim() || payload.kind?.trim() || "";
639
807
  }
640
- //#endregion
641
- //#region extensions/clickclack/src/discussions/routing.ts
642
- function resolveClickClackDiscussionRoute(params) {
643
- if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
644
- const matched = getClickClackDiscussionBindingStore(params.runtime).getByChannel(params.serverBaseUrl, params.channelId);
645
- if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
646
- if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
647
- if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
648
- if (matched.binding.archived) return { state: "revoked" };
649
- if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
650
- if (!bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, matched.binding)) return { state: "revoked" };
651
- const sessionKey = discussionSessionKey({
652
- runtime: params.runtime,
653
- agentId: matched.binding.agentId,
654
- mainSessionKey: matched.sessionKey,
655
- sessionId: matched.binding.sessionId,
656
- accountId: params.accountId,
657
- serverBaseUrl: matched.binding.serverBaseUrl,
658
- channelId: matched.binding.channelId,
659
- externalRef: matched.binding.externalRef
808
+ /**
809
+ * Creates a per-turn activity publisher. Publishing is best-effort: transport
810
+ * failures are reported through `onError` and never interrupt the reply turn.
811
+ */
812
+ function createClickClackActivityPublisher(params) {
813
+ const flushMs = params.flushMs ?? CLICKCLACK_COMMENTARY_FLUSH_MS;
814
+ const commentaryByItem = /* @__PURE__ */ new Map();
815
+ const toolRows = /* @__PURE__ */ new Map();
816
+ let provenance;
817
+ let chain = Promise.resolve();
818
+ const enqueue = (work) => {
819
+ chain = chain.then(work).catch((error) => {
820
+ params.onError?.(error);
821
+ });
822
+ return chain;
823
+ };
824
+ const postRow = (kind, body) => params.client.createActivityMessage({
825
+ channelId: params.target.channelId,
826
+ conversationId: params.target.conversationId,
827
+ body,
828
+ kind,
829
+ turnId: params.turnId,
830
+ provenance
660
831
  });
661
- if (!sessionKey) return { state: "revoked" };
662
- return {
663
- state: "active",
664
- route: {
665
- agentId: matched.binding.agentId,
666
- sessionKey,
667
- systemPrompt: [
668
- "You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
669
- `The main session key is ${matched.sessionKey}.`,
670
- "Observe it with sessions_history and session_status (using changesSince for incremental checks).",
671
- "Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
672
- "These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
673
- ].join(" ")
832
+ const flushCommentary = (segmentKey) => {
833
+ const segment = commentaryByItem.get(segmentKey);
834
+ if (!segment) return Promise.resolve();
835
+ if (segment.timer) {
836
+ clearTimeout(segment.timer);
837
+ segment.timer = void 0;
674
838
  }
839
+ if (!segment.dirty || !segment.body.trim()) return Promise.resolve();
840
+ segment.dirty = false;
841
+ const body = segment.body;
842
+ return enqueue(async () => {
843
+ if (segment.messageId) {
844
+ await params.client.updateMessageBody(segment.messageId, body);
845
+ return;
846
+ }
847
+ const posted = await postRow("agent_commentary", body);
848
+ segment.messageId = posted.id;
849
+ });
675
850
  };
676
- }
677
- //#endregion
678
- //#region extensions/clickclack/src/target.ts
679
- /**
680
- * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
681
- */
682
- function parseClickClackTarget(raw) {
683
- const value = raw.trim();
684
- if (!value) throw new Error("ClickClack target is required");
685
- const [prefix, ...rest] = value.split(":");
686
- const body = rest.join(":").trim();
687
- if (prefix === "channel" && body) return {
688
- chatType: "group",
689
- kind: "channel",
690
- id: body
851
+ const flushAllCommentary = () => {
852
+ const flushes = [...commentaryByItem.keys()].map((key) => flushCommentary(key));
853
+ return Promise.all(flushes).then(() => void 0);
691
854
  };
692
- if (prefix === "thread" && body) return {
693
- chatType: "group",
694
- kind: "thread",
695
- id: body
855
+ const handleCommentary = (payload) => {
856
+ const body = commentaryBody(payload);
857
+ if (!body.trim()) return;
858
+ const key = payload.itemId?.trim() || "turn";
859
+ let segment = commentaryByItem.get(key);
860
+ if (!segment) {
861
+ segment = {
862
+ body: "",
863
+ dirty: false
864
+ };
865
+ commentaryByItem.set(key, segment);
866
+ }
867
+ if (body.length < segment.body.length || body === segment.body) return;
868
+ segment.body = body;
869
+ segment.dirty = true;
870
+ if (!segment.timer) segment.timer = setTimeout(() => {
871
+ segment.timer = void 0;
872
+ flushCommentary(key);
873
+ }, flushMs);
696
874
  };
697
- if (prefix === "dm" && body) return {
698
- chatType: "direct",
699
- kind: "dm",
700
- id: body
875
+ const toolRowKey = (payload) => {
876
+ const toolCallId = payload.toolCallId?.trim();
877
+ if (toolCallId) return toolCallId;
878
+ return (payload.itemId?.trim() ?? "").replace(/^(tool|command):/, "");
701
879
  };
702
- if (!body) return {
703
- chatType: "group",
704
- kind: "channel",
705
- id: value
880
+ const handleDiscreteItem = (payload) => {
881
+ const body = activityBody(payload);
882
+ if (!body) return;
883
+ const kind = TOOL_ITEM_KINDS.has(payload.kind?.trim().toLowerCase() ?? "") ? "agent_tool" : "agent_commentary";
884
+ flushAllCommentary();
885
+ const key = `${kind}:${toolRowKey(payload)}`;
886
+ const existing = toolRows.get(key);
887
+ if (!existing || !toolRowKey(payload)) {
888
+ const row = { body };
889
+ if (toolRowKey(payload)) toolRows.set(key, row);
890
+ enqueue(async () => {
891
+ const posted = await postRow(kind, row.body);
892
+ row.messageId = posted.id;
893
+ row.sentBody = row.body;
894
+ });
895
+ return;
896
+ }
897
+ if (body.length <= existing.body.length) return;
898
+ existing.body = body;
899
+ enqueue(async () => {
900
+ if (existing.messageId && existing.body !== existing.sentBody) {
901
+ await params.client.updateMessageBody(existing.messageId, existing.body);
902
+ existing.sentBody = existing.body;
903
+ }
904
+ });
905
+ };
906
+ return {
907
+ onItemEvent: (payload) => {
908
+ const kind = normalizedItemKind(payload);
909
+ if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
910
+ handleCommentary(payload);
911
+ return;
912
+ }
913
+ if (SKIPPED_ITEM_KINDS.has(kind)) return;
914
+ handleDiscreteItem(payload);
915
+ },
916
+ setProvenance: (next) => {
917
+ provenance = next;
918
+ },
919
+ finalize: async () => {
920
+ await flushAllCommentary();
921
+ await chain;
922
+ }
706
923
  };
707
- throw new Error(`Unsupported ClickClack target: ${raw}`);
708
- }
709
- /** Formats a parsed ClickClack target back into canonical target syntax. */
710
- function buildClickClackTarget(target) {
711
- return `${target.kind}:${target.id}`;
712
- }
713
- /** Normalizes user-entered ClickClack target text for channel routing. */
714
- function normalizeClickClackTarget(raw) {
715
- return buildClickClackTarget(parseClickClackTarget(raw));
716
- }
717
- /** Reports whether a target string can be offered to the ClickClack parser. */
718
- function looksLikeClickClackTarget(raw) {
719
- return /^(channel|thread|dm):/i.test(raw.trim()) || raw.trim().length > 0;
720
924
  }
721
925
  //#endregion
722
926
  //#region extensions/clickclack/src/outbound.ts
@@ -1020,49 +1224,6 @@ function hasClickClackReplyMedia(payload) {
1020
1224
  function resolveClickClackAgentRunId(messageId) {
1021
1225
  return CLICKCLACK_MESSAGE_ID_PATTERN.test(messageId) ? `${CHANNEL_ID$1}:${messageId}` : void 0;
1022
1226
  }
1023
- function resolveAccountAgentRoute(params) {
1024
- const runtime = getClickClackRuntime();
1025
- const route = runtime.channel.routing.resolveAgentRoute({
1026
- cfg: params.cfg,
1027
- channel: CHANNEL_ID$1,
1028
- accountId: params.account.accountId,
1029
- peer: {
1030
- kind: params.isDirect ? "direct" : "channel",
1031
- id: params.target
1032
- }
1033
- });
1034
- const agentId = normalizeAgentId(params.account.agentId ?? route.agentId);
1035
- if (agentId === route.agentId) return route;
1036
- const peer = {
1037
- kind: params.isDirect ? "direct" : "channel",
1038
- id: params.target
1039
- };
1040
- const dmScope = params.cfg.session?.dmScope ?? "main";
1041
- const sessionKey = runtime.channel.routing.buildAgentSessionKey({
1042
- agentId,
1043
- mainKey: params.cfg.session?.mainKey,
1044
- channel: CHANNEL_ID$1,
1045
- accountId: params.account.accountId,
1046
- peer,
1047
- dmScope,
1048
- identityLinks: params.cfg.session?.identityLinks
1049
- });
1050
- const mainSessionKey = runtime.channel.routing.buildAgentSessionKey({
1051
- agentId,
1052
- mainKey: params.cfg.session?.mainKey,
1053
- channel: CHANNEL_ID$1,
1054
- accountId: params.account.accountId,
1055
- dmScope: "main"
1056
- });
1057
- return {
1058
- ...route,
1059
- agentId,
1060
- dmScope,
1061
- sessionKey,
1062
- mainSessionKey,
1063
- lastRoutePolicy: sessionKey === mainSessionKey ? "main" : "session"
1064
- };
1065
- }
1066
1227
  async function dispatchModelReply(params) {
1067
1228
  const runtime = getClickClackRuntime();
1068
1229
  const text = (await runtime.llm.complete({
@@ -1107,38 +1268,7 @@ async function handleClickClackInbound(params) {
1107
1268
  if (!access.shouldDispatch) return;
1108
1269
  const conversationId = message.channel_id || message.direct_conversation_id;
1109
1270
  if (!conversationId) return;
1110
- const isDirect = Boolean(message.direct_conversation_id);
1111
- const target = buildClickClackTarget(isDirect ? {
1112
- chatType: "direct",
1113
- kind: "dm",
1114
- id: message.author_id
1115
- } : {
1116
- chatType: "group",
1117
- kind: "channel",
1118
- id: message.channel_id ?? ""
1119
- });
1120
- const accountRoute = resolveAccountAgentRoute({
1121
- cfg: params.config,
1122
- account: params.account,
1123
- target,
1124
- isDirect
1125
- });
1126
- const discussionResolution = !isDirect && message.channel_id ? resolveClickClackDiscussionRoute({
1127
- runtime,
1128
- config: params.config,
1129
- accountId: params.account.accountId,
1130
- serverBaseUrl: params.account.baseUrl,
1131
- workspaceId: message.workspace_id,
1132
- channelId: message.channel_id
1133
- }) : { state: "unbound" };
1134
- if (discussionResolution.state === "revoked") return;
1135
- const discussionRoute = discussionResolution.state === "active" ? discussionResolution.route : void 0;
1136
- const route = discussionRoute ? {
1137
- ...accountRoute,
1138
- agentId: discussionRoute.agentId,
1139
- sessionKey: discussionRoute.sessionKey,
1140
- lastRoutePolicy: "session"
1141
- } : accountRoute;
1271
+ const { discussionRoute, isDirect, route, target } = access.preparedRoute;
1142
1272
  if (params.account.replyMode === "model" && !discussionRoute) {
1143
1273
  await dispatchModelReply({
1144
1274
  account: params.account,
@@ -1216,10 +1346,7 @@ async function handleClickClackInbound(params) {
1216
1346
  },
1217
1347
  access: {
1218
1348
  commands: { authorized: access.commandAuthorized },
1219
- mentions: {
1220
- canDetectMention: !isDirect,
1221
- wasMentioned: !isDirect
1222
- }
1349
+ mentions: access.mentionFacts
1223
1350
  },
1224
1351
  extra: {
1225
1352
  GroupChannel: message.channel_id,
@@ -1356,7 +1483,10 @@ async function processEvent(params) {
1356
1483
  config: params.config,
1357
1484
  message
1358
1485
  });
1359
- if (!access.shouldDispatch) return;
1486
+ if (!access.shouldDispatch) {
1487
+ params.log?.info(`[${params.account.accountId}] skipped ClickClack message before agent dispatch: kind=${message.direct_conversation_id ? "dm" : "group"} requireMention=${access.requireMention ?? "unknown"} wasMentioned=${access.mentionFacts.wasMentioned} hasAnyMention=${access.mentionFacts.hasAnyMention ?? "unknown"} commandAuthorized=${access.commandAuthorized}`);
1488
+ return;
1489
+ }
1360
1490
  await handleClickClackInbound({
1361
1491
  account: params.account,
1362
1492
  config: params.config,
@@ -1397,14 +1527,16 @@ async function startClickClackGatewayAccount(ctx) {
1397
1527
  const account = {
1398
1528
  ...configuredAccount,
1399
1529
  workspace: workspaceId,
1400
- botUserId: configuredAccount.botUserId ?? me.id
1530
+ botUserId: configuredAccount.botUserId ?? me.id,
1531
+ botHandle: me.handle
1401
1532
  };
1402
1533
  const processIncomingEvent = (event) => processEvent({
1403
1534
  account,
1404
1535
  config: ctx.cfg,
1405
1536
  client,
1406
1537
  event,
1407
- botUserId: account.botUserId
1538
+ botUserId: account.botUserId,
1539
+ log: ctx.log
1408
1540
  });
1409
1541
  if (account.commandMenu) await syncClickClackCommandMenu({
1410
1542
  cfg: ctx.cfg,
@@ -1717,4 +1849,4 @@ const clickClackPlugin = createChatChannelPlugin({
1717
1849
  }
1718
1850
  });
1719
1851
  //#endregion
1720
- export { listPendingDiscussionOpens as C, setClickClackRuntime as D, getClickClackRuntime as E, clearDiscussionBindingGeneration as S, reserveDiscussionBindingGeneration as T, fallbackDiscussionLabel as _, isClickClackDiscussionChannelRevoked as a, slugifyDiscussionLabel as b, discussionAccounts as c, resolveDiscussionBindingAccount as d, bindingMatchesActiveSessionIncarnation as f, discussionExternalRef as g, discussionCredentialFingerprint as h, clearClickClackDiscussionChannelRevoked as i, discussionInfoForBinding as l, getClickClackDiscussionBindingStore as m, buildClickClackTarget as n, markClickClackDiscussionChannelIdentityRevoked as o, bindingMatchesSessionIncarnation as p, parseClickClackTarget as r, markClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, normalizedServerBaseUrl as u, isDiscussionSessionKey as v, recordPendingDiscussionOpen as w, truncateDiscussionDisplayTitle as x, resolveDiscussionLabel as y };
1852
+ export { truncateDiscussionDisplayTitle as C, reserveDiscussionBindingGeneration as D, recordPendingDiscussionOpen as E, slugifyDiscussionLabel as S, listPendingDiscussionOpens as T, discussionCredentialFingerprint as _, setClickClackRuntime as a, isDiscussionSessionKey as b, markClickClackDiscussionChannelIdentityRevoked as c, discussionInfoForBinding as d, normalizedServerBaseUrl as f, getClickClackDiscussionBindingStore as g, bindingMatchesSessionIncarnation as h, getClickClackRuntime as i, markClickClackDiscussionChannelRevoked as l, bindingMatchesActiveSessionIncarnation as m, buildClickClackTarget as n, clearClickClackDiscussionChannelRevoked as o, resolveDiscussionBindingAccount as p, parseClickClackTarget as r, isClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, discussionAccounts as u, discussionExternalRef as v, clearDiscussionBindingGeneration as w, resolveDiscussionLabel as x, fallbackDiscussionLabel as y };