@clawling/clawchat-plugin-openclaw 2026.6.30-1 → 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
@@ -279,12 +307,17 @@ function withClawChatSessionScope(cfg) {
279
307
  };
280
308
  }
281
309
  function buildActivationBootstrapText() {
310
+ // Worded to make the greeting the single required action. Earlier wording
311
+ // ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
312
+ // it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
313
+ // no greeting reached the user. Lead with the chat reply, forbid file/tool
314
+ // detours, and keep the profile update strictly optional and secondary.
282
315
  return [
283
- "ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
284
- "Please do both:",
285
- "1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
286
- "2. If you have local profile information for yourself, such as display name, bio, or avatar, update the connected ClawChat account profile using the available ClawChat tools. Use `clawchat_update_account_profile` for display name/bio/avatar URL, and use `clawchat_upload_avatar_image` first if the avatar is only available as a local image path. If you do not have local profile information, skip profile updates and only greet the user.",
287
- "Do not ask the user for profile information just for this bootstrap.",
316
+ "You are now connected to a ClawChat direct conversation with your user.",
317
+ "Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
318
+ "Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
319
+ "Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
320
+ "Do not ask the user for profile information.",
288
321
  ].join("\n");
289
322
  }
290
323
  function buildActivationBootstrapEnvelope(params) {
@@ -1440,6 +1473,38 @@ export async function startOpenclawClawlingGateway(params) {
1440
1473
  }));
1441
1474
  };
1442
1475
  let dispatchActivationBootstrap = async () => { };
1476
+ // Set by dispatchTurnToAgent when it dispatches the synthetic
1477
+ // `clawchat-bootstrap` turn: true only if the agent actually produced a final
1478
+ // outbound message (the greeting). The bootstrap dispatcher reads it to decide
1479
+ // whether to mark the bootstrap sent or leave it pending for a later retry.
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
+ };
1443
1508
  client.on("state", ({ from, to }) => {
1444
1509
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
1445
1510
  wsReady = to === "connected";
@@ -1512,6 +1577,9 @@ export async function startOpenclawClawlingGateway(params) {
1512
1577
  // The queue keeps the failed frame at the head and will retry after the next reconnect.
1513
1578
  }
1514
1579
  void refreshConversationCacheAfterReady();
1580
+ // Fresh connection → fresh retry budget; cancel any stale timer first.
1581
+ bootstrapRetryAttempts = 0;
1582
+ clearBootstrapRetry();
1515
1583
  void dispatchActivationBootstrap();
1516
1584
  // Re-pull per-group agent settings on every successful (re)connect so the
1517
1585
  // cache stays current after a network interruption or token refresh.
@@ -1523,6 +1591,8 @@ export async function startOpenclawClawlingGateway(params) {
1523
1591
  }
1524
1592
  else if (to === "disconnected") {
1525
1593
  reconnectTracker.markClosed();
1594
+ // Stop the bootstrap retry while down; it re-arms on the next ready.
1595
+ clearBootstrapRetry();
1526
1596
  // §A.1 — clear the proactive timer on disconnect; it re-arms on the next
1527
1597
  // ready, or the gateway re-enter arms a fresh one.
1528
1598
  if (!reconnectWithRefreshedToken)
@@ -2131,6 +2201,12 @@ export async function startOpenclawClawlingGateway(params) {
2131
2201
  });
2132
2202
  const counts = dispatchResult?.counts ?? {};
2133
2203
  const queuedFinal = Boolean(dispatchResult?.queuedFinal);
2204
+ if (turn.senderId === "clawchat-bootstrap") {
2205
+ // Did the agent actually emit a greeting? A final outbound message
2206
+ // means yes; block/tool-only (or nothing) means the greeting never
2207
+ // reached the user, so the bootstrap dispatcher will keep it pending.
2208
+ bootstrapProducedFinal = queuedFinal || (counts.final ?? 0) > 0;
2209
+ }
2134
2210
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
2135
2211
  if (!queuedFinal && Object.values(counts).every((n) => !n)) {
2136
2212
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw NO reply was produced (no final / block / tool dispatched). ` +
@@ -2373,6 +2449,7 @@ export async function startOpenclawClawlingGateway(params) {
2373
2449
  if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent)
2374
2450
  return;
2375
2451
  let bootstrap;
2452
+ let claimedInFlight = false;
2376
2453
  const releaseBootstrap = () => {
2377
2454
  if (!bootstrap || !store.releaseActivationBootstrapClaim)
2378
2455
  return;
@@ -2385,12 +2462,34 @@ export async function startOpenclawClawlingGateway(params) {
2385
2462
  };
2386
2463
  try {
2387
2464
  bootstrap = recordConnection("activation bootstrap claim", () => store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }));
2388
- 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();
2389
2471
  return;
2472
+ }
2390
2473
  const claimedBootstrap = bootstrap;
2474
+ claimedInFlight = true;
2475
+ incrementActivationBootstrapInFlight(accountId);
2476
+ bootstrapProducedFinal = false;
2391
2477
  const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
2392
2478
  if (result !== "submitted") {
2393
2479
  releaseBootstrap();
2480
+ armBootstrapRetry();
2481
+ return;
2482
+ }
2483
+ // Only mark the bootstrap done once the agent has actually sent a greeting
2484
+ // (a final outbound message). If the turn was handed off but produced no
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.
2489
+ if (!bootstrapProducedFinal) {
2490
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`);
2491
+ releaseBootstrap();
2492
+ armBootstrapRetry();
2394
2493
  return;
2395
2494
  }
2396
2495
  recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
@@ -2398,11 +2497,18 @@ export async function startOpenclawClawlingGateway(params) {
2398
2497
  accountId,
2399
2498
  conversationId: claimedBootstrap.conversationId,
2400
2499
  }));
2500
+ // Delivered — stop any pending retry.
2501
+ clearBootstrapRetry();
2401
2502
  }
2402
2503
  catch (err) {
2403
2504
  releaseBootstrap();
2505
+ armBootstrapRetry();
2404
2506
  log?.error?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
2405
2507
  }
2508
+ finally {
2509
+ if (claimedInFlight)
2510
+ decrementActivationBootstrapInFlight(accountId);
2511
+ }
2406
2512
  };
2407
2513
  client.on("message", (env) => {
2408
2514
  void (async () => {
@@ -2514,6 +2620,7 @@ export async function startOpenclawClawlingGateway(params) {
2514
2620
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw runtime abort received; closing client`);
2515
2621
  activeClients.delete(accountId);
2516
2622
  closingForAbort = true;
2623
+ clearBootstrapRetry();
2517
2624
  // §A.1 — stop the proactive refresh timer on shutdown.
2518
2625
  refreshManager.stop();
2519
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-1",
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
@@ -427,12 +453,17 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
427
453
  }
428
454
 
429
455
  function buildActivationBootstrapText(): string {
456
+ // Worded to make the greeting the single required action. Earlier wording
457
+ // ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
458
+ // it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
459
+ // no greeting reached the user. Lead with the chat reply, forbid file/tool
460
+ // detours, and keep the profile update strictly optional and secondary.
430
461
  return [
431
- "ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
432
- "Please do both:",
433
- "1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
434
- "2. If you have local profile information for yourself, such as display name, bio, or avatar, update the connected ClawChat account profile using the available ClawChat tools. Use `clawchat_update_account_profile` for display name/bio/avatar URL, and use `clawchat_upload_avatar_image` first if the avatar is only available as a local image path. If you do not have local profile information, skip profile updates and only greet the user.",
435
- "Do not ask the user for profile information just for this bootstrap.",
462
+ "You are now connected to a ClawChat direct conversation with your user.",
463
+ "Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
464
+ "Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
465
+ "Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
466
+ "Do not ask the user for profile information.",
436
467
  ].join("\n");
437
468
  }
438
469
 
@@ -538,6 +569,8 @@ export interface StartGatewayParams {
538
569
  backoffTimer?: (cb: () => void, ms: number) => void;
539
570
  /** Test hook only — fixed transport-backoff delay (ms) for re-enter. */
540
571
  transportBackoffDelayMs?: number;
572
+ /** Test hook only — retry delay (ms) for re-dispatching a pending activation bootstrap. */
573
+ activationBootstrapRetryDelayMs?: number;
541
574
  }
542
575
 
543
576
  /**
@@ -1816,6 +1849,37 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1816
1849
  );
1817
1850
  };
1818
1851
  let dispatchActivationBootstrap: () => Promise<void> = async () => {};
1852
+ // Set by dispatchTurnToAgent when it dispatches the synthetic
1853
+ // `clawchat-bootstrap` turn: true only if the agent actually produced a final
1854
+ // outbound message (the greeting). The bootstrap dispatcher reads it to decide
1855
+ // whether to mark the bootstrap sent or leave it pending for a later retry.
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
+ };
1819
1883
 
1820
1884
  client.on("state", ({ from, to }) => {
1821
1885
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
@@ -1890,6 +1954,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1890
1954
  // The queue keeps the failed frame at the head and will retry after the next reconnect.
1891
1955
  }
1892
1956
  void refreshConversationCacheAfterReady();
1957
+ // Fresh connection → fresh retry budget; cancel any stale timer first.
1958
+ bootstrapRetryAttempts = 0;
1959
+ clearBootstrapRetry();
1893
1960
  void dispatchActivationBootstrap();
1894
1961
  // Re-pull per-group agent settings on every successful (re)connect so the
1895
1962
  // cache stays current after a network interruption or token refresh.
@@ -1900,6 +1967,8 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1900
1967
  refreshManager.armProactiveTimer(activatedAtMs);
1901
1968
  } else if (to === "disconnected") {
1902
1969
  reconnectTracker.markClosed();
1970
+ // Stop the bootstrap retry while down; it re-arms on the next ready.
1971
+ clearBootstrapRetry();
1903
1972
  // §A.1 — clear the proactive timer on disconnect; it re-arms on the next
1904
1973
  // ready, or the gateway re-enter arms a fresh one.
1905
1974
  if (!reconnectWithRefreshedToken) refreshManager.disarmProactiveTimer();
@@ -2579,6 +2648,12 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2579
2648
  const queuedFinal = Boolean(
2580
2649
  (dispatchResult as { queuedFinal?: boolean } | undefined)?.queuedFinal,
2581
2650
  );
2651
+ if (turn.senderId === "clawchat-bootstrap") {
2652
+ // Did the agent actually emit a greeting? A final outbound message
2653
+ // means yes; block/tool-only (or nothing) means the greeting never
2654
+ // reached the user, so the bootstrap dispatcher will keep it pending.
2655
+ bootstrapProducedFinal = queuedFinal || (counts.final ?? 0) > 0;
2656
+ }
2582
2657
  log?.info?.(
2583
2658
  `[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`,
2584
2659
  );
@@ -2860,6 +2935,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2860
2935
  dispatchActivationBootstrap = async (): Promise<void> => {
2861
2936
  if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent) return;
2862
2937
  let bootstrap: { conversationId: string } | null | undefined;
2938
+ let claimedInFlight = false;
2863
2939
  const releaseBootstrap = () => {
2864
2940
  if (!bootstrap || !store.releaseActivationBootstrapClaim) return;
2865
2941
  const claimedBootstrap = bootstrap;
@@ -2875,13 +2951,37 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2875
2951
  bootstrap = recordConnection("activation bootstrap claim", () =>
2876
2952
  store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }),
2877
2953
  );
2878
- 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
+ }
2879
2961
  const claimedBootstrap = bootstrap;
2962
+ claimedInFlight = true;
2963
+ incrementActivationBootstrapInFlight(accountId);
2964
+ bootstrapProducedFinal = false;
2880
2965
  const result = await handleInboundEnvelope(
2881
2966
  buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
2882
2967
  );
2883
2968
  if (result !== "submitted") {
2884
2969
  releaseBootstrap();
2970
+ armBootstrapRetry();
2971
+ return;
2972
+ }
2973
+ // Only mark the bootstrap done once the agent has actually sent a greeting
2974
+ // (a final outbound message). If the turn was handed off but produced no
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.
2979
+ if (!bootstrapProducedFinal) {
2980
+ log?.info?.(
2981
+ `[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`,
2982
+ );
2983
+ releaseBootstrap();
2984
+ armBootstrapRetry();
2885
2985
  return;
2886
2986
  }
2887
2987
  recordConnection("activation bootstrap sent", () =>
@@ -2891,11 +2991,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2891
2991
  conversationId: claimedBootstrap.conversationId,
2892
2992
  }),
2893
2993
  );
2994
+ // Delivered — stop any pending retry.
2995
+ clearBootstrapRetry();
2894
2996
  } catch (err) {
2895
2997
  releaseBootstrap();
2998
+ armBootstrapRetry();
2896
2999
  log?.error?.(
2897
3000
  `[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`,
2898
3001
  );
3002
+ } finally {
3003
+ if (claimedInFlight) decrementActivationBootstrapInFlight(accountId);
2899
3004
  }
2900
3005
  };
2901
3006
 
@@ -3020,6 +3125,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
3020
3125
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw runtime abort received; closing client`);
3021
3126
  activeClients.delete(accountId);
3022
3127
  closingForAbort = true;
3128
+ clearBootstrapRetry();
3023
3129
  // §A.1 — stop the proactive refresh timer on shutdown.
3024
3130
  refreshManager.stop();
3025
3131
  groupCoalescer.cancelAll();