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

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
@@ -10,6 +10,18 @@ openclaw plugins install @openclaw/clickclack
10
10
 
11
11
  ## Setup
12
12
 
13
+ The recommended setup path uses the one-time command generated by ClickClack:
14
+
15
+ ```sh
16
+ openclaw channels add clickclack --code 'https://clickclack.example.com/#XXXX-XXXX-XXXX'
17
+ ```
18
+
19
+ Split-origin and path-mounted ClickClack deployments generate an exact
20
+ `/api/bot-setup-codes/claim#CODE` endpoint. OpenClaw validates the versioned
21
+ claim response and saves its canonical API base.
22
+
23
+ For manual token setup:
24
+
13
25
  ```sh
14
26
  openclaw channels add clickclack \
15
27
  --base-url https://clickclack.example.com \
@@ -24,6 +36,31 @@ Run `openclaw onboard` for guided setup. The workspace value can be a
24
36
  For the default account only, `--use-env` reads `CLICKCLACK_BOT_TOKEN`; config
25
37
  storage is the normal setup path.
26
38
 
39
+ ### Public and private endpoints
40
+
41
+ `baseUrl` is the public ClickClack URL used in links that people open. Set the
42
+ optional `apiBaseUrl` when the gateway should call ClickClack through a
43
+ different server-to-server endpoint:
44
+
45
+ ```json5
46
+ {
47
+ channels: {
48
+ clickclack: {
49
+ baseUrl: "https://clack.openclaw.ai",
50
+ apiBaseUrl: "http://127.0.0.1:8484",
51
+ token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" },
52
+ workspace: "default",
53
+ },
54
+ },
55
+ }
56
+ ```
57
+
58
+ This same-host pattern lets the public hostname stay fully protected by an
59
+ authentication gateway such as Cloudflare Access while the local OpenClaw
60
+ gateway talks directly to ClickClack. REST requests, setup verification, and
61
+ the realtime WebSocket use `apiBaseUrl`; browser-facing discussion links keep
62
+ using `baseUrl`. When `apiBaseUrl` is unset, it defaults to `baseUrl`.
63
+
27
64
  ## Command menus
28
65
 
29
66
  ClickClack command menus are enabled by default. At gateway startup, the
@@ -35,6 +72,89 @@ Set `commandMenu: false` on an account to disable menu sync. Sync failures do
35
72
  not prevent the gateway from starting, so older tokens and ClickClack servers
36
73
  continue to work without a menu.
37
74
 
75
+ ## Discussions
76
+
77
+ ClickClack can create one managed channel for each OpenClaw session:
78
+
79
+ The account token needs `channels:write`, which is included in `bot:admin` but
80
+ not in the normal `bot:write` setup token. The ClickClack server must also
81
+ support and return the managed-channel fields used by this integration.
82
+
83
+ ```json5
84
+ {
85
+ channels: {
86
+ clickclack: {
87
+ baseUrl: "https://clickclack.example.com",
88
+ token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" },
89
+ workspace: "default",
90
+ discussions: {
91
+ enabled: true,
92
+ workspace: "default",
93
+ controlUrlBase: "https://team.openclaw.ai",
94
+ section: "Sessions",
95
+ },
96
+ },
97
+ },
98
+ }
99
+ ```
100
+
101
+ Opening a session discussion creates a public, externally managed channel and
102
+ stores its binding in the ClickClack plugin's SQLite state. Session archive,
103
+ restore, label, category, and deletion changes are reflected in the channel;
104
+ deletion archives the channel and never deletes its messages. `workspace`
105
+ defaults to the account workspace, and `section` defaults to `Sessions`.
106
+ `controlUrlBase` adds a link back to `/chat?session=<session-key>` in the
107
+ OpenClaw Control UI.
108
+
109
+ Enable discussions on exactly one ClickClack account. Multiple enabled
110
+ discussion accounts are rejected because the session discussion provider does
111
+ not have an account selector.
112
+
113
+ Messages in the managed channel run in a stable side session under the same
114
+ agent id as the attached main session. The plugin installs a scoped host grant
115
+ for `sessions_history`, `session_status`, and `sessions_send` between that side
116
+ session and its attached main session, so `tools.sessions.visibility` can stay
117
+ at its safer default `tree`. A second host-side policy blocks session discovery
118
+ and alternate targets; the side-agent prompt is not the authorization boundary.
119
+ The agent still needs those three tools in its effective tool allowlist.
120
+
121
+ The binding, managed ownership reference, and side-session identity include the
122
+ concrete OpenClaw session id as well as the pinned server and channel. Resetting
123
+ a reusable session key, replacing a binding, or retargeting it therefore revokes
124
+ the old channel locally, archives it when the old credential remains usable, and
125
+ starts a fresh channel and side transcript.
126
+ Messages arriving through an archived, reset, disabled, or retargeted managed
127
+ binding are dropped instead of falling back to the account's normal channel
128
+ routing. Released bindings leave a durable revoked-channel marker so delayed
129
+ realtime events remain fail-closed. Remote ownership is keyed by ClickClack
130
+ server and channel id, so renaming the local account cannot turn a managed
131
+ channel into an ordinary one.
132
+
133
+ Managed-channel ownership references include a durable per-installation id, so
134
+ two OpenClaw gateways using the same ClickClack workspace do not adopt each
135
+ other's discussion channels. They also include the destination and a durable
136
+ binding generation, so an account or workspace round trip cannot re-adopt a
137
+ previous channel. Changing or removing `controlUrlBase` is reflected on the next
138
+ lifecycle reconciliation pass.
139
+
140
+ If a channel-create response is lost, the pending ownership reservation
141
+ temporarily quarantines otherwise-unbound events in that workspace. The same
142
+ coarse reconciler then adopts the created channel or clears/archives the
143
+ ambiguous attempt; a reset cannot make the old channel fall through to ordinary
144
+ routing.
145
+
146
+ When a workspace move keeps the original workspace credential configured, the
147
+ plugin archives the old channel before release. If the token is replaced with a
148
+ workspace-scoped credential that cannot access the old workspace, OpenClaw
149
+ releases the binding into the revoked-channel marker without trying the new token
150
+ against the old channel; archive that leftover channel from ClickClack.
151
+
152
+ The main session gets a read-only `discussion` tool that pulls the latest
153
+ channel messages, including recent thread replies. The pull uses bounded
154
+ history and thread-request budgets; its output says when older active threads
155
+ may have been omitted. It never posts, archives, renames, or otherwise mutates
156
+ the discussion.
157
+
38
158
  ## Docs
39
159
 
40
160
  See `docs/channels/clickclack.md` in the OpenClaw repository, or the published docs at `https://docs.openclaw.ai/channels/clickclack`.
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as createClickClackClient, d as DEFAULT_ACCOUNT_ID, f as listClickClackAccountIds, h as resolveDefaultClickClackAccountId, m as resolveClickClackAccount, p as listEnabledClickClackAccounts, s as clickClackConfigSchema } from "./setup-surface-CNjp76Xx.js";
2
- import { a as setClickClackRuntime, i as getClickClackRuntime, n as buildClickClackTarget, r as parseClickClackTarget, t as clickClackPlugin } from "./channel-CEYthZ9N.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-Baf26N-D.js";
2
+ import { T as setClickClackRuntime, n as buildClickClackTarget, r as parseClickClackTarget, t as clickClackPlugin, w as getClickClackRuntime } from "./channel-BDq0KWDB.js";
3
3
  export { DEFAULT_ACCOUNT_ID, buildClickClackTarget, clickClackConfigSchema, clickClackPlugin, createClickClackClient, getClickClackRuntime, listClickClackAccountIds, listEnabledClickClackAccounts, parseClickClackTarget, resolveClickClackAccount, resolveDefaultClickClackAccountId, setClickClackRuntime };
@@ -1,4 +1,4 @@
1
- import { a as createClickClackClient, c as CLICKCLACK_CHANNEL_ID, d as DEFAULT_ACCOUNT_ID, i as resolveWorkspaceId, l as clickClackConfigAdapter, m as resolveClickClackAccount, n as clickClackSetupAdapter, o as normalizeClickClackCorrelationId, r as resolveChannelId, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-CNjp76Xx.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-Baf26N-D.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";
@@ -10,9 +10,9 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
10
10
  import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
11
11
  import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
12
12
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
13
- import { createHash } from "node:crypto";
13
+ import { createHash, randomUUID } from "node:crypto";
14
14
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
15
- import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
15
+ import { 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
@@ -328,6 +328,336 @@ function createClickClackActivityPublisher(params) {
328
328
  };
329
329
  }
330
330
  //#endregion
331
+ //#region extensions/clickclack/src/discussions/binding-generation.ts
332
+ const DISCUSSION_GENERATIONS_NAMESPACE = "discussion-binding-generations";
333
+ const MAX_PENDING_DISCUSSION_GENERATIONS = 1e4;
334
+ const storesByRuntime$2 = /* @__PURE__ */ new WeakMap();
335
+ function getGenerationStore(runtime) {
336
+ const existing = storesByRuntime$2.get(runtime);
337
+ if (existing) return existing;
338
+ const created = runtime.state.openSyncKeyedStore({
339
+ namespace: DISCUSSION_GENERATIONS_NAMESPACE,
340
+ maxEntries: MAX_PENDING_DISCUSSION_GENERATIONS,
341
+ overflowPolicy: "reject-new"
342
+ });
343
+ storesByRuntime$2.set(runtime, created);
344
+ return created;
345
+ }
346
+ /** Reserves a generation so an interrupted channel create can be adopted on retry. */
347
+ function reserveDiscussionBindingGeneration(params) {
348
+ const store = getGenerationStore(params.runtime);
349
+ const existing = store.lookup(params.sessionKey);
350
+ if (existing?.destinationIdentity === params.destinationIdentity) return existing.generation;
351
+ const generation = (params.createGeneration ?? randomUUID)();
352
+ store.register(params.sessionKey, {
353
+ destinationIdentity: params.destinationIdentity,
354
+ generation
355
+ });
356
+ return generation;
357
+ }
358
+ /** Clears only the completed reservation; future opens must mint a new ownership ref. */
359
+ function clearDiscussionBindingGeneration(params) {
360
+ const store = getGenerationStore(params.runtime);
361
+ const existing = store.lookup(params.sessionKey);
362
+ if (!existing) return;
363
+ if (params.expectedGeneration && existing.generation !== params.expectedGeneration) return;
364
+ store.delete(params.sessionKey);
365
+ }
366
+ /** Quarantines a destination before the first fallible channel create. */
367
+ function recordPendingDiscussionOpen(params) {
368
+ const store = getGenerationStore(params.runtime);
369
+ const existing = store.lookup(params.sessionKey);
370
+ if (!existing || existing.generation !== params.generation) throw new Error("ClickClack discussion generation changed before channel creation");
371
+ store.register(params.sessionKey, {
372
+ ...existing,
373
+ pending: params.pending
374
+ });
375
+ }
376
+ function listPendingDiscussionOpens(runtime) {
377
+ return getGenerationStore(runtime).entries().flatMap((entry) => entry.value.pending ? [{
378
+ sessionKey: entry.key,
379
+ generation: entry.value.generation,
380
+ ...entry.value.pending
381
+ }] : []);
382
+ }
383
+ function hasPendingDiscussionOpenForDestination(params) {
384
+ const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
385
+ return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
386
+ }
387
+ //#endregion
388
+ //#region extensions/clickclack/src/discussions/naming.ts
389
+ function shortSessionHash(sessionKey) {
390
+ return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
391
+ }
392
+ function fallbackDiscussionLabel(sessionKey) {
393
+ return `s-${shortSessionHash(sessionKey)}`;
394
+ }
395
+ function resolveDiscussionLabel(label, sessionKey) {
396
+ return label?.trim() || fallbackDiscussionLabel(sessionKey);
397
+ }
398
+ 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);
400
+ }
401
+ function discussionSessionPeerId(identity) {
402
+ return `disc-${createHash("sha256").update([
403
+ identity.mainSessionKey,
404
+ identity.sessionId,
405
+ identity.serverBaseUrl,
406
+ identity.channelId,
407
+ identity.externalRef
408
+ ].join("\0")).digest("hex").slice(0, 32)}`;
409
+ }
410
+ function isDiscussionSessionKey(sessionKey) {
411
+ return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
412
+ }
413
+ function discussionExternalRef(installationId, mainSessionKey, sessionId, destinationIdentity, bindingGeneration) {
414
+ return `openclaw:${installationId}:${shortSessionHash([
415
+ mainSessionKey,
416
+ sessionId,
417
+ destinationIdentity,
418
+ bindingGeneration
419
+ ].join("\0"))}`;
420
+ }
421
+ function discussionCredentialFingerprint(token) {
422
+ return createHash("sha256").update(token).digest("hex");
423
+ }
424
+ function discussionSessionKey(params) {
425
+ return params.runtime.channel.routing.buildAgentSessionKey({
426
+ agentId: params.agentId,
427
+ channel: "clickclack",
428
+ accountId: params.accountId,
429
+ peer: {
430
+ kind: "channel",
431
+ id: discussionSessionPeerId(params)
432
+ }
433
+ });
434
+ }
435
+ //#endregion
436
+ //#region extensions/clickclack/src/discussions/binding-store.ts
437
+ function bindingMatchesSessionIncarnation(runtime, sessionKey, binding) {
438
+ const entry = runtime.agent.session.getSessionEntry({
439
+ sessionKey,
440
+ readConsistency: "latest"
441
+ });
442
+ return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId);
443
+ }
444
+ function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
445
+ const entry = runtime.agent.session.getSessionEntry({
446
+ sessionKey,
447
+ readConsistency: "latest"
448
+ });
449
+ return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
450
+ }
451
+ const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
452
+ const MAX_DISCUSSION_BINDINGS = 1e4;
453
+ const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
454
+ function channelKey(serverBaseUrl, channelId) {
455
+ return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
456
+ }
457
+ /** SQLite-backed session/channel bindings with a process-local inbound lookup index. */
458
+ var ClickClackDiscussionBindingStore = class {
459
+ #store;
460
+ #sessionByChannel = /* @__PURE__ */ new Map();
461
+ #mainByDiscussionSession = /* @__PURE__ */ new Map();
462
+ #runtime;
463
+ constructor(runtime) {
464
+ this.#runtime = runtime;
465
+ this.#store = runtime.state.openSyncKeyedStore({
466
+ namespace: DISCUSSION_BINDINGS_NAMESPACE,
467
+ maxEntries: MAX_DISCUSSION_BINDINGS,
468
+ overflowPolicy: "reject-new"
469
+ });
470
+ for (const entry of this.#store.entries()) this.#index(entry.key, entry.value);
471
+ }
472
+ get(sessionKey) {
473
+ return this.#store.lookup(sessionKey);
474
+ }
475
+ hasCapacity(sessionKey) {
476
+ return this.get(sessionKey) !== void 0 || this.#store.entries().length < MAX_DISCUSSION_BINDINGS;
477
+ }
478
+ getByChannel(serverBaseUrl, channelId) {
479
+ const key = channelKey(serverBaseUrl, channelId);
480
+ const sessionKey = this.#sessionByChannel.get(key);
481
+ if (!sessionKey) return;
482
+ const binding = this.get(sessionKey);
483
+ if (!binding) {
484
+ this.#sessionByChannel.delete(key);
485
+ return;
486
+ }
487
+ return {
488
+ sessionKey,
489
+ binding
490
+ };
491
+ }
492
+ set(sessionKey, binding) {
493
+ const previous = this.get(sessionKey);
494
+ this.#store.register(sessionKey, binding);
495
+ if (previous) this.#unindex(sessionKey, previous);
496
+ this.#index(sessionKey, binding);
497
+ }
498
+ delete(sessionKey) {
499
+ const previous = this.get(sessionKey);
500
+ const deleted = this.#store.delete(sessionKey);
501
+ if (deleted && previous) this.#unindex(sessionKey, previous);
502
+ return deleted;
503
+ }
504
+ getByDiscussionSession(sideSessionKey) {
505
+ const sessionKey = this.#mainByDiscussionSession.get(sideSessionKey);
506
+ if (!sessionKey) return;
507
+ const binding = this.get(sessionKey);
508
+ return binding ? {
509
+ sessionKey,
510
+ binding
511
+ } : void 0;
512
+ }
513
+ entries() {
514
+ return this.#store.entries().map((entry) => ({
515
+ sessionKey: entry.key,
516
+ binding: entry.value
517
+ }));
518
+ }
519
+ #index(sessionKey, binding) {
520
+ this.#sessionByChannel.set(channelKey(binding.serverBaseUrl, binding.channelId), sessionKey);
521
+ const sideSessionKey = discussionSessionKey({
522
+ runtime: this.#runtime,
523
+ agentId: binding.agentId,
524
+ mainSessionKey: sessionKey,
525
+ sessionId: binding.sessionId,
526
+ accountId: binding.accountId,
527
+ serverBaseUrl: binding.serverBaseUrl,
528
+ channelId: binding.channelId,
529
+ externalRef: binding.externalRef
530
+ });
531
+ if (sideSessionKey) this.#mainByDiscussionSession.set(sideSessionKey, sessionKey);
532
+ }
533
+ #unindex(sessionKey, binding) {
534
+ this.#sessionByChannel.delete(channelKey(binding.serverBaseUrl, binding.channelId));
535
+ const sideSessionKey = discussionSessionKey({
536
+ runtime: this.#runtime,
537
+ agentId: binding.agentId,
538
+ mainSessionKey: sessionKey,
539
+ sessionId: binding.sessionId,
540
+ accountId: binding.accountId,
541
+ serverBaseUrl: binding.serverBaseUrl,
542
+ channelId: binding.channelId,
543
+ externalRef: binding.externalRef
544
+ });
545
+ if (sideSessionKey) this.#mainByDiscussionSession.delete(sideSessionKey);
546
+ }
547
+ };
548
+ function getClickClackDiscussionBindingStore(runtime) {
549
+ const existing = storesByRuntime$1.get(runtime);
550
+ if (existing) return existing;
551
+ const created = new ClickClackDiscussionBindingStore(runtime);
552
+ storesByRuntime$1.set(runtime, created);
553
+ return created;
554
+ }
555
+ //#endregion
556
+ //#region extensions/clickclack/src/discussions/eligibility.ts
557
+ function discussionAccounts(cfg) {
558
+ return listEnabledClickClackAccounts(cfg).filter((account) => account.configured && account.discussions.enabled);
559
+ }
560
+ function normalizedServerBaseUrl(account) {
561
+ return account.baseUrl.replace(/\/+$/u, "");
562
+ }
563
+ /** Resolves the sole live account and rejects bindings pinned to an older destination. */
564
+ function resolveDiscussionBindingAccount(cfg, binding) {
565
+ const accounts = discussionAccounts(cfg);
566
+ if (accounts.length !== 1) return { state: "unavailable" };
567
+ const account = accounts[0];
568
+ if (!account) return { state: "unavailable" };
569
+ if (account.accountId !== binding.accountId || normalizedServerBaseUrl(account) !== binding.serverBaseUrl || account.discussions.workspace !== binding.workspaceRef || binding.credentialFingerprint !== void 0 && discussionCredentialFingerprint(account.token) !== binding.credentialFingerprint) return {
570
+ state: "stale",
571
+ account
572
+ };
573
+ return {
574
+ state: "active",
575
+ account
576
+ };
577
+ }
578
+ //#endregion
579
+ //#region extensions/clickclack/src/discussions/revoked-channel-store.ts
580
+ const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
581
+ const MAX_REVOKED_CHANNELS = 1e5;
582
+ const storesByRuntime = /* @__PURE__ */ new WeakMap();
583
+ function revokedChannelKey(params) {
584
+ return [params.serverBaseUrl.replace(/\/+$/u, ""), params.channelId].join("\0");
585
+ }
586
+ function getStore(runtime) {
587
+ const existing = storesByRuntime.get(runtime);
588
+ if (existing) return existing;
589
+ const created = runtime.state.openSyncKeyedStore({
590
+ namespace: REVOKED_CHANNELS_NAMESPACE,
591
+ maxEntries: MAX_REVOKED_CHANNELS,
592
+ overflowPolicy: "reject-new"
593
+ });
594
+ storesByRuntime.set(runtime, created);
595
+ return created;
596
+ }
597
+ /** Records managed ownership before its live binding is released. */
598
+ function markClickClackDiscussionChannelRevoked(runtime, binding) {
599
+ const value = {
600
+ accountId: binding.accountId,
601
+ serverBaseUrl: binding.serverBaseUrl,
602
+ channelId: binding.channelId,
603
+ revokedAt: Date.now()
604
+ };
605
+ getStore(runtime).register(revokedChannelKey(value), value);
606
+ }
607
+ function markClickClackDiscussionChannelIdentityRevoked(params) {
608
+ const value = {
609
+ accountId: params.accountId,
610
+ serverBaseUrl: params.serverBaseUrl.replace(/\/+$/u, ""),
611
+ channelId: params.channelId,
612
+ revokedAt: Date.now()
613
+ };
614
+ getStore(params.runtime).register(revokedChannelKey(value), value);
615
+ }
616
+ function clearClickClackDiscussionChannelRevoked(params) {
617
+ getStore(params.runtime).delete(revokedChannelKey(params));
618
+ }
619
+ /** Distinguishes a released managed channel from a genuinely ordinary channel. */
620
+ function isClickClackDiscussionChannelRevoked(params) {
621
+ return Boolean(getStore(params.runtime).lookup(revokedChannelKey(params)));
622
+ }
623
+ //#endregion
624
+ //#region extensions/clickclack/src/discussions/routing.ts
625
+ function resolveClickClackDiscussionRoute(params) {
626
+ if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
627
+ const matched = getClickClackDiscussionBindingStore(params.runtime).getByChannel(params.serverBaseUrl, params.channelId);
628
+ if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
629
+ if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
630
+ if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
631
+ if (matched.binding.archived) return { state: "revoked" };
632
+ if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
633
+ if (!bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, matched.binding)) return { state: "revoked" };
634
+ const sessionKey = discussionSessionKey({
635
+ runtime: params.runtime,
636
+ agentId: matched.binding.agentId,
637
+ mainSessionKey: matched.sessionKey,
638
+ sessionId: matched.binding.sessionId,
639
+ accountId: params.accountId,
640
+ serverBaseUrl: matched.binding.serverBaseUrl,
641
+ channelId: matched.binding.channelId,
642
+ externalRef: matched.binding.externalRef
643
+ });
644
+ if (!sessionKey) return { state: "revoked" };
645
+ return {
646
+ state: "active",
647
+ route: {
648
+ agentId: matched.binding.agentId,
649
+ sessionKey,
650
+ systemPrompt: [
651
+ "You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
652
+ `The main session key is ${matched.sessionKey}.`,
653
+ "Observe it with sessions_history and session_status (using changesSince for incremental checks).",
654
+ "Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
655
+ "These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
656
+ ].join(" ")
657
+ }
658
+ };
659
+ }
660
+ //#endregion
331
661
  //#region extensions/clickclack/src/target.ts
332
662
  /**
333
663
  * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
@@ -378,6 +708,42 @@ function looksLikeClickClackTarget(raw) {
378
708
  * and direct messages.
379
709
  */
380
710
  const CLICKCLACK_MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
711
+ const CLICKCLACK_FORMAT_PROFILE = {
712
+ 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
+ chunk: {
733
+ limit: 1024 * 1024,
734
+ unit: "bytes"
735
+ }
736
+ };
737
+ function renderClickClackMarkdown(markdown) {
738
+ return renderMarkdownWithMarkers({
739
+ text: markdown,
740
+ styles: [],
741
+ links: []
742
+ }, {
743
+ styleMarkers: {},
744
+ escapeText: (text) => text
745
+ }, CLICKCLACK_FORMAT_PROFILE);
746
+ }
381
747
  async function createTargetMessage(params) {
382
748
  const parsed = parseClickClackTarget(params.to);
383
749
  const explicitThreadId = params.threadId == null ? "" : String(params.threadId);
@@ -453,7 +819,7 @@ function createOutboundContext(params) {
453
819
  return {
454
820
  account,
455
821
  client: createClickClackClient({
456
- baseUrl: account.baseUrl,
822
+ baseUrl: account.apiEndpoint,
457
823
  token: account.token,
458
824
  correlationId: params.correlationId
459
825
  })
@@ -464,7 +830,7 @@ function createOutboundContext(params) {
464
830
  * message id, or undefined when sanitization removes all content.
465
831
  */
466
832
  async function sendClickClackText(params) {
467
- const text = sanitizeAssistantVisibleText(params.text);
833
+ const text = renderClickClackMarkdown(sanitizeAssistantVisibleText(params.text));
468
834
  if (!text) return;
469
835
  const { account, client } = createOutboundContext(params);
470
836
  const workspaceId = await resolveWorkspaceId(client, account.workspace);
@@ -524,7 +890,7 @@ async function sendClickClackMedia(params) {
524
890
  ...nonces.upload ? { nonce: nonces.upload } : {}
525
891
  });
526
892
  }
527
- const text = sanitizeAssistantVisibleText(params.text) || mediaFilename || upload.filename || "attachment";
893
+ const text = renderClickClackMarkdown(sanitizeAssistantVisibleText(params.text)) || mediaFilename || upload.filename || "attachment";
528
894
  const message = await createTargetMessage({
529
895
  client,
530
896
  workspaceId,
@@ -753,13 +1119,29 @@ async function handleClickClackInbound(params) {
753
1119
  kind: "channel",
754
1120
  id: message.channel_id ?? ""
755
1121
  });
756
- const route = resolveAccountAgentRoute({
1122
+ const accountRoute = resolveAccountAgentRoute({
757
1123
  cfg: params.config,
758
1124
  account: params.account,
759
1125
  target,
760
1126
  isDirect
761
1127
  });
762
- if (params.account.replyMode === "model") {
1128
+ const discussionResolution = !isDirect && message.channel_id ? resolveClickClackDiscussionRoute({
1129
+ runtime,
1130
+ config: params.config,
1131
+ accountId: params.account.accountId,
1132
+ serverBaseUrl: params.account.baseUrl,
1133
+ workspaceId: message.workspace_id,
1134
+ channelId: message.channel_id
1135
+ }) : { state: "unbound" };
1136
+ if (discussionResolution.state === "revoked") return;
1137
+ const discussionRoute = discussionResolution.state === "active" ? discussionResolution.route : void 0;
1138
+ const route = discussionRoute ? {
1139
+ ...accountRoute,
1140
+ agentId: discussionRoute.agentId,
1141
+ sessionKey: discussionRoute.sessionKey,
1142
+ lastRoutePolicy: "session"
1143
+ } : accountRoute;
1144
+ if (params.account.replyMode === "model" && !discussionRoute) {
763
1145
  await dispatchModelReply({
764
1146
  account: params.account,
765
1147
  cfg: params.config,
@@ -774,7 +1156,7 @@ async function handleClickClackInbound(params) {
774
1156
  let activity;
775
1157
  if (params.account.agentActivity && (message.channel_id || message.direct_conversation_id)) activity = createClickClackActivityPublisher({
776
1158
  client: createClickClackClient({
777
- baseUrl: params.account.baseUrl,
1159
+ baseUrl: params.account.apiEndpoint,
778
1160
  token: params.account.token,
779
1161
  correlationId: params.correlationId
780
1162
  }),
@@ -841,7 +1223,10 @@ async function handleClickClackInbound(params) {
841
1223
  wasMentioned: !isDirect
842
1224
  }
843
1225
  },
844
- extra: { GroupChannel: message.channel_id }
1226
+ extra: {
1227
+ GroupChannel: message.channel_id,
1228
+ ...discussionRoute ? { GroupSystemPrompt: discussionRoute.systemPrompt } : {}
1229
+ }
845
1230
  });
846
1231
  const runId = resolveClickClackAgentRunId(message.id);
847
1232
  const activityReplyOptions = activity ? {
@@ -960,7 +1345,7 @@ async function processEvent(params) {
960
1345
  const correlationId = eventCorrelationId(params.event);
961
1346
  const message = await resolveEventMessage({
962
1347
  client: correlationId ? createClickClackClient({
963
- baseUrl: params.account.baseUrl,
1348
+ baseUrl: params.account.apiEndpoint,
964
1349
  token: params.account.token,
965
1350
  correlationId
966
1351
  }) : params.client,
@@ -1006,7 +1391,7 @@ async function startClickClackGatewayAccount(ctx) {
1006
1391
  });
1007
1392
  if (!configuredAccount.configured) throw new Error(`ClickClack is not configured for account "${configuredAccount.accountId}"`);
1008
1393
  const client = createClickClackClient({
1009
- baseUrl: configuredAccount.baseUrl,
1394
+ baseUrl: configuredAccount.apiEndpoint,
1010
1395
  token: configuredAccount.token
1011
1396
  });
1012
1397
  const workspaceId = await resolveWorkspaceId(client, configuredAccount.workspace);
@@ -1228,6 +1613,7 @@ const clickClackPlugin = createChatChannelPlugin({
1228
1613
  configSchema: clickClackConfigSchema,
1229
1614
  config: clickClackConfigAdapter,
1230
1615
  setup: clickClackSetupAdapter,
1616
+ setupContract: clickClackSetupContract,
1231
1617
  setupWizard: clickClackSetupWizard,
1232
1618
  secrets: {
1233
1619
  secretTargetRegistryEntries,
@@ -1333,4 +1719,4 @@ const clickClackPlugin = createChatChannelPlugin({
1333
1719
  }
1334
1720
  });
1335
1721
  //#endregion
1336
- export { setClickClackRuntime as a, getClickClackRuntime as i, buildClickClackTarget as n, parseClickClackTarget as r, clickClackPlugin as t };
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 };
@@ -1,2 +1,2 @@
1
- import { t as clickClackPlugin } from "./channel-CEYthZ9N.js";
1
+ import { t as clickClackPlugin } from "./channel-BDq0KWDB.js";
2
2
  export { clickClackPlugin };