@openclaw/clickclack 2026.7.2-beta.4 → 2026.7.2-beta.5

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.
package/README.md CHANGED
@@ -106,6 +106,11 @@ defaults to the account workspace, and `section` defaults to `Sessions`.
106
106
  `controlUrlBase` adds a link back to `/chat?session=<session-key>` in the
107
107
  OpenClaw Control UI.
108
108
 
109
+ ClickClack-managed embed URLs explicitly advertise host-theme support. The
110
+ Control UI uses that provider-owned capability to apply its full palette before
111
+ the sidebar first paints and to stream live palette changes without rewriting
112
+ opaque or signed discussion URLs from other providers.
113
+
109
114
  Enable discussions on exactly one ClickClack account. Multiple enabled
110
115
  discussion accounts are rejected because the session discussion provider does
111
116
  not have an account selector.
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { S as resolveDefaultClickClackAccountId, b as listEnabledClickClackAccounts, d as createClickClackClient, m as clickClackConfigSchema, v as DEFAULT_ACCOUNT_ID, x as resolveClickClackAccount, y as listClickClackAccountIds } from "./setup-surface-Baf26N-D.js";
2
- import { T as setClickClackRuntime, n as buildClickClackTarget, r as parseClickClackTarget, t as clickClackPlugin, w as getClickClackRuntime } from "./channel-BDq0KWDB.js";
1
+ import { S as resolveDefaultClickClackAccountId, b as listEnabledClickClackAccounts, d as createClickClackClient, m as clickClackConfigSchema, v as DEFAULT_ACCOUNT_ID, x as resolveClickClackAccount, y as listClickClackAccountIds } from "./setup-surface-CD6MUC1g.js";
2
+ import { D as setClickClackRuntime, E as getClickClackRuntime, n as buildClickClackTarget, r as parseClickClackTarget, t as clickClackPlugin } from "./channel-DfcFlLjC.js";
3
3
  export { DEFAULT_ACCOUNT_ID, buildClickClackTarget, clickClackConfigSchema, clickClackPlugin, createClickClackClient, getClickClackRuntime, listClickClackAccountIds, listEnabledClickClackAccounts, parseClickClackTarget, resolveClickClackAccount, resolveDefaultClickClackAccountId, setClickClackRuntime };
@@ -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-Baf26N-D.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-CD6MUC1g.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";
@@ -12,7 +12,7 @@ import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel
12
12
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
13
13
  import { createHash, randomUUID } from "node:crypto";
14
14
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
15
- import { renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
15
+ import { FormatCapabilityProfile, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
16
16
  //#region extensions/clickclack/src/runtime.ts
17
17
  /**
18
18
  * Runtime store for host-provided OpenClaw services used by the ClickClack
@@ -389,14 +389,22 @@ function hasPendingDiscussionOpenForDestination(params) {
389
389
  function shortSessionHash(sessionKey) {
390
390
  return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
391
391
  }
392
- function fallbackDiscussionLabel(sessionKey) {
393
- return `s-${shortSessionHash(sessionKey)}`;
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
394
  }
395
- function resolveDiscussionLabel(label, sessionKey) {
396
- return label?.trim() || fallbackDiscussionLabel(sessionKey);
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();
397
405
  }
398
406
  function slugifyDiscussionLabel(label, sessionKey) {
399
- return label.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80).replace(/-+$/gu, "") || fallbackDiscussionLabel(sessionKey);
407
+ return slugifyLabel(label) || fallbackDiscussionLabel(sessionKey);
400
408
  }
401
409
  function discussionSessionPeerId(identity) {
402
410
  return `disc-${createHash("sha256").update([
@@ -575,6 +583,15 @@ function resolveDiscussionBindingAccount(cfg, binding) {
575
583
  account
576
584
  };
577
585
  }
586
+ /** Public embed/open URLs for one bound discussion channel. */
587
+ function discussionInfoForBinding(binding, account) {
588
+ const baseUrl = normalizedServerBaseUrl(account);
589
+ 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)}`
593
+ };
594
+ }
578
595
  //#endregion
579
596
  //#region extensions/clickclack/src/discussions/revoked-channel-store.ts
580
597
  const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
@@ -708,32 +725,13 @@ function looksLikeClickClackTarget(raw) {
708
725
  * and direct messages.
709
726
  */
710
727
  const CLICKCLACK_MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
711
- const CLICKCLACK_FORMAT_PROFILE = {
728
+ const CLICKCLACK_FORMAT_PROFILE = FormatCapabilityProfile.define({
712
729
  mechanism: "markdown",
713
- constructs: {
714
- bold: "native",
715
- italic: "native",
716
- underline: "native",
717
- strikethrough: "native",
718
- spoiler: "native",
719
- codeInline: "native",
720
- codeBlock: "native",
721
- codeLanguage: "native",
722
- linkLabel: "native",
723
- heading: "native",
724
- bulletList: "native",
725
- orderedList: "native",
726
- taskList: "native",
727
- table: "native",
728
- blockquote: "native",
729
- image: "native",
730
- mention: "native"
731
- },
732
730
  chunk: {
733
731
  limit: 1024 * 1024,
734
732
  unit: "bytes"
735
733
  }
736
- };
734
+ });
737
735
  function renderClickClackMarkdown(markdown) {
738
736
  return renderMarkdownWithMarkers({
739
737
  text: markdown,
@@ -1719,4 +1717,4 @@ const clickClackPlugin = createChatChannelPlugin({
1719
1717
  }
1720
1718
  });
1721
1719
  //#endregion
1722
- export { reserveDiscussionBindingGeneration as C, recordPendingDiscussionOpen as S, setClickClackRuntime as T, isDiscussionSessionKey as _, isClickClackDiscussionChannelRevoked as a, clearDiscussionBindingGeneration as b, discussionAccounts as c, bindingMatchesActiveSessionIncarnation as d, bindingMatchesSessionIncarnation as f, fallbackDiscussionLabel as g, discussionExternalRef as h, clearClickClackDiscussionChannelRevoked as i, normalizedServerBaseUrl as l, discussionCredentialFingerprint as m, buildClickClackTarget as n, markClickClackDiscussionChannelIdentityRevoked as o, getClickClackDiscussionBindingStore as p, parseClickClackTarget as r, markClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, resolveDiscussionBindingAccount as u, resolveDiscussionLabel as v, getClickClackRuntime as w, listPendingDiscussionOpens as x, slugifyDiscussionLabel as y };
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 };
@@ -1,2 +1,2 @@
1
- import { t as clickClackPlugin } from "./channel-BDq0KWDB.js";
1
+ import { t as clickClackPlugin } from "./channel-DfcFlLjC.js";
2
2
  export { clickClackPlugin };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as registerClickClackDiscussions } from "./runtime-api-CV1cOqFO.js";
1
+ import { t as registerClickClackDiscussions } from "./runtime-api-D1H9vheM.js";
2
2
  import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
3
3
  //#region extensions/clickclack/index.ts
4
4
  /**
@@ -1,11 +1,130 @@
1
- import { d as createClickClackClient, f as isClickClackChannelNameConflict, u as ClickClackHttpError, x as resolveClickClackAccount, y as listClickClackAccountIds } from "./setup-surface-Baf26N-D.js";
2
- import { C as reserveDiscussionBindingGeneration, S as recordPendingDiscussionOpen, _ as isDiscussionSessionKey, a as isClickClackDiscussionChannelRevoked, b as clearDiscussionBindingGeneration, c as discussionAccounts, d as bindingMatchesActiveSessionIncarnation, f as bindingMatchesSessionIncarnation, g as fallbackDiscussionLabel, h as discussionExternalRef, i as clearClickClackDiscussionChannelRevoked, l as normalizedServerBaseUrl, m as discussionCredentialFingerprint, o as markClickClackDiscussionChannelIdentityRevoked, p as getClickClackDiscussionBindingStore, s as markClickClackDiscussionChannelRevoked, u as resolveDiscussionBindingAccount, v as resolveDiscussionLabel, x as listPendingDiscussionOpens, y as slugifyDiscussionLabel } from "./channel-BDq0KWDB.js";
1
+ import { d as createClickClackClient, f as isClickClackChannelNameConflict, u as ClickClackHttpError, x as resolveClickClackAccount, y as listClickClackAccountIds } from "./setup-surface-CD6MUC1g.js";
2
+ import { C as listPendingDiscussionOpens, S as clearDiscussionBindingGeneration, T as reserveDiscussionBindingGeneration, _ as fallbackDiscussionLabel, a as isClickClackDiscussionChannelRevoked, b as slugifyDiscussionLabel, c as discussionAccounts, d as resolveDiscussionBindingAccount, f as bindingMatchesActiveSessionIncarnation, g as discussionExternalRef, h as discussionCredentialFingerprint, i as clearClickClackDiscussionChannelRevoked, l as discussionInfoForBinding, m as getClickClackDiscussionBindingStore, o as markClickClackDiscussionChannelIdentityRevoked, p as bindingMatchesSessionIncarnation, s as markClickClackDiscussionChannelRevoked, u as normalizedServerBaseUrl, v as isDiscussionSessionKey, w as recordPendingDiscussionOpen, x as truncateDiscussionDisplayTitle, y as resolveDiscussionLabel } from "./channel-DfcFlLjC.js";
3
3
  import "./api.js";
4
4
  import { resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { registerSessionDiscussionProvider } from "openclaw/plugin-sdk/session-discussion";
7
7
  import { createSessionVisibilityChecker } from "openclaw/plugin-sdk/session-visibility";
8
+ import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
8
9
  import { textResult } from "openclaw/plugin-sdk/tool-results";
10
+ //#region extensions/clickclack/src/discussions/control-session-url.ts
11
+ const SESSION_UUID_SUFFIX_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/iu;
12
+ const SHORT_SESSION_REF_RE = /^(?:.*-)?([0-9a-f]{8,32})$/iu;
13
+ const VALID_AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/iu;
14
+ const INVALID_AGENT_ID_CHARS_RE = /[^a-z0-9_-]+/giu;
15
+ const FIXED_RESERVED_SESSION_RESTS = /* @__PURE__ */ new Set([
16
+ "main",
17
+ "global",
18
+ "boot",
19
+ "sessions"
20
+ ]);
21
+ const SESSION_SLUG_MAX_LENGTH = 48;
22
+ const DEFAULT_AGENT_ID = "main";
23
+ const DEFAULT_MAIN_KEY = "main";
24
+ function optionalString(value) {
25
+ return (value?.trim() ?? "") || null;
26
+ }
27
+ function normalizeAgentId$1(value) {
28
+ const trimmed = value.trim();
29
+ if (!trimmed) return DEFAULT_AGENT_ID;
30
+ if (VALID_AGENT_ID_RE.test(trimmed)) return trimmed.toLowerCase();
31
+ return trimmed.toLowerCase().replace(INVALID_AGENT_ID_CHARS_RE, "-").replace(/^-+|-+$/gu, "").slice(0, 64) || DEFAULT_AGENT_ID;
32
+ }
33
+ function normalizeBasePath(basePath) {
34
+ const trimmed = basePath.trim().replace(/^\/+|\/+$/gu, "");
35
+ return trimmed ? `/${trimmed}` : "";
36
+ }
37
+ function agentSessionKeyParts(sessionKey) {
38
+ const parts = sessionKey.split(":");
39
+ if (parts.length < 3 || parts[0]?.toLowerCase() !== "agent") return null;
40
+ const agentId = optionalString(parts[1]);
41
+ const restSegments = parts.slice(2);
42
+ if (!agentId || restSegments.some((segment) => !segment)) return null;
43
+ return {
44
+ agentId: normalizeAgentId$1(agentId),
45
+ rest: restSegments.join(":")
46
+ };
47
+ }
48
+ function encodePathSegment(segment) {
49
+ if (segment === ".") return "~dot";
50
+ if (segment === "..") return "~dotdot";
51
+ const encoded = encodeURIComponent(segment).replaceAll(".", "%2E");
52
+ return encoded.startsWith("~") ? `~${encoded}` : encoded;
53
+ }
54
+ function isReservedSessionRest(rest, mainKey) {
55
+ const normalized = rest.toLowerCase();
56
+ return FIXED_RESERVED_SESSION_RESTS.has(normalized) || normalized === (optionalString(mainKey)?.toLowerCase() ?? DEFAULT_MAIN_KEY);
57
+ }
58
+ function sessionSlug(displayName) {
59
+ const tokens = (displayName ?? "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").split("-").filter(Boolean);
60
+ while (tokens.length > 0 && /^[0-9a-f]+$/u.test(tokens.at(-1) ?? "")) tokens.pop();
61
+ return tokens.join("-").slice(0, SESSION_SLUG_MAX_LENGTH).replace(/-+$/gu, "");
62
+ }
63
+ function sessionKeyUuid(sessionKey) {
64
+ const uuid = (agentSessionKeyParts(sessionKey)?.rest)?.match(SESSION_UUID_SUFFIX_RE)?.[1];
65
+ return uuid ? uuid.toLowerCase().replaceAll("-", "") : null;
66
+ }
67
+ function sessionPath(params) {
68
+ const rawKey = optionalString(params.sessionKey);
69
+ const parsed = rawKey ? agentSessionKeyParts(rawKey) : null;
70
+ const fallbackAgentId = optionalString(params.fallbackAgentId);
71
+ const agentId = parsed?.agentId ?? (fallbackAgentId ? normalizeAgentId$1(fallbackAgentId) : null);
72
+ if (!rawKey || !agentId || !parsed && rawKey.toLowerCase().startsWith("agent:")) return null;
73
+ const namespace = `${normalizeBasePath(params.basePath)}/chat`;
74
+ const encodedAgentId = encodePathSegment(agentId);
75
+ const rest = parsed?.rest ?? rawKey;
76
+ const normalizedRest = rest.toLowerCase();
77
+ const mainKey = optionalString(params.mainKey)?.toLowerCase() ?? DEFAULT_MAIN_KEY;
78
+ if (!parsed && normalizedRest === DEFAULT_MAIN_KEY || normalizedRest === mainKey || normalizedRest === "global") return `${namespace}/${encodedAgentId}`;
79
+ const uuid = parsed ? sessionKeyUuid(rawKey) : null;
80
+ if (uuid) {
81
+ let length = 8;
82
+ const slug = sessionSlug(params.displayName);
83
+ let sessionRef = `${slug ? `${slug}-` : ""}${uuid.slice(0, length)}`;
84
+ while (length < uuid.length && isReservedSessionRest(sessionRef, params.mainKey)) {
85
+ length += 1;
86
+ sessionRef = `${slug ? `${slug}-` : ""}${uuid.slice(0, length)}`;
87
+ }
88
+ return isReservedSessionRest(sessionRef, params.mainKey) ? null : `${namespace}/${encodedAgentId}/${sessionRef}`;
89
+ }
90
+ const segments = rest.split(":");
91
+ if (segments.some((segment) => !segment)) return null;
92
+ if (segments.length === 1) {
93
+ const segment = segments[0] ?? "";
94
+ if (!isReservedSessionRest(segment, params.mainKey) && SHORT_SESSION_REF_RE.test(segment)) return `${namespace}/${encodedAgentId}/~key/${encodePathSegment(segment)}`;
95
+ }
96
+ return `${namespace}/${encodedAgentId}/${segments.map(encodePathSegment).join("/")}`;
97
+ }
98
+ function controlSessionUrl(baseUrl, sessionKey, fallbackAgentId, mainKey, displayName) {
99
+ if (!baseUrl) return;
100
+ const url = new URL(baseUrl);
101
+ const path = sessionPath({
102
+ sessionKey,
103
+ fallbackAgentId,
104
+ basePath: url.pathname,
105
+ displayName,
106
+ mainKey
107
+ });
108
+ if (!path) return;
109
+ url.pathname = path;
110
+ url.hash = "";
111
+ return url.toString();
112
+ }
113
+ //#endregion
114
+ //#region extensions/clickclack/src/discussions/history-format.ts
115
+ function discussionRecordJson(value) {
116
+ return JSON.stringify(value).replace(/[\u0085\u2028\u2029]/gu, (separator) => `\\u${separator.charCodeAt(0).toString(16).padStart(4, "0")}`);
117
+ }
118
+ /** Renders channel history as line-per-message records for the discussion tool. */
119
+ function formatDiscussionHistory(history) {
120
+ const text = history.messages.map((message) => {
121
+ const author = message.author?.display_name || message.author?.handle || message.author_id || "Unknown";
122
+ return `timestamp=${discussionRecordJson(message.created_at)} [Author ${discussionRecordJson(author)} id=${discussionRecordJson(message.author_id)}] text=${discussionRecordJson(message.body)}`;
123
+ }).join("\n");
124
+ const truncationNote = history.truncated ? "\n[History scan reached its safety bound; older active threads may be omitted.]" : "";
125
+ return text ? `${text}${truncationNote}` : "";
126
+ }
127
+ //#endregion
9
128
  //#region extensions/clickclack/src/discussions/installation.ts
10
129
  const INSTALLATION_NAMESPACE = "discussion-installation";
11
130
  const INSTALLATION_KEY = "current";
@@ -23,6 +142,85 @@ function getClickClackDiscussionInstallationId(runtime) {
23
142
  return store.lookup(INSTALLATION_KEY)?.id ?? id;
24
143
  }
25
144
  //#endregion
145
+ //#region extensions/clickclack/src/discussions/reconcile-scheduler.ts
146
+ const RECONCILE_DEBOUNCE_MS = 250;
147
+ const RECONCILE_RETRY_MS = 1e3;
148
+ const RECONCILE_RETRY_MAX_MS = 6e4;
149
+ /**
150
+ * Per-session debounced reconcile scheduling with failure backoff.
151
+ * Contracts: bursts coalesce onto the earliest scheduled run (steady event
152
+ * traffic can never postpone one); a failed run sets a not-before floor with
153
+ * growing backoff that later events cannot undercut; events during an
154
+ * in-flight run collapse into one follow-up scheduled after settlement; and
155
+ * `supersede()` invalidates callbacks from earlier activations so stop/start
156
+ * cycles cannot corrupt the bookkeeping of the next one.
157
+ */
158
+ var DiscussionReconcileScheduler = class {
159
+ #hooks;
160
+ #timers = /* @__PURE__ */ new Map();
161
+ #retryState = /* @__PURE__ */ new Map();
162
+ #inFlight = /* @__PURE__ */ new Set();
163
+ #followUps = /* @__PURE__ */ new Set();
164
+ #generation = 0;
165
+ constructor(hooks) {
166
+ this.#hooks = hooks;
167
+ }
168
+ /** Invalidates callbacks scheduled by prior activations. */
169
+ supersede() {
170
+ this.#generation += 1;
171
+ }
172
+ clear() {
173
+ for (const scheduled of this.#timers.values()) clearTimeout(scheduled.timer);
174
+ this.#timers.clear();
175
+ this.#retryState.clear();
176
+ this.#inFlight.clear();
177
+ this.#followUps.clear();
178
+ }
179
+ schedule(sessionKey, delayMs = RECONCILE_DEBOUNCE_MS) {
180
+ if (!this.#hooks.shouldSchedule(sessionKey)) return;
181
+ if (this.#inFlight.has(sessionKey)) {
182
+ this.#followUps.add(sessionKey);
183
+ return;
184
+ }
185
+ const retry = this.#retryState.get(sessionKey);
186
+ const fireAt = Math.max(Date.now() + delayMs, retry?.notBefore ?? 0);
187
+ const existing = this.#timers.get(sessionKey);
188
+ if (existing) {
189
+ if (existing.fireAt <= fireAt) return;
190
+ clearTimeout(existing.timer);
191
+ }
192
+ const generation = this.#generation;
193
+ const timer = setTimeout(() => {
194
+ if (generation !== this.#generation) return;
195
+ this.#timers.delete(sessionKey);
196
+ this.#inFlight.add(sessionKey);
197
+ this.#hooks.run(sessionKey).then(() => {
198
+ if (generation !== this.#generation) return;
199
+ this.#retryState.delete(sessionKey);
200
+ }, (error) => {
201
+ if (generation !== this.#generation || !this.#hooks.shouldSchedule(sessionKey)) return;
202
+ const previous = this.#retryState.get(sessionKey);
203
+ const nextRetryMs = Math.min(previous === void 0 ? RECONCILE_RETRY_MS : previous.delayMs * 2, RECONCILE_RETRY_MAX_MS);
204
+ this.#retryState.set(sessionKey, {
205
+ delayMs: nextRetryMs,
206
+ notBefore: Date.now() + nextRetryMs
207
+ });
208
+ this.#hooks.warn(`discussion event reconcile failed for ${sessionKey}; retrying in ${nextRetryMs}ms: ${String(error)}`);
209
+ this.#followUps.add(sessionKey);
210
+ }).finally(() => {
211
+ if (generation !== this.#generation) return;
212
+ this.#inFlight.delete(sessionKey);
213
+ if (this.#followUps.delete(sessionKey)) this.schedule(sessionKey);
214
+ });
215
+ }, Math.max(fireAt - Date.now(), 0));
216
+ timer.unref?.();
217
+ this.#timers.set(sessionKey, {
218
+ fireAt,
219
+ timer
220
+ });
221
+ }
222
+ };
223
+ //#endregion
26
224
  //#region extensions/clickclack/src/discussions/service-open.ts
27
225
  const CHANNEL_NAME_MUTATION_ATTEMPTS$1 = 4;
28
226
  function isDefinitiveNoCreateHttpError(error) {
@@ -34,20 +232,16 @@ function isDefinitiveNoCreateHttpError(error) {
34
232
  429
35
233
  ].includes(error.status);
36
234
  }
37
- function controlSessionUrl(baseUrl, sessionKey) {
38
- if (!baseUrl) return;
39
- const url = new URL(baseUrl);
40
- url.pathname = `${url.pathname.replace(/\/+$/u, "")}/chat`;
41
- url.hash = "";
42
- url.searchParams.set("session", sessionKey);
43
- return url.toString();
44
- }
45
235
  async function resolveAvailableChannelName(params) {
46
236
  const desired = slugifyDiscussionLabel(params.label, params.sessionKey);
47
237
  const channels = params.channels ?? await params.client.channels(params.workspaceId);
48
238
  const occupied = new Set(channels.filter((channel) => channel.id !== params.ownChannelId).map((channel) => channel.name));
49
239
  if (!occupied.has(desired)) return desired;
50
- const fallback = fallbackDiscussionLabel(params.sessionKey);
240
+ for (let suffix = 2; suffix <= 20; suffix += 1) {
241
+ const candidate = `${desired}-${suffix}`;
242
+ if (!occupied.has(candidate)) return candidate;
243
+ }
244
+ const fallback = fallbackDiscussionLabel(params.sessionKey, params.agentId);
51
245
  if (!occupied.has(fallback)) return fallback;
52
246
  for (let suffix = 2;; suffix += 1) {
53
247
  const candidate = `${fallback}-${suffix}`;
@@ -60,16 +254,19 @@ function assertChannelPatch(channel, patch) {
60
254
  "external_managed",
61
255
  "external_ref",
62
256
  "external_url",
257
+ "display_title",
63
258
  "name",
64
259
  "sidebar_section"
65
260
  ]) {
66
261
  const expected = patch[key];
67
262
  if (expected === void 0) continue;
263
+ if (key === "display_title" && !(key in channel)) continue;
68
264
  if ((key === "external_ref" || key === "external_url" || key === "sidebar_section" ? channel[key] ?? "" : channel[key]) !== expected) throw new Error(`ClickClack channel update did not apply ${key}`);
69
265
  }
70
266
  }
71
267
  function assertManagedChannelContract(channel, expected) {
72
- if (channel.external_managed !== true || (channel.external_ref ?? "") !== (expected.externalRef ?? "") || (channel.sidebar_section ?? "") !== (expected.section ?? "") || (channel.external_url ?? "") !== (expected.externalUrl ?? "")) throw new Error(`ClickClack server does not support the managed discussion channel contract for ${expected.sessionKey}`);
268
+ const displayTitleMatches = !("display_title" in channel) || channel.display_title === expected.displayTitle;
269
+ if (channel.external_managed !== true || (channel.external_ref ?? "") !== (expected.externalRef ?? "") || (channel.sidebar_section ?? "") !== (expected.section ?? "") || (channel.external_url ?? "") !== (expected.externalUrl ?? "") || !displayTitleMatches) throw new Error(`ClickClack server does not support the managed discussion channel contract for ${expected.sessionKey}`);
73
270
  }
74
271
  function assertManagedChannelListContract(channels) {
75
272
  if (channels.some((channel) => typeof channel.external_managed !== "boolean" || channel.external_ref !== void 0 && typeof channel.external_ref !== "string" || channel.external_url !== void 0 && typeof channel.external_url !== "string" || channel.sidebar_section !== void 0 && typeof channel.sidebar_section !== "string")) throw new Error("ClickClack server does not advertise the managed discussion contract");
@@ -93,9 +290,13 @@ async function openClickClackDiscussionBinding(params) {
93
290
  await params.reconcilePendingOpen(unresolved);
94
291
  if (listPendingDiscussionOpens(runtime).some((pending) => pending.sessionKey === sessionKey)) throw new Error("A previous ClickClack discussion open is still unresolved; restore its credential and retry");
95
292
  }
96
- const label = resolveDiscussionLabel(entry.label, sessionKey);
293
+ const config = runtime.config.current();
294
+ const agentId = resolveAgentIdFromSessionKey(sessionKey, resolveDefaultAgentId(config));
295
+ const fallback = fallbackDiscussionLabel(sessionKey, agentId);
296
+ const label = resolveDiscussionLabel(entry, sessionKey, agentId);
297
+ const displayTitle = label === fallback ? "" : truncateDiscussionDisplayTitle(label);
97
298
  const section = entry.category?.trim() || account.discussions.section;
98
- const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey);
299
+ const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? "main", config.session?.mainKey, label);
99
300
  const archived = entry.archivedAt !== void 0;
100
301
  return await params.withChannelMutationLock(async () => {
101
302
  if (!store.hasCapacity(sessionKey)) throw new Error("ClickClack discussion binding capacity is exhausted");
@@ -120,13 +321,15 @@ async function openClickClackDiscussionBinding(params) {
120
321
  workspaceId: workspace.id,
121
322
  label,
122
323
  sessionKey,
324
+ agentId,
123
325
  channels,
124
326
  ownChannelId: adopted?.id
125
327
  }),
126
328
  external_managed: true,
127
329
  external_ref: externalRef,
128
330
  external_url: externalUrl ?? "",
129
- sidebar_section: section
331
+ sidebar_section: section,
332
+ display_title: displayTitle
130
333
  };
131
334
  recordPendingDiscussionOpen({
132
335
  runtime,
@@ -213,7 +416,8 @@ async function openClickClackDiscussionBinding(params) {
213
416
  sessionKey,
214
417
  externalRef,
215
418
  section,
216
- externalUrl
419
+ externalUrl,
420
+ displayTitle
217
421
  });
218
422
  if (adopted) assertChannelPatch(resolved, {
219
423
  ...managedFields,
@@ -252,7 +456,7 @@ async function openClickClackDiscussionBinding(params) {
252
456
  }
253
457
  const nextBinding = {
254
458
  accountId: account.accountId,
255
- agentId: resolveAgentIdFromSessionKey(sessionKey),
459
+ agentId,
256
460
  sessionId: entry.sessionId,
257
461
  serverBaseUrl,
258
462
  credentialFingerprint,
@@ -265,7 +469,8 @@ async function openClickClackDiscussionBinding(params) {
265
469
  workspaceRouteId: workspace.route_id,
266
470
  section,
267
471
  archived,
268
- label
472
+ label,
473
+ displayTitle: "display_title" in channel ? channel.display_title : void 0
269
474
  };
270
475
  const currentEntry = runtime.agent.session.getSessionEntry({
271
476
  sessionKey,
@@ -307,17 +512,6 @@ async function openClickClackDiscussionBinding(params) {
307
512
  //#region extensions/clickclack/src/discussions/service.ts
308
513
  const RECONCILE_INTERVAL_MS = 6e4;
309
514
  const CHANNEL_NAME_MUTATION_ATTEMPTS = 4;
310
- function discussionInfoForBinding(binding, account) {
311
- const baseUrl = normalizedServerBaseUrl(account);
312
- return {
313
- state: "open",
314
- embedUrl: `${baseUrl}/embed/channel/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`,
315
- openUrl: `${baseUrl}/app/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`
316
- };
317
- }
318
- function discussionRecordJson(value) {
319
- return JSON.stringify(value).replace(/[\u0085\u2028\u2029]/gu, (separator) => `\\u${separator.charCodeAt(0).toString(16).padStart(4, "0")}`);
320
- }
321
515
  var ClickClackDiscussionService = class {
322
516
  #runtime;
323
517
  #store;
@@ -326,9 +520,16 @@ var ClickClackDiscussionService = class {
326
520
  #bindingGenerationFactory;
327
521
  #timersEnabled;
328
522
  #sessionLocks = /* @__PURE__ */ new Map();
523
+ #reconcileScheduler = new DiscussionReconcileScheduler({
524
+ shouldSchedule: (sessionKey) => !this.#closed && (this.#store.get(sessionKey) !== void 0 || listPendingDiscussionOpens(this.#runtime).some((pending) => pending.sessionKey === sessionKey)),
525
+ run: async (sessionKey) => await this.reconcile(sessionKey),
526
+ warn: (message) => this.#logger().warn(message)
527
+ });
329
528
  #channelMutationLock = Promise.resolve();
330
529
  #timer;
331
530
  #reconcileAllPromise;
531
+ #unsubscribeSessionsChanged;
532
+ #closed = false;
332
533
  constructor(runtime, options = {}) {
333
534
  this.#runtime = runtime;
334
535
  this.#store = getClickClackDiscussionBindingStore(runtime);
@@ -344,6 +545,17 @@ var ClickClackDiscussionService = class {
344
545
  info: async ({ sessionKey }) => await this.info(sessionKey),
345
546
  open: async ({ sessionKey }) => await this.open(sessionKey)
346
547
  };
548
+ if (options.gatewayEvents) this.bindGatewayEvents(options.gatewayEvents);
549
+ if (this.#timersEnabled) this.#ensureTimer();
550
+ }
551
+ bindGatewayEvents(gatewayEvents) {
552
+ this.#unsubscribeSessionsChanged?.();
553
+ this.#closed = false;
554
+ this.#reconcileScheduler.supersede();
555
+ this.#unsubscribeSessionsChanged = gatewayEvents?.onSessionsChanged((event) => {
556
+ this.#reconcileScheduler.schedule(event.sessionKey);
557
+ });
558
+ for (const sessionKey of this.#listReconcileSessionKeys()) this.#reconcileScheduler.schedule(sessionKey, 0);
347
559
  if (this.#timersEnabled) this.#ensureTimer();
348
560
  }
349
561
  hasEnabledAccount() {
@@ -391,44 +603,49 @@ var ClickClackDiscussionService = class {
391
603
  if (current) return discussionInfoForBinding(current, resolved.account);
392
604
  }
393
605
  }
394
- const binding = await openClickClackDiscussionBinding({
395
- runtime: this.#runtime,
396
- store: this.#store,
397
- account,
398
- clientFactory: this.#clientFactory,
399
- installationId: this.#installationId,
400
- bindingGenerationFactory: this.#bindingGenerationFactory,
401
- sessionKey,
402
- ensureTimer: () => this.#ensureTimer(),
403
- reconcilePendingOpen: async (pending) => await this.#reconcilePendingOpen(pending, { allowRetry: false }),
404
- withChannelMutationLock: async (run) => await this.#withChannelMutationLock(run),
405
- finalizePendingBinding: (key, nextBinding) => this.#finalizePendingBinding(key, nextBinding),
406
- warn: (message) => this.#logger().warn(message)
407
- });
606
+ let binding;
607
+ try {
608
+ binding = await openClickClackDiscussionBinding({
609
+ runtime: this.#runtime,
610
+ store: this.#store,
611
+ account,
612
+ clientFactory: this.#clientFactory,
613
+ installationId: this.#installationId,
614
+ bindingGenerationFactory: this.#bindingGenerationFactory,
615
+ sessionKey,
616
+ ensureTimer: () => this.#ensureTimer(),
617
+ reconcilePendingOpen: async (pending) => await this.#reconcilePendingOpen(pending, { allowRetry: false }),
618
+ withChannelMutationLock: async (run) => await this.#withChannelMutationLock(run),
619
+ finalizePendingBinding: (key, nextBinding) => this.#finalizePendingBinding(key, nextBinding),
620
+ warn: (message) => this.#logger().warn(message)
621
+ });
622
+ } finally {
623
+ this.#ensureTimer();
624
+ }
408
625
  if (!binding) return { state: "available" };
409
- this.#ensureTimer();
410
626
  return discussionInfoForBinding(binding, account);
411
627
  });
412
628
  }
413
629
  async reconcile(sessionKey) {
414
- await this.#withSessionLock(sessionKey, async () => {
415
- const binding = this.#store.get(sessionKey);
416
- if (binding) await this.#reconcileBinding(sessionKey, binding);
417
- });
630
+ try {
631
+ await this.#withSessionLock(sessionKey, async () => {
632
+ const binding = this.#store.get(sessionKey);
633
+ if (binding) await this.#reconcileBinding(sessionKey, binding);
634
+ });
635
+ const pending = listPendingDiscussionOpens(this.#runtime).find((candidate) => candidate.sessionKey === sessionKey);
636
+ if (pending) await this.#reconcilePendingOpen(pending);
637
+ } finally {
638
+ this.#ensureTimer();
639
+ }
418
640
  }
419
641
  async reconcileAll() {
420
642
  if (this.#reconcileAllPromise) return await this.#reconcileAllPromise;
421
643
  this.#reconcileAllPromise = (async () => {
422
- for (const { sessionKey } of this.#store.entries()) try {
644
+ for (const sessionKey of this.#listReconcileSessionKeys()) try {
423
645
  await this.reconcile(sessionKey);
424
646
  } catch (error) {
425
647
  this.#logger().warn(`discussion reconcile failed for ${sessionKey}: ${String(error)}`);
426
648
  }
427
- for (const pending of listPendingDiscussionOpens(this.#runtime)) try {
428
- await this.#reconcilePendingOpen(pending);
429
- } catch (error) {
430
- this.#logger().warn(`discussion pending-open reconcile failed for ${pending.sessionKey}: ${String(error)}`);
431
- }
432
649
  })().finally(() => {
433
650
  this.#reconcileAllPromise = void 0;
434
651
  });
@@ -447,18 +664,17 @@ var ClickClackDiscussionService = class {
447
664
  serverBaseUrl: binding.serverBaseUrl,
448
665
  channelId: binding.channelId
449
666
  })) return { text: "No discussion is bound to this session." };
450
- const history = await this.#clientFactory(resolved.account).latestChannelMessages(binding.channelId, limit);
451
- const text = history.messages.map((message) => {
452
- const author = message.author?.display_name || message.author?.handle || message.author_id || "Unknown";
453
- return `timestamp=${discussionRecordJson(message.created_at)} [Author ${discussionRecordJson(author)} id=${discussionRecordJson(message.author_id)}] text=${discussionRecordJson(message.body)}`;
454
- }).join("\n");
455
- const truncationNote = history.truncated ? "\n[History scan reached its safety bound; older active threads may be omitted.]" : "";
456
667
  return {
457
668
  binding,
458
- text: text ? `${text}${truncationNote}` : "The bound discussion has no messages yet."
669
+ text: formatDiscussionHistory(await this.#clientFactory(resolved.account).latestChannelMessages(binding.channelId, limit)) || "The bound discussion has no messages yet."
459
670
  };
460
671
  }
461
672
  cleanup() {
673
+ this.#closed = true;
674
+ this.#unsubscribeSessionsChanged?.();
675
+ this.#unsubscribeSessionsChanged = void 0;
676
+ this.#reconcileScheduler.supersede();
677
+ this.#reconcileScheduler.clear();
462
678
  if (this.#timer) {
463
679
  clearInterval(this.#timer);
464
680
  this.#timer = void 0;
@@ -499,12 +715,17 @@ var ClickClackDiscussionService = class {
499
715
  }
500
716
  const archived = entry ? entry.archivedAt !== void 0 : true;
501
717
  const deleted = entry === void 0;
502
- const label = entry ? resolveDiscussionLabel(entry.label, sessionKey) : binding.label;
718
+ const fallback = fallbackDiscussionLabel(sessionKey, binding.agentId);
719
+ const label = entry ? resolveDiscussionLabel(entry, sessionKey, binding.agentId) : binding.label;
503
720
  const section = entry?.category?.trim() || account.discussions.section;
504
- const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey) ?? "";
721
+ const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? "main", this.#currentConfig().session?.mainKey, label) ?? "";
505
722
  const patch = {};
506
723
  if (archived !== binding.archived) patch.archived = archived;
507
724
  const labelChanged = label !== binding.label;
725
+ const desiredDisplayTitle = label === fallback ? "" : truncateDiscussionDisplayTitle(label);
726
+ const serverSupportsDisplayTitle = this.#store.entries().some(({ binding: candidate }) => candidate.displayTitle !== void 0 && candidate.serverBaseUrl === binding.serverBaseUrl && candidate.accountId === binding.accountId);
727
+ const shouldBackfillDisplayTitle = desiredDisplayTitle !== "" && binding.displayTitle !== desiredDisplayTitle && serverSupportsDisplayTitle;
728
+ if (labelChanged || shouldBackfillDisplayTitle) patch.display_title = desiredDisplayTitle;
508
729
  if (section !== binding.section) patch.sidebar_section = section;
509
730
  if (externalUrl !== binding.externalUrl) patch.external_url = externalUrl;
510
731
  if (Object.keys(patch).length === 0 && !labelChanged) {
@@ -512,24 +733,31 @@ var ClickClackDiscussionService = class {
512
733
  return;
513
734
  }
514
735
  const client = this.#clientFactory(account);
515
- if (labelChanged) await this.#withChannelMutationLock(async () => {
736
+ let updated;
737
+ if (labelChanged) updated = await this.#withChannelMutationLock(async () => {
516
738
  for (let attempt = 0; attempt < CHANNEL_NAME_MUTATION_ATTEMPTS; attempt += 1) {
517
739
  patch.name = await resolveAvailableChannelName({
518
740
  client,
519
741
  workspaceId: binding.workspaceId,
520
742
  label,
521
743
  sessionKey,
744
+ agentId: binding.agentId,
522
745
  ownChannelId: binding.channelId
523
746
  });
524
747
  try {
525
- assertChannelPatch(await client.updateChannel(binding.channelId, patch), patch);
526
- return;
748
+ const renamed = await client.updateChannel(binding.channelId, patch);
749
+ assertChannelPatch(renamed, patch);
750
+ return renamed;
527
751
  } catch (error) {
528
752
  if (!isClickClackChannelNameConflict(error) || attempt === CHANNEL_NAME_MUTATION_ATTEMPTS - 1) throw error;
529
753
  }
530
754
  }
755
+ throw new Error("ClickClack discussion channel name retries were exhausted");
531
756
  });
532
- else assertChannelPatch(await client.updateChannel(binding.channelId, patch), patch);
757
+ else {
758
+ updated = await client.updateChannel(binding.channelId, patch);
759
+ assertChannelPatch(updated, patch);
760
+ }
533
761
  if (deleted) {
534
762
  this.#revokeAndDeleteBinding(sessionKey, binding);
535
763
  return;
@@ -539,7 +767,8 @@ var ClickClackDiscussionService = class {
539
767
  archived,
540
768
  externalUrl,
541
769
  label,
542
- section
770
+ section,
771
+ displayTitle: "display_title" in updated ? updated.display_title : void 0
543
772
  });
544
773
  }
545
774
  async #reconcilePendingOpen(pending, options = {}) {
@@ -637,19 +866,28 @@ var ClickClackDiscussionService = class {
637
866
  return this.#runtime.config.current();
638
867
  }
639
868
  #ensureTimer() {
640
- if (!this.#timersEnabled || this.#timer || this.#store.entries().length === 0 && listPendingDiscussionOpens(this.#runtime).length === 0) return;
869
+ const hasPendingOpens = listPendingDiscussionOpens(this.#runtime).length > 0;
870
+ const needsBindingPoll = this.#unsubscribeSessionsChanged === void 0 && this.#store.entries().length > 0;
871
+ if (this.#closed || !hasPendingOpens && !needsBindingPoll) {
872
+ if (this.#timer) {
873
+ clearInterval(this.#timer);
874
+ this.#timer = void 0;
875
+ }
876
+ return;
877
+ }
878
+ if (!this.#timersEnabled || this.#timer) return;
641
879
  this.#timer = setInterval(() => {
642
880
  this.reconcileAll().catch((error) => {
643
881
  this.#logger().warn(`discussion reconcile pass failed: ${String(error)}`);
644
882
  }).finally(() => {
645
- if (this.#store.entries().length === 0 && listPendingDiscussionOpens(this.#runtime).length === 0 && this.#timer) {
646
- clearInterval(this.#timer);
647
- this.#timer = void 0;
648
- }
883
+ this.#ensureTimer();
649
884
  });
650
885
  }, RECONCILE_INTERVAL_MS);
651
886
  this.#timer.unref?.();
652
887
  }
888
+ #listReconcileSessionKeys() {
889
+ return /* @__PURE__ */ new Set([...this.#store.entries().map(({ sessionKey }) => sessionKey), ...listPendingDiscussionOpens(this.#runtime).map(({ sessionKey }) => sessionKey)]);
890
+ }
653
891
  #logger() {
654
892
  return this.#runtime.logging.getChildLogger({
655
893
  plugin: "clickclack",
@@ -751,6 +989,15 @@ function registerClickClackDiscussions(api) {
751
989
  return;
752
990
  }
753
991
  const service = new ClickClackDiscussionService(api.runtime);
992
+ api.registerService({
993
+ id: "clickclack-discussion-session-events",
994
+ start: ({ gatewayEvents }) => {
995
+ service.bindGatewayEvents(gatewayEvents);
996
+ },
997
+ stop: () => {
998
+ service.cleanup();
999
+ }
1000
+ });
754
1001
  api.registerTool((context) => createClickClackDiscussionTool({
755
1002
  service,
756
1003
  sessionKey: context.sessionKey
@@ -1,4 +1,4 @@
1
- import { d as createClickClackClient, x as resolveClickClackAccount } from "./setup-surface-Baf26N-D.js";
2
- import { T as setClickClackRuntime, r as parseClickClackTarget } from "./channel-BDq0KWDB.js";
3
- import { t as registerClickClackDiscussions } from "./runtime-api-CV1cOqFO.js";
1
+ import { d as createClickClackClient, x as resolveClickClackAccount } from "./setup-surface-CD6MUC1g.js";
2
+ import { D as setClickClackRuntime, r as parseClickClackTarget } from "./channel-DfcFlLjC.js";
3
+ import { t as registerClickClackDiscussions } from "./runtime-api-D1H9vheM.js";
4
4
  export { createClickClackClient, parseClickClackTarget, registerClickClackDiscussions, resolveClickClackAccount, setClickClackRuntime };
@@ -1,4 +1,4 @@
1
- import { a as buildClickClackSetupClaimUrl, o as isClickClackSetupLoopbackHost, s as requireClickClackSetupApiBaseUrl } from "./setup-surface-Baf26N-D.js";
1
+ import { a as buildClickClackSetupClaimUrl, o as isClickClackSetupLoopbackHost, s as requireClickClackSetupApiBaseUrl } from "./setup-surface-CD6MUC1g.js";
2
2
  import { createProviderOperationDeadline, readProviderJsonResponse, readResponseTextLimited, resolveProviderOperationTimeoutMs } from "openclaw/plugin-sdk/provider-http";
3
3
  import { fetchWithSsrFGuard, resolvePinnedHostnameWithPolicy, ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
4
4
  import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -1,4 +1,4 @@
1
- import { _ as clickClackMeta, g as clickClackConfigAdapter, m as clickClackConfigSchema, n as clickClackSetupAdapter, r as clickClackSetupContract, t as clickClackSetupWizard } from "./setup-surface-Baf26N-D.js";
1
+ import { _ as clickClackMeta, g as clickClackConfigAdapter, m as clickClackConfigSchema, n as clickClackSetupAdapter, r as clickClackSetupContract, t as clickClackSetupWizard } from "./setup-surface-CD6MUC1g.js";
2
2
  //#region extensions/clickclack/src/channel.setup.ts
3
3
  const clickClackSetupPlugin = {
4
4
  id: "clickclack",
@@ -1,6 +1,5 @@
1
1
  import { createAccountListHelpers, hasConfiguredAccountValue } from "openclaw/plugin-sdk/account-helpers";
2
2
  import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
- import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
4
3
  import { resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution-runtime";
5
4
  import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
6
5
  import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
@@ -15,7 +14,7 @@ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
15
14
  import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
16
15
  import { WebSocket } from "ws";
17
16
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
18
- import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, applyAccountNameToChannelSection, applySetupAccountConfigPatch, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, hasConfiguredSecretInput, migrateBaseNameToDefaultAccount, moveSingleAccountChannelSectionToDefaultAccount, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
17
+ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, hasConfiguredSecretInput, migrateBaseNameToDefaultAccount, moveSingleAccountChannelSectionToDefaultAccount, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
19
18
  import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup-runtime";
20
19
  import net from "node:net";
21
20
  //#region \0rolldown/runtime.js
@@ -39,8 +38,10 @@ const DEFAULT_RECONNECT_MS = 1500;
39
38
  const MIN_RECONNECT_MS = 100;
40
39
  const MAX_RECONNECT_MS = 6e4;
41
40
  const DEFAULT_DISCUSSIONS_SECTION = "Sessions";
42
- const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
41
+ const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId, resolveAccountConfig: resolveMergedClickClackAccountConfig } = createAccountListHelpers("clickclack", {
43
42
  normalizeAccountId,
43
+ omitKeys: ["defaultAccount"],
44
+ nestedObjectKeys: ["discussions"],
44
45
  hasImplicitDefaultAccount: (cfg) => {
45
46
  const channel = cfg.channels?.clickclack;
46
47
  return Boolean(channel?.baseUrl?.trim() && (hasConfiguredAccountValue(channel.token) || Boolean(channel.tokenFile?.trim()) || Boolean(process.env.CLICKCLACK_BOT_TOKEN?.trim())) && channel.workspace?.trim());
@@ -48,14 +49,7 @@ const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resol
48
49
  });
49
50
  function resolveClickClackAccountConfig(cfg, accountId) {
50
51
  const channel = cfg.channels?.clickclack;
51
- const merged = resolveMergedAccountConfig({
52
- channelConfig: cfg.channels?.clickclack,
53
- accounts: channel?.accounts,
54
- accountId,
55
- omitKeys: ["defaultAccount"],
56
- nestedObjectKeys: ["discussions"],
57
- normalizeAccountId
58
- });
52
+ const merged = resolveMergedClickClackAccountConfig(cfg, accountId);
59
53
  const account = resolveNormalizedAccountEntry(channel?.accounts, accountId, normalizeAccountId);
60
54
  const accountTokenFile = account?.tokenFile?.trim();
61
55
  if (accountTokenFile) return {
@@ -682,59 +676,24 @@ function applyClickClackSetupConfigPatch(params) {
682
676
  accountId,
683
677
  name: params.name
684
678
  });
685
- return applySetupAccountConfigPatch({
679
+ return patchScopedAccountConfig({
686
680
  cfg: accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
687
681
  cfg: namedConfig,
688
682
  channelKey: channel$1
689
683
  }) : namedConfig,
690
684
  channelKey: channel$1,
691
685
  accountId,
692
- patch: params.patch
686
+ patch: params.patch,
687
+ ...params.clearFields ? { clearFields: params.clearFields } : {}
693
688
  });
694
689
  }
695
- function clearClickClackSetupConfigFields(params) {
696
- const clickclack = params.cfg.channels?.clickclack;
697
- if (!clickclack) return params.cfg;
698
- const accountId = normalizeAccountId(params.accountId);
699
- if (accountId === DEFAULT_ACCOUNT_ID) {
700
- const nextClickClack = { ...clickclack };
701
- for (const field of params.fields) delete nextClickClack[field];
702
- return {
703
- ...params.cfg,
704
- channels: {
705
- ...params.cfg.channels,
706
- clickclack: nextClickClack
707
- }
708
- };
709
- }
710
- const currentAccount = clickclack.accounts?.[accountId];
711
- if (!currentAccount) return params.cfg;
712
- const nextAccount = { ...currentAccount };
713
- for (const field of params.fields) delete nextAccount[field];
714
- return {
715
- ...params.cfg,
716
- channels: {
717
- ...params.cfg.channels,
718
- clickclack: {
719
- ...clickclack,
720
- accounts: {
721
- ...clickclack.accounts,
722
- [accountId]: nextAccount
723
- }
724
- }
725
- }
726
- };
727
- }
728
690
  function applyClickClackCredentialConfig(params) {
729
691
  const fieldsToClear = params.useEnv ? ["token", "tokenFile"] : params.tokenFile ? ["token"] : params.token !== void 0 ? ["tokenFile"] : [];
730
- return clearClickClackSetupConfigFields({
731
- cfg: applyClickClackSetupConfigPatch({
732
- cfg: params.cfg,
733
- accountId: params.accountId,
734
- patch: params.useEnv ? {} : params.tokenFile ? { tokenFile: params.tokenFile } : params.token !== void 0 ? { token: params.token } : {}
735
- }),
692
+ return applyClickClackSetupConfigPatch({
693
+ cfg: params.cfg,
736
694
  accountId: params.accountId,
737
- fields: fieldsToClear
695
+ clearFields: fieldsToClear,
696
+ patch: params.useEnv ? {} : params.tokenFile ? { tokenFile: params.tokenFile } : params.token !== void 0 ? { token: params.token } : {}
738
697
  });
739
698
  }
740
699
  const clickClackSetupAdapter = {
@@ -751,7 +710,7 @@ const clickClackSetupAdapter = {
751
710
  const claimUrl = setup.exactClaimUrl && !privateApiBaseUrl ? setup.exactClaimUrl : buildClickClackSetupClaimUrl(privateApiBaseUrl ?? setup.baseUrl);
752
711
  let claim;
753
712
  try {
754
- const { claimClickClackSetupCode } = await import("./setup-claim-CJIUfZS7.js");
713
+ const { claimClickClackSetupCode } = await import("./setup-claim-BFU16zoo.js");
755
714
  claim = await claimClickClackSetupCode({
756
715
  claimUrl,
757
716
  code: setup.code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/clickclack",
3
- "version": "2026.7.2-beta.4",
3
+ "version": "2026.7.2-beta.5",
4
4
  "description": "OpenClaw ClickClack channel plugin",
5
5
  "type": "module",
6
6
  "exports": {
@@ -13,7 +13,7 @@
13
13
  "zod": "4.4.3"
14
14
  },
15
15
  "peerDependencies": {
16
- "openclaw": ">=2026.7.2-beta.4"
16
+ "openclaw": ">=2026.7.2-beta.5"
17
17
  },
18
18
  "peerDependenciesMeta": {
19
19
  "openclaw": {
@@ -136,10 +136,10 @@
136
136
  "allowInvalidConfigRecovery": true
137
137
  },
138
138
  "compat": {
139
- "pluginApi": ">=2026.7.2-beta.4"
139
+ "pluginApi": ">=2026.7.2-beta.5"
140
140
  },
141
141
  "build": {
142
- "openclawVersion": "2026.7.2-beta.4",
142
+ "openclawVersion": "2026.7.2-beta.5",
143
143
  "bundledDist": false
144
144
  },
145
145
  "release": {
@@ -158,7 +158,6 @@
158
158
  "files": [
159
159
  "dist/**",
160
160
  "openclaw.plugin.json",
161
- "npm-shrinkwrap.json",
162
161
  "README.md"
163
162
  ],
164
163
  "bundledDependencies": [
@@ -1,54 +0,0 @@
1
- {
2
- "name": "@openclaw/clickclack",
3
- "version": "2026.7.2-beta.4",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "@openclaw/clickclack",
9
- "version": "2026.7.2-beta.4",
10
- "dependencies": {
11
- "ws": "8.21.1",
12
- "zod": "4.4.3"
13
- },
14
- "peerDependencies": {
15
- "openclaw": ">=2026.7.2-beta.4"
16
- },
17
- "peerDependenciesMeta": {
18
- "openclaw": {
19
- "optional": true
20
- }
21
- }
22
- },
23
- "node_modules/ws": {
24
- "version": "8.21.1",
25
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
26
- "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
27
- "license": "MIT",
28
- "engines": {
29
- "node": ">=10.0.0"
30
- },
31
- "peerDependencies": {
32
- "bufferutil": "^4.0.1",
33
- "utf-8-validate": ">=5.0.2"
34
- },
35
- "peerDependenciesMeta": {
36
- "bufferutil": {
37
- "optional": true
38
- },
39
- "utf-8-validate": {
40
- "optional": true
41
- }
42
- }
43
- },
44
- "node_modules/zod": {
45
- "version": "4.4.3",
46
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
47
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
48
- "license": "MIT",
49
- "funding": {
50
- "url": "https://github.com/sponsors/colinhacks"
51
- }
52
- }
53
- }
54
- }