@clawling/clawchat-plugin-openclaw 2026.10.9-1 → 2026.10.9-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { ClawlingApiError } from "./api-types.js";
2
+ import { readConnectFailureGuidance } from "./connect-failure.js";
2
3
  import { AGENTS_CONNECT_PLATFORM, RECONNECT_GUIDE_URL } from "./onboarding-context.js";
3
4
  /**
4
5
  * Non-consuming pre-check of a connect code. Records the "checked" funnel
@@ -29,8 +30,14 @@ export async function preCheckConnectCode(client, input, log) {
29
30
  };
30
31
  }
31
32
  catch (err) {
33
+ // Still a degrade, never a gate: `/connect` re-evaluates the code and
34
+ // surfaces the same guidance on the error it throws. The guidance is only
35
+ // logged here so a rate-limit or dead code is visible early.
32
36
  const kind = err instanceof ClawlingApiError ? err.kind : "error";
33
- log(`Connect-code pre-check unavailable (${kind}); continuing without it.`);
37
+ const guidance = err instanceof ClawlingApiError ? readConnectFailureGuidance(err.meta?.data) : {};
38
+ const action = guidance.nextAction ? `; next_action: ${guidance.nextAction}` : "";
39
+ const hint = guidance.hint ? `. ${guidance.hint}` : "";
40
+ log(`Connect-code pre-check unavailable (${kind}${action})${hint}; continuing without it.`);
34
41
  return null;
35
42
  }
36
43
  }
@@ -0,0 +1,78 @@
1
+ /** Default wait for `wait_retry` when the server omits / garbles the delay. */
2
+ export const WAIT_RETRY_DEFAULT_SECONDS = 60;
3
+ /** Upper bound on any `wait_retry` wait, whatever the server asks for. */
4
+ export const WAIT_RETRY_MAX_SECONDS = 120;
5
+ function readNonBlankString(value) {
6
+ if (typeof value !== "string")
7
+ return undefined;
8
+ const trimmed = value.trim();
9
+ return trimmed ? trimmed : undefined;
10
+ }
11
+ export function readConnectFailureGuidance(data) {
12
+ if (!data || typeof data !== "object" || Array.isArray(data))
13
+ return {};
14
+ const record = data;
15
+ const out = {};
16
+ const hint = readNonBlankString(record.hint);
17
+ if (hint)
18
+ out.hint = hint;
19
+ const nextAction = readNonBlankString(record.next_action);
20
+ if (nextAction)
21
+ out.nextAction = nextAction;
22
+ const retryAfter = record.retry_after_seconds;
23
+ if (typeof retryAfter === "number" && Number.isFinite(retryAfter)) {
24
+ out.retryAfterSeconds = retryAfter;
25
+ }
26
+ return out;
27
+ }
28
+ /** Delay before the single `wait_retry` retry: server value, capped, with a default. */
29
+ export function waitRetryDelayMs(retryAfterSeconds) {
30
+ const seconds = typeof retryAfterSeconds === "number" && Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
31
+ ? Math.min(retryAfterSeconds, WAIT_RETRY_MAX_SECONDS)
32
+ : WAIT_RETRY_DEFAULT_SECONDS;
33
+ return Math.round(seconds * 1000);
34
+ }
35
+ /**
36
+ * Error thrown by the invite-code login when `/v1/agents/connect` fails.
37
+ * The message keeps the historical `agents/connect failed (<kind>): <msg>`
38
+ * prefix; when the server sent guidance it is appended so an agent reading
39
+ * the CLI / chat-command output knows what to do next.
40
+ */
41
+ export class ConnectFailedError extends Error {
42
+ kind;
43
+ code;
44
+ status;
45
+ hint;
46
+ nextAction;
47
+ retryAfterSeconds;
48
+ constructor(kind, serverMessage, meta = {}) {
49
+ super(formatConnectFailureMessage(kind, serverMessage, meta));
50
+ this.name = "ConnectFailedError";
51
+ this.kind = kind;
52
+ if (meta.code !== undefined)
53
+ this.code = meta.code;
54
+ if (meta.status !== undefined)
55
+ this.status = meta.status;
56
+ if (meta.hint !== undefined)
57
+ this.hint = meta.hint;
58
+ if (meta.nextAction !== undefined)
59
+ this.nextAction = meta.nextAction;
60
+ if (meta.retryAfterSeconds !== undefined)
61
+ this.retryAfterSeconds = meta.retryAfterSeconds;
62
+ }
63
+ }
64
+ function formatConnectFailureMessage(kind, serverMessage, guidance) {
65
+ let text = `agents/connect failed (${kind}): ${serverMessage}`;
66
+ if (guidance.nextAction)
67
+ text += ` (next_action: ${guidance.nextAction})`;
68
+ if (guidance.hint)
69
+ text += `. ${guidance.hint}`;
70
+ return text;
71
+ }
72
+ export function connectFailedError(err) {
73
+ return new ConnectFailedError(err.kind, err.message, {
74
+ ...(err.meta?.code !== undefined ? { code: err.meta.code } : {}),
75
+ ...(err.meta?.status !== undefined ? { status: err.meta.status } : {}),
76
+ ...readConnectFailureGuidance(err.meta?.data),
77
+ });
78
+ }
@@ -8,10 +8,14 @@ import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclaw
8
8
  import { getClawChatStore } from "./storage.js";
9
9
  import { buildOnboardingContext, RECONNECT_GUIDE_URL } from "./onboarding-context.js";
10
10
  import { boundCodeNewIdentityMessage, preCheckConnectCode, unpairableMessage } from "./connect-check.js";
11
+ import { connectFailedError, readConnectFailureGuidance, waitRetryDelayMs } from "./connect-failure.js";
11
12
  // Re-exported: `src/login.runtime.test.ts` and `src/commands.ts` import these
12
13
  // from here, so the historical import path keeps working after the move.
13
14
  import { AGENTS_CONNECT_PLATFORM, AGENTS_CONNECT_TYPE, AGENT_NOT_FOUND_CODE, } from "./onboarding-context.js";
14
15
  export { AGENTS_CONNECT_PLATFORM, AGENTS_CONNECT_TYPE, AGENT_NOT_FOUND_CODE };
16
+ function defaultSleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
15
19
  /**
16
20
  * Thrown when redeeming an invite code would silently replace a live activation.
17
21
  *
@@ -284,8 +288,31 @@ export async function runOpenclawClawlingLogin(params) {
284
288
  // A brand-new identity is precisely "do not replay" — the replay is the only
285
289
  // thing that would bind this code to the incumbent agent.
286
290
  const existingUserId = newAccount ? "" : storedUserId;
291
+ // Rate-limit (`next_action: wait_retry`) budget: one wait-and-retry per
292
+ // activation, shared across both requests below.
293
+ let waitRetryUsed = false;
294
+ const sleep = params.sleep ?? defaultSleep;
295
+ const redeem = async (body) => {
296
+ try {
297
+ return await apiClient.agentsConnect(body);
298
+ }
299
+ catch (err) {
300
+ if (!(err instanceof ClawlingApiError) || waitRetryUsed)
301
+ throw err;
302
+ const guidance = readConnectFailureGuidance(err.meta?.data);
303
+ // Only wait_retry is retried: a rate-limited request did not consume the
304
+ // code. Every other next_action (and any unknown one) is final.
305
+ if (guidance.nextAction !== "wait_retry")
306
+ throw err;
307
+ waitRetryUsed = true;
308
+ const delayMs = waitRetryDelayMs(guidance.retryAfterSeconds);
309
+ runtime.log(`Connect code redeem rate limited — retrying once in ${Math.round(delayMs / 1000)}s …`);
310
+ await sleep(delayMs);
311
+ return await apiClient.agentsConnect(body);
312
+ }
313
+ };
287
314
  try {
288
- result = await apiClient.agentsConnect({
315
+ result = await redeem({
289
316
  code: inviteCode,
290
317
  platform: AGENTS_CONNECT_PLATFORM,
291
318
  type: AGENTS_CONNECT_TYPE,
@@ -306,7 +333,7 @@ export async function runOpenclawClawlingLogin(params) {
306
333
  if (existingUserId && err.meta?.code === AGENT_NOT_FOUND_CODE) {
307
334
  runtime.log(`Stored userId ${existingUserId} no longer exists on this server — retrying as a fresh pairing …`);
308
335
  try {
309
- result = await apiClient.agentsConnect({
336
+ result = await redeem({
310
337
  code: inviteCode,
311
338
  platform: AGENTS_CONNECT_PLATFORM,
312
339
  type: AGENTS_CONNECT_TYPE,
@@ -315,13 +342,13 @@ export async function runOpenclawClawlingLogin(params) {
315
342
  }
316
343
  catch (retryErr) {
317
344
  if (retryErr instanceof ClawlingApiError) {
318
- throw new Error(`agents/connect failed (${retryErr.kind}): ${retryErr.message}`);
345
+ throw connectFailedError(retryErr);
319
346
  }
320
347
  throw retryErr;
321
348
  }
322
349
  }
323
350
  else {
324
- throw new Error(`agents/connect failed (${err.kind}): ${err.message}`);
351
+ throw connectFailedError(err);
325
352
  }
326
353
  }
327
354
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.10.9-1",
3
+ "version": "2026.10.9-2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -1,5 +1,6 @@
1
1
  import { ClawlingApiError } from "./api-types.ts";
2
2
  import type { OpenclawClawlingApiClient } from "./api-client.ts";
3
+ import { readConnectFailureGuidance } from "./connect-failure.ts";
3
4
  import { AGENTS_CONNECT_PLATFORM, RECONNECT_GUIDE_URL } from "./onboarding-context.ts";
4
5
 
5
6
  export interface ConnectPreCheck {
@@ -41,8 +42,14 @@ export async function preCheckConnectCode(
41
42
  userIdStatus: typeof res.user_id_status === "string" ? res.user_id_status : "",
42
43
  };
43
44
  } catch (err) {
45
+ // Still a degrade, never a gate: `/connect` re-evaluates the code and
46
+ // surfaces the same guidance on the error it throws. The guidance is only
47
+ // logged here so a rate-limit or dead code is visible early.
44
48
  const kind = err instanceof ClawlingApiError ? err.kind : "error";
45
- log(`Connect-code pre-check unavailable (${kind}); continuing without it.`);
49
+ const guidance = err instanceof ClawlingApiError ? readConnectFailureGuidance(err.meta?.data) : {};
50
+ const action = guidance.nextAction ? `; next_action: ${guidance.nextAction}` : "";
51
+ const hint = guidance.hint ? `. ${guidance.hint}` : "";
52
+ log(`Connect-code pre-check unavailable (${kind}${action})${hint}; continuing without it.`);
46
53
  return null;
47
54
  }
48
55
  }
@@ -0,0 +1,109 @@
1
+ import type { ClawlingApiError, ClawlingApiErrorKind } from "./api-types.ts";
2
+
3
+ /**
4
+ * Machine-readable guidance the backend attaches to the error envelope's
5
+ * `data` on a failed `POST /v1/agents/connect` (and `/connect/check`). Every
6
+ * field is optional: older servers send none of them, and then the plugin
7
+ * behaves exactly as before.
8
+ *
9
+ * Known `next_action` values (closed enum on the server; any other value is
10
+ * surfaced verbatim and never retried):
11
+ * - `ask_owner_new_code` — the code is dead (unknown / expired); never retry.
12
+ * - `wait_retry` — rate limited; the code was NOT consumed. Wait
13
+ * `retry_after_seconds`, then retry once.
14
+ * - `reconnect` — redeemed from another device; the owner must send
15
+ * a reconnect prompt.
16
+ * - `stop` — an unchanged retry fails the same way.
17
+ */
18
+ export type ConnectNextAction = "ask_owner_new_code" | "wait_retry" | "reconnect" | "stop";
19
+
20
+ export interface ConnectFailureGuidance {
21
+ hint?: string;
22
+ /** A `ConnectNextAction`, or an unknown value passed through verbatim. */
23
+ nextAction?: ConnectNextAction | (string & {});
24
+ retryAfterSeconds?: number;
25
+ }
26
+
27
+ /** Default wait for `wait_retry` when the server omits / garbles the delay. */
28
+ export const WAIT_RETRY_DEFAULT_SECONDS = 60;
29
+ /** Upper bound on any `wait_retry` wait, whatever the server asks for. */
30
+ export const WAIT_RETRY_MAX_SECONDS = 120;
31
+
32
+ function readNonBlankString(value: unknown): string | undefined {
33
+ if (typeof value !== "string") return undefined;
34
+ const trimmed = value.trim();
35
+ return trimmed ? trimmed : undefined;
36
+ }
37
+
38
+ export function readConnectFailureGuidance(data: unknown): ConnectFailureGuidance {
39
+ if (!data || typeof data !== "object" || Array.isArray(data)) return {};
40
+ const record = data as Record<string, unknown>;
41
+ const out: ConnectFailureGuidance = {};
42
+ const hint = readNonBlankString(record.hint);
43
+ if (hint) out.hint = hint;
44
+ const nextAction = readNonBlankString(record.next_action);
45
+ if (nextAction) out.nextAction = nextAction;
46
+ const retryAfter = record.retry_after_seconds;
47
+ if (typeof retryAfter === "number" && Number.isFinite(retryAfter)) {
48
+ out.retryAfterSeconds = retryAfter;
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /** Delay before the single `wait_retry` retry: server value, capped, with a default. */
54
+ export function waitRetryDelayMs(retryAfterSeconds: number | undefined): number {
55
+ const seconds =
56
+ typeof retryAfterSeconds === "number" && Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
57
+ ? Math.min(retryAfterSeconds, WAIT_RETRY_MAX_SECONDS)
58
+ : WAIT_RETRY_DEFAULT_SECONDS;
59
+ return Math.round(seconds * 1000);
60
+ }
61
+
62
+ /**
63
+ * Error thrown by the invite-code login when `/v1/agents/connect` fails.
64
+ * The message keeps the historical `agents/connect failed (<kind>): <msg>`
65
+ * prefix; when the server sent guidance it is appended so an agent reading
66
+ * the CLI / chat-command output knows what to do next.
67
+ */
68
+ export class ConnectFailedError extends Error {
69
+ readonly kind: ClawlingApiErrorKind;
70
+ readonly code?: number;
71
+ readonly status?: number;
72
+ readonly hint?: string;
73
+ readonly nextAction?: ConnectFailureGuidance["nextAction"];
74
+ readonly retryAfterSeconds?: number;
75
+
76
+ constructor(
77
+ kind: ClawlingApiErrorKind,
78
+ serverMessage: string,
79
+ meta: { code?: number; status?: number } & ConnectFailureGuidance = {},
80
+ ) {
81
+ super(formatConnectFailureMessage(kind, serverMessage, meta));
82
+ this.name = "ConnectFailedError";
83
+ this.kind = kind;
84
+ if (meta.code !== undefined) this.code = meta.code;
85
+ if (meta.status !== undefined) this.status = meta.status;
86
+ if (meta.hint !== undefined) this.hint = meta.hint;
87
+ if (meta.nextAction !== undefined) this.nextAction = meta.nextAction;
88
+ if (meta.retryAfterSeconds !== undefined) this.retryAfterSeconds = meta.retryAfterSeconds;
89
+ }
90
+ }
91
+
92
+ function formatConnectFailureMessage(
93
+ kind: ClawlingApiErrorKind,
94
+ serverMessage: string,
95
+ guidance: ConnectFailureGuidance,
96
+ ): string {
97
+ let text = `agents/connect failed (${kind}): ${serverMessage}`;
98
+ if (guidance.nextAction) text += ` (next_action: ${guidance.nextAction})`;
99
+ if (guidance.hint) text += `. ${guidance.hint}`;
100
+ return text;
101
+ }
102
+
103
+ export function connectFailedError(err: ClawlingApiError): ConnectFailedError {
104
+ return new ConnectFailedError(err.kind, err.message, {
105
+ ...(err.meta?.code !== undefined ? { code: err.meta.code } : {}),
106
+ ...(err.meta?.status !== undefined ? { status: err.meta.status } : {}),
107
+ ...readConnectFailureGuidance(err.meta?.data),
108
+ });
109
+ }
@@ -16,6 +16,7 @@ import {
16
16
  import { getClawChatStore, type ClawChatStore } from "./storage.ts";
17
17
  import { buildOnboardingContext, RECONNECT_GUIDE_URL } from "./onboarding-context.ts";
18
18
  import { boundCodeNewIdentityMessage, preCheckConnectCode, unpairableMessage } from "./connect-check.ts";
19
+ import { connectFailedError, readConnectFailureGuidance, waitRetryDelayMs } from "./connect-failure.ts";
19
20
  // Re-exported: `src/login.runtime.test.ts` and `src/commands.ts` import these
20
21
  // from here, so the historical import path keeps working after the move.
21
22
  import {
@@ -68,6 +69,15 @@ export interface LoginParams {
68
69
  * `promptActivationIntentFromStdin`; overridden by tests.
69
70
  */
70
71
  readActivationIntent?: () => Promise<ActivationIntent | null>;
72
+ /**
73
+ * Wait used before the single `next_action: wait_retry` retry of
74
+ * `/v1/agents/connect`. Defaults to a `setTimeout`; overridden by tests.
75
+ */
76
+ sleep?: (ms: number) => Promise<void>;
77
+ }
78
+
79
+ function defaultSleep(ms: number): Promise<void> {
80
+ return new Promise((resolve) => setTimeout(resolve, ms));
71
81
  }
72
82
 
73
83
  /** The two outcomes available once an identity is already stored. */
@@ -396,8 +406,30 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
396
406
  // A brand-new identity is precisely "do not replay" — the replay is the only
397
407
  // thing that would bind this code to the incumbent agent.
398
408
  const existingUserId = newAccount ? "" : storedUserId;
409
+ // Rate-limit (`next_action: wait_retry`) budget: one wait-and-retry per
410
+ // activation, shared across both requests below.
411
+ let waitRetryUsed = false;
412
+ const sleep = params.sleep ?? defaultSleep;
413
+ const redeem = async (body: Parameters<typeof apiClient.agentsConnect>[0]) => {
414
+ try {
415
+ return await apiClient.agentsConnect(body);
416
+ } catch (err) {
417
+ if (!(err instanceof ClawlingApiError) || waitRetryUsed) throw err;
418
+ const guidance = readConnectFailureGuidance(err.meta?.data);
419
+ // Only wait_retry is retried: a rate-limited request did not consume the
420
+ // code. Every other next_action (and any unknown one) is final.
421
+ if (guidance.nextAction !== "wait_retry") throw err;
422
+ waitRetryUsed = true;
423
+ const delayMs = waitRetryDelayMs(guidance.retryAfterSeconds);
424
+ runtime.log(
425
+ `Connect code redeem rate limited — retrying once in ${Math.round(delayMs / 1000)}s …`,
426
+ );
427
+ await sleep(delayMs);
428
+ return await apiClient.agentsConnect(body);
429
+ }
430
+ };
399
431
  try {
400
- result = await apiClient.agentsConnect({
432
+ result = await redeem({
401
433
  code: inviteCode,
402
434
  platform: AGENTS_CONNECT_PLATFORM,
403
435
  type: AGENTS_CONNECT_TYPE,
@@ -419,7 +451,7 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
419
451
  `Stored userId ${existingUserId} no longer exists on this server — retrying as a fresh pairing …`,
420
452
  );
421
453
  try {
422
- result = await apiClient.agentsConnect({
454
+ result = await redeem({
423
455
  code: inviteCode,
424
456
  platform: AGENTS_CONNECT_PLATFORM,
425
457
  type: AGENTS_CONNECT_TYPE,
@@ -427,12 +459,12 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
427
459
  });
428
460
  } catch (retryErr) {
429
461
  if (retryErr instanceof ClawlingApiError) {
430
- throw new Error(`agents/connect failed (${retryErr.kind}): ${retryErr.message}`);
462
+ throw connectFailedError(retryErr);
431
463
  }
432
464
  throw retryErr;
433
465
  }
434
466
  } else {
435
- throw new Error(`agents/connect failed (${err.kind}): ${err.message}`);
467
+ throw connectFailedError(err);
436
468
  }
437
469
  } else {
438
470
  throw err;