@chrysb/alphaclaw 0.8.3-beta.3 → 0.8.3-beta.5

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.
@@ -354,6 +354,10 @@ const createChatWsService = ({
354
354
  let gatewayConnectPromise = null;
355
355
  const pendingGatewayRequests = new Map();
356
356
  const runTargets = new Map();
357
+ /** While `chat.send` is in flight, agent events can arrive before we have `runId` + runTargets — match by session. */
358
+ const pendingSessionBySessionKey = new Map();
359
+ /** Agent events keyed by runId when the event references a run not yet in runTargets (race with chat.send response). */
360
+ const pendingAgentEventsByRunId = new Map();
357
361
  const browserRuns = new WeakMap();
358
362
 
359
363
  const sendJson = (ws, payload = {}) => {
@@ -382,6 +386,9 @@ const createChatWsService = ({
382
386
  };
383
387
 
384
388
  const clearRunTargetsForBrowser = (ws) => {
389
+ for (const [sk, pending] of pendingSessionBySessionKey.entries()) {
390
+ if (pending.ws === ws) pendingSessionBySessionKey.delete(sk);
391
+ }
385
392
  const runs = browserRuns.get(ws);
386
393
  if (!runs) return;
387
394
  for (const runId of runs) runTargets.delete(runId);
@@ -416,6 +423,7 @@ const createChatWsService = ({
416
423
  const markGatewayDisconnected = (reason = "Gateway disconnected") => {
417
424
  gatewaySocket = null;
418
425
  gatewayConnectPromise = null;
426
+ pendingAgentEventsByRunId.clear();
419
427
  rejectAllGatewayRequests(reason);
420
428
  };
421
429
 
@@ -436,6 +444,17 @@ const createChatWsService = ({
436
444
  sessionTarget = targetRow;
437
445
  }
438
446
  if (sessionTarget) return { runId: "", target: sessionTarget };
447
+ const pending = pendingSessionBySessionKey.get(sessionKey);
448
+ if (pending) {
449
+ return {
450
+ runId: resolveRunIdFromPayload(payload),
451
+ target: {
452
+ ws: pending.ws,
453
+ messageId: pending.messageId,
454
+ sessionKey,
455
+ },
456
+ };
457
+ }
439
458
  }
440
459
  if (runTargets.size === 1) {
441
460
  for (const [singleRunId, singleTarget] of runTargets.entries()) {
@@ -446,9 +465,60 @@ const createChatWsService = ({
446
465
  };
447
466
  if (eventName === "agent") {
448
467
  const { runId, target } = resolveTargetForPayload();
449
- if (!target) return;
468
+ if (!target) {
469
+ const runIdEarly = resolveRunIdFromPayload(payload);
470
+ if (runIdEarly && !runTargets.get(runIdEarly)) {
471
+ const list = pendingAgentEventsByRunId.get(runIdEarly) || [];
472
+ list.push(eventPayload);
473
+ pendingAgentEventsByRunId.set(runIdEarly, list);
474
+ }
475
+ return;
476
+ }
450
477
  const stream = String(payload?.stream || "");
451
478
  const data = payload?.data || {};
479
+ if (stream === "tool") {
480
+ const toolPhase = String(data?.phase || "");
481
+ const toolName = String(data?.name || "unknown");
482
+ const toolCallId = String(data?.toolCallId || "");
483
+ if (toolPhase === "start") {
484
+ sendJson(target.ws, {
485
+ type: "tool",
486
+ phase: "call",
487
+ messageId: target.messageId,
488
+ sessionKey: target.sessionKey,
489
+ timestamp: Number(payload?.ts) || Date.now(),
490
+ toolCall: {
491
+ id: toolCallId,
492
+ name: toolName,
493
+ arguments: data?.args || null,
494
+ partialJson: "",
495
+ },
496
+ toolResult: null,
497
+ rawEvent: eventPayload || null,
498
+ });
499
+ } else if (toolPhase === "result") {
500
+ const resultText = collectTextFromUnknownShape(data?.result);
501
+ sendJson(target.ws, {
502
+ type: "tool",
503
+ phase: "result",
504
+ messageId: target.messageId,
505
+ sessionKey: target.sessionKey,
506
+ timestamp: Number(payload?.ts) || Date.now(),
507
+ toolCall: null,
508
+ toolResult: {
509
+ role: "toolResult",
510
+ toolCallId,
511
+ toolName,
512
+ content: resultText
513
+ ? [{ type: "text", text: resultText }]
514
+ : [],
515
+ isError: data?.isError === true,
516
+ },
517
+ rawEvent: eventPayload || null,
518
+ });
519
+ }
520
+ return;
521
+ }
452
522
  const toolCall =
453
523
  extractToolCallFromUnknownShape(payload) ||
454
524
  extractToolCallFromUnknownShape(data);
@@ -557,7 +627,7 @@ const createChatWsService = ({
557
627
  },
558
628
  role: "operator",
559
629
  scopes: kGatewayChatBridgeScopes,
560
- caps: [],
630
+ caps: ["tool-events"],
561
631
  commands: [],
562
632
  permissions: {},
563
633
  auth: { token: getGatewayToken() },
@@ -689,13 +759,21 @@ const createChatWsService = ({
689
759
  });
690
760
  return;
691
761
  }
692
- const result = await requestGateway("chat.send", {
693
- sessionKey,
694
- message: content,
695
- idempotencyKey: crypto.randomUUID(),
696
- });
762
+ pendingSessionBySessionKey.set(sessionKey, { ws, messageId });
763
+ let result;
764
+ try {
765
+ result = await requestGateway("chat.send", {
766
+ sessionKey,
767
+ message: content,
768
+ idempotencyKey: crypto.randomUUID(),
769
+ });
770
+ } catch (err) {
771
+ pendingSessionBySessionKey.delete(sessionKey);
772
+ throw err;
773
+ }
697
774
  const runId = String(result?.runId || "").trim();
698
775
  if (!runId) {
776
+ pendingSessionBySessionKey.delete(sessionKey);
699
777
  sendJson(ws, {
700
778
  type: "error",
701
779
  message: "Something went wrong connecting to the agent.",
@@ -706,12 +784,20 @@ const createChatWsService = ({
706
784
  }
707
785
  runTargets.set(runId, { ws, messageId, sessionKey });
708
786
  registerRunForBrowser(ws, runId);
787
+ pendingSessionBySessionKey.delete(sessionKey);
709
788
  sendJson(ws, {
710
789
  type: "started",
711
790
  sessionKey,
712
791
  runId,
713
792
  messageId,
714
793
  });
794
+ const buffered = pendingAgentEventsByRunId.get(runId);
795
+ if (buffered && buffered.length) {
796
+ pendingAgentEventsByRunId.delete(runId);
797
+ for (const stored of buffered) {
798
+ handleGatewayEvent(stored);
799
+ }
800
+ }
715
801
  };
716
802
 
717
803
  const handleStop = async ({ ws, payload }) => {
@@ -1,6 +1,5 @@
1
1
  const { buildManagedPaths } = require("../internal-files-migration");
2
2
  const { readOpenclawConfig } = require("../openclaw-config");
3
- const { hasScopedBindingFields } = require("../utils/channels");
4
3
  const https = require("https");
5
4
 
6
5
  const registerSystemRoutes = ({
@@ -117,40 +116,6 @@ const registerSystemRoutes = ({
117
116
  .filter(Boolean)
118
117
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
119
118
  .join(" ");
120
- const resolveTelegramAccountIdForAgent = ({ config, agentId }) => {
121
- const normalizedAgentId = String(agentId || "").trim();
122
- if (!normalizedAgentId) return "default";
123
- const bindings = Array.isArray(config?.bindings) ? config.bindings : [];
124
- for (const binding of bindings) {
125
- if (String(binding?.agentId || "").trim() !== normalizedAgentId) continue;
126
- const match = binding?.match || {};
127
- if (String(match.channel || "").trim() !== "telegram") continue;
128
- if (hasScopedBindingFields(match)) continue;
129
- return String(match.accountId || "").trim() || "default";
130
- }
131
- return normalizedAgentId === "main" ? "default" : "";
132
- };
133
- const resolveTelegramChannelNameForAgent = ({ config, agentId }) => {
134
- const telegramConfig =
135
- config?.channels?.telegram && typeof config.channels.telegram === "object"
136
- ? config.channels.telegram
137
- : {};
138
- const accountId = resolveTelegramAccountIdForAgent({ config, agentId });
139
- const hasAccounts =
140
- telegramConfig.accounts && typeof telegramConfig.accounts === "object";
141
- if (hasAccounts) {
142
- const accountConfig =
143
- accountId && telegramConfig.accounts?.[accountId]
144
- ? telegramConfig.accounts[accountId]
145
- : {};
146
- const accountName = String(accountConfig?.name || "").trim();
147
- if (accountName) return accountName;
148
- } else if (accountId === "default") {
149
- const legacyName = String(telegramConfig?.name || "").trim();
150
- if (legacyName) return legacyName;
151
- }
152
- return "Telegram";
153
- };
154
119
  const getDefaultAgentLabel = (config = {}) => {
155
120
  return "Main Agent";
156
121
  };
@@ -182,44 +147,31 @@ const registerSystemRoutes = ({
182
147
  if (!agentId) return "Agent";
183
148
  return getConfiguredAgentLabel(config, agentId);
184
149
  };
185
- const buildSessionLabel = (sessionRow = {}, config = {}) => {
186
- const key = String(sessionRow?.key || "");
187
- const agentLabel = getAgentLabelFromSessionKey(key, config);
188
- const agentKeyMatch = key.match(/^agent:([^:]+):/);
189
- const agentId = String(agentKeyMatch?.[1] || "").trim();
190
- const telegramChannelName = resolveTelegramChannelNameForAgent({
191
- config,
192
- agentId,
193
- });
194
- if (key.endsWith(":main")) return `${agentLabel} - Main Thread`;
195
- const telegramMatch = key.match(/:telegram:direct:([^:]+)$/);
196
- if (telegramMatch) {
197
- return `${agentLabel} - Telegram DM (${telegramChannelName})`;
198
- }
199
- const telegramTopicMatch = key.match(
200
- /:telegram:group:([^:]+):topic:([^:]+)$/,
201
- );
202
- if (telegramTopicMatch) {
203
- const [, groupId, topicId] = telegramTopicMatch;
204
- let groupEntry = null;
205
- try {
206
- groupEntry = topicRegistry?.getGroup?.(groupId) || null;
207
- } catch {}
208
- const groupName = String(groupEntry?.name || "").trim();
209
- const topicName = String(
210
- groupEntry?.topics?.[topicId]?.name || "",
211
- ).trim();
212
- if (groupName && topicName) {
213
- return `${agentLabel} - ${telegramChannelName} ${groupName} · ${topicName}`;
214
- }
215
- if (topicName) return `${agentLabel} - ${telegramChannelName} Topic ${topicName}`;
216
- return `${agentLabel} - ${telegramChannelName} Topic ${topicId}`;
217
- }
218
- const directMatch = key.match(/:direct:([^:]+)$/);
219
- if (directMatch) {
220
- return `${agentLabel} - Direct ${directMatch[1]}`;
150
+ const parseChannelFromSessionKey = (key = "") => {
151
+ const k = String(key || "");
152
+ if (k.includes(":telegram:")) return "telegram";
153
+ if (k.includes(":discord:")) return "discord";
154
+ if (k.includes(":slack:")) return "slack";
155
+ return "";
156
+ };
157
+ const getSessionTopicContext = (sessionKey = "") => {
158
+ const key = String(sessionKey || "");
159
+ const topicMatch = key.match(/:telegram:group:([^:]+):topic:([^:]+)$/);
160
+ if (!topicMatch) {
161
+ return {
162
+ groupName: "",
163
+ topicName: "",
164
+ };
221
165
  }
222
- return key || "Session";
166
+ const [, groupId, topicId] = topicMatch;
167
+ let groupEntry = null;
168
+ try {
169
+ groupEntry = topicRegistry?.getGroup?.(groupId) || null;
170
+ } catch {}
171
+ return {
172
+ groupName: String(groupEntry?.name || "").trim(),
173
+ topicName: String(groupEntry?.topics?.[topicId]?.name || "").trim(),
174
+ };
223
175
  };
224
176
  const syncApiKeyAuthProfilesFromEnvVars = (nextEnvVars) => {
225
177
  if (!authProfiles) return;
@@ -280,21 +232,15 @@ const registerSystemRoutes = ({
280
232
  fallback: {},
281
233
  });
282
234
  return sessions
283
- .filter((sessionRow) => {
284
- const key = getRawSessionKey(sessionRow).toLowerCase();
285
- if (!key) return false;
286
- if (
287
- key.includes(":hook:") ||
288
- key.includes(":cron:") ||
289
- key.includes(":doctor:")
290
- ) {
291
- return false;
292
- }
293
- return true;
294
- })
295
235
  .map((sessionRow) => {
296
236
  const key = getRawSessionKey(sessionRow);
237
+ if (!key) return null;
297
238
  const replyTarget = getSessionReplyTarget(key);
239
+ const agentKeyMatch = key.match(/^agent:([^:]+):/);
240
+ const agentId = String(agentKeyMatch?.[1] || "").trim();
241
+ const channel =
242
+ parseChannelFromSessionKey(key) || replyTarget.replyChannel || "";
243
+ const topicContext = getSessionTopicContext(key);
298
244
  return {
299
245
  key,
300
246
  sessionId: String(sessionRow?.sessionId || sessionRow?.id || ""),
@@ -304,11 +250,16 @@ const registerSystemRoutes = ({
304
250
  sessionRow?.lastActivityAt ||
305
251
  sessionRow?.lastActiveAt,
306
252
  ) || 0,
307
- label: buildSessionLabel(sessionRow, config),
253
+ agentId,
254
+ agentLabel: getAgentLabelFromSessionKey(key, config),
255
+ channel,
256
+ groupName: topicContext.groupName,
257
+ topicName: topicContext.topicName,
308
258
  replyChannel: replyTarget.replyChannel,
309
259
  replyTo: replyTarget.replyTo,
310
260
  };
311
261
  })
262
+ .filter(Boolean)
312
263
  .sort((a, b) => b.updatedAt - a.updatedAt);
313
264
  };
314
265
  const readSystemCronConfig = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.3-beta.3",
3
+ "version": "0.8.3-beta.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },