@clawling/clawchat-plugin-openclaw 2026.5.13-dev.2 → 2026.5.14-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +7 -0
- package/dist/setup-entry.js +31 -1
- package/dist/src/api-client.js +18 -0
- package/dist/src/config-compat.js +120 -0
- package/dist/src/plugin-report.js +36 -0
- package/dist/src/reply-dispatcher.js +25 -19
- package/dist/src/runtime.js +47 -6
- package/index.ts +7 -0
- package/openclaw.plugin.json +7 -0
- package/package.json +1 -1
- package/setup-entry.ts +51 -1
- package/skills/clawchat/SKILL.md +13 -0
- package/src/api-client.ts +36 -0
- package/src/config-compat.ts +154 -0
- package/src/plugin-report.ts +53 -0
- package/src/reply-dispatcher.ts +26 -21
- package/src/runtime.ts +59 -4
package/dist/index.js
CHANGED
|
@@ -16,5 +16,12 @@ export default defineChannelPluginEntry({
|
|
|
16
16
|
registerOpenclawClawlingCommands(api);
|
|
17
17
|
registerClawChatPromptInjection(api);
|
|
18
18
|
registerOpenclawClawlingTools(api);
|
|
19
|
+
// NOTE: the legacy-rename config migration is intentionally NOT registered
|
|
20
|
+
// here. The host's setup-migration runner only loads a plugin's SETUP source
|
|
21
|
+
// (`openclaw.setupEntry`/`runtimeSetupEntry` → setup-entry.ts) and calls its
|
|
22
|
+
// `register(api)` in "setup-only" mode; registrations made in `registerFull`
|
|
23
|
+
// (full/tool-discovery modes) are never collected for migrations, and this
|
|
24
|
+
// gated full-load path doesn't run for the rename scenario anyway. The
|
|
25
|
+
// migration is wired into setup-entry.ts instead.
|
|
19
26
|
},
|
|
20
27
|
});
|
package/dist/setup-entry.js
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
1
|
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { CHANNEL_ID } from "./src/config.js";
|
|
2
3
|
import { openclawClawlingSetupPlugin } from "./src/channel.setup.js";
|
|
3
|
-
|
|
4
|
+
import { migrateLegacyClawChatChannelConfig } from "./src/config-compat.js";
|
|
5
|
+
/**
|
|
6
|
+
* Host setup-source `register` hook.
|
|
7
|
+
*
|
|
8
|
+
* The OpenClaw setup-migration runner (`setup-registry`) loads this module as
|
|
9
|
+
* the plugin's setup source (resolved from `package.json` `openclaw.setupEntry`
|
|
10
|
+
* / `runtimeSetupEntry`), unwraps the default export, and — when that export is
|
|
11
|
+
* an object exposing a `register` function whose `id` matches the plugin id —
|
|
12
|
+
* calls `register(api)` in setup-only mode. This is the ONLY ungated path that
|
|
13
|
+
* collects config migrations for the plugin-rename scenario.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the host's own built-in pattern (e.g. amazon-bedrock setup-api.js:
|
|
16
|
+
* `api.registerConfigMigration((config) => migrateAmazonBedrockLegacyConfig(config))`).
|
|
17
|
+
*/
|
|
18
|
+
export function register(api) {
|
|
19
|
+
api.registerConfigMigration?.((config) => migrateLegacyClawChatChannelConfig(config));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Default export consumed by BOTH host loaders of this setup source:
|
|
23
|
+
* - the channel-setup loader unwraps `default` and reads `.plugin`
|
|
24
|
+
* (`defineSetupPluginEntry` yields `{ plugin }`);
|
|
25
|
+
* - the setup-migration runner unwraps `default` and reads `.register`,
|
|
26
|
+
* rejecting it unless `.id` matches the plugin id.
|
|
27
|
+
* So the default export must carry `plugin`, `id`, and `register` together.
|
|
28
|
+
*/
|
|
29
|
+
export default {
|
|
30
|
+
...defineSetupPluginEntry(openclawClawlingSetupPlugin),
|
|
31
|
+
id: CHANNEL_ID,
|
|
32
|
+
register,
|
|
33
|
+
};
|
package/dist/src/api-client.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { ClawlingApiError, } from "./api-types.js";
|
|
2
2
|
import { CHANNEL_ID } from "./config.js";
|
|
3
|
+
export function buildPluginReportBody(input) {
|
|
4
|
+
return {
|
|
5
|
+
device_id: input.deviceId,
|
|
6
|
+
platform: input.platform,
|
|
7
|
+
plugin_version: input.pluginVersion,
|
|
8
|
+
runtime_name: input.runtimeName,
|
|
9
|
+
runtime_version: input.runtimeVersion,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* §A.0 — decode the access token's `exp` claim locally (base64url-decode the
|
|
5
14
|
* JWT payload segment, read `exp` as epoch seconds). Returns `null` when the
|
|
@@ -416,5 +425,14 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
416
425
|
fd.set("file", file);
|
|
417
426
|
return await call("POST", "/v1/files/upload-url", { body: fd });
|
|
418
427
|
},
|
|
428
|
+
async reportPlugin(input, opts) {
|
|
429
|
+
const path = opts?.authenticated
|
|
430
|
+
? "/v1/agents/me/plugin-report"
|
|
431
|
+
: "/v1/agents/plugin-report";
|
|
432
|
+
await call("POST", path, {
|
|
433
|
+
headers: { "content-type": "application/json" },
|
|
434
|
+
body: JSON.stringify(buildPluginReportBody(input)),
|
|
435
|
+
});
|
|
436
|
+
},
|
|
419
437
|
};
|
|
420
438
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { CHANNEL_ID } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compatibility migration for the plugin rename (commit 260044f): the plugin
|
|
4
|
+
* `id` and channel key changed from the OLD `openclaw-clawchat` to the NEW
|
|
5
|
+
* `clawchat-plugin-openclaw` (= {@link CHANNEL_ID}).
|
|
6
|
+
*
|
|
7
|
+
* Users upgrading from the old version have all their state — channel block
|
|
8
|
+
* (token/userId/ownerUserId/refreshToken/…), `plugins.allow`,
|
|
9
|
+
* `plugins.entries`, and `tools.allow`/`tools.alsoAllow` — keyed under the old
|
|
10
|
+
* id. The new code only reads the new id, so the channel silently fails to
|
|
11
|
+
* load. This migration moves the old-keyed state onto the new id.
|
|
12
|
+
*
|
|
13
|
+
* The function is PURE: it clones the input via `structuredClone` and never
|
|
14
|
+
* mutates its argument. It is IDEMPOTENT: a config with no old id anywhere
|
|
15
|
+
* returns an equivalent config with `changes: []`.
|
|
16
|
+
*/
|
|
17
|
+
export const LEGACY_CHANNEL_ID = "openclaw-clawchat";
|
|
18
|
+
export const TARGET_CHANNEL_ID = CHANNEL_ID;
|
|
19
|
+
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
20
|
+
function isBlockedObjectKey(key) {
|
|
21
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
22
|
+
}
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function isNonEmptyRecord(value) {
|
|
27
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Copy keys from `source` into `target` only when they are absent/undefined in
|
|
31
|
+
* `target` (target is more current — never overwrite an existing value).
|
|
32
|
+
* Recurses into nested records. Skips prototype-pollution keys.
|
|
33
|
+
*/
|
|
34
|
+
function mergeMissing(target, source) {
|
|
35
|
+
for (const [key, value] of Object.entries(source)) {
|
|
36
|
+
if (value === undefined || isBlockedObjectKey(key))
|
|
37
|
+
continue;
|
|
38
|
+
const existing = target[key];
|
|
39
|
+
if (existing === undefined) {
|
|
40
|
+
target[key] = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (isRecord(existing) && isRecord(value))
|
|
44
|
+
mergeMissing(existing, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Replace `from` → `to` in a string array, preserving order and deduping. */
|
|
48
|
+
function replaceAndDedup(list, from, to) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const raw of list) {
|
|
51
|
+
const value = raw === from ? to : raw;
|
|
52
|
+
if (!out.includes(value))
|
|
53
|
+
out.push(value);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export function migrateLegacyClawChatChannelConfig(config) {
|
|
58
|
+
const changes = [];
|
|
59
|
+
if (!isRecord(config))
|
|
60
|
+
return { config, changes };
|
|
61
|
+
const next = structuredClone(config);
|
|
62
|
+
// 1. channels — move/merge the old channel block onto the new id.
|
|
63
|
+
const channels = next.channels;
|
|
64
|
+
if (isRecord(channels)) {
|
|
65
|
+
const oldChannel = channels[LEGACY_CHANNEL_ID];
|
|
66
|
+
if (isNonEmptyRecord(oldChannel)) {
|
|
67
|
+
const newChannel = channels[TARGET_CHANNEL_ID];
|
|
68
|
+
if (!isNonEmptyRecord(newChannel)) {
|
|
69
|
+
channels[TARGET_CHANNEL_ID] = oldChannel;
|
|
70
|
+
changes.push(`Moved channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
mergeMissing(newChannel, oldChannel);
|
|
74
|
+
changes.push(`Merged channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}" (filled missing fields; kept existing values).`);
|
|
75
|
+
}
|
|
76
|
+
delete channels[LEGACY_CHANNEL_ID];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// plugins.* live under a single `plugins` record.
|
|
80
|
+
const plugins = next.plugins;
|
|
81
|
+
if (isRecord(plugins)) {
|
|
82
|
+
// 2. plugins.allow — replace old id with new id (append if missing), dedup.
|
|
83
|
+
const allow = plugins.allow;
|
|
84
|
+
if (Array.isArray(allow) && allow.includes(LEGACY_CHANNEL_ID)) {
|
|
85
|
+
const replaced = replaceAndDedup(allow.filter((v) => typeof v === "string"), LEGACY_CHANNEL_ID, TARGET_CHANNEL_ID);
|
|
86
|
+
plugins.allow = replaced;
|
|
87
|
+
changes.push(`Updated plugins.allow: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
|
|
88
|
+
}
|
|
89
|
+
// 3. plugins.entries — merge-missing the old entry into the new one.
|
|
90
|
+
const entries = plugins.entries;
|
|
91
|
+
if (isRecord(entries)) {
|
|
92
|
+
const oldEntry = entries[LEGACY_CHANNEL_ID];
|
|
93
|
+
if (oldEntry !== undefined) {
|
|
94
|
+
if (isRecord(oldEntry)) {
|
|
95
|
+
const newEntry = entries[TARGET_CHANNEL_ID];
|
|
96
|
+
if (!isRecord(newEntry)) {
|
|
97
|
+
entries[TARGET_CHANNEL_ID] = oldEntry;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
mergeMissing(newEntry, oldEntry);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
delete entries[LEGACY_CHANNEL_ID];
|
|
104
|
+
changes.push(`Merged plugins.entries["${LEGACY_CHANNEL_ID}"] → plugins.entries["${TARGET_CHANNEL_ID}"].`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 4. tools.allow + tools.alsoAllow — replace old id with new id, dedup.
|
|
109
|
+
const tools = next.tools;
|
|
110
|
+
if (isRecord(tools)) {
|
|
111
|
+
for (const key of ["allow", "alsoAllow"]) {
|
|
112
|
+
const list = tools[key];
|
|
113
|
+
if (Array.isArray(list) && list.includes(LEGACY_CHANNEL_ID)) {
|
|
114
|
+
tools[key] = replaceAndDedup(list.filter((v) => typeof v === "string"), LEGACY_CHANNEL_ID, TARGET_CHANNEL_ID);
|
|
115
|
+
changes.push(`Updated tools.${key}: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { config: next, changes };
|
|
120
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
3
|
+
/** Best-effort read of this plugin's package version; "unknown" on failure. */
|
|
4
|
+
export function resolvePluginVersion() {
|
|
5
|
+
try {
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const pkg = require("../package.json");
|
|
8
|
+
return pkg.version ?? "unknown";
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "unknown";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fire one plugin-version report. Best-effort: any failure is logged at debug
|
|
16
|
+
* and swallowed so it can never block, delay, or crash gateway startup.
|
|
17
|
+
*/
|
|
18
|
+
export async function reportPluginVersionSafe(p) {
|
|
19
|
+
try {
|
|
20
|
+
const client = createOpenclawClawlingApiClient({
|
|
21
|
+
baseUrl: p.baseUrl,
|
|
22
|
+
mediaBaseUrl: p.mediaBaseUrl,
|
|
23
|
+
token: p.token,
|
|
24
|
+
});
|
|
25
|
+
await client.reportPlugin({
|
|
26
|
+
deviceId: p.deviceId,
|
|
27
|
+
platform: "openclaw",
|
|
28
|
+
pluginVersion: p.pluginVersion,
|
|
29
|
+
runtimeName: "node",
|
|
30
|
+
runtimeVersion: process.version,
|
|
31
|
+
}, { authenticated: p.authenticated });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
p.log?.debug?.(`clawchat-plugin-openclaw plugin report failed (authenticated=${p.authenticated}): ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -259,7 +259,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
259
259
|
const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, log, } = options;
|
|
260
260
|
const isGroupTarget = target.chatType === "group";
|
|
261
261
|
const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
|
|
262
|
-
const splitFullOutput = outputVisibility === "full"
|
|
262
|
+
const splitFullOutput = outputVisibility === "full";
|
|
263
|
+
const splitNormalBlockOutput = outputVisibility === "normal";
|
|
263
264
|
const ownerDirectTarget = () => {
|
|
264
265
|
const ownerUserId = account.ownerUserId?.trim();
|
|
265
266
|
return ownerUserId ? { chatId: ownerUserId, chatType: "direct" } : null;
|
|
@@ -526,12 +527,12 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
526
527
|
return result;
|
|
527
528
|
};
|
|
528
529
|
const emitFullSegment = async (text, urls = []) => {
|
|
529
|
-
if (outputVisibility !== "full") {
|
|
530
|
+
if (outputVisibility !== "full" && !splitNormalBlockOutput) {
|
|
530
531
|
appendBufferedText(text);
|
|
531
532
|
appendBufferedUrls(urls);
|
|
532
533
|
return;
|
|
533
534
|
}
|
|
534
|
-
if (!splitFullOutput) {
|
|
535
|
+
if (!splitFullOutput && !splitNormalBlockOutput) {
|
|
535
536
|
appendBufferedText(text);
|
|
536
537
|
appendBufferedUrls(urls);
|
|
537
538
|
return;
|
|
@@ -599,6 +600,9 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
599
600
|
if (outputVisibility === "full") {
|
|
600
601
|
await emitFullSegment(text, urls);
|
|
601
602
|
}
|
|
603
|
+
else if (splitNormalBlockOutput) {
|
|
604
|
+
await emitFullSegment(text, urls);
|
|
605
|
+
}
|
|
602
606
|
else if (outputVisibility === "minimal" || outputVisibility === "normal") {
|
|
603
607
|
appendBufferedText(text);
|
|
604
608
|
appendBufferedUrls(urls);
|
|
@@ -630,6 +634,12 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
630
634
|
}
|
|
631
635
|
const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
|
|
632
636
|
const finalUrls = mergeFinalUrls(urls);
|
|
637
|
+
if (isClawChatNoopResponseText(finalText) &&
|
|
638
|
+
!richFragment &&
|
|
639
|
+
finalUrls.length === 0) {
|
|
640
|
+
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
633
643
|
const mediaFragments = await uploadMediaUrls(finalUrls);
|
|
634
644
|
const result = await sendStatic(finalText, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
|
|
635
645
|
if (result?.messageId)
|
|
@@ -641,12 +651,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
641
651
|
onError: (error, info) => {
|
|
642
652
|
const errorText = normalizeReplyErrorText(error);
|
|
643
653
|
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`);
|
|
644
|
-
if (
|
|
654
|
+
if (outputVisibility === "full")
|
|
645
655
|
void emitFullRuntimeText("error", errorText);
|
|
646
|
-
if (isGroupTarget) {
|
|
647
|
-
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
656
|
},
|
|
651
657
|
onIdle: async () => {
|
|
652
658
|
emitTyping(false);
|
|
@@ -673,10 +679,10 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
673
679
|
replyOptions: {
|
|
674
680
|
...base.replyOptions,
|
|
675
681
|
sourceReplyDeliveryMode: "automatic",
|
|
676
|
-
disableBlockStreaming:
|
|
682
|
+
disableBlockStreaming: !splitNormalBlockOutput,
|
|
677
683
|
suppressDefaultToolProgressMessages: true,
|
|
678
|
-
allowProgressCallbacksWhenSourceDeliverySuppressed:
|
|
679
|
-
onReasoningStream:
|
|
684
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: splitFullOutput ? true : undefined,
|
|
685
|
+
onReasoningStream: splitFullOutput
|
|
680
686
|
? async (payload) => {
|
|
681
687
|
if (consumeTerminalSend("reasoning"))
|
|
682
688
|
return;
|
|
@@ -687,14 +693,14 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
687
693
|
reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
688
694
|
}
|
|
689
695
|
: undefined,
|
|
690
|
-
onToolStart:
|
|
696
|
+
onToolStart: splitFullOutput
|
|
691
697
|
? async (payload) => {
|
|
692
698
|
if (consumeTerminalSend("tool-start"))
|
|
693
699
|
return;
|
|
694
700
|
await emitFullSegment(formatToolStartSummary(payload));
|
|
695
701
|
}
|
|
696
702
|
: undefined,
|
|
697
|
-
onToolResult:
|
|
703
|
+
onToolResult: splitFullOutput
|
|
698
704
|
? async (payload) => {
|
|
699
705
|
if (consumeTerminalSend("tool-result"))
|
|
700
706
|
return;
|
|
@@ -704,7 +710,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
704
710
|
await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
705
711
|
}
|
|
706
712
|
: undefined,
|
|
707
|
-
onItemEvent:
|
|
713
|
+
onItemEvent: splitFullOutput
|
|
708
714
|
? async (payload) => {
|
|
709
715
|
if (consumeTerminalSend("item-event"))
|
|
710
716
|
return;
|
|
@@ -713,35 +719,35 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
713
719
|
await emitFullRuntimeText("progress", summarizeProgressPayload(payload));
|
|
714
720
|
}
|
|
715
721
|
: undefined,
|
|
716
|
-
onPlanUpdate:
|
|
722
|
+
onPlanUpdate: splitFullOutput
|
|
717
723
|
? async (payload) => {
|
|
718
724
|
if (consumeTerminalSend("plan-update"))
|
|
719
725
|
return;
|
|
720
726
|
await emitFullRuntimeText("plan", summarizeProgressPayload(payload));
|
|
721
727
|
}
|
|
722
728
|
: undefined,
|
|
723
|
-
onCommandOutput:
|
|
729
|
+
onCommandOutput: splitFullOutput
|
|
724
730
|
? async (payload) => {
|
|
725
731
|
if (consumeTerminalSend("command-output"))
|
|
726
732
|
return;
|
|
727
733
|
await emitFullSegment(formatCommandOutputSummary(payload));
|
|
728
734
|
}
|
|
729
735
|
: undefined,
|
|
730
|
-
onPatchSummary:
|
|
736
|
+
onPatchSummary: splitFullOutput
|
|
731
737
|
? async (payload) => {
|
|
732
738
|
if (consumeTerminalSend("patch-summary"))
|
|
733
739
|
return;
|
|
734
740
|
await emitFullSegment(formatPatchSummary(payload));
|
|
735
741
|
}
|
|
736
742
|
: undefined,
|
|
737
|
-
onCompactionStart:
|
|
743
|
+
onCompactionStart: splitFullOutput
|
|
738
744
|
? async () => {
|
|
739
745
|
if (consumeTerminalSend("compaction-start"))
|
|
740
746
|
return;
|
|
741
747
|
await emitFullSegment("[compaction] started");
|
|
742
748
|
}
|
|
743
749
|
: undefined,
|
|
744
|
-
onCompactionEnd:
|
|
750
|
+
onCompactionEnd: splitFullOutput
|
|
745
751
|
? async () => {
|
|
746
752
|
if (consumeTerminalSend("compaction-end"))
|
|
747
753
|
return;
|
package/dist/src/runtime.js
CHANGED
|
@@ -2,8 +2,9 @@ import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError,
|
|
|
2
2
|
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
3
3
|
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
4
4
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
5
|
-
import { createOpenclawClawlingClient } from "./client.js";
|
|
5
|
+
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.js";
|
|
6
6
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
7
|
+
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
|
|
7
8
|
import { ClawlingApiError } from "./api-types.js";
|
|
8
9
|
import { RefreshManager } from "./refresh-manager.js";
|
|
9
10
|
import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, hasOpenclawClawlingConnectCredentials, } from "./config.js";
|
|
@@ -425,6 +426,19 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
425
426
|
const accountId = account.accountId;
|
|
426
427
|
const store = resolveConnectionStore(params, runtime);
|
|
427
428
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} hasOwnerUserId=${Boolean(account.ownerUserId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
|
|
429
|
+
// Freeze one report device_id for this process; reused for the paired report
|
|
430
|
+
// so the backend links both to the same row. Imported from ./client.ts.
|
|
431
|
+
const reportDeviceId = resolveOpenclawClawlingDeviceId(account);
|
|
432
|
+
const pluginVersion = resolvePluginVersion();
|
|
433
|
+
void reportPluginVersionSafe({
|
|
434
|
+
baseUrl: account.baseUrl,
|
|
435
|
+
mediaBaseUrl: account.mediaBaseUrl,
|
|
436
|
+
token: "",
|
|
437
|
+
deviceId: reportDeviceId,
|
|
438
|
+
pluginVersion,
|
|
439
|
+
authenticated: false,
|
|
440
|
+
log,
|
|
441
|
+
});
|
|
428
442
|
const activationAccount = await waitForActivationCredentials({
|
|
429
443
|
account,
|
|
430
444
|
abortSignal,
|
|
@@ -438,6 +452,17 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
438
452
|
if (!activationAccount)
|
|
439
453
|
return;
|
|
440
454
|
account = activationAccount.account;
|
|
455
|
+
// Paired: link the report row via the authenticated endpoint, reusing the
|
|
456
|
+
// SAME frozen device_id so the backend upserts the existing unpaired row.
|
|
457
|
+
void reportPluginVersionSafe({
|
|
458
|
+
baseUrl: account.baseUrl,
|
|
459
|
+
mediaBaseUrl: account.mediaBaseUrl,
|
|
460
|
+
token: account.token,
|
|
461
|
+
deviceId: reportDeviceId,
|
|
462
|
+
pluginVersion,
|
|
463
|
+
authenticated: true,
|
|
464
|
+
log,
|
|
465
|
+
});
|
|
441
466
|
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null when
|
|
442
467
|
// the credentials came from config (no activation row yet) — in that case the
|
|
443
468
|
// refresh manager relies on the JWT `exp` alone.
|
|
@@ -588,9 +613,18 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
588
613
|
// The refresh token is not part of the resolved account; source it from
|
|
589
614
|
// SQLite first (authoritative after a rotation) then the config channel
|
|
590
615
|
// section. Kept in a mutable cell so a swap updates it in place.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
616
|
+
// §0/§D — a rotated refresh token threaded across a re-enter wins over the
|
|
617
|
+
// stores. The config write that persists a rotation uses `afterWrite:{mode:
|
|
618
|
+
// "none"}`, so the in-memory `cfg` here is the STALE pre-rotation snapshot;
|
|
619
|
+
// re-reading it (config-sourced agents) would submit an already-consumed
|
|
620
|
+
// token on the next refresh → spurious 10003 auto-logout.
|
|
621
|
+
let latestRefreshToken = (typeof params.refreshTokenOverride === "string" && params.refreshTokenOverride.trim()
|
|
622
|
+
? params.refreshTokenOverride.trim()
|
|
623
|
+
: null) ??
|
|
624
|
+
(activationAccount.source === "sqlite" && store?.getActivationCredentials
|
|
625
|
+
? store.getActivationCredentials({ platform: "openclaw", accountId })?.refreshToken
|
|
626
|
+
: null) ??
|
|
627
|
+
readConfigRefreshToken(cfg);
|
|
594
628
|
const refreshManager = new RefreshManager({
|
|
595
629
|
baseUrl: account.baseUrl,
|
|
596
630
|
deviceId: refreshDeviceId,
|
|
@@ -639,6 +673,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
639
673
|
...(params.refreshSetTimer ? { setTimer: params.refreshSetTimer } : {}),
|
|
640
674
|
...(params.refreshClearTimer ? { clearTimer: params.refreshClearTimer } : {}),
|
|
641
675
|
...(params.refreshJitter ? { jitter: params.refreshJitter } : {}),
|
|
676
|
+
...(params.refreshNow ? { now: params.refreshNow } : {}),
|
|
642
677
|
log,
|
|
643
678
|
});
|
|
644
679
|
// §A.3/§A.4 — carry the single-flight latch + min-interval across re-enters so
|
|
@@ -939,6 +974,9 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
939
974
|
void startOpenclawClawlingGateway({
|
|
940
975
|
...params,
|
|
941
976
|
account: { ...params.account },
|
|
977
|
+
// Carry the live refresh token forward (a rotation earlier this lifecycle
|
|
978
|
+
// leaves the in-memory `cfg` stale; see refreshTokenOverride).
|
|
979
|
+
...(latestRefreshToken ? { refreshTokenOverride: latestRefreshToken } : {}),
|
|
942
980
|
transportBackoffReconnect: true,
|
|
943
981
|
refreshReconnectDepth: streak.depth,
|
|
944
982
|
refreshReconnectWindowStartedAt: streak.windowStartedAt,
|
|
@@ -976,8 +1014,10 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
976
1014
|
if (abortSignal.aborted)
|
|
977
1015
|
return;
|
|
978
1016
|
const streak = nextRefreshReconnectStreak();
|
|
979
|
-
// Re-enter with the rotated in-memory account
|
|
980
|
-
//
|
|
1017
|
+
// Re-enter with the rotated in-memory account. SQLite (if present) holds the
|
|
1018
|
+
// rotated pair, but the in-memory `cfg` is the STALE pre-rotation snapshot
|
|
1019
|
+
// (the config write used `afterWrite:{mode:"none"}`), so a config-sourced
|
|
1020
|
+
// agent must NOT re-read it — thread the live refresh token forward instead.
|
|
981
1021
|
await startOpenclawClawlingGateway({
|
|
982
1022
|
...params,
|
|
983
1023
|
account: {
|
|
@@ -987,6 +1027,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
987
1027
|
userId: account.userId,
|
|
988
1028
|
ownerUserId: account.ownerUserId,
|
|
989
1029
|
},
|
|
1030
|
+
...(latestRefreshToken ? { refreshTokenOverride: latestRefreshToken } : {}),
|
|
990
1031
|
refreshReconnectDepth: streak.depth,
|
|
991
1032
|
refreshReconnectWindowStartedAt: streak.windowStartedAt,
|
|
992
1033
|
refreshManagerState: managerState,
|
package/index.ts
CHANGED
|
@@ -20,5 +20,12 @@ export default defineChannelPluginEntry({
|
|
|
20
20
|
registerOpenclawClawlingCommands(api);
|
|
21
21
|
registerClawChatPromptInjection(api as unknown as ClawChatPromptInjectionApi);
|
|
22
22
|
registerOpenclawClawlingTools(api);
|
|
23
|
+
// NOTE: the legacy-rename config migration is intentionally NOT registered
|
|
24
|
+
// here. The host's setup-migration runner only loads a plugin's SETUP source
|
|
25
|
+
// (`openclaw.setupEntry`/`runtimeSetupEntry` → setup-entry.ts) and calls its
|
|
26
|
+
// `register(api)` in "setup-only" mode; registrations made in `registerFull`
|
|
27
|
+
// (full/tool-discovery modes) are never collected for migrations, and this
|
|
28
|
+
// gated full-load path doesn't run for the rename scenario anyway. The
|
|
29
|
+
// migration is wired into setup-entry.ts instead.
|
|
23
30
|
},
|
|
24
31
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "clawchat-plugin-openclaw",
|
|
3
3
|
"kind": "channel",
|
|
4
|
+
"legacyPluginIds": ["openclaw-clawchat"],
|
|
5
|
+
"configContracts": {
|
|
6
|
+
"compatibilityMigrationPaths": [
|
|
7
|
+
"channels.openclaw-clawchat",
|
|
8
|
+
"plugins.entries.openclaw-clawchat"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
"channels": ["clawchat-plugin-openclaw"],
|
|
5
12
|
"skills": ["./skills"],
|
|
6
13
|
"activation": {
|
package/package.json
CHANGED
package/setup-entry.ts
CHANGED
|
@@ -1,4 +1,54 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
1
2
|
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
3
|
+
import { CHANNEL_ID } from "./src/config.ts";
|
|
2
4
|
import { openclawClawlingSetupPlugin } from "./src/channel.setup.ts";
|
|
5
|
+
import { migrateLegacyClawChatChannelConfig } from "./src/config-compat.ts";
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Minimal shape of the host setup-registration api we touch. The host calls
|
|
9
|
+
* `register(api)` on the SETUP source (this module) in `registrationMode:
|
|
10
|
+
* "setup-only"`; `registerConfigMigration` is the hook that collects
|
|
11
|
+
* compatibility migrations (run by `openclaw doctor` / setup). It is optional
|
|
12
|
+
* here so a host that doesn't expose it can't throw.
|
|
13
|
+
*/
|
|
14
|
+
interface SetupRegisterApi {
|
|
15
|
+
registerConfigMigration?: (
|
|
16
|
+
migration: (config: OpenClawConfig) => {
|
|
17
|
+
config: OpenClawConfig;
|
|
18
|
+
changes: string[];
|
|
19
|
+
},
|
|
20
|
+
) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Host setup-source `register` hook.
|
|
25
|
+
*
|
|
26
|
+
* The OpenClaw setup-migration runner (`setup-registry`) loads this module as
|
|
27
|
+
* the plugin's setup source (resolved from `package.json` `openclaw.setupEntry`
|
|
28
|
+
* / `runtimeSetupEntry`), unwraps the default export, and — when that export is
|
|
29
|
+
* an object exposing a `register` function whose `id` matches the plugin id —
|
|
30
|
+
* calls `register(api)` in setup-only mode. This is the ONLY ungated path that
|
|
31
|
+
* collects config migrations for the plugin-rename scenario.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors the host's own built-in pattern (e.g. amazon-bedrock setup-api.js:
|
|
34
|
+
* `api.registerConfigMigration((config) => migrateAmazonBedrockLegacyConfig(config))`).
|
|
35
|
+
*/
|
|
36
|
+
export function register(api: SetupRegisterApi): void {
|
|
37
|
+
api.registerConfigMigration?.((config) =>
|
|
38
|
+
migrateLegacyClawChatChannelConfig(config),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default export consumed by BOTH host loaders of this setup source:
|
|
44
|
+
* - the channel-setup loader unwraps `default` and reads `.plugin`
|
|
45
|
+
* (`defineSetupPluginEntry` yields `{ plugin }`);
|
|
46
|
+
* - the setup-migration runner unwraps `default` and reads `.register`,
|
|
47
|
+
* rejecting it unless `.id` matches the plugin id.
|
|
48
|
+
* So the default export must carry `plugin`, `id`, and `register` together.
|
|
49
|
+
*/
|
|
50
|
+
export default {
|
|
51
|
+
...defineSetupPluginEntry(openclawClawlingSetupPlugin),
|
|
52
|
+
id: CHANNEL_ID,
|
|
53
|
+
register,
|
|
54
|
+
};
|
package/skills/clawchat/SKILL.md
CHANGED
|
@@ -13,6 +13,7 @@ This skill guides agent behavior for ClawChat-aware tasks. Use the registered Cl
|
|
|
13
13
|
|
|
14
14
|
- Use registered ClawChat plugin tools for account/profile, friends, users, moments, comments, reactions, avatar, media, and read-only conversation lookup.
|
|
15
15
|
- If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin.
|
|
16
|
+
- Use the `/clawchat-output` slash command when the user asks to change how much ClawChat runtime output is shown in the current conversation.
|
|
16
17
|
|
|
17
18
|
## OpenClaw CLI
|
|
18
19
|
|
|
@@ -30,6 +31,18 @@ Use `update --force` only when local ClawChat plugin or skill files look corrupt
|
|
|
30
31
|
|
|
31
32
|
If `channels add` reports `Unknown channel: clawchat-plugin-openclaw`, use the runtime slash command `/clawchat-activate CODE` after the operator ensures the plugin is loaded.
|
|
32
33
|
|
|
34
|
+
## Output Visibility
|
|
35
|
+
|
|
36
|
+
When the user asks to change ClawChat output verbosity, use the runtime slash command for the current conversation. Treat natural-language wording as aliases for the three supported modes:
|
|
37
|
+
|
|
38
|
+
| User wording | Command |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| quiet mode, silent mode, minimal output, final-only output, `minimal` | `/clawchat-output minimal` |
|
|
41
|
+
| conversation mode, normal mode, regular mode, default output, `normal` | `/clawchat-output normal` |
|
|
42
|
+
| dev mode, developer mode, verbose mode, full output, `full` | `/clawchat-output full` |
|
|
43
|
+
|
|
44
|
+
Do not edit config files directly for this request. If the slash command returns an error, report that error instead of claiming the mode changed.
|
|
45
|
+
|
|
33
46
|
## Plugin Tool Routing
|
|
34
47
|
|
|
35
48
|
Tool descriptions are authoritative. These routing hints resolve common ambiguity:
|
package/src/api-client.ts
CHANGED
|
@@ -17,6 +17,24 @@ import {
|
|
|
17
17
|
} from "./api-types.ts";
|
|
18
18
|
import { CHANNEL_ID } from "./config.ts";
|
|
19
19
|
|
|
20
|
+
export interface PluginReportInput {
|
|
21
|
+
deviceId: string;
|
|
22
|
+
platform: string;
|
|
23
|
+
pluginVersion: string;
|
|
24
|
+
runtimeName: string;
|
|
25
|
+
runtimeVersion: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildPluginReportBody(input: PluginReportInput): Record<string, string> {
|
|
29
|
+
return {
|
|
30
|
+
device_id: input.deviceId,
|
|
31
|
+
platform: input.platform,
|
|
32
|
+
plugin_version: input.pluginVersion,
|
|
33
|
+
runtime_name: input.runtimeName,
|
|
34
|
+
runtime_version: input.runtimeVersion,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export interface ApiClientOptions {
|
|
21
39
|
baseUrl: string;
|
|
22
40
|
token: string;
|
|
@@ -131,6 +149,15 @@ export interface OpenclawClawlingApiClient {
|
|
|
131
149
|
filename: string;
|
|
132
150
|
mime?: string;
|
|
133
151
|
}): Promise<AvatarUploadResult>;
|
|
152
|
+
/**
|
|
153
|
+
* Report this plugin's version + runtime to member-backend. When
|
|
154
|
+
* `authenticated`, posts to the agent-JWT self-report endpoint (links the row
|
|
155
|
+
* to the caller's agent/owner); otherwise the public unpaired endpoint.
|
|
156
|
+
*/
|
|
157
|
+
reportPlugin(
|
|
158
|
+
input: PluginReportInput,
|
|
159
|
+
opts?: { authenticated?: boolean },
|
|
160
|
+
): Promise<void>;
|
|
134
161
|
}
|
|
135
162
|
|
|
136
163
|
/** Backend envelope codes for `POST /v1/auth/refresh` (§0). */
|
|
@@ -615,5 +642,14 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
615
642
|
fd.set("file", file);
|
|
616
643
|
return await call<AvatarUploadResult>("POST", "/v1/files/upload-url", { body: fd });
|
|
617
644
|
},
|
|
645
|
+
async reportPlugin(input, opts): Promise<void> {
|
|
646
|
+
const path = opts?.authenticated
|
|
647
|
+
? "/v1/agents/me/plugin-report"
|
|
648
|
+
: "/v1/agents/plugin-report";
|
|
649
|
+
await call<unknown>("POST", path, {
|
|
650
|
+
headers: { "content-type": "application/json" },
|
|
651
|
+
body: JSON.stringify(buildPluginReportBody(input)),
|
|
652
|
+
});
|
|
653
|
+
},
|
|
618
654
|
};
|
|
619
655
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { CHANNEL_ID } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compatibility migration for the plugin rename (commit 260044f): the plugin
|
|
6
|
+
* `id` and channel key changed from the OLD `openclaw-clawchat` to the NEW
|
|
7
|
+
* `clawchat-plugin-openclaw` (= {@link CHANNEL_ID}).
|
|
8
|
+
*
|
|
9
|
+
* Users upgrading from the old version have all their state — channel block
|
|
10
|
+
* (token/userId/ownerUserId/refreshToken/…), `plugins.allow`,
|
|
11
|
+
* `plugins.entries`, and `tools.allow`/`tools.alsoAllow` — keyed under the old
|
|
12
|
+
* id. The new code only reads the new id, so the channel silently fails to
|
|
13
|
+
* load. This migration moves the old-keyed state onto the new id.
|
|
14
|
+
*
|
|
15
|
+
* The function is PURE: it clones the input via `structuredClone` and never
|
|
16
|
+
* mutates its argument. It is IDEMPOTENT: a config with no old id anywhere
|
|
17
|
+
* returns an equivalent config with `changes: []`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const LEGACY_CHANNEL_ID = "openclaw-clawchat" as const;
|
|
21
|
+
export const TARGET_CHANNEL_ID = CHANNEL_ID;
|
|
22
|
+
|
|
23
|
+
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
24
|
+
|
|
25
|
+
function isBlockedObjectKey(key: string): boolean {
|
|
26
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isNonEmptyRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Copy keys from `source` into `target` only when they are absent/undefined in
|
|
39
|
+
* `target` (target is more current — never overwrite an existing value).
|
|
40
|
+
* Recurses into nested records. Skips prototype-pollution keys.
|
|
41
|
+
*/
|
|
42
|
+
function mergeMissing(
|
|
43
|
+
target: Record<string, unknown>,
|
|
44
|
+
source: Record<string, unknown>,
|
|
45
|
+
): void {
|
|
46
|
+
for (const [key, value] of Object.entries(source)) {
|
|
47
|
+
if (value === undefined || isBlockedObjectKey(key)) continue;
|
|
48
|
+
const existing = target[key];
|
|
49
|
+
if (existing === undefined) {
|
|
50
|
+
target[key] = value;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (isRecord(existing) && isRecord(value)) mergeMissing(existing, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Replace `from` → `to` in a string array, preserving order and deduping. */
|
|
58
|
+
function replaceAndDedup(list: string[], from: string, to: string): string[] {
|
|
59
|
+
const out: string[] = [];
|
|
60
|
+
for (const raw of list) {
|
|
61
|
+
const value = raw === from ? to : raw;
|
|
62
|
+
if (!out.includes(value)) out.push(value);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function migrateLegacyClawChatChannelConfig(config: OpenClawConfig): {
|
|
68
|
+
config: OpenClawConfig;
|
|
69
|
+
changes: string[];
|
|
70
|
+
} {
|
|
71
|
+
const changes: string[] = [];
|
|
72
|
+
if (!isRecord(config)) return { config, changes };
|
|
73
|
+
|
|
74
|
+
const next = structuredClone(config) as Record<string, unknown>;
|
|
75
|
+
|
|
76
|
+
// 1. channels — move/merge the old channel block onto the new id.
|
|
77
|
+
const channels = next.channels;
|
|
78
|
+
if (isRecord(channels)) {
|
|
79
|
+
const oldChannel = channels[LEGACY_CHANNEL_ID];
|
|
80
|
+
if (isNonEmptyRecord(oldChannel)) {
|
|
81
|
+
const newChannel = channels[TARGET_CHANNEL_ID];
|
|
82
|
+
if (!isNonEmptyRecord(newChannel)) {
|
|
83
|
+
channels[TARGET_CHANNEL_ID] = oldChannel;
|
|
84
|
+
changes.push(
|
|
85
|
+
`Moved channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
mergeMissing(newChannel, oldChannel);
|
|
89
|
+
changes.push(
|
|
90
|
+
`Merged channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}" (filled missing fields; kept existing values).`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
delete channels[LEGACY_CHANNEL_ID];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// plugins.* live under a single `plugins` record.
|
|
98
|
+
const plugins = next.plugins;
|
|
99
|
+
if (isRecord(plugins)) {
|
|
100
|
+
// 2. plugins.allow — replace old id with new id (append if missing), dedup.
|
|
101
|
+
const allow = plugins.allow;
|
|
102
|
+
if (Array.isArray(allow) && allow.includes(LEGACY_CHANNEL_ID)) {
|
|
103
|
+
const replaced = replaceAndDedup(
|
|
104
|
+
allow.filter((v): v is string => typeof v === "string"),
|
|
105
|
+
LEGACY_CHANNEL_ID,
|
|
106
|
+
TARGET_CHANNEL_ID,
|
|
107
|
+
);
|
|
108
|
+
plugins.allow = replaced;
|
|
109
|
+
changes.push(
|
|
110
|
+
`Updated plugins.allow: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. plugins.entries — merge-missing the old entry into the new one.
|
|
115
|
+
const entries = plugins.entries;
|
|
116
|
+
if (isRecord(entries)) {
|
|
117
|
+
const oldEntry = entries[LEGACY_CHANNEL_ID];
|
|
118
|
+
if (oldEntry !== undefined) {
|
|
119
|
+
if (isRecord(oldEntry)) {
|
|
120
|
+
const newEntry = entries[TARGET_CHANNEL_ID];
|
|
121
|
+
if (!isRecord(newEntry)) {
|
|
122
|
+
entries[TARGET_CHANNEL_ID] = oldEntry;
|
|
123
|
+
} else {
|
|
124
|
+
mergeMissing(newEntry, oldEntry);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
delete entries[LEGACY_CHANNEL_ID];
|
|
128
|
+
changes.push(
|
|
129
|
+
`Merged plugins.entries["${LEGACY_CHANNEL_ID}"] → plugins.entries["${TARGET_CHANNEL_ID}"].`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 4. tools.allow + tools.alsoAllow — replace old id with new id, dedup.
|
|
136
|
+
const tools = next.tools;
|
|
137
|
+
if (isRecord(tools)) {
|
|
138
|
+
for (const key of ["allow", "alsoAllow"] as const) {
|
|
139
|
+
const list = tools[key];
|
|
140
|
+
if (Array.isArray(list) && list.includes(LEGACY_CHANNEL_ID)) {
|
|
141
|
+
tools[key] = replaceAndDedup(
|
|
142
|
+
list.filter((v): v is string => typeof v === "string"),
|
|
143
|
+
LEGACY_CHANNEL_ID,
|
|
144
|
+
TARGET_CHANNEL_ID,
|
|
145
|
+
);
|
|
146
|
+
changes.push(
|
|
147
|
+
`Updated tools.${key}: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { config: next as OpenClawConfig, changes };
|
|
154
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
3
|
+
|
|
4
|
+
/** Best-effort read of this plugin's package version; "unknown" on failure. */
|
|
5
|
+
export function resolvePluginVersion(): string {
|
|
6
|
+
try {
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pkg = require("../package.json") as { version?: string };
|
|
9
|
+
return pkg.version ?? "unknown";
|
|
10
|
+
} catch {
|
|
11
|
+
return "unknown";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReportParams {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
mediaBaseUrl: string;
|
|
18
|
+
token: string;
|
|
19
|
+
deviceId: string;
|
|
20
|
+
pluginVersion: string;
|
|
21
|
+
authenticated: boolean;
|
|
22
|
+
log?: { debug?: (msg: string) => void };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fire one plugin-version report. Best-effort: any failure is logged at debug
|
|
27
|
+
* and swallowed so it can never block, delay, or crash gateway startup.
|
|
28
|
+
*/
|
|
29
|
+
export async function reportPluginVersionSafe(p: ReportParams): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
const client = createOpenclawClawlingApiClient({
|
|
32
|
+
baseUrl: p.baseUrl,
|
|
33
|
+
mediaBaseUrl: p.mediaBaseUrl,
|
|
34
|
+
token: p.token,
|
|
35
|
+
});
|
|
36
|
+
await client.reportPlugin(
|
|
37
|
+
{
|
|
38
|
+
deviceId: p.deviceId,
|
|
39
|
+
platform: "openclaw",
|
|
40
|
+
pluginVersion: p.pluginVersion,
|
|
41
|
+
runtimeName: "node",
|
|
42
|
+
runtimeVersion: process.version,
|
|
43
|
+
},
|
|
44
|
+
{ authenticated: p.authenticated },
|
|
45
|
+
);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
p.log?.debug?.(
|
|
48
|
+
`clawchat-plugin-openclaw plugin report failed (authenticated=${p.authenticated}): ${
|
|
49
|
+
err instanceof Error ? err.message : String(err)
|
|
50
|
+
}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -389,7 +389,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
389
389
|
} = options;
|
|
390
390
|
const isGroupTarget = target.chatType === "group";
|
|
391
391
|
const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
|
|
392
|
-
const splitFullOutput = outputVisibility === "full"
|
|
392
|
+
const splitFullOutput = outputVisibility === "full";
|
|
393
|
+
const splitNormalBlockOutput = outputVisibility === "normal";
|
|
393
394
|
const ownerDirectTarget = () => {
|
|
394
395
|
const ownerUserId = account.ownerUserId?.trim();
|
|
395
396
|
return ownerUserId ? { chatId: ownerUserId, chatType: "direct" as const } : null;
|
|
@@ -673,12 +674,12 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
673
674
|
};
|
|
674
675
|
|
|
675
676
|
const emitFullSegment = async (text: string, urls: string[] = []): Promise<void> => {
|
|
676
|
-
if (outputVisibility !== "full") {
|
|
677
|
+
if (outputVisibility !== "full" && !splitNormalBlockOutput) {
|
|
677
678
|
appendBufferedText(text);
|
|
678
679
|
appendBufferedUrls(urls);
|
|
679
680
|
return;
|
|
680
681
|
}
|
|
681
|
-
if (!splitFullOutput) {
|
|
682
|
+
if (!splitFullOutput && !splitNormalBlockOutput) {
|
|
682
683
|
appendBufferedText(text);
|
|
683
684
|
appendBufferedUrls(urls);
|
|
684
685
|
return;
|
|
@@ -753,6 +754,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
753
754
|
if (info?.kind === "block") {
|
|
754
755
|
if (outputVisibility === "full") {
|
|
755
756
|
await emitFullSegment(text, urls);
|
|
757
|
+
} else if (splitNormalBlockOutput) {
|
|
758
|
+
await emitFullSegment(text, urls);
|
|
756
759
|
} else if (outputVisibility === "minimal" || outputVisibility === "normal") {
|
|
757
760
|
appendBufferedText(text);
|
|
758
761
|
appendBufferedUrls(urls);
|
|
@@ -785,6 +788,14 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
785
788
|
}
|
|
786
789
|
const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
|
|
787
790
|
const finalUrls = mergeFinalUrls(urls);
|
|
791
|
+
if (
|
|
792
|
+
isClawChatNoopResponseText(finalText) &&
|
|
793
|
+
!richFragment &&
|
|
794
|
+
finalUrls.length === 0
|
|
795
|
+
) {
|
|
796
|
+
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
788
799
|
const mediaFragments = await uploadMediaUrls(finalUrls);
|
|
789
800
|
const result = await sendStatic(
|
|
790
801
|
finalText,
|
|
@@ -803,13 +814,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
803
814
|
log?.error?.(
|
|
804
815
|
`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`,
|
|
805
816
|
);
|
|
806
|
-
if (
|
|
807
|
-
if (isGroupTarget) {
|
|
808
|
-
log?.error?.(
|
|
809
|
-
`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`,
|
|
810
|
-
);
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
817
|
+
if (outputVisibility === "full") void emitFullRuntimeText("error", errorText);
|
|
813
818
|
},
|
|
814
819
|
onIdle: async () => {
|
|
815
820
|
emitTyping(false);
|
|
@@ -833,10 +838,10 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
833
838
|
replyOptions: {
|
|
834
839
|
...base.replyOptions,
|
|
835
840
|
sourceReplyDeliveryMode: "automatic",
|
|
836
|
-
disableBlockStreaming:
|
|
841
|
+
disableBlockStreaming: !splitNormalBlockOutput,
|
|
837
842
|
suppressDefaultToolProgressMessages: true,
|
|
838
|
-
allowProgressCallbacksWhenSourceDeliverySuppressed:
|
|
839
|
-
onReasoningStream:
|
|
843
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: splitFullOutput ? true : undefined,
|
|
844
|
+
onReasoningStream: splitFullOutput
|
|
840
845
|
? async (payload: ReplyPayload) => {
|
|
841
846
|
if (consumeTerminalSend("reasoning")) return;
|
|
842
847
|
const text = resolvePayloadText(payload);
|
|
@@ -845,13 +850,13 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
845
850
|
if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
846
851
|
}
|
|
847
852
|
: undefined,
|
|
848
|
-
onToolStart:
|
|
853
|
+
onToolStart: splitFullOutput
|
|
849
854
|
? async (payload) => {
|
|
850
855
|
if (consumeTerminalSend("tool-start")) return;
|
|
851
856
|
await emitFullSegment(formatToolStartSummary(payload));
|
|
852
857
|
}
|
|
853
858
|
: undefined,
|
|
854
|
-
onToolResult:
|
|
859
|
+
onToolResult: splitFullOutput
|
|
855
860
|
? async (payload: ReplyPayload) => {
|
|
856
861
|
if (consumeTerminalSend("tool-result")) return;
|
|
857
862
|
const text = resolvePayloadText(payload);
|
|
@@ -859,38 +864,38 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
859
864
|
await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
860
865
|
}
|
|
861
866
|
: undefined,
|
|
862
|
-
onItemEvent:
|
|
867
|
+
onItemEvent: splitFullOutput
|
|
863
868
|
? async (payload: Record<string, unknown>) => {
|
|
864
869
|
if (consumeTerminalSend("item-event")) return;
|
|
865
870
|
if (isToolProgressItem(payload)) return;
|
|
866
871
|
await emitFullRuntimeText("progress", summarizeProgressPayload(payload));
|
|
867
872
|
}
|
|
868
873
|
: undefined,
|
|
869
|
-
onPlanUpdate:
|
|
874
|
+
onPlanUpdate: splitFullOutput
|
|
870
875
|
? async (payload: Record<string, unknown>) => {
|
|
871
876
|
if (consumeTerminalSend("plan-update")) return;
|
|
872
877
|
await emitFullRuntimeText("plan", summarizeProgressPayload(payload));
|
|
873
878
|
}
|
|
874
879
|
: undefined,
|
|
875
|
-
onCommandOutput:
|
|
880
|
+
onCommandOutput: splitFullOutput
|
|
876
881
|
? async (payload: Record<string, unknown>) => {
|
|
877
882
|
if (consumeTerminalSend("command-output")) return;
|
|
878
883
|
await emitFullSegment(formatCommandOutputSummary(payload));
|
|
879
884
|
}
|
|
880
885
|
: undefined,
|
|
881
|
-
onPatchSummary:
|
|
886
|
+
onPatchSummary: splitFullOutput
|
|
882
887
|
? async (payload: Record<string, unknown>) => {
|
|
883
888
|
if (consumeTerminalSend("patch-summary")) return;
|
|
884
889
|
await emitFullSegment(formatPatchSummary(payload));
|
|
885
890
|
}
|
|
886
891
|
: undefined,
|
|
887
|
-
onCompactionStart:
|
|
892
|
+
onCompactionStart: splitFullOutput
|
|
888
893
|
? async () => {
|
|
889
894
|
if (consumeTerminalSend("compaction-start")) return;
|
|
890
895
|
await emitFullSegment("[compaction] started");
|
|
891
896
|
}
|
|
892
897
|
: undefined,
|
|
893
|
-
onCompactionEnd:
|
|
898
|
+
onCompactionEnd: splitFullOutput
|
|
894
899
|
? async () => {
|
|
895
900
|
if (consumeTerminalSend("compaction-end")) return;
|
|
896
901
|
await emitFullSegment("[compaction] finished");
|
package/src/runtime.ts
CHANGED
|
@@ -13,8 +13,9 @@ import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
|
13
13
|
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
14
14
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
15
15
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
16
|
-
import { createOpenclawClawlingClient } from "./client.ts";
|
|
16
|
+
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.ts";
|
|
17
17
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
18
|
+
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
18
19
|
import { ClawlingApiError } from "./api-types.ts";
|
|
19
20
|
import { RefreshManager } from "./refresh-manager.ts";
|
|
20
21
|
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
@@ -487,6 +488,20 @@ export interface StartGatewayParams {
|
|
|
487
488
|
* of resetting on every fresh `RefreshManager`.
|
|
488
489
|
*/
|
|
489
490
|
refreshManagerState?: { rejectedToken: string | null; lastAttemptAt: number };
|
|
491
|
+
/**
|
|
492
|
+
* Internal — the live (possibly rotated) refresh token, carried across a
|
|
493
|
+
* §D/§B gateway re-enter. A rotation persists config with `afterWrite:{mode:
|
|
494
|
+
* "none"}`, so the in-memory `cfg` handed to the re-enter is NOT reloaded and
|
|
495
|
+
* still holds the pre-rotation refresh token. For a "configured" (config-
|
|
496
|
+
* sourced) agent the re-enter would otherwise re-read that stale token and
|
|
497
|
+
* submit an already-consumed refresh token on the next refresh → a spurious
|
|
498
|
+
* `10003` auto-logout. Threading the live token forward closes that gap.
|
|
499
|
+
* SQLite-sourced agents are unaffected (their row already holds the rotated
|
|
500
|
+
* pair), but threading is harmless there too.
|
|
501
|
+
*/
|
|
502
|
+
refreshTokenOverride?: string;
|
|
503
|
+
/** Test hook only — clock override for the refresh manager (min-interval / proactive timer). */
|
|
504
|
+
refreshNow?: () => number;
|
|
490
505
|
/**
|
|
491
506
|
* Internal — set when the current attempt is a plain transport-backoff
|
|
492
507
|
* re-enter (transient/skipped reactive refresh). Carries the current (unchanged)
|
|
@@ -641,6 +656,19 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
641
656
|
log?.info?.(
|
|
642
657
|
`[${accountId}] clawchat-plugin-openclaw runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} hasOwnerUserId=${Boolean(account.ownerUserId)} websocketUrl=${account.websocketUrl || "(empty)"}`,
|
|
643
658
|
);
|
|
659
|
+
// Freeze one report device_id for this process; reused for the paired report
|
|
660
|
+
// so the backend links both to the same row. Imported from ./client.ts.
|
|
661
|
+
const reportDeviceId = resolveOpenclawClawlingDeviceId(account);
|
|
662
|
+
const pluginVersion = resolvePluginVersion();
|
|
663
|
+
void reportPluginVersionSafe({
|
|
664
|
+
baseUrl: account.baseUrl,
|
|
665
|
+
mediaBaseUrl: account.mediaBaseUrl,
|
|
666
|
+
token: "",
|
|
667
|
+
deviceId: reportDeviceId,
|
|
668
|
+
pluginVersion,
|
|
669
|
+
authenticated: false,
|
|
670
|
+
log,
|
|
671
|
+
});
|
|
644
672
|
const activationAccount = await waitForActivationCredentials({
|
|
645
673
|
account,
|
|
646
674
|
abortSignal,
|
|
@@ -653,6 +681,17 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
653
681
|
});
|
|
654
682
|
if (!activationAccount) return;
|
|
655
683
|
account = activationAccount.account;
|
|
684
|
+
// Paired: link the report row via the authenticated endpoint, reusing the
|
|
685
|
+
// SAME frozen device_id so the backend upserts the existing unpaired row.
|
|
686
|
+
void reportPluginVersionSafe({
|
|
687
|
+
baseUrl: account.baseUrl,
|
|
688
|
+
mediaBaseUrl: account.mediaBaseUrl,
|
|
689
|
+
token: account.token,
|
|
690
|
+
deviceId: reportDeviceId,
|
|
691
|
+
pluginVersion,
|
|
692
|
+
authenticated: true,
|
|
693
|
+
log,
|
|
694
|
+
});
|
|
656
695
|
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null when
|
|
657
696
|
// the credentials came from config (no activation row yet) — in that case the
|
|
658
697
|
// refresh manager relies on the JWT `exp` alone.
|
|
@@ -828,10 +867,19 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
828
867
|
// The refresh token is not part of the resolved account; source it from
|
|
829
868
|
// SQLite first (authoritative after a rotation) then the config channel
|
|
830
869
|
// section. Kept in a mutable cell so a swap updates it in place.
|
|
870
|
+
// §0/§D — a rotated refresh token threaded across a re-enter wins over the
|
|
871
|
+
// stores. The config write that persists a rotation uses `afterWrite:{mode:
|
|
872
|
+
// "none"}`, so the in-memory `cfg` here is the STALE pre-rotation snapshot;
|
|
873
|
+
// re-reading it (config-sourced agents) would submit an already-consumed
|
|
874
|
+
// token on the next refresh → spurious 10003 auto-logout.
|
|
831
875
|
let latestRefreshToken: string | null =
|
|
876
|
+
(typeof params.refreshTokenOverride === "string" && params.refreshTokenOverride.trim()
|
|
877
|
+
? params.refreshTokenOverride.trim()
|
|
878
|
+
: null) ??
|
|
832
879
|
(activationAccount.source === "sqlite" && store?.getActivationCredentials
|
|
833
880
|
? store.getActivationCredentials({ platform: "openclaw", accountId })?.refreshToken
|
|
834
|
-
: null) ??
|
|
881
|
+
: null) ??
|
|
882
|
+
readConfigRefreshToken(cfg);
|
|
835
883
|
|
|
836
884
|
const refreshManager = new RefreshManager({
|
|
837
885
|
baseUrl: account.baseUrl,
|
|
@@ -881,6 +929,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
881
929
|
...(params.refreshSetTimer ? { setTimer: params.refreshSetTimer } : {}),
|
|
882
930
|
...(params.refreshClearTimer ? { clearTimer: params.refreshClearTimer } : {}),
|
|
883
931
|
...(params.refreshJitter ? { jitter: params.refreshJitter } : {}),
|
|
932
|
+
...(params.refreshNow ? { now: params.refreshNow } : {}),
|
|
884
933
|
log,
|
|
885
934
|
});
|
|
886
935
|
// §A.3/§A.4 — carry the single-flight latch + min-interval across re-enters so
|
|
@@ -1216,6 +1265,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1216
1265
|
void startOpenclawClawlingGateway({
|
|
1217
1266
|
...params,
|
|
1218
1267
|
account: { ...params.account },
|
|
1268
|
+
// Carry the live refresh token forward (a rotation earlier this lifecycle
|
|
1269
|
+
// leaves the in-memory `cfg` stale; see refreshTokenOverride).
|
|
1270
|
+
...(latestRefreshToken ? { refreshTokenOverride: latestRefreshToken } : {}),
|
|
1219
1271
|
transportBackoffReconnect: true,
|
|
1220
1272
|
refreshReconnectDepth: streak.depth,
|
|
1221
1273
|
refreshReconnectWindowStartedAt: streak.windowStartedAt,
|
|
@@ -1253,8 +1305,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1253
1305
|
}
|
|
1254
1306
|
if (abortSignal.aborted) return;
|
|
1255
1307
|
const streak = nextRefreshReconnectStreak();
|
|
1256
|
-
// Re-enter with the rotated in-memory account
|
|
1257
|
-
//
|
|
1308
|
+
// Re-enter with the rotated in-memory account. SQLite (if present) holds the
|
|
1309
|
+
// rotated pair, but the in-memory `cfg` is the STALE pre-rotation snapshot
|
|
1310
|
+
// (the config write used `afterWrite:{mode:"none"}`), so a config-sourced
|
|
1311
|
+
// agent must NOT re-read it — thread the live refresh token forward instead.
|
|
1258
1312
|
await startOpenclawClawlingGateway({
|
|
1259
1313
|
...params,
|
|
1260
1314
|
account: {
|
|
@@ -1264,6 +1318,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1264
1318
|
userId: account.userId,
|
|
1265
1319
|
ownerUserId: account.ownerUserId,
|
|
1266
1320
|
},
|
|
1321
|
+
...(latestRefreshToken ? { refreshTokenOverride: latestRefreshToken } : {}),
|
|
1267
1322
|
refreshReconnectDepth: streak.depth,
|
|
1268
1323
|
refreshReconnectWindowStartedAt: streak.windowStartedAt,
|
|
1269
1324
|
refreshManagerState: managerState,
|