@openclaw/clickclack 2026.7.2-beta.7 → 2026.8.1-beta.3

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,17 +1,23 @@
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";
1
+ import { _ as DEFAULT_ACCOUNT_ID, b as resolveClickClackAccount, c as resolveWorkspaceId, f as normalizeClickClackCorrelationId, g as clickClackMeta, h as clickClackConfigAdapter, l as ClickClackHttpError, m as CLICKCLACK_CHANNEL_ID, n as clickClackSetupContract, p as clickClackConfigSchema, s as resolveChannelId, t as clickClackSetupWizard, u as createClickClackClient, y as listEnabledClickClackAccounts } from "./setup-surface-BQ1Zyohu.js";
2
2
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
3
+ import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
4
+ import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
5
+ import { readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
3
6
  import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
4
7
  import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
5
8
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
6
9
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
10
+ import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
7
11
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
12
+ import { rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
8
13
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
9
14
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
10
15
  import { createHash, randomUUID } from "node:crypto";
11
16
  import { buildMentionRegexes, normalizeMentionText } from "openclaw/plugin-sdk/channel-mention-gating";
12
17
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
13
18
  import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
14
- import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
19
+ import { buildChannelInboundEventContext, createChannelInboundEnvelopeBuilder, recordChannelBotPairLoopAndCheckSuppression } from "openclaw/plugin-sdk/channel-inbound";
20
+ import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
15
21
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
16
22
  import { FormatCapabilityProfile, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
17
23
  //#region extensions/clickclack/src/discussions/binding-generation.ts
@@ -33,9 +39,20 @@ function getGenerationStore(runtime) {
33
39
  function reserveDiscussionBindingGeneration(params) {
34
40
  const store = getGenerationStore(params.runtime);
35
41
  const existing = store.lookup(params.sessionKey);
36
- if (existing?.destinationIdentity === params.destinationIdentity) return existing.generation;
42
+ const existingAccountId = existing?.accountId ?? existing?.pending?.accountId;
43
+ const existingCredentialFingerprint = existing?.credentialFingerprint ?? existing?.pending?.credentialFingerprint;
44
+ if (existing?.destinationIdentity === params.destinationIdentity && existingAccountId === params.accountId && existingCredentialFingerprint === params.credentialFingerprint) {
45
+ if (!existing.accountId || !existing.credentialFingerprint) store.register(params.sessionKey, {
46
+ ...existing,
47
+ accountId: params.accountId,
48
+ credentialFingerprint: params.credentialFingerprint
49
+ });
50
+ return existing.generation;
51
+ }
37
52
  const generation = (params.createGeneration ?? randomUUID)();
38
53
  store.register(params.sessionKey, {
54
+ accountId: params.accountId,
55
+ credentialFingerprint: params.credentialFingerprint,
39
56
  destinationIdentity: params.destinationIdentity,
40
57
  generation
41
58
  });
@@ -54,6 +71,7 @@ function recordPendingDiscussionOpen(params) {
54
71
  const store = getGenerationStore(params.runtime);
55
72
  const existing = store.lookup(params.sessionKey);
56
73
  if (!existing || existing.generation !== params.generation) throw new Error("ClickClack discussion generation changed before channel creation");
74
+ if (existing.accountId !== params.pending.accountId || existing.credentialFingerprint !== params.pending.credentialFingerprint) throw new Error("ClickClack discussion ownership changed before channel creation");
57
75
  store.register(params.sessionKey, {
58
76
  ...existing,
59
77
  pending: params.pending
@@ -66,6 +84,18 @@ function listPendingDiscussionOpens(runtime) {
66
84
  ...entry.value.pending
67
85
  }] : []);
68
86
  }
87
+ /** Stops destination-wide quarantine after the exact remote channel is known. */
88
+ function clearPendingDiscussionOpen(params) {
89
+ const store = getGenerationStore(params.runtime);
90
+ const existing = store.lookup(params.sessionKey);
91
+ if (!existing || existing.generation !== params.expectedGeneration || !existing.pending) return;
92
+ store.register(params.sessionKey, {
93
+ accountId: existing.accountId ?? existing.pending.accountId,
94
+ credentialFingerprint: existing.credentialFingerprint ?? existing.pending.credentialFingerprint,
95
+ destinationIdentity: existing.destinationIdentity,
96
+ generation: existing.generation
97
+ });
98
+ }
69
99
  function hasPendingDiscussionOpenForDestination(params) {
70
100
  const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
71
101
  return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
@@ -104,10 +134,9 @@ function discussionSessionPeerId(identity) {
104
134
  function isDiscussionSessionKey(sessionKey) {
105
135
  return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
106
136
  }
107
- function discussionExternalRef(installationId, mainSessionKey, sessionId, destinationIdentity, bindingGeneration) {
137
+ function discussionExternalRef(installationId, mainSessionKey, destinationIdentity, bindingGeneration) {
108
138
  return `openclaw:${installationId}:${shortSessionHash([
109
139
  mainSessionKey,
110
- sessionId,
111
140
  destinationIdentity,
112
141
  bindingGeneration
113
142
  ].join("\0"))}`;
@@ -128,22 +157,41 @@ function discussionSessionKey(params) {
128
157
  }
129
158
  //#endregion
130
159
  //#region extensions/clickclack/src/discussions/binding-store.ts
131
- function bindingMatchesSessionIncarnation(runtime, sessionKey, binding) {
160
+ function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
132
161
  const entry = runtime.agent.session.getSessionEntry({
133
162
  sessionKey,
134
163
  readConsistency: "latest"
135
164
  });
136
- return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId);
165
+ return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
137
166
  }
138
- function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
139
- const entry = runtime.agent.session.getSessionEntry({
140
- sessionKey,
167
+ /**
168
+ * Refresh the replaceable session attachment without changing the durable room identity.
169
+ * The store registers persisted state before reindexing, so a failed write leaves the
170
+ * previous attachment authoritative in both persistence and memory.
171
+ */
172
+ function attachBindingToCurrentActiveSession(params) {
173
+ const entry = params.runtime.agent.session.getSessionEntry({
174
+ sessionKey: params.sessionKey,
141
175
  readConsistency: "latest"
142
176
  });
143
- return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
177
+ if (!entry?.sessionId || entry.archivedAt !== void 0) return;
178
+ if (entry.sessionId === params.binding.sessionId) {
179
+ if (params.binding.detachedAt === void 0) return params.binding;
180
+ const { detachedAt: _detachedAt, ...attached } = params.binding;
181
+ params.store.set(params.sessionKey, attached);
182
+ return attached;
183
+ }
184
+ const { detachedAt: _detachedAt, ...retained } = params.binding;
185
+ const attached = {
186
+ ...retained,
187
+ sessionId: entry.sessionId
188
+ };
189
+ params.store.set(params.sessionKey, attached);
190
+ return attached;
144
191
  }
145
192
  const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
146
193
  const MAX_DISCUSSION_BINDINGS = 1e4;
194
+ const MAX_RETAINED_DETACHED_DISCUSSION_BINDINGS = 1e3;
147
195
  const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
148
196
  function channelKey(serverBaseUrl, channelId) {
149
197
  return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
@@ -153,6 +201,7 @@ var ClickClackDiscussionBindingStore = class {
153
201
  #store;
154
202
  #sessionByChannel = /* @__PURE__ */ new Map();
155
203
  #mainByDiscussionSession = /* @__PURE__ */ new Map();
204
+ #detachedAtBySession = /* @__PURE__ */ new Map();
156
205
  #runtime;
157
206
  constructor(runtime) {
158
207
  this.#runtime = runtime;
@@ -210,6 +259,27 @@ var ClickClackDiscussionBindingStore = class {
210
259
  binding: entry.value
211
260
  }));
212
261
  }
262
+ detachedCount() {
263
+ return this.#detachedAtBySession.size;
264
+ }
265
+ oldestDetached() {
266
+ let oldestSessionKey;
267
+ let oldestDetachedAt = Number.POSITIVE_INFINITY;
268
+ for (const [sessionKey, detachedAt] of this.#detachedAtBySession) if (detachedAt < oldestDetachedAt || detachedAt === oldestDetachedAt && (oldestSessionKey === void 0 || sessionKey < oldestSessionKey)) {
269
+ oldestSessionKey = sessionKey;
270
+ oldestDetachedAt = detachedAt;
271
+ }
272
+ if (!oldestSessionKey) return;
273
+ const binding = this.get(oldestSessionKey);
274
+ if (!binding || binding.detachedAt === void 0) {
275
+ this.#detachedAtBySession.delete(oldestSessionKey);
276
+ return this.oldestDetached();
277
+ }
278
+ return {
279
+ sessionKey: oldestSessionKey,
280
+ binding
281
+ };
282
+ }
213
283
  #index(sessionKey, binding) {
214
284
  this.#sessionByChannel.set(channelKey(binding.serverBaseUrl, binding.channelId), sessionKey);
215
285
  const sideSessionKey = discussionSessionKey({
@@ -223,6 +293,7 @@ var ClickClackDiscussionBindingStore = class {
223
293
  externalRef: binding.externalRef
224
294
  });
225
295
  if (sideSessionKey) this.#mainByDiscussionSession.set(sideSessionKey, sessionKey);
296
+ if (binding.detachedAt !== void 0) this.#detachedAtBySession.set(sessionKey, binding.detachedAt);
226
297
  }
227
298
  #unindex(sessionKey, binding) {
228
299
  this.#sessionByChannel.delete(channelKey(binding.serverBaseUrl, binding.channelId));
@@ -237,6 +308,7 @@ var ClickClackDiscussionBindingStore = class {
237
308
  externalRef: binding.externalRef
238
309
  });
239
310
  if (sideSessionKey) this.#mainByDiscussionSession.delete(sideSessionKey);
311
+ this.#detachedAtBySession.delete(sessionKey);
240
312
  }
241
313
  };
242
314
  function getClickClackDiscussionBindingStore(runtime) {
@@ -327,28 +399,43 @@ function isClickClackDiscussionChannelRevoked(params) {
327
399
  //#region extensions/clickclack/src/discussions/routing.ts
328
400
  function resolveClickClackDiscussionRoute(params) {
329
401
  if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
330
- const matched = getClickClackDiscussionBindingStore(params.runtime).getByChannel(params.serverBaseUrl, params.channelId);
402
+ const store = getClickClackDiscussionBindingStore(params.runtime);
403
+ const matched = store.getByChannel(params.serverBaseUrl, params.channelId);
331
404
  if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
332
405
  if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
333
406
  if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
334
- if (matched.binding.archived) return { state: "revoked" };
335
407
  if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
336
- if (!bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, matched.binding)) return { state: "revoked" };
408
+ let binding;
409
+ try {
410
+ binding = attachBindingToCurrentActiveSession({
411
+ runtime: params.runtime,
412
+ store,
413
+ sessionKey: matched.sessionKey,
414
+ binding: matched.binding
415
+ });
416
+ } catch (error) {
417
+ params.runtime.logging.getChildLogger({
418
+ plugin: "clickclack",
419
+ feature: "discussions"
420
+ }).warn(`discussion attachment refresh failed for channel ${params.channelId}: ${String(error)}`);
421
+ return { state: "revoked" };
422
+ }
423
+ if (!binding) return { state: "revoked" };
337
424
  const sessionKey = discussionSessionKey({
338
425
  runtime: params.runtime,
339
- agentId: matched.binding.agentId,
426
+ agentId: binding.agentId,
340
427
  mainSessionKey: matched.sessionKey,
341
- sessionId: matched.binding.sessionId,
428
+ sessionId: binding.sessionId,
342
429
  accountId: params.accountId,
343
- serverBaseUrl: matched.binding.serverBaseUrl,
344
- channelId: matched.binding.channelId,
345
- externalRef: matched.binding.externalRef
430
+ serverBaseUrl: binding.serverBaseUrl,
431
+ channelId: binding.channelId,
432
+ externalRef: binding.externalRef
346
433
  });
347
434
  if (!sessionKey) return { state: "revoked" };
348
435
  return {
349
436
  state: "active",
350
437
  route: {
351
- agentId: matched.binding.agentId,
438
+ agentId: binding.agentId,
352
439
  sessionKey,
353
440
  systemPrompt: [
354
441
  "You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
@@ -363,6 +450,21 @@ function resolveClickClackDiscussionRoute(params) {
363
450
  //#endregion
364
451
  //#region extensions/clickclack/src/group-policy.ts
365
452
  /**
453
+ * Resolves bot-authored message policy using the same exact, wildcard, and
454
+ * account-level precedence as mention gating.
455
+ */
456
+ function resolveClickClackBotPolicy(params) {
457
+ const { account, channelId } = params;
458
+ const channelKey = channelId?.trim();
459
+ const groups = channelKey ? account.groups : void 0;
460
+ const wildcard = groups?.["*"];
461
+ const exact = channelKey ? groups?.[channelKey] : void 0;
462
+ return {
463
+ allowBots: exact?.allowBots ?? wildcard?.allowBots ?? account.allowBots ?? false,
464
+ botLoopProtection: mergePairLoopGuardConfig(account.botLoopProtection, wildcard?.botLoopProtection, exact?.botLoopProtection)
465
+ };
466
+ }
467
+ /**
366
468
  * Resolves the effective group policy for a ClickClack channel.
367
469
  *
368
470
  * Lookup order:
@@ -377,9 +479,10 @@ function resolveClickClackGroupPolicy(params) {
377
479
  requireMention: account.requireMention === true,
378
480
  mentionPatterns: account.mentionPatterns ?? []
379
481
  };
380
- const wildcard = account.groups?.["*"];
381
482
  const channelKey = channelId?.trim();
382
- const exact = channelKey ? account.groups?.[channelKey] : void 0;
483
+ const groups = channelKey ? account.groups : void 0;
484
+ const wildcard = groups?.["*"];
485
+ const exact = channelKey ? groups?.[channelKey] : void 0;
383
486
  return {
384
487
  requireMention: exact?.requireMention ?? wildcard?.requireMention ?? accountPolicy.requireMention,
385
488
  mentionPatterns: exact?.mentionPatterns ?? wildcard?.mentionPatterns ?? accountPolicy.mentionPatterns
@@ -531,6 +634,10 @@ const clickClackIngressIdentity = {
531
634
  isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
532
635
  entryIdPrefix: "clickclack-user"
533
636
  };
637
+ function resolveClickClackBotLoopConversationId(params) {
638
+ if (params.message.parent_message_id && params.message.thread_root_id) return params.message.thread_root_id;
639
+ return params.isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id ?? params.message.author_id;
640
+ }
534
641
  function resolveAccountAgentRoute(params) {
535
642
  const runtime = getClickClackRuntime();
536
643
  const peer = {
@@ -633,6 +740,41 @@ async function resolveClickClackInboundAccess(params) {
633
740
  agentId: preparedRoute.route.agentId,
634
741
  channelId: params.message.channel_id
635
742
  });
743
+ if (params.message.kind !== void 0 && params.message.kind !== "message") return {
744
+ shouldDispatch: false,
745
+ commandAuthorized: false,
746
+ requireMention: effectiveGroupPolicy.requireMention,
747
+ mentionFacts,
748
+ preparedRoute
749
+ };
750
+ const effectiveBotPolicy = resolveClickClackBotPolicy({
751
+ account: params.account,
752
+ channelId: params.message.channel_id
753
+ });
754
+ const isBotAuthor = params.message.author?.kind === "bot";
755
+ const ingressAllowFrom = isBotAuthor ? params.account.allowFrom.filter((entry) => normalizeClickClackUserId(entry) !== "*") : params.account.allowFrom;
756
+ if (!(!isBotAuthor || effectiveBotPolicy.allowBots === true || effectiveBotPolicy.allowBots === "mentions" && (preparedRoute.isDirect || mentionFacts.wasMentioned))) return {
757
+ shouldDispatch: false,
758
+ commandAuthorized: false,
759
+ requireMention: effectiveGroupPolicy.requireMention,
760
+ mentionFacts,
761
+ preparedRoute
762
+ };
763
+ const botLoopNowMs = parseDateStringTimestampMs(params.message.created_at);
764
+ const botLoopProtection = isBotAuthor && params.message.author_id !== params.account.botUserId && params.account.botUserId ? {
765
+ scopeId: params.account.workspace,
766
+ conversationId: resolveClickClackBotLoopConversationId({
767
+ message: params.message,
768
+ isDirect: preparedRoute.isDirect
769
+ }),
770
+ senderId: params.message.author_id,
771
+ receiverId: params.account.botUserId,
772
+ eventId: params.message.id,
773
+ ...botLoopNowMs !== void 0 ? { nowMs: botLoopNowMs } : {},
774
+ config: effectiveBotPolicy.botLoopProtection,
775
+ defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
776
+ defaultEnabled: true
777
+ } : void 0;
636
778
  const allowTextCommands = params.account.replyMode === "agent" && runtime.channel.commands.shouldHandleTextCommands({
637
779
  cfg,
638
780
  surface: CHANNEL_ID$2,
@@ -648,7 +790,13 @@ async function resolveClickClackInboundAccess(params) {
648
790
  kind: preparedRoute.isDirect ? "direct" : "group",
649
791
  id: preparedRoute.isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
650
792
  },
651
- allowFrom: params.account.allowFrom,
793
+ contextBinding: {
794
+ agentId: preparedRoute.route.agentId,
795
+ sessionKey: preparedRoute.route.sessionKey,
796
+ messageId: params.message.id,
797
+ inboundEventKind: "user_request"
798
+ },
799
+ allowFrom: ingressAllowFrom,
652
800
  dmPolicy: "allowlist",
653
801
  groupPolicy: "allowlist",
654
802
  mentionFacts,
@@ -666,7 +814,9 @@ async function resolveClickClackInboundAccess(params) {
666
814
  commandAuthorized: resolved.commandAccess.requested ? resolved.commandAccess.authorized : resolved.senderAccess.allowed,
667
815
  requireMention: effectiveGroupPolicy.requireMention,
668
816
  mentionFacts,
669
- preparedRoute
817
+ botLoopProtection,
818
+ preparedRoute,
819
+ channelIngress: resolved
670
820
  };
671
821
  }
672
822
  //#endregion
@@ -716,15 +866,16 @@ async function syncClickClackCommandMenu(params) {
716
866
  await params.client.setBotCommands(commands);
717
867
  } catch (error) {
718
868
  const status = errorStatus(error);
869
+ const messagePrefix = `[${params.accountId}] ClickClack command menu sync`;
719
870
  if (status === 403) {
720
- params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
871
+ params.log?.warn?.(`${messagePrefix} skipped: ${formatErrorMessage(error)}; verify token/workspace command permissions or set commandMenu: false if menus are not needed`);
721
872
  return;
722
873
  }
723
874
  if (status === 404) {
724
- params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
875
+ params.log?.debug?.(`${messagePrefix} skipped: server does not support /api/bots/self/commands`);
725
876
  return;
726
877
  }
727
- params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
878
+ params.log?.warn?.(`${messagePrefix} failed: ${formatErrorMessage(error)}`);
728
879
  }
729
880
  }
730
881
  //#endregion
@@ -795,7 +946,8 @@ function activityBody(payload) {
795
946
  status: payload.status,
796
947
  summary: payload.summary,
797
948
  progressText: payload.progressText,
798
- meta: payload.meta
949
+ meta: payload.meta,
950
+ commandBearing: payload.commandBearing
799
951
  })?.text?.trim();
800
952
  if (line) return line;
801
953
  const head = payload.name?.trim() || payload.title?.trim();
@@ -908,10 +1060,11 @@ function createClickClackActivityPublisher(params) {
908
1060
  const kind = normalizedItemKind(payload);
909
1061
  if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
910
1062
  handleCommentary(payload);
911
- return;
1063
+ return false;
912
1064
  }
913
- if (SKIPPED_ITEM_KINDS.has(kind)) return;
1065
+ if (SKIPPED_ITEM_KINDS.has(kind)) return false;
914
1066
  handleDiscreteItem(payload);
1067
+ return false;
915
1068
  },
916
1069
  setProvenance: (next) => {
917
1070
  provenance = next;
@@ -1080,9 +1233,9 @@ async function sendClickClackMedia(params) {
1080
1233
  mediaLocalRoots: params.mediaLocalRoots,
1081
1234
  mediaReadFile: params.mediaReadFile
1082
1235
  });
1083
- const filename = media.fileName?.trim() || "attachment";
1084
- mediaFilename = filename;
1085
1236
  const contentType = media.contentType?.trim() || "application/octet-stream";
1237
+ const filename = media.fileName?.trim() || `attachment${extensionForMime(contentType) ?? ""}`;
1238
+ mediaFilename = filename;
1086
1239
  await dispatch();
1087
1240
  upload = await client.createUpload({
1088
1241
  workspaceId,
@@ -1215,6 +1368,219 @@ async function reconcileClickClackUnknownSend(ctx) {
1215
1368
  };
1216
1369
  }
1217
1370
  //#endregion
1371
+ //#region extensions/clickclack/src/progress.ts
1372
+ /**
1373
+ * Publishes ClickClack's native ephemeral agent.progress signal for one
1374
+ * OpenClaw turn. ClickClack renders this as its compact "Agent is
1375
+ * responding" status and the detailed progress lines above the composer.
1376
+ */
1377
+ function normalizedKind(payload) {
1378
+ const kind = payload.kind?.trim().toLowerCase();
1379
+ if (!kind || kind === "preamble" || kind === "analysis" || kind === "thinking" || kind === "reasoning" || kind === "missing") return "commentary";
1380
+ return kind;
1381
+ }
1382
+ function progressText(payload) {
1383
+ const line = buildChannelProgressDraftLine({
1384
+ event: "item",
1385
+ itemId: payload.itemId,
1386
+ toolCallId: payload.toolCallId,
1387
+ itemKind: payload.kind,
1388
+ title: payload.title,
1389
+ name: payload.name,
1390
+ phase: payload.phase,
1391
+ status: payload.status,
1392
+ summary: payload.summary,
1393
+ progressText: payload.progressText,
1394
+ meta: payload.meta,
1395
+ commandBearing: payload.commandBearing
1396
+ })?.text?.trim();
1397
+ if (line) return line;
1398
+ return payload.progressText?.trim() || payload.summary?.trim() || payload.title?.trim() || payload.name?.trim() || payload.meta?.trim() || payload.status?.trim() || "Working";
1399
+ }
1400
+ function isFinal(payload) {
1401
+ const phase = payload.phase?.trim().toLowerCase();
1402
+ const status = payload.status?.trim().toLowerCase();
1403
+ return phase === "end" || status === "completed" || status === "failed" || status === "blocked";
1404
+ }
1405
+ function createLineIdResolver() {
1406
+ const lineIdsByIdentity = /* @__PURE__ */ new Map();
1407
+ const anonymousLinesByKind = /* @__PURE__ */ new Map();
1408
+ let anonymousSequence = 0;
1409
+ return (payload) => {
1410
+ const identities = [payload.itemId?.replace(/^(tool|command):/, ""), payload.toolCallId].map((value) => value?.trim()).filter((value) => Boolean(value));
1411
+ const existingId = identities.map((identity) => lineIdsByIdentity.get(identity)).find(Boolean);
1412
+ if (existingId) {
1413
+ for (const identity of identities) lineIdsByIdentity.set(identity, existingId);
1414
+ return existingId;
1415
+ }
1416
+ if (identities.length > 0) {
1417
+ const id = `item:${identities[0]}`;
1418
+ for (const identity of identities) lineIdsByIdentity.set(identity, id);
1419
+ return id;
1420
+ }
1421
+ const kind = normalizedKind(payload);
1422
+ const anonymousLines = anonymousLinesByKind.get(kind) ?? [];
1423
+ const line = (payload.phase?.trim().toLowerCase() === "start" ? void 0 : anonymousLines.toReversed().find((line) => line.active)) ?? (() => {
1424
+ const created = {
1425
+ id: `item:${kind}:${++anonymousSequence}`,
1426
+ active: true
1427
+ };
1428
+ anonymousLines.push(created);
1429
+ anonymousLinesByKind.set(kind, anonymousLines);
1430
+ return created;
1431
+ })();
1432
+ if (isFinal(payload)) line.active = false;
1433
+ return line.id;
1434
+ };
1435
+ }
1436
+ const CLICKCLACK_PROGRESS_UPDATE_INTERVAL_MS = 100;
1437
+ const CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS = 1e3;
1438
+ function createClickClackAgentProgressPublisher(params) {
1439
+ let sequence = 0;
1440
+ const queue = [];
1441
+ const queuedLines = /* @__PURE__ */ new Map();
1442
+ let drainPromise;
1443
+ let lineDrainTimer;
1444
+ let started = false;
1445
+ let cleared = false;
1446
+ const seenLines = /* @__PURE__ */ new Set();
1447
+ const resolveLineId = createLineIdResolver();
1448
+ const drain = () => {
1449
+ if (drainPromise) return drainPromise;
1450
+ drainPromise = (async () => {
1451
+ while (queue.length > 0) {
1452
+ const frame = queue.shift();
1453
+ if (!frame) continue;
1454
+ if (frame.lineId) queuedLines.delete(frame.lineId);
1455
+ try {
1456
+ await params.client.publishEphemeral({
1457
+ ...params.target,
1458
+ type: "agent.progress",
1459
+ payload: {
1460
+ turn_id: params.turnId,
1461
+ seq: ++sequence,
1462
+ ...frame.payload
1463
+ }
1464
+ });
1465
+ } catch (error) {
1466
+ try {
1467
+ params.onError?.(error);
1468
+ } catch {}
1469
+ }
1470
+ }
1471
+ })().finally(() => {
1472
+ drainPromise = void 0;
1473
+ if (queue.length > 0) drain();
1474
+ });
1475
+ return drainPromise;
1476
+ };
1477
+ const enqueue = (payload) => {
1478
+ queue.push({ payload });
1479
+ drain();
1480
+ };
1481
+ const flushQueuedLines = () => {
1482
+ for (const [lineId, frame] of queuedLines) {
1483
+ queuedLines.delete(lineId);
1484
+ queue.push(frame);
1485
+ }
1486
+ drain();
1487
+ };
1488
+ const discardQueuedLines = () => {
1489
+ queuedLines.clear();
1490
+ const controlFrames = queue.filter((frame) => !frame.lineId);
1491
+ queue.splice(0, queue.length, ...controlFrames);
1492
+ };
1493
+ const waitForDrainWithinFinalizeGrace = async (pending) => {
1494
+ let timeout;
1495
+ let drained = false;
1496
+ try {
1497
+ await Promise.race([pending.then(() => {
1498
+ drained = true;
1499
+ }), new Promise((resolve) => {
1500
+ timeout = setTimeout(resolve, CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS);
1501
+ })]);
1502
+ return drained;
1503
+ } finally {
1504
+ if (timeout) clearTimeout(timeout);
1505
+ }
1506
+ };
1507
+ const scheduleLineDrain = () => {
1508
+ if (lineDrainTimer) return;
1509
+ lineDrainTimer = setTimeout(() => {
1510
+ lineDrainTimer = void 0;
1511
+ flushQueuedLines();
1512
+ }, CLICKCLACK_PROGRESS_UPDATE_INTERVAL_MS);
1513
+ };
1514
+ const enqueueLine = (lineId, payload) => {
1515
+ const queued = queuedLines.get(lineId);
1516
+ if (queued) {
1517
+ const op = payload.op === "finalize" ? "finalize" : queued.payload.op === "append" ? "append" : payload.op;
1518
+ queued.payload = {
1519
+ ...payload,
1520
+ ...op ? { op } : {}
1521
+ };
1522
+ return;
1523
+ }
1524
+ const frame = {
1525
+ lineId,
1526
+ payload
1527
+ };
1528
+ queuedLines.set(lineId, frame);
1529
+ scheduleLineDrain();
1530
+ };
1531
+ return {
1532
+ start() {
1533
+ if (started) return;
1534
+ started = true;
1535
+ enqueue({
1536
+ op: "append",
1537
+ line: {
1538
+ id: "turn",
1539
+ kind: "commentary",
1540
+ text: params.agentLabel ? `${params.agentLabel} is responding` : "Agent is responding",
1541
+ status: "running"
1542
+ }
1543
+ });
1544
+ },
1545
+ onItemEvent(payload) {
1546
+ if (!started || cleared) return false;
1547
+ const id = resolveLineId(payload);
1548
+ const final = isFinal(payload);
1549
+ const kind = normalizedKind(payload);
1550
+ const retractsExistingCommentary = kind === "commentary" && seenLines.has(id) && payload.progressText !== void 0 && payload.progressText.trim() === "";
1551
+ if (retractsExistingCommentary && queuedLines.get(id)?.payload.op === "append") {
1552
+ queuedLines.delete(id);
1553
+ seenLines.delete(id);
1554
+ return false;
1555
+ }
1556
+ const line = {
1557
+ id,
1558
+ kind,
1559
+ text: retractsExistingCommentary ? "" : progressText(payload),
1560
+ status: payload.status?.trim() || (final ? "completed" : "running")
1561
+ };
1562
+ if (payload.name?.trim()) line.tool_name = payload.name.trim();
1563
+ enqueueLine(id, {
1564
+ op: final ? "finalize" : seenLines.has(id) ? "update" : "append",
1565
+ line
1566
+ });
1567
+ seenLines.add(id);
1568
+ return false;
1569
+ },
1570
+ async finalize() {
1571
+ if (!started || cleared) return;
1572
+ cleared = true;
1573
+ if (lineDrainTimer) {
1574
+ clearTimeout(lineDrainTimer);
1575
+ lineDrainTimer = void 0;
1576
+ }
1577
+ flushQueuedLines();
1578
+ enqueue({ op: "clear" });
1579
+ if (!await waitForDrainWithinFinalizeGrace(drain())) discardQueuedLines();
1580
+ }
1581
+ };
1582
+ }
1583
+ //#endregion
1218
1584
  //#region extensions/clickclack/src/inbound.ts
1219
1585
  const CHANNEL_ID$1 = "clickclack";
1220
1586
  const CLICKCLACK_MESSAGE_ID_PATTERN = /^msg_[0-9a-hjkmnp-tv-z]{26}$/u;
@@ -1265,19 +1631,56 @@ async function handleClickClackInbound(params) {
1265
1631
  config: params.config,
1266
1632
  message
1267
1633
  });
1268
- if (!access.shouldDispatch) return;
1634
+ if (!access.shouldDispatch || !access.channelIngress) return;
1269
1635
  const conversationId = message.channel_id || message.direct_conversation_id;
1270
1636
  if (!conversationId) return;
1271
1637
  const { discussionRoute, isDirect, route, target } = access.preparedRoute;
1272
- if (params.account.replyMode === "model" && !discussionRoute) {
1273
- await dispatchModelReply({
1274
- account: params.account,
1275
- cfg: params.config,
1276
- message,
1277
- route,
1278
- target,
1638
+ const progress = params.account.nativeProgress ? createClickClackAgentProgressPublisher({
1639
+ client: createClickClackClient({
1640
+ baseUrl: params.account.apiEndpoint,
1641
+ token: params.account.token,
1279
1642
  correlationId: params.correlationId
1280
- });
1643
+ }),
1644
+ target: message.channel_id ? {
1645
+ workspaceId: message.workspace_id,
1646
+ channelId: message.channel_id
1647
+ } : {
1648
+ workspaceId: message.workspace_id,
1649
+ conversationId
1650
+ },
1651
+ turnId: message.id,
1652
+ agentLabel: params.account.name?.trim() || params.account.botHandle?.trim() || params.account.agentId?.trim() || params.account.accountId,
1653
+ onError: (error) => {
1654
+ runtime.logging.getChildLogger({
1655
+ plugin: "clickclack",
1656
+ feature: "agent-progress"
1657
+ }).warn(`clickclack progress publish failed: ${String(error)}`);
1658
+ }
1659
+ }) : void 0;
1660
+ if (params.account.replyMode === "model" && !discussionRoute) {
1661
+ if (access.botLoopProtection) {
1662
+ const loopResult = recordChannelBotPairLoopAndCheckSuppression(access.botLoopProtection);
1663
+ if (loopResult.suppressed) {
1664
+ runtime.logging.getChildLogger({
1665
+ plugin: "clickclack",
1666
+ feature: "bot-loop-protection"
1667
+ }).warn(`[${params.account.accountId}] ClickClack bot-pair loop suppressed for ${Math.max(0, Math.ceil((loopResult.cooldownUntilMs - Date.now()) / 1e3))}s`);
1668
+ return;
1669
+ }
1670
+ }
1671
+ progress?.start();
1672
+ try {
1673
+ await dispatchModelReply({
1674
+ account: params.account,
1675
+ cfg: params.config,
1676
+ message,
1677
+ route,
1678
+ target,
1679
+ correlationId: params.correlationId
1680
+ });
1681
+ } finally {
1682
+ await progress?.finalize();
1683
+ }
1281
1684
  return;
1282
1685
  }
1283
1686
  let turnProvenance;
@@ -1307,7 +1710,8 @@ async function handleClickClackInbound(params) {
1307
1710
  timestamp: new Date(message.created_at),
1308
1711
  body: message.body
1309
1712
  });
1310
- const ctxPayload = runtime.channel.inbound.buildContext({
1713
+ const ctxPayload = (params.buildContext ?? buildChannelInboundEventContext)({
1714
+ channelIngress: access.channelIngress,
1311
1715
  channel: CHANNEL_ID$1,
1312
1716
  accountId: route.accountId ?? params.account.accountId,
1313
1717
  messageId: message.id,
@@ -1354,20 +1758,27 @@ async function handleClickClackInbound(params) {
1354
1758
  }
1355
1759
  });
1356
1760
  const runId = resolveClickClackAgentRunId(message.id);
1357
- const activityReplyOptions = activity ? {
1358
- onModelSelected: (ctx) => {
1761
+ const activityReplyOptions = {
1762
+ ...activity ? { onModelSelected: (ctx) => {
1359
1763
  turnProvenance = {
1360
1764
  model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
1361
1765
  thinking: ctx.thinkLevel
1362
1766
  };
1363
- activity?.setProvenance(turnProvenance);
1364
- },
1365
- onItemEvent: activity.onItemEvent,
1366
- commentaryProgressEnabled: true,
1367
- suppressDefaultToolProgressMessages: true,
1368
- allowProgressCallbacksWhenSourceDeliverySuppressed: true
1369
- } : void 0;
1370
- const dispatchPromise = runtime.channel.inbound.dispatch({
1767
+ activity.setProvenance(turnProvenance);
1768
+ } } : {},
1769
+ ...progress || activity ? {
1770
+ onItemEvent: (payload) => {
1771
+ progress?.onItemEvent(payload);
1772
+ activity?.onItemEvent(payload);
1773
+ return false;
1774
+ },
1775
+ commentaryProgressEnabled: true,
1776
+ suppressDefaultToolProgressMessages: true,
1777
+ allowProgressCallbacksWhenSourceDeliverySuppressed: true
1778
+ } : {}
1779
+ };
1780
+ progress?.start();
1781
+ const dispatch = () => runtime.channel.inbound.dispatch({
1371
1782
  cfg: params.config,
1372
1783
  channel: CHANNEL_ID$1,
1373
1784
  accountId: params.account.accountId,
@@ -1377,11 +1788,12 @@ async function handleClickClackInbound(params) {
1377
1788
  sessionKey: route.sessionKey
1378
1789
  },
1379
1790
  ctxPayload,
1791
+ botLoopProtection: access.botLoopProtection,
1380
1792
  toolsAllow: params.account.toolsAllow,
1381
- replyOptions: runId || activityReplyOptions ? {
1793
+ replyOptions: {
1382
1794
  ...runId ? { runId } : {},
1383
1795
  ...activityReplyOptions
1384
- } : void 0,
1796
+ },
1385
1797
  delivery: {
1386
1798
  deliver: async (payload) => {
1387
1799
  if (hasClickClackReplyMedia(payload)) throw new Error("ClickClack media reply requires durable delivery");
@@ -1423,8 +1835,9 @@ async function handleClickClackInbound(params) {
1423
1835
  } }
1424
1836
  });
1425
1837
  try {
1426
- await dispatchPromise;
1838
+ await dispatch();
1427
1839
  } finally {
1840
+ await progress?.finalize();
1428
1841
  await activity?.finalize();
1429
1842
  }
1430
1843
  }
@@ -1432,8 +1845,7 @@ async function handleClickClackInbound(params) {
1432
1845
  //#region extensions/clickclack/src/gateway.ts
1433
1846
  const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
1434
1847
  function payloadString(event, key) {
1435
- const value = event.payload?.[key];
1436
- return typeof value === "string" ? value : "";
1848
+ return readStringField(event.payload, key) ?? "";
1437
1849
  }
1438
1850
  function eventCorrelationId(event) {
1439
1851
  return normalizeClickClackCorrelationId(event.payload?.correlation_id);
@@ -1441,25 +1853,16 @@ function eventCorrelationId(event) {
1441
1853
  async function resolveEventMessage(params) {
1442
1854
  const messageId = payloadString(params.event, "message_id");
1443
1855
  if (!messageId) return null;
1444
- const directConversationId = payloadString(params.event, "direct_conversation_id");
1445
- if (directConversationId && typeof params.event.seq === "number") return (await params.client.directMessages(directConversationId, params.event.seq - 1, 10)).find((message) => message.id === messageId) ?? null;
1446
- if (params.event.type === "thread.reply_created") {
1447
- const rootId = payloadString(params.event, "root_message_id");
1448
- if (!rootId) return null;
1449
- return (await params.client.thread(rootId)).replies.find((message) => message.id === messageId) ?? null;
1856
+ try {
1857
+ return await params.client.message(messageId);
1858
+ } catch (error) {
1859
+ if (error instanceof ClickClackHttpError && error.status === 404) return null;
1860
+ throw error;
1450
1861
  }
1451
- if (params.event.channel_id && typeof params.event.seq === "number") return (await params.client.channelMessages(params.event.channel_id, params.event.seq - 1, 10)).find((message) => message.id === messageId) ?? null;
1452
- return null;
1453
- }
1454
- function decodeSocketMessage(data) {
1455
- if (typeof data === "string") return data;
1456
- if (Buffer.isBuffer(data)) return data.toString("utf8");
1457
- if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
1458
- return Buffer.concat(data).toString("utf8");
1459
1862
  }
1460
1863
  function parseSocketEvent(data) {
1461
1864
  try {
1462
- return JSON.parse(decodeSocketMessage(data));
1865
+ return JSON.parse(rawDataToString(data));
1463
1866
  } catch {
1464
1867
  return null;
1465
1868
  }
@@ -1476,8 +1879,11 @@ async function processEvent(params) {
1476
1879
  }) : params.client,
1477
1880
  event: params.event
1478
1881
  });
1479
- if (!message || message.author_id === params.botUserId) return;
1480
- if (message.author?.kind === "bot") return;
1882
+ if (!message) {
1883
+ params.log?.warn?.(`[${params.account.accountId}] skipped unreadable ClickClack message before agent dispatch: type=${params.event.type} messageId=${payloadString(params.event, "message_id") || "unknown"}`);
1884
+ return;
1885
+ }
1886
+ if (message.author_id === params.botUserId) return;
1481
1887
  const access = await resolveClickClackInboundAccess({
1482
1888
  account: params.account,
1483
1889
  config: params.config,
@@ -1492,6 +1898,7 @@ async function processEvent(params) {
1492
1898
  config: params.config,
1493
1899
  message,
1494
1900
  access,
1901
+ buildContext: params.buildContext,
1495
1902
  ...correlationId ? { correlationId } : {}
1496
1903
  });
1497
1904
  }
@@ -1536,16 +1943,19 @@ async function startClickClackGatewayAccount(ctx) {
1536
1943
  client,
1537
1944
  event,
1538
1945
  botUserId: account.botUserId,
1946
+ buildContext: ctx.channelRuntime?.inbound.buildContext,
1539
1947
  log: ctx.log
1540
1948
  });
1541
1949
  if (account.commandMenu) await syncClickClackCommandMenu({
1542
1950
  cfg: ctx.cfg,
1543
1951
  client,
1544
- log: ctx.log
1952
+ log: ctx.log,
1953
+ accountId: account.accountId
1545
1954
  });
1546
1955
  ctx.setStatus({
1547
1956
  accountId: account.accountId,
1548
1957
  running: true,
1958
+ lifecycle: "starting",
1549
1959
  configured: true,
1550
1960
  enabled: account.enabled,
1551
1961
  baseUrl: account.baseUrl
@@ -1601,6 +2011,9 @@ async function startClickClackGatewayAccount(ctx) {
1601
2011
  };
1602
2012
  ctx.abortSignal.addEventListener("abort", abort, { once: true });
1603
2013
  removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
2014
+ socket.on("open", () => {
2015
+ ctx.setStatus(channelReadyPatch({ accountId: account.accountId }));
2016
+ });
1604
2017
  socket.on("message", (data) => {
1605
2018
  if (closing || settled) return;
1606
2019
  messageQueue = messageQueue.then(async () => {
@@ -1616,6 +2029,11 @@ async function startClickClackGatewayAccount(ctx) {
1616
2029
  });
1617
2030
  socket.on("close", () => {
1618
2031
  closing = true;
2032
+ if (!ctx.abortSignal.aborted) ctx.setStatus({
2033
+ accountId: account.accountId,
2034
+ connected: false,
2035
+ lifecycle: "recovering"
2036
+ });
1619
2037
  finishAfterQueuedMessages();
1620
2038
  });
1621
2039
  socket.on("error", (error) => {
@@ -1625,6 +2043,12 @@ async function startClickClackGatewayAccount(ctx) {
1625
2043
  }
1626
2044
  if (closing) return;
1627
2045
  ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
2046
+ ctx.setStatus({
2047
+ accountId: account.accountId,
2048
+ connected: false,
2049
+ lifecycle: "recovering",
2050
+ lastError: error instanceof Error ? error.message : String(error)
2051
+ });
1628
2052
  closing = true;
1629
2053
  socket.close();
1630
2054
  });
@@ -1636,10 +2060,7 @@ async function startClickClackGatewayAccount(ctx) {
1636
2060
  }
1637
2061
  }
1638
2062
  } finally {
1639
- ctx.setStatus({
1640
- accountId: account.accountId,
1641
- running: false
1642
- });
2063
+ ctx.setStatus(channelStoppedPatch({ accountId: account.accountId }));
1643
2064
  }
1644
2065
  }
1645
2066
  //#endregion
@@ -1737,12 +2158,12 @@ const clickClackPlugin = createChatChannelPlugin({
1737
2158
  capabilities: {
1738
2159
  chatTypes: ["direct", "group"],
1739
2160
  threads: true,
2161
+ media: true,
1740
2162
  blockStreaming: true
1741
2163
  },
1742
2164
  reload: { configPrefixes: ["channels.clickclack"] },
1743
2165
  configSchema: clickClackConfigSchema,
1744
2166
  config: clickClackConfigAdapter,
1745
- setup: clickClackSetupAdapter,
1746
2167
  setupContract: clickClackSetupContract,
1747
2168
  setupWizard: clickClackSetupWizard,
1748
2169
  secrets: {
@@ -1804,7 +2225,11 @@ const clickClackPlugin = createChatChannelPlugin({
1804
2225
  name: account.name,
1805
2226
  enabled: account.enabled,
1806
2227
  configured: account.configured,
1807
- baseUrl: account.baseUrl
2228
+ extra: {
2229
+ baseUrl: account.baseUrl,
2230
+ tokenSource: account.tokenSource,
2231
+ tokenStatus: account.tokenStatus
2232
+ }
1808
2233
  })
1809
2234
  }),
1810
2235
  gateway: { startAccount: startClickClackGatewayAccount },
@@ -1849,4 +2274,4 @@ const clickClackPlugin = createChatChannelPlugin({
1849
2274
  }
1850
2275
  });
1851
2276
  //#endregion
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 };
2277
+ export { slugifyDiscussionLabel as C, listPendingDiscussionOpens as D, clearPendingDiscussionOpen as E, recordPendingDiscussionOpen as O, resolveDiscussionLabel as S, clearDiscussionBindingGeneration as T, getClickClackDiscussionBindingStore as _, setClickClackRuntime as a, fallbackDiscussionLabel as b, markClickClackDiscussionChannelIdentityRevoked as c, discussionInfoForBinding as d, normalizedServerBaseUrl as f, bindingMatchesActiveSessionIncarnation as g, attachBindingToCurrentActiveSession as h, getClickClackRuntime as i, reserveDiscussionBindingGeneration as k, markClickClackDiscussionChannelRevoked as l, MAX_RETAINED_DETACHED_DISCUSSION_BINDINGS as m, buildClickClackTarget as n, clearClickClackDiscussionChannelRevoked as o, resolveDiscussionBindingAccount as p, parseClickClackTarget as r, isClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, discussionAccounts as u, discussionCredentialFingerprint as v, truncateDiscussionDisplayTitle as w, isDiscussionSessionKey as x, discussionExternalRef as y };