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

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.
@@ -296,6 +296,12 @@ export function createOpenclawClawlingReplyDispatcher(options) {
296
296
  let typingActive = false;
297
297
  let terminalReplySuppressed = false;
298
298
  let finalDeliverySeen = false;
299
+ // Count of outbound messages that were actually delivered to the target
300
+ // (a real, successful send — not a suppressed/duplicate/failed one). This is
301
+ // the reliable "did anything actually reach the user?" signal; host-side
302
+ // `counts.block`/`counts.final` only reflect what the agent emitted, not
303
+ // whether the send succeeded (a cancelled send still counts as a block).
304
+ let outboundDeliveredCount = 0;
299
305
  const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
300
306
  const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
301
307
  const terminalSendScopeId = options.terminalSendScopeId ?? null;
@@ -502,6 +508,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
502
508
  log,
503
509
  });
504
510
  recordOutboundAck("message", messageId, result);
511
+ if (result)
512
+ outboundDeliveredCount += 1;
505
513
  log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw send complete to=${target.chatId}`);
506
514
  openclawLlmContextDebug.writeSnapshot({
507
515
  visibility: "host_event",
@@ -676,6 +684,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
676
684
  });
677
685
  return {
678
686
  dispatcher: base.dispatcher,
687
+ getOutboundDeliveredCount: () => outboundDeliveredCount,
679
688
  replyOptions: {
680
689
  ...base.replyOptions,
681
690
  sourceReplyDeliveryMode: "automatic",
@@ -1474,10 +1474,11 @@ export async function startOpenclawClawlingGateway(params) {
1474
1474
  };
1475
1475
  let dispatchActivationBootstrap = async () => { };
1476
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;
1477
+ // `clawchat-bootstrap` turn: true only if the greeting was actually delivered
1478
+ // (a real successful outbound send — block or final). The bootstrap dispatcher
1479
+ // reads it to decide whether to mark the bootstrap sent or leave it pending for
1480
+ // a later retry.
1481
+ let bootstrapGreetingDelivered = false;
1481
1482
  // Per-connection retry for a pending activation bootstrap (see the module-level
1482
1483
  // constants). Re-attempts on the live connection while `wsReady`, bounded by
1483
1484
  // ACTIVATION_BOOTSTRAP_MAX_RETRIES; the budget resets on each fresh connect.
@@ -2166,7 +2167,7 @@ export async function startOpenclawClawlingGateway(params) {
2166
2167
  chatType: turn.peer.kind,
2167
2168
  agentId: route.agentId,
2168
2169
  });
2169
- const { dispatcher, replyOptions, markDispatchIdle } = createOpenclawClawlingReplyDispatcher({
2170
+ const { dispatcher, replyOptions, markDispatchIdle, getOutboundDeliveredCount } = createOpenclawClawlingReplyDispatcher({
2170
2171
  cfg,
2171
2172
  runtime,
2172
2173
  account,
@@ -2202,10 +2203,14 @@ export async function startOpenclawClawlingGateway(params) {
2202
2203
  const counts = dispatchResult?.counts ?? {};
2203
2204
  const queuedFinal = Boolean(dispatchResult?.queuedFinal);
2204
2205
  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;
2206
+ // Did the greeting actually reach the user? The reliable signal is a
2207
+ // real, successful outbound send (the agent may deliver the greeting
2208
+ // as a `block`, not a `final`, and a cancelled send still shows up in
2209
+ // host `counts.block`). Treat a queued/counted final as delivered too,
2210
+ // to cover deferred/human-delay finals not yet flushed at this point.
2211
+ const deliveredOutbound = getOutboundDeliveredCount?.() ?? 0;
2212
+ bootstrapGreetingDelivered =
2213
+ deliveredOutbound > 0 || queuedFinal || (counts.final ?? 0) > 0;
2209
2214
  }
2210
2215
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
2211
2216
  if (!queuedFinal && Object.values(counts).every((n) => !n)) {
@@ -2473,21 +2478,21 @@ export async function startOpenclawClawlingGateway(params) {
2473
2478
  const claimedBootstrap = bootstrap;
2474
2479
  claimedInFlight = true;
2475
2480
  incrementActivationBootstrapInFlight(accountId);
2476
- bootstrapProducedFinal = false;
2481
+ bootstrapGreetingDelivered = false;
2477
2482
  const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
2478
2483
  if (result !== "submitted") {
2479
2484
  releaseBootstrap();
2480
2485
  armBootstrapRetry();
2481
2486
  return;
2482
2487
  }
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
2488
+ // Only mark the bootstrap done once the greeting was actually delivered
2489
+ // (a real successful outbound send). If the turn was handed off but nothing
2490
+ // reached the user — e.g. the greeting send was cancelled by a reconnect, or
2491
+ // the agent went off to write a file/note instead of replying — leave the
2487
2492
  // bootstrap pending (release the claim) and retry on the live connection,
2488
2493
  // 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`);
2494
+ if (!bootstrapGreetingDelivered) {
2495
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but greeting was not delivered; leaving pending for retry`);
2491
2496
  releaseBootstrap();
2492
2497
  armBootstrapRetry();
2493
2498
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.6.30-3",
3
+ "version": "2026.6.30-4",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -375,6 +375,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
375
375
  dispatcher: TypedReplyDispatcherResult["dispatcher"];
376
376
  replyOptions: ClawChatReplyOptions;
377
377
  markDispatchIdle: TypedReplyDispatcherResult["markDispatchIdle"];
378
+ /** Number of outbound messages actually delivered to the target so far. */
379
+ getOutboundDeliveredCount: () => number;
378
380
  } {
379
381
  const {
380
382
  cfg,
@@ -430,6 +432,12 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
430
432
  let typingActive = false;
431
433
  let terminalReplySuppressed = false;
432
434
  let finalDeliverySeen = false;
435
+ // Count of outbound messages that were actually delivered to the target
436
+ // (a real, successful send — not a suppressed/duplicate/failed one). This is
437
+ // the reliable "did anything actually reach the user?" signal; host-side
438
+ // `counts.block`/`counts.final` only reflect what the agent emitted, not
439
+ // whether the send succeeded (a cancelled send still counts as a block).
440
+ let outboundDeliveredCount = 0;
433
441
 
434
442
  const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
435
443
  const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
@@ -646,6 +654,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
646
654
  log,
647
655
  });
648
656
  recordOutboundAck("message", messageId, result);
657
+ if (result) outboundDeliveredCount += 1;
649
658
  log?.info?.(
650
659
  `[${account.accountId}] clawchat-plugin-openclaw send complete to=${target.chatId}`,
651
660
  );
@@ -835,6 +844,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
835
844
 
836
845
  return {
837
846
  dispatcher: base.dispatcher,
847
+ getOutboundDeliveredCount: () => outboundDeliveredCount,
838
848
  replyOptions: {
839
849
  ...base.replyOptions,
840
850
  sourceReplyDeliveryMode: "automatic",
package/src/runtime.ts CHANGED
@@ -1850,10 +1850,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1850
1850
  };
1851
1851
  let dispatchActivationBootstrap: () => Promise<void> = async () => {};
1852
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;
1853
+ // `clawchat-bootstrap` turn: true only if the greeting was actually delivered
1854
+ // (a real successful outbound send — block or final). The bootstrap dispatcher
1855
+ // reads it to decide whether to mark the bootstrap sent or leave it pending for
1856
+ // a later retry.
1857
+ let bootstrapGreetingDelivered = false;
1857
1858
  // Per-connection retry for a pending activation bootstrap (see the module-level
1858
1859
  // constants). Re-attempts on the live connection while `wsReady`, bounded by
1859
1860
  // ACTIVATION_BOOTSTRAP_MAX_RETRIES; the budget resets on each fresh connect.
@@ -2603,7 +2604,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2603
2604
  chatType: turn.peer.kind,
2604
2605
  agentId: route.agentId,
2605
2606
  });
2606
- const { dispatcher, replyOptions, markDispatchIdle } =
2607
+ const { dispatcher, replyOptions, markDispatchIdle, getOutboundDeliveredCount } =
2607
2608
  createOpenclawClawlingReplyDispatcher({
2608
2609
  cfg,
2609
2610
  runtime,
@@ -2649,10 +2650,14 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2649
2650
  (dispatchResult as { queuedFinal?: boolean } | undefined)?.queuedFinal,
2650
2651
  );
2651
2652
  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;
2653
+ // Did the greeting actually reach the user? The reliable signal is a
2654
+ // real, successful outbound send (the agent may deliver the greeting
2655
+ // as a `block`, not a `final`, and a cancelled send still shows up in
2656
+ // host `counts.block`). Treat a queued/counted final as delivered too,
2657
+ // to cover deferred/human-delay finals not yet flushed at this point.
2658
+ const deliveredOutbound = getOutboundDeliveredCount?.() ?? 0;
2659
+ bootstrapGreetingDelivered =
2660
+ deliveredOutbound > 0 || queuedFinal || (counts.final ?? 0) > 0;
2656
2661
  }
2657
2662
  log?.info?.(
2658
2663
  `[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`,
@@ -2961,7 +2966,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2961
2966
  const claimedBootstrap = bootstrap;
2962
2967
  claimedInFlight = true;
2963
2968
  incrementActivationBootstrapInFlight(accountId);
2964
- bootstrapProducedFinal = false;
2969
+ bootstrapGreetingDelivered = false;
2965
2970
  const result = await handleInboundEnvelope(
2966
2971
  buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
2967
2972
  );
@@ -2970,15 +2975,15 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2970
2975
  armBootstrapRetry();
2971
2976
  return;
2972
2977
  }
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
2978
+ // Only mark the bootstrap done once the greeting was actually delivered
2979
+ // (a real successful outbound send). If the turn was handed off but nothing
2980
+ // reached the user — e.g. the greeting send was cancelled by a reconnect, or
2981
+ // the agent went off to write a file/note instead of replying — leave the
2977
2982
  // bootstrap pending (release the claim) and retry on the live connection,
2978
2983
  // rather than silently consuming it forever or waiting for the next reconnect.
2979
- if (!bootstrapProducedFinal) {
2984
+ if (!bootstrapGreetingDelivered) {
2980
2985
  log?.info?.(
2981
- `[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`,
2986
+ `[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but greeting was not delivered; leaving pending for retry`,
2982
2987
  );
2983
2988
  releaseBootstrap();
2984
2989
  armBootstrapRetry();