@botcord/openclaw-plugin 0.0.6 → 0.0.7

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.
@@ -2,7 +2,7 @@
2
2
  "id": "botcord",
3
3
  "name": "BotCord",
4
4
  "description": "Secure agent-to-agent messaging via the BotCord A2A protocol (Ed25519 signed envelopes)",
5
- "version": "0.0.5",
5
+ "version": "0.0.7",
6
6
  "channels": ["botcord"],
7
7
  "skills": ["./skills"],
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/openclaw-plugin",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "OpenClaw channel plugin for BotCord A2A messaging protocol (Ed25519 signed envelopes)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/src/inbound.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import { getBotCordRuntime } from "./runtime.js";
6
6
  import { resolveAccountConfig } from "./config.js";
7
7
  import { buildSessionKey } from "./session-key.js";
8
+ import { loadSessionStore } from "openclaw/plugin-sdk/mattermost";
8
9
  import type { InboxMessage, MessageType } from "./types.js";
9
10
 
10
11
  // Envelope types that count as notifications rather than normal messages
@@ -256,12 +257,9 @@ function parseSessionKeyDeliveryContext(sessionKey: string): DeliveryContext | u
256
257
  const parts = sessionKey.split(":");
257
258
  if (parts.length < 5 || parts[0] !== "agent") return undefined;
258
259
 
259
- const KNOWN_CHANNELS = new Set([
260
- "telegram", "discord", "slack", "whatsapp", "signal", "imessage",
261
- ]);
262
260
  const agentName = parts[1]; // e.g. "pm"
263
261
  const channel = parts[2]; // e.g. "telegram"
264
- if (!KNOWN_CHANNELS.has(channel)) return undefined;
262
+ if (!channel) return undefined;
265
263
 
266
264
  const peerId = parts.slice(4).join(":"); // handle colons in id
267
265
  if (!peerId) return undefined;
@@ -273,6 +271,68 @@ function parseSessionKeyDeliveryContext(sessionKey: string): DeliveryContext | u
273
271
  };
274
272
  }
275
273
 
274
+ function parseSessionStoreDeliveryContext(
275
+ core: ReturnType<typeof getBotCordRuntime>,
276
+ cfg: any,
277
+ sessionKey: string,
278
+ ): DeliveryContext[] {
279
+ try {
280
+ const storePath = core.channel.session.resolveStorePath(cfg);
281
+ if (!storePath) return [];
282
+
283
+ const store = loadSessionStore(storePath);
284
+ const trimmedKey = sessionKey.trim();
285
+ const normalizedKey = trimmedKey.toLowerCase();
286
+ let existing = store[normalizedKey] ?? store[trimmedKey];
287
+ let existingUpdatedAt = existing?.updatedAt ?? 0;
288
+
289
+ // Legacy stores may contain differently-cased keys for the same session.
290
+ // Prefer the most recently updated matching entry.
291
+ for (const [candidateKey, candidateEntry] of Object.entries(store)) {
292
+ if (candidateKey.toLowerCase() !== normalizedKey) continue;
293
+ const candidateUpdatedAt = candidateEntry?.updatedAt ?? 0;
294
+ if (!existing || candidateUpdatedAt > existingUpdatedAt) {
295
+ existing = candidateEntry;
296
+ existingUpdatedAt = candidateUpdatedAt;
297
+ }
298
+ }
299
+ if (!existing) return [];
300
+
301
+ const lastRoute = {
302
+ channel: existing.lastChannel,
303
+ to: existing.lastTo,
304
+ accountId: existing.lastAccountId,
305
+ threadId: existing.lastThreadId ?? existing.origin?.threadId,
306
+ };
307
+ const candidates: DeliveryContext[] = [];
308
+ for (const ctx of [existing.deliveryContext, lastRoute]) {
309
+ if (!ctx?.channel || !ctx?.to) continue;
310
+ const normalized: DeliveryContext = {
311
+ channel: String(ctx.channel),
312
+ to: String(ctx.to),
313
+ };
314
+ if (ctx.accountId != null) normalized.accountId = String(ctx.accountId);
315
+ if (ctx.threadId != null) normalized.threadId = String(ctx.threadId);
316
+ candidates.push(normalized);
317
+ }
318
+
319
+ // Deduplicate identical contexts while preserving order.
320
+ const seen = new Set<string>();
321
+ return candidates.filter((ctx) => {
322
+ const key = `${ctx.channel}|${ctx.to}|${ctx.accountId ?? ""}|${ctx.threadId ?? ""}`;
323
+ if (seen.has(key)) return false;
324
+ seen.add(key);
325
+ return true;
326
+ });
327
+ } catch (err: any) {
328
+ console.warn(
329
+ `[botcord] notifySession ${sessionKey}: failed to read deliveryContext from session store:`,
330
+ err?.message ?? err,
331
+ );
332
+ return [];
333
+ }
334
+ }
335
+
276
336
  /** Channel name → runtime send function dispatcher. */
277
337
  type ChannelSendFn = (to: string, text: string, opts: Record<string, unknown>) => Promise<unknown>;
278
338
 
@@ -293,8 +353,8 @@ function resolveChannelSendFn(
293
353
 
294
354
  /**
295
355
  * Deliver a notification message directly to the channel associated with
296
- * the target session (looked up via deliveryContext in the session store).
297
- * Does not trigger an agent turn just sends the text.
356
+ * the target session. Prefer deriving target from session key; fallback to
357
+ * session store deliveryContext when direct routing is unavailable.
298
358
  */
299
359
  export async function deliverNotification(
300
360
  core: ReturnType<typeof getBotCordRuntime>,
@@ -302,19 +362,35 @@ export async function deliverNotification(
302
362
  sessionKey: string,
303
363
  text: string,
304
364
  ): Promise<void> {
305
- // Derive delivery target directly from the session key.
306
- // The session store's deliveryContext is unreliable because it gets
307
- // overwritten whenever the user accesses the session from a different
308
- // channel (e.g. webchat), losing the original channel/to values.
309
- const delivery = parseSessionKeyDeliveryContext(sessionKey);
365
+ const deliveryFromKey = parseSessionKeyDeliveryContext(sessionKey);
366
+ const storeCandidates = parseSessionStoreDeliveryContext(core, cfg, sessionKey);
367
+ const candidates = [
368
+ ...(deliveryFromKey ? [deliveryFromKey] : []),
369
+ ...storeCandidates,
370
+ ];
371
+ let delivery: DeliveryContext | undefined;
372
+ let sendFn: ChannelSendFn | undefined;
373
+
374
+ for (const candidate of candidates) {
375
+ if (!delivery) delivery = candidate;
376
+ const resolved = resolveChannelSendFn(core, candidate.channel);
377
+ if (resolved) {
378
+ delivery = candidate;
379
+ sendFn = resolved;
380
+ break;
381
+ }
382
+ }
383
+
310
384
  if (!delivery) {
311
385
  console.warn(
312
- `[botcord] notifySession ${sessionKey}: cannot derive delivery target from session key — skipping notification`,
386
+ `[botcord] notifySession ${sessionKey}: cannot derive delivery target from session key or session store — skipping notification`,
313
387
  );
314
388
  return;
315
389
  }
316
390
 
317
- const sendFn = resolveChannelSendFn(core, delivery.channel);
391
+ if (!sendFn) {
392
+ sendFn = resolveChannelSendFn(core, delivery.channel);
393
+ }
318
394
  if (!sendFn) {
319
395
  console.warn(
320
396
  `[botcord] unsupported notify channel "${delivery.channel}" — skipping notification`,