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