@alfe.ai/openclaw-chat 0.4.0 → 0.5.0

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;
@@ -1473,19 +1594,37 @@ async function handleAgentRequest(request, log) {
1473
1594
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
1474
1595
  let a2aResponseBuffer = "";
1475
1596
  if (isA2A) resetA2AEndSignal();
1597
+ const peer = conversationType === "group" ? {
1598
+ kind: "group",
1599
+ id: conversationId ?? senderId
1600
+ } : {
1601
+ kind: "direct",
1602
+ id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
1603
+ };
1604
+ let routeSessionKey = null;
1605
+ if (resolveRouteEnvelope) try {
1606
+ routeSessionKey = resolveRouteEnvelope({
1607
+ cfg,
1608
+ channel: "alfe",
1609
+ accountId: "default",
1610
+ peer,
1611
+ runtime: runtime.channel,
1612
+ sessionStore: cfg.session?.store
1613
+ }).route.sessionKey;
1614
+ } catch (err) {
1615
+ log.debug(`Route precompute failed (abort will use raw key): ${err instanceof Error ? err.message : String(err)}`);
1616
+ }
1617
+ activeRun = {
1618
+ requestSessionKey: legacySessionKey,
1619
+ routeSessionKey
1620
+ };
1476
1621
  runGate.sessionKey = (await dispatchInbound({
1477
1622
  cfg,
1478
1623
  runtime: { channel: runtime.channel },
1479
1624
  channel: "alfe",
1480
1625
  channelLabel,
1481
1626
  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
- },
1627
+ peer,
1489
1628
  senderId,
1490
1629
  senderAddress: senderId,
1491
1630
  recipientAddress: "agent",
@@ -1554,6 +1693,7 @@ async function handleAgentRequest(request, log) {
1554
1693
  log.error(`Agent dispatch failed: ${errMsg}`);
1555
1694
  chatClient?.sendResponse(request.id, false, { message: errMsg });
1556
1695
  } finally {
1696
+ activeRun = null;
1557
1697
  unsubscribe();
1558
1698
  clearAllToolUpdates();
1559
1699
  try {
@@ -1662,7 +1802,20 @@ const plugin = {
1662
1802
  apiKey,
1663
1803
  onRequest: (request) => {
1664
1804
  const handle = async () => {
1665
- if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
1805
+ if (request.method === "agent") {
1806
+ const params = request.params;
1807
+ const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
1808
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
1809
+ coalesceKey,
1810
+ onSuperseded: () => {
1811
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
1812
+ chatClient?.sendResponse(request.id, true, {
1813
+ superseded: true,
1814
+ sessionKey: coalesceKey
1815
+ });
1816
+ }
1817
+ } : void 0);
1818
+ } else if (request.method === "chat.abort") handleChatAbort(request, log);
1666
1819
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
1667
1820
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
1668
1821
  else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
@@ -1795,6 +1948,24 @@ const plugin = {
1795
1948
  }
1796
1949
  };
1797
1950
  //#endregion
1951
+ Object.defineProperty(exports, "__agentTurnQueueForTest", {
1952
+ enumerable: true,
1953
+ get: function() {
1954
+ return __agentTurnQueueForTest;
1955
+ }
1956
+ });
1957
+ Object.defineProperty(exports, "__resetAgentTurnQueueForTest", {
1958
+ enumerable: true,
1959
+ get: function() {
1960
+ return __resetAgentTurnQueueForTest;
1961
+ }
1962
+ });
1963
+ Object.defineProperty(exports, "__setActiveRunForTest", {
1964
+ enumerable: true,
1965
+ get: function() {
1966
+ return __setActiveRunForTest;
1967
+ }
1968
+ });
1798
1969
  Object.defineProperty(exports, "admitAgentEvent", {
1799
1970
  enumerable: true,
1800
1971
  get: function() {
@@ -1831,6 +2002,12 @@ Object.defineProperty(exports, "plugin", {
1831
2002
  return plugin;
1832
2003
  }
1833
2004
  });
2005
+ Object.defineProperty(exports, "resolveAbortTargetKeys", {
2006
+ enumerable: true,
2007
+ get: function() {
2008
+ return resolveAbortTargetKeys;
2009
+ }
2010
+ });
1834
2011
  Object.defineProperty(exports, "rewriteAssistantText", {
1835
2012
  enumerable: true,
1836
2013
  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;
@@ -1473,19 +1594,37 @@ async function handleAgentRequest(request, log) {
1473
1594
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
1474
1595
  let a2aResponseBuffer = "";
1475
1596
  if (isA2A) resetA2AEndSignal();
1597
+ const peer = conversationType === "group" ? {
1598
+ kind: "group",
1599
+ id: conversationId ?? senderId
1600
+ } : {
1601
+ kind: "direct",
1602
+ id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
1603
+ };
1604
+ let routeSessionKey = null;
1605
+ if (resolveRouteEnvelope) try {
1606
+ routeSessionKey = resolveRouteEnvelope({
1607
+ cfg,
1608
+ channel: "alfe",
1609
+ accountId: "default",
1610
+ peer,
1611
+ runtime: runtime.channel,
1612
+ sessionStore: cfg.session?.store
1613
+ }).route.sessionKey;
1614
+ } catch (err) {
1615
+ log.debug(`Route precompute failed (abort will use raw key): ${err instanceof Error ? err.message : String(err)}`);
1616
+ }
1617
+ activeRun = {
1618
+ requestSessionKey: legacySessionKey,
1619
+ routeSessionKey
1620
+ };
1476
1621
  runGate.sessionKey = (await dispatchInbound({
1477
1622
  cfg,
1478
1623
  runtime: { channel: runtime.channel },
1479
1624
  channel: "alfe",
1480
1625
  channelLabel,
1481
1626
  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
- },
1627
+ peer,
1489
1628
  senderId,
1490
1629
  senderAddress: senderId,
1491
1630
  recipientAddress: "agent",
@@ -1554,6 +1693,7 @@ async function handleAgentRequest(request, log) {
1554
1693
  log.error(`Agent dispatch failed: ${errMsg}`);
1555
1694
  chatClient?.sendResponse(request.id, false, { message: errMsg });
1556
1695
  } finally {
1696
+ activeRun = null;
1557
1697
  unsubscribe();
1558
1698
  clearAllToolUpdates();
1559
1699
  try {
@@ -1662,7 +1802,20 @@ const plugin = {
1662
1802
  apiKey,
1663
1803
  onRequest: (request) => {
1664
1804
  const handle = async () => {
1665
- if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
1805
+ if (request.method === "agent") {
1806
+ const params = request.params;
1807
+ const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
1808
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
1809
+ coalesceKey,
1810
+ onSuperseded: () => {
1811
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
1812
+ chatClient?.sendResponse(request.id, true, {
1813
+ superseded: true,
1814
+ sessionKey: coalesceKey
1815
+ });
1816
+ }
1817
+ } : void 0);
1818
+ } else if (request.method === "chat.abort") handleChatAbort(request, log);
1666
1819
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
1667
1820
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
1668
1821
  else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
@@ -1795,4 +1948,4 @@ const plugin = {
1795
1948
  }
1796
1949
  };
1797
1950
  //#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 };
1951
+ 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.0",
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": {