@openclaw/clickclack 2026.10.5-beta.4 → 2026.10.10-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/README.md +3 -3
- package/dist/.setup/{channel-BFuhVeiX.mjs → channel-DRcZ_twH.mjs} +238 -122
- package/dist/.setup/{runtime-api-hAV5Muz-.mjs → runtime-api-BrlvVAQJ.mjs} +144 -84
- package/dist/.setup/secret-contract-api--45pFCg3.mjs +2 -0
- package/dist/.setup/{setup-claim-C-9Pvumz.mjs → setup-claim-BPuQuMKW.mjs} +1 -1
- package/dist/.setup/setup-plugin-api-0RMVREuM.mjs +1 -0
- package/dist/.setup/{setup-plugin-api-Qi8u_lra.mjs → setup-plugin-api-Ccxm2ydn.mjs} +1 -1
- package/dist/.setup/{setup-surface-BJ5D55LV.mjs → setup-surface-BJ3jioqA.mjs} +6 -6
- package/dist/.setup/{setup-verify-2ruJKFPj.mjs → setup-verify-DGsYtVh9.mjs} +10 -18
- package/dist/api.js +2 -2
- package/dist/channel-plugin-api.js +1 -2
- package/dist/index.js +1 -1
- package/dist/runtime-api.js +3 -3
- package/dist/secret-contract-api.js +1 -2
- package/dist/setup-entry.js +2 -2
- package/dist/setup-plugin-api.js +1 -2
- package/package.json +4 -4
- package/dist/.setup/secret-contract-api-CBcsgIYZ.mjs +0 -3
- package/dist/.setup/setup-plugin-api-DpKXmzV1.mjs +0 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ openclaw channels add clickclack --code 'https://clickclack.example.com/#XXXX-XX
|
|
|
18
18
|
|
|
19
19
|
Split-origin and path-mounted ClickClack deployments generate an exact
|
|
20
20
|
`/api/bot-setup-codes/claim#CODE` endpoint. OpenClaw validates the versioned
|
|
21
|
-
claim response and saves its
|
|
21
|
+
claim response and saves its normalized API base URL.
|
|
22
22
|
|
|
23
23
|
For manual token setup:
|
|
24
24
|
|
|
@@ -104,7 +104,7 @@ category changes remain reflected in the channel, but session archive, restore,
|
|
|
104
104
|
reset, and deletion never archive or replace it. ClickClack owns channel archive
|
|
105
105
|
and restore independently. `workspace`
|
|
106
106
|
defaults to the account workspace, and `section` defaults to `Sessions`.
|
|
107
|
-
`controlUrlBase` adds
|
|
107
|
+
`controlUrlBase` adds standard `/chat/<agent>/<session-ref>` links to the
|
|
108
108
|
OpenClaw Control UI, preserving base paths. Main sessions use `/chat/<agent>`.
|
|
109
109
|
|
|
110
110
|
ClickClack-managed embed URLs explicitly advertise host-theme support. The
|
|
@@ -131,7 +131,7 @@ and scoped grant. The previous side session cannot access the new main session.
|
|
|
131
131
|
Messages arriving through an inactive, disabled, or retargeted managed
|
|
132
132
|
binding are dropped instead of falling back to the account's normal channel
|
|
133
133
|
routing. Released bindings leave a durable revoked-channel marker so delayed
|
|
134
|
-
realtime events
|
|
134
|
+
realtime events are still rejected. Remote ownership is keyed by ClickClack
|
|
135
135
|
server and channel id, so renaming the local account cannot turn a managed
|
|
136
136
|
channel into an ordinary one.
|
|
137
137
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as __reExport, t as __exportAll } from "./rolldown-runtime-B4FMCO8f.mjs";
|
|
2
|
-
import { c as CLICKCLACK_CHANNEL_ID, d as DEFAULT_ACCOUNT_ID, f as isClickClackAccountCurrent, g as resolveClickClackAccountConfig, h as resolveClickClackAccount, l as clickClackConfigAdapter, m as listEnabledClickClackAccounts, n as clickClackSetupContract, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-
|
|
2
|
+
import { c as CLICKCLACK_CHANNEL_ID, d as DEFAULT_ACCOUNT_ID, f as isClickClackAccountCurrent, g as resolveClickClackAccountConfig, h as resolveClickClackAccount, l as clickClackConfigAdapter, m as listEnabledClickClackAccounts, n as clickClackSetupContract, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-BJ3jioqA.mjs";
|
|
3
3
|
import { a as createClickClackClient, i as ClickClackHttpError, n as resolveChannelId, r as resolveWorkspaceId, s as normalizeClickClackCorrelationId } from "./resolve-Br3ylpSz.mjs";
|
|
4
4
|
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-CF7TKtkU.mjs";
|
|
5
5
|
import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/account-helpers";
|
|
@@ -19,6 +19,7 @@ import { formatNormalizedAllowFromEntries, resolveAllowlistMatchByCandidates } f
|
|
|
19
19
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
20
20
|
import { createHash, randomUUID } from "node:crypto";
|
|
21
21
|
import { createAsyncLock } from "openclaw/plugin-sdk/async-lock-runtime";
|
|
22
|
+
import { assertPluginStateReadAvailable, preparePluginStateRead } from "openclaw/plugin-sdk/plugin-state-runtime";
|
|
22
23
|
import { truncateCodePoints } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
23
24
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
24
25
|
import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
|
|
@@ -54,10 +55,8 @@ const storesByRuntime$2 = /* @__PURE__ */ new WeakMap();
|
|
|
54
55
|
function withGenerationStore(runtime, run) {
|
|
55
56
|
let owner = storesByRuntime$2.get(runtime);
|
|
56
57
|
if (!owner) {
|
|
57
|
-
const store = runtime.state.openKeyedStore(GENERATION_STORE_OPTIONS);
|
|
58
58
|
owner = {
|
|
59
|
-
store,
|
|
60
|
-
...!store.observe || !store.compareAndApply ? { native: runtime.state.openSyncKeyedStore(GENERATION_STORE_OPTIONS) } : {},
|
|
59
|
+
store: runtime.state.openKeyedStoreV2(GENERATION_STORE_OPTIONS),
|
|
61
60
|
withLock: createAsyncLock()
|
|
62
61
|
};
|
|
63
62
|
storesByRuntime$2.set(runtime, owner);
|
|
@@ -66,18 +65,8 @@ function withGenerationStore(runtime, run) {
|
|
|
66
65
|
return currentOwner.withLock(() => run(currentOwner));
|
|
67
66
|
}
|
|
68
67
|
function mutateGeneration(runtime, sessionKey, decide) {
|
|
69
|
-
return withGenerationStore(runtime, async ({ store
|
|
70
|
-
if (native) {
|
|
71
|
-
const current = native.lookup(sessionKey);
|
|
72
|
-
const next = decide(current);
|
|
73
|
-
if (next.value !== current) {
|
|
74
|
-
if (next.value) native.register(sessionKey, next.value);
|
|
75
|
-
else native.delete(sessionKey);
|
|
76
|
-
}
|
|
77
|
-
return next.result;
|
|
78
|
-
}
|
|
68
|
+
return withGenerationStore(runtime, async ({ store }) => {
|
|
79
69
|
const { observe, compareAndApply } = store;
|
|
80
|
-
if (!observe || !compareAndApply) throw new Error("ClickClack generation comparison capabilities changed");
|
|
81
70
|
let observed = await observe(sessionKey);
|
|
82
71
|
for (;;) {
|
|
83
72
|
const next = decide(observed.value);
|
|
@@ -144,6 +133,16 @@ function recordPendingDiscussionOpen(params) {
|
|
|
144
133
|
};
|
|
145
134
|
});
|
|
146
135
|
}
|
|
136
|
+
function getPendingDiscussionOpen(runtime, sessionKey) {
|
|
137
|
+
return withGenerationStore(runtime, async ({ store }) => {
|
|
138
|
+
const entry = await store.lookup(sessionKey);
|
|
139
|
+
return entry?.pending ? {
|
|
140
|
+
sessionKey,
|
|
141
|
+
generation: entry.generation,
|
|
142
|
+
...entry.pending
|
|
143
|
+
} : void 0;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
147
146
|
function listPendingDiscussionOpens(runtime) {
|
|
148
147
|
return withGenerationStore(runtime, async ({ store }) => (await store.entries()).flatMap((entry) => entry.value.pending ? [{
|
|
149
148
|
sessionKey: entry.key,
|
|
@@ -187,18 +186,15 @@ async function readDiscussionSessionEntry(runtime, sessionKey) {
|
|
|
187
186
|
sessionKey,
|
|
188
187
|
readConsistency: "latest"
|
|
189
188
|
};
|
|
190
|
-
return
|
|
189
|
+
return await runtime.agent.session.getSessionEntryAsync(params);
|
|
191
190
|
}
|
|
192
191
|
/**
|
|
193
192
|
* Refresh the replaceable session attachment without changing the durable room identity.
|
|
194
193
|
* The store registers persisted state before reindexing, so a failed write leaves the
|
|
195
194
|
* previous attachment authoritative in both persistence and memory.
|
|
196
195
|
*/
|
|
197
|
-
function attachBindingToCurrentActiveSession(params) {
|
|
198
|
-
const entry = params.runtime.
|
|
199
|
-
sessionKey: params.sessionKey,
|
|
200
|
-
readConsistency: "latest"
|
|
201
|
-
});
|
|
196
|
+
async function attachBindingToCurrentActiveSession(params) {
|
|
197
|
+
const entry = await readDiscussionSessionEntry(params.runtime, params.sessionKey);
|
|
202
198
|
if (!entry?.sessionId || entry.archivedAt !== void 0) return;
|
|
203
199
|
if (entry.sessionId === params.binding.sessionId && params.binding.detachedAt === void 0) return params.binding;
|
|
204
200
|
const { detachedAt: _detachedAt, ...retained } = params.binding;
|
|
@@ -206,8 +202,10 @@ function attachBindingToCurrentActiveSession(params) {
|
|
|
206
202
|
...retained,
|
|
207
203
|
sessionId: entry.sessionId
|
|
208
204
|
};
|
|
209
|
-
params.store.
|
|
210
|
-
|
|
205
|
+
return await params.store.setIfCurrent(params.sessionKey, params.binding, attached, { assertCurrent: () => {
|
|
206
|
+
const current = params.store.get(params.sessionKey);
|
|
207
|
+
if (!current || current.externalRef !== params.binding.externalRef || current.serverBaseUrl !== params.binding.serverBaseUrl || current.channelId !== params.binding.channelId || !bindingMatchesActiveSessionIncarnation(params.runtime, params.sessionKey, attached)) throw new Error("ClickClack discussion session changed before attachment committed");
|
|
208
|
+
} }) ? attached : void 0;
|
|
211
209
|
}
|
|
212
210
|
const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
|
|
213
211
|
const MAX_DISCUSSION_BINDINGS = 1e4;
|
|
@@ -223,18 +221,18 @@ function channelKey(serverBaseUrl, channelId) {
|
|
|
223
221
|
}
|
|
224
222
|
/** SQLite-backed session/channel bindings with a process-local inbound lookup index. */
|
|
225
223
|
var ClickClackDiscussionBindingStore = class {
|
|
224
|
+
#nativeStore;
|
|
226
225
|
#store;
|
|
227
|
-
#asyncStore;
|
|
228
226
|
#sessionByChannel = /* @__PURE__ */ new Map();
|
|
229
227
|
#detachedAtBySession = /* @__PURE__ */ new Map();
|
|
230
228
|
#channelBySession = /* @__PURE__ */ new Map();
|
|
231
229
|
#runtime;
|
|
230
|
+
#withMutation = createAsyncLock();
|
|
232
231
|
#prepared = false;
|
|
233
232
|
#preparing;
|
|
234
233
|
#changedDuringPreparation = /* @__PURE__ */ new Set();
|
|
235
234
|
constructor(runtime) {
|
|
236
235
|
this.#runtime = runtime;
|
|
237
|
-
this.#store = runtime.state.openSyncKeyedStore(BINDING_STORE_OPTIONS);
|
|
238
236
|
}
|
|
239
237
|
async prepare() {
|
|
240
238
|
if (!this.#prepared) await this.entries();
|
|
@@ -242,19 +240,29 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
242
240
|
async getAsync(sessionKey) {
|
|
243
241
|
return await this.#workerStore().lookup(sessionKey);
|
|
244
242
|
}
|
|
243
|
+
async prepareRead(sessionKey) {
|
|
244
|
+
return await preparePluginStateRead(this.#workerStore(), sessionKey) ?? {
|
|
245
|
+
read: () => this.get(sessionKey),
|
|
246
|
+
release() {}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
245
249
|
get(sessionKey) {
|
|
246
|
-
|
|
250
|
+
this.#nativeStore ??= this.#runtime.state.openSyncKeyedStore(BINDING_STORE_OPTIONS);
|
|
251
|
+
return this.#nativeStore.lookup(sessionKey);
|
|
252
|
+
}
|
|
253
|
+
assertReadAvailable() {
|
|
254
|
+
assertPluginStateReadAvailable(this.#workerStore());
|
|
247
255
|
}
|
|
248
256
|
async hasCapacity(sessionKey) {
|
|
249
257
|
return await this.getAsync(sessionKey) !== void 0 || await this.countAsync() < MAX_DISCUSSION_BINDINGS;
|
|
250
258
|
}
|
|
251
|
-
getByChannel(serverBaseUrl, channelId) {
|
|
259
|
+
async getByChannel(serverBaseUrl, channelId) {
|
|
252
260
|
const key = channelKey(serverBaseUrl, channelId);
|
|
253
261
|
const sessionKey = this.#sessionByChannel.get(key);
|
|
254
262
|
if (!sessionKey) return;
|
|
255
|
-
const binding = this.
|
|
263
|
+
const binding = await this.getAsync(sessionKey);
|
|
256
264
|
if (!binding || channelKey(binding.serverBaseUrl, binding.channelId) !== key) {
|
|
257
|
-
this.#sessionByChannel.delete(key);
|
|
265
|
+
if (this.#sessionByChannel.get(key) === sessionKey) this.#sessionByChannel.delete(key);
|
|
258
266
|
return;
|
|
259
267
|
}
|
|
260
268
|
return {
|
|
@@ -262,17 +270,66 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
262
270
|
binding
|
|
263
271
|
};
|
|
264
272
|
}
|
|
265
|
-
|
|
266
|
-
this.#
|
|
273
|
+
async setIfCurrent(sessionKey, expected, binding, options) {
|
|
274
|
+
return await this.#mutateIfCurrent(sessionKey, expected, binding, options);
|
|
275
|
+
}
|
|
276
|
+
async observeCurrent(sessionKey) {
|
|
277
|
+
return await this.#withMutation(async () => {
|
|
278
|
+
const observed = await this.#workerStore().observe(sessionKey);
|
|
279
|
+
this.#publish(sessionKey, observed.value);
|
|
280
|
+
return {
|
|
281
|
+
binding: observed.value,
|
|
282
|
+
condition: {
|
|
283
|
+
namespace: DISCUSSION_BINDINGS_NAMESPACE,
|
|
284
|
+
key: sessionKey,
|
|
285
|
+
comparison: observed.comparison
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async deleteIfCurrent(sessionKey, expected, options) {
|
|
291
|
+
return await this.#mutateIfCurrent(sessionKey, expected, void 0, options);
|
|
292
|
+
}
|
|
293
|
+
async #mutateIfCurrent(sessionKey, expected, next, options) {
|
|
294
|
+
return await this.#withMutation(async () => {
|
|
295
|
+
const worker = this.#workerStore();
|
|
296
|
+
const observed = await worker.observe(sessionKey);
|
|
297
|
+
if (!isDeepStrictEqual(observed.value, expected)) {
|
|
298
|
+
this.#publish(sessionKey, observed.value);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
let authorityRejected = false;
|
|
302
|
+
try {
|
|
303
|
+
const result = await (options?.assertCurrent ? this.#runtime.state.openKeyedStoreV2(BINDING_STORE_OPTIONS, { assertCurrent: () => {
|
|
304
|
+
try {
|
|
305
|
+
options.assertCurrent?.();
|
|
306
|
+
} catch (error) {
|
|
307
|
+
authorityRejected = true;
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
} }) : worker).compareAndApply(sessionKey, observed.comparison, next ? {
|
|
311
|
+
operation: "update",
|
|
312
|
+
action: "set",
|
|
313
|
+
value: next
|
|
314
|
+
} : {
|
|
315
|
+
operation: "delete",
|
|
316
|
+
action: "delete"
|
|
317
|
+
});
|
|
318
|
+
this.#publish(sessionKey, result.status === "conflict" ? result.current.value : next);
|
|
319
|
+
return result.status === "applied";
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (!authorityRejected) throw error;
|
|
322
|
+
const current = await worker.observe(sessionKey);
|
|
323
|
+
if (current.comparison === observed.comparison) throw error;
|
|
324
|
+
this.#publish(sessionKey, current.value);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
#publish(sessionKey, binding) {
|
|
267
330
|
if (!this.#prepared) this.#changedDuringPreparation.add(sessionKey);
|
|
268
331
|
this.#unindex(sessionKey);
|
|
269
|
-
this.#index(sessionKey, binding);
|
|
270
|
-
}
|
|
271
|
-
delete(sessionKey) {
|
|
272
|
-
const deleted = this.#store.delete(sessionKey);
|
|
273
|
-
if (deleted && !this.#prepared) this.#changedDuringPreparation.add(sessionKey);
|
|
274
|
-
if (deleted) this.#unindex(sessionKey);
|
|
275
|
-
return deleted;
|
|
332
|
+
if (binding) this.#index(sessionKey, binding);
|
|
276
333
|
}
|
|
277
334
|
async entries() {
|
|
278
335
|
const load = async () => (await this.#workerStore().entries()).map((entry) => ({
|
|
@@ -291,16 +348,15 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
291
348
|
return await this.#preparing;
|
|
292
349
|
}
|
|
293
350
|
async countAsync() {
|
|
294
|
-
|
|
295
|
-
return store.count ? await store.count() : (await this.entries()).length;
|
|
351
|
+
return await this.#workerStore().count();
|
|
296
352
|
}
|
|
297
353
|
detachedCount() {
|
|
298
354
|
return this.#detachedAtBySession.size;
|
|
299
355
|
}
|
|
300
356
|
#workerStore() {
|
|
301
|
-
return this.#
|
|
357
|
+
return this.#store ??= this.#runtime.state.openKeyedStoreV2(BINDING_STORE_OPTIONS);
|
|
302
358
|
}
|
|
303
|
-
oldestDetached() {
|
|
359
|
+
async oldestDetached() {
|
|
304
360
|
let oldestSessionKey;
|
|
305
361
|
let oldestDetachedAt = Number.POSITIVE_INFINITY;
|
|
306
362
|
for (const [sessionKey, detachedAt] of this.#detachedAtBySession) if (detachedAt < oldestDetachedAt || detachedAt === oldestDetachedAt && (oldestSessionKey === void 0 || sessionKey < oldestSessionKey)) {
|
|
@@ -308,9 +364,9 @@ var ClickClackDiscussionBindingStore = class {
|
|
|
308
364
|
oldestDetachedAt = detachedAt;
|
|
309
365
|
}
|
|
310
366
|
if (!oldestSessionKey) return;
|
|
311
|
-
const binding = this.
|
|
367
|
+
const binding = await this.getAsync(oldestSessionKey);
|
|
312
368
|
if (!binding || binding.detachedAt === void 0) {
|
|
313
|
-
this.#detachedAtBySession.delete(oldestSessionKey);
|
|
369
|
+
if (this.#detachedAtBySession.get(oldestSessionKey) === oldestDetachedAt) this.#detachedAtBySession.delete(oldestSessionKey);
|
|
314
370
|
return this.oldestDetached();
|
|
315
371
|
}
|
|
316
372
|
return {
|
|
@@ -427,106 +483,172 @@ function discussionInfoForBinding(binding, account) {
|
|
|
427
483
|
}
|
|
428
484
|
//#endregion
|
|
429
485
|
//#region extensions/clickclack/src/discussions/revoked-channel-store.ts
|
|
430
|
-
const
|
|
431
|
-
|
|
486
|
+
const STORE_OPTIONS = {
|
|
487
|
+
namespace: "discussion-revoked-channels",
|
|
488
|
+
maxEntries: 1e5,
|
|
489
|
+
overflowPolicy: "reject-new"
|
|
490
|
+
};
|
|
432
491
|
const storesByRuntime = /* @__PURE__ */ new WeakMap();
|
|
492
|
+
const nativeStoresByRuntime = /* @__PURE__ */ new WeakMap();
|
|
433
493
|
function revokedChannelKey(params) {
|
|
434
494
|
return [params.serverBaseUrl.replace(/\/+$/u, ""), params.channelId].join("\0");
|
|
435
495
|
}
|
|
436
496
|
function getStore(runtime) {
|
|
437
497
|
const existing = storesByRuntime.get(runtime);
|
|
438
498
|
if (existing) return existing;
|
|
439
|
-
const created = runtime.state.
|
|
440
|
-
namespace: REVOKED_CHANNELS_NAMESPACE,
|
|
441
|
-
maxEntries: MAX_REVOKED_CHANNELS,
|
|
442
|
-
overflowPolicy: "reject-new"
|
|
443
|
-
});
|
|
499
|
+
const created = runtime.state.openKeyedStoreV2(STORE_OPTIONS);
|
|
444
500
|
storesByRuntime.set(runtime, created);
|
|
445
501
|
return created;
|
|
446
502
|
}
|
|
447
503
|
/** Records managed ownership before its live binding is released. */
|
|
448
|
-
function markClickClackDiscussionChannelRevoked(runtime, binding) {
|
|
504
|
+
async function markClickClackDiscussionChannelRevoked(runtime, sessionKey, binding, options) {
|
|
449
505
|
const value = {
|
|
450
506
|
accountId: binding.accountId,
|
|
451
507
|
serverBaseUrl: binding.serverBaseUrl,
|
|
452
508
|
channelId: binding.channelId,
|
|
453
|
-
revokedAt: Date.now()
|
|
509
|
+
revokedAt: Date.now(),
|
|
510
|
+
binding
|
|
454
511
|
};
|
|
455
|
-
|
|
512
|
+
const bindings = getClickClackDiscussionBindingStore(runtime);
|
|
513
|
+
for (;;) {
|
|
514
|
+
const owner = await bindings.observeCurrent(sessionKey);
|
|
515
|
+
const isCurrent = isDeepStrictEqual(owner.binding, binding);
|
|
516
|
+
const observed = await getStore(runtime).observe(revokedChannelKey(value));
|
|
517
|
+
if (observed.value && !observed.value.binding) return isCurrent;
|
|
518
|
+
if (!isCurrent && observed.value) return false;
|
|
519
|
+
let authorityRejected = false;
|
|
520
|
+
try {
|
|
521
|
+
if ((await (isCurrent && options?.assertCurrent ? runtime.state.openKeyedStoreV2(STORE_OPTIONS, { assertCurrent: () => {
|
|
522
|
+
try {
|
|
523
|
+
options.assertCurrent?.();
|
|
524
|
+
} catch (error) {
|
|
525
|
+
authorityRejected = true;
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
} }) : getStore(runtime)).compareAndApply(revokedChannelKey(value), observed.comparison, {
|
|
529
|
+
operation: "update",
|
|
530
|
+
action: "set",
|
|
531
|
+
value
|
|
532
|
+
}, { conditions: [owner.condition] })).status !== "conflict") return isCurrent;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (authorityRejected) {
|
|
535
|
+
const current = await bindings.observeCurrent(sessionKey);
|
|
536
|
+
if (!isDeepStrictEqual(current.binding, binding)) continue;
|
|
537
|
+
}
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
456
541
|
}
|
|
457
|
-
function markClickClackDiscussionChannelIdentityRevoked(params) {
|
|
542
|
+
async function markClickClackDiscussionChannelIdentityRevoked(params) {
|
|
458
543
|
const value = {
|
|
459
544
|
accountId: params.accountId,
|
|
460
545
|
serverBaseUrl: params.serverBaseUrl.replace(/\/+$/u, ""),
|
|
461
546
|
channelId: params.channelId,
|
|
462
547
|
revokedAt: Date.now()
|
|
463
548
|
};
|
|
464
|
-
getStore(params.runtime).register(revokedChannelKey(value), value);
|
|
549
|
+
await getStore(params.runtime).register(revokedChannelKey(value), value);
|
|
465
550
|
}
|
|
466
|
-
function clearClickClackDiscussionChannelRevoked(params) {
|
|
467
|
-
getStore(params.runtime).delete(revokedChannelKey(params));
|
|
551
|
+
async function clearClickClackDiscussionChannelRevoked(params) {
|
|
552
|
+
await getStore(params.runtime).delete(revokedChannelKey(params));
|
|
468
553
|
}
|
|
469
554
|
/** Distinguishes a released managed channel from a genuinely ordinary channel. */
|
|
470
555
|
function isClickClackDiscussionChannelRevoked(params) {
|
|
471
|
-
|
|
556
|
+
let store = nativeStoresByRuntime.get(params.runtime);
|
|
557
|
+
if (!store) {
|
|
558
|
+
store = params.runtime.state.openSyncKeyedStore(STORE_OPTIONS);
|
|
559
|
+
nativeStoresByRuntime.set(params.runtime, store);
|
|
560
|
+
}
|
|
561
|
+
return revocationApplies(store.lookup(revokedChannelKey(params)), params.binding);
|
|
562
|
+
}
|
|
563
|
+
async function prepareClickClackDiscussionChannelRevocation(params) {
|
|
564
|
+
const facts = await preparePluginStateRead(getStore(params.runtime), revokedChannelKey(params));
|
|
565
|
+
return {
|
|
566
|
+
isRevoked: (binding) => {
|
|
567
|
+
if (binding && revokedChannelKey(binding) !== revokedChannelKey(params)) return true;
|
|
568
|
+
return facts ? revocationApplies(facts.read(), binding) : isClickClackDiscussionChannelRevoked({
|
|
569
|
+
...params,
|
|
570
|
+
binding
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
release: () => facts?.release()
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
async function isClickClackDiscussionChannelRevokedAsync(params) {
|
|
577
|
+
return revocationApplies(await getStore(params.runtime).lookup(revokedChannelKey(params)), params.binding);
|
|
578
|
+
}
|
|
579
|
+
function revocationApplies(revoked, binding) {
|
|
580
|
+
return Boolean(revoked && (!binding || !revoked.binding || isDeepStrictEqual(revoked.binding, binding)));
|
|
472
581
|
}
|
|
473
582
|
//#endregion
|
|
474
583
|
//#region extensions/clickclack/src/discussions/routing.ts
|
|
475
584
|
async function resolveClickClackDiscussionRoute(params) {
|
|
476
585
|
const store = getClickClackDiscussionBindingStore(params.runtime);
|
|
477
586
|
await store.prepare();
|
|
478
|
-
let matched = store.getByChannel(params.serverBaseUrl, params.channelId);
|
|
587
|
+
let matched = await store.getByChannel(params.serverBaseUrl, params.channelId);
|
|
479
588
|
let pending = false;
|
|
480
589
|
if (!matched) {
|
|
481
590
|
pending = await hasPendingDiscussionOpenForDestination(params);
|
|
482
|
-
matched = store.getByChannel(params.serverBaseUrl, params.channelId);
|
|
591
|
+
matched = await store.getByChannel(params.serverBaseUrl, params.channelId);
|
|
483
592
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
|
|
487
|
-
if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
|
|
488
|
-
if (resolveDiscussionBindingAccount(params.runtime.config.current(), matched.binding).state !== "active") return { state: "revoked" };
|
|
489
|
-
let binding;
|
|
593
|
+
let revocation;
|
|
594
|
+
let bindingFacts;
|
|
490
595
|
try {
|
|
491
|
-
|
|
596
|
+
revocation = await prepareClickClackDiscussionChannelRevocation(params);
|
|
597
|
+
if (revocation.isRevoked(matched?.binding)) return { state: "revoked" };
|
|
598
|
+
if (!matched) return { state: pending ? "revoked" : "unbound" };
|
|
599
|
+
if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
|
|
600
|
+
if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
|
|
601
|
+
if (resolveDiscussionBindingAccount(params.runtime.config.current(), matched.binding).state !== "active") return { state: "revoked" };
|
|
602
|
+
bindingFacts = await store.prepareRead(matched.sessionKey);
|
|
603
|
+
let binding;
|
|
604
|
+
try {
|
|
605
|
+
binding = await attachBindingToCurrentActiveSession({
|
|
606
|
+
runtime: params.runtime,
|
|
607
|
+
store,
|
|
608
|
+
sessionKey: matched.sessionKey,
|
|
609
|
+
binding: matched.binding
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
params.runtime.logging.getChildLogger({
|
|
613
|
+
plugin: "clickclack",
|
|
614
|
+
feature: "discussions"
|
|
615
|
+
}).warn(`discussion attachment refresh failed for channel ${params.channelId}: ${String(error)}`);
|
|
616
|
+
return { state: "revoked" };
|
|
617
|
+
}
|
|
618
|
+
if (!binding) return { state: "revoked" };
|
|
619
|
+
const current = bindingFacts.read();
|
|
620
|
+
if (!current || current.externalRef !== binding.externalRef || current.channelId !== binding.channelId || revocation.isRevoked(current) || !bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, binding) || resolveDiscussionBindingAccount(params.runtime.config.current(), binding).state !== "active") return { state: "revoked" };
|
|
621
|
+
const sessionKey = discussionSessionKey({
|
|
492
622
|
runtime: params.runtime,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
623
|
+
agentId: binding.agentId,
|
|
624
|
+
mainSessionKey: matched.sessionKey,
|
|
625
|
+
sessionId: binding.sessionId,
|
|
626
|
+
accountId: params.accountId,
|
|
627
|
+
serverBaseUrl: binding.serverBaseUrl,
|
|
628
|
+
channelId: binding.channelId,
|
|
629
|
+
externalRef: binding.externalRef
|
|
496
630
|
});
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
631
|
+
if (!sessionKey) return { state: "revoked" };
|
|
632
|
+
return {
|
|
633
|
+
state: "active",
|
|
634
|
+
route: {
|
|
635
|
+
agentId: binding.agentId,
|
|
636
|
+
sessionKey,
|
|
637
|
+
systemPrompt: [
|
|
638
|
+
"You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
|
|
639
|
+
`The main session key is ${matched.sessionKey}.`,
|
|
640
|
+
"Observe it with sessions_history and session_status (using changesSince for incremental checks).",
|
|
641
|
+
"Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
|
|
642
|
+
"These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
|
|
643
|
+
].join(" ")
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
} catch {
|
|
502
647
|
return { state: "revoked" };
|
|
648
|
+
} finally {
|
|
649
|
+
bindingFacts?.release();
|
|
650
|
+
revocation?.release();
|
|
503
651
|
}
|
|
504
|
-
if (!binding) return { state: "revoked" };
|
|
505
|
-
const sessionKey = discussionSessionKey({
|
|
506
|
-
runtime: params.runtime,
|
|
507
|
-
agentId: binding.agentId,
|
|
508
|
-
mainSessionKey: matched.sessionKey,
|
|
509
|
-
sessionId: binding.sessionId,
|
|
510
|
-
accountId: params.accountId,
|
|
511
|
-
serverBaseUrl: binding.serverBaseUrl,
|
|
512
|
-
channelId: binding.channelId,
|
|
513
|
-
externalRef: binding.externalRef
|
|
514
|
-
});
|
|
515
|
-
if (!sessionKey) return { state: "revoked" };
|
|
516
|
-
return {
|
|
517
|
-
state: "active",
|
|
518
|
-
route: {
|
|
519
|
-
agentId: binding.agentId,
|
|
520
|
-
sessionKey,
|
|
521
|
-
systemPrompt: [
|
|
522
|
-
"You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
|
|
523
|
-
`The main session key is ${matched.sessionKey}.`,
|
|
524
|
-
"Observe it with sessions_history and session_status (using changesSince for incremental checks).",
|
|
525
|
-
"Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
|
|
526
|
-
"These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
|
|
527
|
-
].join(" ")
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
652
|
}
|
|
531
653
|
//#endregion
|
|
532
654
|
//#region extensions/clickclack/src/group-policy.ts
|
|
@@ -1271,9 +1393,9 @@ function createOutboundContext(params) {
|
|
|
1271
1393
|
}
|
|
1272
1394
|
async function sendClickClackText(params) {
|
|
1273
1395
|
const text = sanitizeAssistantVisibleText(params.text);
|
|
1274
|
-
if (!text) return;
|
|
1396
|
+
if (!text) return { outcome: "not_sent" };
|
|
1275
1397
|
const { account, client } = createOutboundContext(params);
|
|
1276
|
-
return (await createTargetMessage({
|
|
1398
|
+
return { messageId: (await createTargetMessage({
|
|
1277
1399
|
client,
|
|
1278
1400
|
workspaceId: await resolveWorkspaceId(client, account.workspace),
|
|
1279
1401
|
to: params.to,
|
|
@@ -1284,7 +1406,7 @@ async function sendClickClackText(params) {
|
|
|
1284
1406
|
nonce: textDeliveryNonce(params),
|
|
1285
1407
|
onPlatformSendDispatch: params.onPlatformSendDispatch,
|
|
1286
1408
|
assertDirectAdapterHandoff: params.assertDirectAdapterHandoff
|
|
1287
|
-
})).id;
|
|
1409
|
+
})).id };
|
|
1288
1410
|
}
|
|
1289
1411
|
async function sendClickClackMedia(params) {
|
|
1290
1412
|
const nonces = mediaDeliveryNonces(params);
|
|
@@ -1559,13 +1681,6 @@ function createClickClackAgentProgressPublisher(params) {
|
|
|
1559
1681
|
const controlFrames = queue.filter((frame) => !frame.lineId);
|
|
1560
1682
|
queue.splice(0, queue.length, ...controlFrames);
|
|
1561
1683
|
};
|
|
1562
|
-
const waitForDrainWithinFinalizeGrace = async (pending) => {
|
|
1563
|
-
let drained = false;
|
|
1564
|
-
await raceWithTimeout(pending.then(() => {
|
|
1565
|
-
drained = true;
|
|
1566
|
-
}), CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS, () => void 0);
|
|
1567
|
-
return drained;
|
|
1568
|
-
};
|
|
1569
1684
|
const scheduleLineDrain = () => {
|
|
1570
1685
|
if (lineDrainTimer) return;
|
|
1571
1686
|
lineDrainTimer = setTimeout(() => {
|
|
@@ -1650,7 +1765,7 @@ function createClickClackAgentProgressPublisher(params) {
|
|
|
1650
1765
|
}
|
|
1651
1766
|
flushQueuedLines();
|
|
1652
1767
|
enqueue({ op: "clear" });
|
|
1653
|
-
if (!await
|
|
1768
|
+
if (!await raceWithTimeout(drain().then(() => true), CLICKCLACK_PROGRESS_FINALIZE_GRACE_MS, () => false)) discardQueuedLines();
|
|
1654
1769
|
}
|
|
1655
1770
|
};
|
|
1656
1771
|
}
|
|
@@ -2169,18 +2284,18 @@ const clickClackMessageAdapter = defineChannelMessageAdapter({
|
|
|
2169
2284
|
},
|
|
2170
2285
|
send: {
|
|
2171
2286
|
text: async (ctx) => {
|
|
2172
|
-
const
|
|
2287
|
+
const result = await sendClickClackText({
|
|
2173
2288
|
...ctx,
|
|
2174
2289
|
cfg: ctx.cfg
|
|
2175
2290
|
});
|
|
2176
2291
|
const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
|
|
2177
2292
|
const replyToId = ctx.replyToId ?? void 0;
|
|
2178
2293
|
return {
|
|
2179
|
-
...
|
|
2294
|
+
...result,
|
|
2180
2295
|
receipt: createMessageReceiptFromOutboundResults({
|
|
2181
|
-
results: messageId ? [{
|
|
2296
|
+
results: "messageId" in result ? [{
|
|
2182
2297
|
channel: CHANNEL_ID,
|
|
2183
|
-
messageId
|
|
2298
|
+
messageId: result.messageId
|
|
2184
2299
|
}] : [],
|
|
2185
2300
|
threadId,
|
|
2186
2301
|
replyToId,
|
|
@@ -2299,12 +2414,13 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
2299
2414
|
base: { deliveryMode: "direct" },
|
|
2300
2415
|
attachedResults: {
|
|
2301
2416
|
channel: CHANNEL_ID,
|
|
2302
|
-
sendText: async (ctx) => {
|
|
2303
|
-
|
|
2417
|
+
sendText: async (ctx) => ({
|
|
2418
|
+
messageId: "",
|
|
2419
|
+
...await sendClickClackText({
|
|
2304
2420
|
...ctx,
|
|
2305
2421
|
cfg: ctx.cfg
|
|
2306
|
-
})
|
|
2307
|
-
},
|
|
2422
|
+
})
|
|
2423
|
+
}),
|
|
2308
2424
|
sendMedia: async (ctx) => {
|
|
2309
2425
|
const { mediaUrl, onDeliveryResult } = ctx;
|
|
2310
2426
|
if (!mediaUrl) throw new Error("ClickClack media send requires mediaUrl");
|
|
@@ -2323,4 +2439,4 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
2323
2439
|
}
|
|
2324
2440
|
});
|
|
2325
2441
|
//#endregion
|
|
2326
|
-
export {
|
|
2442
|
+
export { clearPendingDiscussionOpen as A, truncateDiscussionDisplayTitle as C, getClickClackDiscussionBindingStore as D, bindingMatchesActiveSessionIncarnation as E, listPendingDiscussionOpens as M, recordPendingDiscussionOpen as N, readDiscussionSessionEntry as O, reserveDiscussionBindingGeneration as P, slugifyDiscussionLabel as S, attachBindingToCurrentActiveSession as T, discussionExternalRef as _, setClickClackRuntime as a, isDiscussionSessionKey as b, isClickClackDiscussionChannelRevokedAsync as c, prepareClickClackDiscussionChannelRevocation as d, discussionAccounts as f, discussionCredentialFingerprint as g, resolveDiscussionBindingAccount as h, getClickClackRuntime as i, getPendingDiscussionOpen as j, clearDiscussionBindingGeneration as k, markClickClackDiscussionChannelIdentityRevoked as l, normalizedServerBaseUrl as m, buildClickClackTarget as n, clearClickClackDiscussionChannelRevoked as o, discussionInfoForBinding as p, parseClickClackTarget as r, isClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, markClickClackDiscussionChannelRevoked as u, discussionSessionKey as v, MAX_RETAINED_DETACHED_DISCUSSION_BINDINGS as w, resolveDiscussionLabel as x, fallbackDiscussionLabel as y };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { h as resolveClickClackAccount, p as listClickClackAccountIds } from "./setup-surface-
|
|
2
|
-
import { A as
|
|
1
|
+
import { h as resolveClickClackAccount, p as listClickClackAccountIds } from "./setup-surface-BJ3jioqA.mjs";
|
|
2
|
+
import { A as clearPendingDiscussionOpen, C as truncateDiscussionDisplayTitle, D as getClickClackDiscussionBindingStore, E as bindingMatchesActiveSessionIncarnation, M as listPendingDiscussionOpens, N as recordPendingDiscussionOpen, O as readDiscussionSessionEntry, P as reserveDiscussionBindingGeneration, S as slugifyDiscussionLabel, T as attachBindingToCurrentActiveSession, _ as discussionExternalRef, b as isDiscussionSessionKey, c as isClickClackDiscussionChannelRevokedAsync, d as prepareClickClackDiscussionChannelRevocation, f as discussionAccounts, g as discussionCredentialFingerprint, h as resolveDiscussionBindingAccount, j as getPendingDiscussionOpen, k as clearDiscussionBindingGeneration, l as markClickClackDiscussionChannelIdentityRevoked, m as normalizedServerBaseUrl, o as clearClickClackDiscussionChannelRevoked, p as discussionInfoForBinding, s as isClickClackDiscussionChannelRevoked, u as markClickClackDiscussionChannelRevoked, v as discussionSessionKey, x as resolveDiscussionLabel, y as fallbackDiscussionLabel } from "./channel-DRcZ_twH.mjs";
|
|
3
3
|
import { a as createClickClackClient, i as ClickClackHttpError, o as isClickClackChannelNameConflict, t as findClickClackWorkspace } from "./resolve-Br3ylpSz.mjs";
|
|
4
4
|
import "../api.js";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { createAsyncLock } from "openclaw/plugin-sdk/async-lock-runtime";
|
|
7
8
|
import { buildControlUiSessionPath, registerSessionDiscussionProvider } from "openclaw/plugin-sdk/session-discussion";
|
|
@@ -19,47 +20,54 @@ var DetachedDiscussionBindingRetention = class {
|
|
|
19
20
|
this.#store = options.store;
|
|
20
21
|
this.#maxRetained = options.maxRetained;
|
|
21
22
|
}
|
|
22
|
-
mark(sessionKey, binding) {
|
|
23
|
-
const current = this.#store.
|
|
24
|
-
if (!current || !
|
|
25
|
-
if (current.detachedAt === void 0) this.#store.
|
|
23
|
+
async mark(sessionKey, binding) {
|
|
24
|
+
const current = await this.#store.getAsync(sessionKey);
|
|
25
|
+
if (!current || !isDeepStrictEqual(current, binding)) return;
|
|
26
|
+
if (current.detachedAt === void 0) await this.#store.setIfCurrent(sessionKey, current, {
|
|
26
27
|
...current,
|
|
27
28
|
detachedAt: Date.now()
|
|
28
|
-
});
|
|
29
|
-
while (this.#store.detachedCount() > this.#maxRetained) if (!this.#pruneOldest()) throw new Error("ClickClack detached discussion binding retention could not be reduced");
|
|
29
|
+
}, { assertCurrent: () => this.#assertDetached(sessionKey, current) });
|
|
30
|
+
while (this.#store.detachedCount() > this.#maxRetained) if (!await this.#pruneOldest()) throw new Error("ClickClack detached discussion binding retention could not be reduced");
|
|
30
31
|
}
|
|
31
|
-
clear(sessionKey, binding) {
|
|
32
|
-
const current = this.#store.
|
|
33
|
-
if (!current || !
|
|
32
|
+
async clear(sessionKey, binding) {
|
|
33
|
+
const current = await this.#store.getAsync(sessionKey);
|
|
34
|
+
if (!current || !isDeepStrictEqual(current, binding)) return;
|
|
34
35
|
if (current.detachedAt === void 0) return current;
|
|
35
36
|
const { detachedAt: _detachedAt, ...retained } = current;
|
|
36
|
-
this.#store.
|
|
37
|
-
return retained;
|
|
37
|
+
return await this.#store.setIfCurrent(sessionKey, current, retained, { assertCurrent: () => this.#assertRoomCurrent(sessionKey, current) }) ? retained : void 0;
|
|
38
38
|
}
|
|
39
39
|
async ensureCapacity(sessionKey) {
|
|
40
40
|
await this.#store.prepare();
|
|
41
|
-
while (!await this.#store.hasCapacity(sessionKey)) if (!this.#pruneOldest()) throw new Error("ClickClack discussion binding capacity is exhausted");
|
|
41
|
+
while (!await this.#store.hasCapacity(sessionKey)) if (!await this.#pruneOldest()) throw new Error("ClickClack discussion binding capacity is exhausted");
|
|
42
42
|
}
|
|
43
|
-
#pruneOldest() {
|
|
43
|
+
async #pruneOldest() {
|
|
44
44
|
for (;;) {
|
|
45
|
-
const oldest = this.#store.oldestDetached();
|
|
45
|
+
const oldest = await this.#store.oldestDetached();
|
|
46
46
|
if (!oldest) return false;
|
|
47
47
|
const current = oldest.binding;
|
|
48
|
-
if (this.#runtime.
|
|
49
|
-
|
|
50
|
-
readConsistency: "latest"
|
|
51
|
-
})) {
|
|
52
|
-
this.clear(oldest.sessionKey, current);
|
|
48
|
+
if (await readDiscussionSessionEntry(this.#runtime, oldest.sessionKey)) {
|
|
49
|
+
await this.clear(oldest.sessionKey, current);
|
|
53
50
|
continue;
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
this.#
|
|
57
|
-
return true;
|
|
52
|
+
const authority = { assertCurrent: () => this.#assertDetached(oldest.sessionKey, current) };
|
|
53
|
+
if (!await markClickClackDiscussionChannelRevoked(this.#runtime, oldest.sessionKey, current, authority)) continue;
|
|
54
|
+
if (await this.#store.deleteIfCurrent(oldest.sessionKey, current, authority)) return true;
|
|
58
55
|
}
|
|
59
56
|
}
|
|
60
57
|
#sameRoom(left, right) {
|
|
61
58
|
return left.serverBaseUrl === right.serverBaseUrl && left.channelId === right.channelId && left.externalRef === right.externalRef;
|
|
62
59
|
}
|
|
60
|
+
#assertRoomCurrent(sessionKey, expected) {
|
|
61
|
+
const current = this.#store.get(sessionKey);
|
|
62
|
+
if (!current || !this.#sameRoom(current, expected)) throw new Error("ClickClack discussion binding changed before retention committed");
|
|
63
|
+
}
|
|
64
|
+
#assertDetached(sessionKey, expected) {
|
|
65
|
+
this.#assertRoomCurrent(sessionKey, expected);
|
|
66
|
+
if (this.#runtime.agent.session.getSessionEntry({
|
|
67
|
+
sessionKey,
|
|
68
|
+
readConsistency: "latest"
|
|
69
|
+
})) throw new Error("ClickClack discussion session returned before retention committed");
|
|
70
|
+
}
|
|
63
71
|
};
|
|
64
72
|
//#endregion
|
|
65
73
|
//#region extensions/clickclack/src/discussions/control-session-url.ts
|
|
@@ -99,7 +107,7 @@ const INSTALLATION_NAMESPACE = "discussion-installation";
|
|
|
99
107
|
const INSTALLATION_KEY = "current";
|
|
100
108
|
/** Returns the durable installation namespace used in server-visible ownership refs. */
|
|
101
109
|
async function getClickClackDiscussionInstallationId(runtime) {
|
|
102
|
-
const store = runtime.state.
|
|
110
|
+
const store = runtime.state.openKeyedStoreV2({
|
|
103
111
|
namespace: INSTALLATION_NAMESPACE,
|
|
104
112
|
maxEntries: 1,
|
|
105
113
|
overflowPolicy: "reject-new"
|
|
@@ -258,10 +266,10 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
258
266
|
serverBaseUrl,
|
|
259
267
|
channelId
|
|
260
268
|
});
|
|
261
|
-
const unresolved =
|
|
269
|
+
const unresolved = await getPendingDiscussionOpen(runtime, sessionKey);
|
|
262
270
|
if (unresolved && (unresolved.accountId !== account.accountId || unresolved.credentialFingerprint !== credentialFingerprint || unresolved.serverBaseUrl !== serverBaseUrl || unresolved.workspaceId !== workspace.id)) {
|
|
263
271
|
await params.reconcilePendingOpen(unresolved);
|
|
264
|
-
if (
|
|
272
|
+
if (await getPendingDiscussionOpen(runtime, sessionKey)) throw new Error("A previous ClickClack discussion open is still unresolved; restore its credential and retry");
|
|
265
273
|
}
|
|
266
274
|
const config = runtime.config.current();
|
|
267
275
|
const agentId = resolveSessionAgentIdStrict({
|
|
@@ -346,14 +354,15 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
346
354
|
assertCurrentAuthority();
|
|
347
355
|
try {
|
|
348
356
|
if (adopted) {
|
|
349
|
-
revokeChannel(adopted.id);
|
|
357
|
+
await revokeChannel(adopted.id);
|
|
358
|
+
assertCurrentAuthority();
|
|
350
359
|
resolved = await client.updateChannel(adopted.id, managedFields);
|
|
351
360
|
} else {
|
|
352
361
|
resolved = await client.createChannel(workspace.id, {
|
|
353
362
|
...managedFields,
|
|
354
363
|
kind: "public"
|
|
355
364
|
});
|
|
356
|
-
revokeChannel(resolved.id);
|
|
365
|
+
await revokeChannel(resolved.id);
|
|
357
366
|
}
|
|
358
367
|
break;
|
|
359
368
|
} catch (error) {
|
|
@@ -369,7 +378,7 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
369
378
|
const recovered = relisted.find((candidate) => candidate.external_managed === true && candidate.external_ref === externalRef);
|
|
370
379
|
if (recovered) {
|
|
371
380
|
adopted = recovered;
|
|
372
|
-
revokeChannel(recovered.id);
|
|
381
|
+
await revokeChannel(recovered.id);
|
|
373
382
|
assertCurrentAuthority();
|
|
374
383
|
resolved = await client.updateChannel(recovered.id, managedFields);
|
|
375
384
|
break;
|
|
@@ -452,7 +461,13 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
452
461
|
};
|
|
453
462
|
try {
|
|
454
463
|
nextBinding.sessionId = assertCurrentAuthority().sessionId;
|
|
455
|
-
store.
|
|
464
|
+
if (!await store.setIfCurrent(sessionKey, void 0, nextBinding, { assertCurrent: () => {
|
|
465
|
+
if (assertCurrentAuthority().sessionId !== nextBinding.sessionId) throw new Error("ClickClack discussion session changed before binding committed");
|
|
466
|
+
} })) {
|
|
467
|
+
await clearPendingDiscussionOpen(generationScope);
|
|
468
|
+
params.warn(`superseded discussion channel remains quarantined: ${channel.id}`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
456
471
|
} catch (error) {
|
|
457
472
|
await clearPendingDiscussionOpen(generationScope);
|
|
458
473
|
params.warn(`unbound discussion channel remains quarantined: ${channel.id}`);
|
|
@@ -490,7 +505,7 @@ async function reconcilePendingDiscussionOpen(params) {
|
|
|
490
505
|
assertManagedChannelListContract(channels);
|
|
491
506
|
const channel = channels.find((candidate) => candidate.external_managed === true && candidate.external_ref === pending.externalRef);
|
|
492
507
|
if (channel) {
|
|
493
|
-
markClickClackDiscussionChannelIdentityRevoked({
|
|
508
|
+
await markClickClackDiscussionChannelIdentityRevoked({
|
|
494
509
|
runtime: params.runtime,
|
|
495
510
|
accountId: pending.accountId,
|
|
496
511
|
serverBaseUrl: pending.serverBaseUrl,
|
|
@@ -567,7 +582,7 @@ var ClickClackDiscussionService = class {
|
|
|
567
582
|
});
|
|
568
583
|
this.#unsubscribeSessionsChanged = gatewayEvents?.onSessionsChanged((event) => {
|
|
569
584
|
this.#withOperation(async () => {
|
|
570
|
-
if (await this.#store.getAsync(event.sessionKey) ||
|
|
585
|
+
if (await this.#store.getAsync(event.sessionKey) || await getPendingDiscussionOpen(this.#runtime, event.sessionKey)) {
|
|
571
586
|
if (activation === this.#activation) this.#reconcileScheduler?.schedule(event.sessionKey);
|
|
572
587
|
}
|
|
573
588
|
}).catch((error) => {
|
|
@@ -593,7 +608,7 @@ var ClickClackDiscussionService = class {
|
|
|
593
608
|
if (existing) {
|
|
594
609
|
const resolved = await this.#resolveBindingForUse(existing);
|
|
595
610
|
if (resolved.state === "retargeted") {
|
|
596
|
-
this.#revokeAndDeleteBinding(sessionKey, existing);
|
|
611
|
+
await this.#revokeAndDeleteBinding(sessionKey, existing);
|
|
597
612
|
return { state: "available" };
|
|
598
613
|
}
|
|
599
614
|
if (resolved.state === "stale") {
|
|
@@ -601,7 +616,6 @@ var ClickClackDiscussionService = class {
|
|
|
601
616
|
return { state: "available" };
|
|
602
617
|
}
|
|
603
618
|
if (resolved.state !== "active") return { state: "none" };
|
|
604
|
-
await this.#finalizePendingBinding(sessionKey, existing);
|
|
605
619
|
await this.#reconcileBinding(sessionKey, existing, resolved.account);
|
|
606
620
|
const current = await this.#store.getAsync(sessionKey);
|
|
607
621
|
if (!current) return { state: this.hasEnabledAccount() ? "available" : "none" };
|
|
@@ -625,10 +639,11 @@ var ClickClackDiscussionService = class {
|
|
|
625
639
|
const existing = await this.#store.getAsync(sessionKey);
|
|
626
640
|
if (existing) {
|
|
627
641
|
const resolved = await this.#resolveBindingForUse(existing);
|
|
628
|
-
if (resolved.state === "retargeted")
|
|
629
|
-
|
|
630
|
-
else if (resolved.state === "
|
|
631
|
-
await this.#
|
|
642
|
+
if (resolved.state === "retargeted") {
|
|
643
|
+
if (!await this.#revokeAndDeleteBinding(sessionKey, existing)) return { state: "available" };
|
|
644
|
+
} else if (resolved.state === "stale") {
|
|
645
|
+
if (!await this.#releaseStaleBinding(sessionKey, existing)) return { state: "available" };
|
|
646
|
+
} else if (resolved.state === "active") {
|
|
632
647
|
await this.#reconcileBinding(sessionKey, existing, resolved.account);
|
|
633
648
|
const current = await this.#store.getAsync(sessionKey);
|
|
634
649
|
if (current) return discussionInfoForBinding(current, resolved.account);
|
|
@@ -666,7 +681,7 @@ var ClickClackDiscussionService = class {
|
|
|
666
681
|
await this.#store.prepare();
|
|
667
682
|
const binding = await this.#store.getAsync(sessionKey);
|
|
668
683
|
if (binding) await this.#reconcileBinding(sessionKey, binding);
|
|
669
|
-
const pending =
|
|
684
|
+
const pending = await getPendingDiscussionOpen(this.#runtime, sessionKey);
|
|
670
685
|
if (pending) await this.#reconcilePendingOpen(pending);
|
|
671
686
|
});
|
|
672
687
|
} finally {
|
|
@@ -699,12 +714,13 @@ var ClickClackDiscussionService = class {
|
|
|
699
714
|
if (!binding) return { text: "No discussion is bound to this session." };
|
|
700
715
|
const resolved = await this.#resolveBindingForUse(binding);
|
|
701
716
|
if (resolved.state !== "active") return { text: "No discussion is bound to this session." };
|
|
702
|
-
const attached = this.#refreshSessionAttachment(sessionKey, binding);
|
|
717
|
+
const attached = await this.#refreshSessionAttachment(sessionKey, binding);
|
|
703
718
|
if (!attached) return { text: "No discussion is bound to this session." };
|
|
704
|
-
if (isClickClackDiscussionChannelRevoked({
|
|
719
|
+
if (!bindingMatchesActiveSessionIncarnation(this.#runtime, sessionKey, attached) || resolveDiscussionBindingAccount(this.#currentConfig(), attached).state !== "active" || isClickClackDiscussionChannelRevoked({
|
|
705
720
|
runtime: this.#runtime,
|
|
706
721
|
serverBaseUrl: binding.serverBaseUrl,
|
|
707
|
-
channelId: binding.channelId
|
|
722
|
+
channelId: binding.channelId,
|
|
723
|
+
binding: attached
|
|
708
724
|
})) return { text: "No discussion is bound to this session." };
|
|
709
725
|
return {
|
|
710
726
|
binding: attached,
|
|
@@ -724,27 +740,25 @@ var ClickClackDiscussionService = class {
|
|
|
724
740
|
}
|
|
725
741
|
async #reconcileBinding(sessionKey, binding, resolvedAccount) {
|
|
726
742
|
await this.#finalizePendingBinding(sessionKey, binding);
|
|
727
|
-
if (
|
|
743
|
+
if (await isClickClackDiscussionChannelRevokedAsync({
|
|
728
744
|
runtime: this.#runtime,
|
|
729
745
|
serverBaseUrl: binding.serverBaseUrl,
|
|
730
|
-
channelId: binding.channelId
|
|
746
|
+
channelId: binding.channelId,
|
|
747
|
+
binding
|
|
731
748
|
})) {
|
|
732
|
-
this.#store.
|
|
749
|
+
await this.#store.deleteIfCurrent(sessionKey, binding);
|
|
733
750
|
return;
|
|
734
751
|
}
|
|
735
|
-
const entry = this.#runtime
|
|
736
|
-
sessionKey,
|
|
737
|
-
readConsistency: "latest"
|
|
738
|
-
});
|
|
752
|
+
const entry = await readDiscussionSessionEntry(this.#runtime, sessionKey);
|
|
739
753
|
if (!entry) {
|
|
740
|
-
this.#detachedBindings.mark(sessionKey, binding);
|
|
754
|
+
await this.#detachedBindings.mark(sessionKey, binding);
|
|
741
755
|
return;
|
|
742
756
|
}
|
|
743
|
-
const activeBinding = this.#detachedBindings.clear(sessionKey, binding);
|
|
757
|
+
const activeBinding = await this.#detachedBindings.clear(sessionKey, binding);
|
|
744
758
|
if (!activeBinding) return;
|
|
745
759
|
const resolved = resolvedAccount ? resolveDiscussionBindingAccount(this.#currentConfig(), activeBinding) : await this.#resolveBindingForUse(activeBinding);
|
|
746
760
|
if (resolved.state === "retargeted") {
|
|
747
|
-
this.#revokeAndDeleteBinding(sessionKey, activeBinding);
|
|
761
|
+
await this.#revokeAndDeleteBinding(sessionKey, activeBinding);
|
|
748
762
|
return;
|
|
749
763
|
}
|
|
750
764
|
if (resolved.state === "stale") {
|
|
@@ -760,14 +774,15 @@ var ClickClackDiscussionService = class {
|
|
|
760
774
|
let observedSiblingTitleSupport = false;
|
|
761
775
|
if (labelForBackfill === activeBinding.label && desiredBackfillTitle !== "" && activeBinding.displayTitle === void 0) {
|
|
762
776
|
const bindings = await this.#store.entries();
|
|
763
|
-
const freshBinding = this.#store.
|
|
777
|
+
const freshBinding = await this.#store.getAsync(sessionKey);
|
|
764
778
|
if (!freshBinding || freshBinding.serverBaseUrl !== activeBinding.serverBaseUrl || freshBinding.channelId !== activeBinding.channelId || freshBinding.externalRef !== activeBinding.externalRef) return;
|
|
765
|
-
if (
|
|
779
|
+
if (await isClickClackDiscussionChannelRevokedAsync({
|
|
766
780
|
runtime: this.#runtime,
|
|
767
781
|
serverBaseUrl: freshBinding.serverBaseUrl,
|
|
768
|
-
channelId: freshBinding.channelId
|
|
782
|
+
channelId: freshBinding.channelId,
|
|
783
|
+
binding: freshBinding
|
|
769
784
|
})) {
|
|
770
|
-
this.#store.
|
|
785
|
+
await this.#store.deleteIfCurrent(sessionKey, freshBinding);
|
|
771
786
|
return;
|
|
772
787
|
}
|
|
773
788
|
bindingForAttachment = freshBinding;
|
|
@@ -776,8 +791,8 @@ var ClickClackDiscussionService = class {
|
|
|
776
791
|
const current = resolveDiscussionBindingAccount(this.#currentConfig(), bindingForAttachment);
|
|
777
792
|
if (current.state !== "active") return;
|
|
778
793
|
const account = current.account;
|
|
779
|
-
const attached = this.#refreshSessionAttachment(sessionKey, bindingForAttachment);
|
|
780
|
-
if (!attached) return;
|
|
794
|
+
const attached = await this.#refreshSessionAttachment(sessionKey, bindingForAttachment);
|
|
795
|
+
if (!attached || attached.sessionId !== entry.sessionId) return;
|
|
781
796
|
const currentBinding = attached;
|
|
782
797
|
const fallback = fallbackDiscussionLabel(sessionKey, currentBinding.agentId);
|
|
783
798
|
const label = resolveDiscussionLabel(entry, sessionKey, currentBinding.agentId);
|
|
@@ -793,6 +808,15 @@ var ClickClackDiscussionService = class {
|
|
|
793
808
|
if (externalUrl !== currentBinding.externalUrl) patch.external_url = externalUrl;
|
|
794
809
|
if (Object.keys(patch).length === 0 && !labelChanged) return;
|
|
795
810
|
const client = this.#clientFactory(account);
|
|
811
|
+
const assertCurrentAuthority = () => {
|
|
812
|
+
const bound = this.#store.get(sessionKey);
|
|
813
|
+
if (!bound || bound.externalRef !== currentBinding.externalRef || bound.serverBaseUrl !== currentBinding.serverBaseUrl || bound.channelId !== currentBinding.channelId || !bindingMatchesActiveSessionIncarnation(this.#runtime, sessionKey, currentBinding) || resolveDiscussionBindingAccount(this.#currentConfig(), currentBinding).state !== "active" || isClickClackDiscussionChannelRevoked({
|
|
814
|
+
runtime: this.#runtime,
|
|
815
|
+
serverBaseUrl: currentBinding.serverBaseUrl,
|
|
816
|
+
channelId: currentBinding.channelId,
|
|
817
|
+
binding: bound
|
|
818
|
+
})) throw new Error("ClickClack discussion authority changed before channel update");
|
|
819
|
+
};
|
|
796
820
|
let updated;
|
|
797
821
|
if (labelChanged) updated = await this.#withChannelMutationLock(() => renameDiscussionChannel({
|
|
798
822
|
client,
|
|
@@ -801,27 +825,27 @@ var ClickClackDiscussionService = class {
|
|
|
801
825
|
label,
|
|
802
826
|
sessionKey,
|
|
803
827
|
agentId: currentBinding.agentId,
|
|
804
|
-
patch
|
|
828
|
+
patch,
|
|
829
|
+
assertCurrentAuthority
|
|
805
830
|
}));
|
|
806
831
|
else {
|
|
832
|
+
assertCurrentAuthority();
|
|
807
833
|
updated = await client.updateChannel(currentBinding.channelId, patch);
|
|
808
834
|
assertChannelPatch(updated, patch);
|
|
809
835
|
}
|
|
810
|
-
const latestBinding = this.#store.get(sessionKey);
|
|
811
|
-
if (!latestBinding || latestBinding.serverBaseUrl !== currentBinding.serverBaseUrl || latestBinding.channelId !== currentBinding.channelId || latestBinding.externalRef !== currentBinding.externalRef) return;
|
|
812
836
|
const nextBinding = {
|
|
813
|
-
...
|
|
837
|
+
...currentBinding,
|
|
814
838
|
externalUrl,
|
|
815
839
|
label,
|
|
816
840
|
section,
|
|
817
841
|
...updated.display_title !== void 0 ? { displayTitle: updated.display_title } : {}
|
|
818
842
|
};
|
|
819
843
|
if (updated.display_title === void 0) delete nextBinding.displayTitle;
|
|
820
|
-
this.#store.
|
|
844
|
+
await this.#store.setIfCurrent(sessionKey, currentBinding, nextBinding, { assertCurrent: assertCurrentAuthority });
|
|
821
845
|
}
|
|
822
|
-
#refreshSessionAttachment(sessionKey, binding) {
|
|
846
|
+
async #refreshSessionAttachment(sessionKey, binding) {
|
|
823
847
|
try {
|
|
824
|
-
return attachBindingToCurrentActiveSession({
|
|
848
|
+
return await attachBindingToCurrentActiveSession({
|
|
825
849
|
runtime: this.#runtime,
|
|
826
850
|
store: this.#store,
|
|
827
851
|
sessionKey,
|
|
@@ -844,20 +868,22 @@ var ClickClackDiscussionService = class {
|
|
|
844
868
|
});
|
|
845
869
|
}
|
|
846
870
|
async #releaseStaleBinding(sessionKey, binding) {
|
|
847
|
-
await
|
|
871
|
+
const pending = await getPendingDiscussionOpen(this.#runtime, sessionKey);
|
|
872
|
+
if (pending?.externalRef === binding.externalRef) await clearDiscussionBindingGeneration({
|
|
848
873
|
runtime: this.#runtime,
|
|
849
|
-
sessionKey
|
|
874
|
+
sessionKey,
|
|
875
|
+
expectedGeneration: pending.generation
|
|
850
876
|
});
|
|
851
|
-
this.#revokeAndDeleteBinding(sessionKey, binding);
|
|
877
|
+
return await this.#revokeAndDeleteBinding(sessionKey, binding);
|
|
852
878
|
}
|
|
853
|
-
#revokeAndDeleteBinding(sessionKey, binding) {
|
|
854
|
-
markClickClackDiscussionChannelRevoked(this.#runtime, binding);
|
|
855
|
-
this.#store.
|
|
879
|
+
async #revokeAndDeleteBinding(sessionKey, binding) {
|
|
880
|
+
if (!await markClickClackDiscussionChannelRevoked(this.#runtime, sessionKey, binding)) return false;
|
|
881
|
+
return await this.#store.deleteIfCurrent(sessionKey, binding);
|
|
856
882
|
}
|
|
857
883
|
async #finalizePendingBinding(sessionKey, binding) {
|
|
858
|
-
const pending =
|
|
859
|
-
if (pending) {
|
|
860
|
-
clearClickClackDiscussionChannelRevoked({
|
|
884
|
+
const pending = await getPendingDiscussionOpen(this.#runtime, sessionKey);
|
|
885
|
+
if (pending?.externalRef === binding.externalRef) {
|
|
886
|
+
await clearClickClackDiscussionChannelRevoked({
|
|
861
887
|
runtime: this.#runtime,
|
|
862
888
|
serverBaseUrl: binding.serverBaseUrl,
|
|
863
889
|
channelId: binding.channelId
|
|
@@ -937,25 +963,52 @@ function isClickClackDiscussionSessionTarget(params) {
|
|
|
937
963
|
const { runtime, targetSessionKey } = params;
|
|
938
964
|
let binding;
|
|
939
965
|
try {
|
|
940
|
-
|
|
966
|
+
const store = getClickClackDiscussionBindingStore(runtime);
|
|
967
|
+
store.assertReadAvailable();
|
|
968
|
+
binding = store.get(targetSessionKey);
|
|
941
969
|
} catch {
|
|
942
970
|
return;
|
|
943
971
|
}
|
|
972
|
+
return currentDiscussionTarget(params, binding, (current) => isClickClackDiscussionChannelRevoked({
|
|
973
|
+
runtime,
|
|
974
|
+
serverBaseUrl: current.serverBaseUrl,
|
|
975
|
+
channelId: current.channelId,
|
|
976
|
+
binding: current
|
|
977
|
+
}));
|
|
978
|
+
}
|
|
979
|
+
function currentDiscussionTarget(params, binding, isRevoked) {
|
|
980
|
+
const { runtime, targetSessionKey } = params;
|
|
944
981
|
if (binding && discussionSessionKey({
|
|
945
982
|
runtime,
|
|
946
983
|
mainSessionKey: targetSessionKey,
|
|
947
984
|
...binding
|
|
948
|
-
}) === params.requesterSessionKey && !
|
|
949
|
-
runtime,
|
|
950
|
-
serverBaseUrl: binding.serverBaseUrl,
|
|
951
|
-
channelId: binding.channelId
|
|
952
|
-
}) && bindingMatchesActiveSessionIncarnation(runtime, targetSessionKey, binding) && resolveDiscussionBindingAccount(runtime.config.current(), binding).state === "active") return {
|
|
985
|
+
}) === params.requesterSessionKey && !isRevoked(binding) && bindingMatchesActiveSessionIncarnation(runtime, targetSessionKey, binding) && resolveDiscussionBindingAccount(runtime.config.current(), binding).state === "active") return {
|
|
953
986
|
sessionKey: targetSessionKey,
|
|
954
987
|
binding
|
|
955
988
|
};
|
|
956
989
|
}
|
|
990
|
+
async function resolveClickClackDiscussionSessionTarget(params) {
|
|
991
|
+
if (!isDiscussionSessionKey(params.requesterSessionKey)) return;
|
|
992
|
+
let facts;
|
|
993
|
+
let revocation;
|
|
994
|
+
try {
|
|
995
|
+
facts = await getClickClackDiscussionBindingStore(params.runtime).prepareRead(params.targetSessionKey);
|
|
996
|
+
const binding = facts.read();
|
|
997
|
+
if (!binding) return;
|
|
998
|
+
revocation = await prepareClickClackDiscussionChannelRevocation({
|
|
999
|
+
runtime: params.runtime,
|
|
1000
|
+
...binding
|
|
1001
|
+
});
|
|
1002
|
+
return currentDiscussionTarget(params, facts.read(), revocation.isRevoked);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return;
|
|
1005
|
+
} finally {
|
|
1006
|
+
revocation?.release();
|
|
1007
|
+
facts?.release();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
957
1010
|
/** Restricts a discussion side session's session tools to its attached main session. */
|
|
958
|
-
function enforceClickClackDiscussionToolTarget(params) {
|
|
1011
|
+
async function enforceClickClackDiscussionToolTarget(params) {
|
|
959
1012
|
const callerSessionKey = params.context.sessionKey;
|
|
960
1013
|
if (!callerSessionKey || !isDiscussionSessionKey(callerSessionKey)) return;
|
|
961
1014
|
const { toolName } = params.event;
|
|
@@ -964,7 +1017,7 @@ function enforceClickClackDiscussionToolTarget(params) {
|
|
|
964
1017
|
const mutatesStatus = toolName === "session_status" && params.event.params.model !== void 0;
|
|
965
1018
|
const selectsHistoryIncarnation = toolName === "sessions_history" && params.event.params.sessionId !== void 0;
|
|
966
1019
|
const targetSessionKey = params.event.params.sessionKey;
|
|
967
|
-
if (TARGETED_SESSION_TOOLS.has(toolName) && typeof targetSessionKey === "string" && !usesAlternateSendTarget && !mutatesStatus && !selectsHistoryIncarnation &&
|
|
1020
|
+
if (TARGETED_SESSION_TOOLS.has(toolName) && typeof targetSessionKey === "string" && !usesAlternateSendTarget && !mutatesStatus && !selectsHistoryIncarnation && await resolveClickClackDiscussionSessionTarget({
|
|
968
1021
|
runtime: params.runtime,
|
|
969
1022
|
requesterSessionKey: callerSessionKey,
|
|
970
1023
|
targetSessionKey
|
|
@@ -1033,7 +1086,14 @@ function registerClickClackDiscussions(api) {
|
|
|
1033
1086
|
targetSessionKey
|
|
1034
1087
|
});
|
|
1035
1088
|
return target ? { expectedSessionId: target.binding.sessionId } : void 0;
|
|
1036
|
-
})
|
|
1089
|
+
}, { async resolveAsync({ requesterSessionKey, targetSessionKey }) {
|
|
1090
|
+
const target = await resolveClickClackDiscussionSessionTarget({
|
|
1091
|
+
runtime: api.runtime,
|
|
1092
|
+
requesterSessionKey,
|
|
1093
|
+
targetSessionKey
|
|
1094
|
+
});
|
|
1095
|
+
return target ? { expectedSessionId: target.binding.sessionId } : void 0;
|
|
1096
|
+
} });
|
|
1037
1097
|
registerSessionDiscussionProvider(service.provider);
|
|
1038
1098
|
api.lifecycle.registerRuntimeLifecycle({
|
|
1039
1099
|
id: "clickclack-discussions",
|
|
@@ -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-BJ3jioqA.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";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as clickClackSetupPlugin } from "./setup-plugin-api-Ccxm2ydn.mjs";
|
|
@@ -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-BJ3jioqA.mjs";
|
|
2
2
|
//#region extensions/clickclack/src/channel.setup.ts
|
|
3
3
|
const clickClackSetupPlugin = {
|
|
4
4
|
id: "clickclack",
|
|
@@ -195,7 +195,7 @@ function resolveClickClackAccount(params) {
|
|
|
195
195
|
mentionPatterns: merged.mentionPatterns ?? [],
|
|
196
196
|
allowBots: merged.allowBots ?? false,
|
|
197
197
|
botLoopProtection: merged.botLoopProtection,
|
|
198
|
-
groups:
|
|
198
|
+
groups: merged.groups ?? {},
|
|
199
199
|
config: {
|
|
200
200
|
...merged,
|
|
201
201
|
allowFrom: merged.allowFrom ?? ["*"]
|
|
@@ -322,7 +322,7 @@ function requireClickClackSetupApiBaseUrl(value, label) {
|
|
|
322
322
|
basePath = pathname;
|
|
323
323
|
}
|
|
324
324
|
const canonical = parsed.origin + basePath;
|
|
325
|
-
if (value !== canonical) throw new Error(`ClickClack ${label} is not
|
|
325
|
+
if (value !== canonical) throw new Error(`ClickClack ${label} is not in the expected URL format`);
|
|
326
326
|
return canonical;
|
|
327
327
|
}
|
|
328
328
|
function requireClickClackSetupClaimUrl(value) {
|
|
@@ -331,7 +331,7 @@ function requireClickClackSetupClaimUrl(value) {
|
|
|
331
331
|
const basePath = parsed.pathname.slice(0, -26);
|
|
332
332
|
const apiBaseUrl = requireClickClackSetupApiBaseUrl(parsed.origin + basePath, "setup URL API base");
|
|
333
333
|
const claimUrl = apiBaseUrl + CLICKCLACK_SETUP_CODE_CLAIM_PATH;
|
|
334
|
-
if (value !== claimUrl) throw new Error("ClickClack setup URL has a
|
|
334
|
+
if (value !== claimUrl) throw new Error("ClickClack setup URL has a claim endpoint in an unexpected URL format.");
|
|
335
335
|
return {
|
|
336
336
|
claimUrl,
|
|
337
337
|
apiBaseUrl
|
|
@@ -450,7 +450,7 @@ const clickClackSetupAdapter = {
|
|
|
450
450
|
const claimUrl = setup.exactClaimUrl && !privateApiBaseUrl ? setup.exactClaimUrl : buildClickClackSetupClaimUrl(privateApiBaseUrl ?? setup.baseUrl);
|
|
451
451
|
let claim;
|
|
452
452
|
try {
|
|
453
|
-
const { claimClickClackSetupCode } = await import("./setup-claim-
|
|
453
|
+
const { claimClickClackSetupCode } = await import("./setup-claim-BPuQuMKW.mjs");
|
|
454
454
|
claim = await claimClickClackSetupCode({
|
|
455
455
|
claimUrl,
|
|
456
456
|
code: setup.code,
|
|
@@ -530,7 +530,7 @@ const clickClackSetupAdapter = {
|
|
|
530
530
|
});
|
|
531
531
|
},
|
|
532
532
|
afterAccountConfigWritten: async ({ cfg, accountId, runtime }) => {
|
|
533
|
-
const { verifyClickClackAccountAfterSetup } = await import("./setup-verify-
|
|
533
|
+
const { verifyClickClackAccountAfterSetup } = await import("./setup-verify-DGsYtVh9.mjs");
|
|
534
534
|
await verifyClickClackAccountAfterSetup({
|
|
535
535
|
cfg,
|
|
536
536
|
accountId,
|
|
@@ -712,7 +712,7 @@ const clickClackSetupWizard = {
|
|
|
712
712
|
})
|
|
713
713
|
}],
|
|
714
714
|
finalize: async ({ cfg, accountId, credentialValues, prompter }) => {
|
|
715
|
-
const { checkClickClackSetupConnection } = await import("./setup-verify-
|
|
715
|
+
const { checkClickClackSetupConnection } = await import("./setup-verify-DGsYtVh9.mjs");
|
|
716
716
|
const result = await checkClickClackSetupConnection({
|
|
717
717
|
cfg,
|
|
718
718
|
accountId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { h as resolveClickClackAccount } from "./setup-surface-
|
|
2
|
-
import { a as createClickClackClient,
|
|
1
|
+
import { h as resolveClickClackAccount } from "./setup-surface-BJ3jioqA.mjs";
|
|
2
|
+
import { a as createClickClackClient, t as findClickClackWorkspace } from "./resolve-Br3ylpSz.mjs";
|
|
3
3
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/setup";
|
|
@@ -32,8 +32,8 @@ async function checkClickClackSetupConnection(params) {
|
|
|
32
32
|
token
|
|
33
33
|
});
|
|
34
34
|
const me = await client.me();
|
|
35
|
-
const
|
|
36
|
-
const workspace = (
|
|
35
|
+
const workspaces = await client.workspaces();
|
|
36
|
+
const workspace = account.workspace.startsWith("wsp_") ? workspaces.find((candidate) => candidate.id === account.workspace) : findClickClackWorkspace(workspaces, account.workspace);
|
|
37
37
|
if (!workspace) throw new Error(`ClickClack workspace not found: ${account.workspace}`);
|
|
38
38
|
return {
|
|
39
39
|
status: "connected",
|
|
@@ -83,20 +83,12 @@ function formatClickClackConnectionLog(result) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
async function verifyClickClackAccountAfterSetup(params) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} catch (error) {
|
|
93
|
-
params.runtime.log(`Connection check failed: ${formatErrorMessage(error)}. Setup was saved; fix the connection and rerun setup.`);
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
params.runtime.log(await probeClickClackGatewayStatus());
|
|
97
|
-
} catch {
|
|
98
|
-
params.runtime.log(GATEWAY_UNKNOWN_MESSAGE);
|
|
99
|
-
}
|
|
86
|
+
const message = formatClickClackConnectionLog(await checkClickClackSetupConnection({
|
|
87
|
+
cfg: params.cfg,
|
|
88
|
+
accountId: params.accountId
|
|
89
|
+
}));
|
|
90
|
+
if (message) params.runtime.log(message);
|
|
91
|
+
params.runtime.log(await probeClickClackGatewayStatus());
|
|
100
92
|
}
|
|
101
93
|
//#endregion
|
|
102
94
|
export { checkClickClackSetupConnection, verifyClickClackAccountAfterSetup };
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as resolveDefaultClickClackAccountId, d as DEFAULT_ACCOUNT_ID, h as resolveClickClackAccount, m as listEnabledClickClackAccounts, p as listClickClackAccountIds, s as clickClackConfigSchema } from "./.setup/setup-surface-
|
|
2
|
-
import { a as setClickClackRuntime, i as getClickClackRuntime, n as buildClickClackTarget, r as parseClickClackTarget, t as clickClackPlugin } from "./.setup/channel-
|
|
1
|
+
import { _ as resolveDefaultClickClackAccountId, d as DEFAULT_ACCOUNT_ID, h as resolveClickClackAccount, m as listEnabledClickClackAccounts, p as listClickClackAccountIds, s as clickClackConfigSchema } from "./.setup/setup-surface-BJ3jioqA.mjs";
|
|
2
|
+
import { a as setClickClackRuntime, i as getClickClackRuntime, n as buildClickClackTarget, r as parseClickClackTarget, t as clickClackPlugin } from "./.setup/channel-DRcZ_twH.mjs";
|
|
3
3
|
import { a as createClickClackClient } from "./.setup/resolve-Br3ylpSz.mjs";
|
|
4
4
|
export { DEFAULT_ACCOUNT_ID, buildClickClackTarget, clickClackConfigSchema, clickClackPlugin, createClickClackClient, getClickClackRuntime, listClickClackAccountIds, listEnabledClickClackAccounts, parseClickClackTarget, resolveClickClackAccount, resolveDefaultClickClackAccountId, setClickClackRuntime };
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { clickClackPlugin };
|
|
1
|
+
export { t as clickClackPlugin } from "./.setup/channel-DRcZ_twH.mjs";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as registerClickClackDiscussions } from "./.setup/runtime-api-
|
|
1
|
+
import { t as registerClickClackDiscussions } from "./.setup/runtime-api-BrlvVAQJ.mjs";
|
|
2
2
|
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
3
3
|
//#region extensions/clickclack/index.ts
|
|
4
4
|
var clickclack_default = defineBundledChannelEntry({
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { h as resolveClickClackAccount } from "./.setup/setup-surface-
|
|
2
|
-
import { a as setClickClackRuntime, r as parseClickClackTarget } from "./.setup/channel-
|
|
1
|
+
import { h as resolveClickClackAccount } from "./.setup/setup-surface-BJ3jioqA.mjs";
|
|
2
|
+
import { a as setClickClackRuntime, r as parseClickClackTarget } from "./.setup/channel-DRcZ_twH.mjs";
|
|
3
3
|
import { a as createClickClackClient } from "./.setup/resolve-Br3ylpSz.mjs";
|
|
4
|
-
import { t as registerClickClackDiscussions } from "./.setup/runtime-api-
|
|
4
|
+
import { t as registerClickClackDiscussions } from "./.setup/runtime-api-BrlvVAQJ.mjs";
|
|
5
5
|
export { createClickClackClient, parseClickClackTarget, registerClickClackDiscussions, resolveClickClackAccount, setClickClackRuntime };
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
|
|
1
|
+
export { t as channelSecrets, n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./.setup/secret-contract-CF7TKtkU.mjs";
|
package/dist/setup-entry.js
CHANGED
|
@@ -4,11 +4,11 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
var setup_entry_default = defineBundledChannelSetupEntry({
|
|
5
5
|
importMetaUrl: import.meta.url,
|
|
6
6
|
plugin: {
|
|
7
|
-
specifier: fileURLToPath(new URL(".setup/setup-plugin-api-
|
|
7
|
+
specifier: fileURLToPath(new URL(".setup/setup-plugin-api-0RMVREuM.mjs", import.meta.url).href),
|
|
8
8
|
exportName: "clickClackSetupPlugin"
|
|
9
9
|
},
|
|
10
10
|
secrets: {
|
|
11
|
-
specifier: fileURLToPath(new URL(".setup/secret-contract-api
|
|
11
|
+
specifier: fileURLToPath(new URL(".setup/secret-contract-api--45pFCg3.mjs", import.meta.url).href),
|
|
12
12
|
exportName: "channelSecrets"
|
|
13
13
|
}
|
|
14
14
|
});
|
package/dist/setup-plugin-api.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { clickClackSetupPlugin };
|
|
1
|
+
export { t as clickClackSetupPlugin } from "./.setup/setup-plugin-api-Ccxm2ydn.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/clickclack",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.10-beta.2",
|
|
4
4
|
"description": "OpenClaw ClickClack channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"zod": "4.6.5"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"openclaw": ">=2026.10.
|
|
16
|
+
"openclaw": ">=2026.10.10-beta.2"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
|
@@ -139,10 +139,10 @@
|
|
|
139
139
|
"allowInvalidConfigRecovery": true
|
|
140
140
|
},
|
|
141
141
|
"compat": {
|
|
142
|
-
"pluginApi": ">=2026.10.
|
|
142
|
+
"pluginApi": ">=2026.10.10-beta.2"
|
|
143
143
|
},
|
|
144
144
|
"build": {
|
|
145
|
-
"openclawVersion": "2026.10.
|
|
145
|
+
"openclawVersion": "2026.10.10-beta.2",
|
|
146
146
|
"bundledDist": false
|
|
147
147
|
},
|
|
148
148
|
"release": {
|