@openclaw/clickclack 2026.9.8 → 2026.10.1-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.
- package/dist/.setup/{channel-Bcwtohk7.mjs → channel-DI3hKiSg.mjs} +42 -142
- package/dist/.setup/{resolve-BHGLohni.mjs → resolve-DPtjHQHt.mjs} +43 -74
- package/dist/.setup/{runtime-api-BvP8DrPQ.mjs → runtime-api-Dm_MQHNq.mjs} +127 -189
- package/dist/.setup/secret-contract-CF7TKtkU.mjs +26 -0
- package/dist/.setup/{secret-contract-api-C1QwGlzF.mjs → secret-contract-api-CBcsgIYZ.mjs} +1 -1
- package/dist/.setup/{setup-claim-Cx8knvXs.mjs → setup-claim-Cfv5ikVH.mjs} +1 -1
- package/dist/.setup/{setup-plugin-api-BH_nw-AP.mjs → setup-plugin-api-BXhjNJRg.mjs} +1 -1
- package/dist/.setup/{setup-plugin-api-C9QcBJuW.mjs → setup-plugin-api-CqfAb-CU.mjs} +1 -1
- package/dist/.setup/{setup-surface-BW1Q06w5.mjs → setup-surface-DcF_7-VV.mjs} +6 -21
- package/dist/.setup/{setup-verify-Msgd70_M.mjs → setup-verify-POQZk8SG.mjs} +5 -14
- package/dist/api.js +3 -3
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +6 -0
- package/dist/index.js +1 -1
- package/dist/runtime-api.js +4 -4
- package/dist/secret-contract-api.js +1 -1
- package/dist/setup-entry.js +2 -2
- package/dist/setup-plugin-api.js +1 -1
- package/package.json +5 -5
- package/dist/.setup/secret-contract-BKkzzjBA.mjs +0 -35
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { h as resolveClickClackAccount, p as listClickClackAccountIds } from "./setup-surface-
|
|
2
|
-
import { C as slugifyDiscussionLabel, D as listPendingDiscussionOpens, E as clearPendingDiscussionOpen, O as recordPendingDiscussionOpen, S as resolveDiscussionLabel, T as clearDiscussionBindingGeneration, _ as getClickClackDiscussionBindingStore, b as fallbackDiscussionLabel, c as markClickClackDiscussionChannelIdentityRevoked, d as discussionInfoForBinding, f as normalizedServerBaseUrl, g as bindingMatchesActiveSessionIncarnation, h as attachBindingToCurrentActiveSession, k as reserveDiscussionBindingGeneration, l as markClickClackDiscussionChannelRevoked, o as clearClickClackDiscussionChannelRevoked, p as resolveDiscussionBindingAccount, s as isClickClackDiscussionChannelRevoked, u as discussionAccounts, v as discussionCredentialFingerprint, w as truncateDiscussionDisplayTitle, x as isDiscussionSessionKey, y as discussionExternalRef } from "./channel-
|
|
3
|
-
import { a as
|
|
1
|
+
import { h as resolveClickClackAccount, p as listClickClackAccountIds } from "./setup-surface-DcF_7-VV.mjs";
|
|
2
|
+
import { C as slugifyDiscussionLabel, D as listPendingDiscussionOpens, E as clearPendingDiscussionOpen, O as recordPendingDiscussionOpen, S as resolveDiscussionLabel, T as clearDiscussionBindingGeneration, _ as getClickClackDiscussionBindingStore, b as fallbackDiscussionLabel, c as markClickClackDiscussionChannelIdentityRevoked, d as discussionInfoForBinding, f as normalizedServerBaseUrl, g as bindingMatchesActiveSessionIncarnation, h as attachBindingToCurrentActiveSession, k as reserveDiscussionBindingGeneration, l as markClickClackDiscussionChannelRevoked, o as clearClickClackDiscussionChannelRevoked, p as resolveDiscussionBindingAccount, s as isClickClackDiscussionChannelRevoked, u as discussionAccounts, v as discussionCredentialFingerprint, w as truncateDiscussionDisplayTitle, x as isDiscussionSessionKey, y as discussionExternalRef } from "./channel-DI3hKiSg.mjs";
|
|
3
|
+
import { a as createClickClackClient, i as ClickClackHttpError, o as isClickClackChannelNameConflict, t as findClickClackWorkspace } from "./resolve-DPtjHQHt.mjs";
|
|
4
4
|
import "../api.js";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { createAsyncLock } from "openclaw/plugin-sdk/async-lock-runtime";
|
|
6
7
|
import { buildControlUiSessionPath, registerSessionDiscussionProvider } from "openclaw/plugin-sdk/session-discussion";
|
|
7
8
|
import { createSessionVisibilityChecker } from "openclaw/plugin-sdk/session-visibility";
|
|
9
|
+
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
8
10
|
import { resolveSessionAgentIdStrict } from "openclaw/plugin-sdk/agent-scope-runtime";
|
|
9
11
|
import { textResult } from "openclaw/plugin-sdk/tool-results";
|
|
10
12
|
//#region extensions/clickclack/src/discussions/binding-retention.ts
|
|
@@ -121,77 +123,55 @@ const RECONCILE_RETRY_MAX_MS = 6e4;
|
|
|
121
123
|
* traffic can never postpone one); a failed run sets a not-before floor with
|
|
122
124
|
* growing backoff that later events cannot undercut; events during an
|
|
123
125
|
* in-flight run collapse into one follow-up scheduled after settlement; and
|
|
124
|
-
*
|
|
125
|
-
* cycles cannot corrupt the bookkeeping of the next one.
|
|
126
|
+
* each service activation owns a separate scheduler scope and retry state.
|
|
126
127
|
*/
|
|
127
128
|
var DiscussionReconcileScheduler = class {
|
|
128
129
|
#hooks;
|
|
129
|
-
#timers = /* @__PURE__ */ new Map();
|
|
130
130
|
#retryState = /* @__PURE__ */ new Map();
|
|
131
131
|
#inFlight = /* @__PURE__ */ new Set();
|
|
132
132
|
#followUps = /* @__PURE__ */ new Set();
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
constructor(scheduler, hooks) {
|
|
134
|
+
this.scheduler = scheduler;
|
|
135
135
|
this.#hooks = hooks;
|
|
136
136
|
}
|
|
137
|
-
/** Invalidates callbacks scheduled by prior activations. */
|
|
138
|
-
supersede() {
|
|
139
|
-
this.#generation += 1;
|
|
140
|
-
}
|
|
141
|
-
clear() {
|
|
142
|
-
for (const scheduled of this.#timers.values()) clearTimeout(scheduled.timer);
|
|
143
|
-
this.#timers.clear();
|
|
144
|
-
this.#retryState.clear();
|
|
145
|
-
this.#inFlight.clear();
|
|
146
|
-
this.#followUps.clear();
|
|
147
|
-
}
|
|
148
137
|
schedule(sessionKey, delayMs = RECONCILE_DEBOUNCE_MS) {
|
|
149
|
-
if (
|
|
138
|
+
if (this.scheduler.signal.aborted) return;
|
|
150
139
|
if (this.#inFlight.has(sessionKey)) {
|
|
151
140
|
this.#followUps.add(sessionKey);
|
|
152
141
|
return;
|
|
153
142
|
}
|
|
154
143
|
const retry = this.#retryState.get(sessionKey);
|
|
155
|
-
const fireAt = Math.max(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
if (generation !== this.#generation) return;
|
|
181
|
-
this.#inFlight.delete(sessionKey);
|
|
182
|
-
if (this.#followUps.delete(sessionKey)) this.schedule(sessionKey);
|
|
183
|
-
});
|
|
184
|
-
}, Math.max(fireAt - Date.now(), 0));
|
|
185
|
-
timer.unref?.();
|
|
186
|
-
this.#timers.set(sessionKey, {
|
|
187
|
-
fireAt,
|
|
188
|
-
timer
|
|
144
|
+
const fireAt = Math.max(this.scheduler.now() + delayMs, retry?.notBefore ?? 0);
|
|
145
|
+
this.scheduler.schedule({
|
|
146
|
+
id: `session:${sessionKey}`,
|
|
147
|
+
atMs: fireAt,
|
|
148
|
+
mode: "earliest",
|
|
149
|
+
run: async () => {
|
|
150
|
+
this.#inFlight.add(sessionKey);
|
|
151
|
+
try {
|
|
152
|
+
await this.#hooks.run(sessionKey);
|
|
153
|
+
this.#retryState.delete(sessionKey);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (this.scheduler.signal.aborted) return;
|
|
156
|
+
const previous = this.#retryState.get(sessionKey);
|
|
157
|
+
const nextRetryMs = Math.min(previous === void 0 ? RECONCILE_RETRY_MS : previous.delayMs * 2, RECONCILE_RETRY_MAX_MS);
|
|
158
|
+
this.#retryState.set(sessionKey, {
|
|
159
|
+
delayMs: nextRetryMs,
|
|
160
|
+
notBefore: this.scheduler.now() + nextRetryMs
|
|
161
|
+
});
|
|
162
|
+
this.#hooks.warn(`discussion event reconcile failed for ${sessionKey}; retrying in ${nextRetryMs}ms: ${String(error)}`);
|
|
163
|
+
this.#followUps.add(sessionKey);
|
|
164
|
+
} finally {
|
|
165
|
+
this.#inFlight.delete(sessionKey);
|
|
166
|
+
if (!this.scheduler.signal.aborted && this.#followUps.delete(sessionKey)) this.schedule(sessionKey);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
189
169
|
});
|
|
190
170
|
}
|
|
191
171
|
};
|
|
192
172
|
//#endregion
|
|
193
173
|
//#region extensions/clickclack/src/discussions/service-open.ts
|
|
194
|
-
const CHANNEL_NAME_MUTATION_ATTEMPTS
|
|
174
|
+
const CHANNEL_NAME_MUTATION_ATTEMPTS = 4;
|
|
195
175
|
function isDefinitiveNoCreateHttpError(error) {
|
|
196
176
|
if (!(error instanceof ClickClackHttpError) || error.status < 400 || error.status >= 500) return false;
|
|
197
177
|
return ![
|
|
@@ -233,6 +213,26 @@ function assertChannelPatch(channel, patch) {
|
|
|
233
213
|
if ((key === "external_ref" || key === "external_url" || key === "sidebar_section" ? channel[key] ?? "" : channel[key]) !== expected) throw new Error(`ClickClack channel update did not apply ${key}`);
|
|
234
214
|
}
|
|
235
215
|
}
|
|
216
|
+
async function renameDiscussionChannel(params) {
|
|
217
|
+
for (let attempt = 0; attempt < CHANNEL_NAME_MUTATION_ATTEMPTS; attempt += 1) {
|
|
218
|
+
const patch = {
|
|
219
|
+
...params.patch,
|
|
220
|
+
name: await resolveAvailableChannelName({
|
|
221
|
+
...params,
|
|
222
|
+
ownChannelId: params.channelId
|
|
223
|
+
})
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
params.assertCurrentAuthority?.();
|
|
227
|
+
const channel = await params.client.updateChannel(params.channelId, patch);
|
|
228
|
+
assertChannelPatch(channel, patch);
|
|
229
|
+
return channel;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (!isClickClackChannelNameConflict(error) || attempt === 3) throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw new Error("ClickClack discussion channel name retries were exhausted");
|
|
235
|
+
}
|
|
236
236
|
function assertManagedChannelContract(channel, expected) {
|
|
237
237
|
const displayTitleMatches = !("display_title" in channel) || channel.display_title === expected.displayTitle;
|
|
238
238
|
if (channel.external_managed !== true || (channel.external_ref ?? "") !== (expected.externalRef ?? "") || (channel.sidebar_section ?? "") !== (expected.section ?? "") || (channel.external_url ?? "") !== (expected.externalUrl ?? "") || !displayTitleMatches) throw new Error(`ClickClack server does not support the managed discussion channel contract for ${expected.sessionKey}`);
|
|
@@ -249,7 +249,8 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
249
249
|
if (!entry || entry.archivedAt !== void 0) return;
|
|
250
250
|
if (!entry.sessionId?.trim()) throw new Error("OpenClaw session does not yet have a concrete session id");
|
|
251
251
|
const client = params.clientFactory(account);
|
|
252
|
-
const
|
|
252
|
+
const workspaces = await client.workspaces();
|
|
253
|
+
const workspace = findClickClackWorkspace(workspaces, account.discussions.workspace);
|
|
253
254
|
if (!workspace) throw new Error(`ClickClack discussions workspace not found: ${account.discussions.workspace}`);
|
|
254
255
|
if (!workspace.route_id) throw new Error("ClickClack discussions workspace is missing its route id");
|
|
255
256
|
const serverBaseUrl = normalizedServerBaseUrl(account);
|
|
@@ -291,11 +292,16 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
291
292
|
destinationIdentity,
|
|
292
293
|
createGeneration: params.bindingGenerationFactory
|
|
293
294
|
});
|
|
295
|
+
const generationScope = {
|
|
296
|
+
runtime,
|
|
297
|
+
sessionKey,
|
|
298
|
+
expectedGeneration: bindingGeneration
|
|
299
|
+
};
|
|
294
300
|
const externalRef = discussionExternalRef(params.installationId, sessionKey, destinationIdentity, bindingGeneration);
|
|
295
301
|
let adopted;
|
|
296
302
|
let managedFields;
|
|
297
303
|
let resolved;
|
|
298
|
-
for (let attempt = 0; attempt < CHANNEL_NAME_MUTATION_ATTEMPTS
|
|
304
|
+
for (let attempt = 0; attempt < CHANNEL_NAME_MUTATION_ATTEMPTS; attempt += 1) {
|
|
299
305
|
adopted = channels.find((candidate) => candidate.external_managed === true && candidate.external_ref === externalRef);
|
|
300
306
|
managedFields = {
|
|
301
307
|
name: await resolveAvailableChannelName({
|
|
@@ -373,17 +379,9 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
373
379
|
resolved = await client.updateChannel(recovered.id, managedFields);
|
|
374
380
|
break;
|
|
375
381
|
}
|
|
376
|
-
if (definitiveNoCreate) await clearDiscussionBindingGeneration(
|
|
377
|
-
runtime,
|
|
378
|
-
sessionKey,
|
|
379
|
-
expectedGeneration: bindingGeneration
|
|
380
|
-
});
|
|
382
|
+
if (definitiveNoCreate) await clearDiscussionBindingGeneration(generationScope);
|
|
381
383
|
} catch {
|
|
382
|
-
if (definitiveNoCreate && !adopted) await clearDiscussionBindingGeneration(
|
|
383
|
-
runtime,
|
|
384
|
-
sessionKey,
|
|
385
|
-
expectedGeneration: bindingGeneration
|
|
386
|
-
});
|
|
384
|
+
if (definitiveNoCreate && !adopted) await clearDiscussionBindingGeneration(generationScope);
|
|
387
385
|
}
|
|
388
386
|
throw error;
|
|
389
387
|
}
|
|
@@ -399,20 +397,12 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
399
397
|
});
|
|
400
398
|
if (adopted) assertChannelPatch(resolved, managedFields);
|
|
401
399
|
} catch (error) {
|
|
402
|
-
await clearPendingDiscussionOpen(
|
|
403
|
-
runtime,
|
|
404
|
-
sessionKey,
|
|
405
|
-
expectedGeneration: bindingGeneration
|
|
406
|
-
});
|
|
400
|
+
await clearPendingDiscussionOpen(generationScope);
|
|
407
401
|
params.warn(`incompatible discussion channel remains quarantined: ${resolved.id}`);
|
|
408
402
|
throw error;
|
|
409
403
|
}
|
|
410
404
|
if (!resolved.route_id) {
|
|
411
|
-
await clearPendingDiscussionOpen(
|
|
412
|
-
runtime,
|
|
413
|
-
sessionKey,
|
|
414
|
-
expectedGeneration: bindingGeneration
|
|
415
|
-
});
|
|
405
|
+
await clearPendingDiscussionOpen(generationScope);
|
|
416
406
|
params.warn(`route-less discussion channel remains quarantined: ${resolved.id}`);
|
|
417
407
|
throw new Error("ClickClack discussion channel is missing its route id");
|
|
418
408
|
}
|
|
@@ -422,11 +412,7 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
422
412
|
readConsistency: "latest"
|
|
423
413
|
});
|
|
424
414
|
if (!currentEntry?.sessionId || currentEntry.archivedAt !== void 0) {
|
|
425
|
-
await clearPendingDiscussionOpen(
|
|
426
|
-
runtime,
|
|
427
|
-
sessionKey,
|
|
428
|
-
expectedGeneration: bindingGeneration
|
|
429
|
-
});
|
|
415
|
+
await clearPendingDiscussionOpen(generationScope);
|
|
430
416
|
params.warn(`unattached discussion channel remains quarantined: ${channel.id}`);
|
|
431
417
|
throw new Error("OpenClaw session became inactive while opening its ClickClack discussion");
|
|
432
418
|
}
|
|
@@ -436,36 +422,23 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
436
422
|
const currentExternalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? agentId, config.session?.mainKey, currentLabel) ?? "";
|
|
437
423
|
let currentChannel = channel;
|
|
438
424
|
if (currentEntry.sessionId !== entry.sessionId || currentLabel !== label || currentDisplayTitle !== displayTitle || currentSection !== section || currentExternalUrl !== (externalUrl ?? "")) try {
|
|
439
|
-
|
|
440
|
-
|
|
425
|
+
currentChannel = await renameDiscussionChannel({
|
|
426
|
+
client,
|
|
427
|
+
workspaceId: workspace.id,
|
|
428
|
+
channelId: channel.id,
|
|
429
|
+
label: currentLabel,
|
|
430
|
+
sessionKey,
|
|
431
|
+
agentId,
|
|
432
|
+
patch: {
|
|
441
433
|
...managedFields,
|
|
442
|
-
name: await resolveAvailableChannelName({
|
|
443
|
-
client,
|
|
444
|
-
workspaceId: workspace.id,
|
|
445
|
-
label: currentLabel,
|
|
446
|
-
sessionKey,
|
|
447
|
-
agentId,
|
|
448
|
-
ownChannelId: channel.id
|
|
449
|
-
}),
|
|
450
434
|
external_url: currentExternalUrl,
|
|
451
435
|
sidebar_section: currentSection,
|
|
452
436
|
display_title: currentDisplayTitle
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
assertCurrentAuthority();
|
|
456
|
-
currentChannel = await client.updateChannel(channel.id, latestManagedFields);
|
|
457
|
-
assertChannelPatch(currentChannel, latestManagedFields);
|
|
458
|
-
break;
|
|
459
|
-
} catch (error) {
|
|
460
|
-
if (!isClickClackChannelNameConflict(error) || attempt === 3) throw error;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
} catch (error) {
|
|
464
|
-
await clearPendingDiscussionOpen({
|
|
465
|
-
runtime,
|
|
466
|
-
sessionKey,
|
|
467
|
-
expectedGeneration: bindingGeneration
|
|
437
|
+
},
|
|
438
|
+
assertCurrentAuthority
|
|
468
439
|
});
|
|
440
|
+
} catch (error) {
|
|
441
|
+
await clearPendingDiscussionOpen(generationScope);
|
|
469
442
|
params.warn(`unattached discussion channel remains quarantined: ${channel.id}`);
|
|
470
443
|
throw error;
|
|
471
444
|
}
|
|
@@ -491,11 +464,7 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
491
464
|
assertCurrentAuthority();
|
|
492
465
|
store.set(sessionKey, nextBinding);
|
|
493
466
|
} catch (error) {
|
|
494
|
-
await clearPendingDiscussionOpen(
|
|
495
|
-
runtime,
|
|
496
|
-
sessionKey,
|
|
497
|
-
expectedGeneration: bindingGeneration
|
|
498
|
-
});
|
|
467
|
+
await clearPendingDiscussionOpen(generationScope);
|
|
499
468
|
params.warn(`unbound discussion channel remains quarantined: ${channel.id}`);
|
|
500
469
|
throw error;
|
|
501
470
|
}
|
|
@@ -524,7 +493,8 @@ async function reconcilePendingDiscussionOpen(params) {
|
|
|
524
493
|
const activeAccounts = discussionAccounts(cfg);
|
|
525
494
|
const retryAccount = activeAccounts.length === 1 ? activeAccounts[0] : void 0;
|
|
526
495
|
if (params.allowRetry !== false && entry?.sessionId && entry.archivedAt === void 0 && retryAccount && normalizedServerBaseUrl(retryAccount) === pending.serverBaseUrl && discussionCredentialFingerprint(retryAccount.token) === pending.credentialFingerprint) {
|
|
527
|
-
|
|
496
|
+
const workspaces = await params.clientFactory(retryAccount).workspaces();
|
|
497
|
+
if (findClickClackWorkspace(workspaces, retryAccount.discussions.workspace)?.id === pending.workspaceId) {
|
|
528
498
|
await params.open(pending.sessionKey);
|
|
529
499
|
return;
|
|
530
500
|
}
|
|
@@ -555,7 +525,6 @@ async function reconcilePendingDiscussionOpen(params) {
|
|
|
555
525
|
//#endregion
|
|
556
526
|
//#region extensions/clickclack/src/discussions/service.ts
|
|
557
527
|
const RECONCILE_INTERVAL_MS = 6e4;
|
|
558
|
-
const CHANNEL_NAME_MUTATION_ATTEMPTS = 4;
|
|
559
528
|
var ClickClackDiscussionService = class {
|
|
560
529
|
#runtime;
|
|
561
530
|
#store;
|
|
@@ -563,14 +532,10 @@ var ClickClackDiscussionService = class {
|
|
|
563
532
|
#installationId;
|
|
564
533
|
#bindingGenerationFactory;
|
|
565
534
|
#detachedBindings;
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#reconcileScheduler
|
|
569
|
-
|
|
570
|
-
run: async (sessionKey) => await this.reconcile(sessionKey),
|
|
571
|
-
warn: (message) => this.#logger().warn(message)
|
|
572
|
-
});
|
|
573
|
-
#channelMutationLock = Promise.resolve();
|
|
535
|
+
#sessionQueue = new KeyedAsyncQueue();
|
|
536
|
+
#scheduler;
|
|
537
|
+
#reconcileScheduler;
|
|
538
|
+
#withChannelMutationLock = createAsyncLock();
|
|
574
539
|
#timer;
|
|
575
540
|
#reconcileAllPromise;
|
|
576
541
|
#unsubscribeSessionsChanged;
|
|
@@ -592,31 +557,31 @@ var ClickClackDiscussionService = class {
|
|
|
592
557
|
store: this.#store,
|
|
593
558
|
maxRetained: options.maxRetainedDetachedBindings ?? 1e3
|
|
594
559
|
});
|
|
595
|
-
this.#timersEnabled = options.startTimer !== false;
|
|
596
560
|
this.provider = {
|
|
597
561
|
id: "clickclack",
|
|
598
562
|
info: async ({ sessionKey }) => await this.info(sessionKey),
|
|
599
563
|
open: async ({ sessionKey }) => await this.open(sessionKey)
|
|
600
564
|
};
|
|
601
|
-
if (options.gatewayEvents) this.bindGatewayEvents(options.gatewayEvents).catch((error) => {
|
|
602
|
-
this.#logger().warn(`discussion activation failed: ${String(error)}`);
|
|
603
|
-
});
|
|
604
|
-
if (this.#timersEnabled && !options.gatewayEvents) this.#withOperation(() => this.#ensureTimer()).catch((error) => {
|
|
605
|
-
this.#logger().warn(`discussion timer initialization failed: ${String(error)}`);
|
|
606
|
-
});
|
|
607
565
|
}
|
|
608
|
-
async bindGatewayEvents(gatewayEvents) {
|
|
566
|
+
async bindGatewayEvents(gatewayEvents, scheduler) {
|
|
609
567
|
const activation = ++this.#activation;
|
|
610
568
|
if (this.#cleanupPromise) await this.#cleanupPromise;
|
|
611
569
|
if (activation !== this.#activation) return;
|
|
570
|
+
if (this.#scheduler) await this.#scheduler.stop();
|
|
571
|
+
if (activation !== this.#activation) return;
|
|
612
572
|
this.#cleanupPromise = void 0;
|
|
613
573
|
this.#unsubscribeSessionsChanged?.();
|
|
614
574
|
this.#closed = false;
|
|
615
|
-
this.#
|
|
575
|
+
this.#scheduler = scheduler.scope();
|
|
576
|
+
this.#timer = void 0;
|
|
577
|
+
this.#reconcileScheduler = new DiscussionReconcileScheduler(this.#scheduler, {
|
|
578
|
+
run: (sessionKey) => this.reconcile(sessionKey),
|
|
579
|
+
warn: (message) => this.#logger().warn(message)
|
|
580
|
+
});
|
|
616
581
|
this.#unsubscribeSessionsChanged = gatewayEvents?.onSessionsChanged((event) => {
|
|
617
582
|
this.#withOperation(async () => {
|
|
618
583
|
if (this.#store.get(event.sessionKey) || (await listPendingDiscussionOpens(this.#runtime)).some((pending) => pending.sessionKey === event.sessionKey)) {
|
|
619
|
-
if (activation === this.#activation) this.#reconcileScheduler
|
|
584
|
+
if (activation === this.#activation) this.#reconcileScheduler?.schedule(event.sessionKey);
|
|
620
585
|
}
|
|
621
586
|
}).catch((error) => {
|
|
622
587
|
this.#logger().warn(`discussion event admission failed: ${String(error)}`);
|
|
@@ -625,7 +590,7 @@ var ClickClackDiscussionService = class {
|
|
|
625
590
|
await this.#withOperation(async () => {
|
|
626
591
|
for (const sessionKey of await this.#listReconcileSessionKeys()) {
|
|
627
592
|
if (activation !== this.#activation) return;
|
|
628
|
-
this.#reconcileScheduler
|
|
593
|
+
this.#reconcileScheduler?.schedule(sessionKey, 0);
|
|
629
594
|
}
|
|
630
595
|
await this.#ensureTimer();
|
|
631
596
|
});
|
|
@@ -634,7 +599,7 @@ var ClickClackDiscussionService = class {
|
|
|
634
599
|
return discussionAccounts(this.#currentConfig()).length === 1;
|
|
635
600
|
}
|
|
636
601
|
async info(sessionKey) {
|
|
637
|
-
return await this.#withOperation(() => this.#
|
|
602
|
+
return await this.#withOperation(() => this.#sessionQueue.enqueue(sessionKey, async () => {
|
|
638
603
|
if (discussionAccounts(this.#currentConfig()).length !== 1) return { state: "none" };
|
|
639
604
|
const existing = this.#store.get(sessionKey);
|
|
640
605
|
if (existing) {
|
|
@@ -658,7 +623,7 @@ var ClickClackDiscussionService = class {
|
|
|
658
623
|
}));
|
|
659
624
|
}
|
|
660
625
|
async open(sessionKey) {
|
|
661
|
-
return await this.#withOperation(() => this.#
|
|
626
|
+
return await this.#withOperation(() => this.#sessionQueue.enqueue(sessionKey, () => this.#open(sessionKey)));
|
|
662
627
|
}
|
|
663
628
|
async #open(sessionKey) {
|
|
664
629
|
this.#installationId ??= await getClickClackDiscussionInstallationId(this.#runtime);
|
|
@@ -706,7 +671,7 @@ var ClickClackDiscussionService = class {
|
|
|
706
671
|
}
|
|
707
672
|
async #reconcile(sessionKey) {
|
|
708
673
|
try {
|
|
709
|
-
await this.#
|
|
674
|
+
await this.#sessionQueue.enqueue(sessionKey, async () => {
|
|
710
675
|
const binding = this.#store.get(sessionKey);
|
|
711
676
|
if (binding) await this.#reconcileBinding(sessionKey, binding);
|
|
712
677
|
const pending = (await listPendingDiscussionOpens(this.#runtime)).find((candidate) => candidate.sessionKey === sessionKey);
|
|
@@ -736,12 +701,10 @@ var ClickClackDiscussionService = class {
|
|
|
736
701
|
});
|
|
737
702
|
}
|
|
738
703
|
async readLatestMessages(sessionKey, limit) {
|
|
739
|
-
return await this.#withOperation(() => this.#
|
|
704
|
+
return await this.#withOperation(() => this.#sessionQueue.enqueue(sessionKey, async () => {
|
|
740
705
|
const binding = this.#store.get(sessionKey);
|
|
741
706
|
if (!binding) return { text: "No discussion is bound to this session." };
|
|
742
707
|
const resolved = await this.#resolveBindingForUse(binding);
|
|
743
|
-
if (resolved.state === "retargeted") return { text: "No discussion is bound to this session." };
|
|
744
|
-
if (resolved.state === "stale") return { text: "No discussion is bound to this session." };
|
|
745
708
|
if (resolved.state !== "active") return { text: "No discussion is bound to this session." };
|
|
746
709
|
const attached = this.#refreshSessionAttachment(sessionKey, binding);
|
|
747
710
|
if (!attached) return { text: "No discussion is bound to this session." };
|
|
@@ -761,13 +724,9 @@ var ClickClackDiscussionService = class {
|
|
|
761
724
|
this.#activation += 1;
|
|
762
725
|
this.#unsubscribeSessionsChanged?.();
|
|
763
726
|
this.#unsubscribeSessionsChanged = void 0;
|
|
764
|
-
this.#reconcileScheduler
|
|
765
|
-
this.#
|
|
766
|
-
|
|
767
|
-
clearInterval(this.#timer);
|
|
768
|
-
this.#timer = void 0;
|
|
769
|
-
}
|
|
770
|
-
this.#cleanupPromise ??= Promise.allSettled(this.#operations).then(() => void 0);
|
|
727
|
+
this.#reconcileScheduler = void 0;
|
|
728
|
+
this.#timer = void 0;
|
|
729
|
+
this.#cleanupPromise ??= Promise.allSettled([this.#scheduler?.stop(), ...this.#operations]).then(() => void 0);
|
|
771
730
|
return this.#cleanupPromise;
|
|
772
731
|
}
|
|
773
732
|
async #reconcileBinding(sessionKey, binding, resolvedAccount) {
|
|
@@ -821,26 +780,15 @@ var ClickClackDiscussionService = class {
|
|
|
821
780
|
if (Object.keys(patch).length === 0 && !labelChanged) return;
|
|
822
781
|
const client = this.#clientFactory(account);
|
|
823
782
|
let updated;
|
|
824
|
-
if (labelChanged) updated = await this.#withChannelMutationLock(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
});
|
|
834
|
-
try {
|
|
835
|
-
const renamed = await client.updateChannel(currentBinding.channelId, patch);
|
|
836
|
-
assertChannelPatch(renamed, patch);
|
|
837
|
-
return renamed;
|
|
838
|
-
} catch (error) {
|
|
839
|
-
if (!isClickClackChannelNameConflict(error) || attempt === 3) throw error;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
throw new Error("ClickClack discussion channel name retries were exhausted");
|
|
843
|
-
});
|
|
783
|
+
if (labelChanged) updated = await this.#withChannelMutationLock(() => renameDiscussionChannel({
|
|
784
|
+
client,
|
|
785
|
+
workspaceId: currentBinding.workspaceId,
|
|
786
|
+
channelId: currentBinding.channelId,
|
|
787
|
+
label,
|
|
788
|
+
sessionKey,
|
|
789
|
+
agentId: currentBinding.agentId,
|
|
790
|
+
patch
|
|
791
|
+
}));
|
|
844
792
|
else {
|
|
845
793
|
updated = await client.updateChannel(currentBinding.channelId, patch);
|
|
846
794
|
assertChannelPatch(updated, patch);
|
|
@@ -910,7 +858,8 @@ var ClickClackDiscussionService = class {
|
|
|
910
858
|
async #resolveBindingForUse(binding) {
|
|
911
859
|
const resolved = resolveDiscussionBindingAccount(this.#currentConfig(), binding);
|
|
912
860
|
if (resolved.state !== "active") return resolved;
|
|
913
|
-
const
|
|
861
|
+
const workspaces = await this.#clientFactory(resolved.account).workspaces();
|
|
862
|
+
const workspace = findClickClackWorkspace(workspaces, resolved.account.discussions.workspace);
|
|
914
863
|
const current = resolveDiscussionBindingAccount(this.#currentConfig(), binding);
|
|
915
864
|
if (current.state !== "active") return current;
|
|
916
865
|
return workspace?.id === binding.workspaceId ? current : { state: "retargeted" };
|
|
@@ -924,18 +873,20 @@ var ClickClackDiscussionService = class {
|
|
|
924
873
|
const needsBindingPoll = this.#unsubscribeSessionsChanged === void 0 && this.#store.count() > 0;
|
|
925
874
|
if (this.#closed || !hasPendingOpens && !needsBindingPoll) {
|
|
926
875
|
if (this.#timer) {
|
|
927
|
-
|
|
876
|
+
this.#timer.cancel();
|
|
928
877
|
this.#timer = void 0;
|
|
929
878
|
}
|
|
930
879
|
return;
|
|
931
880
|
}
|
|
932
|
-
if (!this.#
|
|
933
|
-
this.#timer =
|
|
934
|
-
|
|
881
|
+
if (!this.#scheduler || this.#scheduler.signal.aborted || this.#timer) return;
|
|
882
|
+
this.#timer = this.#scheduler.schedule({
|
|
883
|
+
id: "reconcile-all",
|
|
884
|
+
delayMs: RECONCILE_INTERVAL_MS,
|
|
885
|
+
everyMs: RECONCILE_INTERVAL_MS,
|
|
886
|
+
run: () => this.reconcileAll().catch((error) => {
|
|
935
887
|
this.#logger().warn(`discussion reconcile pass failed: ${String(error)}`);
|
|
936
|
-
})
|
|
937
|
-
}
|
|
938
|
-
this.#timer.unref?.();
|
|
888
|
+
})
|
|
889
|
+
});
|
|
939
890
|
}
|
|
940
891
|
async #listReconcileSessionKeys() {
|
|
941
892
|
return /* @__PURE__ */ new Set([...this.#store.entries().map(({ sessionKey }) => sessionKey), ...(await listPendingDiscussionOpens(this.#runtime)).map(({ sessionKey }) => sessionKey)]);
|
|
@@ -953,20 +904,6 @@ var ClickClackDiscussionService = class {
|
|
|
953
904
|
operation.then(() => this.#operations.delete(operation), () => this.#operations.delete(operation));
|
|
954
905
|
return operation;
|
|
955
906
|
}
|
|
956
|
-
async #withSessionLock(sessionKey, run) {
|
|
957
|
-
const current = (this.#sessionLocks.get(sessionKey) ?? Promise.resolve()).catch(() => void 0).then(run);
|
|
958
|
-
this.#sessionLocks.set(sessionKey, current);
|
|
959
|
-
try {
|
|
960
|
-
return await current;
|
|
961
|
-
} finally {
|
|
962
|
-
if (this.#sessionLocks.get(sessionKey) === current) this.#sessionLocks.delete(sessionKey);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
async #withChannelMutationLock(run) {
|
|
966
|
-
const current = this.#channelMutationLock.catch(() => void 0).then(run);
|
|
967
|
-
this.#channelMutationLock = current;
|
|
968
|
-
return await current;
|
|
969
|
-
}
|
|
970
907
|
};
|
|
971
908
|
//#endregion
|
|
972
909
|
//#region extensions/clickclack/src/discussions/tool-policy.ts
|
|
@@ -1050,7 +987,8 @@ function registerClickClackDiscussions(api) {
|
|
|
1050
987
|
const service = new ClickClackDiscussionService(api.runtime);
|
|
1051
988
|
api.registerService({
|
|
1052
989
|
id: "clickclack-discussion-session-events",
|
|
1053
|
-
|
|
990
|
+
apiVersion: 2,
|
|
991
|
+
start: ({ gatewayEvents, scheduler }) => service.bindGatewayEvents(gatewayEvents, scheduler),
|
|
1054
992
|
stop: () => service.cleanup()
|
|
1055
993
|
});
|
|
1056
994
|
api.registerTool((context) => createClickClackDiscussionTool({
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
+
import { collectConditionalChannelFieldAssignments, createChannelSecretContract, hasConfiguredSecretInputValue } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
3
|
+
//#region extensions/clickclack/src/secret-contract.ts
|
|
4
|
+
const channelSecrets = createChannelSecretContract({
|
|
5
|
+
channelKey: "clickclack",
|
|
6
|
+
account: ["token"],
|
|
7
|
+
channel: ["token"],
|
|
8
|
+
collect(params) {
|
|
9
|
+
const { channel: clickclack } = params;
|
|
10
|
+
const baseTokenFile = normalizeOptionalString(clickclack.tokenFile) ?? "";
|
|
11
|
+
const accountTokenFile = (account) => normalizeOptionalString(account.tokenFile) ?? "";
|
|
12
|
+
const hasImplicitDefault = Boolean(normalizeOptionalString(clickclack.baseUrl)) && Boolean(normalizeOptionalString(clickclack.workspace));
|
|
13
|
+
collectConditionalChannelFieldAssignments({
|
|
14
|
+
...params,
|
|
15
|
+
field: "token",
|
|
16
|
+
topLevelActiveWithoutAccounts: baseTokenFile.length === 0,
|
|
17
|
+
topLevelInheritedAccountActive: ({ account, enabled }) => hasImplicitDefault && baseTokenFile.length === 0 || enabled && baseTokenFile.length === 0 && accountTokenFile(account).length === 0 && !hasConfiguredSecretInputValue(account.token, params.defaults),
|
|
18
|
+
accountActive: ({ account, enabled }) => enabled && accountTokenFile(account).length === 0,
|
|
19
|
+
topInactiveReason: "no enabled ClickClack account inherits this top-level token (tokenFile is configured).",
|
|
20
|
+
accountInactiveReason: "ClickClack account is disabled or tokenFile is configured."
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const { secretTargetRegistryEntries, collectRuntimeConfigAssignments } = channelSecrets;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { collectRuntimeConfigAssignments as n, secretTargetRegistryEntries as r, channelSecrets as t };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-
|
|
1
|
+
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-CF7TKtkU.mjs";
|
|
2
2
|
import "../secret-contract-api.js";
|
|
3
3
|
export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as isClickClackSetupLoopbackHost, i as buildClickClackSetupClaimUrl, o as requireClickClackSetupApiBaseUrl } from "./setup-surface-
|
|
1
|
+
import { a as isClickClackSetupLoopbackHost, i as buildClickClackSetupClaimUrl, o as requireClickClackSetupApiBaseUrl } from "./setup-surface-DcF_7-VV.mjs";
|
|
2
2
|
import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
3
3
|
import { createProviderOperationDeadline, readProviderJsonResponse, readResponseTextLimited, resolveProviderOperationTimeoutMs } from "openclaw/plugin-sdk/provider-http";
|
|
4
4
|
import { fetchWithSsrFGuard, resolvePinnedHostnameWithPolicy, ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as clickClackSetupPlugin } from "./setup-plugin-api-
|
|
1
|
+
import { t as clickClackSetupPlugin } from "./setup-plugin-api-CqfAb-CU.mjs";
|
|
2
2
|
export { clickClackSetupPlugin };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as clickClackConfigAdapter, n as clickClackSetupContract, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-
|
|
1
|
+
import { l as clickClackConfigAdapter, n as clickClackSetupContract, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-DcF_7-VV.mjs";
|
|
2
2
|
//#region extensions/clickclack/src/channel.setup.ts
|
|
3
3
|
const clickClackSetupPlugin = {
|
|
4
4
|
id: "clickclack",
|