@alfe.ai/openclaw-chat 0.4.0 → 0.5.1

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/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { d as AlfePluginConfig, f as AlfeResolvedAccount, l as createAlfeChannelPlugin, o as plugin, u as AlfeChannelConfig } from "./plugin.cjs";
1
+ import { _ as AlfeResolvedAccount, g as AlfePluginConfig, h as AlfeChannelConfig, m as createAlfeChannelPlugin, u as plugin } from "./plugin.cjs";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { d as AlfePluginConfig, f as AlfeResolvedAccount, l as createAlfeChannelPlugin, o as plugin, u as AlfeChannelConfig } from "./plugin.js";
1
+ import { _ as AlfeResolvedAccount, g as AlfePluginConfig, h as AlfeChannelConfig, m as createAlfeChannelPlugin, u as plugin } from "./plugin.js";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as plugin, c as createAlfeChannelPlugin } from "./plugin2.js";
1
+ import { c as plugin, f as createAlfeChannelPlugin } from "./plugin2.js";
2
2
  export { createAlfeChannelPlugin, plugin as default };
package/dist/plugin.cjs CHANGED
@@ -3,10 +3,14 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_plugin = require("./plugin2.cjs");
6
+ exports.__agentTurnQueueForTest = require_plugin.__agentTurnQueueForTest;
7
+ exports.__resetAgentTurnQueueForTest = require_plugin.__resetAgentTurnQueueForTest;
8
+ exports.__setActiveRunForTest = require_plugin.__setActiveRunForTest;
6
9
  exports.admitAgentEvent = require_plugin.admitAgentEvent;
7
10
  exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
8
11
  exports.buildToolActivity = require_plugin.buildToolActivity;
9
12
  exports.default = require_plugin.plugin;
10
13
  exports.joinAssistantText = require_plugin.joinAssistantText;
14
+ exports.resolveAbortTargetKeys = require_plugin.resolveAbortTargetKeys;
11
15
  exports.rewriteAssistantText = require_plugin.rewriteAssistantText;
12
16
  exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
package/dist/plugin.d.cts CHANGED
@@ -402,6 +402,35 @@ interface PluginApi {
402
402
  priority?: number;
403
403
  }): void;
404
404
  }
405
+ declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
406
+ coalesceKey?: string;
407
+ onSuperseded?: () => void;
408
+ }): Promise<void>;
409
+ /**
410
+ * Test-only: reset the agent-turn backlog between unit tests. Not used at
411
+ * runtime (the backlog lives for the process lifetime of one daemon).
412
+ */
413
+ declare function __resetAgentTurnQueueForTest(): void;
414
+ /** Test-only accessor for the coalescing queue (see __tests__/queue.test.ts). */
415
+ declare const __agentTurnQueueForTest: {
416
+ enqueue: typeof enqueueAgentTurn;
417
+ backlogLength: () => number;
418
+ };
419
+ interface ActiveRunRef {
420
+ requestSessionKey: string;
421
+ routeSessionKey: string | null;
422
+ }
423
+ /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
424
+ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
425
+ /**
426
+ * Ordered session keys to try when aborting for `abortSessionKey`. Pure, so it
427
+ * unit-tests without the OpenClaw SDK. The resolved route key (what the run
428
+ * registered under) is preferred; the raw request key is a best-effort
429
+ * fallback. An abort with NO sessionKey targets the single active run. A
430
+ * sessionKey that does NOT match the running turn yields no candidates — we
431
+ * never abort another session's run on a shared daemon.
432
+ */
433
+ declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
405
434
  declare const plugin: {
406
435
  id: string;
407
436
  name: string;
@@ -411,4 +440,4 @@ declare const plugin: {
411
440
  deactivate(api: PluginApi): Promise<void>;
412
441
  };
413
442
  //#endregion
414
- export { joinAssistantText as a, stripLeakedToolSummary as c, AlfePluginConfig as d, AlfeResolvedAccount as f, buildToolActivity as i, createAlfeChannelPlugin as l, admitAgentEvent as n, plugin as o, buildA2ACompletePayload as r, rewriteAssistantText as s, RunEventGate as t, AlfeChannelConfig as u };
443
+ export { AlfeResolvedAccount as _, __setActiveRunForTest as a, buildToolActivity as c, resolveAbortTargetKeys as d, rewriteAssistantText as f, AlfePluginConfig as g, AlfeChannelConfig as h, __resetAgentTurnQueueForTest as i, joinAssistantText as l, createAlfeChannelPlugin as m, RunEventGate as n, admitAgentEvent as o, stripLeakedToolSummary as p, __agentTurnQueueForTest as r, buildA2ACompletePayload as s, ActiveRunRef as t, plugin as u };
package/dist/plugin.d.ts CHANGED
@@ -402,6 +402,35 @@ interface PluginApi {
402
402
  priority?: number;
403
403
  }): void;
404
404
  }
405
+ declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
406
+ coalesceKey?: string;
407
+ onSuperseded?: () => void;
408
+ }): Promise<void>;
409
+ /**
410
+ * Test-only: reset the agent-turn backlog between unit tests. Not used at
411
+ * runtime (the backlog lives for the process lifetime of one daemon).
412
+ */
413
+ declare function __resetAgentTurnQueueForTest(): void;
414
+ /** Test-only accessor for the coalescing queue (see __tests__/queue.test.ts). */
415
+ declare const __agentTurnQueueForTest: {
416
+ enqueue: typeof enqueueAgentTurn;
417
+ backlogLength: () => number;
418
+ };
419
+ interface ActiveRunRef {
420
+ requestSessionKey: string;
421
+ routeSessionKey: string | null;
422
+ }
423
+ /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
424
+ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
425
+ /**
426
+ * Ordered session keys to try when aborting for `abortSessionKey`. Pure, so it
427
+ * unit-tests without the OpenClaw SDK. The resolved route key (what the run
428
+ * registered under) is preferred; the raw request key is a best-effort
429
+ * fallback. An abort with NO sessionKey targets the single active run. A
430
+ * sessionKey that does NOT match the running turn yields no candidates — we
431
+ * never abort another session's run on a shared daemon.
432
+ */
433
+ declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
405
434
  declare const plugin: {
406
435
  id: string;
407
436
  name: string;
@@ -411,4 +440,4 @@ declare const plugin: {
411
440
  deactivate(api: PluginApi): Promise<void>;
412
441
  };
413
442
  //#endregion
414
- export { joinAssistantText as a, stripLeakedToolSummary as c, AlfePluginConfig as d, AlfeResolvedAccount as f, buildToolActivity as i, createAlfeChannelPlugin as l, admitAgentEvent as n, plugin as o, buildA2ACompletePayload as r, rewriteAssistantText as s, RunEventGate as t, AlfeChannelConfig as u };
443
+ export { AlfeResolvedAccount as _, __setActiveRunForTest as a, buildToolActivity as c, resolveAbortTargetKeys as d, rewriteAssistantText as f, AlfePluginConfig as g, AlfeChannelConfig as h, __resetAgentTurnQueueForTest as i, joinAssistantText as l, createAlfeChannelPlugin as m, RunEventGate as n, admitAgentEvent as o, stripLeakedToolSummary as p, __agentTurnQueueForTest as r, buildA2ACompletePayload as s, ActiveRunRef as t, plugin as u };
package/dist/plugin.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as plugin, i as joinAssistantText, n as buildA2ACompletePayload, o as rewriteAssistantText, r as buildToolActivity, s as stripLeakedToolSummary, t as admitAgentEvent } from "./plugin2.js";
2
- export { admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { a as buildA2ACompletePayload, c as plugin, d as stripLeakedToolSummary, i as admitAgentEvent, l as resolveAbortTargetKeys, n as __resetAgentTurnQueueForTest, o as buildToolActivity, r as __setActiveRunForTest, s as joinAssistantText, t as __agentTurnQueueForTest, u as rewriteAssistantText } from "./plugin2.js";
2
+ export { __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.cjs CHANGED
@@ -1186,6 +1186,37 @@ function buildA2ACompletePayload(params) {
1186
1186
  };
1187
1187
  }
1188
1188
  let dispatchInbound = null;
1189
+ let abortEmbeddedAgentRun = null;
1190
+ let resolveActiveRunSessionId = null;
1191
+ let resolveRouteEnvelope = null;
1192
+ /**
1193
+ * Load the OpenClaw SDK surfaces from a single resolver anchor. Returns true
1194
+ * once the critical `dispatchInboundDirectDmWithRuntime` is resolved. The abort
1195
+ * primitives + route resolver are best-effort — an older openclaw without them
1196
+ * degrades `chat.abort` to a logged no-op (Stop still seals the UI) rather than
1197
+ * a hard failure.
1198
+ */
1199
+ function loadOpenClawSdk(req, log, anchor) {
1200
+ let resolvedDispatch = false;
1201
+ try {
1202
+ const channelInbound = req("openclaw/plugin-sdk/channel-inbound");
1203
+ if (channelInbound.dispatchInboundDirectDmWithRuntime) {
1204
+ dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
1205
+ resolvedDispatch = true;
1206
+ }
1207
+ } catch {}
1208
+ try {
1209
+ const harness = req("openclaw/plugin-sdk/agent-harness");
1210
+ if (harness.abortEmbeddedAgentRun) abortEmbeddedAgentRun = harness.abortEmbeddedAgentRun;
1211
+ if (harness.resolveActiveEmbeddedRunSessionId) resolveActiveRunSessionId = harness.resolveActiveEmbeddedRunSessionId;
1212
+ } catch {}
1213
+ try {
1214
+ const envelope = req("openclaw/plugin-sdk/inbound-envelope");
1215
+ if (envelope.resolveInboundRouteEnvelopeBuilderWithRuntime) resolveRouteEnvelope = envelope.resolveInboundRouteEnvelopeBuilderWithRuntime;
1216
+ } catch {}
1217
+ if (resolvedDispatch) log.info(`Resolved OpenClaw SDK from ${anchor} (hard-abort=${abortEmbeddedAgentRun && resolveActiveRunSessionId ? "available" : "unavailable"})`);
1218
+ return resolvedDispatch;
1219
+ }
1189
1220
  /**
1190
1221
  * Resolve OpenClaw SDK functions from the running process.
1191
1222
  *
@@ -1197,21 +1228,11 @@ let dispatchInbound = null;
1197
1228
  function resolveOpenClawSdk(log) {
1198
1229
  const anchors = [require$1.main?.filename, process.argv[1]].filter(Boolean);
1199
1230
  for (const anchor of anchors) try {
1200
- const channelInbound = (0, node_module.createRequire)(anchor)("openclaw/plugin-sdk/channel-inbound");
1201
- if (channelInbound.dispatchInboundDirectDmWithRuntime) {
1202
- dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
1203
- log.info(`Resolved OpenClaw SDK from ${anchor}`);
1204
- return;
1205
- }
1231
+ if (loadOpenClawSdk((0, node_module.createRequire)(anchor), log, anchor)) return;
1206
1232
  } catch {}
1207
1233
  try {
1208
1234
  const derivedPath = (0, node_path.join)((0, node_path.resolve)((0, node_path.dirname)(process.execPath), ".."), "lib", "node_modules", "openclaw", "package.json");
1209
- const channelInbound = (0, node_module.createRequire)(derivedPath)("openclaw/plugin-sdk/channel-inbound");
1210
- if (channelInbound.dispatchInboundDirectDmWithRuntime) {
1211
- dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
1212
- log.info(`Resolved OpenClaw SDK from ${derivedPath}`);
1213
- return;
1214
- }
1235
+ if (loadOpenClawSdk((0, node_module.createRequire)(derivedPath), log, derivedPath)) return;
1215
1236
  } catch {}
1216
1237
  log.warn("OpenClaw SDK not resolvable — chat dispatch will not work");
1217
1238
  }
@@ -1288,11 +1309,111 @@ async function downloadAttachments(attachments, log) {
1288
1309
  }
1289
1310
  return results;
1290
1311
  }
1291
- let agentTurnQueue = Promise.resolve();
1292
- function enqueueAgentTurn(task) {
1293
- const run = agentTurnQueue.then(task, task);
1294
- agentTurnQueue = run.then(() => void 0, () => void 0);
1295
- return run;
1312
+ let agentTurnBacklog = [];
1313
+ let agentTurnDraining = false;
1314
+ function enqueueAgentTurn(task, opts) {
1315
+ return new Promise((resolve, reject) => {
1316
+ const entry = {
1317
+ run: task,
1318
+ coalesceKey: opts?.coalesceKey,
1319
+ onSuperseded: opts?.onSuperseded ?? (() => void 0),
1320
+ resolve,
1321
+ reject
1322
+ };
1323
+ if (entry.coalesceKey) {
1324
+ const survivors = [];
1325
+ for (const queued of agentTurnBacklog) if (queued.coalesceKey === entry.coalesceKey) {
1326
+ try {
1327
+ queued.onSuperseded();
1328
+ } catch {}
1329
+ queued.resolve();
1330
+ } else survivors.push(queued);
1331
+ agentTurnBacklog = survivors;
1332
+ }
1333
+ agentTurnBacklog.push(entry);
1334
+ drainAgentTurns();
1335
+ });
1336
+ }
1337
+ async function drainAgentTurns() {
1338
+ if (agentTurnDraining) return;
1339
+ agentTurnDraining = true;
1340
+ try {
1341
+ while (agentTurnBacklog.length > 0) {
1342
+ const entry = agentTurnBacklog.shift();
1343
+ if (!entry) break;
1344
+ try {
1345
+ await entry.run();
1346
+ entry.resolve();
1347
+ } catch (err) {
1348
+ entry.reject(err);
1349
+ }
1350
+ }
1351
+ } finally {
1352
+ agentTurnDraining = false;
1353
+ }
1354
+ }
1355
+ /**
1356
+ * Test-only: reset the agent-turn backlog between unit tests. Not used at
1357
+ * runtime (the backlog lives for the process lifetime of one daemon).
1358
+ */
1359
+ function __resetAgentTurnQueueForTest() {
1360
+ agentTurnBacklog = [];
1361
+ agentTurnDraining = false;
1362
+ }
1363
+ /** Test-only accessor for the coalescing queue (see __tests__/queue.test.ts). */
1364
+ const __agentTurnQueueForTest = {
1365
+ enqueue: enqueueAgentTurn,
1366
+ backlogLength: () => agentTurnBacklog.length
1367
+ };
1368
+ let activeRun = null;
1369
+ /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
1370
+ function __setActiveRunForTest(ref) {
1371
+ activeRun = ref;
1372
+ }
1373
+ /**
1374
+ * Ordered session keys to try when aborting for `abortSessionKey`. Pure, so it
1375
+ * unit-tests without the OpenClaw SDK. The resolved route key (what the run
1376
+ * registered under) is preferred; the raw request key is a best-effort
1377
+ * fallback. An abort with NO sessionKey targets the single active run. A
1378
+ * sessionKey that does NOT match the running turn yields no candidates — we
1379
+ * never abort another session's run on a shared daemon.
1380
+ */
1381
+ function resolveAbortTargetKeys(active, abortSessionKey) {
1382
+ const keys = [];
1383
+ if (active && (!abortSessionKey || active.requestSessionKey === abortSessionKey)) {
1384
+ if (active.routeSessionKey) keys.push(active.routeSessionKey);
1385
+ if (!keys.includes(active.requestSessionKey)) keys.push(active.requestSessionKey);
1386
+ } else if (abortSessionKey) keys.push(abortSessionKey);
1387
+ return keys;
1388
+ }
1389
+ /**
1390
+ * `chat.abort` handler — genuinely cancels the RUNNING turn on the daemon.
1391
+ *
1392
+ * Invoked OUT-OF-BAND (never through the serialised agent-turn queue) so it
1393
+ * executes immediately while a turn is mid-flight. Resolves the live run's
1394
+ * sessionId from the target session key and fires the run handle's
1395
+ * AbortController via `abortEmbeddedAgentRun`. The in-flight dispatch then
1396
+ * settles (AbortError), so `handleAgentRequest` sends its RPC response and the
1397
+ * chat service seals the UI. When the abort primitives are unavailable (older
1398
+ * openclaw), this degrades to a logged no-op: the UI is still sealed by the
1399
+ * chat service's optimistic idle broadcast, but the turn completes server-side.
1400
+ */
1401
+ function handleChatAbort(request, log) {
1402
+ const { sessionKey } = request.params;
1403
+ const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey);
1404
+ let aborted = false;
1405
+ if (abortEmbeddedAgentRun && resolveActiveRunSessionId) {
1406
+ for (const key of targetKeys) {
1407
+ const sessionId = resolveActiveRunSessionId(key);
1408
+ if (sessionId && abortEmbeddedAgentRun(sessionId)) {
1409
+ aborted = true;
1410
+ log.info(`chat.abort: hard-aborted running turn (sessionId=${sessionId}, key=${key})`);
1411
+ break;
1412
+ }
1413
+ }
1414
+ if (!aborted) log.info(`chat.abort: no active run to abort (candidates=${String(targetKeys.length)})`);
1415
+ } else log.warn("chat.abort: OpenClaw abort primitives unavailable — cannot hard-abort the running turn");
1416
+ chatClient?.sendResponse(request.id, true, { aborted });
1296
1417
  }
1297
1418
  async function handleAgentRequest(request, log) {
1298
1419
  const runtime = pluginRuntime;
@@ -1320,6 +1441,7 @@ async function handleAgentRequest(request, log) {
1320
1441
  sessionKey: null
1321
1442
  };
1322
1443
  let lastThinkingLen = 0;
1444
+ let sawStructuredThinking = false;
1323
1445
  const turnThinking = {
1324
1446
  text: "",
1325
1447
  ts: 0
@@ -1376,20 +1498,22 @@ async function handleAgentRequest(request, log) {
1376
1498
  const { visible, thinking } = splitThinkTags(raw);
1377
1499
  if (thinking.length > lastThinkingLen) {
1378
1500
  const thinkingDelta = thinking.slice(lastThinkingLen);
1379
- chatClient?.sendEvent("chat-activity", {
1380
- runId: evt.runId,
1381
- sessionKey: legacySessionKey,
1382
- seq: evt.seq,
1383
- state: "activity",
1384
- activity: {
1385
- kind: "thinking",
1386
- text: thinkingDelta,
1387
- delta: true,
1388
- ts: evt.ts,
1389
- seq: evt.seq
1390
- }
1391
- });
1392
- recordThinking(thinkingDelta, evt.ts);
1501
+ if (!sawStructuredThinking) {
1502
+ chatClient?.sendEvent("chat-activity", {
1503
+ runId: evt.runId,
1504
+ sessionKey: legacySessionKey,
1505
+ seq: evt.seq,
1506
+ state: "activity",
1507
+ activity: {
1508
+ kind: "thinking",
1509
+ text: thinkingDelta,
1510
+ delta: true,
1511
+ ts: evt.ts,
1512
+ seq: evt.seq
1513
+ }
1514
+ });
1515
+ recordThinking(thinkingDelta, evt.ts);
1516
+ }
1393
1517
  lastThinkingLen = thinking.length;
1394
1518
  }
1395
1519
  const message = visible === raw && thinking.length === 0 ? evt.data : rewriteAssistantText(evt.data, visible);
@@ -1403,6 +1527,7 @@ async function handleAgentRequest(request, log) {
1403
1527
  return;
1404
1528
  }
1405
1529
  if (evt.stream === "thinking") {
1530
+ sawStructuredThinking = true;
1406
1531
  const activity = buildThinkingActivity(evt.data);
1407
1532
  if (activity) {
1408
1533
  chatClient?.sendEvent("chat-activity", {
@@ -1473,19 +1598,37 @@ async function handleAgentRequest(request, log) {
1473
1598
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
1474
1599
  let a2aResponseBuffer = "";
1475
1600
  if (isA2A) resetA2AEndSignal();
1601
+ const peer = conversationType === "group" ? {
1602
+ kind: "group",
1603
+ id: conversationId ?? senderId
1604
+ } : {
1605
+ kind: "direct",
1606
+ id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
1607
+ };
1608
+ let routeSessionKey = null;
1609
+ if (resolveRouteEnvelope) try {
1610
+ routeSessionKey = resolveRouteEnvelope({
1611
+ cfg,
1612
+ channel: "alfe",
1613
+ accountId: "default",
1614
+ peer,
1615
+ runtime: runtime.channel,
1616
+ sessionStore: cfg.session?.store
1617
+ }).route.sessionKey;
1618
+ } catch (err) {
1619
+ log.debug(`Route precompute failed (abort will use raw key): ${err instanceof Error ? err.message : String(err)}`);
1620
+ }
1621
+ activeRun = {
1622
+ requestSessionKey: legacySessionKey,
1623
+ routeSessionKey
1624
+ };
1476
1625
  runGate.sessionKey = (await dispatchInbound({
1477
1626
  cfg,
1478
1627
  runtime: { channel: runtime.channel },
1479
1628
  channel: "alfe",
1480
1629
  channelLabel,
1481
1630
  accountId: "default",
1482
- peer: conversationType === "group" ? {
1483
- kind: "group",
1484
- id: conversationId ?? senderId
1485
- } : {
1486
- kind: "direct",
1487
- id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
1488
- },
1631
+ peer,
1489
1632
  senderId,
1490
1633
  senderAddress: senderId,
1491
1634
  recipientAddress: "agent",
@@ -1554,6 +1697,7 @@ async function handleAgentRequest(request, log) {
1554
1697
  log.error(`Agent dispatch failed: ${errMsg}`);
1555
1698
  chatClient?.sendResponse(request.id, false, { message: errMsg });
1556
1699
  } finally {
1700
+ activeRun = null;
1557
1701
  unsubscribe();
1558
1702
  clearAllToolUpdates();
1559
1703
  try {
@@ -1662,7 +1806,20 @@ const plugin = {
1662
1806
  apiKey,
1663
1807
  onRequest: (request) => {
1664
1808
  const handle = async () => {
1665
- if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
1809
+ if (request.method === "agent") {
1810
+ const params = request.params;
1811
+ const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
1812
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
1813
+ coalesceKey,
1814
+ onSuperseded: () => {
1815
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
1816
+ chatClient?.sendResponse(request.id, true, {
1817
+ superseded: true,
1818
+ sessionKey: coalesceKey
1819
+ });
1820
+ }
1821
+ } : void 0);
1822
+ } else if (request.method === "chat.abort") handleChatAbort(request, log);
1666
1823
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
1667
1824
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
1668
1825
  else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
@@ -1795,6 +1952,24 @@ const plugin = {
1795
1952
  }
1796
1953
  };
1797
1954
  //#endregion
1955
+ Object.defineProperty(exports, "__agentTurnQueueForTest", {
1956
+ enumerable: true,
1957
+ get: function() {
1958
+ return __agentTurnQueueForTest;
1959
+ }
1960
+ });
1961
+ Object.defineProperty(exports, "__resetAgentTurnQueueForTest", {
1962
+ enumerable: true,
1963
+ get: function() {
1964
+ return __resetAgentTurnQueueForTest;
1965
+ }
1966
+ });
1967
+ Object.defineProperty(exports, "__setActiveRunForTest", {
1968
+ enumerable: true,
1969
+ get: function() {
1970
+ return __setActiveRunForTest;
1971
+ }
1972
+ });
1798
1973
  Object.defineProperty(exports, "admitAgentEvent", {
1799
1974
  enumerable: true,
1800
1975
  get: function() {
@@ -1831,6 +2006,12 @@ Object.defineProperty(exports, "plugin", {
1831
2006
  return plugin;
1832
2007
  }
1833
2008
  });
2009
+ Object.defineProperty(exports, "resolveAbortTargetKeys", {
2010
+ enumerable: true,
2011
+ get: function() {
2012
+ return resolveAbortTargetKeys;
2013
+ }
2014
+ });
1834
2015
  Object.defineProperty(exports, "rewriteAssistantText", {
1835
2016
  enumerable: true,
1836
2017
  get: function() {
@@ -1,2 +1,2 @@
1
- import { a as joinAssistantText, c as stripLeakedToolSummary, i as buildToolActivity, n as admitAgentEvent, o as plugin, r as buildA2ACompletePayload, s as rewriteAssistantText, t as RunEventGate } from "./plugin.cjs";
2
- export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { a as __setActiveRunForTest, c as buildToolActivity, d as resolveAbortTargetKeys, f as rewriteAssistantText, i as __resetAgentTurnQueueForTest, l as joinAssistantText, n as RunEventGate, o as admitAgentEvent, p as stripLeakedToolSummary, r as __agentTurnQueueForTest, s as buildA2ACompletePayload, t as ActiveRunRef, u as plugin } from "./plugin.cjs";
2
+ export { ActiveRunRef, RunEventGate, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as joinAssistantText, c as stripLeakedToolSummary, i as buildToolActivity, n as admitAgentEvent, o as plugin, r as buildA2ACompletePayload, s as rewriteAssistantText, t as RunEventGate } from "./plugin.js";
2
- export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { a as __setActiveRunForTest, c as buildToolActivity, d as resolveAbortTargetKeys, f as rewriteAssistantText, i as __resetAgentTurnQueueForTest, l as joinAssistantText, n as RunEventGate, o as admitAgentEvent, p as stripLeakedToolSummary, r as __agentTurnQueueForTest, s as buildA2ACompletePayload, t as ActiveRunRef, u as plugin } from "./plugin.js";
2
+ export { ActiveRunRef, RunEventGate, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.js CHANGED
@@ -1186,6 +1186,37 @@ function buildA2ACompletePayload(params) {
1186
1186
  };
1187
1187
  }
1188
1188
  let dispatchInbound = null;
1189
+ let abortEmbeddedAgentRun = null;
1190
+ let resolveActiveRunSessionId = null;
1191
+ let resolveRouteEnvelope = null;
1192
+ /**
1193
+ * Load the OpenClaw SDK surfaces from a single resolver anchor. Returns true
1194
+ * once the critical `dispatchInboundDirectDmWithRuntime` is resolved. The abort
1195
+ * primitives + route resolver are best-effort — an older openclaw without them
1196
+ * degrades `chat.abort` to a logged no-op (Stop still seals the UI) rather than
1197
+ * a hard failure.
1198
+ */
1199
+ function loadOpenClawSdk(req, log, anchor) {
1200
+ let resolvedDispatch = false;
1201
+ try {
1202
+ const channelInbound = req("openclaw/plugin-sdk/channel-inbound");
1203
+ if (channelInbound.dispatchInboundDirectDmWithRuntime) {
1204
+ dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
1205
+ resolvedDispatch = true;
1206
+ }
1207
+ } catch {}
1208
+ try {
1209
+ const harness = req("openclaw/plugin-sdk/agent-harness");
1210
+ if (harness.abortEmbeddedAgentRun) abortEmbeddedAgentRun = harness.abortEmbeddedAgentRun;
1211
+ if (harness.resolveActiveEmbeddedRunSessionId) resolveActiveRunSessionId = harness.resolveActiveEmbeddedRunSessionId;
1212
+ } catch {}
1213
+ try {
1214
+ const envelope = req("openclaw/plugin-sdk/inbound-envelope");
1215
+ if (envelope.resolveInboundRouteEnvelopeBuilderWithRuntime) resolveRouteEnvelope = envelope.resolveInboundRouteEnvelopeBuilderWithRuntime;
1216
+ } catch {}
1217
+ if (resolvedDispatch) log.info(`Resolved OpenClaw SDK from ${anchor} (hard-abort=${abortEmbeddedAgentRun && resolveActiveRunSessionId ? "available" : "unavailable"})`);
1218
+ return resolvedDispatch;
1219
+ }
1189
1220
  /**
1190
1221
  * Resolve OpenClaw SDK functions from the running process.
1191
1222
  *
@@ -1197,21 +1228,11 @@ let dispatchInbound = null;
1197
1228
  function resolveOpenClawSdk(log) {
1198
1229
  const anchors = [require.main?.filename, process.argv[1]].filter(Boolean);
1199
1230
  for (const anchor of anchors) try {
1200
- const channelInbound = createRequire(anchor)("openclaw/plugin-sdk/channel-inbound");
1201
- if (channelInbound.dispatchInboundDirectDmWithRuntime) {
1202
- dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
1203
- log.info(`Resolved OpenClaw SDK from ${anchor}`);
1204
- return;
1205
- }
1231
+ if (loadOpenClawSdk(createRequire(anchor), log, anchor)) return;
1206
1232
  } catch {}
1207
1233
  try {
1208
1234
  const derivedPath = join(resolve(dirname(process.execPath), ".."), "lib", "node_modules", "openclaw", "package.json");
1209
- const channelInbound = createRequire(derivedPath)("openclaw/plugin-sdk/channel-inbound");
1210
- if (channelInbound.dispatchInboundDirectDmWithRuntime) {
1211
- dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
1212
- log.info(`Resolved OpenClaw SDK from ${derivedPath}`);
1213
- return;
1214
- }
1235
+ if (loadOpenClawSdk(createRequire(derivedPath), log, derivedPath)) return;
1215
1236
  } catch {}
1216
1237
  log.warn("OpenClaw SDK not resolvable — chat dispatch will not work");
1217
1238
  }
@@ -1288,11 +1309,111 @@ async function downloadAttachments(attachments, log) {
1288
1309
  }
1289
1310
  return results;
1290
1311
  }
1291
- let agentTurnQueue = Promise.resolve();
1292
- function enqueueAgentTurn(task) {
1293
- const run = agentTurnQueue.then(task, task);
1294
- agentTurnQueue = run.then(() => void 0, () => void 0);
1295
- return run;
1312
+ let agentTurnBacklog = [];
1313
+ let agentTurnDraining = false;
1314
+ function enqueueAgentTurn(task, opts) {
1315
+ return new Promise((resolve, reject) => {
1316
+ const entry = {
1317
+ run: task,
1318
+ coalesceKey: opts?.coalesceKey,
1319
+ onSuperseded: opts?.onSuperseded ?? (() => void 0),
1320
+ resolve,
1321
+ reject
1322
+ };
1323
+ if (entry.coalesceKey) {
1324
+ const survivors = [];
1325
+ for (const queued of agentTurnBacklog) if (queued.coalesceKey === entry.coalesceKey) {
1326
+ try {
1327
+ queued.onSuperseded();
1328
+ } catch {}
1329
+ queued.resolve();
1330
+ } else survivors.push(queued);
1331
+ agentTurnBacklog = survivors;
1332
+ }
1333
+ agentTurnBacklog.push(entry);
1334
+ drainAgentTurns();
1335
+ });
1336
+ }
1337
+ async function drainAgentTurns() {
1338
+ if (agentTurnDraining) return;
1339
+ agentTurnDraining = true;
1340
+ try {
1341
+ while (agentTurnBacklog.length > 0) {
1342
+ const entry = agentTurnBacklog.shift();
1343
+ if (!entry) break;
1344
+ try {
1345
+ await entry.run();
1346
+ entry.resolve();
1347
+ } catch (err) {
1348
+ entry.reject(err);
1349
+ }
1350
+ }
1351
+ } finally {
1352
+ agentTurnDraining = false;
1353
+ }
1354
+ }
1355
+ /**
1356
+ * Test-only: reset the agent-turn backlog between unit tests. Not used at
1357
+ * runtime (the backlog lives for the process lifetime of one daemon).
1358
+ */
1359
+ function __resetAgentTurnQueueForTest() {
1360
+ agentTurnBacklog = [];
1361
+ agentTurnDraining = false;
1362
+ }
1363
+ /** Test-only accessor for the coalescing queue (see __tests__/queue.test.ts). */
1364
+ const __agentTurnQueueForTest = {
1365
+ enqueue: enqueueAgentTurn,
1366
+ backlogLength: () => agentTurnBacklog.length
1367
+ };
1368
+ let activeRun = null;
1369
+ /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
1370
+ function __setActiveRunForTest(ref) {
1371
+ activeRun = ref;
1372
+ }
1373
+ /**
1374
+ * Ordered session keys to try when aborting for `abortSessionKey`. Pure, so it
1375
+ * unit-tests without the OpenClaw SDK. The resolved route key (what the run
1376
+ * registered under) is preferred; the raw request key is a best-effort
1377
+ * fallback. An abort with NO sessionKey targets the single active run. A
1378
+ * sessionKey that does NOT match the running turn yields no candidates — we
1379
+ * never abort another session's run on a shared daemon.
1380
+ */
1381
+ function resolveAbortTargetKeys(active, abortSessionKey) {
1382
+ const keys = [];
1383
+ if (active && (!abortSessionKey || active.requestSessionKey === abortSessionKey)) {
1384
+ if (active.routeSessionKey) keys.push(active.routeSessionKey);
1385
+ if (!keys.includes(active.requestSessionKey)) keys.push(active.requestSessionKey);
1386
+ } else if (abortSessionKey) keys.push(abortSessionKey);
1387
+ return keys;
1388
+ }
1389
+ /**
1390
+ * `chat.abort` handler — genuinely cancels the RUNNING turn on the daemon.
1391
+ *
1392
+ * Invoked OUT-OF-BAND (never through the serialised agent-turn queue) so it
1393
+ * executes immediately while a turn is mid-flight. Resolves the live run's
1394
+ * sessionId from the target session key and fires the run handle's
1395
+ * AbortController via `abortEmbeddedAgentRun`. The in-flight dispatch then
1396
+ * settles (AbortError), so `handleAgentRequest` sends its RPC response and the
1397
+ * chat service seals the UI. When the abort primitives are unavailable (older
1398
+ * openclaw), this degrades to a logged no-op: the UI is still sealed by the
1399
+ * chat service's optimistic idle broadcast, but the turn completes server-side.
1400
+ */
1401
+ function handleChatAbort(request, log) {
1402
+ const { sessionKey } = request.params;
1403
+ const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey);
1404
+ let aborted = false;
1405
+ if (abortEmbeddedAgentRun && resolveActiveRunSessionId) {
1406
+ for (const key of targetKeys) {
1407
+ const sessionId = resolveActiveRunSessionId(key);
1408
+ if (sessionId && abortEmbeddedAgentRun(sessionId)) {
1409
+ aborted = true;
1410
+ log.info(`chat.abort: hard-aborted running turn (sessionId=${sessionId}, key=${key})`);
1411
+ break;
1412
+ }
1413
+ }
1414
+ if (!aborted) log.info(`chat.abort: no active run to abort (candidates=${String(targetKeys.length)})`);
1415
+ } else log.warn("chat.abort: OpenClaw abort primitives unavailable — cannot hard-abort the running turn");
1416
+ chatClient?.sendResponse(request.id, true, { aborted });
1296
1417
  }
1297
1418
  async function handleAgentRequest(request, log) {
1298
1419
  const runtime = pluginRuntime;
@@ -1320,6 +1441,7 @@ async function handleAgentRequest(request, log) {
1320
1441
  sessionKey: null
1321
1442
  };
1322
1443
  let lastThinkingLen = 0;
1444
+ let sawStructuredThinking = false;
1323
1445
  const turnThinking = {
1324
1446
  text: "",
1325
1447
  ts: 0
@@ -1376,20 +1498,22 @@ async function handleAgentRequest(request, log) {
1376
1498
  const { visible, thinking } = splitThinkTags(raw);
1377
1499
  if (thinking.length > lastThinkingLen) {
1378
1500
  const thinkingDelta = thinking.slice(lastThinkingLen);
1379
- chatClient?.sendEvent("chat-activity", {
1380
- runId: evt.runId,
1381
- sessionKey: legacySessionKey,
1382
- seq: evt.seq,
1383
- state: "activity",
1384
- activity: {
1385
- kind: "thinking",
1386
- text: thinkingDelta,
1387
- delta: true,
1388
- ts: evt.ts,
1389
- seq: evt.seq
1390
- }
1391
- });
1392
- recordThinking(thinkingDelta, evt.ts);
1501
+ if (!sawStructuredThinking) {
1502
+ chatClient?.sendEvent("chat-activity", {
1503
+ runId: evt.runId,
1504
+ sessionKey: legacySessionKey,
1505
+ seq: evt.seq,
1506
+ state: "activity",
1507
+ activity: {
1508
+ kind: "thinking",
1509
+ text: thinkingDelta,
1510
+ delta: true,
1511
+ ts: evt.ts,
1512
+ seq: evt.seq
1513
+ }
1514
+ });
1515
+ recordThinking(thinkingDelta, evt.ts);
1516
+ }
1393
1517
  lastThinkingLen = thinking.length;
1394
1518
  }
1395
1519
  const message = visible === raw && thinking.length === 0 ? evt.data : rewriteAssistantText(evt.data, visible);
@@ -1403,6 +1527,7 @@ async function handleAgentRequest(request, log) {
1403
1527
  return;
1404
1528
  }
1405
1529
  if (evt.stream === "thinking") {
1530
+ sawStructuredThinking = true;
1406
1531
  const activity = buildThinkingActivity(evt.data);
1407
1532
  if (activity) {
1408
1533
  chatClient?.sendEvent("chat-activity", {
@@ -1473,19 +1598,37 @@ async function handleAgentRequest(request, log) {
1473
1598
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
1474
1599
  let a2aResponseBuffer = "";
1475
1600
  if (isA2A) resetA2AEndSignal();
1601
+ const peer = conversationType === "group" ? {
1602
+ kind: "group",
1603
+ id: conversationId ?? senderId
1604
+ } : {
1605
+ kind: "direct",
1606
+ id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
1607
+ };
1608
+ let routeSessionKey = null;
1609
+ if (resolveRouteEnvelope) try {
1610
+ routeSessionKey = resolveRouteEnvelope({
1611
+ cfg,
1612
+ channel: "alfe",
1613
+ accountId: "default",
1614
+ peer,
1615
+ runtime: runtime.channel,
1616
+ sessionStore: cfg.session?.store
1617
+ }).route.sessionKey;
1618
+ } catch (err) {
1619
+ log.debug(`Route precompute failed (abort will use raw key): ${err instanceof Error ? err.message : String(err)}`);
1620
+ }
1621
+ activeRun = {
1622
+ requestSessionKey: legacySessionKey,
1623
+ routeSessionKey
1624
+ };
1476
1625
  runGate.sessionKey = (await dispatchInbound({
1477
1626
  cfg,
1478
1627
  runtime: { channel: runtime.channel },
1479
1628
  channel: "alfe",
1480
1629
  channelLabel,
1481
1630
  accountId: "default",
1482
- peer: conversationType === "group" ? {
1483
- kind: "group",
1484
- id: conversationId ?? senderId
1485
- } : {
1486
- kind: "direct",
1487
- id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
1488
- },
1631
+ peer,
1489
1632
  senderId,
1490
1633
  senderAddress: senderId,
1491
1634
  recipientAddress: "agent",
@@ -1554,6 +1697,7 @@ async function handleAgentRequest(request, log) {
1554
1697
  log.error(`Agent dispatch failed: ${errMsg}`);
1555
1698
  chatClient?.sendResponse(request.id, false, { message: errMsg });
1556
1699
  } finally {
1700
+ activeRun = null;
1557
1701
  unsubscribe();
1558
1702
  clearAllToolUpdates();
1559
1703
  try {
@@ -1662,7 +1806,20 @@ const plugin = {
1662
1806
  apiKey,
1663
1807
  onRequest: (request) => {
1664
1808
  const handle = async () => {
1665
- if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
1809
+ if (request.method === "agent") {
1810
+ const params = request.params;
1811
+ const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
1812
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
1813
+ coalesceKey,
1814
+ onSuperseded: () => {
1815
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
1816
+ chatClient?.sendResponse(request.id, true, {
1817
+ superseded: true,
1818
+ sessionKey: coalesceKey
1819
+ });
1820
+ }
1821
+ } : void 0);
1822
+ } else if (request.method === "chat.abort") handleChatAbort(request, log);
1666
1823
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
1667
1824
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
1668
1825
  else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
@@ -1795,4 +1952,4 @@ const plugin = {
1795
1952
  }
1796
1953
  };
1797
1954
  //#endregion
1798
- export { plugin as a, createAlfeChannelPlugin as c, joinAssistantText as i, buildA2ACompletePayload as n, rewriteAssistantText as o, buildToolActivity as r, stripLeakedToolSummary as s, admitAgentEvent as t };
1955
+ export { buildA2ACompletePayload as a, plugin as c, stripLeakedToolSummary as d, createAlfeChannelPlugin as f, admitAgentEvent as i, resolveAbortTargetKeys as l, __resetAgentTurnQueueForTest as n, buildToolActivity as o, __setActiveRunForTest as r, joinAssistantText as s, __agentTurnQueueForTest as t, rewriteAssistantText as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -27,8 +27,8 @@
27
27
  "openclaw.plugin.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@alfe.ai/chat": "^0.0.14",
31
30
  "@alfe.ai/agent-api-client": "^0.5.0",
31
+ "@alfe.ai/chat": "^0.0.14",
32
32
  "@alfe.ai/config": "^0.3.0"
33
33
  },
34
34
  "peerDependencies": {