@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.
- package/dist/src/runtime.js +113 -6
- package/package.json +1 -1
- package/src/runtime.ts +112 -6
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
|
|
@@ -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
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"Do not ask the user for profile information
|
|
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
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
|
-
"
|
|
432
|
-
"
|
|
433
|
-
"
|
|
434
|
-
"
|
|
435
|
-
"Do not ask the user for profile information
|
|
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)
|
|
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();
|