@openclaw/clickclack 2026.7.2-beta.7 → 2026.8.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/README.md +17 -20
- package/dist/api.js +2 -2
- package/dist/{channel-CECIpaS0.js → channel-DaHgaTy1.js} +495 -77
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{runtime-api-CZ1rmHjv.js → runtime-api-CPL3m5AD.js} +218 -231
- package/dist/runtime-api.js +3 -3
- package/dist/{setup-claim-CP-QZ5bt.js → setup-claim-CQjgPGxU.js} +1 -1
- package/dist/setup-plugin-api.js +1 -2
- package/dist/{setup-surface-DAy8vDJK.js → setup-surface-DjT-gmMe.js} +63 -16
- package/openclaw.plugin.json +201 -0
- package/package.json +8 -5
|
@@ -1,104 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { C as
|
|
1
|
+
import { b as resolveClickClackAccount, d as isClickClackChannelNameConflict, l as ClickClackHttpError, u as createClickClackClient, v as listClickClackAccountIds } from "./setup-surface-DjT-gmMe.js";
|
|
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-DaHgaTy1.js";
|
|
3
3
|
import "./api.js";
|
|
4
|
-
import { resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
|
5
4
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { registerSessionDiscussionProvider } from "openclaw/plugin-sdk/session-discussion";
|
|
5
|
+
import { buildControlUiSessionPath, registerSessionDiscussionProvider } from "openclaw/plugin-sdk/session-discussion";
|
|
7
6
|
import { createSessionVisibilityChecker } from "openclaw/plugin-sdk/session-visibility";
|
|
8
|
-
import {
|
|
7
|
+
import { resolveSessionAgentId } from "openclaw/plugin-sdk/agent-scope-runtime";
|
|
9
8
|
import { textResult } from "openclaw/plugin-sdk/tool-results";
|
|
10
|
-
//#region extensions/clickclack/src/discussions/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return FIXED_RESERVED_SESSION_RESTS.has(normalized) || normalized === (optionalString(mainKey)?.toLowerCase() ?? DEFAULT_MAIN_KEY);
|
|
57
|
-
}
|
|
58
|
-
function sessionSlug(displayName) {
|
|
59
|
-
const tokens = (displayName ?? "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").split("-").filter(Boolean);
|
|
60
|
-
while (tokens.length > 0 && /^[0-9a-f]+$/u.test(tokens.at(-1) ?? "")) tokens.pop();
|
|
61
|
-
return tokens.join("-").slice(0, SESSION_SLUG_MAX_LENGTH).replace(/-+$/gu, "");
|
|
62
|
-
}
|
|
63
|
-
function sessionKeyUuid(sessionKey) {
|
|
64
|
-
const uuid = (agentSessionKeyParts(sessionKey)?.rest)?.match(SESSION_UUID_SUFFIX_RE)?.[1];
|
|
65
|
-
return uuid ? uuid.toLowerCase().replaceAll("-", "") : null;
|
|
66
|
-
}
|
|
67
|
-
function sessionPath(params) {
|
|
68
|
-
const rawKey = optionalString(params.sessionKey);
|
|
69
|
-
const parsed = rawKey ? agentSessionKeyParts(rawKey) : null;
|
|
70
|
-
const fallbackAgentId = optionalString(params.fallbackAgentId);
|
|
71
|
-
const agentId = parsed?.agentId ?? (fallbackAgentId ? normalizeAgentId$1(fallbackAgentId) : null);
|
|
72
|
-
if (!rawKey || !agentId || !parsed && rawKey.toLowerCase().startsWith("agent:")) return null;
|
|
73
|
-
const namespace = `${normalizeBasePath(params.basePath)}/chat`;
|
|
74
|
-
const encodedAgentId = encodePathSegment(agentId);
|
|
75
|
-
const rest = parsed?.rest ?? rawKey;
|
|
76
|
-
const normalizedRest = rest.toLowerCase();
|
|
77
|
-
const mainKey = optionalString(params.mainKey)?.toLowerCase() ?? DEFAULT_MAIN_KEY;
|
|
78
|
-
if (!parsed && normalizedRest === DEFAULT_MAIN_KEY || normalizedRest === mainKey || normalizedRest === "global") return `${namespace}/${encodedAgentId}`;
|
|
79
|
-
const uuid = parsed ? sessionKeyUuid(rawKey) : null;
|
|
80
|
-
if (uuid) {
|
|
81
|
-
let length = 8;
|
|
82
|
-
const slug = sessionSlug(params.displayName);
|
|
83
|
-
let sessionRef = `${slug ? `${slug}-` : ""}${uuid.slice(0, length)}`;
|
|
84
|
-
while (length < uuid.length && isReservedSessionRest(sessionRef, params.mainKey)) {
|
|
85
|
-
length += 1;
|
|
86
|
-
sessionRef = `${slug ? `${slug}-` : ""}${uuid.slice(0, length)}`;
|
|
9
|
+
//#region extensions/clickclack/src/discussions/binding-retention.ts
|
|
10
|
+
var DetachedDiscussionBindingRetention = class {
|
|
11
|
+
#runtime;
|
|
12
|
+
#store;
|
|
13
|
+
#maxRetained;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.#runtime = options.runtime;
|
|
16
|
+
this.#store = options.store;
|
|
17
|
+
this.#maxRetained = options.maxRetained;
|
|
18
|
+
}
|
|
19
|
+
mark(sessionKey, binding) {
|
|
20
|
+
const current = this.#store.get(sessionKey);
|
|
21
|
+
if (!current || !this.#sameRoom(current, binding)) return;
|
|
22
|
+
if (current.detachedAt === void 0) this.#store.set(sessionKey, {
|
|
23
|
+
...current,
|
|
24
|
+
detachedAt: Date.now()
|
|
25
|
+
});
|
|
26
|
+
while (this.#store.detachedCount() > this.#maxRetained) if (!this.#pruneOldest()) throw new Error("ClickClack detached discussion binding retention could not be reduced");
|
|
27
|
+
}
|
|
28
|
+
clear(sessionKey, binding) {
|
|
29
|
+
const current = this.#store.get(sessionKey);
|
|
30
|
+
if (!current || !this.#sameRoom(current, binding)) return;
|
|
31
|
+
if (current.detachedAt === void 0) return current;
|
|
32
|
+
const { detachedAt: _detachedAt, ...retained } = current;
|
|
33
|
+
this.#store.set(sessionKey, retained);
|
|
34
|
+
return retained;
|
|
35
|
+
}
|
|
36
|
+
ensureCapacity(sessionKey) {
|
|
37
|
+
while (!this.#store.hasCapacity(sessionKey)) if (!this.#pruneOldest()) throw new Error("ClickClack discussion binding capacity is exhausted");
|
|
38
|
+
}
|
|
39
|
+
#pruneOldest() {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const oldest = this.#store.oldestDetached();
|
|
42
|
+
if (!oldest) return false;
|
|
43
|
+
const current = this.#store.get(oldest.sessionKey);
|
|
44
|
+
if (!current || current.detachedAt === void 0) continue;
|
|
45
|
+
if (this.#runtime.agent.session.getSessionEntry({
|
|
46
|
+
sessionKey: oldest.sessionKey,
|
|
47
|
+
readConsistency: "latest"
|
|
48
|
+
})) {
|
|
49
|
+
this.clear(oldest.sessionKey, current);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
markClickClackDiscussionChannelRevoked(this.#runtime, current);
|
|
53
|
+
this.#store.delete(oldest.sessionKey);
|
|
54
|
+
return true;
|
|
87
55
|
}
|
|
88
|
-
return isReservedSessionRest(sessionRef, params.mainKey) ? null : `${namespace}/${encodedAgentId}/${sessionRef}`;
|
|
89
56
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (segments.length === 1) {
|
|
93
|
-
const segment = segments[0] ?? "";
|
|
94
|
-
if (!isReservedSessionRest(segment, params.mainKey) && SHORT_SESSION_REF_RE.test(segment)) return `${namespace}/${encodedAgentId}/~key/${encodePathSegment(segment)}`;
|
|
57
|
+
#sameRoom(left, right) {
|
|
58
|
+
return left.serverBaseUrl === right.serverBaseUrl && left.channelId === right.channelId && left.externalRef === right.externalRef;
|
|
95
59
|
}
|
|
96
|
-
|
|
97
|
-
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region extensions/clickclack/src/discussions/control-session-url.ts
|
|
98
63
|
function controlSessionUrl(baseUrl, sessionKey, fallbackAgentId, mainKey, displayName) {
|
|
99
64
|
if (!baseUrl) return;
|
|
100
65
|
const url = new URL(baseUrl);
|
|
101
|
-
const path =
|
|
66
|
+
const path = buildControlUiSessionPath({
|
|
67
|
+
namespace: "chat",
|
|
102
68
|
sessionKey,
|
|
103
69
|
fallbackAgentId,
|
|
104
70
|
basePath: url.pathname,
|
|
@@ -277,7 +243,7 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
277
243
|
sessionKey,
|
|
278
244
|
readConsistency: "latest"
|
|
279
245
|
});
|
|
280
|
-
if (!entry) return;
|
|
246
|
+
if (!entry || entry.archivedAt !== void 0) return;
|
|
281
247
|
if (!entry.sessionId?.trim()) throw new Error("OpenClaw session does not yet have a concrete session id");
|
|
282
248
|
const client = params.clientFactory(account);
|
|
283
249
|
const workspace = (await client.workspaces()).find((candidate) => candidate.id === account.discussions.workspace || candidate.slug === account.discussions.workspace || candidate.name === account.discussions.workspace);
|
|
@@ -286,30 +252,34 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
286
252
|
const serverBaseUrl = normalizedServerBaseUrl(account);
|
|
287
253
|
const credentialFingerprint = discussionCredentialFingerprint(account.token);
|
|
288
254
|
const unresolved = listPendingDiscussionOpens(runtime).find((pending) => pending.sessionKey === sessionKey);
|
|
289
|
-
if (unresolved && (unresolved.accountId !== account.accountId || unresolved.credentialFingerprint !== credentialFingerprint || unresolved.
|
|
255
|
+
if (unresolved && (unresolved.accountId !== account.accountId || unresolved.credentialFingerprint !== credentialFingerprint || unresolved.serverBaseUrl !== serverBaseUrl || unresolved.workspaceId !== workspace.id)) {
|
|
290
256
|
await params.reconcilePendingOpen(unresolved);
|
|
291
257
|
if (listPendingDiscussionOpens(runtime).some((pending) => pending.sessionKey === sessionKey)) throw new Error("A previous ClickClack discussion open is still unresolved; restore its credential and retry");
|
|
292
258
|
}
|
|
293
259
|
const config = runtime.config.current();
|
|
294
|
-
const agentId =
|
|
260
|
+
const agentId = resolveSessionAgentId({
|
|
261
|
+
config,
|
|
262
|
+
sessionKey
|
|
263
|
+
});
|
|
295
264
|
const fallback = fallbackDiscussionLabel(sessionKey, agentId);
|
|
296
265
|
const label = resolveDiscussionLabel(entry, sessionKey, agentId);
|
|
297
266
|
const displayTitle = label === fallback ? "" : truncateDiscussionDisplayTitle(label);
|
|
298
267
|
const section = entry.category?.trim() || account.discussions.section;
|
|
299
|
-
const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ??
|
|
300
|
-
const archived = entry.archivedAt !== void 0;
|
|
268
|
+
const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? agentId, config.session?.mainKey, label);
|
|
301
269
|
return await params.withChannelMutationLock(async () => {
|
|
302
|
-
|
|
270
|
+
params.ensureBindingCapacity(sessionKey);
|
|
303
271
|
let channels = await client.channels(workspace.id);
|
|
304
272
|
assertManagedChannelListContract(channels);
|
|
305
273
|
const destinationIdentity = [serverBaseUrl, workspace.id].join("\0");
|
|
306
274
|
const bindingGeneration = reserveDiscussionBindingGeneration({
|
|
307
275
|
runtime,
|
|
308
276
|
sessionKey,
|
|
277
|
+
accountId: account.accountId,
|
|
278
|
+
credentialFingerprint,
|
|
309
279
|
destinationIdentity,
|
|
310
280
|
createGeneration: params.bindingGenerationFactory
|
|
311
281
|
});
|
|
312
|
-
const externalRef = discussionExternalRef(params.installationId, sessionKey,
|
|
282
|
+
const externalRef = discussionExternalRef(params.installationId, sessionKey, destinationIdentity, bindingGeneration);
|
|
313
283
|
let adopted;
|
|
314
284
|
let managedFields;
|
|
315
285
|
let resolved;
|
|
@@ -353,10 +323,7 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
353
323
|
serverBaseUrl,
|
|
354
324
|
channelId: adopted.id
|
|
355
325
|
});
|
|
356
|
-
resolved = await client.updateChannel(adopted.id,
|
|
357
|
-
...managedFields,
|
|
358
|
-
archived
|
|
359
|
-
});
|
|
326
|
+
resolved = await client.updateChannel(adopted.id, managedFields);
|
|
360
327
|
} else {
|
|
361
328
|
resolved = await client.createChannel(workspace.id, {
|
|
362
329
|
...managedFields,
|
|
@@ -389,10 +356,7 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
389
356
|
serverBaseUrl,
|
|
390
357
|
channelId: recovered.id
|
|
391
358
|
});
|
|
392
|
-
resolved = await client.updateChannel(recovered.id,
|
|
393
|
-
...managedFields,
|
|
394
|
-
archived
|
|
395
|
-
});
|
|
359
|
+
resolved = await client.updateChannel(recovered.id, managedFields);
|
|
396
360
|
break;
|
|
397
361
|
}
|
|
398
362
|
if (definitiveNoCreate) clearDiscussionBindingGeneration({
|
|
@@ -419,89 +383,104 @@ async function openClickClackDiscussionBinding(params) {
|
|
|
419
383
|
externalUrl,
|
|
420
384
|
displayTitle
|
|
421
385
|
});
|
|
422
|
-
if (adopted) assertChannelPatch(resolved,
|
|
423
|
-
...managedFields,
|
|
424
|
-
archived
|
|
425
|
-
});
|
|
386
|
+
if (adopted) assertChannelPatch(resolved, managedFields);
|
|
426
387
|
} catch (error) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
});
|
|
434
|
-
} catch (archiveError) {
|
|
435
|
-
params.warn(`failed to archive incompatible discussion channel ${resolved.id}: ${String(archiveError)}`);
|
|
436
|
-
}
|
|
388
|
+
clearPendingDiscussionOpen({
|
|
389
|
+
runtime,
|
|
390
|
+
sessionKey,
|
|
391
|
+
expectedGeneration: bindingGeneration
|
|
392
|
+
});
|
|
393
|
+
params.warn(`incompatible discussion channel remains quarantined: ${resolved.id}`);
|
|
437
394
|
throw error;
|
|
438
395
|
}
|
|
439
396
|
if (!resolved.route_id) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
});
|
|
447
|
-
} catch (archiveError) {
|
|
448
|
-
params.warn(`failed to archive route-less discussion channel ${resolved.id}: ${String(archiveError)}`);
|
|
449
|
-
}
|
|
397
|
+
clearPendingDiscussionOpen({
|
|
398
|
+
runtime,
|
|
399
|
+
sessionKey,
|
|
400
|
+
expectedGeneration: bindingGeneration
|
|
401
|
+
});
|
|
402
|
+
params.warn(`route-less discussion channel remains quarantined: ${resolved.id}`);
|
|
450
403
|
throw new Error("ClickClack discussion channel is missing its route id");
|
|
451
404
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
405
|
+
const channel = resolved;
|
|
406
|
+
const currentEntry = runtime.agent.session.getSessionEntry({
|
|
407
|
+
sessionKey,
|
|
408
|
+
readConsistency: "latest"
|
|
409
|
+
});
|
|
410
|
+
if (!currentEntry?.sessionId || currentEntry.archivedAt !== void 0) {
|
|
411
|
+
clearPendingDiscussionOpen({
|
|
412
|
+
runtime,
|
|
413
|
+
sessionKey,
|
|
414
|
+
expectedGeneration: bindingGeneration
|
|
415
|
+
});
|
|
416
|
+
params.warn(`unattached discussion channel remains quarantined: ${channel.id}`);
|
|
417
|
+
throw new Error("OpenClaw session became inactive while opening its ClickClack discussion");
|
|
418
|
+
}
|
|
419
|
+
const currentLabel = resolveDiscussionLabel(currentEntry, sessionKey, agentId);
|
|
420
|
+
const currentDisplayTitle = currentLabel === fallback ? "" : truncateDiscussionDisplayTitle(currentLabel);
|
|
421
|
+
const currentSection = currentEntry.category?.trim() || account.discussions.section;
|
|
422
|
+
const currentExternalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? agentId, config.session?.mainKey, currentLabel) ?? "";
|
|
423
|
+
let currentChannel = channel;
|
|
424
|
+
if (currentEntry.sessionId !== entry.sessionId || currentLabel !== label || currentDisplayTitle !== displayTitle || currentSection !== section || currentExternalUrl !== (externalUrl ?? "")) try {
|
|
425
|
+
for (let attempt = 0; attempt < CHANNEL_NAME_MUTATION_ATTEMPTS$1; attempt += 1) {
|
|
426
|
+
const latestManagedFields = {
|
|
427
|
+
...managedFields,
|
|
428
|
+
name: await resolveAvailableChannelName({
|
|
429
|
+
client,
|
|
430
|
+
workspaceId: workspace.id,
|
|
431
|
+
label: currentLabel,
|
|
432
|
+
sessionKey,
|
|
433
|
+
agentId,
|
|
434
|
+
ownChannelId: channel.id
|
|
435
|
+
}),
|
|
436
|
+
external_url: currentExternalUrl,
|
|
437
|
+
sidebar_section: currentSection,
|
|
438
|
+
display_title: currentDisplayTitle
|
|
439
|
+
};
|
|
440
|
+
try {
|
|
441
|
+
currentChannel = await client.updateChannel(channel.id, latestManagedFields);
|
|
442
|
+
assertChannelPatch(currentChannel, latestManagedFields);
|
|
443
|
+
break;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (!isClickClackChannelNameConflict(error) || attempt === CHANNEL_NAME_MUTATION_ATTEMPTS$1 - 1) throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
clearPendingDiscussionOpen({
|
|
450
|
+
runtime,
|
|
451
|
+
sessionKey,
|
|
452
|
+
expectedGeneration: bindingGeneration
|
|
453
|
+
});
|
|
454
|
+
params.warn(`unattached discussion channel remains quarantined: ${channel.id}`);
|
|
455
|
+
throw error;
|
|
456
456
|
}
|
|
457
457
|
const nextBinding = {
|
|
458
458
|
accountId: account.accountId,
|
|
459
459
|
agentId,
|
|
460
|
-
sessionId:
|
|
460
|
+
sessionId: currentEntry.sessionId,
|
|
461
461
|
serverBaseUrl,
|
|
462
462
|
credentialFingerprint,
|
|
463
463
|
externalRef,
|
|
464
|
-
externalUrl:
|
|
464
|
+
externalUrl: currentExternalUrl,
|
|
465
465
|
workspaceRef: account.discussions.workspace,
|
|
466
466
|
workspaceId: workspace.id,
|
|
467
467
|
channelId: channel.id,
|
|
468
468
|
channelRouteId: channel.route_id,
|
|
469
469
|
workspaceRouteId: workspace.route_id,
|
|
470
|
-
section,
|
|
471
|
-
archived,
|
|
472
|
-
label,
|
|
473
|
-
|
|
470
|
+
section: currentSection,
|
|
471
|
+
archived: false,
|
|
472
|
+
label: currentLabel,
|
|
473
|
+
...currentChannel.display_title !== void 0 ? { displayTitle: currentChannel.display_title } : {}
|
|
474
474
|
};
|
|
475
|
-
const currentEntry = runtime.agent.session.getSessionEntry({
|
|
476
|
-
sessionKey,
|
|
477
|
-
readConsistency: "latest"
|
|
478
|
-
});
|
|
479
|
-
if (!currentEntry || currentEntry.sessionId !== entry.sessionId) {
|
|
480
|
-
try {
|
|
481
|
-
assertChannelPatch(await client.updateChannel(channel.id, { archived: true }), { archived: true });
|
|
482
|
-
clearDiscussionBindingGeneration({
|
|
483
|
-
runtime,
|
|
484
|
-
sessionKey,
|
|
485
|
-
expectedGeneration: bindingGeneration
|
|
486
|
-
});
|
|
487
|
-
} catch (archiveError) {
|
|
488
|
-
params.warn(`failed to archive superseded discussion channel ${channel.id}: ${String(archiveError)}`);
|
|
489
|
-
}
|
|
490
|
-
throw new Error("OpenClaw session changed while opening its ClickClack discussion");
|
|
491
|
-
}
|
|
492
475
|
try {
|
|
493
476
|
store.set(sessionKey, nextBinding);
|
|
494
477
|
} catch (error) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
});
|
|
502
|
-
} catch (archiveError) {
|
|
503
|
-
params.warn(`failed to archive unbound discussion channel ${channel.id}: ${String(archiveError)}`);
|
|
504
|
-
}
|
|
478
|
+
clearPendingDiscussionOpen({
|
|
479
|
+
runtime,
|
|
480
|
+
sessionKey,
|
|
481
|
+
expectedGeneration: bindingGeneration
|
|
482
|
+
});
|
|
483
|
+
params.warn(`unbound discussion channel remains quarantined: ${channel.id}`);
|
|
505
484
|
throw error;
|
|
506
485
|
}
|
|
507
486
|
params.finalizePendingBinding(sessionKey, nextBinding);
|
|
@@ -518,6 +497,7 @@ var ClickClackDiscussionService = class {
|
|
|
518
497
|
#clientFactory;
|
|
519
498
|
#installationId;
|
|
520
499
|
#bindingGenerationFactory;
|
|
500
|
+
#detachedBindings;
|
|
521
501
|
#timersEnabled;
|
|
522
502
|
#sessionLocks = /* @__PURE__ */ new Map();
|
|
523
503
|
#reconcileScheduler = new DiscussionReconcileScheduler({
|
|
@@ -539,6 +519,11 @@ var ClickClackDiscussionService = class {
|
|
|
539
519
|
}));
|
|
540
520
|
this.#installationId = options.installationId ?? getClickClackDiscussionInstallationId(runtime);
|
|
541
521
|
this.#bindingGenerationFactory = options.bindingGenerationFactory ?? randomUUID;
|
|
522
|
+
this.#detachedBindings = new DetachedDiscussionBindingRetention({
|
|
523
|
+
runtime,
|
|
524
|
+
store: this.#store,
|
|
525
|
+
maxRetained: options.maxRetainedDetachedBindings ?? 1e3
|
|
526
|
+
});
|
|
542
527
|
this.#timersEnabled = options.startTimer !== false;
|
|
543
528
|
this.provider = {
|
|
544
529
|
id: "clickclack",
|
|
@@ -616,6 +601,7 @@ var ClickClackDiscussionService = class {
|
|
|
616
601
|
ensureTimer: () => this.#ensureTimer(),
|
|
617
602
|
reconcilePendingOpen: async (pending) => await this.#reconcilePendingOpen(pending, { allowRetry: false }),
|
|
618
603
|
withChannelMutationLock: async (run) => await this.#withChannelMutationLock(run),
|
|
604
|
+
ensureBindingCapacity: (key) => this.#detachedBindings.ensureCapacity(key),
|
|
619
605
|
finalizePendingBinding: (key, nextBinding) => this.#finalizePendingBinding(key, nextBinding),
|
|
620
606
|
warn: (message) => this.#logger().warn(message)
|
|
621
607
|
});
|
|
@@ -658,15 +644,16 @@ var ClickClackDiscussionService = class {
|
|
|
658
644
|
if (resolved.state === "retargeted") return { text: "No discussion is bound to this session." };
|
|
659
645
|
if (resolved.state === "stale") return { text: "No discussion is bound to this session." };
|
|
660
646
|
if (resolved.state !== "active") return { text: "No discussion is bound to this session." };
|
|
661
|
-
|
|
647
|
+
const attached = this.#refreshSessionAttachment(sessionKey, binding);
|
|
648
|
+
if (!attached) return { text: "No discussion is bound to this session." };
|
|
662
649
|
if (isClickClackDiscussionChannelRevoked({
|
|
663
650
|
runtime: this.#runtime,
|
|
664
651
|
serverBaseUrl: binding.serverBaseUrl,
|
|
665
652
|
channelId: binding.channelId
|
|
666
653
|
})) return { text: "No discussion is bound to this session." };
|
|
667
654
|
return {
|
|
668
|
-
binding,
|
|
669
|
-
text: formatDiscussionHistory(await this.#clientFactory(resolved.account).latestChannelMessages(
|
|
655
|
+
binding: attached,
|
|
656
|
+
text: formatDiscussionHistory(await this.#clientFactory(resolved.account).latestChannelMessages(attached.channelId, limit)) || "The bound discussion has no messages yet."
|
|
670
657
|
};
|
|
671
658
|
}
|
|
672
659
|
cleanup() {
|
|
@@ -690,62 +677,62 @@ var ClickClackDiscussionService = class {
|
|
|
690
677
|
this.#store.delete(sessionKey);
|
|
691
678
|
return;
|
|
692
679
|
}
|
|
680
|
+
const entry = this.#runtime.agent.session.getSessionEntry({
|
|
681
|
+
sessionKey,
|
|
682
|
+
readConsistency: "latest"
|
|
683
|
+
});
|
|
684
|
+
if (!entry) {
|
|
685
|
+
this.#detachedBindings.mark(sessionKey, binding);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const activeBinding = this.#detachedBindings.clear(sessionKey, binding);
|
|
689
|
+
if (!activeBinding) return;
|
|
693
690
|
const resolved = resolvedAccount ? {
|
|
694
691
|
state: "active",
|
|
695
692
|
account: resolvedAccount
|
|
696
|
-
} : await this.#resolveBindingForUse(
|
|
693
|
+
} : await this.#resolveBindingForUse(activeBinding);
|
|
697
694
|
if (resolved.state === "retargeted") {
|
|
698
|
-
this.#revokeAndDeleteBinding(sessionKey,
|
|
695
|
+
this.#revokeAndDeleteBinding(sessionKey, activeBinding);
|
|
699
696
|
return;
|
|
700
697
|
}
|
|
701
698
|
if (resolved.state === "stale") {
|
|
702
|
-
await this.#releaseStaleBinding(sessionKey,
|
|
699
|
+
await this.#releaseStaleBinding(sessionKey, activeBinding);
|
|
703
700
|
return;
|
|
704
701
|
}
|
|
705
702
|
if (resolved.state !== "active") return;
|
|
706
703
|
const account = resolved.account;
|
|
707
|
-
if (!account.baseUrl || !account.token) throw new Error(`ClickClack discussion account is no longer configured: ${
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
const archived = entry ? entry.archivedAt !== void 0 : true;
|
|
717
|
-
const deleted = entry === void 0;
|
|
718
|
-
const fallback = fallbackDiscussionLabel(sessionKey, binding.agentId);
|
|
719
|
-
const label = entry ? resolveDiscussionLabel(entry, sessionKey, binding.agentId) : binding.label;
|
|
720
|
-
const section = entry?.category?.trim() || account.discussions.section;
|
|
704
|
+
if (!account.baseUrl || !account.token) throw new Error(`ClickClack discussion account is no longer configured: ${activeBinding.accountId}`);
|
|
705
|
+
if (entry.archivedAt !== void 0) return;
|
|
706
|
+
const attached = this.#refreshSessionAttachment(sessionKey, activeBinding);
|
|
707
|
+
if (!attached) return;
|
|
708
|
+
const currentBinding = attached;
|
|
709
|
+
const fallback = fallbackDiscussionLabel(sessionKey, currentBinding.agentId);
|
|
710
|
+
const label = resolveDiscussionLabel(entry, sessionKey, currentBinding.agentId);
|
|
711
|
+
const section = entry.category?.trim() || account.discussions.section;
|
|
721
712
|
const externalUrl = controlSessionUrl(account.discussions.controlUrlBase, sessionKey, account.agentId ?? "main", this.#currentConfig().session?.mainKey, label) ?? "";
|
|
722
713
|
const patch = {};
|
|
723
|
-
|
|
724
|
-
const labelChanged = label !== binding.label;
|
|
714
|
+
const labelChanged = label !== currentBinding.label;
|
|
725
715
|
const desiredDisplayTitle = label === fallback ? "" : truncateDiscussionDisplayTitle(label);
|
|
726
|
-
const serverSupportsDisplayTitle = this.#store.entries().some(({ binding: candidate }) => candidate.displayTitle !== void 0 && candidate.serverBaseUrl ===
|
|
727
|
-
const shouldBackfillDisplayTitle = desiredDisplayTitle !== "" &&
|
|
716
|
+
const serverSupportsDisplayTitle = this.#store.entries().some(({ binding: candidate }) => candidate.displayTitle !== void 0 && candidate.serverBaseUrl === currentBinding.serverBaseUrl && candidate.accountId === currentBinding.accountId);
|
|
717
|
+
const shouldBackfillDisplayTitle = desiredDisplayTitle !== "" && currentBinding.displayTitle !== desiredDisplayTitle && serverSupportsDisplayTitle;
|
|
728
718
|
if (labelChanged || shouldBackfillDisplayTitle) patch.display_title = desiredDisplayTitle;
|
|
729
|
-
if (section !==
|
|
730
|
-
if (externalUrl !==
|
|
731
|
-
if (Object.keys(patch).length === 0 && !labelChanged)
|
|
732
|
-
if (deleted) this.#revokeAndDeleteBinding(sessionKey, binding);
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
719
|
+
if (section !== currentBinding.section) patch.sidebar_section = section;
|
|
720
|
+
if (externalUrl !== currentBinding.externalUrl) patch.external_url = externalUrl;
|
|
721
|
+
if (Object.keys(patch).length === 0 && !labelChanged) return;
|
|
735
722
|
const client = this.#clientFactory(account);
|
|
736
723
|
let updated;
|
|
737
724
|
if (labelChanged) updated = await this.#withChannelMutationLock(async () => {
|
|
738
725
|
for (let attempt = 0; attempt < CHANNEL_NAME_MUTATION_ATTEMPTS; attempt += 1) {
|
|
739
726
|
patch.name = await resolveAvailableChannelName({
|
|
740
727
|
client,
|
|
741
|
-
workspaceId:
|
|
728
|
+
workspaceId: currentBinding.workspaceId,
|
|
742
729
|
label,
|
|
743
730
|
sessionKey,
|
|
744
|
-
agentId:
|
|
745
|
-
ownChannelId:
|
|
731
|
+
agentId: currentBinding.agentId,
|
|
732
|
+
ownChannelId: currentBinding.channelId
|
|
746
733
|
});
|
|
747
734
|
try {
|
|
748
|
-
const renamed = await client.updateChannel(
|
|
735
|
+
const renamed = await client.updateChannel(currentBinding.channelId, patch);
|
|
749
736
|
assertChannelPatch(renamed, patch);
|
|
750
737
|
return renamed;
|
|
751
738
|
} catch (error) {
|
|
@@ -755,21 +742,33 @@ var ClickClackDiscussionService = class {
|
|
|
755
742
|
throw new Error("ClickClack discussion channel name retries were exhausted");
|
|
756
743
|
});
|
|
757
744
|
else {
|
|
758
|
-
updated = await client.updateChannel(
|
|
745
|
+
updated = await client.updateChannel(currentBinding.channelId, patch);
|
|
759
746
|
assertChannelPatch(updated, patch);
|
|
760
747
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
this.#store.set(sessionKey, {
|
|
766
|
-
...binding,
|
|
767
|
-
archived,
|
|
748
|
+
const latestBinding = this.#store.get(sessionKey);
|
|
749
|
+
if (!latestBinding || latestBinding.serverBaseUrl !== currentBinding.serverBaseUrl || latestBinding.channelId !== currentBinding.channelId || latestBinding.externalRef !== currentBinding.externalRef) return;
|
|
750
|
+
const nextBinding = {
|
|
751
|
+
...latestBinding,
|
|
768
752
|
externalUrl,
|
|
769
753
|
label,
|
|
770
754
|
section,
|
|
771
|
-
|
|
772
|
-
}
|
|
755
|
+
...updated.display_title !== void 0 ? { displayTitle: updated.display_title } : {}
|
|
756
|
+
};
|
|
757
|
+
if (updated.display_title === void 0) delete nextBinding.displayTitle;
|
|
758
|
+
this.#store.set(sessionKey, nextBinding);
|
|
759
|
+
}
|
|
760
|
+
#refreshSessionAttachment(sessionKey, binding) {
|
|
761
|
+
try {
|
|
762
|
+
return attachBindingToCurrentActiveSession({
|
|
763
|
+
runtime: this.#runtime,
|
|
764
|
+
store: this.#store,
|
|
765
|
+
sessionKey,
|
|
766
|
+
binding
|
|
767
|
+
});
|
|
768
|
+
} catch (error) {
|
|
769
|
+
this.#logger().warn(`discussion attachment refresh failed for ${sessionKey}: ${String(error)}`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
773
772
|
}
|
|
774
773
|
async #reconcilePendingOpen(pending, options = {}) {
|
|
775
774
|
const currentBinding = this.#store.get(pending.sessionKey);
|
|
@@ -790,7 +789,7 @@ var ClickClackDiscussionService = class {
|
|
|
790
789
|
});
|
|
791
790
|
const activeAccounts = discussionAccounts(cfg);
|
|
792
791
|
const retryAccount = activeAccounts.length === 1 ? activeAccounts[0] : void 0;
|
|
793
|
-
if (options.allowRetry !== false && entry?.sessionId
|
|
792
|
+
if (options.allowRetry !== false && entry?.sessionId && entry.archivedAt === void 0 && retryAccount && normalizedServerBaseUrl(retryAccount) === pending.serverBaseUrl && discussionCredentialFingerprint(retryAccount.token) === pending.credentialFingerprint) {
|
|
794
793
|
if ((await this.#clientFactory(retryAccount).workspaces()).find((candidate) => candidate.id === retryAccount.discussions.workspace || candidate.slug === retryAccount.discussions.workspace || candidate.name === retryAccount.discussions.workspace)?.id === pending.workspaceId) {
|
|
795
794
|
await this.open(pending.sessionKey);
|
|
796
795
|
return;
|
|
@@ -806,7 +805,12 @@ var ClickClackDiscussionService = class {
|
|
|
806
805
|
serverBaseUrl: pending.serverBaseUrl,
|
|
807
806
|
channelId: channel.id
|
|
808
807
|
});
|
|
809
|
-
|
|
808
|
+
clearPendingDiscussionOpen({
|
|
809
|
+
runtime: this.#runtime,
|
|
810
|
+
sessionKey: pending.sessionKey,
|
|
811
|
+
expectedGeneration: pending.generation
|
|
812
|
+
});
|
|
813
|
+
return;
|
|
810
814
|
}
|
|
811
815
|
clearDiscussionBindingGeneration({
|
|
812
816
|
runtime: this.#runtime,
|
|
@@ -819,23 +823,6 @@ var ClickClackDiscussionService = class {
|
|
|
819
823
|
runtime: this.#runtime,
|
|
820
824
|
sessionKey
|
|
821
825
|
});
|
|
822
|
-
const boundAccount = resolveClickClackAccount({
|
|
823
|
-
cfg: this.#currentConfig(),
|
|
824
|
-
accountId: binding.accountId
|
|
825
|
-
});
|
|
826
|
-
if (!boundAccount.configured || binding.serverBaseUrl !== normalizedServerBaseUrl(boundAccount) || !binding.credentialFingerprint || binding.credentialFingerprint !== discussionCredentialFingerprint(boundAccount.token)) {
|
|
827
|
-
this.#revokeAndDeleteBinding(sessionKey, binding);
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
assertChannelPatch(await this.#clientFactory(boundAccount).updateChannel(binding.channelId, { archived: true }), { archived: true });
|
|
831
|
-
this.#revokeAndDeleteBinding(sessionKey, binding);
|
|
832
|
-
}
|
|
833
|
-
async #archiveAndDeleteBinding(sessionKey, binding, account) {
|
|
834
|
-
clearDiscussionBindingGeneration({
|
|
835
|
-
runtime: this.#runtime,
|
|
836
|
-
sessionKey
|
|
837
|
-
});
|
|
838
|
-
assertChannelPatch(await this.#clientFactory(account).updateChannel(binding.channelId, { archived: true }), { archived: true });
|
|
839
826
|
this.#revokeAndDeleteBinding(sessionKey, binding);
|
|
840
827
|
}
|
|
841
828
|
#revokeAndDeleteBinding(sessionKey, binding) {
|
|
@@ -928,7 +915,7 @@ function isClickClackDiscussionSessionTarget(params) {
|
|
|
928
915
|
runtime: params.runtime,
|
|
929
916
|
serverBaseUrl: matched.binding.serverBaseUrl,
|
|
930
917
|
channelId: matched.binding.channelId
|
|
931
|
-
}) &&
|
|
918
|
+
}) && bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, matched.binding) && resolveDiscussionBindingAccount(params.runtime.config.current(), matched.binding).state === "active") return matched;
|
|
932
919
|
}
|
|
933
920
|
/** Restricts a discussion side session's session tools to its attached main session. */
|
|
934
921
|
function enforceClickClackDiscussionToolTarget(params) {
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as setClickClackRuntime, r as parseClickClackTarget } from "./channel-
|
|
3
|
-
import { t as registerClickClackDiscussions } from "./runtime-api-
|
|
1
|
+
import { b as resolveClickClackAccount, u as createClickClackClient } from "./setup-surface-DjT-gmMe.js";
|
|
2
|
+
import { a as setClickClackRuntime, r as parseClickClackTarget } from "./channel-DaHgaTy1.js";
|
|
3
|
+
import { t as registerClickClackDiscussions } from "./runtime-api-CPL3m5AD.js";
|
|
4
4
|
export { createClickClackClient, parseClickClackTarget, registerClickClackDiscussions, resolveClickClackAccount, setClickClackRuntime };
|