@clawling/clawchat-plugin-openclaw 2026.6.30-2 → 2026.6.30-3

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.
@@ -33,6 +33,34 @@ export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
33
33
  const activeClients = new Map();
34
34
  const CLAWCHAT_PLUGIN_SLASH_COMMANDS = new Set(["clawchat-activate", "clawchat-output"]);
35
35
  const ACTIVATION_CREDENTIAL_POLL_INTERVAL_MS = 2_000;
36
+ // Activation-bootstrap retry on the live connection. The bootstrap greeting is
37
+ // otherwise only (re)dispatched on a fresh `connected` edge, so if its send is
38
+ // cancelled mid-turn (e.g. a proactive token-refresh reconnect closed the
39
+ // transport) or a reconnect's claim no-op'd while a stale in-flight dispatch
40
+ // still held the claim, the greeting would stay pending forever on a now-stable
41
+ // connection. These bound an in-connection retry that re-attempts until the
42
+ // greeting lands or the per-connection budget is spent (then it falls back to
43
+ // the next reconnect, as before).
44
+ const ACTIVATION_BOOTSTRAP_RETRY_DELAY_MS = 5_000;
45
+ const ACTIVATION_BOOTSTRAP_MAX_RETRIES = 6;
46
+ // Count of in-flight activation-bootstrap dispatches per account, shared across
47
+ // gateway re-enters so a fresh connection can tell "nothing pending" (don't
48
+ // retry) apart from "a prior dispatch still holds the claim" (retry once it
49
+ // releases).
50
+ const activationBootstrapInFlight = new Map();
51
+ function incrementActivationBootstrapInFlight(accountId) {
52
+ activationBootstrapInFlight.set(accountId, (activationBootstrapInFlight.get(accountId) ?? 0) + 1);
53
+ }
54
+ function decrementActivationBootstrapInFlight(accountId) {
55
+ const next = (activationBootstrapInFlight.get(accountId) ?? 0) - 1;
56
+ if (next <= 0)
57
+ activationBootstrapInFlight.delete(accountId);
58
+ else
59
+ activationBootstrapInFlight.set(accountId, next);
60
+ }
61
+ function activationBootstrapInFlightCount(accountId) {
62
+ return activationBootstrapInFlight.get(accountId) ?? 0;
63
+ }
36
64
  // Upper bound (ms) on how long group ingest waits for the FIRST group-settings
37
65
  // pull to settle before proceeding anyway. Best-effort governance must never
38
66
  // permanently disable group traffic, so a stalled initial GET (endpoint accepts
@@ -1450,6 +1478,33 @@ export async function startOpenclawClawlingGateway(params) {
1450
1478
  // outbound message (the greeting). The bootstrap dispatcher reads it to decide
1451
1479
  // whether to mark the bootstrap sent or leave it pending for a later retry.
1452
1480
  let bootstrapProducedFinal = false;
1481
+ // Per-connection retry for a pending activation bootstrap (see the module-level
1482
+ // constants). Re-attempts on the live connection while `wsReady`, bounded by
1483
+ // ACTIVATION_BOOTSTRAP_MAX_RETRIES; the budget resets on each fresh connect.
1484
+ let bootstrapRetryTimer;
1485
+ let bootstrapRetryAttempts = 0;
1486
+ const bootstrapRetryDelayMs = Math.max(1, params.activationBootstrapRetryDelayMs ?? ACTIVATION_BOOTSTRAP_RETRY_DELAY_MS);
1487
+ const clearBootstrapRetry = () => {
1488
+ if (bootstrapRetryTimer !== undefined) {
1489
+ clearTimeout(bootstrapRetryTimer);
1490
+ bootstrapRetryTimer = undefined;
1491
+ }
1492
+ };
1493
+ const armBootstrapRetry = () => {
1494
+ if (bootstrapRetryTimer !== undefined)
1495
+ return;
1496
+ if (!wsReady)
1497
+ return;
1498
+ if (bootstrapRetryAttempts >= ACTIVATION_BOOTSTRAP_MAX_RETRIES)
1499
+ return;
1500
+ bootstrapRetryTimer = setTimeout(() => {
1501
+ bootstrapRetryTimer = undefined;
1502
+ if (!wsReady)
1503
+ return;
1504
+ bootstrapRetryAttempts += 1;
1505
+ void dispatchActivationBootstrap();
1506
+ }, bootstrapRetryDelayMs);
1507
+ };
1453
1508
  client.on("state", ({ from, to }) => {
1454
1509
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
1455
1510
  wsReady = to === "connected";
@@ -1522,6 +1577,9 @@ export async function startOpenclawClawlingGateway(params) {
1522
1577
  // The queue keeps the failed frame at the head and will retry after the next reconnect.
1523
1578
  }
1524
1579
  void refreshConversationCacheAfterReady();
1580
+ // Fresh connection → fresh retry budget; cancel any stale timer first.
1581
+ bootstrapRetryAttempts = 0;
1582
+ clearBootstrapRetry();
1525
1583
  void dispatchActivationBootstrap();
1526
1584
  // Re-pull per-group agent settings on every successful (re)connect so the
1527
1585
  // cache stays current after a network interruption or token refresh.
@@ -1533,6 +1591,8 @@ export async function startOpenclawClawlingGateway(params) {
1533
1591
  }
1534
1592
  else if (to === "disconnected") {
1535
1593
  reconnectTracker.markClosed();
1594
+ // Stop the bootstrap retry while down; it re-arms on the next ready.
1595
+ clearBootstrapRetry();
1536
1596
  // §A.1 — clear the proactive timer on disconnect; it re-arms on the next
1537
1597
  // ready, or the gateway re-enter arms a fresh one.
1538
1598
  if (!reconnectWithRefreshedToken)
@@ -2389,6 +2449,7 @@ export async function startOpenclawClawlingGateway(params) {
2389
2449
  if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent)
2390
2450
  return;
2391
2451
  let bootstrap;
2452
+ let claimedInFlight = false;
2392
2453
  const releaseBootstrap = () => {
2393
2454
  if (!bootstrap || !store.releaseActivationBootstrapClaim)
2394
2455
  return;
@@ -2401,23 +2462,34 @@ export async function startOpenclawClawlingGateway(params) {
2401
2462
  };
2402
2463
  try {
2403
2464
  bootstrap = recordConnection("activation bootstrap claim", () => store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }));
2404
- if (!bootstrap)
2465
+ if (!bootstrap) {
2466
+ // Nothing claimable right now. If another dispatch (e.g. a pre-reconnect
2467
+ // one that was about to fail) still holds the claim, keep retrying on
2468
+ // this live connection so the greeting isn't stranded once it releases.
2469
+ if (activationBootstrapInFlightCount(accountId) > 0)
2470
+ armBootstrapRetry();
2405
2471
  return;
2472
+ }
2406
2473
  const claimedBootstrap = bootstrap;
2474
+ claimedInFlight = true;
2475
+ incrementActivationBootstrapInFlight(accountId);
2407
2476
  bootstrapProducedFinal = false;
2408
2477
  const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
2409
2478
  if (result !== "submitted") {
2410
2479
  releaseBootstrap();
2480
+ armBootstrapRetry();
2411
2481
  return;
2412
2482
  }
2413
2483
  // Only mark the bootstrap done once the agent has actually sent a greeting
2414
2484
  // (a final outbound message). If the turn was handed off but produced no
2415
- // final reply — e.g. the agent went off to write a file/note instead of
2416
- // replying — leave the bootstrap pending (release the claim) so the next
2417
- // ready re-dispatches it, rather than silently consuming it forever.
2485
+ // final reply — e.g. the greeting send was cancelled by a reconnect, or the
2486
+ // agent went off to write a file/note instead of replying — leave the
2487
+ // bootstrap pending (release the claim) and retry on the live connection,
2488
+ // rather than silently consuming it forever or waiting for the next reconnect.
2418
2489
  if (!bootstrapProducedFinal) {
2419
2490
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`);
2420
2491
  releaseBootstrap();
2492
+ armBootstrapRetry();
2421
2493
  return;
2422
2494
  }
2423
2495
  recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
@@ -2425,11 +2497,18 @@ export async function startOpenclawClawlingGateway(params) {
2425
2497
  accountId,
2426
2498
  conversationId: claimedBootstrap.conversationId,
2427
2499
  }));
2500
+ // Delivered — stop any pending retry.
2501
+ clearBootstrapRetry();
2428
2502
  }
2429
2503
  catch (err) {
2430
2504
  releaseBootstrap();
2505
+ armBootstrapRetry();
2431
2506
  log?.error?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
2432
2507
  }
2508
+ finally {
2509
+ if (claimedInFlight)
2510
+ decrementActivationBootstrapInFlight(accountId);
2511
+ }
2433
2512
  };
2434
2513
  client.on("message", (env) => {
2435
2514
  void (async () => {
@@ -2541,6 +2620,7 @@ export async function startOpenclawClawlingGateway(params) {
2541
2620
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw runtime abort received; closing client`);
2542
2621
  activeClients.delete(accountId);
2543
2622
  closingForAbort = true;
2623
+ clearBootstrapRetry();
2544
2624
  // §A.1 — stop the proactive refresh timer on shutdown.
2545
2625
  refreshManager.stop();
2546
2626
  groupCoalescer.cancelAll();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.6.30-2",
3
+ "version": "2026.6.30-3",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
package/src/runtime.ts CHANGED
@@ -120,6 +120,32 @@ export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
120
120
  const activeClients = new Map<string, ClawlingChatClient>();
121
121
  const CLAWCHAT_PLUGIN_SLASH_COMMANDS = new Set(["clawchat-activate", "clawchat-output"]);
122
122
  const ACTIVATION_CREDENTIAL_POLL_INTERVAL_MS = 2_000;
123
+ // Activation-bootstrap retry on the live connection. The bootstrap greeting is
124
+ // otherwise only (re)dispatched on a fresh `connected` edge, so if its send is
125
+ // cancelled mid-turn (e.g. a proactive token-refresh reconnect closed the
126
+ // transport) or a reconnect's claim no-op'd while a stale in-flight dispatch
127
+ // still held the claim, the greeting would stay pending forever on a now-stable
128
+ // connection. These bound an in-connection retry that re-attempts until the
129
+ // greeting lands or the per-connection budget is spent (then it falls back to
130
+ // the next reconnect, as before).
131
+ const ACTIVATION_BOOTSTRAP_RETRY_DELAY_MS = 5_000;
132
+ const ACTIVATION_BOOTSTRAP_MAX_RETRIES = 6;
133
+ // Count of in-flight activation-bootstrap dispatches per account, shared across
134
+ // gateway re-enters so a fresh connection can tell "nothing pending" (don't
135
+ // retry) apart from "a prior dispatch still holds the claim" (retry once it
136
+ // releases).
137
+ const activationBootstrapInFlight = new Map<string, number>();
138
+ function incrementActivationBootstrapInFlight(accountId: string): void {
139
+ activationBootstrapInFlight.set(accountId, (activationBootstrapInFlight.get(accountId) ?? 0) + 1);
140
+ }
141
+ function decrementActivationBootstrapInFlight(accountId: string): void {
142
+ const next = (activationBootstrapInFlight.get(accountId) ?? 0) - 1;
143
+ if (next <= 0) activationBootstrapInFlight.delete(accountId);
144
+ else activationBootstrapInFlight.set(accountId, next);
145
+ }
146
+ function activationBootstrapInFlightCount(accountId: string): number {
147
+ return activationBootstrapInFlight.get(accountId) ?? 0;
148
+ }
123
149
  // Upper bound (ms) on how long group ingest waits for the FIRST group-settings
124
150
  // pull to settle before proceeding anyway. Best-effort governance must never
125
151
  // permanently disable group traffic, so a stalled initial GET (endpoint accepts
@@ -543,6 +569,8 @@ export interface StartGatewayParams {
543
569
  backoffTimer?: (cb: () => void, ms: number) => void;
544
570
  /** Test hook only — fixed transport-backoff delay (ms) for re-enter. */
545
571
  transportBackoffDelayMs?: number;
572
+ /** Test hook only — retry delay (ms) for re-dispatching a pending activation bootstrap. */
573
+ activationBootstrapRetryDelayMs?: number;
546
574
  }
547
575
 
548
576
  /**
@@ -1826,6 +1854,32 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1826
1854
  // outbound message (the greeting). The bootstrap dispatcher reads it to decide
1827
1855
  // whether to mark the bootstrap sent or leave it pending for a later retry.
1828
1856
  let bootstrapProducedFinal = false;
1857
+ // Per-connection retry for a pending activation bootstrap (see the module-level
1858
+ // constants). Re-attempts on the live connection while `wsReady`, bounded by
1859
+ // ACTIVATION_BOOTSTRAP_MAX_RETRIES; the budget resets on each fresh connect.
1860
+ let bootstrapRetryTimer: ReturnType<typeof setTimeout> | undefined;
1861
+ let bootstrapRetryAttempts = 0;
1862
+ const bootstrapRetryDelayMs = Math.max(
1863
+ 1,
1864
+ params.activationBootstrapRetryDelayMs ?? ACTIVATION_BOOTSTRAP_RETRY_DELAY_MS,
1865
+ );
1866
+ const clearBootstrapRetry = () => {
1867
+ if (bootstrapRetryTimer !== undefined) {
1868
+ clearTimeout(bootstrapRetryTimer);
1869
+ bootstrapRetryTimer = undefined;
1870
+ }
1871
+ };
1872
+ const armBootstrapRetry = () => {
1873
+ if (bootstrapRetryTimer !== undefined) return;
1874
+ if (!wsReady) return;
1875
+ if (bootstrapRetryAttempts >= ACTIVATION_BOOTSTRAP_MAX_RETRIES) return;
1876
+ bootstrapRetryTimer = setTimeout(() => {
1877
+ bootstrapRetryTimer = undefined;
1878
+ if (!wsReady) return;
1879
+ bootstrapRetryAttempts += 1;
1880
+ void dispatchActivationBootstrap();
1881
+ }, bootstrapRetryDelayMs);
1882
+ };
1829
1883
 
1830
1884
  client.on("state", ({ from, to }) => {
1831
1885
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
@@ -1900,6 +1954,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1900
1954
  // The queue keeps the failed frame at the head and will retry after the next reconnect.
1901
1955
  }
1902
1956
  void refreshConversationCacheAfterReady();
1957
+ // Fresh connection → fresh retry budget; cancel any stale timer first.
1958
+ bootstrapRetryAttempts = 0;
1959
+ clearBootstrapRetry();
1903
1960
  void dispatchActivationBootstrap();
1904
1961
  // Re-pull per-group agent settings on every successful (re)connect so the
1905
1962
  // cache stays current after a network interruption or token refresh.
@@ -1910,6 +1967,8 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1910
1967
  refreshManager.armProactiveTimer(activatedAtMs);
1911
1968
  } else if (to === "disconnected") {
1912
1969
  reconnectTracker.markClosed();
1970
+ // Stop the bootstrap retry while down; it re-arms on the next ready.
1971
+ clearBootstrapRetry();
1913
1972
  // §A.1 — clear the proactive timer on disconnect; it re-arms on the next
1914
1973
  // ready, or the gateway re-enter arms a fresh one.
1915
1974
  if (!reconnectWithRefreshedToken) refreshManager.disarmProactiveTimer();
@@ -2876,6 +2935,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2876
2935
  dispatchActivationBootstrap = async (): Promise<void> => {
2877
2936
  if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent) return;
2878
2937
  let bootstrap: { conversationId: string } | null | undefined;
2938
+ let claimedInFlight = false;
2879
2939
  const releaseBootstrap = () => {
2880
2940
  if (!bootstrap || !store.releaseActivationBootstrapClaim) return;
2881
2941
  const claimedBootstrap = bootstrap;
@@ -2891,26 +2951,37 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2891
2951
  bootstrap = recordConnection("activation bootstrap claim", () =>
2892
2952
  store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }),
2893
2953
  );
2894
- if (!bootstrap) return;
2954
+ if (!bootstrap) {
2955
+ // Nothing claimable right now. If another dispatch (e.g. a pre-reconnect
2956
+ // one that was about to fail) still holds the claim, keep retrying on
2957
+ // this live connection so the greeting isn't stranded once it releases.
2958
+ if (activationBootstrapInFlightCount(accountId) > 0) armBootstrapRetry();
2959
+ return;
2960
+ }
2895
2961
  const claimedBootstrap = bootstrap;
2962
+ claimedInFlight = true;
2963
+ incrementActivationBootstrapInFlight(accountId);
2896
2964
  bootstrapProducedFinal = false;
2897
2965
  const result = await handleInboundEnvelope(
2898
2966
  buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
2899
2967
  );
2900
2968
  if (result !== "submitted") {
2901
2969
  releaseBootstrap();
2970
+ armBootstrapRetry();
2902
2971
  return;
2903
2972
  }
2904
2973
  // Only mark the bootstrap done once the agent has actually sent a greeting
2905
2974
  // (a final outbound message). If the turn was handed off but produced no
2906
- // final reply — e.g. the agent went off to write a file/note instead of
2907
- // replying — leave the bootstrap pending (release the claim) so the next
2908
- // ready re-dispatches it, rather than silently consuming it forever.
2975
+ // final reply — e.g. the greeting send was cancelled by a reconnect, or the
2976
+ // agent went off to write a file/note instead of replying — leave the
2977
+ // bootstrap pending (release the claim) and retry on the live connection,
2978
+ // rather than silently consuming it forever or waiting for the next reconnect.
2909
2979
  if (!bootstrapProducedFinal) {
2910
2980
  log?.info?.(
2911
2981
  `[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`,
2912
2982
  );
2913
2983
  releaseBootstrap();
2984
+ armBootstrapRetry();
2914
2985
  return;
2915
2986
  }
2916
2987
  recordConnection("activation bootstrap sent", () =>
@@ -2920,11 +2991,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2920
2991
  conversationId: claimedBootstrap.conversationId,
2921
2992
  }),
2922
2993
  );
2994
+ // Delivered — stop any pending retry.
2995
+ clearBootstrapRetry();
2923
2996
  } catch (err) {
2924
2997
  releaseBootstrap();
2998
+ armBootstrapRetry();
2925
2999
  log?.error?.(
2926
3000
  `[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`,
2927
3001
  );
3002
+ } finally {
3003
+ if (claimedInFlight) decrementActivationBootstrapInFlight(accountId);
2928
3004
  }
2929
3005
  };
2930
3006
 
@@ -3049,6 +3125,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
3049
3125
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw runtime abort received; closing client`);
3050
3126
  activeClients.delete(accountId);
3051
3127
  closingForAbort = true;
3128
+ clearBootstrapRetry();
3052
3129
  // §A.1 — stop the proactive refresh timer on shutdown.
3053
3130
  refreshManager.stop();
3054
3131
  groupCoalescer.cancelAll();