@clawling/clawchat-plugin-openclaw 2026.6.30-2 → 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.
- package/dist/src/reply-dispatcher.js +9 -0
- package/dist/src/runtime.js +103 -18
- package/package.json +1 -1
- package/src/reply-dispatcher.ts +10 -0
- package/src/runtime.ts +100 -18
|
@@ -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",
|
package/dist/src/runtime.js
CHANGED
|
@@ -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
|
|
@@ -1446,10 +1474,38 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1446
1474
|
};
|
|
1447
1475
|
let dispatchActivationBootstrap = async () => { };
|
|
1448
1476
|
// Set by dispatchTurnToAgent when it dispatches the synthetic
|
|
1449
|
-
// `clawchat-bootstrap` turn: true only if the
|
|
1450
|
-
// outbound
|
|
1451
|
-
// whether to mark the bootstrap sent or leave it pending for
|
|
1452
|
-
|
|
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;
|
|
1482
|
+
// Per-connection retry for a pending activation bootstrap (see the module-level
|
|
1483
|
+
// constants). Re-attempts on the live connection while `wsReady`, bounded by
|
|
1484
|
+
// ACTIVATION_BOOTSTRAP_MAX_RETRIES; the budget resets on each fresh connect.
|
|
1485
|
+
let bootstrapRetryTimer;
|
|
1486
|
+
let bootstrapRetryAttempts = 0;
|
|
1487
|
+
const bootstrapRetryDelayMs = Math.max(1, params.activationBootstrapRetryDelayMs ?? ACTIVATION_BOOTSTRAP_RETRY_DELAY_MS);
|
|
1488
|
+
const clearBootstrapRetry = () => {
|
|
1489
|
+
if (bootstrapRetryTimer !== undefined) {
|
|
1490
|
+
clearTimeout(bootstrapRetryTimer);
|
|
1491
|
+
bootstrapRetryTimer = undefined;
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
const armBootstrapRetry = () => {
|
|
1495
|
+
if (bootstrapRetryTimer !== undefined)
|
|
1496
|
+
return;
|
|
1497
|
+
if (!wsReady)
|
|
1498
|
+
return;
|
|
1499
|
+
if (bootstrapRetryAttempts >= ACTIVATION_BOOTSTRAP_MAX_RETRIES)
|
|
1500
|
+
return;
|
|
1501
|
+
bootstrapRetryTimer = setTimeout(() => {
|
|
1502
|
+
bootstrapRetryTimer = undefined;
|
|
1503
|
+
if (!wsReady)
|
|
1504
|
+
return;
|
|
1505
|
+
bootstrapRetryAttempts += 1;
|
|
1506
|
+
void dispatchActivationBootstrap();
|
|
1507
|
+
}, bootstrapRetryDelayMs);
|
|
1508
|
+
};
|
|
1453
1509
|
client.on("state", ({ from, to }) => {
|
|
1454
1510
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
|
|
1455
1511
|
wsReady = to === "connected";
|
|
@@ -1522,6 +1578,9 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1522
1578
|
// The queue keeps the failed frame at the head and will retry after the next reconnect.
|
|
1523
1579
|
}
|
|
1524
1580
|
void refreshConversationCacheAfterReady();
|
|
1581
|
+
// Fresh connection → fresh retry budget; cancel any stale timer first.
|
|
1582
|
+
bootstrapRetryAttempts = 0;
|
|
1583
|
+
clearBootstrapRetry();
|
|
1525
1584
|
void dispatchActivationBootstrap();
|
|
1526
1585
|
// Re-pull per-group agent settings on every successful (re)connect so the
|
|
1527
1586
|
// cache stays current after a network interruption or token refresh.
|
|
@@ -1533,6 +1592,8 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1533
1592
|
}
|
|
1534
1593
|
else if (to === "disconnected") {
|
|
1535
1594
|
reconnectTracker.markClosed();
|
|
1595
|
+
// Stop the bootstrap retry while down; it re-arms on the next ready.
|
|
1596
|
+
clearBootstrapRetry();
|
|
1536
1597
|
// §A.1 — clear the proactive timer on disconnect; it re-arms on the next
|
|
1537
1598
|
// ready, or the gateway re-enter arms a fresh one.
|
|
1538
1599
|
if (!reconnectWithRefreshedToken)
|
|
@@ -2106,7 +2167,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2106
2167
|
chatType: turn.peer.kind,
|
|
2107
2168
|
agentId: route.agentId,
|
|
2108
2169
|
});
|
|
2109
|
-
const { dispatcher, replyOptions, markDispatchIdle } = createOpenclawClawlingReplyDispatcher({
|
|
2170
|
+
const { dispatcher, replyOptions, markDispatchIdle, getOutboundDeliveredCount } = createOpenclawClawlingReplyDispatcher({
|
|
2110
2171
|
cfg,
|
|
2111
2172
|
runtime,
|
|
2112
2173
|
account,
|
|
@@ -2142,10 +2203,14 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2142
2203
|
const counts = dispatchResult?.counts ?? {};
|
|
2143
2204
|
const queuedFinal = Boolean(dispatchResult?.queuedFinal);
|
|
2144
2205
|
if (turn.senderId === "clawchat-bootstrap") {
|
|
2145
|
-
// Did the
|
|
2146
|
-
//
|
|
2147
|
-
//
|
|
2148
|
-
|
|
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;
|
|
2149
2214
|
}
|
|
2150
2215
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
|
|
2151
2216
|
if (!queuedFinal && Object.values(counts).every((n) => !n)) {
|
|
@@ -2389,6 +2454,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2389
2454
|
if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent)
|
|
2390
2455
|
return;
|
|
2391
2456
|
let bootstrap;
|
|
2457
|
+
let claimedInFlight = false;
|
|
2392
2458
|
const releaseBootstrap = () => {
|
|
2393
2459
|
if (!bootstrap || !store.releaseActivationBootstrapClaim)
|
|
2394
2460
|
return;
|
|
@@ -2401,23 +2467,34 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2401
2467
|
};
|
|
2402
2468
|
try {
|
|
2403
2469
|
bootstrap = recordConnection("activation bootstrap claim", () => store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }));
|
|
2404
|
-
if (!bootstrap)
|
|
2470
|
+
if (!bootstrap) {
|
|
2471
|
+
// Nothing claimable right now. If another dispatch (e.g. a pre-reconnect
|
|
2472
|
+
// one that was about to fail) still holds the claim, keep retrying on
|
|
2473
|
+
// this live connection so the greeting isn't stranded once it releases.
|
|
2474
|
+
if (activationBootstrapInFlightCount(accountId) > 0)
|
|
2475
|
+
armBootstrapRetry();
|
|
2405
2476
|
return;
|
|
2477
|
+
}
|
|
2406
2478
|
const claimedBootstrap = bootstrap;
|
|
2407
|
-
|
|
2479
|
+
claimedInFlight = true;
|
|
2480
|
+
incrementActivationBootstrapInFlight(accountId);
|
|
2481
|
+
bootstrapGreetingDelivered = false;
|
|
2408
2482
|
const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
|
|
2409
2483
|
if (result !== "submitted") {
|
|
2410
2484
|
releaseBootstrap();
|
|
2485
|
+
armBootstrapRetry();
|
|
2411
2486
|
return;
|
|
2412
2487
|
}
|
|
2413
|
-
// Only mark the bootstrap done once the
|
|
2414
|
-
// (a
|
|
2415
|
-
//
|
|
2416
|
-
//
|
|
2417
|
-
//
|
|
2418
|
-
|
|
2419
|
-
|
|
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
|
|
2492
|
+
// bootstrap pending (release the claim) and retry on the live connection,
|
|
2493
|
+
// rather than silently consuming it forever or waiting for the next reconnect.
|
|
2494
|
+
if (!bootstrapGreetingDelivered) {
|
|
2495
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but greeting was not delivered; leaving pending for retry`);
|
|
2420
2496
|
releaseBootstrap();
|
|
2497
|
+
armBootstrapRetry();
|
|
2421
2498
|
return;
|
|
2422
2499
|
}
|
|
2423
2500
|
recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
|
|
@@ -2425,11 +2502,18 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2425
2502
|
accountId,
|
|
2426
2503
|
conversationId: claimedBootstrap.conversationId,
|
|
2427
2504
|
}));
|
|
2505
|
+
// Delivered — stop any pending retry.
|
|
2506
|
+
clearBootstrapRetry();
|
|
2428
2507
|
}
|
|
2429
2508
|
catch (err) {
|
|
2430
2509
|
releaseBootstrap();
|
|
2510
|
+
armBootstrapRetry();
|
|
2431
2511
|
log?.error?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2432
2512
|
}
|
|
2513
|
+
finally {
|
|
2514
|
+
if (claimedInFlight)
|
|
2515
|
+
decrementActivationBootstrapInFlight(accountId);
|
|
2516
|
+
}
|
|
2433
2517
|
};
|
|
2434
2518
|
client.on("message", (env) => {
|
|
2435
2519
|
void (async () => {
|
|
@@ -2541,6 +2625,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2541
2625
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw runtime abort received; closing client`);
|
|
2542
2626
|
activeClients.delete(accountId);
|
|
2543
2627
|
closingForAbort = true;
|
|
2628
|
+
clearBootstrapRetry();
|
|
2544
2629
|
// §A.1 — stop the proactive refresh timer on shutdown.
|
|
2545
2630
|
refreshManager.stop();
|
|
2546
2631
|
groupCoalescer.cancelAll();
|
package/package.json
CHANGED
package/src/reply-dispatcher.ts
CHANGED
|
@@ -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
|
@@ -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
|
/**
|
|
@@ -1822,10 +1850,37 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1822
1850
|
};
|
|
1823
1851
|
let dispatchActivationBootstrap: () => Promise<void> = async () => {};
|
|
1824
1852
|
// Set by dispatchTurnToAgent when it dispatches the synthetic
|
|
1825
|
-
// `clawchat-bootstrap` turn: true only if the
|
|
1826
|
-
// outbound
|
|
1827
|
-
// whether to mark the bootstrap sent or leave it pending for
|
|
1828
|
-
|
|
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;
|
|
1858
|
+
// Per-connection retry for a pending activation bootstrap (see the module-level
|
|
1859
|
+
// constants). Re-attempts on the live connection while `wsReady`, bounded by
|
|
1860
|
+
// ACTIVATION_BOOTSTRAP_MAX_RETRIES; the budget resets on each fresh connect.
|
|
1861
|
+
let bootstrapRetryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1862
|
+
let bootstrapRetryAttempts = 0;
|
|
1863
|
+
const bootstrapRetryDelayMs = Math.max(
|
|
1864
|
+
1,
|
|
1865
|
+
params.activationBootstrapRetryDelayMs ?? ACTIVATION_BOOTSTRAP_RETRY_DELAY_MS,
|
|
1866
|
+
);
|
|
1867
|
+
const clearBootstrapRetry = () => {
|
|
1868
|
+
if (bootstrapRetryTimer !== undefined) {
|
|
1869
|
+
clearTimeout(bootstrapRetryTimer);
|
|
1870
|
+
bootstrapRetryTimer = undefined;
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
const armBootstrapRetry = () => {
|
|
1874
|
+
if (bootstrapRetryTimer !== undefined) return;
|
|
1875
|
+
if (!wsReady) return;
|
|
1876
|
+
if (bootstrapRetryAttempts >= ACTIVATION_BOOTSTRAP_MAX_RETRIES) return;
|
|
1877
|
+
bootstrapRetryTimer = setTimeout(() => {
|
|
1878
|
+
bootstrapRetryTimer = undefined;
|
|
1879
|
+
if (!wsReady) return;
|
|
1880
|
+
bootstrapRetryAttempts += 1;
|
|
1881
|
+
void dispatchActivationBootstrap();
|
|
1882
|
+
}, bootstrapRetryDelayMs);
|
|
1883
|
+
};
|
|
1829
1884
|
|
|
1830
1885
|
client.on("state", ({ from, to }) => {
|
|
1831
1886
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
|
|
@@ -1900,6 +1955,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1900
1955
|
// The queue keeps the failed frame at the head and will retry after the next reconnect.
|
|
1901
1956
|
}
|
|
1902
1957
|
void refreshConversationCacheAfterReady();
|
|
1958
|
+
// Fresh connection → fresh retry budget; cancel any stale timer first.
|
|
1959
|
+
bootstrapRetryAttempts = 0;
|
|
1960
|
+
clearBootstrapRetry();
|
|
1903
1961
|
void dispatchActivationBootstrap();
|
|
1904
1962
|
// Re-pull per-group agent settings on every successful (re)connect so the
|
|
1905
1963
|
// cache stays current after a network interruption or token refresh.
|
|
@@ -1910,6 +1968,8 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1910
1968
|
refreshManager.armProactiveTimer(activatedAtMs);
|
|
1911
1969
|
} else if (to === "disconnected") {
|
|
1912
1970
|
reconnectTracker.markClosed();
|
|
1971
|
+
// Stop the bootstrap retry while down; it re-arms on the next ready.
|
|
1972
|
+
clearBootstrapRetry();
|
|
1913
1973
|
// §A.1 — clear the proactive timer on disconnect; it re-arms on the next
|
|
1914
1974
|
// ready, or the gateway re-enter arms a fresh one.
|
|
1915
1975
|
if (!reconnectWithRefreshedToken) refreshManager.disarmProactiveTimer();
|
|
@@ -2544,7 +2604,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2544
2604
|
chatType: turn.peer.kind,
|
|
2545
2605
|
agentId: route.agentId,
|
|
2546
2606
|
});
|
|
2547
|
-
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
2607
|
+
const { dispatcher, replyOptions, markDispatchIdle, getOutboundDeliveredCount } =
|
|
2548
2608
|
createOpenclawClawlingReplyDispatcher({
|
|
2549
2609
|
cfg,
|
|
2550
2610
|
runtime,
|
|
@@ -2590,10 +2650,14 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2590
2650
|
(dispatchResult as { queuedFinal?: boolean } | undefined)?.queuedFinal,
|
|
2591
2651
|
);
|
|
2592
2652
|
if (turn.senderId === "clawchat-bootstrap") {
|
|
2593
|
-
// Did the
|
|
2594
|
-
//
|
|
2595
|
-
//
|
|
2596
|
-
|
|
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;
|
|
2597
2661
|
}
|
|
2598
2662
|
log?.info?.(
|
|
2599
2663
|
`[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`,
|
|
@@ -2876,6 +2940,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2876
2940
|
dispatchActivationBootstrap = async (): Promise<void> => {
|
|
2877
2941
|
if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent) return;
|
|
2878
2942
|
let bootstrap: { conversationId: string } | null | undefined;
|
|
2943
|
+
let claimedInFlight = false;
|
|
2879
2944
|
const releaseBootstrap = () => {
|
|
2880
2945
|
if (!bootstrap || !store.releaseActivationBootstrapClaim) return;
|
|
2881
2946
|
const claimedBootstrap = bootstrap;
|
|
@@ -2891,26 +2956,37 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2891
2956
|
bootstrap = recordConnection("activation bootstrap claim", () =>
|
|
2892
2957
|
store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }),
|
|
2893
2958
|
);
|
|
2894
|
-
if (!bootstrap)
|
|
2959
|
+
if (!bootstrap) {
|
|
2960
|
+
// Nothing claimable right now. If another dispatch (e.g. a pre-reconnect
|
|
2961
|
+
// one that was about to fail) still holds the claim, keep retrying on
|
|
2962
|
+
// this live connection so the greeting isn't stranded once it releases.
|
|
2963
|
+
if (activationBootstrapInFlightCount(accountId) > 0) armBootstrapRetry();
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2895
2966
|
const claimedBootstrap = bootstrap;
|
|
2896
|
-
|
|
2967
|
+
claimedInFlight = true;
|
|
2968
|
+
incrementActivationBootstrapInFlight(accountId);
|
|
2969
|
+
bootstrapGreetingDelivered = false;
|
|
2897
2970
|
const result = await handleInboundEnvelope(
|
|
2898
2971
|
buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
|
|
2899
2972
|
);
|
|
2900
2973
|
if (result !== "submitted") {
|
|
2901
2974
|
releaseBootstrap();
|
|
2975
|
+
armBootstrapRetry();
|
|
2902
2976
|
return;
|
|
2903
2977
|
}
|
|
2904
|
-
// Only mark the bootstrap done once the
|
|
2905
|
-
// (a
|
|
2906
|
-
//
|
|
2907
|
-
//
|
|
2908
|
-
//
|
|
2909
|
-
|
|
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
|
|
2982
|
+
// bootstrap pending (release the claim) and retry on the live connection,
|
|
2983
|
+
// rather than silently consuming it forever or waiting for the next reconnect.
|
|
2984
|
+
if (!bootstrapGreetingDelivered) {
|
|
2910
2985
|
log?.info?.(
|
|
2911
|
-
`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but
|
|
2986
|
+
`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but greeting was not delivered; leaving pending for retry`,
|
|
2912
2987
|
);
|
|
2913
2988
|
releaseBootstrap();
|
|
2989
|
+
armBootstrapRetry();
|
|
2914
2990
|
return;
|
|
2915
2991
|
}
|
|
2916
2992
|
recordConnection("activation bootstrap sent", () =>
|
|
@@ -2920,11 +2996,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2920
2996
|
conversationId: claimedBootstrap.conversationId,
|
|
2921
2997
|
}),
|
|
2922
2998
|
);
|
|
2999
|
+
// Delivered — stop any pending retry.
|
|
3000
|
+
clearBootstrapRetry();
|
|
2923
3001
|
} catch (err) {
|
|
2924
3002
|
releaseBootstrap();
|
|
3003
|
+
armBootstrapRetry();
|
|
2925
3004
|
log?.error?.(
|
|
2926
3005
|
`[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2927
3006
|
);
|
|
3007
|
+
} finally {
|
|
3008
|
+
if (claimedInFlight) decrementActivationBootstrapInFlight(accountId);
|
|
2928
3009
|
}
|
|
2929
3010
|
};
|
|
2930
3011
|
|
|
@@ -3049,6 +3130,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3049
3130
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw runtime abort received; closing client`);
|
|
3050
3131
|
activeClients.delete(accountId);
|
|
3051
3132
|
closingForAbort = true;
|
|
3133
|
+
clearBootstrapRetry();
|
|
3052
3134
|
// §A.1 — stop the proactive refresh timer on shutdown.
|
|
3053
3135
|
refreshManager.stop();
|
|
3054
3136
|
groupCoalescer.cancelAll();
|