@openclaw/clickclack 2026.7.1 → 2026.7.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,155 +1,17 @@
1
- import { createAccountListHelpers, hasConfiguredAccountValue } from "openclaw/plugin-sdk/account-helpers";
2
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
- import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
4
- import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
5
- import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
6
- import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
7
- import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
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-B346XaAu.js";
2
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
8
3
  import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
9
- import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
10
- import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
4
+ import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
11
5
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
12
- import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
13
- import { z } from "zod";
14
6
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
+ import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
15
8
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
16
9
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
17
- import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
18
- import { WebSocket } from "ws";
10
+ import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
19
11
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
20
- //#region extensions/clickclack/src/accounts.ts
21
- /**
22
- * Resolves ClickClack account configuration from root channel config, named
23
- * account overrides, and secret-provider references.
24
- */
25
- const DEFAULT_RECONNECT_MS = 1500;
26
- const MIN_RECONNECT_MS = 100;
27
- const MAX_RECONNECT_MS = 6e4;
28
- const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
29
- normalizeAccountId,
30
- hasImplicitDefaultAccount: (cfg) => {
31
- const channel = cfg.channels?.clickclack;
32
- return Boolean(channel?.baseUrl?.trim() && hasConfiguredAccountValue(channel.token) && channel.workspace?.trim());
33
- }
34
- });
35
- function resolveMergedClickClackAccountConfig(cfg, accountId) {
36
- return resolveMergedAccountConfig({
37
- channelConfig: cfg.channels?.clickclack,
38
- accounts: cfg.channels?.clickclack?.accounts,
39
- accountId,
40
- omitKeys: ["defaultAccount"],
41
- normalizeAccountId
42
- });
43
- }
44
- function resolveClickClackToken(params) {
45
- const resolved = resolveSecretInputString({
46
- value: params.value,
47
- path: params.accountId === DEFAULT_ACCOUNT_ID ? "channels.clickclack.token" : `channels.clickclack.accounts.${params.accountId}.token`,
48
- defaults: params.cfg.secrets?.defaults,
49
- mode: "inspect"
50
- });
51
- if (resolved.status !== "available") {
52
- if (resolved.status === "configured_unavailable" && resolved.ref.source === "env") {
53
- const providerConfig = params.cfg.secrets?.providers?.[resolved.ref.provider];
54
- if (providerConfig) {
55
- if (providerConfig.source !== "env") throw new Error(`Secret provider "${resolved.ref.provider}" has source "${providerConfig.source}" but ref requests "env".`);
56
- if (providerConfig.allowlist && !providerConfig.allowlist.includes(resolved.ref.id)) throw new Error(`Environment variable "${resolved.ref.id}" is not allowlisted in secrets.providers.${resolved.ref.provider}.allowlist.`);
57
- } else if (resolved.ref.provider !== resolveDefaultSecretProviderAlias({ secrets: params.cfg.secrets }, "env")) throw new Error(`Secret provider "${resolved.ref.provider}" is not configured (ref: env:${resolved.ref.provider}:${resolved.ref.id}).`);
58
- return normalizeSecretInputString((params.env ?? process.env)[resolved.ref.id]) ?? "";
59
- }
60
- return "";
61
- }
62
- return normalizeResolvedSecretInputString({
63
- value: resolved.value,
64
- path: "channels.clickclack.token"
65
- }) ?? "";
66
- }
67
- /**
68
- * Builds the normalized account snapshot used by gateway, outbound delivery,
69
- * status reporting, and channel routing.
70
- */
71
- function resolveClickClackAccount(params) {
72
- const accountId = normalizeAccountId(params.accountId);
73
- const merged = resolveMergedClickClackAccountConfig(params.cfg, accountId);
74
- const enabled = params.cfg.channels?.clickclack?.enabled !== false && merged.enabled !== false;
75
- const baseUrl = merged.baseUrl?.trim().replace(/\/$/, "") ?? "";
76
- const token = resolveClickClackToken({
77
- cfg: params.cfg,
78
- value: merged.token,
79
- accountId,
80
- env: params.env
81
- });
82
- const workspace = merged.workspace?.trim() ?? "";
83
- return {
84
- accountId,
85
- enabled,
86
- configured: Boolean(baseUrl && token && workspace),
87
- name: normalizeOptionalString(merged.name),
88
- baseUrl,
89
- token,
90
- workspace,
91
- botUserId: normalizeOptionalString(merged.botUserId),
92
- agentId: normalizeOptionalString(merged.agentId),
93
- replyMode: merged.replyMode === "model" ? "model" : "agent",
94
- model: normalizeOptionalString(merged.model),
95
- systemPrompt: normalizeOptionalString(merged.systemPrompt),
96
- timeoutSeconds: merged.timeoutSeconds,
97
- toolsAllow: merged.toolsAllow,
98
- defaultTo: merged.defaultTo?.trim() || "channel:general",
99
- allowFrom: merged.allowFrom ?? ["*"],
100
- reconnectMs: resolveIntegerOption(merged.reconnectMs, DEFAULT_RECONNECT_MS, {
101
- min: MIN_RECONNECT_MS,
102
- max: MAX_RECONNECT_MS
103
- }),
104
- agentActivity: merged.agentActivity === true,
105
- config: {
106
- ...merged,
107
- allowFrom: merged.allowFrom ?? ["*"]
108
- }
109
- };
110
- }
111
- /**
112
- * Returns all enabled accounts, including the implicit default account when
113
- * legacy top-level ClickClack config is present.
114
- */
115
- function listEnabledClickClackAccounts(cfg) {
116
- return listClickClackAccountIds(cfg).map((accountId) => resolveClickClackAccount({
117
- cfg,
118
- accountId
119
- })).filter((account) => account.enabled);
120
- }
121
- //#endregion
122
- //#region extensions/clickclack/src/config-schema.ts
123
- /**
124
- * Zod-backed config schema for ClickClack channel accounts.
125
- */
126
- const ClickClackAccountConfigSchema = z.object({
127
- name: z.string().optional(),
128
- enabled: z.boolean().optional(),
129
- baseUrl: z.string().url().optional(),
130
- token: buildSecretInputSchema().optional(),
131
- workspace: z.string().optional(),
132
- botUserId: z.string().optional(),
133
- agentId: z.string().optional(),
134
- replyMode: z.enum(["agent", "model"]).optional(),
135
- model: z.string().optional(),
136
- systemPrompt: z.string().optional(),
137
- timeoutSeconds: z.number().int().min(1).max(3600).optional(),
138
- toolsAllow: z.array(z.string()).optional(),
139
- defaultTo: z.string().optional(),
140
- allowFrom: z.array(z.string()).optional(),
141
- reconnectMs: z.number().int().min(100).max(6e4).optional(),
142
- agentActivity: z.boolean().optional()
143
- }).strict();
144
- /**
145
- * Config schema exported to core so `openclaw doctor` and config validation
146
- * understand both default and named ClickClack accounts.
147
- */
148
- const clickClackConfigSchema = buildChannelConfigSchema(ClickClackAccountConfigSchema.extend({
149
- accounts: z.record(z.string(), ClickClackAccountConfigSchema.partial()).optional(),
150
- defaultAccount: z.string().optional()
151
- }).strict());
152
- //#endregion
12
+ import { createHash } from "node:crypto";
13
+ import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
14
+ import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
153
15
  //#region extensions/clickclack/src/runtime.ts
154
16
  /**
155
17
  * Runtime store for host-provided OpenClaw services used by the ClickClack
@@ -212,139 +74,62 @@ async function resolveClickClackInboundAccess(params) {
212
74
  };
213
75
  }
214
76
  //#endregion
215
- //#region extensions/clickclack/src/http-client.ts
216
- /**
217
- * Thin ClickClack REST/websocket client used by gateway, resolver, and outbound
218
- * delivery code.
219
- */
220
- /**
221
- * Serializes optional provenance into the wire fields. Unknown JSON fields
222
- * are ignored by servers without the provenance columns, so these are safe
223
- * to send unconditionally when present.
224
- */
225
- function provenanceFields(provenance) {
226
- const fields = {};
227
- if (provenance?.model?.trim()) fields.author_model = provenance.model.trim();
228
- if (provenance?.thinking?.trim()) fields.author_thinking = provenance.thinking.trim();
229
- if (provenance?.runtime?.trim()) fields.author_runtime = provenance.runtime.trim();
230
- return fields;
77
+ //#region extensions/clickclack/src/command-menu.ts
78
+ const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
79
+ const CLICKCLACK_MAX_COMMANDS = 100;
80
+ const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
81
+ const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
82
+ function truncateCodePoints(value, maxLength) {
83
+ return Array.from(value).slice(0, maxLength).join("");
231
84
  }
232
- const CLICKCLACK_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
233
- /**
234
- * Creates a typed client for the ClickClack API using bearer-token auth.
235
- */
236
- function createClickClackClient(options) {
237
- const baseUrl = options.baseUrl.replace(/\/$/, "");
238
- const fetcher = options.fetch ?? fetch;
239
- const headers = {
240
- Authorization: `Bearer ${options.token}`,
241
- Accept: "application/json"
242
- };
243
- async function request(path, init = {}) {
244
- const requestHeaders = new Headers(init.headers);
245
- for (const [key, value] of Object.entries(headers)) requestHeaders.set(key, value);
246
- if (init.body && !(init.body instanceof FormData)) requestHeaders.set("Content-Type", "application/json");
247
- const response = await fetcher(`${baseUrl}${path}`, {
248
- ...init,
249
- headers: requestHeaders
250
- });
251
- if (!response.ok) {
252
- const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
253
- throw new Error(`ClickClack ${response.status}: ${detail}`);
85
+ function commandArgsHint(spec) {
86
+ if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
87
+ return spec.acceptsArgs ? "[args]" : "";
88
+ }
89
+ function errorStatus(error) {
90
+ if (!error || typeof error !== "object" || !("status" in error)) return;
91
+ const status = error.status;
92
+ return typeof status === "number" ? status : void 0;
93
+ }
94
+ function mapNativeCommandSpecsToClickClackMenu(specs, log) {
95
+ const commands = [];
96
+ const seen = /* @__PURE__ */ new Set();
97
+ const invalidNames = /* @__PURE__ */ new Set();
98
+ for (const spec of specs) {
99
+ if (spec.isAlias) continue;
100
+ const command = spec.name.trim().toLowerCase();
101
+ if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
102
+ invalidNames.add(spec.name.trim() || "<empty>");
103
+ continue;
254
104
  }
255
- return await readProviderJsonResponse(response, "ClickClack response");
105
+ if (seen.has(command)) continue;
106
+ seen.add(command);
107
+ const description = spec.description.trim() || command;
108
+ commands.push({
109
+ command,
110
+ description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
111
+ args_hint: commandArgsHint(spec)
112
+ });
256
113
  }
257
- return {
258
- me: async () => {
259
- return (await request("/api/me")).user;
260
- },
261
- workspaces: async () => {
262
- return (await request("/api/workspaces")).workspaces;
263
- },
264
- channels: async (workspaceId) => {
265
- return (await request(`/api/workspaces/${encodeURIComponent(workspaceId)}/channels`)).channels;
266
- },
267
- channelMessages: async (channelId, afterSeq, limit = 20) => {
268
- return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
269
- },
270
- directMessages: async (conversationId, afterSeq, limit = 20) => {
271
- return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
272
- },
273
- thread: async (messageId) => await request(`/api/messages/${encodeURIComponent(messageId)}/thread`),
274
- createChannelMessage: async (channelId, body, opts) => {
275
- return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages`, {
276
- method: "POST",
277
- body: JSON.stringify({
278
- body,
279
- ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
280
- ...provenanceFields(opts?.provenance)
281
- })
282
- })).message;
283
- },
284
- createThreadReply: async (messageId, body, opts) => {
285
- return (await request(`/api/messages/${encodeURIComponent(messageId)}/thread/replies`, {
286
- method: "POST",
287
- body: JSON.stringify({
288
- body,
289
- ...provenanceFields(opts?.provenance)
290
- })
291
- })).message;
292
- },
293
- createDirectConversation: async (workspaceId, memberIds) => {
294
- return (await request("/api/dms", {
295
- method: "POST",
296
- body: JSON.stringify({
297
- workspace_id: workspaceId,
298
- member_ids: memberIds
299
- })
300
- })).conversation;
301
- },
302
- /**
303
- * POSTs a durable agent activity row (agent_commentary / agent_tool)
304
- * through the normal message create path. Requires a bot token carrying
305
- * the agent_activity:write scope on the ClickClack side.
306
- */
307
- createActivityMessage: async (params) => {
308
- if (!params.channelId && !params.conversationId) throw new Error("createActivityMessage requires a channelId or conversationId");
309
- return (await request(params.channelId ? `/api/channels/${encodeURIComponent(params.channelId)}/messages` : `/api/dms/${encodeURIComponent(params.conversationId ?? "")}/messages`, {
310
- method: "POST",
311
- body: JSON.stringify({
312
- body: params.body,
313
- kind: params.kind,
314
- turn_id: params.turnId,
315
- ...provenanceFields(params.provenance)
316
- })
317
- })).message;
318
- },
319
- /** PATCHes the body of an existing message (activity row coalescing). */
320
- updateMessageBody: async (messageId, body) => {
321
- return (await request(`/api/messages/${encodeURIComponent(messageId)}`, {
322
- method: "PATCH",
323
- body: JSON.stringify({ body })
324
- })).message;
325
- },
326
- createDirectMessage: async (conversationId, body, opts) => {
327
- return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages`, {
328
- method: "POST",
329
- body: JSON.stringify({
330
- body,
331
- ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {}
332
- })
333
- })).message;
334
- },
335
- events: async (workspaceId, afterCursor) => {
336
- const query = new URLSearchParams({ workspace_id: workspaceId });
337
- if (afterCursor) query.set("after_cursor", afterCursor);
338
- return (await request(`/api/realtime/events?${query.toString()}`)).events;
339
- },
340
- websocket: (workspaceId, afterCursor) => {
341
- const url = new URL(`${baseUrl}/api/realtime/ws`);
342
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
343
- url.searchParams.set("workspace_id", workspaceId);
344
- if (afterCursor) url.searchParams.set("after_cursor", afterCursor);
345
- return new WebSocket(url, { headers: { Authorization: `Bearer ${options.token}` } });
114
+ if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
115
+ return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
116
+ }
117
+ async function syncClickClackCommandMenu(params) {
118
+ try {
119
+ const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
120
+ await params.client.setBotCommands(commands);
121
+ } catch (error) {
122
+ const status = errorStatus(error);
123
+ if (status === 403) {
124
+ params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
125
+ return;
346
126
  }
347
- };
127
+ if (status === 404) {
128
+ params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
129
+ return;
130
+ }
131
+ params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
132
+ }
348
133
  }
349
134
  //#endregion
350
135
  //#region extensions/clickclack/src/activity.ts
@@ -542,27 +327,6 @@ function createClickClackActivityPublisher(params) {
542
327
  };
543
328
  }
544
329
  //#endregion
545
- //#region extensions/clickclack/src/resolve.ts
546
- /**
547
- * Resolves a workspace slug/name/id from config to a ClickClack workspace id.
548
- */
549
- async function resolveWorkspaceId(client, workspace) {
550
- if (workspace.startsWith("wsp_")) return workspace;
551
- const found = (await client.workspaces()).find((candidate) => candidate.id === workspace || candidate.slug === workspace || candidate.name === workspace);
552
- if (!found) throw new Error(`ClickClack workspace not found: ${workspace}`);
553
- return found.id;
554
- }
555
- /**
556
- * Resolves a channel name/id from config or target input to a ClickClack
557
- * channel id.
558
- */
559
- async function resolveChannelId(client, workspaceId, channel) {
560
- if (channel.startsWith("chn_")) return channel;
561
- const found = (await client.channels(workspaceId)).find((candidate) => candidate.id === channel || candidate.name === channel);
562
- if (!found) throw new Error(`ClickClack channel not found: ${channel}`);
563
- return found.id;
564
- }
565
- //#endregion
566
330
  //#region extensions/clickclack/src/target.ts
567
331
  /**
568
332
  * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
@@ -612,52 +376,285 @@ function looksLikeClickClackTarget(raw) {
612
376
  * Outbound ClickClack delivery helpers for channel messages, thread replies,
613
377
  * and direct messages.
614
378
  */
615
- /**
616
- * Sends text to a normalized ClickClack target and returns the created message
617
- * id for receipt/session tracking.
618
- */
619
- async function sendClickClackText(params) {
379
+ const CLICKCLACK_MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
380
+ async function createTargetMessage(params) {
381
+ const parsed = parseClickClackTarget(params.to);
382
+ const explicitThreadId = params.threadId == null ? "" : String(params.threadId);
383
+ const replyToId = params.replyToId == null ? "" : String(params.replyToId);
384
+ if (explicitThreadId || parsed.kind === "thread") {
385
+ const rootId = explicitThreadId || parsed.id;
386
+ await params.onPlatformSendDispatch?.();
387
+ return await params.client.createThreadReply(rootId, params.text, {
388
+ provenance: params.provenance,
389
+ nonce: params.nonce
390
+ });
391
+ }
392
+ if (parsed.kind === "dm") {
393
+ await params.onPlatformSendDispatch?.();
394
+ const dm = await params.client.createDirectConversation(params.workspaceId, [parsed.id]);
395
+ return await params.client.createDirectMessage(dm.id, params.text, {
396
+ quotedMessageId: replyToId || void 0,
397
+ nonce: params.nonce
398
+ });
399
+ }
400
+ const channelId = await resolveChannelId(params.client, params.workspaceId, parsed.id);
401
+ await params.onPlatformSendDispatch?.();
402
+ return await params.client.createChannelMessage(channelId, params.text, {
403
+ provenance: params.provenance,
404
+ quotedMessageId: replyToId || void 0,
405
+ nonce: params.nonce
406
+ });
407
+ }
408
+ function durableDeliveryDigest(params) {
409
+ if (!params.deliveryQueueId) return;
410
+ if (!Number.isSafeInteger(params.deliveryPartIndex) || (params.deliveryPartIndex ?? -1) < 0) throw new Error("ClickClack durable delivery requires a stable delivery part index");
411
+ return createHash("sha256").update(`${params.deliveryQueueId}\n${params.deliveryPartIndex}`).digest("hex");
412
+ }
413
+ function mediaDeliveryNonces(params) {
414
+ const digest = durableDeliveryDigest(params);
415
+ if (!digest) return {};
416
+ return {
417
+ message: `openclaw-media:${digest}`,
418
+ upload: `openclaw-upload:${digest}`
419
+ };
420
+ }
421
+ function textDeliveryNonce(params) {
422
+ const digest = durableDeliveryDigest(params);
423
+ return digest ? `openclaw-text:${digest}` : void 0;
424
+ }
425
+ function createDispatchOnce(onPlatformSendDispatch) {
426
+ let dispatched = false;
427
+ return async () => {
428
+ if (dispatched) return;
429
+ await onPlatformSendDispatch?.();
430
+ dispatched = true;
431
+ };
432
+ }
433
+ async function attachUploadRetrySafe(params) {
434
+ try {
435
+ await params.client.attachUpload(params.messageId, params.uploadId);
436
+ } catch (firstError) {
437
+ try {
438
+ if ((await params.client.message(params.messageId)).attachments?.some((attachment) => attachment.id === params.uploadId)) return;
439
+ } catch {}
440
+ try {
441
+ await params.client.attachUpload(params.messageId, params.uploadId);
442
+ } catch {
443
+ throw firstError;
444
+ }
445
+ }
446
+ }
447
+ function createOutboundContext(params) {
620
448
  const account = resolveClickClackAccount({
621
449
  cfg: params.cfg,
622
450
  accountId: params.accountId
623
451
  });
624
- const client = createClickClackClient({
625
- baseUrl: account.baseUrl,
626
- token: account.token
452
+ return {
453
+ account,
454
+ client: createClickClackClient({
455
+ baseUrl: account.baseUrl,
456
+ token: account.token,
457
+ correlationId: params.correlationId
458
+ })
459
+ };
460
+ }
461
+ /**
462
+ * Sends visible text to a normalized ClickClack target and returns the created
463
+ * message id, or undefined when sanitization removes all content.
464
+ */
465
+ async function sendClickClackText(params) {
466
+ const text = sanitizeAssistantVisibleText(params.text);
467
+ if (!text) return;
468
+ const { account, client } = createOutboundContext(params);
469
+ const workspaceId = await resolveWorkspaceId(client, account.workspace);
470
+ const dispatch = createDispatchOnce(params.onPlatformSendDispatch);
471
+ return (await createTargetMessage({
472
+ client,
473
+ workspaceId,
474
+ to: params.to,
475
+ text,
476
+ threadId: params.threadId,
477
+ replyToId: params.replyToId,
478
+ provenance: params.provenance,
479
+ nonce: textDeliveryNonce({
480
+ deliveryQueueId: params.deliveryQueueId,
481
+ deliveryPartIndex: params.deliveryPartIndex
482
+ }),
483
+ onPlatformSendDispatch: dispatch
484
+ })).id;
485
+ }
486
+ /** Resolves, uploads, sends, then attaches one file to a ClickClack message. */
487
+ async function sendClickClackMedia(params) {
488
+ const nonces = mediaDeliveryNonces({
489
+ deliveryQueueId: params.deliveryQueueId,
490
+ deliveryPartIndex: params.deliveryPartIndex
627
491
  });
492
+ const preloadedMedia = nonces.upload ? void 0 : await loadOutboundMediaFromUrl(params.mediaUrl, {
493
+ maxBytes: CLICKCLACK_MAX_UPLOAD_BYTES,
494
+ mediaAccess: params.mediaAccess,
495
+ mediaLocalRoots: params.mediaLocalRoots,
496
+ mediaReadFile: params.mediaReadFile
497
+ });
498
+ const { account, client } = createOutboundContext(params);
628
499
  const workspaceId = await resolveWorkspaceId(client, account.workspace);
629
- const parsed = parseClickClackTarget(params.to);
630
- const explicitThreadId = params.threadId == null ? "" : String(params.threadId);
631
- const replyToId = params.replyToId == null ? "" : String(params.replyToId);
632
- if (explicitThreadId || parsed.kind === "thread") {
633
- const rootId = explicitThreadId || parsed.id;
634
- const message = await client.createThreadReply(rootId, params.text, { provenance: params.provenance });
500
+ const persistedUpload = nonces.upload ? await client.findUploadByNonce({
501
+ workspaceId,
502
+ nonce: nonces.upload
503
+ }) : void 0;
504
+ const dispatch = createDispatchOnce(params.onPlatformSendDispatch);
505
+ let upload = persistedUpload;
506
+ let mediaFilename = preloadedMedia?.fileName?.trim();
507
+ if (!upload) {
508
+ const media = preloadedMedia ?? await loadOutboundMediaFromUrl(params.mediaUrl, {
509
+ maxBytes: CLICKCLACK_MAX_UPLOAD_BYTES,
510
+ mediaAccess: params.mediaAccess,
511
+ mediaLocalRoots: params.mediaLocalRoots,
512
+ mediaReadFile: params.mediaReadFile
513
+ });
514
+ const filename = media.fileName?.trim() || "attachment";
515
+ mediaFilename = filename;
516
+ const contentType = media.contentType?.trim() || "application/octet-stream";
517
+ await dispatch();
518
+ upload = await client.createUpload({
519
+ workspaceId,
520
+ buffer: media.buffer,
521
+ filename,
522
+ contentType,
523
+ ...nonces.upload ? { nonce: nonces.upload } : {}
524
+ });
525
+ }
526
+ const text = sanitizeAssistantVisibleText(params.text) || mediaFilename || upload.filename || "attachment";
527
+ const message = await createTargetMessage({
528
+ client,
529
+ workspaceId,
530
+ to: params.to,
531
+ text,
532
+ threadId: params.threadId,
533
+ replyToId: params.replyToId,
534
+ nonce: nonces.message,
535
+ onPlatformSendDispatch: dispatch
536
+ });
537
+ await attachUploadRetrySafe({
538
+ client,
539
+ messageId: message.id,
540
+ uploadId: upload.id
541
+ });
542
+ return message.id;
543
+ }
544
+ function collectReconciliationMediaUrls(ctx) {
545
+ const planned = ctx.renderedBatchPlan?.items[0]?.mediaUrls;
546
+ if (planned?.length) return planned.map((url) => url.trim()).filter(Boolean);
547
+ const payload = ctx.payloads[0];
548
+ return [payload?.mediaUrl, ...payload?.mediaUrls ?? []].map((url) => url?.trim()).filter((url) => Boolean(url));
549
+ }
550
+ /**
551
+ * Completes an unknown durable send through ClickClack's message/upload nonces.
552
+ * Media recovery never rereads the original source after restart.
553
+ */
554
+ async function reconcileClickClackUnknownSend(ctx) {
555
+ if (ctx.payloads.length !== 1 || (ctx.renderedBatchPlan?.items.length ?? 1) !== 1) return {
556
+ status: "unresolved",
557
+ error: "ClickClack reconciliation requires exactly one payload"
558
+ };
559
+ const mediaUrls = collectReconciliationMediaUrls(ctx);
560
+ const { account, client } = createOutboundContext({
561
+ cfg: ctx.cfg,
562
+ accountId: ctx.accountId
563
+ });
564
+ const workspaceId = await resolveWorkspaceId(client, account.workspace);
565
+ const effectiveReplyToId = ctx.effectiveReplyToId !== void 0 ? ctx.effectiveReplyToId : ctx.replyToMode === "off" ? void 0 : ctx.replyToId;
566
+ const payload = ctx.payloads[0];
567
+ const caption = ctx.renderedBatchPlan?.items[0]?.text ?? payload?.text ?? "";
568
+ if (mediaUrls.length === 0) {
569
+ const nonce = textDeliveryNonce({
570
+ deliveryQueueId: ctx.queueId,
571
+ deliveryPartIndex: 0
572
+ });
573
+ if (!nonce || !sanitizeAssistantVisibleText(caption)) return { status: "not_sent" };
574
+ const message = await client.findMessageByNonce({
575
+ workspaceId,
576
+ nonce
577
+ });
578
+ if (!message) return { status: "not_sent" };
579
+ const receipt = createMessageReceiptFromOutboundResults({
580
+ results: [{
581
+ channel: "clickclack",
582
+ messageId: message.id
583
+ }],
584
+ threadId: ctx.threadId == null ? void 0 : String(ctx.threadId),
585
+ replyToId: effectiveReplyToId ?? void 0,
586
+ kind: "text"
587
+ });
635
588
  return {
636
- to: params.to,
637
- messageId: message.id
589
+ status: "sent",
590
+ messageId: message.id,
591
+ receipt
638
592
  };
639
593
  }
640
- if (parsed.kind === "dm") {
641
- const dm = await client.createDirectConversation(workspaceId, [parsed.id]);
642
- const message = await client.createDirectMessage(dm.id, params.text, { quotedMessageId: replyToId || void 0 });
594
+ const parts = await Promise.all(mediaUrls.map(async (_mediaUrl, index) => {
595
+ const nonces = mediaDeliveryNonces({
596
+ deliveryQueueId: ctx.queueId,
597
+ deliveryPartIndex: index
598
+ });
599
+ if (!nonces.upload || !nonces.message) throw new Error("ClickClack durable media nonces were not derived");
600
+ const [upload, message] = await Promise.all([client.findUploadByNonce({
601
+ workspaceId,
602
+ nonce: nonces.upload
603
+ }), client.findMessageByNonce({
604
+ workspaceId,
605
+ nonce: nonces.message
606
+ })]);
643
607
  return {
644
- to: params.to,
645
- messageId: message.id
608
+ upload,
609
+ message
646
610
  };
611
+ }));
612
+ for (const part of parts) {
613
+ if (part.message && !part.upload) return {
614
+ status: "unresolved",
615
+ error: `ClickClack message ${part.message.id} exists without its nonce-keyed upload`,
616
+ retryable: false
617
+ };
618
+ if (!part.message) return { status: "not_sent" };
647
619
  }
648
- const channelId = await resolveChannelId(client, workspaceId, parsed.id);
649
- const message = await client.createChannelMessage(channelId, params.text, {
650
- provenance: params.provenance,
651
- quotedMessageId: replyToId || void 0
620
+ const messageIds = [];
621
+ for (const part of parts) {
622
+ const message = part.message;
623
+ const upload = part.upload;
624
+ if (!message || !upload) throw new Error("ClickClack reconciliation state changed unexpectedly");
625
+ if (!message.attachments?.some((attachment) => attachment.id === upload.id)) await attachUploadRetrySafe({
626
+ client,
627
+ messageId: message.id,
628
+ uploadId: upload.id
629
+ });
630
+ messageIds.push(message.id);
631
+ }
632
+ const receipt = createMessageReceiptFromOutboundResults({
633
+ results: messageIds.map((messageId) => ({
634
+ channel: "clickclack",
635
+ messageId
636
+ })),
637
+ threadId: ctx.threadId == null ? void 0 : String(ctx.threadId),
638
+ replyToId: effectiveReplyToId ?? void 0,
639
+ kind: "media"
652
640
  });
641
+ const messageId = messageIds.at(-1);
653
642
  return {
654
- to: params.to,
655
- messageId: message.id
643
+ status: "sent",
644
+ ...messageId ? { messageId } : {},
645
+ receipt
656
646
  };
657
647
  }
658
648
  //#endregion
659
649
  //#region extensions/clickclack/src/inbound.ts
660
650
  const CHANNEL_ID$1 = "clickclack";
651
+ const CLICKCLACK_MESSAGE_ID_PATTERN = /^msg_[0-9a-hjkmnp-tv-z]{26}$/u;
652
+ function hasClickClackReplyMedia(payload) {
653
+ return Boolean(payload.mediaUrl?.trim() || payload.mediaUrls?.some((mediaUrl) => typeof mediaUrl === "string" && mediaUrl.trim()));
654
+ }
655
+ function resolveClickClackAgentRunId(messageId) {
656
+ return CLICKCLACK_MESSAGE_ID_PATTERN.test(messageId) ? `${CHANNEL_ID$1}:${messageId}` : void 0;
657
+ }
661
658
  function resolveAccountAgentRoute(params) {
662
659
  const runtime = getClickClackRuntime();
663
660
  const route = runtime.channel.routing.resolveAgentRoute({
@@ -702,10 +699,10 @@ function resolveAccountAgentRoute(params) {
702
699
  };
703
700
  }
704
701
  async function dispatchModelReply(params) {
705
- const text = (await getClickClackRuntime().llm.complete({
702
+ const runtime = getClickClackRuntime();
703
+ const text = (await runtime.llm.complete({
706
704
  agentId: params.route.agentId,
707
705
  model: params.account.model,
708
- maxTokens: 96,
709
706
  purpose: "clickclack bot reply",
710
707
  systemPrompt: params.account.systemPrompt,
711
708
  messages: [{
@@ -713,14 +710,21 @@ async function dispatchModelReply(params) {
713
710
  content: params.message.body
714
711
  }]
715
712
  })).text.trim();
716
- if (!text) return;
713
+ if (!text) {
714
+ runtime.logging.getChildLogger({
715
+ plugin: "clickclack",
716
+ feature: "model-reply"
717
+ }).warn(`[${params.account.accountId}] ClickClack model reply produced no sendable text`);
718
+ return;
719
+ }
717
720
  await sendClickClackText({
718
721
  cfg: params.cfg,
719
722
  accountId: params.account.accountId,
720
723
  to: params.target,
721
724
  text,
722
725
  threadId: params.message.parent_message_id ? params.message.thread_root_id : void 0,
723
- replyToId: params.message.id
726
+ replyToId: params.message.id,
727
+ correlationId: params.correlationId
724
728
  });
725
729
  }
726
730
  /**
@@ -758,7 +762,8 @@ async function handleClickClackInbound(params) {
758
762
  cfg: params.config,
759
763
  message,
760
764
  route,
761
- target
765
+ target,
766
+ correlationId: params.correlationId
762
767
  });
763
768
  return;
764
769
  }
@@ -767,7 +772,8 @@ async function handleClickClackInbound(params) {
767
772
  if (params.account.agentActivity && (message.channel_id || message.direct_conversation_id)) activity = createClickClackActivityPublisher({
768
773
  client: createClickClackClient({
769
774
  baseUrl: params.account.baseUrl,
770
- token: params.account.token
775
+ token: params.account.token,
776
+ correlationId: params.correlationId
771
777
  }),
772
778
  target: message.channel_id ? { channelId: message.channel_id } : { conversationId: message.direct_conversation_id },
773
779
  turnId: message.id,
@@ -820,6 +826,20 @@ async function handleClickClackInbound(params) {
820
826
  OriginatingTo: target,
821
827
  CommandAuthorized: access.commandAuthorized
822
828
  });
829
+ const runId = resolveClickClackAgentRunId(message.id);
830
+ const activityReplyOptions = activity ? {
831
+ onModelSelected: (ctx) => {
832
+ turnProvenance = {
833
+ model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
834
+ thinking: ctx.thinkLevel
835
+ };
836
+ activity?.setProvenance(turnProvenance);
837
+ },
838
+ onItemEvent: activity.onItemEvent,
839
+ commentaryProgressEnabled: true,
840
+ suppressDefaultToolProgressMessages: true,
841
+ allowProgressCallbacksWhenSourceDeliverySuppressed: true
842
+ } : void 0;
823
843
  const dispatchPromise = runtime.channel.inbound.dispatchReply({
824
844
  cfg: params.config,
825
845
  channel: CHANNEL_ID$1,
@@ -831,21 +851,13 @@ async function handleClickClackInbound(params) {
831
851
  recordInboundSession: runtime.channel.session.recordInboundSession,
832
852
  dispatchReplyWithBufferedBlockDispatcher: runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
833
853
  toolsAllow: params.account.toolsAllow,
834
- replyOptions: activity ? {
835
- onModelSelected: (ctx) => {
836
- turnProvenance = {
837
- model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
838
- thinking: ctx.thinkLevel
839
- };
840
- activity?.setProvenance(turnProvenance);
841
- },
842
- onItemEvent: activity.onItemEvent,
843
- commentaryProgressEnabled: true,
844
- suppressDefaultToolProgressMessages: true,
845
- allowProgressCallbacksWhenSourceDeliverySuppressed: true
854
+ replyOptions: runId || activityReplyOptions ? {
855
+ ...runId ? { runId } : {},
856
+ ...activityReplyOptions
846
857
  } : void 0,
847
858
  delivery: {
848
859
  deliver: async (payload) => {
860
+ if (hasClickClackReplyMedia(payload)) throw new Error("ClickClack media reply requires durable delivery");
849
861
  const text = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
850
862
  if (!text.trim()) return;
851
863
  await sendClickClackText({
@@ -855,9 +867,25 @@ async function handleClickClackInbound(params) {
855
867
  text,
856
868
  threadId: message.parent_message_id ? message.thread_root_id : void 0,
857
869
  replyToId: message.id,
858
- provenance: turnProvenance
870
+ provenance: turnProvenance,
871
+ correlationId: params.correlationId
859
872
  });
860
873
  },
874
+ durable: (payload) => {
875
+ if (!hasClickClackReplyMedia(payload)) return false;
876
+ const threadId = message.parent_message_id ? message.thread_root_id : void 0;
877
+ return {
878
+ to: target,
879
+ threadId,
880
+ replyToId: message.id,
881
+ requiredCapabilities: deriveDurableFinalDeliveryRequirements({
882
+ payload,
883
+ threadId,
884
+ replyToId: message.id,
885
+ reconcileUnknownSend: true
886
+ })
887
+ };
888
+ },
861
889
  onError: (error) => {
862
890
  throw error instanceof Error ? error : /* @__PURE__ */ new Error(`clickclack dispatch failed: ${String(error)}`);
863
891
  }
@@ -875,10 +903,14 @@ async function handleClickClackInbound(params) {
875
903
  }
876
904
  //#endregion
877
905
  //#region extensions/clickclack/src/gateway.ts
906
+ const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
878
907
  function payloadString(event, key) {
879
908
  const value = event.payload?.[key];
880
909
  return typeof value === "string" ? value : "";
881
910
  }
911
+ function eventCorrelationId(event) {
912
+ return normalizeClickClackCorrelationId(event.payload?.correlation_id);
913
+ }
882
914
  async function resolveEventMessage(params) {
883
915
  const messageId = payloadString(params.event, "message_id");
884
916
  if (!messageId) return null;
@@ -908,8 +940,13 @@ function parseSocketEvent(data) {
908
940
  async function processEvent(params) {
909
941
  if (params.event.type !== "message.created" && params.event.type !== "thread.reply_created") return;
910
942
  if (payloadString(params.event, "author_id") === params.botUserId) return;
943
+ const correlationId = eventCorrelationId(params.event);
911
944
  const message = await resolveEventMessage({
912
- client: params.client,
945
+ client: correlationId ? createClickClackClient({
946
+ baseUrl: params.account.baseUrl,
947
+ token: params.account.token,
948
+ correlationId
949
+ }) : params.client,
913
950
  event: params.event
914
951
  });
915
952
  if (!message || message.author_id === params.botUserId) return;
@@ -924,9 +961,27 @@ async function processEvent(params) {
924
961
  account: params.account,
925
962
  config: params.config,
926
963
  message,
927
- access
964
+ access,
965
+ ...correlationId ? { correlationId } : {}
928
966
  });
929
967
  }
968
+ async function drainEventBacklog(params) {
969
+ let afterCursor = params.afterCursor;
970
+ while (!params.abortSignal.aborted) {
971
+ const events = (await params.client.eventPage(params.workspaceId, {
972
+ afterCursor,
973
+ limit: CLICKCLACK_EVENT_PAGE_LIMIT
974
+ })).events;
975
+ for (const event of events) {
976
+ if (params.abortSignal.aborted) return afterCursor;
977
+ if (!event.cursor || event.cursor === afterCursor) throw new Error("ClickClack event backlog returned a non-advancing cursor");
978
+ await params.onEvent(event);
979
+ afterCursor = event.cursor;
980
+ }
981
+ if (events.length === 0) return afterCursor;
982
+ }
983
+ return afterCursor;
984
+ }
930
985
  async function startClickClackGatewayAccount(ctx) {
931
986
  const configuredAccount = resolveClickClackAccount({
932
987
  cfg: ctx.cfg,
@@ -944,6 +999,18 @@ async function startClickClackGatewayAccount(ctx) {
944
999
  workspace: workspaceId,
945
1000
  botUserId: configuredAccount.botUserId ?? me.id
946
1001
  };
1002
+ const processIncomingEvent = (event) => processEvent({
1003
+ account,
1004
+ config: ctx.cfg,
1005
+ client,
1006
+ event,
1007
+ botUserId: account.botUserId
1008
+ });
1009
+ if (account.commandMenu) await syncClickClackCommandMenu({
1010
+ cfg: ctx.cfg,
1011
+ client,
1012
+ log: ctx.log
1013
+ });
947
1014
  ctx.setStatus({
948
1015
  accountId: account.accountId,
949
1016
  running: true,
@@ -955,23 +1022,25 @@ async function startClickClackGatewayAccount(ctx) {
955
1022
  let initialized = false;
956
1023
  try {
957
1024
  while (!ctx.abortSignal.aborted) {
958
- const backlog = await client.events(workspaceId, afterCursor);
959
1025
  if (!initialized) {
960
- for (const event of backlog) afterCursor = event.cursor || afterCursor;
1026
+ const page = await client.eventPage(workspaceId, { includeTail: true });
1027
+ if (page.tailCursor !== void 0) afterCursor = page.tailCursor;
1028
+ else for (const event of page.events) afterCursor = event.cursor || afterCursor;
961
1029
  initialized = true;
962
- } else for (const event of backlog) {
963
- afterCursor = event.cursor || afterCursor;
964
- await processEvent({
965
- account,
966
- config: ctx.cfg,
967
- client,
968
- event,
969
- botUserId: account.botUserId
970
- });
971
- }
1030
+ } else afterCursor = await drainEventBacklog({
1031
+ client,
1032
+ workspaceId,
1033
+ afterCursor,
1034
+ abortSignal: ctx.abortSignal,
1035
+ onEvent: processIncomingEvent
1036
+ });
1037
+ if (ctx.abortSignal.aborted) break;
972
1038
  const socket = client.websocket(workspaceId, afterCursor);
973
- await new Promise((resolve, reject) => {
1039
+ await new Promise((resolve) => {
974
1040
  let settled = false;
1041
+ let closing = false;
1042
+ let loggedMessageFailure = false;
1043
+ let messageQueue = Promise.resolve();
975
1044
  let removeAbortListener;
976
1045
  const finishSocketCycle = () => {
977
1046
  if (settled) return;
@@ -980,6 +1049,20 @@ async function startClickClackGatewayAccount(ctx) {
980
1049
  removeAbortListener = void 0;
981
1050
  resolve();
982
1051
  };
1052
+ const finishAfterQueuedMessages = () => {
1053
+ messageQueue.then(() => finishSocketCycle(), () => finishSocketCycle());
1054
+ };
1055
+ const reconnectAfterMessageFailure = (error) => {
1056
+ if (settled || ctx.abortSignal.aborted) return;
1057
+ if (!loggedMessageFailure) {
1058
+ loggedMessageFailure = true;
1059
+ ctx.log?.warn?.(`[${account.accountId}] ClickClack event processing failed; reconnecting: ${error instanceof Error ? error.message : formatErrorMessage(error)}`);
1060
+ }
1061
+ if (!closing) {
1062
+ closing = true;
1063
+ socket.close();
1064
+ }
1065
+ };
983
1066
  const abort = () => {
984
1067
  socket.close();
985
1068
  finishSocketCycle();
@@ -987,36 +1070,38 @@ async function startClickClackGatewayAccount(ctx) {
987
1070
  ctx.abortSignal.addEventListener("abort", abort, { once: true });
988
1071
  removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
989
1072
  socket.on("message", (data) => {
990
- (async () => {
1073
+ if (closing || settled) return;
1074
+ messageQueue = messageQueue.then(async () => {
991
1075
  const event = parseSocketEvent(data);
992
1076
  if (!event) {
993
1077
  ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
994
1078
  return;
995
1079
  }
1080
+ await processIncomingEvent(event);
996
1081
  afterCursor = event.cursor || afterCursor;
997
- await processEvent({
998
- account,
999
- config: ctx.cfg,
1000
- client,
1001
- event,
1002
- botUserId: account.botUserId ?? ""
1003
- });
1004
- })().catch((e) => reject(e instanceof Error ? e : new Error(`ClickClack ws message failed: ${formatErrorMessage(e)}`, { cause: e })));
1082
+ });
1083
+ messageQueue.catch(reconnectAfterMessageFailure);
1084
+ });
1085
+ socket.on("close", () => {
1086
+ closing = true;
1087
+ finishAfterQueuedMessages();
1005
1088
  });
1006
- socket.on("close", finishSocketCycle);
1007
1089
  socket.on("error", (error) => {
1008
1090
  if (settled || ctx.abortSignal.aborted) {
1009
1091
  finishSocketCycle();
1010
1092
  return;
1011
1093
  }
1094
+ if (closing) return;
1012
1095
  ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
1013
- finishSocketCycle();
1096
+ closing = true;
1014
1097
  socket.close();
1015
1098
  });
1016
1099
  });
1017
- if (!ctx.abortSignal.aborted) await new Promise((resolve) => {
1018
- setTimeout(resolve, account.reconnectMs);
1019
- });
1100
+ if (!ctx.abortSignal.aborted) try {
1101
+ await sleepWithAbort(account.reconnectMs, ctx.abortSignal);
1102
+ } catch (error) {
1103
+ if (!ctx.abortSignal.aborted) throw error;
1104
+ }
1020
1105
  }
1021
1106
  } finally {
1022
1107
  ctx.setStatus({
@@ -1031,40 +1116,84 @@ async function startClickClackGatewayAccount(ctx) {
1031
1116
  * ClickClack channel plugin definition: target parsing, account config, status,
1032
1117
  * gateway startup, and outbound delivery wiring.
1033
1118
  */
1034
- const CHANNEL_ID = "clickclack";
1035
- const meta = { ...getChatChannelMeta(CHANNEL_ID) };
1119
+ const CHANNEL_ID = CLICKCLACK_CHANNEL_ID;
1036
1120
  const clickClackMessageAdapter = defineChannelMessageAdapter({
1037
1121
  id: CHANNEL_ID,
1038
- durableFinal: { capabilities: {
1039
- text: true,
1040
- replyTo: true,
1041
- thread: true,
1042
- messageSendingHooks: true
1043
- } },
1044
- send: { text: async (ctx) => {
1045
- const result = await sendClickClackText({
1046
- cfg: ctx.cfg,
1047
- accountId: ctx.accountId,
1048
- to: ctx.to,
1049
- text: ctx.text,
1050
- threadId: ctx.threadId,
1051
- replyToId: ctx.replyToId
1052
- });
1053
- const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
1054
- const replyToId = ctx.replyToId ?? void 0;
1055
- return {
1056
- messageId: result.messageId,
1057
- receipt: createMessageReceiptFromOutboundResults({
1058
- results: [{
1059
- channel: CHANNEL_ID,
1060
- messageId: result.messageId
1061
- }],
1062
- threadId,
1063
- replyToId,
1064
- kind: "text"
1065
- })
1066
- };
1067
- } }
1122
+ durableFinal: {
1123
+ capabilities: {
1124
+ text: true,
1125
+ media: true,
1126
+ replyTo: true,
1127
+ thread: true,
1128
+ messageSendingHooks: true,
1129
+ reconcileUnknownSend: true
1130
+ },
1131
+ reconcileUnknownSendKinds: {
1132
+ text: true,
1133
+ media: true
1134
+ },
1135
+ reconcileUnknownSend: reconcileClickClackUnknownSend
1136
+ },
1137
+ send: {
1138
+ text: async (ctx) => {
1139
+ const messageId = await sendClickClackText({
1140
+ cfg: ctx.cfg,
1141
+ accountId: ctx.accountId,
1142
+ to: ctx.to,
1143
+ text: ctx.text,
1144
+ threadId: ctx.threadId,
1145
+ replyToId: ctx.replyToId,
1146
+ deliveryQueueId: ctx.deliveryQueueId,
1147
+ deliveryPartIndex: ctx.deliveryPartIndex,
1148
+ onPlatformSendDispatch: ctx.onPlatformSendDispatch
1149
+ });
1150
+ const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
1151
+ const replyToId = ctx.replyToId ?? void 0;
1152
+ return {
1153
+ ...messageId ? { messageId } : {},
1154
+ receipt: createMessageReceiptFromOutboundResults({
1155
+ results: messageId ? [{
1156
+ channel: CHANNEL_ID,
1157
+ messageId
1158
+ }] : [],
1159
+ threadId,
1160
+ replyToId,
1161
+ kind: "text"
1162
+ })
1163
+ };
1164
+ },
1165
+ media: async (ctx) => {
1166
+ const messageId = await sendClickClackMedia({
1167
+ cfg: ctx.cfg,
1168
+ accountId: ctx.accountId,
1169
+ to: ctx.to,
1170
+ text: ctx.text,
1171
+ mediaUrl: ctx.mediaUrl,
1172
+ mediaAccess: ctx.mediaAccess,
1173
+ mediaLocalRoots: ctx.mediaLocalRoots,
1174
+ mediaReadFile: ctx.mediaReadFile,
1175
+ threadId: ctx.threadId,
1176
+ replyToId: ctx.replyToId,
1177
+ deliveryQueueId: ctx.deliveryQueueId,
1178
+ deliveryPartIndex: ctx.deliveryPartIndex,
1179
+ onPlatformSendDispatch: ctx.onPlatformSendDispatch
1180
+ });
1181
+ const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
1182
+ const replyToId = ctx.replyToId ?? void 0;
1183
+ return {
1184
+ messageId,
1185
+ receipt: createMessageReceiptFromOutboundResults({
1186
+ results: [{
1187
+ channel: CHANNEL_ID,
1188
+ messageId
1189
+ }],
1190
+ threadId,
1191
+ replyToId,
1192
+ kind: "media"
1193
+ })
1194
+ };
1195
+ }
1196
+ }
1068
1197
  });
1069
1198
  /**
1070
1199
  * Channel plugin instance registered by the bundled ClickClack entry.
@@ -1072,7 +1201,7 @@ const clickClackMessageAdapter = defineChannelMessageAdapter({
1072
1201
  const clickClackPlugin = createChatChannelPlugin({
1073
1202
  base: {
1074
1203
  id: CHANNEL_ID,
1075
- meta,
1204
+ meta: clickClackMeta,
1076
1205
  capabilities: {
1077
1206
  chatTypes: ["direct", "group"],
1078
1207
  threads: true,
@@ -1080,22 +1209,12 @@ const clickClackPlugin = createChatChannelPlugin({
1080
1209
  },
1081
1210
  reload: { configPrefixes: ["channels.clickclack"] },
1082
1211
  configSchema: clickClackConfigSchema,
1083
- config: {
1084
- listAccountIds: (cfg) => listClickClackAccountIds(cfg),
1085
- resolveAccount: (cfg, accountId) => resolveClickClackAccount({
1086
- cfg,
1087
- accountId
1088
- }),
1089
- defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg),
1090
- isConfigured: (account) => account.configured,
1091
- resolveAllowFrom: ({ cfg, accountId }) => resolveClickClackAccount({
1092
- cfg,
1093
- accountId
1094
- }).allowFrom,
1095
- resolveDefaultTo: ({ cfg, accountId }) => resolveClickClackAccount({
1096
- cfg,
1097
- accountId
1098
- }).defaultTo
1212
+ config: clickClackConfigAdapter,
1213
+ setup: clickClackSetupAdapter,
1214
+ setupWizard: clickClackSetupWizard,
1215
+ secrets: {
1216
+ secretTargetRegistryEntries,
1217
+ collectRuntimeConfigAssignments
1099
1218
  },
1100
1219
  messaging: {
1101
1220
  targetPrefixes: ["clickclack", "cc"],
@@ -1162,16 +1281,39 @@ const clickClackPlugin = createChatChannelPlugin({
1162
1281
  base: { deliveryMode: "direct" },
1163
1282
  attachedResults: {
1164
1283
  channel: CHANNEL_ID,
1165
- sendText: async ({ cfg, to, text, accountId, threadId, replyToId }) => await sendClickClackText({
1166
- cfg,
1167
- accountId,
1168
- to,
1169
- text,
1170
- threadId,
1171
- replyToId
1172
- })
1284
+ sendText: async ({ cfg, to, text, accountId, threadId, replyToId, deliveryQueueId, deliveryPartIndex, onPlatformSendDispatch }) => {
1285
+ return { messageId: await sendClickClackText({
1286
+ cfg,
1287
+ accountId,
1288
+ to,
1289
+ text,
1290
+ threadId,
1291
+ replyToId,
1292
+ deliveryQueueId,
1293
+ deliveryPartIndex,
1294
+ onPlatformSendDispatch
1295
+ }) ?? "" };
1296
+ },
1297
+ sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId, threadId, replyToId, deliveryQueueId, deliveryPartIndex, onPlatformSendDispatch }) => {
1298
+ if (!mediaUrl) throw new Error("ClickClack media send requires mediaUrl");
1299
+ return { messageId: await sendClickClackMedia({
1300
+ cfg,
1301
+ accountId,
1302
+ to,
1303
+ text,
1304
+ mediaUrl,
1305
+ mediaAccess,
1306
+ mediaLocalRoots,
1307
+ mediaReadFile,
1308
+ threadId,
1309
+ replyToId,
1310
+ deliveryQueueId,
1311
+ deliveryPartIndex,
1312
+ onPlatformSendDispatch
1313
+ }) };
1314
+ }
1173
1315
  }
1174
1316
  }
1175
1317
  });
1176
1318
  //#endregion
1177
- export { getClickClackRuntime as a, DEFAULT_ACCOUNT_ID as c, resolveClickClackAccount as d, resolveDefaultClickClackAccountId as f, createClickClackClient as i, listClickClackAccountIds as l, buildClickClackTarget as n, setClickClackRuntime as o, parseClickClackTarget as r, clickClackConfigSchema as s, clickClackPlugin as t, listEnabledClickClackAccounts as u };
1319
+ export { setClickClackRuntime as a, getClickClackRuntime as i, buildClickClackTarget as n, parseClickClackTarget as r, clickClackPlugin as t };